1. 程式人生 > 其它 >foreach的this為什麼不能繼承作用域_淺談JavaScript閉包和作用域問題

foreach的this為什麼不能繼承作用域_淺談JavaScript閉包和作用域問題

技術標籤:foreach的this為什麼不能繼承作用域

當你宣告一個變數的時候,一般是這樣的:

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)儲存在堆裡,棧裡儲存他們的記憶體地址(如下圖)。

6d0ed45b3e0e737e155fd26f267ad0e1.png

String,Number,Boolean等類都派生自Object物件,因此通過 new 關鍵字構造的她們都屬於物件,而不是簡單的值型別。

例子裡的變數b,通過String建構函式宣告,則b的__proto__指向String函式的prototype物件,因而b也繼承有String函式的prototype的所有屬性。

而變數a的宣告方法是直接通過等號賦值,則變成了一個簡單的值型別,儲存在棧中。

dd33cf9ee84f4a9ccd1ca49e92fe12e8.png

作用域和上下文環境

乍一聽這個名詞我們可能有點不太能理解,我們先這麼淺薄地理解:

作用域是函式的一塊“領地”,上下文環境儲存作用域內的引數名和值,例如:

var a = 1,    foo = function(b){        console.log(a+b);    };foo(2);複製程式碼

1、 因為我們的程式碼在全域性環境內執行,在執行程式碼之前(即預編譯),將先建立全域性上下文環境,再把全域性上下文環境壓入 上下文棧 :

全域性上下文環境aundefinedfooundefinedthiswindow

8ae52a727d31162cf7ddef5550b9b8d5.png

2、然後我們執行程式碼(到呼叫foo函式之前),然後為變數賦值:

全域性上下文環境a1foofunctionthiswindow

3、 然後我們呼叫foo函式,我們的上下文環境就要轉到foo函式內部,並把foo函式執行上下文環境壓入 上下文棧 ,:

foo函式執行上下文環境b2arguments[2]thiswindow

這時候 上下文棧 內有兩個上下文環境:全域性上下文 和 foo函式執行上下文。

f2e3379e9fb871dfa4517b43fb17b3cb.png

但是在執行的時候,我們發現foo函式的作用域裡沒有變數a,我們要到哪裡取呢?

答案是建立這個函式的作用域裡取,foo函式是全域性建立的,因此我們就回到全域性作用域裡找變數a。

然後看到全域性上下文環境中,有變數a,它的值是1。

a + b == 1 + 2 == 3

因此,最後控制檯輸出:3

4、到此為止,foo函式執行完畢,foo函式執行上下文環境出棧銷燬,最後留下全域性上下文環境:

a2616e8c0accab7bbf5d2345b5ca5ee1.png

把整個過程連在一起,就是下面這張圖:

64a4f570573a3ce07a53b984184d92bb.png

作用域和上下文的區別

作用域只是一個“地盤”,一個抽象的概念,其中沒有變數。

要通過作用域對應的執行上下文環境來獲取變數的值。

同一個作用域下,不同的呼叫會產生不同的執行上下文環境,繼而產生不同的變數的值。

所以,作用域中變數的值是在執行過程中產生的確定的,而作用域卻是在函式建立時就確定了。

閉包

概念

瞭解到我們的先導知識後,我們最後來看閉包,閉包有兩種情況:

  1. 函式作為返回值function fn(){ return function foo(){ console.log('閉包'); } } //呼叫fn立即執行foo 複製程式碼
  2. 函式作為引數被傳遞/* * @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傳入 複製程式碼

閉包的重點其實就在於,函式執行完畢之後,上下文環境不會被銷燬,例如:

f64ce906e52f6aafd9795dc25e38eba5.png

我們會發現,在給變數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的區別

  1. 作用域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,函式就能夠正常執行。
aa9bf10af40ed257e811741f9665cb2d.png
  1. 變數提升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