嘿,大家好!今天咱们来聊聊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()
是一个强大的工具,只要你了解它的特性和陷阱,就能用它写出更简洁、更高效的代码。
今天的讲座就到这里,希望大家有所收获!下次再见!