重溫基礎:ES6系列(六)

640?wx_fmt=png

ES6系列目錄

  • 1 let 和 const命令

  • 2 變量的解構賦值

  • 3 字符串的拓展

  • 4 正則的拓展

  • 5 數值的拓展

  • 6 函數的拓展

  • 7 數組的拓展

  • 8 對象的拓展

  • 9 Symbol

  • 10 Set和Map數據結構

  • 11 Proxy

  • 12 Promise對象

  • 13 Iterator和 for...of循環

  • 14 Generator函數和應用

  • 15 Class語法和繼承

  • 16 Module語法和加載實現


所有整理的文章都收錄到我《Cute-JavaScript》系列文章中,訪問地址:http://js.pingan8787.com

14 Generator函數和應用

14.1 基本概念

Generator函數是一種異步編程解決方案。原理Genenrator函數會返回一個遍歷器對象,依次遍歷 Generator函數內部的每一個狀態。Generator函數是一個普通函數,有以下兩個特徵:

  • function關鍵字與函數名之間有個星號;

  • 函數體內使用 yield表達式,定義不同狀態;

通過調用 next方法,將指針移向下一個狀態,直到遇到下一個 yield表達式(或 return語句)爲止。簡單理解, Generator函數分段執行, yield表達式是暫停執行的標記,而 next恢復執行。

function * f (){	
    yield 'hi';	
    yield 'leo';	
    return 'ending';	
}	
let a = f();	
a.next();  // {value: 'hi', done : false}	
a.next();  // {value: 'leo', done : false}	
a.next();  // {value: 'ending', done : true}	
a.next();  // {value: undefined, done : false}

14.2 yield表達式

yield表達式是暫停標誌,遍歷器對象的 next方法的運行邏輯如下:

  1. 遇到 yield就暫停執行,將這個 yield後的表達式的值,作爲返回對象的 value屬性值。

  2. 下次調用 next往下執行,直到遇到下一個 yield

  3. 直到函數結束或者 return爲止,並返回 return語句後面表達式的值,作爲返回對象的 value屬性值。

  4. 如果該函數沒有 return語句,則返回對象的 value爲 undefined 。

注意:

  • yield只能用在 Generator函數裏使用,其他地方使用會報錯。

// 錯誤1	
(function(){	
    yiled 1;  // SyntaxError: Unexpected number	
})()	
// 錯誤2  forEach參數是個普通函數	
let a = [1, [[2, 3], 4], [5, 6]];	
let f = function * (i){	
    i.forEach(function(m){	
        if(typeof m !== 'number'){	
            yield * f (m);	
        }else{	
            yield m;	
        }	
    })	
}	
for (let k of f(a)){	
    console.log(k)	
}
  • yield表達式如果用於另一個表達式之中,必須放在圓括號內。

function * a (){	
    console.log('a' + yield);     //  SyntaxErro	
    console.log('a' + yield 123); //  SyntaxErro	
    console.log('a' + (yield));     //  ok	
    console.log('a' + (yield 123)); //  ok	
}
  • yield表達式用做函數參數或放在表達式右邊,可以不加括號

function * a (){	
    f(yield 'a', yield 'b');    //  ok	
    lei i = yield;              //  ok	
}

14.3 next方法

yield本身沒有返回值,或者是總返回 undefinednext方法可帶一個參數,作爲上一個 yield表達式的返回值。

function * f (){	
    for (let k = 0; true; k++){	
        let a = yield k;	
        if(a){k = -1};	
    }	
}	
let g =f();	
g.next();    // {value: 0, done: false}	
g.next();    // {value: 1, done: false}	
g.next(true);    // {value: 0, done: false}

這一特點,可以讓 Generator函數開始執行之後,可以從外部向內部注入不同值,從而調整函數行爲。

function * f(x){	
    let y = 2 * (yield (x+1));	
    let z = yield (y/3);	
    return (x + y + z);	
}	
let a = f(5);	
a.next();   // {value : 6 ,done : false}	
a.next();   // {value : NaN ,done : false}  	
a.next();   // {value : NaN ,done : true}	
// NaN因爲yeild返回的是對象 和數字計算會NaN	
let b = f(5);	
b.next();     // {value : 6 ,done : false}	
b.next(12);   // {value : 8 ,done : false}	
b.next(13);   // {value : 42 ,done : false}	
// x 5 y 24 z 13

