面向对象(二)
内容承接 面向对象(一)
原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的指针。就拿前面的例子来说,Person.prototype. constructor
指向 Person
。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
可以通过 isPrototypeOf()
方法来确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1)) // true
ECMAScript 5
增加了一个新方法,叫Object.getPrototypeOf()
,在所支持的实现中,这个方法返回[[Prototype]]
的值。例如:
alert(Object.getPrototypeOf(person1) === Person.prototype) // true
1)原型与 in 操作符
有两种方式使用in
操作符:单独使用和在for-in
循环中使用。在单独使用时,in
操作符会在通过对象能够访问给定属性时返回 true
,无论该属性存在于实例中还是原型中
。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
}
Person.prototype.sayName = function(){
alert(this.name)
}
Person.prototype.name = 'superYY'
const yy = new Person('yy', '22', 'web')
console.log('name' in yy) // 来自实例
function Person(age, job) {
this.age = age
this.job = job
}
Person.prototype.sayName = function(){
alert(this.name)
}
Person.prototype.name = 'superYY'
const yy = new Person('yy', '22', 'web')
console.log('name' in yy) // 来自原型
name
属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用 'name' in yy
始终都返回 true
,无论该属性存在于实例中还是存在于原型中。
同时使用
hasOwnProperty()
方法和in
操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object)
}
由于in
操作符只要通过对象能够访问到属性就返回true
, hasOwnProperty()
只在属性存在于实例中时才返回 true
,因此只要in
操作符返回true
而hasOwnProperty()
返回false
,就可以确定属性是原型中的属性。
在使用for-in
循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]] 标记为 false 的属性)的实例属性也会在for-in
循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在IE8
及更早版本中例外。详见《javavscript高级程序设计3》P154
扩展一:Object.keys()
要取得对象上所有可枚举的实例属性
,可以使用 ECMAScript 5
的 Object.keys()
方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person(age, job) {
this.age = age
this.job = job
}
Person.prototype.sayName = function(){
alert(this.name)
}
Person.prototype.name = 'superYY'
const yy = new Person('yy', '22', 'web')
console.log(Object.keys(yy)) // Array(2) 0: "age" 1: "job" length: 2
扩展二:Object.getOwnPropertyNames()
如果你想要得到所有实例属性,无论它是否可枚举
,都可以使用Object.getOwnPropertyNames()
方法。
function Person(age, job) {
this.age = age
this.job = job
}
Person.prototype.sayName = function(){
alert(this.name)
}
Person.prototype.name = 'superYY'
const yy = new Person('yy', '22', 'web')
console.log(Object.getOwnPropertyNames(Person.prototype)) // Array(3) 0: "constructor" 1: "sayName" 2: "name" length: 3
2)更简单的原型语法
读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,
function Person() {
}
Person.prototype = {
name: 'yy',
age: 22,
job: 'Software Engineer',
sayName: function () {
alert(this.name)
}
}
constructor
属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype
对象,这个对象也会自动获得constructor
属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype
对象,因此constructor
属性也就变成了新对象的constructor
属性(指向Object
构造函数),不再指向 Person
函数。
此时,尽管
instanceof
操作符还能返回正确的结果,但通过constructor
已经无法确定对象的类型了,如下所示。
const friend = new Person() // 与 Person.prototype 绑定
alert(friend instanceof Object) // true
alert(friend instanceof Person) // true
alert(friend.constructor === Person) // false
alert(friend.constructor === Object) // true
在此,用 instanceof
操作符测试Object
和Person
仍然返回true
,但constructor
属性则等于Object
而不等于Person
了。如果 constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值。
function Person() {
}
Person.prototype = {
constructor : Person,
name: 'yy',
age: 22,
job: 'Software Engineer',
sayName: function () {
alert(this.name)
}
}
const friend = new Person() // 与 Person.prototype 绑定
alert(friend instanceof Object) // true
alert(friend instanceof Person) // true
alert(friend.constructor === Person) // true
alert(friend.constructor === Object) // true
以上代码特意包含了一个 constructor
属性,并将它的值设置为 Person
,从而确保了通过该属性能够访问到适当的值。
以这种方式重设
constructor
属性会导致它的[[Enumerable]]
特性被设置为true
。默认情况下,原生的constructor
属性是不可枚举的,因此如果你使用兼容ECMAScript 5
的JavaScript
引擎,可以试一试Object.defineProperty()
。
// 重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
3)原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。
const friend = new Person()
Person.prototype.sayHi = function(){
alert("hi")
}
friend.sayHi() //"hi"(没有问题!)
即使person
实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用person.sayHi()
时,首先会在实例中搜索名为sayHi
的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi
属性并返回保存在那里的函数。
function Person(){
}
const yy = new Person()
Person.prototype = {
constructor: Person,
name : 'yy',
age : 18,
job : 'student',
sayName : function () {
alert(this.name)
}
}
yy.sayName() //error
重写后的原型对象
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
构造函数模式
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
function Person(name, age, job){
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby', 'Court']
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name)
}
}
const person1 = new Person('yy', 22, 'web')
const person2 = new Person('Greg', 27, 'Doctor')
person1.friends.push('Van')
alert(person1.friends) // "Shelby,Count,Van"
alert(person2.friends) // "Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor
和方法 sayName()
则是在原型中定义的。而修改了person1.friends
(向其中添加一个新字符串),并不会影响到person2.friends
,因为它们分别引用了不同的数组。
这种构造函数与原型混成的模式,是目前在
ECMAScript
中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。
动态原型模式
有其他OO
语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。
换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
if(typeof sayName !== 'function') { // 动态创建共享的函数 sayName
Person.prototype.sayName = function() {
console.log(this.name)
}
}
}
const person = new Person('yy', 22, 'web')
person.sayName() // yy
注意构造函数代码中加粗的部分。这里只在 sayName()
方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。
寄生构造函数模式
通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)
构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数(工厂模式)。下面是一个例子。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改
Array
构造函数,因此可以使用这个模式。
function SpecialArray() {
// 创建数组
let values = new Array()
//添加值
values.push.apply(values, arguments)
//添加方法
values.toPipedString = function(){
return this.join("|")
}
//返回数组
return values
}
const colors = new SpecialArray("red", "blue", "green")
alert(colors.toPipedString()) //"red|blue|green"
在这个例子中,我们创建了一个名叫SpecialArray
的构造函数。在这个函数内部,首先创建了一个数组,然后push()
方法(用构造函数接收到的所有参数)初始化了数组的值。
随后,又给数组实例添加了一个toPipedString()
方法,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。接着,我们调用了 SpecialArray
构造函数,向其中传入了用于初始化数组的值,此后又调用了 toPipedString()
方法。
关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。
console.log(colors)
也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof
操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
稳妥构造函数模式
道格拉斯·克罗克福德(Douglas Crockford
)发明了JavaScript
中的稳妥对象(durable objects
)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this
的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this
和 new
),或者在防止数据被其他应用程序(如Mashup
程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this
;二是不使用 new
操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person
构造函数重写如下。
function Person(name, age, job){
//创建要返回的对象
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name)
}
//返回对象
return o
}
const friend = Person('yy', 22, 'web')
friend.sayName(); //yy
在以这种模式创建的对象中,除了使用 sayName()
方法之外,没有其他办法访问 name
的值。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此
instanceof
操作符对这种对象也没有意义。
下一篇: 面向对象(三)