JavaScript数组的那些事儿

发布于 大漠

JavaScript中的数组是其中重要的一部分,它提供了很多种操作数组的方法。当涉及到遍历一个数组、查找元素、排序或任何你想要的东西时,JavaScript都可能有一个数组方法可提供给我们使用。然而,尽管它们都很有用,但其中一些知识点仍然不那么为人所知和使用。在这篇文章中将会介绍一些方法,让你在使用数组的时候变得更轻易。甚至可以将接下来的内容当作是JavaScript数组方法的使用指南。

要点

在处理数组时,你需要知道四件件:mapfilterreduce和Spread操作符。它们强大而有用。

map

你会经常使用到它。基本上,每次你需要修改数组的元素时都首先会想到使用map

map()方法会创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

const numbers = [1, 2, 3, 4]
const numbersPlusOne = numbers.map( n => n + 1) // numbers数组中的每个元素加1,返回一个新的数组
console.log(numbers)
console.log(numbersPlusOne)

你也可以创建一个新数组,它只保留一个对象的一个特定属性:

const allBlogs = [
    {
        title: 'CSS',
        tags: ['CSS', 'Web']
    },
    {
        title: 'JavaScript',
        tags: ['JavaScript', 'Web', 'ES6']
    }
]

const allTitles = allBlogs.map(blog => blog.title)
console.log(allTitles)

我们也可以写一个函数,让该函数也具备类似map的功能:

function map(collection, callback) {
    var iterationInputs = []
    for (var i = 0; i < collection.length; i++) {
        iterationInputs.push(callback(colleection[i]))
    }
    return iterationInputs
}

需要转换数组时,可以考虑使用map()方法

filter

当你想过滤一个数组时可以使用filter。就像map一样,它接受一个函数作为唯一的参数,该参数在数组的每个元素上调用。这个函数需要返回一个布尔值:

  • true:元素将保留在数组中
  • false:元素不会保留在数组中

最终返回结果也会得到一个新数组,该新数组中保留了你想要留下的元素。比如下面这个示例,只希望在新数组中保留原数组数中的奇数:

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const oddNumbers = numbers.filter(n => n % 2 !== 0)
console.log(numbers)
console.log(oddNumbers)

也可以使用该方法删除数组中的特定项:

const participants = [
    {
        id: 'a3f47',
        username: 'john'
    },
    {
        id: 'fek28',
        username: 'mary'
    },
    {
        id: 'n3j44',
        username: 'sam'
    }
]

function removeParticipant (participants, id) {
    return participants.filter(participant => participant.id !== id)
}

console.log(removeParticipant(participants, 'a3f47'))

同样的,可以使用一个函数来重写filter的功能:

function filter(collection, callback) {
    var filteredArray = []

    for (var i = 0; i < collection.length; i++) {
        if (callback(collection[i])) {
            filteredArray.push(collection[i])
        }
    }

    return filteredArray
}

reduce

reduce是数组提供的方法中最难理解的方法,但一旦掌握了该方法,可以做的事情就很多。reduce会取一个数组中的值并将它们合成一个值。它接受两个参数,一个回调函数(reducer函数)和一个可选的初始值(默认情况是数组的第一项)。reducer本身有四个参数:

  • 累计器:它在reducer中累加返回值
  • 数组的当前值
  • 当前索引值
  • 源数组

您的 reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。大多数情况下,只需要使用前两个参数:累计器当前值

来看一个最常见的reduce例子:

const numbers = [1, 2, 3, 4, 5, 6]
const total = numbers.reduce((total, n) => total + n)
console.log(total) // => 21

在第一次迭代中,累计器(total)的初始值为1,返回的值是1 + n,其中n = 2,因此累计器的值是3;在第二次迭代中,累计器的值是3,返回的值是 3 + 3 = 6。整个迭代过程,依此类推。

同样的,我们也可以写一个函数实现类似reduce的功能:

function reduce(list, callback, initialValue) {
    let accumulator = initialValue

    for (let currentIndex = 0; currentIndex < list.length; currentIndex++) {
        currentValue = list[currentIndex]
        accumulator = callback(accumulator, currentValue, currentIndex, list)
    }

    return accumulator
}

