作用域鏈與閉包

1.作用域鏈

1.1 作用域鏈是什么?

作用域鏈正是內(nèi)部上下文所有變量對象(包括父變量對象)的列表。
首先,代碼在其對應的環(huán)境中執(zhí)行時,數(shù)據(jù)是保存在其變量對象中的。但是有時會使用其范圍外的數(shù)據(jù)。當需要查找變量的值時,現(xiàn)將變量解析,然后從鏈表的第一項開始尋找,找到為止。

1.2 作用域鏈的形成

全局環(huán)境中,作用域鏈由一個全局對象組成。
而函數(shù)中,作用域鏈的形成分為兩個過程
(1)函數(shù)創(chuàng)建時:會創(chuàng)建一個預先包含全局變量對象的作用域鏈,被保存在內(nèi)部的【【scope】】屬性中。
(2)當調(diào)用函數(shù)時,會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境,然后通過復制函數(shù)的【【scope】】屬性中的對象構建起執(zhí)行環(huán)境的作用域鏈。然后又有一個活動對象被創(chuàng)建并推入執(zhí)行作用域鏈的最前端。

即函數(shù)作用域鏈 = AO + scope

1.3 作用域鏈形成的實際列子

引用湯姆大叔的文章

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); // 60

全局上下文的變量對象是:

globalContext.VO === Global = {
x: 10
foo: <reference to function>
};

在“foo”創(chuàng)建時,“foo”的[[scope]]屬性是:

foo.[[Scope]] = [
globalContext.VO
];

在“foo”激活時(進入上下文),“foo”上下文的活動對象是:

fooContext.AO = {
y: 20,
bar: <reference to function>
};

“foo”上下文的作用域鏈為:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
fooContext.AO,
globalContext.VO
];

內(nèi)部函數(shù)“bar”創(chuàng)建時,其[[scope]]為:

bar.[[Scope]] = [
fooContext.AO,
globalContext.VO
];

在“bar”激活時,“bar”上下文的活動對象為:

barContext.AO = {
z: 30
};

“bar”上下文的作用域鏈為:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
barContext.Scope = [
barContext.AO,
fooContext.AO,
globalContext.VO
];

1.4 此過程中閉包的形成

函數(shù)(調(diào)用時),會創(chuàng)建一個執(zhí)行環(huán)境及相應的作用域鏈,然后使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象(AO),但在作用域鏈中,外部函數(shù)的活動對象始終處于第二位,外部的外部處于第三位,以此類推。
執(zhí)行環(huán)境有一個表示變量的對象--變量對象(VO),全局環(huán)境的VO始終存在,函數(shù)這樣的局部環(huán)境的VO只在函數(shù)的執(zhí)行過程中存在。
內(nèi)部函數(shù)中將會將外部函數(shù)的活動對象添加到其作用域鏈中,當外部函數(shù)執(zhí)行完畢后,其執(zhí)行環(huán)境的作用域鏈會被銷毀,而活動對象會仍然保留在內(nèi)存中。

由于作用域鏈的形成機制,導致了某些函數(shù)在執(zhí)行完畢后,并不會銷毀其活動對象,因為其被包含在了其他函數(shù)的作用域鏈中。

2. 閉包

可以以正常數(shù)據(jù)形式存在的函數(shù)(比方說:當參數(shù)傳遞,接受函數(shù)式參數(shù)或者以函數(shù)值返回)都稱作 第一類函數(shù)(一般說第一類對象)。在ECMAScript中,所有的函數(shù)都是第一類對象。

在函數(shù)中,如果包含了自由變量(并未在函數(shù)內(nèi)部聲明),那么即產(chǎn)生了閉包。因為它涉及到了另外一個環(huán)境的數(shù)據(jù),那么會將另外一個環(huán)境中的VO暫存起來。

2.2 閉包實戰(zhàn)

2.2.1如下代碼輸出多少?如果想輸出3,那如何改造代碼?

var fnArr = [];
for (var i = 0; i < 10; i ++) {
  fnArr[i] =  function(){
    return i
  };
}
console.log( fnArr[3]() )

