js的作用域和作用域鏈

jiangjie1105發表於2019-09-17

1.什麼是作用域

作用域是你的程式碼在執行時,某些特定部分中的變數,函式,物件的可訪問性,也就是作用域決定了變數和函式的可訪問範圍,即作用域控制者變數與函式的可見性和宣告週期 作用域的主要功能是:

收集並維護所有聲名的識別符號

依照特定的規則對識別符號進行查詢

確定當前的程式碼對識別符號的訪問許可權

function outFun2() {
    var inVariable = "內層變數2";
}
outFun2();//要先執行這個函式,否則根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined

複製程式碼

上面的程式碼中 變數inVariable在全域性作用域中沒有被宣告 所以在全域性作用域下取值會報錯 我們可以這樣理解: 作用域就是一個獨立的地盤 ,讓變數不會外洩 暴漏出去也就是說作用域最大的作用就是隔離變數 ,不同作用域下同名變數不會有衝突

ES6之前js沒有塊級作用域 只有區域性作用域和函式作用域 ES6的出現為我們提供了“塊級作用域

可以通過新增的命令 let 和 const 來實現”

2.全域性作用域和函式作用域

所謂全域性作用域 就是在程式碼中的任何地方都能訪問到的物件擁有全域性作用域 一般來說以下情況具有全域性作用域

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


複製程式碼

從上面程式碼中可以看到globleVariable和globalFunc在任何地方都可以訪問到, 反之不具有全域性作用域特性的變數只能在其作用域內使用。

2.未定義變數直接賦值的變數(由於變數提升使之成為全域性變數)

function func1(){
    special = 'special_variable';
    var normal = 'normal_variable';
}
func1();
console.log(special);    //special_variable
console.log(normal)     // normal is not defined

複製程式碼

雖然可以在全域性作用域中宣告函式以及變數 使之成為全域性變數但是不建議這麼做 因為這可能會導致和其他變數名衝突 一方面如果我們再使用let 和 const聲名變數 當命名發生衝突時會報錯。

// 變數衝突
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.1函式作用域

定在函式中的變數救在函式作用域中 並且函式在每次呼叫時都有一個不同的作用域,這意味著同名變數可以用在不同的函式中。因為這些變數繫結在不同的函式中,擁有不同的作用域,彼此之間不能訪問

functon 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是同級作用域。


2.2在ES6塊級作用域未講解之前注意點

變數提升:

變數提升(hosting) 在程式碼區中任意地方申明變數和在最開始(最上面)申明是一樣的。也就是說,看起來一個變數可以在申明之前被使用 這種行為就是變數提升 看起來就像變數的申明被自動移到了函式或全域性程式碼的最頂上。請看一段程式碼:

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 jiangjiejie';
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';

複製程式碼

可以看到:

  • 函式的聲名被提到了最頂上;
  • 申明只進行了一次 因此後面 var foo ='i am koala';會被忽略
  • 函式申明的優先順序優於變數申明,且函式宣告會連帶定義一起被提升(這裡與變數不同)

2.3塊級作用域

es6新增了let和const命令,可以用來建立塊級作用域變數,使用let命令聲名的變數只在let命令所在的程式碼塊內有效

let 宣告的語法與 var 的語法一致。你基本上可以用 let 來代替 var 進行變數宣告,但會將變數的作用域限制在當前程式碼塊中。塊級作用域有以下幾個特點:

  • 1.變數不會提升到程式碼塊頂部 且不允許從外部訪問塊級作用域內部變數
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的let和const不允許反覆聲名 和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.作用域鏈

3.1 javascript是如何執行的???

js的作用域和作用域鏈

3.1分析階段

JavaScript編譯器編譯完成,生成程式碼後進行分析

  • 分析函式引數
  • 分析變數聲名
  • 分析函式聲名

分析階段的核心就是再分析完成後(也就是接下來函式執行階段的瞬間)會建立一個AO(active Object活動物件)

3.1.2執行階段

分析階段成功後,會把ao給執行階段

  • 引擎詢問作用域,作用域中是否有這個叫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上如果有與函式名同名的屬性,則會被此函式覆蓋,但是下面這種情況宣告的函式不會覆蓋AO鏈中同名的屬性

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

複製程式碼

進入執行階段

分析階段分析成功後,會把給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,知道找到最後的全域性作用域,而這一條形成的“ao鏈”就是JavaScript中的作用域鏈

3.3找 過程LHS和RLHS查詢特殊說明

LHS = 變數賦值或寫入記憶體。想象為將文字檔案儲存到硬碟中。 RHS = 變數查詢或從記憶體中讀取。想象為從硬碟開啟文字檔案。

3.3.1LHS和RHS特性

  • 都會在所有作用域中查詢
  • 嚴格模式下,找不到所需的變數時,引擎都會丟擲ReferenceError 異常
  • 非嚴格模式下,LHs會比較特殊,會自動建立一個全域性變數
  • 查詢成功時,如果對變數的值進行不合理的操作,比如:對一個非函式型別的值進行函式呼叫,引擎會丟擲Typeerror異常

3.3.2LHS和RHS舉例說明

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)

複製程式碼

作用域鏈總結

js的作用域和作用域鏈

相關文章