在JavaScript中,Scope(做用域)是一個很是重要的概念,對不少剛接觸JavaScript的開發者來講,這個概念理解起來並不容易。本文的目的就是對JavaScript Scope的知識點作一次梳理,但願經過本文能幫助你更好的理解Scope。bash
在JavaScript中,Scope通常譯爲做用域,能夠通俗易懂的理解爲:做用域限定了在某個特定範圍內能夠獲得的變量、函數和對象等資源。這裏注意幾個關鍵點,首先做用域起着限定做用,其次它限定在某個特定範圍,超過這個範圍是不被容許的,最後它規定了能夠獲得什麼,能夠獲得包括變量、函數聲明和對象等。Scope能夠分爲Global Scope(全局做用域)和 Local 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中,這個變量能夠稱爲局部變量。ip
Local Scope又包括Function Scope(函數做用域)和Block 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中,子做用域能夠獲取父級做用域中的變量和函數聲明,反之則不行。
在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
或者let
和const
聲明,在非嚴格模式(use strict
)下,變量將提高爲全局變量。
// global scope
function showName() {
console.log(name)
name = '老王'
console.log(name)
}
showName() // 老王, 老王
console.log(name) // 老王
name = '老李'
showName() // 老李, 老王
console.log(name) // 老李
複製代碼
這樣name
變量將能夠被隨意修改,可能會產品意想不到的bug。
在ES6出現以前,JavaScript並無嚴格意義上的Block Scope,這讓不少有其餘語言開發經驗的開發者感到很困惑。除了不推薦使用的eval
和with
,只有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開發者帶來了全新的變量聲明方式,let
和const
,其中let
和var
同樣,都是用來聲明變量,而const
則是用來聲明常亮,顧名思義,其值是不可改變的。這裏要重點說明的是,let
和const
是做用於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
是沒法獲取name2
和name3
的,這就是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。這說明了var
和let
是的不一樣的,var
聲明的變量會提高,是做用於Global Scope或Function Scope中的,在代碼片斷一的setTimeout
開始執行的時候for
循環已經運行結束了,這個時候i
的值是10,因此打印出了10個10。而let
是做用於Block Scope的,每次循環聲明的i
只在當前的Block Scope中有效,因此每次打印出的i
都不相同。
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是做用於代碼執行階段,關心的是函數在哪被調用。
var
定義的變量做用於Function Scope,而且會出現Hoisting
現象。let
和const
定義的變量做用於Block Scope,在Block Scope以外沒法引用。理解Scope的相關概念對學好JavaScript很是重要,但願經過以上的梳理,能讓你對JavaScript Scope有更加清晰的認識。若是有什麼解釋的不清楚的或者理解有誤的地方,歡迎留言交流。