不积跬步之JavaScript的数组

为学习<数据结构与算法>做准备,我们有必要梳理一下数组,因为我们需要它来模拟各种数据结构,如栈,列表,队列等。而实际上JavaScript的数组要比很多其他语言的数组功能强大太多了,在java语言中,如果我们实现删除第一个元素,就需要把所有后面的元素都向前挪一位,而在JavaScript中,只需要调用shift方法就可以了,而其他类似的方法还有很多,所以我们开始吧!

数组的length属性

数组的length属性非常有意思,它和数组下标密不可分,它的取值范围是0 到 2的32次方 = 4294967296 -1 的整数.

所以如果我们设置它为负数或者大于4294967296-1就会报错。

var namelistA = new Array(4294967296); // 2的32次方 = 4294967296 
var namelistC = new Array(-100) // 负号

console.log(namelistA.length); // RangeError: 无效数组长度 
console.log(namelistC.length); // RangeError: 无效数组长度

数组的方法

Array.from() ---ie不支持

从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。例如:

Array.from('foo'); 
// [ "f", "o", "o" ]

const set = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(set);
// [ "foo", "bar", "baz" ]

Array.isArray() --- ie9支持

判断是否是数组,这个方法要优先于使用instanceOf
因为Array.isArray能检测iframes

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

如果没有这个方法 Array.isArray(),我们可以添加全局方法:

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

关于判断数组是否是数组,可以看这篇文章

Array.of() 创建一个数组实例,而不考虑参数的类型和数量。

它和数组的构造函数的区别就是关于对整数的处理,构造函数Array(7)会创造一个有七个空元素的数组,而Array.of(7)会创建一个只有一个元素7的数组。

这个方法ie就不支持,所以可以添加如下方法来做兼容

if (!Array.of) {
  Array.of = function() {
    return Array.prototype.slice.call(arguments);
  };
}

修改方法

我们把数组的方法分为修改方法和迭代方法,修改方法会把调用它的原生数组对象修改了,而迭代方法不会修改原生的对象。

copyWithin()

方法浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。就是调换数组中元素。

const array1 = ['a', 'b', 'c', 'd', 'e'];

// copy to index 0 the element at index 3
console.log(array1.copyWithin(0, 3, 4));
// expected output: Array ["d", "b", "c", "d", "e"]

// copy to index 1 all elements from index 3 to the end
console.log(array1.copyWithin(1, 3));
// expected output: Array ["d", "d", "e", "d", "e"]

fill()

填充方法,用一个固定值填充数组从索引起始位置到终止位置,但是不包括终止位置。

[1, 2, 3].fill(4);               // [4, 4, 4]
[1, 2, 3].fill(4, 1);            // [1, 4, 4]
[1, 2, 3].fill(4, 1, 2);         // [1, 4, 3]
[1, 2, 3].fill(4, 1, 1);         // [1, 2, 3]
[1, 2, 3].fill(4, 3, 3);         // [1, 2, 3]
[1, 2, 3].fill(4, -3, -2);       // [4, 2, 3]
[1, 2, 3].fill(4, NaN, NaN);     // [1, 2, 3]
[1, 2, 3].fill(4, 3, 5);         // [1, 2, 3]
Array(3).fill(4);                // [4, 4, 4]
[].fill.call({ length: 3 }, 4);  // {0: 4, 1: 4, 2: 4, length: 3}

这个方法会修改调用数组,而不是返回一个新的数组。

pop()

该方法删除数组最后一个元素并返回,同时修改其length,也就是修改其原对象。

let myFish = ["angel", "clown", "mandarin", "surgeon"];

let popped = myFish.pop();

console.log(myFish); 
// ["angel", "clown", "mandarin"]

console.log(popped); 
// surgeon

pop 方法有意具有通用性。该方法和 call()apply() 一起使用时,可应用在类似数组的对象上。pop方法根据 length属性来确定最后一个元素的位置。如果不包含length属性或length属性不能被转成一个数值,会将length置为0,并返回undefined

push()

和上一个方法相反,它在数组最后一个位置添加元素

var sports = ["soccer", "baseball"];
var total = sports.push("football", "swimming");

console.log(sports); 
// ["soccer", "baseball", "football", "swimming"]

console.log(total);  
// 4

reverse()

