什麼是作用域?
作用域定義了變量的可見性或可訪問性。大白話來説,就是一個變量能不能被訪問或引用,是由它的作用域決定的。
在 JavaScript 中有三種作用域。
- 全局作用域
- 函數作用域(局部作用域)
- 塊作用域
let globalVariable = "我是全局作用域下的變量"
function func() {
let localVariable = "我是局部作用域下的變量"
}
if (true) {
let blockVariable = "我是塊作用域下的變量"
}
全局作用域 Global Scope
一個在最外層定義的變量便處於全局作用域,全局作用域內的變量可以在程序的任意地方訪問。
var globalVariable = "全局作用域變量"
function func() {
// 在函數內訪問全局作用域的變量
console.log("函數內訪問:", globalVariable)
}
func()
console.log("函數外訪問:", globalVariable)
輸出:
函數內訪問: 全局作用域變量
函數外訪問: 全局作用域變量
使用 var 關鍵字 在大括號內(包括純粹的大括號、if、while、for)定義的變量仍然`屬於全局作用域。
if (true) {
var globalVariable = "全局作用域變量"
}
console.log("外部訪問:", globalVariable)
輸出:
外部訪問: 全局作用域變量
函數作用域(局部作用域) Function Scope(Local Scope)
在函數內定義的變量則屬於函數作用域,又稱局部作用域。局部作用域內的變量只能在自身作用域內被訪問。
function func(params) {
var localVariable = "局部作用域變量"
console.log("函數內訪問:", localVariable)
}
func()
console.log("外部訪問:", localVariable) // Uncaught ReferenceError: localVariable is not defined
輸出:
函數內訪問: 局部作用域變量
Uncaught ReferenceError: localVariable is not defined
例子中,我們嘗試在外部訪問局部作用域中定義的變量,報了 變量未定義 的錯誤。
塊作用域 Block Scope
ES6 中引入了 let 與 const,與 var 不同的是。之前的例子中,在大括號(包括純粹的大括號、if、while、for)間用 var 定義的變量處在全局作用域。如果我們用 let 與 const 在大括號中定義,變量將處於塊作用域。
塊作用域內的變量只能在自身作用域內被訪問。
let 與 const 的不同點在於, const 定義的是一個常量,無法修改定義後的值。
{
let blockVariable = "塊作用域變量"
console.log("塊內訪問:", blockVariable)
}
console.log("外部訪問:", blockVariable) // Uncaught ReferenceError: blockVariable is not defined
輸出:
塊內訪問: 塊作用域變量
Uncaught ReferenceError: blockVariable is not defined
例子中,我們嘗試在外部訪問塊作用域中定義的變量,報了 變量未定義 的錯誤。
什麼是作用域鏈? Scope Chain
當一個變量在當前作用域無法找到時,便會嘗試尋找其外層的作用域,如果還找不到,再繼續往外尋找(只會往外尋找,不會尋找兄弟作用域,更不會往內尋找)。這種如同鏈條一樣的尋找規則便被稱為作用域鏈。
let variable1 = "我是變量 1,外部的"
let variable2 = "我是變量 2"
function func() {
let variable1 = "我是變量 1,內部的"
{
let variable3 = "我是變量 3"
}
{
// 往外尋找,在上一層函數內找到了
console.log(variable1)
// 往外尋找,直到全局作用域
console.log(variable2)
// 找不到,報錯
console.log(variable3) // Uncaught ReferenceError: variable3 is not defined
}
}
func()
輸出:
我是變量 1,內部的
我是變量 2
Uncaught ReferenceError: variable3 is not defined
在例子中,打印 variable1 變量時,由於在上層作用域也就是函數中就找到了 variable1 變量,便停止了尋找,不會找到全局作用域下的 variable1 變量。
尋找 variable2 變量時,在上層作用域中未找到,便一直找到了上上層作用域,也就是全局作用域下的 variable2 變量。
尋找 variable3 變量時,由於 variable3 變量被定義在兄弟作用域中,並不會被尋找到,因為作用域鏈的規則是隻會往上層作用域尋找,並不會尋找兄弟作用域。因此這裏報了變量未定義的錯誤。
函數的作用域是它定義時的作用域,而不是調用時
function func() {
let variable = "我是 func 內的變量"
function func2() {
console.log(variable)
}
return func2
}
{
let variable = "我是大括號內的變量"
let func2 = func()
func2()
}
輸出:
我是 func 內的變量
在例子中,執行 func2 函數時往上尋找的作用域是在 func2 定義時的作用域,而不是調用時的作用域。
如果找不到變量會怎樣?
如果一個變量直到全局作用域也找不到便會執行以下操作。
- 非嚴格模式:隱式聲明全局變量
- 嚴格模式:報錯
非嚴格模式
非嚴格模式下,嘗試賦值一個變量時,如果找不到則會隱性聲明成全局域的變量。
{
variable = "我是一個隱性聲明的變量"
}
console.log(variable)
輸出:
我是一個隱性聲明的變量
以上的例子中,variable 由於未聲明,因此被隱性聲明成了全局作用域下的變量,這使得在最外部也能打印出 variable 變量的值。
非嚴格模式下,嘗試使用一個變量的值時,如果找不到同樣會報錯。
{
console.log(variable) // Uncaught ReferenceError: variable is not defined
}
輸出:
Uncaught ReferenceError: variable is not defined
以上的例子中,由於使用 variable 時未定義,因此報了未定義的錯誤。
嚴格模式
加入 "use strict" 表明是嚴格模式,嚴格模式下不論賦值還是使用未事先聲明的變量都會報錯。
"use strict"
{
variable = "我是一個隱性聲明的變量" // Uncaught ReferenceError: variable is not defined
}
輸出:
Uncaught ReferenceError: variable is not defined
作用域的好處?
防止命名衝突:你寫了一萬行的代碼文件,如果沒有作用域,你要給每個變量取獨一無二的名字,屁股想想也知道是種折磨。安全性: 變量不會被外部訪問,保證了變量值不會被隨意修改。你定義在函數內的變量,如果能在幾千行之後不小心被修改,腳趾頭想想也知道是種折磨。更高級的語法:封裝、面向對象等的實現離不開對變量的隔離,這是依靠作用域所達到的。
説人話!
寫代碼時不用區分它什麼全局使用域、局部作用域、塊作用域啥的概念。只用記得大括號就是一個作用域,尋找變量永遠是從內往外找。現在我們的編輯器基本都有縮進格式化, 從當前代碼塊的位置一層一層往左,就是它所能引用到的所有變量。
打個比方,就像我們每個家庭就是一個作用域,當我們需要一筆手術費掏不出錢的時候,肯定是先在家裏找,問問父母兄弟姐妹啥的,不會去求助其他陌生的家庭。還沒有的話就往外到熟人關係這個作用域裏問問。還不行就向街道居委會求助。居委會也沒辦法再向國家求援。從最親近的關係找起,一層一層圈子往外,這就是作用域與作用域鏈。
最後強烈建議大家使用 let 命名變量,放棄 var!