JavaScript基礎複習(五) 作用域(鏈),閉包,this,執行上下文

作用域

作用域: 變量與函數的可訪問範圍量

分爲

  • 全局作用域: 在代碼中任何地方都能訪問到的對象擁有全局作用域
  • 局部作用域: 一般只在固定的代碼片段內可訪問到。最常見的是在函數體內定義的變量,只能在函數體內使用。

在函數體內,局部變量的優先級高於同名的全局變量。如果在函數內聲明的一個局部變量或者函數參數中帶有的變量和全局變量重名,那麼全局變量就被局部變量所遮蓋。

聲明提前:JavaScript 函數裏聲明的所有變量(但不涉及賦值)都被「提前」至函數體的頂部
由於 JavaScript 沒有塊級作用域,因此一些程序員特意將變量聲明放在函數體頂部,而不是將聲明靠近放在使用變量之處。這種做法使得他們的源代碼非常清晰地反映了真實的變量作用域。

var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        // 這裏可以訪問color、anotherColor和tempColor
    }
    // 這裏可以訪問color和anotherColor,但不能訪問tempColor
    swapColors();
}
// 這裏只能訪問color
changeColor();

全局環境、changeColor() 的局部環境和 swapColors() 的局部環境。

全局環境中有一個變量 color 和一個函數 changeColor()。changeColor() 的局部環境中有一個名爲 anotherColor 的變量和一個名爲 swapColors() 的函數,但它也可以訪問全局環境中的變量 color。swapColors() 的局部環境中有一個變量tempColor,該變量只能在這個環境中訪問到。無論全局環境還是 changeColor() 的局部環境都無權訪問 tempColor。然而,在 swapColors() 內部則可以訪問其他兩個環境中的所有變量,因爲那兩個環境是它的父執行環境。

內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。每個環境都可以向上搜索作用域鏈,以查詢變量和函數名;但任何環境都不能通過向下搜索作用域鏈而進入另一個執行環境。

閉包

由於在 Javascript 語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成定義在一個函數內部的函數

本質就是上級作用域內變量的生命週期,因爲被下級作用域內引用,而沒有被釋放。就導致上級作用域內的變量,等到下級作用域執行完以後才正常得到釋放。

用處有兩個:

  • 一個是可以讀取函數內部的變量(作用域鏈),
  • 另一個就是讓這些變量的值始終保持在內存中。
function fun() {                             
    var n = 1;                                      
    add = function() {    
    //add 是一個全局變量,add 的值是一個匿名函數。而這個匿名函數本身也是一個閉包,和 fun2 處於同一作用域,所以 add 相當於是一個 setter,可以在函數外部對函數   內部的局部變量進行操作
        n += 1 
    }
    function fun2(){
        console.log(n);
    }
    return fun2;
}
var result = fun();  
result(); // 1
add();
result(); // 2   
function fun() {   
    var n = 1;
    var  add = function() {
        n += 1
    }
    function fun2(){
        console.log(n);
    }
    return fun2;
}
var result = fun();  
result(); // 1
add();
result(); // 1

使用閉包解決一些問題:

var add = function() {
    var counter = 0;
    var plus = function() {return counter += 1;}  //閉包 
    return plus;
}

var puls2 = add();
console.log(puls2());
console.log(puls2());
console.log(puls2());
// 計數器 counter 受 add() 函數的作用域保護,只能通過 puls2 方法修改。

