深刻理解 JavaScript, 從做用域與做用域鏈開始

16bec383586ebd7e?w=1280&h=545&f=jpeg&s=33822

1. 什麼是做用域

做用域是你的代碼在運行時,某些特定部分中的變量,函數和對象的可訪問性。換句話說,做用域決定了變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期javascript

2. JavaScript中的做用域

在 JavaScript 中有兩種做用域java

  • 全局做用域
  • 局部做用域

若是一個變量在函數外面或者大括號{}外聲明,那麼就定義了一個全局做用域,在ES6以前局部做用域只包含了函數做用域,ES6爲咱們提供的塊級做用域,也屬於局部做用域git

2.1 全局做用域

擁有全局做用域的對象能夠在代碼的任何地方訪問到, 在js中通常有如下幾種情形擁有全局做用域:程序員

  1. 最外層的函數以及最外層變量:
var globleVariable= 'global';  // 最外層變量
function globalFunc(){         // 最外層函數
    var childVariable = 'global_child';  //函數內變量
    function childFunc(){        // 內層函數
        console.log(childVariable);
    }
    console.log(globleVariable)
}
console.log(globleVariable);  // global
globalFunc();                 // global
console.log(childVariable)   // childVariable is not defined
console.log(childFunc)       // childFunc is not defined

從上面代碼中能夠看到globleVariableglobalFunc在任何地方均可以訪問到, 反之不具備全局做用域特性的變量只能在其做用域內使用。es6

  1. 未定義直接賦值的變量(因爲變量提高使之成爲全局變量)
function func1(){
    special = 'special_variable';
    var normal = 'normal_variable';
}
func1();
console.log(special);    //special_variable
console.log(normal)     // normal is not defined

雖然咱們能夠在全局做用域中聲明函數以及變量, 使之成爲全局變量, 可是不建議這麼作,由於這可能會和其餘的變量名衝突,一方面若是咱們再使用const或者let聲明變量, 當命名發生衝突時會報錯。github

// 變量衝突
var globleVariable = "person";
let globleVariable = "animal"; // Error, thing has already been declared

另外一方面若是你使用var申明變量,第二個申明的一樣的變量將覆蓋前面的,這樣會使你的代碼很難調試。面試

var name = 'koala'
var name = 'xiaoxiao'
console.log(name);  // xiaoxiao

2.2 局部做用域

和全局做用於相反,局部做用域通常只能在固定代碼片斷內能夠訪問到。最多見的就是函數做用域koa

2.2.1 函數做用域

定義在函數中的變量就在函數做用域中。而且函數在每次調用時都有一個不一樣的做用域。這意味着同名變量能夠用在不一樣的函數中。由於這些變量綁定在不一樣的函數中,擁有不一樣做用域,彼此之間不能訪問。
//全局做用域
function test(){
    var num = 9;
    // 內部能夠訪問
    console.log("test中:"+num);
}
//test外部不能訪問
console.log("test外部:"+num);

注意點:函數

  • 若是在函數中定義變量時,若是不添加var關鍵字,形成變量提高,這個變量成爲一個全局變量。
function doSomeThing(){
    // 在工做中必定避免這樣寫
    thing = 'writting';
    console.log('內部:'+thing);
}
console.log('外部:'+thing)
  • 任何一對花括號{...}中的語句集都屬於一個塊, 在es6以前,在塊語句中定義的變量將保留在它已經存在的做用域中:
var name = '程序員成長指北';
for(var i=0; i<5; i++){
    console.log(i)
}
console.log('{}外部:'+i);
// 0 1 2 3 4  {}外部:5

咱們能夠看到變量name和變量i是同級做用域。spa

2.2.2 在ES6塊級做用域未講解以前注意點

變量提高

變量提高英文名字hoisting,MDN中對它的解釋是變量申明是在任意代碼執行前處理的,在代碼區中任意地方申明變量和在最開始(最上面)的地方申明是同樣的。也就是說,看起來一個變量能夠在申明以前被使用!這種行爲就是所謂的「hoisting」,也就是變量提高,看起來就像變量的申明被自動移動到了函數或全局代碼的最頂上。
看一段代碼:

var tmp = new Date();
function f() {
    console.log(tmp);
    if(false) {
        var tmp='hello';
    }
}

這道題應該不少小夥伴在面試中遇到過,有人會認爲輸出的是當前日期。可是正確的結果是undefined。這就是因爲變量提高形成的,在這裏申明提高了,定義的內容並不會提高,提高後對應的代碼以下:

