javascript-函式的5個高階技巧
函式對任何一門語言來說都是一個核心的概念,在javascript中更是如此。本文將介紹函式的5個高階技巧

作用域安全的建構函式
建構函式其實就是一個使用new操作符呼叫的函式
function Person(name,age,job){this.name=name;this.age=age;this.job=job; }var person=new Person('match',28,'Software Engineer'); console.log(person.name);//match
如果沒有使用new操作符,原本針對Person物件的三個屬性被新增到window物件
function Person(name,age,job){this.name=name;this.age=age;this.job=job; } var person=Person('match',28,'Software Engineer'); console.log(person);//undefinedconsole.log(window.name);//match
window的name屬性是用來標識連結目標和框架的,這裡對該屬性的偶然覆蓋可能會導致頁面上的其它錯誤,這個問題的解決方法就是建立一個作用域安全的建構函式
function Person(name,age,job){if(this instanceof Person){this.name=name;this.age=age;this.job=job; }else{return new Person(name,age,job); } }var person=Person('match',28,'Software Engineer'); console.log(window.name); // ""console.log(person.name); //'match'var person= new Person('match',28,'Software Engineer'); console.log(window.name); // ""console.log(person.name); //'match'
但是,對建構函式竊取模式的繼承,會帶來副作用。這是因為,下列程式碼中,this物件並非Polygon物件例項,所以建構函式Polygon()會建立並返回一個新的例項
function Polygon(sides){if(this instanceof Polygon){this.sides=sides;this.getArea=function(){return 0; } }else{return new Polygon(sides); } }functionRectangle(wifth,height){ Polygon.call(this,2);this.width=this.width;this.height=height;this.getArea=function(){return this.width * this.height; }; }var rect= new Rectangle(5,10); console.log(rect.sides); //undefined
如果要使用作用域安全的建構函式竊取模式的話,需要結合原型鏈繼承,重寫Rectangle的prototype屬性,使它的例項也變成Polygon的例項
function Polygon(sides){if(this instanceof Polygon){this.sides=sides;this.getArea=function(){return 0; } }else{return new Polygon(sides); } }functionRectangle(wifth,height){ Polygon.call(this,2);this.width=this.width;this.height=height;this.getArea=function(){return this.width * this.height; }; } Rectangle.prototype= new Polygon();var rect= new Rectangle(5,10); console.log(rect.sides); //2
惰性載入函式
因為各瀏覽器之間的行為的差異,我們經常會在函式中包含了大量的if語句,以檢查瀏覽器特性,解決不同瀏覽器的相容問題。比如,我們最常見的為dom節點新增事件的函式
function addEvent(type, element, fun) {if (element.addEventListener) { element.addEventListener(type, fun, false); }else if(element.attachEvent){ element.attachEvent('on' + type, fun); }else{ element['on' + type] = fun; } }
每次呼叫addEvent函式的時候,它都要對瀏覽器所支援的能力進行檢查,首先檢查是否支援addEventListener方法,如果不支援,再檢查是否支援attachEvent方法,如果還不支援,就用dom0級的方法新增事件。這個過程,在addEvent函式每次呼叫的時候都要走一遍,其實,如果瀏覽器支援其中的一種方法,那麼他就會一直支援了,就沒有必要再進行其他分支的檢測了。也就是說,if語句不必每次都執行,程式碼可以執行的更快一些。
解決方案就是惰性載入。所謂惰性載入,指函式執行的分支只會發生一次,有兩種實現惰性載入的方式
1、第一種是在函式被呼叫時,再處理函式。函式在第一次呼叫時,該函式會被覆蓋為另外一個按合適方式執行的函式,這樣任何對原函式的呼叫都不用再經過執行的分支了
我們可以用下面的方式使用惰性載入重寫addEvent()
function addEvent(type, element, fun) {if (element.addEventListener) { addEvent = function (type, element, fun) { element.addEventListener(type, fun, false); } }else if(element.attachEvent){ addEvent = function (type, element, fun) { element.attachEvent('on' + type, fun); } }else{ addEvent = function (type, element, fun) { element['on' + type] = fun; } }return addEvent(type, element, fun); }
在這個惰性載入的addEvent()中,if語句的每個分支都會為addEvent變數賦值,有效覆蓋了原函式。最後一步便是呼叫了新賦函式。下一次呼叫addEvent()時,便會直接呼叫新賦值的函式,這樣就不用再執行if語句了
但是,這種方法有個缺點,如果函式名稱有所改變,修改起來比較麻煩
2、第二種是宣告函式時就指定適當的函式。 這樣在第一次呼叫函式時就不會損失效能了,只在程式碼載入時會損失一點效能
以下就是按照這一思路重寫的addEvent()。以下程式碼建立了一個匿名的自執行函式,通過不同的分支以確定應該使用哪個函式實現
var addEvent = (function () {if (document.addEventListener) {return function (type, element, fun) { element.addEventListener(type, fun, false); } }else if (document.attachEvent) {return function (type, element, fun) { element.attachEvent('on' + type, fun); } }else {return function (type, element, fun) { element['on' + type] = fun; } } })();
函式繫結
在javascript與DOM互動中經常需要使用函式繫結,定義一個函式然後將其繫結到特定DOM元素或集合的某個事件觸發程式上,繫結函式經常和回撥函式及事件處理程式一起使用,以便把函式作為變數傳遞的同時保留程式碼執行環境
<button id="btn">按鈕</button><script> var handler={ message:"Event handled.", handlerFun:function(){ alert(this.message); } }; btn.onclick = handler.handlerFun;</script>
上面的程式碼建立了一個叫做handler的物件。handler.handlerFun()方法被分配為一個DOM按鈕的事件處理程式。當按下該按鈕時,就呼叫該函式,顯示一個警告框。雖然貌似警告框應該顯示Event handled,然而實際上顯示的是undefiend。這個問題在於沒有儲存handler.handleClick()的環境,所以this物件最後是指向了DOM按鈕而非handler
可以使用閉包來修正這個問題
<button id="btn">按鈕</button><script>var handler={ message:"Event handled.", handlerFun:function(){ alert(this.message); } }; btn.onclick = function(){ handler.handlerFun(); }</script>
當然這是特定於此場景的解決方案,建立多個閉包可能會令程式碼難以理解和除錯。更好的辦法是使用函式繫結
一個簡單的繫結函式bind()接受一個函式和一個環境,並返回一個在給定環境中呼叫給定函式的函式,並且將所有引數原封不動傳遞過去
function bind(fn,context){return function(){return fn.apply(context,arguments); } }
這個函式似乎簡單,但其功能是非常強大的。在bind()中建立了一個閉包,閉包使用apply()呼叫傳入的函式,並給apply()傳遞context物件和引數。當呼叫返回的函式時,它會在給定環境中執行被傳入的函式並給出所有引數
<button id="btn">按鈕</button><script>function bind(fn,context){return function(){return fn.apply(context,arguments); } } var handler={ message:"Event handled.", handlerFun:function(){ alert(this.message); } }; btn.onclick = bind(handler.handlerFun,handler);</script>
ECMAScript5為所有函式定義了一個原生的bind()方法,進一步簡化了操作
只要是將某個函式指標以值的形式進行傳遞,同時該函式必須在特定環境中執行,被繫結函式的效用就突顯出來了。它們主要用於事件處理程式以及setTimeout()和setInterval()。然而,被繫結函式與普通函式相比有更多的開銷,它們需要更多記憶體,同時也因為多重函式呼叫稍微慢一點,所以最好只在必要時使用
函式柯里化
與函式繫結緊密相關的主題是函式柯里化(function currying),它用於建立已經設定好了一個或多個引數的函式。函式柯里化的基本方法和函式繫結是一樣的:使用一個閉包返回一個函式。兩者的區別在於,當函式被呼叫時,返回的函式還需要設定一些傳入的引數
function add(num1,num2){return num1+num2; }function curriedAdd(num2){return add(5,num2); } console.log(add(2,3));//5console.log(curriedAdd(3));//8
這段程式碼定義了兩個函式:add()和curriedAdd()。後者本質上是在任何情況下第一個引數為5的add()版本。儘管從技術來說curriedAdd()並非柯里化的函式,但它很好地展示了其概念
柯里化函式通常由以下步驟動態建立:呼叫另一個函式併為它傳入要柯里化的函式和必要引數。下面是建立柯里化函式的通用方式
function curry(fn){var args = Array.prototype.slice.call(arguments, 1);return function(){var innerArgs = Array.prototype.slice.call(arguments), finalArgs = args.concat(innerArgs);return fn.apply(null, finalArgs); }; }
curry()函式的主要工作就是將被返回函式的引數進行排序。curry()的第一個引數是要進行柯里化的函式,其他引數是要傳入的值。為了獲取第一個引數之後的所有引數,在arguments物件上呼叫了slice()方法,並傳入引數1表示被返回的陣列包含從第二個引數開始的所有引數。然後args陣列包含了來自外部函式的引數。在內部函式中,建立了innerArgs陣列用來存放所有傳入的引數(又一次用到了slice())。有了存放來自外部函式和內部函式的引數陣列後,就可以使用concat()方法將它們組合為finalArgs,然後使用apply()將結果傳遞給函式。注意這個函式並沒有考慮到執行環境,所以呼叫apply()時第一個引數是null。curry()函式可以按以下方式應用
function add(num1, num2){return num1 + num2; }var curriedAdd = curry(add, 5); alert(curriedAdd(3)); //8
在這個例子中,建立了第一個引數繫結為5的add()的柯里化版本。當呼叫cuurriedAdd()並傳入3時,3會成為add()的第二個引數,同時第一個引數依然是5,最後結果便是和8。也可以像下例這樣給出所有的函式引數:
function add(num1, num2){return num1 + num2; }var curriedAdd2 = curry(add, 5, 12); alert(curriedAdd2()); //17
在這裡,柯里化的add()函式兩個引數都提供了,所以以後就無需再傳遞給它們了
函式柯里化還常常作為函式繫結的一部分包含在其中,構造出更為複雜的bind()函式
function bind(fn, context){var args = Array.prototype.slice.call(arguments, 2);return function(){var innerArgs = Array.prototype.slice.call(arguments), finalArgs = args.concat(innerArgs);return fn.apply(context, finalArgs); }; }
對curry()函式的主要更改在於傳入的引數個數,以及它如何影響程式碼的結果。curry()僅僅接受一個要包裹的函式作為引數,而bind()同時接受函式和一個object物件。這表示給被繫結的函式的引數是從第三個開始而不是第二個,這就要更改slice()的第一處呼叫。另一處更改是在倒數第3行將object物件傳給apply()。當使用bind()時,它會返回繫結到給定環境的函式,並且可能它其中某些函式引數已經被設好。要想除了event物件再額外給事件處理程式傳遞引數時,這非常有用
var handler = { message: "Event handled", handleClick: function(name, event){ alert(this.message + ":" + name + ":" + event.type); } };var btn = document.getElementById("my-btn"); EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
handler.handleClick()方法接受了兩個引數:要處理的元素的名字和event物件。作為第三個引數傳遞給bind()函式的名字,又被傳遞給了handler.handleClick(),而handler.handleClick()也會同時接收到event物件
ECMAScript5的bind()方法也實現函式柯里化,只要在this的值之後再傳入另一個引數即可
var handler = { message: "Event handled", handleClick: function(name, event){ alert(this.message + ":" + name + ":" + event.type); } };var btn = document.getElementById("my-btn"); EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "my-btn"));
javaScript中的柯里化函式和繫結函式提供了強大的動態函式建立功能。使用bind()還是curry()要根據是否需要object物件響應來決定。它們都能用於建立複雜的演算法和功能,當然兩者都不應濫用,因為每個函式都會帶來額外的開銷
函式重寫
由於一個函式可以返回另一個函式,因此可以用新的函式來覆蓋舊的函式
function a(){ console.log('a'); a = function(){ console.log('b'); } }
這樣一來,當我們第一次呼叫該函式時會console.log('a')會被執行;全域性變數a被重定義,並被賦予新的函式
當該函式再次被呼叫時, console.log('b')會被執行
再複雜一點的情況如下所示
var a = (function(){ function someSetup(){var setup = 'done'; } function actualWork(){ console.log('work'); } someSetup();return actualWork; })()
我們使用了私有函式someSetup()和actualWork(),當函式a()第一次被呼叫時,它會呼叫someSetup(),並返回函式actualWork()的引用
這裡推薦一下我的學習交流群:731771211,裡面都是學習前端的,如果你想製作酷炫的網頁,想學習程式設計。自己整理了一份2018最全面前端學習資料,從最基礎的HTML+CSS+JS【炫酷特效,遊戲,外掛封裝,設計模式】到移動端HTML5的專案實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
點選: ofollow,noindex">加入