1.1.什么是工厂模式?
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。工厂模式是一种创建型模式,简单来说,工厂模式就是创建对象的一种方式。
1.2.工厂模式有什么用?
作用:创建对象;降低代码冗余度。
应用场景:当你想要批量生产同种类的对象的时候;比如,你想生成一个班级的40个学生,每个学生都有姓名、年龄等特征。这时候你创建一个“工厂”,把信息丢到工厂里,工厂就给你造一个人出来,非常方便。
1.3.为什么用工厂模式?
从工厂模式的作用出发来看,工厂模式的主要作用就是用来产生对象的。那么别的创建对象的模式有什么缺点?
a.用字面量的方式创建对象
字面量就是用来描述变量的;一般来说,给变量赋值的时候,等号右边的都可以看作是字面量(因为等号右边的都是用来描述这个变量的,比如描述一个变量为字符串(字符串字面量)、一个数组(数组字面量)、一个对象(对象字面量),等等)。
var person = {
name: "zhangsan",
age: 18,
gender: 'male',
sayName: function(){
console.log(this.name);
}
}
缺点:用字面量的方式来创建对象,最大的缺点就是,这个对象是一次性的,如果有四十个同学,这个代码就要写四十次,有点小麻烦。
b.new Object()创建对象
Object是JavaScript提供的构造函数;new Object()就是利用JavaScript提供的构造函数实例化了一个对象;
var person = new Object();
//为这个实例化的对象添加属性
person.name = "zhangsan";
person.age = 18;
person.gender = 'male';
person.sayName = function(){
console.log(this.name)
}
缺点:可以发现它是先实例化了一个对象,然后再为对象添加属性,这样就看不出来是个整体(像上面的用字面量来创建,属性都包在一个大括号里面,这样就很好看出这是个整体)。
因此,我们为了使创建对象更加方便(不像字面量创建那样一次性),也为了写的代码更像个整体,就可以交给工厂模式来做。、
1.4.使用工厂模式创建对象
//将创建对象的代码封装在一个函数中
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');
优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造个人出来。
缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。
所以,我们可以使用自定义构造函数模式。
2.构造函数模式
ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。
JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。
2.1.自定义构造函数
// 自定义构造函数
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
在这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。
-
没有显式地创建对象。
-
属性和方法直接赋值给了 this。
-
没有 return。
另外,要注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。
2.2.创建Person实例
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor
属性,这两个对象的constructor 属性指向 Person,
如下所示:
console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true
2.3.instanceof
constructor 本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型更可靠的方式。
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。或者说判断一个对象是某个对象的实例。
前面案例中的每个对象都是 Object 的实例,同时也是 Person 的实例,如下面调用
//instanceof 操作符的结果所示:
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在这个案例中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。
2.4.使用函数表达式自定义构造函数
构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:
var Person = function (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", 27, "female");
person1.sayName(); // zhangsan
person2.sayName(); // lisi
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
补充:
在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数:
function Person() {
this.name = "larry";
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person();
var person2 = new Person;
person1.sayName(); // larry
person2.sayName(); // larry
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
2.5.构造函数也是函数
构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。比如,前面的案例中定义的 Person()可以像下面这样调用:
var Person = function (name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function () {
console.log(this.name);
};
}
// 作为构造函数
var person = new Person("Jacky", 29, "male");
person.sayName(); // Jacky
// 作为函数调用
Person("lisi", 27, "female"); // 添加到全局对象 node global 浏览器 window
global.sayName(); // lisi
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "wangwu", 25, "male");
o.sayName(); // wangwu
这个案例展示了典型的构造函数调用方式,即使用 new 操作符创建一个新对象。然后是普通函数的调用方式。
这时候没有使用 new 操作符调用 Person(),结果会将属性和方法添加到全局对象。这里要记住,在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this 始终指向 Global 对象(在浏览器中就是 window 对象)。因此在上面的调用之后,Global 对象上就有了一个 sayName()方法,调用它会返回"lisi"。
最后的调用方式是通过 call()(或 apply())调用函数,同时将特定对象指定为作用域。这里的调用将对象 o 指定为 Person()内部的 this 值,因此执行完函数代码后,所有属性和 sayName()方法都会添加到对象 o 上面。
2.6.构造函数的问题
方法还是冗余,因为每次创建实例对象时候还是会在给每个对象设置一个方法,要是有n个对象就是有n个方法。
3.原型模式
每个函数都会创建一个 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); // truefunction 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
这里,所有属性和 sayName()方法都直接添加到了 Person 的 prototype 属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。
3.1.原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。因此,在调用 person1.sayName()时,会发生两步搜索。首先,JavaScript 引擎会问:“person1 实例有 sayName 属性吗?”答案是没有。然后,继续搜索并问:“person1 的原型有 sayName 属性吗?”答案是有。于是就返回了保存在原型上的这个函数。在调用 person2.sayName()时,会发生同样的搜索过程,而且也会返回相同的结果。这就是原型用于在多个对象实例间共享属性和方法的原理。
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。
下面看一个案例:
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();
var person2 = new Person();
person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
console.log(person2.name); // zhangsan,来自原型
在这个案例中,person1 的 name 属性遮蔽了原型对象上的同名属性。虽然 person1.name 和person2.name 都返回了值,但前者返回的是"lisi"(来自实例),后者返回的是"zhangsan"(来自原型)。当 console.log()访问 person1.name 时,会先在实例上搜索个属性。因为这个属性在实例上存在,所以就不会再搜索原型对象了。而在访问 person2.name 时,并没有在实例上找到这个属性,所以会继续搜索原型对象并使用定义在原型上的属性。我们也可以通过hasOwnProperty()可以查看访问的是实例属性还是原型属性。
只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为 null,也不会恢复它和原型的联系。不过,使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
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();
var person2 = new Person();
// 通过hasOwnProperty()可以查看访问的是实例属性还是原型属性
console.log(person1.hasOwnProperty('name')); //false
person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
//只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性
console.log(person1.hasOwnProperty('name')); //true
console.log(person2.name); // zhangsan,来自原型
console.log(person2.hasOwnProperty('name'));//false
delete person1.name;
console.log(person1.name); // zhangsan,来自原型
console.log(person1.hasOwnProperty('name'));//false
这个修改后的案例中使用 delete 删除了 person1.name,这个属性之前以"lisi"遮蔽了原型上的同名属性。然后原型上 name 属性的联系就恢复了,因此再访问 person1.name 时,就会返回原型对象上这个属性的值。
3.2.原型与in操作符
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();
var person2 = new Person();
// 无论属性是在实例上还是在原型上,都可以检测到
console.log("name" in person1); // true
console.log("name" in person2); // true
// 判断一个属性是否是原型属性
function hasPrototypeProperty(object, name) {
//不在实例中但是可以访问到的属性属于原型属性
return !object.hasOwnProperty(name) && (name in object);
}
console.log(hasPrototypeProperty(person1, 'name'));//true
3.4.更简单的原型模式
在前面的案例中,每次定义一个属性或方法都会把 Person.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
4.组合模式
组合使用构造函数模式和原型模式。
构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.firends = ['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.firends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.firends); // [ 'zhangsan', 'lisi' ]
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true