14.4 for...of循環

for...of循環會自動遍歷,不用調用 next方法,需要注意的是, for...of遇到 next返回值的 done屬性爲 true就會終止, return返回的不包括在 for...of循環中。

function * f(){	
    yield 1;	
    yield 2;	
    yield 3;	
    yield 4;	
    return 5;	
}	
for (let k of f()){	
    console.log(k);	
}	
// 1 2 3 4  沒有 5 

14.5 Generator.prototype.throw()

throw方法用來向函數外拋出錯誤,並且在Generator函數體內捕獲。

let f = function * (){	
    try { yield }	
    catch (e) { console.log('內部捕獲', e) }	
}	
let a = f();	
a.next();	
try{	
    a.throw('a');	
    a.throw('b');	
}catch(e){	
    console.log('外部捕獲',e);	
}	
// 內部捕獲 a	
// 外部捕獲 b

14.6 Generator.prototype.return()

return方法用來返回給定的值,並結束遍歷Generator函數,如果 return方法沒有參數,則返回值的 value屬性爲 undefined

function * f(){	
    yield 1;	
    yield 2;	
    yield 3;	
}	
let g = f();	
g.next();          // {value : 1, done : false}	
g.return('leo');   // {value : 'leo', done " true}	
g.next();          // {value : undefined, done : true}

14.7 next()/throw()/return()共同點

相同點就是都是用來恢復Generator函數的執行,並且使用不同語句替換 yield表達式。

  • next()將 yield表達式替換成一個值。

let f = function * (x,y){	
    let r = yield x + y;	
    return r;	
}	
let g = f(1, 2); 	
g.next();   // {value : 3, done : false}	
g.next(1);  // {value : 1, done : true}	
// 相當於把 let r = yield x + y;	
// 替換成 let r = 1;
  • throw()將 yield表達式替換成一個 throw語句。

g.throw(new Error('報錯'));  // Uncaught Error:報錯	
// 相當於將 let r = yield x + y	
// 替換成 let r = throw(new Error('報錯'));
  • next()將 yield表達式替換成一個 return語句。

g.return(2); // {value: 2, done: true}	
// 相當於將 let r = yield x + y	
// 替換成 let r = return 2;

14.8 yield* 表達式

用於在一個Generator中執行另一個Generator函數,如果沒有使用 yield*會沒有效果。

function * a(){	
    yield 1;	
    yield 2;	
}	
function * b(){	
    yield 3;	
    yield * a();	
    yield 4;	
}	
// 等同於	
function * b(){	
    yield 3;	
    yield 1;	
    yield 2;	
    yield 4;	
}	
for(let k of b()){console.log(k)}	
// 3	
// 1	
// 2	
// 4

14.9 應用場景

  1. 控制流管理

// 使用前	
f1(function(v1){	
    f2(function(v2){	
        f3(function(v3){	
            // ... more and more	
        })	
    })	
})	
// 使用Promise 	
Promise.resolve(f1)	
    .then(f2)	
    .then(f3)	
    .then(function(v4){	
        // ...	
    },function (err){	
        // ...	
    }).done();	
// 使用Generator	
function * f (v1){	
    try{	
        let v2 = yield f1(v1);	
        let v3 = yield f1(v2);	
        let v4 = yield f1(v3);	
        // ...	
    }catch(err){	
        // console.log(err)	
    }	
}	
function g (task){	
    let obj = task.next(task.value);	
  // 如果Generator函數未結束,就繼續調用	
  if(!obj.done){	
      task.value = obj.value;	
      g(task);	
  }	
}	
g( f(initValue) );
  1. 異步編程的使用 在真實的異步任務封裝的情況:

let fetch = require('node-fetch');	
function * f(){	
    let url = 'http://www.baidu.com';	
    let res = yield fetch(url);	
    console.log(res.bio);	
}	
// 執行該函數	
let g = f();	
let result = g.next();	
// 由於fetch返回的是Promise對象,所以用then	
result.value.then(function(data){	
    return data.json();	
}).then(function(data){	
    g.next(data);	
})

