1. 程式人生 > >深入理解ES6 class

深入理解ES6 class

class基本宣告

在說class之前,想必大家肯定會想到constructor function. 看下面程式碼:

function Foo(name) {
  this.name = name
}

class Bar {
  constructor(name){
    this.name = name
  }
}
f = new Foo('xhs')
b = new Bar('xhs')
複製程式碼

兩個差不多吧,foo function是在new的時候,把this指向當前的新建立的空物件,並且會把進行屬性分配。bar class是在constructor

裡進行接收引數。

但是兩個還是 有些不同

  • class宣告並不像function宣告,他不存在提升。他類似let宣告,存在TDZ(temporal dead zone)
  • class中的程式碼都會自動的使用嚴格模式,沒辦法選擇。
  • 所有的方法都是不可列舉的(non-enumerable), 注:非綁定當前物件的方法。
  • class內所有方法內部都缺少[[Construct]]方法,所以如果對這些方法進行new會出錯。
  • 不攜帶new操作符呼叫class會報錯。
  • 嘗試在類的方法中改變類名會出錯。

考慮到上面這幾點,下面來看一個等價的例子:

class PersonClass {

    // equivalent of the PersonType constructor
    constructor(name) {
        this.name = name;
    }

    // equivalent of PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
}
複製程式碼

上面的程式碼將等價下面無class的語法

