我们知道:Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
比如,我想创建两个person对象。包含不同的信息。用Object构造函数的方法,代码如下:
var person1 = new Object();
person1.name = "Tom";
person1.age = 23;
person1.job = "Player";
person1.sayName = function(){
console.log(this.name);
}
var person2 = new Object();
perosn2.name = "Jerry";
person2.age = 22;
person2.job = "Doctor";
person2.sayName = function(){
console.log(this.name);
}
以上代码是不是有很多相似又重复的地方。
解决办法:工厂模式
一、工厂模式
这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var person1 = createPerson("Tom", 23, "Player");
var person2 = createPerson("Jerry", 22, "Doctor");
优点:解决了创建很多个相似对象的问题。
缺点:没有解决对象识别的问题(即怎样知道一个对象的类型)。
解决办法:构造函数模式
二、构造函数模式
首先我们要知道构造函数分两类:
- 原生构造函数:例如 Object() Array() Date()
- 自定义构造函数:顾名思义,自己定义的构造函数,例如:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");
1、以上代码是使用构造函数模式将工厂模式的例子重写了一遍。这两个函数的区别是:
- 没有显式的创建对象(new)
- 直接将方法和属性赋给了this对象
- 没有return语句
- 函数名(Person) 第一个字母用大写。构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头
要创建Person 的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
2、上面的代码中,person3 和person4 分别保存着Person 的一个不同的实例。
这两个对象都有一个constructor(构造函数)属性,该属性指向Person。(这句是JS红皮书中的话,其实是有误的,这两个对象实例其实没有costructor属性,是它们的原型有这个属性,通过原型链就可以访问到)
console.log(person3.constructor == Person); // true
console.log(person4.constructor == Person); // true
console.log(person3 instanceof Object); //true
console.log(person3 instanceof Person); //true
可以看出来,创建的person3和person4的两个对象实例既是Object的实例,又是Person的实例。
由此可知:创建自定义构造函数可以吧它的实例标识为一种特定的类型,这也正是构造函数胜过工厂模式的地方。
3、将构造函数当做函数
构造函数与普通函数的唯一区别就是调用它们的方式不同
我们要知道的是:构造函数也是函数。所以,任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样
//当做构造函数使用
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"
//当做普通函数使用
Person("Jerry", 21, "Doctor"); //添加到window
window.sayName(); //"Jerry"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Bob", 25, "Nurse");
o.sayName(); //"Bob"
4、构造函数的缺点
构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。前面例子中的person3和person4这两个实例都有一个sayName()方法,但是这两个方法不是同一个Function实例。因为JS中函数也是对象。所以每定义一个函数,也就实例化了一个对象:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(htis.name)");
}
从这个角度看构造函数,更容易明白创建的每一个Person实例都包含一个不同的Function实例。所以,不同实例上的同名函数是不相等的。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");
console.log(person3.sayName == person4.sayName); //false
然而,创建两个完成同样任务的Function 实例的确没有必要;况且有this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person3 = new Person("Tom", 23, "Player");
var person4 = new Person("Tom", 22, "Doctor");
console.log(person3.sayName == person4.sayName); //true
在这个例子中,我们把sayName()函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName 属性设置成等于全局的sayName 函数。这样一来,由于sayName 包含的是一个指向函数的指针,因此person1 和person2 对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。
三、原型模式
我们需要知道的定义:
1、我们创建的每一个函数都有一个prototype属性,这个属性指向一个对象——原型对象。
2、每个原型对象都有一个constructor属性,指向这个属性指向 prototype属性所在函数的指针。
3、原型对象的用途:包含可以由特定类型的所有实例共享的属性和方法。不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.job = "Player";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); //"Tom"
var person2 = new Person();
person2.sayName(); //"Tom"
console.log(person1.sayName == person2.sayName); //true
1、理解原型对象
需要知道的:
1、我们创建的每一个函数都有一个prototype属性,这个属性指向一个对象——原型对象。
2、每个原型对象都有一个constructor属性,指向这个属性指向 prototype属性所在函数的指针。
3、当调用构造函数创建一个新实例(person),每个实例都有一个 __ proto __ 属性,指向原型对象。
如下图:
检测方法:
1、
isPrototypeOf()
来确定实例与原型之间是否存在对应关系
console.log(Person.prototype.isPrototypeOf(person1)); //true
2、Object.getPrototypeOf()
,在所有支持的实现中,这个方法返回[[Prototype]]的值
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Tom"
3、hasOwnProperty()方法
可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object 继承来的)只在给定属性存在于对象实例中时,才会返回true
2、原型与in操作符
in 操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
function Person(){}
Person.prototype.name = "Tom";
Person.prototype.age = 23;
Person.prototype.job = "Player";
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //true
person1.name = "Bob";
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
3、更简单的原型语法
前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示。
function Person() {}
Person.prototype = {
name:"Tom",
age:23,
job:"Player",
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 == Person); //false
console.log(friend.constructor == Object); //true
这里的唯一区别就是constructor不再指向Person。
4、原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
function Person(){}
var friend = new Person();
Person.prototype.sayHi = function(){
console.log("hi");
}
friend.sayHi(); //"hi"
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子
function Person(){}
var friend = new Person();
Person.prototype = {
constructor:Person,
name:"Tom",
age:23,
job:"Player",
sayName:function(){
console.log(this.name);
}
};
friend.sayName(); //error
5、原生对象的原型
我们知道所有的引用类型都会有自定义的一些方法,比如Array的sort()方法。其实都是Array()构造函数对应的原型的方法。
所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。
console.log(typeof Array.prototype.sort); //"function"
6、原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。
原型模式的最大问题是由其共享的本性所导致的。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟(如前面的例子所示),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了。来看下面的例子。
function Person(){}
Person.prototype = {
constructor:Person,
name:"Tom",
age:23,
job:"Player",
friends:["Jerry","Bob"],
sayName:function(){
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Mary");
console.log(person1.friends); //["Jerry", "Bob", "Mary"]
console.log(person2.friends); //["Jerry", "Bob", "Mary"]
console.log(person1.friends === person2.friends); //true
当通过person1修改friends属性的时候,其实是修改了原型,所以person2也会共享这个修改后的属性。但是实例一般应该要有属于自己的全部属性的。
四、组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Bob","Mary"];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person("Tom", 23, "Player");
var person2 = new Person("Jerry", 21, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); // ["Bob", "Mary", "Van"]
console.log(person2.friends); //["Bob", "Mary"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
上述的例子中,实例的属性都是在构造函数中定义,所有实例共享的方法和属性则是在原型中定义
五、动态原型模式
我们组合使用构造函数和原型模式是分开的,如何写到一块呢,这时候就用到动态原型模式了。如下:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"
六、寄生构造函数模式
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
};
return o;
}
var person = new Person("Tom", 23, "Player");
person.sayName(); //"Tom"
在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。除了使用new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return 语句,可以重写调用构造函数时返回的值。
建议在能用其他方式的情况下不用寄生构造函数模式
七、稳妥构造函数模式 (了解即可)
道格拉斯·克罗克福德(Douglas Crockford)发明了JavaScript 中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this 和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person 构造函数重写如下