你要了解的 JavaScript —— Scope

在JavaScript中,Scope(做用域)是一個很是重要的概念,對不少剛接觸JavaScript的開發者來講,這個概念理解起來並不容易。本文的目的就是對JavaScript Scope的知識點作一次梳理,但願經過本文能幫助你更好的理解Scope。bash

#Scope究竟是什麼?

在JavaScript中,Scope通常譯爲做用域,能夠通俗易懂的理解爲:做用域限定了在某個特定範圍內能夠獲得的變量、函數和對象等資源。這裏注意幾個關鍵點,首先做用域起着限定做用,其次它限定在某個特定範圍,超過這個範圍是不被容許的,最後它規定了能夠獲得什麼,能夠獲得包括變量、函數聲明和對象等。Scope能夠分爲Global Scope(全局做用域)和 Local Scope(局部做用域)。函數

#Global Scope

若是一個變量定義在任何函數或花括號{}以外,那麼這個變量就處於Global Scope之中,這個變量稱爲全局變量。性能

// global scope

let name = '老王'

function alertName() {
    alert(name)
}

複製代碼

定義在Global Scope中的變量,能夠在代碼中的任何位置使用。儘管這樣看起來很酷,可是仍是不建議這麼作。看下面的代碼。優化

代碼片斷一:ui

// global scope

let name = '老王'

let name = '老李' // Uncaught SyntaxError: Identifier 'name' has already been declared
複製代碼

代碼片斷二:spa

// global scope

var name = '老王'

var name = '老李'

console.log(name) // 老李
複製代碼

代碼片斷一出現了報錯,經過let或者const定義了一個變量,而後再次定義同名變量,則會提示報錯信息,這是由於產生了命名衝突。儘管代碼片斷二沒有出現報錯,可是經過var定義了一個變量,而後再次定義同名變量,則變量的值被覆蓋了。code

事實上,定義在Global Scope中的變量和函數越多,產生命名衝突的風險也就越大。因而可知,定義變量的時候應該儘量的定義在Local Scope中,而非Global Scope中,避免污染全局命名空間。對象

#Local Scope

若是一個變量定義在函數或者花括號{}之中,那麼其只能被一部分特定的代碼所使用,咱們能夠認爲它處於Local Scope中,這個變量能夠稱爲局部變量。ip

Local Scope又包括Function Scope(函數做用域)和Block Scope(塊級做用域)。先來看Function Scope。內存

Function Scope

在很長一段時間裏,在JavaScript中聲明一個變量,只能經過var來聲明,在函數中經過var聲明的變量是做用在Function Scope之中的。

// global scope 

function showName() {
    // function scope
    
    var name = '老王'
    
    if (true) {
        console.log(name)
    }
}

function getName() {
    // function scope
    
    return name
}

console.log(name) // undefined

showName() // 老王

getName() // undefined
複製代碼

經過以上代碼能夠發現,在getName這個函數中,沒有聲明name變量,可是它也沒法獲取showName中的name變量,這是由於在沒有任何關係的函數之間,函數的Function Scope之間是相互隔離的。同時也能夠發如今Global Scope中也是沒法獲取showName中的name變量的,這是由於name變量被限定在Function Scope之中。既然Global Scope沒法獲取Function Scope中的變量,那麼Function Scope可不能夠獲取Global Scope中的變量呢?

// global scope 

function showName() {
    // function scope
    
    if (true) {
        console.log(name)
    }
}

var name = '老王'

console.log(name) // 老王

showName() // 老王
複製代碼

經過以上代碼可知,在JavaScript中,子做用域能夠獲取父級做用域中的變量和函數聲明,反之則不行。

Hoisting(變量提高)

在JavaScript中,有一個特別的現象,在函數開始執行的階段,JavaScript Engine(JavaScript引擎)會將經過var聲明的變量提高到函數的最頂端(其實函數的聲明也會),這就是所謂的Hoisting。

// global scope

name = '老王'
age = 40

var name
var age
複製代碼

以上代碼等價於

// global scope

var name;
var age;

name = '老王';
age = 40;
複製代碼

值得注意的是變量提高只會提高變量的聲明,變量的賦值並不會提高,變量的賦值仍要等到代碼執行到賦值語句的位置纔會賦值。

// function scope
console.log(name) // undefined

name = '老王'

console.log(name) // 老王

var name

console.log(name) // 老王
複製代碼

還有一種狀況是須要避免出現的,那就是若是定義在函數中的變量沒有使用var或者letconst聲明,在非嚴格模式(use strict)下,變量將提高爲全局變量。

// global scope
function showName() {
    console.log(name) 
    
    name = '老王'
    
    console.log(name)  
}

showName() // 老王, 老王

console.log(name) // 老王

name = '老李'

showName() // 老李, 老王

console.log(name) // 老李
複製代碼

這樣name變量將能夠被隨意修改,可能會產品意想不到的bug。

Block Scope

在ES6出現以前,JavaScript並無嚴格意義上的Block Scope,這讓不少有其餘語言開發經驗的開發者感到很困惑。除了不推薦使用的evalwith,只有try/catch中的catch塊擁有Block Scope。

try {
    alert(age)
    
    var name = '老王'
    
} catch(err) {
    console.log(err) // ReferenceError: age is not defined
}

console.log(name) // 老王

console.log(err) // Uncaught ReferenceError: err is not defined
複製代碼

