JavaScript 的原型(prototype)及其实例是不容易理解的东西,这里总结一下。
一、原型与实例
先看下面的例子:
function Foo(value) { this.name = value; this.prototype.type = "example"; } var foo1 = new Foo("hello"); var foo2 = new Foo("world");
在这个例子中,总共出现三种不同的东西:Foo 是构造函数(constructor),通过使用 new 关键字,我们调用这个构造函数,从 Foo.prototype 这个原型对象中生成 foo1 和 foo2 这两个实例(instance)。三者的关系图示如下:
Foo
Foo.prototype =====> foo1 和 foo2
举个实际的例子作类比:原型对象 Foo.prototype 相当于纸张,构造函数 Foo 相当于打印机,而实例 foo1 和 foo2 相当于我们打印出来的文章。
默认情形 Foo 的原型 Foo.prototype 是一个空的对象,我们可以在 Foo 构造函数中添加该原型对象的一些属性,例如上例中的我们增加了 type 属性到 Foo.prototype 中。原型中的属性是被所有实例中共享的,因此 foo1.type 和 foo2.type 都等于 "example";但是构造函数中的属性是与实例有关的,因此 foo1.name = "hello" 不同于 foo2.name = "world"。
对 Foo 的原型对象的修改也可以放在 Foo 外面,上面的例子这样写也是可以的:
function Foo(value) { this.name = value; } Foo.prototype.type = "example"; var foo1 = new Foo("hello"); var foo2 = new Foo("world");
对于构造函数,通过 prototype 属性可以访问它的原型;而对于实例 foo1 和 foo2,可以通过内部属性 __proto__ (在ECMAScript 标准中称为 [[Prototype]])来访问它们的原型。即有
foo1.__proto__ === foo2.__proto__ === Foo.prototype
这个 __proto__ 属性是隐藏的,在实际编程中最好不要使用它。因为利用构造函数的 prototype 属性就可以访问原型对象了。
最后,利用 constructor 属性可以访问原型对象或者实例对象的构造函数。即有
Foo.prototype.constructor === Foo foo1.constructor === foo2.constructor === Foo
这样,对于这个简单的例子,三者的关系就明朗了。
二、原型与继承
在 JavaScript 中,一个构造函数的原型对象是可以自己设定的,如果我们将该原型对象指向另外一个对象,就达到了继承的目的。例如:
function Parent() {}; Parent.prototype.familyName = "Warner"; function Child(name) { this.givenName = name; } Child.prototype = new Parent(); var child1 = new Child("Tom"); var child2 = new Child("Jerry"); child1.gender = "male"; child2.gender = "female";
在这个例子中,我们指定 Child 构造函数的原型对象为 Parent 函数的原型对象的实例,从而 child1 和 child2 都继承了 Parent 函数的原型对象的 familyName 属性。即有
child1.familyName === child2.familyName === "Warner"
在使用原型链实现继承之后,当我们要查找实例的某个属性时,是沿着原型链逐级往上的。比如我们要获取 child1.familyName 的值,首先是在 child1 对象中查找,没找到;接着到 child1.__proto__ 即 Child.prototype 对象中查找,还是没找到。接着就到 child1.__proto__.__proto__ 即 Child.prototype.__proto__ 即 Parent.prototype 对象中查找,终于找到了child1.familyName = "Warner"。如果要获取 child1.givenName 的值,则需要两步。而获取 child1.gender,一步就找到了。
console.log(child1.gender, child1.givenName, child1.familyName); // male Tom Warner
另外,对于继承的情形, child1,child2,以及 Child.prototype 的 constructor 属性指向了 Parent 函数。即有
child1.constructor === child2.constructor === Child.prototype.constructor === Parent
三、默认的原型
对于任何的函数(不管它是否作为构造函数)和对象,它们可以分别看成 Function() 和 Object() 构造函数的实例,因此它们实际上都有一个默认的原型对象,分别指向 Function.prototype 和 Object.prototype。即对于下面的例子:
fun1 = function() {}; fun2 = new Function(); obj1 = {}; obj2 = new Object();
我们有如下的结果:
fun1.__proto__ === fun2.__proto__ === Function.prototype; obj1.__proto__ === obj2.__proto__ === Object.prototype;
有意思的是,Function() 和 Object() 自身也是函数,因此它们的原型对象,同样指向 Function.prototype。即有
Object.__proto__ === Function.__proto__ === Function.prototype
最后,Function.prototype 的 __proto__ 属性指向 Object.prototype,而 Object.prototype 的 __proto__ 为空。即有
Function.prototype.__proto__ === Object.prototype Object.prototype.__proto__ === null
实际上,在 JavaScript 中,所有数据类型都是对象的实例。而 Object.prototype 这个原型已经处于最顶层了。
四、例子及解释
function Foo() {}; Foo.prototype.type = "example"; var foo1 = new Foo(); function Bar() {}; Bar.prototype = {type: "example"}; var bar1 = new Bar();
上面两种方式看似一样,实际却不同:foo1 的构造函数为 Foo,而 bar1 的构造函数变成了 Object,即为
foo1.constructor === Foo.prototype.constructor === Foo bar1.constructor === Bar.prototype.constructor === Object
这是因为 Foo.prototype.type = "example"; 没有修改 Foo.prototype 的指向。而 Bar.prototype = {type: "example"}; 修改了 Bar.prototype 的指向,相当于继承了 Object ,从而使得 bar1 的构造函数变成了 Object()。即上面的 Bar 代码相当于如下:
var obj = new Object(); obj.type = "example"; function Bar() {}; Bar.prototype = obj; var bar1 = new Bar();
注记:如果一个函数作为构造函数,而在它内部又用 return 语句返回一个对象,将会导致它所生成的实例指向这个返回的对象。因此,在构造函数中使用 return 是画蛇添足,反而添乱。
参考资料:
[1] Javascript Object Hierarchy
[2] Constructors considered mildly confusing
[3] 构造函数 - JavaScript 秘密花园
[4] new - JavaScript | MDN
[5] constructor - JavaScript | MDN
[6] __proto__ - JavaScript | MDN