原型系统
提到原型,一般会从构造函数说起,通过new关键字构建一个对象,那么它的原型(__proto__,亦称隐式原型)是构造函数的prototype。
这里我们换个角度,抛开构造函数,因为构造函数的并不是原型系统的本质。理解原型系统的本质,请记住下面这两条:
- JS的所有对象,都有一个__proto__属性,__proto__属性引用的对象,就是该对象的原型。
- 从一个对象上读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
上面两条,也可以说是从运行时的角度来看原型系统。无论我们用什么方式来构造的对象,在运行时或者说在内存中,都是对象之间在交互、工作。
从这角度理解原型和原型链非常简单,对象的__proto__就是它的原型,__proto__构成的链就是原型链。
执行const p1 = {} 构建的对象
上图就是一个简单对象在运行时中的样子。
多说一嘴上图中的p0,也就是Object.prototype,是JavaScript的内置对象。也就是JavsScript语言运行起来就有的对象,我们用字面量{}语法、Object构造器或者class关键字创建的对象的原型都是它。
执行const p2 = Object.create(p1) 构建原型链
抛开了构造器,我们使用ES5增加的方法Object.create来构建原型链。
为什么要抛开构造器
抛开构造函数来谈原型,我们就得到了一个最核心、最本质的原型系统概念。
基于原型的面向对象 vs 基于类的面向对象
在讨论构造器之前,要讨论两种面向对象的流派。基于原型的面向对象和 基于类的面向对象。
基于类的面向对象可以说非常主流,Java、C#、C++等都是基于类的面向对象。在这种编程思维中,要先有类,再通过类来实例对象。强大的类型系统是它的优点将万事万物都分门别‘类’。类之间也有了封装继承多态的概念。
但JS是基于原型的面向对象,想理解它先要抛开基于类的概念,不强调分门别类,对象是不用类型、你声明它就存在于内存的、一个动态的(可任意增删属性,这点基于类的系统也难做到),只有对象之间存在关系的面向对象思维。而对象之间的关系,就是用原型联系的,通过原型链来实现逻辑复用。
两种思维孰优孰劣?应该是个有千秋,没有最好的只有最适合的。强大的类型系统有利于搭建复杂的后端系统。在前端很少有多层继承关系,而是更强调组件化的项目中,类型系统也不一定好用,基于原型也不错,还有动态性。
构造器不属于基于原型的面向对象思维的内容
在JS语言设计之初,作者受到公司和商业的因素,需要把语言设计的像Java,所以有了构造器、new关键字、this关键字。所以说构造器是从基于类的面向对象衍生出来的Java语言的舶来品。
于是乎,ES5有了Object.create,有了它我们可以轻松的制造原型链,可以剔除基于类的东西,用完全是基于原型的面向对象思维来写程序。
构造器还有个缺点是,function在语法上存在二义性,它可能是个函数,也可能是个构造器。如果我们不用构造器来编织原型系统,可以让function只表示函数这一种语义。
回头再看构造器
JS中存在的编程思维
分两大类,面向对象和函数式,面向对象思维又分为上面提到的基于对象和基于类的。
- 基于原型的面向对象:ES3支持的不好,ES5有了Object.create得以完善。
- 基于类的面向对象:ES3、ES5支持的都不好,ES6有了class、extends关键字,得以完善。
- 函数式:因为函数在JS中也是个对象,可以当作参数传递,又支持闭包,所有JS可以用函数式的思维编程。
JS支持3种编程思维是个非常有意思的事,其他语言如Java,是根据基于类的面向对象思维打造的语言。JS却不是为哪种思维量身打造的,却哪种都可以支持,这成了一把双刃剑,难以掌握其精髓,却又很强大。
JS基于类的面向对象
这里我们重点讨论一下基于类的面向对象。我之前做过几年C#开发,学习JS之处,总喜欢用基于类的思维来理解JS,让JS也能满足基于类的那种继承。
我们来看代码:
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function(x,y) {
this.x += x;
this.y += y;
}
// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // 调用父类构造函数(call super constructor).
this.z = 0;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.scale = function(val) {
this.x = this.x * val;
this.y = this.y * val;
}
var rect = new Rectangle();
rect.move(1,1);
rect.scale(0.8);
复制代码
我们来画图:
如上图,我们的代码构建了一条原型链,使得rect实例有自己的字段x、y、z,可以通过原型链找到scale、move方法。
这是个很有趣的现象,我们写的基于类的面向对象思维的代码,在运行时是用原型系统在工作,却达到了基于类的面向思维中继承(子类的实例拥有父类实例的属性和行为)的效果。
在ES6中,有了class、extends,我们有了更优雅的方式来写基于类的代码。
class Rectangle {
constructor(height, width) {
super()
this.z = 0;
}
scale(val) {
this.x = this.x * val;
this.y = this.y * val;
}
}
复制代码
但ES6只是写法上更优雅,在运行时,js还是基于原型系统。
术语
显式原型&隐式原型
- 显式原型:prototype
- 隐式原型:__proto__
[[prototype]]和__proto__
[[prototype]]和__proto__意义相同,均表示对象的内部属性,其值指向对象原型。前者在一些书籍、规范中表示一个对象的原型属性,后者则是在浏览器实现中指向对象原型。
总结
通过这篇文章我们介绍了JS原型系统的本质,想理解它必须先抛弃用基于类面向对象那一套思想。了解原型系统之后,再去看JS中基于原型的基于类的面向对象思想都是怎样实践的。这样整体了解之后,才算是对JS原型有了比较好的理解。