JS進階學習筆記-設計模式
本文絕大多數(shù)概念是基于ES5之前的,本人認為這些都是學習或者掌握ES6必須要理解的坑律,因為ES6其實就是對ES5之前的方法的包裝,理解透徹ES5之前的概念,那么學ES6就極其簡單柴淘。
1. 初窺
1.1 靈活的JS
JS極其靈活,對于同樣一段業(yè)務代碼秘通,有的可以寫的很復雜为严,有的卻可以寫的很簡單。
例如:啟動和停止一個動畫
面向過程方式:
// 開始
function startAnimation() {
console.log("start...")
}
// 結(jié)束
function stopAnimation(){
console.log("stop...")
}
// 調(diào)用
startAnimation();
stopAnimation()
面向?qū)ο蠓绞揭唬?/p>
// 創(chuàng)建一個對象
var Animation = function(){}
Animation.prototype.start = function(){
console.log("start...")
}
Animation.prototype.stop = function(){
console.log("stop...")
}
// 使用
var test = new Animation();
test.start();
test.stop();
面向?qū)ο蠓绞蕉?/p>
var Animation = function(){}
Animation.prototype = {
start: function(){
console.log("start...")
},
stop: function(){
console.log("stop...")
}
}
更進一步:
// 在Function上增加方法
Function.prototype.method = function(name, fn){
this.prototype[name] = fn;
return this; // 為了可以鏈式調(diào)用
}
var Animation = function(){};
Animation.method('start', function(){
console.log("start...");
return this;// 為了可以鏈式調(diào)用
})
Animation.method('stop',function(){
console.log('stop...');
})
// 使用
var test = new Animation();
// 鏈式調(diào)用
test.start().stop();
new Animation().start().stop();
這里加了返回值return this
,是便于鏈式調(diào)用肺稀,stop()
中沒有加第股,是為了考慮邏輯,即new Animation().stop().start(); `是錯誤的话原。
對于在原型prototype
上增加的方法夕吻,如Function.prototype.method
等,必須使用new
關(guān)鍵字繁仁,才能調(diào)用涉馅。后續(xù)會講解。
1.2 JS是弱類型語言
在js中定義變量黄虱,不像java控漠,是不需要聲明其類型。但是,這并不意味著js中沒有類型盐捷,其實偶翅,js中變量在一初始化的時候就決定了變量的類型,再給變量賦予不同的類型時碉渡,變量對應的類型就隨之改變聚谁。
也就是說,js中的變量會根據(jù)所賦予的值而改變類型滞诺。
類型 | 分類 | 內(nèi)存位置 | typeof 輸出 |
---|---|---|---|
boolean布爾 | 基本數(shù)據(jù)類型(又稱原始數(shù)據(jù)類型) | 棧 | boolean |
number數(shù)字 | 基本數(shù)據(jù)類型(NAN也屬于number) | 棧 | number |
string字符串 | 基本數(shù)據(jù)類型 | 棧 | string |
null空 | 基本數(shù)據(jù)類型 | 棧 | object |
undefined未定義 | 基本數(shù)據(jù)類型 | 棧 | undefined |
symbol獨一無二的值 | 基本數(shù)據(jù)類型 | 棧 | symbol |
Object對象 | 引用數(shù)據(jù)類型(function形导,array...) | 堆,棧中保存引用的地址 | function习霹、object |
一般朵耕,可以使用typeof
來判斷數(shù)據(jù)類型,但是并不準確淋叶。為了精確判斷阎曹,建議使用這個方法:Object.prototype.toString.call(要判斷的變量)
,它返回如:"[object Function]"
1.3 函數(shù)是一等公民
在JS中煞檩,函數(shù)是一等公民(First-class Function)处嫌。它可以存儲在變量中,可以作為參數(shù)傳給其他函數(shù)斟湃,可以作為返回值從其他函數(shù)傳出熏迹,還可以運行時進行構(gòu)造。
-
匿名函數(shù)自執(zhí)行(IIFE)凝赛,又稱立即調(diào)用函數(shù)表達式注暗。
// 無參 (function(){ var x = 3; var y = 7; console.log(x*y) })() // 有參 (function(x,y){ console.log(x*y) })(3,7)
除了使用
()
包裹的方式墓猎,其實還有~function(){...}()
或!function(){...}()
等方式友存,都可以實現(xiàn)自執(zhí)行。一般建議使用括號的方式陶衅,安全易讀。
1.4 對象的易變性
JS雖然不像java是強制面向?qū)ο缶幊陶Z言直晨,但是在JS中一切都是對象(基本數(shù)據(jù)類型搀军,也可以包裝成對象),而且所有的對象都是異變的勇皇。例如罩句,給函數(shù)添加屬性:
function test(){
test.num++;
console.log(test.num)
}
test.num = 10;
test(); // 輸出 11
這意味著,我們可以對先前定義的類和實例化的對象進行修改敛摘,例如:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function(){
return this.name
},
getAge: function(){
return this.age
}
}
// 實例化對象
var tom = new Person('Tom',12);
var jerry = new Person('jerry',12);
// 給對象新增方法
Person.prototype.sayHi = function() {
return 'Hi, ' + this.getName() + '!';
}
jerry.sayHello = function(){
return 'Hello, ' + this.getName() + '!';
}
tom.sayHi(); // Hi, Tom门烂!
jerry.sayHi(); // Hi, Jerry!
jerry.sayHello(); // Hello, Jerry!
tom.sayHello(); // Cannot set property 'sayHello' of undefined
這里,我們在實例化Person
對象之后新增了sayHi
方法,并且成功調(diào)用屯远,即我們可以擴展實例化之后的對象蔓姚。通過prototype
我們給所有的實例都新增了sayHi
的方法。這里需要理解繼承與原型鏈,后面會講慨丐。
每個實例對象( object )都有一個私有屬性(稱之為 proto )指向它的構(gòu)造函數(shù)的原型對象(prototype )坡脐。該原型對象也有一個自己的原型對象( proto ) ,層層向上直到一個對象的原型對象為 null
房揭。根據(jù)定義备闲,null
沒有原型,并作為這個原型鏈中的最后一個環(huán)節(jié)捅暴。
幾乎所有 JavaScript 中的對象都是位于原型鏈頂端的 Object
的實例恬砂。
1.5 繼承
JS中使用的是基于對象的繼承,即原型式(prototype)繼承蓬痒。比如泻骤,Function B
繼承Function A
:
- 類型被定義在
.prototype
中 - 用
Object.create()
來繼承
function A(){};
A.prototype = {
varA: "AAA",
doSomething: function(){
console.log("AAA...")
}
}
function B(){};
B.prototype = Object.create(A.prototype, {
varB: {
value: "BBB",
enumerable: true,
configurable: true,
writable: true
},
doSomething : {
value: function(){ // override覆寫
// A.prototype.doSomething.apply(this, arguments);
console.log("BBB...")
},
enumerable: true,
configurable: true,
writable: true
}
})
B.prototype.constructor = B;
var b = new B();
b.doSomething(); // BBB...
console.log(b.varA); // AAA
console.log(b.varB); // BBB
還可以模仿基于類(class)的繼承:ECMAScript6 引入了一套新的關(guān)鍵字用來實現(xiàn) class,但JavaScript 仍然基于原型,這里只不過相當于語法糖乳幸,背后的實現(xiàn)還是原型式繼承瞪讼。這些新的關(guān)鍵字包括 class
, constructor
,static
粹断,extends
和 super
符欠。
"use strict";
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Student extends Person{
// 構(gòu)造方法
constructor(name,age,score){
// super可以認為指代父類
super(name,age)
// 擴展
this.score = score
}
get getScore() {
return this.score;
}
set setScore(score) {
this.score = score
}
}
這里只是簡單的了解了一下JS中的繼承,后面會深入研究瓶埋。
1.6 JS中的設計模式
JS是如此的靈活希柿,有如此的簡單與復雜,為了更加方便維護代碼养筒,寫出可重用的代碼曾撤,還是得使用設計模式椎眯。
可維護性潦闲,設計模式有利于降低模塊間的耦合,便于開發(fā)和維護煌抒。
易于表達巫湘,使用不同的設計模式装悲,可以清晰的表達不同業(yè)務邏輯的實現(xiàn)意圖。
提升性能尚氛,使用設計模式诀诊,在某些程度上可以減少代碼量。
2. <a name="interface">接口</a>
“函數(shù)要面向接口阅嘶,而不是實現(xiàn)” —— GoF《設計模式》
2.1 接口的概念
接口可以規(guī)定一個對象中應該具有哪些屬性和方法属瓣,但不用具體實現(xiàn)這些方法载迄,實現(xiàn)了這些接口的對象,必須實現(xiàn)這項屬性或方法抡蛙。也就是說接口的出現(xiàn)給對象增加了標準护昧。
2.2 接口的優(yōu)缺點
優(yōu)點:
- 增加類的重用性。比如A和B均實現(xiàn)了接口C溜畅,若我會使用A類捏卓,那么B類也很容易就會使用了;或者我熟悉C接口慈格,不管誰實現(xiàn)了這個接口怠晴,我就能很容易去使用它。
- 增加代碼的可讀性浴捆。比如我事先吧接口文檔寫好蒜田,然后實現(xiàn)了一些程序,當其他程序員來接手時选泻,只需要看接口文檔冲粤,就能很容易知道我的程序中類的特性,從而更容易更改程序以及實現(xiàn)自己的業(yè)務邏輯页眯。
- 增加代碼的可維護性梯捕,接口可以降低對象間的耦合度。利用接口窝撵,各個組件之間只暴露少許接口共其他組件調(diào)用傀顾,如果修改了某個組件,并不會影響其他組件碌奉。
- 測試和調(diào)試更方便短曾。如果一些類都實現(xiàn)了某個接口,但是卻沒能完全繼承接口中定義的標準赐劣,程序很容易就被發(fā)現(xiàn)嫉拐;如果業(yè)務擴展,接口需要新增一個方法魁兼,那么實現(xiàn)了這些接口的類婉徘,如果忘了添加這個方法,也會報錯咐汞。
缺點:
- JS是弱類型語言盖呼,接口的使用原則會強化類型的作用,降低JS語言的靈活性碉考。
- 接口一旦被定義,如果再次改動挺身,需要把所有實現(xiàn)了該接口的類改動侯谁,比如,某些類不需要這個方法,就會增加額外的開銷墙贱。
- JS沒有提供對接口的內(nèi)置支持热芹,就算某些類實現(xiàn)了這個接口,如果不遵循接口的標準惨撇,并且不加嚴格的限制與檢查伊脓,是無法保證接口的實現(xiàn)的。
2.3 JS模仿接口
2.3.1 用注釋描述接口
用注釋描述接口是最簡單也是效果最差的方法魁衙。即使用interface 和 implements關(guān)鍵字表示接口报腔,但是將他們寫在注視中,因為JS并不認識剖淀,避免與法錯誤纯蛾。
通俗地講,注釋中告知纵隔,該類需現(xiàn)了這些接口翻诉,需要定義這些方法:
/* 定義接口
接口一
interface Composite{
function add(child);
function remove(child);
function getChild(index);
}
接口二
interface FormItem{
function save();
}
*/
var CompositeForm = function(id, method, action){// implements Composite,FormItem
};
// 實現(xiàn)接口Composite
CompositeForm.prototype.add = function(child){console.log("add...")}
CompositeForm.prototype.remove = function(child){console.log("remove...")}
CompositeForm.prototype.getChild = function(index){console.log("getChild...")}
// 實現(xiàn)接口FormItem
CompositeForm.prototype.save = function(){console.log("save...")}
呵,有點東施效顰的味道捌刮。這個方式不能保證完全實現(xiàn)了接口碰煌,就算不按照接口來,也不會報錯绅作,對測試和調(diào)試也無幫助芦圾。但是勝在簡單,也確是實現(xiàn)了接口的意圖棚蓄。
2.3.2 用屬性檢查模仿接口
其實堕扶,這種方式就是在上面的方式基礎(chǔ)上,增加了屬性判斷梭依,判斷類中實現(xiàn)了那些接口稍算,稍微嚴謹一些。接口仍然寫在注釋中役拴。
var CompositeForm = function(id,method,action){
// 顯示地申明糊探,本類實現(xiàn)了如下接口
this.implementsInterfaces = ['Composite', 'FormItem'];
}
// 定義一個添加實例的方法,并檢測實例是否實現(xiàn)了接口
function addForm(formInstance) {
// 判斷實例是否實現(xiàn)了接口河闰,否則拋出錯誤
if(!implements(formInstance, 'Composite','FormItem')){
throw new Error('Object does not implements a required interface!');
}
}
// 定義判斷是否實現(xiàn)接口的方法
function implements(object) {
for (var i = 1; i < arguments.length; i++) {
var interfaceName = arguments[i];
var interfaceFound = false;
// 遍歷implementsInterfaces科平,看是否實現(xiàn)了對應的接口
for (var j = 0; j < object.implementsInstances.length; j ++) {
if (object.implementsInterfaces[j] == interfaceName) {
interfaceFound = true;
break;
}
}
if(!interfaceFound){
return false; // 沒有實現(xiàn)接口
}
}
return true; // 實現(xiàn)了接口
}
這里CompositeForm
說他實現(xiàn)了某某接口,那就判斷一下姜性,發(fā)現(xiàn)確實實現(xiàn)了瞪慧,OK,否則就會跑出錯誤部念。但是這個簡陋的判斷并沒有保證弃酌,完全實現(xiàn)接口中定義的方法氨菇,所以還是會埋下隱患。
2.3.3 用鴨式辯型模仿接口
“像鴨子一樣走路并且嘎嘎叫的就是鴨子” —— James Whitcomb Riley鴨子類型
基于這種思想:類是否申明自己支持哪些接口并不重要妓湘,只要它具有這些接口中的方法就行查蓉。即如果對象具有與接口定義的方法同名的所有方法,那么就可以認為它實現(xiàn)了這個接口榜贴。
var Interface = function(name, methods) {
// 保證傳入方法名和方法集合豌研,兩個參數(shù)
if (arguments.length !== 2) {
throw new Error(
"Interface constructor called with " + arguments.length + "arguments, but expected exactly 2."
);
}
this.name = name;
this.methods = [];
for (var i = 0, len = methods.length; i < len; i++) {
// 方法名類型string
if (typeof methods[i] !== "string") {
throw new Error(
"Interface constructor expects method names to be passed in as a string."
);
}
this.methods.push(methods[i]);
}
};
// 定義static方法,可以直接通過類名調(diào)用而不用實例化
Interface.ensureImplements = function(object) {
if (arguments.length < 2) {
throw new Error(
"Function Interface.ensureImplements called with " + arguments.length + " arguments, but expected at least 2."
);
}
for (var i = 1, len = arguments.length; i < len; i++) {
var interface = arguments[i];
if (interface.constructor !== Interface) {
throw new Error(
"Function Interface.ensureImplements expects arguments two and above to be instance of Interface."
);
}
for (var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
var method = interface.methods[j];
if (!object[method] || typeof object[method] !== "function") {
// 拋出錯誤:沒有實現(xiàn)接口以及方法沒有找到
throw new Error(
"Function interface.ensureImplements:object does not implements the " +
interface.name + "interface. Method " + method + " was not found"
);
}
}
}
};
上面的類定義了接口以及接口的檢測唬党,可以作為工具類使用鹃共。
// 使用:
// 定義接口,以及接口中的方法
var Composite = new Interface("Composite", ["add", "remove", "getChild"]);
var FormItem = new Interface("FormItem", ["save"]);
var CompositeForm = function(id, method, action) {
// implements Composite,FormItem
};
var myForm = new CompositeForm();
// 實現(xiàn)接口
myForm.prototype.add = function(){}
myForm.prototype.remove = function(){}
// 檢測實例是否完全實現(xiàn)了接口
function addForm(formInstance) {
Interface.ensureImplements(formInstance, Composite, FormItem);
// 其他操作
}
addForm(myForm);
Interface.ensureImplements
方法會對實例嚴格檢測初嘹,如果實例中沒有完全實現(xiàn)對應的接口就會拋出錯誤及汉。這樣,我們就實現(xiàn)了接口的概念屯烦。
2.3.4 依賴于接口的設計模式
- 工廠模式
- 組合模式
- 裝飾者模式
- 命令模式
3. 封裝
封裝(encapsulation)就是將不必要暴露的方法或?qū)傩噪[藏起來坷随,不讓外部方法訪問,以防止篡改或濫用驻龟,降低代碼間的耦合度温眉,提升安全性。
JS中沒有像Java中的關(guān)鍵字private翁狐,實現(xiàn)封裝的方法——閉包(closure)类溢。
3.1 類中屬性的私有化
在類中,一般使用下劃線開頭來定義私有屬性露懒,這是總所周知的命名規(guī)范闯冷,它表明一個屬性(或方法)僅提供在對象內(nèi)部使用。
注意懈词,這種方式只是一種約定蛇耀,并不能強制性決定外部不能訪問。
// 定義一個三角形類坎弯,構(gòu)造方法
var Triangle = function(x,y,z){
this.setX(x);
this.setY(y);
this.setZ(z);
}
// 判斷能否構(gòu)成三角形纺涤,靜態(tài)方法,通過類名調(diào)用抠忘,不用new實例化
Triangle.isTriangle = function(x,y,z){
return x+y>z && x+z>y && y+z>x;
}
// 實例方法撩炊,必須new之后才能調(diào)用
Triangle.prototype = {
setX: function(x){
this._x = x;
},
setY: function(y){
this._y = y;
},
setZ: function(z){
this._z = z;
},
getX: function(){
if(!Triangle.isTriangle(this._x,this._y,this._z)){
throw new Error('不能構(gòu)成三角形!')
}
return this._x;
},
getY: function(){
if(!Triangle.isTriangle(this._x,this._y,this._z)){
throw new Error('不能構(gòu)成三角形!')
}
return this._y;
},
getZ: function(){
if(!Triangle.isTriangle(this._x,this._y,this._z)){
throw new Error('不能構(gòu)成三角形!')
}
return this._z;
}
}
這樣一來,雖然我們傳進去的參數(shù)是x,y,z崎脉,我們是直接獲取不到的拧咳,可以通過get方法獲取。
var san = new Triangle(3,4,5);
san.x; // undefined;
san.getX(); // 3
san._x; // 3
但是囚灼,我們其實是可以通過_x
獲取的骆膝。加上下劃線只是表明這些屬性是私有的砾淌,最好不要直接訪問。
3.2 作用域谭网、嵌套函數(shù)和閉包
在JS中,只有函數(shù)具有作用域赃春。即愉择,在一個函數(shù)內(nèi)部聲明的變量在函數(shù)外部通常是無法訪問的。
function outer(){
var a=10;
function inner(){
a *= 2;
}
inner();
return a;
}
outer(); // 20
outer(); // 20
a; // undefined
這里织中,在函數(shù)內(nèi)部定義函數(shù)锥涕,內(nèi)部函數(shù)訪問了外部函數(shù)定義的變量a,函數(shù)外面是訪問不到變量a的狭吼。即层坠,函數(shù)內(nèi)部有局部作用域。那么刁笙,如果想訪問變量a呢?
function outer(){
var a=10;
// 將內(nèi)部函數(shù)返回
return function(){
a *= 2;
return a;
};
}
var test = outer();
test(); // 20
test(); // 40
test(); // 60
var demo = outer();
demo(); // 20
這樣就形成了閉包破花。閉包是函數(shù)和聲明該函數(shù)的詞法環(huán)境的組合。
閉包疲吸,即通過函數(shù)內(nèi)部定義函數(shù)座每,在函數(shù)外面通過訪問這個內(nèi)部函數(shù)(可以訪問外部函數(shù)的定義的變量)從而實現(xiàn)了操作函數(shù)內(nèi)的局部變量的現(xiàn)象。
閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁摘悴。通過閉包峭梳,可以訪問函數(shù)內(nèi)的局部變量。
var Triangle = (function(){
// private static attributes 私有靜態(tài)屬性
var id = 0;
// private static method 私有靜態(tài)方法
function yy(){
console.log('I am the best tiangle!')
}
yy();
// return constructor 返回構(gòu)造方法
return function(x,y,z){
// private attributes 私有屬性
var _x,_y,_z;
console.log(this);
// privileged methods 特權(quán)方法
this.getX = function(){
return _x;
}
this.setX = function(x){
_x = x;
}
this.getY = function(){
return _y;
}
this.setY = function(y){
_y = y;
}
this.getZ = function(){
return _z;
}
this.setZ = function(z){
_z = z;
}
this.getId = function(){
return id;
}
// Constructor code 構(gòu)造區(qū)代碼
id++; // 統(tǒng)計Triangle實例化對象個數(shù)
this.setX(x);
this.setX(y);
this.setX(z);
}
})()
// public static method 公有靜態(tài)方法
Triangle.isTriangle = function(x,y,z){
return x+y>z && x+z>y && y+z>x;
}
// public no-privileged methods 公有非特權(quán)方法
Triangle.prototype = {
}
// 判斷能不能構(gòu)成三角形
if(Triangle.isTriangle(3,4,5)){
var t1 = new Triangle(3,4,5)
t1.getId(); // 1
t1.getX(); // 3
var t2 = new Triangle(3,4,5);
t1.getId(); // 2
}
下面蹂喻,我們來理解一下這些“新概念”:
var A = (function(){
// 這里定義私有屬性和方法
var haha = "我沒有鄙視你!反正你也看不到葱椭!";
function hehe(){
console.log(haha);
}
hehe();
// 返回構(gòu)造方法
return function(x){
// 定義私有屬性
var _x;
// 定義特權(quán)方法,外部通過特權(quán)方法訪問和操作內(nèi)部私有屬性
this.getX = function(){
return _x;
}
this.setX = function(x){
_x = x;
}
// 通過特權(quán)方法口四,不僅可以訪問內(nèi)部函數(shù)的局部變量孵运,還可以訪問外部函數(shù)的局部變量
this.getHaha = function(){
console.log("自己暴露了自己!G宰!掐松!(⊙o⊙)…完蛋,被發(fā)現(xiàn)了粪小!");
return haha;
}
// 構(gòu)造區(qū)代碼塊,只要實例化就會被調(diào)用
this.setX(x);
console.log("我被實例化了");
}
})()
// 普通公有方法(實例屬性)大磺,不能訪問內(nèi)部變量
A.prototype = {
say: function(){
console.log("我是公有方法,你需要new之后才能調(diào)用");
},
sayHi: function(){
console.log("Hello world!");
},
hehe: "heheheheh"
};
A.staticMethod = function(){
console.log("我是靜態(tài)方法探膊,你不要new杠愧,就可以通過類名調(diào)用");
}
這樣,我們就實現(xiàn)了對類的私有屬性以及方法的封裝逞壁。需要理解的概念有閉包流济,靜態(tài)方法锐锣,原型鏈,new關(guān)鍵字绳瘟,作用域雕憔,構(gòu)造方法。
4. <a name="extend">繼承</a>
4.1 類式繼承
// 定義一個Person類
function Person(name) {
this.name = name;
}
Person.prototype.getName = function(){
return this.name;
}
Person.prototype = {
className: "Person",
getName: function(){
return this.name
},
}
// 使用new關(guān)鍵字實例化類
var p = new Person('Tom');
p.getName();
下面糖声,通過原型鏈來實現(xiàn)繼承:
// 定義一個Author類,繼承Person
function Author(name, books) {
// 1.調(diào)用父類構(gòu)造方法
Person.call(this, name);
this.books = books;
}
// 2.將子類的原型指向父類
Author.prototype = new Person();
// 3.更新子類的構(gòu)造方法
Author.prototype.constructor = Author;
// 多態(tài)斤彼,子類實現(xiàn)自己的屬性
Author.prototype = {
className: "Author",
getBooks: function(){
return this.books;
}
}
// 使用new關(guān)鍵字實例化類
var a = new Author('Jerry', 'Tom and Jerry!');
a.getName(); // Jerry
這樣就實現(xiàn)了Author類繼承Person類的操作。三步走:
1.子類創(chuàng)造構(gòu)造函數(shù)蘸泻,并在構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)琉苇,將參數(shù)傳給父類。
Person.call(this,name)
2.設置子類的原型鏈(prototype,要么指向另一個對象悦施,要么指向null)并扇,使他指向父類對象。
Author.prototype=new Author()
-
3.將子類的構(gòu)造方法更新為子類對象抡诞。
Author.prototype.construtor=Author
原型鏈查找:在訪問對象的某個屬性時(如name屬性)穷蛹,首先在該類的實例屬性上(
Author.name
)查找,沒找到昼汗,就會在該類的原型上查找(Author.__proto__
),即prototype所指向的對象俩莽;如果還沒找到,繼續(xù)向上查找(Author.__proto__.__proto__
),直到直到找到這個屬性或者查到原型鏈最頂端(Object.prototype
最終指向null)乔遮。
4.2 封裝繼承的方法
為了簡化類的聲明扮超,ES6中實現(xiàn)了extends關(guān)鍵字,如果我們自己實現(xiàn)一個extend函數(shù)呢蹋肮?
/*extend function*/
function extend(subClass, superClass){
// 空函數(shù)F出刷,避免創(chuàng)建超類的實例
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}
// 實現(xiàn)繼承
function Author(name,books){
// 更改父類的this指向,相當于ES6繼承中的super
Person.call(this,name);
this.books=books;
}
// 繼承
extend(Author,Person);
// 必須繼承之后再新增方法
Author.prototype.getBooks = function(){
return this.books;
}
上面可以實現(xiàn)繼承坯辩,子類中需要直接調(diào)用父類的構(gòu)造方法Person.call(this,name)
馁龟,來更新this指向,否則子類獲取不到this對象漆魔。下面對這一部分進行改善:
/*extend function*/
function extend(subClass, superClass){
// 空函數(shù)F坷檩,避免創(chuàng)建超類的實例
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
// superclass屬性弱化子類和超類之間的耦合
// 將父類保存起來
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor==Object.prototype.constructor){
subClass.prototype.constructor = superClass;
}
}
// 實現(xiàn)繼承
function Author(name,books){
// 改善
Author.superclass.constructor.call(this,name);
this.books=books;
}
extend(Author,Person);
Author.prototype.getBooks = function(){
return this.books;
}
4.3 原型式繼承
原型式繼承和類式繼承截然不同。原型式繼承改抡,要從對象的角度考慮矢炼。下面我們用對象的方式重新實現(xiàn)上面的Person、Author類的繼承:
-
1.用一個類聲明定義對象的結(jié)構(gòu)
/* Person Prototype Object*/ var Person = { name: 'person', getName: function(){ return this.name; } }
-
2.實例化該類創(chuàng)建一個新對象
var Author = clone(Person); Author.name = 'author' Author.books = []; Author.getBooks = function(){ return this.books; } // 每次實例化都要使用clone方法 var author = clone(Author); author.name = 'zkk' author.books = ['JavaScript設計模式', '深入淺出ES6']; author.getName(); author.getBooks();
-
3.編寫
clone
方法阿纤,實現(xiàn)繼承// 創(chuàng)建一個以superClass為原型的空對象 function clone(superClass) { function F(){}; F.prototype = object; return new F; }
4.4 類式繼承與原型式繼承
類式繼承的用法更廣泛句灌,一般封裝的庫,都是用這種方式。
原型式繼承一般用得比較少胰锌,但是最好還是要理解這種方式骗绕。
原型式繼承更能節(jié)省內(nèi)存空間,并且更簡潔资昧。但類式繼承因為是模仿傳統(tǒng)語言的繼承方式酬土,更容易被理解,所以被廣泛應用格带。
鑒于此诺凡,一般建議還是使用類式繼承,但不要忘記還有原型式繼承践惑。
4.5 部分繼承——摻元類(Mixin Class)
摻元類(或混合類 mixin class): 即通過擴充(augmentation)的方式擁有某些類中的方法而不用繼承這些類,那么提供這些方法的類(被共享方法的類)就是摻元類。
-
定義mixin class提供方法:
// 定義一個Mixin class var Mixin = function(){}; Mixin.prototype = { serialize: function(){ var output = []; for(key in this){ output.push(key + ': ' + this[key]); } return output.join(', '); }, other: function(){}; }
-
將mixin class中提供的函數(shù)擴充到其他類中:
// 利用augemnt將Mixin中的serialize方法擴充到Author類中 augment(Author, Mixin, 'serialize') var author = new Author('Joe', ['HTML','CSS']); var str = author.serialize();
-
實現(xiàn)擴充方法augment:
// 參數(shù):接受的類嘶卧,提供的類尔觉,方法1,方法2芥吟,侦铜。。钟鸵。(方法名為可選參數(shù)) function augment(receive, giveClass){ if(arguments[2]){ // 明確要擴充的方法钉稍,那么只提供這些方法 for(var i=2,len=arguments.length;i<len;i++){ receiveClass.prototype[arguments[i]] = giveClass.prototype[arguments[i]]; } } else { // 沒有傳入要擴充的方法,默認全部提供 for(methodName in giveClass.prototype){ if(!receiveClass.prototype[methodName]){ receiveClass.prototype[methodName] = giveClass.prototype[methodName]; } } } }
JS中一個對象只能有一個原型對象棺耍,所以不允許子類繼承多個父類贡未,但是可以通過摻元類的方式實現(xiàn)擴充多個類中的不同方法。
5. 鏈式調(diào)用
5.1 什么是鏈式調(diào)用蒙袍?
例如俊卤,在Jquery項目中,有:
$("#id").addClass('active').siblings().removeClass('active');
這就是 鏈式調(diào)用,即利用.
操作符害幅,在目標對象上連續(xù)調(diào)用函數(shù)的方式消恍。這種方式,可以減少代碼量以现,但是會覆蓋上一次函數(shù)的返回值狠怨。
5.2 鏈式調(diào)用原理
鏈式調(diào)用的核心技術(shù)就是在每個方法后面返回當前對象,即return this
邑遏,比如:一個人佣赖,吃完飯,工作记盒,然后睡覺茵汰,實現(xiàn)如下:
function Person(){};
Person.prototype = {
eat: function(){
console.log("eat...");
},
work: function(){
console.log("work...")
},
sleep: function(){
console.log("sleep...")
}
}
// 調(diào)用
var p = new Person();
p.eat();
p.work();
p.sleep();
這是最基本的實現(xiàn)方式,如果用鏈式調(diào)用呢孽鸡?
function Person(){};
Person.prototype = {
eat: function(){
console.log("eat...");
return this;
},
work: function(){
console.log("work...");
return this;
},
sleep: function(){
console.log("sleep...");
return this;
}
}
// 調(diào)用
var p = new Person();
p.eat().work().sleep();
// 或者
new Person().eat().work().sleep();
通過改造蹂午,在每個方法中都返回了當前對象栏豺,即return this
,這樣每次函數(shù)的返回值都是當前對象實例豆胸,就可以繼續(xù)調(diào)用它的方法了奥洼。
5.3 使回調(diào)函數(shù)從鏈式調(diào)用中獲取數(shù)據(jù)
發(fā)現(xiàn)一個問題,就是如果我想要方法返回數(shù)據(jù)晚胡,而不是this對象灵奖,那么怎么實現(xiàn)呢?
鏈式調(diào)用適合于賦值器方法估盘,但對于取值器方法(返回特定數(shù)據(jù))并不友好瓷患,但我們可以使回調(diào)函數(shù)獲取所要的數(shù)據(jù)∏餐祝或者在特定的get方法后面直接返回數(shù)據(jù)而不是當前對象擅编。
function Person(name){
var _name = name;
// 特權(quán)方法
this.setName = function(name){
_name = name;
return this;
};
this.getName = function(){
// 這里沒有返回this
return _name;
}
}
// 這里只能先調(diào)用setName,在鏈式調(diào)用get
new Person('zkk').setName('ZKKK').getName();
new Person('zkk').getName();
// 而不能先get在set
new Person('zkk').getName().steName();
var myname;
function getMyName(name){
myname = name;
}
// 改造上面的getName
this.getName = function(callback){
// 通過回調(diào)函數(shù)獲取name
callback.call(this, _name)
return this;
};
// 這時,我就可以隨意獲取或者修改name了
new Person('zkk').getName(getMyName).setName('zkkk').getName(getMyName)...
5.4 設計自己的JS庫——支持鏈式調(diào)用
// 定義自己的庫
(function(window){
// 給Function添加新的方法箫踩,使每個函數(shù)實例都能使用
Function.prototype.method = function(name, fn){
this.prototype[name] = fn;
return this;
}
function _zkk(name, age){
this.name = name;
this.age = age;
};
zkk.method('getName', function(){
return this.name;
});
zkk.method('setName', function(name){
this.name = name;
return this;
});
// 掛載到window上
window.zkk = function(name,age) {
return new _zkk(name,age);
}
})(window)
6. 單例模式(Singleton)
6.1 基本概念
單體類爱态,只有一個實例對象,即只會被實例化一次境钟,所有的屬性共享相同的資源锦担。
通過對象字面量創(chuàng)建一個最簡單的Singleton:
因為JS中普通的對象是不能通過new關(guān)鍵字來實例化的,所以它只有一個實例對象就是它本身慨削。
/*Basic Singleton*/
var Singleton = {
key1: "value1",
key2: "value2",
func1: function(){},
func2: function(){}
}
6.2 基本用途
利用單例模式劃分命名空間:
比如:A.js
和B.js
中都有一個方法sayHi()
// A.js
function sayHi(){console.log('Hi! AAA')}
// B.js
function sayHi(){console.log('Hi! BBB')}
當兩者都被引入同一個文件時洞渔,后引入的會覆蓋縣引入的,這是通過命名空間就可以解決這個問題缚态。
/* use namespace*/
var A = {
sayHi:function(){
console.log('Hi! AAA')
}
};
var B = {
sayHi:function(){
console.log('Hi! BBB')
}
};
// 通過命名空間調(diào)用痘煤,不會產(chǎn)生覆蓋的問題很容易區(qū)分
A.sayHi();
B.sayHi();
6.3 私有屬性的單例——閉包
// 創(chuàng)建擁有私有變量的單例---閉包
var ss = (function() {
// private attribute
var name = "我是private屬性"
// public attribute
return {
key1: "我是lublic屬性;
",
key2: "value2",
// public訪問私有屬性
func1: function() {
console.log(name);
},
func2: function() {}
};
})();
// 測試
var a = ss;
var b = ss;
a.key1; // 我是lublic屬性;
b.key1; // 我是lublic屬性;
a.key1 = "aaa";
b.key1; // aaa;
a===b; // true
單例類一旦被實例化之后,所有的實例(其實就只有一個實例化對象)都共享相同的資源猿规,改變其中一個衷快,另一個也會鞥這改變。利用這個特性我們可以使多個對象共相同的全局資源姨俩。
6.4惰性實例化
單體對象都是在被一加載時實例化的蘸拔,能不能在我需要的時候再實例化呢?這樣可以節(jié)省資源环葵,這就是惰性實例化调窍,即懶加載技術(shù)(lazy loading)。懶加載單體的關(guān)鍵是借助一個靜態(tài)方法(static method)张遭,調(diào)用Singleton.getInstance().methodName()
來實現(xiàn)邓萨。getInstance
方法會檢測單體是否已經(jīng)被實例化,如果還沒有就創(chuàng)建并返回實例。
- 1.把單體的所有代碼移到private方法
constructor
中 - 2.通過public方法
getInstance
調(diào)用private方法constructor
來控制實例的創(chuàng)建 - 3.通過private屬性
uniqueInstance
來檢測單例是否已經(jīng)實例化
var Singleton = (function() {
// 保存單體實例化對象
var uniqueInstance;
// 將所有的單體內(nèi)容包裹在這個私有方法里面
function constructor() {
// private attribute
var name = "我是private屬性";
// public attribute
return {
key1: "我是lublic屬性",
key2: "value2",
// public訪問私有屬性
func1: function() {
console.log(name);
},
func2: function() {}
};
}
return {
// 通過這個public方法暴露出去
getInstance: function(){
if(!uniqueInstance){ // 檢測是否已經(jīng)實例化
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
// 測試
Singleton.getInstance().func1();
簡單梳理一下缔恳,結(jié)構(gòu)如下:
var singleton = (function(){
// 保存單體類對象
var uniqueInstance;
function constructor(){
// 單體類主體
return {}
}
return {
getInstance: function(){
// 控制單體類實例化
if(!uniqueInstance){
uniqueInstance = constructor();
}
return uniqueInstance;
}
}
})();
6.5 分支技術(shù)
分支(branching)是一種用來把瀏覽器見得差異封裝到在運行期間進行設置的動態(tài)方法中的技術(shù)宝剖。比如創(chuàng)建XHR對象,大多數(shù)瀏覽器使用new XMLHttpRequest()
創(chuàng)建歉甚,低版本IE確是用new ActiveXObject("Microsoft.XMLHTTP")
來創(chuàng)建万细,如果不使用分支技術(shù),每次調(diào)用的時候纸泄,瀏覽器都要檢測一次赖钞。可以利用分支技術(shù)聘裁,在初始化的時候加載特定的代碼雪营,再次調(diào)用的時候就不要再次檢測了。
大概邏輯如下:
var s = 'A';
var Singleton = (function(str){
var A = {
func1: function(){}
};
var B = {
func1: function(){}
};
return str === 'A'? A: B;
})(s)
封裝好的創(chuàng)建XHR對象的函數(shù):
function createXMLHttpRequest() {
try {
// 主流瀏覽器
return new XMLHttpRequest();
} catch (e) {
try {
// IE6
return new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try {
// IE5.5及以下
return new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
// 未知
throw e;
}
}
}
}
下面使用分支技術(shù)創(chuàng)建XHR對象:
var XHRFactory = (function() {
var standard = {
createXMLHttpRequest: function() {
return new XMLHttpRequest();
}
};
var activeXNew = {
createXMLHttpRequest: function() {
return new ActiveXObject("Msxml2.XMLHTTP");
}
};
var activeXOld = {
createXMLHttpRequest: function() {
return new ActiveXObject("Microsoft.XMLHTTP");
}
};
try {
standard.createXMLHttpRequest();
return standard;
} catch (e) {
try {
activeXNew.createXMLHttpRequest();
return activeXNew;
} catch (e) {
try {
activeXOld.createXMLHttpRequest();
return activeXOld;
} catch (e) {
throw e;
}
}
}
})();
這樣只需調(diào)用XHRFactory.createXMLHttpRequest()
就可以獲取特定瀏覽器環(huán)境的XML對象衡便。這里并不是說用分支創(chuàng)建的XHR方式更好献起,而是強調(diào)怎么使用分支技術(shù)來實現(xiàn)按需加載。
7. 工廠模式(Factory)
如果一個類中有成員屬性是另一個類的實例砰诵,常規(guī)操作是用new關(guān)鍵字和類構(gòu)造方法創(chuàng)建實例,這樣一來捌显,就會導致兩個類產(chǎn)生依賴性茁彭,即耦合度,為了降低耦合度扶歪,可以考慮使用工廠模式理肺。
7.1 初探工廠模式
比如超市提供各色商品,顧客去超市買食物:
var Market = function() {};
Market.prototype = {
getFood: function(name) {
var food;
switch (name) {
case 'milk':
// 超市自己制造的食品
food = new Milk();
break;
case 'bread':
food = new Bread();
break;
default:
food = null;
}
// food通過檢查了嗎善镰?
return food;
},
};
function Milk(){};
function Bread(){};
new Market().getFood('milk');
很顯然妹萨,超市不應該可以制造食物,而是提供食物炫欺,食物應該交由食品加工廠制造乎完,而且還要確保制造的食物是健康的、可食用的品洛,即校驗合格的树姨。
下面,將食品加工交給食品加工廠桥状,并且要校驗合格:
// 食物標準:健康帽揪、綠色、可食用
var Food = new Interface('Food', ['isHealth', 'isGreen', 'isEdible']);
// 食物加工廠
var FoodFactory = {
createFood: function(name) {
var food;
switch (name) {
case "milk":
food = new Milk();
break;
case "bread":
food = new Bread();
break;
default:
food = null;
}
// 食物檢測
Interface.ensureImplements(food,Food);
return food;
}
};
function Milk(){};
// 實現(xiàn)標準
Milk.prototype = {
isHealth: function(){},
isGreen: function(){},
isEdiable: function(){}
}
var Market = function() {};
Market.prototype = {
getFood: function(name) {
var food = FoodFactory.createFood(name);
food.isHealth();
food.isGreen();
food.isEdible();
// 現(xiàn)在可以放心出售了
return food;
}
};
這里使用接口定義了食物標準辅斟,并且在工廠類中檢測是否實現(xiàn)了這些標準转晰。Interface類的具體實現(xiàn)請看第2章-接口2.2.3。
這樣,我們把食物制造工作從超市移交給食品加工廠了查邢,完成了解耦操作蔗崎。這是只是最簡單的工廠模式,真正的工廠模式比這更復雜侠坎。
7.2 深入工廠模式
工廠是一個將其成員對象的實例化通過子類來實現(xiàn)蚁趁。
繼續(xù)用超市的例子來理解,現(xiàn)在超市盈利了实胸,開辦了自己的食品超市A專門自產(chǎn)自銷食品他嫡,食品加工不再借由外面的加工廠了,而是自己的食品超市自己加工(自產(chǎn)自銷)庐完。
var Market = function(){};
Market.prototype = {
getFood: function(name)
// 這里將成員對象實例化交給抽象類
var food = this.createFood(name);
food.isHealth();
food.isGreen();
food.isEdible();
return food;
},
// 這是一個抽象類钢属,需要子類來實現(xiàn)它
createFood: function(name){
throw new Error('Unsupported operation on an abstract class.');
}
}
上面 定義抽象類來實現(xiàn)成員實例化方法,但是抽象類是不能直接調(diào)用的门躯,它需要通過子類繼承才能被實例化淆党。
var FoodMarketA = function() {};
// 實現(xiàn)繼承
extend(FoodMarketA, Market);
// 實現(xiàn)抽象類方法
FoodMarketA.prototype.createFood = function(name) {
var food;
switch (name) {
case "milk":
food = new Milk();
break;
case "bread":
food = new Bread();
break;
default:
food = null;
}
// 食物檢測
Interface.ensureImplements(food, Food);
return food;
};
// 這樣顧客就可以到專門的食品超市購買食物
var foodMarketA = new FoodMarketA();
var milk = foodMarketA.getFood('milk');
FoodMarketA
繼承了Market
,并且實現(xiàn)了抽象方法讶凉,Market
就相當于一個的連鎖超市總部染乌,可以在全開連鎖店,不僅可以有FoodMarketA
懂讯,還可以擴張FoodMarketB
等荷憋,只需要實現(xiàn)繼承就可以了。
這里的繼承extend
方法具體實現(xiàn)請參考第4節(jié)-繼承4.2褐望。
7.3 工廠模式利弊
- 利:弱化對象間的耦合勒庄,防止代碼的重復。通過工廠模式瘫里,可以先創(chuàng)建一個抽象的父類实蔽,然后子類實現(xiàn)繼承實現(xiàn)具體的方法,從而把成員對象的創(chuàng)建交由專門的類去創(chuàng)建谨读。關(guān)鍵技術(shù):接口和繼承局装,即制定標準和實現(xiàn)標準。
- 弊:邏輯復雜劳殖,有可能使問題復雜化贼邓,代碼不夠清晰,閱讀代碼需要查接口以及繼承關(guān)系闷尿。
8. 橋接模式(Bridge)
8.1 橋接模式概述
“將抽象與其實現(xiàn)隔離開來塑径,以便二者獨立變化√罹撸” —— GOF
在實現(xiàn)API的時候统舀,橋接模式非常有用匆骗。再設計一個Javascript API的時候,可以與整個模式來弱化它與所有使用它的類和對象之間的耦合誉简。
在一個類中碉就,你可能定義有許多獲取方法(getter)、設置方法(setter)以及其他方法闷串,無論是用來創(chuàng)建Web服務API還是普通的取值器(accessor)方法還是賦值器(mutator)方法瓮钥,都借助橋接模式可以簡潔代碼。
8.2 用橋接模式聯(lián)結(jié)多個類
在現(xiàn)實中通過媒介可以把多個不同的事物聯(lián)結(jié)起來烹吵,例如:人用電腦打字碉熄,簡單實現(xiàn) 如下:
var People = function(name,age){
this.name = name;
this.age = age;
};
People.prototype = {
type: function(){
console.log('type...');
}
};
var Computer = function(brand) {
this.brand = brand;
};
var Word = function(text){
this.text = text;
};
var Print = function(name,age,brand,text){
this.people = new People(name,age);
this.computer = new Computer(brand);
this.word = new Word(text);
this.print = function(){
console.log('一個名叫' + this.people.name + this.people.age + '歲的人,用' + this.computer.brand + '電腦肋拔,打印出了一段話:' + this.word.text);
}
}
//
var p = new Print('zkk', 24, 'Apple', 'Hello World!');
p.print();
這里定義了三個類:People
锈津、Computer
、Word
凉蜂,通過橋接類Print
將它們聯(lián)系在一起琼梆。Print
不用關(guān)注具體的人、電腦窿吩、詞語是怎么產(chǎn)生的茎杂,只需調(diào)用即可。
8.3 接模式應用
比如手動實現(xiàn)一個最簡單的forEach遍歷數(shù)組:
Array.prototype.myForEach = function(fn) {
for(var i=0,len=this.length;i<len;i++){
fn.call(this, this[i], i, this);
}
}
這里forEach
不用關(guān)注回調(diào)函數(shù)fn
的具體實現(xiàn)纫雁,這里只是調(diào)用一下煌往。回調(diào)函數(shù)fn
想怎么實現(xiàn)就怎么實現(xiàn)先较,是壓根不影響forEach
函數(shù)的携冤。
還有比如悼粮,事件監(jiān)聽回調(diào)闲勺、借助特權(quán)函數(shù)訪問私有數(shù)據(jù)的手段健蕊,等等都有用到橋接模式丝里。
記住翠忠,橋接模式的核心在于將抽象與具體實現(xiàn)分離忱详,完成解耦操作击孩。
8.4 橋接模式利弊
- 利:將抽象與實現(xiàn)分離趾断,獨立管理各個部分的邏輯匆浙,降低耦合刃泌,有利于代碼的維護昧穿。
- 弊:提高了系統(tǒng)的復雜度勺远。
9. 組合模式(composite)
9.1 理解組合模式
組合模式又稱部分-整體模式,將對象組合成樹形結(jié)構(gòu)以表示“部分-整體”的層次結(jié)構(gòu)时鸵,組合模式使得用戶對單個對象和組合對象的使用具有一致性胶逢。掌握組合模式的重點是要理解清楚 “部分/整體” 還有 ”單個對象“ 與 "組合對象" 的含義厅瞎。
組合模式主要有三個角色:
- 抽象組件(Component):抽象類,主要定義了參與組合的對象的公共接口
- 子對象(Leaf):組成組合對象的最基本對象
- 組合對象(Composite):由子對象組合起來的復雜對象
9.2 組合模式應用
比如DOM操作中最常用的就是給一個元素添加或刪除一個樣式:
var div = document.getElementById('div');
div.classList.add('red');
div.classList.remove('green');
// 再來一個
var p = document.getElementById('p');
p.classList.add('red')
p.classList.remove('green');
每次操作都要調(diào)用各種方法初坠,很是繁瑣和簸,我想要一次操作大量的節(jié)點,就很是費力了碟刺。
var OperateClass = function(){
this.domList = [];
};
OperateClass.prototype = {
addDom: function(classNameOrId) {
var dom = document.querySelector(classNameOrId);
this.domList.push(dom);
},
removeClass: function(className) {
for(var i=0,len=this.domList.length;i<len;i++){
if(this.domList[i] && this.domList[i].classList.contains(className)){
this.domList[i].classList.remove(className);
}
}
},
addClass:function(className) {
for(var i=0,len=this.domList.length;i<len;i++){
if(this.domList[i] && !this.domList[i].classList.contains(className)){
this.domList[i].classList.add(className);
}
}
}
}
var oc = new OperateClass();
oc.addDom('.item-1');
oc.addDom('.item-2');
oc.addDom('.item-3');
oc.addClass('red');
oc.addClass('bg-green');
通過定義一個組合對象锁保,把每個Dom操作都統(tǒng)一起來,可以實現(xiàn)一次性對大量Dom進行添加或者刪除className
的操作半沽。
總結(jié)一下爽柒,完整的模式如下:
// 1.抽象組件(Component)定義公共接口
var Component = new Interface('Component', ['func1', 'func2']);
// 2.組合對象(Composite)
var Composite = function(){
this.leafList = [];
};
// 3.實現(xiàn)繼承,并實現(xiàn)方法
extend(Composite, Component);
Composite.prototype = {
add: function(leaf){
this.leafList.push(leaf);
},
func1: function(){
for(var i=0,len=this.leafList.length;i<len;i++){
if(this.leafList[i] instanceof Component){
this.leafList[i].func1();
}
}
},
func2:function(){
for(var i=0,len=this.leafList.length;i<len;i++){
if(this.leafList[i] instanceof Component){
this.leafList[i].func2();
}
}
}
};
// 4.葉子對象(Leaf)
var Leaf1 = function(){};
extend(Leaf1, Component);
Leaf1.prototype = {
func1: function(){
console.log('func1...');
},
func2:function(){
console.log('func2...');
}
};
var Leaf2 = function(){};
Leaf2.prototype = {
func1: function(){
console.log('func1...');
},
func2:function(){
console.log('func2...');
}
};
// 操作所有葉子節(jié)點
var composite = new Composite();
composite.add(new Leaf1());
composite.add(new Leaf2());
composite.func1();
composite.func2();
這里定義了一個公共接口Component
抄囚,所有的組件(Composite
和Leaf
)都繼承了這個接口霉赡,然后把Leaf
組件打包給Composite
,由Composite
一次性完成對Leaf
所有的操作幔托。
上面代碼中使用了兩個函數(shù)interface和extend穴亏,具體實現(xiàn)請看第2節(jié)和第4節(jié)。
9.3 組合模式利弊
- 利:在組合模式中重挑,各個對象之間的耦合很松散嗓化,只要實現(xiàn)了相同的接口,相同的操作就可以很方便實現(xiàn)谬哀。