聲明:本文引用了《你不知道的JavaScript(上卷)》一書作用域篇章的部分代碼示例和文字描述
文章的開始乳愉,我們先提出這么幾個問題:
- 作用域是什么兄淫?
- 作用域的工作模式?
- 作用域有哪幾類蔓姚?
- 什么情況下會產(chǎn)生作用域捕虽?
- 作用域有什么用,或者說如何利用坡脐?
作用域是什么
作用泄私,標識符被訪問或調(diào)用, 域备闲,空間晌端、范圍的意思,如最簡單的代碼 var a = '123'
恬砂,a變量保存在哪里(此處保存在變量環(huán)境
)咧纠?后續(xù)的操作中如何找到它?
這些問題泻骤,需要一套規(guī)則來存儲變量漆羔,以及定義如何查找它們,這套規(guī)則就叫 作用域
狱掂,而完整的查找鏈條則被稱為作用域鏈
關于變量環(huán)境 + 詞法環(huán)境演痒,大家有興趣可以另行查找資料學習
function foo(a) {
var secondName = '鐵錘'
sayHellow()
function sayHellow() {
console.log('hello i am ' + firstName + secondName)
console.log(message) // ReferenceError: message not defined
}
}
var firstName = '李'
foo('鐵錘')
當執(zhí)行sayHello()
中的console.log('hello i am ' + firstName + secondName)
, 需要對 firstName + secondName 兩個變量執(zhí)行RHS
查找(相對的,還有LHS
符欠,區(qū)別是此時變量是賦值操作的目標(LHS)嫡霞,還是被賦值操作使用的值(RHS))
- firstName: sayHello() 中找不到,則往上/往外查找到foo()希柿,也沒找到诊沪,繼續(xù)往上查找养筒,OK,在全局作用域找到了
- secondName: 同上
- message: 同樣的邏輯端姚,直到全局作用域都沒有找到晕粪,拋出
ReferenceError
作用域的工作模式
作用域分兩種工作模型,一種是我們常見的詞法作用域
渐裸,大部分編程語言使用的也是這種(當然包括JavaScript)巫湘,
- 詞法作用域:大部分編程語言使用這種,在詞法分析階段決定昏鹃,因此可以理解為
靜態(tài)作用域
- 動態(tài)作用域:Bash腳本(如git bash)尚氛,Perl中的一些模式(如shell命令),
詞法作用域
說到詞法作用域洞渤,可以先說明一下JavaScript的編譯3個階段
- 分詞/詞法分析:把一行代碼分解成代碼塊(詞法單元)阅嘶,如
var a = 2;
會被分解成var, a, =, 2, ;
- 解析/語法分析:將上一個步驟生成的詞法單元流轉換成一顆逐級嵌套的的樹,也就是
AST
抽象語法樹(Abstract Syntax tree) - 代碼生成:將AST語法樹轉換成一組機器指令:聲明一個叫做a的變量载迄,并為它分配內(nèi)存讯柔,然后把2這個值儲存在a中
詞法作用域是定義在編譯-詞法分析階段的作用域,詞法分析的對象是你的源代碼护昧,也就是說詞法作用域由你寫代碼時將變量和塊作用域寫在哪里來決定的魂迄,因此詞法分析器處理代碼時會保持作用域不變(大部分情況如此,除了eval + with)
function foo(a) {
var b = a * 2
function bar(c) {
comsole.log(a, b, c)
}
bar(b*3)
}
foo(2)
// 1惋耙、全局作用域:一個標識符:foo
// 2捣炬、foo函數(shù)作用域:3個標識符: 形參a, b, bar
// 3、bar函數(shù)作用域:1個標識符:形參c
欺騙詞法(eval +with)
eval
eval函數(shù)可以接收一個字符串參數(shù)怠晴,并把該字符串當做可執(zhí)行代碼進行執(zhí)行遥金,字符串參數(shù)是動態(tài)的,所以其執(zhí)行過程中可做變量聲明蒜田,或者變量修改,因此eval函數(shù)所處的外部函數(shù)的作用域有可能會被修改
function foo(str) {
eval(str)
console.log(msg) // 我在eval函數(shù)中被聲明
}
var msg = '我在全局作用域'
foo('var msg = "我在eval函數(shù)中被聲明"')
注意:在嚴格模式中选泻,eval(...)有自己的作用域冲粤,并不會影響到所處作用域
function foo(str) {
"use strict"
eval(str)
console.log(msg) // ReferenceError: a is not defined
}
foo('var msg = "嘗試修改所處作用域"')
with
with通常被當做重復引用同一個對象的多個屬性的快捷方式,可以不用重復引用對象本身
var obj = {a: 1, b: 2, c: 3}
// 正常情況下挨個屬性賦值
obj.a = 11
obj.b = 12
obj.c = 13
// 使用with
with(obj) {
a = 11
b = 12
c = 13
}
with可以將傳入的對象處理成完全隔離的詞法作用域页眯,而它的屬性則自動處理為定義在該作用域內(nèi)的詞法標識符梯捕,但是這個塊內(nèi)部正常的var 聲明并不會被限制在塊內(nèi)部,而是被添加到with執(zhí)行時所處的作用域中
function foo(obj) {
with(obj) {
a = 2
}
// console.log(a) // foo(o2) 時打印 2窝撵,
}
var o1 = {
a: 3
}
var o2 = {
b: 4
}
foo(o1)
console.log(o1.a) // 2
foo(o2)
console.log(o2.a) // undefined
console.log(a) // 2 a被泄漏到了全局作用域(LHS 導致的傀顾,嚴格模式的話,將會阻止在全局作用域聲明)
性能
eval可以動態(tài)執(zhí)行JavaScript代碼碌奉,with可以方便訪問對象屬性短曾,看起來都是非常棒的特性寒砖,但是,JavaScript引擎會在編譯階段進行數(shù)項的性能優(yōu)化嫉拐,其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法分析哩都,來預先確定所有的變量和函數(shù)的定義位置,并在執(zhí)行過程中快速找到標識符婉徘。因此漠嵌,如果引擎在代碼中發(fā)現(xiàn)了eval(...) / with(...),JS引擎出于正確性和嚴謹性的考慮盖呼,它只能認為自己做的優(yōu)化是無效的( 這兩者都可能會改變所處的作用域甚至全局作用域)儒鹿,因此運行效率將會降低。
作用域有哪幾類
在JavaScript中作用域分為兩類:
- 函數(shù)作用域
- 塊級作用域
函數(shù)作用域
每聲明一個函數(shù)几晤,JS引擎都會為它創(chuàng)建一個函數(shù)作用域挺身,屬于這個函數(shù)的所有變量都可以在整個函數(shù)范圍被訪問(也包括函數(shù)體中嵌套的作用域),當發(fā)生變量查找時锌仅,從代碼所屬作用域開始查找章钾,當前作用域查找不到時,逐層往外查找热芹,最終查找到全局作用域的查找邏輯贱傀,也就是所謂的 作用域鏈
隱藏內(nèi)部實現(xiàn)
在Java中有的屬性可以被定義為private(私有屬性),它只屬于當前對象伊脓,并且不希望被外界其他對象訪問府寒。這就是最小特權原則
,也叫最小授權
或者 最小暴露
原則报腔,一個對象或者方法的設計株搔,應最小限度地暴露必要內(nèi)容,比如某個模塊或者對象的API 設計
// bad
var eatWhat = '吃什么'
var drinkWhat = '喝什么'
function haveFun() {
letUsHappy()
}
function letUsHappy() {
console.log(eatWhat)
console.log(drinkWhat)
}
// good : 該是我的變量和方法纯蛾,都處在我自己的函數(shù)作用域當中纤房,類似外界無法訪問我的私有屬性和方法
function haveFun() {
var eatWhat = '吃什么'
var drinkWhat = '喝什么'
function letUsHappy() {
console.log(eatWhat)
console.log(drinkWhat)
}
letUsHappy()
}
避免同名標識符沖突
這個根據(jù)作用域鏈
的查找規(guī)則就比較好理解了,也就是說如果當前作用域內(nèi)已經(jīng)可以查找到對應的標識符翻诉,標識符查找也就不會再往外查找了
var name = '我是全局作用域的名字'
function foo() {
var name = '我是foo 函數(shù)內(nèi)部的名字'
console.log(name)
}
foo()
塊作用域
首先塊的概念是什么炮姨,JavaScript中有哪些塊?
if() {} // if塊
while() {} // while塊
{} // 獨立的代碼塊
for() {} // for循環(huán)的迭代塊
由此碰煌,我們可以粗暴地理解為{...}
就是一個塊舒岸,不過這并不意味著這里面就有塊級作用域了,在ES6let
關鍵字之前芦圾,我們可以認為JavaScript是沒有塊級作用域的概念的蛾派,除了以下幾個奇怪的兄弟
// with 塊
var obj = {a: 1}
with(obj) {
a = 2 // 此處有塊級作用域,外部無法訪問a變量
}
// try/catch 的catch分句
try {
// doSomething error
} cathc(error) {
console.log(error) // error只能在該分句內(nèi)訪問
}
console.log(error) // ReferenceError: error not defined
let / const
let / const是es6新增的變量聲明方式(保存在詞法環(huán)境
),用于聲明一個局部變量洪乍,使用let / const關鍵字聲明的變量會隱式劫持聲明所處的塊眯杏,被聲明的變量只能在該塊內(nèi)被訪問或者修改,由此就形成了塊級作用域
{
let a = '我處在塊級作用域'
const b = '我也處在塊級作用域'
}
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) // Uncaught ReferenceError: b is not defined
const 用于聲明一個常量
典尾,該變量的值是不可以修改的役拴,不過當聲明的變量的值是引用類型
時,稍微有點怪異
const a = '1'
const b = {name: 'white'}
b.name = 'black' // 正常 b是引用類型钾埂,b.name修改的是被引用的值河闰,而沒有修改b(內(nèi)存指針)本身
b = {name: 'white'} // Uncaught TypeError: Assignment to constant variable.
a = '2' // Uncaught TypeError: Assignment to constant variable.
你可能注意到了,上面兩份代碼示例中褥紫,錯誤類型是不一樣的:ReferenceError
vs TypeError
- ReferenceError:reference(引用)姜性,這種錯誤發(fā)生在標識符查找出錯時,或者嘗試去訪問一個未定義的變量時(此處需注意
變量提升
髓考,也就是聲明提前) - TypeError:變量查找已經(jīng)完成部念,但是執(zhí)行了錯誤的操作,如調(diào)用了不存在的方法
var msg = 123;msg.forEach(...)