JS工作中的小貼士之”閉包“與事件委托的”阻止冒泡“
說下閉包的由來
function a() { var i = 0; function b() { console.log(i); } return b; } var c = a(); c();
一般來說,當(dāng)一個(gè)函數(shù)內(nèi)部匿名函數(shù)用到了自己的變量,并且這個(gè)匿名函數(shù)被返回了,這就建立了一個(gè)閉包,比如上面的代碼
這個(gè)時(shí)候,就算a調(diào)用結(jié)束被銷毀,i也會(huì)存在不會(huì)消失當(dāng)a定義時(shí),js解釋器會(huì)將函數(shù)a的作用域鏈設(shè)置為定義a時(shí)所在環(huán)境當(dāng)執(zhí)行a時(shí),a會(huì)進(jìn)入相應(yīng)的執(zhí)行環(huán)境,執(zhí)行環(huán)境創(chuàng)建后才會(huì)有作用域scope屬性,然后創(chuàng)建一個(gè)活動(dòng)對(duì)象,然后將其置為作用域鏈的頂端
現(xiàn)在a的作用域鏈就有a的活動(dòng)對(duì)象以及window
然后為活動(dòng)對(duì)象加入arguments屬性
這個(gè)時(shí)候a的返回函數(shù)b的引用給了c,b的作用域鏈包含a的活動(dòng)對(duì)象引用,所以c可以訪問到a的活動(dòng)對(duì)象,這個(gè)時(shí)候a返回后不會(huì)被GC
以上便是對(duì)閉包的簡單介紹,說多了就容易繞進(jìn)去了,我們這里簡單結(jié)束,然后進(jìn)入實(shí)際的場(chǎng)景加以說明
實(shí)際場(chǎng)景
同事的疑惑
之前一個(gè)同事讓我去看一個(gè)代碼:
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: '測(cè)試', age: 11 });
代碼本意很簡單,希望對(duì)傳入的對(duì)象生成get/set方法,但是他這里就遇到一個(gè)閉包問題:
導(dǎo)致這個(gè)問題的原因就是返回值內(nèi)部使用的k永遠(yuǎn)是“age”,這個(gè)k便是由于getXXX函數(shù)共享的活動(dòng)對(duì)象,這里修改也比較簡單
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: '測(cè)試', age: 11 });
在for循環(huán)內(nèi)部創(chuàng)建一個(gè)立即執(zhí)行函數(shù),將k傳入,這個(gè)時(shí)候getXXX函數(shù)共享的就是各個(gè)匿名函數(shù)的“k”了
生成唯一ID
生成唯一ID也是閉包一個(gè)經(jīng)典的使用方式
function getUUID() { var id = 0; return function () { return ++id; } } var uuid = getUUID();
這段代碼其實(shí)非常有意義,我們?cè)跒g覽器中不停的執(zhí)行uuid()確實(shí)會(huì)得到不同的值,但是如果我們只使用getUUID()()的話每次值仍然一樣
導(dǎo)致這個(gè)問題的原因是,我們將getUUID執(zhí)行后的結(jié)果賦予uuid,這個(gè)時(shí)候uuid就保存對(duì)其中匿名函數(shù)的引用,而匿名函數(shù)保存著getUUID的活動(dòng)對(duì)象,所以id一直未銷毀
而直接調(diào)用的話,每次都會(huì)重新生成活動(dòng)對(duì)象,所以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); };
這段代碼十分有意思,當(dāng)我們第一次調(diào)用時(shí)候會(huì)創(chuàng)建一個(gè)iframe對(duì)象,而第二次調(diào)用時(shí)候iframe對(duì)象就存在了,我們這里將代碼做一定簡化后
var getUUID = function () { var i = 0; getUUID = function () { return i++; }; return getUUID(); };
這樣調(diào)整后,其實(shí)并不存在返回函數(shù),但是我們其實(shí)依然形成了閉包
事件委托與閉包
我們都知道jquery的on是采用的事件委托,但是真正了解什么事事件委托仍然要花一定功夫,于是我們這里來試試
閉包是事件委托實(shí)現(xiàn)的基石,我們最后就以事件委托深入學(xué)習(xí)下閉包結(jié)束今天閉包的學(xué)習(xí)吧
加入我們頁面下有如下dom結(jié)構(gòu)
<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就自己簡單實(shí)現(xiàn)吧
事件委托原理
首先事件委托實(shí)現(xiàn)的基石是事件冒泡,我們?cè)陧撁娴拿看吸c(diǎn)擊最終都會(huì)冒泡到其父元素,所以我們?cè)赿ocument處可以捕捉到所有的事件
知道了這個(gè)問題后,我們可以自己實(shí)現(xiàn)一個(gè)簡單的delegate事件綁定方式:
function delegate(selector, type, fn) { document.addEventListener(type, fn, false); } delegate('#input', 'click', function () { console.log('ttt'); });
這段代碼是最簡單的實(shí)現(xiàn),首先我們無論點(diǎn)擊頁面什么地方都會(huì)執(zhí)行click事件,當(dāng)然這顯然不是我們想要看到的情況,于是我們做處理,讓每次點(diǎn)擊時(shí)候觸發(fā)他應(yīng)有的事件
這里有幾個(gè)問題比較尖銳:
① 既然我們事件是綁定到document上面,那么我怎么知道我現(xiàn)在是點(diǎn)擊的什么元素呢
② 就算我能根據(jù)e.target獲取當(dāng)前點(diǎn)擊元素,但是我怎么知道是哪個(gè)元素具有事件呢
③ 就算我能根據(jù)selector確定當(dāng)前點(diǎn)擊的哪個(gè)元素需要執(zhí)行事件,但是我怎么找得到是哪個(gè)事件呢
如果能解決以上問題的話,我們后面的流程就比較簡單了
確定點(diǎn)擊元素是否觸發(fā)事件
首先,我們點(diǎn)擊時(shí)候可以使用e.target獲取當(dāng)前點(diǎn)擊元素,然后再根據(jù)selector依次尋找其父DOM,如果找得到就應(yīng)該觸發(fā)事件
因?yàn)檫@些都是要在觸發(fā)時(shí)候才能決定,所以我們需要重寫其fn回調(diào)函數(shù),于是簡單操作后:
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); //當(dāng)前點(diǎn)擊元素 var el = e.target; //確定選擇器找到的元素是否包含當(dāng)前點(diǎn)擊元素,如果包含就應(yīng)該觸發(fā)事件 /************* 注意,此處只是簡單實(shí)現(xiàn),實(shí)際應(yīng)用會(huì)有許多判斷 *************/ 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); }
于是我們可以展開調(diào)用了:
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'); });
我們這里來簡單解析下整個(gè)程序
① 我們調(diào)用delegate為body增加事件
② 在具體綁定時(shí)候,我們將其中的回調(diào)給重寫了
③ 在具體點(diǎn)擊時(shí)候(綁定幾次事件實(shí)際就會(huì)觸發(fā)幾次click),會(huì)獲取當(dāng)前元素,查看其選擇器搜索的元素是否包含他,如果包含的話便觸發(fā)事件
④ 由于這里每次注冊(cè)時(shí)候都會(huì)形成一個(gè)閉包,傳入的callback被維護(hù)起來了,所以每次調(diào)用便能找到自己的回調(diào)函數(shù)(這里對(duì)閉包理解很有幫助)
⑤ 最后重寫event句柄的currentTarget,于是一次事件委托就結(jié)束了
PS:我這里實(shí)現(xiàn)還有問題的,比如在event的處理上就有問題,但是作為demo的話我便不去關(guān)注了,有興趣的朋友自己去看zepto實(shí)現(xiàn)吧
事件委托的問題
事件委托可以提高效率但是有一個(gè)比較煩的事情就是阻止冒泡沒用
拿上面代碼來說,有一個(gè)inner元素和一個(gè)wrapper元素,他們是互相包裹關(guān)系
但是其執(zhí)行順序并不是先內(nèi)再外的事件冒泡順序,因?yàn)槭录拷壎ǖ搅薲ocument上面,所以這里執(zhí)行順序便是以其注冊(cè)順序所決定
這里有一個(gè)問題便是如何“阻止冒泡”
在inner處完了執(zhí)行
e.stopImmediatePropagation()
是可以達(dá)到目的的,但是仍然要求inner元素必須注冊(cè)到之前
除此之外,就只給這種會(huì)嵌套的元素綁定一個(gè)事件,又e.target決定到底執(zhí)行哪個(gè)事件,具體各位自己斟酌
以上問題在使用backbone可能實(shí)際會(huì)遇到
相關(guān)文章
Navigator?sendBeacon頁面關(guān)閉也能發(fā)送請(qǐng)求方法示例
這篇文章主要為大家介紹了Navigator?sendBeacon頁面關(guān)閉也能發(fā)送請(qǐng)求的方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06前端頁面適配之postcss-px-to-viewport實(shí)現(xiàn)步驟
postcss-px-to-viewport是一個(gè)PostCSS插件,它可以將px單位轉(zhuǎn)換為視口單位(vw、vh?或?vmin),這篇文章主要給大家介紹了關(guān)于前端頁面適配之postcss-px-to-viewport的實(shí)現(xiàn)步驟,需要的朋友可以參考下2024-03-03javascript模擬php函數(shù)in_array
就是判斷一個(gè)元素是否存在于數(shù)組中的函數(shù),既然js里string都有indexOf函數(shù),為什么不在Array對(duì)象里設(shè)置一個(gè)這樣的函數(shù)呢,其實(shí)就用indexOf這個(gè)思想挺好的,不知道制定JS標(biāo)準(zhǔn)的人是基于什么考慮,把這樣一個(gè)如此常用的功能沒考慮在內(nèi)的。2015-04-04微信小程序錄音實(shí)現(xiàn)功能并上傳(使用node解析接收)
在我們的日常開發(fā)中經(jīng)常會(huì)遇到錄音功能,并上傳到服務(wù)器,今天小編給大家分享微信小程序錄音功能實(shí)現(xiàn)并上傳錄音文件,使用node解析接收,需要的朋友可以參考下2020-02-02hammer.js實(shí)現(xiàn)圖片手勢(shì)放大效果
這篇文章主要為大家詳細(xì)介紹了hammer.js實(shí)現(xiàn)圖片手勢(shì)放大效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08JavaScript算法題之如何將一個(gè)數(shù)組旋轉(zhuǎn)k步
這篇文章主要給大家介紹了關(guān)于JavaScript算法題之如何將一個(gè)數(shù)組旋轉(zhuǎn)k步的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03'webpack-dev-server'?不是內(nèi)部或外部命令也不是可運(yùn)行的程序?或批處理文件的最
這篇文章主要介紹了'webpack-dev-server'?不是內(nèi)部或外部命令也不是可運(yùn)行的程序?或批處理文件的最新解決方法,文中給大家補(bǔ)充介紹了webpack-dev-server的介紹與用法,需要的朋友可以參考下2023-02-02JS代碼屏蔽F12,右鍵,粘貼,復(fù)制,剪切,選中,操作實(shí)例
在本篇文章里小編給大家分享的是關(guān)于利用JS代碼屏蔽F12,右鍵,粘貼,復(fù)制,剪切,選中,操作,需要的朋友們學(xué)習(xí)下。2019-09-09js string 轉(zhuǎn) int 注意的問題小結(jié)
Javascript將string類型轉(zhuǎn)換int類型的過程中總會(huì)出現(xiàn)不如意的問題,下面為大家介紹下js string轉(zhuǎn)int的一些注意的問題,感興趣的朋友可以參考下2013-08-08