首頁 > 軟體

js作用域及作用域鏈工作引擎

2022-07-06 14:01:58

前言

我們需要先知道的是引擎,引擎的工作簡單粗暴,就是負責javascript從頭到尾程式碼的執行。引擎的一個好朋友是編譯器,主要負責程式碼的分析和編譯等;引擎的另一個好朋友就是今天的主角--作用域。那麼作用域用來幹什麼呢?作用域鏈跟作用域又有什麼關係呢?

一、作用域(scope)

作用域的定義:作用域是在執行時程式碼中的某些特定部分中變數,函數和物件的可存取性。

1.作用域的分類

  • 全域性作用域
var name="global";
function foo(){
    console.log(name);
}
foo();//global

這裡函數foo()內部並沒有宣告name變數,但是依然列印了name的值,說明函數內部可以存取到全域性作用域,讀取name變數。再來一個例子:

hobby='music';
function foo(){
    hobby='book';
    console.log(hobby);
}
foo();//book

這裡全域性作用域和函數foo()內部都沒有宣告hobby這個變數,為什麼不會報錯呢?這是因為hobby='music';寫在了全域性作用域,就算沒有var,let,const的宣告,也會被掛在window物件上,所以函數foo()不僅可以讀取,還可以修改值。也就是說hobby='music';等價於window.hobby='music';。

2.函數體作用域

函數體的作用域是通過隱藏內部實現的。換句話說,就是我們常說的,內層作用域可以存取外層作用域,但是外層作用域不能存取內層。原因,說到作用域鏈的時候就迎刃而解了。

function foo(){
    var age=19;
    console.log(age);
}
console.log(age);//ReferenceError:age is not defined

很明顯,全域性作用域下並沒有age變數,但是函數foo()內部有,但是外部存取不到,自然而然就會報錯了,而函數foo()沒有呼叫,也就不會執行。

3.塊級作用域

塊級作用域更是見怪不怪,像我們接觸的let作用域,程式碼塊{},for迴圈用let時的作用域,if,while,switch等等。然而,更深刻理解塊級作用域的前提是,我們需要先認識認識這幾個名詞:

--識別符號:能在作用域生效的變數。函數的引數,變數,函數名。需要格外注意的是:函數體內部的識別符號外部存取不到。

--函數宣告:function 函數名(){}

--函數表示式: var 函數名=function(){}

--自執行函數: (function 函數名(){})();自執行函數前面的語句必須有分號,通常用於隱藏作用域。

接下來我們就用一個例子,一口氣展示完吧

function foo(sex){
    console.log(sex);
}
var f=function(){
    console.log('hello');
}
var height=180;
(
    function fn(){
        console.log(height);
    }
)();
foo('female');
//依次列印:
//180
//female
//hello

分析一下:識別符號:foo,sex,height,fn;函數宣告:function foo(sex){};函數表示式:var f=function(){};自執行函數:(function fn(){})();需要注意,自執行函數fn()前面的var height=180;語句,分號不能拋棄。否則,你可以試一下。

二、預編譯

說好只是作用域和作用域鏈的,但是考慮到理解作用域鏈的必要性,這裡還是先聊聊預編譯吧。先討論預編譯在不同環境發生的情況下,是如何進行預編譯的。

  • 發生在程式碼執行之前

(1)宣告提升

console.log(b);
var b=123;//undefined

這裡列印undefined,這不是報錯,與Refference:b is not defined不同。這是程式碼執行之前,預編譯的結果,等同於以下程式碼:

var b;//宣告提升
console.log(b);//undefined
b=123;

(2)函數宣告整體提升

test();//hello123  呼叫函數前並沒有宣告,但是任然列印,是因為函數宣告整體提升了
function test(){
    var a=123;
    console.log('hello'+a);
}

2.發生在函數執行之前

理解這個只需要掌握四部曲:

(1)建立一個AO(Activation Object)

(2)找形參和變數宣告,然後將形參和變數宣告作為AO的屬性名,屬性值為undefined

(3)將實參和形參統一

