JavsScript中对象继承关系变得无关紧要,对于一个对象来说重要的是它能做什么,而不是它从哪里来。
JavaScript提供了一套更为丰富的代码重用模式。它可以模拟那些基于类的模式,同时它也可以支持其他更具表现力的模式。
JavaScript是一门基于原型的语言,这意味着对象直接从其他对象继承。
一、伪类
1、原理
javascript原型机制:不直接让对象从其他对象继承,反而插入了一个多余的间接层:通过构造器函数产生对象。
当一个函数对象被创建时,Function构造器产生的函数对象会运行类型这样一些代码:
this.prototype={constructor:this}
新函数对象被赋予一个prototype属性,它的值是一个包含constructor属性且属性值为该新函数的对象。这个prototype对象是存放继承特征的地方。
当采用构造器调用模式,即用new前缀去调用一个函数时,函数执行的方式会被修改。如果new运算符是一个方法而不是一个运算符,它可能会像这样执行:
Function.method('new',function () { //创建一新对象,它继承自构造器函数的原型对象。 var that=Object.create(this.prototype); //调用构造器函数,绑定-this-到新对象上。 var other=this.apply(that,arguments); //如果它的返回值不是一个对象,就返回该对象。 return (typeof other==='object'&&other)||that; });
2、伪类,即使用new前缀
定义一构造器并扩充它的原型:
var Mammal=function(name){ this.name=name; } Mammal.prototype.get_name=function(){ return this.name; } Mammal.prototype.says=function(){ return this.saying || ''; }
现在构造一个实例:
var myMammal=new Mammal('Herb the Mammal'); var name=myMammal.get_name();//"Herb the Mammal"
构造另一个伪类来继承Mamal,这是通过定义它的constructor函数并替换它的prototype为一个Mammal的实例来实现的。
var Cat=function(name){ this.name=name; //重复实现了一遍 this.saying='meow'; } //替换cat.prototype为一个新的Mammal实例 Cat.prototype=new Mammal(); //扩充新原型对象,增加purr和get_name方法。 Cat.prototype.purr=function(n){ var i,s=''; for(i=0;i<n;i+=1){ if(s){ s+='-' } s+='r'; } return s; } Cat.prototype.get_name=function(){ return this.says()+' '+this.name+' '+this.says(); } var myCat=new Cat('Henrietta'); var says=myCat.says();//"meow" var purr=myCat.purr(5);//"r-r-r-r-r" var name=myCat.get_name();//"meow Henrietta meow"
伪类模式本意是想向面向对象靠拢,但它看起来格格不入。
我们隐藏一些丑陋的细节,通过使用method方法来定义一个inherits方法实现。
Function.prototype.method=function(name,func){ if(!this.prototype[name]){ this.prototype[name]=func; } return this; } Function.method('inherits',function(Parent){ this.prototype=new Parent(); return this; }); var Cat=function(name){ this.name=name; this.saying='meow' } .inherits(Mammal) .method('purr',function(n){ var i,s=''; for(i=0;i<n;i+=1){ if(s){ s+='-' } s+='r'; } return s; }) .method('get_name',function(){ return this.says()+' '+this.name+' '+this.says(); }); var myCat=new Cat('Henrietta'); var says=myCat.says();//"meow" var purr=myCat.purr(5);//"r-r-r-r-r" var name=myCat.get_name();//"meow Henrietta meow"
问题:以上虽然隐藏了prototype操作细节,但是问题还在:有了像“类” 的构造器函数,但仔细看它们,你会惊讶地发现:
1、没有私有环境,所有的属性都是公开的。
2、使用构造器函数存在一个严重的危害。如果调用构造函数时忘记了在前面加上new前缀,那么this将不会被绑定到一个新对象上。悲剧的是,this将被绑定到全局对象上,所以你不但没有扩充新对象,反而破坏了全局变量环境。
这是一个严重的语言设计错误。为了降低这个问题带来的风险,所有的构造器函数都约定命名成首字母大写的形式,并且不以首字母大写的形式拼写任何其他的东西。
一个更好的备选方案就是根本不使用new。
二、对象说明符
构造器要接受一大串参数,要记住参数的顺序非常困难。所以编写构造器时让它接受一个简单的对象说明符,更友好。
//接受一大串参数 var myObject=maker(f,l,m,c,s); //对象字面量更友好 var myObject=maker({ first:f, middle:m, last:l, state:s, city:c });
对象字面量好处:
- 多个参数可以按任何顺序排列
- 构造器如果聪明的使用了默认值,一些参数可以忽略掉
- 和JSON一起用时,可以把JSON对象传给构造器,返回一个构造完全的对象
三、原型
原型模式中,摒弃类,转而专注于对象。概念:一个新对象可以继承一个旧对象的属性。 通过构造一个有用的对象开始,接着可以构造更多和那个对象类似的对象。这就可以完全避免把一个应用拆解成一系列嵌套抽象类的分类过程。
1、差异化继承
用对象字面量构造一个有用的对象。
var myMammal={ name:"Herb the Mammal", get_name:function(){ return this.name; }, says:function(){ return this.saying || ''; } }
一旦有了想要的对象,就可以利用Object.create方法构造出更多的实例。
var myCat=Object.create(myMammal); myCat.name='Henrietta'; myCat.saying='meow'; myCat.purr=function(n){ var i,s=''; for(i=0;i<n;i++){ if(s){ s+='-'; } s+='r'; } return s; } myCat.get_name=function(){ return this.says+' '+this.name+' '+this.says; }
这是一种“差异化继承(differential inheritance)”,通过定制一新的对象,我们指明它与所基于的基本对象的区别。
2、差异化继承优势
差异化继承,对某些数据结构继承于其他数据结构的情形非常有用。
例:假定我们要解析一门类似JavaScript这样用一对花括号指示作用域的语言。定义在某个作用域里的条目在该作用域外是不可见的。
但在某种意义上,一个内部作用域会继承它的外部作用域。JavaScript在表示这样的关系上做得非常好。
当遇到一个左花括号时block函数被调用,parse函数将从scope中寻找符号,并且它定义了新的符号时扩充scope。
var block=function(){ //记住当前的作用域。构造一包含了当前作用域中所有对象的新的作用域 var oldScope=scope; scope=Object.create(scope); //传递左花括号作为参数调用advance advance('{'); //使用新的作用域进行解析 parse(scope); //传递右花括号作为参数调用advance并抛弃新作用域,恢复原来老的作用域 advance('}'); scope=oldScope; }
四、函数化
至此,上面我们看到的继承模式的一个弱点就是:没法包含隐私。对象的所有属性都是可见的。
应用模块模式,可以解决这个问题。
1、模块模式
从构造一个生成对象的函数开始。我们以小写字母开头来命名它,因为它并不需要使用new前缀。该函数包括4个步骤:
- 创建一个新对象。有很多的方式去构造一个对象
- 对象字面量构造
- new调用一个构造器函数
- Object.create方法构造一个已经尽的对象的新实例
- 调用任意一个会返回一个对象的函数
- 有选择性地定义私有实例变量和方法。这些就是函数中通过var 语句定义的普通变量。
- 给这个新对象扩充方法。这些方法拥有特权去访问参数,以及在第二步中通过var语句定义的变量。
- 返回那个新对象。
下面是一个函数化构造器的伪代码模板(加粗的文本表示强调):
var constructor =function(spec,my){ var that,其他私有实例变量; my=my||{}; 把共享的变量和函数添加到my中 that=一个新对象 添加给that的特权方法 return that; }
说明:
spec对象包含构造器需要构造一新实例的所有信息。spec的内容可能被复制到私有变量中,或者被其他函数改变,或者方法可以在需要的时候访问spec的信息。(一个简化的方式是替换spec为一个单一的值。当构造对象过程汇总并不需要整个spec对象的时候,这是有用的)
my对象是一个为继承链中的构造器提供秘密共享的容器。 my对象可以选择性地使用。如果没有传入一个my对象,那么会创建一个my对象。
接下来,声明该对象私有的实例变量和方法。 通过简单的声明变量就可以做到。构造器的变量和内部函数变成了该实例的私有成员。内部函数可以访问spec,my,that,以及其他私有变量。
接下来,给my变量添加共享的秘密成员。这是通过赋值语句来实现的:
my.member=value;
现在,我们构造了一个新对象并把它赋值给that。构造新对象可能是通过调用函数化构造器,传给它一个spec对象(可能就是传递给当前构造器的同一个spec对象)和my对象。my对象允许其他的构造器分享我们放到my中的资料。其他的构造器可能也会把自己可分享的秘密成员放进my对象里,以便我们的构造器可以利用它。
接下来,扩充that,加入组成该对象接口的特权方法。我们可以分配一个新函数称为that的成员方法。或者,更安全地,我们可以先把函数定义为私有方法,然后再把它们分配给that:
var methodical=function(){
...
};
that.methodical=methodical;
/*分开两步去定义methodical的好处是,如果其他方法想要调用methodical,它们可以直接调用methodical()而不是that.methodical()。 如果该实例被破坏或篡改,甚至that.methodical被替换掉了, 调用methodical的方法同样会继续工作,因为它们私有的methodical不受该实例被修改的影响。*/
2、应用
我们把这个模式应用到mammal例子里。此处不需要my,所以我们先抛开它,但会使用一个spec对象。
var mammal=function(spec){ var that={}; that.get_name=function(){ return spec.name; }; that.says=function(){ return spec.saying || ''; } return that; } var myMammal=mammal({name:'Herb'});
此时name就是私有属性,被保护起来了。
在伪类模式里,构造器函数Cat不得不重复构造器Mammal已经完成的工作。在函数化模式中那不再重要了,因为构造器Cat将会调用构造器Mammal,让Mammal去做对象创建中的大部分工作,所以Cat只需关注自身的差异即可。
var cat=function(spec){ spec.saying=spec.saying || 'meow'; var that=mammal(spec); that.purr=function(n){ var i,s=''; for(i=0;i<n;i++){ if(s){ s+='-'; } s+='r'; } return s; }; that.get_name=function(){ return that.says()+' '+spec.name+' '+that.says(); }; return that; } var myCat=cat({name:'Henrietta'});
函数化模式还给我们提供了一个处理父类方法的方法。
我们会构造一个superior方法,它取得一个方法名并返回调用那个方法的函数。该函数会调用原来的方法,尽管属性已经变化了。
/*有点难理解*/
Object.method('superior',function(name){ //传入方法名name var that=this,method=that[name]; return function(){ return method.apply(that,argumetns); } });
把调用superior应用在coolcat上, coolcat就像cat一样,除了它有一个更酷的调用父类cat的方法的get_name方法。
它只需要一点点准备工作。我们会声明一个super_get_name变量,并且把调用superior方法所返回的结果赋值给它。
var coolcat=function(spec){ //coolcat有一个更酷的调用父类cat的方法的get_name方法 var that=cat(spec); var super_get_name=that.superior('get_name'); that.get_name=function(n){ return 'like '+super_get_name()+'baby'; } return that; } var myCoolCat=coolcat({name:'Bix'}); var name=myCoolCat.get_name();//"like meow Bix meowbaby"
函数模块化有很大的灵活性。它相比伪类模式不仅带来的工作更少,还让我们得到更好的封装和信息隐藏,以及访问父类方法的能力。
/*有点难理解*/
如果对象的所有状态都是私有的,那么该对象就称为了一个“防伪(tamper-proof)对象” 。该对象的属性可以被替换或删除,但该对象的完整性不会受到损害。
如果我们用函数化的模式创建一个对象,并且该对象的所有方法都不使用this或that,那么该对象就是持久性(durable)的。
一个持久性的对象不会被入侵。访问一个持久性的对象时,除非有方法授权,否则攻击者不能访问对象的内部状态。
总结一下以上整个完美的继承链的代码:
<script> /* *****mammal object***** */ var mammal=function(spec){ var that={}; that.get_name=function(){ return spec.name; }; that.says=function(){ return spec.saying || ''; } return that; } //call var myMammal=mammal({name:'Herb'}); /* *****cat object***** */ var cat=function(spec){ spec.saying=spec.saying || 'meow'; var that=mammal(spec); that.purr=function(n){ var i,s=''; for(i=0;i<n;i++){ if(s){ s+='-'; } s+='r'; } return s; }; that.get_name=function(){ return that.says()+' '+spec.name+' '+that.says(); }; return that; } //call var myCat=cat({name:'Henrietta'}); /*user-defined Method*/ Function.prototype.method=function(name,func){ if(!this.prototype[name]){ this.prototype[name]=func; } return this; } Object.method('superior',function(name){ //传入方法名name var that=this,method=that[name]; return function(){ return method.apply(that,arguments); } }); /* *****coolcat object***** */ var coolcat=function(spec){ //coolcat有一个更酷的调用父类cat的方法的get_name方法 var that=cat(spec); var super_get_name=that.superior('get_name'); that.get_name=function(n){ return 'like '+super_get_name()+'baby'; } return that; } //call var myCoolCat=coolcat({name:'Bix'}); var name=myCoolCat.get_name();//"like meow Bix meowbaby" </script>
五、部件(Parts)
我们可以从一套部件中把对象组装出来。
例如,我们可以构造一个给任何对象添加简单事件处理特性的函数。它会给对象添加一个on方法,一个fire方法和一个私有的事件注册表对象:
<script> var eventuality=function(that){ var registry={}; //注册表 that.fire=function(event){ //在一个对象上触发一个事件。该事件可以是一个包含事件名称的字符串, //或者是一个拥有包含事件名称的type属性的对象。 //通过'on'方法注册的事件处理程序中匹配事件名称的函数将被调用 var array, func, handler, i, type=typeof event ==='string'?event:event.type; //如果这个事件存在一组事件处理程序,那么就遍历它们并按顺序依次执行。 if(registry.hasOwnProperty(type)) { array=registry[type]; for(i=0;i<array.length;i++){ handler=array[i]; //每个处理程序包含一个方法和一组可选的参数。 //如果该方法是一个字符串形式的名称,那么寻找到该函数。 func=handler.method; if(typeof func==='string'){ func=this[func]; } //调用一个处理程序。如果该条目包含参数,那么传递它们过去。否则,传递该事件对象。 func.apply(this,handler.paramenters || [event]); } } return this; } that.on=function(type,method,parameters){ //注册一个事件。构造一条处理程序条目。将它插入到处理程序数组中, //如果这种类型的事件还不存在,就构造一个。 var handler={ method:method, parameters:parameters }; if(registry.hasOwnProperty(type)){ registry[type].push(handler); }else{ registry[type]=[handler]; } return this; } return that; } </script>
我们可以在任何单独的对象上调用eventuality,授予它事件处理方法。 我们也可以赶在that被返回前在一个构造器函数中调用它。eventlity(that);
用这种方式,一个构造器函数可以从一套布局中把对象组装出来。JavaScript的弱类型在此处是一个巨大的优势,因为我们无须花费精力去了解对象在类型系统中的继承关系。相反,我们只需要专注于它们的个性特征。
如果我们想要eventuality访问该对象的私有状态,可以把私有成员集my传递给它。
参考:
https://www.zybuluo.com/zhangzhen/note/77227
本文作者starof,因知识本身在变化,作者也在不断学习成长,文章内容也不定时更新,为避免误导读者,方便追根溯源,请诸位转载注明出处:http://www.cnblogs.com/starof/p/4904929.html有问题欢迎与我讨论,共同进步。