三种创建对象的模式深入理解对象
虽然使用Object构造函数或字面量形式可以很方便的创建对线,但存在着明显的不足:
创建具有同样接口的多个对象需要重复编写很多代码
a.用字面量创建对象
var person = {
name: "zhangsan",
age: 18,
gender: 'male',
sayName: function(){
console.log(this.name);
}
}
缺点:用字面量的方式来创建对象,最大的缺点就是,这个对象是一次性的,如果有四十个同学,这个代码就要写四十次,有点小麻烦。
b.用构造函数的形式创建对象
var person = new Object();
//为这个实例化的对象添加属性
person.name = "zhangsan";
person.age = 18;
person.gender = 'male';
person.sayName = function(){
console.log(this.name)
}
缺点:可以发现它是先实例化了一个对象,然后再为对象添加属性,这样就看不出来是个整体(像上面的用字面量来创建,属性都包在一个大括号里面,这样就很好看出这是个整体)。
1.javaScript工厂模式
工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。工厂模式是一种创建型模式,简单来说,工厂模式就是创建对象的一种方式。
//将创建对象的代码封装在一个函数中
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');
-
工厂模式有什么用?
作用:创建对象;降低代码冗余度。
应用场景:当你想要批量生产同种类的对象的时候;比如,你想生成一个班级的40个学生,每个学生都有姓名、年龄等特征。这时候你创建一个“工厂”,把信息丢到工厂里,工厂就给你造一个人出来,非常方便。
-
工厂模式的优缺点
优点:只要我们往工厂函数里面塞参数,工厂函数就会像生产产品一样造个人出来。
缺点:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。
2.构造函数模式
JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。
// 自定义构造函数
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 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。
-
创建Person实例
var person1 = new Person('zhangsan', 29, 'male'); var person2 = new Person('lisi', 19, 'female');
创建Person的实例时,应使用new操作符。以这种方式调用构造函数会有如下特性。
(1)在内存中创建一个对象
(2)这个对象内部的
__proto__
特性被赋值为构造函数的prototype属性(3)构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
(4)执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个
constructor
属性,这两个对象的constructor
属性指向 Person,console.log(person1.constructor === Person); // true console.log(person2.constructor === Person); // true
-
构造函数也是一个函数
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') global.sayName() //lisi // 在另一个对象的作用域中调用 var o = new Object() Person.call(o,'wangwu',25,'male') o.sayName() //wangwu
这个案例展示了,当我们对构造函数使用new操作符,即创建了一个对象,发挥了构造函数的作用。
当我们没有使用new操作符调用Person(),结果会将属性和方法添加到全局对象。这里,在调用一个函数但没有明确设置它的this指向(即没有作为对象的方法调用,或者没有使用call()、apply()、bind()),this的指向始终为global对象(在浏览器中就是window对象)。因此当我们调用了函数后,global上面就会有一个sayName()方法,调用返回传入的name属性为‘lisi’。
最后我们通过call()方法,将对象o的this指向指定为Person()内部的this值,所有属性和sayName()方法又将添加到对象o上。
-
构造函数的问题
function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; this.sayName = new Function("console.log(this.name)"); // 逻辑等价 } var person1 = new Person() var person2 = new Person() console.log(person1.sayName === person2.sayName); // false
从上面案例可以看到,虽然两个实例同名却不相等,是因为每个实例创建方法时,都会创建一个自己实例的方法,以这种方式创建函数会带来不同的作用域链和标识符解析。但是都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。
3.原型模式
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。使用原型对象的好处在于,在原型上定义的属性和方法可以被对象实例所共享。原来在构造函数中直接给对象实例赋值,而在原型中可以直接将值赋给它们的原型。
function Person(){}
Person.prototype.name = 'zhangsan'
Person.prototype.age = 29
Person.prototype.gender = 'male'
Person.prototype.sayName = function(){
console.log(this.name);
}
// 创建Person实例
var person1 = new Person()
person1.sayName() //zhangsan
var person2 = new Person()
person2.sayName() //zhangsan
console.log(person1.sayName === person2.sayName); //true
上述代码中所有属性和sayName()方法都添加至Person的prototype属性中。这种模式下定义的属性和方法都是Person实例所共享的,因此person1和person2都有相同的属性和相同的sayName方法
更简单的原型模式
在上面案例中,每次定义一个方法都要写一遍Person.prototype。为减少代码冗余,也为从视觉上更好的体现封装原型的功能,直接通过一个包含所有属性和方法的对象字面量来重写原型。如下
function Person(){}
// 使用字面量形式封装原型
Person.prototype ={
name:'zhangsan',
age:'20',
gender: 'male',
sayName:function(){
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
解决这个问题?
可以在重写原型对象时,专门设置constructor的值;
但是这种方法会创建一个[[Enumerable]]为true的属性。而原生constructor属性默认是不可枚举的。
function Person(){}
Person.prototype ={
// 这种方法恢复constructor属性会创建一个[[Enumerable]]为true的属性
constructor:Person,
name:'zhangsan',
age:'29',
gender:'male',
sayName(){
console.log(this.name);
}
}
var person1 = new Person()
console.log(person1.constructor == Person);//true
console.log(person1.constructor == Object);//false
-
原型模式的问题
原型模式的问题在于弱化了构造传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。再就是所有属性在实例之间是共享的,对于方法而言比较合适,另外对于包含原始值的属性影响也不大,毕竟可以通过在实例上添加同属性名来简单地遮蔽原型上的属性。真正的问题在于引用值的属性。
function Person(){} Person.prototype = { constructor:Person, name:'zhangsan', friend:['lisi','wangwu'], sayName:function(){ console.log(this.name); } } var person1 = new Person() var person2 = new Person() person1.friend.push('zhaoliu') console.log(person1.friend);//[ 'lisi', 'wangwu', 'zhaoliu' ] console.log(person2.friend);//[ 'lisi', 'wangwu', 'zhaoliu' ] console.log(person1.friend === person2.friend);//true
上述代码中,创建两个Person的实例。person1.friend通过push方法向数组中添加了一个字符串,由于这个friend属性存在于Person.prototype上,新加入的字符串也会在实例person2.friend中显示出来。但一般来说,不同的实例应该有属于自己的属性副本。这也是实际开发中不单独使用原型模式的原因。
4.组合模式
组合模式指组合使用构造函数和原型模式
构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式目前在ECMAScript中使用最为广泛,认同度最高的一种创建自定义类型的方法。
// 组合模式 function Person(name,age,gender){ this.name = name this.age = age this.gender = gender this.friends = ['zhangsan','lisi'] } Person.prototype ={ constructor:Person, sayName:function(){ console.log(this.name); } } var p1 = new Person('larry',44,'male') var p2 = new Person('tarry',39,'female') p1.friends.push('robinson') console.log(p1.friends);//[ 'zhangsan', 'lisi', 'robinson' ] console.log(p2.friends);//[ 'zhangsan', 'lisi' ] console.log(p1.friends === p2.friends); // false console.log(p1.sayName === p2.sayName); // true