ES6中变量的解构赋值和扩展操作符的使用技巧

简介: ES6中提供了一种非常方便的赋值方法:解构赋值。此外,还有组成解构的`...`及其作为展开操作符时,在实际中有着很多的巧妙用法...

ES6中提供了一种非常方便的赋值方法:解构赋值。此外,还有组成解构的...及其作为展开操作符时,在实际中有着很多的巧妙用法。

解构(Destructuring)是指按照一定的模式,从数组和对象中提取数据,对变量进行赋值。具体可以看后面的介绍。

展开操作符或扩展操作符、剩余操作符。

解构赋值的介绍和使用

数组或对象的解构赋值

通常,为变量赋值,只能一个个直接指定值。

let a=1;
let b=2;
let c=3;

ES6中的解构赋值写法:

let [a,b,c]=[1,2,3];

即 从数组中提取值,按照对应位置,对变量赋值。

这种写法属于“模式匹配”,需要保证等号两边的模式相同。只要模式结构能够匹配上,左边的变量就可以赋值成功。

因此对于嵌套数字、对象等结构,都可以使用解构赋值。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p: [x, { y }] } = obj;

只要对应的模式能够匹配,就可以赋值成功。数组的元素按次序排列,变量的取值由它的位置决定对象通过属性取值,变量需要与属性同名,才能取到正确的值

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

如果解构不成功,变量的值就等于undefined

不完全解构是指等号左边的模式只匹配右边结构的一部分。

let arr1 = [1, 2, 3];
let [x, y] = arr1;
x // 1
y // 2

let arr2 = [1, [2, 3], 4];
let [a, [b], d] = arr2;
a // 1
b // 2
d // 4

对于数组解构,等号右边可以数组或Set(严格将只要就要Iterator接口就可以),否则会报错;对于对象,等号右侧必须为对象。

let [x, y, z] = new Set(['a', 'b', 'c']);

对象的解构赋值可以取到继承的属性。

解构中...的使用

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

数组作为特殊的对象,进行对象属性的解构。

由于数组本质是特殊的对象,因此:

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last  // 3

对象解构赋值时的变量名和嵌套

不同于属性名的变量

如果对象的属性名和变量名不一致,想要解构成功,需要写成如下方式:

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

也就是在解构赋值的左侧,也以key:value的形式,对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。赋值的不是属性,而是属性名对应的变量。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };

// 实际为:

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

对象的简写形式{ foo, bar },实际表示的是{ foo: foo, bar: bar },也是ES6提供的扩展。

let foo = 'a';
let bar = 'b';

let obj={ foo, bar };

console.log(obj); // {foo: 'a', bar: 'b'}

嵌套对象解构时的模式

嵌套结构的对象进行解构时,需要注意用于匹配嵌套的模式。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

上面的解构中,p是模式,而不是变量。如果想要p也作为变量赋值,应该使用如下的方式:

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

解构时指定默认值

解构赋值时允许指定默认值。

  • 数组解构时的默认值
let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

ES6 内部使用严格相等运算符(===)判断是否为undefined,判断一个变量是否有值。也就是,只有真正的没有该值或值为undefined,默认值才会生效。

let [x = 1] = [null];
x // null
  • 对象的解构指定默认值
let {x = 3} = {};
x // 3

let {x, y = 5} = {x: 1};
x // 1
y // 5

let {x: y = 3} = {};
y // 3

let {x: y = 3} = {x: 5};
y // 5

let { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
  • 解构时默认值为表达式时的惰性求值

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1];

x能取到值,即不会获取默认值,函数f也不会执行。此代码等价于:

let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}
  • 默认值如果为变量,则必须已经声明
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

函数参数的解构赋值

函数参数使用解构

作为函数的参数使用解构赋值。

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3


function add({x, y}){
  console.log(x , y);
}

add({x:1, y:2}); // 1 2

传入的参数被解构为x、y变量在函数内部使用。

