ES6

参阅 阮一峰 ECMAScript 6 入门 ,作一个归纳整理吧。。。

ES6 : 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

ES6 的第一个版本在 2015 年 6 月发布,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)

声明变量

  • var (ES5)
  • function (ES5)
  • let
  • const
  • class
  • import

var/let/const:

  • scope:

      {
        var a = 1;
        let b = 10;
        const c = 20;
      }
    
      a               // 1
      b              // ReferenceError: b is not defined.
      c              // ReferenceError: c is not defined.
    
  • var : 声明的变量在全局范围内都有效;会发生”变量提升“现象(即变量可以在声明之前使用,值为undefined)
      var a = [];
      for (var i = 0; i < 10; i++) {
        a[i] = function () {
          console.log(i);
        };
      }
      a[6]();                     // 10
    
  • let : 声明的变量只在所在的代码块内有效(局部有效);声明的变量一定要在声明后使用,否则报错(“暂时性死区”)
      var a = [];
      for (let i = 0; i < 10; i++) {
        a[i] = function () {
          console.log(i);
        };
      }
      a[6]();                     // 6
    
  • const : 声明一个只读的常量;与let相同,只在声明所在的块级作用域内有效,同样存在暂时性死区
    • ES5 常量写法:
        Object.defineProperty(window,"PI",{
            value:3.1415926,
            writable:false
        });
        console.log(window.PI);
      
    • ES6 常量写法(使用const)
        const PI = 3.1415;
        PI                                           // 3.1415
        PI = 3;                                      // TypeError: Assignment to constant variable.
      
    • scope:
        const foo;                                // SyntaxError: Missing initializer in const declaration
        if (true) {
          const MAX = 5;
          MAX                                      // 5
        }
        MAX                                        // Uncaught ReferenceError: MAX is not defined
      
  • 注:

    • const实际上保证的是变量指向的内存地址中保存的数据不得改动
    • 所以对于简单类型的数据(例如:数值、字符串、布尔值)可以保证只读,但对于复合类型的数据就无法保证了

        const foo = {};
        foo.prop = 123;              // 成功
        foo = {};                    // TypeError: "foo" is read-only
      
        const a = [];
        a.push('Hello');             //  成功
        a.length = 0;                //  成功
        a = ['Dave'];                // 报错
      
    • letconst之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量

块级作用域 {}

  1. 可代替闭包

    • ES5 闭包(执行函数表达式 IIFE)写法:
        (function () {
          var tmp = ...;
          ...
        }());
      
    • ES6 块级作用域写法:
        {
          let tmp = ...;
          ...
        }
      
  2. 声明的函数在不同环境下可能会有差异,建议使用函数表达式,而不是函数声明语句

    • 使用函数声明语句 -- 不推荐,不同环境下会有差异:
        {
          let a = 'secret';
          function f() {return a;}
        }
      
      • 浏览器环境: 函数声明类似于var(即会提升到全局作用域和函数作用域的头部)
      • 其他环境:函数声明类似于let (对作用域之外没有影响)
    • 使用函数表达式方式(推荐方式)
        {
          let a = 'secret';
          let f = function () {return a;};
        }
      

顶层对象 global

为同一段代码能够在各种环境,都能取到顶层对象,引入global 注:

  • 浏览器中顶层对象: windows,self
  • Node中顶层对象: global
  • 一般通用方法是使用this,但有局限性

示例:

  1. 全局变量与顶层对象

    • ES5中,全局变量与顶层对象等价
        var a = 1;
        window.a                 // 1  -- Node 的 REPL 环境,可以写成 global.a,或者用通用方法this.a
      
    • ES6中,全局变量与顶层对象不等价
        let a = 1;
        window.a                 // undefined
      
  2. 使用垫片库system.global取到global

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

扩展运算符 ...

...变量 : 将剩余传入的参数值,存入一个数组变量中 ...对象 : 拷贝对象的可遍历属性给一个新对象

示例:

  1. function rest 参数

     function add(...values) {
       let sum = 0;
       for (let val of values) {
         sum += val;
       }
       return sum;
     }
     add(2, 5, 3)                 // 10
    
    • 注:rest参数只能是最后一个参数,否则会报错
      (ES5 使用arguments对象,类似数组,但非数组,可使用Array.prototype.slice.call(arguments)转换为数组)
  2. 解构赋值

     let [head, ...tail] = [1, 2, 3, 4];
     head             // 1
     tail             // [2, 3, 4]
    
  3. 拷贝对象的可遍历属性,同Object.assign

     let aClone = { ...a };                                  // 等同 let aClone = Object.assign({}, a);
     let abClone = { ...a, ...b };                           // 等同 let abClone = Object.assign({}, a, b);
    
     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,这个函数是会执行
        let obj={
            a:1,
            get x(){
               throw new Error('get x error!');
            }
        }
        let c={...obj};    //  get x error!
      

解构赋值 (Destructuring)

从等式右边的对象中提取值,赋给左边对应变量:

  • 模式匹配:只要等号两边的模式相同,左边的变量就会被赋予对应的值
  • 类型转换:若等号右边的值不是对象或数组,就先将其转为对象
  • 浅拷贝:解构赋值的拷贝都是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本
  • 注:
    • undefined/null: 无法转为对象,对它们进行解构赋值,会报错
    • ...:扩展运算符的解构赋值(...a),只能读取对象自身的属性

eg:

let a = 1;
let b = 2;

// 可合并成:
let [a, b] = [1, 2];
a    // 1
b    // 2

解构数组赋值

右边数据结构具有 Iterator 接口,即可用数组形式解构赋值,否则报错

示例:

  1. 完全解构

     let [foo, [[bar], baz]] = [1, [[2], 3]];
     foo                 // 1
     bar                 // 2
     baz                 // 3
    
     let [x, , y] = [1, 2, 3];
     x                     // 1
     y                     // 3
    
     let [head, ...tail] = [1, 2, 3, 4];
     head                 // 1
     tail                 // [2, 3, 4]
    
  2. 不完全解构

     let [a, [b], d] = [1, [2, 3], 4];
     a // 1
     b // 2
     d // 4
    
     let [x, y, ...z] = ['a'];
     x                     // "a"
     y                     // undefined
     z                     // []
    
  3. 解构不成功

     let [foo] = [];
     foo                // undefined
    
     //报错
     let [foo] = 1;
     let [foo] = false;
     let [foo] = {};
     let [foo] = null;
    
  4. 对数组进行对象属性的解构,使用:属性名表达式
     let arr = [1, 2, 3];
     let {0 : first, [arr.length - 1] : last} = arr;
     first                 // 1
     last                 // 3
    

解构对象赋值

对象的属性没有次序,变量须与属性同名或给变量指定对应属性,才能取到正确的值 (解构数组赋值:是按照数组顺序位置给对应变量赋值的)

示例:

  1. 单层结构对象

     let { foo, bar } = { foo: "aaa", bar: "bbb" };
     foo                 // "aaa"
     bar                 // "bbb"
    
     let { x } = { foo: "aaa", bar: "bbb" };
     x                    // undefined
    
     let { foo: x } = { foo: 'aaa', bar: 'bbb' };
     x                    // aaa
    
  2. 嵌套结构的对象

     let obj = {
       p: [ 'Hello',{ y: 'World' }]
     };
    
     let { p: [x, { y }] } = obj;  // 这时p是模式,不是变量,不会被赋值
     x                             // "Hello"
     y                             // "World"
    
     let { p, p: [x, { y }] } = obj;
     p                             // ["Hello", {y: "World"}]
     x                             // "Hello"
     y                             // "World"
    
     // 嵌套对象,若子对象所在的父属性不存在,会报错
     let {foo: {bar}} = {baz: 'baz'};        // 报错,因为foo不存在
    

解构函数参数赋值

function add([x, y]){
  return x + y;
}
add([1, 2]);                                    // 3

函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy

[[1, 2], [3, 4]].map(([a, b]) => a + b);        // [ 3, 7 ]

解构基础类型赋值

会先转为对象

示例:

  1. 字符串

     const [a, b, c, d, e] = 'hello';
     a                 // "h"
     b                 // "e"
    
     let {length : len} = 'hello';
     len                 // 5
    
  2. 数值
     let {toString: s} = 123;
     s === Number.prototype.toString         // true
    
  3. 布尔值
     let {toString: s} = true;
     s === Boolean.prototype.toString         // true
    
  4. undefinednull: 无法转换为对象,解构报错
     let { prop: x } = undefined;             // TypeError
     let { prop: y } = null;                  // TypeError
    

指定默认值

undefined会触发使用默认值

示例:

  1. 数组

     let [x, y = 'b'] = ['a'];         // x='a', y='b'
    
     // 默认值可以是一个表达式,表达式是惰性求值的,即只有在用到的时候,才会求值
     function f() {
       console.log('aaa');
     }
     let [x = f()] = [1];                // x 能取到值,所以函数f根本不会执行
    
     // 默认值可以引用解构赋值的其他变量,但该变量必须已经声明
     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 -- 因为x用y做默认值时,y还没有声明
    
  2. 对象

     // 注:默认值生效的条件是,对象的属性值严格等于(===)undefined
     var {x, y = 5} = {x: 1};
     x                     // 1
     y                     // 5
    
     var {x: y = 3} = {};
     y                     // 3
    
     var {x = 3} = {x: null};
     x                     // null
    
  3. 函数

     function move({x = 0, y = 0} = {}) {        // 为变量x,y指定默认值
       return [x, y];
     }
     move({x: 3, y: 8});         // [3, 8]
     move({x: 3});               // [3, 0]
     move({});                   // [0, 0]
     move();                     // [0, 0]
    
     function move({x, y} = { x: 0, y: 0 }) {        // 为函数参数对象整体指定默认值,而不是为变量x和y指定默认值
       return [x, y];,
     }
     move({x: 3, y: 8});         // [3, 8]
     move({x: 3});               // [3, undefined]
     move({});                   // [undefined, undefined]
     move();                     // [0, 0]
    

