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

I don't understand about spread syntax inside objects

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

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

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

但这些代码运行良好。

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

请告诉我上述代码为何有效。

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"]

扩展语法 (...) 允许在需要零个或多个参数(用于函数调用)或元素(用于数组文字)或对象的地方扩展数组表达式或字符串等可迭代对象在需要零个或多个 key-value 对(对于对象文字)的地方扩展表达式。

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

好吧,这就是 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};

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

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

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

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

总结:

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

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

语言中没有定义传播运算符。有 spread syntax 但作为其他类型语法的 sub-category。这听起来只是语义,但它对 howwhy ... works.

有非常实际的影响

操作员每次都以相同的方式行事。如果您将 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 and in section 12.3.8.1 Runtime Semantics: ArgumentListEvaluation(ECMAScript 语言规范 link),它本质上定义了“如果参数列表具有 ...,则像这样评估代码”。我将为您省去规范中使用的无聊语言(如果您想查看,请随时访问 link)。

要采取的步骤的关键点是使用 ...args 引擎将尝试获取 args 的迭代器。本质上就是由iteration protocol (MDN link). For that, it will try calling a method defined with @@iterator (or @@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 for an object literal. Section 12.2.6.8 Runtime Semantics: PropertyDefinitionEvaluation(ECMAScript 语言规范 link)的一种类型,定义了如何处理它。我再给你定义一下。

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

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

    • target 将是新对象
    • source 将是 foo
    • excludedItems 将是一个空列表,因此无关紧要
  2. 如果source(提示,代码中这是foo)是null或者undefined 操作结束,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 的情况下,传播只是一个 no-op:不会复制任何属性并且操作正常完成(不会抛出错误)。

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

因为像 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);