顺序转换方法,第一个变最后一个,最后一个变第一个。

const a = [1, 2, 3];

console.log(a); // [1, 2, 3]

a.reverse(); 

console.log(a); // [3, 2, 1]

shift()

方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。

用这个方法我们可以模拟出栈,

const array1 = [1, 2, 3];

const firstElement = array1.shift();

console.log(array1);
// expected output: Array [2, 3]

console.log(firstElement);
// expected output: 1

shift 方法移除索引为 0 的元素(即第一个元素),并返回被移除的元素,其他元素的索引值随之减 1。如果length 属性的值为 0 (长度为 0),则返回undefined

shift 方法并不局限于数组:这个方法能够通过 callapply 方法作用于类似数组的对象上。但是对于没有length 属性(从0开始的一系列连续的数字属性的最后一个)的对象,调用该方法可能没有任何意义。

这个方法和pop方法相似。也是通用性的。

sort()

排序算法,如果不指定排序算法,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。例如 "Banana" 会被排列到 "cherry" 之前。当数字按由小到大排序时,9 出现在 80 之前,但因为(没有指明 compareFunction),比较的数字会先被转换为字符串,所以在Unicode顺序上 "80" 要比 "9" 要靠前。

const months = ['March', 'Jan', 'Feb', 'Dec'];
months.sort();
console.log(months);
// expected output: Array ["Dec", "Feb", "Jan", "March"]

const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1);
// expected output: Array [1, 100000, 21, 30, 4]

排序算法我们可以写成这样:

function compare(a, b) {
  if (a < b ) {           // 按某种排序标准进行比较, a 小于 b
    return -1;
  }
  if (a > b ) {
    return 1;
  }
  // a must be equal to b
  return 0;
}

如果是数组的对象排序,我们可以这样写:

var items = [
  { name: 'Edward', value: 21 },
  { name: 'Sharpe', value: 37 },
  { name: 'And', value: 45 },
  { name: 'The', value: -12 },
  { name: 'Magnetic' },
  { name: 'Zeros', value: 37 }
];

// sort by value
items.sort(function (a, b) {
  return (a.value - b.value)
});

这里就不展开了。

splice()

强大的splice方法。可攻可受。

该方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

它有三个参数,start,deleteCount,item1,item2,item3

start:指定修改的起始位置,从0开始,如果超出数组长度,并不会报错,而是从数组末尾开始添加;如果是负数,则从末尾开始计算,-1就是length-1,如果负数的绝对值大于数组的长度,则从0开始。

deleteCount:可选,删除元素的个数,如果是0则表示不删除,而必须添加一个元素。

  • 如果该参数大于start后的元素个数,则表示删除start后的所有元素,包括start位置的元素

item1,item2,item3...:可选,表示要添加进数组的元素,如果不指定,则splice将只删除元素。

我们知道了上面的三个参数,splice方法就是三个参数的配合使用,它的返回值是删除的元素组成的数组,如果没有删除就返回空数组。

模仿pop方法,删除最后一个元素
const array = ["A","B","C"];
let element = array.splice(array.length-1,1);
console.log(element);//["C"]
console.log(array)//["A", "B"]

模仿push方法,在数组的后面添加一个元素
const array = ["A","B","C"];
let element = array.splice(array.length,0,"D");
console.log(element);//[]
console.log(array)//["A", "B","C","D"]
模仿unshift方法,在数组的开头添加元素
const array = ["A","B","C"];
let element = array.splice(0,0,"D");
console.log(element);//[]
console.log(array)//["D",A", "B","C"]
模仿shift方法,删除数组开头的元素
const array = ["A","B","C"];
let element = array.splice(0,1);
console.log(element);//["A"]
console.log(array)//["B","C"]
最后删除2位置以后的所有元素
const array = ["A","B","C"];
let element = array.splice(1);
console.log(element) //["B",""]

unshift()

在数组的首位插入元素

let arr = [4,5,6];
arr.unshift(1,2,3);
console.log(arr); // [1, 2, 3, 4, 5, 6]

以上的方法在调用时都会修改原数组对象。而下面的方法则绝对不会修改原数组对象,而是返回新的数组实例或者不做更改。

访问方法:

concat()

连接方法,它可以把两个数组连接为一个数组,并返回一个新的实例。

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);