应用示例

  1. 交换变量的值

     let x = 1;
     let y = 2;
     [x, y] = [y, x];        // x=2,y=1
    
  2. 合并数组

     // ES5
     var params=['hello',true,7];
     var other=[1,2].concat(params);
     console.log(other);
    
     // ES6
     // 利用扩展运算符合并数组
     var params=['hello',true,7];
     var other=[1,2,...params];
     console.log(other);
    
  3. 函数返回多个值: 函数返回多个值,只能将它们放在数组或对象里,通过解构赋值,取出这些值很方便

     function example() {
       return [1, 2, 3];
     }
     let [a, b, c] = example();
    
  4. 函数参数的定义:方便地将一组参数与变量名对应起来

     // 参数是一组有次序的值
     function f([x, y, z]) { 
         ... 
     }
     f([1, 2, 3]);
    
     // 参数是一组无次序的值
     function f({x, y, z}) { 
         ... 
     }
     f({z: 3, y: 2, x: 1});
    
  5. 提取 JSON 数据

     let jsonData = {
       id: 42,
       status: "OK",
       data: [867, 5309]
     };
     let { id, status, data: number } = jsonData;
     console.log(id, status, number);        // 42, "OK", [867, 5309]
    
  6. 遍历 Map 结构 :任何部署了 Iterator 接口的对象,都可以用for...of循环遍历

     const map = new Map();
     map.set('first', 'hello');
     map.set('second', 'world');
     for (let [key, value] of map) {
       console.log(key + " is " + value);
     }
     // first is hello
     // second is world
    
     // 只获取键名
     for (let [key] of map) { ...}
    
     // 只获取键值
     for (let [,value] of map) { ...}
    
  7. import模块的部分项

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

注: 圆括号

// 错误
let x;
{x} = {x: 1};  // SyntaxError: syntax error  因为{x}会被当成一个代码块

// 正确
let x;
({x} = {x: 1});

Iterator 遍历器

提供一种统一的遍历接口,供for...of(或while)消费(循环遍历)

  • 本质:创建一个指针对象,通过next方法移动指针,指向遍历对象的成员,返回成员信息

  • 属性:

    • value : 当前成员的值
    • done : 布尔值,表示遍历是否结束
  • 方法:

    • next :指针跳到下一个成员(遍历器必需部署此方法)
    • return : 循环遍历中提前退出(出错,break)时触发调用 (可选部署)
      • 注:必须返回一个对象
      • 使用场景:一个对象在完成遍历前,需要清理或释放资源
    • throw : 主要配合 Generator 函数使用(可选部署)
  • 可遍历性(iterable):

    • 部署了Iterator 接口的数据结构,此数据结构即是“可遍历的”
    • Symbol.iterator属性:当前数据结构默认的遍历器生成函数(即Iterator接口),执行这个函数,就会返回一个遍历器对象

        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 }
      
  • 原生具备Iterator接口的数据结构:

    • Array/TypedArray
    • Set/Map
    • String
    • function的arguments对象
    • Dom NodeList
    • Generator对象
  • 遍历操作:

    • for...of 循环
      • 循环读取键值(value)
      • 遍历所有数据结构的统一的方法
      • 内部调用的是数据结构的Symbol.iterator方法,可以与breakcontinuereturn配合使用(forEach不行)
    • for...in 循环
      • 循环读取键名(key),
      • 任意顺序,不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
      • 主要是为遍历对象而设计
    • 示例:

      • 遍历数组

          let arr = ['a', 'b', 'c'];
          arr.foo = 'hello';
        
          // for...in循环读取键名(key),注意:数组的key为数字,但循环键名为字符串
          for (let i in arr) {
            console.log(i);     // "0", "1", "2", "foo"
          }
        
          // for...of循环读取键值(value),注意:数组的遍历器接口只返回具有数字索引的属性
          for (let i of arr) {
            console.log(i);     // a,b,c -- 不会返回数组arr的foo属性
          }
        
      • 遍历对象

          // 对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用
          let es6 = {
            edition: 6,
            committee: "TC39",
            standard: "ECMA-262"
          };
        
          for (let e of es6) {
            console.log(e);        // TypeError: es6[Symbol.iterator] is not a function
          }
        
          // for...in可以遍历普通对象
          for (let e in es6) {
            console.log(e);        // edition,committee,standard
          }
        

Proxy 代理器

用于修改某些操作的默认行为,相当于在目标对象之前架设一层“拦截”,对外界的访问进行过滤和改写

Proxy 对象

构造Proxy实例对象:

  • 方式一:var proxy = new Proxy(target, handler);

      var person = {
        name: "张三"
      };
    
      var proxy = new Proxy(person, {
        get: function(target, property) {
          if (property in target) {
            return target[property];
          } else {
            throw new ReferenceError("Property \"" + property + "\" does not exist.");
          }
        }
      });
    
      proxy.name // "张三"
      proxy.age // 抛出一个错误
    
  • 方式二:let {proxy,revoke}=Proxy.revocable(target,handler); : 生成一个可取消的 Proxy 实例

    • Proxy.revocable方法返回一个对象
    • 该对象的proxy属性是Proxy实例
    • 该对象的revoke属性是一个函数,可以取消Proxy实例

      let target = {};
      let handler = {};
      
      let {proxy, revoke} = Proxy.revocable(target, handler);
      
      proxy.foo = 123;
      proxy.foo // 123
      revoke();
      proxy.foo // TypeError: Revoked
      
  • 参数说明:

    • target:所要拦截的目标对象
    • handler:对象,用于定制拦截行为,若为空对象{},则没有任何拦截效果,访问proxy对象等同于访问target对象
  • 注:Proxy代理后,目标对象内部的this会指向Proxy代理对象

      const target = {
        m: function () {
          console.log(this === proxy);
        }
      };
      const handler = {};
    
      const proxy = new Proxy(target, handler);
    
      target.m() // false
      proxy.m()  // true
    

Proxy 支持的拦截操作:

  • 对象属性
    • get(target,propKey,receiver) : 拦截对象属性的读取,eg: proxy.foo,proxy['foo']
    • set(target,propKey,value,receiver) : 拦截对象属性的设置,eg: proxy.foo=v,proxy['foo']=v
    • has(target,propKey) : propKey in proxy
    • deleteProperty(target,propKey): delete proxy[propKey]
  • 函数调用
    • apply(target,ctx,args): 拦截 Proxy 实例作为函数调用的操作,eg: proxy(...args),proxy.call(ctx,...args),proxy.apply(...)
    • construct(target,args): 拦截 Proxy 实例作为构造函数调用的操作,eg: new proxy(...args)
  • 属性描述对象
    • defineProperty(target,propKey,propDesc): 拦截添加新属性,eg: Object.defineProperty(proxy, propKey, propDesc),Object.defineProperties(proxy, propDescs)
    • ownKeys(target): 拦截对象自身属性的读取操作, eg: Object.getOwnPropertyNames,Object.getOwnPropertySymbols,Object.keys,for...in
    • getOwnPropertyDescriptor(target,propKey): 拦截获取属性描述对象, eg: Object.getOwnPropertyDescriptor(proxy, propKey)
  • 对象原型
    • getPropertyOf(target): 拦截获取对象原型, eg: Object.getPrototypeOf(proxy),instanceof
    • setPropertyOf(target,proto): 拦截设置对象原型, eg: Object.setPrototypeOf(proxy, proto)
  • 对象扩展
    • isExtensible(target): Object.isExtensible(proxy)
    • preventExtensions(target): Object.preventExtensions(proxy)

应用示例:

  1. Proxy对象作为普通函数调用 VS 作为构造函数调用

     var handler = {
       get: function(target, name) {
         if (name === 'prototype') {
           return Object.prototype;
         }
         return 'Hello, ' + name;
       },
    
       apply: function(target, thisBinding, args) {
         return args[0];
       },
    
       construct: function(target, args) {
         return {value: args[1]};
       }
     };
    
     var fproxy = new Proxy(function(x, y) {
       return x + y;
     }, handler);
    
     fproxy(1, 2) // 1
     new fproxy(1, 2) // {value: 2}
     fproxy.prototype === Object.prototype // true
     fproxy.foo === "Hello, foo" // true
    
  2. 私有变量

    • ES3 写法

        var Person=function(){
            var data={
                name:'Tom',
                sex:'male',
                age:15
            }
            this.get=function(key){
                return data[key];
            }
            this.set=function(key,value){
                if(key!=='sex')
                    data[key]=value;
            }
        }
      
        var person=new Person();
        person.set('name','Jack');
        person.set('sex','female');
        console.table({
            name: person.get('name'),
            sex: person.get('sex'),
            age: person.get('age')
        }); // Jack,male,15
      
    • ES5 写法
        var Person={
            name:'Tom',
            age: 15
        }
        Object.defineProperty(Person,'sex',{
            writable:false,
            value:'male'
        })
        Person.name='Jack';
        console.table({
            name: Person.name,
            age: Person.age,
            sex: Person.sex
        }); // Jack,male,15
        Person.sex='female';    // will throw exception
      
    • ES6

        let Person={
            name:'Tom',
            sex:'male',
            age:15
        };
        let person=new Proxy(Person,{
            get(target,key){
                return target[key]
            }
            set(target,key){
                if(key!=='sex')
                    target[key]=value;
            }
        });
      
        person.set('name','Jack');
        console.table({
            name: person.get('name'),
            sex: person.get('sex'),
            age: person.get('age')
        }); // Jack,male,15
        person.set('sex','female');    // will throw exception
      

Reflect 对象

  1. 将Object的一些方法放到Reflect上,使用Reflect代替Object的一些方法,例如:

    • defineProperty方法

        // 老写法
        try {
          Object.defineProperty(target, property, attributes);
          // success
        } catch (e) {
          // failure
        }
      
        // 新写法
        if (Reflect.defineProperty(target, property, attributes)) {
          // success
        } else {
          // failure
        }
      
    • 判断对象是否有某属性

        // 老写法
        'assign' in Object // true
      
        // 新写法
        Reflect.has(Object, 'assign') // true
      
    • 方法调用

        // 老写法
        Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
      
        // 新写法
        Reflect.apply(Math.floor, undefined, [1.75]) // 1
      
  2. 与Proxy对象的方法一一对应,可通过Reflect获取对象原有的默认行为,例如:

     var loggedObj = new Proxy(obj, {
       get(target, name) {
         console.log('get', target, name);
         return Reflect.get(target, name);
       },
       deleteProperty(target, name) {
         console.log('delete' + name);
         return Reflect.deleteProperty(target, name);
       },
       has(target, name) {
         console.log('has' + name);
         return Reflect.has(target, name);
       }
     });
    

对象 Object

对象属性