分析:代碼的本意是想,fnArr3中,是調(diào)用的第數(shù)組中的四個函數(shù),并且函數(shù)范圍值與數(shù)組下標是一樣的。這里會直接輸出10,因為數(shù)組中函數(shù)僅僅是聲明了,I并未賦值,等到調(diào)用時,其作用域中并不包含I,就向上級尋找I,而此時所有的函數(shù)都是共享一個父作用域,I的值為10.
如果要輸出3,就是讓每一次為數(shù)組賦值時,能夠讓函數(shù)能夠記住此時i的值,并在調(diào)用的時候能夠取到。
所以我們將循環(huán)更改一下:
for (var i = 0; i < 10; i ++) {
(function(j){
fnArr[j] = function(){
return j
};
})(i);
}
這里我們將賦值過程用一個立即執(zhí)行函數(shù)封裝了起來,并且通過參數(shù)傳值,將i的值保存在了外層立即執(zhí)行函數(shù)的執(zhí)行上下文中。

2.2.2 封裝一個 Car 對象

var Car = (function(){
    var speed = 0;
    function set(speed1){
        speed = speed1
        console.log(speed)
    }
    function get(){
        console.log(speed)
        return speed;
    }
    function speedUp(){
        speed++;
        console.log(speed)
    }
    function speedDown(){
        speed--;
        console.log(speed)
    }
    return {
        set: set,
        get: get,
        speedUp: speedUp,
        speedDown: speedDown
    }
})()
Car.set(30)
Car.get() //30
Car.speedUp()
Car.get() //31
Car.speedDown()
Car.get()  //30

這里用一個立即執(zhí)行函數(shù),并返回我們需求的對象的形式來封裝。這樣的好處是所有的數(shù)據(jù)都被封裝在了立即執(zhí)行函數(shù)中,不會暴露在外面。

2.2.3 如下代碼輸出多少?如何連續(xù)輸出 0,1,2,3,4

for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log('delayer:' + i )
  }, 0)
}

分析:setTimeout外層創(chuàng)建一個閉包

for(var i=0; i<5; i++){
    (function (i){
    setTimeout(function(){
        console.log('delayer:' + i )
    }, 0)
    })(i)
}

2.2.4 如下代碼輸出多少?

function makeCounter() {
  var count = 0

  return function() {
    return count++
  };
}

var counter = makeCounter()
var counter2 = makeCounter();

console.log( counter() ) // 0
console.log( counter() ) // 1

console.log( counter2() ) // 0
console.log( counter2() ) // 1

分析:makeCounter都是返回了一個函數(shù)。且couter
與couter2所返回的函數(shù)是同名的。但是每次調(diào)用時,進入執(zhí)行上下文,然后綁定了新的作用域鏈了。

2.2.5 補全代碼,實現(xiàn)數(shù)組按姓名、年紀、任意字段排序

var users = [
    { name: "John", age: 20, company: "Baidu" },
    { name: "Pete", age: 18, company: "Alibaba" },
    { name: "Ann", age: 19, company: "Tecent" }
]
function byField(string){
    return function(user1,user2){
        return user1[string] >user2[string]
    }
}
users.sort(byField('age'))

2.2.6 寫一個 sum 函數(shù),實現(xiàn)如下調(diào)用方式

console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4

解答:

function sum(val1){
  return function(val2){
    return val1+val2;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 作用域鏈 作用域(scope)作用域是程序源代碼中定義變量的區(qū)域,規(guī)定了當前執(zhí)行代碼對變量和函數(shù)可訪問的范圍。ES...
    LeoCong閱讀 244評論 0 0
  • 閉包是js中一個極為NB的武器,但也不折不扣的成了初學者的難點。因為學好閉包就要學好作用域,正確理解作用域鏈,然而...
    faremax閱讀 451評論 0 1
  • 來源:仗劍走天涯! 關于javascript的作用域的一些總結,主要參考以上文章,加上自己的整理的理解。 近日對j...
    Michael_林閱讀 1,021評論 0 1
  • 作用域 在JavaScript中,我們可以將作用域定義為一套規(guī)則,這套規(guī)則用來管理引擎如何在當前作用域以及嵌套的子...
    Stago閱讀 174評論 0 0
  • “莉比斯,你回來啦?” “嗯,維恩今天過得還好嗎?” “還好。南部的戰(zhàn)爭怎么樣了?” “別擔心,快結束了。明天你就...
    青梔閱讀 320評論 0 0

友情鏈接更多精彩內(nèi)容