實現JavaScript繼承

雪飛鴻發表於2020-12-27

使用TypeScript或者ES2015+標準中的extends關鍵字是很容易實現繼承的,但這不是本文的重點。JS使用了基於原型(prototype-based)的繼承方式,extends只是語法糖,本文重點在於不使用extends來自己實現繼承,以進一步理解JS中的繼承,實際工作中肯定還是要優先考慮使用extends關鍵字的。

原型 & 原型鏈

原型用於物件屬性的查詢。畫出下面程式碼中的原型鏈圖示:

class Person {
    private _name: string;
​
    constructor(name: string) {
        this._name = name;
    }
​
    get getName(): string {
        return this._name;
    }
}
​
let person = new Person("xfh");

 

 

圖中,__proto__表示例項的原型物件,prototype表示建構函式的原型物件。不再推薦使用__proto__,將來可能會被廢棄,可使用Object.getPrototypeOf()來獲取物件的原型。

關於原型/鏈,記住以下幾點:

  • 原型鏈的終點是null,從這個角度,可以將null看作所有Object的基類

  • 例項的原型物件和它建構函式的原型物件是同一個物件(比較拗口)

  • 所有的函式(包括建構函式及Function自身)都是Function的例項

  • 函式是普通的物件,只是具備了可呼叫(callable)功能 ,想到了Python中的類裝飾器,也是具備了可呼叫功能的普通類

  • 所有的物件終歸是Object的例項,即Object位於所有物件的原型鏈上

// 原型鏈的終點是null
Object.getPrototypeOf(Object.prototype)===null // true
Object.prototype instanceof Object // false
// 例項和建構函式的原型物件是同一個物件
Object.getPrototypeOf(Function)===Function.prototype // true
// 所有的函式(包括建構函式及Function自身)都是Function的例項
Function instanceof Function // true,Function是自己的例項
Object instanceof Function // true,建構函式是Function的例項
// 所有的物件終歸是Object的例項,即Object位於所有物件的原型鏈上
Function.prototype instanceof Object // true
Function instanceof Object // true
Object instanceof Object // true

 

