ES6常用知識點總結(下)

慕斯不想說話發表於2019-04-13

目錄導航

 17、Generator

  是 ES6 提供的一種非同步程式設計解決方案。 語法上是一個狀態機,封裝了多個內部狀態 。執行 Generator 函式會返回一個遍歷器物件。這一點跟promise很像,promise是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。
  Generator 函式是一個普通函式,但是有兩個特徵。

1、function關鍵字與函式名之間有一個星號(位置不固定);

2、函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    var hw = helloWorldGenerator();
    hw.next()  // { value: 'hello', done: false }
    hw.next()// { value: 'world', done: false }
    hw.next()// { value: 'ending', done: true }
    hw.next() // { value: undefined, done: true }
複製程式碼

  該函式有三個狀態:hello,world 和 return 語句(結束執行)。呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件(Iterator Object)。下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態(執行yield後面的語句,直到遇到yield或者return語句)。

 17.1、 yield表示式

  yield表示式就是暫停標誌。並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。yield表示式後面的表示式,只有當呼叫next方法、內部指標指向該語句時才會執行。   yield表示式與return語句既有相似之處,也有區別。相似之處在於,都能返回緊跟在語句後面的那個表示式的值。區別在於每次遇到yield,函式暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。

  注意:

1、 yield表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。
2、 yield表示式如果用在另一個表示式之中,必須放在圓括號裡面。

    function* demo() {
      console.log('Hello' + yield); // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
      console.log('Hello' + (yield)); // OK
      console.log('Hello' + (yield 123)); // OK
    }
複製程式碼

3、 yield表示式用作函式引數或放在賦值表示式的右邊,可以不加括號。

    function* demo() {
      foo(yield 'a', yield 'b'); // OK
      let input = yield; // OK
    }
複製程式碼

  任意一個物件的Symbol.iterator方法,等於該物件的遍歷器生成函式,呼叫該函式會返回該物件的一個遍歷器物件。
  Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    [...myIterable] // [1, 2, 3]
複製程式碼

  Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。

    function* gen(){
      // some code
    }
    var g = gen();
    g[Symbol.iterator]() === g   // true
複製程式碼

 17.2、 next方法的引數

  yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。 從語義上講,第一個next方法用來啟動遍歷器物件,所以不用帶有引數。

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    var g = f();
    console.log(g.next()) // { value: 0, done: false }
    console.log (g.next()) // { value: 1, done: false }
    console.log (.next(true) )// { value: 0, done: false } 執行i=-1,然後i++變成了0
複製程式碼

  再看下面的一個例子

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    var a = foo(5);
    console.log(a.next()) // Object{value:6, done:false}
    console.log(a.next()) // Object{value:NaN, done:false},此時的y等於undefined
    console.log(a.next()) // Object{value:NaN, done:true}
    var b = foo(5);
    console.log(b.next()) // { value:6, done:false }
    console.log(b.next(12)) // { value:8, done:false } 此時的y=2*12 
    console.log(b.next(13)) // { value:42, done:true } 5+24+13
