理解 JavaScript 的prototype
属性不太容易。你也许知道它同面向对象编程(OOP)和对象继承有关,但未必对其技术原理非常清楚。
原型继承
面向对象编程可以通过很多途径实现。其他的语言,比如 Java,使用基于类的模型实现: 类及对象实例区别对待。但在 JavaScript 中没有类的概念,取而代之的是一切皆对象。JavaScript 中的继承通过原型继承实现:一个对象直接从另一对象继承。对象中包含其继承体系中祖先的引用——对象的 prototype 属性。
class
关键字是在 ES6 中首次引入 JavaScript 的。其实,它并没有为面向对象继承引入新模型,class
关键字通过语法糖,实现了本文介绍的原型特性和构造函数。
JavaScript 实现继承的语言特性
以下语言特性共同实现了 JavaScript 继承。
- 当尝试访问 JavaScript 对象中不存在的属性时,解析器会查找匹配的对象原型。例如调用
car.toString()
,如果car
没有toString
方法,就会调用car
对象的原型。 这个查找过程会一直递归, 直到查找到匹配的原型或者继承链尽头。 - 调用
new Car()
会创建一个新的对象,并初始化为Car.prototype
。 这样就允许为新对象设置原型链。需要注意的是,new Car()
只有当Car
是函数时才有意义。 此类函数即所谓构造函数。 - 调用对象的一个成员函数时,
this
的值被绑定为当前对象。例如调用"abc".toString()
,this
的值被设置为"abc"
,然后调用toString
函数。该技术支持代码重用:同样的代码,可在this
为各种不同的值时调用。对象的成员函数,也被称为对象的方法。
举个栗子
我们用面向对象编程,实现一个计算矩形周长的例子。
1 2 3 4 5 6 7 8 9 10 11 |
function Rectangle(x, y) { this.x = x; this.y = y; } Rectangle.prototype.perimeter = function() { return 2 * (this.x + this.y); } var rect = new Rectangle(1, 2); console.log(rect.perimeter()); // outputs '6' |
首先,我们定义构造函数 Rectangle
。 按照规范,我们大写构造函数名首字母,表明它可以用 new
调用,以示与其他常规函数的区别。构造函数自动将 this
赋值为一空对象,然后代码中用 x
和 y
属性填充它,以备后用。
然后, Rectangle.prototype
新增一个通过 x
和 y
属性计算周长成员函数。 注意 this
的使用,在不同的对象中,this
会有不同的值,这些代码都可以正常工作。
最后, 一个名为 rect
的对象创建出来了。 它继承了 Rectangle.prototype
, 我们可以调用 rect.perimeter()
, 然后将结果打印到控制台。
prototype 属性名称带来的误解
有一些关于 JavaScript 的原型的误解。 一个对象的原型与对象的 prototype
属性并非一回事。 前者用于在原型链中匹配不存在的属性。后者用于通过 new
关键字创建对象,它将作为新创建对象的原型。 理解二者的差异,将帮助你彻底理解 JavaScript 中的原型特性。
在我们的例子中, Rectangle.prototype
是用 new Rectangle()
创建出来对象的原型, 而 Rectangle
的原型实际上是 JavaScript 的 Function.prototype
。(子对象的原型是父对象的 prototype 属性)
对象中保存原型的变量,也被称之为内部原型引用(the internal prototype link),历史上也曾称之为 __proto__ ,对这个称谓始终存在一些争议。 更精确的,它可以被称为 Object.getPrototypeOf(...)
的返回值。