... 作用
扩展运算符(spread)是三个点(...),用于取出参数对象中的所有可遍历属性,浅拷贝到当前对象之中。
常见用法
- 浅拷贝数组
js
const a1 = ['test1', 'test2'];
const a2 = [...a1];
a2[0] = 'test2';
a2 // ['test2', 'test2']
- 合并数据
js
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
- 解构赋值
js
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
- 字符串/类数组转为真正的数组
因为任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
js
[...'test']
// [ "t", "e", "s", "t"]
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
基本实现原理
如果不用 ...
,如何实现一样的功能?由上面的用法,可以知道。扩展运算符主要就是浅拷贝可遍历对象属性,那么我们可以用es5的写法实现如下:
js
// 简单版实现
function _spread() {
for (var ar = [], i = 0; i < arguments.length; i++){
ar = ar.concat(arguments[i]);
}
return ar;
};
用上面的例子测试一下,结果如下:
js
const a1 = ['test1', 'test2'];
const a2 = _spread(a1);
a2[0] = 'test2';
a2 // ['test2', 'test2']
显然,上面的例子,我们没有考虑到属性的可遍历性判断,那么需要进一步优化。
严谨实现
其实这里分几种情况来考虑就好:
- 判断是否为数组,数组一定可迭代,则直接复制数组后返回结果即可。
- 判断是否为实现了遍历器(Iterator)接口的对象,若实现了则转为数组。
- 如果没有实现遍历器(Iterator)接口的对象,则判断是否为普通字符串/Map/Set等。
- 均不满足以上条件的话,则抛错。
所以,最后实现为:
js
function _toConsumableArray(arr) {
return (
_arrayWithoutHoles(arr) || // 判断是否为数组
_iterableToArray(arr) || // 判断是否为实现了遍历器(Iterator)接口的对象
_unsupportedIterableToArray(arr) || // 判断是否为普调字符串/Map/Set等
_nonIterableSpread() // 则抛错
);
}
function _nonIterableSpread() {
throw new TypeError(
"Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
);
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))
return _arrayLikeToArray(o, minLen);
}
function _iterableToArray(iter) {
if (
(typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) ||
iter["@@iterator"] != null
)
return Array.from(iter);
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return _arrayLikeToArray(arr);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
更多原理探究
JS中
...
的原理是使用迭代器(Iterator)对象来实现的。当使用...
运算符来展开一个可迭代对象h时,JS引擎会自动调用该对象的[Symbol.iterator]()
方法,获取该对象的迭代器对象(Iterator)。然后通过不断调用迭代器对象的next()
方法来获取可迭代对象中的每个元素,直到迭代器对象返回{ done: true }
表示迭代结束
Symbol.iterator
是 JavaScript 中的一个内置 Symbol,它代表了一个对象的默认迭代器。当一个对象具有Symbol.iterator
方法时,它就是一个可迭代对象,可以使用for...of
循环或者...
运算符进行迭代。
Symbol.iterator
方法返回一个迭代器对象,该迭代器对象有一个next()
方法,每次调用next()
方法都会返回一个包含value
和done
两个属性的对象。其中value
表示当前迭代到的值,done
表示迭代是否结束。
我们平时在想一些 es6 的实现的时候,如果想知道其实现原理,那么可以考虑它的 es5 实现,而如果想知道它的严谨实现的话。可以直接去看 babel 编译后的 es5 代码长什么样即可。babel 在线编译网址:www.babeljs.cn/repl
如图: