美文网首页es6相关
阮一峰ES6教程读书笔记(一)解构赋值、let和const

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

作者: 前端艾希 | 来源:发表于2019-07-31 15:02 被阅读0次

    About

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

    一、详解let和const命令

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

    (一)var和let的区别

    • var声明的变量的作用域为全局作用域或者函数作用域,而let声明的变量多一个块级作用域
    • var声明的变量可以进行变量提升,即在声明变量之前使用变量,而let声明的变量必须要在变量声明语句之后使用,否则会报错
    • let声明的变量存在“暂时性死区”,即从区块开始至变量声明语句之前是无法访问变量的
    • 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里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

    • 允许在块级作用域内声明函数。
    • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    • 同时,函数声明还会提升到所在的块级作用域的头部。
      所以上述代码可翻译为:
    // 在使用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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/ucctdctx.html