一個JS面試題

---只有程序員才懂的幽默---
一程序員去面試,面試官問:“你畢業才兩面,這三年工作經驗是怎么來的?!”程序員回答:“加班。”

最近一段時間呢,被一個JS面試題刷屏了,接下來我們也簡單談一下這道JS面試題所引發的思考。
題目是這樣的:

//比較下面兩段代碼,試述兩段代碼的不同之處
// A--------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope(); 
// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

首先A、B兩段代碼輸出返回的都是 “local scope”,如果對這一點還有疑問的同學請自覺回去溫習一下js作用域的相關知識。
接下來我們分析下這兩段代碼的執行過程:
首先是A:

進入全局環境上下文,全局環境被壓入環境棧,contextStack = [globalContext]
全局上下文環境初始化,

globalContext={
    variable object:[scope, checkscope],
    scope chain: variable object // 全局作用域鏈
}

,同時checkscope函數被創建,此時 checkscope.[[Scope]] = globalContext.scopeChain
執行checkscope函數,進入checkscope函數上下文,checkscope被壓入環境棧,contextStack=[checkscopeContext, globalContext]。隨后checkscope上下文被初始化,它會復制checkscope函數的[[Scope]]變量構建作用域,即 checkscopeContext={ scopeChain : [checkscope.[[Scope]]] }
checkscope的活動對象被創建 此時 checkscope.activationObject = [arguments], 隨后活動對象被當做變量對象用于初始化,checkscope.variableObject = checkscope.activationObject = [arguments, scope, f],隨后變量對象被壓入checkscope作用域鏈前端,(checckscope.scopeChain = [checkscope.variableObject, checkscope.[[Scope]] ]) == [[arguments, scope, f], globalContext.scopeChain]
函數f被初始化,f.[[Scope]] = checkscope.scopeChain。
checkscope執行流繼續往下走到 return f(),進入函數f執行上下文。函數f執行上下文被壓入環境棧,contextStack = [fContext, checkscopeContext, globalContext]。函數f重復 第4步 動作。最后 f.scopeChain = [f.variableObject,checkscope.scopeChain]
函數f執行完畢,f的上下文從環境棧中彈出,此時 contextStack = [checkscopeContext, globalContext]。同時返回 scope, 解釋器根據f.scopeChain查找變量scope,在checkscope.scopeChain中找到scope(local scope)。
checkscope函數執行完畢,其上下文從環境棧中彈出,contextStack = [globalContext]
如果你理解了A的執行流程,那么B的流程在細節上一致,唯一的區別在于B的環境棧變化不一樣,

A: contextStack = [globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [fContext, checkscopeContext, globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [globalContext]

B: contextStack = [globalContext] —> contextStack = [checkscopeContext, globalContext] —> contextStack = [fContext, globalContext] —> contextStack = [globalContext]

也就是說,真要說這兩段代碼有啥不同,那就是他們執行過程中環境棧的變化不一樣,其他的兩種方式都一樣。

其實對于理解這兩段代碼而言最根本的一點在于,javascript是使用靜態作用域的語言,他的作用域在函數創建的時候便已經確定(不含arguments)。

說了這么一大坨偏理論的東西,能堅持看下來的同學估計都要睡著了…是的,這么一套理論性的東西糾結有什么用呢,我只要知道函數作用域在創建時便已經生成不就好了么。沒有實踐價值的理論往往得不到重視。那我們來看看,當我們了解到這一套理論之后我們的世界到底會發生了什么變化:
這樣一段代碼:

function setFirstName(firstName){
 
    return function(lastName){
        return firstName+" "+lastName;
    }
}
 
var setLastName = setFirstName("kuitos");
var name = setLastName("lau");
 
// 調用setFirstName函數時返回一個匿名函數,該匿名函數會持有setFirstName函數作用域的變量對象(里面包含arguments和firstName),不管匿名函數是否會使用該變量對象里的信息,這個持有邏輯均不會改變。
// 也就是當setFirstName函數執行完之后其執行環境被銷毀,但是他的變量對象會一直保存在內存中不被銷毀(因為被匿名函數hold)。同樣的,垃圾回收機制會因為變量對象被一直hold而不做回收處理。這個時候內存泄露就發生了。這時候我們需要做手動釋放內存的處理。like this:
setLastName = null;
// 由于匿名函數的引用被置為null,那么其hold的setFirstName的活動對象就能被安全回收了。
// 當然,現代瀏覽器引擎(以V8為首)都會嘗試回收閉包所占用的內存,所以這一點我們也不必過多處理。
                                                  2016-11-30 00:58:53
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容