console.log(array3);
// expected output: Array ["a", "b", "c", "d", "e", "f"]

concat方法创建一个新的数组,它由被调用的对象中的元素组成,每个参数的顺序依次是该参数的元素(如果参数是数组)或参数本身(如果参数不是数组)。它不会递归到嵌套数组参数中。

concat方法不会改变原数组,或者是对象,而是进行浅拷贝,如果是对象类型,只会引入对象的连接,如果是字符串或者其他的类型,则会进行拷贝。

includes

这个方法用来判断数组是否包含一个元素,返回truefalse.

[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true

它还有另一个参数,就是查找的起始位置,从什么地方开始查找。

var arr = ['a', 'b', 'c'];

arr.includes('c', 3);   // false
arr.includes('c', 100); // false

如果查找的位置超出数组的长度,则不会进行查找。

这个方法是一个新方法,ie不支持。

我们可以使用该方法来做一些去掉if语句的优化。

join()

将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。

const elements = ['Fire', 'Air', 'Water'];

console.log(elements.join());
// expected output: "Fire,Air,Water"

console.log(elements.join(''));
// expected output: "FireAirWater"

console.log(elements.join('-'));
// expected output: "Fire-Air-Water"

指定一个字符串来分隔数组的每个元素。如果需要,将分隔符转换为字符串。如果缺省该值,数组元素用逗号(,)分隔。如果separator是空字符串(""),则所有元素之间都没有任何字符。

slice()

返回一个新的数组对象,这一对象是一个由beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];

console.log(animals.slice(2));
// expected output: Array ["camel", "duck", "elephant"]

console.log(animals.slice(2, 4));
// expected output: Array ["camel", "duck"]

console.log(animals.slice(1, 5));
// expected output: Array ["bison", "camel", "duck", "elephant"]

start:可选,如果省略,则从0开始,如果是负数,则从数组的后面开始计算。如果大于数组的长度,则返回空数组,

end:可选,如果省略,则拷贝到数组的末尾,如果是负数,则从数组的后面开始计算,如果大于数组的长度,则返回数组的最后末尾。

slice:方法会返回一个新的数组对象,进行浅拷贝,对象元素是拷贝对象的引用,如果是字符串,整数,布尔值则进行拷贝。浅拷贝对象,则会造成修改对象,原对象也会修改。

const array = ["A","B","C"];
let data = array.slice();
console.log(data) //  ["A","B","C"];

直接复制一个数组,返回一个新的。

indexOf()

这个方法和字符串的indexOf一样,我一直以为只有字符串才有这个方法。它的作用是查找数组中元素的位置,如果存在就返回索引,如果没有找到就返回-1.

var array = [2, 5, 9];
array.indexOf(2);     // 0
array.indexOf(7);     // -1
array.indexOf(9, 2);  // 2
array.indexOf(2, -1); // -1
array.indexOf(2, -3); // 0

那么如何找出数组中所有该元素的位置呢?

var indices = [];
var array = ['a', 'b', 'a', 'c', 'a', 'd'];
var element = 'a';
var idx = array.indexOf(element);
while (idx != -1) {
  indices.push(idx);
  idx = array.indexOf(element, idx + 1);
}
console.log(indices);
// [0, 2, 4]

lastIndexOf()

这个方法和上面indexOf作用一样,只是从数组的末尾位置开始查找。

const animals = ['Dodo', 'Tiger', 'Penguin', 'Dodo'];

console.log(animals.lastIndexOf('Dodo'));
// expected output: 3

console.log(animals.lastIndexOf('Tiger'));
// expected output: 1

迭代方法

终于到了迭代方法,这里。

迭代方法他们都需要一个回调函数,在遍历每一个元素时,把该元素放入回调函数中,进行执行操作。在这个遍历的过程中,它会把length属性缓冲到某个地方,在遍历的时候,如果你在遍历的过程中添加了新的元素,后面的遍历并不会被遍历到。而如果你删除了某个元素,则有可能造成未知的影响。所以在遍历的时候不要对原数组对象进行任何修改操作。

forEach()

我们熟悉的forEach,它的作用和map很像,区别是map会返回一个数组,而forEach则返回undefined,所以它无法链式调用。我们这里就说一下它的一些特性吧。

