前言
理解原型和原型链,有助于更好的理解JavaScript中的继承机制。
最近比较有空,所以想写一篇关于原型和原型链的文章,如写得不好请见谅。
原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
function Person(name) {
this.name = name
}
Person.prototype.age = 18
Person.prototype.sayName = function() {
alert(this.name)
}
var person = new Person('张三')
person.sayName() // 张三
上述我们创建了一个Person构造函数,并创建了一个name实例和原型对象的age属性和sayName方法,接下来new实例对象都拥有这些属性方法
Person构造函数的prototype属性指向原型对象,person实例的[[prototype]]指向原型对象(在chrome浏览器中是__proto__),我们可以通过isPrototypeOf()
方法来确认对象之间是否存在原型关系。ES5新增了一个新方法,Object.PrototypeOf()
,这个方法返回[[prototype]]的值
alert(Peron.prototype.isPrototypeOf(person)) // true
alert(Object.PrototypeOf(person) == Person.prototype) // true
alert(Object.PrototypeOf(peroson).name // '张三'
当我们访问一个实例对象的某个属性时,如果在实例中找到具有给定名字的属性,则返回属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到具有给定名字的属性,则返回该属性的值
看下面这段代码
function Person() {
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 18;
Person.prototype.sayName = function() { alert(this.name) };
var person1 = new Person();
var person2 = new Person();
person1.name = 'lisi';
console.log(person1.name); // 'lisi'——来自实例
console.log(person2.name); // 'zhangsan'——来自原型
console.log(person1.hasOwnProperty('name')) // true
delete person1.name; // 'zhangsan'——来自原型
console.log(person1.name);
console.log(person1.hasOwnProperty('name')) // false
console.log(person2.hasOwnProperty('name')) // false
在这个例子中,person1的name被一个新值给屏蔽了。当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过可以使用delete操作符删除实例属性,从而让我们重新访问原型中的属性。
使用hasOwnProperty()
方法,可以检测属性是否来自实例,只在给定属性存在于对象实例中时,才会返回true
我们还可以通过定义函数封装一个检测该属性到底存在于对象中,还是存在于原型中的方法
function hasProtoypeProperty(object, name) {
return !object.hasProperty(name) && (name in object)
}
function Person() {
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 18;
Person.prototype.sayName = function() { alert(this.name) };
var person = new Person();
console.log(hasProtoypeProperty(person, 'name')); // true
person.name = 'lisi'
console.log(hasProtoypeProperty(person, 'name')); // false
很多同学可能注意到前面例子每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,我们可以使用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person() {}
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
上述代码中有一个问题,Person.prototype中的constructor
属性不再指向Person,此时我们需要手动将constructor
属性指向Person
function Person() {}
Person.prototype = {
constructor: Person,
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
var person = new Person();
for(var val in person) {
console.log(val); // 'constructor','name','age','sayName'
}
以这种方式重设constructor
属性会导致它的[[Enumerable]]特性被设置为true,导致可以通过for-in遍历出来,最好的办法是通过ES5中的Object.defineProperty()
方法设置constructor
属性
function Person() {}
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function() {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
原型还有一个特点,是具有动态性,即我们对原型对象所做的修改都能够立即从实例上反映出来,即使是先创建实例后修改原型对象也是如此
function Person() { }
var person1 = new Person();
Person.prototype = {
name: 'zhangsan',
age: 19,
sayName: function () {
console.log(this.name)
}
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
person1.sayName(); // Uncaught TypeError: person.sayName is not a function
var person2 = new Person();
person2.sayName(); // zhangsan
前面提过,调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,上述代码中,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系。实例中的指针指向原型,而不指向构造函数。
原型链
在JavaScript中,原型链是实现继承的主要的方法,理解原型链有助于我们理解JavaScript中的继承机制。
原型链,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,我们都知道,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),实例对象都包含一个指向原型对象的内部指针(__proto__),让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,假如另一个原型又是另外一个类型的实例,如此层层递进,就构成了实例与原型的链条,简称原型链。
在JavaScript中,每个实例对象都有一个__proto__属性指向它的构造函数的原型对象prototype,原型对象也有一个自己的原型对象__proto__,层层向上直到一个对象的原型对象为null
,几乎所有JavaScript中的对象都是位于原型链顶端的Object
的实例。
function Father() {
this.name = 'zhangsan';
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Son() {
this.age = 19
}
Son.prototype = new Father();
var xiaoming = new Son();
xiaoming.sayName(); // 'zhangsan'
上述代码中,让Son构造函数的原型指向Father构造函数的实例,此时就实现了JavaScript中最简单的继承原理。让我们通过下图来更好的理解原型链。
上图很清晰的描绘了原型链中的继承机制,对象最顶层Object的原型的原型是指向一个null
。注意,在为原型添加方法的代码一定要放在替换原型的语句之后,也不能通过对象字面量创建原型方法,因为这样做就会重写原型链。
function Father() {
this.name = 'zhangsan';
}
Father.prototype.sayName = function() {
console.log(this.name);
}
function Son() {
this.age = 19
}
Son.prototype = new Father();
Son.prototype = {
sayAge: function() {
console.log(this.age)
}
}
var xiaoming = new Son();
xiaoming.sayName(); // Uncaught TypeError: xiaoming.sayName is not a function
使用对象字面量的方式到导致Father和Son之间的关系被切断,因此最后调用sayName()
方法就会报错。
总结
以上就是我对原型和原型链的理解,原型链虽然强大,但它也存在一些问题,最主要的问题就是包含引用类型值的原型属性会被所有实例共享,本文只是对原型和原型链进行分析,关于继承问题不在本文的讨论范围之内,这是cc的第一篇文章,也算是小白文章,如写得不好请指出,我会及时更正,谢谢大家~