Descriptor属性描述对象: 对象的每个属性都有一个描述对象,用来控制该属性的行为

  • 数据属性描述对象包含:

    • value
    • writable
    • enumerable
    • configurable
  • 获取对象自身属性(非继承属性)的描述对象

    • Object.getOwnPropertyDescriptor
    • Object.getOwnPropertyDescriptors
    • Reflect.getOwnPropertyDescriptors
  • 某属性的描述对象的enumerable:可枚举性,若为false,即不可枚举,则一下操作会忽略该属性

    • for...in循环
    • Object.keys()
    • JSON.stringify()
    • Object.assign()
    • 注:
      • 以上操作除了for...in会包含继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性
      • ES6 规定,所有 Class 的原型的方法都是不可枚举的

__prop__属性 (前后各两个下划线): 等于Object.prototype.__proto__,即一个对象的__prop__属性值就是对象的原型

  • 操作对象的prototype对象(原型对象)的方法:
    • Object.setPrototypeOf(object, prototype);
    • Object.getPrototypeOf(object);
    • Object.create(...)

获取对象自身属性的操作 (即不包括继承属性):

  • 可枚举属性(无Symbol):Object.keys(obj) -- ES2017 引入了Object.values,Object.entries,作为遍历一个对象的补充手段,供for...of循环使用
  • 可枚举和不可枚举属性(无Symbol):Object.getOwnPropertyNames(obj)
  • Symbol属性:Object.getOwnPropertySymbols(obj)
  • 所有(可枚举,不可枚举,Symbol):Reflect.ownKeys(obj)

示例:

  1. Descriptor 属性描述对象

     const obj = {
       foo: 123,
       get bar() { return 'abc' }
     };
    
     Object.getOwnPropertyDescriptor(obj, 'foo')
     Object.getOwnPropertyDescriptors(obj)
    
     // descriptor对象
     // { foo:
     //    { value: 123,
     //      writable: true,
     //      enumerable: true,         // 可枚举性
     //      configurable: true
     //     },
     //   bar:
     //    { get: [Function: get bar],
     //      set: undefined,
     //      enumerable: true,
     //      configurable: true } 
     // }
    
  2. 读取/遍历对象

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

对象比较

  • == 相等运算符:自动转换数据类型
  • === 严格相等运算符:NaN不等于自身,+0与-0相等
  • Object.is 同值相等:比较两个值是否严格相等,与===相比,NaN等于自身,+0-0不等
+0 === -0                 //true
NaN === NaN                 // false

Object.is(+0, -0)         // false
Object.is(NaN, NaN)        // true

Object.is('foo', 'foo')    // true
Object.is({}, {})          // false

对象拷贝

浅拷贝: 只能进行值的复制,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用

  1. Object.assign(target,src1,src2,...) : 拷贝可被枚举的自有属性到目标对象(浅拷贝,同名属性替换, 取值函数求值后再复制)

     // 浅拷贝,同名属性替换
     const target = { a: 1, b: 1,d:{e:'hello',f:'world'} };
     const source1 = { b: 2, c: 2 };
     const source2 = { c: 3,d:{g:'say'} };
     Object.assign(target, source1, source2);                // {a:1, b:2, c:3,d:{g:'say'}}
    
    • 参数注意点:
      • 只有一个参数,即只有target,则返回target(不是对象会先转换成对象返回)
      • 传入不是对象的参数,会先转成对象(eg:字符串可转换为字符数组,数组视为属性名为 0、1、2 的对象)
      • 传入无法转成对象的参数(eg: undefined,null,数值,布尔值):
        • 作为第一个参数(即target)会报错;
        • 不是第一个参数(即source),会跳过
    • 只拷贝属性值,不会拷贝它背后的赋值方法或取值方法,取值函数求值后再复制值
        // 不会复制取值函数,会用取值函数求值后再复制
        const source = {
          get foo() { return 1 }
        };
        Object.assign({}, source)                                        // { foo: 1 }
      
    • 使用Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法添加描述对象,可实现正确拷贝

      // 配合Object.defineProperties方法添加描述对象
      const source = {
      set foo(value) {
        console.log(value);
      }
      };
      const target = {};
      Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
      
      Object.getOwnPropertyDescriptor(target, 'foo');
      // {
      //   get: undefined,
      //   set: [Function: set foo],
      //   enumerable: true,
      //   configurable: true
      //}
      
  2. Object.create(proto [, propertyDescriptors ]) :创建一个新对象,对象继承到__proto__属性上

     const person = {
       isHuman: false,
       printIntroduction: function () {
         console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
       }
     };
    
     const me = Object.create(person);
    
     me.name = "Matthew";             // "name" is a property set on "me", but not on "person"
     me.isHuman = true;               // inherited properties can be overwritten
     me.printIntroduction();          // "My name is Matthew. Am I human? true"
    
    • proto:新创建对象的原型对象,可为null
    • propertyDescriptors:可选项,新对象属性的描述对象(其自身定义的属性,不是其原型链上的属性)

        let o = Object.create({}, { p: { value: 42 } }) 
        // 创建一个以空对象为原型,拥有一个属性p的对象 
        // 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的
      
        // p的属性描述对象的enumerable默认是false, 改成true,Object.values就会返回属性p的值
        Object.values(obj)     // []
      
        o.p                    // 42
        o.p = 20               // 失败
      
        o.__proto__            // Object {}
        o.__proto__.p          // undefined
      
        var o = Object.create(Object.prototype, {
        foo: {                                    // foo会成为所创建对象的数据属性 
            writable:true,
            configurable:true,
            value: "hello"
          },
        bar: {                                    // bar会成为所创建对象的访问器属性  
            configurable: false,                // false,下面set,get方法不起作用
            get: function() { return 10 },
            set: function(value) {
              console.log("Setting `o.bar` to", value);
            }
          }
        });
        console.log(o);                         // {foo:'hello'}
      
  3. Object.create(),new Object(),{} 区别

     // test1,test2,test3的__proto 一样
     var test1 = {};
     var test2 = new Object();
     var test3 = Object.create(Object.prototype);
    
     // 创建一个原型为null的对象,test4.__proto__为undefined, 没有继承原型属性和方法,不同于test1,2,3
     var test4 = Object.create(null);
    
     var test = Object.create({x:123,y:345});
     console.log(test);                                               //{}
     console.log(test.x);                                            //123
     console.log(test.__proto__.x);                            //123
     console.log(test.__proto__.x === test.x);          //true
    
     var test1 = new Object({x:123,y:345});
     console.log(test1);                                              //{x:123,y:345}
     console.log(test1.x);                                            //123
     console.log(test1.__proto__.x);                            //undefined
     console.log(test1.__proto__.x === test1.x);        //false
    
     var test2 = {x:123,y:345};
     console.log(test2);                                                 //{x:123,y:345};
     console.log(test2.x);                                                //123
     console.log(test2.__proto__.x);                                //undefined
     console.log(test2.__proto__.x === test2.x);            //false
    

综合示例: 克隆一个对象(包括对象原型的属性,浅拷贝)

// 写法一,注:__proto__属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三
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)
);

super对象

指向当前对象的原型对象

示例:

  1. 调用当前对象原型对象的属性

     const proto = { foo: 'hello'};
     const obj = {
       foo: 'world',
       find() {
         return super.foo;                                        // 引用了原型对象proto的foo属性,同 Object.getPrototypeOf(this).foo
       }
     };
     Object.setPrototypeOf(obj, proto);
     obj.find()                                                         // "hello"
    
  2. 调用当前对象原型对象的方法

     const proto = {
       x: 'hello',
       foo() {
         console.log(this.x);
       },
     };
    
     const obj = {
       x: 'world',
       foo() {
         super.foo();                                                    // 同 Object.getPrototypeOf(this).foo.call(this),this绑定的是当前obj
       }
     }
    
     Object.setPrototypeOf(obj, proto);
     obj.foo()                                                             // "world"
    
  3. 注: 只能用在对象的方法中(注:方法为简写方式才可以让 JavaScript 引擎确认,定义的是对象的方法)

    • super用在属性里面,报错
        const obj = {
          foo: super.foo
        }
      
    • super用在一个函数里面,然后赋值给foo属性,报错

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

函数 function

name属性

返回函数的函数名

function foo() {}
foo.name                                     // "foo"

const a = function baz() {};
a.name                                       // "baz"

注:

  • 匿名函数: ES5返回空字符串,ES6返回赋给的变量名
      var f = function () {};
      f.name                                  // ES5 ""; ES6 "f"
    
  • Function构造函数返回的函数实例: anonymous
      (new Function).name                      // "anonymous"
    
  • bind返回的函数: name属性值会加上bound前缀

      function foo() {};
      foo.bind({}).name                         // "bound foo"
    
      (function(){}).bind({}).name            // "bound "
    

函数参数

  1. 通过解构赋值设置参数

     function add([x, y]){                        // 参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y
       return x + y;
     }
     add([1, 2]);                                // 3
    
     [[1, 2], [3, 4]].map(([a, b]) => a + b);    // [ 3, 7 ]
    
  2. 处理可变参数: 使用rest参数...参数名(类似ES5的arguments)

     function add(...values) {    // values同ES5 Array.prototype.slice.call(arguments);
       let sum = 0;
       for (let val of values) {
         sum += val;
       }
       return sum;
     }
     add(2, 5, 3)                 // 10
    

参数默认值

  • 直接写在参数定义的后面

      function log(x, y = 'World') {
        console.log(x, y);
      }
      log('Hello')              // Hello World
      log('Hello', 'China')     // Hello China
    
  • 可以使用表达式/函数(惰性求值)

      let x = 99;
      function foo(p = x + 1) {
        console.log(p);
      }
      foo()             // 100
      x = 100;
      foo()             // 101
    
  • 使用解构赋值设置默认值

      /* 1. 为函数参数对象整体指定默认值,eg: 为{x,y}对象整体指定默认值,而不是为变量x和y指定默认值 */
      function m2({x, y} = { x: 0, y: 0 }) {
        return [x, y];
      }
      m2()                    // [0,0]
      m2({})                  // [undefined,undefined]
      m2({x: 3})              // [3, undefined]
      m2({x:3,y:8})           // [3, 8]
    
      /* 2. 为函数某个具体参数指定默认值,eg:为变量y指定默认值 */
      function foo({x, y = 5} = {}) {
        console.log(x, y);
      }
      foo()                     // undefined 5
      foo({})                   // undefined 5
      foo({x:3})                // 3 5
      foo({x:3,y:8})            // 3 8
    
  • 注:指定了默认值后,函数的length属性将失真,会返回没有指定默认值的参数个数

      // 函数的length属性: 函数预期传入的参数个数
      (function(...args) {}).length                 // 0
      (function (a) {}).length                      // 1
    
      // 设置默认参数后,函数的length属性将失真:
      (function (a, b, c = 5) {}).length             // 2
      (function (a, b = 1, c) {}).length             // 1    -- 默认值以后的参数也不计数
    

