虽然Object构造函数(var obj = new Object() )和对象字面量(var obj = {})都可以用来创建单个对象,但是这种方式存在明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
为了解决这个问题,开发者开始使用工厂模式。
1.工厂模式
这种模式抽象了创建具体对象的过程
由于JavaScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节
// 工厂模式 function createPerson(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 person1 = createPerson('zhangsan', 18, 'student'); var person2 = createPerson('lisi', 27, 'Soft Engineer');
函数能够接受参数来创建一个包含所有必要信息的Person对象。
可以无数次的调用这个函数,而每次都会返回一个包含三个属性和一个方法的对象。
优点:解决了创建多个相似对象的问题
缺点:没有解决对象识别问题(即怎样知道一个对象的类型)
随着JavaScript的发展,有一个新的模式出现了---构造函数模式
2.构造函数模式
// 构造函数 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person('zhangsan', 18, 'student'); var person2 = new Person('lisi', 27, 'Soft Engineer');
与工厂函数的不同之处:
- 没有显示创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
- 构造函数首字母大写
要创建Person的新实例,必须使用new操作符,以这种方式调用函数实际上会经历以下4个步骤:
- 创建一个新对象(var o = new Object() 或者 var o = {} )
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的所有代码(为新对象添加属性和方法)
- 返回新对象(return o;)
person1和person2分别保存着Person对象的不同实例,每个实例对象都有一个constructor(构造函数)属性,该属性指向Person
该属性(constructor)最初是用来标识对象类型
// 对象的constructor属性最初用来标识对象类型 console.log(person1.constructor == Person); //true console.log(person2.constructor == Person); //true
但是检测对象类型,还是建议使用 instanceof 操作符
person1 instanceof Person; //true person1 instanceof Object; //true person2 instanceof Person; //true person2 instanceof Object; //true
优点:创建自定义构造函数,可以将它的实例对象标识为一直能够特定类型,可以通过constructor属性来判断对象类型
缺点:构造函数里的方法,在每个实例对象上都要重新创建,也就是person1和person2实例上的方法是来自不同Function实例
// 构造函数 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; // this.sayName = function() { // alert(this.name); // }; this.sayName = new Function("alert(this.name);") }
alert(person1.sayName === person2.sayName); // false
可以将方法抽离出来,将函数定义转移到构造函数外来解决这个问题
// 构造函数 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName; } // 抽离出函数 function sayName() { alert(this.name); } var person1 = new Person('zhangsan', 18, 'student'); var person2 = new Person('lisi', 27, 'Soft Engineer');
但是这样又会引发新的问题:
- 在全局作用域中定义的函数实际上只能够被某个函数调用,这全局作用域有点名不副实
- 如果这个对象要定义很多方法,那么就要定义很多全局函数
于是,又有了一种新的模式来解决上述问题-----原型模式
3.原型模式
我们创建的每个函数都有一个prototype (原型)属性,这个属性是一个指针,指向一个原型对象。
即原型对象,就是调用构造函数而创建的实例对象的原型。
原型对象的用途:包含由特定类型的所有实例对象共享的属性和方法
// 原型模式 function Person() {} // 构造函数 Person.prototype.name = 'Ami'; //构造函数的原型对象 Person.prototype
Person.prototype.age = 18;
Person.prototype.job = 'student';
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person(); // 实例对象
person1.sayName(); //Ami
var person2 = new Person();
person2.sayName(); //Ami
怎么理解原型对象?
任何时候,只要创建了一个新函数, 就会根据一组特定的规则为该函数创建一个prototype属性,该属性指向新函数的原型对象。
在默认情况下,所有原型对象都会自动获取一个constructor属性,该属性是一个指向prototype属性的指针。
创建了自定义的构造函数之后,构造函数的原型对象会取得一个constructor属性,其他方法,则从Object继承而来。
当调用构造函数去创建一个实例对象后,该实例对象内部包含一个指针([[prototype]]),指向构造函数的原型对象,这就形成了一个闭环。
根据上面例子阐释:
Person(构造函数)--------->prototype属性--------->Person.prototype(Person的原型对象)--------->constructor属性-------> Person(构造函数)------>new Person()---->person1(实例对象)---->__proto__----Person.prototype(构造函数的原型对象)
|
|
Person(构造函数)------>new Person()---->person1(实例对象)---->__proto__----Person.prototype(构造函数的原型对象)
每个函数上都有prototype属性,指向构造函数的原型对象
每个对象上都有constructor属性,指向构造函数
每个实例对象上都有一个[[prototype]](__proto__)属性,指向构造函数的原型对象
var person1 = new Person(); // 实例对象 person1.sayName(); //Ami var person2 = new Person(); person2.sayName(); //Ami
//每个实例对象都有一个隐藏的[[prototype]]属性(__proto__),指向原型对象,现代浏览器在每个对象上都支持一个属性__proto__,这个
//__proto__属性是存在于实例对象和构造函数的原型对象之间的一种连接
原型对象存在的问题:
- 所有属性和方法都公用一个对象(原型对象),这就导致了下面的问题,如果在实例对象上创建了一个和原型对象上的同名方法或者属性,那么,实例对象上的方法或属性就屏蔽原型对象上的属性和方法,称为“同名屏蔽”现象
- 每添加一个属性或者方法,就要敲一遍Person.prototype
为了解决问题2,又提出了一种更简单的原型语法,就是重写原型对象
function Person() {} Person.prototype = { name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } };
但是,这样又带来了新的问题:那就是将Person.prototype设置为等于一个以对象字面量形式创建的新对象,导致了constructor属性不在指向构造函数Person了。
解析:因为每当新建一个对象,新对象就会自动获取一个constructor属性,新对象的constructor属性指向的是构造函数Object,而不是构造函数Person了。
instanceof操作符还能返回正确结果,但是通过constructor属性,已经无法确定对象类型了。
var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //true console.log(friend.constructor == Person); //false
解决方法:
将constructor属性强制指向构造函数Person
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //false console.log(friend.constructor == Person); //true
但是,这样又出问题了,这种方式导致原来不可枚举的属性constructor ,变成了可枚举属性了,心中万马奔腾~~~
如何解决呢?
如果你的浏览器兼容ES5,可以用Object.defineProperty()
function Person() {} Person.prototype = { // constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person }); var friend = new Person(); console.log(friend instanceof Object); //true console.log(friend instanceof Person); //true console.log(friend.constructor == Object); //false console.log(friend.constructor == Person); //true
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
obj
要在其上定义属性的对象。prop
要定义或修改的属性的名称。descriptor
将被定义或修改的属性描述符
解决了上述问题,原型模式还存在什么问题呢?
还有一个问题:那就是原型的动态性带来的问题
首先来说一下在原型链中查找值和设置值的区别:
(这个会在下一篇文章做一个详细的解释)
查找:当在原型链中查找某一属性时,首先看实例对象上有没有该属性,如果有,则返回;如果没有,就去该实例对象的原型对象上去查找,如果也没有,最后查找顶层对象(Object)的原型对象,如果还没有,则返回undefined。
设置:首先看实例对象上有没有该属性,如果有,则修改这个属性,如果没有,则新建一个属性,不会再往原型对象上去查找了。
由于原型链的查找是一次搜索过程,因此我们对原型对象所做的任何修改都能立即从实例对象上反映出来----即使是先创建实例对象,在修改原型对象,也是如此。
举个栗子:
function Person() {}
var friend = new Person();
Person.prototype.sayHi = function() { console.log('Hi'); }; friend.sayHi(); // Hi
解析:
虽然我们是先创建实例对象。但是当我们调用friend.sayHi()时,会先从实例对象上查找有没有这个方法,如果没有回继续去原型对象上查找,最终在原型对象上找到了这个方法,所以,即使是friend实例是在原型对象上添加新方法之前创建的,但是,实例对象仍然可以访问这个方法。
原因是:实例对象和原型之间有松散的连接关系。实力与原型之间的关系是一个指针,而非一个副本,因此可以在原型中找到新的sayHi属性,并返回保存在里面的函数。
因此,可以随时随地给原型添加属性或者方法,都可以立即在所有实例对象中反映出来。
但是,如果重写了整个原型对象,那么又会出现问题了
我们知道,当调用构造函数新建实例对象时,会为实例对象添加一个指向最初构造函数原型对象的[[prototype]]指针(也可以成为__proto__),但是,如果重写真个原型对象,就相当于把原型修改为另一个对象,就等于切断了构造函数和最初原型对象之间的联系。
function Person() {} var friend = new Person(); //指向最初的Person 实例对象的指针指向的是构造函数的原型对象,而不是构造函数 Person.prototype = { constructor: Person, // 指向构造函数Person name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; friend.sayName(); //Uncaught TypeError: friend.sayName is not a function
正确的写法应该是:
- 创建构造函数
- 给构造函数的原型对象上添加属性和方法
- 实例化对象
- 调用实例对象的属性和方法
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', sayName: function() { console.log(this.name); } }; var friend = new Person(); friend.sayName(); // Ami
原型模式最大的问题:就是由共享的本性所导致的
function Person() {} Person.prototype = { constructor: Person, name: 'Ami', age: 18, job: 'student', friends: ['zhangsan', 'lisi'], sayName: function() { console.log(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push('wanger'); console.log(person1.friends); // ["zhangsan", "lisi", "wanger"] console.log(person2.friends); // ["zhangsan", "lisi", "wanger"] console.log(person1.friends === person2.friends); // true
由于共享性,导致其中一个实例修改了原型上的属性或者方法,其他所有实例都会跟着改变,所以很少有人会单独使用原型模式,于是就有了下面的组合模式
4.组合使用构造函数模式和原型模式
构造函数模式:用于定义实例属性
原型模式:用于定义方法和共享属性
//构造函数模式 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ['zhangsan', 'lisi']; } // 原型模式 Person.prototype = { constructor: Person, sayName: function() { console.log(this.name); } }; var person1 = new Person('Jack', 28, 'Doctor'); var person2 = new Person('Tom', 34, 'Teacher'); person1.friends.push('wanger'); console.log(person1.friends); // ["zhangsan", "lisi", "wanger"] console.log(person2.friends); // ["zhangsan", "lisi"] console.log(person1.friends === person2.friends); // false console.log(person1.sayName === person2.sayName); // true
下面三种作为了解