ES6 引入了 class
语法,其本质是 ES5 的对象构造方式的语法糖。本文介绍 class
语法在 ES5 中等价的实现。
注意,本文目的在于探究其实现本质,不涉及调用方式检查等细节的实现。
我们分别来看无继承与有继承的实现。
单个 class
constructor
class
的 constructor
和 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 标准,class
的 extend
可以让“子类”继承“父类”的属性和方法,其中包括静态属性与方法。
在 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 属性是否出现在对象的原型链中的任何位置。
我们可以知道 ClassA
的 prototype
对象出现在了 ClassB
的原型链上。以下代码可以证明这一点:
console.log(Object.getPrototypeOf(ClassB.prototype) === ClassA.prototype); // true
这证明 ClassB
原型对象的原型对象是 ClassA
的原型对象(有点像绕口令),如下图
因此必然有
Object.setPrototypeOf(ClassB.prototype, ClassA.prototype);
classB
获得了 ClassA
与 ClassB
的所有方法
// classB 获得了 ClassA 与 ClassB 的所有方法
console.log(classB.getA()); // 1
console.log(classB.getB()); // 2
由于 ClassB
原型对象的原型对象是 ClassA
的原型对象,因此这一点容易理解。其关系如下图
所有 ClassB 的实例都可以从原型链上获得 getA
与 getB
方法。
classB
获得了 ClassA
与 ClassB
定义的所有数据域,且这些数据域都位于 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)
。这也是为什么 ClassA
的 a
数据域会出现在 classB
的 this
上。
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'
以上三点都将 ClassA
与 ClassB
作为构造函数看待,这一条需要将其作为对象看待。
我们知道,静态属性的定义方式是这样的
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
换成 ClassA
,objectB
换成 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
调用等,这些细节无关本质,因此在本文中就不涉及了。
参考文献
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof
https://es6.ruanyifeng.com/#docs/class