// direct equivalent of PersonClass
let PersonType2 = (function() {

    "use strict";

    const PersonType2 = function(name) {

        // make sure the function was called with new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }

        this.name = name;
    }

    Object.defineProperty(PersonType2.prototype, "sayName", {
        value: function() {

            // make sure the method wasn't called with new
            if (typeof new.target !== "undefined") {
                throw new Error("Method cannot be called with new.");
            }

            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonType2;
}());
複製程式碼

我們來分析上面這個無class語法的程式碼段。

首先注意到這裡有兩個PersonType2的宣告(let宣告在作用域外面,constIIFE裡),這個就是禁止類方法覆蓋類名。 在構造方法裡有new.target來檢測確保通過new呼叫,與之相對的是對方法的檢測,排除new方法呼叫的可能,否則拋錯。在下面就是enumerable: false,最後返回這個建構函式. 雖然上面的程式碼可以實現class的效果,但是明顯,class更加簡潔方便。

類的常量名稱。

常量是不可被改變的,否則就會報錯。類的名稱只是在內部使用const,這就意味著在內部不可以改變名稱,外面卻可以。

class Foo {
   constructor() {
       Foo = "bar";    // 執行的時候報錯。
   }
}

// 這裡不會報錯。
Foo = "baz";
複製程式碼

class表示式

classfunction類似,也可以使用表示式。

let PersonClass = class {
    // equivalent of the FunctionName constructor
    constructor(name) {
        this.name = name;
    }
    // equivalent of FunctionName.prototype.sayName
    sayName() {
        console.log(this.name);
    }
};
複製程式碼

可以發現,表示式語法類似,使用class的表示式還是宣告都只是風格的不同,不像建構函式的宣告和表示式有著提升的區別。

當然,上面的表示式是一個匿名錶達式,我們可以建立一個攜帶名稱的表示式。

let PersonClass = class PersonClass2 {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
};
console.log(typeof PersonClass);        // "function"
console.log(typeof PersonClass2);  // undefined
複製程式碼

可以發現上面輸出PersonClass2是未定義,因為他只有存在類定義中, 如需瞭解,我們做下面的一個轉變:

// direct equivalent of PersonClass named class expression
let PersonClass = (function() {

    "use strict";

    const PersonClass2 = function(name) {

        // make sure the function was called with new
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }

        this.name = name;
    }

    Object.defineProperty(PersonClass2.prototype, "sayName", {
        value: function() {

            // make sure the method wasn't called with new
            if (typeof new.target !== "undefined") {
                throw new Error("Method cannot be called with new.");
            }

            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });

    return PersonClass2;
}());
複製程式碼

這個轉變與上面的**class宣告略有不同,class宣告的時候,內部與外部的名稱相同,但是在class表示式** 中,卻不同。

Classes第一等公民

在程式設計世界中,當某個東西可以作為一個值使用時,這意味著它可以被傳遞到函式中,從函式返回,可以分配給變數,它被認為是一等的公民。所以在javascript中,function是第一等公民. ES6中使用class沿用了這一傳統,所以class有很多方式去使用它,下面來看將他作為一個引數:

function createObject(classDef) {
    return new classDef();
}
let obj = createObject(class {
    sayHi() {
        console.log("Hi!");
    }
});
obj.sayHi();        // "Hi!"
複製程式碼

class有一個有意思的是使用立即執行來建立單例

let person = new class {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(this.name);
    }
}("xhs");
person.sayName();       // "xhs"
複製程式碼

這樣就建立了一個單例。

訪問的屬性

雖說應該是在class constructor中定義自己的一些屬性,但是class允許你在原型上通過set&get來定義獲取屬性。

class CustomHTMLElement {
    constructor(element) {
        this.element = element;
    }
    get html() {
        return this.element.innerHTML;
    }
    set html(value) {
        this.element.innerHTML = value;
    }
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,\
 "html");
console.log("get" in descriptor);   // true
console.log("set" in descriptor);   // true
console.log(descriptor.enumerable); // false
複製程式碼

他類似下面這種無class的情況:

// direct equivalent to previous example
let CustomHTMLElement = (function() {
    "use strict";
    const CustomHTMLElement = function(element) {     
        if (typeof new.target === "undefined") {
            throw new Error("Constructor must be called with new.");
        }
        this.element = element;
    }
    Object.defineProperty(CustomHTMLElement.prototype, "html", {
        enumerable: false,
        configurable: true,
        get: function() {
            return this.element.innerHTML;
        },
        set: function(value) {
            this.element.innerHTML = value;
        }
    });

    return CustomHTMLElement;
}());
複製程式碼

可以發現,最終都是在Object.defineProperty中處理。

Generator 方法

class內部的方法是支援generator方法的。

class Collection {

    constructor() {
        this.items = [];
    }

    *[Symbol.iterator]() {
        yield *this.items.values();
    }
}

var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}
複製程式碼

對於generatoriterator不瞭解的,可在此瞭解

Static 成員

es6之前,使用靜態方法需要像下面這般處理:

function PersonType(name) {
    this.name = name;
}

// static method
PersonType.create = function(name) {
    return new PersonType(name);
};

// instance method
PersonType.prototype.sayName = function() {
    console.log(this.name);
};

var person = PersonType.create("xhs");
複製程式碼

現在在es6中只需要新增關鍵字static即可:

class PersonClass {

    // equivalent of the PersonType constructor
    constructor(name) {
        this.name = name;
    }

    // equivalent of PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }

    // equivalent of PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}

let person = PersonClass.create("xhs");
複製程式碼

派生繼承

es6之前,實現一個繼承是有多種方式,適當的繼承有以下步驟:

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

function Square(length) {
    Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        value:Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

var square = new Square(3);
console.log(square.getArea());              // 9
console.log(square instanceof Square);      // true
console.log(square instanceof Rectangle);   // true
複製程式碼

Square繼承自Rectangle,這使得Square.prototype需繼承自Rectangle.prototype,並且呼叫到new Rectangle(Rectangle.call(this, length, length)),這經常會迷惑一些新手。 所以出現了es6的繼承,他使得更加容易瞭解.

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }

    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    constructor(length) {

        // same as Rectangle.call(this, length, length)
        super(length, length);
    }
}

var square = new Square(3);

console.log(square.getArea());              // 9
console.log(square instanceof Square);      // true
console.log(square instanceof Rectangle);   // true
複製程式碼

直接通過extends來繼承,子類中通過super來呼叫父類的建構函式,並傳遞引數。 這樣從其他類繼承的類稱為派生類,派生類在出現的constructor中需要指定super(),否則會出錯。如果不出現 constructor,則預設會新增constructor.

使用super()的時候,需要記住下面這幾點 1. 你只可以在派生類(extends)中使用super(),否則會出錯。 2. constructor中的super()使用必須在this之前使用,因為他負責一些初始化,所以在此之前使用this會出錯。 3. 派生類中避免使用super()的唯一方法是在constructor返回一個物件(非原始型別)。

class的影子方法

這個類似於原型鏈的property,因為派生類是繼承的,所以可能存在同名的方法。 具體的關於shadowing property

繼承靜態成員

這個就類似派生繼承裡的方法,也可以被繼承。

表示式派生的類

只要一個表示式內部存在[[Constructor]]並且有prototype,那就可以被extends. 看下面這個例子:

let SerializableMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

let AreaMixin = {
    getArea() {
        return this.length * this.width;
    }
};

function mixin(...mixins) {
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base;
}

class Square extends mixin(AreaMixin, SerializableMixin) {
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    }
}

