zoukankan      html  css  js  c++  java
  • ES6的学习记录

    1、let命令:

      基本用法:

        ES6新增了let命令,用来声明变量。用法类似于var,但是所声明的变量只在let命令所在的代码块内有效。

    {
        let a = 10; 
        var b = 1;
    }
    
    
    console.log(a);    // ReferenceError : a is not defined
    console.log(b);    // 1

        for循环的计数器,就很适合用let命令。

    for (let i = 0; i < 10; i ++) {
        //...
    }
    
    console.log(i);    // ReferenceError : i is not defined

        下面的代码如果使用var,最后输出的是10. 

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

         如果使用let,声明的变量仅在块级作用域内有效,最后输出的就是6.

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

        另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的自作用域。

    for (let i = 0; i < 3; i ++) {
        let i = 'abc';
        console.log(i);
    }
    
    // abc
    // abc
    // abc

      不存在变量提升:

        var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

        为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

    // var的情况
    console.log(foo);    // undefined
    var foo = 2;
    
    // let的情况
    console.log(bar);    // ReferenceError
    let bar = 2;

      暂时性死区:

        只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

    var tmp = 123;
    
    if (true) {
        tmp = 'abc';    // ReferenceError
        let tmp;
    }

        ES6明确规定,如果区块中存在letconst命令,这个区块对这些命令的声明,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

        总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上成为“暂时性死区”(简称TDZ)。

    if (true) {
        // TDZ开始
        tmp = 'abc';    // ReferenceError
        console.log(tmp);    // ReferenceError
    
        let tmp;    //TDZ结束
        console.log(tmp);    // undefined
    
        tmp = 123;
        console.log(tmp);    // 123
    }

        “暂时性死区”也意味着typeof不是一个百分百安全的操作。变量在声明之前都是“死区”,只要用到就会报错。因此typeof会抛出一个ReferenceError。作为比较,如果一个 变量根本没有声明,使用typeof反而不会报错。

    typeof x;    //ReferenceError
    let x;
    
    typeof undeclared_variable;    //undefined

        有些“死区”比较隐蔽,不太容易发现。

    function bar(x = y, y = 2) {
        return [x, y];
    }
    
    bar();    //报错,原因是x默认等于y,但此时y还没有声明,属于“死区”。
    function bar(x = 2, y = x) {
        return [x, y];
    }
    
    bar();    // [2, 2]

        另外,下面代码也会报错,与var的行为不同。

    var x = x;    // 不报错
    
    let x = x;    //ReferenceError: x is not defined (也是因为暂时性死区导致)

      不允许重复声明:

        let不允许在相同作用域内,重复声明同一个变量。

    // 报错
    function funca(){
        let a = 10;
        var a = 1;
    }
    
    // 报错
    function funca(){
        let a = 10;
        let a = 1;
    }

        因此,不能在函数内部重新声明参数。

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

    2、块级作用域:

      为什么需要块级作用域?

        ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景:

         (1)内层变量可能会覆盖外层变量。

    var tmp = new Date();
    
    function f() {
      console.log(tmp);
      if (false) {
        var tmp = 'hello world';
      }
    }
    
    f(); // undefined

        (2)用来计数的循环变量泄露为全局变量。

    var s = 'hello';
    
    for (var i = 0; i < s.length; i++) {
      console.log(s[i]);
    }
    
    console.log(i); // 5

      ES6的块级作用域:

        let实际上为JavaScript新增了块级作用域。

    function f1() {
      let n = 5;
      if (true) {
        let n = 10;
      }
      console.log(n); // 5
    }

        ES6 允许块级作用域的任意嵌套。

    {{{{{let insane = 'Hello World'}}}}};

        外层作用域无法读取内层作用域的变量。

    {{{{
      {let insane = 'Hello World'}
      console.log(insane); // 报错
    }}}};

        内层作用域可以定义外层作用域的同名变量。

    {{{{
      let insane = 'Hello World';
      {let insane = 'Hello World'}
    }}}};

        块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

    // IIFE 写法
    (function () {
      var tmp = ...;
      ...
    }());
    
    // 块级作用域写法
    {
      let tmp = ...;
      ...
    }

      块级作用域与函数声明:

        ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

    // 情况一
    if (true) {
      function f() {}
    }
    
    // 情况二
    try {
      function f() {}
    } catch(e) {
      // ...
    }    

        上面两种函数声明,根据 ES5 的规定都是非法的。

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

        ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

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

        上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

    // ES5 环境
    function f() { console.log('I am outside!'); }
    
    (function () {
      function f() { console.log('I am inside!'); }
      if (false) {
      }
      f();
    }());

        ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?

        原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

        • 允许在块级作用域内声明函数。
        • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
        • 同时,函数声明还会提升到所在的块级作用域的头部。

        注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

        根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。

    // 浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }
    
    (function () {
      if (false) {
        // 重复声明一次函数f
        function f() { console.log('I am inside!'); }
      }
    
      f();
    }());
    // Uncaught TypeError: f is not a function

         上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。

    // 浏览器的 ES6 环境
    function f() { console.log('I am outside!'); }
    (function () {
      var f = undefined;
      if (false) {
        function f() { console.log('I am inside!'); }
      }
    
      f();
    }());
    // Uncaught TypeError: f is not a function

        考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

    // 函数声明语句
    {
      let a = 'secret';
      function f() {
        return a;
      }
    }
    
    // 函数表达式
    {
      let a = 'secret';
      let f = function () {
        return a;
      };
    }

        另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

    // 不报错
    'use strict';
    if (true) {
      function f() {}
    }
    
    // 报错
    'use strict';
    if (true)
      function f() {}

      do表达式:

        本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

    {
      let t = f();
      t = t * t + 1;
    }

        上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。

        现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式,然后就会返回内部最后执行的表达式的值。

    let x = do {
      let t = f();
      t * t + 1;
    };

        上面代码中,变量x会得到整个块级作用域的返回值(t * t + 1)。

    3、const命令:

      基本用法:

        const声明一个只读的常量。一旦声明,常量的值就不能改变。

    const PI = 3.1415;
    PI // 3.1415
    
    PI = 3;
    // TypeError: Assignment to constant variable.

        const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

    const foo;
    // SyntaxError: Missing initializer in const declaration

        const的作用域与let命令相同:只在声明所在的块级作用域内有效。

    if (true) {
      const MAX = 5;
    }
    
    MAX // Uncaught ReferenceError: MAX is not defined

        const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

    if (true) {
      console.log(MAX); // ReferenceError
      const MAX = 5;
    }

        const声明的常量,也与let一样不可重复声明。

    var message = "Hello!";
    let age = 25;
    
    // 以下两行都会报错
    const message = "Goodbye!";
    const age = 30;

      本质:

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

    const foo = {};
    
    // 为 foo 添加一个属性,可以成功
    foo.prop = 123;
    foo.prop // 123
    
    // 将 foo 指向另一个对象,就会报错
    foo = {}; // TypeError: "foo" is read-only   
    const a = [];
    a.push('Hello'); // 可执行
    a.length = 0;    // 可执行
    a = ['Dave'];    // 报错

        如果真的想将对象冻结,应该使用Object.freeze方法。

    const foo = Object.freeze({});
    
    // 常规模式时,下面一行不起作用;
    // 严格模式时,该行会报错
    foo.prop = 123;

        除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

    var constantize = (obj) => {
      Object.freeze(obj);
      Object.keys(obj).forEach( (key, i) => {
        if ( typeof obj[key] === 'object' ) {
          constantize( obj[key] );
        }
      });
    };

      ES6 声明变量的六种方法:

        ES5 只有两种声明变量的方法:var命令和function命令。

        ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

    4、顶层对象的属性:

        顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

    window.a = 1;
    console.log(a);     // 1
    
    a = 2;
    console.log(window.a);     // 2

        顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

        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

    5、global对象:

        ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。

        • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
        • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
        • Node 里面,顶层对象是global,但其他环境都不支持。

        同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

        • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
        • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
        • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全政策),那么evalnew Function这些方法都可能无法使用。

        综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。

    // 方法一
    (typeof window !== 'undefined'
       ? window
       : (typeof process === 'object' &&
          typeof require === 'function' &&
          typeof global === 'object')
         ? global
         : this);
    
    // 方法二
    var getGlobal = function () {
      if (typeof self !== 'undefined') { return self; }
      if (typeof window !== 'undefined') { return window; }
      if (typeof global !== 'undefined') { return global; }
      throw new Error('unable to locate global object');
    };

        现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。

        垫片库system.global模拟了这个提案,可以在所有环境拿到global

    // CommonJS 的写法
    require('system.global/shim')();
    
    // ES6 模块的写法
    import shim from 'system.global/shim'; shim();

        上面代码可以保证各种环境里面,global对象都是存在的。

    // CommonJS 的写法
    var global = require('system.global')();
    
    // ES6 模块的写法
    import getGlobal from 'system.global';
    const global = getGlobal();

        上面代码将顶层对象放入变量global

     6、数组的解构赋值:

      基本用法:

        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 [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 // []

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

    let [foo] = [];
    let [bar, foo] = [1];

        以上两种情况都属于解构不成功,foo的值都会等于undefined

        另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

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

        上面两个例子,都属于不完全解构,但是可以成功。

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

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

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

        对于 Set 结构,也可以使用数组的解构赋值。

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

        事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

    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

        上面代码中,fibs是一个 Generator 函数(参见《Generator 函数》一章),原生具有 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'

        注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

    let [x = 1] = [undefined];
    x // 1
    
    let [x = 1] = [null];
    x // null

        上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

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

    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 = 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 is not defined

        上面最后一个表达式之所以会报错,是因为xy做默认值时,y还没有声明。

    7、对象的解构赋值:

      解构不仅可以用于数组,还可以用于对象。

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

        对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

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

        上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

        如果变量名与属性名不一致,必须写成下面这样。

    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'

        这实际上说明,对象的解构赋值是下面形式的简写。

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

        也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

    let { foo: baz } = { foo: "aaa", bar: "bbb" };
    baz // "aaa"
    foo // error: foo is not defined

        上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

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

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

        下面是另一个例子。

    const node = {
      loc: {
        start: {
          line: 1,
          column: 5
        }
      }
    };
    
    let { loc, loc: { start }, loc: { start: { line }} } = node;
    line // 1
    loc  // Object {start: Object}
    start // Object {line: 1, column: 5}

        上面代码有三次解构赋值,分别是对locstartline三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,locstart都是模式,不是变量。

        下面是嵌套赋值的例子。

    let obj = {};
    let arr = [];
    
    ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
    
    obj // {prop:123}
    arr // [true]

        对象的解构也可以指定默认值。

    var {x = 3} = {};
    x // 3
    
    var {x, y = 5} = {x: 1};
    x // 1
    y // 5
    
    var {x: y = 3} = {};
    y // 3
    
    var {x: y = 3} = {x: 5};
    y // 5
    
    var { message: msg = 'Something went wrong' } = {};
    msg // "Something went wrong"

        默认值生效的条件是,对象的属性值严格等于undefined

    var {x = 3} = {x: undefined};
    x // 3
    
    var {x = 3} = {x: null};
    x // null

        上面代码中,属性x等于null,因为nullundefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。

        如果解构失败,变量的值等于undefined

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

        如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

    // 报错
    let {foo: {bar}} = {baz: 'baz'};

        上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错,请看下面的代码。

    let _tmp = {baz: 'baz'};
    _tmp.foo.bar // 报错

        如果要将一个已经声明的变量用于解构赋值,必须非常小心。

    // 错误的写法
    let x;
    {x} = {x: 1};
    // SyntaxError: syntax error

        上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

    // 正确的写法
    let x;
    ({x} = {x: 1});
  • 相关阅读:
    Centos7安装Zabbix4.0步骤
    linux异常处理:selinux配置错误导致无法重启
    自学Python5.4-类 _init_方法
    自学Python5.3-类和对象的简单操作
    自学Linux Shell9.3-基于Red Hat系统工具包:RPM属性依赖的解决方式-YUM在线升级
    自学Linux Shell9.4-基于Red Hat系统工具包存在两种方式之二:源码包
    自学Linux Shell9.2-基于Red Hat系统工具包存在两种方式之一:RPM包
    JS 对象API之修改、删除对象的属性
    JS 对象API之判断自有属性、共有属性
    JS 对象API之判断父对象是否在子对象的原型链上
  • 原文地址:https://www.cnblogs.com/minozMin/p/es6.html
Copyright © 2011-2022 走看看