使用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");

实现JavaScript继承-LMLPHP

关于原型/链,记住以下几点:

  • 原型链的终点是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`关键字的区别如下:

实现继承

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

12-27 19:33