展开运算符与rest参数

展开运算符(...)与rest参数(剩余参数)

Posted by HuangCanCan on July 13, 2019

前言

这周在工作中遇到了vue.js中的对象展开运算符的语法,有点没搞懂。现在记录学习一下。 这里主要学习一下,ES6的rest参数,展开运算符(对象展开运算符、数组展开运算符、函数展开运算符),感觉它们有点相似。

rest参数 (剩余参数)

剩余参数的语法:允许我们将一个不定数量的参数表示为一个数组。

语法:

function(a, b, ...theArgs) {
// ...
}

由于JavaScript函数允许接收任意个参数,于是我们就不得不用arguments来获取所有参数:

function foo(a, b) {
    var i, rest = [];
    if (arguments.length > 2) {
        for (i = 2; i<arguments.length; i++) {
            rest.push(arguments[i]);
        }
    }
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

为了获取除了已定义参数a、b之外的参数,我们不得不用arguments,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest参数,有没有更好的方法?

ES6标准引入了rest参数,上面的函数可以改写为:

function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []

rest参数只能写在最后,前面用…标识,从运行结果可知,传入的参数先绑定a、b,多余的参数以数组形式交给变量rest,所以,不再需要arguments我们就获取了全部参数。

rest参数前面用…标识,只能放在最后一个参数(类似于java的可变参数),rest参数用数组接收

如果传入的参数连正常定义的参数都没填满,rest参数会接收一个空数组(注意不是undefined)。

注: 剩余参数和 arguments对象之间的区别主要有三个:

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
  • arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach或pop。
  • arguments对象还有一些附加的属性 (如callee属性)。

展开运算符

字面量数组构造或字符串: […iterableObj, ‘4’, …‘hello’, 6];

构造字面量对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性): let objClone = { …obj };

函数展开运算符 (在函数调用时使用展开运算符)

等价于apply的方式

如果想将数组元素迭代为函数参数,一般使用Function.prototype.apply 的方式进行调用。

function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction.apply(null, args);

使用展开运算符:

function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction(...args);

所有参数都可以通过展开语法来传值,也不限制多次使用展开语法

function myFunction(v, w, x, y, z) { }
var args = [0, 1];
myFunction(-1, ...args, 2, ...[3]);

在 new 表达式中应用

使用 new 关键字来调用构造函数时,不能直接使用数组+ apply 的方式(apply 执行的是调用 [[Call]] , 而不是构造 [[Construct]])。当然, 有了展开语法, 将数组展开为构造函数的参数就很简单了:

var dateFields = [1970, 0, 1]; // 1970年1月1日
var d = new Date(...dateFields);

如果不使用展开语法, 想将数组元素传给构造函数, 实现方式可能是这样的:

function applyAndNew(constructor, args) {
function partial () {
    return constructor.apply(this, args);
};
if (typeof constructor.prototype === "object") {
    partial.prototype = Object.create(constructor.prototype);
}
return partial;
}


function myConstructor () {
console.log("arguments.length: " + arguments.length);
console.log(arguments);
this.prop1="val1";
this.prop2="val2";
};

var myArguments = ["hi", "how", "are", "you", "mr", null];
var myConstructorWithArguments = applyAndNew(myConstructor, myArguments);

console.log(new myConstructorWithArguments);
// (myConstructor构造函数中):           arguments.length: 6
// (myConstructor构造函数中):           ["hi", "how", "are", "you", "mr", null]
// ("new myConstructorWithArguments"中): {prop1: "val1", prop2: "val2"}

数组展开运算符 (构造字面量数组时使用展开运算符)

没有展开语法的时候,只能组合使用 push, splice, concat 等方法,来将已有数组元素变成新数组的一部分。有了展开语法, 通过字面量方式, 构造新数组会变得更简单、更优雅:

var parts = ['shoulders', 'knees']; 
var lyrics = ['head', ...parts, 'and', 'toes']; 
// ["head", "shoulders", "knees", "and", "toes"]