15 Class語法和繼承

15.1 介紹

ES6中的 class可以看作只是一個語法糖,絕大部分功能都可以用ES5實現,並且,類和模塊的內部,默認就是嚴格模式,所以不需要使用use strict指定運行模式

// ES5	
function P (x,y){	
    this.x = x;	
    this.y = y;	
}	
P.prototype.toString = function () {	
  return '(' + this.x + ', ' + this.y + ')';	
};	
var a = new P(1, 2);	
// ES6	
class P {	
    constructor(x, y){	
        this.x = x;	
        this.y = y;	
    }	
    toString(){	
        return '(' + this.x + ', ' + this.y + ')';	
    }	
}	
let a = new P(1, 2);

值得注意: ES6的的所有方法都是定義在 prototype屬性上,調用類的實例的方法,其實就是調用原型上的方法。

class P {	
    constructor(){ ... }	
    toString(){ ... }	
    toNumber(){ ... }	
}	
// 等同於	
P.prototyoe = {	
    constructor(){ ... },	
    toString(){ ... },	
    toNumber(){ ... }	
}	
let a = new P();	
a.constructor === P.prototype.constructor; // true

類的屬性名可以使用表達式

let name = 'leo';	
class P {	
    constructor (){ ... }	
    [name](){ ... }	
}

Class不存在變量提升: ES6中的類不存在變量提升,與ES5完全不同:

new P ();   // ReferenceError	
class P{...};

Class的name屬性name屬性總是返回緊跟在 class後的類名。

class P {}	
P.name;  // 'P'

15.2 constructor()方法

constructor()是類的默認方法,通過 new實例化時自動調用執行,一個類必須有 constructor()方法,否則一個空的 constructor()會默認添加。constructor()方法默認返回實例對象(即 this)。

class P { ... }	
// 等同於	
class P {	
    constructor(){ ... }	
}

15.3 類的實例對象

與ES5一樣,ES6的類必須使用 new命令實例化,否則報錯。

class P { ... }	
let a = P (1,2);     // 報錯	
let b = new P(1, 2); // 正確

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

class P {	
    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是原型對象的屬性(因爲定義在Point類上)

15.4 Class表達式

與函數一樣,類也可以使用表達式來定義,使用表達式來作爲類的名字,而 class後跟的名字,用來指代當前類,只能再Class內部使用。

let a = class P{	
    get(){	
        return P.name;	
    }	
}	
let b = new a();	
b.get(); // P	
P.name;  // ReferenceError: P is not defined

如果類的內部沒用到的話,可以省略 P,也就是可以寫成下面的形式。

let a = class { ... }

15.5 私有方法和私有屬性

由於ES6不提供,只能變通來實現:

  • 1.使用命名加以區別,如變量名前添加 _,但是不保險,外面也可以調用到。

class P {	
    // 公有方法	
    f1 (x) {	
        this._x(x);	
    }	
    // 私有方法	
    _x (x){	
        return this.y = x;	
    }	
}
  • 2.將私有方法移除模塊,再在類內部調用 call方法。

class P {	
    f1 (x){	
        f2.call(this, x);	
    }	
}	
function f2 (x){	
    return this.y = x;	
}
  • 3.使用 Symbol爲私有方法命名。

const a1 = Symbol('a1');	
const a2 = Symbol('a2');	
export default class P{	
    // 公有方法	
    f1 (x){	
        this[a1](x);	
    }	
    // 私有方法	
    [a1](x){	
        return this[a2] = x;	
    }	
}

15.6 this指向問題

類內部方法的 this默認指向類的實例,但單獨使用該方法可能報錯,因爲 this指向的問題。

class P{	
    leoDo(thing = 'any'){	
        this.print(`Leo do ${thing}`)	
    }	
    print(text){	
        console.log(text);	
    }	
}	
let a = new P();	
let { leoDo } = a;	
leoDo(); // TypeError: Cannot read property 'print' of undefined	
// 問題出在 單獨使用leoDo時,this指向調用的環境,	
// 但是leoDo中的this是指向P類的實例,所以報錯

解決方法

