用簡單的方式解釋傳說中的做用域與做用域鏈

今年第十號颱風‘安比’來啦,外面狂風大做暴雨連連,哪敢出門啊因此就安心待在家裏寫一篇博客總結下本身最近學習心得,emmmmmm....那就開始吧。數組

做用域

坦白說平常開發過程當中並不會常常關注 做用域 這個東西,並且即便程序出現了八阿哥 ( Bug ) 估計不少人也不會考慮到做用域這一起。可是...(此處應有強調),理解做用域對咱們寫出健壯、高效的代碼有很大幫助。因此呢今年筆者就挑選了這個話題和你們一塊兒探討下。瀏覽器

什麼叫 做用域?

這裏筆者沒有去查閱各類官方的解釋,姑且就目前的學習和工做的心得來闡述下吧:
做用域 簡單來講就是 變量的做用區域。好比咱們定義一個變量 var value = 1, 那麼能訪問到這個變量的地方都是她的做用區域,簡稱 做用域。做用域能夠分爲 全局做用域函數做用域塊做用域(ES6)。閉包

全局做用域

沒有什麼比代碼來得更實在,如今新建一個 index.js 文件,內容以下:函數

var value = 1;

console.log(`Window Area: ${value}`);   //A

function showValue() {
    console.log(`Function Area: ${value}`);  //B
}
showValue();

console.log(`Window Area: ${window.value}`);   //C

運行結果以下:學習

Debugger listening on ws://127.0.0.1:11698/e05297d1-af34-4244-a55f-a818c5d2951a
Debugger attached.
Window Area: 1     <----訪問到了Value
Function Area: 1   <----訪問到了Value
Window Area: 1     <----訪問到了Value

咱們在全局環境定義一個變量 value, 在 A 行和 B 行都能正確訪問到,而且在C 行從 window 中正確得讀取到了值。那麼就能夠說在window對象中變量是全局變量,她做用的做用區就是 全局做用域。並且咱們都知道,ES6以前聲明變量只有用 var, 因此聲明一個變量就須要 var name=xxx。但若是咱們建立一個變量沒有用 var 會發生什麼事情,經過改寫剛纔的代碼咱們來作個試驗:spa

var originValue = 1;
newValue = 10086;
console.log(`Window Area 'Origin': ${originValue}`);   //A
console.log(`Window Area 'New': ${newValue}`);   //B

function showValue() {
    console.log(`Function Area 'Origin': ${originValue}`);  //C
    console.log(`Function Area 'New': ${newValue}`);  //D
}

showValue();

經過運行看一下結果:3d

Debugger listening on ws://127.0.0.1:28526/d0af124a-5020-4211-b186-bbd80e0d1403
Debugger attached.
Window Area 'Origin': 1
Window Area 'New': 10086
Function Area 'Origin': 1
Function Area 'New': 10086

emmmm...好像沒有什麼不一樣。等等....先放下手裏40米的大刀,我沒有在忽悠在讀的朋友,請接着往下看。code

函數做用域

看名字你們就能猜到函數做用域其實就是變量只在一個函數體中起做用,沒毛病。可是有個例外,那就是閉包。固然閉包不是本次的討論內容,固然會在下一篇單獨拿出來和你們一塊兒坍探討。言歸正傳,咱們繼續來看函數做用域。對象

首先咱們來看一個例子:ip

function showValue() {
    var innerValue = 10086;
    console.log(`Inner Value: ${innerValue}`);  //A
}

showValue();

console.log(`Access Inner Value: ${innerValue}`);  //B

看下運行結果,果不其然出錯了:

Debugger listening on ws://127.0.0.1:15677/f3bc723c-4354-4416-87f0-25c7b9df6b64
Debugger attached.
Inner Value: 10086
ReferenceError: innerValue is not defined

在函數中能正常的訪問 innerValue 這個變量,而到了函數外面就訪問不到,顯示 innerValue is not defined 因此這就是函數做用域的表現形式。

但再若是,咱們在函數中的建立的變量沒有使用 var,會發生什麼呢:

