js对象和对象继承
对象
ECMA-262把对象定义为“无序属性的集合,其属性可以包含基本值、对象或者函数。” 我们可以吧ECMAScript的对象想象成散列表:无非是一组名值对,其中值可以是数据或者函数。
1 | var person = new Object() |
等价于:
1 | var person = { |
属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
数据
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。上面例子定义的属性,他们的这个特性默认值都为true
。[[Enumerable]]
:表示能否通过for-in
循环返回属性。上面例子定义的属性,他们的这个特性默认值都为true
。[[Writable]]
:表示能否修改属性的值。上面例子定义的属性,他们的这个特性默认值都为true
。[[Value]]
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined
。
要修改默认的特性,必须使用ECMAScript5的Object.defineProperty()
方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符(descriptor)对象。其中描述符对象的属性必须是:configurable、enumerable、writable和value。
1 | var person = {} |
如果把属性定义为不可配置的,就再也不能把它变成可配置的了。此时再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误:
1 | var person = {} |
也就是说,可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable
特性设置为false
之后就会有限制了。
访问器属性
访问器属性不包含数据值;它们包含一对getter
和setter
函数:在读取访问器属性时,会调用getter
函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter
函数,这个函数决定如何处理数据。
访问器属性有4个特性:
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对于直接在对象上定义的属性,他们的这个特性默认值都为true
。
[[Enumerable]]
:表示能否通过for-in
循环返回属性。对于直接在对象上定义的属性,他们的这个特性默认值都为true
。[[Get]]
:在读取属性时调用的函数。默认为undefined
。[[Set]]
:在写入属性时调用的函数。默认为undefined
。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。
1 | var book = { |
_year
前面的下划线是一种常用的符号,用来表示只能通过对象方法访问的属性。
创建对象
工厂模式
工厂模式抽象了创建具体对象的过程。
1 | function createPerson(name, age ,job){ |
工厂模式虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数模式
ECMAScript中的构造函数可以用来创建特定类型对象。
1 | function Person(name, age, job){ |
在这个例子中,Person()函数替代了createPerson()函数。Person()函数存在以下不同:
- 没有显性地创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句
要创建一个Person
新实例,必须使用new
操作符。
new
操作符实现原理:见另一篇文章。
将构造函数当作函数
构造函数和其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过new
操作符来调用,那它就可以当作构造函数;对任何函数,如果不通过new
操作符来调用,就跟普通函数没什么两样。
1 | // 当作构造函数使用 |
构造函数的问题
构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
在上面的例子中,person1和person2都有一个名为sayName()的方法,但这两个方法并不是同一个实例。创建两个完成同样任务的Function实例的确没有必要。因此可以使用原型模式来解决。
原型模式
我们创建的每一个函数都会有一个prototype(原型)属性
,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型对象的好处是可以让所有对象实例共享它包含的属性和方法,即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
1 | function Person(){ |
理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype
属性,这个属性指向函数的原型对象。默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性
,这个属性是一个指向prototype
属性所在函数的指针。
拿前面的例子来说: Person.prototype.construtor
指向 Person
当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性[[prototype]]
——在Firefox、safari、和Chrome中为__proto__
,指向构造函数的原型对象。
如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
1 | function Person(){ |
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。即这个属性只会阻止我们访问原型中的那个属性,但不会修改哪个属性。
使用hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。只有给定属性存在于对象实例中,才会返回true
。
1 | function Person(){ |
原型与in
操作符
有两种方式使用in
操作符:单独使用和在for-in
循环中使用。在单独使用中,in
操作符会在通过对象能够访问给定属性时返回true
,无论该属性存在于实例还是在原型中。
1 | function Person(){ |
同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。可以定义一个判断该属性是否只存在原型对象中:
1 | function hasPrototypeProperty(object, name){ |
要取得对象上所有可枚举的实例属性,可以使用Object.key()方法。该方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
1 | function Person(){ |
如果想获得所有实例属性,无论它是否可以枚举,都可以使用Object.getOwnPropertyNames()
方法
1 | var keys = Object.getOwnPropertyNames(Person.prototype) |
原型的动态性
在原型中查找值的过程是一次搜索。
1 | var friend = new Person() |
即使friend实例是在添加新方法之前创建的,但它们仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。
而当重写整个原型对象时,就切断了构造函数与最初原型之间的联系。
实例中的指针仅指向原型,而不指向构造函数。
1 | var friend = new Person() |
原生对象的原型
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
1 | console.log(Array.prototype.sort) // "function" |
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法。
1 | String.prototype.startsWith = function(text){ |
原型对象的问题
原型中所有的属性是被很多实例共享的,这种共享对于函数来说非常合适,但对于包含引用类型的属性来说,问题就比较突出。
1 | var friend = new Person() |
由于friend
数组存在于Person.prototype
而非 person1
中,所以对于person1.friend
的修改也会通过person2.friend
反映出来。
组合使用构造函数模式和原型模式
创建自定义类型最常见的方式,就是组合使用构造函数于原型模式。构造函数用户定义实例属性,而原型模式用于定义方法和共享的属性。
1 | function Person(name, age, job){ |
动态原型模式
1 | function Person(name, age, job){ |
这段代码只会在初次调用构造函数的时候才会执行,此后,原型已经完成初始化,不需要再做什么修改了。 这里对原型所做的修改,能够立即在所有实例中得到反映。
使用动态原型模式时,不能使用对象字面量重写原型。
寄生(parasitic)构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像典型的构造函数。
1 | function Person(name, age, job){ |
构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return`
语句,可以重写调用构造函数返回的值。
这种模式可以在特殊情况下用来为对象创建构造函数。假设我们像创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此我们使用这个模式
1 | function SpecialArray(){ |
有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部创建的对抗没有什么不同。因此不能依赖
instanceof
来确定对象类型。
稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this
的对象。 适合在一些安全的环境中(禁止使用this和new)
1 |
|
这样变量friend
保存的是一个稳妥对象,而除了调用sayName()
方法外,没有别的方式可以访问其数据成员。
继承
实现继承主要是依靠原型链来实现的。
原型链
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针。
假设让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型对象的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概论。
代码大致如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function SuperType(){
this.property = true
}
SuperType.prototype.getSuperValue = function(){
return this.property
}
function SubType(){
this.subProperty = false
}
// 继承了SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function(){
return this.subProperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true
实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在与SuperType
的实例的所有属性和方法,现在也存在于SubType.prototype
中了。
要注意
instance.constructor
现在指向的是SuperType
,这是因为SubType.prototype
中的constructor
被重写的缘故。
通过实现原型链,本质上扩展了原型搜索机制。
别忘记默认的原型
所有引用类型默认都继承了Object
,而这个继承也是通过原型链实现的。
所有函数的默认原型都是Object
的实例,因此默认原型都会包含一个内部指针,指向Objcet.prototype
。所以所有自定义类型都会继承toString()
、valueOf()
等默认方法的根本原因。
确定原型和实例的关系
使用instanceof
操作符,只用用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true
。
- instanceof 底层如何工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function instanceof(L,R){
var O = R.prototype //获取构造函数的显性原型对象
var L = L.__proto__ // 获取实例的隐形原型对象
while(true){
if(L === null){ // 遍历完原型链仍未匹配
return false
}
if(L === O){ // 当显性原型严格等于隐形原型时,返回true
return true
}
L = L.__proto__ // 向上遍历原型链
}
}
谨慎定义方法
给原型添加的代码一定要放在替换原型的语句后。
1 | function SuperType(){ |
在通过原型链实现继承时,不能使用对象字面量创建原型对象。因为这样就会重写原型链。
原型的问题
最主要的问题时来自包含引用类型值的原型。
1 | function SuperType(){ |
当SubType
通过原型链继承了SuperType 之后,SubType.prototype就变成了SuperType的一个实例,因此它也用了一个它自己的colors属性——就跟专门创建了一个SubType.prototype.colors属性一样。所以SubType的所有实例都会共享这一个colors属性。
原型链的第二个问题:在创建子类型实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
因为
new
出来的实例是赋值给SubType.prototype
,若该实例的属性值因为参数的不同发生变化,那么会导致SubType.prototype
中的属性值也发生变化,会影响到SubType
的其他对象实例。
因此在实践中很少会单独使用原型链。
借用构造函数
通过使用apply()
或call()
方法在新创建的对象上执行构造函数。
1 | function SuperType(){ |
等价于:
1 | function SuperType(){ |
传递参数
借用构造函数可以在子类型构造函数中向超类型构造函数传递参数。
1 | function SuperType(name){ |
借用构造函数的问题
方法都在构造函数中定义,因此函数复用也就无从谈起。而且在超类型原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。所以实际中也很少单独使用。
组合继承
组合继承将原型链和借用构造函数的技术组合到一块。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型链上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
1 | function SuperType(name){ |
组合继承上JS中最常用的继承模式,而且instanceof()
和isPrototypeof()
也能识别基于组合继承创建的对象。
原型式继承
基于已有的对象创建新对象,同时还不必因此创建自定义类型。
1 | function object(o){ |
object()
对传入其中的对象进行了一次浅复制。
ES5通过新增的Object.create()
方法规范了原型式继承。该方法接收两个参数:一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象。
1 | var person = { |
在没有必要兴师动众地创建构造函数,而只是想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任。但是包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
寄生式继承
寄生式继承和原型式继承紧密相关。
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用来封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
1 | function creatAnother(original){ |
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
寄生组合式继承
组合继承有一点不足:组合继承最大的问题就是无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候(new SuperType()
),一次是在子类型构造函数内部(SuperType.call( this )
)
调用两次SuperType
构造函数会让SubType
有两组name
和colors
属性,一组在实例上,一组在SubType.prototype
中。
通过寄生组合式继承可以解决这个问题:
我们无需为了指定子类型的原型而调用超类型的构造函数,我们所需的只是超类型的原型的一个副本,因此我们可以用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。1
2
3
4
5function inheritPrototype(SubType, SuperType){
var prototype = object(SuperType.prototype) //创建对象
prototype.constructor = SubType //增强对象
SubType.prototype = prototype // 指定对象
}