工厂模式
使用工厂模式创建对象,用函数封装创建对象。
- 优点:减少重复大量的代码。
- 缺点:没有解决对象识别的问题。即不知道对象的类型。
- 对象类型:内部对象、宿主对象、开发人员自定义的对象。
function createPerson(name, jobs) {
var o = new Object();
o.name = name;
o.jobs = jobs;
o.getName = function () {
console.log(this.name);
}
return o;
}
var person1 = createPerson('peter', 'codingmonkey');
var person2 = createPerson('lynn', 'doctor');
构造函数
如果通过构造函数模式来修改前面的例子重写如下:
function Person(name, jobs) {
this.name = name;
this.jobs = jobs;
this.getName = function () {
console.log(this.name);
}
}
var person1 = new Person('peter', 'codingmonkey');
var person2 = new Person('lynn', 'doctor');
构造函数模式与工程模式创建对象的区别:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。
构造函数和普通函数的区别:是否使用 new 操作符来调用。
new 操作符
使用 new 操作符调用构造函数都会经历的步骤:
1. 创建一个新的对象;
2. 因为新的对象需要访问到构造函数原型链上的属性或方法,那么就需要进行新的对象执行原型链的链接;
3. 会将构造函数的作用域赋给新对象,那么新对象会绑定到函数调用的 this;
4. 执行构造函数中的代码,为这个新对象添加属性或方法;
5. 如果构造函数没有返回对象类型,那么就是返回这个新对象。
模拟 new 操作符的实现代码:
function createNew(Con, ...args) {
let obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}
constructor
每个实例对象会有一个 constructor 属性,这个相当于一个指针,它指向创建当前实例对象的对象。
构造函数的优点:解决了识别对象类型,就是通过 constructor 属性来标识对象类型的。
例子:
person1.constructor === Person // true
构造函数的缺点:每个方法都要在每个实例对象上面重新创建一次:
function Person(name, jobs) {
this.name = name;
this.jobs = jobs;
// 这个 getName 方法每次都会创建相同的方法
// 等于 this.getName = new Function('console.log(this.name)')
this.getName = function () {
console.log(this.name);
}
}
解决方法有2个:
- 第一个解决方法是定义一个全局 getName 的方法赋给 this.getName = getName。
- 第二个解决方法是使用原型模式。
原型模式
概念:每个函数都会有一个 prototype(原型) 的属性,这个属性是一个指针,指向一个对象,而这个对象的作用就是让所有对象实例共享它所包含的属性和方法。那么解决构造函数定义的方法,只需要添加到原型对象中即可:
function Person(name, jobs) {
this.name = name;
this.jobs = jobs;
}
Person.prototype.getName = function () {
console.log(this.name);
}
var person1 = new Person('peter', 16);
var person2 = new Person('lynn', 16);
person1.getName(); // peter
person2.getName(); // lynn
原型对象
- 每个函数都会有一个 prototype 的属性,这个属性指向函数的原型对象。
- 所有的原型对象都会有一个 constructor 的属性,这个属性指向 prototype 属性所在函数的指针。
- 调用构造函数创建一个新实例时,该实例会包含一个指针
[[Prototype]]
,这个指针通过 __proto__ 属性来访问,这个指针指向构造函数的原型对象。
function Person(name, jobs) {
this.name = name;
this.jobs = jobs;
}
var person1 = new Person('peter', 'codingmonkey');
// 第二点
console.log(Person.prototype.constructor === Person); // true
// 第三点
console.log(person1.__proto__ === Person.prototype); // true
如何确定 __proto__ 与原型对象的关系呢?
- isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
- Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值。
Person.prototype.isPrototypeOf(person1) // true
Object.getPrototypeOf(person1) === Person.prototype // true
原型的作用:
每当代码读取某个对象的某个属性时,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。直到最后,还没有找到该属性,则返回undefined。
实例对象与原型对象
如果实例对象和原型对象上存在相同的属性或方法,不会影响到原型对象上的属性或者方法,只是访问属性时,优先读取了实例对象上的属性,而不会再去原型对象上面进行查询返回。
function Person() {
}
Person.prototype.name = 'peter';
var person1 = new Person();
person1.name = "lynn";
console.log(person1.name); // lynn -- 来自于实例对象的 name 属性
delete person1.name;
console.log(person1.name); // peter -- 来自于原型对象的 name 属性
如何区分实例对象的属性或者原型对象上的属性呢?
- 使用 hasOwnPrototype() 方法可以检测一个属性是存在于实例中还是原型中,如果存在实例中,返回true,如果存在原型中返回false。
function Person() {
}
Person.prototype.name = 'peter';
var person1 = new Person();
person1.name = "lynn"; // 对象实例
console.log(person1.hasOwnProperty('name')); // true
delete person1.name; // 删除后,原型对象上存在 name 属性
console.log(person1.hasOwnProperty('name')); // false
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系。
function Person(name, jobs) {
this.name = name;
this.jobs = jobs;
}
Person.prototype.getName = function () {
console.log(this.name);
}
var person1 = new Person('peter', 'codingmonkey');
person1.getName(); // peter
// 重写原型对象
Person.prototype = {
constructor: Person,
getJobs: function () {
console.log(this.jobs);
}
}
person1.getJobs(); // TypeError: person1.getJobs is not a function
原型的缺点:某些场景下,通过构造函数实例的2个对象,如果都想对象独立一个属性时,但实际上,这2个实例对象会共享同一个原型属性。
function Person(name) {
this.name = name;
}
Person.prototype.friends = ['kelly'];
var person1 = new Person('peter');
var person2 = new Person('peter');
// 在 person1 上改变 friends 数组
person1.friends.push('jim');
console.log(person1.friends); // [ 'kelly', 'jim' ]
// person2实例也会受到影响
console.log(person2.friends); // [ 'kelly', 'jim' ]
解决方法:使用构造函数模式 + 原型对象模式创建对象。
in 操作符
使用 in 操作符会通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中。
使用 in 操作符还有一种情况,就是在 for-in 循环中,因为 in 操作符访问对象时,也会访问到原型对手上的属性,如果不想遍历时候访问原型对象上的属性时,所以建议在 for-in 循环里加上 hasOwnProperty() 方法判断,保证代码的健壮性。
function Person() {
}
// 原型对象上的属性
Person.prototype.name = 'peter';
var person1 = new Person();
// 实例对象属性
person1.jobs = 'codingmonkey'
for (let item in person1) {
console.log(item); // jobs, name
}
for (let item in person1) {
if (person1.hasOwnProperty(item)) {
console.log(item) // jobs
}
}
总结
可以采用下列模式创建对象。
- 工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
- 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
- 原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
资料来源
- 《JavaScript高级程序设计(第3版)》, by Nicholas C.Zakas (作者) 李松峰 , 曹力 (译者)
- 面试官问:能否模拟实现JS的new操作符, by 若川。
- 重学 JS 系列:聊聊 new 操作符, by yck