ES6 class 与 ES5 构造函数的转换

2019-04-04JavaScript

ES6 引入了 class 语法,其本质是 ES5 的对象构造方式的语法糖。本文介绍 class 语法在 ES5 中等价的实现。

注意,本文目的在于探究其实现本质,不涉及调用方式检查等细节的实现。

我们分别来看无继承与有继承的实现。


单个 class

constructor

classconstructor 和 ES5 的构造函数没有任何区别,可以认为是模仿其他面向对象语言的产物。

class ClassA
{
    constructor(a)
    {
        this.a = a;
    }
}

等价于

function ClassA(a)
{
    this.a = a;
}

原因是,对于以上两种定义方式,都有

console.log(typeof ClassA);     // 'function'

console.log(ClassA.prototype.constructor === ClassA);   // true

const classA = new ClassA(1);

console.log(classA.hasOwnProperty('a'));    // true

console.log(classA);    // ClassA { a: 1 }

方法

class 中的方法定义在其原型对象上。

class ClassA
{
    constructor(a)
    {
        this.a = a;
    }

    getA()
    {
        return this.a;
    }
}

等价于

function ClassA(a)
{
    this.a = a;
}

ClassA.prototype.getA = function ()
{
    return this.a;
};

原因是

class ClassA
{
    constructor(a)
    {
        this.a = a;
    }

    getA()
    {
        return this.a;
    }
}

const classA = new ClassA(1);

console.log(classA.hasOwnProperty('getA'));     // false

console.log(classA.getA === ClassA.prototype.getA);     // true

console.log(classA.getA === Object.getPrototypeOf(classA).getA);    // true

Object.setPrototypeOf(classA, null);

console.log(typeof classA.getA);    // 'undefined'

继承的 class

根据 ES6 标准,classextend 可以让“子类”继承“父类”的属性和方法,其中包括静态属性与方法。

在 ES6 中,继承的行为如下所示:

class ClassA
{
    constructor(a)
    {
        this.a = a;
    }

    getA()
    {
        return this.a;
    }
}

ClassA.hello = 'hello';     // 静态属性 

class ClassB extends ClassA
{
    constructor(a, b)
    {
        super(a);
        this.b = b;
    }

    getB()
    {
        return this.b;
    }
}

const classB = new ClassB(1, 2);

// classB 既是 ClassB 的实例又是 ClassA 的实例
console.log(classB instanceof ClassB);  // true
console.log(classB instanceof ClassA);  // true

// classB 获得了 ClassA 与 ClassB 的所有方法
console.log(classB.getA());     // 1
console.log(classB.getB());     // 2

// classB 获得了 ClassA 与 ClassB 定义的所有数据域,且这些数据域都位于 classB 上
console.log(classB);            // ClassB { a: 1, b: 2 }
console.log(classB.hasOwnProperty('a'));    // true

// ClassB 的原型对象是 ClassA,以实现静态属性的继承
console.log(Object.getPrototypeOf(ClassB) === ClassA);  // true
console.log(ClassB.hello);      // 'hello'
Object.getPrototypeOf(ClassB).hello = 'hello world';
console.log(ClassA.hello);      // 'hello world'

从以上代码我们可以得到以下几个结论

classB 既是 ClassB 的实例又是 ClassA 的实例

// classB 既是 ClassB 的实例又是 ClassA 的实例
console.log(classB instanceof ClassB);  // true
console.log(classB instanceof ClassA);  // true

instanceof 的实现机制

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

我们可以知道 ClassAprototype 对象出现在了 ClassB 的原型链上。以下代码可以证明这一点:

console.log(Object.getPrototypeOf(ClassB.prototype) === ClassA.prototype);  // true

这证明 ClassB 原型对象的原型对象是 ClassA 的原型对象(有点像绕口令),如下图

prototype

因此必然有

Object.setPrototypeOf(ClassB.prototype, ClassA.prototype);

classB 获得了 ClassAClassB 的所有方法

// classB 获得了 ClassA 与 ClassB 的所有方法
console.log(classB.getA());     // 1
console.log(classB.getB());     // 2

由于 ClassB 原型对象的原型对象是 ClassA 的原型对象,因此这一点容易理解。其关系如下图

prototype

