自定义创建对象的几种方法

对象是JS的核心,JS中的所有的引用类型都是继承自Object对象的,想要更好的掌握JS的精髓,那么首先就需要更好的理解对象。这里就对象的创建进行整理分析。

对象的创建方法:

1:使用object构造函数实例化一个对象


var newObject = new Object();
newObject.a = "aaa";


2:使用对象字面量创建的对象


var newObject = {};
newObject.a = "aaa";


或者也可以在字面量时直接添加方法和属性,例如:


var newObject = {a:"aaa",b:"bbb"};


通过把object构造函数进行实例化,和通过对象字面量创建的对象,创建对象,可以直接添加新对象的属性和方法。但是有一个缺点就是,这样只能创建单个对象,如果想要再创建一个类似的对象,就要重复执行上述的创建对象的过程,会产生大量的重复代码。所以为了能解决这个问题,就出现了更多的创建对象的方法。

3:工厂模式创建对象

工厂模式是软件工程领域一种常用的设计模式。关于设计模式的方法这里不做研究。

工厂模式创建对象的实例:


function createObject(name,age,sex){
    var o = new Object();
    
    o.name = name;
    o.age = age;
    o.sex = sex;
    o.sayName = function(){
	return this.name;
    }
    return o;
}
var persons = createObject("zhang",28,"male");
var persons1 = createObject("ling",28,"male");
console.log(persons.name+"--"+persons1.name);


这样,如果我们需要创建一系列的相同类型的对象,就可以直接调用封装好的函数即可。

4:构造函数模式

构造函数模式也是一种常见的设计模式。

构造函数模式创建对象示例代码:


function createObject(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayName = function(){
	return this.name;
    }
}
var persons = new createObject("zhang",28,"male");
var persons1 = new createObject("ling",28,"male");
console.log(persons.name+"--"+persons1.name);         //zhang--ling


构造函数也是函数的一种,所以也可以直接使用,在直接使用时,this是指向window对象的,所以添加的属性就会被添加到window对象上面。这个要特别注意。

比如如下的方法使用上述的构造函数:


var persons = new createObject("zhang",28,"male");
var persons1 = createObject("ling",28,"male");
console.log(persons.name+"--"+persons1.name);


此时因为persons1因为没有name的方法,会抛出错误。

为了能确保构造函数被正确的实例化,那么可以通过使用作用域安全的构造函数创建对象。

5:作用域安全的构造函数模式:


function createObject(name,age,sex){
    if(this instanceof createObject){
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.sayName = function(){
	    return this.name;
	};
    }else{
	return new createObject(name,age,sex);
    }
}
var persons = new createObject("zhang",28,"male");
var persons1 = createObject("ling",28,"male");
console.log(persons.name+"--"+persons1.name);


通过检测当前的作用域是否正确,从而进行不同的操作,防止了作用域出现问题,导致的代码抛出错误。可以解决构造函数模式出现的问题。

虽然这样的方法已经可以比较好的实现了对象的创建,但是在使用构造函数创建对象时,每一个方法都需要在实例上重新创建一遍,比如上述代码中的sayName方法,这个方法在所有的实例中,只是一段逻辑显示,它可以是共有的。但是通过上面的方法创建的实例,它们每一个实例都包含了sayName方法,这样也降低了代码的重复利用。

从下面的方法,可以证明不同实例中的sayName方法是不同的。


console.log(persons.sayName == persons1.sayName);   //false


那么如何解决这个问题呢,最简单的方法,可以把sayName的方法指向另外一个独立的函数,在使用sayName方法时,去调用这个函数,写成代码就是:


function createObject(name,age,sex){
    if(this instanceof createObject){
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.sayName = sayName;
    }else{
	return new createObject(name,age,sex);
    }
}
function sayName(){
    return this.name;
}
var persons = new createObject("zhang",28,"male");
var persons1 = createObject("ling",28,"male");
console.log(persons.sayName == persons1.sayName);     //true


这样,sayName方法的实现逻辑就不会存在于每个实例中了,实现了方法的共用。

这样也有一个缺点就是,如果如果方法有很多,那么就要定义很多的全局函数,这无疑也是对代码的性能有些许的影响(要尽量的少使用全局变量(代码优化–避免全局变量)),那么下面的方法就可以解决这个问题。

6:原型模式:

原型模式也是一个很重要的设计模式。在创建的每一个函数都会有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处就是,可以让所有的实例共享它所包含的属性和方法。


