这对于了解正在发生的事情非常重要,所以我必须从它开始。
语言中没有定义传播运算符。有传播语法,但作为其他类型语法的子类别。这听起来只是语义,但它对 如何 和 为什么 ... 工作有非常实际的影响。
操作员每次都以相同的方式行事。如果您将delete 运算符用作delete obj.x,那么无论上下文如何,您总是会得到相同的结果。与typeof 或什至-(减号)相同。运算符定义将在代码中完成的操作。它总是相同的动作。有时运算符可能会像 + 一样重载:
console.log("a" + "b"); //string concatenation
console.log(1 + 2); //number addition
但它仍然不会随上下文而变化 - 在哪里你把这个表达式。
... 语法不同 - 它在不同的地方不是相同的运算符:
const arr = [1, 2, 3];
const obj = { foo: "hello", bar: "world" };
console.log(Math.max(...arr)); //spread arguments in a function call
function fn(first, ...others) {} //rest parameters in function definition
console.log([...arr]); //spread into an array literal
console.log({...obj}); //spread into an object literal
这些都是不同的语法片段,看起来相似,表现相似,但绝对不一样。如果... 是运算符,您可以更改操作数并仍然有效,但情况并非如此:
const obj = { foo: "hello", bar: "world" };
console.log(Math.max(...obj)); //spread arguments in a function call
//not valid with objects
function fn(...first, others) {} //rest parameters in function definition
//not valid for the first of multiple parameters
const obj = { foo: "hello", bar: "world" };
console.log([...obj]); //spread into an array literal
//not valid when spreading an arbitrary object into an array
因此,... 的每次使用都有单独的规则,并且与其他任何使用方式不同。
原因很简单:... 根本不是一个的东西。该语言定义了不同事物的语法,例如函数调用、函数定义、数组字面量和对象。让我们关注最后两个:
这是有效的语法:
const arr = [1, 2, 3];
// ^^^^^^^^^
// |
// +--- array literal syntax
console.log(arr);
const obj = { foo: "hello", bar: "world!" };
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// |
// +--- object literal syntax
console.log(obj);
但这些不是:
const arr = [0: 1, 1: 2, 2: 3];
//invalid - you cannot have key-value pairs
const obj = { 1, 2, 3 };
//invalid - you need key-value pairs
不足为奇 - 不同的语法有不同的规则。
同样,这同样适用于使用 ... — [...arr] 和 {...obj} 只是您可以在 JavaScript 中使用的两种不同类型的代码,但 ... 用法之间没有重叠,只是您可以如何使用1 都是 [1] 和 { 1: "one" },但两者的含义不同。
当你在函数调用中使用 spread 并传播到一个对象中时,实际上会发生什么?
这是需要回答的真正问题。毕竟这些是不同的操作。
您的带有console.log(...false) 和console.log({...false}) 的示例特别演示了函数调用和对象字面量的用法,因此我将讨论这两个。请注意,数组文字扩展语法[...arr] 在有效和无效方面的行为非常相似,但在这里并不十分相关。重要的是为什么对象会有不同的行为,所以我们只需要一个例子来比较。
函数调用传播fn(...args)
规范甚至没有这个构造的特殊名称。它只是ArgumentList 的一种类型,在12.3.8.1 Runtime Semantics: ArgumentListEvaluation 部分(ECMAScript 语言规范链接)中它本质上定义了“如果参数列表具有...,则像这样评估代码”。我将为您省去规范中使用的无聊语言(如果您想查看,请随时访问链接)。
要采取的步骤中的关键点是,对于...args,引擎将尝试获取args 的迭代器。本质上是由iteration protocol(MDN 链接)定义的。为此,它将尝试调用使用@@iterator(或@@asyncIterator)定义的方法。这就是你得到 TypeError 的地方——它发生在 args 没有公开这样的方法时。没有方法,意味着它不是可迭代的,因此引擎无法继续调用该函数。
为了完整起见,如果args 是一个可迭代的,那么引擎将逐步遍历整个迭代器直到耗尽并从结果中创建参数。这意味着我们可以在函数调用中使用任意带有扩展语法的迭代:
const iterable = {
[Symbol.iterator]() { //define an @@iterator method to be a valid iterable
const arr = ["!", "world", "hello"];
let index = arr.length;
return {
next() { //define a `next` method to be a valid iterator
return { //go through `arr` backwards
value: arr[--index],
done: index < 0
}
}
}
}
}
console.log(...iterable);
对象传播{...obj}
规范中仍然没有此构造的特殊名称。它是对象字面量的 PropertyDefinition 类型。 12.2.6.8 Runtime Semantics: PropertyDefinitionEvaluation 部分(ECMAScript 语言规范链接)定义了如何处理它。我将再次为您省去定义。
区别在于obj 元素在传播其属性时的处理方式。为此,将执行抽象操作CopyDataProperties ( target, source, excludedItems )(ECMAScript 语言规范链接)。这可能值得一读,以更好地了解究竟发生了什么。我将只关注重要的细节:
-
使用表达式{...foo}
-
target 将成为新对象
-
source 将是 foo
-
excludedItems 将是一个空列表,因此无关紧要
-
如果source(提醒一下,代码中的foo)是null或undefined,则操作结束,target从CopyDataProperties操作中返回。否则,继续。
-
下一个重要的事情是foo 将变成一个对象。这将使用像这样定义的ToObject ( argument ) 抽象操作(再次提醒您不会在此处获得null 或undefined):
| Argument Type |
Result |
| Undefined |
Throw a TypeError exception. |
| Null |
Throw a TypeError exception. |
| Boolean |
Return a new Boolean object whose [[BooleanData]] internal slot is set to argument. See 19.3 for a description of Boolean objects. |
| Number |
Return a new Number object whose [[NumberData]] internal slot is set to argument. See 20.1 for a description of Number objects. |
| String |
Return a new String object whose [[StringData]] internal slot is set to argument. See 21.1 for a description of String objects. |
| Symbol |
Return a new Symbol object whose [[SymbolData]] internal slot is set to argument. See 19.4 for a description of Symbol objects. |
| BigInt |
Return a new BigInt object whose [[BigIntData]] internal slot is set to argument. See 20.2 for a description of BigInt objects. |
| Object |
Return argument. |
我们将调用此操作的结果from。
-
from 中所有可枚举的自有属性都将连同它们的值一起写入target。
-
展开操作完成,target 是使用对象文字语法定义的新对象。完成了!
更进一步的总结,当你使用对象字面量的扩展语法时,被扩展的源将首先被转换为一个对象,然后只有自己的可枚举属性实际上会被复制到被实例化的对象上。在null 或undefined 被传播的情况下,传播只是一个空操作:不会复制任何属性并且操作正常完成(不会引发错误)。
这与函数调用中的传播方式非常不同,因为它不依赖于迭代协议。您传播的项目根本不必是可迭代的。
由于像 Number 和 Boolean 这样的原始包装器不会产生任何自己的属性,因此没有可以从它们复制的内容:
const numberWrapper = new Number(1);
console.log(
Object.getOwnPropertyNames(numberWrapper), //nothing
Object.getOwnPropertySymbols(numberWrapper), //nothing
Object.getOwnPropertyDescriptors(numberWrapper), //nothing
);
const booleanWrapper = new Boolean(false);
console.log(
Object.getOwnPropertyNames(booleanWrapper), //nothing
Object.getOwnPropertySymbols(booleanWrapper), //nothing
Object.getOwnPropertyDescriptors(booleanWrapper), //nothing
);
但是,字符串对象确实有自己的属性,其中一些是可枚举的。这意味着您可以将字符串传播到对象中:
const string = "hello";
const stringWrapper = new String(string);
console.log(
Object.getOwnPropertyNames(stringWrapper), //indexes 0-4 and `length`
Object.getOwnPropertySymbols(stringWrapper), //nothing
Object.getOwnPropertyDescriptors(stringWrapper), //indexes are enumerable, `length` is not
);
console.log({...string}) // { "0": "h", "1": "e", "2": "l", "3": "l", "4": "o" }
以下是值在传播到对象中时的行为方式的更好说明:
function printProperties(source) {
//convert to an object
const from = Object(source);
const descriptors = Object.getOwnPropertyDescriptors(from);
const spreadObj = {...source};
console.log(
`own property descriptors:`, descriptors,
`\nproduct when spread into an object:`, spreadObj
);
}
const boolean = false;
const number = 1;
const emptyObject = {};
const object1 = { foo: "hello" };
const object2 = Object.defineProperties({}, {
//do a more fine-grained definition of properties
foo: {
value: "hello",
enumerable: false
},
bar: {
value: "world",
enumerable: true
}
});
console.log("--- boolean ---");
printProperties(boolean);
console.log("--- number ---");
printProperties(number);
console.log("--- emptyObject ---");
printProperties(emptyObject);
console.log("--- object1 ---");
printProperties(object1);
console.log("--- object2 ---");
printProperties(object2);