(4)在函數體內找函數宣告,將函數名作為AO物件的屬性名,屬性值予函數體 那麼接下來就放大招了:

var global='window';
function foo(name,sex){
    console.log(name);
    function name(){};
    console.log(name);
    var nums=123;
    function nums(){};
    console.log(nums);
    var fn=function(){};
    console.log(fn);
}
foo('html');

這裡的結果是什麼呢?分析如下:

//從上到下
//1、建立一個AO(Activation Object)
AO:{
    //2、找形參和變數宣告,然後將形參和變數宣告作為AO的屬性名,屬性值為undefined
    name:undefined,
    sex:undefined,
    nums=undefined,
    fn:undefined,
    //3、將實參和形參統一
    name:html,
    sex:undefined,
    nums=123,
    fn:function(){},
    //4、在函數體內找函數宣告,將函數名作為AO物件的屬性名,屬性值予函數體
    name:function(){},
    sex:undefined,
    fn:function(){},
    nums:123//這裡不僅存在nums變數宣告,也存在nums函數宣告,但是取前者的值
    以上步驟得到的值,會按照後面步驟得到的值覆蓋前面步驟得到的值
}
//依次列印
//[Function: name]
//[Function: name]
//123
//[Function: fn]

3.發生在全域性(內層作用域可以存取外層作用域)

同發生在函數執行前一樣,發生在全域性的預編譯也有自己的三部曲:

(1)建立GO(Global Object)物件

(2)找全域性變數宣告,將變數宣告作為GO的屬性名,屬性值為undefined

(3)在全域性找函數宣告,將函數名作為GO物件的屬性名,屬性值賦予函數體

舉個栗子:

var global='window';
function foo(a){
    console.log(a);
    console.log(global);
    var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);

這個例子比較簡單,一樣的步驟和思路,就不在贅述分析了,相信你已經會了。列印結果依次是:

[Function: fn]
123
window
ReferenceError: b is not defined

好啦,進入正軌,我們接著說作用域鏈。

三、作用域鏈

作用域鏈就可以幫我們找到,為什麼內層可以存取到外層,而外層存取不到內層?但是同樣的,在認識作用域鏈之前,我們需要見識見識一些更加晦澀抽象的名詞。

執行期上下文:當函數執行的時候,會建立一個稱為執行期上下文的物件(AO物件),一個執行期上下文定義了一個函數執行時的環境。 函數每次執行時,對應的執行上下文都是獨一無二的,所以多次呼叫一個函數會導致建立多個執行期上下文,當函數執行完畢,它所產生的執行期上下文會被銷燬。

查詢變數:從作用域鏈的頂端依次往下查詢。

 [[scope]]:作用域屬性,也稱為隱式屬性,僅支援引擎自己存取。函數作用域,是不可存取的,其中儲存了執行期上下文的結合。

我們先看一眼函數的自帶屬性:

function test(){//函數被建立的那一刻,就攜帶name,prototype屬性
      console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);存取不到,作用域屬性,也稱為隱式屬性
// test() --->AO:{}執行完畢會回收
// test() --->AO:{}執行完畢會回收

接下來看看作用域鏈怎麼實現的:

var global='window';
function foo(){
    function fn(){
        var fn=222;
    }
    var foo=111;
    console.log(foo);
}
foo();

分析:

GO:{
    foo:function(){}
}
fooAO:{
    foo:111,
    fn:function(){}
}
fnAO:{
    fn:222
}
// foo定義時 foo.[[scope]]---->0:GO{}
// foo執行時 foo.[[scope]]---->0:AO{}  1:GO{}  後存取的在前面
//fn定義時 fn.[[scope]]---->0:fnAO{} 1:fooAO{}  2:GO{}
fnAO:fn的AO物件;fooAO:foo的AO物件

綜上而言:作用域鏈就是[[scope]]中所儲存的執行期上下文物件的集合,這個集合呈鏈式連結,我們把這種鏈式連結叫做作用域鏈。

以上就是js作用域及作用域鏈工作引擎的詳細內容,更多關於js作用域作用域鏈的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com