• es6精华


    函数:

    ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。function log(x, y = 'World') {

      console.log(x, y);
    }
    
    log('Hello') // Hello World
    log('Hello', 'China') // Hello China
    log('Hello','')

    参数变量是默认声明的,所以不能用letconst再次声明。
    function foo(x = 5) {
      let x = 1; // error
      const x = 2; // error
    }上面代码中,参数变量x是默认声明的,在函数体中,不能用letconst再次声明,否则会报错。

    使用参数默认值时,函数不能有同名参数。
    // 不报错
    function foo(x, x, y) {
      // ...
    }
    
    // 报错
    function foo(x, x, y = 1) {
      // ...
    }
    //

    参数默认值不是传值的,而是每次都重新计算默认值表达式的值。
    let x = 99;
    function foo(p = x + 1) {
      console.log(p);
    }
    
    foo() // 100
    
    x = 100;
    foo() // 101

    作用域

    一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域

    var x = 1;
    function f(x, y = x) {
      console.log(y);
    }
    f(2)

    rest 参数

    function add(...values) {

      let sum = 0;
      for (var val of values) {
        sum += val;
      }
      return sum;
    }
    add(2, 5, 3) // 10

    name 属性

    var f = function () {};
    // ES5
    f.name // ""
    // ES6
    f.name // "f"

    箭头函数

    如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

    // 报错
    let getTempItem = id => { id: id, name: "Temp" };
    
    // 不报错
    let getTempItem = id => ({ id: id, name: "Temp" });

    使用注意点

    (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

    上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。

    function foo() {
      setTimeout(() => {
        console.log('id:', this.id);
      }, 100);
    }
    
    var id = 21;
    
    foo.call({ id: 42 });
    // id: 42
    

    上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42

    var handler = {
      id: '123456',
    
      init: function() {
        document.addEventListener('click',
          event => this.doSomething(event.type), false);
      },
    
      doSomething: function(type) {
        console.log('Handling ' + type  + ' for ' + this.id);
      }
    };

    上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。

    this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

    function foo() {
      setTimeout(() => {
        console.log('id:', this.id);
      }, 100);
    }
    
    // ES5
    function foo() {
      var _this = this;
    
      setTimeout(function () {
        console.log('id:', _this.id);
      }, 100);
    }
    

    上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

    function foo() {
      return () => {
        return () => {
          return () => {
            console.log('id:', this.id);
          };
        };
      };
    }
    
    var f = foo.call({id: 1});
    
    var t1 = f.call({id: 2})()(); // id: 1
    var t2 = f().call({id: 3})(); // id: 1
    var t3 = f()().call({id: 4}); // id: 1
    

    上面代码之中,只有一个this,就是函数foothis,所以t1t2t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this

    function foo() {
      setTimeout(() => {
        console.log('args:', arguments);
      }, 100);
    }
    
    foo(2, 4, 6, 8)
    // args: [2, 4, 6, 8]
    

    上面代码中,箭头函数内部的变量arguments,其实是函数fooarguments变量。

    另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

    (function() {
      return [
        (() => this.x).bind({ x: 'inner' })()
      ];
    }).call({ x: 'outer' });
    // ['outer']
    

    上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this

    双冒号运算符

    foo::bar;
    // 等同于
    bar.bind(foo);
    foo::bar(...arguments);
    // 等同于
    bar.apply(foo, arguments);
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn(obj, key) {
      return obj::hasOwnProperty(key);
    }
     
     
     
     
     
     
    
    
     

    let:

    1、let声明的变量只在它所在的代码块有效。

    例:{let a=10;var b=1};a;

    var a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 6
    2、不存在变量提升,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
    3、暂时性死区:
      ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
    4、
    不允许重复声明:
    let不允许在相同作用域内,重复声明同一个变量。
    5、ES6 的块级作用域:
    function f1() {
      let n = 5;
      if (true) {
        let n = 10;
      }
      console.log(n); // 5
    }外层代码块不受内层代码块的影响
    外层作用域无法读取内层作用域的变量。
    6、外层作用域无法读取内层作用域的变量。
    {{{{
      {let insane = 'Hello World'}
      console.log(insane); // 报错
    }}}};
    7、内层作用域可以定义外层作用域的同名变量。
    {{{{
      let insane = 'Hello World';
      {let insane = 'Hello World'}
    }}}};
    8、块级作用域内声明的函数,行为类似于var声明的变量。
    function f() { console.log('I am outside!'); }
    (function () {
      if (false) {
        // 重复声明一次函数f
        function f() { console.log('I am inside!'); }
      }
      f();
    }());

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

    // 浏览器的 ES6 环境

    function f() { console.log('I am outside!'); }
    (function () {
      var f = undefined;
      if (false) {
        function f() { console.log('I am inside!'); }
      }
      f();
    }());
    9、ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

    const 命令

    1、const声明一个只读的常量。一旦声明,常量的值就不能改变。改变常量的值会报错

    2、对于const来说,只声明不赋值,就会报错。

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

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

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

    本质

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

    es5与es6针对全局对象的改变

    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

    Generator

    Generator 函数会返回一个遍历器对象,
    Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
    Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();

    hw.next()
    // { value: 'hello', done: false }
    
    hw.next()
    // { value: 'world', done: false }
    
    hw.next()
    // { value: 'ending', done: true }
    
    hw.next()
    // { value: undefined, done: true }

    第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。

    第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值worlddone属性的值false,表示遍历还没有结束。

    第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

    第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。

    总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

    async 

    const fs = require('fs');
    
    const readFile = function (fileName) {
      return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data) {
          if (error) return reject(error);
          resolve(data);
        });
      });
    };
    
    const gen = function* () {
      const f1 = yield readFile('/etc/fstab');
      const f2 = yield readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    

    写成async函数,就是下面这样。

    const asyncReadFile = async function () {
      const f1 = await readFile('/etc/fstab');
      const f2 = await readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
    function timeout(ms) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value);
    }
    
    asyncPrint('hello world', 50);
    

    上面代码指定 50 毫秒以后,输出hello world

     

    async函数对 Generator 函数的改进,体现在以下四点。

    (1)内置执行器。

    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

    asyncReadFile();
    

    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

    (2)更好的语义。

    asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

    (3)更广的适用性。

    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

    (4)返回值是 Promise。

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

    字符串方法

    includes(), startsWith(), endsWith(),

    repeat()

    • includes():返回布尔值,表示是否找到了参数字符串。
    • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
    • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
    • repeat方法返回一个新字符串,表示将原字符串重复n次。
    let s = 'Hello world!';
    
    s.startsWith('Hello') // true
    s.endsWith('!') // true
    s.includes('o') // true
    这三个方法都支持第二个参数,表示开始搜索的位置。
    let s = 'Hello world!';
    
    s.startsWith('world', 6) // true
    s.endsWith('Hello', 5) // true
    s.includes('Hello', 6) // false
    'x'.repeat(3) // "xxx"
    'hello'.repeat(2) // "hellohello"
    'na'.repeat(0) // 

    参数如果是小数,会被取整。
    'na'.repeat(2.9) // "nana"

    如果repeat的参数是负数或者Infinity,会报错。
    'na'.repeat(Infinity)
    // RangeError
    'na'.repeat(-1)
    // RangeError

    es2017新增padStart(),padEnd()

    padStart()用于头部补全,padEnd()用于尾部补全。

    'x'.padStart(5, 'ab') // 'ababx'
    'x'.padStart(4, 'ab') // 'abax'
    
    'x'.padEnd(5, 'ab') // 'xabab'
    'x'.padEnd(4, 'ab') // 'xaba'

    padStartpadEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

    如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

    'xxx'.padStart(2, 'ab') // 'xxx'
    'xxx'.padEnd(2, 'ab') // 'xxx'
    'abc'.padStart(10, '0123456789')
    // '0123456abc'

    'x'.padStart(4) // '   x'
    'x'.padEnd(4) // 'x   '

    '1'.padStart(10, '0') // "0000000001"
    '12'.padStart(10, '0') // "0000000012"
    '123456'.padStart(10, '0') // "0000123456"

    另一个用途是提示字符串格式。

    '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
    '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

    模板字符串

    如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

    $('#list').html(`
    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    `);
    如果你不想要这个换行,可以使用trim方法消除它。
    $('#list').html(`
    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    `.trim());

    模板字符串中嵌入变量,需要将变量名写在${}之中。

    模板字符串之中还能调用函数。

    function fn() {
      return "Hello World";
    }
    
    `foo ${fn()} bar`
    // foo Hello World bar

    模板字符串甚至还能嵌套。

    const tmpl = addrs => `
      <table>
      ${addrs.map(addr => `
        <tr><td>${addr.first}</td></tr>
        <tr><td>${addr.last}</td></tr>
      `).join('')}
      </table>
    `;
     
    *******数值新增
    // ES5的写法
    parseInt('12.34') // 12
    parseFloat('123.45#') // 123.45
    
    // ES6的写法
    Number.parseInt('12.34') // 12
    Number.parseFloat('123.45#') // 123.45

    Number.isInteger()

    Number.isInteger()用来判断一个数值是否为整数。

    Number.isInteger(25) // true
    Number.isInteger(25.1) // false

    字符串正则新增方法:

    u 修饰符:ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

    /^uD83D/u.test('uD83DuDC2A') // false
    /^uD83D/.test('uD83DuDC2A') // true
    

    上面代码中,uD83DuDC2A是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false

    一旦加上u修饰符号,就会修改下面这些正则表达式的行为。

    (1)点字符

    Math.trunc() § 

    Math.trunc方法用于去除一个数的小数部分,返回整数部分。

    对于非数值,Math.trunc内部使用Number方法将其先转为数值。

    Math.trunc('123.456') // 123
    Math.trunc(true) //1
    Math.trunc(false) // 0
    Math.trunc(null) // 0

    对于空值和无法截取整数的值,返回NaN

    Math.trunc(NaN);      // NaN
    Math.trunc('foo');    // NaN
    Math.trunc();         // NaN
    Math.trunc(undefined) // NaN
    

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    Math.trunc = Math.trunc || function(x) {
      return x < 0 ? Math.ceil(x) : Math.floor(x);
    };

    Math.sign() § 

    Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

    它会返回五种值。

    • 参数为正数,返回+1
    • 参数为负数,返回-1
    • 参数为 0,返回0
    • 参数为-0,返回-0;
    • 其他值,返回NaN
    Math.sign(-5) // -1
    Math.sign(5) // +1
    Math.sign(0) // +0
    Math.sign(-0) // -0
    Math.sign(NaN) // NaN


    数组的扩展

    1. 扩展运算符提供了数组合并的新写法。

      const arr1 = ['a', 'b'];
      const arr2 = ['c'];
      const arr3 = ['d', 'e'];
      
      // ES5 的合并数组
      arr1.concat(arr2, arr3);
      // [ 'a', 'b', 'c', 'd', 'e' ]
      
      // ES6 的合并数组
      [...arr1, ...arr2, ...arr3]
      // [ 'a', 'b', 'c', 'd', 'e' ]
    2. 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

      const [...butLast, last] = [1, 2, 3, 4, 5];
      // 报错
      
      const [first, ...middle, last] = [1, 2, 3, 4, 5];
      // 报错
    3. 扩展运算符还可以将字符串转为真正的数组。

      [...'hello']
      // [ "h", "e", "l", "l", "o" ]
    4. (5)实现了 Iterator 接口的对象

      任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

      let nodeList = document.querySelectorAll('div');
      let array = [...nodeList];
    5. (5)实现了 Iterator 接口的对象

      任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

      let nodeList = document.querySelectorAll('div');
      let array = [...nodeList];
    6. 对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。

      let arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
      };
      
      // TypeError: Cannot spread non-iterable object.
      let arr = [...arrayLike];
      

      上面代码中,arrayLike是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。

    7. (6)Map 和 Set 结构,Generator 函数

      扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

      let map = new Map([
        [1, 'one'],
        [2, 'two'],
        [3, 'three'],
      ]);
      
      let arr = [...map.keys()]; // [1, 2, 3]
      

      Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

    8. Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

      const go = function*(){
        yield 1;
        yield 2;
        yield 3;
      };
      
      [...go()] // [1, 2, 3]
    9. 上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。

      如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。

      const obj = {a: 1, b: 2};
      let arr = [...obj]; // TypeError: Cannot spread non-iterable object
    10. Array.from() § 

      Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

      下面是一个类似数组的对象,Array.from将它转为真正的数组。

      let arrayLike = {
          '0': 'a',
          '1': 'b',
          '2': 'c',
          length: 3
      };
      
      // ES5的写法
      var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
      
      // ES6的写法
      let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
    11. 实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。
    12. // NodeList对象
      let ps = document.querySelectorAll('p');
      Array.from(ps).filter(p => {
        return p.textContent.length > 100;
      });
      
      // arguments对象
      function foo() {
        var args = Array.from(arguments);
        // ...
      }
      

      上面代码中,querySelectorAll方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter方法。

      只要是部署了 Iterator 接口的数据结构,Array.from都能将其转为数组。

    13. Array.from('hello')
      // ['h', 'e', 'l', 'l', 'o']
      
      let namesSet = new Set(['a', 'b'])
      Array.from(namesSet) // ['a', 'b']
      

      上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被Array.from转为真正的数组。

      如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

      Array.from([1, 2, 3])
      // [1, 2, 3]
      

      值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

    14. 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

      Array.from({ length: 3 });
      // [ undefined, undefined, undefined ]
      

      上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

    15. Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

      Array.from(arrayLike, x => x * x);
      // 等同于
      Array.from(arrayLike).map(x => x * x);
      
      Array.from([1, 2, 3], (x) => x * x)
      // [1, 4, 9]
    16. 下面的例子将数组中布尔值为false的成员转为0

      Array.from([1, , 2, , 3], (n) => n || 0)
      // [1, 0, 2, 0, 3]
    17. 另一个例子是返回各种数据的类型。

      function typesOf () {
        return Array.from(arguments, value => typeof value)
      }
      typesOf(null, [], NaN)
      // ['object', 'object', 'number']
    18. Array.of()

      Array.of方法用于将一组值,转换为数组。

      Array.of(3, 11, 8) // [3,11,8]
      Array.of(3) // [3]
      Array.of(3).length // 1
    19. 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

      Array() // []
      Array(3) // [, , ,]
      Array(3, 11, 8) // [3, 11, 8]
    20. Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

      Array.of() // []
      Array.of(undefined) // [undefined]
      Array.of(1) // [1]
      Array.of(1, 2) // [1, 2]
    21. Array.of方法可以用下面的代码模拟实现。

      function ArrayOf(){
        return [].slice.call(arguments);
      }

      数组实例的 copyWithin()

      数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

      Array.prototype.copyWithin(target, start = 0, end = this.length)
      

      它接受三个参数。

      • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
      • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
      • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。

      这三个参数都应该是数值,如果不是,会自动转为数值。

      [1, 2, 3, 4, 5].copyWithin(0, 3)
      // [4, 5, 3, 4, 5]
      

      上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。

    22. 数组实例的 find() 和 findIndex()

      数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

    23. [1, 4, -5, 10].find((n) => n < 0)
      // -5
      

      上面代码找出数组中第一个小于 0 的成员。

      [1, 5, 10, 15].find(function(value, index, arr) {
        return value > 9;
      })
    24. 这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

      function f(v){
        return v > this.age;
      }
      let person = {name: 'John', age: 20};
      [10, 12, 26, 15].find(f, person);    // 26
      

      上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。

    25. 另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。

      [NaN].indexOf(NaN)
      // -1
      
      [NaN].findIndex(y => Object.is(NaN, y))
      // 0
      

      上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

    26. 数组实例的 fill()

      fill方法使用给定值,填充一个数组。

      ['a', 'b', 'c'].fill(7)
      // [7, 7, 7]
      
      new Array(3).fill(7)
      // [7, 7, 7]
    27. fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

      ['a', 'b', 'c'].fill(7, 1, 2)
      // ['a', 7, 'c']
      

      上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。

    28. 注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

      let arr = new Array(3).fill({name: "Mike"});
      arr[0].name = "Ben";
      arr
      // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
      
      let arr = new Array(3).fill([]);
      arr[0].push(5);
      arr
      // [[5], [5], [5]]
    29. 数组实例的 entries(),keys() 和 values()

    for (let index of ['a', 'b'].keys()) {
      console.log(index);
    }
    // 0
    // 1
    
    for (let elem of ['a', 'b'].values()) {
      console.log(elem);
    }
    // 'a'
    // 'b'
    
    for (let [index, elem] of ['a', 'b'].entries()) {
      console.log(index, elem);
    }
    // 0 "a"
    // 1 "b"

    如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

    let letter = ['a', 'b', 'c'];
    let entries = letter.entries();
    console.log(entries.next().value); // [0, 'a']
    console.log(entries.next().value); // [1, 'b']
    console.log(entries.next().value); // [2, 'c']

    数组实例的 includes()

    [1, 2, 3].includes(2)     // true
    该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
    [1, 2, 3].includes(3, 3);  // false
    [1, 2, 3].includes(3, -1); // true

    没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

    if (arr.indexOf(el) !== -1) {
      // ...
    }
    

    indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

    [NaN].indexOf(NaN)
    // -1

    下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

    const contains = (() =>
      Array.prototype.includes
        ? (arr, value) => arr.includes(value)
        : (arr, value) => arr.some(el => el === value)
    )();
    contains(['foo', 'bar'], 'baz'); // => false
    • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
    • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
     

    数组实例的 flat(),flatMap()

    数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

    [1, 2, [3, 4]].flat()  // [1, 2, 3, 4]

    flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

    [1, 2, [3, [4, 5]]].flat()
    // [1, 2, 3, [4, 5]]
    
    [1, 2, [3, [4, 5]]].flat(2)
    // [1, 2, 3, 4, 5]

    如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

    [1, [2, [3]]].flat(Infinity)
    // [1, 2, 3]

    如果原数组有空位,flat()方法会跳过空位。

    [1, 2, , 4, 5].flat()
    // [1, 2, 4, 5]

    flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

    // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
    [2, 3, 4].flatMap((x) => [x, x * 2])
    // [2, 4, 3, 6, 4, 8]

    注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    0 in [undefined, undefined, undefined] // true
    0 in [, , ,] // false
    

    上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

    • forEach()filter()reduce()every() 和some()都会跳过空位。
    • map()会跳过空位,但会保留这个值
    • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
    • [1,undefined,3].join()
      "1,,3"
      [1,null,3].join()
      "1,,3"

    • // forEach方法
      [,'a'].forEach((x,i) => console.log(i)); // 1
      
      // filter方法
      ['a',,'b'].filter(x => true) // ['a','b']
      
      // every方法
      [,'a'].every(x => x==='a') // true
      
      // reduce方法
      [1,,2].reduce((x,y) => x+y) // 3
      
      // some方法
      [,'a'].some(x => x !== 'a') // false
      
      // map方法
      [,'a'].map(x => 1) // [,1]
      
      // join方法
      [,'a',undefined,null].join('#') // "#a##"
      
      // toString方法
      [,'a',undefined,null].toString() // ",a,,"
    • Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

      Array.from(['a',,'b'])
      // [ "a", undefined, "b" ]
      

      扩展运算符(...)也会将空位转为undefined

      [...['a',,'b']]
      // [ "a", undefined, "b" ]
    • for...of循环也会遍历空位。

      let arr = [, ,];
      for (let i of arr) {
        console.log(1);
      }
      // 1
      // 1
    • 对象的扩展

      1. Object.is('foo', 'foo')
        // true
        Object.is({}, {})
        // false
      2. +0 === -0 //true
        NaN === NaN // false
        
        Object.is(+0, -0) // false
        Object.is(NaN, NaN) // true
      3. ES5 可以通过下面的代码,部署Object.is

        Object.defineProperty(Object, 'is', {
          value: function(x, y) {
            if (x === y) {
              // 针对+0 不等于 -0的情况
              return x !== 0 || 1 / x === 1 / y;
            }
            // 针对NaN的情况
            return x !== x && y !== y;
          },
          configurable: true,
          enumerable: false,
          writable: true
        });
      4. Object.assign() § 

        const target = { a: 1 };
        
        const source1 = { b: 2 };
        const source2 = { c: 3 };
        
        Object.assign(target, source1, source2);
        target // {a:1, b:2, c:3}
      5. 如果该参数不是对象,则会先转成对象,然后返回。

        typeof Object.assign(2) // "object"
      6. 由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。

        Object.assign(undefined) // 报错
        Object.assign(null) // 报错
      7. 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefinednull不在首参数,就不会报错。

        let obj = {a: 1};
        Object.assign(obj, undefined) === obj // true
        Object.assign(obj, null) === obj // true
      8. 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

        const v1 = 'abc';
        const v2 = true;
        const v3 = 10;
        
        const obj = Object.assign({}, v1, v2, v3);
        console.log(obj); // { "0": "a", "1": "b", "2": "c" }
     

    Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

    Object.assign({b: 'c'},
      Object.defineProperty({}, 'invisible', {
        enumerable: false,
        value: 'hello'
      })
    )
    // { b: 'c' }

    Object.assign({b: 'c'},
        Object.defineProperty({}, 'invisible', {
           enumerable: true,
           value: 'hello'
       })
    )
    // {b: "c", invisible: "hello"}

    属性名为 Symbol 值的属性,也会被Object.assign拷贝。

    Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
    // { a: 'b', Symbol(c): 'd' }
    (1)浅拷贝
    const obj1 = {a: {b: 1}};
    const obj2 = Object.assign({}, obj1);
    
    obj1.a.b = 2;
    obj2.a.b // 2

    (2)同名属性的替换

    对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

    const target = { a: { b: 'c', d: 'e' } }
    const source = { a: { b: 'hello' } }
    Object.assign(target, source)
    // { a: { b: 'hello' } }

    (3)数组的处理

    Object.assign可以用来处理数组,但是会把数组视为对象。

    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]

    (4)取值函数的处理

    Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

    const source = {
      get foo() { return 1 }
    };
    const target = {};
    
    Object.assign(target, source)
    // { foo: 1 }

    常见用途

    (1)为对象添加属性

    class Point {
      constructor(x, y) {
        Object.assign(this, {x, y});
      }
    }
    

    上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例

    (2)为对象添加方法

    Object.assign(SomeClass.prototype, {
      someMethod(arg1, arg2) {
        ···
      },
      anotherMethod() {
        ···
      }
    });
    
    // 等同于下面的写法
    SomeClass.prototype.someMethod = function (arg1, arg2) {
      ···
    };
    SomeClass.prototype.anotherMethod = function () {
      ···
    };

    (3)克隆对象

    function clone(origin) {
      return Object.assign({}, origin);
    }
    

    上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

    不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

    function clone(origin) {
      let originProto = Object.getPrototypeOf(origin);
      return Object.assign(Object.create(originProto), origin);
    }

    (4)合并多个对象

    将多个对象合并到某个对象。

    const merge =
      (target, ...sources) => Object.assign(target, ...sources);
    

    如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

    const merge =
      (...sources) => Object.assign({}, ...sources);

    可枚举性

    对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, 'foo')
    //  {
    //    value: 123,
    //    writable: true,
    //    enumerable: true,
    //    configurable: true
    //  }

    描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。

    目前,有四个操作会忽略enumerablefalse的属性。

    • for...in循环:只遍历对象自身的和继承的可枚举的属性。
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

    这四个操作之中,前三个是 ES5 就有的,最后一个Object.assign()是 ES6 新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

    Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
    // false
    
    Object.getOwnPropertyDescriptor([], 'length').enumerable
    // false
    

    上面代码中,toStringlength属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。

    总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

    属性的遍历 § 

    ES6 一共有 5 种方法可以遍历对象的属性。

    (1)for...in

    for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

    (2)Object.keys(obj)

    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

    (3)Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

    (4)Object.getOwnPropertySymbols(obj)

    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

    (5)Reflect.ownKeys(obj)

    Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

    以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    • 首先遍历所有数值键,按照数值升序排列。
    • 其次遍历所有字符串键,按照加入时间升序排列。
    • 最后遍历所有 Symbol 键,按照加入时间升序排列。
    Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
    // ['2', '10', 'b', 'a', Symbol()]
    

    上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

    Object.getOwnPropertyDescriptors() § 

    前面说过,Object.getOwnPropertyDescriptor方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。

    const obj = {
      foo: 123,
      get bar() { return 'abc' }
    };
    
    Object.getOwnPropertyDescriptors(obj)
    // { foo:
    //    { value: 123,
    //      writable: true,
    //      enumerable: true,
    //      configurable: true },
    //   bar:
    //    { get: [Function: get bar],
    //      set: undefined,
    //      enumerable: true,
    //      configurable: true } }
    

    上面代码中,Object.getOwnPropertyDescriptors方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

    该方法的实现非常容易。

    function getOwnPropertyDescriptors(obj) {
      const result = {};
      for (let key of Reflect.ownKeys(obj)) {
        result[key] = Object.getOwnPropertyDescriptor(obj, key);
      }
      return result;
    }
    

    该方法的引入目的,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题

    const source = {
      set foo(value) {
        console.log(value);
      }
    };
    
    const target1 = {};
    Object.assign(target1, source);
    
    Object.getOwnPropertyDescriptor(target1, 'foo')
    // { value: undefined,
    //   writable: true,
    //   enumerable: true,
    //   configurable: true }
    

    上面代码中,source对象的foo属性的值是一个赋值函数,Object.assign方法将这个属性拷贝给target1对象,结果该属性的值变成了undefined。这是因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

    这时,Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法,就可以实现正确拷贝。

    const source = {
      set foo(value) {
        console.log(value);
      }
    };
    
    const target2 = {};
    Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
    Object.getOwnPropertyDescriptor(target2, 'foo')
    // { get: undefined,
    //   set: [Function: set foo],
    //   enumerable: true,
    //   configurable: true }
    

    上面代码中,两个对象合并的逻辑可以写成一个函数。

    const shallowMerge = (target, source) => Object.defineProperties(
      target,
      Object.getOwnPropertyDescriptors(source)
    );
    

    Object.getOwnPropertyDescriptors方法的另一个用处,是配合Object.create方法,将对象属性克隆到一个新对象。这属于浅拷贝。

    const clone = Object.create(Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj));
    
    // 或者
    
    const shallowClone = (obj) => Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj)
    );
    

    上面代码会克隆对象obj

    另外,Object.getOwnPropertyDescriptors方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。

    const obj = {
      __proto__: prot,
      foo: 123,
    };
    

    ES6 规定__proto__只有浏览器要部署,其他环境不用部署。如果去除__proto__,上面代码就要改成下面这样。

    const obj = Object.create(prot);
    obj.foo = 123;
    
    // 或者
    
    const obj = Object.assign(
      Object.create(prot),
      {
        foo: 123,
      }
    );
    

    有了Object.getOwnPropertyDescriptors,我们就有了另一种写法。

    Object.getOwnPropertyDescriptors也可以用来实现 Mixin(混入)模式。

    let mix = (object) => ({
      with: (...mixins) => mixins.reduce(
        (c, mixin) => Object.create(
          c, Object.getOwnPropertyDescriptors(mixin)
        ), object)
    });
    
    // multiple mixins example
    let a = {a: 'a'};
    let b = {b: 'b'};
    let c = {c: 'c'};
    let d = mix(c).with(a, b);
    
    d.c // "c"
    d.b // "b"
    d.a // "a"
    

    上面代码返回一个新的对象d,代表了对象ab被混入了对象c的操作。

    __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf() 

    // es5 的写法
    const obj = {
      method: function() { ... }
    };
    obj.__proto__ = someOtherObj;
    
    // es6 的写法
    var obj = Object.create(someOtherObj);
    obj.method = function() { ... };
    该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,
    而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。



    Object.create也可以实现继承,但不是完美继承,如下:

    function Abc(){console.log(1111)}
    var bace = new Abc()
    var bacea = Object.create(bace)
    Abc.prototype.name = [1,2,3]
    bace.name.push(9)

    bacea.name    // [1, 2, 3, 4, 9]     

    bace.name   // [1, 2, 3, 4, 9]

    var bacea4 = Object.create(bace)
    bacea4.name //[1, 2, 3, 4, 9]
    bacea4.name.push(0)
    bacea4.name// [1, 2, 3, 4, 9, 0]
    bacea.name // [1, 2, 3, 4, 9, 0]

    实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。

    Object.defineProperty(Object.prototype, '__proto__', {
      get() {
        let _thisObj = Object(this);
        return Object.getPrototypeOf(_thisObj);
      },
      set(proto) {
        if (this === undefined || this === null) {
          throw new TypeError();
        }
        if (!isObject(this)) {
          return undefined;
        }
        if (!isObject(proto)) {
          return undefined;
        }
        let status = Reflect.setPrototypeOf(this, proto);
        if (!status) {
          throw new TypeError();
        }
      },
    });
    
    function isObject(value) {
      return Object(value) === value;
    }

    如果一个对象本身部署了__proto__属性,该属性的值就是对象的原型。

    Object.getPrototypeOf({ __proto__: null })
    // null

    如果一个对象本身部署了__proto__属性,该属性的值就是对象的原型。

    Object.getPrototypeOf({ __proto__: null })
    // null

    Object.setPrototypeOf() § 

    Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

    // 格式
    Object.setPrototypeOf(object, prototype)
    
    // 用法
    const o = Object.setPrototypeOf({}, null);
    

    该方法等同于下面的函数。

    function (obj, proto) {
      obj.__proto__ = proto;
      return obj;
    }

    下面是一个例子。

    let proto = {};
    let obj = { x: 10 };
    Object.setPrototypeOf(obj, proto);
    
    proto.y = 20;
    proto.z = 40;
    
    obj.x // 10
    obj.y // 20
    obj.z // 40

    上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

    如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。

    Object.setPrototypeOf(1, {}) === 1 // true
    Object.setPrototypeOf('foo', {}) === 'foo' // true
    Object.setPrototypeOf(true, {}) === true // true
    

    由于undefinednull无法转为对象,所以如果第一个参数是undefinednull,就会报错。

    Object.getPrototypeOf()

    该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

    Object.getPrototypeOf(obj);
    

    下面是一个例子。

    function Rectangle() {
      // ...
    }
    
    const rec = new Rectangle();
    
    Object.getPrototypeOf(rec) === Rectangle.prototype
    // true
    
    Object.setPrototypeOf(rec, Object.prototype);
    Object.getPrototypeOf(rec) === Rectangle.prototype
    // false
    

    如果参数不是对象,会被自动转为对象。

    // 等同于 Object.getPrototypeOf(Number(1))
    Object.getPrototypeOf(1)
    // Number {[[PrimitiveValue]]: 0}
    
    // 等同于 Object.getPrototypeOf(String('foo'))
    Object.getPrototypeOf('foo')
    // String {length: 0, [[PrimitiveValue]]: ""}
    
    // 等同于 Object.getPrototypeOf(Boolean(true))
    Object.getPrototypeOf(true)
    // Boolean {[[PrimitiveValue]]: false}
    
    Object.getPrototypeOf(1) === Number.prototype // true
    Object.getPrototypeOf('foo') === String.prototype // true
    Object.getPrototypeOf(true) === Boolean.prototype // true

    如果参数是undefinednull,它们无法转为对象,所以会报错。

    Object.getPrototypeOf(null)
    // TypeError: Cannot convert undefined or null to object
    
    Object.getPrototypeOf(undefined)
    // TypeError: Cannot convert undefined or null to object

    super 关键字

    我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

    const proto = {
      foo: 'hello'
    };
    
    const obj = {
      foo: 'world',
      find() {
        return super.foo;
      }
    };
    
    Object.setPrototypeOf(obj, proto);
    obj.find() // "hello"
    

    上面代码中,对象objfind方法之中,通过super.foo引用了原型对象protofoo属性。

    const veee = {foo:'world',find(){return super.foo}}
    veee.find()  //undefined   因为super指向当前对象的原形对象就是prototype

    注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

    // 报错
    const obj = {
      foo: super.foo
    }
    
    // 报错
    const obj = {
      foo: () => super.foo
    }
    
    // 报错
    const obj = {
      foo: function () {
        return super.foo
      }
    }
    

    上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

    JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

    const proto = {
      x: 'hello',
      foo() {
        console.log(this.x);
      },
    };
    
    const obj = {
      x: 'world',
      foo() {
        super.foo();
      }
    }
    
    Object.setPrototypeOf(obj, proto);
    
    obj.foo() // "world"
    

    上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

    上面的貌似只是函数才行,如果super

    let {keys, values, entries} = Object;
    let obj = { a: 1, b: 2, c: 3 };
    
    for (let key of keys(obj)) {
      console.log(key); // 'a', 'b', 'c'
    }
    
    for (let value of values(obj)) {
      console.log(value); // 1, 2, 3
    }
    
    for (let [key, value] of entries(obj)) {
      console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
    }

    Object.values只返回对象自身的可遍历属性。

    const obj = Object.create({}, {p: {value: 42}});
    Object.values(obj) // []
    

    上面代码中,Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的,因为p的属性描述对象的enumerable默认是falseObject.values不会返回这个属性。只要把enumerable改成trueObject.values就会返回属性p的值。

     

    自己实现Object.entries方法,非常简单。

    // Generator函数的版本
    function* entries(obj) {
      for (let key of Object.keys(obj)) {
        yield [key, obj[key]];
      }
    }
    
    // 非Generator函数的版本
    function entries(obj) {
      let arr = [];
      for (let key of Object.keys(obj)) {
        arr.push([key, obj[key]]);
      }
      return arr;
    }

    解构赋值 § 

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }

    let { x, y, ...z } = null; // 运行时错误
    let { x, y, ...z } = undefined; // 运行时错误
    

    解构赋值必须是最后一个参数,否则会报错。

    let { ...x, y, z } = obj; // 句法错误
    let { x, ...y, ...z } = obj; // 句法错误

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

    另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1;
    let { ...o3 } = o2;
    o3 // { b: 2 }
    o3.a // undefined

    下面是另一个例子。

    const o = Object.create({ x: 1, y: 2 });//{}__proto__: x: 1y: 由create复制的是它原型链上的
    o.z = 3;
    
    let { x, ...newObj } = o;
    let { y, z } = newObj;
    x // 1
    y // undefined
    z // 3
    

    上面代码中,变量x是单纯的解构赋值,所以可以读取对象o继承的属性;变量yz是扩展运算符的解构赋值,只能读取对象o自身的属性,所以变量z可以赋值成功,变量y取不到值

    let aClone = { ...a };
    // 等同于
    let aClone = Object.assign({}, a);
    

    上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。

    // 写法一
    const clone1 = {
      __proto__: Object.getPrototypeOf(obj),
      ...obj
    };
    
    // 写法二
    const clone2 = Object.assign(
      Object.create(Object.getPrototypeOf(obj)),
      obj
    );
    
    // 写法三
    const clone3 = Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj)
    )
    

    上面代码中,写法一的__proto__属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三。

    如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

    let aWithOverrides = { ...a, x: 1, y: 2 };
    // 等同于
    let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
    // 等同于
    let x = 1, y = 2, aWithOverrides = { ...a, x, y };
    // 等同于
    let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });

    扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

    // 并不会抛出错误,因为 x 属性只是被定义,但没执行
    let aWithXGetter = {
      ...a,
      get x() {
        throw new Error('not throw yet');
      }
    };
    
    // 会抛出错误,因为 x 属性被执行了
    let runtimeError = {
      ...a,
      ...{
        get x() {
          throw new Error('throw now');
        }
      }
    };

    Class 的基本语法

     

    另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

    class Point {
      constructor(x, y) {
        // ...
      }
    
      toString() {
        // ...
      }
    }
    
    Object.keys(Point.prototype)
    // []
    Object.getOwnPropertyNames(Point.prototype)
    // ["constructor","toString"]

    上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。

    类的属性名,可以采用表达式。

    let methodName = 'getArea';
    
    class Square {
      constructor(length) {
        // ...
      }
    
      [methodName]() {
        // ...
      }
    }

    严格模式

    类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

    考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

    class Point {
      // ...
    }
    
    // 报错
    var point = Point(2, 3);
    
    // 正确
    var point = new Point(2, 3);

    //定义类
    class Point {
    
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    
    }
    
    var point = new Point(2, 3);
    
    point.toString() // (2, 3)
    
    point.hasOwnProperty('x') // true
    point.hasOwnProperty('y') // true
    point.hasOwnProperty('toString') // false
    point.__proto__.hasOwnProperty('toString') // true
    

    上面代码中,xy都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与 ES5 的行为保持一致。

    var p1 = new Point(2,3);
    var p2 = new Point(3,2);
    
    p1.__proto__ === p2.__proto__
    //true

    __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

    var p1 = new Point(2,3);
    var p2 = new Point(3,2);
    
    p1.__proto__.printName = function () { return 'Oops' };
    
    p1.printName() // "Oops"
    p2.printName() // "Oops"
    
    var p3 = new Point(4,2);
    p3.printName() // "Oops"
    

    上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。

    与函数一样,类也可以使用表达式的形式定义。

    const MyClass = class Me {
      getClassName() {
        return Me.name;
      }
    };

    let person = new class {
      constructor(name) {
        this.name = name;
      }
    
      sayName() {
        console.log(this.name);
      }
    }('张三');
    
    person.sayName(); // "张三"
    

    上面代码中,person是一个立即执行的类的实例。

    不存在变量提升

    类不存在变量提升(hoist),这一点与 ES5 完全不同。

    new Foo(); // ReferenceError
    class Foo {}
    

    上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

    {
      let Foo = class {};
      class Bar extends Foo {
      }
    }
    

    上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

    私有方法和私有属性

    现有的方法

    私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。

    一种做法是在命名上加以区别。

    class Widget {
    
      // 公有方法
      foo (baz) {
        this._bar(baz);
      }
    
      // 私有方法
      _bar(baz) {
        return this.snaf = baz;
      }
    
      // ...
    }
    

    上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

    另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

    class Widget {
      foo (baz) {
        bar.call(this, baz);
      }
    
      // ...
    }
    
    function bar(baz) {
      return this.snaf = baz;
    }
     上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

    私有属性的提案

    目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

    class Point {
      #x;
    
      constructor(x = 0) {
        #x = +x; // 写成 this.#x 亦可
      }
    
      get x() { return #x }
      set x(value) { #x = +value }
    }
    

    上面代码中,#x就是私有属性,在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#xx是两个不同的属性。

    私有属性可以指定初始值,在构造函数执行时进行初始化。

    class Point {
      #x = 0;
      constructor() {
        #x; // 0
      }
    }

    之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator。

    这种写法不仅可以写私有属性,还可以用来写私有方法。

    class Foo {
      #a;
      #b;
      #sum() { return #a + #b; }
      printSum() { console.log(#sum()); }
      constructor(a, b) { #a = a; #b = b; }
    }
    
    上面代码中,#sum()就是一个私有方法。

    另外,私有属性也可以设置 getter 和 setter 方法。

    class Counter {
      #xValue = 0;
    
      get #x() { return #xValue; }
      set #x(value) {
        this.#xValue = value;
      }
    
      constructor() {
        super();
        // ...
      }
    }
    

    上面代码中,#x是一个私有属性,它的读写都通过get #x()set #x()来完成。

    私有属性不限于从this引用,类的实例也可以引用私有属性。

    class Foo {
      #privateValue = 42;
      static getPrivateValue(foo) {
        return foo.#privateValue;
      }
    }
    
    Foo.getPrivateValue(new Foo()); // 42
    

    上面代码允许从实例foo上面引用私有属性。

    但是,直接从实例上引用私有属性是不可以的,只能在类的定义中引用。

    class Foo {
      #bar;
    }
    let foo = new Foo();
    foo.#bar; // 报错
    

     

    上面代码直接从实例引用私有属性,导致报错。

    this 的指向

    类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

    class Logger {
      printName(name = 'there') {
        this.print(`Hello ${name}`);
      }
    
      print(text) {
        console.log(text);
      }
    }
    
    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined
    

    上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

    一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

    class Logger {
      constructor() {
        this.printName = this.printName.bind(this);
      }
    
      // ...
    }
    

    另一种解决方法是使用箭头函数。

    class Logger {
      constructor() {
        this.printName = (name = 'there') => {
          this.print(`Hello ${name}`);
        };
      }
    
      // ...
    }
    

    还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this

    function selfish (target) {
      const cache = new WeakMap();
      const handler = {
        get (target, key) {
          const value = Reflect.get(target, key);
          if (typeof value !== 'function') {
            return value;
          }
          if (!cache.has(value)) {
            cache.set(value, value.bind(target));
          }
          return cache.get(value);
        }
      };
      const proxy = new Proxy(target, handler);
      return proxy;
    }
    
    const logger = selfish(new Logger());
    

    name 属性

    由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。

    class Point {}
    Point.name // "Point"
    

    name属性总是返回紧跟在class关键字后面的类名。

     

    Class 的取值函数(getter)和存值函数(setter) § 

    与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

    class MyClass {
      constructor() {
        // ...
      }
      get prop() {
        return 'getter';
      }
      set prop(value) {
        console.log('setter: '+value);
      }
    }
    
    let inst = new MyClass();
    
    inst.prop = 123;
    // setter: 123
    
    inst.prop
    // 'getter'
     存值函数和取值函数是设置在属性的 Descriptor 对象上的。
    class CustomHTMLElement {
      constructor(element) {
        this.element = element;
      }
    
      get html() {
        return this.element.innerHTML;
      }
    
      set html(value) {
        this.element.innerHTML = value;
      }
    }
    
    var descriptor = Object.getOwnPropertyDescriptor(
      CustomHTMLElement.prototype, "html"
    );
    
    "get" in descriptor  // true
    "set" in descriptor  // true
    

    上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。

    Class 的 Generator 方法

    如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

    class Foo {
      constructor(...args) {
        this.args = args;
      }
      * [Symbol.iterator]() {
        for (let arg of this.args) {
          yield arg;
        }
      }
    }
    
    for (let x of new Foo('hello', 'world')) {
      console.log(x);
    }
    // hello
    // world
    

    上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。

    Class 的静态方法

    类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    Foo.classMethod() // 'hello'
    
    var foo = new Foo();
    foo.classMethod()
    // TypeError: foo.classMethod is not a function
    

    上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

    注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

    class Foo {
      static bar () {
        this.baz();
      }
      static baz () {
        console.log('hello');
      }
      baz () {
        console.log('world');
      }
    }
    
    Foo.bar() // hello
    

    上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

    父类的静态方法,可以被子类继承。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    class Bar extends Foo {
    }
    
    Bar.classMethod() // 'hello'
    

    上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

    静态方法也是可以从super对象上调用的。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }
    
    Bar.classMethod() // "hello, too"
     
     目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。
    // 以下两种写法都无效
    class Foo {
      // 写法一
      prop: 2
    
      // 写法二
      static prop: 2
    }
    
    Foo.prop // undefined
    

    目前有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。

    (1)类的实例属性

    类的实例属性可以用等式,写入类的定义之中。

    class MyClass {
      myProp = 42;
    
      constructor() {
        console.log(this.myProp); // 42
      }
    }
    

    上面代码中,myProp就是MyClass的实例属性。在MyClass的实例上,可以读取这个属性。

    以前,我们定义实例属性,只能写在类的constructor方法里面。

    class ReactCounter extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    }
    

    上面代码中,构造方法constructor里面,定义了this.state属性。

    有了新的写法以后,可以不在constructor方法里面定义。

    class ReactCounter extends React.Component {
      state = {
        count: 0
      };
    }
    

    这种写法比以前更清晰。

    为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。

    class ReactCounter extends React.Component {
      state;
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    }

     (2)类的静态属性

    类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。

    class MyClass {
      static myStaticProp = 42;
    
      constructor() {
        console.log(MyClass.myStaticProp); // 42
      }
    }
    

    同样的,这个新写法大大方便了静态属性的表达。

    // 老写法
    class Foo {
      // ...
    }
    Foo.prop = 1;
    
    // 新写法
    class Foo {
      static prop = 1;
    }
    

    上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好。

    new.target 属性

    new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

    function Person(name) {
      if (new.target !== undefined) {
        this.name = name;
      } else {
        throw new Error('必须使用 new 命令生成实例');
      }
    }
    
    // 另一种写法
    function Person(name) {
      if (new.target === Person) {
        this.name = name;
      } else {
        throw new Error('必须使用 new 命令生成实例');
      }
    }
    
    var person = new Person('张三'); // 正确
    var notAPerson = Person.call(person, '张三');  // 报错

    需要注意的是,子类继承父类时,new.target会返回子类。

    class Rectangle {
      constructor(length, width) {
        console.log(new.target === Rectangle);
        // ...
      }
    }
    
    class Square extends Rectangle {
      constructor(length) {
        super(length, length);
      }
    }
    
    
    var obj =newSquare(3); // 输出 false 

    利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

    class Shape {
      constructor() {
        if (new.target === Shape) {
          throw new Error('本类不能实例化');
        }
      }
    }
    
    class Rectangle extends Shape {
      constructor(length, width) {
        super();
        // ...
      }
    }
    
    var x = new Shape();  // 报错
    var y = new Rectangle(3, 4);  // 正确
    

    上面代码中,Shape类不能被实例化,只能用于继承。

    注意,在函数外部,使用new.target会报错。 

    Class 的继承

     class ColorPoint extends Point{
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
    
      toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
      }
    }
    

    上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

    子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

    ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

    如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

    class ColorPoint extends Point {
    }
    
    // 等同于
    class ColorPoint extends Point {
      constructor(...args) {
        super(...args);
      }

    另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    class ColorPoint extends Point {
      constructor(x, y, color) {
        this.color = color; // ReferenceError
        super(x, y);
        this.color = color; // 正确
      }
    }
    
    上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。 

    下面是生成子类实例的代码。

    let cp = new ColorPoint(25, 8, 'green');
    
    cp instanceof ColorPoint // true
    cp instanceof Point // true
    
    上面代码中,实例对象cp同时是ColorPointPoint两个类的实例,这与 ES5 的行为完全一致。 
     最后,父类的静态方法,也会被子类继承。
    class A {
      static hello() {
        console.log('hello world');
      }
    }
    
    class B extends A {
    }
    
    B.hello()  // hello world
    

    上面代码中,hello()A类的静态方法,B继承A,也继承了A的静态方法。

    Object.getPrototypeOf()

    Object.getPrototypeOf方法可以用来从子类上获取父类。

    Object.getPrototypeOf(ColorPoint) === Point
    // true
    
    因此,可以使用这个方法判断,一个类是否继承了另一个类。 
     

    super 关键字

    super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

    第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

    class A {}
    
    class B extends A {
      constructor() {
        super();
      }
    }
    

    上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

    注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)

    class A {
      constructor() {
        console.log(new.target.name);
      }
    }
    class B extends A {
      constructor() {
        super();
      }
    }
    new A() // A
    new B() // B
    

    上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B

    作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

    class A {}
    
    class B extends A {
      m() {
        super(); // 报错
      }
    }
    

    上面代码中,super()用在B类的m方法之中,就会造成句法错误。

    第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    class A {
      p() {
        return 2;
      }
    }
    
    class B extends A {
      constructor() {
        super();
        console.log(super.p()); // 2
      }
    }
    
    let b = new B();
     上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

    这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

    class A {
      constructor() {
        this.p = 2;
      }
    }
    
    class B extends A {
      get m() {
        return super.p;
      }
    }
    
    let b = new B();
    b.m // undefined
    

    上面代码中,p是父类A实例的属性,super.p就引用不到它。

    如果属性定义在父类的原型对象上,super就可以取到。

    class A {}
    A.prototype.x = 2;
    
    class B extends A {
      constructor() {
        super();
        console.log(super.x) // 2
      }
    }
    
    let b = new B();
    

    上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

     

    ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

    class A {
      constructor() {
        this.x = 1;
      }
      print() {
        console.log(this.x);
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      m() {
        super.print();
      }
    }
    
    let b = new B();
    b.m() // 2
     上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)
     由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
    class A {
      constructor() {
        this.x = 1;
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
        super.x = 3;
        console.log(super.x); // undefined
        console.log(this.x); // 3
      }
    }
    
    let b = new B();
    

    上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

     如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
    class Parent {
      static myMethod(msg) {
        console.log('static', msg);
      }
    
      myMethod(msg) {
        console.log('instance', msg);
      }
    }
    
    class Child extends Parent {
      static myMethod(msg) {
        super.myMethod(msg);
      }
    
      myMethod(msg) {
        super.myMethod(msg);
      }
    }
    
    Child.myMethod(1); // static 1
    
    var child = new Child();
    child.myMethod(2); // instance 2
    

    上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

     另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
    class A {
      constructor() {
        this.x = 1;
      }
      static print() {
        console.log(this.x);
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      static m() {
        super.print();
      }
    }
    
    B.x = 3;
    B.m() // 3
    

    上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

     
     
     
     
     
     

     

     

    不存在变量提升

    类不存在变量提升(hoist),这一点与 ES5 完全不同。

    new Foo(); // ReferenceError
    class Foo {}
    

    上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

    {
      let Foo = class {};
      class Bar extends Foo {
      }
    }
    

    上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

  • 相关阅读:
    【转】VS2013编译libjpeg库
    玩转百度地图(二)之画圆,高德地图、搜搜地图、搜狗地图等稍微修改即可
    JAVA自动生成正则表达式工具类
    S2SH商用后台权限系统第三讲
    自定义表单验证指令
    关于input/textarea提交内容空格回车转换问题,以及ng-model去除空格问题
    angular ui-router 缓存问题
    ionic 发送请求返回一直都是404
    ionic中获取坐标方法
    ionic的scroll的使用出现的问题
  • 原文地址:https://www.cnblogs.com/chaoyuehedy/p/9265010.html
Copyright © 2020-2023  润新知