1.除了抛出错误,你没有任何办法来终止或者跳出循环遍历,如果你想要中途跳出循环,请换一种实现方式,例如:
  • 一个简单的 for 循环
  • for...of / for...in 循环
  • Array.prototype.every()
  • Array.prototype.some()
  • Array.prototype.find()
  • Array.prototype.findIndex()
2.它只可以对有效值进行遍历

什么叫有效值,一个数组是这样const arr = [1,,3,7],那么它的有效值就是1,3,7而中间的那个值就不会被遍历到而是跳过。

const arraySparse = [1,3,,7];
let numCallbackRuns = 0;

arraySparse.forEach(function(element){
  console.log(element);
  numCallbackRuns++;
});

console.log("numCallbackRuns: ", numCallbackRuns);

// 1
// 3
// 7
// numCallbackRuns: 3
3.它不可以链式调用

因为它每一次执行完毕回调函数,返回的是undefined,它无法向mapreduce那样。

4.它在调用时不会改变原数组,准确的说是,它不改变原数组,但是保不齐它的回调函数会改变。

entries()方法

该方法返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对。

关于它的使用这里就不在描述,可以看一个比较复杂的例子:

function sortArr(arr) {
    var goNext = true;
    var entries = arr.entries();
    while (goNext) {
        var result = entries.next();
        if (result.done !== true) {
            result.value[1].sort((a, b) => a - b);
            goNext = true;
        } else {
            goNext = false;
        }
    }
    return arr;
}

var arr = [[1,34],[456,2,3,44,234],[4567,1,4,5,6],[34,78,23,1]];
sortArr(arr);

/*(4) [Array(2), Array(5), Array(5), Array(4)]
    0:(2) [1, 34]
    1:(5) [2, 3, 44, 234, 456]
    2:(5) [1, 4, 5, 6, 4567]
    3:(4) [1, 23, 34, 78]
    length:4
    __proto__:Array(0)
*/

every()方法

该方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

用人话说就是,检查所有人,是不是大于30岁?

需要注意的是,调用这个方法的数组是空数组,则永远返回true

const isBelowThreshold = (currentValue) => currentValue < 40;

const array1 = [1, 30, 39, 29, 10, 13];

console.log(array1.every(isBelowThreshold));
// expected output: true

我们有必要听一下它的描述:

every 方法为数组中的每个元素执行一次 callback 函数,直到它找到一个会使 callback 返回 false 的元素。如果发现了一个这样的元素,every方法将会立即返回 false。否则,callback 为每一个元素返回 trueevery 就会返回 truecallback 只会为那些已经被赋值的索引调用。不会为那些被删除或从未被赋值的索引调用。

callback 在被调用时可传入三个参数:元素值,元素的索引,原数组。

如果为 every 提供一个 thisArg 参数,则该参数为调用 callback 时的 this 值。如果省略该参数,则 callback 被调用时的 this 值,在非严格模式下为全局对象,在严格模式下传入 undefined。详见 this 条目。

every 不会改变原数组。

every 遍历的元素范围在第一次调用 callback 之前就已确定了。在调用 every 之后添加到数组中的元素不会被 callback 访问到。如果数组中存在的元素被更改,则他们传入 callback 的值是 every 访问到他们那一刻的值。那些被删除的元素或从来未被赋值的元素将不会被访问到。

every 和数学中的"所有"类似,当所有的元素都符合条件才会返回true。正因如此,若传入一个空数组,无论如何都会返回 true。(这种情况属于无条件正确:正因为一个空集合没有元素,所以它其中的所有元素都符合给定的条件。)

使用箭头函数:

[12, 5, 8, 130, 44].every(x => x >= 10); // false
[12, 54, 18, 130, 44].every(x => x >= 10); // true

some() 方法

该方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。

它和上面every()方法都是用来判断数组中的元素符合一个给定的条件。区别是every是判断所有的元素全都符合才返回true,只要一个不符合就返回false,而some是只要有一个元素符合就返回true.

需要注意的是:如果用一个空数组进行测试,在任何情况下它返回的都是false。

看一下例子:数组中是否有大于10的元素存在。

function isBiggerThan10(element, index, array) {
  return element > 10;
}

