创建对象的几种方式
创建对象有很多种方式,首先是最简单基本的两种方式:
①创建一个object实例
var o = new Object(); o.name = "a"; o.age = "12"; o.setName = function(){ alert(this.name); }
②对象字面量
var o = { name: "a", age: "12", setName: function(){ alert(this.name); } };
这两种方法创建单个对象是没什么问题,但很明显的,若需要创建大量对象,就会产生很多重复的代码。所以以下就讲一下能解决这个问题的7种模式。
一、工厂模式
工厂模式:用一个函数来封装创建对象的细节,返回创建的对象。
function createPerson(name, age){ var o = new Object(); o.name = name; o.age = age; o.setName = function(){ alert(this.name); }; return o; } var p1 = createPerson('a', 12); var p2 = createPerson('b', 23);
虽然工厂模式解决了创建多个相似对象的问题,但是却无法识别对象类型。
二、构造函数模式
function Person(name, age){ this.name = name; this.age = age; this.setName = function(){ alert(this.name); }; } var p1 = new Person('a', 12); var p2 = new Person('b', 23); alert(p1.constructor == Person);//true alert(p1 instanceof Object);//true alert(p1 instanceof Person);//true
它和工厂模式代码上的主要区别就在于它显式的创建了一个此构造函数的实例,并且还可以注意到构造函数以一个大写字母开头。
new创建实例的过程:
①创建一个新对象
②将构造函数的作用域赋给新创建的对象
③为新创建的对象添加属性
④返回新对象
这种模式下,实例就有了特定的类型,比如上面的代码中的p1,它的类型不再只是Object,还有Person。
以这种方式定义的构造函数定义在Global对象中。
但这种模式并不是没有缺点:在构造函数中创建的方法会在每个实例上重新创建一遍。在js中,方法即函数,也是一个对象,以上的创建方法就相当于:
function Person(name,age){ this.name = name; this.age = age; this.setName = new Function(" alert(this.name)"); } var p1 = new Person('a', 12); var p2 = new Person('b', 23); alert(p1.setName == p2.setName);//false
每创建一个实例,方法都会被创建一次,所以两个实例的方法并不是同一个方法,但其实两个方法要完成的是相同的任务,那可不可以把方法定义到构造函数外面呢?
function Person(name, age){ this.name = name; this.age = age; this.setName = setName; } function setName(){ alert(this.name); } var p1 = new Person('a', 12); var p2 = new Person('b', 23);
当然可以,但是定义在全局作用域中的函数就使我们创建的自定义引用类型不再具有封装性了,如果对象需要很多方法,那么就需要定义很多个全局函数。
三、原型模式
每个被我们创建的函数都有一个原型属性,它是一个指针,指向了一个原型对象,而这个原型对象包含了可以由特定类型的所有实例共享的属性和方法。很明显地,原型模式的好处:让所有对象实例可以共享它所包含的属性和方法。
function Person(){ } Person.prototype.name = 'a'; Person.prototype.age = '12'; Person.prototype.setName = function(){ alert(this.name); }; var p1 = new Person(); var p2 = new Person(); alert(p1.setName == p2.setName);//true alert(Object.getPrototypeOf(p1));//Object object alert(Object.getPrototypeOf(p1) == Person.prototype);//true
原型对象和构造函数还有实例的关系:
原型对象中有一个constructor属性指向构造函数,而构造函数中有一个prototype属性指向原型对象,实例中又有一个内部指针[[prototype]]指向原型对象。
因为constructor属性在原型对象中,所以这个属性是共享的,可以被对象实例访问。而[[prototype]]是一个内部指针,所有实现都无法访问到(但是在ff、safari、chrome中每个对象都支持一个属性__proto__,其他实现中,这个属性对脚本则是完全不可见的),只能通过isPrototypeOf()方法来确定对象之间是否存在这种关系或者用Object.getPrototypeOf(instance)返回[[prototype]]的值。
可以通过对象实例访问保存在原型中的值,但是却不能通过对象实例重写原型的值,只能屏蔽原型中的那个属性。
搜索过程如下:
每次代码读取一个对象的属性时,会进行一次搜索。先搜索实例对象里的属性,若没有,再去实例对应的原型对象中搜索。
若在实例中设置了和原型对象中同名的属性,那么即使再将实例中设置的同名属性设置为null,原型对象中的那个属性也将不再能被获取到,它会阻断我们访问原型中的哪个属性,但不会修改,除非使用delete操作符删除实例属性。
原型对象也可以用对象字面量的方式:
function Person(){} Person.prototype = { name: 'a', age: 12, setName: function(){ alert(this.name); } };
但是用这种方式相当于将原型对象重写了一遍,此时constructor就不再指向Person了,而是指向了Object,所以应该在对象字面量中设置下constructor属性(重新设置的constructor属性的[[enumerable]]会被设置为true)。
还需要注意的一点是,原本即便实例创建在原型之前也没有关系,但若用对象字面量就会出错:
function Person(){} var p1=new Person(); Person.prototype = { name: 'a', age: 12, setName: function(){ alert(this.name); } }; p1.setName();//error
因为实例对象的内部指针是指向原型对象的,而若在创建实例之后重写原型对象,那么原型对象就不是同一个了,这将会切断现有原型与任何之前已经存在的对象实例之间的联系。
原型对象的问题:
function Person(){} Person.prototype = { name: 'a', age: 12, friends : ['z', 'x'], setName: function(){ alert(this.name); }; }; var p1 = new Person(); var p2 = new Person(); p1.friends.push('q'); alert(p1.friends);//['z','x','q'] alert(p2.friends);//['z','x','q']
当对实例一的引用类型属性push值时,也会让实例二的属性也得到push的那个值。
四、组合模式
结合原型模式和构造函数模式,让构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。
function Person(name, age){ this.name = name; this.age = age; this.friends = ['z', 'x']; } Person.prototype.setName = function(){ alert(this.name); }; var p1 = new Person(); var p2 = new Person(); p1.friends.push('q'); alert(p1.friends);//['z','x','q'] alert(p2.friends);//['z','x']
这种模式最大限度地节省了内存,还支持向构造函数中传递参数,集两种模式之长。
五、动态原型模式
动态原型模式拥有上一个模式的优点并且更像类。
function Person(name, age){ this.name = name; this.age = age;
// 只有在第一次调用构造函数时会添加此函数 if(typeof this.setName != 'function'){ Person.prototype.setName = function(){ alert(this.name); }; } }
如果用了这种模式,那么就不能再使用对象字面量重写原型,因为会切断现有实例与新原型之间的联系。
六、寄生构造函数模式
寄生构造函数模式:创建一个构造函数,构造函数中封装了创建对象的代码,返回这个对象。
function Person(name, age){ var o = new Object(); o.name = name; o.age = age; o.setName = function(){ alert(this.name); }; return o; } var p1=new Person('a', 12);
这个模式一般用于创建一个具有额外方法的引用类型:
function SpecialArray(){ var values = new Array(); values.push.apply(values, arguments); values.toPipedString = function(){ return this.join('|'); }; return values; } var color = new SpecialArray('red', 'green');
alert(color instanceof SpecialArray); // false alert(color.toPipedString());//red|green
这个模式返回的对象与构造函数或者构造函数的原型属性之间没有关系,建议在可使用其他模式的情况下,尽量不使用这种模式。
七、稳妥构造函数模式
稳妥构造函数模式:适合在安全的环境中,防止数据被其他应用程序改动,它不能使用this和new。这种模式创建的对象和构造函数之间也没有什么关系。
function Person(name, age){ var o = new Object(); var name = name; o.sayName = function(){ alert(name); }; return o; } var p1 = Person('a',12); p1.sayName();//a
这个模式和寄生构造函数模式一样,返回的对象与构造函数或者构造函数的原型属性之间也没有关系。