JavsScript 設計模式(一)
讀完本文可以學到
- this花鹅、call、apply交掏、bind
- 閉包
- 什么是高階函數(shù)
- 高階函數(shù)的應用妆偏,包括AOP、currying盅弛、debounce等
[TOC]
之所以在學習設計模式之前钱骂,先理解這些內容,是因為設計模式里面很多函數(shù)作為參數(shù)挪鹏,或者作為返回的值见秽,還有很多call和apply的使用。
1. this讨盒、call解取、apply和bind
1.1 this
我們知道,this指向一個對象返顺,具體的指向由運行時基于函數(shù)的執(zhí)行環(huán)境動態(tài)來綁定的禀苦,需要注意的地方時蔓肯,this不一定指向函數(shù)被聲明時的環(huán)境≌穹Γ總結來說就是this指向最后調用它的那個對象蔗包。
看一個例子:
var outername = 'window';
var obj = {
fn: function(){
console.log(this.name);
}
}
console.log(obj.fn())
輸出的結果是undefined,因為最后調用fn的對象是obj慧邮,而對象obj中沒有name的定義调限,所有就是undefined。這說明this永遠指向最后調用它對象赋咽,也不會繼續(xù)向上一個對象尋找this.name。
改變this的指向吨娜,除了不常用的with和eval之外脓匿,通常有5種方式
- 作為對象的方法調用
當作為函數(shù)的方法調用時,this指向該對象
var outername = 'window';
var obj = {
fn: function(){
console.log(this.name);
console.log(this === obj);
}
}
obj.fn()
- 作為普通函數(shù)調用
此時this指向window對象
var outername = 'window';
var obj = {
fn: function(){
console.log(this.name);
console.log(this === obj);
}
}
var objFn = obj.fn宦赠;
objFn();
- 作為構造函數(shù)調用
通常情況下陪毡,構造器里面的this就是指向返回的這個對象
var Myconstructor = function (){
this.name ='inner'
};
var myInstance = new Myconstructor();
myInstance.name;
這里需要注意的是,如果構造函數(shù)中沒有顯示的返回一對象勾扭,那么this指向的就是構造函數(shù)毡琉,否則指向返回的那個對象。
var Myconstructor = function (){
this.name ='inner';
return {
name: 'inreturn',
};
};
var myInstance = new Myconstructor();
myInstance.name;
ES6 的箭頭函數(shù)
箭頭函數(shù)的this始終指向函數(shù)定義時的this妙色,而非執(zhí)行時桅滋。通過Function.prototype.call或者Function.prototype.apply來調用
call和apply可以動態(tài)的傳入函數(shù)的this
var obj = {
name: 'obj';
getName: function(){
return this.name;
}
}
var obj1 = {
name: 'obj1'
}
obj.getName()
obj.getName.call(obj1);
1.2 call
語法:
fun.call(thisArg[, arg1[, arg2[, ...]]])
thisArg代表fun運行時指定的this的值
call() 方法調用一個函數(shù), 其具有一個指定的this值和分別地提供的參數(shù)(參數(shù)的列表)
我們先看個例子:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
如何來簡單的實現(xiàn)這個call方法呢?
我們把上面的代碼修改成:
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1
可以看到,this指向了foo身辨,但是缺點是在foo上增加了一個方法丐谋,不過沒關系,我們用完刪了不就可以了煌珊,所以我們來看看步驟:
- 把函數(shù)設置為對象的屬性
- 執(zhí)行該函數(shù)
- 刪除該函數(shù)
Function.prototype.call2 = function(context) {
// 首先要獲取調用call的函數(shù)号俐,用this可以獲取
context.fn = this;
context.fn();
delete context.fn;
}
看一個如何模擬call方法
Function.prototype.call = function(context) {
var context = context || window;
context.fn = this;
var args = Array.prototype.slice.call(arguments,1);
var result = context.fn(...args);
delete context.fn
return result;
}
如何去模擬實現(xiàn)call可以參考
https://github.com/mqyqingfeng/Blog/issues/11
1.3 apply
語法:
fun.apply(thisArg, [argsArray])
apply() 方法調用一個函數(shù), 其具有一個指定的this值,以及作為一個數(shù)組(或類似數(shù)組的對象)提供的參數(shù)
apply和call的區(qū)別在于傳入?yún)?shù)不一樣定庵,但是還有一個比較大的區(qū)別吏饿,call的運行效率要比apply的高。
前段時間看了underscore的源碼蔬浙,其中有這么一部分
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
case 2: return function(value, other) {
return func.call(context, value, other);
};
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
其他中間那些func.call是可以不寫的猪落,直接最后return func.apply不就可以了么。
apply的模擬方法和call類似就不說啦
1.4 bind
語法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind()方法創(chuàng)建一個新的函數(shù), 當被調用時畴博,將其this關鍵字設置為提供的值许布,在調用新函數(shù)時,在任何提供之前提供一個給定的參數(shù)序列绎晃。也就是說bind方法是創(chuàng)建一個方法蜜唾,我們必須手動的調用
作用:
- 創(chuàng)建綁定函數(shù)杂曲,最簡單的用法是創(chuàng)建一個函數(shù),使這個函數(shù)不論怎么調用都有同樣的 this 值
this.x = 9;
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 返回 81
var retrieveX = module.getX;
retrieveX(); // 返回 9, 在這種情況下袁余,"this"指向全局作用域
// 創(chuàng)建一個新函數(shù)擎勘,將"this"綁定到module對象
// 新手可能會被全局的x變量和module里的屬性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
- 偏函數(shù), 另一個最簡單的用法是使一個函數(shù)擁有預設的初始參數(shù)
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
- 配合setTimeout
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' +
this.petalCount + ' petals!');
};
var flower = new LateBloomer();
flower.bloom(); // 一秒鐘后, 調用'declare'方法
我們都知道bind方法是有兼容性問題的颖榜,那么我們怎么實現(xiàn)一個bind方法呢棚饵?
我們看看最簡單的版本
Function.prototype.bind = function(context) {
var self = this;
return function() {
self.apply(context);
};
};
進階版本:
Function.prototype.bind = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var bindArgs = Array.prototype.slice.call(arguments);
self.apply(context, arg.contact(bindArgs));
};
}
可以傳參數(shù),可以返回函數(shù)了掩完,但是還有一個問題沒解決噪漾,因為bind方法還有一個特點:
一個綁定函數(shù)也能使用new操作符創(chuàng)建對象:這種行為就像把原函數(shù)當成構造器。提供的 this 值被忽略且蓬,同時調用時的參數(shù)被提供給模擬函數(shù)欣硼。
MDN上的版本
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 獲取調用時(fBound)的傳參.bind 返回的函數(shù)入?yún)⑼沁@么傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關系
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
2. 閉包
2.1 概念
MDN 對閉包的定義為:
閉包是指那些能夠訪問自由變量的函數(shù)
那么這里我們需要理解什么是自由變量
自由變量是指在函數(shù)中使用的,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量
舉個例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
foo 函數(shù)可以訪問a恶阴,并且a不是函數(shù)參數(shù)也不是函數(shù)局部變量诈胜,所有foo函數(shù)和a就形成了閉包。
理論上來說所有的函數(shù)都是閉包冯事,但是在實踐角度來說閉包的定義是:
- 首先是一個函數(shù)
- 函數(shù)的上下文即使已經(jīng)銷毀焦匈,它依然存在
- 函數(shù)的代碼中應用了自由變量
我們先看一個簡單的例子:
var func = function () {
var a = 1;
return function() {
a++;
alert(a);
}
}
var f = func();
f();
f();
f();
我們可以看到,當退出函數(shù)之后昵仅,局部變量a并沒有消失缓熟。只是因為在執(zhí)行var f = func()時,f返回了一個匿名函數(shù)的引用摔笤,這個函數(shù)可以訪問到a變量荚虚,所以a沒有被銷毀
2.2 應用
- 封裝變量
閉包可以把一些不需要暴露在全局變量中的變量封裝為私有變量
var mult = function() {
var a = 1;
for(var i = 0; i < arguments.length; i++) {
a = a * arguments[i];
}
return a;
}
這個函數(shù)本身沒啥問題,那么如果我們需要把算出來的結果緩存一波籍茧,怎么辦呢版述?
var cache = {};
var mult = function() {
var args = Array.prototype.join.call(arguments, ',');
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return cache[args] = a;
}
這樣似乎可以解決問題,但是caches暴露在了外部寞冯,我們可能改變它的值渴析。
var mult = (function() {
var cache = {};
return function(){
var args = Array.prototype.join.call(arguments, ',');
if (cache[args]) {
return cache[args];
}
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return cache[args] = a;
}
})()
我們看到這個函數(shù)和上面沒啥大的區(qū)別,就是用一個立即執(zhí)行函數(shù)把之前的mult函數(shù)包裹了起來吮龄,然后返回俭茧。
- 延續(xù)局部變量的壽命
我們看一個例子:
var report = function(src) {
var img = new Image();
img.src = src;
}
report('http://xxxx.img.jpg')
這個代碼看起來好像沒啥問題,但是在IE7之類的低版本瀏覽器會有問題漓帚,report函數(shù)并不是每次都可以發(fā)起http請求母债,原因就是img是report的局部變量,當report函數(shù)調用結束之后,img局部變量就銷毀了毡们,這個時候有可能還么發(fā)起http請求迅皇,所有請求就丟失掉啦。
var report = (function() {
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
}
})()
2.3 閉包與設計模式
在設計模式中衙熔,有很多的地方用到了閉包登颓,例如命令模式等等。后續(xù)再詳細的介紹
3. 高階函數(shù)概念
高階函數(shù)是指至少滿足下列兩個條件之一的函數(shù)
- 函數(shù)可以作為參數(shù)被傳遞
- 函數(shù)可以作為返回值輸出
3.1 函數(shù)作為參數(shù)傳遞
- 作為回調函數(shù)
如要我們需要對業(yè)務做一個流程的控制红氯,可以利用回調函數(shù)來實現(xiàn)框咙,例如做完A,然后才去做B
var doA = function (callback) {
console.log('do a')
callback();
}
var doB = function(){
console.log('do b');
}
doA(doB);
- Array.prototype.sort
接受一個函數(shù)作為參數(shù)
3.2 函數(shù)作為返回值
其實簽名的例子就有函數(shù)作為返回值的例子痢甘,我們接下來看兩個例子
- 判斷數(shù)據(jù)類型
我們之前其實知道了如何去判斷一個數(shù)據(jù)是否為數(shù)組或者字符串
var isArray = function(obj) {
return Object.prototype.toString.call(obj) == '[object Array]'
};
var isString = function(obj) {
return Object.prototype.toString.call(obj) == '[object String]'
};
其實這些代碼都是重復的喇嘱,我們可以返回一個函數(shù)來減少代碼
var isType = function(type) {
return function(obj) {
return Object.prototype.toString.call(obj) == '[object ' + type + ' ]';
};
}
var isString = isType('String');
var isArray = isType('Array');
- getSingle
我們再來看一個單例模式的例子:
var getSingle = function ( fn ) {
var ret;
return function () {
return ret || ( ret = fn.apply( this, arguments ) );
};
};
我們可以看到這里既把函數(shù)作為了輸出,又把函數(shù)作為了返回值塞栅,我們看單例模式的應用:
var getNode = getSingle(function(){
return document.createElement('script');
});
var node1 = getNode();
var node2 = getNode();
node1 === node2
4. 高階函數(shù)的應用
4.1 高階函數(shù)與AOP
AOP 也叫做面向切面編程者铜,作用就是把一些和核心業(yè)務邏輯無關的代碼抽離出來,比如說:日志處理构蹬、安全控制王暗、異常處理等等悔据。在Java中可以利用反射和動態(tài)代理來實現(xiàn)AOP庄敛,而對于JavaScript這種動態(tài)語言來說,它天生就有這種能力科汗,利用Function.prototype就可以很容易做到AOP編程
Function.prototype.before = function(beforefn) {
var _self = this;
// 這里保持了原函數(shù)的引用
return function(){
// 返回包含了原函數(shù)和新函數(shù)的代理函數(shù)
beforefn.apply(this, arguments);
// 執(zhí)行函數(shù)藻烤,并且保證this不被劫持,新函數(shù)接受的參數(shù)也會被傳入原來的函數(shù)头滔,新函數(shù)在原函數(shù)之前執(zhí)行
return _self.apply(this, arguments);
// 執(zhí)行原函數(shù)并返回原函數(shù)的執(zhí)行結果
// 并且保證this不被劫持
}
}
Function.prototype.after = function(afterfn) {
var _self = this;
return function() {
var ret = _self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
};
}
var func = function(){
console.log('2');
}
func = func.before(function(){
console.log('1');
}).after(function(){
console.log('3');
});
func();
這其實就是裝飾者模式
4.2 高階函數(shù)與柯里化
柯里化定義
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.
柯里化作用
- 延遲計算
- 參數(shù)復用
- 動態(tài)生產函數(shù)的作用
看個例子吧
function add(a, b) {
return a + b;
}
add(1, 2) // 3
// 假設有一個 curry 函數(shù)可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3
用閉包把參數(shù)保存起來怖亭,當參數(shù)的數(shù)量足夠執(zhí)行函數(shù)了,就開始執(zhí)行函數(shù)坤检,有沒有毛病
我們看一個版本的實現(xiàn)
function curry(fn, args) {
var length = fn.length;
args = args || [];
return function() {
var _args = args.slice(0),
arg, i;
for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
_args.push(arg);
}
if (_args.length < length) {
return curry.call(this, fn, _args);
}
else {
return fn.apply(this, _args);
}
}
}
var fn = curry(function(a, b, c) {
console.log([a, b, c]);
});
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
再看一個高顏值的寫法
var curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg) => judge(...args, args
4.3 高階函數(shù)與函數(shù)防抖
其實函數(shù)防抖以前分享過一次兴猩,但是只是分享了場景和underscore的api,這里我們仔細看一下如何去實現(xiàn)一個函數(shù)防抖
防抖的原理就是早歇,隨便怎么觸發(fā)事件倾芝,但是事件只會在n秒之后才執(zhí)行,但是如果在n秒內你又觸發(fā)了箭跳,那就重新計時晨另,總之就是我一定要等你觸發(fā)完事件了,并且n秒內不再觸發(fā)谱姓,我才執(zhí)行
先看第一版
function debounce(func, wait) {
var timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
}
}
container.onmousemove = debounce(doSomething, 1000);
如果不使用debounce函數(shù)借尿, doSomething中的this指向的是container函數(shù),使用了之后this指向了window對象,所以上面的版本是有問題的
function debounce(func, wait) {
var timeout;
return function(){
clearTimeout(timeout);
var context = this;
timeout = setTimeout(function(){
func.apply(context);
}, wait);
};
}
但是這個函數(shù)還是有問題路翻,因為dosomething(e)中的e在經(jīng)過debounce后打印出來的是undefined狈癞。其實只需要把參數(shù)綁定到新的函數(shù)就可以啦
function debounce(func, wait) {
var timeout;
return function(){
clearTimeout(timeout);
var context = this;
var args = arguments;
timeout = setTimeout(function(){
func.apply(context, args);
}, wait);
};
}
我們做了兩件事
- 改變了this的指向
- 綁定了參數(shù)event
我們可以考慮一個新的需求,我們希望事件立即被觸發(fā)帚桩,然后等停止觸發(fā)n秒后亿驾,才重新觸發(fā)函數(shù)
function debounce(func, wait, immediate) {
var timeout, result;
return function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已經(jīng)執(zhí)行過,不再執(zhí)行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
}
接下來我們看看underscore的實現(xiàn)
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result;
var later = function() {
var last = _.now() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
參考資料:
書籍:《JavaScript設計模式與實踐》
博客鏈接:https://github.com/mqyqingfeng/Blog/issues/22