使用閉包的注意事項:

  • 由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在 IE 中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除或設置爲 null,斷開變量和內存的聯繫。
  • 閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(public method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。

this

this 是 JavaScript 的關鍵字,指函數執行時的上下文,跟函數定義時的上下文無關。隨着函數使用場合的不同,this 的值會發生變化。但是有一個總的原則,那就是 this 指代的是調用函數的那個對象。

this 的指向

this 永遠指向最後調用它的那個對象

    // this  始終指向最後調用它的那個對象
    var name = "windowsName";
    var a = {
        name : null,
        name: "Cherry",
        fn : function () {
            console.log(this.name);  
        }
    }

    var f = a.fn;
    f();   // windowsName   window 調用的

    a.fn();  // Cherry   a 調用的
    
    window.a.fn(); // Cherry  最後調用的

改變 this 的指向

  • 使用 ES6 的箭頭函數
  • 在函數內部使用 _this = this
  • 使用 apply、call、bind
  • new 實例化一個對象

全局上下文

在全局上下文中,也就是在任何函數體外部,this 指代全局對象。

// 在瀏覽器中,this 指代全局對象 window
console.log(this === window);  // true

函數上下文

在函數上下文中,也就是在任何函數體內部,this 指代調用函數的那個對象。

函數調用中的 this

function f1(){
    return this;
}
console.log(f1() === window); // true

如上代碼所示,直接定義一個函數 f1(),相當於爲 window 對象定義了一個屬性。直接執行函數 f1(),相當於執行 window.f1()。所以函數 f1() 中的 this 指代調用函數的那個對象,也就是 window 對象。

function f2(){
    "use strict"; // 這裏是嚴格模式
    return this;
}
console.log(f2() === undefined); // true

如上代碼所示,在「嚴格模式」下,禁止 this 關鍵字指向全局對象(在瀏覽器環境中也就是 window 對象),this 的值將維持 undefined 狀態。

對象方法中的 this

var o = {
    name: "stone",
    f: function() {
        return this.name;
    }
};
console.log(o.f()); // "stone"

如上代碼所示,對象 o 中包含一個屬性 name 和一個方法 f()。當我們執行 o.f() 時,方法 f() 中的 this 指代調用函數的那個對象,也就是對象 o,所以 this.name 也就是 o.name。

注意,在何處定義函數完全不會影響到 this 的行爲,我們也可以首先定義函數,然後再將其附屬到 o.f。這樣做 this 的行爲也一致。如下代碼所示:

var fun = function() {
    return this.name;
};
var o = { name: "stone" };
o.f = fun;
console.log(o.f()); // "stone"

類似的,this 的綁定只受最靠近的成員引用的影響。在下面的這個例子中,我們把一個方法 g() 當作對象 o.b 的函數調用。在這次執行期間,函數中的 this 將指向 o.b。事實上,這與對象本身的成員沒有多大關係,最靠近的引用纔是最重要的。

o.b = {
    name: "sophie"
    g: fun,
};
console.log(o.b.g()); // "sophie"

eval() 方法中的 this

eval() 方法可以將字符串轉換爲 JavaScript 代碼,使用 eval() 方法時,this 指向哪裏呢?答案很簡單,看誰在調用 eval() 方法,調用者的執行環境中的 this 就被 eval() 方法繼承下來了。如下代碼所示:

// 全局上下文
function f1(){
    return eval("this");
}
console.log(f1() === window); // true

// 函數上下文
var o = {
    name: "stone",
    f: function() {
        return eval("this.name");
    }
};
console.log(o.f()); // "stone"

call() 和 apply() 方法中的 this

call() 和 apply() 是函數對象的方法,它的作用是改變函數的調用對象,它的第一個參數就表示改變後的調用這個函數的對象。因此,this 指代的就是這兩個方法的第一個參數。

var x = 0;  
function f() {    
    console.log(this.x);  
}  
var o = {};  
o.x = 1;
o.m = f;  
o.m.apply(); // 0

call() 和 apply() 的參數爲空時,默認調用全局對象。因此,這時的運行結果爲 0,證明 this 指的是全局對象。如果把最後一行代碼修改爲:

o.m.apply(o); // 1

運行結果就變成了 1,證明了這時 this 指代的是對象 o。

bind() 方法中的 this

ECMAScript 5 引入了 Function.prototype.bind。調用 f.bind(someObject) 會創建一個與 f 具有相同函數體和作用域的函數,但是在這個新函數中,this 將永久地被綁定到了 bind 的第一個參數,無論這個函數是如何被調用的。如下代碼所示:

function f() {
    return this.a;
}

var g = f.bind({
    a: "stone"
});
console.log(g()); // stone

var o = {
    a: 28,
    f: f,
    g: g
};
console.log(o.f(), o.g()); // 28, stone

DOM 事件處理函數中的 this

一般來講,當函數使用 addEventListener,被用作事件處理函數時,它的 this 指向觸發事件的元素。如下代碼所示:

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <title>test</title>
    </head>
    <body>
        <button id="btn" type="button">click</button>
        <script>
            var btn = document.getElementById("btn");
            btn.addEventListener("click", function(){
                this.style.backgroundColor = "#A5D9F3";
            }, false);
        </script>
    </body>
</html>

但在 IE 瀏覽器中,當函數使用 attachEvent ,被用作事件處理函數時,它的 this 卻指向 window。如下代碼所示:

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <title>test</title>
    </head>
    <body>
        <button id="btn" type="button">click</button>
        <script>
            var btn = document.getElementById("btn");
            btn.attachEvent("onclick", function(){
                console.log(this === window);  // true
            });
        </script>
    </body>
</html>

內聯事件處理函數中的 this

當代碼被內聯處理函數調用時,它的 this 指向監聽器所在的 DOM 元素。如下代碼所示:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 會顯示 button,注意只有外層代碼中的 this 是這樣設置的。如果 this 被包含在匿名函數中,則又是另外一種情況了。如下代碼所示:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在這種情況下,this 被包含在匿名函數中,相當於處於全局上下文中,所以它指向 window 對象。

執行上下文

執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行

  • 全局 一個程序中只能存在一個全局執行上下文
  • 函數 只有在函數被調用的時候纔會被創建
  • eval()

執行上下文的生命週期包括三個階段:創建階段→執行階段→回收階

JavaScript 引擎創建了執行上下文棧來管理執行上下文。可以把執行上下文棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則

執行機制

Event Loop(事件循環)

  • 同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。
  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。

那怎麼知道主線程執行棧爲空啊?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('發送成功!');
    }
})
console.log('代碼執行結束');

上面是一段簡易的ajax請求代碼:

  • ajax進入Event Table,註冊回調函數success。
  • 執行console.log(‘代碼執行結束’)。
  • ajax事件完成,回調函數success進入Event Queue。
  • 主線程從Event Queue讀取回調函數success並執行。

微任務、宏任務與Event-Loop

這一次,徹底弄懂 JavaScript 執行機制

this

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章