javascript中的設計模式有哪些

青灯夜游
發布: 2022-01-26 16:49:36
原創
3601 人瀏覽過

javascript中的設計模式有:單一範例模式、策略模式、代理模式、迭代器模式、「發布-訂閱」模式、指令模式、組合模式、範本方法模式、享元模式、職責鏈模式、中介者模式、裝飾者模式、狀態模式、適配器模式、外觀模式等。

javascript中的設計模式有哪些

本教學操作環境:windows7系統、javascript1.8.5版、Dell G3電腦。

JavaScript中常見的十五種設計模式

#一、單例模式

1. 定義

保證一個類別只有一個實例,並提供一個存取它的全域存取點

2.核心

確保只有一個實例,並提供全域存取

3. 實作

假設要設定一個管理員,多次呼叫也只設定一次,我們可以使用閉包快取一個內部變數來實作這個單例

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a
登入後複製

這是比較簡單的做法,但是假如我們還要設定一個HR呢?就得複製一遍程式碼了

所以,可以改寫單例內部,實現地更通用一些

// 提取出通用的单例
function getSingleton(fn) {
    var instance = null;

    return function() {
        if (!instance) {
            instance = fn.apply(this, arguments);
        }

        return instance;
    }
}
登入後複製

再進行調用,結果還是一樣

// 获取单例
var managerSingleton = getSingleton(function(name) {
    var manager = new SetManager(name);
    return manager;
});

managerSingleton('a').getName(); // a
managerSingleton('b').getName(); // a
managerSingleton('c').getName(); // a
登入後複製

這時,我們當添加HR時,就不需要更改獲取單例內部的實現了,僅需要實現添加HR所需要做的,再調用即可

function SetHr(name) {
    this.hr = name;
}

SetHr.prototype.getName = function() {
    console.log(this.hr);
};

var hrSingleton = getSingleton(function(name) {
    var hr = new SetHr(name);
    return hr;
});

hrSingleton('aa').getName(); // aa
hrSingleton('bb').getName(); // aa
hrSingleton('cc').getName(); // aa
登入後複製

或者,僅想要創建一個p層,不需要將物件實例化,直接呼叫函數

結果為頁面中僅有第一個建立的p

function createPopup(html) {
    var div = document.createElement('div');
    div.innerHTML = html;
    document.body.append(div);

    return div;
}

var popupSingleton = getSingleton(function() {
    var div = createPopup.apply(this, arguments);
    return div;
});

console.log(
    popupSingleton('aaa').innerHTML,
    popupSingleton('bbb').innerHTML,
    popupSingleton('bbb').innerHTML
); // aaa  aaa  aaa
登入後複製

二、策略模式

1. 定義

定義一系列的演算法,把它們一個個封裝起來,並且使它們可以互相替換。

2. 核心

將演算法的使用和演算法的實作分開。

一個基於策略模式的程式至少由兩部分組成:

第一個部分是一組策略類,策略類封裝了具體的演算法,並負責具體的計算過程。

第二個部分是環境類別Context,Context接受客戶的請求,隨後把請求委託給某一個策略類別。要做到這一點,說明Context 中要維持對某個策略物件的引用

3. 實作

策略模式可以用來組合一系列演算法,也可用於組合一系列業務規則

假設需要透過成績等級來計算學生的最終得分,每個成績等級有對應的加權值。我們可以利用物件字面量的形式直接定義這個群組策略

// 加权映射关系
var levelMap = {
    S: 10,
    A: 8,
    B: 6,
    C: 4
};

// 组策略
var scoreLevel = {
    basicScore: 80,

    S: function() {
        return this.basicScore + levelMap['S']; 
    },

    A: function() {
        return this.basicScore + levelMap['A']; 
    },

    B: function() {
        return this.basicScore + levelMap['B']; 
    },

    C: function() {
        return this.basicScore + levelMap['C']; 
    }
}

// 调用
function getScore(level) {
    return scoreLevel[level] ? scoreLevel[level]() : 0;
}

console.log(
    getScore('S'),
    getScore('A'),
    getScore('B'),
    getScore('C'),
    getScore('D')
); // 90 88 86 84 0
登入後複製

在組合業務規則方面,比較經典的是表單的驗證方法。這裡列出比較關鍵的部分

// 错误提示
var errorMsgs = {
    default: '输入数据格式不正确',
    minLength: '输入数据长度不足',
    isNumber: '请输入数字',
    required: '内容不为空'
};

