【JS学习笔记】Array

306 阅读15分钟

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

Array

创建

const arr1 = new Array(),		// []
      arr2 = new Array(10),             // arr.length == 10;
      arr3 = new Array(10,11),	        // [10, 11]
      arr4 = new Array("10"),           // ["10"]
      arr5 = [10];                      // [10]

通过Array()构造函数创建数组,如果只传入一个参数,且参数是数字,那么数组的长度等于数字。这样创建的数组里面没有任何元素,只有相应的空位(empty),而不是undefined,当然通过[]获取时,还是会显示undefined。

数组的length属性是不可遍历的,但却是可写的。虽然可以通过Object.definePropertywritable,让其变成不可写,然后给数组添加元素时会报错。

用数组字面量[]创建会更加直观。

const arr = new Array(10),
      arr2 = Array(10);

两种创建方式没有区别。

Array静态方法

from()

ES6新增。

可以将类数组(有length属性和可索引元素的结构)转换成数组实例,浅克隆。

可以将可迭代的结构,转换成数组实例(迭代每一项并存入数组)。

Array.from(arrayLike[, mapFn[, thisArg]])
  1. arrayLike想要转换成数组的伪数组对象或可迭代对象。

  2. mapFn可选,传入一个函数,新数组中的每个元素会执行该回调函数,相当于调用map。

    Array.from(obj, mapFn, thisArg) 就相当于Array.from(obj).map(mapFn, thisArg)

  3. thisArg 可选,设置执行回调函数 mapFnthis 对象,对箭头函数无效。


Array.from("array");
// ["a", "r", "r", "a", "y"]

let strObj = new String("abcd");
strObj[4] = "e";
strObj[5] = "f";
Array.from(strObj);
// ["a", "b", c", "d"]

内部应该是对传进来的参数进行了转型,调用了Object(arrayLike),所以把字符串转换成了字符串对象,而字符串对象是一个原生的类数组,from根据类数组的长度确定迭代的次数,通过类数组的索引取出每一位的值,因为String对象的长度是不可改变的,所以即使增加了索引也无法迭代出来。

其实字符串对象实现了iterable接口,是一个可迭代的数据结构,可以通过[Symbol.iterator]方法,创建迭代器。从而迭代每一个字符。

Array.from(true);
Array.from(1);
// []

因为这些原始类型被转换成对象后,并没有length属性,所以内部创建了一个长度为0的数组,并返回。

内部实现详见MDN

// 此处待补

还可以将集合、映射和可迭代对象转换为一个新数组。

function createArr(len) {
    return Array.from(new Array(len), num =>Math.floor(Math.random() *10));
}

利用第二参数完成一些简单的操作。

Array.from(new Array(10));
// [undefined, undefined, undefined]

自动填满,全是undefined。

of()

可以把传入的参数转换为数组。

Array.of(false, 1, "2", [3], {index: 4}, function five() {});
// [false, 1, "2", Array(1), {…}, ƒ]

传入的参数个数不限,类型不限。

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

在ES6之前要实现这样的功能,只能通过arguments来收集参数,在转换成数组返回。

isArray()

用于确定传递的值是否为Array类型。

在网页中只有一个全局作用域时,用instanceof就可以判断。

但是,如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的Array构造函数。

Array.isArray()不用管它是在哪个全局执行上下文中创建的。

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

上述代码有相同的效果。

数值空位

const arr1 = new Array(3);
const arr2 = [ , , ,];
// [empty × 3]
arr2.length === 3    
// true

如果用数组字面量创建数组,里面不填元素,只有逗号时,每一个逗号及其前面的空位算作一个长度" ,"

Array.from(arr1);
// [undefined, undefined, undefined]

for(const val of arr2) { val === undefined }
// true true true

[,,,,].fill(1)
// [1,1,1,1]

ES6规范重新定义了如何处理这些空位,普遍将这些空位当成值为undefined的元素。其中包括Array.from()和迭代方法(for-of)。

Array.from会把空位转换成undefined,for-of把数组这个可迭代结构迭代出来,其值也是undefined。fill把每一个空位都填充了1。

其本质应该是,每次取值都用arr[i],如果是空位就会返回undefined,所以只要长度变化了,其他空位都当成undefined处理。

for(const val in [,,,,]) { console.log(val) }
// 什么也没有输出

new Array(3).map(num => num*2)
// 返回[empty × 3]

[1,,,,3].join("-")
// "1----3"  相当于 "1" + "-" + "" + "-" + "" + "-" + "" + "-" + "3"

[1,,,,3].toString()
// "1,,,,3"

ES6之前的方法会忽略数组内的空位。

就像for循环和map都会跳过空位,join()把空位当成空字符串来进行拼接。