上面的函数有一个前提,用户提供的输入总是有效的

我们来看一些示例,加强我们对reduce的理解。假设我们有一个这样的数组:

const wizards = [
    {
        name: 'Cho Chang',
        house: 'Ravenclaw',
        points: 100
    },
    {
        name: 'Luna Lovegood',
        house: 'Ravenclaw',
        points: 100
    },
    {
        name: "Cedric Diggory",
        house: "Hufflepuff",
        points: 12
    },
    {
        name: "Lin Manuel Miranda",
        house: "Slytherin",
        points: 5000
    },
    {
        name: "Draco Malfoy",
        house: "Slytherin",
        points: -20
    }
]

数据有了,开始看看reduce的相关示例。

keyBy

先来看一个keyBy的示例:

function keyBy(list, key) {
    return list.reduce((acc, item) => {
        const index = item[key]
        acc[index] = item
        return acc
    }, {})
}

该函数接受listkey两个参数,并通过key上的属性对这些对象进行键控。在这个示例中,我们的初始值是{}。我们遍历list中的每一项(item),获取item[key]并将其用作累计器中的key。假设我们的list就是上面声明的数组wizards,我们可以调用keyBy(wizards, 'name')得到下面的结果:

groupBy

接下来是groupBy,该函数接受一个list参数,并根据给定函数(或iteratee)的输出对象进行分组。

function groupBy(list, iteratee) {
    return list.reduce((acc, item) => {
        const index = iteratee(item)
        if (!acc[index]) {
            acc[index] = []
        }
        acc[index].push(item)
        return acc
    },{})
}

同样,我们取一个列表,但这次我们取一个函数或iteratee。我们对列表中的每一项调用iteratee,并使用它来确定在累计器(或acc)上放置该项的键。然后,我们确保在键上分配一个数组,并将项push到该数组。

如果我们调用groupBy(wizards, wizard => wizard.house)后会输出下面这样的结果:

chunk

另一个典型的例子是chunk。在这个示例中,我们希望获取一个较大的列表,并将其拆分为一个较小的列表。

function chunk(list, size) {
    return list.reduce((acc, item) => {
        let lastIndex = acc.length - 1
        if (acc[lastIndex].length >= size) {
            acc.push([])
            lastIndex++
        }
        acc[lastIndex].push(item)
        return acc
    },[[]])
}

上面的代码,我们取了一个list和列表的size。其初始值是一个数组,其中只有一个空数组。然后我们遍历list并生成更小的list。每次我们的子列表达到一定的大小,就会进入下一个子列表。

如果我们调用chunkedWizards = chunk(wizard, 3)得到的chunkedwizard的值如下图所示:

flatten

我们已经向wizards分成多个chunkedWizards,但如果我们想要撤销它,就可以使用flatten

function flatten(list) {
    return list.reduce((acc, item) => {
        if (Array.isArray(item)) {
            acc = acc.concat(item)
        } else {
            acc.push(item)
        }
        return acc
    },[])
}

在这个示例中,我们获取一个list并对其进行迭代。每当我们看到一个数组时,我们就用累计器(acc)来表示它。每当我们看到其他东西,我们就把它推到累计器上。这次累计器的初始值是一个空数组或[]

我们运行flatten(chunkedwizard)将会得到原始列表:

flatMap

我们可以在上面的基础上做更进一步的处理。借助数组的map()将给定函数的结果做扁平化处理:

function flatMap(list, func) {
    return list.reduce((acc, item) => {
        const mappedItem = func(item)
        if (Array.isArray(mappedItem)) {
            acc = acc.concat(mappedItem)
        } else {
            acc.push(mappedItem)
        }
        return acc
    },[])
}

接下来调用:

flatMap(wizards, wizard => [wizard.name, wizard.house, wizard.points])

其输出的结果如下:

特别声明,上面几个函数的详细介绍和代码来源于@MatthewGerstman的《Reduce, Reduce, Reduce》一文。

事实上,reduce功能非常的强大,你可以用它来实现mapfilter的功能:

const map = (arr, fn) => {
    return arr.reduce((mappedArr, element) => {
        return [...mappedArr, fn(element)]
    }, [])
}

