【js 笔记】读阮一峰老师 es6 入门笔记 —— 第二章
第二章:变量的解构赋值
在es6 版本前,如果要为多个变量赋不同值,我想是件比较麻烦的事情。但es6 版本新推出了一个新技术那就是今天的主角变量的解构赋值。
变量解构赋值分为两种方法:数组解构赋值 和 对象解构赋值
一:数组解构赋值
1.1 数组解构赋值的基本语法
以前为多个变量赋不同的值只能这样:
let a = 1; let b = 2; let c = 3;
但有了解构赋值便可以这样赋,下面例子是数组解构赋值的基本语法
let [a, b, c] = [1, 2, 3];
所以匹配方式为:左边被声明的变量名一一对应着右边对应数组位置的变量值。也就是说在左边数组中 Index 为 0 的变量a 它的值是 右边数组 index 为 0 的1。
他们的赋值关系是通过数组的下标一一对应的。
再看一个复杂的例子:
let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3
代码可分解为这样
let [foo, [[bar], baz]] = [1, [[2], 3]]; // 第一轮分解 foo = 1; [[bar], baz] = [[2], 3]; // 第二轮分解 baz = 3; [bar] = [2]; // 第三轮分解 bar = 2; / 所以结果为 // foo = 1 // bar = 2 // baz = 3
由上面的代码可以看出,为一个三维数组解构赋值
第一轮首先将 右边数组的第一个元素赋值到左边数组第一个元素,也就是 foo = 1
第二轮 右边数组第二个元素赋值到左边数组的第二个元素中,因为左边数组第二个元素又是一个数组所以对应的右边数组也应该是一个数组。
它们又会做一次匹配将右边数组的第一个元素赋值到左边数组第一个元素,第二个也是这样(看上面代码第二轮分解)
最后bar 匹配到 2 这个值
上面的次序是个人想出来的并非官方答案
总之,数组解构赋值的方式就是 将右边数组与左边数组的位置一一对应去赋值
1.2 数组不完全的结构,如果没有对应的值而且也没有默认值就会被赋上undefined,如果有默认值则被赋默认值
在上面的例子中,基本就是完全结构的结果(每一个变量都能匹配到对应的值)。而匹配也可以不完全地进行匹配,右边数组的元素个数可以小于左边数组元素个数也可以大于左边元素个数
看如下例子:
let [foo] = [];
let [bar, foo] = [1];
上例第一条语句,右边数组为空,因此foo 无法被赋值,因此foo = undefined,第二条也是如此
再来一个例子:
let [a, [b], d] = [1, [2, 3], 4]; a // 1 b // 2 d // 4
上例中,右边元素个数比左边元素个数多,但左边数组的元素依旧只会匹配回对应下标位置的值
倘若第一个例子有默认值,那么将会是如下效果:
let [foo = 1] = []; // foo = 1 let [bar, foo = 2] = [1]; // foo = 2
es6 规定了默认值赋值的规则,当右边数组对应左边数组位置的值为 undefined 则会将左边数组对应位置的变量赋上默认值,如果页没有默认值那么就为 undefined
大概意思如下:
let leftArr = [a, b, c]; let value = [1, 2]; for (let i = 0; i < left.length; i++) { if (value[i] === undefined) { leftArr[i] = leftArr[i].__default__; }else { leftArr[i] = value[i]; } }
上面的 __default__ 是自己理解的意思,并不真实存在
另外,在判断语句中可以看出,右边数组判断是否存在值是用 === 全等于去判断是否为undefined 的,所以如果遍历到的值为 null 这些不全等于 undefined 的值是不会让左边数组的变量赋上默认值的,而是直接将 null 赋给它,例子:
let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null
当右边数组对应值全等于undefined 的时候才会触发默认值的赋值。
默认值还可以是一个函数,当没能完成赋值时才会调用函数,例子:
function f() { return 'aaa'; } let [x = f(), y = f()] = [1];
// x = 1
// y = 'aaa'
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError
最后一条语句报错的原因是,y 还没声明就被x 调用作为初始值所以报错
1.3 数组解构,右边的匹配值一定是要有Iterator 接口的对象
例子:
// 报错 let [foo] = 1; let [foo] = false; let [foo] = NaN; let [foo] = undefined; let [foo] = null; let [foo] = {};
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
使用一些具有 Iterator 接口的表达式也是可以实现数组方式的解构赋值
let [x, y, z] = new Set(['a', 'b', 'c']); x // "a" function* fibs() { let a = 0; let b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } let [first, second, third, fourth, fifth, sixth] = fibs(); sixth // 5
二:对象解构赋值
2.1 基本语法
我们知道对象实际上是一个无序列表,靠每一个键标识着值得存放位置,那应该怎么声明呢?如下例子:
let { foo, bar } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb"
如果左边是一个对象,那么右边也应该对应是一个对象,上例左边数组中用了 es6 的新特性,就是如果对象中键和值的名字相同,那么就不用写冒号了
用es5 的语法表达如下:
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" }; // 所以 foo = "aaa"; bar = "bbb";
假若变量名和键不一样就应该用回传统的 es5 对象语法
所以匹配方式为:右边对象对应左边对象的键,为左边对象对应键的值赋值,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。
和数组一样,对象的解构法则也可以嵌套解构,例子如下:
let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj; x // "Hello" y // "World"
分解:
let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj; // 第一轮 p = ['Hello', { y: 'World' }]; [x, {y}] = ['Hello', { y: 'World' }] // 第二轮 x = 'hello'; {y} = { y: 'World' }; // 第三轮 y = 'world'; // 所以 // x = 'hello' // y = 'world'
左边p 键 对应的值是 [x, { y }],所以使得 [x, { y }] 与对应值内部的 ['Hello', { y: 'World' }] 进行数组解构赋值
更深嵌套的例子:
const node = { loc: { start: { line: 1, column: 5 } } }; let { loc, loc: { start }, loc: { start: { line }} } = node;
分解为:
const node = { loc: { start: { line: 1, column: 5 } } }; let { loc, loc: { start }, loc: { start: { line }} } = node; // 第一轮 loc = { start: { line: 1, column: 5 } }; {start} = {start: {line: 1,column: 5}} { start: { line }} = {start: {line: 1,column: 5}} //第二轮 start = {line: 1,column: 5 }; { line } = {line: 1,column: 5} // 第三轮 line = 1
对象的解构也支持默认值,其规则和数组的解构方式一样,例子:
let {x, y = 5} = {x: 1}; x // 1 y // 5 let { message: msg = 'Something went wrong' } = {}; msg // "Something went wrong"
let {zoo = 3} = {x: null};
zoo // null
2.2 如果解构模式是嵌套的对象,而且子对象的父层引用未能赋上值,那么将会报错。
例子:
let {foo: {bar}} = {baz: 'baz'};
其实可以拆分成这样
let {foo: {bar}} = {baz: 'baz'}; {bar} = undefined; undefined.bar = undefined; // 所以报错
2.3 要将事先声明了的变量进行解构赋值,那么就必须将解构语句用括号括住
例子:
// 错误的写法 let x; {x} = {x: 1}; // SyntaxError: syntax error
上面的例子先声明了变量x 再将x 用作解构赋值,但是会报错。
因为浏览器引擎会误以为是两个语句块的赋值,所以报错。
正确的写法应当是将整个解构赋值语句用括号括住,例子:
// 正确的写法 let x; ({x} = {x: 1});
只要不将花括号放在语句句首才不会保证不报错!
2.5 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
let {} = [true, false]; let {} = 'abc'; let {} = [];
尽管有效但避免应用,因为这样毫无意义
三:解构的一些特殊应用
3.1 可以将一些对象的属性及方法抽取出来
3.1.1 获取对象内部的方法和属性
通过以对象形式的解构,可以将一些常用的对象属性抽取出来保存在变量中,例子:
let { log, sin, cos } = Math;
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
上例中,左边的变量会保存右边 Math 对象中的log, sin, cos 方法,之后调用这些方法可以直接就调用这些变量就可以了
而数组也算是一个对象,所以左边的变量依旧可以保存其内部的方法
3.1.2 获取字符串的每一个字符
通过以数组的形式对字符串进行解构可以达到遍历的效果,例子:
let [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o"
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError let { prop: y } = null; // TypeError
3.1.3 利用解构为函数参数传递多个参数
利用解构传递向函数传递多个参数,不仅方便了多个参数的传递,而且还可以为参数赋予默认值,当某个参数没有传递值的时候就可以赋上默认的值,例子:
function add([x, y]){ return x + y; } add([1, 2]); // 3
函数 add 参数 [x, y] 匹配到传递的参数值 [1, 2] 所以解构成功
下面是函数参数执行默认值的例子:
注意,如果函数的参数是采用的对象解构模式那么必须向如下格式那样写参数,否则会报错 Uncaught TypeError: Cannot match against 'undefined' or 'null'.
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]
实际上当没有参数传递的时候会执行这条解构语句 {x = 0, y = 0} = {} ,空对象没有可以被匹配的值所以参数会用默认值去初始化自己
当函数有参数的时候会执行 {x = 0, y = 0} = {实际参数} 这条解构语句
下面这种写法还会导致不同的结果:
function move({x, y} = { x: 0, y: 0 }) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0]
上面代码是为函数move
的参数指定默认值,而不是为变量x
和y
指定默认值,当没有参数的时候自然机会执行 {x, y} = { x: 0, y: 0 } 这样的解构语句,所以参数被右边的值初始化了
而传一个空的对象作为参数,因为函数的参数并没有默认值所以为undefined
3.1.4 交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
3.4.5提取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]
3.1.7 输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
四:解构语句中的圆括号使用
4.1 不使用圆括号的情况
1、 变量声明语句
在有let 声明的前提下,解构赋值不必有括号
// 全部报错 let [(a)] = [1]; let {x: (c)} = {}; let ({x: c}) = {}; let {(x: c)} = {}; let {(x): c} = {}; let { o: ({ p: p }) } = { o: { p: 2 } };
2、函数参数
函数参数里也不必用圆括号
// 报错 function f([(z)]) { return z; } // 报错 function f([z,(x)]) { return x; }
3、赋值语句的模式
赋值语句模式应该将整个解构语句用括号括住而不是一部分括住
// 全部报错
let a;
let b; ({ p: a }) = { p: 42 }; ([b]) = [5];
// 正确 let a; let b; ({ p: a } = { p: 42 }); ([b] = [5]);
4.2 可使用圆括号的情况
可使用圆括号的情况就只有一种就是,括号包含的内容不属于模式的一部分,例如:
[(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确
第一句: 元素b 并不是模式的一部分,而是模式里的一个元素
第二句:将赋值语句整句用圆括号包住 符合规定
第三句:同第一句一样情况