由于行为不一致和存在性能隐患,尽量避免使用相同数组空位。

数组索引

const arr = [1,2,3,4];
arr[8] = 9;
// [1, 2, 3, 4, empty × 4, 9]
// arr.length == 8 + 1

如果把一个值设置给超过数组最大索引的索引,数组长度会自动扩展到索引值加1。

arr.length = 3;
// [1,2,3]

数组里只剩下3个元素,末尾其他元素就被删除了。

arr.length = 6;
// [1, 2, 3, empty × 3]

length设置为大于数组元素个数的值,新添的元素都是空位。空位会被ES6之前的方法忽略,例如forEach。

Array实例方法

迭代器方法

  1. keys()

    返回数组索引的迭代器。

    const arr = ["a", "b", "c"],
          indexIt = arr.keys();
    indexIt.toString()
    // "[object Array Iterator]"
    // Symbol.toStringTag
    

    keys()方法可以看作迭代器创建函数,返回一个迭代器。

    indexIt === indexIt[Symbol.iterator]()
    // true
    

    同样这个迭代器也实现了可迭代协议,它可以调用Symbol.iterator返回自身,也就是说,这两个方法返回的迭代器对象是同一个

    这样做的目的在于可以用ES6新增的迭代方法(Array.from()、for-of、展开操作符、数组解构等),因为这些方法,根据传入的可迭代结构的对象,自动调用这个对象的Symbol.iterator方法,创建一个迭代器,然后进行迭代操作。

    Array.from(indexIt);  // Array.from(arr.keys());
    // [0, 1, 2]
    indexIt.next();
    // Object Iterator {value: undefined, done: true}
    

    把数组索引迭代器转型成数组。

    因为是同一个迭代器,而且迭代器又是一次性的(临时对象),所以Array.from()调用完,再获取下一个元素时就没了。

    const emptyArr = new Array(10);
    [...empty.keys()]
    // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

    数组内的空位索引也会返回。

    const emptyArrIndexIt = emptyArr.keys();
    emptyArr.length = 4;
    Array.from(emptyArrIndexIt);
    // [0, 1, 2, 3]
    

    迭代器储存的是数组的原地址,数组改变,迭代器也跟着变化。

  2. values()

    返回数组值的迭代器

    const arr = ["a", "b", "c", "d"],
          valuesIt = arr.values(),
          valuesArr = Array.from(valuesIt);
    // ["a", "b", "c", "d"]
    valuesIt === valuesIt[Symbol.iterator]()
    // true
    

    和keys()一样,实现思想一样。

    Array.prototype.values === Array.prototype[Symbol.iterator]  
    // true 
    

    Array.prototype.valuesArray.prototype[Symbol.iterator]的默认实现,两个迭代器创建函数一样

  3. entries()

    返回数组键值对的迭代器。

    const arr = ["a", "b", "c"],
          entriesIt = arr.entries(),
          entriesArr = Array.from(entriesIt);
    // [[0, "a"], [1, "b"], [2, "c"]]
    

    利用迭代器实现二维数组排序。

    function arrSort(arr) {
        // 得到数组键值对迭代器
        let entries = arr.entries();
        // 判断下一个是否存在
        let hasNext = true;
        // 循环排序
        while(hasNext) {
            let next = entries.next();
            // 存在就排序
            if(!next.done) {
                next.value[1].sort((a, b) => a - b);
            }else {
                // 不存在就让下一次进不了循环
                hasNext = false;
            }
        }
    }
    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)
    */
    

    迭代器的保存的就是原数组的地址,所以排序也会改变原数组。

    这三个方法都差不多。

复制和填充方法

ES6新增了两个方法:批量复制copyWithin(),填充数组方法fill()

  1. copyWithin()

    浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。

  2. fill()

转换方法

  1. toString()

    返回由数组中每个元素的等效字符串(相当于每个元素都调用String()转型成字符串)拼接而成的一个逗号分隔的字符串。

    多维数组也是,想把里面的数组转换成字符串,再转换外面的。

    const arr1 = new Array(10),
          arr2 = [1,null,undefined];
    arr1.toString();   // ",,,,,,,,,"
    arr2.toString();   // "1,,"
    

    注意:数组内的元素如果是null或undefined或空位时,调用toString会用""空字符串拼接。事实上下面的转换方法也都如此。

  2. valueOf()

    MDN上很透彻

  3. toLocaleString()

  4. join()

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

    Array.prototype.toString()一样,会用String()转型函数,把元素都变成字符串了再拼接。

    const arr = ["1", new Boolean(true), 1000, Object()];
    arr.join();
    // "1,true,1000,[object Object]"
    

    无参数时,默认使用,号拼接。

    const arr1 = [1,2,3,4];
    arr1.join("-");        // "1-2-3-4"
    arr1.join(" + ");	   // "1 + 2 + 3 + 4"
    arr1.join("|");		   // "1|2|3|4"
    const empty = [,,,,];
    empty.join();          // ",,,"
    empty.join("-");       // "---"
    

