1、前言
ES6作为ES的一个划时代的版本,使得JS这门语言在编写大型且健壮的应用程序更进一步。ES6主要增加了很多语法糖,这些语法糖为我们的开发提效,减少实际开发中的bug功不可没。但是,这些语法糖是如何化腐朽为神奇的,其底层到底是怎么工作的呢,我觉得对于一个有追求的前端程序员来说还是有必要搞懂JS引擎到底为我们做了多少工作,做到,知其然,也知其所以然。可以训练我们在实际的项目开发中快速定位问题的能力。本文主要阐述...(扩展运算符)在实际开发中的应用场景,以及分析对应场景的编译结果,阐述扩展运算符的运行原理。
2、常见用法及编译结果分析
2.1 对象属性处理
取出某些键,将剩余的键收集到一个对象中,在实际开发中,可能你不想处理某些键(有点儿类似lodash的omit等操作)。或者说,需要对键值,需要分开处理的场景。
const obj = { a: 1, b: 2, c: 3 };
const { a, ...rest } = obj;
console.log(a, obj);
编译后的结果:
/**
* 移除source对象中包含exclude的键(包含Symbol)
* @param {Object} source 源对象
* @param {Object} excluded 用来匹配需要删除的键的对象
* @returns {Object}
*/
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
// 如果当前环境支持Symbol,Symbol类型的key也可以拷贝,但是对于不能枚举的Symbol属性则不拷贝
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
// 不处理不可枚举的Symbol
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
/**
* 宽松的移除source对象中包含exclude的键
* @param {Object} source 源对象
* @param {Object} excluded 用来匹配需要删除的键的对象
* @returns {Object}
*/
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
// 不对当前对象原型上的属性进行处理
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
var obj = {
a: 1,
b: 2,
c: 3,
};
var a = obj.a,
rest = _objectWithoutProperties(obj, ["a"]);
console.log(a, obj);
通过分析这个编译结果,我惊讶的发现,在对象做展开收集的时候,竟然可以能把Symbol也能处理到,学到了呀。
2.2 多个对象合并
这种场景,我个人认为是Object.assign在对象合并时的简化写法。
const obj1 = {
a: 1,
b: 2,
c: 3,
};
const obj2 = {
a: "xxx",
d: "ddd",
k: 2,
};
const newObj = {
...obj1,
...obj2,
};
console.log(newObj);
编译后:
"use strict";
/**
* 获取一个对象上包含Symbol在内的所有key
* @param {Object} object 目标对象
* @param {boolean} enumerableOnly 在获取Symbol类型的key时,是否只获取可枚举的
* @returns
*/
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
// 过滤掉Symbol的key中不可枚举的
enumerableOnly &&
(symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
})),
keys.push.apply(keys, symbols);
}
return keys;
}
/**
* 对象展开,将除了target以外的所有key全部合并到target中
* @param {Object} target
* @returns
*/
function _objectSpread(target) {
// 从第一个参数开始,分别对后面的参数进行处理
for (var i = 1; i < arguments.length; i++) {
var source = null != arguments[i] ? arguments[i] : {};
/* 这段是babel编译的结果,但是看起来不是那么直观,也于是决定对其进行改写 */
// i % 2
// ? ownKeys(Object(source), !0).forEach(function (key) {
// _defineProperty(target, key, source[key]);
// })
// : Object.getOwnPropertyDescriptors
// ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))
// : ownKeys(Object(source)).forEach(function (key) {
// Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
// });
/* 为什么对 序号为偶数的key才去单独处理,没看懂这样的意义是什么? */
if (i % 2) {
ownKeys(Object(source), !0).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else {
if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
}
return target;
}
/**
* 以兼容的方式定义对象的属性和值
* @param {object} obj
* @param {string} key
* @param {any} value
* @returns
*/
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true });
} else {
obj[key] = value;
}
return obj;
}
var obj1 = {
a: 1,
b: 2,
c: 3,
};
var obj2 = {
a: "xxx",
d: "ddd",
k: 2,
};
var newObj = _objectSpread(_objectSpread({}, obj1), obj2);
console.log(newObj);
其中_objectSpread辅助函数完成的功能则和Object.assign相同,也证明了我之前的观点。因此,在实际开发中这个场景下的注意点就变成了对Object.assign的使用了,用...显然更简洁。但是上述代码有个地方没太看懂,为什么要对序号为偶数的key只获取可枚举的Symbol,出于性能考虑吗?目前还不得而知。
2.3 浅拷贝
const obj = {
a: 1,
b: {},
};
const copy = { ...obj };
编译后的代码和多个对象合并相似,此处不再赘述。
2.4 数组合并
const arr = [1, 2, 3];
const b = [3, ...arr];
console.log(b);
编译后:
var arr = [1, 2, 3];
var b = [3].concat(arr);
console.log(b);
数组合并利用的是concat,是真的简洁啊,也没有什么好说的。
2.5 简化函数参数传递
对于某些函数接受一堆参数,如:
Math.min(...values: number[]): number;
或者不想或者不能(严格模式,或者箭头函数场景)使用arguments获取参数。
Math.min(...arr);
function D(...args) {
console.log(args);
}
function E(A, B, ...others) {
console.log(A, B, others);
}
编译后:
"use strict";
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
}
/**
* 对不含[Symbol.iterator]接口调用时报错
*/
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.");
}
/**
* 将其它类型转化成数组
* @param {any} o
* @param {number} minLen
* @returns
*/
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);
// 字节数组或者arguments对象
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
/* 需要注意的是,这儿对于一个普通对象是没有任何操作的哟 */
}
/**
* 把包含Symbol.iterator接口的对象转成数组
* @param {{ [Symbol.iterator]: any }} iter
* @returns
*/
function _iterableToArray(iter) {
if ((typeof Symbol !== "undefined" && iter[Symbol.iterator] != null) || iter["@@iterator"] != null) {
return Array.from(iter);
}
}
/**
* 将数组充满,比如这类数组 new Array(100),但是如果直接用forEach遍历,会跳过100个元素,经过这个操作这后会变成100个undefined,相当于填满了数组上的洞
* @param {Array<any>} arr
* @returns
*/
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
return _arrayLikeToArray(arr);
}
}
/**
* 将类数组对象转成数组
* @param {any} arr 类数组对象
* @param {number} len 类数组对象的长度
* @returns
*/
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;
}
Math.min.apply(Math, _toConsumableArray(arr));
function D() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
console.log(args);
}
function E(A, B) {
for (var _len2 = arguments.length, others = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
others[_key2 - 2] = arguments[_key2];
}
console.log(A, B, others);
}
2.6 类数组对象,Map,Set,String的处理。
const str = "23333";
const chars = [...str];
[...new Set([1, 1, 2])];
[...new Map()];
[...document.querySelectorAll("div")];
const func = function () {
const args = [...arguments];
console.log(args);
};
编译后,都调用上述的_toConsumableArray辅助函数,此处不再赘述。
3、总结
扩展运算符的用法主要分三类:
- 1、对象处理:
主要是对于对象的属性进行操作,提取,合并等操作。对于采用扩展运算符进行浅克隆,底层有类似于
Object.assign的逻辑,因此实际开发中这类场景可以多采用...用以简化代码。 - 2、数组处理
主要是通过
concat操作,并返回一个新数组。 - 3、对含有
Symbol.iterator对象转数组的处理。 主要是通过使用Array.from将其转化为数组,有兴趣的读者可以参看一下Array.from的polyfill,或者某些场景下使用ES5及以前的Array.prototype.slice,若不含有Symbol.iterator是不能使用扩展运算符进行展开的。这儿有一个易错点。 在React中,我们很有可能会看到这种代码
class MyButton extends Component {
render() {
const props = { ...this.props, name: '我自己封装的Button'};
return <Button {...props} />
}
}
这个扩展运算符对对象的props进行批量传递,是React生态自己的支持(相当于是人家多了一个插件去转码这个语法,而原生JS是不支持这种语法的)。各位读者需要注意一下区别。
另外:
interface Console {
// 已省略无关代码
log(...data: any[]): void;
}
看起来,好像跟我们的之前Math.min函数参数的定义形式差不多,那我们是否就可以console.log(...obj)了呢?
答案是不可以的,因为Object是没有定义Symbol.iterator的,如果你执意这样做,我们可以借用一下Array的Symbol.iterator,即:
const obj = { a: 1, b: 2, c: 3};
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
console.log(...obj);
同样,这类操作扩展运算符还是相对简洁的。
综上所述,在合理的场景下,我们可以尽量的多用扩展运算符以简化我们的代码(有些读者可能会说,babel在转化的过程中定义了这么多辅助函数,代码量会好多好多,这个不比担心,在生产环境下,我们都会用它的@babel/plugin-transform-runtime插件,将这些辅助函数全部抽离到一个包里面去,这样就不用担心代码的重复定义的问题了),这样可读性也更好了,后来的开发者维护起来也不用那么头疼了。
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,请联系作者本人,邮箱404189928@qq.com,你们的意见将会帮助我更好的进步。本文乃作者原创,若转载请联系作者本人。