function createObject(){
}
createObject.prototype.name = "zhang";
createObject.prototype.age = 28;
createObject.prototype.sex = "male";
createObject.prototype.sayName = function(){
    return this.name;
}
var persons = new createObject();
var persons1 = new createObject();
console.log(persons.sayName == persons1.sayName);   //true


这个时候,同样存在一个作用域的问题,就是如果我们不小心把new给落下,那么代码会抛出错误,例如:


var persons = new createObject();
var persons1 = createObject();
console.log(persons.sayName == persons1.sayName);   //抛出了错误。


依然使用相同的方法,把作用域安全话。


function createObject(){
    if(!(this instanceof createObject)){
	return new createObject();
    }
}
var persons = new createObject();
var persons1 = createObject();
console.log(persons.sayName == persons1.sayName);   //true


如此即可。

当一个实例被初始化时,它的方法和属性都是固定的,都是原型链上的内容。这肯定不是我们想要的,这样我们就需要对实例中的属性进行变换。可以如此:


persons.name = "ling";


如此这样,则实例persons中添加了一个name属性,它的值是“ling”,这个值并不是说把原来的处于原型链上的name的值改变了,而是在实例中,重新添加了属性该实例的一个新的属性。

我们可以这样证明,原来的处于原型链中的name值是依然存在的:


persons.name = "ling";
console.log(persons.name);     //ling


此时,因为定义了实例中的属性,原型链中的属性被覆盖。这里显示的内容值为:ling


delete persons.name;
console.log(persons.name);     //zhang


//把实例中的属性删除,再次进行取值,显示为原型链中的值:zhang

从这里,我们也可以看出几个问题:

1:实例中对已有属性进行重新赋值,不会影响原型链中的同名属性的属性值。

2:在定义了实例中的值之后,使用实例进行取值时,优先获取实例中的属性,如果实例中没有找到想要的属性,则会使用原型链中的属性值。

3:实例中的属性是可以被删除的,原型链中的属性是不可被删除的。

对于一个属性,如何才能确定,这个属性是存在于实例中,还是存在于原型链中?

这就要使用一个hasOwnProperty()方法,这个方法定义在Object对象的prototype中,构造函数的对象是继承自Object方法的,而基于构造函数的实例,是继承自构造函数的,所以都存在这个方法。


console.log(persons.hasOwnProperty("name"));     //ture/false


当存在于实例中时,则会返回true,反之为false

这样也只能判断是不是存在于实例中,那么如何判断一个属性只存在于原型链中呢?那就要用到in方法了,不过in方法对于存在实例和原型链中的都会返回true,所以这里记得借助之前判断是否存在于实例中的方法同时使用进行判断了。


console.log(!persons.hasOwnProperty("name") && "name" in persons); //true/flase


当只存在于原型链中时,返回true,否则返回为false。至于当返回fasle时,是为何,就得按照情况再做判断了。

当然啦,原型链使用的时候,会出现原型链断裂的情况,这个情况由几种情况导致,参考:

同时,原型链因为其所有方法的共有性,也存在问题,比如当原型链的取值是引用类型,就会出现问题了。

例如:


function createObject(){
    if(!(this instanceof createObject)){
	return new createObject();
    }
}
createObject.prototype.name = "zhang";
createObject.prototype.age = 28;
createObject.prototype.sex = "male";
createObject.prototype.friends = ["andy","lina","luna"];
createObject.prototype.sayName = function(){
    return this.name;
}
var persons = new createObject();
var persons1 = new createObject();
persons.friends.push("lion");
console.log(persons.friends);    //andy,lina,luna,lion
console.log(persons1.friends);   //andy,lina,luna,lion


这样就是已经出问题了,这个主要是因为,数组是引用类型的一种,引用类型是存放在堆内的,而上述代码中,在原型链上的friends属性,保存的只是一个连接,当访问这个属性时,就会去堆中寻找对应的内容。说白了就是因为,原型链中的引用类型,一般都是包含有很多的数据,在我们使用的操作时,通常只是去修改这个数据,修改数据不会在实例中添加同名的属性,所以依旧是修改的原型链中的数据,原型链的公用性,就导致了,这个修改会反映到所有基于该对象的实例中去。

下面我们看一下这个例子:


persons.friends.push("lion");
console.log(persons.hasOwnProperty("friends"));    //false
console.log(!persons.hasOwnProperty("friends") && ("friends" in persons)); //true


