JS `Array.prototype.fill()`:用静态值填充数组元素

嘿,大家好!今天咱们来聊聊JavaScript里一个看似简单,实则暗藏玄机的家伙:Array.prototype.fill()。别看它只有短短几个字母,用好了能让你的代码更简洁,用不好嘛…嗯,可能会让你踩到一些意想不到的坑。

一、初识fill():填鸭式教育的典范

fill()方法顾名思义,就是用一个静态的值去填充数组的一部分或全部元素。它就像一个勤劳的油漆工,帮你把数组刷成你想要的颜色。它的基本语法是这样的:

array.fill(value[, start[, end]])
  • value:要填充的值,可以是任何数据类型,比如数字、字符串、对象等等。
  • start:可选参数,填充的起始位置,默认是0。如果为负数,则从数组末尾开始计算,-1代表最后一个元素,-2代表倒数第二个元素,以此类推。
  • end:可选参数,填充的结束位置(不包括该位置的元素)。默认是数组的长度。如果为负数,同样从数组末尾开始计算。

咱们来举几个简单的例子:

// 1. 用5填充整个数组
const arr1 = [1, 2, 3, 4, 5];
arr1.fill(5);
console.log(arr1); // 输出: [5, 5, 5, 5, 5]

// 2. 从索引2开始,用'a'填充到数组末尾
const arr2 = [1, 2, 3, 4, 5];
arr2.fill('a', 2);
console.log(arr2); // 输出: [1, 2, 'a', 'a', 'a']

// 3. 从索引1开始,到索引3结束(不包括索引3),用0填充
const arr3 = [1, 2, 3, 4, 5];
arr3.fill(0, 1, 3);
console.log(arr3); // 输出: [1, 0, 0, 4, 5]

// 4. 使用负数索引:从倒数第三个元素开始,到倒数第一个元素结束(不包括倒数第一个),用'b'填充
const arr4 = [1, 2, 3, 4, 5];
arr4.fill('b', -3, -1);
console.log(arr4); // 输出: [1, 2, 'b', 'b', 5]

这些例子展示了fill()的基本用法。是不是感觉挺简单的?别高兴太早,好戏还在后头呢。

二、fill()的陷阱:复制引用带来的困扰

fill()最容易让人掉坑的地方在于,当value是对象时,它填充的是对象的引用,而不是对象的拷贝。这意味着,数组中的所有元素都指向同一个对象!修改其中一个元素,会影响到所有其他元素。

const arr5 = new Array(3).fill({}); // 创建一个长度为3的数组,并用空对象填充
console.log(arr5); // 输出: [{}, {}, {}]

arr5[0].name = 'Alice';
console.log(arr5); // 输出: [{ name: 'Alice' }, { name: 'Alice' }, { name: 'Alice' }]

看到没?我们只修改了arr5[0]name属性,结果整个数组的元素都变了。这是因为fill()填充的是同一个空对象的引用。

如何避免这个陷阱呢?

方法很简单:不要直接使用对象字面量或现有的对象作为fill()的参数。而是应该在填充的时候,为每个元素创建一个新的对象。

// 正确的做法:使用Array.from()或扩展运算符
const arr6 = Array.from({ length: 3 }, () => ({})); // 创建一个长度为3的数组,并用新的空对象填充
console.log(arr6); // 输出: [{}, {}, {}]

arr6[0].name = 'Alice';
console.log(arr6); // 输出: [{ name: 'Alice' }, {}, {}]

// 或者使用扩展运算符
const arr7 = [...Array(3)].map(() => ({}));
console.log(arr7); // 输出: [{}, {}, {}]

arr7[0].name = 'Alice';
console.log(arr7); // 输出: [{ name: 'Alice' }, {}, {}]

这里,我们使用了Array.from()map()方法,为数组的每个元素都创建了一个新的空对象。这样,修改其中一个元素就不会影响到其他元素了。

再看一个例子,加深理解:

const obj = { count: 0 };
const arr8 = new Array(3).fill(obj);

arr8[0].count++; // 修改第一个元素的count属性
console.log(arr8); // 输出: [{ count: 1 }, { count: 1 }, { count: 1 }]

const arr9 = Array.from({length:3}, () => ({count: 0}));

