JS中创建自定义类型的常用模式总结【工厂模式,构造函数模式,原型模式,动态原型模式等】
本文实例讲述了JS中创建自定义类型的常用模式。分享给大家供大家参考,具体如下:
虽然在ES6中,已经出了class的语法,貌似好像不用了解ES5中的这些老东西了,但是越深入学习,你会发现理解这些模式的重要性。
在本文中,我会描述7种常用的创建自定义类型的模式:工厂模式、构造函数模式、原型模式、组合使用构造函数模式、动态原型模式、寄生构造函数模式、稳妥构造函数模式。分别给出他们的示例代码,并分析他们的利弊,方便读者选择具体的方式来构建自己的自定义类型。
最后,我会指出ES6中的class语法,本质上其实还是利用了组合使用构造函数模式进行创建自定义类型。
1.工厂模式
废话不多说,先上工厂模式的实例代码:
functioncreatePerson(name,age,job){ varo=newObject();//创建对象 o.name=name;//赋予对象细节 o.age=age;//赋予对象细节 o.job=job;//赋予对象细节 o.sayName=function(){//赋予对象细节 alert(this.name); }; returno;//返回该对象 } varperson1=createPerson("Nicholas",29,"SoftwareEngineer"); varperson2=createPerson("Greg",27,"Doctor");
优点:解决了创建多个相似对象的问题;
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),对于对象的方法没有做到复用。
2.构造函数模式
functionPerson(name,age,job){ this.name=name;//对象的所有细节全部挂载在this对象下面 this.age=age; this.job=job; this.sayName=function(){ alert(this.name); }; } varperson1=newPerson("Nicholas",29,"SoftwareEngineer"); varperson2=newPerson("Greg",27,"Doctor");
说到构造函数模式就不得不提到new操作符了。我们来看看new这个操作符到底做了什么:
①创建一个对象;
②将构造函数内的this指向这个新创建的对象,同时将该函数的prototype的引用挂载在新对象的原型下;
③执行函数内的细节,也就是将属性和方法挂载在新对象下;
④隐式的返回新创建的对象。
优点:解决了对象识别的问题;
缺点:对于自定义类型的方法每次都要新创建一个方法函数实例,没有做到函数复用。如果把所有方法函数写到父级作用域中,是做到了函数复用,但同时方法函数只能在父级作用域的某个类型中进行调用,这对于父级作用域有点名不副实,同时对于自定义引用类型没有封装性可言。
3.原型模式
functionPerson(){ } Person.prototype.name="Nicholas"; Person.prototype.age=29; Person.prototype.job="SoftwareEngineer"; Person.prototype.sayName=function(){ alert(this.name); }; varperson1=newPerson(); person1.sayName();//"Nicholas" varperson2=newPerson(); person2.sayName();//"Nicholas" alert(person1.sayName==person2.sayName);//true
理解要点:
①无论什么时候,只要创建了一个新函数,就会根据一组特定规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
②在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。至于原型中的其他方法则都是从Object继承而来。
③当调用构造函数创建了一个新实例后,该实例的内部将包含一个指针[[prototype]](内部属性),指向构造函数的原型对象。
④当调用构造函数创建一个新实例后,该实例的实例环境,即构造函数,会针对原型对象上的非引用类型的原型属性,在构造函数中自动构建相应的实例环境属性。也就是说,之后根据构造函数创建的实例,它的实例属性中的非引用类型属性,都仍是根据构造函数中的实例环境属性创建的。
但是为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。如下所示:
functionPerson(){ } Person.prototype={ name:"Nicholas", age:29, job:"SoftwareEngineer", sayName:function(){ alert(this.name); } };
但是这种写法,其本质上完全重写了默认的prototype对象,因此constrctor属性也就变成了新对象的constructor属性(指向Object构造函数),不在指向Person函数。尽管此时,instanceOf操作符还能返回正确的结果。
如果constructor属性真的很重要,可以像下面这样特意将它设置回适当的值:
functionPerson(){ } Person.prototype={ constructor:Person, name:"Nicholas", age:29, job:"SoftwareEngineer", sayName:function(){ alert(this.name); } };
注意,以这种方式重设constructor属性会导致他的[[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的,因此,如果你使用兼容ECMAScript5的JavaScript引擎,你可以试试Object.defineProperty()方法:
functionPerson(){ } Person.prototype={ name:"Nicholas", age:29, job:"SoftwareEngineer", sayName:function(){ alert(this.name); } }; //重设构造函数,只适用于ECMAScript5兼容的浏览器 Object.defineProperty(Person.prototype,"constructor",{ enumerable:false, value:Person });
注意,重写原型对象会切断新原型与已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
优点:对自定义类型的方法解决了函数复用的问题。
缺点:
①不能为构造函数传递初始化参数;
②原型模式中实现了对于包含引用类型值的属性的共享,这就意味着一个实例中修改了该引用类型值,所有实例的该属性都会被修改!!!
4.组合使用构造函数模式和原型模式
在组合使用构造函数模式和原型模式中,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,而且还支持向构造函数传递参数。如以下示例代码所示:
functionPerson(name,age,job){ this.name=name; this.age=age; this.job=job; this.friends=["Shelby","Court"]; } Person.prototype={ sayName:function(){ alert(this.name); } } Object.defineProperty(Person.prototype,"constructor",{ enumerable:false, value:Person ); varperson1=newPerson("Nicholas",29,"SoftwareEngineer"); varperson2=newPerson("Greg",27,"Doctor"); person1.friends.push("Van"); alert(person1.friends);//"Shelby,Count,Van" alert(person2.friends);//"Shelby,Count" alert(person1.friends===person2.friends);//false alert(person1.sayName===person2.sayName);//true
优点:能为构造函数传递初始化参数;该复用复用,不该复用的没复用。
缺点:封装性不好,构造函数和原型分别独立于父级作用域进行申明。
5.动态原型模式(推荐)
该模式把所有信息都封装在构造函数中,通过构造函数来实现初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。请看以下示例代码:
functionPerson(name,age,job){ //属性 this.name=name; this.age=age; this.job=job; //方法 if(typeofthis.sayAge!="function"){//此处应该永远去判断新添加的属性和方法 Person.prototype.sayName=function(){ alert(this.name); }; Person.prototype.sayAge=function(){ alert(this.age); }; } } varfriend=newPerson("Nicholas",29,"SoftwareEngineer"); friend.sayName();
if语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。
注意,使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果已经创建的实例的情况下重写原型,那么就会切断新原型与现有实例之间的联系。
优点:封装性非常好;还可使用instanceOf操作符确定它的类型。
缺点:无。
6.寄生构造函数模式
除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。请看以下代码:
functionPerson(name,age,job){ varo=newObject(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; returno; } varfriend=newPerson("Nicholas",29,"SoftwareEngineer"); friend.sayName();//"Nicholas"
在使用new操作符下,构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖instanceOf操作符来确定对象类型;对于对象的方法没有做到复用。
7.稳妥构造函数模式
先来了解下稳妥对象:指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者再防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。以下为示例代码:
functionPerson(name,age,job){ varo=newObject();//创建要返回的对象 //可以在这里定义私有变量和函数 o.sayName=function(){//添加方法 alert(name); }; returno;//返回对象 } varfriend=Person("Nicholas",29,"SoftwareEngineer"); friend.sayName();//"Nicholas"
其原理就是利用闭包,保有对私有变量和私有方法的引用。
优点:不可能有别的方法访问到传入到构造函数中的原始数据。
缺点:没有解决对象识别的问题(即不知道这个对象是什么类型),不能依赖instanceOf操作符来确定对象类型;对于对象的方法没有做到复用。
8.ES6中的class
咱们这块以class实例来展开讲述:
classParent{ name="qck"; sex="male"; //实例变量 sayHello(name){ console.log('qcksaidHello!',name); } constructor(location){ this.location=location; } }
我们来看看这段代码通过babel编译后的_createClass函数:
var_createClass=function(){ functiondefineProperties(target,props){ for(vari=0;i首先该方法是一个自执行函数,接收的一参是构造函数本身,二参是为构造函数的原型对象需要添加的方法或者属性,三参是需要为构造函数添加的静态属性对象。从这个函数就可以看出class在创建自定义类型时,用了原型模式。
我们看看编译后的结果是如何调用_createClass的:
varParent=function(){//这里是自执行函数 _createClass(Parent,[{//Parent的实例方法,通过修改Parent.prototype来完成 key:"sayHello", value:functionsayHello(name){ console.log('qcksayHello!',name); } }]); functionParent(location){//在Parent构造函数中添加实例属性 _classCallCheck(this,Parent); this.name="qck"; this.sex="male"; this.location=location; } returnParent; }(); function_classCallCheck(instance,Constructor){ if(!(instanceinstanceofConstructor)){ thrownewTypeError("Cannotcallaclassasafunction"); } }这里调用_createClass的地方就证实了我们刚才的想法——确实应用了原型模式:我们的class上的方法,其实是通过修改该类(实际上是函数)的prototype来完成的。
而通过返回的构造函数,我们可以发现:实例属性还是通过构造函数方式来添加的。
最后,我们来看看_classCallCheck方法,它其实是一层校验,保证了我们的实例对象是特定的类型。
所以,综上所述,ES6中的class只是个语法糖,它本质上还是用组合使用构造函数模式创建自定义类型的,这也就是为什么我们要学上面那些知识的初衷。
感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行结果。
更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。