typeof操作符與instanceof`關鍵字的區別如下:

Keep in mind the only valuable purpose of typeof operator usage is checking the Data Type. If we wish to check any Structural Type derived from Object it is pointless to use typeof for that, as we will always receive "object". The indeed proper way to check what sort of Object we are using is instanceof keyword. But even in that case there might be misconceptions.

實現繼承

JS中物件成員分為三類:例項、靜態、原型。例項成員繫結到具體例項上(通常是this上),靜態成員繫結到建構函式上,原型成員就存在原型物件上:

/**
 * 從基類繼承成員
 * @param child 子類建構函式或例項
 * @param base 基類建構函式或例項
 */
function inheritMembers(child, base) {
    let ignorePropertyNames = ["name", "caller", "prototype", "__proto__", "length", "arguments"];
    let propertyNames = Object.getOwnPropertyNames(base);
    for (let propertyName of propertyNames) {
        if (ignorePropertyNames.includes(propertyName)) {
            continue;
        }
        let descriptor = Object.getOwnPropertyDescriptor(base, propertyName);
        if (!descriptor) {
            continue;
        }
        Object.defineProperty(child, propertyName, descriptor);
    }
}
/**
 * 從基類繼承原型及靜態成員
 * @param thisCtor 子類建構函式
 * @param baseCtor 基類建構函式
 */
function inheritSharedMembers(thisCtor, baseCtor) {
    if (typeof thisCtor !== "function" || typeof baseCtor !== "function") {
        throw TypeError("引數必須是函式:thisCtor,baseCtor");
    }
    // 繼承原型成員
    thisCtor.prototype = Object.create(baseCtor.prototype);
    thisCtor.prototype.constructor = thisCtor;
    // 繼承靜態成員
    inheritMembers(thisCtor, baseCtor);
}
/**
 * 呼叫子類及父類建構函式建立子類例項,並繼承父類例項成員(這也是呼叫父類建構函式的原因)
 * @param thisInstance 子類例項
 * @param baseInstance 父類例項
 */
function createInstance(thisInstance, baseInstance) {
    inheritMembers(thisInstance, baseInstance);
    return thisInstance;
}
​
// 建構函式
function Animal(tag) {
    // 例項屬性
    this.tag = tag;
}
// 靜態方法,需通過建構函式來呼叫
Animal.bark = function () {
    console.log("static func, this= " + this + ", typeof this=" + typeof this);
};
// 原型方法,需通過例項來呼叫
Animal.prototype.getInfo = function () {
    console.log("property func, tag:" + this.tag);
};
​
function Dog(name = null) {
    this.name = name ?? "default";
}
// 新增子類原型方法
Dog.prototype.dogBark = function () {
    console.log("dog bark");
};
// 繼承父類原型及靜態成員
inheritSharedMembers(Dog, Animal);
​
var animal = new Animal("animal");
Animal.bark();
// TypeError: animal.bark is not a function
// animal.bark();
animal.getInfo();
// property getInfo not exist on type 'typeof Animal'
// Animal.getInfo();
​
​
let dog = createInstance(new Dog("dog"), new Animal("dog"));
​
dog.getInfo();
dog.dogBark();
Dog.bark();
console.log(dog.name);

 

最後使用v4.1.3版本的TS,編譯為ES5版本的JS,看看TS背後是如何實現繼承的:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        // 只能在建構函式中使用this關鍵字
        this.name = name;
        this.age = age;
    }
    // 靜態方法中呼叫本類中的另一個靜態方法時,可以使用this.methodName的形式
    // 在外部呼叫時只能類名.方法名的形式,所以此時方法內部,this是指向建構函式的
    // 即,this.methodName等價於類名.方法名
    static static_method() {
        // 這裡this指向Person類,typeof this=function
        // 可以看出class Person本質上是建構函式,class只是語法糖
        console.log(`static method, this=${this}, typeof this=${typeof this}`);
    }
}
​
// 使用extends繼承
class Chinese extends Person {
    constructor(name: string, age: number) {
        // 必須呼叫父類建構函式,且需要在子類建構函式使用this關鍵字之前呼叫,否則會產生錯誤:
        // A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
        super(name, age);
    }
​
    sayHello() {
        console.log(`I'm ${this.name}, I'm ${this.age} years old.`)
    }
}
​
​
let cn = new Chinese('xfh', 26);
​
cn.sayHello();
Chinese.static_method();

編譯後程式碼如下:

"use strict";
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Person = /** @class */ (function () {
    function Person(name, age) {
        // 只能在建構函式中使用this關鍵字
        this.name = name;
        this.age = age;
    }
    // 靜態方法中呼叫本類中的另一個靜態方法時,可以使用this.methodName的形式
    // 在外部呼叫時只能類名.方法名的形式,所以此時方法內部,this是指向建構函式的
    // 即,this.methodName等價於類名.方法名
    Person.static_method = function () {
        // 這裡this指向Person類,typeof this=function
        // 可以看出class Person本質上是建構函式,class只是語法糖
        console.log("static method, this=" + this + ", typeof this=" + typeof this);
    };
    return Person;
}());
// 使用extends繼承
var Chinese = /** @class */ (function (_super) {
    __extends(Chinese, _super);
    function Chinese(name, age) {
        // 必須呼叫父類建構函式,且需要在子類建構函式使用this關鍵字之前呼叫,否則會產生錯誤:
        // A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.
        return _super.call(this, name, age) || this;
    }
    Chinese.prototype.sayHello = function () {
        console.log("I'm " + this.name + ", I'm " + this.age + " years old.");
    };
    return Chinese;
}(Person));
var cn = new Chinese('xfh', 26);
cn.sayHello();
Chinese.static_method();

推薦閱讀

JavaScript data types and data structures

Object.prototype.__proto__

Object prototypes

相關文章