[2, 5, 8, 1, 4].some(isBiggerThan10);  // false
[12, 5, 8, 1, 4].some(isBiggerThan10); // true

filter()方法

该方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];

const result = words.filter(word => word.length > 6);

console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

通过一个给定的条件来筛选符合的元素并返回一个新的数组。所以我们可以通过这个filter方法来实现一个remove的操作,原理非常简单,筛选不是这个元素的元素并返回,就相当于删除了它了。

const arr = [1,2,3,4,5,6];
const after_arr =  arr.filter(item=>item !== 3);
console.loog(arrter_arr);
//[1,2,4,5,6]

find()方法

找到符合给定条件的第一个元素,并返回,否则返回undefined.

const array1 = [5, 12, 8, 130, 44];

const found = array1.find(element => element > 10);

console.log(found);
// expected output: 12

这个方法有一个特殊的点:那就是对[1,2,3,,5,6]这种数组,它并不是遍历有效值,而是从0 ~ length-1,也就是中间那个没有值的位置也会遍历到,那么这样的话就没有forEach这种只遍历有效值的方法有效率。

findIndex()方法

该方法和上面的方法很像,区别是这个方法返回索引,而上面的方法则返回元素。

const array1 = [5, 12, 8, 130, 44];

const isLargeNumber = (element) => element > 13;

console.log(array1.findIndex(isLargeNumber));
// expected output: 3

keys()方法

该方法返回一个包含数组中每个索引键的Array Iterator对象。

它特殊的地方是会返回没有对应元素的索引。

var arr = ["a", , "c"];
var sparseKeys = Object.keys(arr);
var denseKeys = [...arr.keys()];
console.log(sparseKeys); // ['0', '2']
console.log(denseKeys);  // [0, 1, 2]

map()方法

我们最最常用的方法。该方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

const array1 = [1, 4, 9, 16];

// pass a function to map
const map1 = array1.map(x => x * 2);

console.log(map1);
// expected output: Array [2, 8, 18, 32]

map 方法会给原数组中的每个元素都按顺序调用一次 callback 函数。callback 每次执行后的返回值(包括 undefined)组合起来形成一个新数组。 callback 函数只会在有值的索引上被调用;那些从来没被赋过值或者使用delete 删除的索引则不会被调用。

如果你不打算用map返回的新数组,那么你就不要用这个方法,这样会很让人疑惑,你可以使用forEach或者for ... of等其他的方法来代替.

下面的例子演示如何在一个 String 上使用 map 方法获取字符串中每个字符所对应的 ASCII 码组成的数组:

var map = Array.prototype.map
var a = map.call("Hello World", function(x) { 
  return x.charCodeAt(0); 
})
// a的值为[72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]

更多的map信息可以来MDN查询阅读。

reduce()方法

该方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

reducer 函数接收4个参数:

  1. Accumulator (acc) (累计器)
  2. Current Value (cur) (当前值)
  3. Current Index (idx) (当前索引)
  4. Source Array (src) (源数组)

reducer 函数的返回值分配给累计器,该返回值在数组的每个迭代中被记住,并最后成为最终的单个结果值。

reduce为数组中的每一个元素依次执行callback函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:

accumulator 累计器
currentValue 当前值
currentIndex 当前索引
array 数组
回调函数第一次执行时,accumulatorcurrentValue的取值有两种情况:如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,`currentValue``取数组中的第二个值。

注意:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。

如果数组为空且没有提供initialValue,会抛出TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。

提供初始值通常更安全,正如下面的例子,如果没有提供initialValue,则可能有三种输出:

var maxCallback = ( acc, cur ) => Math.max( acc.x, cur.x );
var maxCallback2 = ( max, cur ) => Math.max( max, cur );

// reduce() 没有初始值
[ { x: 22 }, { x: 42 } ].reduce( maxCallback ); // 42
[ { x: 22 }            ].reduce( maxCallback ); // { x: 22 }
[                      ].reduce( maxCallback ); // TypeError

// map/reduce; 这是更好的方案,即使传入空数组或更大数组也可正常执行
[ { x: 22 }, { x: 42 } ].map( el => el.x )
                        .reduce( maxCallback2, -Infinity );

关于reducereduceRight方法的更多信息,可以看MDN官网的文章。

关于数组的学习就到这里把。

over...

推荐阅读更多精彩内容