JavaScript 原型链
大部分面向对象的编程语言,都是以“类”(class
)作为对象体系的语法基础。JavaScript
语言中是没有class
的概念的(ES6之前,ES6中虽然提供了class
的写法,但实现原理并不是传统的“类”class
概念,仅仅是一种写法), 但是它依旧可以实现面向对象的编程,这就是通过JavaScript
中的“原型对象”(prototype
)来实现的。
#prototype 属性
请看这样一个例子:
function Person(name, gender) {
this.name = name;
this.gender = gender;
this.sayHello = function() {
console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
}
这样定义了一个构造函数,我们创建对象就可以使用这个构造函数作为模板来生成。不过以面向对象的思想来看,不难发现其中的一点问题:name
和gender
属性是每个实例都各不相同,作为一个自身的属性没有问题,而sayHello
方法,每个实例对象应该都有,而且都一样,给每个实例对象一个全新的、完全不同(虽然代码内容一样,但JavaScript
中每个sayHello
的值都在内存中单独存在)的sayHello
方法是没有必要的。
var zs = new Person('zhang san', 'male'),
xh = new Person('xiao hong', 'female');
zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female
zs.sayHello === xh.sayHello; // false
上面代码中展示了zs.sayHell
和xh.sayHello
这两个作用相同,而且看起来代码内容也是完全一样的对象,实际是两个独立的,互不相关的对象。
面向对象思想中,是将公共的、抽象的属性和方法提取出来,作为一个基类,子类继承这个基类,从而继承到这些属性和方法。而JavaScript
中则可以通过prototype
属性来实现类似的作用。以下是上面代码的改进示例:
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
Person.prototype.sayHello = function() {
console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
var zs = new Person('zhang san', 'male'),
xh = new Person('xiao hong', 'female');
zs.sayHello(); // Hello,I am zhang san . I'm a male
xh.sayHello(); // Hello,I am xiao hong . I'm a female
zs.sayHello === xh.sayHello; // true
这时将sayHello
方法定义到Person
对象上的prototype
属性上,取代了在构造函数中给每个实例对象添加sayHello
方法。可以看到,其还能实现和之前相同的作用,而且zs.sayHell
和xh.sayHello
是相同的内容,这样就很贴近面向对象的思想了。那么zs
和xh
这两个对象,是怎么访问到这个sayHello
方法的呢?
在浏览器控制台中打印出zs
,将其展开,可以看到下面的结果:
zs;
/**
*
Person
gender: "male"
name: "zhang san"
__proto__: Object
constructor: function Person(name, gender)
arguments: null
caller: null
length: 2
name: "Person"
prototype: Object
sayHello:function()
arguments:null
caller:null
length:0
name:""
prototype:Object
*/
zs
这个对象只有两个自身的属性gender
和name
,这和其构造函数Person
的模板相同,并且可以在Person
对象的__proto__
属性下找到sayHello
方法。那么这个__proto__
是什么呢?它是浏览器环境下部署的一个对象,它指的是当前对象的原型对象,也就是构造函数的prototype
属性。
现在就可以明白了,我们给构造函数Person
对象的prototype
属性添加了sayHello
方法,zs
和xh
这两个通过Person
构造函数产生的对象,是可访问到Person
对象的prototype
属性的,所以我们定义在prototype
下的sayHello
方法,Person
的实例对象都可以访问到。
关于构造函数的new
命令原理是这样的:
- 创建一个空对象,作为将要返回的对象实例
- 将这个空对象的原型,指向构造函数的
prototype
属性- 将这个空对象赋值给函数内部的
this
关键字- 开始执行构造函数内部的代码
#constructor 属性
prototype
下有一个属性constructor
,默认指向此prototype
对象所在的构造函数。
如上例中的zs
下__proto__
的constructor
值为function Person(name, gender)
。
由于此属性定义在prototype
属性上,所以它可以在所有的实例对象中获取到。
zs.constructor;
// function Person(name, gender) {
// this.name = name;
// this.gender = gender;
// }
zs.hasOwnProperty('constructor'); // false
zs.constructor === Person; // true
zs.constructor === Function; // false
zs.constructor === Object; // false
将constructor
属性放在prototype
属性中的一个作用是,可以通过这个属性来判断这个对象是由哪个构造函数产生的,上面代码中,zs
是由Person
构造函数产生的,而不是Function
或者Object
构造函数产生。
constructor
属性的另一个作用就是:提供了一种继承的实现模式。
function Super() {
// ...
}
function Sub() {
Sub.superclass.constructor.call(this);
// ...
}
Sub.superclass = new Super();
上面代码中,Super
和Sub
都是构造函数,在Sub
内部的this
上调用Super
,就会形成Sub
继承Super
的效果,miniui中是这样实现继承的:
mini.Control = function(el) {
mini.Control.superclass.constructor.apply(this, arguments);
// ...
}
// 其中的superclass指代父类的prototype属性
我们自己写一个例子:
// 父类
function Animal(name) {
this.name = name;
this.introduce = function() {
console.log('Hello , My name is', this.name);
}
}
Animal.prototype.sayHello = function() {
console.log('Hello, I am:', this.name);
}
// 子类
function Person(name, gender) {
Person.superclass.constructor.apply(this, arguments);
this.gender = gender;
}
Person.superclass = new Animal();
// 子类
function Dog(name) {
Dog.superclass.constructor.apply(this, arguments);
}
Dog.superclass = new Animal();
基本原理就是在子类中使用父类的构造函数。在Person
和Dog
中均没有对name
属性和introduce
方法进行操作,只是使用了父类Animal
的构造函数,就可以将name
属性和introduce
方法继承来,请看下面例子:
var zs = new Person('zhang san', 'male');
zs; // Person {name: "zhang san", gender: "male"}
zs.sayHello(); // Uncaught TypeError: zs.sayHello is not a function(…)
zs.introduce(); // Hello , My name is zhang san
var wangCai = new Dog("旺财");
wangCai; // Dog {name: "旺财"}
wangCai.introduce(); // Hello , My name is 旺财
确实实现了我们需要的效果。可是我们发现在调用zs.sayHello()
时报错了。为什么呢?
其实不难发现问题,我们的Person.superclass
是Animal
的一个实例,是有sayHello
方法的,但是我们在Perosn
构造函数的内部,只是使用了Person.superclass.constructor
。而Person.superclass.constructor
指的仅仅是Animal
构造函数本身,并没有包括Animal.prototype
,所以没有sayHello
方法。
一种改进方法是:将自定义的superclass
换为prototype
,即:
function Person(name, gender) {
Person.prototype.constructor.apply(this, arguments);
this.gender = gender;
}
Person.prototype = Animal.prototype;
var zs = new Person('zhang san', 'male');
zs.sayHello(); // Hello, I am: zhang san
zs.introduce() // Hello , My name is zhang san
这样就全部继承到了Animal.prototype
下的方法。
但是一般不要这样做,上面写法中
Person.prototype = Animal.prototype;
等号两端都是一个完整的对象,进行赋值时,Person.prototype
的原对象完全被Animal.prototype
替换,切断了和之前原型链的联系,而且此时Person.prototype
和Animal.prototype
是相同的引用,给Person.prototype
添加的属性方法也将添加到Animal.prototype
,反之亦然,这将引起逻辑混乱。
因此我们在原型上进行扩展是,通常是添加属性,而不是替换为一个新对象。
// 好的写法
Person.prototype.sayHello = function() {
console.log('Hello,I am', this.name, '. I\'m a', this.gender);
};
Person.prototype. // .. 其他属性
// 不好的写法
Person.prototype = {
sayHello:function(){
console.log('Hello,I am', this.name, '. I\'m a', this.gender);
},
// 其他属性方法 ...
}
#JavaScript 原型链
JavaScript
的所有对象都有构造函数,而所有构造函数都有prototype
属性(其实是所有函数都有prototype
属性),所以所有对象都有自己的原型对象。
对象的属性和方法,有可能是定义在自身,也有可能是定义在它的原型对象。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype chain)。
zs.sayHello(); // Hello,I am zhang san . I'm a male
zs.toString(); // "[object Object]"
例如上面的zs
对象,它的原型对象是Person
的prototype
属性,而Person
的prototype
本身也是一个对象,它的原型对象是Object.prototype
。
zs
本身没有sayHello
方法,JavaScript
通过原型链向上继续寻找,在Person.prototype
上找到了sayHello
方法。toString
方法在zs
对象本身上没有,Person.prototype
上也没有,因此继续沿原型链查找,最终可以在Object.prototype
上找到了toString
方法。
而Object.prototype
的原型指向null
,由于null
没有任何属性,因此原型链到Object.prototype
终止,所以Object.prototype
是原型链的最顶端。
“原型链”的作用是,读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype
还是找不到,则返回undefined
。
如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overiding
)。
JavaScript中通过原型链实现了类似面向对象编程语言中的继承,我们在复制一个对象时,只用复制其自身的属性即可,无需将整个原型链进行一次复制,Object.prototype
下的hasOwnProperty
方法可以判断一个属性是否是该对象自身的属性。
实例对象、构造函数、prototype
之间的关系可用下图表示:
#instranceof 运算符
instanceof
运算符返回一个布尔值,表示指定对象是否为某个构造函数的实例。由于原型链的关系,所谓的实例并不一定是某个构造函数的直接实例,更准确的描述,应该是:返回一个后者的原型对象是否在前者的原型链上
zs instanceof Person; // true
zs instanceof Object ;// true
var d = new Date();
d instanceof Date; // true
d instanceof Object; // true
#原型链相关属性和方法
#Object.prototype.hasOwnProperty()
hasOwnProperty()
方法用来判断某个对象是否含有指定的自身属性。这个方法可以用来检测一个对象是否含有特定的自身属性,和 in
运算符不同,该方法会忽略掉那些从原型链上继承到的属性。
zs.hasOwnProperty('name'); // true
zs.hasOwnProperty('gender'); // true
zs.hasOwnProperty('sayHello'); // fasle
Person.prototype.hasOwnProperty('sayHello'); // true
zs.hasOwnProperty('toString'); // fasle
Object.prototype.hasOwnProperty('toString'); // true
#Object.prototype.isPrototypeOf()
对象实例的isPrototypeOf
方法,用来判断一个对象是否是另一个对象的原型。
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
上面代码表明,只要某个对象处在原型链上,isProtypeOf
都返回true
。
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
看起来这个方法和instanceof
运算符作用类似,但实际使用是不一样的。
例如:
zs instanceof Person ; // true;
Person.isPrototypeOf(zs);// false
Person.prototype.isPrototypeOf(zs); // true
zs instanceof Person
可理解为判断Person.prototype
在不在zs
的原型链上。 而Person.isPrototypeOf(zs)
指的就是Person
本身在不在zs
的原型链上,所以返回false
,只有Person.prototype.isPrototypeOf(zs)
才为 true
。
#Object.getPrototypeOf()
ES5Object.getPrototypeOf
方法返回一个对象的原型。这是获取原型对象的标准方法。
// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true
// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true
// f 为 F 的实例对象,则 f 的原型是 F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true
Object.getPrototypeOf("foo");
// TypeError: "foo" is not an object (ES5 code)
Object.getPrototypeOf("foo");
// String.prototype (ES6 code)
此方法是ES5方法,需要IE9+。在ES5中,参数只能是对象,否则将抛出异常,而在ES6中,此方法可正确识别原始类型。
#Object.setPrototypeOf()
ES5Object.setPrototypeOf
方法可以为现有对象设置原型,返回一个新对象。接受两个参数,第一个是现有对象,第二个是原型对象。
var a = {x: 1};
var b = Object.setPrototypeOf({}, a);
// 等同于
// var b = {__proto__: a};
b.x // 1
上面代码中,b
对象是Object.setPrototypeOf
方法返回的一个新对象。该对象本身为空、原型为a
对象,所以b
对象可以拿到a
对象的所有属性和方法。b
对象本身并没有x
属性,但是JavaScript引擎找到它的原型对象a
,然后读取a
的x
属性。
new
命令通过构造函数新建实例对象,实质就是将实例对象的原型,指向构造函数的prototype
属性,然后在实例对象上执行构造函数。
var F = function () {
this.foo = 'bar';
};
// var f = new F();等同于下面代码
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
#Object.create()
ES5Object.create
方法用于从原型对象生成新的实例对象,它接收两个参数:第一个为一个对象,新生成的对象完全继承前者的属性(即新生成的对象的原型此对象);第二个参数为一个属性描述对象,此对象的属性将会被添加到新对象。(关于属性描述对象可参考:MDN - Object.defineProperty())
上面代码举例:
var zs = new Person('zhang san', 'male');
var zs_clone = Object.create(zs);
zs_clone; // {}
zs_clone.sayHello(); // Hello,I am zhang san . I'm a male
zs_clone.__proto__ === zs; // true
// Person
// __proto__: Person
// gender: "male"
// name: "zhang san"
// __proto__: Object
可以 看出 创建的新对象zs_clone
的原型为zs
,从而获得了zs
的全部属性和方法。但是其自身属性为空,若需要为新对象添加自身属性,则使用第二个参数即可。
var zs_clone = Object.create(zs, {
name: { value: 'zhangsan\'s clone' },
gender: { value: 'male' },
age: { value: '25' }
});
zs_clone; // Person {name: "zhangsan's clone", gender: "male", age: "25"}