应用示例:

  1. 利用参数默认值,指定某一个参数不得省略,若省略就抛出一个错误
     function throwIfMissing() {
       throw new Error('Missing parameter');
     }
     function foo(mustBeProvided = throwIfMissing()) {
       return mustBeProvided;
     }
     foo()        // Error: Missing parameter
    
  2. 利用参数默认值,指定某一个参数是可以省略的(将参数默认值设为undefined)
     function foo(optional = undefined) { 
         //···
     }
    

函数绑定运算符 ::

用来取代call、apply、bind调用

  • 对象::函数 : 会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面

      bar.bind(foo);        // ES5
      foo::bar;             // ES6
    
      bar.apply(foo, arguments);        // ES5
      foo::bar(...arguments);           // ES6
    
  • ::对象.方法 :等于将该方法绑定在该对象上面
      var log = console.log.bind(console);    // ES5
      let log = ::console.log;                // ES6, 同 let log = console::console.log;
    
  • 若双冒号运算符的运算结果,还是一个对象,可采用链式写法

箭头函数 arrow-function

简化函数编写形式

// ES3,ES5
function a(){
    exp
}

// ES6
// 只有一个参数,可省略"()"
// 表达式直接作为返回值时,可省略"{}"
(arg)=>{
    exp
}

示例:

var f = function () { return 5 };                          // ES5
var f = () => 5;                                          // ES6

var sum = function(num1, num2) { return num1 + num2;};    // ES5
var sum = (num1, num2) => num1 + num2;                    // ES6

[1,2,3,4,5].map(function(v){ return v+1; });              // ES5
[1,2,3,4,5].map(v=>v+1);                                  // ES6

//无返回
let fn = () => void doesNotReturn();

//返回一个对象(为防止语法歧义报错,用圆括号包起来)
let getTempItem = id => ({ id: id, name: "Temp" });

//使用rest参数
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)        // [1,[2,3,4,5]]

限制:

  • 不能用作构造函数(即不能用new)
  • 不能使用arguments对象,用rest参数代替
  • 不能用作Generator函数(即不能使用yield)

this对象: 在箭头函数中,this对象的指向是固定的,为定义时所在的对象,不是使用时所在的对象

  • 实际上:箭头函数没有自己的 thisargumentssupernew.target,只能引用外层代码块的对应变量
  • 因为没有自己的this(使用外层代码块的this),所以不能用作构造函数,也不能使用callapplybind这些方法去改变this的指向

示例:

  1. ES3,ES5 原始写法 : this 指向的是该函数被调用的对象

     var factory=function(){
         this.a='a';
         this.b='b';
         this.c={
             a:'a+',
             b:function(){
                 return this.a;
             }
         }
     }
     console.log(new factory().c.b());            // a+     -- this指向c
    
  2. ES6 箭头函数 : this 指向的是定义时this的指向

     var factory=function(){
          this.a='a';
          this.b='b';
          this.c={
             a: 'a+',
             b: ()=>{
                 return this.a;
             }
         }
     }
     console.log(new factory().c.b());    // a    -- 同外层代码this,指向factory
    
  3. 箭头函数转成 ES5写法(注意this)

     // ES6 箭头函数
     function foo() {
       setTimeout(() => {
         console.log('id:', this.id);            // this -- foo
       }, 100);
     }
    
     // ES5 原始写法
     function foo() {
       var _this = this;
       setTimeout(function () {
         console.log('id:', _this.id);
       }, 100);
     }
    

优化:尾调用,尾递归

尾调用: 函数的最后一步是返回调用另一个函数

function f(x){
  return g(x);
}

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,直接用内层函数的调用帧取代外层函数的调用帧即可 注:

  • ES6支持尾调用优化,且只在严格模式下开启
  • 只有不再用到外层函数的内部变量才可取代
  • 例:以下三种情况,都不属于尾调用

      function f(x){
        let y = g(x);
        return y;                // 因为调用后还有赋值操作
      }
    
      function f(x){
        return g(x) + 1;    // 因为调用后还有操作
      }
    
      function f(x){
        g(x);                    // 函数最后一步为 return undefined;
      }
    

尾递归: 尾调用自身

函数调用自身,因为调用栈太多,容易发生“栈溢出”错误(stack overflow); 而尾递归,由于只存在一个调用帧,所以不会发生“栈溢出”错误

应用示例:

  1. 计算n的阶乘: n!
    • 非尾递归实现: 最多需要保存n个调用记录,复杂度 O(n)
        function factorial(n) {
          if (n === 1) return 1;
          return n * factorial(n - 1);
        }
        factorial(5)             // 120
      
    • 尾递归实现(将所有用到的内部中间变量改写成函数的参数): 只保留一个调用记录,复杂度 O(1)
        function factorial(n, total=1) {
          if (n === 1) return total;
          return factorial(n - 1, n * total);
        }
        factorial(5, 1)             // 120
      
  2. Fibonacci 数列

    • 非尾递归的 Fibonacci 数列实现

        function Fibonacci (n) {
          if ( n <= 1 ) {
                return 1
            };
          return Fibonacci(n - 1) + Fibonacci(n - 2);
        }
      
        Fibonacci(10)               // 89
        Fibonacci(100)             // 堆栈溢出
        Fibonacci(500)             // 堆栈溢出
      
    • 尾递归优化过的 Fibonacci 数列实现

        function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
          if( n <= 1 ) {
                return ac2
            };
          return Fibonacci2 (n - 1, ac2, ac1 + ac2);
        }
      
        Fibonacci2(100)             // 573147844013817200000
        Fibonacci2(1000)           // 7.0330367711422765e+208
        Fibonacci2(10000)         // Infinity
      

注: 尾递归优化只在严格模式下生效,正常模式下可采用“循环”换掉“递归”的方式进行优化

Symbol

  1. ES6新增的原始数据类型,类似于字符串的数据类型 (Javascript其他原始数据类型有:undefined,null,Boolean,String,Number,Object)
  2. 表示独一无二的值 (例如:可以用来保证对象的属性名是独一无二的)
  3. 通过Symbol()函数生成,可以接受一个字符串作为参数,表示对Symbol实例的描述
  4. 注:不能使用new,基本上,它是一种类似于字符串的数据类型
let s = Symbol(); 
typeof s                                // "symbol"

let s1 = Symbol('foo');
s1                                     // Symbol(foo)
s1.toString()                         // "Symbol(foo)"  

//相同参数的Symbol函数的返回值是不相等
let s2 = Symbol('foo');
s1 === s2                             // false

// 用于对象属性
let mySymbol = Symbol();
let a = {
  [mySymbol]: 'Hello!'
};
a[mySymbol]                         // "Hello!"

Set/WeakSet

Set

  • 类似于数组,但是成员的值都是唯一,可枚举(Array.from方法可以将 Set 结构转为数组)
  • 可以接受一个具有 iterable 接口的数据结构作为参数(例如数组),用来初始化
  • 内部使用同值相等判断两个值是否相同(比严格相等===,多了NaN和0的比较),注:两个对象总是不相等的
  • 属性:
    • Set.prototype.constructor:构造函数,默认就是Set函数。
    • Set.prototype.size:返回Set实例的成员总数。
  • 方法(操作):
    • add(value)
    • delete(value)
    • has(value)
    • clear()
  • 方法(遍历):
    • keys()
    • values()
    • entries()
    • forEach()