  • 1.在類裏面綁定 this

class P {	
    constructor(){	
        this.name = this.name.bind(this);	
    }	
}
  • 2.使用箭頭函數

class P{	
    constructor(){	
        this.name = (name = 'leo' )=>{	
            this.print(`my name is ${name}`)	
        }	
    }	
}

15.7 Class的getter和setter

使用 getset關鍵詞對屬性設置取值函數和存值函數,攔截屬性的存取行爲。

class P {	
    constructor (){ ... }	
    get f (){	
        return 'getter';	
    }	
    set f (val) {	
        console.log('setter: ' + val);	
    }	
}	
let a = new P();	
a.f = 100;   // setter : 100	
a.f;          // getter

15.8 Class的generator方法

只要在方法之前加個( *)即可。

class P {	
    constructor (...args){	
        this.args = args;	
    }	
    *[Symbol.iterator](){	
        for (let arg of this.args){	
            yield arg;	
        }	
    }	
}	
for (let k of new P('aa', 'bb')){	
    console.log(k);	
}	
// 'aa'	
// 'bb'

15.9 Class的靜態方法

由於類相當於實例的原型,所有類中定義的方法都會被實例繼承,若不想被繼承,只要加上 static關鍵字,只能通過類來調用,即“靜態方法”。

class P (){	
    static f1 (){ return 'aaa' };	
}	
P.f1();    // 'aa'	
let a = new P();	
a.f1();    // TypeError: a.f1 is not a function

如果靜態方法包含 this關鍵字,則 this指向類,而不是實例。

class P {	
    static f1 (){	
        this.f2();	
    }	
    static f2 (){	
        console.log('aaa');	
    }	
    f2(){	
        console.log('bbb');	
    }	
}	
P.f2();  // 'aaa'

並且靜態方法可以被子類繼承,或者 super對象中調用。

class P{	
    static f1(){ return 'leo' };	
}	
class Q extends P { ... };	
Q.f1();  // 'leo'	
class R extends P {	
    static f2(){	
        return super.f1() + ',too';	
    }	
}	
R.f2();  // 'leo , too'

15.10 Class的靜態屬性和實例屬性

ES6中明確規定,Class內部只有靜態方法沒有靜態屬性,所以只能通過下面實現。

// 正確寫法	
class P {}	
P.a1 = 1;	
P.a1;      // 1	
// 無效寫法	
class P {	
    a1: 2,          // 無效	
    static a1 : 2,  // 無效	
}	
P.a1;      // undefined

新提案來規定實例屬性和靜態屬性的新寫法

  • 1.類的實例屬性

class P {	
    prop = 100;   // prop爲P的實例屬性 可直接讀取	
    constructor(){	
        console.log(this.prop); // 100	
    }	
}

有了新寫法後,就可以不再 contructor方法裏定義。constructor裏面已經定義的實例屬性,新寫法允許直接列出

// 之前寫法:	
class RouctCounter extends React.Component {	
    constructor(prop){	
        super(prop);	
        this.state = {	
            count : 0	
        }	
    }	
}	
// 新寫法	
class RouctCounter extends React.Component {	
    state;	
    constructor(prop){	
        super(prop);	
        this.state = {	
            count : 0	
        }	
    }	
}
  • 2.類的靜態屬性static關鍵字就可以。

class P {	
    static prop = 100;	
    constructor(){console.log(this.prop)}; // 100	
}

新寫法方便靜態屬性的表達。

// old 	
class P  { .... }	
P.a = 1;	
// new 	
class P {	
    static a = 1;	
}

15.11 Class的繼承

主要通過 extends關鍵字實現,繼承父類的所有屬性和方法,通過 super關鍵字來新建父類構造函數的 this對象。

class P { ... }	
class Q extends P { ... }	
class P { 	
    constructor(x, y){	
        // ...	
    }	
    f1 (){ ... }	
}	
class Q extends P {	
    constructor(a, b, c){	
        super(x, y);  // 調用父類 constructor(x, y)	
        this.color = color ;	
    }	
    f2 (){	
        return this.color + ' ' + super.f1(); 	
        // 調用父類的f1()方法	
    }	
}

子類必須在 constructor()調用 super()否則報錯,並且只有 super方法才能調用父類實例,還有就是,父類的靜態方法,子類也可以繼承到

class P { 	
    constructor(x, y){	
        this.x = x;	
        this.y = y;	
    }	
    static fun(){	
        console.log('hello leo')	
    }	
}	
// 關鍵點1 調用super	
class Q extends P {	
    constructor(){ ... }	
}	
let a = new Q(); // ReferenceError 因爲Q沒有調用super	
// 關鍵點2 調用super	
class R extends P {	
    constructor (x, y. z){	
        this.z = z; // ReferenceError 沒調用super不能使用	
        super(x, y);	
        this.z = z; // 正確	
    }	
}	
// 關鍵點3 子類繼承父類靜態方法	
R.hello(); // 'hello leo'

super關鍵字

