執行上下文、變量對象、做用域鏈、this

這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,懇求star哈~javascript


不論是前端老司機,仍是前端小白,看到標題中列舉的這些概念,想必都是頭大。其實你知道麼?這些概念背後是有聯繫的,理清楚他們的關係,你才能準確且牢靠地記住他們。前端

也只有理清楚這些基本且重要的概念,你才能在前端的道路上越走越遠。java

好了,讓咱們開始吧。git

執行上下文

執行上下文能夠理解爲函數運行的環境。每一個函數執行時,都會給對應的函數建立這樣一個執行環境。github

JS運行環境大概包括三種狀況:全局環境、函數環境、eval環境(不推薦使用,因此不討論)。面試

一個JS程序中,一定會產生多個執行上下文,JS引擎會以棧的方式處理它們,這個棧,咱們稱之爲函數調用棧。棧底永遠都是全局上下文,棧頂就是當前正在執行的上下文。瀏覽器

因爲棧是先進後出的結構,咱們不難推出如下四點:bash

  • 只有棧頂的上下文處於執行中,其餘上下文須要等待
  • 全局上下文只有惟一的一個,它在瀏覽器關閉時出棧
  • 函數的執行上下文的個數沒有限制
  • 每次某個函數被調用,就會有個新的執行上下文爲其建立。

固然,光知道這些仍是不夠,咱們還必須瞭解執行上下文的生命週期。閉包

執行上下文的生命週期

當調用一個函數時,一個新的執行上下文就會被建立。而一個執行上下文的生命週期能夠分爲兩個階段。app

建立階段

在這個階段中,執行上下文會分別建立變量對象,創建做用域鏈,以及肯定this的指向。

代碼執行階段

建立完成以後,就會開始執行代碼,這個時候,會完成變量賦值,函數引用,以及執行其餘代碼。

至此,咱們終於知道執行上下文跟變量對象、做用域鏈及this的關係。

接下來咱們重點介紹這三個概念。

變量對象

當一個函數被調用時,執行上下文就建立了,執行上下文包含了函數全部聲明的變量和函數,保存這些變量跟函數的對象,咱們稱之爲變量對象。

變量對象的建立,依次經歷瞭如下幾個過程。

  • 創建arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。
  • 檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是函數名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
  • 檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名創建一個屬性,屬性值爲undefined。若是該變量名的屬性已經存在,爲了防止同名的函數被修改成undefined,則會直接跳過,原屬性值不會被修改。
    舉個反例,不少人對如下代碼存在疑問,既然變量聲明的foo遇到函數聲明的foo會跳過,但是爲何最後foo的輸出結果仍然是被覆蓋了?
function foo() { console.log('function foo') }
var foo = 20;

console.log(foo); // 20
複製代碼

這是由於上面的三條規則僅僅適用於變量對象的建立過程。也就是執行上下文的建立過程。而foo = 20是在執行上下文的執行過程當中運行的,輸出結果天然會是20。對比下例。

console.log(foo); // function foo
function foo() { console.log('function foo') }
var foo = 20;
複製代碼
// 上慄的執行順序爲

// 首先將全部函數聲明放入變量對象中
function foo() { console.log('function foo') }

// 其次將全部變量聲明放入變量對象中,可是由於foo已經存在同名函數,所以此時會跳過undefined的賦值
// var foo = undefined;

// 而後開始執行階段代碼的執行
console.log(foo); // function foo
foo = 20;
複製代碼

再看一個例子:

// demo01
function test() {
    console.log(a);
    console.log(foo());

    var a = 1;
    function foo() {
        return 2;
    }
}

test();
複製代碼

咱們直接從test()的執行上下文開始理解。全局做用域中運行test()時,test()的執行上下文開始建立。爲了便於理解,咱們用以下的形式來表示

// 建立過程
testEC = {
    // 變量對象
    VO: {},
    scopeChain: {}
}

// 由於本文暫時不詳細解釋做用域鏈,因此把變量對象專門提出來講明

// VO 爲 Variable Object的縮寫,即變量對象
VO = {
    arguments: {...},  //注:在瀏覽器的展現中,函數的參數可能並非放在arguments對象中,這裏爲了方便理解,我作了這樣的處理
    foo: <foo reference>  // 表示foo的地址引用
    a: undefined
}

複製代碼

未進入執行階段以前,變量對象中的屬性都不能訪問!可是進入執行階段以後,變量對象轉變爲了活動對象,裏面的屬性都能被訪問了,而後開始進行執行階段的操做。

變量對象和活動對象其實都是同一個對象,只是處於執行上下文的不一樣生命週期。不過只有處於函數調用棧棧頂的執行上下文中的變量對象,纔會變成活動對象。

// 執行階段
VO ->  AO   // Active Object
AO = {
    arguments: {...},
    foo: <foo reference>,
    a: 1,
    this: Window
}
複製代碼

所以,上面的例子demo1,執行順序就變成了這樣

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;
}

test();
複製代碼

做用域鏈與閉包

變量對象講完了,接着是做用域鏈,這裏就不得不先提下做用域。

做用域

做用域最大的用處就是隔離變量,不一樣做用域下同名變量不會有衝突。

JavaScript中只有全局做用域與函數做用域。言外之意是:javascript除了全局做用域以外,只有函數能夠建立的做用域

JavaScript代碼的整個執行過程,分爲兩個階段,代碼編譯階段代碼執行階段

編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段做用域規則會肯定。