和参数列表的展开类似, ... 在构造字面量数组时, 可以在任意位置多次使用.

数组拷贝(copy)

var arr = [1, 2, 3];
var arr2 = [...arr]; // like arr.slice()
arr2.push(4); 

// arr2 此时变成 [1, 2, 3, 4]
// arr 不受影响

提示: 实际上, 展开语法和Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。如果想对多维数组进行深拷贝, 下面的示例就有些问题了。

var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
// Now array a is affected as well: [[], [2], [3]]

连接多个数组

Array.concat 函数常用于将一个数组连接到另一个数组的后面。如果不使用展开语法, 代码可能是下面这样的:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// 将 arr2 中所有元素附加到 arr1 后面并返回
var arr3 = arr1.concat(arr2);

使用展开运算符:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
var arr3 = [...arr1, ...arr2];

Array.unshift 方法常用于在数组的开头插入新元素/数组。 不使用展开语法, 示例如下:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// 将 arr2 中的元素插入到 arr1 的开头
Array.prototype.unshift.apply(arr1, arr2) // arr1 现在是 [3, 4, 5, 0, 1, 2]

如果使用展开语法, 代码如下: [请注意, 这里使用展开语法创建了一个新的 arr1 数组, Array.unshift 方法则是修改了原本存在的 arr1 数组]:

var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1 = [...arr2, ...arr1]; // arr1 现在为 [3, 4, 5, 0, 1, 2]

对象展开运算符 (构造字面量对象时使用展开语法)

Rest/Spread Properties for ECMAScript 提议(stage 4) 对字面量对象 增加了展开特性。其行为是, 将已有对象的所有可枚举(enumerable)属性拷贝到新构造的对象中。

浅拷贝(Shallow-cloning, 不包含 prototype) 和对象合并, 可以使用更简短的展开语法。而不必再使用Object.assign() 方式。

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

提示: Object.assign() 函数会触发 setters,而展开语法则不会。

提示: 不能替换或者模拟 Object.assign() 函数:

var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
const merge = ( ...objects ) => ( { ...objects } );

var mergedObj = merge ( obj1, obj2);
// Object { 0: { foo: 'bar', x: 42 }, 1: { foo: 'baz', y: 13 } }

var mergedObj = merge ( {}, obj1, obj2);
// Object { 0: {}, 1: { foo: 'bar', x: 42 }, 2: { foo: 'baz', y: 13 } }

在这段代码中, 展开操作符并没有按预期的方式执行: 而是先将多个解构变为剩余参数(rest parameter), 然后再将剩余参数展开为字面量对象.

对象展开运算符让你可以通过展开运算符 (...) , 以更加简洁的形式将一个对象的可枚举属性拷贝(Object.assign())至另一个对象。

如:

let aClone = { ...a };//对象展开运算符
相当于:
let aClone = Object.assign({}, a);

let ab = { ...a, ...b };//对象展开运算符
相当于:
let ab = Object.assign({}, a, b);

let xyWithAandB = { x: 1, ...a, y: 2, ...b, ...a };
相当于:
let xyWithAandB = Object.assign({ x: 1 }, a, { y: 2 }, b, a);

// Null/Undefined Are Ignored
let emptyObject = { ...null, ...undefined }; // no runtime error

通过对象展开运算符,将多个对象合并为一个,返回一个新的对象。

注意

在数组或函数参数中使用展开运算符时, 只能用于可迭代对象

内置类型默认的迭代器对象有:Array,TypeArray,String,Map,Set, Object不是迭代对象

var obj = {'key1': 'value1'};
var array = [...obj]; // TypeError: obj is not iterable

在函数调用时使用展开运算符,请注意不能超过 JavaScript 引擎限制的最大参数个数。

总结

剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于,剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。

参考

展开语法

对象展开运算符

Object Rest/Spread Properties for ECMAScript

vuex state的mapState辅助函数

rest参数 剩余参数