arr9[0].count++; // 修改第一个元素的count属性
console.log(arr9); // 输出: [{ count: 1 }, { count: 0 }, { count: 0 }]

arr8的例子仍然是错误的,而arr9则是正确的。 记住,fill()填充的是引用,而不是拷贝!

三、fill()的妙用:初始化数组的利器

虽然fill()有陷阱,但它在初始化数组方面却非常有用。比如,你可以用它来创建一个指定长度的数组,并用一个默认值填充。

// 1. 创建一个长度为10的数组,并用0填充
const arr10 = new Array(10).fill(0);
console.log(arr10); // 输出: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// 2. 创建一个长度为5的数组,并用空字符串填充
const arr11 = new Array(5).fill('');
console.log(arr11); // 输出: ['', '', '', '', '']

// 3. 创建一个二维数组 (注意避开引用陷阱!)
const rows = 3;
const cols = 4;
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
console.log(matrix);
// 输出:
// [
//   [0, 0, 0, 0],
//   [0, 0, 0, 0],
//   [0, 0, 0, 0]
// ]

这些例子展示了fill()在初始化数组方面的用途。在某些情况下,它比循环更简洁高效。

四、fill()与稀疏数组:填补空缺的能手

fill()还可以用来填充稀疏数组。稀疏数组是指数组中存在空槽(holes)的数组,这些空槽的值不是undefined,而是完全不存在。

const arr12 = new Array(5); // 创建一个长度为5的稀疏数组
console.log(arr12); // 输出: [ <5 empty items> ]

arr12.fill(1);
console.log(arr12); // 输出: [ 1, 1, 1, 1, 1 ]

fill()会用指定的值填充稀疏数组中的所有空槽,使其变成一个稠密数组。

五、fill()的性能考量:速度与激情

fill()的性能通常比循环更好,尤其是在处理大型数组时。这是因为fill()是原生方法,由浏览器引擎直接实现,效率更高。

下面是一些简单的性能测试(仅供参考,实际性能可能因浏览器和环境而异):

// 使用fill()
const startTimeFill = performance.now();
const arr13 = new Array(1000000).fill(0);
const endTimeFill = performance.now();
console.log(`fill() took ${endTimeFill - startTimeFill} milliseconds`);

// 使用循环
const startTimeLoop = performance.now();
const arr14 = [];
for (let i = 0; i < 1000000; i++) {
  arr14[i] = 0;
}
const endTimeLoop = performance.now();
console.log(`Loop took ${endTimeLoop - startTimeLoop} milliseconds`);

在我的测试中,fill()通常比循环快得多。当然,具体的性能差异取决于数组的大小、填充的值以及运行环境等因素。

六、fill()与其他数组方法的比较

方法 功能 是否修改原数组 是否创建新数组 是否处理稀疏数组 备注
fill() 用静态值填充数组元素 当填充对象时,填充的是引用,需要注意避免陷阱。
map() 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。 不会修改原数组,返回一个新数组。必须提供回调函数。
forEach() 对数组的每个元素执行一次给定的函数。 不会创建新数组,主要用于迭代数组元素。
slice() 返回数组的一部分浅拷贝到一个新数组对象中。 不会修改原数组,返回一个新数组。可以用于复制数组。
splice() 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。 修改原数组,返回一个包含被删除元素的数组。

七、总结:fill()的正确打开方式

Array.prototype.fill()是一个方便实用的数组方法,可以用来填充数组元素,初始化数组,以及填补稀疏数组。但是,在使用fill()时,一定要注意避免填充对象引用带来的陷阱。

以下是一些使用fill()的最佳实践:

  • 明确你的目标: 在使用fill()之前,先想清楚你想要做什么,是初始化数组,还是填充现有数组?
  • 小心对象引用: 如果要填充的是对象,一定要确保填充的是对象的拷贝,而不是引用。可以使用Array.from()或扩展运算符来创建新的对象。
  • 注意性能: 在处理大型数组时,fill()通常比循环更高效。
  • 结合其他方法: fill()可以与其他数组方法(如map()filter()reduce()等)结合使用,实现更复杂的功能。

总而言之,fill()是一个强大的工具,只要你了解它的特性和陷阱,就能用它写出更简洁、更高效的代码。

今天的讲座就到这里,希望大家有所收获!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注