var tmp = new Date();
function f() {
    var tmp;
    console.log(tmp);
    if(false) {
        tmp='hello';
    }
}
f();

console在輸出的時候,tmp變量僅僅申明瞭但未定義。因此輸出undefined。雖然可以輸出,可是並不推薦這種寫法推薦的作法是在申明變量的時候,將所用的變量都寫在做用域(全局做用域或函數做用域)的最頂上,這樣代碼看起來就會更清晰,更容易看出來哪一個變量是來自函數做用域的,哪一個又是來自做用域鏈

重複聲明

看一個例子:

// var
var name = 'koloa';
console.log(name); // koala
if(true){
    var name = '程序員成長指北';
    console.log(name); // 程序員成長指北
}
console.log(name); // 程序員成長指北

雖然看起來裏面name申明瞭兩次,但上面說了,js的var變量只有全局做用域和函數做用域兩種,且申明會被提高,所以實際上name只會在最頂上開始的地方申明一次,var name='程序員成長指北'的申明會被忽略,僅用於賦值。也就是說上面的代碼實際上跟下面是一致的。

// var
var name = 'koloa';
    console.log(name); // koala
if(true){
    name = '程序員成長指北';
    console.log(name); // 程序員成長指北
}
console.log(name); // 程序員成長指北
變量和函數同時出現的提高

若是有函數和變量同時聲明瞭,會出現什麼狀況呢?看下面但代碼

console.log(foo);
var foo ='i am koala';
function foo(){}

輸出結果是function foo(){},也就是函數內容

若是是另一種形式呢?

console.log(foo);
var foo ='i am koala';
var foo=function (){}

輸出結果是undefined

對兩種結果進行分析說明:

第一種:函數申明。就是上面第一種,function foo(){}這種形式

另外一種:函數表達式。就是上面第二種,var foo=function(){}這種形式

第二種形式其實就是var變量的聲明定義,所以上面的第二種輸出結果爲undefined應該就能理解了。

而第一種函數申明的形式,在提高的時候,會被整個提高上去,包括函數定義的部分!所以第一種形式跟下面的這種方式是等價的!

var foo=function (){}
console.log(foo);
var foo ='i am koala';

緣由是:

  1. 函數聲明被提高到最頂上;
  2. 申明只進行一次,所以後面var foo='i am koala'的申明會被忽略。
  3. 函數申明的優先級優於變量申明,且函數聲明會連帶定義一塊兒被提高(這裏與變量不一樣)

接下來說,在ES6中引入的塊級做用域以後的事!

2.2.2 塊級做用域

ES6新增了 letconst命令,能夠用來建立塊級做用域變量,使用 let命令聲明的變量只在 let命令所在 代碼塊內有效。

let 聲明的語法與 var 的語法一致。你基本上能夠用 let 來代替 var 進行變量聲明,但會將變量的做用域限制在當前代碼塊中。塊級做用域有如下幾個特色:

  • 變量不會提高到代碼塊頂部且不容許從外部訪問塊級做用域內部變量
console.log(bar);//拋出`ReferenceErro`異常: 某變量 `is not defined`
let bar=2;
for (let i =0; i<10;i++){
    console.log(i)
}
console.log(i);//拋出`ReferenceErro`異常: 某變量 `is not defined`

其實這個特色帶來了許多好處,開發者須要檢查代碼時候,能夠避免在做用域外意外但使用某些變量,並且保證了變量不會被混亂但複用,提高代碼的可維護性。就像代碼中的例子,一個只在for循環內部使用的變量i不會再去污染整個做用域。

  • 不容許反覆聲明

ES6的letconst不容許反覆聲明,與var不一樣

// var
function test(){
    var name = 'koloa';
    var name = '程序員成長指北';
    console.log(name); // 程序員成長指北
}

// let || const
function test2(){
    var name ='koloa';
    let name= '程序員成長指北'; 
    // Uncaught SyntaxError: Identifier 'count' has already been declared
}

看到這裏是否是感受到了塊級做用域的出現仍是頗有必要的。

3. 做用域鏈

在講解做用域鏈以前先說一下,先了解一下 JavaScript是如何執行的?

3.1 JavaScript是如何執行的?

16b94c342168e6da?w=2198&h=1138&f=png&s=308834
JavaScript代碼執行分爲兩個階段:

