javascript----创建对象
如何优雅的去创建一个对象
在javascript中,创建一个对象有很多方法,但很多时候我们得根据我们的需求去选择其中的一种来达到实现代码简单、优雅的表现,以下先来看下javascript中有哪些创建对象的方法,并指出优缺点再进行分类。
对象字面量形式-------------------------------快速,单纯地创建某个对象
这个是最基本的了,就不再作描述
形式都是
var objectName = {
property1:value1,
property2:value2,
... ,
functionName1:function(){...};
functionName2:function(){...};
}
这种类型
优点
简洁明了,适合快速创建某个对象
缺点
很明显,创建一群类似的对象,就要这样子重写好多遍,明显不合适,于是就有了以下各种模式的讨论和比较了
工厂模式------------------------适用于小型项目或者未成型的项目
工厂模式主要考虑到在ECMAScript中无法创建类,因此用函数来封装以特定接口创建对象的细节
如下:
function createStudent(name,age,grade){ var o = new Object(); o.name = name; o.age = age; o.grade = grade; o.sayName = function(){ alert(this.name); }; return o; } var student1 = createStudent('LiAo',22,666); var stundet2 = createStudent('Lan',22,999);
优点: 根据接受的参数来构建一个包含所有信息的对象,应用于多个相似对象的情况
缺点:没有解决对象识别的问题(即怎样知道一个对象的原型)
构造函数模式------------------适用于大型项目,需要定义各种特定类型
ECMAScript中,构造函数可以用来创建特定类型的对象,从而定义对象类型的属性和方法
如:
function Student(name,age,grade){ this.name = name; this.age = age; this.grade = grade; this.sayName = function(){ alert(this.name); }; } var student1 = new Student('LiAo',22,666); var student2 = new Student('Lan',22,999);
与工厂模式的区别:
- 没有显示地创建对象(交给了new操作符后台,同时绑定原型,没有配合new使用即this直接绑定在调用此函数的对象上,不算创建了对象)
- 直接将属性和方法赋给this对象(因为this是动态绑定的)
- 没有return语句(return语句交给了new操作符来指定
- 作为构造函数,应该以一个大写字母开头以作区别
然而有了构造函数还不够,需要配合new操作符使用来创建对象
以下来深究下new一个对象,Javascript背后都发生了什么
首先构造函数我们还是使用上面Student函数
其实var student1 = new Student('LiAo',22,666);
我们先把它当作 var student1 = objectFactory(Student,'LiAo',22,666);
现在看看objectFactory到底是什么妖魔鬼怪,你才知道js在new的背后干了些什么,看以下代码:
var objectFactory = function(){ var obj = new Object(); //创建一个空的对象; var Constructor = [].shift.call(arguments); //此处类数组对象arguments借用了数组的shift方法,取出第一项作为构造器 obj.__proto__ = Contructor.prototype; //给新创建的对象指向正确的圆原型对象 var ret = Constructor.apply(obj,arguments);//借用构造器给obj设置属于它自己的属性,此时的ret要根据构造函数是否返回值,不返回则为undefined return typeof ret ==='object' ? ret : obj; //若构造函数不返回对象,则ret = obj }; var studen1 = objectFactory(Student,'LiAo',22,666); console.log(student1.name) // LiAo
现在你大概懂了new操作符背后都干了些什么了吧,大概简述如下:
- 创建一个新对象
- 新对象的原型指向构造器的原型
- 借用外部传入的构造器为新对象设置唯一的属性
- 返回新对象
优点: 可以将构造函数的实例标识为一种特定的类型,而不像工厂模式所有实例都是Object的直接实例
缺点: 每个方法都要在每个实例上重新创建一遍,不同实例上的同名函数是不相等的,有不同的作用域链和标识符解析
针对构造函数缺点的解决方案(非最佳实践):
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
把函数定义转移到构造函数外面来解决这个问题,但是这样就催生了另一个问题:让全局作用域有点名不副实,如果对象定义很多个函数,就要创建很多个全局函数,没有封装性可言, 这个问题就涉及到下面要讨论的原型模式了.
原型模式------------------------------------------------很少单独使用
从原型说起:
每个创建的函数都有一个prototype(原型)属性,为一个指针指向一个对象,而这个对象用途为:包含可以由特定类型的所有实例共享的属性和方法,构造函数创建了实例后,该实例的原型对象就指向了构造函数的原型属性,从而在实例中搜索标识符的时候可以延伸到原型对象中搜索
换句话说,就是不必在构造函数中定义共享的属性和方法,可以直接添加 到原型对象中.
注意 虽然可以通过对象实例来访问保存在原型中的值,但不能通过对象实例重写原型中的值,重写只会在实例中创建属性,从而屏蔽了原型中的属性.(这里主要讲创建对象的模式,就不在演示这些例子的代码啦,大家有兴趣可以回去翻翻高程三)
好的! 现在继续讲回原型
更简单的原型语法:
function Person(){ } Person.prototype = { name:"LiAo", age:22, job:'Student', sayName:function(){ alert(this.name); } };
上面我们重写了Person函数实例的原型对象,看起来语法看起来很简单,但是默认的constructor属性不再指向Person了,而是指向Object,但是记得现在不能用instanceof操作符来检测实例的引用类型,因为都是显示为true
student1 = new Person(); alert(student1 instanceof Object) // true; alert(student1 instanceof Person) // true
因为Person也是继承Object的,此时可以用原型对象的constructor来检测
如果constructor在你的开发需求中真的很重要,可以在重写的对象中特意设一个constructor属性指向Person
Person.prototype = { constructoe:Person, .... .... }
注意用这种方式来重设constructor属性会导致他的[[Enumerable]]设置为true,从默认不可枚举变成可枚举属性了,可以试试用es5中的Object.defineProperty()来定义constructor属性
Object.defineProperty(Person.prototype,'constructor',{ enumerable:false, value:Person });
尽管可以重写原型对象,但是要在实例化对象之前重写对象,否则已经实例化的对象会断开与新原型对象的联系,如:
function Person(){}; var friend = new Person(); Person.prototype = { constructor:Person, name:'LiAo', age:22, job:'student', sayName:function(){ console.log(this.name); } } friend.sayName(); // error
好了,感觉又跑偏了,关于原型的细节不是随随便便就能说完的,后续我再根据这个知识点做一个专门的总结吧,现在还是回到我们的主题,优雅地创建对象!
原型的优点:
原型模式很好地弥补了构造函数的缺点,并且简化了初始化参数这一环节
随之而来的缺点:
共享的本质导致:我们有时候并不想共享某些属性,特别是引用类型,某个实例更改了引用类型同时也会修改掉其他实例的引用类型,因为在原型上定义属性对于实例来说都是共享的.为了解决这个问题,于是又催生了下面的设计模式------组合模式
组合模式---------------------------------------使用最广泛,认同度最高
组合模式本身不难理解,就是组合使用构造函数模式和原型模式
使用过程中注意区分好自身需求就好:
构造函数模式用于定义实例属性.
原型模式用于定义方法和共的属性.
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ['Ye','Chen']; } Person.prototype = { constructor:Perosn, sayName:function(){ alert(this.name); } }
优点已经很明显了,我就不再作阐述
动态原型模式-----------------------------------------必要时候才使用原型
动态原型模式倡导仅在必要时候才需要初始化原型
function Person(name,age,job){ this.name = name; this.age = age; this.job = job; if(typeof this.sayName != 'function'){ Perosn.prototype.sayName = function(){ alert(this.name); }; } }
动态原型模式有两个特点
- 可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
如果不进行检查的话,每次初始化一个实例时,原型对象上的共享方法都会重新绑定一次,而进行检查的话,可以回去看看上面的objectFactory函数,第一次初始化,每次经过检查就无需在原型对象上再进行绑定. - if语句检查的可以是初始化之后应该存在的任何属性或方法 —— 不必用一大堆if语句检查每个属性和每个方法,只要检查其中一个即可。
假如你需要定义很多共享属性或者方法,那么只需要检查一个就行,因为他们存在一致性,一者存在则所有者都存在.
动态原型模式的优点 - 把构造函数和原型模式结合在一个函数上,更有一体性
- 可以使用instanceof操作符确定实例的类型(因为没有重写原型对象)
动态原型模式的缺点 - 我觉得动态原型模式结合了以上每个模式的优点,但是降低了代码的可读性,如果习惯构造函数和原型对象分开写的人可能觉得这并不那么优雅
- 另外还有就是不能采用重写原型那样简洁的方法指定共享属性或方法,如果共享属性和方法特别多的话,就要写好多Person.prototype.属性(方法)这样不优雅的代码.
总之,见仁见智吧,每个人阅读代码的思维是不一样的,找到自己顺眼的一款就好了.
寄生构造函数模式-------------用于创建一个具有额外功能的特殊对象
我对寄生构造函数模式的记忆是想象成在构造函数里面再寄生一个构造函数,如下
function Person(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name); } return o ; }
你肯定会发现这个很像是典型的构造函数,内部又像工厂模式,你大可理解为构造函数和工厂模式的结合版.但是上面的例子用其他模式都有很好实现方法,不倡导如此使用,而是建议把寄生构造函数模式用于一些特殊情况,比如,创建一个具有额外方法的特殊数组,因为this无法修改,不能有this = new Array()这种情况,因此可以使用这个模式,返回一个对象替代this:
function SpecilArray(){ var values = new Array(); values.push.apply(values,arguments); values.toPipedString = function(){ return this.join('|'); }; return values; } var colors = new SpecialArray('red','blue','green'); alert(colors.toPipedString()); // red|blue|green
需要注意的是:返回的对象与构造函数或者构造函数的原型属性之间没有关系.
至于为什么还有使用new操作符这个问题,我是这样理解的:
参考上面的objectFactory,在这里,你会发现new不new都一样的(因为Person函数中除了闭包外,没有使用this,如果使用了this的话,那结果就会不一样)。此时使用了new反而会在后台多运行几行没必要的代码,这里使用new的话我想估计是让Person这个构造函数名副其实吧。
寄生构造函数模式的优点
- 可以基于引用类型创建一个特殊的对象
寄生构造函数模式的缺点
- 如上所示的注意点,因此不能用instanceof操作符来确定对象类型
- 适用范围太窄,若能用其他模式实现创建对象,就不建议用次方法.
稳妥构造函数模式----------------------------适合在安全执行环境下使用
顾名思义,稳妥构造函数模式的目的是创建一个稳妥对象,稳妥对象是指没有公共属性,而且方法也不引用this对象,稳妥对象最适用于一些安全的环境或者防止数据被其他应用程序修改时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:①新创建对象的实例方法不引用this。②不适用new操作符调用构造函数。
因此可以根据上面Person构造函数重写如下:
function Person(name,age,job){ var o = new Object(); o.sayName = function(){ alert(name); }; return o ; var friend = Person('LiAo',22,'Student')
这也是高程三的栗子,但是真的要创建稳妥对象,首先是不能出现公共属性,如果上面函数加了一句 o.name = name,那么外部仍然可以通过friend.name来得到name的值,所以这时候并不能称为所谓的稳妥对象。或者说这个例子并不适合创建稳妥对象,因为name,age,job这些属性应该要跟新对象o绑定起来,如果要构建一个名副其实的稳妥对象,那么可以这么写
function Person(name,age,job){ var o = new Object(); o.sayAge = function(){ alert(age); }; o.sayJob = function(){ alert(job) }; o.sayName = function(){ alert(name); }; return o ; var friend = Person('LiAo',22,'Student') o.sayAge(); //22 o.sayName(); 'LiAo' o.sayJob(); 'Student'
这样的话就算一个名副其实的稳妥对象了,但是何必呢,我觉得稳妥对象中的稳妥应该相对每个开发者而言,结合使用公共属性和稳妥属性来达到对有必要的数据进行保护的目的才是稳妥。
稳妥构造函数模式的优点
防止原始数据被其他应用环境进行修改,适合在某些安全执行环境下使用
稳妥构造函数模式的缺点
跟寄生构造函数一样都是对象实例与构造函数之前没有任何关系,因此不能用instanceof操作符来判断类型。
最后的话
上面基本讲完了ES6之前的所有创建对象的模式,分别有:
- 对象字面量形式
- 工厂模式
- 构造函数模式
- 原型模式
- 组合模式
- 动态原型模式
- 寄生构造函数模式
- 稳妥构造函数模式
大家可以根据自己需求来选择不同的模式来创建对象,我觉得平时的创建对象的方法大多都是对象字面量和组合模式这两种形式,其他模式看自己是否要区分类型或者考虑安全性、代码优雅、是否要创建特殊对象等因素来进行选择使用。
attention:上面的知识和例子大多参考红宝书高程三,同时夹带一些个人看法,如有错误或者不同看法,欢迎评论交(si)流(bi)哈,共同进步.