这对于了解正在发生的事情非常重要,所以我必须从它开始。
语言中没有定义扩展运算符。有扩展语法,但作为其他类型语法的子类别。这听起来只是语义,但它对如何以及为什么 ...
起作用有非常实际的影响。
操作员每次都以相同的方式行事。如果您使用delete
运算符 as 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
不足为奇 - 不同的语法有不同的规则。
同样,这同样适用于 using ...
— [...arr]
and{...obj}
只是您可以在 JavaScript 中使用的两种不同类型的代码,但...
用法之间没有重叠,只是如何将1
两者用作[1]
和{ 1: "one" }
但两次的含义不同。
当您在函数调用中使用传播并传播到对象中时,实际会发生什么?
这是真正需要回答的问题。毕竟,这些是不同的操作。
您的示例使用console.log(...false)
并console.log({...false})
演示了一个函数调用和一个对象字面量的用法,所以我将讨论这两个。请注意,数组字面量扩展语法[...arr]
在有效和无效方面的行为非常相似,但在这里并不是很相关。重要的是为什么对象的行为不同,所以我们只需要一个例子来比较。
函数调用传播 fn(...args)
规范甚至没有针对此构造的特殊名称。它只是12.3.8.1ArgumentList
节运行时语义:ArgumentListEvaluation(ECMAScript 语言规范链接)中的一种类型,它本质上定义了“如果参数列表已经...
评估了这样的代码”。我会为您省去规范中使用的无聊语言(如果您想查看该链接,请随时访问该链接)。
从要采取的步骤来看,关键点是...args
引擎将尝试获取 的迭代器args
。本质上是由迭代协议(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节运行时语义:PropertyDefinitionEvaluation(ECMAScript 语言规范链接)定义了如何处理它。我将再次为您提供定义。
不同之处在于在obj
传播其属性时如何准确处理元素。为此,执行抽象操作CopyDataProperties ( target, source, excludedItems )
(ECMAScript 语言规范链接)。这个可能值得一读,以更好地了解到底发生了什么。我将只关注重要的细节:
用表情 {...foo}
target
将是新对象
source
将会 foo
excludedItems
将是一个空列表,所以它无关紧要
如果source
(提醒,这foo
在代码中)是null
或undefined
操作结束并target
从CopyDataProperties
操作返回。否则,继续。
下一个重要的事情是foo
将它变成一个对象。这将使用ToObject ( argument )
定义如下的抽象操作(再次提醒您不会得到null
或undefined
在这里):
参数类型 |
结果 |
不明确的 |
抛出 TypeError 异常。 |
空值 |
抛出 TypeError 异常。 |
布尔值 |
返回一个新的布尔对象,其 [[BooleanData]] 内部插槽设置为参数。有关布尔对象的描述,请参见 19.3。 |
数字 |
返回一个新的 Number 对象,其 [[NumberData]] 内部槽设置为参数。有关 Number 对象的说明,请参见 20.1。 |
string |
返回一个新的 String 对象,其 [[StringData]] 内部槽设置为参数。有关 String 对象的描述,请参见 21.1。 |
象征 |
返回一个新的 Symbol 对象,其 [[SymbolData]] 内部槽设置为参数。参见 19.4 了解 Symbol 对象的描述。 |
大整数 |
返回一个新的 BigInt 对象,其 [[BigIntData]] 内部槽设置为参数。有关 BigInt 对象的描述,请参见 20.2。 |
目的 |
返回参数。 |
我们将调用此操作的结果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);