栈方法

使用数组提供的方法,让它看起来像某种数据结构。会改变原数组

栈是一种后进先出的结构。从尾端加入,从尾端移出。

  1. push()

    const arr = [1,2,3];
    arr.push(4);
    arr.push(5,6,7);
    // [1,2,3,4,5,6,7]
    

    将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

    const obj = {
        0: "a",
        1: "b"
    }
    Array.prototype.push.call(obj, "c");
    // {0: "c", 1: "b", length: 1}
    

    push 方法具有通用性。该方法和call()apply()一起使用时,可应用在类数组上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

    const str = "abcd";
    Array.prototype.push.call(str, "e", "f", "g");
    // TypeError: Cannot assign to read only property 'length' of object '[object String]'
    

    String类数组不适用,因为其长度是不可变的。

    const a = ["a", "b"];
    const b = ["c", "d"];
    a.push(...b);          				// 展开运算符
    Array.prototype.push.apply(a, b);   // apply接收放在数组里的参数
    

    把一个数组增加到另一个数组末尾。

    像数组一样使用对象,详见MDN。

  2. pop()

    const arr = ["a", "b", "c", "d", "e"];
    let deleteEle = arr.pop();
    // "e"
    

    pop 方法从一个数组中删除并返回最后一个元素。

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

    如果你在一个空数组上调用 pop(),它返回undefined。也就是说,如果数组删完了,再删除会返回undefined

通过push()在栈顶增加元素,pop()移出栈顶的元素(弹栈)。模拟一个栈的结构。

队列方法

队列是一种先进先出的数据结构。从尾端加入,从首端移出。

  1. shift()

    从数组中删除第一个元素,并返回该元素的值。

    const arr = ["a", "b", "c", "d", "e"];
    let deleteEle = arr.pop();
    // "a"
    

    pop()类似,一个删除首,一个删除尾。

  2. push()

    用来从尾端加入元素。

  3. unshift()

    将一个或多个元素添加到数组的开头,并返回该数组的新长度。

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

    如果传入多个参数,它们会被以块的形式插入到对象的开始位置,它们的顺序和被作为参数传入时的顺序一致。 所以,传入多个参数调用一次 unshift ,和传入一个参数调用多次 unshift (例如,循环调用),它们将得到不同的结果。

  4. pop()

    从尾端删除元素。

shift()push()正方向上的队列。unshift()pop()是反方向上的队列。

排序方法

  1. reverse()

    将数组中元素的位置颠倒,并返回该数组。改变原数组

    const arr1 = [1,2,3,4,5],
          arr2 = [["a", "b", "c"], [1, 2, 3]];
    arr1.reverse();   // [5,4,3,2,1]
    arr2.reverse();   // [[1, 2, 3], ["a", "b", "c"]]
    

    数组内的元素颠倒了。

    var obj = {
        0: "a",
        1: "b",
        2: "c",
        length: 2
    }
    Array.prototype.reverse.call(obj);
    // {0: "b", 1: "a", 2: "c", length: 2}
    

    同样可以应用于类数组,会根据类数组的lengthindex属性改变。明明有三个元素,但长度只有2,所以第三个元素就没有位置颠倒。

  2. sort()

    对数组的元素进行排序。

    const arr = [1, 1000, 4, 32];
    arr.sort();
    // [1, 1000, 32, 4]
    
    const eleArr = [1, "a", "A"];
    eleArr.sort();
    // [1, "A", "a"]
    // charCode
    // 	1  --> 49
    // "a" --> 97
    // "A" --> 65
    

    默认排序顺序是将元素转换为字符串,按照转换为的字符串的诸个字符的Unicode位点进行排序。

    调用sort()后,原数组已经排序完成。

    arr.sort(function(a, b) {
        return a - b;
    })
    // [1, 4, 32, 1000]
    

    sort()方法可以接受一个比较函数,用于判断哪个值排在前面。

    比较函数接受两个参数(a和b),分别表示数组内连续的两个元素。如果函数的返回值小于0,a和b不会互换位置。如果函数的返回值大于0,a和b就交换位置。

    如果数组内的元素是数字,a - b小于零,位置保持不变,表示数字小的排在前面,数字大的排在后面。升序排列。

    其他根据数据结构传入的比较函数详见MDN。