所有 ClassB 的实例都可以从原型链上获得 getAgetB 方法。

classB 获得了 ClassAClassB 定义的所有数据域,且这些数据域都位于 classB

// classB 获得了 ClassA 与 ClassB 定义的所有数据域,且这些数据域都位于 classB 上
console.log(classB);            // ClassB { a: 1, b: 2 }
console.log(classB.hasOwnProperty('a'));    // true

这一点是 ES6 的所谓“类”(JavaScript 中不存在类的概念)与其他面向对象语言的根本不同。

在其他面向对象语言中,对子类实例的构造会先完成父类实例的构造,也就是说,对子类实例的构造实质上构造了两个对象。但是对于 ES6 的 class 语法,不管如何继承,始终只会构造一个对象,所有父类的数据域都会直接添加到构造对象上。

这个添加过程是什么时候做的?在“子类”在构造函数中调用 super() 时。在“子类”的构造函数中,super(arguments) 的调用等价于 superClass.prototype.constructor.call(this, ...arguments)。这也是为什么 ClassAa 数据域会出现在 classBthis 上。

ClassB 的原型对象是 ClassA,以实现静态属性的继承

// ClassB 的原型对象是 ClassA,以实现静态属性的继承
console.log(Object.getPrototypeOf(ClassB) === ClassA);  // true
console.log(ClassB.hello);      // 'hello'
Object.getPrototypeOf(ClassB).hello = 'hello world';
console.log(ClassA.hello);      // 'hello world'

以上三点都将 ClassAClassB 作为构造函数看待,这一条需要将其作为对象看待。

我们知道,静态属性的定义方式是这样的

const objectA = {};
objectA.propertyA = 1;

如果我想让很多对象都共享同一个 propertyA,最容易想到的就是通过原型继承:

const objectB = {};
console.log(objectB.propertyA);     // undefined
Object.setPrototypeOf(objectB, objectA); 
console.log(objectB.propertyA);     // 1

// 修改原型的属性实质上是修改 objectA 上的属性
Object.getPrototypeOf(objectB).propertyA = 2;
console.log(objectA.propertyA);     // 2

把以上代码的 objectA 换成 ClassAobjectB 换成 ClassB,就是 ES6 class 继承静态属性的原理。


总结

根据以上的分析结果,我们可以写出完整的转换代码:

class ClassA
{
    constructor(a)
    {
        this.a = a;
    }

    getA()
    {
        return this.a;
    }
}

ClassA.hello = 'hello';

class ClassB extends ClassA
{
    constructor(a, b)
    {
        super(a);
        this.b = b;
    }

    getB()
    {
        return this.b;
    }
}

在 ES5 中等价于

function ClassA(a)
{
    this.a = a;
}

ClassA.hello = 'hello';

ClassA.prototype.getA = function ()
{
    return this.a;
};

function ClassB(a, b)
{
    ClassA.prototype.constructor.call(this, a); // 添加 ClassA 的数据域到 this
    this.b = b;
}

ClassB.prototype.getB = function ()
{
    return this.b;
};

Object.setPrototypeOf(ClassB.prototype, ClassA.prototype);  // 创建原型链继承方法
Object.setPrototypeOf(ClassB, ClassA);  // 继承静态属性

利用立即执行函数,我们可以写出更加一体化的代码:

var ClassA = (
    function ()
    {
        function ClassA(a)
        {
            this.a = a;
        }

        ClassA.hello = 'hello';

        ClassA.prototype.getA = function ()
        {
            return this.a;
        };
        return ClassA;
    }
)();

var ClassB = (
    function (ClassA)
    {
        function ClassB(a, b)
        {
            ClassA.prototype.constructor.call(this, a);
            this.b = b;
        }

        ClassB.prototype.getB = function ()
        {
            return this.b;
        };

        Object.setPrototypeOf(ClassB.prototype, ClassA.prototype);
        Object.setPrototypeOf(ClassB, ClassA);

        return ClassB;
    })(ClassA);

当然,这样的转换忽略了很多细节问题。例如在 class 中定义的方法是不可枚举的、class 定义的构造函数不能通过非 new 调用等,这些细节无关本质,因此在本文中就不涉及了。


参考文献

  1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof

  2. https://es6.ruanyifeng.com/#docs/class