示例:

  1. 无参构造使用Set

     let s= new Set();
     s.add(1).add(2).add(2);
     s.size                                       // 2
    
     s.has(1)                                     // true
     s.has(2)                                     // true
     s.has(3)                                     // false
     s.delete(2);
     s.has(2)                                     // false
    
  2. 可枚举对象作为参数构造

     const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
     items.size                                     // 5
    
     const s = new Set();
     [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
    
     // 去除数组的重复成员
     [...new Set(array)]
    
  3. 遍历

     let s2= new Set(['red', 'green', 'blue']);
    
     s2.keys()                    // ['red', 'green', 'blue']
     s2.values()                  // ['red', 'green', 'blue']
     s2.entries()                 // [ ['red','red'], ['green','green'], ['blue','blue'] ]
    
     // for...of循环遍历
     for (let x of s2) {          // 默认遍历器生成函数就是它的values方法
       console.log(x);
     }
     // red
     // green
     // blue
    
     // forEach循环遍历
     s2.forEach((value, key) => console.log(key + ' : ' + value))
     // red:red
     // green:green
     // blue:blue
    
    • 注:keys/values() Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致,默认遍历器生成函数就是values方法
  4. 应用:Set 实现并集(Union)、交集(Intersect)和差集(Difference)

     let a = new Set([1, 2, 3]);
     let b = new Set([4, 3, 2]);
    
     // 并集
     let union = new Set([...a, ...b]);                                // Set {1, 2, 3, 4}
    
     // 交集
     let intersect = new Set([...a].filter(x => b.has(x)));            // set {2, 3}
    
     // 差集
     let difference = new Set([...a].filter(x => !b.has(x)));          // Set {1}
    

WeakSet

  • 与Set区别:
    • 成员只能是对象,而不能是其他类型的值
    • 成员对象都是弱引用,随时可能消失(即如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中)
    • 垃圾回收机制运行前后可能会导致成员数不一样,所以ES6 规定 WeakSet不可遍历
  • 方法:
    • add(value)
    • delete(value)
    • has(value)

示例:

  1. 构造使用 WeakSet

     const ws = new WeakSet();
     const obj = {};
     const foo = {};
    
     ws.add(window);
     ws.add(obj);
    
     ws.has(window);             // true
     ws.has(foo);                // false
     ws.delete(window);
     ws.has(window);                // false
    
     // WeakSet 没有size属性,不能遍历:因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在
     ws.size                     // undefined
     ws.forEach                     // undefined
    
  2. 有参构造 WeakSet(注:WeakSet的成员只能是对象)

     // 可以接受具有 Iterable 接口的对象
     const a = [[1, 2], [3, 4]];
     const ws1 = new WeakSet(a);            // WeakSet {[1, 2], [3, 4]} 注:a数组的成员成为 WeakSet 的成员,不是a数组本身
    
     const b = [3, 4];
     const ws2 = new WeakSet([3, 4]);      // Uncaught TypeError: Invalid value used in weak set(…) 注:b数组的成员不是对象
    
  3. 应用:Weakset 储存 DOM 节点,不用担心这些节点从文档移除时,会引发内存泄漏

     const foos = new WeakSet()            // foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏
     class Foo {
       constructor() {
         foos.add(this)
       }
       method () {
         if (!foos.has(this)) {
           throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
         }
       }
     }
    

Map/WeakMap

Map

  • 键值对集合,类似对象(Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,key不限于字符串,各种类型的值包括对象都可以当作键,是一种更完善的 Hash 结构实现)
  • 键唯一,跟内存地址绑定的,只要内存地址不一样,就视为两个键(0和-0是一个键,NaN视为同一个键)
  • Map 的遍历顺序就是插入顺序
  • 属性:
    • size
  • 方法(操作):
    • set(key,value)
    • get(key)
    • has(key)
    • delete(key)
    • clear()
  • 方法(遍历):
    • keys()
    • values()
    • entries()
    • forEach()

示例:

  1. 无参数构造

     const m = new Map();
     const o = {p: 'Hello World'};
    
     m.set(o, 'content')
     m.get(o)                        // "content"
    
     m.set(1,'Hello') 
     m.get(1)                        // "Hello"
    
     m.get('a')                        // undefined
    
     m.set(undefined, 3);            
     m.get(undefined)                // 3
    
     m.set(['a'], 555);
     m.get(['a'])                    // undefined
    
     m.set(1,'a').set(2,'b')
    
  2. 可枚举对象作为参数构造

     const m= new Map([
       ['name', '张三'],
       ['title', 'Author']
     ]);                             // Map {'name':'张三','title':'Author'}
    
     // 相当于
     param.forEach(
       ([key, value]) => m.set(key, value)
     );
    
     m.size                            // 2
     m.has('name')                    // true
     m.get('name')                    // "张三"
    
  3. 遍历

     m.keys()        // ['name','title']
     m.values()        // ['张三','Author']
     m.entries()        // [ ['name', '张三'],['title', 'Author'] ]
    
     // forEach循环遍历
     m.forEach(function(value, key, map) {
       console.log("Key: %s, Value: %s", key, value);
     });
    
     // for...of循环遍历
     for (let [key, value] of map) {
       console.log(key, value);
     }
     for (let [key, value] of map.entries()) {
       console.log(key, value);
     }
     for (let item of map.entries()) {
       console.log(item[0], item[1]);
     }
    
     // name 张三
     // title Author
    
    • 注:entries() 是Map结构的默认遍历器接口(部署在Symbol.iterator属性上,即map[Symbol.iterator] === map.entries
  4. Map <-> Array 转换

    • Map -> Array: 使用扩展运算符 ...
        const myMap = new Map()
          .set(true, 7)
          .set({foo: 3}, ['abc']);
        [...myMap]                                    // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
      
    • Array -> Map: 直接作为参数传入Map构造函数
        new Map([
          [true, 7],
          [{foo: 3}, ['abc']]
        ])                                           // Map { true:7, {foo:3}:['abc'] }
      

Weakmap

  • 与Map区别:
    • 只接受对象作为键名(null除外)
    • 键名所指向的对象为弱引用(不计入垃圾回收机制,即一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用)
    • 注:WeakMap 弱引用的只是键名,而不是键值,所以即使在 WeakMap 外部消除了键值的引用,WeakMap 内部的引用依然存在
    • 没有遍历操作(即没有keys()、values(),entries(),forEach方法),也没有size属性
    • 无法清空,即不支持clear方法
  • 方法:
    • get()、set()、has()、delete()
  • 应用(WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏)
    • DOM 节点作为键名(在网页的 DOM 元素上添加数据,当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除,有助于防止内存泄漏)
    • 部署私有属性

示例:

  1. 构造使用 WeakMap

     const wm = new WeakMap();
    
     const key = {foo: 1};
     wm.set(key, 2);                        // set 添加成员
     wm.get(key)                         // get 获取成员
    
     // 只接受对象作为键名(null除外)
     wm.set(1, 2)                        // TypeError: 1 is not an object!
     wm.set(Symbol(), 2)                    // TypeError: Invalid value used as weak map key
    
     // size、forEach、clear 方法都不存在
     wm.size                             // undefined
     wm.forEach                             // undefined
     wm.clear                             // undefined
    
     // 可接受一个数组,作为构造函数的参数
     const k1 = [1, 2, 3];
     const k2 = [4, 5, 6];
     const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
     wm2.get(k2) // "bar"
    
  2. 应用:DOM 节点作为键名存储在Weakmap中,防止内存泄漏

     // myElement是一个 DOM 节点,每当发生click事件,就更新一下状态
     // 一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险
    
     let myElement = document.getElementById('logo');
    
     let myWeakmap = new WeakMap();
     myWeakmap.set(myElement, {timesClicked: 0});
    
     myElement.addEventListener('click', function() {
       let logoData = myWeakmap.get(myElement);
       logoData.timesClicked++;
     }, false);
    
  3. 应用:Weakmap 部署私有属性,实例消失,它们也就随之消失

     // Countdown类的两个内部属性_counter和_action,是实例的弱引用
     // 如果删除实例,它们也就随之消失,不会造成内存泄漏
    
     const _counter = new WeakMap();
     const _action = new WeakMap();
    
     class Countdown {
       constructor(counter, action) {
         _counter.set(this, counter);
         _action.set(this, action);
       }
       dec() {
         let counter = _counter.get(this);
         if (counter < 1) return;
         counter--;
         _counter.set(this, counter);
         if (counter === 0) {
           _action.get(this)();
         }
       }
     }
    
     const c = new Countdown(2, () => console.log('DONE'));
    
     c.dec()
     c.dec()
     // DONE
    

类 Class

类的数据类型就是函数,类本身就指向构造函数

  • constructor 构造函数
    • 默认返回实例对象,即this(this 代表实例对象)
    • 无参构造,可以不显式定义,会默认添加一个空的constructor方法
  • new 创建实例对象
    • new 构造函数(args): 从prototype对象生成一个实例对象 ( 注:ES6 class必须使用new调用,否则会报错 )
    • new.target: 一般在构造函数中调用,返回new命令作用于的那个构造函数,若不是用new命令调用,返回undefined (这个属性可以用来确定构造函数是怎么调用的)

类成员:

  • 变量:定义在this上,是类的实例对象自身的属性,属于类实例对象
      class Point {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }
      }
    
    • 提案:实例属性: 用等式直接写入类的定义之中(以前只能写在类的constructor方法里面)
        class MyClass {
          myProp = 42;
          constructor() {
            console.log(this.myProp); // 42
          }
        }
      
  • 方法: 定义在class上,是原型对象prototype的属性,属于类
      class Point {
          say(){
              console.log("Hello");
          }
      }
    
  • 注:getter/setter 对某个属性设置存值函数和取值函数(部署在Descriptor属性描述对象上),拦截该属性的存取行为

      class MyClass {
        get prop() {
          return 'getter';
        }
        set prop(value) {
          console.log('setter: '+value);
        }
      }
    
      let inst = new MyClass();
    
      inst.prop = 123;        // setter: 123
      inst.prop                    // 'getter'
    

私有属性/方法:

  • 利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值

      const bar = Symbol('bar');
      const snaf = Symbol('snaf');
    
      export default class myClass{
        // 公有方法
        foo(x) {
          this[bar](x);
        }
    
        // 私有方法
        [bar](x) {
          return this[snaf] = x;
        }
      }
    
  • 提案:使用#表示
      class Foo {
        #a;
        #b;
        #sum() { return #a + #b; }
        printSum() { console.log(#sum()); }
        constructor(a, b) { #a = a; #b = b; }
      }
    

静态属性/方法: 直接通过类来调用(实例上调用,会抛出错误,表示不存在)

  • 静态属性:ES6 没有静态属性,可在类外部定义实现

      class Foo {}
    
      Foo.prop = 1;        // 为Foo类定义了一个静态属性prop
      Foo.prop // 1
    
    • 提案:在实例属性写法前面加上static关键字

        class MyClass {
          static myStaticProp = 42;
      
          constructor() {
            console.log(MyClass.myStaticProp); // 42
          }
        }
      
  • 静态方法: 可以与非静态方法重名 ( 注:static方法中的this指向类,不是实例 )

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

示例:

  1. 定义使用类

    • ES5方式

        // 构造函数:
        function Point(x, y) {
          this.x = x;
          this.y = y;
        }
        // 类的所有方法都定义在类的prototype属性上面:
        Point.prototype={
            constructor:Point,
            toString:function(){
                return '(' + this.x + ', ' + this.y + ')';};
            }
        }
      
        // 使用
        var p = new Point(1, 2);
        console.log(p.toString());
      
    • ES6方式

        class Point {
          constructor(x, y) {
            this.x = x;
            this.y = y;
          }
      
          toString() {
            return '(' + this.x + ', ' + this.y + ')';
          }
        }
      
        var p = new Point(1, 2);
        console.log(p.toString());
      
    • 注:

      • 类的数据类型就是函数,类本身就指向构造函数
          typeof Point // "function"
          Point === Point.prototype.constructor // true
        
      • 类属性定义在this变量上,属于类实例对象; 类方法定义在class上 ,属于类
          p.hasOwnProperty('x') // true
          p.hasOwnProperty('y') // true
          p.hasOwnProperty('toString') // false
          p.__proto__.hasOwnProperty('toString') // true
        
      • 类的内部所有定义的方法,ES6定义的不可枚举,ES5方式定义的可枚举

          /* ES5 */
          Object.keys(Point.prototype)    // ["toString"]
          Object.getOwnPropertyNames(Point.prototype)    // ["constructor","toString"]
        
          /* ES6 */
          Object.keys(Point.prototype)    // []
          Object.getOwnPropertyNames(Point.prototype)    // ["constructor","toString"]
        
      • ES6 class必须使用new调用,否则会报错
          var point = Point(2, 3);        // 报错
          var point = new Point(2, 3);    // 正确
        
  2. 无参构造类( constructor可以不显式定义)

    • ES5方式

        /* Javascript */
      
        function Point() {}
        Point.prototype.toString=function(){...}
      
        // 等同于
        function Point(){}
        Point.prototype={
            constructor:Point,
            toString:function(){...}
        }
      
    • ES6方式

        class Point{
            toString(){...}
        }
      
        // 等同于
        class Point {
          constructor() {}
          toString(){...}
        }
      
  3. 匿名类

     let person = new class {
       constructor(name) {
             this.name = name;
       }
       sayName() {
         console.log(this.name);
       }
     }('张三');
    
     person.sayName();     // "张三"
    
  4. new.target: 用在构造函数中调用,返回new命令作用于的那个构造函数

    • ES5方式

        function Person(name) {
          if (new.target === Person) {
            this.name = name;
          } else {
            throw new Error('必须使用 new 命令生成实例');
          }
        }
      
        var person = new Person('张三'); // 正确
        var notAPerson = Person.call(person, '张三');  // throw Error: 必须使用 new 命令生成实例
      
    • ES6

        class Rectangle {
          constructor(length, width) {
            console.log(new.target === Rectangle);
            this.length = length;
            this.width = width;
          }
        }
      
        var obj = new Rectangle(3, 4);         // 输出 true
        var obj2 = Rectangle(2, 3);         // 报错,ES6 class必须使用new调用
      
    • 应用:创建不能独立使用、必须继承后才能使用的类

        //Class内部调用new.target,返回当前Class;子类继承父类时,返回子类
        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);  // 正确
      

extened 继承

class A {
  constructor() {
    console.log(new.target.name);  // new.target 指向new命令作用于的那个构造函数
  }
}

class B extends A {
  constructor() {
    super();                  // 相当于 A.prototype.constructor.call(this), 这里this指的是B的实例 -- super代表父类的构造函数
  }
}

new A()     // A
new B()     // B

继承机制:

  • ES5:创造子类的实例对象this,再将父类的方法添加到this上面
  • ES6:将父类实例对象的属性和方法加到this上面,再用子类的构造函数修改this
    • 子类必须在constructor方法中调用super方法,且只有调用super之后,才可以使用this关键字,否则新建实例时会报错
    • 子类会继承父类的静态方法,也可通过super对象调用父类的静态方法
  • 获取对象原型: Object.getPrototypeof(obj)
    • 可以用来从子类上获取父类,使用这个方法判断,一个类是否继承了另一个类
    • eg: Object.getPrototypeOf(ColorPoint) === Point 为 true

原生构造函数的继承(原生类的继承):

  • 语言内置的构造函数,ES5无法继承,ES6可以( ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承)
  • ECMAScript 的原生构造函数:
    • Boolean()
    • Number()
    • String()
    • Array()
    • Date()
    • Function()
    • RegExp()
    • Error()
    • Object()
  • 特例:继承Object的子类,无法通过super方法向父类Object传参

      class NewObj extends Object{
        constructor(){
          super(...arguments);
        }
      }
    
      var o = new NewObj({attr: true});
      o.attr === true                                      // false
    
    • 因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数
  • 应用实例:定义一个带版本功能的数组

      class VersionedArray extends Array {
        constructor() {
          super();
          this.history = [[]];
        }
        commit() {
          this.history.push(this.slice());
        }
        revert() {
          this.splice(0, this.length, ...this.history[this.history.length - 1]);
        }
      }
    
      var x = new VersionedArray();
    
      x.push(1);
      x.push(2);
      x                                 // [1, 2]
      x.history                     // [[]]
      x.commit();
      x.history                    // [[], [1, 2]]
    
      x.push(3);
      x                                 // [1, 2, 3]
      x.history                     // [[], [1, 2]]
      x.revert();
      x                                 // [1, 2]
    

super/this 关键字

this: 一般指向该方法运行时所在的环境

  • 在类中:
    • 普通方法中:指向类的实例;
    • 静态方法中:指向类;
  • 在子类中:
    • 普通方法中:指向子类实例;注:用super对象调用父类方法时,父类方法中的this指向的也是子类实例
    • 静态方法中:指向子类;注:用super对象调用父类方法时,父类方法中的this指向的也是子类
  • 在箭头函数中:
    • 指向是固定的,为定义时所在的对象,不是使用时所在的对象(因为箭头函数没有自己的this,只能使用外层代码块的this)
    • 注:箭头函数不能用作构造函数,也不能使用call,apply,bind这些方法去改变this的指向

示例:

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);            // this 指向类的实例
  }
  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
