了解原型
我们创建的每个函数都有一个原型属性,它是一个指向对象的指针,其目的是包含可由特定类型的所有实例共享的属性和技术。 请参见以下示例:
function Person(){ } Person.prototype.name = 'ccc' Person.prototype.age = 18 Person.prototype.sayName = function (){ console.log(this.name); } var person1 = new Person() person1.sayName() // --> ccc var person2 = new Person() person2.sayName() // --> ccc console.log(person1.sayName === person2.sayName) // --> true
理解原型对象
根据前面的代码,见右图:
需要理解三件事:
只要我们创建一个新的函数,就会按照一组特定的规则为该函数创建一个prototype属性,指向该函数的原型对象。 也就是说,Person(构造函数)有一个原型指针,指向Person.prototype。 默认情况下,每个prototype对象也会创建一个constructor(构造函数)属性,它是一个指向prototype属性所在函数的指针。 里面有一个指针(内部属性),指向构造函数的原型对象。 即person1和person2都有一个内部属性__proto__(在ECMAscript中,这个指针称为[[prototype]],虽然脚本中没有访问[[prototype]]的标准形式,但是firefox,即chrome都支持一个名为 __proto__) 的属性指向 Person.prototype
注意:person1 和 person2 实例与构造函数之间没有直接关系。
正如我们之前提到的,在所有实现中都很难访问[[prototype]],那么我们如何知道实例和原型对象之间是否存在关系呢? 这可以通过两种方式确定:
实例属性和原型属性之间的关系
我们之前提到,原型最初只包含构造函数属性,该属性也是共享的,因此可以通过对象实例访问。 虽然可以通过对象实例访问原型中存储的值,但是原型中的值无法通过对象实例重绘。 如果我们向实例添加一个属性,并且该属性与实例原型中的属性同名javascript对象原型,则该属性将在实例上创建,而原型中的属性将被阻止。 如下:
function Person() {} Person.prototype.name = "ccc"; Person.prototype.age = 18; Person.prototype.sayName = function() { console.log(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.name = 'www' // 在person1中添加一个name属性 person1.sayName() // --> 'www'————'来自实例' person2.sayName() // --> 'ccc'————'来自原型' console.log(person1.hasOwnProperty('name')) // --> true console.log(person2.hasOwnProperty('name')) // --> false delete person1.name // --> 删除person1中新添加的name属性 person1.sayName() // -->'ccc'————'来自原型'
我们如何区分属性是实例上的属性还是原型上的属性? 这里你可以通过 hasOwnProperty() 方法来测量某个属性是否存在于实例或原型中。 (该方法继承自Object)
下图详细分析了前面反例在不同情况下的实现与原型的关系:(省略了Person构造函数的关系)
更简单的原型句子模式
我们不可能像前面的反例一样,一直输入Person.prototype而不添加属性和技能。 为了减少不必要的打字,更常见的做法如下:
function Person(){} Person.prototype ={ name: 'ccc', age: 18, sayName: function () { console.log(this.name) } }
在代码中,我们将 Person.prototype 设置为等于作为对象文字创建的新对象。 最终结果是相同的,除了一个例外,构造函数属性不再指向 Person。 前面我们提到,每次创建函数时,都会同时创建它的原型对象,并且这个对象也会手动获取构造函数属性。 但我们使用的new句型中javascript对象原型,默认的prototype对象本质上是完全重绘的,所以constructor属性就变成了new对象的constructor属性(指向Object的构造函数),而不再指向Person函数。 此时,虽然instanceof操作符能够返回正确的结果,但是通过构造函数来判断对象的类型已经很难了。 如下:
var person1 = new Person() console.log(person1 instanceof Object) // --> true console.log(person1 instanceof Person) // --> true console.log(person1.constructor === Person) // --> false console.log(person1.constructor === Object) // --> true
这里使用instanceof操作符测试Object和Person总是返回true,并且构造函数属性等于Object,不等于Person。 如果构造函数确实很重要,可以写成如下:
function Person(){} Person.prototype ={ constructor: Person, // --> 重设 name: 'ccc', age: 18, sayName: function () { console.log(this.name) } }
但这会带来一个新的问题,用上面的方法重置构造函数属性会导致其[[Enumerable]]属性被设置为true。 默认情况下,本机构造函数属性不可枚举。 所以如果你想使用兼容ECMAscript5的JavaScript引擎,你可以尝试Object.defineProperty()。
function Person(){} Person.constructor = { name: 'ccc', age: 18, sayName: function(){ console.log(this.name) } } // 重设构造函数,只适用于ECMAscript5兼容的浏览器 Object.defineProperty(Person.constructor, "constructor", { enumerable: false, value: Person })
原型动力学
由于在原型中查找值的过程是一个搜索,因此我们对原型对象所做的任何更改都会立即反映在实例上。 例如:
function Person(){} var person1 = new Person() Person.prototype.sayHi= function(){ console.log('hi') } person1.sayHi()
上面的代码中,我们首先创建了一个Person实例并保存在person1中,然后在Person.prototype中添加了sayHi()方法。 尽管 person1 是在添加新方法之前创建的,但它仍然可以使用此技巧。 原因是实例和原型之间的松耦合关系。
虽然你可以随时给原型添加属性和技能,并且会立即体现在实例中。 但如果重绘整个原型对象,情况就不同了。 看下面的代码:
function Person(){} var person1 = new Person() Person.prototype = { name: 'ccc', age: 18, sayName: function(){ console.log(this.name) } } person1.sayName() // --> error
看右边的分析:
当调用构造函数时,会在实例中添加一个指向原始原型的[[prototype]]指针,将原型更改为另一个原型相当于切断了构造函数与原始原型之间的联系。 请记住:实例中的指针仅指向原型,而不指向构造函数。
原型链
简单回顾一下构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,实例包含一个指向原型对象的内部指针。 那么如果我们让原型对象等于另一种类型的实例会发生什么呢? 显然,此时的原型对象会包含一个指向另一个原型的指针,相应的,另一个原型也包含一个指向另一个构造函数的指针。 如果另一个原型是另一个类型的实例,则上述关系仍然成立。 这样的渐进层形成了实例和原型链。 这就是所谓原型链的基本概念。
图中相互关联的原型组成的支链结构就是原型链,也就是红线。