前言
在之前我们所了解的创建对象的方法,无非就是字面量方式或者构造函数创建对象。
当我们使用字面量方式来创建对象时,
我们来思考一下,假如我们要创建一个学生类型的对象,属性有姓名、年龄、性别这三个简单的属性。
var student1={
name:'xiaoming',
age:20,
gender:'male'
}
可以看到,创建一个学生,我们只需要5行代码就能创建一个对象了。但是如果我们要同时创建一个班级的学生,需要多少行?
var student1={
name:'xiaoming',
age:20,
gender:'male'
}
var student2={
name:'xiaoming',
age:20,
gender:'male'
}
....
显而易见的,当我们使用字面量方式来创建多个对象就会很麻烦。
当我们使用构造函数方式来创建对象时,我们先来试着用这种方式来创建一些对象。
var student1=new Object();
student1.name="xiaoming";
student1.age=20;
student1.gender="male";
var student2=new Object();
student2.name="xiaoming";
student2.age=20;
student2.gender="male";
var student3=new Object();
student3.name="xiaoming";
student3.age=20;
student3.gender="male";
var student4=new Object();
student4.name="xiaoming";
student4.age=20;
student4.gender="male";
我们可以明显地观察到,即使我们已经尽量使创建对象的过程规范化,但是代码看起来还是有些"混乱"。
我们比较难观察到每一个对象的整体,这就是使用构造函数方式创建对象的缺点。
为了解决字面量方式创建对象时代码量大的问题和使用构造函数创建对象时代码不像一个"整体"的问
题,我们来学习一些新的创建对象方式。
一、工厂模式
我们首先来看一下第一种方式,工厂模式。
何为工厂模式,显而易见,工厂就是批量生产的意思,我们只需要打造一个模板,就可以快速地生产产品。
工厂模式创建对象就是根据这种模式来实现的,当我们要创建若干个同类型的对象时,采用工厂模式的方式来创建对象就很方便。
比如我们上面所说的,我们要创建50个学生类型的对象,属性为姓名、年龄、性别。
function createPerson(name, age, gender) {
var student = new Object();
student.name=name;
student.age=age;
student.gender=gender;
return student;
}
如你所见,这是一个函数,我们在函数内部创建一个空对象student,我们要给函数传入参数name,age,gender。
我们传入参数后就通过点方式给student对象创建属性,并将这个带有属性的student对象返回。
即我们将创建对象的代码封装在一个函数中,我们将这样一个函数称为"工厂函数"。当我们要创建一个学生类型的对象时,只需要调用工厂函数,将对应的属性值传入工厂函数,就能得到一个student对象。
var student1=createPerson('xiaoming',20,'male');
相当于:
var student1={
name:'xiaoming',
age:20,
gender:'male'
}
var student2=createPerson('xiaohong',20,'famale');
相当于:
var student2={
name:'xiaohong',
age:20,
gender:'famale'
}
从上面的代码我们可以看到,我们只需要在工厂函数中传参数,工厂函数就能给我们返回一个学生类型的对象。
十分方便,对比之前的方式代码量少,而且整体性很强。
那么工厂模式创建对象有没有什么缺点吗?
有的,工厂模式创建对象本质上相当于构造函数方式创建对象,只是将创建函数的过程进行了封装。
而且我们使用工厂模式创建对象时,我们只知道我们创建的对象是一个对象实例。
这里就设计到一些概念,我们不进行深入了解,只需要知道不仅仅可以通过Object来创建实例,也有别的Person、Animal、…等其他构造函数来创建实例。
所以使用工厂模式创建对象,对于所创建的对象,我们无法确定这个对象是哪个构造函数的实例,我们只知道它是一个对象。这句话确实不好理解,我所理解的就是
function createPerson(name, age, gender) {
var student = new Object();
student.name=name;
student.age=age;
student.gender=gender;
return student;
}
从以上可以看出student对象其实是Object构造函数的实例,我们无法用这个工厂函数创建出其他类型的实例对象,只能创建出Object构函数的实例对象,这就是工厂函数的缺点。如果我的理解是错的,可以在下方评论。工厂模式的缺点可以通过下一种方式来解决。
二、构造函数模式
我们可以通过构造函数模式的方式来创建对象。
在JavaScript当中,我们可以自定义一个构造函数,构造函数本身就是一个函数,只不过可以用来创建对象,我们可以自定义对象类型的属性和方法。
构造函数模式与工厂模式相比,有很大的变化,首先构造函数模式不再有显示创建对象的过程了,并且也没有返回值。
构造函数的函数名首字母一般采用大写的形式,这不是强制的,只是一种规范,有助于我们区分普通函数和构造函数。
我们来写一个简单的自定义构造函数Person
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
this.sayName=function(){
console.log(this.name);
}
}
var p1=new Person('Curry',30,'male');
这就是一个简单的自定义函数,并且我们使用Person构造函数来创建了一个Person实例p1。
console.log(p1);
Person {
name: 'Curry',
age: 30,
gender: 'male',
sayName: [Function (anonymous)]
}
可能有人无法理解这种方式是怎么创建对象的,因为它并没有创建对象的过程,也没有给对象创建属性。
这就是因为构造函数Person前的new操作符的神奇之处了。
在JavaScript中使用new操作符调用构造函数,
会在内存之中生成一个新对象。
构造函数内部的this指向指向新对象。
并且除非我们return一个非空的对象,否则会自动返回刚创建的对象。
我们试着来理解一下
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
this.sayName=function(){
console.log(this.name);
}
}
var p1=new Person('Curry',30,'male');
首先,生成一个新对象。
我们可以理解为
var p1=new Person();
只不过它是隐式的创建对象。
其次this指向新创建的对象也就是p1
那么
this.name=name;
this.age=age;
this.gender=gender;
this.sayName=function(){
console.log(this.name);
}
我们可以看成
p1.name=name;
p1.age=age;
p1.gender=gender;
p1.sayName=function(){
console.log(this.name)
}
是否理解了呢?
最后,我们并没有return一个非空对象,所以返回创建的对象。
那么构造函数模式创建对象有没有问题呢?
有的,我们再来看一下构造函数创建对象的代码
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
this.sayName=function(){
console.log(this.name);
}
}
var p1=new Person('Curry',30,'male');
var p2=new Person('Klay',30,'male');
我们要知道,使用构造函数来创建实例时,其定义的方法会在每个实例上都创建一遍,对于p1,p2来说,
他们都有一个sayName方法。虽然这两个方法的名字是一样的,但是它们并不是同一个Function实例,
我们所了解的,在ECMAScript中函数也是对象,所以我们每次定义函数时,都会初始化一个对象。我们
每使用Person构造函数创建一个Person实例,都会生成一个Function实例,如果我们创建40个Person实
例呢?是不是也会生成40个Function实例呢?每个 Person 实例都会有自己的 Function 实例用于显示
name 属性,既然都是做同样的事情,我们没必要定义n个不同的Function实例。要解决这个问题,我们
可以将函数定义转移到构造函数外部。
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
this.sayName=sayName;
}
function sayName() {
console.log(this.name);
}
var p1=new Person('Curry',30,'male');
var p2=new Person('Klay',30,'male');
这样看貌似没有问题了,即使我们创建n个Person实例也不会生成多个Function实例了。
但是真的是这样吗?将函数定义转移到了构造函数外部,虽然解决了相同逻辑的函数重复定义的问题,
但是严格来说,sayName这个函数只有我们的Person实例才可以调用吧,将函数定义在外部是不是其他
对象也可以调用?这就导致了全局作用域的混乱。如果我们的构造函数有多个方法,那么是不是就要在
全局作用域中定义多个函数,这样就会很混乱,导致代码不能很好地聚集一起。
三、原型模式
每个函数都有一个prototype属性指向它的原型对象,构造函数当然一样。原型对象上的属性和方法可以被特定的引用类型的实例共享。构造函数创建的实例也可以共享构造函数的原型对象上的属性和方法,所以我们将原本写在构造函数中的属性和方法赋值给它的原型对象,那么构造函数的实例就能够共享这些属性和方法了,与构造函数模式不同,原型模式定义的属性和方法是所有实例共享的,不同的实例访问的同名方法其实是同一个,这样就避免了每个实例要创建一个具有同样功能的方法。
function Person(){}
Person.prototype.name = "Curry";
Person.prototype.age = 30;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); // Curry
var person2 = new Person();
person2.sayName(); // Curry
console.log(person1.sayName === person2.sayName); // true
可以看到Person构造函数创建的两个实例person1和person2访问到的属性和方法其实是相同的,或者原型模式下的所有实例对象访问的同名属性和方法就是同一个。
那么这样问题就浮出水面了,原型模式创建实例的过程是不需要我们传入参数的,所以我们所有的实例都默认取得了相同的属性。那我们怎么让实例拥有自己的值呢?
可能有的同学会想到了一种方法,点访问创建属性。
function Person() { }
Person.prototype.name = "Curry";
Person.prototype.age = 30;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Klay";
console.log(person1.name); // Klay,来自实例
console.log(person2.name); // Curry,来自原型
可以看到person1和person2的同名属性name输出的结果是不一样的,这是为什么呢?
这就涉及到了一个概念了,原型链。
什么叫原型链呢?
假设我们使用构造函数Object创建出一个构造实例Animal,再用Animal构造函数创建出一个Dog构造实例,再用Dog构造函数创建
一个Keji实例,那么这里就有一条很长的原型链了。
当Keji实例要访问一个属性时,会先在它本身上搜索这个属性,如果它本身没有这个属性就会沿着指向原型对象的指针前往Dog原型对象寻找该同名属性,如果Dog原型对象还找不到这个属性,则继续向上搜索,前往Animal原型对象搜索这个同名属性,如果Animal原型对象还搜索不到这个属性,则最后前往Object原型对象搜索,如果Object原型对象也没有这个属性,则返回Null或者Undefined。从要搜索的实例开始,沿着创建它的构造函数的原型对象向上的这条线路,称为原型链。
这个只是我所理解的原型链,也许不准确。这里不过多讲解原型链的知识了。
继续看为什么person1和person2访问的同名属性name为什么不一样呢?
这是因为如果沿着原型链搜索时,如果实例本身存在我们搜索的属性时,搜索就停止了,不会继续向上搜索了。这就叫属性"遮蔽",因为实例的name属性遮蔽了原型对象的name属性,既然可以在p1找到name属性,那么也就没有必要向上搜索了,直接返回p1的name。
原型模式也不是没也问题的,原型模式弱化了向构造函数传递参数的能力,这就导致了所有实例默认取得了相同的属性值。这个不是主要的问题,如果这个属性是原始类型的属性还可以使用上面的方法,点方式赋值,遮蔽掉原型的同名属性。主要问题是包含引用类型的属性。
function Person() { }
Person.prototype = {
constructor: Person,
name: "Curry",
friends: ["Klay", "Dream"],
sayName() {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("KD");
console.log(person1.friends); // [ 'Klay', 'Dream', 'KD' ]
console.log(person2.friends); // [ 'Klay', 'Dream', 'KD' ]
console.log(person1.friends === person2.friends); // true
这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建了两个Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
四、组合模式
组合模式创建对象综合了构造函数模式和原型模式的优点,解决了两种模式的问题。
组合模式即在构造函数上定义实例属性,那么在创建对象上只需要传入这些参数。在原型对象用于定义方法和共享属性。
组合模式解决了前三种模式的问题,综合了三种模式的优点。
组合模式是目前在ECMAScript中使用得最广泛、认同度最高的一种创建对象的方法。
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.hobby=['basketball','golf'];
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name);
}
};
var p1 = new Person('Curry', 20, 'male');
var p2 = new Person('Klay', 20, 'male');
p1.hobby.push('football');
console.log(p1.hobby); // [ 'basketball','golf', 'football' ]
console.log(p2.hobby); // [ 'basketball','golf']
console.log(p1.hobby=== p2.hobby); // false
console.log(p1.sayName === p2.sayName); // true
如有问题欢迎在下方评论指正。