logger.printName();                        // Hello there

注:单独使用类方法,而此类方法中使用this调用其他类方法,可能会报错,eg:将printName方法提取出来单独使用会报错

const { printName } = logger;
printName();                              // TypeError: Cannot read property 'print' of undefined

// 解决方案: 在构造方法中绑定this
class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }
  // ...
}

super

  • super(...): super方法代表父类的构造函数,只能用在子类的构造函数之中,用在其他地方就会报错
  • super.xxx: super对象一般指向当前对象的原型对象, 只能用在对象的方法中
    • 在子类:
      • 普通方法中: super指向父类的原型对象;注:通过super调用父类的方法时,父类方法内部的this指向的是子类实例
      • 静态方法中:super指向父类本身;注:通过super方法调用父类的方法时,父类方法内部的this指向的是子类
  • 注:使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错

示例:

  1. super方法 vs super对象

     class Point {
       constructor(x, y) {
         this.x = x;
         this.y = y;
       }
       p() {
         return 2;
       }
     }
    
     class ColorPoint extends Point {
       constructor(x, y, color) {
         this.color = color;                 // ReferenceError
         super(x, y);                        // 调用父类构造函数
         this.color = color;                 // 正确,super之后,才可以使用this
         console.log(super.p());             // 2 -- super指向父类原型对象,相当于Point.prototype.p()
    
         console.log(super);                   // 报错: 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
    
         // super.valueOf()表明super是一个对象,指向父类原型对象,super使得this指向B的实例,所以返回的是一个B的实例
         console.log(super.valueOf() instanceof B);    // true 
    
       }
     }
    
     let cp = new ColorPoint(25, 8, 'green');
    
     cp instanceof ColorPoint         // true
     cp instanceof Point              // true
    
  2. super对象调用父类静态方法和普通方法

     class Parent {
       static hello() {
         console.log('hello world');
       }
       static myMethod(msg) {
         console.log('static', msg);
       }
       myMethod(msg) {
         console.log('instance', msg);
       }
     }
    
     class Child extends Parent {
       static myMethod(msg) {
         super.myMethod(msg);        // 在子类静态方法中,super对象指向父类本身,调用父类静态方法
       }
       myMethod(msg) {
         super.myMethod(msg);        // 在子类普通方法中,super对象指向父类原型对象,调用父类方法
       }
     }
    
     // 调用子类静态方法
     Child.hello();              // hello world
     Child.myMethod(1);          // static 1
    
     // 调用子类普通方法
     var child = new Child();
     child.myMethod(2);          // instance 2
    

Decorator 修饰器

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;

只能用于修饰类和类属性(eg:不能用于函数,因为存在函数提升),本质即编译时执行的函数 (不是在运行时)

  • 类修饰器
    • 参数(只有一个):
      • target:所要修饰的目标类(即类本身)
  • 类属性修饰器
    • 参数(三个):
      • target : 类的原型对象(类.prototype)-- 修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型
      • name : 修饰的类属性名
      • decriptor:修饰类属性的描述对象
    • 返回:该属性的描述对象Descriptor
  • 修饰器参数扩展:
    • 可以通过在修饰器外面再封装一层函数来传入入其他参数
  • 多个修饰器:
    • 会像剥洋葱一样,先从外到内进入,然后由内向外执行
  • 第三方模块提供的修饰器:
    • core-decorators.js
      • @autobind : 使得方法中的this对象,绑定原始对象
      • @readonly : 使得属性或方法不可写
      • @override : 检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错
      • @deprecate/@deprecated : 在控制台显示一条警告,表示该方法将废除
      • @suppressWarnings : 抑制deprecated修饰器导致的console.warn()调用 (异步代码发出的调用除外)
    • traits-decorator
      • @traits : 效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等

示例:

  1. 类修饰器

     @testable
     class MyTestableClass {
       // ...
     }
    
     function testable(target) {    // target为所要修饰的目标类(类本身),即MyTestClass
       target.isTestable = true;    // 为类MyTestClass添加静态属性isTestable,想添加实例属性,可以通过目标类的prototype对象操作
     }
    
     MyTestableClass.isTestable     // true
    
  2. 类属性修饰器

     function readonly(target, name, descriptor){
       descriptor.writable = false;
       return descriptor;
     }
    
     // descriptor对象原来的值如下
     // {
     //   value: specifiedFunction,
     //   enumerable: false,
     //   configurable: true,
     //   writable: true
     // };
    
     class Person {
       @readonly                                                                // readonly(Person.prototype, 'name', descriptor);
       name() { return `${this.first} ${this.last}` }
     }
    
     class Math {
       @log
       add(a, b) {
         return a + b;
       }
     }
    
     function log(target, name, descriptor) {
       var oldValue = descriptor.value;
    
       descriptor.value = function() {
         console.log(`Calling ${name} with`, arguments);
         return oldValue.apply(this, arguments);
       };
    
       return descriptor;
     }
    
     const math = new Math();
     math.add(2, 4);                            // Calling add with 2,4
    
  3. 多个修饰器

     function dec(id){
       console.log('evaluated', id);
       return (target, property, descriptor) => console.log('executed', id);
     }
    
     class Example {
         @dec(1)
         @dec(2)
         method(){}
     }
    
     const example=new Example();
     example.method();
     // evaluated 1
     // evaluated 2
     // executed 2
     // executed 1
    
  4. 应用: 实现Mixin模式(在一个对象之中混入另外一个对象的方法)

    • 使用类修饰器实现

        // mixins.js
        export function mixins(...list) {              // 可以在修饰器外面再封装一层函数,以便传入其他参数
          return function (target) {
            Object.assign(target.prototype, ...list)   // 添加实例属性
          }
        }
      
        // main.js
        import { mixins } from './mixins'
      
        const Foo = {
          foo() { console.log('foo') }
        };
      
        // 在MyClass类上面“混入”Foo对象的foo方法
        @mixins(Foo)
        class MyClass {}
      
        let obj = new MyClass();
        obj.foo()                                                 // 'foo'
      
    • 通过类的继承实现 Mixin (上面的方法会改写MyClass类的prototype对象)

        // 返回一个继承superclass的子类,该子类包含一个foo方法 
        let MyMixin = (superclass) => class extends superclass {           
          foo() {
            console.log('foo from MyMixin');
          }
        };
      
        class MyClass extends MyMixin(MyBaseClass) {}
      
        let c = new MyClass();
        c.foo();                                                     // "foo from MyMixin"
      
  5. 应用: React 与 Redux 库结合使用

     class MyReactComponent extends React.Component {}
     export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
    
     // 有了装饰器,就可以写成如下方式:
     @connect(mapStateToProps, mapDispatchToProps)
     export default class MyReactComponent extends React.Component {}
    

