每一个类都有一个 prototype 属性,这是一个静态属性,该属性值包含了标识该类的一个对象,这个对象称为原型对象。
原型实际上就是一个数据集合,即普通对象。继承于 Object 类,由于 JavaScript 自动创建并依附于每一个构造函数。
在原型对象上定义了一些内部属性用于描述该类,其中就包含该类的基类的信息。通过该信息, JavaScript 解释引擎就可以知道该类的基类。同时基类也有相同的构成,因此 JavaScript 解释引擎就可以知道基类的基类,这就建立起了一个链条,因为描述基类信息的内部属性称为 [Prototype] ,所以,该链条也被称为原型链( prototype chain )。
原型链条的尽头是 Object 的原型对象,该对象的内部属性 [Prototype] 的值是 null 。要想了解原型链,就必须了解内部属性 Prototype 。
要想真正理解 JavaScript 对象原型链,必须从类的内部构造谈起。
按照 ECMA-262 标准的规定:
当一个类定义时,它有原型对象( prototype object ),原型对象必须声明有多个内部特定的属性,作为类的特性。
其中两个内部属性就是 [Prototype] 和 [Class] (当然还有其它内部属性,这里我们略过不作介绍。注意这些内部属性我们在描述时使用方括号包围)。
ECMA-262 标准还规定:
在创建类的实例时,该实例隐式地包含有对自身原型对象的引用。因此,实例中也包含有内部属性 [Prototype] 的定义。
内部属性 [Prototype] 和 [Class] 的表示如下(为了突出这两个内部属性,我们使用方括号来标注)。
1 .内部属性 [Prototype] 。
[Prototype] 属性表示该类的父类的原型对象(注意是父类的原型对象,并非当前类的原型对象),一般都是使用属性 __proto__
来访问,不过 IE
封闭了该属性,不能在外部访问,其它几个浏览器都可以在外部公开访问该属性。
注意区分内部属性与静态属性 prototype ,其首字母大小写有别。
2 .内部属性 [Class] 。
[Class] 属性表示类名,是一个字符串。下面是 ECMA-262 标准第 5 版规定的所有内建 JavaScript 类的 [Class] 属性值:
'Arguments',
'Array',
'Boolean',
'Date',
'Error',
'Function',
'JSON',
'Math',
'Number',
'Object',
'RegExp',
'String';
例如,内建类 Array 定义时,它有原型对象,并定义了内部属性 [Prototype] 和 [Class] :
{
"Prototype": `Object 的原型对象`,
"Class": "Array"
}
也就是说,当类 Array 定义时,必须声明这两个属性, [Class] 标示类名, [Prototype] 标示父类的原型,这样,在运行时, JavaScript 解释引擎通过 [Class] 就能知道该类的名称信息,通过 [Prototype] 就能找到该类的父类的原型对象信息。
每个内建 JavaScript 类的内部属性 [Prototype] 的值都是 Object 对象(因为它们都直接继承自 Object ),除了 Object 自身的 [Prototype] 属性,因为 Object 类是根类,所以它没有父类,因此代表父类原型对象的 [Prototype] 属性的值为 null ,也即:
{
"Prototype": null,
"Class": "Object"
}
鉴于这种定义,内部属性 [Prototype] 表示了一个关系,这种关系构成了继承关系,同时也构成了一个原型链。
内部属性 [Prototype] 规定了当前类的父类的原型对象,这究竟能说明什么呢?下面我们使用 Array 类的定义来说明一下。
类 Array 的原型对象的内部属性定义如下:
{
"Prototype": `Object 的原型对象`,
"Class": "Array"
}
内部属性 [Prototype] 本来是不允许在 JavaScript 程序中访问的,但是 Firefox 等浏览器却公开了一个名为 __proto__
的属性用于访问 [Prototype]
,这为用户理解原型链提供了极大的帮助。
因为使用 __proto__
属性可以访问到 Array 原型对象中定义的内部属性 [Prototype] 的值,所以,就存在如下的全等式:
Array.prototype.__proto__ === Object.prototype;
也即 Array 的原型对象中的内部属性 [Prototype] 的值就是 Object 的原型对象。
下面的等式也是成立的:
new Array().__proto__.__proto__ === Object.prototype;
当对原型对象应用 __proto__
属性时,返回对当前类的父类的原型对象的引用;当对实例应用 __proto__
属性时,返回对当前实例所表示类的原型对象的引用。也即下面的全等式是成立的:
new Array().__proto__ === Array.prototype;
Object 的原型对象的内部属性 [Prototype] 的值是 null ,因此,下面的代码返回 null :
alert(Object.prototype.__proto__); // null
来看自定义类,例如,如果存在下面的自定义类:
function Person() {}
那么, Person 类的父类的原型对象也就是 Object 的原型对象,因此就存在下面的全等式:
Person.prototype.__proto__ === Object.prototype;
同样,如果存在自定义类 Person 及其子类 Child :
function Person() {}
function Child() {} // 一个新类
Child.prototype = new Person(); // 作为 Person 类的子类
Child 类的父类的原型对象也就是 Person 的原型对象,那么就存在下面的全等式:
Child.prototype.__proto__ === Person.prototype;
因为它们都继承 Object 类,那么就应该同时存在下面的两个全等式:
Person.prototype.__proto__ === Object.prototype;
Child.prototype.__proto__ === Object.prototype;
但是,第 2 个全等式是不正确的,该全等式在类 Child 不继承类 Person 时成立,但是类 Child 继承了类 Person ,那么原型链就会下移一步。
因此,下面的全等式才是正确的(即 Child 的父类的父类的原型对象也就是 Object 的原型对象):
Child.prototype.__proto__.__proto__ === Object.prototype;
注意,加粗部分实际等于 Person.prototype ,也就是下面的全等式:
Person.prototype.__proto__ === Object.prototype;
照 ECMA-262 标准的规定:
每一个类都有一个 prototype 属性标识类的原型对象,该属性是静态属性,它有两个作用:实现继承和分享属性。
实现继承和分享属性都是原型链的组成。
使用 prototype 静态属性实现继承是通过改变内部属性 [Prototype] 的值来实现的,例如,存在下面的两个类的定义:
function Person() {}
function Person() {}
它们分别存在下面的内部属性定义:
{
"Prototype": `Object 的原型对象`,
"Class": "Person"
}
{
"Prototype": `Object 的原型对象`,
"Class": `Child`
}
假如类 Person 和类 Child 存在继承关系,那么可以使用下面的代码实现:
Child.prototype = new Person();
因为 Person 实例也隐式包含有 Person 类的内部属性 [Prototype] ,因此当执行这个赋值操作时, JavaScript 解释引擎可以获取 Person 实例的 [Prototype] 属性值,然后更改 Child 的内部属性 [Prototype] ,现在存在下面的内部属性定义:
{
Prototype:Person 的原型对象 ,
Class:"Child";
}
Child.prototype.__proto__ === Object.prototype
因为现在内部属性 [Prototype] 的值已经更改, JavaScript 解释引擎找到该属性的值,看到是 Person 就会去查找 Person 的定义而不是 Object 。
ECMA-262 标准有一个明确的规定:
在创建类的实例时,该实例隐式地包含有对自身原型对象的引用。因此,实例中也包含有内部属性 [Prototype] 的定义。
一个类的所有属性定义都在原型对象上,当类的实例访问一个属性时, JavaScript 解释引擎就会去原型对象上查找属性的定义,然后再执行。
由于原型对象中也包含有内部属性 [Prototype] 定义,因此,这一规定使得我们更容易理解如何分享属性,例如,假如存在如下的继承关系:
function GrandFather() {
this.g1 = '';
this.g2 = '';
}
function Father() {
this.p3 = '';
this.p4 = '';
}
function Child() {
this.c5 = '';
}
Father.prototype = new GrandFather();
Child.prototype = new Father();
那么在实例化 GrandFather 时,这个实例隐式地包含有自身内部属性 [Prototype] 的值,该值包含有 GrandFather 类属性的描述。同样,在实例化 Father 时,这个实例隐式地包含有自身内部属性 [Prototype] 的值,该值包含有 Father 类属性的描述;在实例化 Child 时,这个实例隐式地包含有自身内部属性 [Prototype] 的值,该值包含有 Child 类属性的描述。
如果 Child 的实例调用一个属性,那么它就会按照下面的步骤执行:
因为方法就是类型为 Function 的属性,因此这一过程同样适用于方法。
__proto__
和 prototype 属性的区别prototype 属性是一个静态属性, __proto__
属性则是一个实例属性。 prototype 属性表示类的原型对象, __proto__
属性表示原型对象中定义的内部属性 [Prototype] 的值。
IE 不支持使用 __proto__
属性,其它几个主流浏览器都支持该属性。
类的每个新实例都有一个 __proto__
属性,用于引用创建它的构造方法的 prototype 属性,也就是该类的原型对象。也即存在如下的全等式:
new Array("abc")).__proto__===Array.prototype
__proto__
属性同时还有同名的静态 __proto__
属性,也是返回原型对象,但浏览器并没有实现如下的等式:
Array.__proto__ === Array.prototype;
也就是说,静态 __proto__
属性并不等于静态 prototype 属性。一般不要使用静态 __proto__
属性。
使用 ECMAScript5 新增的 Object.getPrototypeOf() 方法可以得到指定对象的 prototype 属性,即获取一个实例的原型对象。
在 ECMAScript3 中,没有规定可以获取实例的原型对象,虽然一些浏览器可以使用 __proto__
属性,但这是非标准的方式,现在,使用 Object.getPrototypeOf() 方法可以完成相同的功能,并且
IE 浏览器也支持。
该方法的语法格式如下:
Object.getPrototypeOf(obj);
该方法是一个静态方法,参数 obj 指定一个对象,该方法要获取这个对象的原型。
例如下面的全等式是成立的:
Object.getPrototypeOf(new Array('abc')) === Array.prototype;
用户可以使用下面的代码实现浏览器兼容:
if (typeof Object.getPrototypeOf !== 'function') {
if (typeof 'test'.__proto__ === 'object') {
Object.getPrototypeOf = function (object) {
return object.__proto__;
};
} else {
Object.getPrototypeOf = function (object) {
return object.constructor.prototype;
};
}
}
查看原型链的定义,获取原型链的必要信息有多种方法。
使用 isPrototypeOf() 方法可以查看一个对象是否在指定的对象原型链中。如果该对象位于原型链中,则返回 true ;如果原型链中缺少目标对象或 object 参数不为实例,则返回 false 。
该方法的语法格式如下:
Class.prototype.isPrototypeOf(object);
参数 object 是一个类的实例。
例如使用下面的代码检测原型链:
function GrandFather() {}
function Father() {}
Father.prototype = new GrandFather();
function Child() {}
Child.prototype = new Father();
// 创建实例
var oGrandFather = new GrandFather();
var oFather = new Father();
var oChild = new Child();
// 所有对象都可以从 Object 原型链中找到
console.log(Object.prototype.isPrototypeOf(oGrandFather));
console.log(Object.prototype.isPrototypeOf(oFather));
console.log(Object.prototype.isPrototypeOf(oChild));
// 所有子类实例都可以从基类原型链中找到
console.log(GrandFather.prototype.isPrototypeOf(oFather));
console.log(GrandFather.prototype.isPrototypeOf(oChild));
console.log(Father.prototype.isPrototypeOf(oChild));
查找过程很容易理解,举下面的一个操作为例:
GrandFather.prototype.isPrototypeOf(oChild);
当执行该方法时,它会首先查看 oChild 的内部属性 [Prototype] 的值,该值表示父类的原型对象,使用该值与 GrandFather.prototype 匹配,如果全等就结束操作,如果不全等,查找内部属性 [Prototype] 的值所标示的父类的原型定义,再查找父类的内部属性 [Prototype] 的值,然后与 GrandFather.prototype 匹配,如果全等就结束操作,如果不全等,就继续上溯。最后直到与 Object.prototype 匹配。但是,这同时意味着没有与 GrandFather.prototype 匹配,所以操作会返回 false 。
使用 Object.hasOwnProperty() 方法可以查看指定对象是否定义了特定属性,该方法的语法格式如下:
oObject.hasOwnProperty(propName);
参数 propName 是一个字符串值,用于指定要查询的属性名。
例如下面的代码检测 Object 是否定义了属性 prop :
var o = new Object();
o.prop = 'exists';
console.log(o.hasOwnProperty('prop') + ''); // true
delete o.prop; // 删除 prop 属性
console.log(o.hasOwnProperty('prop') + ''); // false;
很多内建类都从 Object 继承该方法,例如下面的代码检测 Array 是否定义了属性 length :
var a = new Array();
alert(a.hasOwnProperty('length')); // true
对于成员方法,可以使用原型来检测,因为所有的对象都共享同一个方法成员。例如下面的代码,所有的 Array 对象共享一个公用 join 方法,因此可以这样检测:
var a = new Array();
alert(a.hasOwnProperty('join')); // false
alert(Array.prototype.hasOwnProperty('join')); // true
该方法无法检查该对象的原型链中是否具有特定属性,属性必须是对象本身定义的一个成员。
例如,如果类 A 从类 B 继承了属性 onePro ,属性 onePro 在类 B 定义,那么,使用下面的语法检测类 A 的实例时返回 false ,检测类 B 的实例时返回 true :
function A() {}
function B() {
this.onePro = '';
}
A.prototype = new B();
var a = new A();
var b = new B();
alert(a.hasOwnProperty('onePro')); // false
alert(b.hasOwnProperty('onePro')); // true
alert(A.prototype.hasOwnProperty('onePro')); // true
alert(a.__proto__.hasOwnProperty('onePro')); // tru
使用 Object.hasOwnProperty() 方法可以过滤那些从原型链继承的方法和属性,这一点对于循环语句非常重要,例如下面的代码:
function A() {}
function B() {
this.onePro = '';
}
A.prototype = new B();
var a = new A();
var b = new B();
for (var i in b) {
if (b.hasOwnProperty(i)) {
alert(i);
}
}
使用 Object.propertyIsEnumerable() 方法可以查看指定的属性是否存在及是否可枚举。如果返回值为 true ,则该属性存在并且可以在 for … in 循环中枚举。
该方法的语法格式如下:
Object.propertyIsEnumerable(propName);
参数 propName 是一个字符串值,用于指定属性名。
自定义的属性是可枚举的,但是内置属性通常是不可枚举的。
要检查的属性必须直接定义于目标对象上,而不是从其基类继承,该方法不检查目标对象的原型链。例如下面的代码检测 Object 是否定义了属性 prop :
var o = new Object();
o.prop = ' 属性值 ';
console.log(o.propertyIsEnumerable('prop') + ''); //true
很多内建类都从 Object 继承该方法,例如下面的代码检测 Array 是否定义了属性 length ,但是该属性不可以枚举,所以返回 false :
var a = new Array();
alert(a.propertyIsEnumerable('length')); // false