工厂模式
工厂模式通过一个工厂函数创建对象。通过字面量创建对象固然直观,但当我们需要一次性创建多个具有相似属性的对象时,通过字面量创建的方式会造成大量的代码冗余。
//将创建对象的代码封装在一个函数中
function createPerson(name, age, gender) {
var person = new Object();
person.name = name;
person.age = age;
person.gender = gender;
person.sayName = function () {
console.log(this.name);
}
return person;
}
//利用工厂函数来创建对象
var person1 = createPerson("zhangsan", 18, 'male');
var person2 = createPerson("lisi", 20, 'female');
特点
如上图代码所示,通过一个函数传入参数作为对象的属性,函数执行返回一个对象供我们使用,这种方式在创建大量对象的同时不会造成代码过度冗余,创建对象时所传入的属性为实例对象所拥有的属性 不会被“同辈”对象所共享
构造函数模式
js中的构造函数是用于创建特定类型对象的。js原有的构造函数有Object、Array、String等。
当然 在实际开发中 开发人员也可以自己创建自定义的构造函数,用于创建适合项目开发的对象。
// 自定义构造函数
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function () {
console.log(this.name);
}
}
var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');
person1.sayName(); // zhangsan
person2.sayName(); // lisi
以上代码和第一个例子工厂模式创建对象类似,两者区别如下:
- 没有通过new Object显式的创建对象。
- 属性和方法通过this赋给了调用者
- 没有return一个对象
需要注意的是:
构造函数在命名时需要将首字母大写 这是从面向对象编程语言中借鉴的惯例 用于区分构造函数和非构造函数
通过new操作符创建实例时,js会进行以下的操作:
- 在内存中创建一个新的对象。
- 新对象内部的[[prototype]]特性被赋值成构造函数的prototype属性。
- 构造函数内部的this指向这个新对象
- 执行构造函数的函数体代码(通常是给对象添加属性和方法)
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
特点
构造函数同工厂函数一样、函数体中创建的属性和方法被分别的添加在构造出的新对象中,互不相关,也就是说,对于person1和person2来说 虽然他们都有sayname函数,但这两个函数被分别创建在不同的内存空间中,这在函数定义过多时会占用多余的内存。
这样的问题可以通过将函数定义在全局定义域的方式来解决,具体做法为:
- 将需要共享的函数定义在公共作用域
- 在函数中将这个函数作为成员属性引用
这样的做法虽然解决了内存占用的问题,但又迎来了新的问题:作用域污染。
原型模式
每个函数都会创建一个prototype属性,这个属性指向一个对象,这个对象包含了对象实例共享的属性和方法,这个对象实际上就是通过调用构造函数创建的对象的原型。
简要的说:在构造函数的prototype对象上定义的属性和方法可以被构造函数构造出的实例对象所调用
function Person(){}
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); // zhangsan
var person2 = new Person();
person2.sayName(); // zhangsan
console.log(person1.sayName == person2.sayName); // true
以上代码中:构造函数体为空,所有的属性和方法写在了Person构造函数的prototype属性(也就是一个对象)中。
通过Person构造函数new一个实例对象后依旧拥有name 、age等属性方法,这是通过原型对象共享获得的,具体可以参照原型链继承。
此时,共有的方法被放在了原型对象中,实例对象中并没有具体的方法和属性,当然我们也可以手动给实例对象添加同名属性或方法,当我们通过实例对象调用的时候,如果实例对象中有此方法或属性,就会优先使用实例中的,而不会去寻找原型对象中的属性和方法,这是一种遮蔽手段,也是我们“个性化”实例对象的方法。
关于构造函数还有一个注意点:上方代码中使用点语法给prototype原型对象添加属性和方法。还有一种更直观和简单的方式书写prototype原型对线——字面量重新书写
function Person() {}
Person.prototype = {
name: "zhangsan",
age: 29,
gender: "male",
sayName() {
console.log(this.name);
}
};
// 在这个案例中,Person.prototype 被设置为等于一个通过对象字面量创建的新对象。最终结果是一样的,只有一个问题:这样重写之后,Person.prototype的 constructor 属性就不指向 Person了。在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。
var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true
在以上案例中 直接使用大括号创建了一个新对象作为构造函数Person的原型对象,这样虽然简单直观,但也导致了一个新的问题出现 此原型对象的constructor属性并不指向Person构造函数(众所周知 原型对象中有一constructor属性指向他的构造函数)
如何解决这一问题呢 最简单粗暴的方法就是直接给prototype对象添加属性constructor 将值指向Person构造函数,但这并不可取 因为JS默认对象的constructor属性特性为enumerable的值为false,即不可枚举,而这种暴力方式的方式创建的属性为可枚举属性
我们可以通过Object.defineProperty()函数来重新定义constructor属性
function Person() { }
Person.prototype = {
//这种方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性
//constructor: Person,
name: "zhangsan",
age: 29,
gender: "male",
sayName() {
console.log(this.name);
}
};
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var person1 = new Person()
console.log(person1.constructor == Person); //true
console.log(person1.constructor == Object); //false
组合模式
单独使用构造函数模式 会造成内存占用的浪费
只使用原型模式 又会造成属性共享带来的不便。
于是 我们可以集二者之精华 将两者组合使用 这就是组合模式。
在构造函数中定义实例对象的属性,满足实例对象的“个性化” 。在原型对象中定义共有的成员属性和方法,保证内存性能的节约。这种模式是目前使用最广泛,认同度最高的一种创建自定义类型的方法
代码如下所示:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.friends = ['zhangsan', 'lisi'];
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name);
}
};
var p1 = new Person('larry', 44, 'male');
var p2 = new Person('terry', 39, 'male');
p1.firends.push('robin');
console.log(p1.friends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.friends); // [ 'zhangsan', 'lisi' ]
console.log(p1.friends === p2.friends); // false
console.log(p1.sayName === p2.sayName); // true
总结
四种模式各有各的优点和缺点,需要开发者在实际开发中根据情况考虑决定用哪一种方式来创建一个对象,满足项目的使用场景。
工厂模式和构造函数适合批量创建对象,不会造成代码冗余。但会造成在逻辑上多个对象调用同一个函数却实际开发中占用了多份内存性能的情况
原型模式会将所有原型属性和方法继承给实例对象使用,这样不会造成性能浪费,但会造成实例属性共享错乱的情况。