ES6經常使用知識點總結(下)

目錄導航node

 1七、Generator

  是 ES6 提供的一種異步編程解決方案。 語法上是一個狀態機,封裝了多個內部狀態 。執行 Generator 函數會返回一個遍歷器對象。這一點跟promise很像,promise是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。
  Generator 函數是一個普通函數,可是有兩個特徵。git

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

二、函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。github

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語句)。shell

 17.一、 yield表達式

  yield表達式就是暫停標誌。並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。   yield表達式與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。express

  注意:編程

一、 yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。
二、 yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。json

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

三、 yield表達式用做函數參數或放在賦值表達式的右邊,能夠不加括號。api

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.二、 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'));
複製代碼

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

  結果分析:第一次調用next(),執行到yield 1結束;第二次調用next()執行到yield結束;第三次調用next("a")執行 var a=yield中的yield;第四次調用next("b")方法調用var a=yield語句和var b=yield中的yield;

 17.三、 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.四、 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.五、 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.六、 next()、throw()、return()的共同點及區別

  它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。

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

17.七、 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.八、 做爲對象的屬性的Generator函數

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

 17.九、 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.十、 應用

  一、 異步操做的同步表達。 經過 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();
      }
    }
複製代碼

  二、 控制流管理

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都必須是同步的,不能有異步操做。   三、 部署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
複製代碼

  四、 做爲數據結構

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.十一、 Generator函數的異步調用(**須要好好理解弄懂**)

  異步編程的方法主要有這幾種:

一、回調函數(耦合性太強)
二、事件監聽
三、發佈/訂閱
四、Promise 對象
五、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);
複製代碼

1八、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.一、 Async的語法

一、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)
    )
複製代碼

二、Promise對象的狀態變化。
  async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。

 18.二、 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.三、 錯誤處理

  若是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.四、 使用注意點

  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.五、 實例:按順序完成異步操做

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.六、 異步遍歷器

  異步遍歷器的最大的語法特色,就是調用遍歷器的next方法,返回的是一個 Promise 對象。

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

 18.七、 異步 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命令用於將函數內部的值輸出。

1九、Class

 19.一、class的基本語法

 新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法 而已。ES6 的類,徹底能夠看做構造函數的另外一種寫法。 事實上,類的全部方法都定義在類的prototype屬性上面。

一、ES6 的類,徹底能夠看做構造函數的另外一種寫法。類自己就指向構造函數。

Point === Point.prototype.constructor // true
複製代碼

二、類的全部方法都定義在類的prototype屬性上面。

三、在類的實例上面調用方法,其實就是調用原型上的方法。

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.一、 constructor方法

  constructor方法默認返回實例對象(即this),徹底能夠指定返回另一個對象。類必須使用new調用,不然會報錯。

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

 19.二、 類的實例

  與 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.三、取值函數(getter)和存值函數(setter)

  在「類」的內部可使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。

 19.四、 屬性表達式

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

 19.五、 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的注意事項:
  一、嚴格模式。類和模塊的內部,默認就是嚴格模式。
  二、不存在提高。類不存在變量提高。
  三、name屬性老是返回緊跟在class關鍵字後面的類名。
  四、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
    }
複製代碼

  五、 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.六、 靜態方法

  若是在一個方法前,加上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.七、 實力屬性的新寫法

  這個屬性也能夠定義在類的最頂層,其餘都不變。這種新寫法的好處是,全部實例對象自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些實例屬性。

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

 19.八、 靜態屬性

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

 19.九、 私有方法和私有屬性

  一、 將私有方法移出模塊,由於模塊內部的全部方法都是對外可見的。

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

  二、利用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.十、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.十一、 類的繼承

  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()
      }
    }
複製代碼

一、 super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。
二、 子類必須在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);
      }
    }
複製代碼

三、 ES5 的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this。
四、 在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,基於父類實例,只有super方法才能調用父類實例。 5 子類實例對象cp同時是ColorPoint和Point(父類)兩個類的實例,這與 ES5 的行爲徹底一致。
6 父類的靜態方法,也會被子類繼承。

 19.十二、 Object.getPrototypeOf()

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

 19.1三、 Super關鍵字

一、 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
複製代碼

二、 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.1四、 類的 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.1五、實例的 __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寫法種類:

一、使用大括號指定所要輸出的一組變量。export {firstName, lastName, year}; 二、直接使用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'
複製代碼

  一、下面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。

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

  二、一個模塊只能有一個默認輸出,所以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';
複製代碼

  三、由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。

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

  四、若是想在一條import語句中,同時輸入默認方法(default)和其餘接口,能夠寫成下面這樣。

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

  五、 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),有幾點須要注意。

  一、 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
  二、 模塊腳本自動採用嚴格模式,無論有沒有聲明use strict。
  三、 模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口。
  四、 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
  五、 同一個模塊若是加載屢次,將只執行一次。
  利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。

const isNotModuleScript = this !== undefined;
複製代碼

 20、9 ES6 模塊與 CommonJS 模塊

   ES6 模塊與 CommonJS 模塊徹底不一樣。 它們有兩個重大差別。

一、CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
二、 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內部變量的區別:

  一、ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊。
  二、如下這些頂層變量在 ES6 模塊之中都是不存在的。

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

 20.十、 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.十一、 CommonJS加載ES6模塊(import()函數)

  CommonJS 模塊加載 ES6 模塊,不能使用require命令,而要使用import()函數。ES6 模塊的全部輸出接口,會成爲輸入對象的屬性。

 20.十二、 CommonJS 模塊的加載原理。

  require命令第一次加載該腳本,就會執行整個腳本,而後在內存生成一個對象。
{
      id: '...',
      exports: { ... },
      loaded: true,
      ...
    }
複製代碼

  該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略了。之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,CommonJS 模塊不管加載多少次,都只會在第一次加載時運行一次,之後再加載,就返回第一次運行的結果,除非手動清除系統緩存。

 20.1三、 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
複製代碼

執行結果以下:

  在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.1四、 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改寫成函數表達式,也會報錯。

2一、編程風格(性能優化)

  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;//對象
複製代碼
相關文章
相關標籤/搜索