JavaScript中的函數是整個語言中最有趣的一部分,它們強大而且靈活。接下來,我們來討論JavaScript中函數的一些常用技巧:
一、函數綁定
函數綁定是指創建一個函數,可以在特定的this環境中已指定的參數調用另一個函數。
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; var btn = document.getElementById("btn"); btn.onclick = handler.handleClick; //undefined:click
此處,message為undefined,因為沒有保存handler.handleClick的環境。
接下來我們實現一個將函數綁定到制定環境中的函數。
function bind(fn,context) { return function() { return fn.apply(context,arguments); } }
bind函數按如下方式使用:
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; function bind(fn,context) { return function() { return fn.apply(context,arguments); } } var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler); //handled:click
ECMAScript為所有函數定義了一個原生的bind函數
var handler = { message: "handled", handleClick: function(event) { console.log(this.message + ":" + event.type); } }; function bind(fn,context) { return function() { return fn.apply(context,arguments); } } var btn = document.getElementById("btn"); btn.onclick = handler.handleClick.bind(handler); //handled:click
支持原生bind方法的瀏覽器有IE9+、Firefox 4+和chrome。
被綁定函數與普通函數相比有更多的開銷,消耗更多內存,同時也因為多重函數調用稍微慢一點,所以最好只在必要時調用。
二、函數柯里化
函數柯里化(function currying)用於創建已經設置好了一個或多個參數的函數。其思想是使用一個閉包返回一個函數。
柯里化函數創建步驟:調用另一個函數並傳入要柯里化的函數和必要參數。創建柯里化函數的通用方式如下:
function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); }; }
我們可以按如下方式使用curry()函數:
function add(n1,n2) { return n1 + n2; } var currAdd = curry(add,5); alert(currAdd(3)); //8
function add(n1,n2) { return n1 + n2; } var currAdd = curry(add,2,3); alert(currAdd()); //5
柯里化作為函數綁定的一部分包含在其中,構造更加復雜的bind()函數:
function bind(fn,context) { var args = Array.prototype.slice.call(arguments, 2); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context,finalArgs); }; }
使用bind時,它會返回綁定到給定環境的函數,並且其中的某些函數參數已經被設置好。當你想除了event對象再額外給事件處理函數傳遞參數時是很有用的。
var handler = { message: "handled", handleClick: function(name,event) { console.log(this.message + ":" + name +":" + event.type); } }; var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler,"btn");
三、函數尾調用與尾遞歸
3.1尾調用
尾調用就是指某個函數的最后一步調用另一個函數
function fn() { g(1); }
尾調用不一定在函數尾部,只要是最后一步操作即可。
function f(x) { if (x > 0) { return m(x) } return n(x); }
m、n都是尾調用,它們都是函數f的最后一步操作。
我們知道,函數調用會在內存形成一個"調用記錄",又稱"調用幀"(call frame),保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那么在A的調用記錄上方,還會形成一個B的調用記錄。等到B運行結束,將結果返回到A,B的調用記錄才會消失。如果函數B內部還調用函數C,那就還有一個C的調用記錄棧,以此類推。所有的調用記錄,就形成一個"調用棧"(call stack)。
尾調用由於是函數的最后一步操作,所以不需要保留外層函數的調用記錄,因為調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就可以了。
尾調用優化:只保留內層函數的調用記錄。如果所有函數都是尾調用,那么可以做到每次執行時,調用記錄只有一項,這樣可以大大節省內存。注意:ES5中還沒有這個優化機制。
3.2尾遞歸
尾遞歸就是指在函數的最后一步調用自己。
在JS的遞歸調用中,JS引擎將為每次遞歸開辟一段內存用以儲存遞歸截止前的數據,這些內存的數據結構以“棧”的形式存儲,這種方式開銷非常大,並且一般瀏覽器可用的內存非常有限。所以遞歸次數多的時候,容易發生棧溢出。但是對於尾遞歸來說,由於我們只需要保存 一個調用的記錄,所以不會發生錯誤。因此,尾調用優化是很重要的。ES6規定,所有ECMAScript的實現,都必須部署尾調用優化。
函數遞歸改寫為尾遞歸:
下面是一個求階乘的函數:
function factorial(n) { if(n === 1) { return 1; } return n * factorial(n - 1); }
function tFactorial(n,total) { if(n === 1) { return total; } return tFactorial(n - 1, n * total); } function factorial(n) { return tFactorial(n,1); } factorial(10);
另外,我們也可以借助上面提到的柯里化來實現改寫:
function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); }; } function tailFactorial(total, n) { if (n === 1) { return total; } return tailFactorial(n * total, n - 1); } const factorial = curry(tailFactorial, 1); alert(factorial(10));
使用ES6中函數的默認值:
function factorial(n, total = 1) { if (n === 1) { return total }; return factorial(n - 1, n * total); } factorial(10);
最后,我們要注意:ES6中的尾調用優化只是在嚴格模式下開啟的。這是因為正常模式下函數內部的兩個變量arguments和fn.caller可以跟蹤函數的調用棧。尾調用優化發生時,函數的調用棧會改寫,因此上面兩個變量就會失真。嚴格模式禁用這兩個變量,所以尾調用模式僅在嚴格模式下生效。
四、函數節流
瀏覽器中進行某些計算或處理要比其它操作消耗更多的CPU時間和內存,譬如DOM操作。如果我們嘗試進行過多的DOM相關的操作可能會導致瀏覽器掛起,甚至崩潰。例如,如果我們在onresize事件處理程序內部進行DOM操作,很可能導致瀏覽器崩潰(尤其是在IE中)。為此,我們要進行函數節流。
函數節流是指某些代碼不能在沒有間斷的情況連續重復進行。實現方法:函數在第一次被調用的時候,會創建一個定時器,指定時間間隔之后執代碼。之后函數被調用的時候,它會清除前一次的定時器並設置另一個。如果前一個定時器已經執行,那么這個操作沒有任何意義。如果前一個定時器沒有執行,那么就相當於將它替換成一個新的定時器。基本形式如下:
var processor = { tmID: null, exeProcess: function() { }, process: function() { clearTimeout(this.tmID); var that = this; this.tmID = setTimeout(function() { that.exeProcess(); },100); } } processor.process();
我們可以簡化如下:
function throttle(fn,context) { clearTimeout(fn.tid); fn.tid = setTimeout(function() { fn.call(context); },100); }
接下來,我們看一下上面的函數的應用。如下是一個resize事件的事件處理函數:
window.onresize = function() { var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px"; }
上面的代碼為window添加了一個resize事件處理函數,但是這可能會造成瀏覽器運行緩慢。這時,我們就用到了函數節流了。
function resizeDiv() { var div = document.getElementById("myDiv"); div.style.height = div.offsetWidth + "px"; } window.onresize = function() { throttle(resizeDiv); }
五、函數惰性載入
因為瀏覽器之間的差異,我們在使用某些函數的時候需要檢查瀏覽器的能力,這樣就可能存在很多條件判斷的代碼。例如,添加事件的代碼
var addEvent = function(el,type,handle) { if(el.addEventListener) { el.addEventListener(type,handle,false); } else if(el.attachEvent) { el.attachEvent("on"+type,handle); } else { el["on" + type] = handle; } }
然而,能力檢測只需要進行一次就可以了。沒必要調用函數的時候都需要進行一次判斷,這樣顯然造成沒必要的浪費。我們可以用函數的惰性載入技巧來解決上述問題。
惰性載入表示函數執行的分支只會發生一次,實現方式有兩種。
第一種就是在函數第一次被調用時,自身會被覆蓋成另一個更合適的函數,如下:
var addEvent = function(el,type,handle) { if(el.addEventListener) { addEvent = function(el,type,handle){ el.addEventListener(type,handle,false); } } else if(el.attachEvent) { addEvent = function(el,type,handle){ el.attachEvent("on"+type,handle); } } else { addEvent = function(el,type,handle){ el["on" + type] = handle; } } addEvent(el,type,handle); }
或者簡單一點:
var addEvent = function(el,type,handle){ addEvent = el.addEventListener ? function(el,type,handle){ el.addEventListener(type,handle,false); } : function(el,type,handle){ el.attachEvent("on"+type,handle); }; addEvent(el,type,handle); }
第二種是在聲明函數時就指定適合的函數:
var addEvent = (function(el,type,handle) { if(addEventListener) { return function(el,type,handle){ el.addEventListener(type,handle,false); } } else if(attachEvent) { return function(el,type,handle){ el.attachEvent("on"+type,handle); } } else { return function(el,type,handle){ el["on" + type] = handle; } } })();
當我們在使用構造函數創建實例的時候,如果我們忘記使用new,那么該函數就相當於普通的函數被調用。由於this是在運行時才綁定的,所以this會映射到全局對象window上。也就是說,調用該函數相當於為全局對象添加屬性,這會污染全局空間,造成不必要的錯誤。
function Person(name,age) { this.name = name; this.age = age; } var Marco = Person('Marco',22); console.log(name); // Marco
function Person(name,age) { if(this instanceof Person) { this.name = name; this.age = age; } else { return new Person(name,age); } } var Marco = Person('Marco',22); console.log(name); //undefined
這樣,調用Person構造函數時,無論是否使用new操作符,都會返回一個Person的實例,這就避免了在全局對象上意外設置屬性。
七、惰性實例化
惰性實例化避免了在頁面中js初始化執行的時候就實例化了類。如果在頁面中沒有使用到這個實例化的對象,那么這就造成了一定的內存浪費和性能消耗,那么如果將一些類的實例化推遲到需要使用它的時候才開始去實例化,那么這就避免了剛才說的問題,做到了“按需供應”。惰性實例化應用到資源密集、配置開銷較大、需要加載大量數據的單體時是很有用的。如下:
var myNamespace2 = function(){ var Configure = function(){ var privateName = "someone's name"; var privateReturnName = function(){ return privateName; } var privateSetName = function(name){ privateName = name; } //返回單例對象 return { setName:function(name){ privateSetName(name); }, getName:function(){ return privateReturnName(); } } } //儲存configure的實例 var instance; return { init:function(){ //如果不存在實例,就創建單例實例 if(!instance){ instance = Configure(); } //將Configure創建的單例 for(var key in instance){ if(instance.hasOwnProperty(key)){ this[key]=instance[key]; } } this.init = null; return this; } } }(); //使用方式: myNamespace2.init(); myNamespace2.getName();
八、函數劫持
JavaScript函數劫持即javascript hijacking,通俗來講就是通過替換js函數的實現來達到劫持該函數的目的。我們可以這樣實現函數劫持:保存原函數的實現,替換為我們自己的函數實現。添加我們的處理邏輯之后調用原來的函數實現。如下:
var _alert = alert; window.alert = function(str) { // 我們的處理邏輯 console.log('ending...'); _alert(str); } alert(111);
反劫持
1)首先我們要判斷某個函數是否被劫持
var _alert = alert; window.alert = function(str) { // 我們的處理邏輯 console.log('ending...'); _alert(str); } console.log(alert); console.log(_alert);
結果:
function (str) { // 我們的處理邏輯 console.log('ending...'); _alert(str); } function alert() { [native code] }
可以發現內置的函數體為[native code],那我們就可以根據這個判斷函數是否被劫持了。
2)如何反劫持
我們要回復被劫持的函數,可以通過創建個新的環境,然后用新環境里的干凈的函數來恢復我們這里被劫持的函數,怎么創建新環境?創建新的iframe好了,里面就是個全新的環境。
var _alert = alert; window.alert = function(str) { // 我們的處理邏輯 console.log('ending...'); _alert("呵呵"); } function unHook() { var f = document.createElement("iframe"); f.style.border = "0"; f.style.width = "0"; f.style.height = "0"; document.body.appendChild(f); var d = f.contentWindow.document; d.write(""); d.close(); } unHook(); alert(111); // 11
以上