說下閉包的由來
function a() { var i = 0; function b() { console.log(i); } return b; } var c = a(); c();
一般來說,當一個函數內部匿名函數用到了自己的變量,並且這個匿名函數被返回了,這就建立了一個閉包,比如上面的代碼
這個時候,就算a調用結束被銷毀,i也會存在不會消失當a定義時,js解釋器會將函數a的作用域鏈設置為定義a時所在環境當執行a時,a會進入相應的執行環境,執行環境創建後才會有作用域scope屬性,然後創建一個活動對象,然後將其置為作用域鏈的頂端
現在a的作用域鏈就有a的活動對象以及window
然後為活動對象加入arguments屬性
這個時候a的返回函數b的引用給了c,b的作用域鏈包含a的活動對象引用,所以c可以訪問到a的活動對象,這個時候a返回後不會被GC
以上便是對閉包的簡單介紹,說多了就容易繞進去了,我們這裡簡單結束,然後進入實際的場景加以說明
實際場景
同事的疑惑
之前一個同事讓我去看一個代碼:
var User = function (opts) { var scope = this; for (var k in opts) { scope['get' + k] = function () { return opts[k]; }; scope['set' + k] = function (v) { return opts[k] = v; }; } }; var u = new User({ name: '測試', age: 11 });
代碼本意很簡單,希望對傳入的對象生成get/set方法,但是他這裡就遇到一個閉包問題:
導致這個問題的原因就是返回值內部使用的k永遠是“age”,這個k便是由於getXXX函數共享的活動對象,這裡修改也比較簡單
var User = function (opts) { var scope = this; for (var k in opts) { (function (k) { scope['get' + k] = function () { return opts[k]; }; scope['set' + k] = function (v) { return opts[k] = v; }; })(k); } }; var u = new User({ name: '測試', age: 11 });
在for循環內部創建一個立即執行函數,將k傳入,這個時候getXXX函數共享的就是各個匿名函數的“k”了
生成唯一ID
生成唯一ID也是閉包一個經典的使用方式
function getUUID() { var id = 0; return function () { return ++id; } } var uuid = getUUID();
這段代碼其實非常有意義,我們在浏覽器中不停的執行uuid()確實會得到不同的值,但是如果我們只使用getUUID()()的話每次值仍然一樣
導致這個問題的原因是,我們將getUUID執行後的結果賦予uuid,這個時候uuid就保存對其中匿名函數的引用,而匿名函數保存著getUUID的活動對象,所以id一直未銷毀
而直接調用的話,每次都會重新生成活動對象,所以id是不能保存的
一段有意思的代碼
Util.tryUrl = function (url) { var iframe = document.createElement('iframe'); iframe.height = 1; iframe.width = 1; iframe.frameBorder = 0; iframe.style.position = 'absolute'; iframe.style.left = '-9999px'; iframe.style.top = '-9999px'; document.body.appendChild(iframe); Util.tryUrl = function (url) { iframe.src = url; }; U.tryUrl(url); };
這段代碼十分有意思,當我們第一次調用時候會創建一個iframe對象,而第二次調用時候iframe對象就存在了,我們這裡將代碼做一定簡化後
var getUUID = function () { var i = 0; getUUID = function () { return i++; }; return getUUID(); };
這樣調整後,其實並不存在返回函數,但是我們其實依然形成了閉包
事件委托與閉包
我們都知道jquery的on是采用的事件委托,但是真正了解什麼事事件委托仍然要花一定功夫,於是我們這裡來試試
閉包是事件委托實現的基石,我們最後就以事件委托深入學習下閉包結束今天閉包的學習吧
加入我們頁面下有如下dom結構
<input id="input" value="input" type="button" /> <div id="div"> 我是div</div> <span id="span">我是span</span> <div id="wrapper"> <input id="inner" value="我是inner" type="button"/> </div>
我們使用zepto的話是使用如下方式綁定事件
$.on('click', 'selector', fn)
我們這裡沒有zepto就自己簡單實現吧
事件委托原理
首先事件委托實現的基石是事件冒泡,我們在頁面的每次點擊最終都會冒泡到其父元素,所以我們在document處可以捕捉到所有的事件
知道了這個問題後,我們可以自己實現一個簡單的delegate事件綁定方式:
function delegate(selector, type, fn) { document.addEventListener(type, fn, false); } delegate('#input', 'click', function () { console.log('ttt'); });
這段代碼是最簡單的實現,首先我們無論點擊頁面什麼地方都會執行click事件,當然這顯然不是我們想要看到的情況,於是我們做處理,讓每次點擊時候觸發他應有的事件
這裡有幾個問題比較尖銳:
① 既然我們事件是綁定到document上面,那麼我怎麼知道我現在是點擊的什麼元素呢
② 就算我能根據e.target獲取當前點擊元素,但是我怎麼知道是哪個元素具有事件呢
③ 就算我能根據selector確定當前點擊的哪個元素需要執行事件,但是我怎麼找得到是哪個事件呢
如果能解決以上問題的話,我們後面的流程就比較簡單了
確定點擊元素是否觸發事件
首先,我們點擊時候可以使用e.target獲取當前點擊元素,然後再根據selector依次尋找其父DOM,如果找得到就應該觸發事件
因為這些都是要在觸發時候才能決定,所以我們需要重寫其fn回調函數,於是簡單操作後:
var arr = []; var slice = arr.slice; var extend = function (src, obj) { var o = {}; for (var k in src) { o[k] = src[k]; } for (var k in obj) { o[k] = obj[k]; } return o; }; function delegate(selector, type, fn) { var callback = fn; var handler = function (e) { //選擇器找到的元素 var selectorEl = document.querySelector(selector); //當前點擊元素 var el = e.target; //確定選擇器找到的元素是否包含當前點擊元素,如果包含就應該觸發事件 /************* 注意,此處只是簡單實現,實際應用會有許多判斷 *************/ if (selectorEl.contains(el)) { var evt = extend(e, { currentTarget: selectorEl }); evt = [evt].concat(slice.call(arguments, 1)); callback.apply(selectorEl, evt); var s = ''; } var s = ''; }; document.addEventListener(type, handler, false); }
於是我們可以展開調用了:
delegate('#input', 'click', function () { console.log('input'); }); delegate('#div', 'click', function () { console.log('div'); }); delegate('#wrapper', 'click', function () { console.log('wrapper'); }); delegate('#span', 'click', function () { console.log('span'); }); delegate('#inner', 'click', function () { console.log('inner'); });
我們這裡來簡單解析下整個程序
① 我們調用delegate為body增加事件
② 在具體綁定時候,我們將其中的回調給重寫了
③ 在具體點擊時候(綁定幾次事件實際就會觸發幾次click),會獲取當前元素,查看其選擇器搜索的元素是否包含他,如果包含的話便觸發事件
④ 由於這裡每次注冊時候都會形成一個閉包,傳入的callback被維護起來了,所以每次調用便能找到自己的回調函數(這裡對閉包理解很有幫助)
⑤ 最後重寫event句柄的currentTarget,於是一次事件委托就結束了
PS:我這裡實現還有問題的,比如在event的處理上就有問題,但是作為demo的話我便不去關注了,有興趣的朋友自己去看zepto實現吧
事件委托的問題
事件委托可以提高效率但是有一個比較煩的事情就是阻止冒泡沒用
拿上面代碼來說,有一個inner元素和一個wrapper元素,他們是互相包裹關系
但是其執行順序並不是先內再外的事件冒泡順序,因為事件全部綁定到了document上面,所以這裡執行順序便是以其注冊順序所決定
這裡有一個問題便是如何“阻止冒泡”
在inner處完了執行
e.stopImmediatePropagation()
是可以達到目的的,但是仍然要求inner元素必須注冊到之前
除此之外,就只給這種會嵌套的元素綁定一個事件,又e.target決定到底執行哪個事件,具體各位自己斟酌
以上問題在使用backbone可能實際會遇到