var x = new Square(3);
console.log(x.getArea());               // 9
console.log(x.serialize());             // "{"length":3,"width":3}"
複製程式碼

他仍然可以工作,因為mixin方法返回的是一個function.滿足[[Constructor]]prototype的要求。可以發現這裡例子中,雖然基類是空的,但是仍然使用了super(),否則報錯. 如果mixin中有多個相同的prototype,則以最後一個為準。

extends後面可以使用任何的表示式,但是並不是所有的表示式都會生成有效的類。有這些情況是不可以的。

  • null
  • generator function 在這些情況下,嘗試使用new去例項化一個物件,會報錯,因為這些內部不存在[[Constructor]]

繼承內部的屬性

自從陣列存在,開發者幾乎都想通過繼承定製自己的陣列型別,在es5及更早之前的版本,這幾乎是不可能的。使用經典繼承並不會使程式碼正常執行。 例如:

// 內建的陣列行為
var colors = [];
colors[0] = "red";
console.log(colors.length);         // 1

colors.length = 0;
console.log(colors[0]);             // undefined

// es5中嘗試陣列繼承

function MyArray() {
    Array.apply(this, arguments);
}

MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
        value: MyArray,
        writable: true,
        configurable: true,
        enumerable: true
    }
});

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);         // 0

colors.length = 0;
console.log(colors[0]);             // "red"
複製程式碼

可以發現,這個是不能繼承內部的屬性。es6的一個目標就是繼承內部的屬性方法。因此es6 class的繼承和es5的經典繼承略有不同: ES5的經典繼承首先呼叫的是派生類中的this,然後基類的建構函式再被呼叫,這就意味著this是作為派生類的第一個例項開始。基類的其他屬性進行修飾 ES6class 卻是恰恰相反: ES6class繼承,this首先是由基類來建立,後面通過派生類的建構函式來改變。這樣才會導致開始就是由基類內建的功能來接收所有的功能 再來看看下面的例子:

class MyArray extends Array {
    // empty
}

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length);         // 1

colors.length = 0;
console.log(colors[0]);             // undefined
複製程式碼

這樣就會完全繼承Array的內建功能。

Symbol.species屬性

extends一個有趣的事情就是任何繼承了內建的功能,最終返回。內建的例項都會自動返回到派生類的例項。例如上面的MyArray繼承自Array,像slice這樣返回的是MyArray這個派生類的例項。

class MyArray extends Array {
    // empty
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);

console.log(items instanceof MyArray);      // true
console.log(subitems instanceof MyArray);   // true
複製程式碼

在上面的程式碼中,MyArray例項返回slice()方法.正常情況下, slice()方法繼承自Array並且返回Array的例項。實際上是Symbol.species在幕後進行改變。

Symbol.species是用來定義返回函式的一個靜態訪問器屬性,這個返回的函式是每當需要在例項方法內建立例項的時候使用到的建構函式(而不是直接使用建構函式)。