console.log(map([1, 2, 3, 4], n => n + 1)) // => [2, 3, 4, 5]

const filter = (arr, fn) => {
    return arr.reduce((filteredArr, element) => {
        return fn(element) ? [...filteredArr] : [...filteredArr, element]
    }, [])
}

console.log(filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0)) // => [1, 3, 5]

简单的小结一下

上面主要介绍了数组的reducefiltermap的使用。@Una kravets在她的博文《An Illustrated (and Musical) Guide to Map, Reduce, and Filter Array Methods》中提供了三张图,非常表象的描述了这三个方法的功能:

Array.map()

Array.filter()

Array.reduce()

Spread操作符(ES2015)

使用Spread操作符(...)可以帮助我们更好的处理数组,比如说复制或合并多个数组:

const numbers = [1, 2, 3]
const numbersCopy = [...numbers]
console.log(numbersCopy) // => [1, 2, 3]

const otherNumbers = [4, 5, 6]
const numbersConcatenated = [...numbers, ...otherNumbers]
console.log(numbersConcatenated) // => [1, 2, 3, 4, 5, 6]

注意,Spread操作符只能对原始数组进行浅拷贝。那么浅拷贝是什么呢?

浅拷贝将尽可能少地复制原始元素。因此,当数组包含 数字字符串布尔值(基本类型)时,数组拷贝不会有问题。然而,这对于对象或数组不一样。只有对原始值的引用才会被复制。也就是说,如果对包含对象的数组进行浅拷贝,并且修改了复制数组中的对象,那么原始数组中的对象也将被修改,因此它们具有相同的引用。

const arr = ['foo', 42, { name: 'Thomas' }]
let copy = [...arr]

copy[0] = 'bar'

console.log(arr)    // => ["foo", 42, { name: "Thomas" }] (没有变化)
console.log(copy)   // => ["bar", 42, { name: "Thomas" }]

copy[2].name = 'Hello'

console.log(arr)    // => ["foo", 42, { name: "Hello" }] (有变化)
console.log(copy)   // => ["bar", 42, { name: "Hello" }]

因此,如果你想要创建一个包含对象或数组的数组的真实副本,可以使用lodash中的cloneDeep函数

其他更好的数组方法

JavaScript中有关于数组的相关方法,除了上面的所说的之外还有一些其他的方法,这些方法在具体的场合之下可以帮助我们解决一些问题,比如搜索数组中的一个元素,取出数组中的一部分等等。

includes() (ES2016)

很多时候我们会使用indexOf来确定某个元素是否在数组中。在数组中其实有一个更好的方法来达到类似的效果,这个函数就是includes()方法。我们可以给这个方法提供一个参数(要检测的元素),如果数组中有该元素,则返回的值是true,反之返回的是false

const sports = ['football', 'archery', 'judo']
const hasFootball = sports.includes('football')
const noBall = sports.includes('ball')
console.log(hasFootball)    // => true
console.log(noBall)         // => false

concat()

我们可以使用concat()方法将两个或多个数组合并在一起,返回一个新的数组。

const numbers = [1, 2, 3]
const otherNumbers = [4, 5, 6]

const numbersConcatenated = numbers.concat(otherNumbers)
console.log(numbersConcatenated)    // => [1, 2, 3, 4, 5, 6]

// 你可以使用下面这个函数合并任意多个数组
function concatAll(arr, ...arrays) {
    return arr.concat(...arrays)
}

console.log(concatAll([1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]))   // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

forEach()

当你想要对一个数组进行遍历操作时,可以使用forEach()方法,它接受一个函数作为参数。事实上它本身接受三个参数:当前值索引数组

const numbers = [1, 2, 3, 4, 5]
numbers.forEach(console.log)

输出的结果如下:

indexOf()

它用于返回数组中给定元素的第一个索引值。indexOf()也被广泛用于检查元素是否存在array

const sports = ['football', 'archery', 'judo']
const judoIndex = sports.indexOf('judo')

console.log(judoIndex) // => 2

时至今日,该方法不怎么使用了。

find()

find()filter()有点类似。你必须为它提供一个测试每个数组元素的函数。但是find()会在找到一个通过测试的元素时立即停止的测试元素。而filter()将会遍历整个数组。

