【问题标题】:I don't understand about spread syntax inside objects我不了解对象内部的传播语法
【发布时间】:2021-02-12 15:03:37
【问题描述】:

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

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}) // {}

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

【问题讨论】:

  • 添加 "use strict"; Object.defineProperty(Number.prototype, Symbol.iterator, { enumerable: false, configurable: true, writable: true, value: ({ [Symbol.iterator]: function*(){ for(let i = 0; i < Math.abs(this); ++i){ yield i * (this < 0 ? -1 : 1); } } })[Symbol.iterator] }); 以使 console.log(...1) 工作。 ??????

标签: javascript spread-syntax


【解决方案1】:

There is no spread operator!

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

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

操作员每次都以相同的方式行事。如果您将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 语言规范链接)。这可能值得一读,以更好地了解究竟发生了什么。我将只关注重要的细节:

  1. 使用表达式{...foo}

    • target 将成为新对象
    • source 将是 foo
    • excludedItems 将是一个空列表,因此无关紧要
  2. 如果source(提醒一下,代码中的foo)是nullundefined,则操作结束,targetCopyDataProperties操作中返回。否则,继续。

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

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

  1. from 中所有可枚举的自有属性都将连同它们的值一起写入target

  2. 展开操作完成,target 是使用对象文字语法定义的新对象。完成了!

更进一步的总结,当你使用对象字面量的扩展语法时,被扩展的源将首先被转换为一个对象,然后只有自己的可枚举属性实际上会被复制到被实例化的对象上。在nullundefined 被传播的情况下,传播只是一个空操作:不会复制任何属性并且操作正常完成(不会引发错误)。

这与函数调用中的传播方式非常不同,因为它不依赖于迭代协议。您传播的项目根本不必是可迭代的。

由于像 NumberBoolean 这样的原始包装器不会产生任何自己的属性,因此没有可以从它们复制的内容:

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);

【讨论】:

  • “函数定义中的其余参数对多个参数中的第一个无效” — not yet valid
  • @user4642212 我没有看到那个提议。我认为有可能做f = (...initial, last) =&gt; last 会很酷。它并不经常需要,但如果需要,您可以通过其他方式实现它,但与其他代码相比,它仍然有点突出。通常,通过迭代器进行快速转发也是一个好主意,即使它类似地有点极端。除此之外,我非常感谢您所做的编辑,谢谢!
【解决方案2】:

对象分布完全不同。它映射到Object.assign()internally

所以const a = {...1}const a = Object.assign({}, 1) 相同 这里Object.assign({},1)1 视为object 而不是number。因此,您没有抛出任何异常。

此外,如果您对数组 [...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))

PS:Object.assign() spec

【讨论】:

  • 这并不完全正确。应用传播时,所有这些原始值都被强制转换为对象。错误消息说它们不是 iterable。它适用于对象传播,因为它不检查可迭代性。数组展开确实检查可迭代性,并且这些原始值都不是可迭代的。 [..."hello"] 会起作用,但 [...{}] 不会。它也不适用于参数,因为它们检查可迭代性,就像数组一样。
  • "它在内部映射到 Object.assign()" 它没有!Object.assignproperty copy used when spreading 的步骤是 非常相似,但关键区别在于每个步骤的最后一步 - Object.assign 将执行 Set 而传播则执行 CreateDataPropertyIn one case setters will be called, in the other - will be overwritten
【解决方案3】:

这就是 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 将其视为数组,如果它的可迭代 else 只是抛出错误。 true false 不可迭代,大括号中的对象也不可迭代。这就是为什么对象保持空白,因为......不会在非迭代项目上工作。 这行得通

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

这不是 - kaboom

a = [...false];

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

a = {...false};

我希望你明白了。其他任何事情都只是弹出后续问题。

【讨论】:

  • ({...false}) 仍然是一个空对象,因为 Object.getOwnPropertyDescriptors(false) 是空的。 Spread 只复制自己的属性,falseObject(false) 都没有。
  • 1. let arrayLike 不是 an array-like - 它缺少length 属性。没有它,它只是一个带有整数作为键的对象,而不是一个完全成熟的数组。 2. Array-likes 不可迭代。您显示的是数字索引,that's not the same as being iterable。 3. 对于something to be iterable,它必须公开一个名为Symbol.iterator 的方法,并且必须产生一个迭代器。
  • @VLAZ 我同意你的观点,一个真正的数组需要定义长度。然而我故意忽略了它,重点是强调为什么一些对象会迭代和 JS 的美,在某些地方它会抛出错误,而在其他时候它会默默地继续而不停止代码。借口是带来很少的背景知识,并在最后关注 Kaboom 和 shhhh 示例。它是可迭代性的概念,这是 OP 在问题中要求的基本且令人困惑的概念。
  • @user4642212 我支持您对已接受答案的评论。精确点展开将检查迭代。您对我的回答的评论确实是最相关的。
【解决方案4】:
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"]

扩展语法 (...) 允许在预期零个或多个参数(对于函数调用)或元素(对于数组字面量)或对象表达式的地方扩展可迭代的对象,例如数组表达式或字符串在需要零个或多个键值对(对于对象字面量)的地方进行扩展。

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

【讨论】:

  • 没错 — 您刚刚从源代码中复制了它 — 但是如果您指出 OP 的代码具体如何不符合此描述,则此答案会更有用。
猜你喜欢
  • 2019-02-12
  • 2021-12-01
  • 2019-07-23
  • 1970-01-01
  • 2018-11-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多