模块 Module

模块加载方案:

  • CommonJS:用于服务器,动态加载(运行时加载)
  • AMD:用于浏览器,动态加载(运行时加载)
  • ES6:浏览器和服务器通用,静态加载(编译时加载)

动态加载 vs 静态加载:

  • 动态加载:
    • 运行时加载,无法在编译时做“静态优化”
    • 模块输出的是值的拷贝,不存在动态更新
  • 静态加载:
    • 编译时加载,编译时就能确定模块的依赖关系,以及输入和输出的变量;
    • 模块输出的是值的引用(类似Unix 系统的“符号连接”),可动态更新

ES6 Module

  • 自动采用严格模式,不管有没有在模块头部加上"use strict" (ES5引入的)
  • 一个模块就是一个独立的文件,该文件内部的内容,外部无法获取
  • 导出/导入
    • export ... :定义模块的对外接口,可处于模块顶层的任何位置
      • 一个module可以有多条export,但只能有一条export default
      • export default
        • 指定默认输出,这样 import 时就可以指定一个任意名字给加载项
        • 本质上,就是输出一个叫做default的变量或方法 ( 将default后面的值,赋给default变量 ),然后系统允许你为它取任意名字
    • import ... from ...:加载模块,Singleton 模式,静态加载(在静态解析阶段执行,所以它是一个模块之中最早执行的)
      • 静态执行,不能使用表达式和变量,不能使用逻辑判断动态加载
      • 输入的加载项是只读的(本质是输入接口),即不允许在加载模块的脚本里面改写接口
      • 多次重复执行同一句import语句或多次加载同一module,也只会执行一次
      • import <module> : 不导入任何值,仅仅执行所加载的模块,eg: import 'lodash';
    • export ... from ...:
      • 在一个模块之中,输入输出同一个模块,export 和 import 可复合成一条
      • 注: export * from 'xxx' 会忽略export default输出
    • 可使用as重命名加载对象
  • 注:
    • 模块之中,顶层的this指向undefined(CommonJS 模块的顶层this指向当前模块)
    • 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面 -- 但不建议这样使用

示例:

  1. export...

    • export 变量

        // 写法一
        export var m = 1;
      
        // 写法二
        var m = 1;
        export {m};
      
        // 写法三
        var m = 1;
        export {m as n};
      
    • export 方法

        // 写法一
        export function f() {};
      
        //写法二
        function f() {}
        export {f};
      
        // 写法三
        function f() {}
        export {f as fun};
      
    • export 多个,重命名
        export {m,f}
        export {m as n, f as fun, f as fun2}
      
  2. import...from...

    • cicle.js (module export):
        // circle.js
        export function area(radius) {
          return Math.PI * radius * radius;
        }
        export function circumference(radius) {
          return 2 * Math.PI * radius;
        }
      
    • import 部分:

        import { area, circumference } from './circle';
        console.log('圆面积:' + area(4));
        console.log('圆周长:' + circumference(14));
      
        // 输入的变量都是只读的,因为它的本质是输入接口,不允许在加载模块的脚本里面,改写接口
        area.foo = 'hello';            // 合法操作
        area = {};                         // Syntax Error : 'area' is read-only;
      
    • import 所有(整体加载):

        import * as circle from './circle';
        console.log('圆面积:' + circle.area(4));
        console.log('圆周长:' + circle.circumference(14));
      
        // 整体加载所在的那个对象,应该是可以静态分析的,不允许运行时改变
        circle.foo = 'hello';                    // Syntax Error
        circle.area = function () {};        // Syntax Error
      
    • 注:

      • 以上加载部分和整体加载的区别
      • import是静态执行,不能使用表达式和变量

          // 报错
          import { 'f' + 'oo' } from 'my_module';
        
          // 报错
          let module = 'my_module';
          import { foo } from module;    
        
          // 报错
          if (x === 1) {
            import { foo } from 'module1';
          } else {
            import { foo } from 'module2';
          }
        
      • 多次重复执行同一句import语句或多次加载同一module,也只会执行一次

          import 'lodash';       
          import 'lodash';      
          // 只执行一次,等同于:import 'lodash';
        
          import { foo } from 'my_module';
          import { bar } from 'my_module';
          // 只执行一次,等同于:import { foo, bar } from 'my_module';
        
  3. export default ...

    • export和export default混合

        export default function (obj) { ···}
        export function each(obj, iterator, context) {···}
        export { each as forEach };
      
        import _ , { each, forEach } from 'lodash';        // _ 即代表export default的内容
      
    • export和export default比较

        // 第一组:export
        export function crc32() { ...};
        import {crc32} from 'crc32';            // import 使用大括号 {}
      
        // 第二组:export default
        export default function crc32() { ...}  // 同 export default function () { ... }
        import a from 'crc32';                    // import 不使用大括号{},且可直接指定一个任意的名字
      
    • export default 变量(export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句)

        var a = 1; 
        export default a;                     // 正确
        export default var a = 1;            // 错误
      
        export default 42;        // 正确
        export 42;                // 错误
      
  4. export ... from ...

    • 部分导入导出: export {...} from ...

        export { foo, bar } from 'my_module';    // foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口
      
        // 可以简单理解为
        import { foo, bar } from 'my_module';
        export { foo, bar };
      
    • 整体导入导出:export * from ... ( 注:会忽略模块的default方法)

        // 整体输出
        export * from 'my_module';                    // 会忽略模块的default方法
      
        //默认接口的写法
        export { default } from 'my_module';
        export { default as es6 } from './my_module';
      

浏览器环境加载

