foreach的this為什麼不能繼承作用域_淺談JavaScript閉包和作用域問題
當你宣告一個變數的時候,一般是這樣的:
var a = 'a string';var b = new String('a string');複製程式碼
但這個時候你用typeof函式檢測這兩個變數的型別,就會發現以下結果:
console.log(typeof a);//stringconsole.log(typeof b);//object複製程式碼
這是為什麼呢?
這就要說到javaScript的變數儲存,變數儲存有兩種方式:
其一:簡單的值型別(undefined、number、string、boolean)儲存在棧裡。
其二:引用型別(函式、陣列、物件、null)儲存在堆裡,棧裡儲存他們的記憶體地址(如下圖)。
String,Number,Boolean等類都派生自Object物件,因此通過 new 關鍵字構造的她們都屬於物件,而不是簡單的值型別。
例子裡的變數b,通過String建構函式宣告,則b的__proto__指向String函式的prototype物件,因而b也繼承有String函式的prototype的所有屬性。
而變數a的宣告方法是直接通過等號賦值,則變成了一個簡單的值型別,儲存在棧中。
作用域和上下文環境
乍一聽這個名詞我們可能有點不太能理解,我們先這麼淺薄地理解:
作用域是函式的一塊“領地”,上下文環境儲存作用域內的引數名和值,例如:
var a = 1, foo = function(b){ console.log(a+b); };foo(2);複製程式碼
1、 因為我們的程式碼在全域性環境內執行,在執行程式碼之前(即預編譯),將先建立全域性上下文環境,再把全域性上下文環境壓入 上下文棧 :
全域性上下文環境aundefinedfooundefinedthiswindow
2、然後我們執行程式碼(到呼叫foo函式之前),然後為變數賦值:
全域性上下文環境a1foofunctionthiswindow
3、 然後我們呼叫foo函式,我們的上下文環境就要轉到foo函式內部,並把foo函式執行上下文環境壓入 上下文棧 ,:
foo函式執行上下文環境b2arguments[2]thiswindow
這時候 上下文棧 內有兩個上下文環境:全域性上下文 和 foo函式執行上下文。
但是在執行的時候,我們發現foo函式的作用域裡沒有變數a,我們要到哪裡取呢?
答案是建立這個函式的作用域裡取,foo函式是全域性建立的,因此我們就回到全域性作用域裡找變數a。
然後看到全域性上下文環境中,有變數a,它的值是1。
a + b == 1 + 2 == 3
因此,最後控制檯輸出:3
4、到此為止,foo函式執行完畢,foo函式執行上下文環境出棧銷燬,最後留下全域性上下文環境:
把整個過程連在一起,就是下面這張圖:
作用域和上下文的區別
作用域只是一個“地盤”,一個抽象的概念,其中沒有變數。
要通過作用域對應的執行上下文環境來獲取變數的值。
同一個作用域下,不同的呼叫會產生不同的執行上下文環境,繼而產生不同的變數的值。
所以,作用域中變數的值是在執行過程中產生的確定的,而作用域卻是在函式建立時就確定了。
閉包
概念
瞭解到我們的先導知識後,我們最後來看閉包,閉包有兩種情況:
- 函式作為返回值function fn(){ return function foo(){ console.log('閉包'); } } //呼叫fn立即執行foo 複製程式碼
- 函式作為引數被傳遞/* * @params n,m number型 * @return 返回兩個數相加額結果 */ function add(n,m){ return n+m; } //閉包 (function fn(f){ var n = 1,m = 2; f(n,m); // 呼叫add函式 })(add); // add函式作為引數f傳入 複製程式碼
閉包的重點其實就在於,函式執行完畢之後,上下文環境不會被銷燬,例如:
我們會發現,在給變數f賦值的時候函式fn()就執行完了,按理說,上下文環境應該銷燬,我們應該訪問不到a。
但其實fn的上下文環境並沒有出棧,fn函式的上下文環境依舊可以訪問到。
這也是為什麼說閉包會導致記憶體洩漏和增加記憶體開銷。
應用場景
那在什麼場景下可以用到閉包呢
1、模擬私有變數
var Counter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var counter1 = Counter();// 上下文環境一 var counter2 = Counter();// 上下文環境二,與環境一不共享變數複製程式碼
每個閉包有自己的上下文環境,上文的例子中,建立了兩個私有成員,變數privateCounter和函式changeBy。
這兩個變數都必須要用Counter.increment,Counter.decrement 和 Counter.value這三個方法中的一個呼叫,這就實現了私有成員。
2、函式防抖
/** * @function debounce 函式防抖 * @param {Function} fn 需要防抖的函式 * @param {Number} interval 間隔時間 * @return {Function} 經過防抖處理的函式 * */ function debounce(fn, interval) { let timer = null; // 定時器 return function() { // 清除上一次的定時器 clearTimeout(timer); // 拿到當前的函式作用域 let _this = this; // 拿到當前函式的引數陣列 let args = Array.prototype.slice.call(arguments, 0); // 開啟倒計時定時器 timer = setTimeout(function() { // 通過apply傳遞當前函式this,以及引數 fn.apply(_this, args); // 預設300ms執行 }, interval || 300) } }複製程式碼
3、函式節流
/** * @function throttle 函式節流 * @param {Function} fn 需要節流的函式 * @param {Number} interval 間隔時間 * @return {Function} 經過節流處理的函式 * */function throttle(fn, interval) { let timer = null; // 定時器 let firstTime = true; // 判斷是否是第一次執行 // 利用閉包 return function() { // 拿到函式的引數陣列 let args = Array.prototype.slice.call(arguments, 0); // 拿到當前的函式作用域 let _this = this; // 如果是第一次執行的話,需要立即執行該函式 if(firstTime) { // 通過apply,綁定當前函式的作用域以及傳遞引數 fn.apply(_this, args); // 修改標識為null,釋放記憶體 firstTime = null; } // 如果當前有正在等待執行的函式則直接返回 if(timer) return; // 開啟一個倒計時定時器 timer = setTimeout(function() { // 通過apply,綁定當前函式的作用域以及傳遞引數 fn.apply(_this, args); // 清除之前的定時器 timer = null; // 預設300ms執行一次 }, interval || 300) }}複製程式碼
4、setTimeout場景
for(var i = 0; i < 5 ; i++ ){ setTimeout(function(){console.log(i)},100);}複製程式碼
我們知道js是單執行緒的,setTimeout則是非同步方法,因此每次遍歷碰到setTimeout,就把裡面的程式碼放到待執行棧裡,等for迴圈遍歷結束,再執行。
而因為i是用var定義的值型別,直接儲存在棧內,每一次迴圈,i的值都被新值覆蓋,因此最後一次迴圈結束,i=5。
然後才開始執行五次console.log(i);
即得到輸出:5 5 5 5 5。
要解決這個問題,第一個方法是使用自執行函式提供閉包條件,再把i值儲存到閉包中。
自執行函式會立即執行,因此setTimeout函式不會被壓入待執行棧而立即執行。
for(var i = 0; i < 5 ; i++ ){ (function(i){ setTimeout(function(){console.log(i)},100); })(i)}// => 0 1 2 3 4複製程式碼
還有一種方法是把var改成let,此中原理也可參照擴充套件閱讀中var和let的區別。
擴充套件
var和let的區別
- 作用域var 的作用域在整個函式let 的作用域在{}內,例如:function fn(flag){ if(flag){ let i = 'success'; } console.log(i) } fn(true); //VM167:5 Uncaught ReferenceError: i is not defined 複製程式碼我們發現,i 的作用域在 if 語句裡,一旦 if 語句執行結束,i 就被銷燬,因此,當代碼執行到console.log(i)的時候,自然找不到變數。把let給改成var,函式就能夠正常執行。
- 變數提升var 在函式宣告的時候就建立了空間,並被賦值為undefined。(function fn(){ console.log(a); // =>undefined var a = 1; console.log(a); // =>1 })() 複製程式碼執行的順序相當於:1、var a = undefined; 2、console.log(a); // =>undefined 3、a = 1; 4、console.log(a); // =>1 let 則只有到執行到宣告語句的時候才建立空間。(function fn(){ console.log(a); let a = 1; console.log(a); })() //Uncaught ReferenceError: a is not defined 複製程式碼執行的順序是:1、console.log(a)2、找不到a3、丟擲異常Uncaught ReferenceError: a is not defined