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

kka*_*gil 4 javascript spread-syntax

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

console.log(...false) // TypeError not iterable
console.log(...1) // TypeError not iterable
console.log(...null) // TypeError not iterable
console.log(...undefined) // TypeError not iterable
Run Code Online (Sandbox Code Playgroud)

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

但是这些代码运行良好。

console.log({...false}) // {}
console.log({...1}) // {}
console.log({...null}) // {}
console.log({...undefined}) // {}
Run Code Online (Sandbox Code Playgroud)

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

VLA*_*LAZ 10

没有传播运算符!

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

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

操作员每次都以相同的方式行事。如果您使用delete运算符 as delete obj.x,那么无论上下文如何,您总是会得到相同的结果。与typeof或什至-(减号)相同。运算符定义将在代码中完成的操作。它总是相同的动作。有时运算符可能会被重载,例如+

console.log("a" + "b"); //string concatenation
console.log(1 + 2);     //number addition
Run Code Online (Sandbox Code Playgroud)

但它仍然不随上下文变化——你把这个表达放在什么地方

...语法是不同的-它不是在不同的地方相同的操作:

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
Run Code Online (Sandbox Code Playgroud)

这些都是不同的语法,看起来相似,行为相似,但绝对不一样。如果...是运算符,您可以更改操作数并且仍然有效,但情况并非如此:

const obj = { foo: "hello", bar: "world" };

console.log(Math.max(...obj)); //spread arguments in a function call
                               //not valid with objects
Run Code Online (Sandbox Code Playgroud)

function fn(...first, others) {} //rest parameters in function definition
                                 //not valid for the first of multiple parameters
Run Code Online (Sandbox Code Playgroud)

const obj = { foo: "hello", bar: "world" };

console.log([...obj]); //spread into an array literal
                       //not valid when spreading an arbitrary object into an array
Run Code Online (Sandbox Code Playgroud)

因此,每次使用...都有单独的规则,并且与任何其他使用不同。

原因很简单:...是不是一个东西都没有。该语言定义了不同事物的语法,例如函数调用、函数定义、数组文字和对象。让我们专注于最后两个:

这是有效的语法:

const arr = [1, 2, 3];
//          ^^^^^^^^^
//              |
//              +--- array literal syntax

console.log(arr);

const obj = { foo: "hello", bar: "world!" };
//          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                         |
//                         +--- object literal syntax

console.log(obj);
Run Code Online (Sandbox Code Playgroud)

但这些不是:

const arr = [0: 1, 1: 2, 2: 3];
//invalid - you cannot have key-value pairs
Run Code Online (Sandbox Code Playgroud)

const obj = { 1, 2, 3 };
//invalid - you need key-value pairs
Run Code Online (Sandbox Code Playgroud)

不足为奇 - 不同的语法有不同的规则。

此外,同样适用于使用...-[...arr]{...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);
Run Code Online (Sandbox Code Playgroud)

对象传播 {...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 对象,其 [[StringData]] 内部槽设置为参数。有关 String 对象的描述,请参见 21.1。
象征 返回一个新的 Symbol 对象,其 [[SymbolData]] 内部槽设置为参数。Symbol 对象的描述见 19.4。
大整数 返回一个新的 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
);
Run Code Online (Sandbox Code Playgroud)

但是,字符串对象确实有自己的属性,其中一些是可枚举的。这意味着您可以将字符串扩展到对象中:

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" }
Run Code Online (Sandbox Code Playgroud)

这是一个更好的说明,当值传播到一个对象时,它会如何表现:

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);
Run Code Online (Sandbox Code Playgroud)


bUf*_*f23 4

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

所以与 此处const a = {...1}相同,已视为不作为。因此,您没有抛出任何异常。const a = Object.assign({}, 1)Object.assign({},1)1objectnumber

此外,如果您对数组尝试了相同的操作[...1],它应该会抛出错误,因为它不会视为1并且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))
Run Code Online (Sandbox Code Playgroud)

PS:Object.assign() 规范

  • 这并不完全正确。当应用展开时,所有这些原始值都被强制转换为对象。错误消息表明它们不是_iterable_。它适用于对象扩展,因为它不检查可迭代性。数组扩展_确实_检查可迭代性,并且这些原始值都不是可迭代的。`[..."hello"]` 会起作用,但 `[...{}]` 不会。它也不适用于参数,因为它们检查可迭代性,就像数组一样。 (6认同)
  • “*它在内部映射到`Object.assign()`*” **它没有!** [`Object.assign`](https://tc39.es/ecma262/#sec-object.分配)和[传播时使用的属性副本](https://tc39.es/ecma262/#sec-copydataproperties)“非常”相似,但关键区别是每个中的最后一步 - `Object.assign` 将执行传播时的 [`Set`](https://tc39.es/ecma262/#sec-set-opv- throw) 确实 [`CreateDataProperty`](https://tc39.es/ecma262/#sec-createdataproperty) 。[在一种情况下,setter *将*被调用,在另一种情况下 - 将被覆盖](https://jsbin.com/cayihayufe/1/edit?js,console) (3认同)