javascript系列--javascript深入理解--作用域,作用域鏈,閉包的面試題解

一、概要

作用域和作用域鏈是js中非常重要的特性,關係到理解整個js體系,閉包是對作用域的延伸,其他語言也有閉包的特性。

那什麼是作用域?作用域指的是一個變量和函數的作用範圍。

1、js中函數內聲明的所有變量在函數體內始終是可見的;

2、在ES6中有全局作用域和局部作用域,但是沒有沒有塊級作用域(catch只在其內部生效);

3、局部變量的優先級高於全局變量。

二、作用域

我們來舉幾個栗子:

2.1變量提升

var scope="global";
function scopeTest(){
    console.log(scope);
    var scope="local"  
}
scopeTest(); //undefined

上面的代碼輸出是undefined,這是因爲局部變量scope變量提升了,等效於下面

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined

注意,如果在局部作用域中忘記var,那麼變量就被聲明爲全局變量。

var scope="global";
function scopeTest(){
    console.log(scope);
    scope="local"  
}
scopeTest(); //global
var scope="global";
function scopeTest(){
    scope="local" 
    console.log(scope);
}
scopeTest(); //local

2.2沒有塊級作用域

和我們其他常用語言不同的是,js中沒有塊級作用域

var data = [];

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

data[0]();    // 3
data[1]();    // 3
data[2]();    // 3

2.3作用域鏈

每個函數都有自己的執行上下文環境,當代碼在這個環境中執行時候,會創建變量對象的作用域鏈,

那什麼是作用域鏈?作用域鏈式是一個對象列表。

作用域鏈的作用?他保證了變量對象的有序訪問。

作用域鏈開始的地方:當前代碼執行環境的變量對象,常被稱之爲“活躍對象”(AO),變量的查找會從第一個鏈的對象開始,如果對象中包含變量屬性,那麼就停止查找,如果沒有就會繼續向上級作用域查找,直到找到全局對象中,如果找不到就會報ReferenceError。

2.4閉包

function createClosure(){
    var name = "jack";
    return {
        setStr:function(){
            name = "rose";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面在函數中反悔了兩個閉包,這兩個閉包都維持着對外部作用域的引用,因此不管在哪調用都是能夠訪問外部函數中的變量。在一個函數內部定義的函數,閉包中會將外部函數的自由對象添加到自己的作用域中,所以可以通過內部函數訪問外部函數的屬性,這就是js模擬私有變量的一種方式。

注意:由於閉包會額外的附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會比其他函數多佔用些內存空間,過度使用會導致內存佔用增加。

三、閉包面試題解

由於作用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引起了一個副作用,如果內部函數在一個循環中,那麼變量的值始終爲最後一個值。

var data = [];

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

data[0]();    // 3
data[1]();    // 3
data[2]();    // 3

如果想強制返回逾期結果,怎麼整?

方法一:立即執行函數

for (var i = 0; i < 3; i++) {
    (function(num) {
        setTimeout(function() {
            console.log(num);
        }, 1000);
    })(i);
}
// 0
// 1
// 2

方法二:返回一個匿名函數賦值

var data = [];

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

data[0]();    // 0
data[1]();    // 1
data[2]();    // 2

無論上是立即執行函數還是返回一個匿名函數賦值,原理上都是因爲變量的按值傳遞,所以會將變量i的值賦值給實參num,在匿名函數的內部又創建了一個用於訪問num的匿名函數,這樣每一個函數都有一個num的副本,互不影響。

方法三:使用es6的let

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

解釋一下原理:

var data = [];// 創建一個數組data;

// 進入第一次循環
{

let i = 0; // 注意:因爲使用let使得for循環爲塊級作用域
           // 此次 let i = 0 在這個塊級作用域中,而不是在全局環境中
data[0] = function() {
    console.log(i);
};

}

循環時,let聲明瞭i,所以整個塊是塊級作用域,那麼data[0]這個函數就成了一個閉包,這裏用{}表述,只是希望通過它來說明let存在的時候,這個for循環塊是塊級作用域,而不是全局作用域。

上面的塊級作用域,就像函數作用域一樣,寒暑表執行完畢,其中的變量會被銷燬,但是因爲這個代碼塊中存在一個閉包,閉包的作用域鏈中引用着塊級作用域,所以在閉包被調用之前,這個塊級作用域內部的變量不會被銷燬。

// 進入第二次循環
{

let i = 1; // 因爲 let i = 1 和上面的 let i = 0     
           // 在不同的作用域中,所以不會相互影響
data[1] = function(){
     console.log(i);
}; 

}
當執行data[1]()時,進入下面的執行環境。

{

 let i = 1; 
 data[1] = function(){
      console.log(i);
 }; 

}

在上面這個執行環境中,它會首先尋找該執行環境中是否存在i,沒有找到,就沿着作用域鏈繼續向上找,在其所在的塊級作用域執行環境中,找到i=1,於是輸出1。



## 四、思考題

代碼1:

var scope = "global scope";
function checkscope(){

var scope = "local scope";
function f(){
    return scope;
}
return f;

}

checkscope()(); //local scope

代碼2:

var scope = "global scope";
function checkscope(){

var scope = "local scope";
function f(){
    return scope;
}
return f;

}

var foo = checkscope();
foo(); //local scope


## 四、參考

1、https://segmentfault.com/a/1190000000618597

2、https://www.cnblogs.com/zhuzhenwei918/p/6131345.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章