操作方法

  1. concat()

    合并两个或多个数组。不会改变现有数组,返回一个新数组。

    const arr1 = [1,2,3,4],
          arr2 = [6,7,8];
    const newArr = arr1.concat(5, arr2);
    // [1, 2, 3, 4, 5, 6, 7, 8]
    

    不管参数传入的是数字,还是数组,都会添加至新数组末尾。可能期望是把一个数组添加至新数组末尾,但它会打平数组,就像...arr2一样,展开数组,把每一个元素添加至末尾。

    arr2[Symbol.isConcatSpreadable] = false;
    arr1.concat(5,arr2);
    // [1, 2, 3, 4, 5, [6, 7, 8]]
    

    可以通过知名符号:Symbol.isConcatSpreadable来控制是否打平数组或类数组。默认是不会打平类数组的。

    const herb = {
        name: "herb",
        age: 18,
        sex: "男"
    }
    const num = [1, 2];
    const arr = num.concat(herb);
    // [1, 2, {name: "herb", age: 18, sex: "男"}]
    arr[2].sex = "女";
    // {name: "herb", age: 18, sex: "女"}
    

    返回一个浅克隆的数组,修改对象的属性,外面的对象属性也会跟着改变。

  2. slice()

    浅克隆一个数组,可以指定克隆的范围,返回一个新数组。

    const colors = ["red", "green", "blue", "yellow", "purple"];
    const colors1 = colors.slice();
    // ["red", "green", "blue", "yellow", "purple"]
    const colors2 = colors.slice(1,3);
    // ["green", "blue"]
    const colors3 = colors.slice(-2);
    // ["yellow", "purple"]
    

    基本上的细节和String.protototype.slice()差不多。

  3. splice()

    可以任意位置增加、删除数组中的元素,会改变原数组。

    array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
    

    start开始位置,支持负数。

    deleteCount删除的个数。

    item1, item2,...start位置开始,添加进数组的元素。

    返回一个数组,里面是从数组中删除的元素。

    const colors = ["red", "green", "blue", "yellow", "purple"];
    colors.splice();               // 无事发生,应该是判断arguments.length == 0 
    colors.splice(undefined);      // 全部删除,因为Number(undefined) == NaN,如果参数为NaN就变成0,从零开始全部删除。
    colors = ["red", "green", "blue", "yellow", "purple"];   // 重置
    colors.splice(false, 1, "red"); // Number(false) == 0, 从零开始删除一个,再在零的位置上增加一个。
    

    参数先转成数字类型,再转成整型,如果是NaN就为0,如果该方法支持负数,那索引为负数时,加上数组的长度,如果还为负数就变为0,如果参数进行了这么多次转换还不符合逻辑,就什么都不做,String.prototype.substring会交换参数位置,左小右大。

搜索和位置方法

  1. 严格相等搜索

    • indexOf()

      在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

      arr.indexOf(searchElement[, fromIndex])
      

      searchElement要搜索的元素,fromIndex从哪里开始,支持负数,默认从零开始。

      采用严格相等(===)

    • lastIndexOf()

      indexOf()类似,只不过是从数组的后面向前查找。

      无论第二个参数怎么输入,都是从后面向前查找。

    • includes()

      ES7新增的方法,判断一个数组是否包含一个指定的值。

      const arr = new Array(10);
      arr.includes(undefined);
      // true
      arr.indexOf(undefined);
      // -1
      

      把空位当成undefiend。

  2. 断言函数搜索

    • find()

      arr.find(callback[, thisArg])
      

      callback 断言函数,按照定义的断言函数搜索数组,从最小的索引开始,每个索引都会调用这个函数。断言函数接受三个参数:元素、索引、数组本身。元素是数组中当前搜索的元素,索引是当前元素的索引,数组是正在搜索的数组。返回true表示匹配成功。

      thisArg可选参数,指定断言函数内部的this指向。

      const team = [
          {
          	name: "herb",
          	age:22
      	},
          {
              name: "ice",
              age: 18
          }
      ]
      team.find((people) => people.age > 15);
      // {name: "herb", age: 22}
      team.find((people) => people.name = "herb");
      // {name: "herb", age: 22}
      

      找打匹配项后,就不再继续搜索了。断言函数可以很灵活的进行判断,找到符合你要求的值,但找到就不往后找了。

      当在回调中删除数组中的一个值时,当访问到这个位置时,其传入的值是 undefined。详见MDN。

    • findIndex()

      通过断言函数找到第一个匹配的元素,返回其在数组的索引。

      find()类似。

      详见MDN。

迭代方法

  1. every()
  2. some()
  3. forEach()
  4. map()
  5. filter()

归并方法

  1. reduce()
  2. reduceRight()
待补。。。