深入理解JS之Scope鏈


JS被很多人認為是『拙劣的語言』,被這門語言里的各種離奇的事情整的團團轉,這篇文章主要來講講JS中的Scope鏈,其主要是影響JS中的變量作用域。

注:本文適合稍有一定JS基礎的同學

目錄:


  1. 初步認識
  2. 預編譯
  3. 不同Scope進行操作
  4. Scope鏈
  5. 例題
  6. this變量
  7. new操作符

初步認識

首先,來看一段代碼:

var a = 1;

if(true){
    var b = 1;
}

console.log(b) //1

我們從最基本的開始,在面向對象的強語言中(Java,C……),其作用域都是基於塊的(即:{}),塊內可以對塊外的變量進行操作,但是塊外卻對塊內的變量是無法操作的。但是JS呢?一門弱語言,其並沒有實現基於塊的作用域,而是基於function的,因此上面的代碼運行出來的結果b並不是undefined,說明最終a和b是定義在一個Scope內的。

其Scope如圖:

預編譯

var a = 1;
var b = 2;

function doit(){
    console.log(b);
    var b = 3;
    console.log(b);
}

doit();

console.log(b);

//undefined
//3
//2

講到了Scope,不得不講一講js的預編譯,為什么我們得到的第一個log的結果為undefined呢?按照強語言的思路來說這里應該是2才對呀,這就是js的預編譯。js的代碼在首次被加載完成后進行編譯時,會將所有的function和var提前進行聲明,但是並不會對其進行賦值,賦值則都是在該代碼塊進行執行時才會對其進行賦值,那么第一個log則是在預編譯為b進行了聲明后,這時b是沒有時行賦值的,所以會log出undefined。

由於js是基於function來創建Scope的,所以只有doit執行時才會創建新的Scope,其Scope如圖:

如果不對變量進行var的話,它是不會存在於function執行的時創建的新Scope的。

不同Scope進行操作

var a = 1;
var b = 2;

function doit(){
    var b = 3;
    console.log(a);
}

doit();

在doit的function里,加了一句console.log(a),這里就有個問題了,這里的a是從哪里來呢?

這里的doit funciton在執行的時候創建了一個新的Scope,如上一個例子講的,但是這個Scope里只創建了b變量,去哪找a呢?我們把上面這個換一換,才是真正正確的Scope:

在js中,var和function在預編譯中,作用都是一樣的,都是提前聲明了變量,但是並沒有對其進行賦值,所以這段代碼完整的Scope應該是擁有這三個變量的,只是doit是一個指向堆的引用。而在doit執行時,才會創建新的Scope,由於js語言的特殊性,雖然doit在這個Scope里定義的,但是其執行環境可以通過引用改變到任何地方,但是doit這個函數的定義環境永遠都是確定的,即這個Scope內。

我們使用__proto__的思想去理解Scope鏈,當函數執行時,會在新的Scope內創建一個引用(我們假設它為__parent__),而這個引用指向則是在function定義時的Scope,在進行變量查找時,則會先在自身的Scope內進行查找,如果沒有找到變量,則會根據__parent__來查找到定義時的Scope,在該Scope里進行變量的查找,如圖:

Scope鏈

就像上圖那樣,每個Scope都擁有一個__parent__,所有即使這個function無論在什么環境中進行執行,其父Scope都是這個function創建的Scope,雖然js很亂,但是是亂得有規有矩。

當代碼在瀏覽器中執行,去查找變量時,往往都是如下圖過程:

優先查找自己Scope,如果查找不到則根據Scope鏈去查找最近的同名變量,如果一直查找到了Top Scope(在瀏覽器中則是window)還未找到的話,則這個變量會被認為_"Uncaught ReferenceError: **** is not defined"_,如果直接使用的話則會報錯。

我們為什么可以在代碼中的任何地方使用document,location等太多變量,都是一直通過Scope鏈查找到了Top Scope從window中取得的。

例題

拿來最基礎的前端面試題來進行分析:

<button></button>
<button></button>
<button></button>
<button></button>
<button></button>
<button></button>

<script>
var buttons = document.getElementsByTagName("button");