比如,使用map时,解构获取元素为变量:

[[1, 2], [3, 4], [5, 6]].map(([a, b]) => a + b);
// [3, 7, 11]

函数参数的解构使用默认值

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move();   // [0, 0]

...展开运算符或剩余操作符

...有两种作用:

展开运算符

有时也被称为“扩展操作符”。
  1. 展开运算符(spread operator),用于把arrayobject结构展开;
let a = [1,2,3];
let b = [0, ...a, 4]; // [0,1,2,3,4]

let obj = { a: 1, b: 2 };
let obj2 = { ...obj, c: 3 }; // { a:1, b:2, c:3 }
let obj3 = { ...obj, a: 3 }; // { a:3, b:2 }

作为展开操作符展开对象时,会出现属性覆盖的问题,这一点需要注意。利用这一点,也可以处理参数的默认值覆盖。

function operator(opt){
    opt={prop1:value1,prop2:value2,...opt}

    // 类似
    // opt=Object.assign({},{prop1:value1,prop2:value2},opt);
}

不同通常在参数中使用解构赋值时的默认值,比这中方式更简洁。

展开对象时仅获取对象自身的可枚举属性,也就是会丢失原型链上的属性或方法。

比如,通过class类创建的对象实例,在展开时就会丢失其方法。

class A {
  a = 1;
  m() {
  }
}
let a=new A();
let b={...a};
b.p;
b.m(); // TypeError: b.m is not a function

class定义的实例方法位于原型对象上:

a // console中输出a
A 
  a: 12
  [[Prototype]]: Object
      constructor: class A
      m: ƒ m()
      [[Prototype]]: Object
      ...

展开操作符展开复制时,使用的是浅拷贝。

剩余操作符

  1. 剩余操作符(rest operator),是解构的一种,意思就是把剩余的项放到一个数组或对象里面赋值给变量,除了变量赋值,也常用在函数的剩余参数中。

剩余操作符符不仅针对数组,也可以用于对象。

let a = [1,2,3];
let [b, ...c] = a;
b; // 1
c; // [2,3]
 
// 或
let a = [1,2,3];
let [b, ...[c,d,e]] = a;
b; // 1
c; // 2
d; // 3
e; // undefined

函数的剩余参数,用于获取函数的多余参数,这样就不需要使用arguments对象。

function test(a, ...rest){
  console.log(a); // 1
  console.log(rest); // [2,3]
}
 
test(1,2,3)

剩余操作符应用于对象时:

let obj = { a: 1, b: 2 };
let {...obj2} = obj; 
obj2;   // {a: 1, b: 2}

解构赋值的实际使用

交换变量的值

简洁、易读、语义清晰的交换变量值。

let x=1;
let y=2;

[x,y] = [y,x];

从函数返回值中获取多个变量

函数只能返回一个值,想返回多个值,只能将它们放在数组或对象里返回。使用解构赋值,可以方便的取出来。

// 返回数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

函数参数中方便地指定变量名

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

非常方便地提取JSON对象数据(解构对象)

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

防止了一个属性一个属性的获取赋值:

let id = jsonData.id;
let number = jsonData.data;
// ...

函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

指定参数的默认值,避免在函数体内部再写var foo = config.foo || 'default foo';这样的默认值处理相关的语句。

遍历Map结构

方便的获取键名和键值:

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}

只获取键名,或者只获取键值:

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

输入模块的指定方法

非常常见的方式,在引入一个模块时,通常要指定输入的方法,通过解构赋值非常清晰简洁。现在开发中使用最普遍的场景。

const { SourceMapConsumer, SourceNode } = require("source-map");

扩展操作符的实际使用

将字符串每个字符展开为数组

let str = 'hello';
let arr = [...str];
// let arr = str.split('');  
console.log(arr);  // ['h', 'e', 'l', 'l', 'o']

将类数组对象转换为数组

js中,类数组对象是拥有一个 length 属性和若干索引属性的任意对象

类数组对象非常常见,比如:

  • NodeListdocument.querySelectorAll() 返回的对象;
  • HTMLCollectiondocument.getElementsByTagName()document.getElementsByClassName() 等方法返回的对象;
  • 字符串;
  • Arguments:函数里的参数对象;

类数组对象不是数组,想要使用数组的方法,需要将其转换为数组,或改变数组的原始方法的this指向类数组对象并执行。

  • 利用apply方法通过Array构造函数将类数组对象转换为数组
let nodeList = document.querySelectorAll('div');
console.log(nodeList instanceof NodeList); // true

let arr = Array.apply(null, nodeList);
console.log(arr instanceof Array);  // true
  • Array.from将类数组对象转换为数组
let arr3 = Array.from(nodeList);
console.log(arr3 instanceof Array);  // true
  • 通过改变this指向执行数组方法
let arr2 = [].slice.call(nodeList);
// let arr3 = Array.prototype.slice.call(nodeList);
// let arr4 = [].slice.apply(nodeList);
// let arr5 = Array.prototype.slice.apply(nodeList);
console.log(arr2 instanceof Array);  // true

let arr2 = Array.prototype.slice.call(nodeList,2,5);
console.log(arr2);

let arr3 = Array.prototype.map.call(nodeList,function(node){
    console.log(node);
});
  • 展开操作符将类数组对象转换为数组
let nodeList = document.querySelectorAll('div')
let arr = [...nodeList]
console.log(arr instanceof Array)  // true

拷贝数组和对象

拷贝数组通常可以使用concat()slice()new ArrayArray.from()等实现。

let arr = [1, 2, 1, 0];
let arr2 = arr.concat();


let arr3 = arr.slice();
console.log(arr===arr2);   // false
console.log(arr2===arr3);  // false
console.log(arr===arr3);   // false

通过展开操作,实现对数组的拷贝(新建数组)

let arr = [1, 2, 1, 0];
let arr2 = [...arr];

console.log(arr===arr2);  // false

对象的拷贝,一般的做法是使用Object.assign({}, obj)

let obj = { foo: 'xx', bar: 12};
let obj2 = Object.assign({}, obj);

展开操作符展开拷贝对象。

let obj = { foo: 'xx', bar: 12};
let obj2 = {...obj};

合并数组或对象

  • concat合并数组
arr1.concat(arr2)合并数组会返回一个新的数组,不会改变原数组。
let arr1 = [1, 3, 5];
let arr2 = [2, 4, 6];
let arr3 = arr1.concat( arr2 );
console.log(arr3);  // [1, 3, 5, 2, 4, 6]
  • 使用展开操作符合并数组
let arr1 = [1, 3, 5];
let arr2 = [2, 4, 6];
let arr3 = [...arr1, ...arr2];
console.log(arr3);  // [1, 3, 5, 2, 4, 6]

替代合并对象的Object.assign({}, obj1, obj2)方法:

let obj1 = { foo: 'xx' };
let obj2 = { bar: 12 };

let obj3 = Object.assign({}, obj1, obj2);

// 合并两个对象
let obj4 = {...obj1, ...obj2};

将数组或类数组对象展开为函数的参数列表

通常函数都是通过一系列的参数列表接受传递的参数,如果想要将数组项应用到函数中,通常需要借助apply方法。

let arr = [1, 3, 5];
function fn(a, b, c) { }; 
fn.apply(null, arr);

如果使用扩展操作符,将会很简洁:

let arr = [1, 3, 5];
function fn(a, b, c) { }; 
fn(...arr);

这对于处理很多数组不存在的方法很有帮助。比如数组没有默认的求最大值、最小值等的方法,但是Math对象中有提供。因此,实际处理就会变成如下的形式:

let arr = [1,3,5,6,0,12,1];
Math.min.apply(null, arr);  // 0
Math.max.apply(null, arr);  // 12

// 使用扩展操作符
Math.min(...arr);  // 0
Math.max(...arr);  // 12

这样,可以扩展很多数组能使用的方法。

数组通过 reduce 方法获取最大或最小值的实现:

arr.reduce((acc, cur) => Math.max(acc, cur));  // 12
arr.reduce((acc, cur) => Math.min(acc, cur));  // 0

作为函数的剩余参数替代arguments

前面已经介绍了...作为函数剩余参数的使用。

在只有剩余参数时,函数接受传递的任意多个参数:

function totalSum(...arr){
  return arr.reduce((acc, cur) => acc + cur);
}

console.log( totalSum(1, 5) );      // 6
console.log( doSum(1, 3, 5) );      // 9
console.log( doSum(1, 3, 5, 9) );   // 18

向数组中添加项或向对象添加属性

数组原生提供了几种向数组中添加数据项的方法,如:

let arr = [10,9];

// 向头部添加
arr.unshift(1, 2);
console.log(arr);  // [1, 2, 10, 9]

// 向尾部添加
arr.push(6, 7);
console.log(arr);  // [1,2, 10, 9, 6, 7]

// 在任意位置添加
arr.splice(2, 0, 3, 4);
console.log(arr);  // [1, 2, 3, 4, 10, 9, 6, 7]

使用扩展操作符:

let arr = [8,7];
arr = [...arr, 5];
arr = [1, 2, ...arr, 6];
arr = [0, ...arr];
console.log(arr); // [0, 1, 2, 8, 7, 5, 6]

对象添加属性的方式直接.propName赋值即可。

let obj = { foo: 'xx', bar: 12};
obj.a = 'aaa';
console.log(obj); // {foo: 'xx', bar: 12, a: 'aaa'}

使用扩展操作符(注意属性的覆盖或重写):

let obj = { foo: 'xx', bar: 12};
obj = {...obj, a:'aaa'};
console.log(obj); // {foo: 'xx', bar: 12, a: 'aaa'}

使用扩展操作符添加数组项或对象属性的处理,本质上是新建了一个数组或对象。相对来说,还是推荐原来的方式。

利用向对象添加属性的方式,可以通过扩展操作符,向对象中添加Getter/Setter的处理【本质上也是新建对象】

为js对象的属性添加Getter/Setter方法的标准方式有如下两种:

let obj = { 
  foo: 'xx', 
  get bar(){ return 12; },
  set bar(value) { console.log(value); }
};

// 或者
let obj = { 
  foo: 'xx'
};
Object.defineProperty(obj,'bar',{
  get:function(){
    return 12;
  },
  set:function(value){ 
    console.log(`Setter:${value}`); 
  }
  // 或
  // get() {return 12;},
  // set(value) { console.log(`Setter:${value}`); }
});

使用扩展操作符,在展开对象时,添加Getter/Setter:

let obj = { 
  foo: 'xx'
};
obj = {
  ...obj,
  get bar(){ return 1; }
}

参考

相关文章
|
7月前
|
JavaScript
ES6之变量的解构赋值
ES6之变量的解构赋值
|
7月前
|
JSON JavaScript 前端开发
ES6 变量的解构赋值
ES6 变量的解构赋值
|
1月前
es6变量声明与解构赋值
ES6的变量声明与解构赋值特性使得代码更加简洁、清晰,提高了开发效率和代码的可读性,在实际开发中被广泛应用。
ES6学习(2)解构赋值
ES6学习(2)解构赋值
|
5月前
ES6 解构赋值【详解】
ES6 解构赋值【详解】
29 0
|
JavaScript 前端开发 网络架构
ES6 解构赋值
ES6 解构赋值
86 0
ES6语法: 解构赋值
ES6语法: 解构赋值
57 0
Es6解构赋值
例如现在有一个本地存储里面存的是用户信息,然后需要拿到里面的id,名称等等非常麻烦
|
JSON JavaScript 前端开发
ES6(变量的解构赋值)
ES6(变量的解构赋值)
71 0