const users = [
    {
        id: 'af35',
        name: 'john'
    },
    {
        id: '6gbe',
        name: 'mary'
    },
    {
        id: '932j',
        name: 'gary'
    }
]

const user = users.find(user => user.id === '6gbe')

console.log(user)  // => { id: '6gbe', name: 'mary' }

所以你想过滤整个数组时,可以使用filter()。当确定要搜索数组中的唯一元素时,则使用find()

findIndex()

它和findIndex()方法完全相同,只是它返回找到的第一个元素的索引,而不是直接返回元素。

const users = [
    {
        id: 'af35',
        name: 'john'
    },
    {
        id: '6gbe',
        name: 'mary'
    },
    {
        id: '932j',
        name: 'gary'
    }
]

const user = users.findIndex(user => user.id === '6gbe')

console.log(user)  // => 1

你可能会认为findIndex()indexOf()是相同的。事实上他们还是有所差异的。indexOf()的第一个参数是一个原始值(BooleanNumberStringNullUndefinedSymbol),而findIndex()的第一个参数是一个回调函数。

因此,当你需要搜索原始值数组中元素的索引时,可以使用indexOf()。如果你有更复杂的元素,如对象,那得使用findIndex()

slice()

slice()可以帮助我们提取数组的一部分或复制数组。但是要小心,就像spread操作符一样,slice()返回该部分的一个浅拷贝。

const numbers = [1, 2, 3, 4, 5]
const copy = numbers.slice()

假设你想检索一定数量的聊天消息,并且只想显示其中的五个。我们可以使用以下两种方法:一种是for循环,另一种就是slice()方法。

const nbMessages = messages.length < 5 ? messages.length : 5
let messagesToShow = []

for (let i = 0; i < nbMessages; i++) {
    messagesToShow.push(posts[i])
}

const messagesToShow = messages.slice(0, 5)

some()

如果你想检测数组中至少有一个元素通过了测试,那可以使用some()方法。就像map()filter()find()一样,some()方法将回调函数作为唯一的参数。如果至少有一个元素通过测试,则返回true,否则返回false

在实际的业务开发中,可以使用some()方法来处理权限的需求,比如:

const users = [
    {
        id: 'fe34',
        permissions: ['read', 'write'],
    },
    {
        id: 'a198',
        permissions: [],
    },
    {
        id: '18aa',
        permissions: ['delete', 'read', 'write'],
    }
]

const hasDeletePermission = users.some(user =>
    user.permissions.includes('delete')
)

console.log(hasDeletePermission) // => true

every()

every()some()类似,不同的是some()只要有一个元素符合条件即返回true,而every()不同,需要所有元素都通过条件才返回true

const users = [
    {
        id: 'fe34',
        permissions: ['read', 'write'],
    },
    {
        id: 'a198',
        permissions: [],
    },
    {
        id: '18aa',
        permissions: ['delete', 'read', 'write'],
    }
]

const hasAllReadPermission = users.every(user =>
    user.permissions.includes('read')
)

console.log(hasAllReadPermission) // => false

flat() (ES2019)

这是即将要出现的新方法。基本上,flat()方法将所有子数组元素连接到一个新数组中来创建一个新数组。它接受一个参数,一个数字。它表示你想把数组压平的深度:

onst numbers = [1, 2, [3, 4, [5, [6, 7]], [[[[8]]]]]]

const numbersflattenOnce = numbers.flat()
console.log(numbersflattenOnce) // => [1, 2, 3, 4, Array[2], Array[1]]

const numbersflattenTwice = numbers.flat(2)
console.log(numbersflattenTwice) // => [1, 2, 3, 4, 5, Array[2], Array[1]]

const numbersFlattenInfinity = numbers.flat(Infinity)
console.log(numbersFlattenInfinity) // => [1, 2, 3, 4, 5, 6, 7, 8]

flatMap() (ES2019)

该方法会在每个元素上运行一个map()函数,然后它将数组压扁一次。

const sentences = [
    'This is a sentence',
    'This is another sentence',
    "I can't find any original phrases",
]

const allWords = sentences.flatMap(sentence => sentence.split(' '))
console.log(allWords)

