es6之扩展运算符 (…) 简称三个点

对象的扩展运算符

理解对象的扩展运算符其实没有那么难,只要记住一句话就可以轻松掌握它:smiley:

对象中的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中

1
2
let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

上面的代码就相当于

1
2
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

**Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target**)。

Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。 Object.assign 的详细使用请看这里

同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

1
2
let bar = {a: 1, b: 2};
let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}

这里有点需要注意的是扩展运算符对对象实例的拷贝属于一种浅拷贝。肯定有人要问什么是浅拷贝?我们知道javascript中有两种数据类型,分别是基础数据类型和引用数据类型。基础数据类型是按值访问的,常见的基础数据类型有NumberBooleanStringNullUndefinedSymbolBigInt,这类变量的拷贝的时候会完整的复制一份;引用数据类型比如Array,在拷贝的时候拷贝的是对象的引用,当原对象发生变化的时候,拷贝对象也跟着变化,比如:

1
2
3
4
let obj1 = { a: 1, b: 2};
let obj2 = { ...obj1, b: 'aqing'};
console.log(obj1); // {a: 1, b: 2}
console.log(obj2); // {a: 1, b: "aqing"}

上面这个例子扩展运算符拷贝的对象是基础数据类型,因此对obj2的修改并不会影响obj1

1
2
3
4
5
let obj1 = { a: 1, b: 2, c: {name: 'a'}};
let obj2 = { ...obj1};
obj2.c.name = 'aqing';
console.log(obj1); // {a: 1, b: 2, c: {name: 'aqing'}}
console.log(obj2); // {a: 1, b: 2, c: {name: 'aqing'}}

这里可以看到,对obj2的修改影响到了被拷贝对象obj1,原因上面已经说了,因为obj1中的对象c是一个引用数据类型,拷贝的时候拷贝的是对象的引用。

解构赋值与扩展运算符

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

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

注意:由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。而且解构赋值必须是最后一个参数,否则会报错。

1
2
3
4
5
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误

let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误

注意,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

1
2
3
4
5
6
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined

上面代码中,对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。

如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。

1
2
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}

对象的扩展运算符等同于使用Object.assign()方法.

1
2
3
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);

上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。

1
2
3
4
5
6
7
8
9
10
11
// 写法一
const clone1 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);

// 写法二
const clone2 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)

比如看上面的例子 拷贝对象原型的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
let o1 = {
a: 1
};
let o2 = {
b: 2
};
o2.__proto__ = o1;
let o3 = Object.assign(
Object.create(Object.getPrototypeOf(o2)),
o2
);
console.log(o3) // { b: 2 }
console.log(o3.a) // 1

扩展运算符可以用于合并两个对象。

1
2
3
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);

数组中的扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

它也可以替代函数的 apply 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);

比如举一个简单的例子:求一个数组中最大的元素

1
2
3
4
5
6
7
8
// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

扩展运算符在数组中的常见应用

  • 复制数组

    数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

1
2
3
4
5
const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1 // [2, 2]
   上面的例子中,`a2`并不是`a1`的克隆,而是指向同一份数据的另一个指针。对`a2`的修改会影响到`a1`。

但是 ES5 也有解决的方法 如下:

1
2
3
4
5
const a1 = [1, 2];
const a2 = a1.concat();

a2[0] = 2;
a1 // [1, 2]

​ 上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。

​ 扩展运算符提供了复制数组的简便写法。如下: a2都是a1的克隆。

1
2
3
4
5
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

​ 还是记住那句话:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

  • 可以将数组转换为参数序列
1
2
3
4
5
6
function add(x, y) {
return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42
  • 与解构赋值结合

    扩展运算符可以与解构赋值结合起来,用于生成数组

    1
    2
    3
    4
    5
    6
    7
    const [first, ...rest] = [1, 2, 3, 4, 5];
    first // 1
    rest // [2, 3, 4, 5]

    const [first, ...rest] = ["foo"];
    first // "foo"
    rest // []

    但是有一点要注意:**如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。**

    1
    2
    3
    4
    const [...rest, last] = [1, 2, 3, 4, 5];
    // 报错
    const [first, ...rest, last] = [1, 2, 3, 4, 5];
    // 报错
  • 扩展运算符还可以将字符串转为真正的数组。

    1
    2
    [...'hello']
    // [ "h", "e", "l", "l", "o" ]

总结

对象中的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中 。 好好回味一下这句话吧。



愿你的坚持终有收获。