JavaScript 中是没有类的,class 是通过 [[Prototype]] 原型链模拟出来的。
代码组织方式:【对象关联】VS【类和继承】
对象关联(对象之间互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。两种设计模式体现了两种编码风格,对象关联 VS 类和继承。
设计模式1:行为委托
观点:JavaScript 中有个很重要的机制,这个机制的本质就是【对象之间的关联关系】。
这个观点对于理解【行为委托】是非常基础并且非常重要的。
【行为委托】,比起【类和继承】是另一种更少见但是更强大的设计模式。
这里说的【类和继承】指的就是面向对象。
“你是我的原型吗?”
使用对象关联时,所有的对象都是通过 [[Prototype]] 委托互相关联,下面是内省的方法,
非常简单:
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
var b1 = Object.create( Bar );
//-----------------------------
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
//-----------------------------
// 让 b1 关联到 Foo 和 Bar
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
上面说到的机制是哪个机制?
[[Prototype]]机制。
这个机制是什么?
[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象。
具体来说就是,如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
显式原型:prototype
隐式原型:__proto__ 另一种表示方式:[[Prototype]]
__proto__ 是每个对象都具有的属性
prototype 是 Function 独有的属性
function Foo() {}
var a1 = new Foo();
a1; // Foo {}
Foo {} 是 Chrome 浏览器的返回结果,谷歌浏览器表达的是,由名为 Foo 的函数构造的对象。
FireFox 的返回结果是 Object {} ,火狐浏览器表达的是由名为 Object 的函数构造了一个空对象,意思是“Object() 构造出的对象”。
设计模式2:面向对象
面向对象的设计模式【类的设计模式】,更多的是面向类。
一个 class 作为一种类型,定义了该类型的对象的属性和行为。
class 是 OOP 的实现核心,具体功能由该类型的具体对象承载。
面向对象语言中的类的主要特征:
封装:它提供了数据与相应方法结合的实现。class 封装和组织其内部的状态和相应的过程。通过调用类提供的方法来获取内部状态,调用方法的对对象内部状态进行修改(通过方法的副作用)。封装隐藏了方法的具体实现,仅通过调用方法对对象进行输入输出,避免了大型工程中的不同状态和逻辑的混乱。提高了工程的组织性,也提高了生产效率。
继承: 通过使用现有类拓展产生新的类,在保留父对象的特性的同时对父对象进行拓展,大大提高了代码的复用。通过继承产生子类的过程一般尊从一般到特殊的逻辑关系。通过继承可以将设计阶段划分,不同层次的类实现不同层次的功能,层次化的结构利于工程的管理和展开。
多态: 我认为多态性是 OOP 的核心。多态使得对象对于同一消息能有不同的应答方式。通过继承一个基类,覆盖,重载其方法,父对象就可以在不修改代码的同时根据当前它的子对象的特性以不同的方式运作。可以增加代码的灵活性和扩展性。
面向对象设计模式的代码示例
下面这段代码展示的是如何在不使用任何“类”辅助库或者语法的情况下,使用纯 JavaScript 实现类风格的代码:
// 父类
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.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.prototype );
// 重写 render(..)
Button.prototype.render = function($where) {
// “super”调用
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
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(..)
,只是添加一些按钮特有的行为。
可以看到代码中出现了丑陋的显式伪多态,即通过 Widget.call
和 Widget.prototype.render.call
从“子类”方法中引用“父类”中的基础方法。呸!
class 语法糖
这里简单介绍一下如何使用 class 来实现相同的功能:
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
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 );
} );
毫无疑问,使用 ES6 的 class 之后,上一段代码中许多丑陋的语法都不见了,super(…)
函数棒极了。(尽管深入探究就会发现并不是那么完美!)
尽管语法上得到了改进,但实际上这里并没有真正的类,class 仍然是通过 [[Prototype]]
机制实现的。
无论你使用的是传统的原型语法还是 ES6 中的新语法糖,你仍然需要用“类”的概念来对
问题(UI 控件)进行建模。这种做法会为你带来新的麻烦。
行为委托设计模式的代码示例
下面的例子使用对象关联风格委托来更简单地实现 Widget/Button:
var Widget = {
init: function(width,height){
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function($where){
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 = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
行为委托 | 177
// 委托调用
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同样只是一个对。(当然,它会通过委托关联到Widget!)
从设计模式的角度来说,我们并没有像类一样在两个对象中都定义相同的方法名render(..)
,相反,我们定义了两个更具描述性的方法名(insert(..)
和build(..)
)。同理,初始化方法分别叫作init(..)
和setup(..)
。
在委托设计模式中,除了建议使用不相同并且更具描述性的方法名之外,还要通过对象关联避免丑陋的显式伪多态调用(Widget.call
和Widget.prototype.render.call
),代之以简单的相对委托调用this.init(..)
和this.insert(..)
。
从语法角度来说,我们同样没有使用任何构造函数、.prototype
或new
,实际上也没必要使用它们。
如果你仔细观察就会发现,之前的一次调用(varbtn1=newButton(..)
)
现在变成了两次(varbtn1=Object.create(Button)
和btn1.setup(..)
)。
乍一看这似乎是一个缺点(需要更多代码)。
但是这一点其实也是对象关联风格代码相比传统原型风格代码有优势的地方。为什么呢?
使用类构造函数的话,你需要(并不是硬性要求,但是强烈建议)在同一个步骤中实现构造和初始化。然而,在许多情况下把这两步分开(就像对象关联代码一样)更灵活。
举例来说,假如你在程序启动时创建了一个实例池,然后一直等到实例被取出并使用时才执行特定的初始化过程。这个过程中两个函数调用是挨着的,但是完全可以根据需要让它们出现在不同的位置。
对象关联可以更好地支持关注分离(separation of concerns)原则,创建和初始化并不需要合并为一个步骤。
强烈推荐的设计模式:【行为委托】
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。
JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。
也就是说,我们可以选择在 JavaScript 中努力实现类机制,也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。
对象关联(对象之间互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。
对象关联可以用基于 [[Prototype]] 的【行为委托】非常自然地实现。
总地来说,在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患。
对象关联示例代码: 能不能使用对象关联风格的行为委托来实现更简单的设计呢?当然可以!
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(
"Please enter a username & password!"
);
}
else if (user.length < 5) {
return this.failure(
"Password must be 5+ characters!"
);
}
// 如果执行到这里说明通过验证
return true;
},
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.failure( "Auth Failed: " + err );
};
由于 AuthController
只是一个对象(LoginController
也一样),因此我们不需要实例化(比如 new AuthController()
),只需要一行代码就行:
AuthController.checkAuth();
借助对象关联,你可以简单地向委托链上添加一个或多个对象,而且同样不需要实例化:
var controller1 = Object.create( AuthController );
var controller2 = Object.create( AuthController );
在行为委托模式中,AuthController
和 LoginController
只是对象,它们之间是兄弟关系,并不是父类和子类的关系。
代码中 AuthController
委托了 LoginController
,反向委托也完全没问题。
这种模式的重点在于只需要两个实体(LoginController
和 AuthController
),而之前的模式(类和继承)需要三个。
我们不需要 Controller
基类来“共享”两个实体之间的行为,因为委托足以满足我们需要的功能。
同样,前面提到过,我们也不需要实例化类,因为它们根本就不是类,它们只是对象。
此外,我们也不需要合成,因为两个对象可以通过委托进行合作。
最后,我们避免了面向类设计模式中的多态。我们在不同的对象中没有使用相同的函数名success(..)
和 failure(..)
, 这样就不需要使用丑陋的显式伪多态。 相反,在 AuthController
中它们的名字是 accepted(..)
和 rejected(..)
——可以更好地描述它们的行为。
总结:我们用一种(极其)简单的设计实现了同样的功能,这就是对象关联风格代码和行为委托设计模式的力量。
在 ES6 中 我 们 可 以 在 任 意 对 象 的 字 面 形 式 中 使 用 简 洁 方 法 声 明(concise method declaration),所以对象关联风格的对象可以这样声明(和 class 的语法糖一样):
var LoginController = {
errors: [],
getUser() { // 妈妈再也不用担心代码里有 function 了!
// ...
},
getPassword() {
// ...
}
// ...
};
唯一的区别是对象的字面形式仍然需要使用“,”来分隔元素,而 class 语法不需要。这个区别对于整体的设计来说无关紧要。
此外,在 ES6 中,你可以使用对象的字面形式(这样就可以使用简洁方 法定义)来改写之前繁琐的属性赋值语法(比如 AuthController 的定 义),然后用Object.setPrototypeOf(..)
来修改它的 [[Prototype]]:
// 使用更好的对象字面形式语法和简洁方法
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url,data) {
// ...
}
// ...
};
// 现在把 AuthController 关联到 LoginController
Object.setPrototypeOf( AuthController, LoginController );
使用 ES6 的简洁方法可以让对象关联风格更加人性化(并且仍然比典型的原型风格代码更加简洁和优秀)。你完全不需要使用类就能享受整洁的对象语法!
【不考虑自我引用的版本】
更简洁的设计之完整代码:
// 使用简洁方法
var LoginController = {
errors: [],
getUser() {
return document.getElementById(
"login_username"
).value;
},
getPassword() {
return document.getElementById(
"login_password"
).value;
},
validateEntry(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;
},
showDialog(title,msg) {
// 给用户显示标题和消息
},
failure(err) {
this.errors.push( err );
this.showDialog( "Error", "Login invalid: " + err );
}
};
// 使用更好的对象字面形式语法和简洁方法
var AuthController = {
errors: [],
checkAuth() {
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 ) );
}
},
server(url,data) {
return $.ajax( {
url: url,
data: data
} );
},
accepted() {
this.showDialog( "Success", "Authenticated!" )
},
rejected(err) {
this.failure( "Auth Failed: " + err );
}
};
// 现在把 AuthController 关联到 LoginController
Object.setPrototypeOf( AuthController, LoginController );
【需要自我引用的版本】
简洁方法有一个非常小但是非常重要的缺点:
自我引用更难!因为会出现匿名函数。
如果你需要自我引用的话,那最好使用传统的具名函数表达式来定义对应的函数( · baz: function baz(){…}· ),不要使用简洁方法。
// 传统的具名函数表达式
var LoginController = {
errors: [],
getUser: function getUser() {
return document.getElementById(
"login_username"
).value;
},
getPassword: function getPassword() {
return document.getElementById(
"login_password"
).value;
},
validateEntry: function validateEntry(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;
},
showDialog: function showDialog(title,msg) {
// 给用户显示标题和消息
},
failure: function failure(err) {
this.errors.push( err );
this.showDialog( "Error", "Login invalid: " + err );
}
};
// 使用更好的对象字面形式语法和传统的具名函数表达式
var AuthController = {
errors: [],
checkAuth: function checkAuth() {
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 ) );
}
},
server: function server(url,data) {
return $.ajax( {
url: url,
data: data
} );
},
accepted: function accepted() {
this.showDialog( "Success", "Authenticated!" )
},
rejected: function rejected(err) {
this.failure( "Auth Failed: " + err );
}
};
// 现在把 AuthController 关联到 LoginController
Object.setPrototypeOf( AuthController, LoginController );
以上内容摘自《你不知道的 JavaScript》