上面这个示例中,数组中有三个句子,你希望获取句子中所有单词。这个时候就可以直接使用flatMap(),而不是使用map()将所有的句子拆分为单词,然后将数组压扁。

flatMap()无关,但你可以使用reduce()来给句子中的单词计数:

const wordsCount = allWords.reduce((count, word) => {
    count[word] = count[word] ? count[word] + 1 : 1
    return count
}, {})

console.log(wordsCount)

flatMap()也经常用于响应式编程(Reactive Programming),你可以点击这里查看相关示例

join()

我们可以使用join()方法基于数组的元素来创建字符串。它允许通过连接数组的所有元素(由提供的分隔符来分隔)来创建一个新的字符串:

const participants = ['john', 'mary', 'gary']
const participantsFormatted = participants.join(', ')
console.log(participantsFormatted) // => john, mary, gary

下面是一个更真实的示例,使用filter()过滤出符合条件的参与者,并创建一个新字符串:

const potentialParticipants = [
    { id: 'k38i', name: 'john', age: 17 },
    { id: 'baf3', name: 'mary', age: 13 },
    { id: 'a111', name: 'gary', age: 24 },
    { id: 'fx34', name: 'emma', age: 34 },
]

const participantsFormatted = potentialParticipants
    .filter(user => user.age > 18)
    .map(user => user.name)
    .join(', ')

console.log(participantsFormatted) // => gary, emma

from()

这是一个静态方法,它从类数组或可迭代对象(例如字符串)中创建一个新数组。

const nodes = document.querySelectorAll('p') 
const todoItems = Array.from(nodes)

你是否看到我们使用Array而不是Array实例?这就是为什么from()被称为静态方法

然后你就可以在这些节点上做一些操作,比如使用forEach在每个节点上注册一个事件监听器:

todoItems.forEach(item => {
    item.addEventListener('click', function() {
        console.log(`You clicked on ${item.innerHTML}`)
    })
})

isArray

isArray是数组的另一个静态方法。它告诉你传递的值是否是数组。比如:

const nodes = document.querySelectorAll('p')
console.log(Array.isArray(nodes))       // => false

const todoItems = Array.from(nodes)
console.log(Array.isArray(todoItems))   // => true

改变原数组的一些数组方法

下面这些方法也是一些常见的数组方法。不同之处在于,它们会修改原始数组。

改变数组没有什么错,但是记住它是有好处的!

对于所有这些方法,如果你不想改变原始数组,只需要先做一个浅拷贝或深拷贝:

const arr = [1, 2, 3, 4, 5]
const copy = [...arr] // 或 arr.slice()

sort()

sort()它会对数组中的元素进行排序。默认的排序方法将所有元素转换为字符串,并按字母的顺序排序:

const names = ['john', 'mary', 'gary', 'anna']
names.sort()

console.log(names)     // => ['anna', 'gary', 'john', 'mary']

如果对一个数字数组进行排序,使用sort()并不会得到你想要的结果:

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort()

console.log(numbers)    // => [12, 17, 187, 23, 3, 90]

那么如何对数字数组进行排序呢?

