JavaScript - 为什么 Array.prototype.fill 在填充诸如“new Object()”之类的任何内容时实际上会填充对象的“指针”

IT技术 javascript arrays
2021-01-15 07:26:32

我试图使用 Array.prototype.fill 方法来创建 anxn 2D 数组,但代码有问题,我最终发现里面的所有数组实际上都是指向同一个数组的“指针”

样本:

var matrix = new Array(10).fill(new Array(10), 0);

我从概念上认为这可以创建一个 10 x 10 2D 阵列。但是,如果我为矩阵 [0][0] 赋值:

matrix[0][0] = 1;

结果实际上是:

matrix[0][0] === 1;
matrix[1][0] === 1;
matrix[2][0] === 1;
matrix[3][0] === 1;
matrix[4][0] === 1;
matrix[5][0] === 1;
matrix[6][0] === 1;
matrix[7][0] === 1;
matrix[8][0] === 1;
matrix[9][0] === 1;

并且每次如果我试图为矩阵中的任何位置赋值,其他子数组中的所有相应位置也会改变。

我想知道为什么会这样?

任何人都可以回答这个令人头疼的问题吗?

4个回答

我想知道为什么会这样?

Array#fill您提供作为第一个参数,并用该值的副本填充数组

你给它的值是一个数组的引用,所以自然它给你的是一个充满该引用副本的数组。不是数组的副本,是引用的副本

例如,出于与此代码完全相同的原因,它会以这种方式运行:

var a = new Array(10);
var b = a;

...让我们ab两者都引用同一个数组(两者都包含相同的;对我们创建的单个数组的引用)。

让我们对它进行一些 Unicode 艺术:

这段代码运行后:

var a = new Array(10);
var b = a;

我们在内存中有这个(减去一些不相关的细节):

a:Ref89895−−−+
             |
             | +−−−−−−−−−−−−−−−−−+
             +−−−−−−>| 数组 |
             | +−−−−−−−−−−−−−−−−−+
             | | 长度:10 |
b:Ref89895−−−+ +−−−−−−−−−−−−−−−−+

ab包含一个参考,我在此处显示为 Ref89895,尽管我们从未看到实际值。这就是 复制的内容b = a,而不是数组本身。

同样,当您执行以下操作时:

var matrix = new Array(10).fill(new Array(10), 0);

你最终得到

                   +−−−−−−−−−−−−−−−−−+
矩阵:Ref89895−−−>| 数组 |
                   +−−−−−−−−−−−−−−−−−+
                   | 长度:10 |
                   | 0:Ref55462 |--\
                   | 1:Ref55462 |--\\
                   | 2: Ref55462 |--\\\
                   | 3: Ref55462 |--\\\\ +−−−−−−−−−−−−−−−+
                   | 4:Ref55462 |---++++++->| 数组 |
                   | 5: Ref55462 |--///// +−−−−−−−−−−−−−−−+
                   | 6:Ref55462 |--//// | 长度:10 |
                   | 7: Ref55462 |--/// +−−−−−−−−−−−−−−−+
                   | 8:Ref55462 |--//
                   | 9:Ref55462 |--/
                   +−−−−−−−−−−−−−−−−−+

要创建一个 10 位数组,其中 10 个位中的每一个本身都是 0 的 10 位数组,我可能会使用Array.fromfillwith map

// Array.from
var matrix = Array.from({length: 10}, function() {
    return new Array(10).fill(0);
});

// fill and map
var matrix = new Array(10).fill().map(function() {
    return new Array(10).fill(0);
});

或在 ES2015 中:

// Array.from
let matrix = Array.from({length: 10}, () => new Array(10).fill(0));

// fill and map
let matrix = new Array(10).fill().map(() => new Array(10).fill(0));
这太棒了!非常感谢您为绘制图表所做的努力,这很有帮助!我认为发生这个错误是因为我实际上更熟悉 C++ 而不是很熟悉 JavaScript。
2021-03-22 07:26:32
@Soviut:是的,很好,而且更干净。顺便说一句,您错过了一个,我将其添加到您的答案中。
2021-04-01 07:26:32
@AndyRightNow:很高兴有帮助!(是的,C++ 确实有一些 JavaScript 没有的自动对象复制语义,因此根据我们的假设Array#fill在 C++ 中的编写方式,它可能只复制引用,也可能复制对象。)
2021-04-04 07:26:32

这是因为当您调用时,array.fill()您只传递了一个new Array()值。这就是用单个值填充数组的重点。

如果您想获取现有数组并为每个索引分配一个新实例,您可以使用.map().

var matrix = new Array(10).fill().map(function(item, index, arr) {
    return new Array(10);
});

要以嵌套方式执行此操作,您似乎希望可以映射两次。

// the callback function's item, index and array properties are optional
var matrix = new Array(10).fill().map(function(item, index, arr) {
    return new Array(10).fill(0);
});

如果您使用 ES6,这可以进一步简化为单行。

var matrix = new Array(10).fill().map(() => new Array(10).fill(0));

之所以.map()有效是因为它为数组中的每个项目调用提供的回调函数。

.fill()之前空白的原因.map()是用值填充数组。默认情况下undefined,每个项目都有一个指定大小的新数组.fill()将临时填充数组以便它可以被映射。

在您的特定情况下,由于Array.prototype.fill()用单个值填充并且在 JS 对象(例如数组)中是引用类型,因此所有索引都带有对第一个的引用。在 JS 中创建 ND 数组可能不像看起来那么简单,因为数组将通过引用进行复制。对于通用arrayND函数,您需要一个数组克隆实用程序函数。让我们来看看;

Array.prototype.clone = function(){
  return this.map(e => Array.isArray(e) ? e.clone() : e);
};
function arrayND(...n){
  return n.reduceRight((p,c) => c = (new Array(c)).fill().map(e => Array.isArray(p) ? p.clone() : p ));
}
var array3D = arrayND(5,5,5,"five");
array3D[0][1][2] = 5;
console.log(array3D);

arrayND 函数采用无限数量的参数。最后一个指定填充我们的 ND 数组的默认值,前面的参数是它们代表的每个维度的大小。(第一个参数是维度 1,第二个参数是维度 2 等等...)

是的,我完全同意它非常棘手(或者只是我对它不够熟悉)。谢谢你的好回答!
2021-03-13 07:26:32

通过递归,您可以创建更多维度;)

const range = r => Array(r).fill().map((v, i) => i);
const range2d = (x, y) => range(x).map(i => range(y));
const rangeMatrix = (...ranges) => (function ranger(ranged) {
  return ranges.length ? ranger(range(ranges.pop()).map(i => ranged)) : ranged
})(range(ranges.pop()));

let arr10x10 = range2d(10, 10);
let arr3x5 = range2d(3, 2);
let arr4x3 = rangeMatrix(4, 3);
let arr4x3x2 = rangeMatrix(4, 3, 2);
let arr4x3x2x5 = rangeMatrix(4, 3, 2, 5);

console.log(arr10x10);
console.log(arr3x5);
console.log(arr4x3);
console.log(arr4x3x2);
console.log(arr4x3x2x5);