1.什麼是閉包?
面試官問:瓦特is閉包?
A童鞋:I node,閉包is 幾拉呱啦,巴拉巴拉...
MDN:函式與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函式訪問外部函式作用域。在JavaScript,函式在每次建立時生成閉包。
直觀表現:
function father() {
var name = "Dede"; // name 是一個被 father 建立的區域性變數
function child() { // child() 是內部函式,一個閉包
console.log('My father name is ' + name); // 使用了父函式中宣告的變數
}
child();
}
father();
// 輸出
// My father name is Dede
複製程式碼
2.閉包的作用?
巴拉巴拉巴拉....相信大家都耳熟能祥
主要用處:
-
1.讓外部訪問函式內部變數成為可能;
-
2.區域性變數會常駐在記憶體中;(節流、防抖等高階函式實現)(優點亦是缺點)
-
3.可以避免使用全域性變數,防止全域性變數汙染;
3.閉包的優缺點?
巴拉巴拉巴拉.....相信你也可以說出來
- 會造成記憶體洩漏(有一塊記憶體空間被長期佔用,而不被釋放)
4.分析
很多人在面試的時候都會可以或多或少回答出這些基本概念,可是其中又有多少人只是在背答案而沒有真正理解清楚其概念?
更別提如何去靈活運用了。
- 許多高階函式中都涉及到了閉包的運用,例如:節流、防抖、promise等高階函式;
- 另外一些外掛,框架中也運用到了閉包。
5.結論
閉包可以說在js這門語言學習生涯中第一個分水嶺。----來自‘偉’大的小呆
真正掌握閉包的工程師,才算是開始有突破的趨勢了。
閉包中包涵的概念有多少,來捋一捋?
-
1.最基本函式的運用
-
2.變數的作用域
-
3.記憶體的分配,指標指向
-
4.垃圾回收機制
作用域就可以延伸到this的指向等等...
記憶體的分配完後,垃圾回收機制緊隨其後...
等等,你以為真的結束了嗎,其實才剛剛開始!
6.一道經典的面試題
6.1 第一題
你能正確搞清楚,每一步都發生了什麼,最後的結果是什麼嗎?
這題考察的就是在JS中記憶體的分配,和變數引用(指標)的問題。
我習慣用指標來描述這個引用的概念,可能不是很正確,但很好理解。
6.2 第二題
好的,這題有點難,我們先來一個小demo練手:
這題,稍微懂點JS引用型別的童鞋,應該都知道:
- 1.a實際上不是{name: lili}這個物件,只是該物件的地址引用。
- 2.b=a,也就是指向了{name: lili}這個物件。
- 3.a指向的物件的name屬性重新賦值為cat,現在該物件為{name: cat}
- 4.列印a.name,會先找到a指向的那個記憶體空間,然後把資料取出來,最後輸出cat。
- 5.列印b.name,因為b也指向了該物件,所以輸出的是cat。
你可以測試 a===b a是全等於b的,因為它們都是引用型別,只是一個指標,所引用的地址是一樣的。
這題沒有難度其實,稍微理解下引用型別的概念就可以掌握了。
6.3 解決第一題
回到這題
找這題就比較複雜了,我們要一步一步來解答:
- 1.首先js會在記憶體中分配一塊空間用於存放{n: 1}這個資料(這裡叫明明),然後把該空間的地址引用賦值給a。
- 2.變數b也指向這個物件{n: 1}明明。
- 3.a.x=a={n: 2},這一步操作好像爭議頗多,我也不是很清楚。
- 4.我認為有一種解答還是比較有說服力,就是說不管是a.x=a={n: 2},還是a=a.x={n: 2},賦值的順序是有優先順序的,就像運算操作符乘除優先順序大於加減這樣的概念。這裡,物件屬性訪問操作的優先順序要選大於訪問物件。
- 5.所有就是a.x會先做賦值運算,得到一個結果,a.x={n: 2},也就是a(或者b)指向的那個物件(明明)會多一個屬性x={n: 2}。
- 6.接著繼續賦值,變數a會重新指向一個新的地址,該地址空間存放著{n: 2}這麼一個物件(叫二狗子)。
- 7.到了這裡就清楚了,最後a是一個新的地址的引用,該地址存放的二狗子物件是{n: 2},那麼通過a.x訪問該物件的屬性x,但該物件只有一個屬性為n,那麼將列印出undefined。
a = {
n: 2
}
a.x === undefined
複製程式碼
- 8.而b指標指向的物件明明裡面的資料結構是:
b = {
n: 1,
x: {
n: 2
}
}
複製程式碼
- 9.並且可以嘗試b.x === a,將會輸出true。
我個人認為,這是最好理解的一種思路,同時我也在不斷學習中,如果有大佬指出概念不對的地方,我會虛心接受。
6.4 再來一題驗證思路
- 1.首先分配一塊空間給物件{name: bibi},然後將該物件的地址引用賦值給obj1。
- 2.通過getObj函式得到一個物件賦值給obj2。
- 3.傳入函式的引數是obj1,也就是傳入了一個指標,然後在函式內部,將該指標指向的物件的name屬性修改為jock。
- 4.重新分配一塊空間給物件{name: lili},將該物件的地址引用賦值給形參obj,最後return 該指標obj。
- 5.變數obj2將得到一個值為物件{name: lili}的地址引用,也就是新物件的指標。
最後列印obj1,將輸出的是name=jcok。列印obj2,將輸出的是name=lili。 並且使用 obj1 === obj2 將得到false的結果。
好了,這部分就是記憶體分配,指標地址引用的部分,不再深入。
6.4 垃圾回收機制
那麼閱讀到這裡,你應該對引用型別的概念更清楚一些了,腦海裡有了指標的概念後,就容易多了。
講到記憶體的分配,自然有記憶體的釋放。而垃圾回收機制就是記憶體釋放的一個過程。JS中是自動回收那些分配的空間,它有一些相應的策略。
最簡單的來說,就是分配了一個記憶體空間,該空間使用後若無地址引用,也就是沒有任何一個從根開始的指標指向該地址空間,那麼JS就會派部隊去清除該地址空間的資料,然後釋放這塊空間佔用的記憶體。
這裡指出,應該是沒有一個從根節點開始訪問的那個值,才會被刪除。有可能一個物件它有一個或者多個指標指向它,但是沒有從根開始的引用或者是指向,這個物件也將被回收。專業名詞叫做:可達性,也就是可以被訪問到的意思。
JS的垃圾回收機制一般可分為標記清除和引用計數這兩大塊,具體不細講。
6.5 總算明白了a = null,並不是將原先a指向的物件給刪除掉
很多時候,總認為我給a賦值一個null,那麼就意味著我把原先a指向的物件中的資料給刪除了,其實並沒有。你只是讓該物件失去了地址引用,並不是你在刪除它,是JS會去監視那些不可達的值,然後通過垃圾回收機制去刪除它的。
也就是說,你a=null後,之前的物件並沒有馬上被刪除,應該是J會在一個合適的時機去做這件事。而且,a=null並不意味著該物件就真的失去了地址引用,有可能你在別的地方還用其他變數保留了該物件的地址引用,那麼你想要的效果將會不奏效。
7.再講閉包,重新認識
說了那麼多好像跟閉包無關的概念,但是恰恰是這些概念,才能使你充分理解閉包的精髓,並且能夠去運用閉包解決一些實際問題。
7.1 變數常駐記憶體
根據MDN提供的術語,在js中,閉包生成的時機是函式建立的時候。但是我還是不太懂這什麼意思啊,所有我只能去看看別人的理解,然後找到一句我認為我可以理解的話:
閉包就是可以建立一個獨立的環境,每個閉包裡面的環境都是獨立的,互不干擾。閉包會發生記憶體洩漏,每次外部函式執行的時候,外部函式的引用地址不同,都會重新建立一個新的地址。但凡是當前活動物件中有被內部子集引用的資料,那麼這個時候,這個資料不刪除,保留一根指標給內部活動物件。
我彭大師從不抄襲,也不會打妄語。
這句話告訴我們,在內部函式引用了外部函式的資料時,這個資料不會被刪除,會保留一個指標引用給內部物件,從而這個資料的記憶體空間就不會被回收機制幹掉,也就有了閉包會讓變數常駐記憶體這個特點。
7.2 高階函式:節流、防抖的實現。
從而我們來分析一個我們經常被問到的兩個函式:節流於防抖。
這兩個函式是會一起被提及的,它們的概念呢也經常會被混淆,有時候傻傻分不清節流和防抖的區別,以及它們使用的場景。
- 場景1:只是在腦子裡有一個概念,我要優化一些操作的時候,就會想起節流和防抖,但是具體用哪一個,我並不是很清楚,需要再百度一下,然後再選擇一個用。
- 場景2:面試的時候,面試官總愛讓你手寫節流、防抖這兩個高階函式。你在面試前總是背的半死,到了筆試的時候,總是會想不起來怎麼寫,或者總是會寫錯一些步驟。
所以,本節我們就一起徹底的弄清楚節流和防抖的概念,以及在真實程式碼中的運用和如何自己手寫一個這樣的函式。
我們不從什麼是節流和防抖這兩個概念入手,這樣還是生搬概念,無法讓人簡單去理解。
我們從實際的運用中去把這兩個概念提取出來,這樣就成為你自己的東西了。
7.3 搜尋框優化
第一輪:
某一天,你開發了一個搜尋框的功能,這個功能用於關鍵詞檢索。
你仔細檢查,自測無誤後,提交程式碼,滿心歡喜讓測試開始測試,自認為天衣無縫,沒有任何bug。
不久測試小姐姐說,你這個頁面有bug啊。
???
你心想,怎麼可能,我明明自己測了很多次,根本沒有任何問題,你說說看,哪裡會有問題??
測試小姐姐:你這個輸入框,我才輸入了一個字,我還沒輸入完,這頁面怎麼就開始有搜尋結果了呢??
First Blood!!
嗯??一定是小姐姐打字太慢了,我去看看怎麼回事。
你一看程式碼,原來你用的是input,change事件,當input繫結的value的值發生變化的時候,就會觸發此事件,然後去呼叫搜尋介面。
但是好像,不需要一有值就馬上去搜尋,這樣第一個問題就是,使用者沒有輸入完,就去查詢結果。第二個問題,就是一直頻繁的請求介面,造成了沒必要的請求,造成了效能的損耗。
然後,作為一個合格的程式設計師,你會去思考,該怎麼去優化這個搜尋功能。按照常規的思路走,要不我在輸入框旁邊加個按鈕,點選才搜尋,這樣就不會有沒輸入完就開始搜尋的問題了,也不會一直頻繁的請求介面了。
第二輪:
於是你,開始了新的一輪修水管的任務了。噼裡啪啦,半小時後,你又滿心歡喜的提交了程式碼,然後到了小姐姐手上。
沒過多久,小姐姐就跟你說,又有問題,你這個互動邏輯很繁瑣啊,使用者輸入完,還要去點選那個醜醜的按鈕,很不便捷。其二,我壓力測試,我可以一直去點那個按鈕,這樣一直在傳送http請求,造成了網路擁堵,要是有人故意這樣搞破破壞,一直請求後臺,會對伺服器的效能和頻寬帶來嚴重的影響。
此刻,你的內心是崩潰的,我明明優化了,怎麼好像問題更多了。
Double kill!!!
經過一段自我懷疑自我調整的過程,你又有了新的優化方案,我去限制你使用者請求介面的頻率不就好了嗎,我讓你延遲一會,才會去觸發呼叫介面,豈不妙哉!
第三輪:
你開始在呼叫介面的地方,寫了一個setTimeout函式,延時1秒去呼叫介面。你測了幾遍感覺沒什麼問題,小心翼翼的檢查幾次後,提交程式碼到測試小姐姐手上。
經過小姐姐一番揉虐,小姐姐說:你這個還是不行啊,雖然第一次是延遲了1秒請求,但是我點了100次,但是過1秒後,馬上就有100個請求出現了。
Triple kill!!!
第四輪:
你思考的方向是對的,但是還是沒有起到作用。首先確定,使用延時這個方法是可行的,但是怎麼限制在一段時間內,只能有一次的請求。比如設定一個時間為2秒鐘,在這個期間,可能會觸發10次的chang事件,那麼怎麼把10次事件只觸發一次呢?
思考點:10次請求如何只觸發一次?
思路,從定時器本身下手。什麼是定時器?
setTimeout(() => {
console.log('螢幕前的你,若是男生,則帥;女生,則膚白貌美!')
}, 1000)
複製程式碼
上述程式碼執行後,過1秒鐘左右是不是會輸出:
螢幕前的你,若是男生,則帥;女生,則膚白貌美!
這句話。
如果我們將其用一個函式包裹起來:
function sayToYou() {
setTimeout(() => {
console.log('螢幕前的你,若是男生,則帥;女生,則膚白貌美!')
}, 1000)
}
sayToYou()
複製程式碼
是不是結果跟上面的一樣。
那如果我點選了十次按鈕,就會觸發十次sayToYou函式,只不過是延時1秒後,馬上發輸出10句。
那麼我想要的是連續快速的點選十次後,只有一次的sayToYou函式中的定時器的回撥能夠被觸發,那該怎麼辦呢??
是不是可以用一個變數將定時器先儲存起來,如果在一段時間內(第一個定時器定時的時間範圍內)又有新的函式被觸發了,那麼先判斷這個變數是不是存在了,如果存在上一個(第一個)定時器變數,那麼我就將上一個(第一個)定時器清除掉,不讓它觸發回撥任務。這樣之前的點選事件是不是就無效了。只有當前的點選事件繼續進入定時器內,等待這段時間過去,才會觸發回撥事件。
那麼我們就這樣做吧,改造一下我們的sayToYou函式:
let timer = null
function sayToYou() {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
console.log('螢幕前的你,若是男生,則帥;女生,則膚白貌美!')
clearTimeout(timer)
}, 1000)
}
// 用迴圈 模擬高頻點選按鈕100次
for(let i = 0; i < 100; i++) {
sayToYou()
}
// 過一秒鐘左右,只輸出一次。
複製程式碼
執行次程式碼,你即將熱淚盈眶,因為和你設想的結果一毛一樣。
首先我們遮蔽清除定時器的那行,將得到100句的輸出:
接著不遮蔽那句清除判斷:
是不是和我們預期一樣 只會輸出一句!!!
那麼我們接著優化一下我們的程式碼
現在雖然是可以在高頻觸發的情況下,只會觸發一次回撥,但是我們還要額外去宣告一個變數,那如果我有很多地方都需要呼叫這個函式怎麼辦??
難道要去多個地方都定義一個變數這樣,豈不是很麻煩??
那麼,說了這麼多,引入了這麼多js概念,終於到了重頭戲了,我們在前面討論閉包的時候,是不是有說到閉包的優點,我們們是不是可以利用起來。
現在想想閉包有什麼優點(特點)?
回答:使變數常駐記憶體中
ok,那麼我們將閉包的特性引入進來,改造一下:
- 1.首先,我們們要建立一個閉包
- 2.內部函式使用外部函式中的變數
- 3.返回這個函式
改造後的程式碼:
function debounce() {
let timer = null
function sayToYou() {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
console.log('螢幕前的你,若是男生,則帥;女生,則膚白貌美!')
clearTimeout(timer)
}, 1000)
}
return sayToYou
}
// 使用一個變數得到防抖的返回函式
const sayToYou = debounce()
// 用迴圈 模擬高頻點選按鈕100次
for(let i = 0; i < 100; i++) {
// 執行此函式
sayToYou()
}
複製程式碼
分析:仔細看,我們的sayToYou函式是不是裡面邏輯一行程式碼都沒有改動,我們改動的是什麼?
- 1.另起一個叫做debounce的函式,裡面定義了一個timer變數。
- 2.一個內部函式sayToYou,也就是我們之前那個sayToYou函式。(無更改)
- 3.最後return此函式。
- 4.引用的方式變化,外部呼叫的sayToYou函式是通過呼叫debounce函式獲得的返回值。
上面debounce我們們給它稱為父神,那麼裡面的sayToYou我們們給它叫做造娃,由於父神的記憶不是很好(老年痴呆?),父神需要在手臂上(timer)劃一道痕來標記那個娃。造娃的過程很簡單,父神抓一把泥巴開始捏娃,然後在手臂上劃一下。(造娃需要1秒鐘),造完後,父神手一揮,手臂上的痕跡就不見了。
//當父神執行的時候,也就是父神開始造娃的時候了,失手將催娃口訣遺失在人間
const sayToYou 催娃口訣 = debounce()
// 這個時候有一些愚蠢的凡人,一直在向上天祈禱(催娃)
// 要讓催娃口訣生效,必須加兩個神祕的符號()
for(let i = 0; i < 100; i++) {
//催娃
催娃口訣()
sayToYou()
}
// 雖然這個時候被催娃了100遍,但是父神一看手上的痕跡還在啊,你催個蛋蛋娃,給本神等著。
// 最後,娃造完後,父神大手一揮,手臂的痕跡不見了,娃就落地了,呱呱大叫:你個大丑逼!
複製程式碼
執行結果
這就是結合閉包的用法,優化了我們的函式。
7.4 徹底要理解閉包
其實大家可能還是不能體會到下面這句話的意思。
“每次外部函式執行的時候,外部函式的引用地址不同,都會重新建立一個新的地址。但凡是當前活動物件中有被內部子集引用的資料,那麼這個時候,這個資料不刪除,保留一根指標給內部活動物件。”
這句話太長,我們要分成兩句話:
- 1.父函式每執行一次,都會建立一個新的引用地址。
- 2.會保留一根資料指標給內部函式使用。
結合到一起,意思就是,同一個引用地址的內部函式所引用的那個變數是同一根指標。
為了更直觀顯示出區別,我們要改下程式碼:
執行過程:a的到了父函式執行的一個地址引用,同時,獲得了本次父函式執行後的i的指標。
執行3次a函式:
-
- i = 0 => i++ => 1
-
- i = 1 => i++ => 2
-
- i = 2 => i++ => 3
那麼接著來一個例子:
我們多寫了一個b函式,然後執行的地方改成執行兩次a,一次b。
得到的結果是 1 2 1
這說明了什麼?
說明了每次外部函式執行的時候,外部函式的引用地址不同,都會重新建立一個新的地址。
那既然a和b引用的地址不同,那麼它們就會各自擁有一個閉包變數i(是不同的地址)
所以當執行完兩次a函式後,a所引用的地址中的內部變數i為2,接著執行b函式,不要天真的認為此時i是2,其實這個時候b所引用的地址中的內部變數i是0,所以執行完b函式將要輸出的是1。
思考1 console.log(a === b)
那麼,我們將上面程式碼小小的改變一下:
這個時候,控制檯要輸出什麼?你知道答案嗎?
思考2 console.log(a === b)
7.5 那你的防抖函式好像和市面上的不同啊!?
那麼這一小節,我們將繼續優化我們們的防抖函式,努力讓它稱為你想要的樣子!
市面上的防抖函式好像是這樣用的嘛:
const sayHello() {
console.log('hello')
}
const a = debounce(sayHello, 1000)
// 輸出 hello
a()
複製程式碼
這有何難?我們們把閉包的本質都理解的差不多了,傳兩個引數進去而已,不在話下!
來,讓我們看下,第一個引數為一個函式的地址,第二個引數為延時的時間。
// 火影1代
function debounce(fn, delay) {
let timer = 0
return function () {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(fn, delay)
}
}
const sayHello = () => console.log('hello, every body!' )
const a = b = debounce(sayHello, 1000)
a()
a()
b()
複製程式碼
我們通過設定兩個引數,就貌似達到了市面版的效果,執行後也只有一句歡迎資訊。
但是,如果我的sayHello函式並不是直接輸出一句話,而是在呼叫的時候傳一個name進去,這個時候怎麼辦呢?
嗯?好像可以繼續優化:
// 火影2代
function debounce(fn, delay) {
let timer = 0
return function () {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...arguments)
clearTimeout(timer)
}, delay)
}
}
const sayHello = name => console.log(`hello, ${name}!`)
const a = b = debounce(sayHello, 1000)
a('jcok')
a('lili')
b('cat')
複製程式碼
改動點:
- 1.這裡我們將sayHello改造了一番,讓它可以支援傳入一個name,並且輸出歡迎資訊。
- 2、在防抖函式內部,fn執行的地方,通過解構arguments這個全域性變數拿到sayHello傳入的變數。(可能有萌新對這個arguments很陌生,還有對...arguments這樣的語法感到疑惑,這裡推薦你去練個手,多列印幾次arguments就大概明白了。至於解構也就是三個點號...這部分內容屬於ES6,自行檢視)
最後我們高頻抖動了三次,分別傳了jock,lili,cat三個名字進去,最後只會列印出在1000毫秒內最後被觸發的那一個,也就是列印hello, cat!
讓我們執行一下:
事實上,結果跟我們想要的完全一致。
做到這裡,你以為就完了嗎??no,實際上跟市面上的版本還少了一個this的指向。
我們這裡的sayHello函式預設是掛載在window物件身上的,所以沒有在fn中修正指向,也是可以正常執行的。
但是,防抖函式是用於很多地方,如果你沒有正確的修正this的指向,那麼可能會有bug產生。
來個實際的例子:
function debounce(fn, delay) {
let timer = 0
return function () {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...arguments)
clearTimeout(timer)
}, delay)
}
}
const obj = {
name: 'god',
sayHello: function () {
console.log(`hello, ${this.name}!`)
}
}
const a = debounce(obj.sayHello, 1000)
a() // hello, undefined!
複製程式碼
修改點:
- 1.將sayHello函式寫入到一個物件obj身上。(這裡注意,不能再用箭頭函式了)
你會發現,執行程式後,輸出的竟然是hello, undefined!(這裡測試方便是在NodeJs 環境中執行的,可實際上NodeJS中沒有document和window。)
而你不相信,單獨執行一次obj.sayHello(),得到的輸出是hello, god!
這裡就是因為在debounce中呼叫fn的時候,this的指向預設是window,而window物件沒有name的屬性,所以,將列印的是hello, undefined!
如果你不相信,那麼我們將在window身上掛載一個name屬性叫做john,然後執行程式試試。
保持debounce和obj的程式碼不變:
1.先執行一次debounce(obj.sayHello, 1000)()
得到的結果居然是hello, ! 我愣了兩秒,反應過來,可能window物件上已經有了一個name屬性為'', 所以,我把window列印了一下:
果然,和我推測的一樣,那就沒毛病了,如果我們在sayhHello中列印的不是name,而是一個window身上沒有的屬性,將會輸出的應該是我們之前推測的hello, undefined!
2.接著修改window.name屬性為'john'
繼續呼叫一下debounce(obj.sayHello, 1000)(),得到的結果是和我們預測的一致:hello, john!
所以,得出的結論與我們推測的一致,最後我們還需要去修改this的指向。
7.6 較為完整一個防抖函式誕生
function debounce(fn, delay, _this) {
let timer = 0
return function () {
if(timer) {
clearTimeout(timer)
}
// 這一句很關鍵
const that = _this ? _this : this
timer = setTimeout(() => {
fn.apply(that, arguments)
clearTimeout(timer)
}, delay)
}
}
const obj = {
name: 'god',
sayHello: function () {
console.log(`hello, ${this.name}!`)
}
}
const a = debounce(obj.sayHello, 1000, obj)
a() // hello, god!
複製程式碼
這裡注意我們必須要主動傳入這個obj這個作為當前上下文給fn去apply這個this,不然即使我們使用fn.apply(this, arguments)這樣去修正this指向,但是得到的結果仍然和你想要的不一致。
const that = _this ? _this : this
複製程式碼
如果你不顯式地傳入一個上下文,那麼預設使用當前上下文。
換句話說就是debounce的return最終執行的環境的當前上下文會作為預設上下文傳入該that中,然後去修正fn的this指向。(沒有主動傳入一個上下文的情況下)
這裡很多小夥伴會疑惑了,大家不妨可以試一下。
7.7 一個更為完整的測試案例
function debounce(fn, delay, _this) {
let timer = 0
return function () {
if(timer) {
clearTimeout(timer)
}
const that = _this ? _this : this
timer = setTimeout(() => {
fn.apply(that, arguments)
clearTimeout(timer)
}, delay)
}
}
const obj1 = {
name: 'obj1',
sayHello: function () {
console.log(`hello, ${this.name}!`)
}
}
const obj2 = {
name: 'obj2',
a: debounce(obj1.sayHello, 1000)
}
const obj3 = {
name: 'obj3',
a: debounce(obj1.sayHello, 1000, obj1)
}
const a = debounce(obj1.sayHello, 1000)
const b = debounce(obj1.sayHello, 1000, obj1)
obj1.sayHello() // 沒有使用防抖優化
obj2.a() // 使用了防抖優化,預設上下文為obj2
obj3.a() // 顯式的改變上下文為obj1
a() // 使用了防抖優化 預設上下文為window(在瀏覽器環境中)
b() // 顯式的改變上下文為obj1
複製程式碼
大家可以測試下上面的程式碼,看看輸出情況和我上面分析的對不對。
8總結
一句話概括:JS博大精深,豈是我等宵小之輩可以覬覦!?
後話
我是小呆,歡迎你與我討論技術。