以下的內建型別定義了Symbol.species:

  • Array
  • ArrayBuffer
  • Map
  • Promise
  • Set
  • RegExp
  • Typed Arrays

上面的每一個都有預設的Symbol.species,他返回this,意味著該屬性始終返回建構函式。

我們來定義一個帶有Symbol.species的類

class MyClass {
    static get [Symbol.species]() {
        return this;
    }

    constructor(value) {
        this.value = value;
    }

    clone() {
        return new this.constructor[Symbol.species](this.value);
    }
}
複製程式碼

可以發現上面這段程式碼,有個靜態的訪問器屬性,而且也可以看到上面只有getter,並沒有setter,因為要修改內建的型別,這是不可能的。 所有呼叫this.constructor[Symbol.species]的都會返回派生類 MyClass. 如clone呼叫了,並且返回了一個新的例項。 再看下面的例子:

class MyClass {
    static get [Symbol.species]() {
        return this;
    }

    constructor(value) {
        this.value = value;
    }

    clone() {
        return new this.constructor[Symbol.species](this.value);
    }
}

class MyDerivedClass1 extends MyClass {
    // empty
}

class MyDerivedClass2 extends MyClass {
    static get [Symbol.species]() {
        return MyClass;
    }
}

let instance1 = new MyDerivedClass1("foo"),
    clone1 = instance1.clone(),
    instance2 = new MyDerivedClass2("bar"),
    clone2 = instance2.clone();

console.log(clone1 instanceof MyClass);             // true
console.log(clone1 instanceof MyDerivedClass1);     // true
console.log(clone2 instanceof MyClass);             // true
console.log(clone2 instanceof MyDerivedClass2);     // false
複製程式碼

在上面的程式碼中:

  1. MyDerivedClass1繼承自MyClass並且沒有改變Symbol.species屬性, 返回了MyDerivedClass1的例項。
  2. MyDerivedClass2繼承自MyClass並且改變了Symbol.species屬性返回MyClass.當MyDerivedClass2例項呼叫clone方法的時候,返回的是MyClass的例項. 使用Symbol.species,任何派生類都可以確定方法返回例項時返回的值的型別。

例如,Array使用Symbol.species指定用於返回陣列的方法的類。在從Array派生的類中,可以確定從繼承方法返回的物件型別。如下:

class MyArray extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);

console.log(items instanceof MyArray);      // true
console.log(subitems instanceof Array);     // true
console.log(subitems instanceof MyArray);   // false
複製程式碼

上面的程式碼是重寫了Symbol.species,他繼承自Array.所有繼承的陣列的方法,這樣使用的就是Array的例項,而不是MyArray的例項.

通常情況下,要想在類方法中使用this.constructor方法,就應該使用Symbol.species屬性.

類的建構函式中使用new.target

你可以在類的建構函式中使用new.target去確定class是如何被呼叫的。一些簡單的情況之下,new.target等於方法或者類的建構函式.

class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}

// new.target is Rectangle
var obj = new Rectangle(3, 4);      // outputs true
複製程式碼

因為class呼叫必須使用new,所以這種情況下就等於Rectangle(constructor name). 但是值卻不總是一樣,如下:

class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length)
    }
}

// new.target is Square
var obj = new Square(3);      // outputs false
複製程式碼

可以發現,這裡就不是Rectangle了,而是Square.這個很重要,他可以根據呼叫方式來判斷當前的target. 基於上面這點,我們就可以定義一個不可以被例項化的基類。例如:

// abstract base class
class Shape {
    constructor() {
        if (new.target === Shape) {
            throw new Error("This class cannot be instantiated directly.")
        }
    }
}

class Rectangle extends Shape {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

var x = new Shape();                // throws error

var y = new Rectangle(3, 4);        // no error
console.log(y instanceof Shape);    // true
複製程式碼

注意: 因為class必須使用new呼叫,因此new.target在建構函式中永遠不可能是undefined


作者:xiaohesong
連結:https://juejin.im/post/5b9b068ae51d450e6c749f32
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。