React中的这三个点是做什么用的?

1458

在这段使用JSX的React代码中,...是做什么的?它叫什么名字?

<Modal {...this.props} title='Modal heading' animation={false}>

40
注意:... 运算符在不同的语境中表现不同。在这个语境中,它是由 @T.J. Crowder 描述的“扩展”运算符。在另一个语境中,它也可能是由 @Tomas Nikodym 描述的“剩余”运算符。 - doub1ejack
1
"..."正在将this.props数组解构为其各个值 - S.Alvi
它会从数组中复制所有先前的数据。 - Build Though
它在这两种情况下的行为不同: 1.当用于对象数组名称后面时,它将获取所有元素并将它们克隆到新的对象或数组中。在这种情况下,所有的props都会传递给Modal组件。当你想要向数组中添加新的对象而不操作第一个对象(例如更新状态)时,这会很有用。 2. 当用于对象/数组元素列表的末尾,如const {a,b, ...rest} = obj。现在,你有了一个新的obj/arr,其中包含除a、b之外的其余元素。 - Hayyaun
24个回答

1602
这是属性扩展符。它在ES2018中添加(数组/可迭代对象的扩展早在ES2015中),但通过转译,React项目长期支持它(作为“JSX扩展属性”,尽管你也可以在其他地方使用它,而不仅仅是属性)。 {...this.props}展开props中的“自有”可枚举属性,作为你正在创建的Modal元素上的离散属性。例如,如果this.props包含a: 1b: 2,那么
<Modal {...this.props} title='Modal heading' animation={false}>

将会与

<Modal a={this.props.a} b={this.props.b} title='Modal heading' animation={false}>

但它是动态的,所以无论在props中有什么“自有”属性都会被包含在内。
由于children是props中的“自有”属性,因此spread将其包括在内。因此,如果包含此代码的组件具有子元素,则它们将传递给Modal。将子元素放置在开标签和闭标签之间只是语法糖——好的语法糖——用于在开标签中放置一个children属性。例如:

class Example extends React.Component {
  render() {
    const { className, children } = this.props;
    return (
      <div className={className}>
      {children}
      </div>
    );
  }
}
ReactDOM.render(
  [
    <Example className="first">
      <span>Child in first</span>
    </Example>,
    <Example className="second" children={<span>Child in second</span>} />
  ],
  document.getElementById("root")
);
.first {
  color: green;
}
.second {
  color: blue;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

使用扩展符不仅对于这种用例非常方便,而且对于创建一个具有现有对象的大部分(或全部)属性的新对象也非常有用 - 当您更新状态时经常会出现这种情况,因为您无法直接修改状态。
this.setState(prevState => {
    return {foo: {...prevState.foo, a: "updated"}};
});

this.state.foo替换为一个新对象,该对象具有与foo相同的所有属性,除了a属性变为"updated":

const obj = {
  foo: {
    a: 1,
    b: 2,
    c: 3
  }
};
console.log("original", obj.foo);
// Creates a NEW object and assigns it to `obj.foo`
obj.foo = {...obj.foo, a: "updated"};
console.log("updated", obj.foo);
.as-console-wrapper {
  max-height: 100% !important;
}


2
将子元素放置在开放和关闭标签之间是否会覆盖children属性,还是它们会被合并? - Snackoverflow
4
这是一个非常有趣的问题。据我所见,它并没有被“children”的文档所涵盖。通过实验,我发现你通过名为children的属性提供的子元素将被放弃,而你在开始标记和结束标记之间指定的子元素将会生效,但如果这是未定义行为,我肯定不会依赖它。 - T.J. Crowder

535

... 被称为 扩展属性,正如名称所表示的那样,它允许一个表达式被展开。

var parts = ['two', 'three'];
var numbers = ['one', ...parts, 'four', 'five']; // ["one", "two", "three", "four", "five"]

在这种情况下(我将简化它)。

// Just assume we have an object like this:
var person= {
    name: 'Alex',
    age: 35 
}

这个:

<Modal {...person} title='Modal heading' animation={false} />

等于

<Modal name={person.name} age={person.age} title='Modal heading' animation={false} />

简而言之,这是一种巧妙的捷径,我们可以这样说


1
变量person = { name:'Alex',age:35} 是JSON,即JavaScript对象表示法。...person的值为name='Alex',age=35,其目的是为了让你在一个JSON结构中有1000000个这样的键值对,并且你想将它们全部传递给组件,你只需要使用...符号,它们就会全部被传递。你不需要一个一个地枚举它们。 - John McGovern
1
这不是ECMA文档,不要太认真对待!我最初以这种方式回答了这个问题,过了一会儿其他人改变了他们的答案,看起来像我的,只是为了获得一些赞。 - Mehdi Raash
1
最好提供一个将一个对象展开到另一个对象中的示例,因为这本质上就是JSX展开在幕后执行的操作。 - Andy
整体上看起来非常类似于 Python 的“解包”符号,例如 **my_dict - aquirdturtle

355

这三个点在ES6中代表了展开运算符,它允许我们在JavaScript中做很多事情:

  1. 连接数组

     var shooterGames = ['Call of Duty', 'Far Cry', 'Resident Evil'];
     var racingGames = ['Need For Speed', 'Gran Turismo', 'Burnout'];
     var games = [...shooterGames, ...racingGames];
    
     console.log(games)  // ['Call of Duty', 'Far Cry', 'Resident Evil',  'Need For Speed', 'Gran Turismo', 'Burnout']
    
  2. 解构数组

  3.    var shooterGames = ['Call of Duty', 'Far Cry', 'Resident Evil'];
       var [first, ...remaining] = shooterGames;
       console.log(first); //Call of Duty
       console.log(remaining); //['Far Cry', 'Resident Evil']
    
  4. 合并两个对象

     var myCrush = {
       firstname: 'Selena',
       middlename: 'Marie'
     };
    
     var lastname = 'my last name';
    
     var myWife = {
       ...myCrush,
       lastname
     }
    
     console.log(myWife); // {firstname: 'Selena',
                          //   middlename: 'Marie',
                          //   lastname: 'my last name'}
    

另一个使用三个点的方法是被称为剩余参数(Rest Parameters),它使得将函数的所有参数作为一个数组传入成为可能。

  1. 将函数的参数作为数组

  function fun1(...params) {

  }

4
为了更清晰明了,请在示例之前提及剩余参数。 - j obe
2
3.5. 对象解构 - Andy
3
这是一个很棒的回答,作为一名Python开发者,我可以将扩展运算符翻译成使用*或**符号进行打包/解包,基本上是做同样的事情。 - Federico Baù
关于第四点:为什么要使用 rest 参数而不是 JavaScript 函数中可用的 arguments 对象? - Wasbeer
@Wasbeer https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode#%E4%BD%BF%E7%94%A8_strict_mode_%E7%AE%80%E5%8C%96_eval_%E5%92%8C_arguments - Koray Tugay
显示剩余3条评论

142

...(JavaScript中的三个点)被称为Spread Syntax或Spread Operator。它允许可迭代对象(例如数组表达式或字符串)扩展,也可以在任何位置扩展对象表达式。这不仅适用于React,它是JavaScript运算符。

这里所有的答案都很有帮助,但我想列出最常用的实际使用场景Spread Syntax(扩展语法)的用例。

1. 合并数组(连接数组)

各种各样的方法来组合数组,但扩展运算符允许您在数组的任何位置放置它。如果您想要合并两个数组并在数组中的任何位置放置元素,可以按如下方式执行:

var arr1 = ['two', 'three'];
var arr2 = ['one', ...arr1, 'four', 'five'];

// arr2 = ["one", "two", "three", "four", "five"]

2. 复制数组

过去我们想要复制一个数组时,通常使用 Array.prototype.slice() 方法。但是,你也可以使用扩展运算符来达到同样的效果。

var arr = [1,2,3];
var arr2 = [...arr];
// arr2 = [1,2,3]

3. 调用函数不使用Apply方法

在ES5中,要将一个由两个数字组成的数组传递给doStuff()函数,通常会使用Function.prototype.apply()方法,如下所示:

function doStuff (x, y, z) {}
var args = [0, 1, 2];

// Call the function, passing args
doStuff.apply(null, args);

然而,通过使用展开运算符,您可以将一个数组传递到函数中。

doStuff(...args);

4. 解构数组

您可以将解构和剩余运算符一起使用,将信息提取到变量中:

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

5. 函数参数作为 Rest 参数

ES6中也有三个点(...),它表示一个 Rest 参数,将函数的所有剩余参数收集到一个数组中。

function f(a, b, ...args) {
  console.log(args);
}

f(1, 2, 3, 4, 5); // [3, 4, 5]

6. 使用数学函数

任何使用拓展语法作为参数的函数,都可以被那些能够接受任意数量参数的函数所使用。

let numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1

7. 合并两个对象

您可以使用扩展运算符来合并两个对象。这是一种更简单和更清晰的方法。

var carType = {
  model: 'Toyota',
  yom: '1995'
};

var carFuel = 'Petrol';

var carData = {
  ...carType,
  carFuel
}

console.log(carData);
// {
//  model: 'Toyota',
//  yom: '1995',
//  carFuel = 'Petrol'
// }

8. 将字符串拆分为单独字符

您可以使用展开操作符将字符串拆分为单独的字符。

let chars = ['A', ...'BC', 'D'];
console.log(chars); // ["A", "B", "C", "D"]
你可以考虑更多使用 Spread 运算符的方式。我在这里列出的是其流行用例。

98

JavaScript 中的三个点是 展开/收集操作符

展开操作符

展开语法 允许在期望多个参数的地方扩展表达式。

myFunction(...iterableObj);

[...iterableObj, 4, 5, 6]

[...Array(10)]

剩余参数

使用剩余参数语法来处理带有可变数量参数的函数。

function(a, b, ...theArgs) {
  // ...
}

数组的展开/剩余运算符在ES6中被引入。同时,也有一个针对对象展开/剩余属性的 State 2 提案

TypeScript也支持这种语法,并且可以将其转译为旧版本的ECMAScript,但可能会有一些问题


Spread/rest现在已经是Stage4,完成了。我认为它包含在ES9/2018中 https://github.com/tc39/proposal-object-rest-spread/blob/master/README.md - SeanMC

48

这是ES6的一个特性,在React中也使用。请看下面的例子:

function Sum(x, y, z) {
   return x + y + z;
}
console.log(Sum(1, 2, 3)); // 6

如果我们最多只有三个参数,那么这种方法是可以的。但是,如果我们需要添加例如110个参数,应该怎么做呢?我们应该定义所有这些参数并逐个添加吗?

当然,有一种更简单的方法,称为展开运算符(spread)。 你不需要传递所有这些参数,而是写成:

function (...numbers){} 

我们不知道有多少参数,但我们知道这些参数非常多。

基于ES6,我们可以将上述函数重写为下面的形式,并使用展开和映射使它变得异常简单:

let Sum = (...numbers) => {
    return numbers.reduce((prev, current) => prev + current);
}
console.log(Sum(1, 2, 3, 4, 5, 6, 7, 8, 9)); // 45

41

向Brandon Morelli致敬。他在这里完美地解释了,但链接可能会失效,所以我只是粘贴下面的内容:

扩展语法就是三个点:... 它允许可迭代对象在期望0个或更多参数的地方进行扩展。 没有上下文的定义很难理解。让我们探索一些不同的用例来帮助理解这意味着什么。

示例1-插入数组

看一下下面的代码。在此代码中,我们不使用扩展语法:

var mid = [3, 4];
var arr = [1, 2, mid, 5, 6];

console.log(arr);

在上面,我们创建了一个名为mid的数组。 然后我们创建了第二个包含mid数组的数组。 最后,我们记录输出结果。 你期望arr会打印什么?点击上面的运行按钮查看发生了什么。这是输出:

[1, 2, [3, 4], 5, 6]

这是您预期的结果吗?

通过将 mid 数组插入到 arr 数组中,我们得到了一个数组内部的数组。如果这是目标,那就没问题了。但是如果您只想要一个包含1到6值的单个数组呢?为了实现这一点,我们可以使用拓展语法!请记住,拓展语法允许我们的数组元素扩展。

让我们看下面的代码。除了我们现在使用拓展语法将 mid 数组插入到 arr 数组中之外,其他都一样:

var mid = [3, 4];
var arr = [1, 2, ...mid, 5, 6];

console.log(arr);

当你点击运行按钮时,这是结果:

[1, 2, 3, 4, 5, 6]

太棒了!

还记得你刚才读到的扩展语法定义吗?这就是它发挥作用的地方。正如你所看到的,当我们创建arr数组并在mid数组上使用扩展运算符时,mid数组不仅被插入,而且被展开。这个展开意味着mid数组中的每个元素都会被插入到arr数组中。结果是一个从1到6的数字单一数组,而不是嵌套的数组。

例二 - 数学

JavaScript具有内置的数学对象,可以让我们进行一些有趣的数学计算。在此示例中,我们将查看Math.max()。如果您不熟悉,Math.max()返回零或多个数字中最大的数字。以下是几个示例:

Math.max();
// -Infinity
Math.max(1, 2, 3);
// 3
Math.max(100, 3, 4);
// 100

若你想找到多个数字中的最大值,Math.max()方法需要使用多个参数。不幸的是,你不能直接使用单个数组作为输入。在引入扩展语法之前,使用 .apply() 是在数组上使用 Math.max() 的最简单方式。

var arr = [2, 4, 8, 6, 0];

function max(arr) {
  return Math.max.apply(null, arr);
}

console.log(max(arr));

它可以正常工作,但非常令人不爽。

现在看看我们如何使用扩展语法完全相同的功能:

var arr = [2, 4, 8, 6, 0];
var max = Math.max(...arr);

console.log(max);

不需要创建一个函数并使用apply方法返回Math.max()的结果,我们只需要两行代码! 展开语法扩展了数组元素,并将每个元素分别输入到Math.max()方法中!

示例3-复制数组

在JavaScript中,您不能通过将新变量设置为已经存在的数组来仅仅复制一个数组。请考虑以下代码示例:

var arr = ['a', 'b', 'c'];
var arr2 = arr;

console.log(arr2);

当你按下运行按钮时,你将会得到如下输出:

['a', 'b', 'c']

乍一看,似乎它起作用了——似乎我们已经将arr的值复制到了arr2中。但事实并非如此。你知道,在JavaScript中处理对象(数组是一种对象类型)时,我们是按引用分配而不是按值分配的。这意味着arr2被赋予与arr相同的引用。换句话说,我们对arr2所做的任何操作也会影响原始的arr数组(反之亦然)。请看下面的例子:

var arr = ['a', 'b', 'c'];
var arr2 = arr;

arr2.push('d');

console.log(arr);

在上面的代码中,我们向arr2中添加了一个新元素d。然而,当我们输出arr的值时,你会发现d的值也被添加到了这个数组中:

['a', 'b', 'c', 'd']

不需要担心!我们可以使用展开运算符!考虑下面的代码。它几乎与上面的代码相同。不过这里我们在一对方括号中使用了展开运算符:

var arr = ['a', 'b', 'c'];
var arr2 = [...arr];

console.log(arr2);

点击“运行”,您将看到预期输出:

['a', 'b', 'c']

以上,arr中的数组值扩展为单个元素,然后分配给arr2。我们现在可以随意更改arr2数组而不会影响原始的arr数组:

var arr = ['a', 'b', 'c'];
var arr2 = [...arr];

arr2.push('d');

console.log(arr);

再说一遍,这个方法可行是因为数组arr的值被扩展来填充我们arr2数组定义的括号。因此,我们将arr2设置为等于arr的各个值,而不是像第一个示例中那样设置arr的引用。

额外示例-字符串转换成数组

作为最后一个有趣的示例,您可以使用展开语法将字符串转换为数组。只需在一对方括号内使用展开语法:

var str = "hello";
var chars = [...str];

console.log(chars);


1
谢谢您的解释,我和我的同事刚刚讨论了扩展运算符,您的解释非常有帮助。 - Kay Angevare

38

对于想要简单快速理解的人:

首先,这不仅仅是React的语法。这是ES6中称为展开语法的语法,它可以迭代(合并、添加等)数组和对象。在这里阅读更多相关内容。

因此,回答这个问题:

假设你有这个标签:

<UserTag name="Supun" age="66" gender="male" />

而你需要做的是:

const user = {
  "name": "Joe",
  "age": "50"
  "test": "test-val"
};

<UserTag name="Supun" gender="male"  {...user} age="66" />

那么标签将等于这个:

<UserTag name="Joe" gender="male" test="test-val" age="66" />

因此,当您在React标签中使用扩展语法时,它会将标签的属性作为对象属性进行合并(如果存在则替换)给定的对象user。此外,您可能已经注意到一件事情,即它只替换前面的属性,而不是后面的属性。因此,在这个例子中,age保持不变。


最后不应该是age=50吗? - Don Cheadle
8
不,因为我在年龄之前添加了“{...user}”,所以年龄标签不会被替换。 - Supun Praneeth

24

这只是在JSX中以一种不同的方式定义props

它使用ES6中的...数组和对象运算符(对象运算符尚不完全支持),因此基本上,如果您已经定义了您的props,您可以通过这种方式将其传递给您的元素。

所以在您的情况下,代码应该是这样的:

function yourA() {
  const props = {name='Alireza', age='35'};
  <Modal {...props} title='Modal heading' animation={false} />
}

所以您定义的道具现在已被分离并可以在需要时重用。

它等同于:

function yourA() {
  <Modal name='Alireza' age='35' title='Modal heading' animation={false} />
}

以下是React团队对JSX中展开运算符的引用:

JSX展开属性 如果您预先知道要放在组件上的所有属性,那么使用JSX就很容易:

var component = <Component foo={x} bar={y} />;

修改 Props 是不好的
如果您不知道要设置哪些属性,可能会想在稍后将它们添加到对象中:

var component = <Component />;
component.props.foo = x; // bad
component.props.bar = y; // also bad
这是一个反模式,因为这意味着我们无法在更早的时候帮助您检查正确的propTypes。这意味着您的propTypes错误最终会出现具有神秘堆栈跟踪的情况。 props应该被视为不可变的。在其他地方突变props对象可能会导致意料之外的后果,所以在这一点上理想情况下它应该是一个被冻结的对象。
扩展属性:现在你可以使用JSX的一个新特性叫做扩展属性。
var props = {};
    props.foo = x;
    props.bar = y;
    var component = <Component {...props} />;

你传入的对象属性被复制到组件的props中。

你可以多次使用它或与其他属性组合使用。规范顺序很重要。后面的属性会覆盖之前的。

var props = { foo: 'default' };
var component = <Component {...props} foo={'override'} />;
console.log(component.props.foo); // 'override'
奇怪的 ... 符号是什么意思?
在ES6中,数组已经支持了 ... 运算符(或展开运算符)。此外,还有一个ECMAScript提案来支持对象的剩余和展开属性。我们正在利用这些已经支持和正在发展的标准,以便在JSX中提供更清晰的语法。

"props"是什么?"Properties"?还是字面值? - Peter Mortensen

21

对于来自Python世界的人,JSX Spread Attributes相当于解包参数列表(Python的**操作符)。

我知道这是一个JSX问题,但有时候通过类比可以更快地理解它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接