執行階段由引擎完成,主要任務是執行可執行代碼,執行上下文在這個階段建立。

理解這點很重要,咱們面試過程當中,常常會被問到「自由變量」的取值問題。

什麼是「自由變量」?先看個例子:

var x = 10;
function fn() {
    var b = 20;
    console.log(x+b); // x在這裏就是一個自由變量
}
複製代碼

取x的值時,須要到另外一個做用域中取,x就被稱做「自由變量」。

「自由變量」的取值,難倒一片的人,不信,看看下面這個例子:

var x = 10;
function fn() {
    console.log(x);
}
function show(f){
    var x = 20;
    (function () {
        f(); // 這裏輸出什麼???
    })();
}
show(fn);
複製代碼

你的第一反應是否是20?答案是10!!

其實這個問題很簡單,自由變量要到建立這個函數的那個做用域中取值——是「建立」,而不是「調用」

爲何呢?由於做用域是在代碼編譯過程就肯定下來的,而後就不會改變,這就是所謂的「靜態做用域」。

本例中,在fn函數取自由變量x的值時,要到哪一個做用域中取?——要到建立fn函數的那個做用域中取——不管fn函數將在哪裏調用。fn明顯是在全局環境下建立的,x明顯就是10。

做用域鏈

上面的例子,只是跨一個做用域去尋找。

若是跨了一步,還沒找到呢?——接着跨!——一直跨到全局做用域爲止。要是在全局做用域中都沒有找到,那就是真的沒有了。

這個一步一步「跨」的路線,咱們稱之爲——做用域鏈。

咱們拿文字總結一下取自由變量時的這個「做用域鏈」過程:(假設a是自由量)

第一步,如今當前做用域查找a,若是有則獲取並結束。若是沒有則繼續;

第二步,若是當前做用域是全局做用域,則證實a未定義,結束;不然繼續;

第三步,(不是全局做用域,那就是函數做用域)將建立該函數的做用域做爲當前做用域;

第四步,跳轉到第一步。

閉包

閉包是一種特殊的對象。

它由兩部分組成。執行上下文(代號A),以及在該執行上下文中建立的函數(代號B)。

當B執行時,若是訪問了A中變量對象中的值,那麼閉包就會產生。

// demo01
function foo() {
    var a = 20;
    var b = 30;

    function bar() {
        return a + b;
    }

    return bar;
}

var bar = foo();
bar();
複製代碼

上面的例子,首先有執行上下文foo,在foo中定義了函數bar,而經過對外返回bar的方式讓bar得以執行。當bar執行時,訪問了foo內部的變量a,b。所以這個時候閉包產生。

閉包的應用場景

除了面試,在實踐中,閉包有兩個很是重要的應用場景。分別是模塊化與柯里化。

this

this或許是最讓初學者頭疼的概念了吧。this難就難在指向上。

請記住:this的指向,是在函數被調用的時候肯定的,在函數執行過程當中,this一旦被肯定,就不可更改了

咱們來看看幾種狀況:

全局對象中的this

全局環境中的this,指向它自己。

函數中的this

在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。若是調用者函數,被某一個對象所擁有,那麼該函數在調用時,內部的this指向該對象。若是函數獨立調用,那麼該函數內部的this,則指向undefined。可是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。切記,函數執行過程當中,this一旦被肯定,就不可更改。

'use strict';
var a = 20;
function foo () {
    var a = 1;
    var obj = {
        a: 10,
        c: this.a + 20,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;

}
console.log(foo());    // ?
console.log(window.foo());  // ?
複製代碼

執行foo()時,函數獨立調用,因此this指向undefined(由於是嚴格模式),因此執行this.a時報錯。

執行window.foo()時,this.a = 20,結果爲40.

function foo() {
    console.log(this.a)
}

function active(fn) {
    fn(); // 真實調用者,爲獨立調用
}

var a = 20;
var obj = {
    a: 10,
    getA: foo
}

active(obj.getA); // 20
複製代碼

使用call,apply顯示指定this

call與applay

構造函數與原型方法上的this

function Person(name, age) {

    // 這裏的this指向了誰?
    this.name = name;
    this.age = age;   
}

Person.prototype.getName = function() {

    // 這裏的this又指向了誰?
    return this.name;
}

// 上面的2個this,是同一個嗎,他們是否指向了原型對象?

var p1 = new Person('Nick', 20);
p1.getName();
複製代碼

this,是在函數調用過程當中肯定,所以,搞明白new的過程當中到底發生了什麼就變得十分重要。

經過new操做符調用構造函數,會經歷如下4個階段。

  • 建立一個新的對象;
  • 將構造函數的this指向這個新對象;
  • 指向構造函數的代碼,爲這個對象添加屬性,方法等;
  • 返回新對象。

所以,當new操做符調用構造函數時,this其實指向的是這個新建立的對象,最後又將新的對象返回出來,被實例對象p1接收。所以,咱們能夠說,這個時候,構造函數的this,指向了新的實例對象,p1。

而原型方法上的this就好理解多了,根據上邊對函數中this的定義,p1.getName()中的getName爲調用者,他被p1所擁有,所以getName中的this,也是指向了p1。

寫在最後

本文提到的概念,都是JavaScript中相對晦澀的,平時開發過程當中,要多思考其原理,這是一個必經的階段,只要不斷加深理解,咱們才能真正掌握這些概念,也只有掌握好這些概念,咱們才能在前端的道理上越走越遠。

相關文章
相關標籤/搜索