事实上也不复杂,sort()接受一个比较函数(只接受一个函数)。这个函数接受两个参数:第一个元素(我们称之为a)和第二个元素(b,然后对这两个元素进行比较,而这两个元素之间的比较需要返回一个数字:

  • 如果返回的值是一个负数,则表示a排在b之前
  • 如果返回的值是一个正数,则表示a排在b之后
  • 如果返回的值是的0,表示没有变化

将上面的数字排序的示例修改之后:

const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort((a, b) => a - b)

console.log(numbers) // => [3, 12, 17, 23, 90, 187]

我们也可以按照最近的日期进行排序:

const posts = [
    {
        title: 'Create a Discord bot under 15 minutes',
        date: new Date(2018, 11, 26),
    },
    { 
        title: 'How to get better at writing CSS', 
        date: new Date(2018, 06, 17) 
    },
    { 
        title: 'JavaScript arrays', 
        date: new Date() 
    }
]

posts.sort((a, b) => a.date - b.date) 

console.log(posts)

fill()

fill()方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。fill()的一个重要用途是用静态值填充新数组。

function fakeUser() {
    return {
        id: 'fe38',
        name: 'thomas'
    }
}

const posts = Array(3).fill(fakeUser())

console.log(posts)

reverse()

reverse()sort()有点类似,不同的是,reverse()会在适当的位置反转数组。

const numbers = [1, 2, 3, 4, 5]

numbers.reverse()
console.log(numbers) // => [5, 4, 3, 2, 1]

pop()

pop()会从数组中删除最后一个元素并返回它。

const messages = ['Hello', 'Hey', 'How are you?', "I'm fine"]
const lastMessage = messages.pop()

console.log(messages)       // => ['Hello', 'Hey', 'How are you?']
console.log(lastMessage)    // => I'm fine

可替代的数组方法

接下来的方法中,你将找到改变原始数组的方法,这些方法可以很容易地替换为其他方法。这些方法让我们提高一些意识:

一些数组方法有副作用,而且可以找到相应的替代方法!

push()

push()方法允许你向数组中添加一个或多个元素。它还通常用于基于旧数组构建新数组。

onst todoItems = [1, 2, 3, 4, 5]

const itemsIncremented = []

for (let i = 0; i < items.length; i++) {
    itemsIncremented.push(items[i] + 1)
}

console.log(itemsIncremented) // => [2, 3, 4, 5, 6]

const todos = ['Write an article', 'Proofreading']

todos.push('Publish the article')

console.log(todos)  // => ['Write an article', 'Proofreading', 'Publish the article']

如果你想基于另一个类似itemsIncremented的数组来构建数组,那么我们可以map()filter()reduce()等方法。比如,使用map()

const itemsIncremented = todoItems.map(x => x + 1)

如果你想在需要添加新元素时使用push(),那么spread操作符就很有用:

const todos = ['Write an article', 'Proofreading']
console.log([...todos, 'Publish the article']) // => ['Write an article', 'Proofreading', 'Publish the article']

splice()

splice()通常用于删除某个索引处的元素。你也可以使用filter()做同样的操作:

const months = ['January', 'February', 'March', 'April', ' May']

months.splice(2, 1) 
console.log(months)     // => ['January', 'February', 'April', 'May']

const monthsFiltered = months.filter((month, i) => i !== 3)
console.log(monthsFiltered)     // => ['January', 'February', 'April', 'May']

如果我们需要删除很多元素时,则需要使用slice():

const months = ['January', 'February', 'March', 'April', ' May']

months.splice(1, 3) 
console.log(months) // => ['January', 'May']

const monthsSliced = [...months.slice(0, 1), ...months.slice(4)]
console.log(monthsSliced) // => ['January', 'May']

shift()

shift()方法会删除数组的第一个元素并返回它。你也可以使用spreadrest

const numbers = [1, 2, 3, 4, 5]

const firstNumber = numbers.shift()
console.log(firstNumber) // => 1
console.log(numbers)    // => [2, 3, 4, 5]

const [firstNumber, ...numbersWithoutOne] = numbers
console.log(firstNumber)        // => 1
console.log(numbersWithoutOne) // => [2, 3, 4, 5]

unshift()

unshift()允许向数组的开头添加一个或多个元素。也可以使用spread操作符来做同样的事情:

const numbers = [3, 4, 5]

numbers.unshift(1, 2)
console.log(numbers) // => [1, 2, 3, 4, 5]

const newNumbers = [1, 2, ...numbers]
console.log(newNumbers) // => [1, 2, 3, 4, 5]

简单的小结

  • 如果需要对数组进行操作行,不要使用for循环,也不要重复造轮子,因为有可能有一个方法可以替代他帮你实现你想要的事情
  • 大多数情况下,将使用map()filter()reduce()spread操作符
  • 有很多数组方法是很好,比如slice()some()flatMap()等,但我们需要掌握它们,并在适当的时候使用它们
  • 副作用会导致不必要的改变。注意哪些方法会改变原始数组
  • slice()spread操作符进行浅拷贝
  • 可改变数组的一些旧方法可以找到一些适当的新方法来替代

扩展阅读

特别声明,文章中大部分示例代码都来源于@thomas_lombart的《What you should know about JavaScript arrays》一文。jordan retro 11 mens black