ECMAScript 2015引入了类机制。为了保证最小的改动,JavaScript 通过原型模式来实现 OO 语义。
像 GoF 里介绍的原型模式,其实都是基于 Java 这样标准的面向对象语言,所需要的就是对原型对象的深拷贝。原型设计模式避免了工厂模式中复杂的创建者,也就是针对每种对象的工厂。
在 JavaScript 里一开始就有了原型。但这种原型是浅拷贝的,这基本上是基于效率的考虑。从这个机制上,JavaScript 的原型更接近于静态全局对象,比如 Scala 里的伴生对象。
基于这个机制,JavaScript 实现继承就和 Java 这样传统的面向对象语言没有特别大的区别,原型链和 JVM 中的动态分派(dispatch)是一致的,都需要沿着继承关系向上查找方法。当然 JVM 为了性能,会通过建表的方式避免进行链式搜索,这就是虚方法表。JavaScript 还没有类似的机制,可能对于浏览器而言,对于内存的需求要优于时间的需求吧。
接下来看看这种设计的巧妙。比如,我希望模拟一种类似于 Java 的类[1]:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
复制代码
这里this
绑定的执行上下文是一个尚未完全初始化的Rectangle
。对于弱类型的 JavaScript 而言,这种暴露是完全被接受的。对于静态方法,其实就相当于使用call
进行调用。
对于this
,类语法和此前函数的语法有一个区别:
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined
Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined
复制代码
不论在严格还是非严格模式下,上述代码都是undefined
。但如果是函数嵌套的调用,那么会在非严格模式下绑定到global对象,即window
。这是因为class
中的代码从语法上必须严格绑定到类上。
其实上面和原型链没有太大关系,但他体现了 JavaScript 这一套设计的历史遗留。class
是 ES6 才引入的,而new
早已出现。相比之前 JavaScript 复杂的对象创建方式(可以参考《JavaScript 高级程序设计》),ES6 的类设计是一个重要的改进。
调用new Rectangle()
的结果是继承了函数中的prototype
到对象中(注意到__proto__
这个字段并不在规范中,应该使用Object.getPrototypeOf
)。这一方式实现了类似于 Java 静态域的效果。所以如果使用Array.prototypeProp = 3
这样的写法,只是绑定值到Array
这个构造方法,而不是原型中,无法通过实例对象来访问——但是Array.prototype.prototypeProp = 3
可以让每个实例进行访问。从对象的语义上,前者是不完备的,比如 Java 就允许通过实例对象访问静态域。
最后是如何实现继承的链式结构。这一点原型链的实现很简单,只需要让每个原型中保存(这里保存的是指针)父类的原型即可,这也正是原型设计模式的思路。
和 JavaScript 奇怪的作用域链一样,基于原型的对象模型将很多概念和实现混杂在了一起,构成了前端学习的不必要困难之一。
参考资料
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Prototype_methods ↩︎