// 规则集
var rules = {
    minLength: function(value, length, errorMsg) {
        if (value.length < length) {
            return errorMsg || errorMsgs[&#39;minLength&#39;]
        }
    },
    isNumber: function(value, errorMsg) {
        if (!/\d+/.test(value)) {
            return errorMsg || errorMsgs[&#39;isNumber&#39;];
        }
    },
    required: function(value, errorMsg) {
        if (value === &#39;&#39;) {
            return errorMsg || errorMsgs[&#39;required&#39;];
        }
    }
};

// 校验器
function Validator() {
    this.items = [];
};

Validator.prototype = {
    constructor: Validator,
    
    // 添加校验规则
    add: function(value, rule, errorMsg) {
        var arg = [value];

        if (rule.indexOf(&#39;minLength&#39;) !== -1) {
            var temp = rule.split(&#39;:&#39;);
            arg.push(temp[1]);
            rule = temp[0];
        }

        arg.push(errorMsg);

        this.items.push(function() {
            // 进行校验
            return rules[rule].apply(this, arg);
        });
    },
    
    // 开始校验
    start: function() {
        for (var i = 0; i < this.items.length; ++i) {
            var ret = this.items[i]();
            
            if (ret) {
                console.log(ret);
                // return ret;
            }
        }
    }
};

// 测试数据
function testTel(val) {
    return val;
}

var validate = new Validator();

validate.add(testTel(&#39;ccc&#39;), &#39;isNumber&#39;, &#39;只能为数字&#39;); // 只能为数字
validate.add(testTel(&#39;&#39;), &#39;required&#39;); // 内容不为空
validate.add(testTel(&#39;123&#39;), &#39;minLength:5&#39;, &#39;最少5位&#39;); // 最少5位
validate.add(testTel(&#39;12345&#39;), &#39;minLength:5&#39;, &#39;最少5位&#39;);

var ret = validate.start();

console.log(ret);
登入後複製

4. 優缺點

#優點

可以有效避免多重條件語句,將一系列方法封裝起來也更直觀,利於維護

缺點

往往策略集會比較多,我們需要事先就了解定義好所有的情況

三、代理模式

1. 定義

為一個物件提供一個代用品或占位符,以便控制對它的訪問

2. 核心

當客戶不方便直接訪問一個物件或不滿足需要的時候,提供一個替身物件來控制對這個物件的訪問,客戶實際上訪問的是替身對象。

替身物件對請求做出一些處理之後, 再把請求轉交給本體物件

代理程式和本體的介面具有一致性,本體定義了關鍵功能,而代理程式是提供或拒絕對它的訪問,或在訪問本體之前做一些額外的事情

3. 實作

代理模式主要有三種:保護代理、虛擬代理、快取代理

保護代理主要實現了存取主體的限制行為,以過濾字元作為簡單的例子

// 主体,发送消息
function sendMsg(msg) {
    console.log(msg);
}

// 代理,对消息进行过滤
function proxySendMsg(msg) {
    // 无消息则直接返回
    if (typeof msg === &#39;undefined&#39;) {
        console.log(&#39;deny&#39;);
        return;
    }
    
    // 有消息则进行过滤
    msg = (&#39;&#39; + msg).replace(/泥\s*煤/g, &#39;&#39;);

    sendMsg(msg);
}


sendMsg(&#39;泥煤呀泥 煤呀&#39;); // 泥煤呀泥 煤呀
proxySendMsg(&#39;泥煤呀泥 煤&#39;); // 呀
proxySendMsg(); // deny
登入後複製

它的意圖很明顯,在存取主體之前進行控制,沒有訊息的時候直接在代理程式中回傳了,拒絕存取主體,這資料保護代理程式的形式

有訊息的時候對敏感字元進行了處理,這屬於虛擬代理的模式

虛擬代理程式在控制對主體的訪問時,加入了一些額外的操作

在滾動事件觸發的時候,也許不需要頻繁觸發,我們可以引入函數節流,這是一種虛擬代理的實現

// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
    delay = delay || 200;
    
    var timer = null;

    return function() {
        var arg = arguments;
          
        // 每次操作时,清除上次的定时器
        clearTimeout(timer);
        timer = null;
        
        // 定义新的定时器,一段时间后进行操作
        timer = setTimeout(function() {
            fn.apply(this, arg);
        }, delay);
    }
};

var count = 0;

// 主体
function scrollHandle(e) {
    console.log(e.type, ++count); // scroll
}

// 代理
var proxyScrollHandle = (function() {
    return debounce(scrollHandle, 500);
})();

window.onscroll = proxyScrollHandle;
登入後複製

快取代理可以為一些開銷大的運算結果提供暫時的緩存,提升效率

來個栗子,快取加法操作

// 主体
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(&#39;,&#39;);
        
        // 如果有,则直接从缓存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100
登入後複製

四、迭代器模式

1. 定義

迭代器模式是指提供一個方法順序來存取一個聚合物件中的各個元素,而又不需要暴露該物件的內部表示。

2. 核心

在使用迭代器模式之後,即使不關心物件的內部構造,也可以依序存取其中的每個元素

3. 實作

#

JS中数组的map forEach 已经内置了迭代器

[1, 2, 3].forEach(function(item, index, arr) {
    console.log(item, index, arr);
});
登入後複製

不过对于对象的遍历,往往不能与数组一样使用同一的遍历代码

我们可以封装一下

function each(obj, cb) {
    var value;

    if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; ++i) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    } else {
        for (var i in obj) {
            value = cb.call(obj[i], i, obj[i]);

            if (value === false) {
                break;
            }
        }
    }
}

each([1, 2, 3], function(index, value) {
    console.log(index, value);
});

each({a: 1, b: 2}, function(index, value) {
    console.log(index, value);
});

// 0 1
// 1 2
// 2 3

// a 1
// b 2
登入後複製

再来看一个例子,强行地使用迭代器,来了解一下迭代器也可以替换频繁的条件语句

虽然例子不太好,但在其他负责的分支判断情况下,也是值得考虑的

function getManager() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log(&#39;A&#39;);
    } else if (year >= 2100) {
        console.log(&#39;C&#39;);
    } else {
        console.log(&#39;B&#39;);
    }
}

getManager(); // B
登入後複製

将每个条件语句拆分出逻辑函数,放入迭代器中迭代

function year2000() {
    var year = new Date().getFullYear();

    if (year <= 2000) {
        console.log(&#39;A&#39;);
    }

    return false;
}

function year2100() {
    var year = new Date().getFullYear();

    if (year >= 2100) {
        console.log(&#39;C&#39;);
    }

    return false;
}

function year() {
    var year = new Date().getFullYear();

    if (year > 2000 && year < 2100) {
        console.log(&#39;B&#39;);
    }

    return false;
}

function iteratorYear() {
    for (var i = 0; i < arguments.length; ++i) {
        var ret = arguments[i]();

        if (ret !== false) {
            return ret;
        }
    }
}

var manager = iteratorYear(year2000, year2100, year); // B
登入後複製

五、发布-订阅模式

1. 定义

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

2. 核心

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅

3. 实现

JS中的事件就是经典的发布-订阅模式的实现

// 订阅
document.body.addEventListener(&#39;click&#39;, function() {
    console.log(&#39;click1&#39;);
}, false);

document.body.addEventListener(&#39;click&#39;, function() {
    console.log(&#39;click2&#39;);
}, false);

// 发布
document.body.click(); // click1  click2
登入後複製

自己实现一下

小A在公司C完成了笔试及面试,小B也在公司C完成了笔试。他们焦急地等待结果,每隔半天就电话询问公司C,导致公司C很不耐烦。

一种解决办法是 AB直接把联系方式留给C,有结果的话C自然会通知AB

这里的“询问”属于显示调用,“留给”属于订阅,“通知”属于发布

// 观察者
var observer = {
    // 订阅集合
    subscribes: [],

    // 订阅
    subscribe: function(type, fn) {
        if (!this.subscribes[type]) {
            this.subscribes[type] = [];
        }
        
        // 收集订阅者的处理
        typeof fn === &#39;function&#39; && this.subscribes[type].push(fn);
    },

    // 发布  可能会携带一些信息发布出去
    publish: function() {
        var type = [].shift.call(arguments),
            fns = this.subscribes[type];
        
        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }
        
        // 挨个处理调用
        for (var i = 0; i < fns.length; ++i) {
            fns[i].apply(this, arguments);
        }
    },
    
    // 删除订阅
    remove: function(type, fn) {
        // 删除全部
        if (typeof type === &#39;undefined&#39;) {
            this.subscribes = [];
            return;
        }

        var fns = this.subscribes[type];

        // 不存在的订阅类型,以及订阅时未传入处理回调的
        if (!fns || !fns.length) {
            return;
        }

        if (typeof fn === &#39;undefined&#39;) {
            fns.length = 0;
            return;
        }

        // 挨个处理删除
        for (var i = 0; i < fns.length; ++i) {
            if (fns[i] === fn) {
                fns.splice(i, 1);
            }
        }
    }
};

// 订阅岗位列表
function jobListForA(jobs) {
    console.log(&#39;A&#39;, jobs);
}

function jobListForB(jobs) {
    console.log(&#39;B&#39;, jobs);
}

// A订阅了笔试成绩
observer.subscribe(&#39;job&#39;, jobListForA);
// B订阅了笔试成绩
observer.subscribe(&#39;job&#39;, jobListForB);


// A订阅了笔试成绩
observer.subscribe(&#39;examinationA&#39;, function(score) {
    console.log(score);
});

// B订阅了笔试成绩
observer.subscribe(&#39;examinationB&#39;, function(score) {
    console.log(score);
});

// A订阅了面试结果
observer.subscribe(&#39;interviewA&#39;, function(result) {
    console.log(result);
});

observer.publish(&#39;examinationA&#39;, 100); // 100
observer.publish(&#39;examinationB&#39;, 80); // 80
observer.publish(&#39;interviewA&#39;, &#39;备用&#39;); // 备用

observer.publish(&#39;job&#39;, [&#39;前端&#39;, &#39;后端&#39;, &#39;测试&#39;]); // 输出A和B的岗位


// B取消订阅了笔试成绩
observer.remove(&#39;examinationB&#39;);
// A都取消订阅了岗位
observer.remove(&#39;job&#39;, jobListForA);

observer.publish(&#39;examinationB&#39;, 80); // 没有可匹配的订阅,无输出
observer.publish(&#39;job&#39;, [&#39;前端&#39;, &#39;后端&#39;, &#39;测试&#39;]); // 输出B的岗位
登入後複製

4. 优缺点

优点

一为时间上的解耦,二为对象之间的解耦。可以用在异步编程中与MV*框架中

缺点

创建订阅者本身要消耗一定的时间和内存,订阅的处理函数不一定会被执行,驻留内存有性能开销

弱化了对象之间的联系,复杂的情况下可能会导致程序难以跟踪维护和理解

六、命令模式

1. 定义

用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

命令(command)指的是一个执行某些特定事情的指令

2. 核心

命令中带有execute执行、undo撤销、redo重做等相关命令方法,建议显示地指示这些方法名

3. 实现

简单的命令模式实现可以直接使用对象字面量的形式定义一个命令

var incrementCommand = {
    execute: function() {
        // something
    }
};
登入後複製

不过接下来的例子是一个自增命令,提供执行、撤销、重做功能

采用对象创建处理的方式,定义这个自增

// 自增
function IncrementCommand() {
    // 当前值
    this.val = 0;
    // 命令栈
    this.stack = [];
    // 栈指针位置
    this.stackPosition = -1;
};

IncrementCommand.prototype = {
    constructor: IncrementCommand,

    // 执行
    execute: function() {
        this._clearRedo();
        
        // 定义执行的处理
        var command = function() {
            this.val += 2;
        }.bind(this);
        
        // 执行并缓存起来
        command();
        
        this.stack.push(command);

        this.stackPosition++;

        this.getValue();
    },
    
    canUndo: function() {
        return this.stackPosition >= 0;
    },
    
    canRedo: function() {
        return this.stackPosition < this.stack.length - 1;
    },

    // 撤销
    undo: function() {
        if (!this.canUndo()) {
            return;
        }
        
        this.stackPosition--;

        // 命令的撤销,与执行的处理相反
        var command = function() {
            this.val -= 2;
        }.bind(this);
        
        // 撤销后不需要缓存
        command();

        this.getValue();
    },
    
    // 重做
    redo: function() {
        if (!this.canRedo()) {
            return;
        }
        
        // 执行栈顶的命令
        this.stack[++this.stackPosition]();

        this.getValue();
    },
    
    // 在执行时,已经撤销的部分不能再重做
    _clearRedo: function() {
        this.stack = this.stack.slice(0, this.stackPosition + 1);
    },
    
    // 获取当前值
    getValue: function() {
        console.log(this.val);
    }
};
登入後複製

再实例化进行测试,模拟执行、撤销、重做的操作

var incrementCommand = new IncrementCommand();

// 模拟事件触发,执行命令
var eventTrigger = {
    // 某个事件的处理中,直接调用命令的处理方法
    increment: function() {
        incrementCommand.execute();
    },

    incrementUndo: function() {
        incrementCommand.undo();
    },

    incrementRedo: function() {
        incrementCommand.redo();
    }
};


eventTrigger[&#39;increment&#39;](); // 2
eventTrigger[&#39;increment&#39;](); // 4

eventTrigger[&#39;incrementUndo&#39;](); // 2

eventTrigger[&#39;increment&#39;](); // 4

eventTrigger[&#39;incrementUndo&#39;](); // 2
eventTrigger[&#39;incrementUndo&#39;](); // 0
eventTrigger[&#39;incrementUndo&#39;](); // 无输出

eventTrigger[&#39;incrementRedo&#39;](); // 2
eventTrigger[&#39;incrementRedo&#39;](); // 4
eventTrigger[&#39;incrementRedo&#39;](); // 无输出

eventTrigger[&#39;increment&#39;](); // 6
登入後複製

此外,还可以实现简单的宏命令(一系列命令的集合)

var MacroCommand = {
    commands: [],

    add: function(command) {
        this.commands.push(command);

        return this;
    },

    remove: function(command) {
        if (!command) {
            this.commands = [];
            return;
        }

        for (var i = 0; i < this.commands.length; ++i) {
            if (this.commands[i] === command) {
                this.commands.splice(i, 1);
            }
        }
    },

    execute: function() {
        for (var i = 0; i < this.commands.length; ++i) {
            this.commands[i].execute();
        }
    }
};

var showTime = {
    execute: function() {
        console.log(&#39;time&#39;);
    }
};

var showName = {
    execute: function() {
        console.log(&#39;name&#39;);
    }
};

var showAge = {
    execute: function() {
        console.log(&#39;age&#39;);
    }
};

MacroCommand.add(showTime).add(showName).add(showAge);

MacroCommand.remove(showName);

MacroCommand.execute(); // time age
登入後複製

七、组合模式

1. 定义

是用小的子对象来构建更大的 对象,而这些小的子对象本身也许是由更小 的“孙对象”构成的。

2. 核心

可以用树形结构来表示这种“部分- 整体”的层次结构。

调用组合对象 的execute方法,程序会递归调用组合对象 下面的叶对象的execute方法

但要注意的是,组合模式不是父子关系,它是一种HAS-A(聚合)的关系,将请求委托给 它所包含的所有叶对象。基于这种委托,就需要保证组合对象和叶对象拥有相同的 接口

此外,也要保证用一致的方式对待 列表中的每个叶对象,即叶对象属于同一类,不需要过多特殊的额外操作

3. 实现

使用组合模式来实现扫描文件夹中的文件

// 文件夹 组合对象
function Folder(name) {
    this.name = name;
    this.parent = null;
    this.files = [];
}

Folder.prototype = {
    constructor: Folder,

    add: function(file) {
        file.parent = this;
        this.files.push(file);

        return this;
    },

    scan: function() {
        // 委托给叶对象处理
        for (var i = 0; i < this.files.length; ++i) {
            this.files[i].scan();
        }
    },

    remove: function(file) {
        if (typeof file === &#39;undefined&#39;) {
            this.files = [];
            return;
        }

        for (var i = 0; i < this.files.length; ++i) {
            if (this.files[i] === file) {
                this.files.splice(i, 1);
            }
        }
    }
};

// 文件 叶对象
function File(name) {
    this.name = name;
    this.parent = null;
}

File.prototype = {
    constructor: File,

    add: function() {
        console.log(&#39;文件里面不能添加文件&#39;);
    },

    scan: function() {
        var name = [this.name];
        var parent = this.parent;

        while (parent) {
            name.unshift(parent.name);
            parent = parent.parent;
        }

        console.log(name.join(&#39; / &#39;));
    }
};
登入後複製

构造好组合对象与叶对象的关系后,实例化,在组合对象中插入组合或叶对象

var web = new Folder(&#39;Web&#39;);
var fe = new Folder(&#39;前端&#39;);
var css = new Folder(&#39;CSS&#39;);
var js = new Folder(&#39;js&#39;);
var rd = new Folder(&#39;后端&#39;);

web.add(fe).add(rd);

var file1 = new File(&#39;HTML权威指南.pdf&#39;);
var file2 = new File(&#39;CSS权威指南.pdf&#39;);
var file3 = new File(&#39;JavaScript权威指南.pdf&#39;);
var file4 = new File(&#39;MySQL基础.pdf&#39;);
var file5 = new File(&#39;Web安全.pdf&#39;);
var file6 = new File(&#39;Linux菜鸟.pdf&#39;);

css.add(file2);
fe.add(file1).add(file3).add(css).add(js);
rd.add(file4).add(file5);
web.add(file6);

rd.remove(file4);

// 扫描
web.scan();
登入後複製

扫描结果为

4. 优缺点

优点

可 以方便地构造一棵树来表示对象的部分-整体 结构。在树的构造最终 完成之后,只需要通过请求树的最顶层对 象,便能对整棵树做统一一致的操作。

缺点

创建出来的对象长得都差不多,可能会使代码不好理解,创建太多的对象对性能也会有一些影响

八、模板方法模式

1. 定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。

2. 核心

在抽象父类中封装子类的算法框架,它的 init方法可作为一个算法的模板,指导子类以何种顺序去执行哪些方法。

由父类分离出公共部分,要求子类重写某些父类的(易变化的)抽象方法

3. 实现

模板方法模式一般的实现方式为继承

以运动作为例子,运动有比较通用的一些处理,这部分可以抽离开来,在父类中实现。具体某项运动的特殊性则有自类来重写实现。

最终子类直接调用父类的模板函数来执行

// 体育运动
function Sport() {

}

Sport.prototype = {
    constructor: Sport,
    
    // 模板,按顺序执行
    init: function() {
        this.stretch();
        this.jog();
        this.deepBreath();
        this.start();

        var free = this.end();
        
        // 运动后还有空的话,就拉伸一下
        if (free !== false) {
            this.stretch();
        }
        
    },
    
    // 拉伸
    stretch: function() {
        console.log(&#39;拉伸&#39;);
    },
    
    // 慢跑
    jog: function() {
        console.log(&#39;慢跑&#39;);
    },
    
    // 深呼吸
    deepBreath: function() {
        console.log(&#39;深呼吸&#39;);
    },

    // 开始运动
    start: function() {
        throw new Error(&#39;子类必须重写此方法&#39;);
    },

    // 结束运动
    end: function() {
        console.log(&#39;运动结束&#39;);
    }
};

// 篮球
function Basketball() {

}

Basketball.prototype = new Sport();

// 重写相关的方法
Basketball.prototype.start = function() {
    console.log(&#39;先投上几个三分&#39;);
};

Basketball.prototype.end = function() {
    console.log(&#39;运动结束了,有事先走一步&#39;);
    return false;
};


// 马拉松
function Marathon() {

}

Marathon.prototype = new Sport();

var basketball = new Basketball();
var marathon = new Marathon();

// 子类调用,最终会按照父类定义的顺序执行
basketball.init();
marathon.init();
登入後複製

九、享元模式

1. 定义

享元(flyweight)模式是一种用于性能优化的模式,它的目标是尽量减少共享对象的数量

2. 核心

运用共享技术来有效支持大量细粒度的对象。

强调将对象的属性划分为内部状态(属性)与外部状态(属性)。内部状态用于对象的共享,通常不变;而外部状态则剥离开来,由具体的场景决定。

3. 实现

在程序中使用了大量的相似对象时,可以利用享元模式来优化,减少对象的数量

举个栗子,要对某个班进行身体素质测量,仅测量身高体重来评判

// 健康测量
function Fitness(name, sex, age, height, weight) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.height = height;
    this.weight = weight;
}

// 开始评判
Fitness.prototype.judge = function() {
    var ret = this.name + &#39;: &#39;;

    if (this.sex === &#39;male&#39;) {
        ret += this.judgeMale();
    } else {
        ret += this.judgeFemale();
    }

    console.log(ret);
};

// 男性评判规则
Fitness.prototype.judgeMale = function() {
    var ratio = this.height / this.weight;

    return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};

// 女性评判规则
Fitness.prototype.judgeFemale = function() {
    var ratio = this.height / this.weight;
    
    return this.age > 20 ? (ratio > 4) : (ratio > 3);
};


var a = new Fitness(&#39;A&#39;, &#39;male&#39;, 18, 160, 80);
var b = new Fitness(&#39;B&#39;, &#39;male&#39;, 21, 180, 70);
var c = new Fitness(&#39;C&#39;, &#39;female&#39;, 28, 160, 80);
var d = new Fitness(&#39;D&#39;, &#39;male&#39;, 18, 170, 60);
var e = new Fitness(&#39;E&#39;, &#39;female&#39;, 18, 160, 40);

// 开始评判
a.judge(); // A: false
b.judge(); // B: false
c.judge(); // C: false
d.judge(); // D: true
e.judge(); // E: true
登入後複製

评判五个人就需要创建五个对象,一个班就几十个对象

可以将对象的公共部分(内部状态)抽离出来,与外部状态独立。将性别看做内部状态即可,其他属性都属于外部状态。

这么一来我们只需要维护男和女两个对象(使用factory对象),而其他变化的部分则在外部维护(使用manager对象)

// 健康测量
function Fitness(sex) {
    this.sex = sex;
}

// 工厂,创建可共享的对象
var FitnessFactory = {
    objs: [],

    create: function(sex) {
        if (!this.objs[sex]) {
            this.objs[sex] = new Fitness(sex);
        }

        return this.objs[sex];
    }
};

// 管理器,管理非共享的部分
var FitnessManager = {
    fitnessData: {},
    
    // 添加一项
    add: function(name, sex, age, height, weight) {
        var fitness = FitnessFactory.create(sex);
        
        // 存储变化的数据
        this.fitnessData[name] = {
            age: age,
            height: height,
            weight: weight
        };

        return fitness;
    },
    
    // 从存储的数据中获取,更新至当前正在使用的对象
    updateFitnessData: function(name, obj) {
        var fitnessData = this.fitnessData[name];

        for (var item in fitnessData) {
            if (fitnessData.hasOwnProperty(item)) {
                obj[item] = fitnessData[item];
            }
        }
    }
};

// 开始评判
Fitness.prototype.judge = function(name) {
    // 操作前先更新当前状态(从外部状态管理器中获取)
    FitnessManager.updateFitnessData(name, this);

    var ret = name + &#39;: &#39;;

    if (this.sex === &#39;male&#39;) {
        ret += this.judgeMale();
    } else {
        ret += this.judgeFemale();
    }

    console.log(ret);
};

// 男性评判规则
Fitness.prototype.judgeMale = function() {
    var ratio = this.height / this.weight;

    return this.age > 20 ? (ratio > 3.5) : (ratio > 2.8);
};

// 女性评判规则
Fitness.prototype.judgeFemale = function() {
    var ratio = this.height / this.weight;
    
    return this.age > 20 ? (ratio > 4) : (ratio > 3);
};


var a = FitnessManager.add(&#39;A&#39;, &#39;male&#39;, 18, 160, 80);
var b = FitnessManager.add(&#39;B&#39;, &#39;male&#39;, 21, 180, 70);
var c = FitnessManager.add(&#39;C&#39;, &#39;female&#39;, 28, 160, 80);
var d = FitnessManager.add(&#39;D&#39;, &#39;male&#39;, 18, 170, 60);
var e = FitnessManager.add(&#39;E&#39;, &#39;female&#39;, 18, 160, 40);

// 开始评判
a.judge(&#39;A&#39;); // A: false
b.judge(&#39;B&#39;); // B: false
c.judge(&#39;C&#39;); // C: false
d.judge(&#39;D&#39;); // D: true
e.judge(&#39;E&#39;); // E: true
登入後複製

不过代码可能更复杂了,这个例子可能还不够充分,只是展示了享元模式如何实现,它节省了多个相似的对象,但多了一些操作。

factory对象有点像单例模式,只是多了一个sex的参数,如果没有内部状态,则没有参数的factory对象就更接近单例模式了

十、职责链模式

1. 定义

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链 传递该请求,直到有一个对象处理它为止

2. 核心

请求发送者只需要知道链中的第一个节点,弱化发送者和一组接收者之间的强联系,可以便捷地在职责链中增加或删除一个节点,同样地,指定谁是第一个节点也很便捷

3. 实现

以展示不同类型的变量为例,设置一条职责链,可以免去多重if条件分支

// 定义链的某一项
function ChainItem(fn) {
    this.fn = fn;
    this.next = null;
}

ChainItem.prototype = {
    constructor: ChainItem,
    
    // 设置下一项
    setNext: function(next) {
        this.next = next;
        return next;
    },
    
    // 开始执行
    start: function() {
        this.fn.apply(this, arguments);
    },
    
    // 转到链的下一项执行
    toNext: function() {
        if (this.next) {
            this.start.apply(this.next, arguments);
        } else {
            console.log(&#39;无匹配的执行项目&#39;);
        }
    }
};

// 展示数字
function showNumber(num) {
    if (typeof num === &#39;number&#39;) {
        console.log(&#39;number&#39;, num);
    } else {
        // 转移到下一项
        this.toNext(num);
    }
}

// 展示字符串
function showString(str) {
    if (typeof str === &#39;string&#39;) {
        console.log(&#39;string&#39;, str);
    } else {
        this.toNext(str);
    }
}

// 展示对象
function showObject(obj) {
    if (typeof obj === &#39;object&#39;) {
        console.log(&#39;object&#39;, obj);
    } else {
        this.toNext(obj);
    }
}

var chainNumber = new ChainItem(showNumber);
var chainString = new ChainItem(showString);
var chainObject = new ChainItem(showObject);

// 设置链条
chainObject.setNext(chainNumber).setNext(chainString);

chainString.start(&#39;12&#39;); // string 12
chainNumber.start({}); // 无匹配的执行项目
chainObject.start({}); // object {}
chainObject.start(123); // number 123
登入後複製

这时想判断未定义的时候呢,直接加到链中即可


// 展示未定义
function showUndefined(obj) {
    if (typeof obj === &#39;undefined&#39;) {
        console.log(&#39;undefined&#39;);
    } else {
        this.toNext(obj);
    }
}

var chainUndefined = new ChainItem(showUndefined);
chainString.setNext(chainUndefined);

chainNumber.start(); // undefined
登入後複製

由例子可以看到,使用了职责链后,由原本的条件分支换成了很多对象,虽然结构更加清晰了,但在一定程度上可能会影响到性能,所以要注意避免过长的职责链。

十一、中介者模式

1. 定义

所有的相关 对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可

2. 核心

使网状的多对多关系变成了相对简单的一对多关系(复杂的调度处理都交给中介者)

使用中介者后

3. 实现

多个对象,指的不一定得是实例化的对象,也可以将其理解成互为独立的多个项。当这些项在处理时,需要知晓并通过其他项的数据来处理。

如果每个项都直接处理,程序会非常复杂,修改某个地方就得在多个项内部修改

我们将这个处理过程抽离出来,封装成中介者来处理,各项需要处理时,通知中介者即可。

var A = {
    score: 10,

    changeTo: function(score) {
        this.score = score;

        // 自己获取
        this.getRank();
    },
    
    // 直接获取
    getRank: function() {
        var scores = [this.score, B.score, C.score].sort(function(a, b) {
            return a < b;
        });

        console.log(scores.indexOf(this.score) + 1);
    }
};

var B = {
    score: 20,

    changeTo: function(score) {
        this.score = score;

        // 通过中介者获取
        rankMediator(B);
    }
};

var C = {
    score: 30,

    changeTo: function(score) {
        this.score = score;

        rankMediator(C);
    }
};

// 中介者,计算排名
function rankMediator(person) {
    var scores = [A.score, B.score, C.score].sort(function(a, b) {
        return a < b;
    });

    console.log(scores.indexOf(person.score) + 1);
}

// A通过自身来处理
A.changeTo(100); // 1

// B和C交由中介者处理
B.changeTo(200); // 1
C.changeTo(50); // 3
登入後複製

ABC三个人分数改变后想要知道自己的排名,在A中自己处理,而B和C使用了中介者。B和C将更为轻松,整体代码也更简洁

最后,虽然中介者做到了对模块和对象的解耦,但有时对象之间的关系并非一定要解耦,强行使用中介者来整合,可能会使代码更为繁琐,需要注意。

十二、装饰者模式

1. 定义

以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。

是一种“即用即付”的方式,能够在不改变对 象自身的基础上,在程序运行期间给对象动态地 添加职责

2. 核心

是为对象动态加入行为,经过多重包装,可以形成一条装饰链

3. 实现

最简单的装饰者,就是重写对象的属性

var A = {
    score: 10
};

A.score = &#39;分数:&#39; + A.score;
登入後複製

可以使用传统面向对象的方法来实现装饰,添加技能

function Person() {}

Person.prototype.skill = function() {
    console.log(&#39;数学&#39;);
};

// 装饰器,还会音乐
function MusicDecorator(person) {
    this.person = person;
}

MusicDecorator.prototype.skill = function() {
    this.person.skill();
    console.log(&#39;音乐&#39;);
};

// 装饰器,还会跑步
function RunDecorator(person) {
    this.person = person;
}

RunDecorator.prototype.skill = function() {
    this.person.skill();
    console.log(&#39;跑步&#39;);
};

var person = new Person();

// 装饰一下
var person1 = new MusicDecorator(person);
person1 = new RunDecorator(person1);

person.skill(); // 数学
person1.skill(); // 数学 音乐 跑步
登入後複製

在JS中,函数为一等对象,所以我们也可以使用更通用的装饰函数

// 装饰器,在当前函数执行前先执行另一个函数
function decoratorBefore(fn, beforeFn) {
    return function() {
        var ret = beforeFn.apply(this, arguments);
        
        // 在前一个函数中判断,不需要执行当前函数
        if (ret !== false) {
            fn.apply(this, arguments);
        }
    };
}


function skill() {
    console.log(&#39;数学&#39;);
}

function skillMusic() {
    console.log(&#39;音乐&#39;);
}

function skillRun() {
    console.log(&#39;跑步&#39;);
}

var skillDecorator = decoratorBefore(skill, skillMusic);
skillDecorator = decoratorBefore(skillDecorator, skillRun);

skillDecorator(); // 跑步 音乐 数学
登入後複製

十三、状态模式

1. 定义

事物内部状态的改变往往会带来事物的行为改变。在处理的时候,将这个处理委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为

2. 核心

区分事物内部的状态,把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部

3. 实现

以一个人的工作状态作为例子,在刚醒、精神、疲倦几个状态中切换着

// 工作状态
function Work(name) {
    this.name = name;
    this.currentState = null;

    // 工作状态,保存为对应状态对象
    this.wakeUpState = new WakeUpState(this);
    // 精神饱满
    this.energeticState = new EnergeticState(this);
    // 疲倦
    this.tiredState = new TiredState(this);

    this.init();
}

Work.prototype.init = function() {
    this.currentState = this.wakeUpState;
    
    // 点击事件,用于触发更新状态
    document.body.onclick = () => {
        this.currentState.behaviour();
    };
};

// 更新工作状态
Work.prototype.setState = function(state) {
    this.currentState = state;
}

// 刚醒
function WakeUpState(work) {
    this.work = work;
}

// 刚醒的行为
WakeUpState.prototype.behaviour = function() {
    console.log(this.work.name, &#39;:&#39;, &#39;刚醒呢,睡个懒觉先&#39;);
    
    // 只睡了2秒钟懒觉就精神了..
    setTimeout(() => {
        this.work.setState(this.work.energeticState);
    }, 2 * 1000);
}

// 精神饱满
function EnergeticState(work) {
    this.work = work;
}

EnergeticState.prototype.behaviour = function() {
    console.log(this.work.name, &#39;:&#39;, &#39;超级精神的&#39;);
    
    // 才精神1秒钟就发困了
    setTimeout(() => {
        this.work.setState(this.work.tiredState);
    }, 1000);
};

// 疲倦
function TiredState(work) {
    this.work = work;
}

TiredState.prototype.behaviour = function() {
    console.log(this.work.name, &#39;:&#39;, &#39;怎么肥事,好困&#39;);
    
    // 不知不觉,又变成了刚醒着的状态... 不断循环呀
    setTimeout(() => {
        this.work.setState(this.work.wakeUpState);
    }, 1000);
};


var work = new Work(&#39;曹操&#39;);
登入後複製

点击一下页面,触发更新状态的操作

4. 优缺点

优点

状态切换的逻辑分布在状态类中,易于维护

缺点

多个状态类,对于性能来说,也是一个缺点,这个缺点可以使用享元模式来做进一步优化

将逻辑分散在状态类中,可能不会很轻易就能看出状态机的变化逻辑

十四、适配器模式

1. 定义

是解决两个软件实体间的接口不兼容的问题,对不兼容的部分进行适配

2. 核心

解决两个已有接口之间不匹配的问题

3. 实现

比如一个简单的数据格式转换的适配器

// 渲染数据,格式限制为数组了
function renderData(data) {
    data.forEach(function(item) {
        console.log(item);
    });
}

// 对非数组的进行转换适配
function arrayAdapter(data) {
    if (typeof data !== &#39;object&#39;) {
        return [];
    }

    if (Object.prototype.toString.call(data) === &#39;[object Array]&#39;) {
        return data;
    }

    var temp = [];

    for (var item in data) {
        if (data.hasOwnProperty(item)) {
            temp.push(data[item]);
        }
    }

    return temp;
}

var data = {
    0: &#39;A&#39;,
    1: &#39;B&#39;,
    2: &#39;C&#39;
};

renderData(arrayAdapter(data)); // A B C
登入後複製

十五、外观模式

1. 定义

为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使子系统更加容易使用

2. 核心

可以通过请求外观接口来达到访问子系统,也可以选择越过外观来直接访问子系统

3. 实现

外观模式在JS中,可以认为是一组函数的集合

// 三个处理函数
function start() {
    console.log(&#39;start&#39;);
}

function doing() {
    console.log(&#39;doing&#39;);
}

function end() {
    console.log(&#39;end&#39;);
}

// 外观函数,将一些处理统一起来,方便调用
function execute() {
    start();
    doing();
    end();
}


// 调用init开始执行
function init() {
    // 此处直接调用了高层函数,也可以选择越过它直接调用相关的函数
    execute();
}

init(); // start doing end
登入後複製

【相关推荐:javascript学习教程

以上是javascript中的設計模式有哪些的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板