JavaScript中的函數是整個語言中最有趣的一部分,它們強大並且靈活。接下來,咱們來討論JavaScript中函數的一些經常使用技巧:javascript
1、函數綁定java
函數綁定是指建立一個函數,能夠在特定的this環境中已指定的參數調用另外一個函數。chrome
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。app
被綁定函數與普通函數相比有更多的開銷,消耗更多內存,同時也由於多重函數調用稍微慢一點,因此最好只在必要時調用。函數
2、函數柯里化性能
函數柯里化(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、函數尾調用與尾遞歸
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能夠跟蹤函數的調用棧。尾調用優化發生時,函數的調用棧會改寫,所以上面兩個變量就會失真。嚴格模式禁用這兩個變量,因此尾調用模式僅在嚴格模式下生效。
4、函數節流
瀏覽器中進行某些計算或處理要比其它操做消耗更多的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); }
5、函數惰性載入
由於瀏覽器之間的差別,咱們在使用某些函數的時候須要檢查瀏覽器的能力,這樣就可能存在不少條件判斷的代碼。例如,添加事件的代碼
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的實例,這就避免了在全局對象上意外設置屬性。
7、惰性實例化
惰性實例化避免了在頁面中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();
8、函數劫持
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
以上