3.1.1 分析階段

javascript編譯器編譯完成,生成代碼後進行分析

  • 分析函數參數
  • 分析變量聲明
  • 分析函數聲明

分析階段的核心,在分析完成後(也就是接下來函數執行階段的瞬間)會建立一個AO(Active Object 活動對象)

3.1.2 執行階段

分析階段分析成功後,會把給AO(Active Object 活動對象)給執行階段

  • 引擎詢問做用域,做用域中是否有這個叫X的變量
  • 若是做用域有X變量,引擎會使用這個變量
  • 若是做用域中沒有,引擎會繼續尋找(向上層做用域),若是到了最後都沒有找到這個變量,引擎會拋出錯誤。

執行階段的核心就是,具體怎麼,後面會講解LHS查詢RHS查詢

3.1.3 JavaScript執行舉例說明

看一段代碼:

function a(age) {
    console.log(age);
    var age = 20
    console.log(age);
    function age() {
    }
    console.log(age);
}
a(18);
首先進入分析階段

前面已經提到了,函數運行的瞬間,建立一個AO (Active Object 活動對象)

AO = {}

第一步:分析函數參數:

形式參數:AO.age = undefined
實參:AO.age = 18

第二步,分析變量聲明:

// 第3行代碼有var age
// 但此前第一步中已有AO.age = 18, 有同名屬性,不作任何事
即AO.age = 18

第三步,分析函數聲明:

// 第5行代碼有函數age
// 則將function age(){}付給AO.age
AO.age = function age() {}

函數聲明注意點:AO上若是有與函數名同名的屬性,則會被此函數覆蓋。可是一下面這種狀況

var age = function () {
            console.log('25');
        }

聲明的函數並不會覆蓋AO鏈中同名的屬性

進入執行階段

分析階段分析成功後,會把給AO(Active Object 活動對象)給執行階段,引擎會詢問做用域,的過程。因此上面那段代碼AO鏈中最初應該是

AO.age = function age() {}
//以後
AO.age=20
//以後
AO.age=20

因此最後的輸出結果是:

function age(){
    
}
20
20

3.2 做用域鏈概念

看了前面一個完整的javascript函數執行過程,讓咱們來講下做用域鏈的概念吧。JavaScript上每個函數執行時,會先在本身建立的AO上找對應屬性值。若找不到則往父函數的AO上找,再找不到則再上一層的AO,直到找到大boss:window(全局做用域)。 而這一條造成的「AO鏈」 就是JavaScript中的做用域鏈。

3.3 過程LHS和RHS查詢特殊說明

LHS,RHS 這兩個術語就是出如今引擎對變量進行查詢的時候。在《你不知道的Javascript(上)》也有很清楚的描述。在這裏,我想引用freecodecamp 上面的回答來解釋:

LHS = 變量賦值或寫入內存。想象爲將文本文件保存到硬盤中。 RHS = 變量查找或從內存中讀取。想象爲從硬盤打開文本文件。 Learning Javascript, LHS RHS

3.3.1 LHS和RHS特性

  • 都會在全部做用域中查詢
  • 嚴格模式下,找不到所需的變量時,引擎都會拋出ReferenceError異常。
  • 非嚴格模式下,LHR稍微比較特殊: 會自動建立一個全局變量
  • 查詢成功時,若是對變量的值進行不合理的操做,好比:對一個非函數類型的值進行函數調用,引擎會拋出TypeError異常

3.3.2 LHS和RHS舉例說明

例子來自於《你不知道的Javascript(上)》

function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

直接看引擎在做用域這個過程:
LSH(寫入內存):

c=, a=2(隱式變量分配), b=

RHS(讀取內存)

讀foo(2), = a, a ,b
(return a + b 時須要查找a和b)

3.4 做用域鏈總結

最後對做用域鏈作一個總結,引用《你不知道的Javascript(上)》中的一張圖解釋

16b94c2e994197b0?w=1420&h=1552&f=png&s=223558

今天就分享這麼多,若是對分享的內容感興趣,能夠關注公衆號「程序員成長指北」,或者加入技術交流羣,你們一塊兒討論。

文章同步到
程序員成長指北(ID:coder_growth)
做者:koala 一個有趣的人

github博客地址:
https://github.com/koala-codi...

16b8a3d23a52b7d0?w=940&h=400&f=jpeg&s=217901

相關文章
相關標籤/搜索