複製程式碼

  通過next方法的引數,向 Generator 函式內部輸入值的例子。

    //例子1
    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`);
      console.log(`2. ${yield}`);
      return 'result';
    }
    let genObj = dataConsumer();
    genObj.next();// Started。執行了 console.log('Started');和`1. ${yield}`這兩句
    genObj.next('a') // 1. a。執行了 console.log(`1. ${yield}`);和`2. ${yield}`這兩句
    console.log(genObj.next('b') )   //2.b    {value: "result", done: true}。執行了console.log(`2. ${yield}`);和return 'result';這兩句
複製程式碼

  上面的console.log(1. ${yield});分兩步執行,首先執行yield,等到執行next()時再執行console.log();

    //例子2
    function* dataConsumer() {
      console.log('Started');
      yield 1;
      yield;
      var a=yield;
      console.log("1. "+a);
      var b=yield;
      console.log("2. "+b);
      return 'result';
    }
    let genObj = dataConsumer();
    console.log( genObj.next())
    console.log(genObj.next());
    console.log(genObj.next('a'))
    console.log( genObj.next('b'));
複製程式碼

  輸出結果如下:四次輸出結果如紅線框中所示

ES6常用知識點總結(下)
  結果分析:第一次呼叫next(),執行到yield 1結束;第二次呼叫next()執行到yield結束;第三次呼叫next("a")執行 var a=yield中的yield;第四次呼叫next("b")方法呼叫var a=yield語句和var b=yield中的yield;

 17.3、 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
複製程式碼

  一旦next方法的返回物件的done屬性為true,for...of迴圈就會中止,且不包含該返回物件,所以上面程式碼的return語句返回的6,不包括在for...of迴圈之中。
  除了for...of迴圈以外,擴充套件運算子(...)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的 Iterator 物件,作為引數,並且遇到Generator 函式中的return語句結束。

    function* numbers () {
      yield 1
      yield 2
      return 3
      yield 4
    }
    // 擴充套件運算子
    [...numbers()] // [1, 2]
    // Array.from 方法
    Array.from(numbers()) // [1, 2]
    // 解構賦值
    let [x, y] = numbers();
    x // 1
    y // 2
    // for...of 迴圈
    for (let n of numbers()) {
      console.log(n)
    }
    // 1,2
複製程式碼

 17.4、 Generator.prototype.throw()

  在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。如果是全域性throw()命令,只能被函式體外的catch語句捕獲。

    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('內部捕獲', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');//被內部捕獲,所以下面的程式碼還能正常執行
      i.throw('b');//被外部捕獲
    } catch (e) {
      console.log('外部捕獲', e);
    }
    // 內部捕獲 a
    // 外部捕獲 b
複製程式碼

  如果 Generator 函式內部沒有部署try...catch程式碼塊,那麼throw方法丟擲的錯誤,將被外部try...catch程式碼塊捕獲。

    var g = function* () {
      while (true) {
        yield;
        console.log('內部捕獲', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');//被外部捕獲,所以下面的程式碼不執行了
      i.throw('b');
    } catch (e) {
      console.log('外部捕獲', e);
    }
    // 外部捕獲 a
複製程式碼

  如果 Generator 函式內部和外部,都沒有部署try...catch程式碼塊,那麼程式將報錯,直接中斷執行。 throw方法丟擲的錯誤要被內部捕獲,前提是必須至少執行過一次next方法。

    function* gen() {
      try {
        yield 1;
      } catch (e) {
        console.log('內部捕獲');
      }
    }
    
    var g = gen();
    g.throw(1);
    // Uncaught 1
複製程式碼

  throw方法被捕獲以後,會附帶執行下一條yield表示式。也就是說,會附帶執行一次next方法。

    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
複製程式碼

  另外,throw命令與g.throw方法是無關的,兩者互不影響。

    var gen = function* gen(){
      yield console.log('hello');
      yield console.log('world');
    }
    
    var g = gen();
    g.next();
    
    try {
      throw new Error();
    } catch (e) {
      g.next();
    }
    // hello
    // world
複製程式碼

  一旦 Generator 執行過程中丟擲錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此後還呼叫next方法,將返回一個value屬性等於undefined、done屬性等於true的物件,即 JavaScript 引擎認為這個 Generator 已經執行結束了。

    function* g() {
      yield 1;
      console.log('throwing an exception');
      throw new Error('generator broke!');//中斷函式的執行
      yield 2;
      yield 3;
    }
    
    function log(generator) {
      var v;
      console.log('starting generator');
      try {
        v = generator.next();
        console.log('第一次執行next方法', v);
      } catch (err) {
        console.log('捕捉錯誤', v);
      }
      try {
        v = generator.next();
        console.log('第二次執行next方法', v);//因為上面程式碼呼叫時報錯了,所以不會執行該語句
      } catch (err) {
        console.log('捕捉錯誤', v);
      }
      try {
        v = generator.next();
        console.log('第三次執行next方法', v);
      } catch (err) {
        console.log('捕捉錯誤', v);
      }
      console.log('caller done');
    }
    log(g());
    // starting generator
    // 第一次執行next方法 { value: 1, done: false }
    // throwing an exception
    // 捕捉錯誤 { value: 1, done: false }
    // 第三次執行next方法 { value: undefined, done: true }
    // caller done
複製程式碼

17.5、 Generator.prototype.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 }
複製程式碼

  如果 Generator 函式內部有try...finally程式碼塊,且正在執行try程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行。

    function* numbers () {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers();
    g.next() // { value: 1, done: false }
    g.next() // { value: 2, done: false }
    g.return(7) // { value: 4, done: false }
    g.next() // { value: 5, done: false }
    g.next() // { value: 7, done: true }
    g.next() // { value: undefined, done: true }
複製程式碼

17.6、 next()、throw()、return()的共同點及區別

  它們的作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式。

next()是將yield表示式替換成一個值。
throw()是將yield表示式替換成一個throw語句。
return()是將yield表示式替換成一個return語句。

17.7、 yield* 表示式

  用到yield*表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。

    function* foo() {
        yield 'a';
        yield 'b';
    }
    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!';
    	return "test"
    }
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    var gen = outer1()
    console.log(gen.next().value) // "open"
    var test=gen.next().value // 返回一個遍歷器物件
    console.log(test.next().value) //"hello"
    console.log(test.next().value)// "test"
    console.log(gen.next().value) // "close"
複製程式碼

  yield*後面的 Generator 函式(沒有return語句時),等同於在 Generator 函式內部,部署一個for...of迴圈。

    function* concat(iter1, iter2) {
      yield* iter1;
      yield* iter2;
    }
    // 等同於
    function* concat(iter1, iter2) {
      for (var value of iter1) {
        yield value;
      }
      for (var value of iter2) {
        yield value;
      }
    }
複製程式碼

  如果yield*後面跟著一個陣列,由於陣列原生支援遍歷器,因此就會遍歷陣列成員。

    function* gen(){
      yield* ["a", "b", "c"];
    }
    console.log(gen().next()) // { value:"a", done:false }
複製程式碼

  實際上,任何資料結構只要有 Iterator 介面,就可以被yield*遍歷。 如果被代理的 Generator 函式有return語句,那麼就可以向代理它的 Generator 函式返回資料。

    function* foo() {
      yield 2;
      yield 3;
      return "foo";
    }
    
    function* bar() {
      yield 1;
      var v = yield* foo();
      console.log("v: " + v);
      yield 4;
    }
    var it = bar();
    it.next()
    // {value: 1, done: false}
    it.next()
    // {value: 2, done: false}
    it.next()
    // {value: 3, done: false}
    it.next();
    // "v: foo"
    // {value: 4, done: false}
    it.next()
    // {value: undefined, done: true}
    
    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i=0; i < tree.length; i++) {
          yield* iterTree(tree[i]);
        }
      } else {
        yield tree;
      }
    }
    const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
    for(let x of iterTree(tree)) {
      console.log(x);
    }
    // a  // b   // c   // d   // e
複製程式碼

 17.8、 作為物件的屬性的Generator函式

    let obj = {
      * myGeneratorMethod() {
        •••
      }
    };
複製程式碼

 17.9、 Generator函式的this

  Generator 函式總是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函式的例項,也繼承了 Generator 函式的prototype物件上的方法

    function* g() {}
    g.prototype.hello = function () {
      return 'hi!';
    };
    let obj = g();
    obj instanceof g // true
    obj.hello() // 'hi!'
複製程式碼

  通過生成一個空物件,使用call方法繫結 Generator 函式內部的this。

    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var obj = {};
    var f = F.call(obj);//調動F()並且把obj作為this傳進去,這樣給obj新增a、b、c屬性
    console.log(f.next());  // Object {value: 2, done: false}
    console.log(f.next());  // Object {value: 3, done: false}
    console.log(f.next());  // Object {value: undefined, done: true}
    console.log(obj.a) // 1
    console.log(obj.b) // 2
    console.log(obj.c) // 3
複製程式碼

  將obj換成F.prototype。將這兩個物件統一起來。再將F改成建構函式,就可以對它執行new命令了。

    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    function F() {
      return gen.call(gen.prototype);
    }
    var f = new F();
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    f.a // 1
    f.b // 2
    f.c // 3
複製程式碼

  多個執行緒(單執行緒情況下,即多個函式)可以並行執行,但是隻有一個執行緒(或函式)處於正在執行的狀態,其他執行緒(或函式)都處於暫停態(suspended),執行緒(或函式)之間可以交換執行權。並行執行、交換執行權的執行緒(或函式),就稱為協程。

 17.10、 應用

  1、 非同步操作的同步表達。 通過 Generator 函式部署 Ajax 操作,可以用同步的方式表達。

    function makeAjaxCall(url,callBack){
        var xhr;
        if (window.XMLHttpRequest)
        {
            //IE7+, Firefox, Chrome, Opera, Safari 瀏覽器執行程式碼
            xhr=new XMLHttpRequest();
        }else{
            // IE6, IE5 瀏覽器執行程式碼
            xhr=new ActiveXObject("Microsoft.XMLHTTP");
        }
        xhr.open("GET",makeAjaxCall,true);//確保瀏覽器相容性。
        xhr.onreadystatechange=function(){
            if (xhr.readyState==4 && xhr.status==200)
            {   
               if(xhr.status>=200&&xhr.status<300||xhr.status==304){
                  callBack(xhr.responseText;); 
               } 
            }
        }
        xmlhttp.send();
    }

    function* main() {
        var result = yield request("https://juejin.im/editor/posts/5cb209e36fb9a068b52fb360");
        var resp = JSON.parse(result);
        console.log(resp.value);
    }
    function request(url) {
          makeAjaxCall(url, function(response){
            it.next(response);//將response作為上一次yield的返回值
          });
    }
    var it = main();
    it.next();
複製程式碼

  使用yield表示式可以手動逐行讀取檔案。

    function* numbers() {
      let file = new FileReader("numbers.txt");
      try {
        while(!file.eof) {
          yield parseInt(file.readLine(), 10);
        }
      } finally {
        file.close();
      }
    }
複製程式碼

  2、 控制流管理

    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
複製程式碼

  使用Promise

    Promise.resolve(step1)
      .then(step2)
      .then(step3)
      .then(step4)
      .then(function (value4) {
        // Do something with value4
      }, function (error) {
        // Handle any error from step1 through step4
      })
      .done();
複製程式碼

  使用Generator

    function* longRunningTask(value1) {
      try {
        var value2 = yield step1(value1);
        var value3 = yield step2(value2);
        var value4 = yield step3(value3);
        var value5 = yield step4(value4);
        // Do something with value4
      } catch (e) {
        // Handle any error from step1 through step4
      }
    }
    scheduler(longRunningTask(initialValue));
    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函式未結束,就繼續呼叫
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }
    function step1(value){
    	return value*2;
    }
    function step2(value){
    	return value*2;
    }
    function step3(value){
    	return value*2;
    }
    function step4(value){
    	return value*2;
    }
複製程式碼

  注意,上面這種做法,只適合同步操作,即所有的task都必須是同步的,不能有非同步操作。   3、 部署iterator介面

    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);
    }
    // foo 3
    // bar 7
複製程式碼

  4、 作為資料結構

    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt');
      yield fs.readFile.bind(null, 'world.txt');
      yield fs.readFile.bind(null, 'and-such.txt');
    }
    for (task of doStuff()) {}
      // task是一個函式,可以像回撥函式那樣使用它
複製程式碼

17.11、 Generator函式的非同步呼叫(**需要好好理解弄懂**)

  非同步程式設計的方法主要有這幾種:

1、回撥函式(耦合性太強)
2、事件監聽
3、釋出/訂閱
4、Promise 物件
5、generator
  1. 使用Generator來封裝非同步函式

    var fetch = require('node-fetch');
    function* gen(){
      var url = 'https://api.github.com/users/github';
      var result = yield fetch(url);
      console.log(result.bio);
    }
    var g = gen();
    var result = g.next();
    result.value.then(function(data){
      return data.json();
    }).then(function(data){
      g.next(data);
    });
複製程式碼

  首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。   2. Thunk函式
  編譯器的“傳名呼叫”實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體。這個臨時函式就叫做 Thunk 函式。

    function f(m) {
      return m * 2;
    }
    f(x + 5);
    // 等同於
    var thunk = function () {
      return x + 5;
    };
    function f(thunk) {
      return thunk() * 2;
    }
    f(thunk)
    // 正常版本的readFile(多引數版本)
    fs.readFile(fileName, callback);
    // Thunk版本的readFile(單引數版本)
    var Thunk = function (fileName) {
      return function (callback) {
        return fs.readFile(fileName, callback);
      };
    };
    var readFileThunk = Thunk(fileName);
    readFileThunk(callback);
複製程式碼

  3. 基於 Promise 物件的自動執行

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

  然後,手動執行上面的 Generator 函式。

    var g = gen();
    g.next().value.then(function(data){
      g.next(data).value.then(function(data){
        g.next(data);
      });
    });
複製程式碼

  自動執行器寫法:

    function run(gen){
      var g = gen();
      function next(data){
        var result = g.next(data);
        if (result.done) return result.value;
        result.value.then(function(data){
          next(data);
        });
      }
      next();
    }
    run(gen);
複製程式碼

18、async函式

  async函式是Generator 函式的語法糖。async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。 async函式對 Generator 函式的改進,體現在以下四點。

  1. 內建執行器。 呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。也就是說,async函式的執行,與普通函式一模一樣,只要一行。
  2. 更好的語義。 async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。
  3. 更廣的適用性。 await命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時會自動轉成立即 resolved 的 Promise 物件)。
  4. 返回值是 Promise。 async函式的返回值是 Promise 物件,進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖

 18.1、 Async的語法

1、async函式返回一個 Promise 物件。
  async函式內部return語句返回的值,會成為then方法回撥函式的引數。async函式內部丟擲錯誤,會導致返回的 Promise 物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

    async function f() {
      return 'hello world';
    }
    f().then(v => console.log(v))
    // "hello world"
    async function f() {
      throw new Error('出錯了');
    }
    f().then(
      v => console.log(v),
      e => console.log(e)
    )
複製程式碼

2、Promise物件的狀態變化。
  async函式返回的 Promise 物件,必須等到內部所有await命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return語句或者丟擲錯誤。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。

 18.2、 Await命令

  正常情況下,await命令後面是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。

    async function f() {
      // 等同於
      // return 123;
      return await 123;
    }
    f().then(v => console.log(v))
    // 123
複製程式碼

  另一種情況是,await命令後面是一個thenable物件(即定義then方法的物件),那麼await會將其等同於 Promise 物件。

 18.3、 錯誤處理

  如果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:出錯了
複製程式碼

 18.4、 使用注意點

  1) await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中。

    async function myFunction() {
      try {
        await somethingThatReturnsAPromise();
      } catch (err) {
        console.log(err);
      }
    }
    // 另一種寫法
    async function myFunction() {
      await somethingThatReturnsAPromise()
      .catch(function (err) {
        console.log(err);
      });
    }
複製程式碼

  2) 多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。

    // 寫法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    // 寫法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;//直接返回
    let bar = await barPromise;
複製程式碼

  3) await命令只能用在async函式之中,如果用在普通函式,就會報錯。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      // 報錯
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }
複製程式碼

  如果確實希望多個請求併發執行,可以使用Promise.all方法。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      let promises = docs.map((doc) => db.post(doc));
      let results = await Promise.all(promises);
      console.log(results);
    }
複製程式碼

  4) async 函式可以保留執行堆疊。

    const a = () => {
      b().then(() => c());
    };
複製程式碼

  當b()執行的時候,函式a()不會中斷,而是繼續執行。等到b()執行結束,可能a()早就執行結束了,b()所在的上下文環境已經消失了。如果b()或c()報錯,錯誤堆疊將不包括a()。

    const a = async () => {
      await b();
      c();
    };
複製程式碼

  b()執行的時候,a()是暫停執行,上下文環境都儲存著。一旦b()或c()報錯,錯誤堆疊將包括a()。

 18.5、 例項:按順序完成非同步操作

    async function logInOrder(urls) {
      for (const url of urls) {
        const response = await fetch(url);
        console.log(await response.text());
      }
    }
複製程式碼

  上面程式碼的問題是所有遠端操作都是繼發。只有前一個 URL 返回結果,才會去讀取下一個 URL,這樣做效率很差,非常浪費時間。

    async function logInOrder(urls) {
      // 併發讀取遠端URL
      const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
      });
      // 按次序輸出
      for (const textPromise of textPromises) {
        console.log(await textPromise);
      }
    }
複製程式碼

  雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。

 18.6、 非同步遍歷器

  非同步遍歷器的最大的語法特點,就是呼叫遍歷器的next方法,返回的是一個 Promise 物件。

    asyncIterator
      .next()
      .then(
        ({ value, done }) => /* ... */
      );
複製程式碼

 18.7、 非同步 Generator 函式

  語法上,非同步 Generator 函式就是async函式與 Generator 函式的結合。

    async function* gen() {
      yield 'hello';
    }
    const genObj = gen();
    genObj.next().then(x => console.log(x));
    // { value: 'hello', done: false }
複製程式碼

 非同步 Generator 函式內部,能夠同時使用await和yield命令。可以這樣理解,await命令用於將外部操作產生的值輸入函式內部,yield命令用於將函式內部的值輸出。

19、Class

 19.1、class的基本語法

 新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法 而已。ES6 的類,完全可以看作建構函式的另一種寫法。 事實上,類的所有方法都定義在類的prototype屬性上面。

1、ES6 的類,完全可以看作建構函式的另一種寫法。類本身就指向建構函式。

    Point === Point.prototype.constructor // true
複製程式碼

2、類的所有方法都定義在類的prototype屬性上面。

3、在類的例項上面呼叫方法,其實就是呼叫原型上的方法。

    p1.constructor === Point.prototype.constructor // true

    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    Point.prototype.toString = function () {
      return '(' + this.x + ', ' + this.y + ')';
    };
    var p = new Point(1, 2);
    //改成類的寫法
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    typeof Point // "function"
    Point === Point.prototype.constructor // true 類本身就指向建構函式。
    var p1=new Point(2,4);
    p1.constructor === Point.prototype.constructor // true
    
    Point.prototype.constructor === Point // true
    Object.keys(Point.prototype)// []
複製程式碼

  上面程式碼中,toString方法是Point類內部定義的方法,它是不可列舉的。這一點與 ES5 的行為不一致。

19.1、 constructor方法

  constructor方法預設返回例項物件(即this),完全可以指定返回另外一個物件。類必須使用new呼叫,否則會報錯。

    class Foo {
      constructor() {
        return Object.create(null);
      }
    }
    new Foo() instanceof Foo
    // false
複製程式碼

 19.2、 類的例項

  與 ES5 一樣,例項的屬性除非顯式定義在其本身(即定義在this物件上),否則都是定義在原型上(即定義在class上)。

    //定義類
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    var point = new Point(2, 3);
    point.toString() // (2, 3)
    point.hasOwnProperty('x') // true
    point.hasOwnProperty('y') // true
    point.hasOwnProperty('toString') // false
    point.__proto__.hasOwnProperty('toString') // true
    //toString是原型上的方法,構造方法中的才是例項屬性
複製程式碼

  與 ES5 一樣,類的所有例項共享一個原型物件。

    var p1 = new Point(2,3);
    var p2 = new Point(3,2);
    p1.__proto__ === p2.__proto__
    //true
複製程式碼

 19.3、取值函式(getter)和存值函式(setter)

  在“類”的內部可以使用get和set關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為。

 19.4、 屬性表示式

    let methodName = 'getArea';
    class Square {
      constructor(length) {
        // ...
      }
      [methodName]() {
        // ...
      }
    }
複製程式碼

 19.5、 Class表示式

    const MyClass = class Me {
      getClassName() {
        return Me.name;
      }
    };
複製程式碼

  這個類的名字是Me,但是Me只在 Class 的內部可用,指代當前類。在 Class 外部,這個類只能用MyClass引用。

    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(); // "張三" 
複製程式碼

  class的注意事項:
  1、嚴格模式。類和模組的內部,預設就是嚴格模式。
  2、不存在提升。類不存在變數提升。
  3、name屬性總是返回緊跟在class關鍵字後面的類名。
  4、Generator 方法。Symbol.iterator方法返回一個Foo類的預設遍歷器,for...of迴圈會自動呼叫這個遍歷器。

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

  5、 This的指向。 類的方法內部如果含有this,它預設指向類的例項。 但是,必須非常小心,一旦單獨使用該方法,很可能報錯。this會指向該方法執行時所在的環境(由於 class 內部是嚴格模式,所以 this 實際指向的是undefined)

    class Logger {
      printName(name = 'there') {
        this.print(`Hello ${name}`);
      }
      print(text) {
        console.log(text);
      }
    }
    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined 本來是例項的方法,但是此時printName()不是例項呼叫的,所以this指向不明,預設為undefined
複製程式碼

  一個比較簡單的解決方法是,在構造方法中繫結this,這樣就不會找不到print方法了。

    class Logger {
      constructor() {
        this.printName = this.printName.bind(this);
      }
      // ...
    }
複製程式碼

 19.6、 靜態方法

  如果在一個方法前,加上static關鍵字,就表示該方法不會被例項繼承,而是直接通過類來呼叫,這就稱為“靜態方法”。 如果靜態方法包含this關鍵字,這個this指的是類,而不是例項。靜態方法可以與非靜態方法重名。
    class Foo {
      static bar() {
        this.baz();
      }
      static baz() {
        console.log('hello');
      }
      baz() {
        console.log('world');
      }
    }
    Foo.bar() // hello
複製程式碼

  父類的靜態方法,可以被子類繼承。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    class Bar extends Foo {
    }
    Bar.classMethod() // 'hello'
複製程式碼

  靜態方法也是可以從super物件上呼叫的。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }
    Bar.classMethod() // "hello, too"
複製程式碼

 19.7、 實力屬性的新寫法

  這個屬性也可以定義在類的最頂層,其他都不變。這種新寫法的好處是,所有例項物件自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些例項屬性。

    class IncreasingCounter {
      _count = 0;
      get value() {
        console.log('Getting the current value!');
        return this._count;
      }
      increment() {
        this._count++;
      }
    }
複製程式碼

 19.8、 靜態屬性

    class MyClass {
      static myStaticProp = 42;
      constructor() {
        console.log(MyClass.myStaticProp); // 42
      }
    }
複製程式碼

 19.9、 私有方法和私有屬性

  1、 將私有方法移出模組,因為模組內部的所有方法都是對外可見的。

    class Widget {
      foo (baz) {
        bar.call(this, baz);
      }
      // ...
    }
    function bar(baz) {
      return this.snaf = baz;
    }
複製程式碼

  2、利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。一般情況下無法獲取到它們,因此達到了私有方法和私有屬性的效果。但是也不是絕對不行,Reflect.ownKeys()依然可以拿到它們。

    const bar = Symbol('bar');
    const snaf = Symbol('snaf');
    export default class myClass{
      // 公有方法
      foo(baz) {
        this[bar](baz);
      }
      // 私有方法
      [bar](baz) {
        return this[snaf] = baz;
      }
      // ...
    };
複製程式碼

 19.10、new.target()

  ES6 為new命令引入了一個new.target屬性,該屬性一般用在建構函式之中,返回new命令作用於的那個建構函式 。如果建構函式不是通過new命令或Reflect.construct()呼叫的,new.target會返回undefined,因此這個屬性可以用來確定建構函式是怎麼呼叫的。 Class 內部呼叫new.target,返回當前Class。在函式外部,使用new.target會報錯。

    function Person(name) {
        if (new.target !== undefined) {
        this.name = name;
    } else {
            throw new Error('必須使用 new 命令生成例項');
        }
    }
    // 另一種寫法
    function Person(name) {
        if (new.target === Person) {
            this.name = name;
        } else {
            throw new Error('必須使用 new 命令生成例項');
        }
    }
    var person = new Person('張三'); // 正確
    var notAPerson = Person.call(person, '張三');  // 報錯 
複製程式碼

  子類繼承父類時,new.target會返回子類。主要是看new後面的類是哪個

    class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        // ...
      }
    }
    class Square extends Rectangle {
      constructor(length,width) {
        super(length, width);
      }
    }
    var c=new Rectangle(1,2);
    var obj = new Square(3); // 輸出 false
複製程式碼

 19.11、 類的繼承

  Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 呼叫父類的constructor(x, y)
        this.color = color;
      }
      toString() {
        return this.color + ' ' + super.toString(); // 呼叫父類的toString()
      }
    }
複製程式碼

1、 super關鍵字,它在這裡表示父類的建構函式,用來新建父類的this物件。
2、 子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類自己的this物件,必須先通過父類的建構函式完成塑造,得到與父類同樣的例項屬性和方法,然後再對其進行加工,加上子類自己的例項屬性和方法。如果不呼叫super方法,子類就得不到this物件。 或者是不寫constructor(){},寫了必須寫super()。

    class Point { /* ... */ }
    class ColorPoint extends Point {
          constructor() {
          }
    }
    let cp = new ColorPoint(); // ReferenceError
    ————————————————————————————————————————————————————————————
    class ColorPoint extends Point {
    }
    // 等同於
    class ColorPoint extends Point {
      constructor(...args) {
        super(...args);
      }
    }
複製程式碼

3、 ES5 的繼承,實質是先創造子類的例項物件this,然後再將父類的方法新增到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。
4、 在子類的建構函式中,只有呼叫super之後,才可以使用this關鍵字,否則會報錯。這是因為子類例項的構建,基於父類例項,只有super方法才能呼叫父類例項。 5 子類例項物件cp同時是ColorPoint和Point(父類)兩個類的例項,這與 ES5 的行為完全一致。
6 父類的靜態方法,也會被子類繼承。

 19.12、 Object.getPrototypeOf()

  Object.getPrototypeOf方法可以用來從子類上獲取父類。可以使用這個方法判斷,一個類是否繼承了另一個類。
    Object.getPrototypeOf(ColorPoint) === Point// true
複製程式碼

 19.13、 Super關鍵字

1、 super作為函式呼叫時,代表父類的建構函式 。ES6 要求,子類的建構函式必須執行一次super函式。 super雖然代表了父類A的建構函式,但是返回的是子類B的例項。 作為函式時,super()只能用在子類的建構函式之中,用在其他地方就會報錯。

    class A {
      constructor() {
        console.log(new.target.name);//new.targe建構函式
      }
    }
    class B extends A {
      constructor() {
        super();
      }
    }
    new A() // A
    new B() // B
複製程式碼

2、 super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類。所以定義在父類例項上的方法或屬性,是無法通過super呼叫的。

    lass A {
      p() {
        return 2;
      }
    }
    class B extends A {
      constructor() {
        super();
        console.log(super.p()); // 2
      }
    }
    let b = new B();
複製程式碼

  在子類普通方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類例項。

    class A {
      constructor() {
        this.x = 1;
      }
      print() {
        console.log(this.x);
      }
    }
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      m() {
        super.print();
      }
    }
    let b = new B();
    b.m() // 2
複製程式碼

  由於this指向子類例項,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類例項的屬性。

    class A {
      constructor() {
        this.x = 1;
      }
    }
    class B extends A {
      constructor() {
        super();
        this.x = 2;
        super.x = 3;//此時的super相當於this
        console.log(super.x); // undefined
        console.log(this.x); // 3
      }
    }
    let b = new B();
複製程式碼

  而當讀取super.x的時候,讀的是A.prototype.x,所以返回undefined。

    class A {
      constructor() {
        this.x = 1;
      }
      static print() {
        console.log(this.x);
      }
    }
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      static m() {
        super.print();
      }
    }
    B.x = 3;
    B.m() // 3
複製程式碼

  靜態方法B.m裡面,super.print指向父類的靜態方法。這個方法裡面的this指向的是B,而不是B的例項。

 19.14、 類的 prototype 屬性和__proto__屬性

   ES5 實現之中,每一個物件都有__proto__屬性,指向對應的建構函式的prototype屬性。

instance.__proto__===A.prototype//instance是A的例項
複製程式碼

   Class作為建構函式的語法糖,同時有prototype屬性和__proto__屬性,因此同時存在兩條繼承鏈。

(1)子類的__proto__屬性,表示建構函式的繼承, 總是指向父類。
(2)子類prototype屬性的__proto__屬性,**表示方法的繼承,**總是指向父類的prototype屬性。

    class A {
    }
    class B extends A {
    }
    console.log(B.__proto__ === A) // true,
    console.log(B.prototype.__proto__ === A.prototype )// true,
    // 等同於
    Object.create(A.prototype);
複製程式碼

   作為一個物件,子類(B)的原型(__proto__屬性)是父類(A);作為一個建構函式,子類(B)的原型物件(prototype屬性)是父類的原型物件(prototype屬性)的例項。

 19.15、例項的 __proto__ 屬性

  子類例項的__proto__屬性的__proto__屬性,指向父類例項的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。(p2是子類,p1是父類)

    p2.__proto__.__proto__ === p1.__proto__ // true
    解析:
    p2.__proto__===p2的類.prototype;
    p2的類.prototype.__proto__===p2的類的父類的.prototype
    p1.__proto__===p2的類的父類的.prototype。
複製程式碼

  因此,通過子類例項的__proto__.__proto__屬性,可以修改父類例項的行為。

    p2.__proto__.__proto__.printName = function () {
      console.log('Ha');
    };
    p1.printName() // "Ha"
複製程式碼

20、Module

 20、1 嚴格模式

  ES6 的模組自動採用嚴格模式,不管你有沒有在模組頭部加上"use strict";。 嚴格模式主要有以下限制。

  1. 變數必須宣告後再使用。
  2. 函式的引數不能有同名屬性,否則報錯。
  3. 不能使用with語句。
  4. 不能對只讀屬性賦值,否則報錯。
  5. 不能使用字首 0 表示八進位制數,否則報錯。
  6. 不能刪除不可刪除的屬性,否則報錯。
  7. 不能刪除變數delete prop,會報錯,只能刪除屬性delete global[prop]。
  8. eval不會在它的外層作用域引入變數(沒懂)。
  9. eval和arguments不能被重新賦值。
  10. arguments不會自動反映函式引數的變化。
  11. 不能使用arguments.callee。(指向用於arguments物件的函式)
  12. 不能使用arguments.caller,值為undefined。(caller屬性儲存著調動當前函式的函式的引用)
  13. 禁止this指向全域性物件。
  14. 不能使用fn.caller和fn.arguments獲取函式呼叫的堆疊。
  15. 增加了保留字(比如protected、static和interface)。

 20、2 export的用法

  export命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。   export寫法種類:

1、使用大括號指定所要輸出的一組變數。export {firstName, lastName, year}; 2、直接使用export關鍵字輸出該變數。export var year = 1958;

    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    等同於下面這中寫法
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    export {firstName, lastName, year};
複製程式碼

  通常情況下,export輸出的變數就是本來的名字,但是可以使用as關鍵字重新命名。

    function v1() { ... }
    function v2() { ... }
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
複製程式碼

  注意1:export命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係。

    // 報錯
    export 1;
    // 報錯
    var m = 1;
    export m;
    // 報錯
    function f() {}
    export f;
複製程式碼

  注意2:export語句輸出的介面,與其對應的值是動態繫結關係 ,即通過該介面,可以取到模組內部實時的值。

    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);
複製程式碼

  注意3:export命令可以出現在模組的任何位置,只要處於模組頂層就可以。

    function foo() {
      export default 'bar' // SyntaxError
    }
    foo()
複製程式碼

 20、3 import的用法

  import命令輸入的變數都是隻讀的,因為它的本質是輸入介面。也就是說,不允許在載入模組的指令碼里面,改寫介面。
    import {a} from './xxx.js'
    a = {}; // Syntax Error : 'a' is read-only;
    但是,如果a是一個物件,改寫a的屬性是允許的。
    import {a} from './xxx.js'
    a.foo = 'hello'; // 合法操作
複製程式碼

  import後面的from指定模組檔案的位置,可以是相對路徑,也可以是絕對路徑,.js字尾可以省略。如果只是模組名,不帶有路徑,那麼必須有配置檔案,告訴 JavaScript 引擎該模組的位置。

    import {myMethod} from 'util';
    //util是模組檔名,由於不帶有路徑,必須通過配置,告訴引擎怎麼取到這個模組。
複製程式碼

  注意,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 { area, circumference } from './circle';
    console.log('圓面積:' + area(4));
    console.log('圓周長:' + circumference(14));
複製程式碼

 20、4 模組的整體載入 import *

  整體載入的寫法: import * from "module"

    import * as circle from './circle';
    console.log('圓面積:' + circle.area(4));
    console.log('圓周長:' + circle.circumference(14));
複製程式碼

 20、5 export default

  用到export default命令,為模組指定預設輸出。
    // export-default.js
    export default function () {
      console.log('foo');
    }
    // import-default.js
    import customName from './export-default'; 
    //因為是預設輸出的,所以這時import命令後面,不使用大括號。並且可以隨意取名。
    customName(); // 'foo'
複製程式碼

  1、下面程式碼中,foo函式的函式名foo,在模組外部是無效的。載入的時候,視同匿名函式載入。

    function foo() {
      console.log('foo');
    }
    export default foo;
複製程式碼

  2、一個模組只能有一個預設輸出,因此export default命令只能使用一次。所以,import命令後面才不用加大括號,因為只可能唯一對應export default命令。 本質上,export default就是輸出一個叫做default的變數或方法,然後系統允許你為它取任意名字。但是建議import時還是用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';
複製程式碼

  3、因為export default命令的本質是將後面的值,賦給default變數,所以可以直接將一個值寫在export default之後。

    // 正確
    export default 42;
    // 報錯
    export 42;
複製程式碼

  4、如果想在一條import語句中,同時輸入預設方法(default)和其他介面,可以寫成下面這樣。

    import _, { each, forEach } from 'lodash';
複製程式碼

  5、 export default也可以用來輸出類。

    // MyClass.js
    export default class { ... }
    // main.js
    import MyClass from 'MyClass';
    let o = new MyClass();
複製程式碼

 20、5 export和import的複合寫法

    export { foo, bar } from 'my_module';
    // 可以簡單理解為
    import { foo, bar } from 'my_module';
    export { foo, bar };
複製程式碼

  寫成一行以後,foo和bar實際上並沒有被匯入當前模組,只是相當於對外轉發了這兩個介面,導致當前模組不能直接使用foo和bar。 預設介面的寫法如下。

    export { default } from 'foo';
複製程式碼

  具名介面改為預設介面的寫法如下。

    export { es6 as default } from './someModule';
    // 等同於
    import { es6 } from './someModule';
    export default es6;
複製程式碼

  同樣地,預設介面也可以改名為具名介面。

    export { default as es6 } from './someModule';
複製程式碼

 20、6 模組的繼承

    // circleplus.js
    export * from 'circle';
    export var e = 2.71828182846;
    export default function(x) {
      return Math.exp(x);
    }
複製程式碼

  上面程式碼中的export*,表示再輸出circle模組的所有屬性和方法。*注意,export 命令會忽略circle模組的default方法。

    // main.js
    import * as math from 'circleplus';//整體載入的寫法
    import exp from 'circleplus';
    console.log(exp(math.e));
    import exp表示,將circleplus模組的預設方法載入為exp方法。
複製程式碼

 20、7 Import()

  可以實現動態載入。執行時執行,也就是說,什麼時候執行到這一句,就會載入指定的模組。import()返回一個 Promise 物件。

  注意:import()載入模組成功以後,這個模組會作為一個物件,當作then方法的引數。因此,可以使用物件解構賦值的語法,獲取輸出介面。

    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...•
    });
複製程式碼

  上面程式碼中,export1和export2都是myModule.js的輸出介面,可以解構獲得。 如果模組有default輸出介面,可以用引數直接獲得。

    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
複製程式碼

  上面的程式碼也可以使用具名輸入的形式。

    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
複製程式碼

 20、8 module的載入實現

  瀏覽器載入 ES6 模組,也使用script標籤,但是要加入type="module"屬性。
    <script type="module" src="./foo.js"></script>
    <!-- 等同於 -->
    <script type="module" src="./foo.js" defer></script>
複製程式碼

  對於外部的模組指令碼(上例是foo.js),有幾點需要注意。

  1、 程式碼是在模組作用域之中執行,而不是在全域性作用域執行。模組內部的頂層變數,外部不可見。
  2、 模組指令碼自動採用嚴格模式,不管有沒有宣告use strict。
  3、 模組之中,可以使用import命令載入其他模組(.js字尾不可省略,需要提供絕對 URL 或相對 URL),也可以使用export命令輸出對外介面。
  4、 模組之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模組頂層使用this關鍵字,是無意義的。
  5、 同一個模組如果載入多次,將只執行一次。
  利用頂層的this等於undefined這個語法點,可以偵測當前程式碼是否在 ES6 模組之中。

    const isNotModuleScript = this !== undefined;
複製程式碼

 20、9 ES6 模組與 CommonJS 模組

   ES6 模組與 CommonJS 模組完全不同。 它們有兩個重大差異。

1、CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
2、 CommonJS 模組是執行時載入。 ,ES6 模組是編譯時輸出介面。

  第二個差異是因為 CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。
  第一個差異是因為CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。ES6模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。

    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      counter: counter,
      incCounter: incCounter,
    };
    // main.js
    var mod = require('./lib');
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 3
複製程式碼

  這是因為mod.counter是一個原始型別的值 ,會被快取。除非寫成一個函式,才能得到內部變動後的值。

    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      get counter() {
        return counter
      },  
      incCounter: incCounter,
    };
    // main.js
    var mod = require('./lib');
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 4
複製程式碼

  可以對obj新增屬性,但是重新賦值就會報錯。 因為變數obj指向的地址是隻讀的,不能重新賦值,這就好比main.js創造了一個名為obj的const變數。

    // lib.js
    export let obj = {};
    // main.js
    import { obj } from './lib';
    obj.prop = 123; // OK
    obj = {}; // TypeError
複製程式碼

  commonJS和ES6內部變數的區別:

  1、ES6 模組之中,頂層的this指向undefined;CommonJS 模組的頂層this指向當前模組。
  2、以下這些頂層變數在 ES6 模組之中都是不存在的。

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

 20.10、 ES6載入CommonJS模組(整體輸入)

  Node 會自動將module.exports屬性,當作模組的預設輸出,即等同於export default xxx。
    // a.js
    module.exports = {
      foo: 'hello',
      bar: 'world'
    };
    // 等同於
    export default {
      foo: 'hello',
      bar: 'world'
    };
複製程式碼

  由於 ES6 模組是編譯時確定輸出介面,CommonJS 模組是執行時確定輸出介面,所以採用import命令載入 CommonJS 模組時,不允許採用下面的寫法。

    // 不正確
    import { readFile } from 'fs';
複製程式碼

  因為fs是 CommonJS格式,只有在執行時才能確定readFile介面,而import命令要求編譯時就確定這個介面。解決方法就是改為整體輸入。

    // 正確的寫法一
    import * as express from 'express';
    const app = express.default();
    // 正確的寫法二
    import express from 'express';
    const app = express();
複製程式碼

 20.11、 CommonJS載入ES6模組(import()函式)

  CommonJS 模組載入 ES6 模組,不能使用require命令,而要使用import()函式。ES6 模組的所有輸出介面,會成為輸入物件的屬性。

 20.12、 CommonJS 模組的載入原理。

  require命令第一次載入該指令碼,就會執行整個指令碼,然後在記憶體生成一個物件。
    {
      id: '...',
      exports: { ... },
      loaded: true,
      ...
    }
複製程式碼

  該物件的id屬性是模組名,exports屬性是模組輸出的各個介面,loaded屬性是一個布林值,表示該模組的指令碼是否執行完畢。其他還有很多屬性,這裡都省略了。以後需要用到這個模組的時候,就會到exports屬性上面取值。即使再次執行require命令,也不會再次執行該模組,而是到快取之中取值。也就是說,CommonJS 模組無論載入多少次,都只會在第一次載入時執行一次,以後再載入,就返回第一次執行的結果,除非手動清除系統快取。

 20.13、 CommonJS的迴圈載入

  一旦出現某個模組被"迴圈載入",就只輸出已經執行的部分,還未執行的部分不會輸出。
    //a.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
    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
    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 main.js
複製程式碼

執行結果如下:

ES6常用知識點總結(下)

  在main.js中的詳細執行過程如下:

  a.js指令碼先輸出一個done變數,然後載入另一個指令碼檔案b.js。注意,此時a.js程式碼就停在這裡,等待b.js執行完畢,再往下執行。 b.js執行到第二行,就會去載入a.js,這時,就發生了“迴圈載入”。系統會去a.js模組對應物件的exports屬性取值,可是因為a.js還沒有執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。(a.js已經執行的部分,只有一行。)然後,b.js接著往下執行,等到全部執行完畢,再把執行權交還給a.js。於是,a.js接著往下執行,直到執行完畢。

 20.14、 ES6模組的迴圈載入

  ES6 模組是動態引用,如果使用import從一個模組載入變數(即import foo from 'foo'),那些變數不會被快取,而是成為一個指向被載入模組的引用

    // a.mjs
    import {bar} from './b';
    console.log('a.mjs');
    console.log(bar);
    export let foo = 'foo';
    //function foo() { return 'foo' }
    //export {foo};
    // b.mjs
    import {foo} from './a';
    console.log('b.mjs');
    console.log(foo);
    export let bar = 'bar';
    //function bar() { return 'bar' }
    //export {bar};

    $ node --experimental-modules a.mjs
    b.mjs
    ReferenceError: foo is not defined
複製程式碼

  上述程式碼的詳細執行過程如下:

  首先,執行a.mjs以後,引擎發現它載入了b.mjs,因此會優先執行b.mjs,然後再執行a.mjs。接著,執行b.mjs的時候,已知它從a.mjs輸入了foo介面,這時不會去執行a.mjs,而是認為這個介面已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個介面根本沒定義,因此報錯。這可以通過將foo寫成函式來解決這個問題。 這是因為函式具有提升作用(提升到頂部),在執行import {bar} from './b'時,函式foo就已經有定義了,所以b.mjs載入的時候不會報錯。這也意味著,如果把函式foo改寫成函式表示式,也會報錯。

21、程式設計風格(效能優化)

  1. 建議不再使用var命令,而是使用let命令取代。
  2. 在let和const之間,建議優先使用const,尤其是在全域性環境,不應該設定變數,只應設定常量。 原因:一個是const可以提醒閱讀程式的人,這個變數不應該改變;另一個是const比較符合函數語言程式設計思想,運算不改變值,只是新建值,而且這樣也有利於將來的分散式運算;最後一個原因是 JavaScript 編譯器會對const進行優化,所以多使用const,有利於提高程式的執行效率,也就是說let和const的本質區別,其實是編譯器內部的處理不同。
  3. 靜態字串一律使用單引號或反引號,不使用雙引號。動態字串使用反引號。
    // bad
    const a = "foobar";
    const b = 'foo' + a + 'bar';
    // good
    const a = 'foobar';
    const b = `foo${a}bar`;
複製程式碼
  1. 解構賦值 使用陣列成員對變數賦值時,優先使用解構賦值。
    const arr = [1, 2, 3, 4];
    // bad
    const first = arr[0];
    const second = arr[1];
    // good
    const [first, second] = arr;
複製程式碼

  函式的引數如果是物件的成員,優先使用解構賦值。

    // 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 }) {
    }
複製程式碼
  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,
    };
複製程式碼

  物件儘量靜態化,一旦定義,就不得隨意新增新的屬性。如果新增屬性不可避免,要使用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;
複製程式碼
  1. 使用擴充套件運算子(...)拷貝陣列。使用 Array.from 方法,將類似陣列的物件轉為陣列。
    const itemsCopy = [...items];
    const foo = document.querySelectorAll('.foo');
    const nodes = Array.from(foo);
複製程式碼
  1. 簡單的、單行的、不會複用的函式,建議採用箭頭函式。如果函式體較為複雜,行數較多,還是應該採用傳統的函式寫法。
  2. 不要在函式體內使用 arguments 變數,使用 rest 運算子(...)代替。
    // bad
    function concatenateAll() {
      const args = Array.prototype.slice.call(arguments);
      return args.join('');
    }
    // good
    function concatenateAll(...args) {
      return args.join('');
    }
複製程式碼
  1. 使用預設值語法設定函式引數的預設值。
    // bad
    function handleThings(opts) {
      opts = opts || {};
    }
    // good
    function handleThings(opts = {}) {
      // ...
    }
複製程式碼
  1. 注意區分 Object 和 Map,只有模擬現實世界的實體物件時,才使用 Object。如果只是需要key: value的資料結構,使用 Map 結構。因為 Map 有內建的遍歷機制。
  2. 總是用 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;
      }
    }
複製程式碼
  1. 使用extends實現繼承,因為這樣更簡單,不會有破壞instanceof運算的危險。
  2. 如果模組只有一個輸出值,就使用export default,如果模組有多個輸出值,就不使用export default。export default與普通的export不要同時使用。
  3. 不要在模組輸入中使用萬用字元。因為這樣可以確保你的模組之中,有一個預設輸出(export default)。
    // bad
    import * as myObject from './importModule';
    // good
    import myObject from './importModule';
複製程式碼
  1. 如果模組預設輸出一個函式,函式名的首字母應該小寫。如果模組預設輸出一個物件,物件名的首字母應該大寫。
    function makeStyleGuide() {
    }
    export default makeStyleGuide;//函式
    const StyleGuide = {
      es6: {
      }
    };
    export default StyleGuide;//物件
複製程式碼

相關文章