我不了解对象内的传播语法

IT技术 javascript spread-syntax
2021-03-16 07:31:59

我不了解对象内的传播语法。

console.log(...false) // TypeError not iterable
console.log(...1) // TypeError not iterable
console.log(...null) // TypeError not iterable
console.log(...undefined) // TypeError not iterable

我理解上面的代码由于非迭代器而发生错误。

但是这些代码运行良好。

console.log({...false}) // {}
console.log({...1}) // {}
console.log({...null}) // {}
console.log({...undefined}) // {}

请让我知道为什么上述代码有效。

4个回答

没有传播运算符!

这对于了解正在发生的事情非常重要,所以我必须从它开始。

语言中没有定义扩展运算符有扩展语法,但作为其他类型语法的子类别。这听起来只是语义,但它对如何以及为什么 ...起作用有非常实际的影响

操作员每次都以相同的方式行事。如果您使用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 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 可迭代的,那么引擎将遍历整个迭代器,直到耗尽并从结果中创建参数。这意味着我们可以在函数调用中使用任何具有扩展语法的任意迭代:

对象传播 {...obj}

规范中仍然没有此构造的特殊名称。PropertyDefinition是对象字面的一种。12.2.6.8运行时语义:PropertyDefinitionEvaluation(ECMAScript 语言规范链接)定义了如何处理它。我将再次为您提供定义。

不同之处在于在obj传播其属性时如何准确处理元素。为此,执行抽象操作CopyDataProperties ( target, source, excludedItems )(ECMAScript 语言规范链接)。这个可能值得一读,以更好地了解到底发生了什么。我将只关注重要的细节:

  1. 用表情 {...foo}

    • target 将是新对象
    • source 将会 foo
    • excludedItems 将是一个空列表,所以它无关紧要
  2. 如果source(提醒,这foo在代码中)是nullundefined操作结束并targetCopyDataProperties操作返回否则,继续。

  3. 下一个重要的事情是foo将它变成一个对象。这将使用ToObject ( argument )定义如下抽象操作(再次提醒您不会得到nullundefined在这里):

参数类型 结果
不明确的 抛出 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

  1. from可枚举的所有属性都将写入target其值。

  2. 展开操作完成并且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);

“在函数定义无效的第一多个参数的其余的力学参数” - 不是有效
2021-04-25 07:31:59
@user4642212 我没有看到那个提议。我认为有可能做f = (...initial, last) => last会很酷。它并不经常需要,但是当需要时,您可以通过其他方式实现它,但与其余代码相比,它仍然有些突出。通过迭代器进行快速转发通常也是一个不错的主意,即使它同样有点极端情况。除此之外,我非常感谢您所做的编辑,谢谢!
2021-05-14 07:31:59

对象传播是完全不同的。它映射到Object.assign() 内部.

所以,const a = {...1}是同const a = Object.assign({}, 1) 这里Object.assign({},1)已治疗1object不作为number因此,您没有收到任何抛出的异常。

此外,如果您对数组尝试过相同的操作,[...1]它应该会抛出错误,因为它不会将1as 视为object并且您会得到与..1.

总结一下:

console.log({...false}) => console.log(Object.assign({}, false))
console.log({...1}) => console.log(Object.assign({}, 1))
console.log({...null}) => console.log(Object.assign({}, null))
console.log({...undefined}) => console.log(Object.assign({}, undefined))

PS:Object.assign() 规范

这并不完全正确。当应用扩展时,所有这些原始值都被强制转换为对象。错误消息说它们不可迭代它适用于对象传播,因为它不检查可迭代性。数组展开确实检查可迭代性,并且这些原始值都不是可迭代的。[..."hello"]会起作用,但[...{}]不会。它也不适用于参数,因为它们检查可迭代性,就像数组一样。
2021-05-10 07:31:59
它映射到Object.assign()内部它没有!对于步骤Object.assign传播内容时复制使用性能非常相似,但关键的区别是在每个最后一步-Object.assign将执行Set,而不会蔓延CreateDataProperty在一种情况下调用setter ,在另一种情况下- 将被覆盖
2021-05-19 07:31:59

