你不知道的JavaScript-上卷-读书笔记-part3

书评

豆瓣 这本书很适合初级前端开发者上升至中级前端开发者,很好的阐述了JavaScript的闭包,原型,类,编译,赋值的问题.而且这还是上卷,还会有中卷,下卷,等等之类的.我会从这本书里选取一些比较重要的内容放在这篇文章当中(实际上这本书全部内容都重要). let’s do it

对象关联

[[protorype]]机制就是存在于对象中的一个内部链接,它会引用其他对象.当在对象本身上未找到属性则会继续顺着[[prototype]]关系的对象上进行查找

创建关联

var foo={ 
   something:function(){ 
       console.log("tell me ") 
   } 
} 

var bar=Object.create(foo); 
bar.something();//tell me 

Object.create会创建一个新对象(bar)并且把它关联到我们指定的对象(foo).Object.create(null)会创建一个null[[prototype]]链接的对象,由于这个对象没有原型链,所以用instanceof无法进行判断,总是会返回false.这样无[[prototype]]的对象通常被称为”字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据.

Object的polyfill(兼容)代码

if(Object.create){ 
   Object.create=function(o){ 
       function F(){} 
       f.prototype=o; 
       return new F(); 
   } 
} 

行为委托

如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在[[prototype]]关联的对象上进行查找.同理.如果在后者中也没有找到需要的引用就会继续查找它的[[prototype]],以此类推.这一系列对象的链接被称为原型链.换句话说.JavaScript这个机制的本质就是对象之间的关联关系.

委托理论

看代码

var Task={ 
   setId:function(){ 
       this.id=ID 
   }, 
   outputId:function(){ 
       console.log(this.id) 
   } 
} 
//让XYZ委托Task 
var XYZ=Object.prototype(Task); 

XYZ.prepareTask=function(ID,Label){ 
   this.setID(ID); 
   this.label=Label; 
} 

XYZ.outpurTaskDetails=function(){ 
   this.outputID(); 
   console.log(this.label); 
} 

ABC=Object.create(Task); 

这种编码风格称为”对象关联”,我们真正关系的只是XYZ对象(和ABC对象)委托了Task对象.如果是类设计的话 那我们会在XYZ这个类上面重写一次output但是这对象关联风格上并不需要,我们只需要委托即可对象关联风格还有一些不同之处

  1. 在上述代码中,id与label数据成员都是直接存储在XYZ上(而不是Task).通常来说在委托设计中最好把状态保存在委托者(XYZ,ABC)而不是委托目标(Task)上.

  2. 在类设计模式中,我们故意让父类(Task)和子类(XYZ)中都有outputTask方法,这样就可以利用重写(多态)的优势.但是在委托行为中恰好相反:我们会尽量避免在原型链中的不同级别使用相同的命名,否则就需要消除引用歧异.

  3. this.setID(ID);XYZ中的方法首先会寻找XYZ自身是否含有setID(),但是XYZ中并没有这个方法名,因此会通过原型链委托关联的Tas寻找.

在API接口设计中,委托最好在内部实现,不要直接暴露出去,在之前的例子中,我们并没有让开发者通过API直接调用XYZ.setID().相反我们把委托隐藏在了API内部

比较思维模式

下面是典型的(“原型”)面向对象风格

function Foo(who){ 
   this.me=who; 
} 

Foo.prototype.identify=function(){ 
   return "i am "+this.me; 
} 

function Bar(who){ 
   Foo.call(this,who);//耦合了 
} 

Bar.prototype=Object.create(Foo.protorype); 

Bar.prototype.speak=function(){ 
   console.log("hello"+this.identify()+"."); 
} 

var b1=new Bar("b1"); 
var b2=new Bar("b2"); 

b1.speak(); 
b2.speak(); 

子类Bar继承了父类Foo,然后生成了b1和b2两个实例.b1委托了Bar.protorype.后者委托了Foo.protorype

下面来看如何使用对象关联风格来编写功能完全相同的代码

Foo={ 
   init:function(){ 
       this.me=who 
   }, 
   identify:function(){ 
       return "i am"+this.me 
   } 
}; 

Bar=Object.create(Foo); 

Bar.speak=function(){ 
  console.log("hello"+this.identify()+"."); 
} 

var b1=Object.create(Bar); 
b1.init("b1"); 
var b2=Object.create(Bar); 
b2.init("b2"); 
b1.speak(); 
b2.speak(); 

这段代码中,我们同样利用原型链把b1委托给了Bar并把Bar委托给了Foo.我们只是把对象关联起来,并不需要哪些复杂的模仿类行为(构造函数,原型,new)