从上面的代码中,可以看出,persons实例中,并没有friends属性,而是依然使用的原型链中的friends属性的。再看下面的方法。


persons.friends = ["lion"];
console.log(persons.hasOwnProperty("friends"));  //true
console.log(!persons.hasOwnProperty("friends") && ("friends" in persons));//false
console.log(persons.friends);    //lion
console.log(persons1.friends);   //andy,lina,luna,lion


这段代码,我们可以看出,persons的实例中,存在了friends属性了,所以经过实例中的属性值变化,不会影响到基于该对象的其他实例。

从上面我们也可以了解到,这个问题出现的原因就是,原型链的共享特性导致的,但是之所以使用原型链,就是为了使用它的共享特性,这个不就会产生冲突了吗?为了能解决这个问题,也就出现了下一种创建对象的方法:组合使用构造函数模式和原型模式。

7:组合使用构造函数模式和原型模式

这种创建对象的方法,是创建自定义对象的最常见的方法。构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。这样,每个实例都会有自己的一份实例属性的副本,又同时共享着对方法的引用,最大限度的节省了内存,这种模式还支持向构造函数传递参数,可谓是一种很好的创建对象的方法。


function createObject(name,age,sex){
    if(this instanceof createObject){
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.friends = ["andy","lina","luna"];
    }else{
	return new createObject(name,age,sex);
    }
}

createObject.prototype.sayName = function(){
    return this.name;
}
var persons = new createObject("zhang",28,"male");
var persons1 = new createObject("ling",28,"male");
persons.friends.push("lion");
console.log(persons.friends);      //andy,lina,luna,lion
console.log(persons1.friends);     //andy,lina,luna


这样,我们既可以最大化的利用共有的属性和方法,也不用担心某个实例中的变化,会引起其他实例的变化,是当前使用最广泛,认同度最高的一种创建自定义对象的方法。

8:动态原型模式

记不记得惰性载入函数的思想?当调用或者加载函数的时候,会根据当前的浏览器,支持的方法等,重新定义函数,使得之后再次使用该函数时,不再需要进行兼容性判断,而直接使用已经处理好的兼容性代码,这样就可以减少每次调用该函数时,进行兼容性判断所需要的时间,提升些许性能。如果您对惰性载入函数了解不多,请点击:(代码优化–惰性载入函数),或者自己查看相关的知识点。

而对象的动态原型模式,我觉得就是和惰性载入函数有着相同的思想。

我们可以想想,一个做好的前端网页,可能包含很多功能,这样就会包含很多个构造函数,用于相应功能进行实例化,但并不是每一个人使用时,都把相应的功能都使用到,所以有些功能我们可以选择只有在必要时,才进行处理,比如,原型链上的属性和方法,我们可以在对该构造函数首次实例化的时候,添加原型链上的方法,这样一个好处就是,可以减少内存的占用,我个人觉得这个其实和ajax技术是相同的思想。下面就可以先看看代码的实现:


function createObject(name,age,sex){
    if(this instanceof createObject){
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.friends = ["andy","lina","luna"];
	if(typeof this.sayName != "function"){
	    createObject.prototype.sayName = function(){
		return this.name;
 	    }
	}
    }else{
	return new createObject(name,age,sex);
    }
}
var persons = new createObject("zhang",28,"male");

persons.friends.push("lion");
console.log(persons.friends);      //andy,lina,luna,lion
console.log(persons.sayName());    //zhang


注意代码中的prototype部分,这段代码,只有在执行这个createObject函数时,才可能会被创建,而如果在页面的活动期间,都没有用的对createObject的实例化,那么就一直不会在createObject的原型链上面添加sayName方法,这样就可以减少部分内存,让代码更优,当createObject被首次实例化时,就会在原型链上添加sayName方法,从而使得在以后的实例化时,不再需要执行这段代码。因为它们原型链上的方法是可以共享的。并且要记住一点,这里对原型上所做的修改,能过立即在所有的实例中得到反映。

说到这里,这种写法在每次实例化时,都会去判断是否有sayName方法的存在,虽然每次判断都是不成立的,但是在惰性载入函数思想中,有这么一句话说的好:没有if语句的代码,比有if语句的代码,有更优的性能。

9:动态原型模式使用惰性载入函数思想模式

那这里,是不是依然可以把惰性载入的思想,用到这里呢?试试看吧:


