...是 javascript ES6 新增的运算符,表示展开语法或者剩余参数,以下是两种含义的详细说明:

展开语法

展开语法 (Spread syntax), 可以在函数调用/数组构造时,将数组表达式或者 string 在语法层面展开;还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开。(译者注: 字面量一般指 [1, 2, 3] 或者 {name: "mdn"} 这种简洁的构造方式)

语法

函数调用:

myFunction(...iterableObj);

字面量数组构造或字符串:

[...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"}

构造字面量数组时使用展开语法

构造字面量数组时更给力!

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

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 } }

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

只能用于可迭代对象

在数组或函数参数中使用展开语法时,该语法只能用于 可迭代对象

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

展开多个值

在函数调用时使用展开语法,请注意不能超过 JavaScript 引擎限制的最大参数个数。更多详细信息,请参考:apply()


剩余参数

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

语法

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

描述

如果函数的最后一个命名参数以...为前缀,则它将成为一个由剩余参数组成的真数组,其中从0(包括)到theArgs.length(排除)的元素由传递给函数的实际参数提供。

在上面的例子中,theArgs将收集该函数的第三个参数(因为第一个参数被映射到a,而第二个参数映射到b)和所有后续参数。

剩余参数和 arguments对象的区别

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

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

从 arguments 到数组

引入了剩余参数来减少由参数引起的样板代码。

// Before rest parameters, "arguments" could be converted to a normal array using:

function f(a, b) {

  var normalArray = Array.prototype.slice.call(arguments);
  // -- or --
  var normalArray = [].slice.call(arguments);
  // -- or --
  var normalArray = Array.from(arguments);

  var first = normalArray.shift(); // OK, gives the first argument
  var first = arguments.shift(); // ERROR (arguments is not a normal array)

}

// Now we can easily gain access to a normal array using a rest parameter

function f(...args) {
  var normalArray = args;
  var first = normalArray.shift(); // OK, gives the first argument
}

解构剩余参数

剩余参数可以被解构,这意味着他们的数据可以被解包到不同的变量中。请参阅解构赋值

function f(...[a, b, c]) {
  return a + b + c;
}

f(1)          // NaN (b and c are undefined)
f(1, 2, 3)    // 6
f(1, 2, 3, 4) // 6 (the fourth parameter is not destructured)

示例

因为theArgs是个数组,所以你可以使用length属性得到剩余参数的个数:

function fun1(...theArgs) {
  alert(theArgs.length);
}

fun1();  // 弹出 "0", 因为 theArgs 没有元素
fun1(5); // 弹出 "1", 因为 theArgs 只有一个元素
fun1(5, 6, 7); // 弹出 "3", 因为 theArgs 有三个元素

下例中,剩余参数包含了从第二个到最后的所有实参,然后用第一个实参依次乘以它们:

function multiply(multiplier, ...theArgs) {
  return theArgs.map(function (element) {
    return multiplier * element;
  });
}

var arr = multiply(2, 1, 2, 3);
console.log(arr);  // [2, 4, 6]

下例演示了你可以在剩余参数上使用任意的数组方法,而arguments对象不可以:

function sortRestArgs(...theArgs) {
  var sortedArgs = theArgs.sort();
  return sortedArgs;
}

alert(sortRestArgs(5,3,7,1)); // 弹出 1,3,5,7

function sortArguments() {
  var sortedArgs = arguments.sort();
  return sortedArgs; // 不会执行到这里
}

alert(sortArguments(5,3,7,1)); // 抛出 TypeError 异常:arguments.sort is not a function

为了在arguments对象上使用Array方法,它必须首先被转换为一个真正的数组。

function sortArguments() {
  var args = Array.prototype.slice.call(arguments);
  var sortedArgs = args.sort();
  return sortedArgs;
}
console.log(sortArguments(5, 3, 7, 1)); // shows 1, 3, 5, 7

结论

当我们在javascript代码中看到三个点 (…) 时,它要么是剩余参数,要么是展开运算符。

有一种简单的方法可以区分它们:

  1. 当三个点 (…) 位于函数参数的末尾时,它是“剩余参数”并将参数列表的其余部分收集到一个数组中。
  2. 当三个点 (…) 出现在函数调用等中时,它是“展开语法”,并将数组扩展为列表。