类与对象

控件”类”

下面用纯JavaScript实现类风格的代码

// 父类 
function Widget(width,height){ 
   this.width=width||50; 
   this.height=height||50; 
   this.$elem=null; 
} 

Widget.protorype.render=function($where){ 
   if(this.$elem){ 
       this.$elem.css({ 
           width:this.width+"px", 
           height:this.height+"px",     
       }).appendTo($where) 
   } 
} 

//子类 
function Button(width,height,label){ 
   //调用"super"构造函数 
   Widget.call(this,width,height); 
   this.label=label||"Default"; 

   this.$elem=$("\<button\>").text(this.label) 
} 

//让Button继承"Widget" 
Button.prototype=Object.create(Widget.protorype) 

//重写render(..) 
Button.protorype.render=function($where){ 
   //"super"调用 
   Widget.prototype.render.call(this,$where); 
   this.$elem.click(this.onClick.Bind(this)) 
} 

Button.prototype.onClcik=function(){ 
   console.log("Button"+this.label+"clicked") 
} 

$(document).ready(function(){ 
   var $body=$(document.body); 
   var btn1=new Button(125,30,"Hello"); 
   var btn2=new Button(150,40,"World"); 

   btn1.render($body); 
   btn2.render($body);     
}) 

在面向对象设计模式中,我们需要在父类中定义基础的render,然后在子类中重写它,子类并不会替换基础的render,只是添加一些按钮特有的行为.

委托控件对象

var Widget={ 
   init:function(){ 
       this.width=width||50; 
       this.height=height||50; 
       this.$elem=null; 
   }, 
   insert:function(){ 
       if(this.$elem){ 
       this.$elem.css({ 
               width:this.width+"px", 
               height:this.height+"px",     
           }).appendTo($where) 
       } 
   }, 
} 

var Button=Object.create(Widget); 

Button.setup=function(width,height,label){ 
   // 委托调用 
   this.init(width,height); 
   this.label=label||"Default"; 
   this.$elem.click(this.onClick.Bind(this)) 
} 

Button.build=function($where){ 
   //委托调用 
   this.insert($where) 
   this.$elem.click(this.onClick.bind(this)); 
} 

Button.onClick=function(evt){ 
  console.log("Button"+this.label+"clicked") 
} 

$(document).ready(function(){ 
   var $body=$(document.body); 
   var btn1=Object.create(Button); 
   btn1.setup(125,30,"Hello"); 

   var btn2=Object.create(Button); 
   btn2.setup(150,40,"World"); 

   btn1.build($body); 
   btn2.build($body);    
}) 

使用对象关联风格来编写代码时不需要把Widget和Button当做父类和子类.相反,Widget只是一个对象,包含一组通用的函数,任何类型的控件都可以委托,Button同样只是一个对象.

从设计模式的角度来说,我们并没有像类一样在两个对象中都定义了相同的方法名Render()相反,我们定义了两个更具有描述性的方法名.从语法角度来说,我们统一没有使用任何的构造函数,.protorype或者new,实际上也没有必要使用它们.

但是之前的一次调用new Button()变成了两次 var btn1=Object.create(Button);btn1.setup(125,30,"Hello");使用类构造函数的话,需要在同一个步骤实现构造和初始化,而然许多情况下把这两步分开更加灵活

更简洁的设计

在传统类的设计模式中,我们会把基础的函数定义在名为Controller的类中,然后派生两个子类LoginController和AuthController,它们都继承自Controller并且重写了一些基础行为

//父类 
function Controller(){ 
   this.errors=[]; 
}; 
Controller.prototype.showDialog=function(title,msg){ 
   //给用户显示标题和消息 
}; 
Controller.prototype.success=function(msg){ 
   this.showDialog("Sucess",msg); 
}; 
Controller.prototype.failure=function(err){ 
   this.errors.push(err); 
   this.showDialog("Error",err) 
}; 

//子类 
function LoginController(){ 
   Controller.call(this); 
} 

//把子类关联到父类 
LoginController.prototype=Object.create(Controller.prototype); 

LoginController.prototype.getUser=function(){ 
   return document.getElementById("login_username").value; 
}; 

LoginController.prototype.getPassword=function(){ 
   return document.getElementById("login_password").value 
} 

LoginController.prototype.validateEntry=function(user,pw){ 
   user=user||this.getUser(); 
   pw=pw||this.getPassword(); 

   if(!(user&&pw)){ 
       return this.failure("please enter a username & password!") 
   }else if(user.length<5){ 
       return this.failure("Password must be 5+ characters") 
   } 

   //如果执行到这里说明通过验证 
   return true; 
} 

