当我们想要创建同一种类型的对象时,如果仅仅是使用字面量或者一个一个属性添加的方式,会有很多重复的代码。所以当需要创建很多同一类型的对象时,提供了一些模式使之更为简便。
工厂模式
function ceatePerson (name, age, job) {
var o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function () {
alert(this.name)
}
return o
}
var p1 = ceatePerson('Nicholas', 29, 'Software Engineer')
var p2 = ceatePerson('Greg', 27, 'Doctor')
工厂模式是我们能想到最直接的方式,通过调用方法创建相同类型的对象,但是这种形式并没有让人觉得p1和p2是同一类型的,他没有一个类的概念,没有解决对象识别的问题。
构造函数模式
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
alert(this.name)
}
}
同样是定义一个函数,当我们直接调用和使用new关键字调用的时候,如何执行这个方法是不同的,这是js提供给我们的自定义类型的方式,通过new关键字调用的函数叫构造函数。
直接调用
Person('Greg', 27, 'Doctor')
sayName() // 'Greg'
直接调用Person函数,此时的this值指向window,也就是在全局作用域中添加了name,age,job属性和sayName方法。
使用new关键字调用
var p1 = new Person('Nicholas', 29, 'Software Engineer')
var p2 = new Person('Greg', 27, 'Doctor')
当我们使用new关键字调用函数的时候,js实际上为我们做了四件事:
- 创建一个新对象(new Object())
- 将函数的作用域赋给新对象(将this指针指向这个新对象)、
- 执行构造函数中的代码 (为新对象添加属性)
- 返回新对象
判断对象的类型
- 使用new关键字调用生成的对象都有一个constructor属性,这个属性都指向调用的函数
p1.constructor == Person // true
p2.constructor == Person // true
- instanceof (推荐使用)
p1 instanceof Object // true
p1 instanceof Person // true
构造函数模式的问题
从上面的写法上看可能不易察觉,但当我们将sayName改成以下的写法,我们就能发现,每当我们创建一个Person类型的对象,都会创建一个新的sayName方法,但其实每个sayName都是一样的,这样的重复是没有意义的。
function Person (name, age, job) {
...
this.sayName = new Function("alert(this.name)")
}
也许我们想可以在全局上先声明一个sayName方法,然后在Person内将sayName属性指向全局的这个方法,但是这样也会使全局作用域上充满其实只和某种类型相关的方法,这并不算一个好办法。
原型模式
在开始使用原型模式创建对象之前,请记住三点,这对以下的理解非常重要:
- 当创建了一个新函数时,会为该函数创建一个prototype属性,指向函数的原型对象
- 默认所有的原型对象都会有constructor属性,指向原型对应的函数
- 当调用构造函数创建实例时,实例会包含一个[[prototype]]指针,指向构造函数的原型对象
function Person () {
}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function () {
alert(this.name)
}
var person1 = new Person()
根据上面的三点画出整个执行过程
确定实例和原型对象间的关系
实例对象和构造函数其实没有直接关联,而是关联了构造函数的原型对象
- isPrototypeOf()
Person.prototype.isPrototypeOf(person1) //true
- Object.getPrototypeOf()
Object.getPrototypeOf(person1) == Person.prototype //true
- 确认属性来自实例还是原型: hasOwnProperty
// 实例上的属性返回 true,原型上的属性返回 false
person1.hasOwnProperty('name') // false
- 确认是否包含属性(无论来自实例还是原型): in操作符
'name' in person1 // true
注意点
- [[prototype]]属性在浏览器上的一般表现为__proto__
- 当读取对象属性时,会先搜索实例本身,有则返回,没有再继续搜索其原型对象,如此向上查询
- 实例的属性会屏蔽原型上的同名属性,但delete操作符可以删除实例上的属性,从而可以重新访问原型上的属性
- 当我们整个给prototype重新赋值,而不是增加属性时,我们其实改变了构造函数中prototype的指向,新的指向并不会默认添加constructor属性指向构造函数,所以需要手动添加。
Person.prototype = {
name : 'Nicholas'
age : 29
job : 'Software Engineer'
sayName : function () {
alert(this.name)
}
}
// 手动添加constructor的指向
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
在改变prototype的指向后,之前创建的实例其实就已经切断了和当前原型的关系。
- 原生引用类型也都是采用原型模式创建的。
原型模式的问题
原型模型中,所有的属性都是共享的,这对于函数很合适,但是对于其他属性其实我们希望的是每个实例有自己个性化的属性值。
组合使用构造函数和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。
// 构造函数模式
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
}
// 原型模式
Person.prototype.sayName = function () {
alert(this.name)
}
这样的组合方法其实已经很完美了,后面介绍的几种模式其实没那么重要了。
动态原型模式
要是找茬说组合使用构造函数和原型模式还有什么缺点,可能就是在创建Person对象的时候,分了两部分去定义实例属性和原型属性,而不是放在一个方法里定义的,可以使用动态原型模式把所有属性定义封装在构造函数中。
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
// 只有在sayName方法不存在的情况下,才会添加到原型上,也就是这段代码只有在初次调用构造函数时才执行
if (typeof this.sayName != 'function'){
Person.prototype.sayName = function () {
alert(this.name)
}
}
}
寄生构造函数模式
寄生构造函数模式和工厂模式很像,一般用于寄生在其他构造函数之上的,不常使用。比如我们想修改一些原生引用类型:
function specialArray() {
var values = new Array()
// 添加值
values.push.apply(values, arguments)
// 添加方法
values.toPipedString = function() {
return this.join('|')
}
return value
}
var colors = new specialArray('red', 'blue')
var s = colors.toPipedString() // red|blue