嗯,这就是 JS 的美妙之处,这要归功于可 迭代协议这意味着它意味着数组或映射。默认情况下,这两个都在语言构造中分配了行为,即它是一组我们可以一个接一个迭代的项目。我们还可以根据需要统计和添加和删除项目。

Example.JS 默认将它们理解为系列集合或集合或组。

const array1 = [1, 4, 9, 16];
console.log(array1.length);
array1.push(5);
console.log(array1.length);

现在这些不是 JS 中唯一的可迭代对象类型,字符串也是。

string = 'abc';
console.log(string.length)
string = string+'d';
console.log(string.length)
console.log(string[3])

然后有类似数组的对象也可以迭代

let arrayLike = {
  0: "Hello",
  1: "World",
};
console.log(arrayLike[1])
现在让我们通过一个例子来理解你的第二个例子的实例,运行下面的代码 console.log 并抛出错误。因为默认情况下,对象不像数组和类似数组的对象那样具有迭代行为。由于扩展运算符声明将三个点之后的任何点视为数组,如果它适合构造。这样{...false}做几乎什么发生在下面的例子为b。它仍然是一个空对象,因为对象需要键值配对。

a = [1,2,3];
b={1,2,3};


console.log(a[1]);
console.log(b[1]);

a 不需要配对键值定义,默认情况下它自己这样做,正如众所周知的索引。

a = [4,5,6];
b={1:4,2:5,3:6};


console.log(a[1]);
console.log(b[1]);
在同一张纸条上阅读这个例子

a = [1,2,3];
b=[4,5,6];
c= [...a,...b];
d = [...a,b[1]];

console.log(c);
console.log(d);

...(三个点)只告诉 Js 将其视为数组,如果它是可迭代的,否则只会抛出错误。true false 不可迭代,大括号中的对象也不可迭代。这就是为什么对象保持空白,因为......不会在非迭代项目上工作。 这有效

a = [1,2,3];
b = {...a};
console.log(b)

这不 - kaboom

a = [...false];

这也不起作用,只是保持沉默 - shshshs

a = {...false};

我希望你明白这一点。其他任何事情都会弹出后续问题。

2021-04-29 07:31:59
@VLAZ 我同意你的观点,一个真正的数组需要定义长度。然而我故意忽略了它,制作的目的是强调为什么有些对象会迭代和 JS 的美在某些地方它会如何抛出错误,而在其他时候它会默默地继续而不停止代码。借口是在最后带来很少的背景理解并专注于 KAboom 和 shhhh 示例。它是可迭代性的概念,这是 OP 在问题中要求的必不可少且令人困惑的概念。
2021-04-30 07:31:59
({...false})仍然是一个空对象,因为Object.getOwnPropertyDescriptors(false)是空的。Spread 只复制自己的属性,既false没有Object(false)也没有。
2021-05-03 07:31:59
@ user4642212 我支持您对已接受答案的评论。传播的精确点将检查迭代。您对我的回答的评论确实最相关。
2021-05-05 07:31:59
1.let arrayLike不是 一个数组类-它缺少一个length属性。没有它,它只是一个带有整数键的对象,而不是一个完全成熟的数组。2. 类数组不可迭代你已经证明是数字索引,这是一样的是迭代3. 对于可迭代的东西, 它必须公开一个命名的方法Symbol.iterator,该方法必须产生一个迭代器。
2021-05-08 07:31:59
For example
var array1 = [1, 2, 3, 4];
var array2 = [5, 6, 7, 8];
array2 = [ ...array1, ...array2 ] // [1, 2, 3, 4, 5, 6, 7, 8]

/** spread array example */
var str1 = "hello";
var result_ary = [...str1] // ["h", "e", "l", "l", "o"]

Spread 语法 (...) 允许在需要零个或多个参数(用于函数调用)或元素(用于数组文字)的地方扩展诸如数组表达式或字符串之类的可迭代对象,或者扩展对象表达式需要零个或多个键值对(用于对象文字)的地方。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax

这没有错——你刚刚从源代码中复制了它——但是如果你指出 OP 的代码具体不符合这个描述,这个答案会更有用。
2021-05-03 07:31:59