ES6新纪元下

作者: 追逐_e6cf | 来源:发表于2019-03-02 21:35 被阅读42次

    七、Iterator

    迭代器是一种接口、是一种机制。
    为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
    Iterator 的作用有三个:

    1. 为各种数据结构,提供一个统一的、简便的访问接口;
    2. 使得数据结构的成员能够按某种次序排列;
    3. 主要供for...of消费。
      Iterator本质上,就是一个指针对象。
      过程是这样的:
      (1)创建一个指针对象,指向当前数据结构的起始位置。
      (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
      (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
      (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
      普通函数实现Iterator。
    function myIter(obj){
      let i = 0;
      return {
        next(){
          let done = (i>=obj.length);
          let value = !done ? obj[i++] : undefined;
          return {
            value,
            done,
          }
        }
      }
    }
    

    原生具备 Iterator 接口的数据结构如下:Array、Map、Set、String、函数的arguments对象、NodeList对象。
    下面的例子是数组的Symbol.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 }
    

    下面是另一个类似数组的对象调用数组的Symbol.iterator方法的例子。

    let iterable = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3,
      [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
      console.log(item); // 'a', 'b', 'c'
    }
    

    注意,普通对象部署数组的Symbol.iterator方法,并无效果。

    let iterable = {
      a: 'a',
      b: 'b',
      c: 'c',
      length: 3,
      [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
      console.log(item); // undefined, undefined, undefined
    }
    

    字符串是一个类似数组的对象,也原生具有 Iterator 接口。

    var someString = "hi";
    typeof someString[Symbol.iterator]
    // "function"
    
    var iterator = someString[Symbol.iterator]();
    
    iterator.next()  // { value: "h", done: false }
    iterator.next()  // { value: "i", done: false }
    iterator.next()  // { value: undefined, done: true }
    

    八、Generator

    1. 基本概念。
      Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
      执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
      跟普通函数的区别:
    • function关键字与函数名之间有一个星号;
    • 函数体内部使用yield表达式,定义不同的内部状态。
    • Generator函数不能跟new一起使用,会报错。
    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    
    var hw = helloWorldGenerator();
    

    上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
    调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。
    下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
    ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· }
    function*foo(x, y) { ··· }
    
    1. yield 表达式。
      由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
      遍历器对象的next方法的运行逻辑如下。
      (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
      (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
      (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
      (4)如果该函数没有return语句,则返回的对象的value属性值为undefined
      yield表达式与return语句的相同之处:
      都能返回紧跟在语句后面的那个表达式的值。
      yield表达式与return语句的不同之处:
      每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield
      yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
      另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
    console.log('Hello' + yield 123); // SyntaxError
    console.log('Hello' + (yield 123)); // OK
    
    1. 与Iterator接口的关系。
      由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
    Object.prototype[Symbol.iterator] = function* (){
      for(let i in this){
        yield this[i];
      }
    }
    //--------------
    function* iterEntries(obj) {
      let keys = Object.keys(obj);
      for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
      }
    }
    
    let myObj = { foo: 3, bar: 7 };
    
    for (let [key, value] of iterEntries(myObj)) {
      console.log(key, value);
    }
    
    1. next方法的参数。
      yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    
    var g = f();
    
    g.next() // { value: 0, done: false }
    g.next() // { value: 1, done: false }
    g.next(true) // { value: 0, done: false }
    

    Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);
    a.next() // Object{value:6, done:false}
    a.next() // Object{value:NaN, done:false}
    a.next() // Object{value:NaN, done:true}
    
    var b = foo(5);
    b.next() // { value:6, done:false }
    b.next(12) // { value:8, done:false }
    b.next(13) // { value:42, done:true }
    
    1. for...of 循环。
      for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。
    function *foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3 4 5
    
    function* fibonacci() {
      let [prev, curr] = [1, 1];
      while(true){
        [prev, curr] = [curr, prev + curr];
        yield curr;
      }
    }
    
    for (let n of fibonacci()) {
      if (n > 10000000) break;
      console.log(n);
    }
    
    1. Generator.prototype.return()
      Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    var g = gen();
    
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true }
    g.next()        // { value: undefined, done: true }
    
    1. yield*
      如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
    function* foo() {
      yield 'a';
      yield 'b';
    }
    
    function* bar() {
      yield 'x';
      foo();
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "y"
    

    foobar都是 Generator 函数,在bar里面调用foo,是不会有效果的。
    这个就需要用到yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

    function* bar() {
      yield 'x';
      yield* foo();
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      yield 'a';
      yield 'b';
      yield 'y';
    }
    
    // 等同于
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    
    for (let v of bar()){
      console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    

    再来看一个对比的例子。

    function* inner() {
      yield 'hello!';
    }
    
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    
    var gen = outer1()
    gen.next().value // "open"
    gen.next().value // 返回一个遍历器对象
    gen.next().value // "close"
    
    function* outer2() {
      yield 'open'
      yield* inner()
      yield 'close'
    }
    
    var gen = outer2()
    gen.next().value // "open"
    gen.next().value // "hello!"
    gen.next().value // "close"
    

    上面例子中,outer2使用了yield*outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。
    从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

    1. 作为对象属性的 Generator 函数
      如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
    let obj = {
      * myGeneratorMethod() {
        ···
      }
    };
    

    九、async函数

    1. 含义。
      ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是 Generator 函数的语法糖。
      async函数使用时就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
      async函数对 Generator 函数的区别:
      (1)内置执行器。
      Generator 函数的执行必须靠执行器,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
      (2)更好的语义。
      asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
      (3)正常情况下,await命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve的 Promise 对象。
      (4)返回值是 Promise。
      async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
      进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
    2. 错误处理。
      如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject
    async function f() {
      await new Promise(function (resolve, reject) {
        throw new Error('出错了');
      });
    }
    
    f()
    .then(v => console.log(v))
    .catch(e => console.log(e))
    // Error:出错了
    

    上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,可以参考后文的“async 函数的实现原理”。
    防止出错的方法,也是将其放在try...catch代码块之中。

    async function f() {
      try {
        await new Promise(function (resolve, reject) {
          throw new Error('出错了');
        });
      } catch(e) {
      }
      return await('hello world');
    }
    

    如果有多个await命令,可以统一放在try...catch结构中。

    async function main() {
      try {
        const val1 = await firstStep();
        const val2 = await secondStep(val1);
        const val3 = await thirdStep(val1, val2);
    
        console.log('Final: ', val3);
      }
      catch (err) {
        console.error(err);
      }
    }
    
    1. 应用
    var fn = function (time) {
      console.log("开始处理异步");
      setTimeout(function () {
        console.log(time);
        console.log("异步处理完成");
        iter.next();
      }, time);
    
    };
    
    function* g(){
      console.log("start");
      yield fn(3000)
      yield fn(500)
      yield fn(1000)
      console.log("end");
    }
    
    let iter = g();
    iter.next();
    

    下面是async函数的写法。

    var fn = function (time) {
      return new Promise(function (resolve, reject) {
        console.log("开始处理异步");
        setTimeout(function () {
          resolve();
          console.log(time);
          console.log("异步处理完成");
        }, time);
      })
    };
    
    var start = async function () {
      // 在这里使用起来就像同步代码那样直观
      console.log('start');
      await fn(3000);
      await fn(500);
      await fn(1000);
      console.log('end');
    };
    
    start();
    

    十、Class

    1. 用法
      class跟let、const一样:不存在变量提升、不能重复声明。
      ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
      ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
    //es5
    function Fn(x, y) {
      this.x = x;
      this.y = y;
    }
    
    Fn.prototype.add = function () {
      return this.x + this.y;
    };
    
    //等价于
    //es6
    class Fn{
      constructor(x,y){
        this.x = x;
        this.y = y;
      }
      
      add(){
        return this.x + this.y;
      }
    }
    
    var F = new Fn(1, 2);
    console.log(F.add()) //3
    

    构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

    class Fn {
      constructor() {
        // ...
      }
    
      add() {
        // ...
      }
    
      sub() {
        // ...
      }
    }
    
    // 等同于
    
    Fn.prototype = {
      constructor() {},
      add() {},
      sub() {},
    };
    

    类的内部所有定义的方法,都是不可枚举的(non-enumerable),这与es5不同。

    //es5
    var Fn = function (x, y) {
      // ...
    };
    
    Fn.prototype.add = function() {
      // ...
    };
    
    Object.keys(Fn.prototype)
    // ["add"]
    Object.getOwnPropertyNames(Fn.prototype)
    // ["constructor","add"]
    
    //es6
    class Fn {
      constructor(x, y) {
        // ...
      }
    
      add() {
        // ...
      }
    }
    
    Object.keys(Fn.prototype)
    // []
    Object.getOwnPropertyNames(Fn.prototype)
    // ["constructor","add"]
    
    1. 严格模式
      类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
      考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
    2. constructor
      constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
    class Fn {
    }
    
    // 等同于
    class Fn {
      constructor() {}
    }
    

    constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

    class Foo {
      constructor() {
        return Object.create(null);
      }
    }
    
    new Foo() instanceof Foo
    // false
    //constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
    
    1. 类必须使用new调用
      类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
    class Foo {
      constructor() {
        return Object.create(null);
      }
    }
    
    Foo()
    // TypeError: Class constructor Foo cannot be invoked without 'new'
    
    1. Class表达式
      与函数一样,类也可以使用表达式的形式定义。
    const MyClass = class Me {
      getClassName() {
        return Me.name;
      }
    };
    

    上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是MeMe只在 Class 的内部代码可用,指代当前类。

    let inst = new MyClass();
    inst.getClassName() // Me
    Me.name // ReferenceError: Me is not defined
    

    如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

    const MyClass = class { /* ... */ };
    

    采用 Class 表达式,可以写出立即执行的 Class。

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

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

    1. 私有方法和私有属性
      私有方法/私有属性是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。
      通常是在命名上加以区别。
    class Fn {
    
      // 公有方法
      foo () {
        //....
      }
    
      // 假装是私有方法(其实外部还是可以访问)
      _bar() {
        //....
      }
    }
    
    1. 原型的属性
      class定义类时,只能在constructor里定义属性,在其他位置会报错。
      如果需要在原型上定义方法可以使用:
    • Fn.prototype.prop = value;
    • Object.getPrototypeOf()获取原型,再来扩展
    • Object.assign(Fn.prototype,{在这里面写扩展的属性或者方法})
    1. Class的静态方法
      类相当于实例的原型,所有在类中定义的方法,都会被实例继承。
      如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
      ES6 明确规定,Class 内部只有静态方法,没有静态属性。
    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    Foo.classMethod() // 'hello'
    
    var foo = new Foo();
    foo.classMethod()
    // TypeError: foo.classMethod is not a function
    
    //静态属性只能手动设置
    class Foo {
    }
    
    Foo.prop = 1;
    Foo.prop // 1
    
    1. get、set
      存值函数和取值函数
    class Fn{
        constructor(){
            this.arr = []
        }
        get bar(){
            return this.arr;
        }
        set bar(value){
            this.arr.push(value)
        }
    }
    
    
    let obj = new Fn();
    
    obj.menu = 1;
    obj.menu = 2;
    
    console.log(obj.menu)//[1,2]
    console.log(obj.arr)//[1,2]
    
    1. 继承
    class Fn {
    }
    
    class Fn2 extends Fn {
    }
    

    子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

    class Point { /* ... */ }
    
    class ColorPoint extends Point {
      constructor() {
        // super()//必须调用
      }
    }
    
    let cp = new ColorPoint(); // ReferenceError
    

    父类的静态方法也会被继承。

    1. Object.getPrototypeOf()
      Object.getPrototypeOf方法可以用来从子类上获取父类。
    Object.getPrototypeOf(Fn2) === Fn
    // true
    

    因此,可以使用这个方法判断,一个类是否继承了另一个类。

    1. super关键字
      super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
      第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
      作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
    class A {}
    
    class B extends A {
      constructor() {
        super();
      }
    }
    

    上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。
    注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)
    第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

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

    上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()
    由于this指向子类,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

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

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

    十一、Module

    1. export命令
      模块功能主要由两个命令构成:exportimport
      export命令用于规定模块的对外接口。
      import命令用于输入其他模块提供的功能。
      一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
      export输出变量的写法:
    // profile.js
    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    

    还可以一起导出。

    // profile.js
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    
    export {firstName, lastName, year};
    //跟上面写法等价,推荐这种写法。
    

    export命令除了输出变量,还可以输出函数或类(class)。

    export function multiply(x, y) {
      return x * y;
    };
    

    通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

    function v1() { ... }
    function v2() { ... }
    
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
    

    export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

    // 报错
    export 1;
    
    // 报错
    var m = 1;
    export m;
    //正确写法
    // 写法一
    export var m = 1;
    
    // 写法二
    var m = 1;
    export {m};
    
    // 写法三
    var n = 1;
    export {n as m};
    

    同样的,functionclass的输出,也必须遵守这样的写法。

    // 报错
    function f() {}
    export f;
    
    // 正确
    export function f() {};
    
    // 正确
    function f() {}
    export {f};
    

    export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。但是不建议这样做。

    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);
    

    上面代码输出变量foo,值为bar,500 毫秒之后变成baz
    export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下面的import命令也是如此

    1. import命令
      使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
    // main.js
    import {firstName, lastName, year} from './profile';
    
    function setName(element) {
      element.textContent = firstName + ' ' + lastName;
    }
    

    上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
    如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

    import { lastName as surname } from './profile';
    

    import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。
    注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

    foo();
    
    import { foo } from 'my_module';
    //import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
    

    由于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 { foo } from 'my_module';
    import { bar } from 'my_module';
    
    // 等同于
    import { foo, bar } from 'my_module';
    

    除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
    注意,模块整体加载所在的那个对象,不允许运行时改变。下面的写法都是不允许的。

    import * as circle from './circle';
    
    // 下面两行都是不允许的
    circle.foo = 'hello';
    circle.area = function () {};
    
    1. export default
      使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。
      为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
    // export-default.js
    export default function () {
      console.log('foo');
    }
    

    其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    // import-default.js
    import customName from './export-default';
    customName(); // 'foo'
    

    需要注意的是,这时import命令后面,不使用大括号。
    export default命令用在非匿名函数前,也是可以的。

    // export-default.js
    export default function foo() {
      console.log('foo');
    }
    
    // 或者写成
    
    function foo() {
      console.log('foo');
    }
    
    export default foo;
    

    上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。
    比较一下默认输出和正常输出。

    // 第一组
    export default function crc32() { // 输出
      // ...
    }
    
    import crc32 from 'crc32'; // 输入
    
    // 第二组
    export function crc32() { // 输出
      // ...
    };
    
    import {crc32} from 'crc32'; // 输入
    

    上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。
    export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。
    本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

    // modules.js
    function add(x, y) {
      return x * y;
    }
    export {add as default};
    // 等同于
    // export default add;
    
    // app.js
    import { default as foo } from 'modules';
    // 等同于
    // import foo from 'modules';
    

    正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

    // 正确
    export var a = 1;
    
    // 正确
    var a = 1;
    export default a;
    
    // 错误
    export default var a = 1;
    

    上面代码中,export default a的含义是将变量a的值赋给变量default。所以,最后一种写法会报错。
    同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

    // 正确
    export default 42;
    
    // 报错
    export 42;
    
    1. export和import的复合写法
      如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
    export { foo, bar } from 'my_module';
    
    // 等同于
    import { foo, bar } from 'my_module';
    export { foo, bar };
    

    模块的接口改名和整体输出,也可以采用这种写法。

    // 接口改名
    export { foo as myFoo } from 'my_module';
    
    // 整体输出
    export * from 'my_module';
    

    相关文章

      网友评论

        本文标题:ES6新纪元下

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