function createObject(name,age,sex){
    console.log(this instanceof createObject);     //注1 true
    createObject = function(name,age,sex){
	this.name = name;
	this.age = age;
	this.sex = sex;
	this.friends = ["andy","lina","luna"];
    };                                            //注2
    if(typeof this.sayName != "function"){
	createObject.prototype.sayName = function(){
	    return this.name;
	};
    }                                              //注3
    console.log(this instanceof createObject);     //注4  false
    return new createObject(name,age,sex);         //注5
}
var persons = new createObject("zhang",28,"male");
console.log(persons.sayName());    //zhang
console.log(createObject);         //可以在chrome的控制台看到重写后的createObject代码。


看下这段代码,在首次调用的时候,进行了createObject构造函数的重写,注意下代码中的几个标注位置:

  • 注1和注4说明了,在重写createObject前后,createObject的变化,所以在注5中,必须要使用new进行首个实例的创建。
  • 注2,重写的createObject实例内部的属性和方法。
  • 注3,判断如果有sayName方法,则不对createObject构造函数的原型链上继续添加sayName方法。

这里,有一点是需要注意的,因为我们使用的构造函数有可能会继承自其它的构造函数,其它的构造函数如果有sayName的方法,这里的原型链上的方法就不会被添加,因为我们原型链上的方法肯定不会只有一个,所以这里再对原型链上的方法进行添加时,就要多多注意了,至于如何改善,就要按需求进行相应的控制了,这里只是对这种思想进行简短的说明。也是为了证明,构造函数和惰性载入函数思想是可以结合在一起使用的。

注意:这里只是我个人的想法,暂时也没有发现其他关于该种方法的文章,虽然我自己有对这种方法进行了测试,但是是否有其他问题,这里并不能完全保证,如果您发现其中有什么问题,请提出指正,非常感谢。

10:寄生构造函数模式

示例代码:


function createObject(name,age,sex){
    var o = new Object();
    
    o.name = name;
    o.age = age;
    o.sex = sex;
    o.sayName = function(){
	return this.name;
    }
    return o;
}
var persons = new createObject("zhang",28,"male");
console.log(persons.sayName());                  //zhang


这种模式,我还是没有看出来,它比工程模式好在哪里。唯一的区别就是在创建实例时,使用的new操作符,其他都和工厂模式是相同的,想不明白,如果您知道原因,是否可以指教一下,非常感谢。

11:稳妥的构造函数模式

道格拉斯-克罗克福德(Douglas Crockford)发明了JavaScript中的稳妥对象的概念。所谓稳妥对象,指的就是没有公共属性,其方法也不引用this对象。稳妥对象最适合在一些安全的环境中(这些环境禁止使用thisnew),或者再防止数据被其他应用程序改动时使用。

示例代码:


function createObject(name,age,sex){
    var o = new Object();

    //可以在这里定义私有变量和函数
    //共有方法
    o.sayName = function(){
	return name;
    }
    return o;
}
var persons = new createObject("zhang",28,"male");
console.log(persons.sayName());                  //zhang


这个其实就是类似于闭包的概念,创建一个块级作用域,该作用域中的变量和函数只有作用域内部的方法才可以调用和修改,而外部使用该方法时,则只能使用一些公开的供外部使用的接口。这种概念在当前还是很流行的一种方法。

最常见最简单的就是在定义一个对象时


createObject = function(){
    var name="a",age="1",sex="male";
	
    function getString(){
	return "name="+name+";age="+age+";sex="+sex;
    }
    function setInfo(n,a,s){
    	name = n;
	age = a;
	sex = s;
    }
    return {
	set:setInfo,
	get:getString
    };
}();
createObject.set("zhang",28,"male");
console.log(createObject.get());                  //name=zhang;age=28;sex=male


就比如上面的代码,对于对象createObject,只公开了两个方法,setget,当然这里我只是举个例子,实际中使用会比这个复杂,这种只公开部分的写法即安全,又让使用者更容易使用,不需要去了解其他的而一些内部实现,体现了代码的封装性,有很好的优势,并且在实际中,经常被使用,这个方法呢,可以归结到对象字面量的方法中。

这里就暂时包含这些个方法了,如果又落下或者我没有说到的方法,希望您能赐教,如果发现文中的描述不当或者错误,也请提出,非常感谢!

本文地址:http://www.zhangyunling.com/?p=48

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>