使用type="module"

  • 默认异步加载
  • 按在页面出现的顺序依次执行加载
  • 模块:
    • 代码是在模块作用域之中运行,而不是在全局作用域运行
    • 可以使用import命令加载其他模块,.js后缀不可省略
    • 顶层的this关键字返回undefined,而不是指向window,利用这个特点,可以侦测当前代码是否在 ES6 模块之中(eg:const isNotModuleScript = this !== undefined;
  • 转码:若浏览器不支持 ES6 Module,可以将其转为 ES5 的写法,eg:使用Babal,SystemJS

示例:

  1. 浏览器加载javascript(使用 type="application/javascript" 默认语言,可省略)

     // 默认同步加载
     <script src="path/to/myModule.js"></script>
    
     // 异步加载
     // defer: 渲染完再执行;多个defer时,会按照它们在页面出现的顺序加载;
     // async:下载完就执行,会中断渲染,执行完成后恢复渲染; 多个async时,不保证加载顺序;
     <script src="path/to/myModule.js" defer></script>
     <script src="path/to/myModule.js" async></script>
    
  2. 浏览器加载ES6 Module

     <script type="module" src="./foo.js"></script>
     <!-- 等同于 -->
     <script type="module" src="./foo.js" defer></script>
    

Node环境加载

CommonJS Module VS ES6 Module:

操作 CommonJS Module ES6 Module
输出 值的拷贝,不存在动态更新 值的引用,可动态更新
加载 运行时加载,无法做“静态优化”
(加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才会生成)
编译时加载,可做“静态优化”
(只是一种静态定义,编译时就能确定模块的依赖关系,以及输入和输出的变量)
循环加载 使用require命令加载脚本
( 返回的是当前已经执行的部分的值,而不是代码全部执行后的值)
使用import命令加载
( 返回的是引用,需要开发者自己保证,真正取值的时候能够取到值)
第一次加载: 会执行整个脚本,在内存中生成一个对象;
第N次加载:不会再执行,直接到缓存中取值,除非手动清除缓存
加载项不会被缓存,已加载项不会重复加载

注:

  1. CommonJS Module第一次加载在内存中生成的对象如下:

     {
       id: '...',               // 模块名
       exports: { ... },        // 模块输出的各个接口(以后需要用到这个模块的时候,就会到exports属性上面取值)
       loaded: true,            // 表示该模块的脚本是否执行完毕
       ...
     }
    
  2. CommonJS Module 循环加载示例:(加载返回的是当前已经执行的部分的值,而不是代码全部执行后的值)

    • a.js : 加载 b.js
        exports.done = false;
        var b = require('./b.js');
        console.log('在 a.js 之中,b.done = %j', b.done);
        exports.done = true;
        console.log('a.js 执行完毕');
      
    • b.js : 加载 a.js
        exports.done = false;
        var a = require('./a.js');
        console.log('在 b.js 之中,a.done = %j', a.done);
        exports.done = true;
        console.log('b.js 执行完毕');
      
    • main.js : 加载 a.js,b.js
        var a = require('./a.js');
        var b = require('./b.js');
        console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
      
    • node执行

        $ node main.js
      
        在 b.js 之中,a.done = false
        b.js 执行完毕
        在 a.js 之中,b.done = true
        a.js 执行完毕
        在 main.js 之中, a.done=true, b.done=true
      

Node 模板加载方案:

Node 有自己的 CommonJS 模块格式,与 ES6 模块格式不兼容,所以ES6 模块和 CommonJS 需采用各自的加载方案

  • ES6
    • 采用.mjs后缀文件名
    • 使用export/import命令,不能使用require命令
  • CommonJS
    • 使用module.export/require命令
  • ES6 module 加载 CommonJS module:
    • 使用 import ... from ... 命令
    • Node 会将module.exports属性,当作模块的默认输出,即等同于export default ...
  • CommonJS module 加载 ES6 module:
    • 使用import(...)函数
    • ES6 模块的所有输出接口,会成为输入对象的属性(注:不能使用require命令)
  • 注:
    • 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但不建议这样使用
    • ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量
      • ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
      • 在 ES6 模块之中不存在的顶层变量:arguments,require,module,exports,filename,dirname

说明:

  1. import ... from ... 命令:异步加载,只支持加载本地模块,不支持加载远程模块
    • 模块名不含路径:会去node_modules目录寻找这个模块
    • 模块名包含路径:会按照路径去寻找这个名字的脚本文件
    • 省略了后缀名的加载,依次尝试:
      • 依次尝试四个后缀名:mjs,js,json,node;
      • 尝试加载该目录下的package.json的main字段指定的脚本;
      • 尝试加载该目录下的名为index,后缀为mjs,js,json,node的文件
  2. import(...)函数:同步加载
    • 返回一个Promise对象,实现动态加载,类似于 Node 的require方法(异步加载)
    • 与所加载的模块没有静态连接关系,可以用在任何地方,非模块的脚本也可以使用

示例:

  1. ES6 Module加载CommonJS Module : 使用 import...from...命令

    • a.js (CommonJS module): CommonJS模块的输出都定义在module.exports这个属性上面

        module.exports = {
          foo: 'hello',
          bar: 'world'
        };
      
        // 等同于 ES6:
        export default {
          foo: 'hello',
          bar: 'world'
        };
      
    • ES6 Module导入a.js (注:需使用整体输入):使用Node的import...from...命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default ...

        // 写法一
        import baz from './a';
        // baz = {foo: 'hello', bar: 'world'};
      
        // 写法二
        import {default as baz} from './a';
        // baz = {foo: 'hello', bar: 'world'};
      
        // 写法三
        import * as baz from './a';
        // baz = {
        //   get default() {return module.exports;},
        //   get foo() {return this.default.foo}.bind(baz),
        //   get bar() {return this.default.bar}.bind(baz)
        // }
        baz.default         // {foo: 'hello', bar: 'world'}
        baz.foo                // hello
      
        // 注意:ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,只有在运行时才能确定foo,所以下面方式不正确
        import { foo } from './a'
      
  2. CommonJS module加载ES6 module: 使用import(...)函数(注:不能使用require命令),ES6 模块的所有输出接口,会成为输入对象的属性

    • ES6 Module:es.js
        export let foo = { bar:'my-default' };
        export { foo as bar };
        export function f() {};
        export class c {};
      
    • CommonJS 导入 es.js
        const es_namespace = await import('./es');
        // es_namespace = {
        //   get foo() {return foo;}
        //   get bar() {return foo;}
        //   get f() {return f;}
        //   get c() {return c;}
        // }
      

实践

var/let/const

  1. let取代var

    • 两者语义相同,且let没有副作用:var命令存在变量提升效用,let命令没有这个问题,let只在其声明的代码块内有效
  2. 优先使用const

    • 防止了无意间修改变量值所导致的错误(函数应该都设置为const)
    • const比较符合函数式编程思想,运算不改变值,只是新建值,这样也有利于将来的分布式运算;
    • JavaScript编译器会对const进行优化,有利于提高程序的运行效率(let和const的本质区别,其实是编译器内部的处理不同)
    • 长远来看,JavaScript 可能会有多线程的实现,const利于保证线程安全

解构赋值

以下情况优先使用解构赋值

  1. 使用数组成员对变量赋值

     const arr = [1, 2, 3, 4];
    
     // bad
     const first = arr[0];
     const second = arr[1];
    
     // good
     const [first, second] = arr;
    
  2. 函数的参数是对象的成员

     // bad
     function getFullName(user) {
       const firstName = user.firstName;
       const lastName = user.lastName;
     }
    
     // good
     function getFullName(obj) {
       const { firstName, lastName } = obj;
     }
    
     // best
     function getFullName({ firstName, lastName }) {
     }
    
  3. 函数返回多个值,优先使用对象的解构赋值(注:不是数组的解构赋值),便于以后添加返回值,以及更改返回值的顺序

     // bad
     function processInput(input) {
       return [left, right, top, bottom];
     }
    
     // good
     function processInput(input) {
       return { left, right, top, bottom };
     }
    
     const { left, right } = processInput(input);
    

String

  • 静态字符串: 使用单引号或反引号
  • 动态字符串: 使用反引号
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;

Array

  1. 拷贝数组: 使用扩展运算符...

     // bad
     const len = items.length;
     const itemsCopy = [];
     let i;
    
     for (i = 0; i < len; i++) {
       itemsCopy[i] = items[i];
     }
    
     // good
     const itemsCopy = [...items];
    
  2. 对象转为数组: 使用 Array.from 方法

     const foo = document.querySelectorAll('.foo');
     const nodes = Array.from(foo);
    

Object

  1. 定义对象

    • 单行定义:最后一个成员不以逗号结尾
    • 多行定义:最后一个成员可以逗号结尾

      // bad
      const a = { k1: v1, k2: v2, };
      const b = {
      k1: v1,
      k2: v2
      };
      
      // good
      const a = { k1: v1, k2: v2 };
      const b = {
      k1: v1,
      k2: v2,
      };
      
  2. 尽量静态化

    • 定义后尽量不添加新的属性
    • 使用Object.assign方法添加属性

        // bad
        const a = {};
        a.x = 3;
      
        // if reshape unavoidable
        const a = {};
        Object.assign(a, { x: 3 });
      
        // good
        const a = { x: null };
        a.x = 3;
      
    • 动态属性名: 可在创造对象的时候,用属性表达式定义
        // bad
        const obj = {
            id: 5,
            name: 'San Francisco',
        };
        obj[getKey('enabled')] = true;
      
        // good
        const obj = {
          id: 5,
          name: 'San Francisco'
          ,[getKey('enabled')]: true,
        };
      
  3. 尽量简洁表达属性和方法(易于描述和书写)

     var ref = 'some value';
    
     // bad
     const atom = {
       ref: ref,
       value: 1,
       addValue: function (value) {
         return atom.value + value;
       },
     };
    
     // good
     const atom = {
       ref,
       value: 1,
       addValue(value) {
         return atom.value + value;
       },
     };
    

Function

  1. 建议尽量使用箭头函数的情况:

    • 立即执行函数
        (() => {
          console.log('Welcome to the Internet.');
        })();
      
    • 原来一些需要使用函数表达式的场合

        // bad
        [1, 2, 3].map(function (x) {
          return x * x;
        });
      
        // good
        [1, 2, 3].map((x) => {
          return x * x;
        });
      
        // best
        [1, 2, 3].map(x => x * x);
      
    • 取代Function.prototype.bind(不再用 self/_this/that 绑定 this)

        // bad
        const self = this;
        const boundMethod = function(...params) {
          return method.apply(self, params);
        }
      
        // acceptable
        const boundMethod = method.bind(this);
      
        // best
        const boundMethod = (...params) => method.apply(this, params);
      
    • 注:简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法
  2. 函数参数

    • 使用rest运算符...代替arguments变量 (arguments 是一个类似数组的对象,而rest运算符可以提供一个真正的数组)

        // bad
        function concatenateAll() {
          const args = Array.prototype.slice.call(arguments);
          return args.join('');
        }
      
        // good
        function concatenateAll(...args) {
          return args.join('');
        }
      
    • 使用默认值语法设置参数的默认值

        // bad
        function handleThings(opts) {
          opts = opts || {};
        }
      
        // good
        function handleThings(opts = {}) {
          // ...
        }
      

Class

  1. 用Class取代需要 prototype的操作(Class语法更简洁易理解)

     // bad
     function Queue(contents = []) {
       this._queue = [...contents];
     }
     Queue.prototype.pop = function() {
       const value = this._queue[0];
       this._queue.splice(0, 1);
       return value;
     }
    
     // good
     class Queue {
       constructor(contents = []) {
         this._queue = [...contents];
       }
       pop() {
         const value = this._queue[0];
         this._queue.splice(0, 1);
         return value;
       }
     }
    
  2. 用extends实现继承(extend语法更简单,且不会有破坏instanceof运算的危险)

     // bad
     const inherits = require('inherits');
     function PeekableQueue(contents) {
       Queue.apply(this, contents);
     }
     inherits(PeekableQueue, Queue);
     PeekableQueue.prototype.peek = function() {
       return this._queue[0];
     }
    
     // good
     class PeekableQueue extends Queue {
       peek() {
         return this._queue[0];
       }
     }
    

Module

  1. 使用import取代require

     // bad
     const moduleA = require('moduleA');
     const func1 = moduleA.func1;
     const func2 = moduleA.func2;
    
     // good
     import { func1, func2 } from 'moduleA';
    
  2. 使用export取代module.exports

     // commonJS的写法
     var React = require('react');
     var Breadcrumbs = React.createClass({
       render() {
         return <nav />;
       }
     });
     module.exports = Breadcrumbs;
    
     // ES6的写法
     import React from 'react';
     class Breadcrumbs extends React.Component {
       render() {
         return <nav />;
       }
     };
     export default Breadcrumbs;
    
  3. 尽量不使用通配符来确保至少有一个默认输出(export default)

     // bad
     import * as myObject from './importModule';
    
     // good
     import myObject from './importModule';
    

ESLint

一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码

  1. 安装

     // 安装ESLint
     $ npm i -g eslint
    
     //安装 Airbnb 语法规则
     $ npm i -g eslint-config-airbnb
    
     //安装import、a11y、react 插件
     $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
    
  2. 配置(项目的根目录下新建一个.eslintrc文件)

     {
       "extends": "eslint-config-airbnb"
     }
    
  3. 使用,eg:

    • index.js

        var unusued = 'I have no purpose!';
      
        function greet() {
            var message = 'Hello, World!';
            alert(message);
        }
      
        greet();
      
    • 使用 ESLint 检查index.js,发现错误

        $ eslint index.js
        index.js
          1:1  error  Unexpected var, use let or const instead          no-var
          1:5  error  unusued is defined but never used                 no-unused-vars
          4:5  error  Expected indentation of 2 characters but found 4  indent
          4:5  error  Unexpected var, use let or const instead          no-var
          5:5  error  Expected indentation of 2 characters but found 4  indent
      
        × 5 problems (5 errors, 0 warnings)
      
      • 不应该使用var命令,而要使用let或const
      • 定义了变量,却没有使用
      • 行首缩进为 4 个空格,而不是规定的 2 个空格

Reference