在JS中创建对象的方式
在JS中创建对象有很多种方法,而创建自定义类型的最常见的方式,就是使用组合使用构造函数模式和原型模式创建对象。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,那么来看看为什么这种方式是最常用的。
先简单介绍在JS中创建对象的方式有如下几种:
- 工厂模式
- 构造函数模式
- 原型模式
- 组合使用构造函数模式和原型模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
依次来看:
- 工厂模式
这种模式就是抽象了创建具体对象的过程,也是最基本的一种设计模式,就像下面这样咯:
function createPerson(name,age,gender){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.gender = gender;
obj.sayName = function(){
alert(this.name);
};
return obj;
}
//接下来就可以创建对象了
var person = createPerson("Stan",0000,"male");
如果创建多个这种类似的对象,当然很ok啦,但是有更好的模式创建对象。
2. 构造函数模式
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){
alert(this.name);
};
}
//然后可以用new操作符来创建Person的新实例
var person = new Person("Stan",0000,"male");
//最直观的就是代码比工厂模式少吧。
//另外,创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型,这是构造函数模式胜过工厂模式的地方(努力理解中。)
//也可以像下面这种创建并调用
Person("Stan",0000,"male");
window.sayName();
//或是在另一个对象的作用域中调用
var obj = new Object();
Person.call(obj,"Stan",0000,"male");
obj.sayName();
//这里是在obj对象的作用域中调用Person(),因此调用后obj就拥有了所有属性和sayName()方法
这里说说构造函数模式的问题,定义在构造函数中的方法在每次实例化的时候都会被创建一次,并且每次被创建的方法都是一个新的对象(JS中函数即对象),即创建两个完成同样任务的Function实例是没有必要的,也就是说,如果一个方法可以被共享使用的话,不应该这么做。如果写成下面这样:
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
这样把sayName()定义成全局函数,虽然解决了多个函数做同一样件事情而不用每次创建的问题,但是假如需要N个全局函数,那么我们这个自定义的引用类型就没有丝毫的封装性可言了。所以有更好的原型模式可以解决这个问题
3.原型模式
我们创建的每个函数都 一个prototype属性,这个属性是一个指针,指向一个对象(原型对象),使用原型对象的好处是不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。就像下面这样:
function Person(){}
Person.prototype.name = "Stan";
Person.prototype.age = 0000;
Person.prototype.gender = "male";
Person.prototype.sayName = function(){
alert(this.name);
}
var person = new Person();
person.sayName();
//原型对象中的所有属性和方法都是可以被实例所共享的
当我们在调用person.sayName()方法时,会先后执行两次搜索,先从对象实例本身开始,如果在实例中找到该方法,则调用 ,若没有找到,会继续搜索指针指向的原型对象,找到则调用方法。有一个问题,如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,该属性将会屏蔽掉原型中的那个属性,就像下面这样:
function Person(){}
Person.prototype.name = "Stan";
var person = new Person();
person.name = "Joe";
alert(person.name);//结果是Joe
即使将这个name属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接 。可以使用delete操作符完全的删除实例属性,从而可以重新访问到原型中的属性。像下面这样:
function Person(){}
Person.prototype.name = "Stan";
var person = new Person();
person.name = null;
alert(person.name);//结果是null,而不是Stan
//可以这样做:
delete person.name;
alert(person.name);
另外还可以把原型语法像下面这样写:
function Person(){}
Person.prototype = {
name : "Stan",
age : 0000,
gender : "male",
sayName : function(){
alert(this.name);
}
};
这种写法实际上是重写了原型对象,所以接下来看一个问题,即原型的动态性
所谓的原型的动态性,即随时可以为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,像下面这样:
function Person(){}
var person = new Person();
Person.prototype.name = "Stan";
alert(person.name);
虽然person实例是在添加新属性之前创建的,但是仍然可以立即在实例中访问到name属性,但是如果全部重写了原型对象,就会出问题了,像下面这样:
function Person(){}
var person = new Person();
Person.prototype = {
name : "Stan",
age : 0000,
gender : "male",
sayName : function(){
alert(this.name);
}
};
alert(person.name);//undefined
这是为什么呢?因为重写原型切断了现有原型(重写后的原型)与任何之前已经存在的对象实例之间的联系,person引用的仍然是最初的原型,这里person实例最初的原型中除了默认的一些属性外,是没有name属性的,所以就会undefined咯!
原型对象看似还可以,但它也是有问题的,什么问题呢,就是其共享的本性,分析下,原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性也还行,因为我们还可以通过在实例上添加一个同名属性来隐藏掉原型中的对应属性(不会影响到其它的实例的属性),但是如果包含引用类型值的属性来说,问题就来了,看下面:
function Person(){}
Person.prototype = {
colors : ["red","green","pink"]
};
var person1 = new Person();
person1.colors.push("black");
var person2 = new Person();
alert(person2.colors); // red,green,pink,black
看到问题了吧,大多数时候,实例一般都是要属于自己的全部属性的,即我们不会这么单独使用原型模式,所以这才到今天我们要说的主题:组合使用构造函数模式和原型模式创建对象
怎么组合呢,其实就是用构造函数模式定义实例属性(不会被共享),而用原型模式用于定义方法和共享的属性,另外这种组合模式还支持向构造函数传递参数,像下面这样:
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.colors = ["red","green","pink"];
}
Person.prototype = {
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Stan",0000,"male");
var person2 = new Person("Joe",1111,"female");
person1.colors.push("black");
alert(person1.colors); // red,green,pink,black
alert(person2.colors); // red,green,pink
alert(person1.sayName == person2.sayName); // true
-
动态原型模式
动态原型模式把所有信息都封装到构造函数中,而通过在构造函数中初始化原型(仅在必要的条件下),又保持了同时使用构造函数和原型的优点。即可以检查某个应该存在的方法是否有效,来决定是否需要初始化原型,看下面:
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
这里需要说明的是if里代码只会在初次调用构造函数时才会执行,当然了,下次调用构造函数时,this.sayName已经存在嘛,而且if里对原型所在的修改会立即在所有实例中得到反映,这里可能就是体现其动态的所在之处。同样,这里要记住不能使用对象字面量重写原型,上篇中已提到过,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
-
寄生构造函数模式
先看下面的代码:
function SpecialArray(){
var array = new Array();
array.push.apply(array,arguments);
array.toPipedString = function(){
return this.join("|");
};
return array;
}
var colors = new SpecialArray("red","green","pink");
alert(colors.toPipedString());// red|green|pink
alert(colors instanceof SpecialArray); // false
详细说明下,我们知道当我们自定义一个构造函数后,使用new的方式来创建一个对象时,默认会返回一个新对象实例,构造函数中是没有return 语句的。而这里所谓的寄生构造函数,基本思想是创建一个函数,这个函数的作用仅仅是为了某一个特定的功能而添加一些代码,最后再将这个对象返回,除了使用了new操作符并把包装的函数叫做构造函数外,这个模式跟工厂模式没有任何区别。另外,这个SpecialArray()返回的对象,与SpecialArray()构造函数或者与构造函数的原型对象之间没有任何关系,就像你在SpecialArray()外面创建的其他对象一样,所以如果用instanceof操作符来检测的话,结果只能是false咯。所以这是它的问题
-
稳妥构造函数模式
先说稳妥二字,别人定义了一个稳妥对象,即没有公共属性,而且其方法也不引用this对象,这种模式适应于一些安全环境中(禁止使用this和new),或防止数据被其他应用程序改动,像下面这样:
function Person(name,age,gender){
var obj = new Object();
obj.sayName = function(){
alert(name);
};
return obj;
}
var person = Person("Stan",0000,"male"); // 这里没有使用new操作符
person.sayName(); // Stan
alert(person instanceof Person); // false
说明一下,这里person中保存了一个稳妥对象,除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或属性,但也不可能有别的办法访问传入到构造函数中的原始数据 。同样与寄生函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有任何关系.
13:19:27 2017-08-28