本文深入解析JavaScript中几种核心的对象创建设计模式,包括工厂模式、构造函数模式、原型模式及其组合应用。详细对比各模式的优缺点,如对象识别、内存共享及参数传递等问题,并介绍动态原型模式的实现机制,帮助开发者根据实际需求选择最优方案。
工厂模式
工厂模式
function createPerson(name, age){
var o = new Object(); // 创建一个对象
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
}
return o; // 返回这个对象
}
var person1 = createPerson('ccc', 18)
var person2 = createPerson('www', 18)
尽管工厂模式有效解决了批量创建相似对象的问题,但它未能解决对象识别问题,即无法准确判断具体对象的类型。示例如下:
person1 instanceof createPerson // --> false
person1 instanceof Object // --> true
构造函数模式
构造函数模式
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
person1.sayName() // --> 'ccc'
通过构造函数创建的 person1 和 person2 分别拥有独立的实例,且两者的 constructor 属性均指向 Person。这一特性使得构造函数模式在对象类型识别上优于工厂模式。代码如下:
console.log(person1 instanceof Person) // --> true
console.log(person1 instanceof Object) // --> true
console.log(person2 instanceof Person) // --> true
console.log(person2 instanceof Object) // --> true
构造函数模式与工厂模式的主要区别在于:
- 代码中未显式创建对象实例。
- 直接将属性和方法赋值给 this 对象。
- 构造函数内部没有 return 语句。
若要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下四个步骤:
- 创建一个新对象。
- 将构造函数的作用域赋给新对象(此时 this 指向该新对象)。
- 执行构造函数中的代码(为该新对象添加属性)。
- 返回新对象。
构造函数模式存在一个重要问题:每个方法都要在每个实例上重新创建一遍。
person1 和 person2 中虽然都有一个名为 sayName() 的方法,但它们并非同一个 Function 实例。因为在 ECMAScript 中函数即对象,每定义一个函数便实例化了一个对象。从逻辑角度而言,此时的构造函数也可定义如下:
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = new Function('console.log(this.name)') // eslint: The Function constructor is eval. (no-new-func)
}
这会导致不同实例上的同名函数不相等,例如执行 console.log(person1.sayName() === person2.sayName()) 将返回 false。然而,创建两个完全相同的 Function 实例并无必要。可以通过将函数定义移至构造函数外部来解决此问题。
function Person(name , age){
this.name = name;
this.age = age;
this.sayName = sayName
}
function sayName(){
console.log(this.name)
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
由于 sayName 包含指向函数的指针,person1 和 person2 对象便共享了在全局作用域中定义的同一个 sayName() 函数。这种做法确实解决了函数重复创建的问题,但引入了新问题:在全局作用域中定义的函数实际上只能被某个特定对象调用,这使得全局作用域名不副实。 带来的新问题: 如果对象需要定义大量方法,则需定义多个全局函数,导致自定义引用类型完全丧失封装性。
原型模式
原型模式
关于原型及原型链的详细内容不在本文讨论范围内,此处仅探讨原型设计模式。 我们创建的每一个函数都有一个 prototype(原型)属性,该属性是一个指针,指向一个对象。此对象的用途是包含可以由特定类型的所有实例共享的属性和方法。即不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中。
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
原型模式存在以下问题:
它省略了为构造函数传递参数以初始化参数的环节,导致所有实例在默认情况下都将取得相同的属性值。另外,原型模式的最大问题源于其共享的本性。具体问题如下:
function Person(){
}
Person.prototype = {
constructor: Person,
name: 'ccc',
age: 18,
friends:['www', 'aaa'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('bbb')
console.log(person1.friends) // --> ["www", "aaa", "bbb"]
console.log(person2.friends) // --> ["www", "aaa", "bbb"]
console.log(person1.friends === person2.friends) // --> true
带来的新问题:
如果初衷确实是让所有实例共用一个数组,那么上述结果是符合预期的。然而,实例通常需要具备属于自己的全部属性,正是由于这一问题,我们很少看到有人单独使用原型模式。
组合模式与动态原型模式
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。这种方式还支持向构造函数传递参数。
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ['aaa', 'bbb']
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name)
}
}
var person1 = new Person('ccc', 18)
var person2 = new Person('www', 18)
person1.friends.push('ddd')
console.log(person1.friends) // --> ["aaa", "bbb", "ddd"]
console.log(person2.friends) // --> ["aaa", "bbb"]
console.log(person1.friends === person2.friends) // --> false
console.log(person1.sayName === person2.sayName) // --> true
这种构造函数与原型混成的模式,是目前 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认方式。
动态原型模式
动态原型模式可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age){
// 属性
this.name = name
this.age = age
// 方法
if(typeof this.sayName !== 'function'){
Person.prototype.sayName = function(){
console.log(this.name)
}
}
}
var person1 = new Person('ccc', 18)
person1.sayName() // --> ccc
这里只有在 sayName() 方法不存在的情况下,才会将其添加到原型中。这段代码只会在初次调用构造函数时执行。 注意:
- 在此处对原型所做的修改,能够立即在所有实例中得到反映。
- 使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,就会切断现有实例与新原型之间的联系(参考原型与原型链中的内容)。
其他模式简介
其它模式
还有寄生构造函数模式和稳妥构造函数模式,可自行了解。以上所有知识内容来自《JavaScript高级程序设计》(第三版)。
以上就是详解js中的几种常用设计模式的详细内容,更多关于JS 设计模式的资料请关注本站其它相关文章!