ES6-review

ES6知识点温习

Posted by Mickey on February 5, 2018

一年前,系统学习过一次ES6语法,随着时间的推移,记忆有些模糊,加之一年的工作产出了一些沉淀,因此,准备温习一遍,记录一下新的体会,参考书籍为阮一峰老师的ECMAScript 6 入门,感谢🙏

  • 在ES5中,var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined,为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错

      // var 的情况
      console.log(foo); // 输出undefined
      var foo = 2;
    	
      // let 的情况
      console.log(bar); // 报错ReferenceError
      let bar = 2;
    
  • const命令只是限制变量对应的地址不可改变,但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了,如果真的想将对象冻结,应该使用Object.freeze方法

      const foo = {};
    
      // 为 foo 添加一个属性,可以成功
      foo.prop = 123;
      foo.prop // 123
    	
      // 将 foo 指向另一个对象,就会报错
      foo = {}; // TypeError: "foo" is read-only
    	
      var constantize = (obj) => {
        Object.freeze(obj);
        Object.keys(obj).forEach( (key, i) => {
          if ( typeof obj[key] === 'object' ) {
            constantize( obj[key] );
          }
        });
      };
    
  • 解构赋值的默认值,只有在原本赋值不存在或者===undefined的情况下,才会使用默认赋值,例如,原本赋值为null,解构赋值也不会采用默认值

      let [x, y = 'b'] = ['a']; // x = 'a', y = 'b'
      let [x, y = 'b'] = ['a', undefined]; // x = 'a', y = 'b'
      let [x, y = 'b'] = ['a', null]; // x = 'a', y = null
    
  • js中,可以用’\u0061’表示Unicode码形式,但是,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示,ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符

      "\u0061" // "a"
      "\uD842\uDFB7" // "𠮷"
      "\u20BB7" // " 7"
      "\u{20BB7}" // "𠮷"
    
  • ES5中提供charCodeAt和String.fromCodeAt来进行码点和对应字符的转换,但是这两个api不能识别 32 位的 UTF-16 字符,ES6新增了codePointAt和String.fromPointAt两个API,需要注意的是,两个API分别是挂在字符串对象和String上的。codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for…of循环,因为它会正确识别 32 位的 UTF-16 字符

      var s = "𠮷a";
      s.charCodeAt(0) // 55362
      s.charCodeAt(1) // 57271
      s.codePointAt(0) // 134071
      s.codePointAt(1) // 57271
      s.codePointAt(2) // 97
      String.fromCharCode(0x20BB7) // "ஷ"
      String.fromCodePoint(0x20BB7) // "𠮷"
      let s = '𠮷a';
      for (let ch of s) {
        console.log(ch.codePointAt(0).toString(16));
      }
      // 20bb7
      // 61
    
  • ES6字符串提供一个repeat()方法,将原字符串重复n次

      'x'.repeat(3) // "xxx"
      'hello'.repeat(2) // "hellohello"
      'na'.repeat(0) // ""
      'na'.repeat(2.9) // "nana"
    
  • ES2017引入了字符串补全长度的功能,我个人觉得这个功能非常有用,如果某个字符串不够指定长度,会在头部或尾部补全,padStart()用于头部补全,padEnd()用于尾部补全

      'x'.padStart(5, 'ab') // 'ababx'
      'x'.padStart(4, 'ab') // 'abax'
    	
      'x'.padEnd(5, 'ab') // 'xabab'
      'x'.padEnd(4, 'ab') // 'xaba'
    
  • ES6新增了识别大于\uFFFF字符的方法,对应的,在正则表达式中新增了一个u的flags,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符,也就是说,会正确处理四个字节的 UTF-16 编码

      /^\uD83D/u.test('\uD83D\uDC2A') // false
      /^\uD83D/.test('\uD83D\uDC2A') // true
    
  • ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。 y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义

      var s = 'aaa_aa_a';
      var r1 = /a+/g;
      var r2 = /a+/y;
    	
      r1.exec(s) // ["aaa"]
      r2.exec(s) // ["aaa"]
    	
      r1.exec(s) // ["aa"]
      r2.exec(s) // null
    
  • 正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character),ES2018 引入s修饰符,使得.可以匹配任意单个字符

  • 正则表达式具名组匹配,在ES5中,正则表达式使用圆括号进行组匹配,组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号,ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用,如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法,类似于\1的写法,这两种写法可以兼容一起使用

      const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
    
      const matchObj = RE_DATE.exec('1999-12-31');
      const year = matchObj[1]; // 1999
      const month = matchObj[2]; // 12
      const day = matchObj[3]; // 31
    	
      const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
    
      const matchObj = RE_DATE.exec('1999-12-31');
      const year = matchObj.groups.year; // 1999
      const month = matchObj.groups.month; // 12
      const day = matchObj.groups.day; // 31
    
      const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
      RE_TWICE.test('abc!abc!abc') // true
      RE_TWICE.test('abc!abc!ab') // false
    
  • ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示,如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法

      0b111110111 === 503 // true
      0o767 === 503 // true
      Number('0b111')  // 7
      Number('0o10')  // 8
    
  • ES6在Number上挂载了一些全局方法,区别如下

    • Number.isFinite() vs isFinite()

      isFinite()会先调用Number(),再判断,Number.isFinite()直接进行判断

        isFinite('1') // true
        Number.isFinite('1') // false
      
    • Number.isNaN() vs isNaN()

      和isFinite()一样,isNaN()会先调用Number(),Number.isNaN()直接进行判断

        isNaN('NaN') // true
        Number.isNaN('NaN') //  false
      
    • Number.parseInt() 和 Number.parseFloat()和全局方法完全一样
    • Number.isInteger() 判断是否为整数,数字大于Number.MAX_SAFE_INTEGER或者数字小于Number.MIN_VALUE,会出现精度缺失,由于js中整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值

        Number.isInteger(25) // true
        Number.isInteger(25.0) // true
        Number.isInteger() // false
        Number.isInteger(null) // false
        Number.isInteger('15') // false
        Number.isInteger(true) // false
      
    • Number.EPSILON 可以认为是js的最小精度
    • Number.MAX_SAFE_INTEGER/Number.MIN_SAFE_INGETER代表js的精度范围内的最大整数和最小整数,js的安全范围是-2^53到2^53,不包含两个端点
    • Number.isSafeInteger() 数字是否存在于js的整数安全范围内
  • ES5中,如果要给函数参数设置默认值,一般是 param   ’’ 的方式,这样当传入bool值为false的参数时,都会采用默认值,因此需要先进行undefined的判断,不是非常方便,ES6中可以直接在参数上写默认值,毕竟方便
  • ES6函数默认值与解构赋值结合使用,避免出错

      function foo({x, y = 5}) {
        console.log(x, y);
      }
      foo() // TypeError: Cannot read property 'x' of undefined
    	
      function foo({x, y = 5} = {}) {
        console.log(x, y);
      }
    	
      foo() // undefined 5
    
  • ES6中为函数的非尾部参数设定默认值的时候,若要使用默认值,需要显示的传入undefined,不然会报错

      function f(x = 1, y) {
        return [x, y];
      }
    	
      f(, 1) // 报错
      f(undefined, 1) // [1, 1]
    
  • 尾调用

    简单来说,尾调用就是一个函数的最后一步是调用另一个函数,下方对函数m和n的调用都属于尾调用

      function f(x) {
        if (x > 0) {
          return m(x)
        }
        return n(x);
      }
    
  • 尾调用优化

    尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

    我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

    尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

      function f() {
        let m = 1;
        let n = 2;
        return g(m + n);
      }
      f();
    	
      // 等同于
      function f() {
        return g(3);
      }
      f();
    	
      // 等同于
      g(3);
    

    上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧

  • ES6中规定,函数循环调用自己,必须写成尾循环形式,这样只需要保存一个调用帧,否则需要保存非常多的调用帧,容易发生stack overflow尾递归优化

  • ES2017允许函数的最后一个参数后面加上逗号

  • 和上面一样,ES6增加了对4字节unicode的支持,但是ES5中,字符串length和reverse()都不能正确识别4字节unicode,ES6可以通过…运算符将字符串转为数组来正确识别

      'x\uD83D\uDE80y'.length // 4
      [...'x\uD83D\uDE80y'].length // 3 
    
      let str = 'x\uD83D\uDE80y';
    
      str.split('').reverse().join('')
      // 'y\uDE80\uD83Dx'
    
      [...str].reverse().join('')
      // 'y\uD83D\uDE80x'
    
  • ES6中新增了Array.from()方法,用于将类数组转为数组,第二个参数类似map方法,操作数组中的元素并且返回,这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法

      let spans = document.querySelectorAll('span.name');
    
      // map()
      let names1 = Array.prototype.map.call(spans, s => s.textContent);
    
      // Array.from()
      let names2 = Array.from(spans, s => s.textContent)
    
      Array.from({ length: 2 }, () => 'jack')
      // ['jack', 'jack']
    
  • ES6中新增了find()和findIndex()方法,均接受一个函数,返回第一个为true的数组成员和成员index,ES5中的indexOf()方法不能识别NaN,但是findIndex可以借助Object.is()进行操作

      [1, 5, 10, 15].find(function(value, index, arr) {
          return value > 9;
      }) // 10
        
      [1, 5, 10, 15].findIndex(function(value, index, arr) {
          return value > 9;
      }) // 2
        
      [NaN].indexOf(NaN)
      // -1
    
      [NaN].findIndex(y => Object.is(NaN, y))
      // 0
    
  • ES5中new Array(3)会得到[empty * 3],ES6中数组多了一个fill方法,可以用于初始化空数组,也可以用于抹去数组中现有的元素,fill(num, beginIndex, endIndex)

      new Array(3).fill(3) // [3, 3, 3]
      [1, 2, 3].fill(1, 1, 2) // [1, 1, 3]
    
  • ES5中在{}中对对象进行赋值只能使用标识符,在ES6中,可以通过表达式来对对象进行赋值,注意,属性名表达式与简洁表示法,不能同时使用,会报错

      let propKey = 'foo';
    
      let obj = {
          [propKey]: true,
          ['a' + 'bc']: 123
      };
    
      // 报错
      const foo = 'bar';
      const bar = 'abc';
      const baz = { [foo] };
    
      // 正确
      const foo = 'bar';
      const baz = { [foo]: 'abc'};
    
  • ES5中严格模式NaN === NaN为false,+0 === -0为true,为了区分这种现象,ES6新增了Object.is()方法,用于判断两个参数是否相等

  • ES2018中将数组中…运算符引入了对象,扩展运算符的解构赋值,不能复制继承自原型对象的属性

      let o1 = { a: 1 };
      let o2 = { b: 2 };
      o2.__proto__ = o1;
      let { ...o3 } = o2;
      o3 // { b: 2 }
      o3.a // undefined
    
  • ES6中的set使用的是Object.is()判断是否相同来去重的,所以5和”5”是不同的,NaN和NaN是相同的,对象都是不同的,毕竟地址不同

  • ES6的set的keys,values返回结果是一样的,其实set自带Iterator属性,可以直接for…of…遍历

  • ES6的map和set一样,对象是不一样的,两个不同地址的对象作为key取到的是不一样的value

  • ES6新增了Symbol这种基本类型,作为唯一标识符,两个Symbol()是不相等的,两个Symbol(‘value’)也是不相等的,如果想要两个Symbol相等,可以使用Symbol.for()方法,这个方法会在全局注册,再次赋值的时候会去全局查找,Symbol.keyFor()方法可以知道Symbol.for()方法的参数

      let s = Symbol()
      typeof s // symbol
      Symbol() === Symbol() // false
      Symbol('a') === Symbol('a') // false
      Symbol.for('a') === Symbol.for('a') // true
      Symbol.keyFor(Symbol()) // undefined
      Symbol.keyFor(Symbol.for('s')) // "s"
    
  • 在对象中使用Symbol作为唯一key,要用[]赋值,因为点运算符会被认为是字符串

  • 四个操作会忽略enumerable为false的属性

    • for…in循环:只遍历对象自身的和继承的可枚举的属性
    • Object.keys():返回对象自身的所有可枚举的属性的键名
    • JSON.stringify():只串行化对象自身的可枚举的属性
    • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性
  • ES6 一共有 5 种方法可以遍历对象的属性

    • for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)
    • Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名
    • Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名
    • Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名
    • Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
    • 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则,首先遍历所有数值键,按照数值升序排列;其次遍历所有字符串键,按照加入时间升序排列;最后遍历所有 Symbol 键,按照加入时间升序排列
  • ES6中Reflect对象上的静态方法

    • Reflect.apply(target, thisArg, args),绑定this对象后执行给定函数

        const ages = [11, 33, 12, 54, 18, 96];
      
        // 旧写法
        const youngest = Math.min.apply(Math, ages);
        const oldest = Math.max.apply(Math, ages);
        const type = Object.prototype.toString.call(youngest);
      
        // 新写法
        const youngest = Reflect.apply(Math.min, Math, ages);
        const oldest = Reflect.apply(Math.max, Math, ages);
        const type = Reflect.apply(Object.prototype.toString, youngest, []);
      
    • Reflect.construct(target, args),等同于new target(…args),这提供了一种不使用new,来调用构造函数的方法

        function Greeting(name) {
            this.name = name;
        }
      
        // new 的写法
        const instance = new Greeting('张三');
      
        // Reflect.construct 的写法
        const instance = Reflect.construct(Greeting, ['张三']);
      
    • Reflect.get(target, name, receiver),查找并返回target对象的name属性,如果没有该属性,则返回undefined,如果name属性部署了读取函数(getter),则读取函数的this绑定receiver

        var myObject = {
            foo: 1,
            bar: 2,
        }
        Reflect.get(myObject, 'foo') // 1
        Reflect.get(myObject, 'bar') // 2
      
        var myObject = {
            foo: 1,
            bar: 2,
            get baz() {
                return this.foo + this.bar;
            },
        };
      
        var myReceiverObject = {
            foo: 4,
            bar: 4,
        };
      
        Reflect.get(myObject, 'baz', myReceiverObject) // 8
      
    • Reflect.set(target, name, value, receiver),设置target对象的name属性等于value,注意,如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截

        let p = {
            a: 'a'
        };
      
        let handler = {
            set(target, key, value, receiver) {
                console.log('set');
                Reflect.set(target, key, value, receiver)
            },
            defineProperty(target, key, attribute) {
                console.log('defineProperty');
                Reflect.defineProperty(target, key, attribute);
            }
        };
      
        let obj = new Proxy(p, handler);
        obj.a = 'A';
        // set
        // defineProperty
      
    • Reflect.defineProperty(target, name, desc),和Object.definePropert用法一样
    • Reflect.deleteProperty(target, name),删除对象属性,类似与delete obj.param

        let a = {
            name: 'snowwinter',
        }
        Reflect.deleteProperty(a, 'name')
      
    • Reflect.has(target, name),判断属性是否存在于对象之中

        let a = {
            name: 'snowwinter',
        }
        'name' in a // true
        Reflect.has(a, 'name') // true
      
    • Reflect.ownKeys(target),返回对象所有的键名,包括不可枚举的以及Symbol对象

        var myObject = {
            foo: 1,
            bar: 2,
            [Symbol.for('baz')]: 3,
            [Symbol.for('bing')]: 4,
        };
      
        // 旧写法
        Object.getOwnPropertyNames(myObject)
        // ['foo', 'bar']
      
        Object.getOwnPropertySymbols(myObject)
        //[Symbol(baz), Symbol(bing)]
      
        // 新写法
        Reflect.ownKeys(myObject)
        // ['foo', 'bar', Symbol(baz), Symbol(bing)]
      
    • Reflect.isExtensible(target),对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展

        const myObject = {};
      
        // 旧写法
        Object.isExtensible(myObject) // true
      
        // 新写法
        Reflect.isExtensible(myObject) // true
      
        Object.isExtensible(1) // false
        Reflect.isExtensible(1) // 报错
      
    • Reflect.preventExtensions(target),对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功
    • Reflect.getOwnPropertyDescriptor(target, name),和Object.getOwnPropertyDescriptor一样
    • Reflect.getPrototypeOf(target),和Object.getPrototypeOf一样,用于获取对象的__proto__
    • Reflect.setPrototypeOf(target, prototype),和Object.setPrototypeOf一样,同于设置对象的__proto__
  • ES2018中给Promise增加了finally方法,用于指定不管 Promise 对象最后状态如何,都会执行的操作

      promise
      .then(result => {···})
      .catch(error => {···})
      .finally(() => {···});
    
  • 同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 的方法,鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法

      const f = () => console.log('now');
      (async () => f())();
      console.log('next');
      // now
      // next
    
      const f = () => console.log('now');
      (
          () => new Promise(
               resolve => resolve(f())
          )
      )();
      console.log('next');
      // now
      // next
    
      const f = () => console.log('now');
      Promise.try(f);
      console.log('next');
      // now
      // next
    
  • ES5和ES6的类方法的可遍历性是不一样的,ES5中,挂在prototype上的方法是可以遍历的,而ES6中写在class中的方法是不可遍历的

      class Point {
          constructor(x, y) {
              // ...
          }
    
          toString() {
              // ...
          }
      }
    
      Object.keys(Point.prototype)
      // []
      Object.getOwnPropertyNames(Point.prototype)
      // ["constructor","toString"]
    
      var Point = function (x, y) {
          // ...
      };
    
      Point.prototype.toString = function() {
          // ...
      };
    
      Object.keys(Point.prototype)
      // ["toString"]
      Object.getOwnPropertyNames(Point.prototype)
      // ["constructor","toString"]
    
  • Object.getPrototypeOf(instance)可以获得实例对应的类的原型,Object.getPrototypeOf(Child)可以获得子类继承的父类

      class Parent {}
      class Child extends Parent {}
      Child.__proto__ == Parent // true
      Child.prototype.__proto__ == Parent.prototype // true
    
  • 子类中super用作对象的时候在静态方法之中指向父类,在普通方法之中指向父类的原型对象

  • 编程风格的规范

    • 在let和const之间,优先使用const
    • 函数要返回多个数值的时候,使用对象而不是数组,这样便于解构赋值,不用严格按照顺序
    • 对象字面量定义的时候,尽量把所有的key都定义好,就算当前没有使用,也建议先赋null
    • 区分Object和Map,只有模拟现实世界的实体对象的时候,才用Object,如果只是需要key:value的数据结构,使用map结构,因为map有内建的遍历机制
  • ES6规定,一个数据结构只要具有Symbol.iterator属性,就可以认为是”可遍历的”(iterable)

      const obj = {
          [Symbol.iterator]: function() {
              return {
                  next: function () {
                      value: 1,
                      done: true
                  }
              }
          }
      }
    
      let arr = ['a', 'b', 'c'];
      let iter = arr[Symbol.iterator]();
        
      iter.next() // { value: 'a', done: false }
      iter.next() // { value: 'b', done: false }
      iter.next() // { value: 'c', done: false }
      iter.next() // { value: undefined, done: true }
    
  • ES中,对象没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定

     let obj = {
         data: [ 'hello', 'world' ],
         [Symbol.iterator]() {
             const self = this;
             let index = 0;
             return {
                 next() {
                     if (index < self.data.length) {
                         return {
                             value: self.data[index++],
                             done: false
                         };
                     } else {
                         return { value: undefined, done: true };
                     }
                 }
             };
         }
     };