深刻理解JS之Scope鏈

JS被不少人認爲是『拙劣的語言』,被這門語言裏的各類離奇的事情整的團團轉,這篇文章主要來說講JS中的Scope鏈,其主要是影響JS中的變量做用域。javascript

注:本文適合稍有必定JS基礎的同窗html

目錄:


  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內的。java

其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的。app

不一樣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:this

在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.

相關文章
相關標籤/搜索