前端面試之js相關問題(一)

前端雜貨鋪發表於2018-01-09

最近我也是經歷過面試別人和去面試的人了,總結幾個常被提及的面試問題,做一下解答和備忘。

JavaScript 中 this 是如何工作的 ?

先來看看這個題目:

var x = 0;
var foo = {
    x:1,
    bar:{
    x:2,
    baz: function () {
       console.log(this.x)
     }
    }
}

var a = foo.bar.baz
foo.bar.baz() // 2
a() //0
複製程式碼
  • this 永遠指向函式執行時所在的物件,而不是函式建立時所在的物件
  • 匿名函式和不處於任何物件中的函式,This指向window
  • call, apply, with指的This是誰就是誰。
  • 普通函式呼叫,函式被誰呼叫,This就指向誰

上面的例子中,baz被bar呼叫所以指向的指bar. a 執行時所在的物件是 window,所以指向的是window。

作用域鏈?

理解執行環境和上下文

函式呼叫都有與之相關的作用域和上下文。從根本上說,作用域是基於函式(function-based)而上下文是基於物件(object-based)。換句話說,作用域是和每次函式呼叫時變數的訪問有關,並且每次呼叫都是獨立的。上下文總是關鍵字 this 的值,是呼叫當前可執行程式碼的物件的引用。

執行上下文分有globalfunctioneval,一個函式可以產生無數個執行上下文,一系列的執行上下文從邏輯上形成了 執行上下文棧,棧底總是全域性上下文,棧頂是當前(活動的)執行上下文。

執行上下文三屬性:this指標,變數物件(資料作用域),作用域鏈

作用域鏈 即:一變數在自己的作用域中沒有,那麼它會尋找父級的,直到最頂層。過程如下:

  • 任何在執行上下文時刻的作用域都由作用域鏈來實現
  • 在一個函式被定義的時候, 會將它定義時刻的scope chain連結到這個函式物件的[[scope]]屬性
  • 在一個函式物件被呼叫的時候,會建立一個活動物件(也就是一個物件), 然後對於每一個函式的形參,都命名為該活動物件的命名屬性, 然後將這個活動物件做為此時的作用域鏈(scope chain)最前端, 並將這個函式物件的[[scope]]加入到scope chain中.

上面的文字大家可以好好琢磨一下,可以更好的理解函式作用域。

函式宣告提升和變數宣告提升(Hoisting) ?

我們先來了解js編譯器在執行程式碼的過程:
以執行一段function程式碼為例:
第一步:建立可執行上下文(以下簡稱為EC),壓入當前的EC棧中。EC中包括了以下資訊:

  • 詞法環境(=環境記錄項(儲存變數、函式宣告和形參)+ 外部詞法環境(function的[[scope]]屬性,作用域鏈的本質))
  • this的指標
  • 變數環境(與環境記錄項的值相同,但不再發生變動。)

第二步:收集函式宣告變數宣告形參,儲存在環境記錄項內。這個收集的過程,就是一般所謂的宣告提升現象的本質。如果發現了重複的識別符號,則優先順序函式宣告形參變數宣告(優先順序低的會被無視)。

第三步:開始執行程式碼,環境記錄項內沒有的識別符號會根據作用域鏈查詢識別符號對應的值,環境記錄項亦有可能因賦值語句而被修改。

第四步:函式執行完畢,EC棧被彈出、銷燬。

好了,第二步說的很清楚了 宣告提升(Hoisting)現象就是在收集函式、變數宣告和形參的過程會根據函式宣告、形參、變數宣告的順序優先順序來收集。

例子:

var a = 1;  
function b() {  
    a = 10;  
    return;  
    function a() {}  
}  
b();  
console.log(a); 
// 輸出1 由於函式宣告提升,b內的實際是這樣:
// function b() {  
//    function a() {}; 這裡是函式宣告提升
//    a = 10;  
//    return;  
//    function a() {}  
// }
複製程式碼
理解了嗎?
勘誤:謝謝github上有同學的指正關於部落格中的一個問題 · Issue #1 · stephenzhao/hexo-theme-damon,上面的正確執行應該為先進行預編譯,所以先執行function a(){},然後會進行對a的賦值操作。
//正確的順序應該為:
// function b() {  
//    function a() {}  
//    a = 10;  
//    return;  
// }
複製程式碼

什麼是閉包,如何使用它,為什麼要使用它?

還是上面的題目,做個變形。

var x = 0;
var foo = {
    x:1,
    bar:function () {
        console.log(this.x);
        var that = this;
        return function () {
           console.log(this.x)
           console.log(that.x)
        }
    }
}


foo.bar()       // 1
foo.bar()()     // this: 0, that: 1
複製程式碼

上面的例子中ba'r裡面返回了一個匿名函式,這個匿名函式可以在外部被呼叫即:foo.bar()() 讀取到了bar的執行上下文的變數物件 that,這個函式就形成了一個閉包。

好了,我們理解了上面的套路,下面來解釋閉包就好理解了。

閉包就是能夠讀取其它函式內部變數的函式

在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成“定義在一個函式內部的函式”

var x = 0;
var bar:function () {
        var n = 999;
        return function () {
           return n;
        }
    }
var outer = bar();
outer() // 999
複製程式碼

用途:

  1. 讀取函式內部的變數
  2. 讓這些變數的值始終保持在記憶體中

我們修改一下上面的程式碼

var add;
var bar = function () {
        var n = 999;
        add = function () {
            n += 1;
        }
        return function () {
           return n;
        }
    }
var outer = bar();
outer() // 999 
add();
outer(); // 1000
複製程式碼

說明,n一直儲存在記憶體當中,而沒有在bar()執行完成之後被銷燬;
原因:
bar裡面的匿名函式被賦值給了outer,這個導致在outer沒有被銷燬的時候,該匿名函式一直存在記憶體當中,而匿名函式的存在依賴於bar,所以bar需要使用都在記憶體當中,所以bar並不會在呼叫結束後唄垃圾回收機制給收回。

而上面的add接受的也是一個匿名函式,該匿名函式本身也是閉包,所以也可以在外部操作裡面的變數。

注意點

  1. 會導致記憶體洩漏,慎用
  2. 閉包會修改內部變數的值,所以在使用閉包作為物件的公用方法時要謹慎。
    閉包的一個應用,單例模式

單例模式的定義是產生一個類的唯一例項

單例模式在js中經常會遇到,比如 var a = {}; 其實就是一個單例子。

但是我們寫一個更有意義的單例:

var singleton = function( fn ){
    var result;
    return function(){
        return result || ( result = fn .apply( this, arguments ) );
    }
}
複製程式碼

更簡潔一點的:

var singleton = (function () {
    var instance;
    return function (object) {
        if(!instance){
            instance = new object();
        }
        return instance;
    }
    })();
複製程式碼

又是半夜,這兩天在看里約奧運會的比賽,林丹和李宗偉的那場比賽是今年看過的經次於nba總決賽最後一場的精彩程度。一個偉大的英雄,需要另一個偉大的對手來成就,感謝林丹,感謝李宗偉世界會記住你們。晚安。

接下來的文章講解一些關於js物件導向的東西,敬請關注我的專欄 《前端雜貨鋪》


相關文章