//重写基础的failure() 
LoginController.prototype.failure=function(err){ 
   //"super"调用 
   Controller.prototype.failure.call(this,"Login invalid:"+err) 
} 

// 子类 
function AuthController(login){ 
   Controller.call(this); 
   //合成 
   this.login=login 
} 

// 把子类关联到父类 
AuthController.prototype=Object.create(Controller.protorype) 

AuthController.protorype.server=function(url,data){ 
   return $.ajax({ 
       url:url, 
       data:data 
   }) 
}; 

AuthController.prototype.checkAuth=function(){ 
   var user=this.login.getUser; 
   var pw=this.login.getPassword(); 

   if(this.login.validateEntry(user,pw)){ 
       this.server("/check-auth",{ 
           user:user, 
           pw:pw     
       }).then(this.success.bind(this)).fail(this.failure.bind(this)) 
   } 
} 

// 重写基础的success() 
AuthController.prototype.success=function(){ 
   // "super"调用 
   Controller.prototype.success.call(this,"Authenticated!"); 
} 

// 重写基础的failure() 
AuthController.prototype.failure=function(err){ 
   //"super"调用 
   Controller.prototype.failure.call(this,"Auth Failed:"+err) 
} 

var auth=new AuthController(); 
auth.checkAuth(new LoginController());//除了继承我们还需要合成 

所有控制器共享的基础行为是success(),failure和showDialog.子类LoginController和AuthController通过重写failure()和success来扩展默认基础类行为.此外,AuthController需要一个LoginController的实例来进行登录表单交互,因此这个实例变成了一个数据属性.另一个需要注意的是我们在基础的基础上进行了一些合成.AuthController需要使用LoginController,因此我们实例化后者并用一个类的成员属性this.login来引用它,这样AuthController就可以调用LoginController的行为.

反类设计

对象关联来设计

var LoginController={ 
   errors=[], 
   getUser:function(){ 
       return document.getElementById("login_username").value 
   }, 
   getPassword:function(){ 
       return document.getElementById("login_password").value 
   }, 
   validateEntry:function(user,pw){ 
       user=user||this.getUser(); 
       pw=pw||this.getPassword(); 
       if(!(user&&pw)){ 
           return this.failure{ 
               return this.failure("Please enter a username & password") 
           } 
       }else if(user.length<5){ 
           return this.failure("Password must be 5+ characters") 
       } 
       return false 
   }, 
   showDialog:function(title,msg){ 
       //给用户显示标题和消息 
   }, 
   failure:function(err){ 
       this.errors.push(err); 
       this.showDialog("Error","Login invalid"+err) 
   } 
}; 

//让 AuthController委托LoginController 
var AuthController=Object.create(LoginController); 

AuthController.errors=[]; 
AuthController.checkAuth=function(){ 
   var user=this.getUser(); 
   var pw=this.getPassword(); 

   if(this.validateEntry(user,pw)){ 
       this.server("/check-auth",{ 
           user:user, 
           pw:pw 
       }).then(this.accepted.bind(this)).fail(this.rejected.bind(this)) 
   } 
} 
AuthController.server=function(url,data){ 
   return $.ajax({ 
       url:url, 
       data:data 
   }) 
} 
AuthController.accepted=function(){ 
   this.showDialog("Success","Authenticated!") 
} 
AuthController.rejected=function(err){ 
   this.showDialog("Auth Failed"+err) 
} 

由于AuthController只是一个对象,LoginController也一样.因为我们不需要实例化,只需要一行代码 AuthController.checkAuth()借助对象关联可以简单的向委托链上添加一个或者多个对象,而且同样不需要实例化

var controller1=Object.create(AuthController); 
var controller2=Object.create(AuthController); 

在行为委托模式中,AuthController和LoginController只是对象,他们之间是兄弟关系,并不是父类和子类关系,代码中AuthController委托了LoginController,反向委托也没问题.这样我们避免了需要新建一个controller来共享两个实体之间的行为,因为委托足以满足我们需要的功能,并且也不需要实例化,因为它们根本不是类,他们只是对象,此外,我们也不需要合成,因为两个对象可以通过委托进行合作我们用一种极其简单的设计实现了相同的功能,这就是对象关联风格代码和行为委托设计模式的力量

笔记总结

..码字好累..这本书特别好,尤其是对象关联风格模式代码.后续的博文我会尝试着用这种风格去实现设计模式