因而可知,err變量只限定在catch的Block Scope之中,而try則沒有本身Block Scope。

ES6的發佈爲廣大JavaScript開發者帶來了全新的變量聲明方式,letconst,其中letvar同樣,都是用來聲明變量,而const則是用來聲明常亮,顧名思義,其值是不可改變的。這裏要重點說明的是,letconst是做用於Block Scope的。

// global scope

function showName() {
    // function scope
    
    var name1 = '老王'
    
    if (true) {
        // block scope
        
        let name2 = '老李'
        
        const name3 = '老趙'
        
        console.log(name3) // 老趙
    }
    
    console.log(name1) // 老王
    
    console.log(name2) // ReferenceError: name2 is not defined
}

showName()
複製代碼

經過以上代碼發現,在Block Scope以外,console.log是沒法獲取name2name3的,這就是Block Scope的限制。

JavaScript是自帶Garbage collection(垃圾回收)機制的,咱們實際開發過程不用特別關心內存回收的問題,由於JavaScript Engine已經幫咱們處理好了。但在開發大型應用的時候,性能問題會逐漸凸顯,關於性能,其實涉及到不少方面,這裏不一一細說,其中有一點是咱們能夠利用Block Scope來提升垃圾回收的效率。

既然在Block Scope以外,代碼是沒法獲取其中的變量的,Garbage collection一旦發現變量或對象沒有被引用,就會將內存及時的回收以備他用。因此利用這個機制,咱們能夠優化部分代碼的書寫方式。

// global scope

var name = '老王'

{
    // block scope
    
    let age = 40
    
    console.log(age) // 40
}

console.log(name) // 老王
複製代碼

經過加上花括號,來建立一個Block Scope,一旦代碼執行結束,Block Scope中的變量將沒法獲取,那麼它就會被及時回收。

接下來咱們來看看實際運用中Function Scope和Block Scope的區別。 代碼片斷一:

// global scope

for(var i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    },100);
}
複製代碼

代碼片斷二:

// global scope

for(let i = 0; i < 10; i++){
    setTimeout(function(){
        console.log(i);
    },100);
}
複製代碼

運行代碼片斷一,將會打印出10個10。運行代碼片斷二,會打印出0、一、二、三、四、五、六、七、八、9。這說明了varlet是的不一樣的,var聲明的變量會提高,是做用於Global Scope或Function Scope中的,在代碼片斷一的setTimeout開始執行的時候for循環已經運行結束了,這個時候i的值是10,因此打印出了10個10。而let是做用於Block Scope的,每次循環聲明的i只在當前的Block Scope中有效,因此每次打印出的i都不相同。

#Lexical Scope(詞法做用域)

Lexical Scope又稱爲Static Scope(靜態做用域),通俗點說就是你在寫代碼的時候,某個變量的做用域就已經肯定好了,你能夠經過直接看代碼就可以分析出變量的值。

// global scope

function getName() {
    // function scope
    
    var name = '老王'
    
    return function () {
        // function scope
        
        console.log(name)
    }
}

let showName = getName()

function getUser() {
    // function scope
    
    var name = '老李'
    showName()
}

console.log(showName()) // 老王

getUser() // 老王
複製代碼

當代碼執行到getName內部的console.log(name)時候,無論外部是如何調用的,都是經過向上查找到var name = '老王'這條聲明賦值語句,從而獲取name變量的值。因此說在函數還未執行以前,就能夠根據Static Scope找到變量對應的值,且這種關係是肯定的,不會發生改變,這就是詞法做用域。

既然有Static Scope,那麼也有Dynamic Scope(動態做用域)。動態做用域意味着在代碼執行階段,變量的取值是會根據做用域的不一樣而發生變化。

// global scope
function user1() {
    // function scope
    
    var name = '老王'
    
    getName()
}

function user2() {
    // function scope
    
    var name = '老李'
    
    getName()
}

function getName() {
    console.log(name)
}

user1() // undefined

user2() // undefined
複製代碼

看以上代碼,getName中並無聲明name變量,因此執行結果都爲undefined。若是JavaScript支持動態做用域,那麼user1()的執行結果將是老王user2()的執行結果將是老李。事實上,JavaScript並無Dynamic Scope,它只有Lexical Scope,它們的本質區別是,Lexical Scope是做用於代碼編寫階段,關心的是函數在哪聲明;而Dynamic Scope是做用於代碼執行階段,關心的是函數在哪被調用。

#總結

  1. 做用域能夠理解爲限定了在某個特定範圍內能夠得到的變量、函數和對象等資源。
  2. 定義在Global Scope中的變量,能夠在代碼任何位置引用,不過最好不要在Global Scope中定義太多的全局變量。
  3. var定義的變量做用於Function Scope,而且會出現Hoisting現象。
  4. letconst定義的變量做用於Block Scope,在Block Scope以外沒法引用。
  5. 子做用域能夠獲取父級做用域中的變量和函數,反之則不行。
  6. JavaScript只有Lexical Scope沒有Dynamic Scope,Lexical Scope在函數編寫的階段就已經肯定好了。

理解Scope的相關概念對學好JavaScript很是重要,但願經過以上的梳理,能讓你對JavaScript Scope有更加清晰的認識。若是有什麼解釋的不清楚的或者理解有誤的地方,歡迎留言交流。

相關文章
相關標籤/搜索