在JavaScript中,创建对象有多种方式,每种方式都有其优缺点。本文将介绍四种常见的对象创建模式:工厂模式、构造函数模式、原型模式和组合模式,并分析它们的特点以及如何优化。
1. 工厂模式
工厂模式是一种简单的对象创建方式,它使用一个函数来封装创建对象的细节。
function createPerson(name, age) {const obj = new Object();obj.name = name;obj.age = age;obj.sayName = function() {console.log(this.name);};return obj;
}const person1 = createPerson("Alice", 25);
const person2 = createPerson("Bob", 30);
1.1. 优点
1. 简单易用,避免了重复代码;
2. 可以创建多个相似对象;
1.2. 缺点
1. 创建的对象无法识别类型,因为都是Object类型;
2. 每个对象都有自己独立的方法实例,造成内存浪费;
1.3. 优化方案
可以将方法移到工厂函数外部,减少内存消耗;
function sayName() {console.log(this.name);
}function createPerson(name, age) {const obj = new Object();obj.name = name;obj.age = age;obj.sayName = sayName;return obj;
}
2. 构造函数模式
构造函数模式使用new操作符来创建特定类型的对象。
function Person(name, age) {this.name = name;this.age = age;this.sayName = function() {console.log(this.name);};
}const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
2.1. 优点
1. 可以识别对象类型,通过instanceof检测;
2. 符合传统的面向对象语言创建对象的方式;
2.2. 缺点
1. 每个方法都要在每个实例上重新创建一遍,造成内存浪费;
2. 如果方法很多,会占用大量不必要的内存;
2.3. 优化方案
将方法定义在构造函数外部
function Person(name, age) {this.name = name;this.age = age;this.sayName = sayName;
}function sayName() {console.log(this.name);
}
3. 原型模式
原型模式利用原型链来实现属性和方法的共享。
function Person() {}Person.prototype.name = "Default";
Person.prototype.age = 0;
Person.prototype.sayName = function() {console.log(this.name);
};const person1 = new Person();
person1.name = "Alice";
person1.age = 25;const person2 = new Person();
person2.name = "Bob";
person2.age = 30;
3.1. 优点
1. 所有对象实例共享原型上的属性和方法,节省内存;
2. 可以在运行时动态修改原型,影响所有实例;
3.2. 缺点
1. 所有实例默认取得相同的属性值,不太灵活;
2. 对于包含引用类型值的属性,如数组,一个实例修改会影响所有实例;
3.3. 优化方案
组合使用构造函数模式和原型模式
4. 组合模式(构造函数+原型)
组合模式是使用最广泛、认同度最高的一种创建自定义类型的方法。
function Person(name, age) {// 实例属性this.name = name;this.age = age;
}// 共享方法
Person.prototype = {constructor: Person,sayName: function() {console.log(this.name);}
};const person1 = new Person("Alice", 25);
const person2 = new Person("Bob", 30);
4.1. 优点
1. 每个实例有自己的属性副本;
2. 所有实例共享方法引用,节省内存;
3. 可以向构造函数传递参数,灵活性高;
4. 是目前ECMAScript中使用最广泛的方式;
4.2. 缺点
1. 构造函数和原型分开定义,可能让代码组织不够清晰;
2. 对于习惯其他语言的开发者来说,这种模式可能不太直观;
4.3. 优化方案
使用动态原型模式,将所有信息封装在构造函数中。
function Person(name, age) {// 属性this.name = name;this.age = age;// 方法(只在第一次调用构造函数时添加到原型)if (typeof this.sayName !== "function") {Person.prototype.sayName = function() {console.log(this.name);};}
}
注意:使用动态原型模式时,不能用对象字面量重写原型。
function Person(name) {this.name = name;if (typeof this.getName != "function") {Person.prototype = {constructor: Person,getName: function () {console.log(this.name);}}}
}var person1 = new Person('Jack1');
var person2 = new Person('Jack2');// 报错 并没有该方法
person1.getName();// 注释掉上面的代码,这句是可以执行的。
person2.getName();
开始执行 var person1 = new Person('Jack1')
new 的执行过程:
1. 首先新建一个对象;
2. 然后将对象的原型指向 Person.prototype;
3. 然后 Person.apply(obj);
4. 返回这个对象;
注意这个时候,回顾下 apply 的实现步骤,会执行 obj.Person 方法,这个时候就会执行 if 语句里的内容,注意构造函数的 prototype 属性指向了实例的原型,使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了。
如果你就是想用字面量方式写代码,可以尝试下这种:
function Person(name) {this.name = name;if (typeof this.getName != "function") {Person.prototype = {constructor: Person,getName: function () {console.log(this.name);}}return new Person(name)}
}var person1 = new Person('Jack1');
var person2 = new Person('Jack2');// 报错 并没有该方法
person1.getName();// 注释掉上面的代码,这句是可以执行的。
person2.getName();
5. 总结
在实际开发中,组合模式(构造函数+原型)是最常用的方式,它结合了构造函数模式和原型模式的优点,避免了各自的缺点,是创建自定义类型的首选方法。