for(var i = 0, l = buttons.length; i < l; i++){
    buttons[i].onclick = function(){
        alert(i);
    }
}
</script>

很多人知道這個例子最終結果是什么樣的,即點擊每個button則都alert(6),並沒有達到我們預想中的結果,但是大部分人並不能對這個問題說出個所以然,只能說到“最后不就是加到6了嘛”。我們從Scope鏈來分析這個例子,這里遍歷了6次,定義了6個匿名function,並將其賦值於了不同按鈕的onclick事件,而這6個匿名function的定義Scope都是相同的,當用戶進行點擊時,會執行對應的一個匿名function,該function創建的Scope中並沒有i這個變量,所以它會根據__parent__來找到定義這個function的Scope,找到了i,但是這個i的這時值為6,並且這六個function都是找到了這個值為6的i,所以點擊它們都會相同地彈出i這個值。

正確的寫法有很多,但是思路只有一個,那就是改變匿名函數的創建Scope,並且該Scope又與i存在的Scope不同,這就是大家說的閉包,其實閉包就是Scope,每個函數都會創建Scope,創建閉包,下面兩種寫法都是改變了匿名函數的創建Scope,並在該Scope中保存了獨一無二的index值。

寫法1:

for(var i = 0, l = buttons.length; i < l; i++){
    (function(index)
        buttons[index].onclick = function(){
            alert(index);
        }
    )(i);
}

寫法2:

for(var i = 0, l = buttons.length; i < l; i++){
    buttons[i].onclick = (function(index){
        return function(){
            alert(index);
        }
    })(i);
}

this變量

this作為js中最為靈活的變量,也是弄暈了一批一批青年們。開始之前,我們先來上一段代碼:

var a = 1;
function doit(){
    var b = 2;
    
    return function(){
        var c = 3;
        console.log(this);
    }
}

doit()();

那么問題來了,這里的this會和a,b,c中的哪一個變量有關系呢?

先思考一段時間

···
···
···
···

答案是a,通過this.a可以獲取到a的值,即:1。

對於this,我們可以理解為:特殊的Scope引用變量,其指向當前函數的執行環境Scope(並不是定義時的Scope


我們用上面的例子來理解,雖然this是寫在最里面的function的,但是這個function的最終執行是在最外面的Scope進行執行的,所以this指向的是最外層的Scope,而a是定義在最外層的Scope中的,則這時我們可以使用this.a來獲取到a的值。

在使用this時:明確了this指向的Scope再使用this,由於js的對引用並沒有限制,所以這個函數的執行環境永遠是不確定的,所以this去對應的Scope中取值時是不一定能取得到的。

callapply則可以去改變函數執行的Scope,從而改變this的指向,對於這兩個方法的使用,這里不再詳解。

new操作符

js作為一個弱語言,在ES6之前並沒有class之說,現在所有瀏覽器都不直接支持ES6(除非手動打開),但是我們想要實現對類的創建該怎么做呢?乒乒乓乓來段代碼:

function Person(name,sex,age){
    this.name = name;
    this.sex = sex;

    var Age = age;
}

var man = new Person("Bob", "male", "17");

在這個new的過程中,它到底做了什么呢?我們來按步分析一下:

1.創建Object Scope

創建一個空的Scope

2.在該Object Scope下執行

Person("Bob", "male", "17");

則這時Person里的this是指向這個Object Scope,所以this.namethis.sex則是為Object Scope賦值了新的變量和值。

3.得到:

而Age去哪了呢?根據上面Scope的知道,Age則是被創建在Person自身的Scope內,並非Object Scope,這時Person函數創建出來的Scope則擁有四個變量,即:name,sex,age,Age;這個Age就像是強語言中的private一樣,外界是無法獲取到的,這樣我們則會生成一個類似於類的實現方法。

所以來說,下面的代碼:

console.log(man.name) //Bob
console.log(man.sex)  //male
console.log(man.Age)  //undefined

我們可以得到name和sex,但是並不能得到Age。

End

對於Scope的介紹結束啦,希望本文能為你更深地理解js起到幫助,BTW,js並不是拙劣的語言,當你真正熟悉了它,你會覺得它如此地好用

Finish.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM