es6相关

阮一峰ES6教程读书笔记(一)解构赋值、let和const

2019-07-31  本文已影响0人  前端艾希

About

读完阮一峰大神ES5教程后自觉获益匪浅,遂拜读其ES6教程。为记录所感所得,打算写《阮一峰ES6教程读后感》系列文章,有兴趣的朋友可以关注一下。

一、详解let和const命令

letconstES6新增的变量声明命令,他们的用法与var类似,但是经它们声明的变量与var声明的变量有很大的区别。

(一)var和let的区别

1. 块级作用域

我们先看看这个例子:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

其实我们预期的答案是6,但是如果这样写代码,我们无法的到预期的答案,因为a[6]里面存储的函数语句中的i指向的是一个全局变量,当for循环完毕后,i的值为10,即函数语句中的每一个i都指向了同一个地址,而这个地址中存储的数字为10,所以我们无论通过数组a中哪一个元素中的函数去打印i得到的答案都是10

为了得到我们预期的答案,我们应该将代码进行一定的改写:

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

可以看到这里面的for循环的i是通过let声明的,所以i仅在当前循环中有效,在其他循环中无法访问到当前循环的i,我们可以理解为每一个i是独立的,所以数组a中每一个元素中存储的函数语句中的i指向的是不同的地址,每个地址存储的是当前循环中的i

2. 不存在变量提升

正常的逻辑是我们必须先声明一个变量,然后再去使用,如果在没有声明就去使用而仅仅告诉我undefined,这显然不能让我们满意。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

通过上面的代码我们可以发现,使用let声明的变量终于纠正了这一点,我们无法在声明变量之前去使用它。

3. 暂时性死区(temporal dead zone,简称 TDZ)

正因为使用let声明的变量不存在变量提升现象,所以会出现暂时性死区的现象

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
  tmp = '2333'
  console.log(tmp) // 2333
}

尽管我们知道在使用大括号括起来的代码块内使用let声明变量,该变量的作用域为块级作用域,但是该块级作用域并非整个区块,从该区块开始到声明变量之前的区域被称之为“暂时性死区”,因为在这一区域内我们无法访问到后面声明的变量。

4. 不允许重复声明

在使用let声明的变量的作用域内,不允许重复声明,无论你用letvar还是const。在函数内也一样:

function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

(二)块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。比如:

// 在使用ES5标准的浏览器中
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}()); // 'I am inside!'

因为ES5中,函数也是“一等公民”具有变量提升的作用,但是函数的变量提升不仅仅是函数名的变量提升,而是包括整个函数体的提升,所以上述代码可翻译为:

// 在使用ES5标准的浏览器中
function f() { console.log('I am outside!'); }

(function () {
  // 重复声明一次函数f
  function f() { console.log('I am inside!'); }
  if (false) {
    }
  f();
}()); // 'I am inside!'

但是在使用ES6标准的浏览器中,上述代码就会报错ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

// 在使用ES6标准的浏览器中
function f() { console.log('I am outside!'); }

(function () {
  var f  // 此时f为undefined
  if (false) {
  }

  f();
}()); // Uncaught TypeError: f is not a function

(三)const命令

letconst的用法差不多,只不过是const一般被用来声明常量,即声明语句中必须赋值,并且声明之后不能更改

1. const命令的本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

(四)顶层对象的属性

ES6以前的标准中,我们使用varfunction声明的全局变量会变成顶层对象的属性,ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

二、变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

(一)基本用法

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

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值

1. 解构成功

解构成功的条件是=两边的模式完全相同

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

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

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

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

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

2. 不完全解构

因为我们使用解构的目的是为了给=左边的变量赋值,那么如果从左到右都能找到对应元素,那么就能完成解构,如果此时从右到左无法完成一一对应,那么就称之为不完全解构,例如:

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

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

3. 解构失败

如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

(二)解构表达式默认赋值

同声明函数一样,我们可以在函数表达式中声明实参时给实参赋默认值,如果调用该函数时,形参为空,那么实参就采用声明函数时的默认值。解构赋值表达式同理

let [foo = true] = [];
foo // true

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

(三)对象的解构赋值

与数组一样,解构也可以用于嵌套结构的对象。

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

(四)字符串解构赋值和布尔值解构赋值

1. 字符串解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

2. 布尔值解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

(五)解构赋值的作用

1. 交换变量的值

以前,这个功能只在Python中使用过,如今可以在JavaScript中使用还是很开心的

[x, y] = [y, x]

2. 从函数返回多个值

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

3. 函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来。

4. 提取JSON数据

这个太有用了,因为我们从接口中获得的数据一般都是JSON对象,所以使用结构赋值可以使我们的代码更简洁

const {ret, data} = res.data

5. 遍历Map结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。这个在Vue中也是经常使用

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

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

6. 输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

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

参考链接

作者:阮一峰
链接:http://es6.ruanyifeng.com/#docs/destructuring

上一篇下一篇

猜你喜欢

热点阅读