underscore是一個非常不錯的基礎javascript庫,他提供了很多實用的方法,彌補了javascript原生 API調用的一些不足,讀他的文檔的時候,讀到array,object,uitlity的時候已經非常興奮了。覺得足夠用了。但是我看到function這部分的時候,發現這些函數真的非常的有意義,結合源代碼來看看這部分function相關的功能。
_.bind方法
最常見的方法。作用是改變默認的function中的this指向。需要說明的是在ECMA 5這個版本中function已經自帶了一個bind方法,參見
這里(該文章具體介紹了bind的集中使用場景)。bind的使用方法是:
_.bind(function, object, [*arguments])
下面是一個使用demo:
var func = function(greeting){
//this指向的是bind的第二個參數
// greeting 是bind的第三個參數
return greeting + ': ' + this.name
};
// bind返回的是一個新的function對象
var newfunc = _.bind(func, {name : 'moe'}, 'hi');
func();
原本以為這個bind的源碼會很簡單,無非就是用apply返回新的function,但是看源碼發現挺講究:
_.bind = function bind(func, context) {
var bound, args;
//如果function存在原生的bind方法使用原生的bind
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
//不是function,拋異常
if (!_.isFunction(func)) throw new TypeError;
//將后面的參數轉化成數組
args = slice.call(arguments, 2);
return bound = function() {
//如果當前的this已經指向的一個function的實例,就不需要再改變this的指向,因為此時的function已經作為一個構造函數在使用
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
//否則function視為構造函數使用,要保證this為構造函數的實例
ctor.prototype = func.prototype;
var self = new ctor;
//將function強制轉換成一個類的構造函數
var result = func.apply(self, args.concat(slice.call(arguments)));
//Object(result) === result 只有當result是Object時才會成立,基本的數據類型如number,string則不成立
if (Object(result) === result) return result;
return self;
};
};
代碼實現還是考慮了普通函數調用,構造函數調用,通過成員函數調用的情況,邏輯實現的很全面。
_.bindAll
bindAll方法可以將一個對象中所有的成員函數的this都指向這個對象,什么情況下對象的成員函數的this不指向對象呢?比如:
var buttonView = {
label : 'underscore',
onClick : function(){ alert('clicked: ' + this.label); },
onHover : function(){ console.log('hovering: ' + this.label); }
};
_.bindAll(buttonView);
//當成員函數作為事件監聽的時候,因為默認的事件監聽,this都會指向當前事件源
//bindAll之后可以保證onClick中的this仍指向buttonView
jQuery('#underscore_button').bind('click', buttonView.onClick);
_.memoize(function, [hashFunction])
該方法可以緩存函數返回結果,如果一個函數計算需要很長的時間,多次反復計算可以只計算一次緩存結果,默認的緩存key是函數調用時的第一個參數,也可以自己定義function(第二個參數)來計算key
_.memoize = function(func, hasher) {
var memo = {};//緩存存放位置
//_.indentity默認取數組第一個元素
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
delay : _.delay(function, wait, [*arguments])
delay方法在指定的wait后面執行函數與setTimeout功能一致
defer: _.defer(function, [*arguments])
defer也是延遲執行方法,不同的是他能保證在當前堆棧中的所有的代碼跑完之后再執行function。其實就是setTimeout(fn,1);
throttle:_.throttle(function, wait)
throttle這個單詞的意思是使減速,用于控制頻繁觸發的 function的的頻率,比如,拖動頁面滾動條時scroll方法會以很高的頻率觸發,如果在scroll的處理事件中做了很費時的操作,會導致瀏覽器假死,如果使用了throttle后,function被觸發的頻率可以降低。
document.body.onscroll = _.throttle(function(){
console.log("scrolling:"+(document.body.scrollTop|| document.body.scrollTop);
},100);
scroll事件默認50ms觸發一次,但是使用throttle之后事件觸發頻率為100ms一次
debounce: _.debounce(function, wait, [immediate])
debounce 本意是“使反跳”,這個翻譯是在讓人看不明白。同樣用于處理頻繁觸發的事件,處理方法時,對于頻繁處理的時間,只在第一次觸發(是否觸發取決于immdiate 參數),和事件頻繁觸發最后一次觸發(有最多wait的延時)。拿滾動事件為例,滾動事件50ms觸發一次,如果設置wait為100ms。則在最后一次觸發scroll事件時,也就是停止滾動時,在100ms后觸發function。如果immediate參數為true,開始滾動時也會觸發function
document.body.onscroll = _.debounce(function(){
//一次滾動過程觸發兩次該函數
console.log("scrolling:"+(document.body.scrollTop|| document.body.scrollTop);
},100,true);
在整個滾動過程中觸發function,對于只關注整個滾動前后變化的處理非常有用。
下面是_.debounce和_.throttle的源碼:
_.debounce = function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;//最后一次調用時清除延時
if (!immediate) result = func.apply(context, args);
};
var callNow = immediate && !timeout;
//每次func被調用,都是先清除延時再重新設置延時,這樣只有最后一次觸發func再經過wait延時后才會調用func
clearTimeout(timeout);//
timeout = setTimeout(later, wait);
//如果第一次func被調用 && immediate ->立即執行func
if (callNow) result = func.apply(context, args);
return result;
};
};
_.throttle = function(func, wait) {
var context, args, timeout, throttling, more, result;
//延時wait后將more throttling 設置為false
var whenDone = _.debounce(function(){
more = throttling = false;
}, wait);
return function() {
context = this; args = arguments;
var later = function() {
timeout = null;
if (more) { //more:最后一次func調用時,確保還能再調用一次
result = func.apply(context, args);
}
whenDone();
};
if (!timeout) timeout = setTimeout(later, wait);
if (throttling) {
more = true;
} else {
//每次觸發func 有會保證throttling 設置為true
throttling = true;
result = func.apply(context, args);
}
//每次觸發func 在 wait延時后將 more throttling 設置為false
whenDone();
return result;
};
};
once: _.once(function)
once能確保func只調用一次,如果用func返回一個什么對象,這個對象成了單例。源碼也比較簡單,無非就是用一個標志位來標示是否運行過,緩存返回值
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
memo = func.apply(this, arguments);
func = null;
return memo;
};
};
wrap: _.wrap(function, wrapper)
wrap可以將函數再包裹一層,返回一個新的函數,新的函數里面可以調用原來的函數,可以將原函數的處理結果再處理一次返回。類似與AOP切面。在函數處理前/后動態的添加一些額外的處理,下面是一個使用demo
var hello = function(name) { return "hello: " + name; };
//wrap返回一個新的函數
hello = _.wrap(hello, function(func) {
// 在新函數內部可以繼續調用原函數
return "before, " + func("moe") + ", after";
});
hello();
wrap的源碼:
_.wrap = function(func, wrapper) {
return function() {
//將原函數當新函數的一個參數傳入
var args = [func];
push.apply(args, arguments);
return wrapper.apply(this, args);
};
};
compose: _.compose(*functions)
將多個函數處理過程合并,每個函數可以調用前面函數的運行結果,_.compose(func1,func2);相當于func1(func2())。看看他的源碼:
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
//循環調用參數中的function
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];//先調用的函數結果最為下一個函數的參數
};
};
after:_.after(count, function)
創建一個新的函數,當func反復調用時,count次才調用一次,比如:
function a(){
alert("a");
}
var afterA = _.after(3,a);
afterA();//調用
afterA();//不alert
afterA();//不alert
afterA();//調用
源碼:
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) {
return func.apply(this, arguments);
}
};
};
總結:
上面這些函數在開發中經常能用到,能解決很多特定的問題。undercore的源碼也看得出非常老道,可以非常好的學習資料。
參考資料:
ES5中的bind介紹
從underscore.js的源碼學習javascript
帶注釋的underscore源碼