關于JavaScript中的作用域

聲明:本文引用了《你不知道的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(...)
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末氨菇,一起剝皮案震驚了整個濱河市儡炼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌查蓉,老刑警劉巖乌询,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異豌研,居然都是意外死亡妹田,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門鹃共,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鬼佣,“玉大人,你說我怎么就攤上這事霜浴【е裕” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵坷随,是天一觀的道長房铭。 經(jīng)常有香客問我,道長温眉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任翁狐,我火速辦了婚禮类溢,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己闯冷,他們只是感情好砂心,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛇耀,像睡著了一般辩诞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纺涤,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天译暂,我揣著相機與錄音,去河邊找鬼撩炊。 笑死外永,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的拧咳。 我是一名探鬼主播伯顶,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼骆膝!你這毒婦竟也來了祭衩?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阅签,失蹤者是張志新(化名)和其女友劉穎掐暮,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愉择,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡劫乱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锥涕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衷戈。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖层坠,靈堂內(nèi)的尸體忽然破棺而出殖妇,到底是詐尸還是另有隱情,我是刑警寧澤破花,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布谦趣,位于F島的核電站,受9級特大地震影響座每,放射性物質(zhì)發(fā)生泄漏前鹅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一峭梳、第九天 我趴在偏房一處隱蔽的房頂上張望舰绘。 院中可真熱鬧,春花似錦、人聲如沸捂寿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秦陋。三九已至蔓彩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驳概,已是汗流浹背赤嚼。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抡句,地道東北人探膊。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像待榔,于是被迫代替她去往敵國和親逞壁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容