  • 1.當函數調用,代表父類的構造函數,但必須執行一次。

class P {... };	
class R extends P {	
    constructor(){	
        super();	
    }	
}
  • 2.當對象調用,指向原型對象,在靜態方法中指向父類。

class P {	
    f (){ return 2 };	
}	
class R extends P {	
    constructor (){	
        super();	
        console.log(super.f()); // 2	
    }	
}	
let a = new R()

注意super指向父類原型對象,所以定義在父類實例的方法和屬性,是無法通過 super調用的,但是通過調用 super方法可以把內部 this指向當前實例,就可以訪問到。

class P {	
    constructor(){	
        this.a = 1;	
    }	
    print(){	
        console.log(this.a);	
    }	
}	
class R extends P {	
    get f (){	
        return super.a;	
    }	
}	
let b = new R();	
b.a; // undefined 因爲a是父類P實例的屬性	
// 先調用super就可以訪問	
class Q extends P {	
    constructor(){	
        super();   // 將內部this指向當前實例	
        return super.a;	
    }	
}	
let c = new Q();	
c.a; // 1	
// 情況3	
class J extends P {	
    constructor(){	
        super();	
        this.a = 3;	
    }	
    g(){	
        super.print();	
    }	
}	
let c = new J();	
c.g(); // 3  由於執行了super()後 this指向當前實例

16 Module語法和加載實現

16.1 介紹

ES6之前用於JavaScript的模塊加載方案,是一些社區提供的,主要有 CommonJSAMD兩種,前者用於服務器,後者用於瀏覽器export命令對外暴露接口,使用 import命令輸入其他模塊暴露的接口。

// CommonJS模塊	
let { stat, exists, readFire } = require('fs');	
// ES6模塊	
import { stat, exists, readFire } = from 'fs';

16.2 嚴格模式

ES6模塊自動採用嚴格模式,無論模塊頭部是否有 "use strict"嚴格模式有以下限制

  • 變量必須聲明後再使用

  • 函數的參數不能有同名屬性,否則報錯

  • 不能使用 with語句

  • 不能對只讀屬性賦值,否則報錯

  • 不能使用前綴 0 表示八進制數,否則報錯

  • 不能刪除不可刪除的屬性,否則報錯

  • 不能刪除變量 deleteprop,會報錯,只能刪除屬性 delete*global[prop]

  • eval不會在它的外層作用域引入變量

  • eval和 arguments不能被重新賦值

  • arguments不會自動反映函數參數的變化

  • 不能使用 arguments.callee

  • 不能使用 arguments.caller

  • 禁止 this指向全局對象

  • 不能使用 fn.caller和 fn.arguments獲取函數調用的堆棧