function showValue() {
    innerValue = 10086;
    console.log(`Inner Value: ${innerValue}`);  //A
}

showValue();

console.log(`Access Inner Value: ${innerValue}`);  //B
console.log(`Access Inner Value: ${window.innerValue}`);  //C

重點看 C 行,咱們從 window 對象中訪問這個變量,看運行後瀏覽器控制檯打印結果:

Inner Value: 10086
Access Inner Value: 10086
Access Inner Value: 10086

此時很奇怪的事情發生了,只要去掉一個 var,運行結果就大相徑庭了,並且咱們還能夠在window對象中獲取到這個變量。因此咱們能夠獲得這樣一個結論:在函數體中建立變量而不用 var 這個關鍵字聲明,那麼就默認把這個變量放到 window 中,也就是做爲一個全局變量,那麼既然是全局變量那麼其做用域也就是全局的了。因此這個例子就告訴咱們,在函數中建立一個變量,必定要帶上關鍵字( var,固然ES6還給咱們提供了letconst), 初非有特殊需求,否則很容易引起各類奇怪的八阿哥。

塊做用域

所謂的 塊做用域 就是 某個變量只會在某一段代碼中有效,一旦超出這個塊那就會失效,也就是說會被當作垃圾回收了。嚴格來講ES6以前並無 塊做用域 ,可是能夠藉助 當即執行函數 人爲實現, 原理就是當一個函數執行完之後其中的全部變量會被垃圾回收機制給回收掉 ( 可是也有例外,那就是閉包 )。
當即執行函數的形式很簡單 (Function)(arguments),來一段代碼樂呵樂呵吧:

var value = 10086;

//------------------Start---------
(function () {
    var newValue = 10001;
    value += newValue;
})()
//------------------End-----------

console.log(`value: ${value}`);
console.log(`newValue: ${newValue}`);

咱們在全局環境中定義一個變量 value, 而後又在當即執行函數中定義了一個變量 newValue,將這個變量與 value 相加並從新賦值給 value 變量。運行結果以下:

Debugger listening on ws://127.0.0.1:45745/9cbc93f9-a6f0-4d31-899f-70767afcd305
Debugger attached.
value: 20087
ReferenceError: newValue is not defined

並無如預期那樣讀取到 newValue 變量,緣由就是她已經被回收掉了。

可是ES6對此進行了改進,只要使用 花括號{} 就能夠實現一個塊做用域。咱們來改寫下前一段代碼:

let value = 10086;

{
    let newValue = 10001;
    value += newValue;
}

console.log(`value: ${value}`);
console.log(`newValue: ${newValue}`);

首先你們都能注意到咱們使用 let 這個關鍵詞聲明瞭變量,再看運行結果:

Debugger listening on ws://127.0.0.1:44728/a37871fd-4088-4910-8b32-6f48ce78b6e6
Debugger attached.
value: 20087
ReferenceError: newValue is not defined

與前者相同。因此筆者在這裏建議,開發過程當中應該儘可能使用 let 或者 const,這樣對本身建立的變量有更好的控制。而不至於出現 做用域控制失穩(筆者意淫出來的形容詞) 或者 變量覆蓋

因此接下來來具體演示這兩個問題:

做用域控制失穩

代碼:

var functionList = [];
for (var index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

這個例子還算比較經典的例子,不理解做用域的朋友可能會認爲數組中的第一個函數打印 1, 第二個函數打印 2, 第三個函數打印 3。下面咱們來看下運行結果:

Debugger listening on ws://127.0.0.1:6247/d2d6f0d0-d094-4cfa-9653-b8525b43b7c0
Debugger attached.
4
4
4

打印出三個 4。這是由於var出來的變量不具備局部做用的能力,所以即便在每一次循環時候把變量 index 傳給 函數,可是本質上每個函數內部還是index而不是每一次循環對應的數字。上面的代碼等價於:

var functionList = [];
var index;
for (index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

console.log(`index: ${index}`)

看下運行結果:

Debugger listening on ws://127.0.0.1:28208/a38766b5-6baf-4341-822e-2ebefa5e8ac6
Debugger attached.
4
4
4
index: 4

因此能夠意識到,index 變量已經進入了全局變量中,因此每個函數打印的是 循環後的index

固然有兩種改寫方式來實現咱們預想的結果,第一種是使用 當即執行函數 ,第二章是 let 關鍵字。下面來各自實現一下:

當即執行函數
var functionList = [];
for (var index = 1; index < 4; index++) {
    (function (index) {
        functionList.push(function () {
            console.log(index)
        })
    })(index)
};

functionList.forEach(function (func) {
    func()
});

運行結果:

Debugger listening on ws://127.0.0.1:49005/030eb056-d268-4244-a01e-1c0cf3deca24
Debugger attached.
1
2
3
let 關鍵字
var functionList = [];
for (let index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

運行結果:

Debugger listening on ws://127.0.0.1:44616/7a55c820-0524-4493-85ef-9ac413996418
Debugger attached.
1
2
3

上面兩種寫法的原理很簡單,就是 把每一次循環的index做用域控制在當前循環中 。因此不少狀況下, ES6真是友好得不得了。建議你們能夠學一下ES6。

變量覆蓋

var聲明變量時候不會去檢查有沒有重名的變量名,例如:

var origin = 'Hello World';
var origin = 'second Hello World';
console.log(origin);

運行結果:

Debugger listening on ws://127.0.0.1:24251/3a808b2e-c3f9-410c-b216-e4f6cba7046f
Debugger attached.
second Hello World

看似很日常的表現,可是若是在項目工程中覆蓋了某個已經在以前聲明的變量,那麼後果是沒法預計的。那 let ( const也同樣 ) 聲明一個變量有什麼好處呢?改一下代碼:

let origin = 'Hello World';
let origin = 'second Hello World';
console.log(origin);

const ORIGIN = 'Hello World';
const ORIGIN = 'second Hello World';
console.log(ORIGIN);

而後,運行就報了一個錯誤:

SyntaxError: Identifier 'origin' has already been declared

SyntaxError: Identifier 'ORIGIN' has already been declared

說明用 let 或者 const 關鍵字聲明變量會預先檢查是否有重名變量,若是存在的話會給出錯誤。神器啊...

做用域鏈

所謂的 做用域鏈,筆者的理解就是 訪問某個變量所經歷的維度造成的鏈式路徑。可能有誤或者不專業,望朋友們多多海涵哈哈... 千言萬語不敵一段代碼,下面直接上代碼吧:

var origin = 'Hello World';

function first(origin) {
    second(origin);
}

function second(origin) {
    third(origin);
}

function third(origin) {
    console.log(origin)
}

first(origin);

運行後會如預期同樣打印:

Debugger listening on ws://127.0.0.1:29015/4092f9c8-d65d-4b91-ab95-e3ba99ef1860
Debugger attached.
Hello World

由於讀取某個變量會首先檢查該函數體中有沒有 origin,若是沒有的話會一直循着調用棧一直往上找,若是到 window 還沒找到的話會拋出:

ReferenceError: origin is not defined

若是仍有疑問可直接看圖:

clipboard.png

但若是咱們在 second方法 中再定義一個 origin變量會怎麼樣?

var origin = 'Hello World';

function first(origin) {
    second(origin);
}

function second(origin) {
    var origin = 'second Hello World';
    third(origin);
}

function third(origin) {
    console.log(origin)
}

first(origin);

看運行結果:

Debugger listening on ws://127.0.0.1:15222/ee92e38f-833e-4983-8765-9514495c2bc5
Debugger attached.
second Hello World

此時打印的字符是在second中定義的字符,因此咱們能夠猜到 讀取變量只要讀取到對應的變量名就會中止查找,不會繼續向上找

clipboard.png

簡單得介紹完做用域鏈後,本篇博客也結束了。也是筆者目前寫得最長的一篇。爲了犒勞本身,今晚吃什麼呢?哈哈...

相關文章
相關標籤/搜索