JS创建对象的7种常用模式
2018-01-26 00:00
302 查看
我们都知道Object构造函数或者对象字面量都可以用来创建单个对象,但是这个方式有一个明显的缺点:使用同一个接口创建很多对象,会产生大量 的重复代码。那么怎么解决呢?第一种工厂模式就是应需求发展产生的。
(一)工厂模式
工厂模式抽象了创建具体对象的过程,考虑到ECMAScript中无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。
工厂模式例子:
函数createPerson()根据接受的参数创建一个包含所有必要信息的Person对象,可以无数次的调用这个函数且每次均返回一个包含三个属性和一个方法的对象。
问题:解决了创建多个相似对象的问题,但是无法知道一个对象的类型,引出了构造函数的解决方案。
(二)构造函数模式
ECMAScript中的构造函数始终都应该以一个大写字母开头,非构造函数则应该以一个小写字母开头(作此约定是因为构造函数也是函数,只不过可以创建对象),构造函数可用来创建特定类型的对象,像原生构造函数Object和Array在运行时会自动出现在执行环境中,可以调用对象类型的属性和方法。因此,根据这个思路可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。重写前面的例子:
此例中,Person()函数取代了createPerson()函数,不同之处:
1.没有显示地创建对象;
2.将属性和方法赋给了this对象;
3.没有return语句。
创建Person的新实例,必须使用new操作符。任何函数只要通过new操作符来调用,那它就可以作为构造函数,而任何函数,如果不通过new操作符来调用就跟普通函数没有什么两样。以此方式调用构造函数实际会经历4个步骤:
1.创建一个新对象;
2.将构造函数的作用域赋给新对象(因此this就指向这个新对象);
3.执行构造函数中的代码(为这个新对象添加属性(方法也属于属性));
4.返回新对象;
为什么我们一直强调调用函数是构造函数?因为构造函数有一个constructor(构造函数)属性,对象的constructor属性最初是用来标识对象类型的(当然了检测类型靠谱的方法是instanceof操作符),就可以解决工厂类型的缺点:无法识别对象类型。本例中person1和person2分别保存着Person的一个不同实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person,所有对象均继承自Object。
上面采用构造函数模式创建的函数,每个方法都要在每个实例上重新创建一遍,需要执行上面提到的4个步骤,结合下面求证说明虽然person1和person2都有一个名为sayName()的方法,但是两个方法不是同一个Function的实例,为什么呢?因为ECMAScript中的函数是对象,每定义一个函数就是实例化了一个对象。简单说就是虽然创建Function新实例的机制相同但是函数内部会做不同的作用域和标识符解析,不同实例上的同名函数是不相等的。事实上:创建两个完成同样任务的Function实例的确没有必要,而且函数定义的有this对象,有this对象在根本不用在执行代码前就把函数绑定到特定对象上。
问题:怎么解决我想实现像原生构造函数Object和Array那样可以既可以在全局作用域中实现属性和方法又不仅仅是被绑定到某个对象上调用?原型模式就是这样产生的。
(三)原型模式
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype(原型)属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造)属性,这个属性是一个指向prototype属性所在函数的指针。创建了自定义的构造函数之后其原型对象默认只会取得constructor属性,其他方法,则是从Object继承而来。根据这些我们可以有一条思路:不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象上。
这个例子:将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数 变成了空函数。person1和person2访问的都是同一组属性和同一个sayName()函数。
分析具体的实例,先看一下原型模式的工作原理:
Person.prototype指向了原型对象自动获得constructor(构造)属性,而Person.prototype.constructor又指回了Person。(在默认情况下,所有原型对象都会自动获得一个constructor(构造)属性,这个属性是一个指向prototype属性所在函数的指针)原型对象中除了包含constructor(构造)属性之外,还包括后来添加的其他属性。Person的实例person1和person2都包含了一个内部属性,属性只指向Person.prototype,也就是说这两个实例与构造函数没有直接的关系,可以调用person1.sayName()是通过查找对象属性的过程实现的。
搜索过程:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先会从实例本身开始,找到就停止,没有找到则继续向原型搜索, 先后执行两次。
1.解析器:“实例person1有sayName属性么?”
2.实例1:“没有”;
3.解析器:“person1的原型有sayName属性么?”
4.实例1:“有”,读取保存在原型对象中的函数。
注意:虽然可以通过对象实例访问保存在原型中的值,但是却不能通过对象实例重写原型中的值,在实例中添加了一个属性,而该属性与实例原型中的一个属性名同名,则实例中创建,原型中屏蔽。
person1查找的时候先在实例中找到,则不继续查找,person2直到原型中搜索到目标值。也就是说实例与构造函数的原型对象之间的连接被半路切断了,那么使用删除delete操作符会发生什么呢?会不会恢复连接?
问题:当出现引用类型值的属性时会发生严重的问题,看这个例子:
为什么呢?回顾一下搜索过程,person1开始查找friends数组,但是friends数组只在原型中存在,那么找到了并根据语句修改数据,此时原型中friends数组值已经变为3组,person2去查找的时候也是在原型中才能搜索到,此时它与person1.friends查找时指向的是同一个数组,找的值也必定相同。实际上我们不希望person2找的值与person1相同,所以引用类型会出现一些严重共享问题。为了优化这种情况,设计人员开发出组合使用构造函数模式和原型模式。
(四)组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。这样,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。
实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。修改了person1.friends(添加了新的字符)并不会影响到person2.friends,因为引用了不同的数组(属性的副本中找到数组)。这种模式是目前使用最广泛、认同度最高的一种创建自定义类型的方法。这么好用的模式,那么别的语言开发人员看到这些独立的构造函数和原型时可能会不习惯,他们只想关注你的模式中是否有我需要用到的方法,我怎么去初始化,动态原型模式就是去解决这个问题。
(五)动态原型模式
动态原型模式把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点,实际使用中我们可以便捷的检查某个应用已经存在的方法是否有效从而决定是否需要初始化原型供新的业务使用。但是注意一点,如果使用这个原型模式时,不能使用对象字面量去重写原型,原因是前面的详细分析,数组指针的指向会发生变化,也就是引用变得不同了,会切断现有实例与新原型之间的联系。
这个实例中if语句检查的可以是初始化之后存在的任何一个方法或者属性----不必罗列出所有的属性和方法,只要写一个即可完成检查,(原型中存在则实例中必然可以搜索到,2次搜索必然有能找到的),另外,上述实例还可以使用instanceof操作符确定类型。如果前面的几种模式都不适合要求,那怎么办呢?可以采用寄生构造函数模式。
(六)寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,那可以为这个对象创建构造函数返回我们需要的形式。但是有一点要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系(构造函数返回的对象与构造函数外部创建的对象没啥关系,即引用了不同的数组,那就不能依赖instanceof操作符确定返回对象的类型,无法继而利用熟悉类型的方法和属性去达到目的 。)
在这个实例中,SpecialArray函数创建了一个对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句可以重写调用构造函数时返回的值。不能依赖instanceof操作符确定返回对象的类型还有一种模式是稳妥构造函数模式。
(七)稳妥构造函数模式
顾名思义,所谓稳妥指没有公共属性,而且其方法也不引用this的对象,运行在安全的环境中(不使用this和new的声明方式)防止被修改。与寄生模式不同的是:1.不使用new操作符调用函数,2.新创建实例方法不引用this。
变量friend中保存的是一个稳妥对象,除了调用sayName()外没有别的方法可以访问其数据成员。即使有其他代码会给这个对象添加方法或者数据成员,但是不可能别的办法访问传入到构造函数中的原始数据。
(一)工厂模式
工厂模式抽象了创建具体对象的过程,考虑到ECMAScript中无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。
工厂模式例子:
function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; return o; } var person1=createPerson("zhangsan",24,"doctor"); var person2=createPerson("lisi",25,"teacher");
函数createPerson()根据接受的参数创建一个包含所有必要信息的Person对象,可以无数次的调用这个函数且每次均返回一个包含三个属性和一个方法的对象。
问题:解决了创建多个相似对象的问题,但是无法知道一个对象的类型,引出了构造函数的解决方案。
(二)构造函数模式
ECMAScript中的构造函数始终都应该以一个大写字母开头,非构造函数则应该以一个小写字母开头(作此约定是因为构造函数也是函数,只不过可以创建对象),构造函数可用来创建特定类型的对象,像原生构造函数Object和Array在运行时会自动出现在执行环境中,可以调用对象类型的属性和方法。因此,根据这个思路可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。重写前面的例子:
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.sayName=function(){ alert(this.name); }; } var person1=new Person("zhangsan",24,"doctor"); var person2=new Person("lisi",25,"teacher");
此例中,Person()函数取代了createPerson()函数,不同之处:
1.没有显示地创建对象;
2.将属性和方法赋给了this对象;
3.没有return语句。
创建Person的新实例,必须使用new操作符。任何函数只要通过new操作符来调用,那它就可以作为构造函数,而任何函数,如果不通过new操作符来调用就跟普通函数没有什么两样。以此方式调用构造函数实际会经历4个步骤:
1.创建一个新对象;
2.将构造函数的作用域赋给新对象(因此this就指向这个新对象);
3.执行构造函数中的代码(为这个新对象添加属性(方法也属于属性));
4.返回新对象;
为什么我们一直强调调用函数是构造函数?因为构造函数有一个constructor(构造函数)属性,对象的constructor属性最初是用来标识对象类型的(当然了检测类型靠谱的方法是instanceof操作符),就可以解决工厂类型的缺点:无法识别对象类型。本例中person1和person2分别保存着Person的一个不同实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person,所有对象均继承自Object。
alert(person1.constructor==Person);//true alert(person2.constructor==Person);//true //使用instanceof校验 alert(person1 instance Object);//true alert(person2 instance Object);//true alert(person1 instance Person);//true alert(person2 instance Person);//true
上面采用构造函数模式创建的函数,每个方法都要在每个实例上重新创建一遍,需要执行上面提到的4个步骤,结合下面求证说明虽然person1和person2都有一个名为sayName()的方法,但是两个方法不是同一个Function的实例,为什么呢?因为ECMAScript中的函数是对象,每定义一个函数就是实例化了一个对象。简单说就是虽然创建Function新实例的机制相同但是函数内部会做不同的作用域和标识符解析,不同实例上的同名函数是不相等的。事实上:创建两个完成同样任务的Function实例的确没有必要,而且函数定义的有this对象,有this对象在根本不用在执行代码前就把函数绑定到特定对象上。
alert(person1.sayName==Person2.sayName);//false
问题:怎么解决我想实现像原生构造函数Object和Array那样可以既可以在全局作用域中实现属性和方法又不仅仅是被绑定到某个对象上调用?原型模式就是这样产生的。
(三)原型模式
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype(原型)属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造)属性,这个属性是一个指向prototype属性所在函数的指针。创建了自定义的构造函数之后其原型对象默认只会取得constructor属性,其他方法,则是从Object继承而来。根据这些我们可以有一条思路:不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象上。
这个例子:将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数 变成了空函数。person1和person2访问的都是同一组属性和同一个sayName()函数。
function Person(){} Person.prototype.name="zhangsan"; Person.prototype.age=24; Person.prototype.job="doctor"; Person.prototype.sayName=function(){ alert(this.name); } var person1=new Person(); person1.sayName();//"zhangsan" var person2=new Person(); person2.sayName();//"zhangsan" alert(person1.sayname==person2.sayname);//true
分析具体的实例,先看一下原型模式的工作原理:
Person.prototype指向了原型对象自动获得constructor(构造)属性,而Person.prototype.constructor又指回了Person。(在默认情况下,所有原型对象都会自动获得一个constructor(构造)属性,这个属性是一个指向prototype属性所在函数的指针)原型对象中除了包含constructor(构造)属性之外,还包括后来添加的其他属性。Person的实例person1和person2都包含了一个内部属性,属性只指向Person.prototype,也就是说这两个实例与构造函数没有直接的关系,可以调用person1.sayName()是通过查找对象属性的过程实现的。
搜索过程:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先会从实例本身开始,找到就停止,没有找到则继续向原型搜索, 先后执行两次。
1.解析器:“实例person1有sayName属性么?”
2.实例1:“没有”;
3.解析器:“person1的原型有sayName属性么?”
4.实例1:“有”,读取保存在原型对象中的函数。
注意:虽然可以通过对象实例访问保存在原型中的值,但是却不能通过对象实例重写原型中的值,在实例中添加了一个属性,而该属性与实例原型中的一个属性名同名,则实例中创建,原型中屏蔽。
function Person(){} Person.prototype.name="zhangsan"; Person.prototype.age=24; Person.prototype.job="doctor"; person.prototype.sayName=fuction(){ alert(this.name); } var person1=new Person(); var person2=new Person(); person1.name="lisi"; alert(person1.name);//"lisi"---来自实例 alert(person2.name)//"zhangsan"---来自原型
person1查找的时候先在实例中找到,则不继续查找,person2直到原型中搜索到目标值。也就是说实例与构造函数的原型对象之间的连接被半路切断了,那么使用删除delete操作符会发生什么呢?会不会恢复连接?
function Person(){} Person.prototype.name="zhangsan"; Person.prototype.age=24; Person.prototype.job="doctor"; person.prototype.sayName=fuction(){ alert(this.name); } var person1=new Person(); var person2=new Person(); person1.name="lisi"; alert(person1.name);//"lisi"---来自实例 alert(person2.name)//"zhangsan"---来自原型
delect person1.name;
alert(person1.name);//"zhangsan"---来自原型
问题:当出现引用类型值的属性时会发生严重的问题,看这个例子:
function Person(){} Person.prototype={ constructor:Person, name:"zhangsan", age:24, job:"doctor", friends:["lisi","wangerma"], sayName:fuction(){ alert(this.name); } }; var person1=new Person(); var person2=new Person(); person1.friends.push("nini"); alert(person1.friends);//"zhangsan,lisi,nini" alert(person2.friends)//"zhangsan,lisi,nini" alert(person1.friends==person2.friends);//true
为什么呢?回顾一下搜索过程,person1开始查找friends数组,但是friends数组只在原型中存在,那么找到了并根据语句修改数据,此时原型中friends数组值已经变为3组,person2去查找的时候也是在原型中才能搜索到,此时它与person1.friends查找时指向的是同一个数组,找的值也必定相同。实际上我们不希望person2找的值与person1相同,所以引用类型会出现一些严重共享问题。为了优化这种情况,设计人员开发出组合使用构造函数模式和原型模式。
(四)组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。这样,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。
<!DOCTYPE html> <html> <head> <title>Hybrid Pattern Example</title> <script type="text/javascript"> function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["haha", "heihei"]; } Person.prototype = { constructor: Person, sayName : function () { alert(this.name); } }; var person1 = new Person("zhangsan", 24, "teacher"); var person2 = new Person("lisi", 27, "Doctor"); person1.friends.push("wawa"); alert(person1.name);//zhangsan alert(person2.name);//lisi alert(person1.hasOwnProperty("name"));//true,使用hasOwnProperty()方法可以检测什么时候访问的是实例属性,什么时候访问的是原型属性 alert(person1.friends); //"haha,heihei,wawa" alert(person2.friends); //"haha,heihei" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true alert(person1.hasOwnProperty("sayName"));//false ,访问的是原型属性 </script> </head> <body> </body> </html>
实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法sayName()则是在原型中定义的。修改了person1.friends(添加了新的字符)并不会影响到person2.friends,因为引用了不同的数组(属性的副本中找到数组)。这种模式是目前使用最广泛、认同度最高的一种创建自定义类型的方法。这么好用的模式,那么别的语言开发人员看到这些独立的构造函数和原型时可能会不习惯,他们只想关注你的模式中是否有我需要用到的方法,我怎么去初始化,动态原型模式就是去解决这个问题。
(五)动态原型模式
动态原型模式把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点,实际使用中我们可以便捷的检查某个应用已经存在的方法是否有效从而决定是否需要初始化原型供新的业务使用。但是注意一点,如果使用这个原型模式时,不能使用对象字面量去重写原型,原因是前面的详细分析,数组指针的指向会发生变化,也就是引用变得不同了,会切断现有实例与新原型之间的联系。
function Person(name,age,job){ //属性 this.name=name; this.age=age; this.job=job; //方法,只会在初次调用构造函数时没有这个方法时加入,按顺序执行后原型已完成初始化,此后原型不做修改。并且做的修改会立即在所有的实例中生效。 if(typeof this.sayName!="function"){ Person.prototype.sayName=function(){ alert(this.name); }; } } var friend=new Person("hahaha",21,"student"); friend.sayName();
这个实例中if语句检查的可以是初始化之后存在的任何一个方法或者属性----不必罗列出所有的属性和方法,只要写一个即可完成检查,(原型中存在则实例中必然可以搜索到,2次搜索必然有能找到的),另外,上述实例还可以使用instanceof操作符确定类型。如果前面的几种模式都不适合要求,那怎么办呢?可以采用寄生构造函数模式。
(六)寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,那可以为这个对象创建构造函数返回我们需要的形式。但是有一点要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系(构造函数返回的对象与构造函数外部创建的对象没啥关系,即引用了不同的数组,那就不能依赖instanceof操作符确定返回对象的类型,无法继而利用熟悉类型的方法和属性去达到目的 。)
function SpecialArray(){ //创建数组 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"
在这个实例中,SpecialArray函数创建了一个对象,并以相应的属性和方法初始化该对象,然后又返回了这个对象。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句可以重写调用构造函数时返回的值。不能依赖instanceof操作符确定返回对象的类型还有一种模式是稳妥构造函数模式。
(七)稳妥构造函数模式
顾名思义,所谓稳妥指没有公共属性,而且其方法也不引用this的对象,运行在安全的环境中(不使用this和new的声明方式)防止被修改。与寄生模式不同的是:1.不使用new操作符调用函数,2.新创建实例方法不引用this。
function Person(name,age,job){ //创建要返回的对象 var o=new Object(); //定义私有变量个函数 //添加方法 o.sayName-function(){ alert(name); }; //返回对象 return o; } var friend=Person("zhangsan",24,"teacher"); friend.sayName();//"zhangsan"
变量friend中保存的是一个稳妥对象,除了调用sayName()外没有别的方法可以访问其数据成员。即使有其他代码会给这个对象添加方法或者数据成员,但是不可能别的办法访问传入到构造函数中的原始数据。
相关文章推荐
- JS 创建对象的7种模式
- JavaScript 创建对象的7种模式
- 浅谈 JS 创建对象的 8 种模式
- js创建对象的几种常用方式
- js中组合使用构造函数模式和原型模式创建对象
- js 创建对象的几种常用的方式
- js创建对象的几种常用方式
- js创建对象的几种常用方式小结(推荐)
- js 创建对象 经典模式全面了解
- JavaScript创建对象的7种模式
- js创建对象的构造函数模式+原型模式和组合继承\Hybrid Pattern & combination inheritance
- 自我学习——javascript——常用的对象创建模式
- js创建对象的几种常用方式小结(推荐)
- js 创建对象 经典模式全面了解
- JS创建对象之组合使用构造函数模式和原型模式
- 浅谈js对象的创建和对6种继承模式的理解和遐想
- JS创建对象的几种模式
- js创建对象的几种常用方式小结(推荐)
- js创建对象的几种常用方式小结
- JS创建对象最常用的方法就是使用JSON格式的语法