  • 增加了保留字(比如 protected、 static和 interface

特別是,ES6中頂層 this指向 undefined,即不應該在頂層代碼使用 this

16.3 export命令

使用 export向模塊外暴露接口,可以是方法,也可以是變量。

// 1. 變量	
export let a = 'leo';	
export let b = 100;	
// 還可以	
let a = 'leo';	
let b = 100;	
export {a, b};	
// 2. 方法	
export function f(a,b){	
    return a*b;	
}	
// 還可以	
function f1 (){ ... }	
function f2 (){ ... }	
export {	
    a1 as f1,	
    a2 as f2	
}

可以使用 as重命名函數的對外接口。特別注意export暴露的必須是接口,不能是值。

// 錯誤	
export 1; // 報錯	
let a = 1;	
export a; // 報錯	
// 正確	
export let a = 1; // 正確	
let a = 1;	
export {a};       // 正確	
let a = 1;	
export { a as b}; // 正確

暴露方法也是一樣:

// 錯誤	
function f(){...};	
export f;	
// 正確	
export function f () {...};	
function f(){...};	
export {f};

16.4 import命令

加載 export暴露的接口,輸出爲變量。

import { a, b } from '/a.js';	
function f(){	
    return a + b;	
}

import後大括號指定變量名,需要與 export的模塊暴露的名稱一致。as爲輸入的變量重命名。

import { a as leo } from './a.js';

import不能直接修改輸入變量的值,因爲輸入變量只讀只是個接口,但是如果是個對象,可以修改它的屬性。

// 錯誤	
import {a} from './f.js';	
a = {}; // 報錯	
// 正確	
a.foo = 'leo';  // 不報錯

import命令具有提升效果,會提升到整個模塊頭部最先執行,且多次執行相同 import只會執行一次。

16.5 模塊的整體加載

當一個模塊暴露多個方法和變量,引用時可以用 *整體加載。

// a.js	
export function f(){...}	
export function g(){...}	
// b.js	
import * as obj from '/a.js';	
console.log(obj.f());	
console.log(obj.g());

但是,不允許運行時改變:

import * as obj from '/a.js';	
// 不允許	
obj.a = 'leo';   	
obj.b = function(){...}; 

16.6 export default 命令

使用 exportdefault命令,爲模塊指定默認輸出,引用的時候直接指定任意名稱即可。

// a.js	
export default function(){console.log('leo')};	
// b.js	
import leo from './a.js';	
leo(); // 'leo'

exportdefault暴露有函數名的函數時,在調用時相當於匿名函數。

// a.js	
export default function f(){console.log('leo')};	
// 或者	
function f(){console.log('leo')};	
export default f;	
// b.js	
import leo from './a.js';

exportdefault其實是輸出一個名字叫 default的變量,所以後面不能跟變量賦值語句。

// 正確	
export let a= 1;	
let a = 1;	
export default a;	
// 錯誤	
export default let a = 1;

exportdefault命令的本質是將後面的值,賦給 default變量,所以可以直接將一個值寫在 exportdefault之後。

// 正確	
export detault 1;	
// 錯誤	
export 1;

16.7 export 和 import 複合寫法

常常在先輸入後輸出同一個模塊使用,即轉發接口,將兩者寫在一起。

export {a, b} from './leo.js';	
// 理解爲	
import {a, b} from './leo.js';	
export {a, b}

常見的寫法還有:

// 接口改名	
export { a as b} from './leo.js';	
// 整體輸出	
export *  from './leo.js';	
// 默認接口改名	
export { default as a } from './leo.js';

常常用在模塊繼承

16.8 瀏覽器中的加載規則

ES6中,可以在瀏覽器使用 <script>標籤,需要加入 type="module"屬性,並且這些都是異步加載,避免瀏覽器阻塞,即等到整個頁面渲染完,再執行模塊腳本,等同於打開了 <script>標籤的 defer屬性。

<script type="module" src="./a.js"></script>

另外,ES6模塊也可以內嵌到網頁,語法與外部加載腳本一致:

<script type="module">	
    import a from './a.js';	
</script>

注意點

  • 代碼是在模塊作用域之中運行,而不是在全局作用域運行。模塊內部的頂層變量,外部不可見。

  • 模塊腳本自動採用嚴格模式,不管有沒有聲明 usestrict

  • 模塊之中,可以使用 import命令加載其他模塊( .js後綴不可省略,需要提供 絕對URL 或 相對URL),也可以使用 export命令輸出對外接口。

  • 模塊之中,頂層的 this關鍵字返回 undefined,而不是指向 window。也就是說,在模塊頂層使用 this關鍵字,是無意義的。

  • 同一個模塊如果加載多次,將只執行一次。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章