JavaScript中的new Array的push使用问题

675 阅读5分钟

前言

前阵子写代码的时候无意间发现了一个问题,就是通过new Array初始化的非一维数组,在对某一维的数组进行push操作的时候,会同时改变其他的数组,例如:

let newArr = new Array(4).fill(new Array(4).fill(0))

newArr[0].push(1)
console.log(newArr);

我们定义一个长度为4的数组,数组的每一项都是长度为4的数组,并且都初始化为0,这时候我们想要在第一项的数组后面通过push操作加上一个1,这时候的我们期望的数组是:

image.png

但实际上,我们获取到的数据状态是:

image.png

很显然出现了一个bug,那么我们就要从两方面来排查,到底是二维数组这样使用push有问题,还是new Array创建二维数组的时候有一些需要注意事项。

Array的push

push方法,对于大家来说都是非常熟悉的方法了,主要作用就是在数组的末尾添加元素,可以是一个,也可以是多个,多个的时候一般都是数组的拼接,例如

let arr1 = [1,2,3]
let arr2 = [4,5,6]

arr1.push(...arr2)

那push方法作为Array的原型方法到底是怎么实现的呢?

ECMA官网中我们可以找到关于push的解释,通过官网的解释,我们可以写出一个push方法

Array.prototype.myPush = function(...items) {
    // 1. Let O be ? ToObject(this value). 
    // 定义一个O接收this,同时this要转换为一个对象
    let O = Object(this);
    // 2. Let len be ? LengthOfArrayLike(O).
    // 通过let定义长度len,x >>> 0 是为了保证x为数字类型
    let len = this.length >>> 0;
    // 3. Let be the number of elements in .argCountitems
    let argCount = items.length >>> 0;
    // 4. If + > 253 - 1, throw a TypeError exception.lenargCount
    // 如果len个argCount相加超过js能表示的最大正整数,则抛出异常
    if(len + argCount > 2 ** 53 - 1) {
        throw new TypeError("超出最大值");
    }

    /**
     * 5. For each element of , doEitems
     * a. Perform ? Set(, ! ToString(𝔽()), , true).OlenE
     * b. Set to + 1.lenlen
     * 如果没有超出最大值,开始循环items
     * 将items的每一个值都赋值给原数组的最后一位
     */

    for(let i = 0; i < argCount; i++) {
        O[len + i] = items[i];
    }

    // 获取到新的数组的长度
    let newLen = len + argCount;
    // 将新的长度赋值给数组
    O.length = newLen;

    // 返回新的长度
    return newLen;
}

注意:push方法返回的是新的数组长度,所以如果我们直接console.log(arr.push(1));的话,得到的就是一个新数组的长度。

从push方法的底层实现中我们可以看到,我们改变数组,是通过this来获取原来的数组,如果这个二维数组的this指向都是一样的,那也就会同时改变。在这个思路下,我们去探索一下new Array到底发生了什么。

new的时候发生了什么

大部分情况下,我都是使用let arr = [];直接定义一个数组,不过如果需要初始化一个较长的数组,还是需要使用到new Array。

new想必对于大家来说都非常熟悉了, 在java中,我们都是通过new来实例化一个类。但是我们也知道,在JavaScript当中并没有类,包括es6中的class本质上也是一个构造函数。

那么JavaScript中的new到底在干什么呢?如果背过前端八股文的同学,这时候就会想到:

在调用 new 的过程中会发生以上四件事情:

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

这里我们需要注意第三点: 让函数的this指向这个对象。

这时候我们就可以有足够的理由怀疑,通过new Array的二维数组,他们的每一项的this是同一个。

怎么判断一个数组的this指向相不相同,刚刚在push方法中我们就看到,可以通过原型方法获取到当前的this。

那么我们在Array的原型方法上定义一个getThis,返回当前的this,然后判断一下this是否相等,如果通过new Array定义的数组的this相等,就证明我们的猜想是正确的。

Array.prototype.getThis = function() {
    return this
}

let newArr = new Array(4).fill(new Array(4).fill(0))
let arr = [[1,1,1,1],[1,1,1,1],[1,1,1,1],[1,1,1,1]]

console.log(newArr[0].getThis() === newArr[1].getThis()); // true
console.log(arr[0].getThis() === arr[1].getThis()); // false

个人认为通过this比较好理解,如果写getThis判断过多的话,其实我们可以直接通过 === 来进行判断。

根本原因

原本我以为是由于new Array的问题,最开始分析的时候也只是从new和push的角度出发,忘了在这个情境下,还存在一个fill方法需要我们注意。非常感谢评论区的朋友的提醒。

根据个人的验证,我们在使用new Array创建的二维数组实际上是一个由相同的数组对象组成的数组,而不是每个元素都是单独的数组对象。 因此,当使用fill方法填充数组时,是将相同的数组对象填充到各个位置(所以前面提到的获取的this会相同)。而使用push方法时,实际上是向数组中添加相同的数组对象,因此所有位置的数组对象都会被修改。如果要创建多个独立的数组对象,可以使用循环或其他方法来创建新的数组对象。

补充

这些只是个人的学习过程中遇到的一些问题的笔记和思路,技术不可尽信书,如果还存在其他问题,非常欢迎和感谢大家的指正,让我能够正确地学习。