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

书评

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

对象关联

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

创建关联

1
2
3
4
5
6
7
8
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(兼容)代码

1
2
3
4
5
6
7
if(Object.create){
Object.create=function(o){
function F(){}
f.prototype=o;
return new F();
}
}

行为委托

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

委托理论

看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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内部

比较思维模式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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实现类风格的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 父类
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,只是添加一些按钮特有的行为.

委托控件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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并且重写了一些基础行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//父类
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的行为.

反类设计

对象关联来设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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()
借助对象关联可以简单的向委托链上添加一个或者多个对象,而且同样不需要实例化

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

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

笔记总结

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