作用域
在學習作用域之前塘安,先了解兩個重要的概念:編譯器、引擎
編譯器:負責詞法分析及代碼生成等編譯過程
引擎:負責整個 JavaScript
程序的編譯和執(zhí)行
什么是作用域
通俗的來講就是變量起作用的范圍社搅。比較規(guī)范的解釋(引用《你不知道的 JavaScript 》上卷),負責收集并維護由所有聲明的標識符(變量)組成的一系列查詢乳规,并實施一套非常嚴格的規(guī)則形葬,確定當前執(zhí)行代碼對這些標識符的訪問權(quán)限。
在ES6
之前暮的,JavaScript只有全局作用域和函數(shù)作用域笙以,與其他類型語言不同的是它沒有塊級作用域。
if(true){
var a = 1;//全局作用域
}
console.log(a); // 1
function foo(){
var b = 1;//函數(shù)作用域
console.log(a); //1
}
console.log(b); // ReferenceError
在上面的代碼中冻辩,a
屬于全局作用域猖腕,if
后的花括號并沒有形成塊級作用域拆祈,而 b
屬于 foo
函數(shù)的作用域,在JavaScript
中函數(shù)外部作用域訪問不到函數(shù)內(nèi)部作用域倘感,所以在全局作用域中訪問foo
函數(shù)作用域變量b
會報錯放坏。
在es6
之后,JavaScript
擁有了塊級作用域
if (true) {
let a = 1
}
console.log(a) // ReferenceError
在if
老玛、for
淤年、while
、try...catch
等在大括號中使用let
逻炊、const
聲明的變量會形成塊級作用域互亮,如果在外部訪問會報錯。
作用域如何工作
變量提升
剛開始接觸 JavaScript
的同學可能會對變量先聲明后使用的現(xiàn)象十分不解余素,要理解它我們得了解JavaScript
編譯的兩個原則:①編譯時聲明 ②運行時賦值
var a = 2;
//相當于↓
var a; //編譯時
a = 2; //運行時
上面這段代碼 var a = 2
只做一件事豹休,對a
進行賦值 ,不過瀏覽器引擎不這么看, 它會被分為 var a
和 a = 2
兩步進行桨吊,一個在編譯器編譯時聲明變量威根,另一個在引擎運行時賦值。
編譯器首先將上面這段程序分解為詞法單元视乐,然后將詞法單元解析成一個樹結(jié)構(gòu)(AST
抽象語法樹)洛搀。在開始代碼生成時,編譯器遇到var a
佑淀,編譯器詢問作用域是否已經(jīng)聲明了這個變量留美;如果是,編譯器忽略該聲明伸刃,否則在當前作用域集合聲明一個新的變量谎砾,命名為a
。
引擎執(zhí)行a = 2
首先詢問作用域捧颅,在當前的作用域集合中是否存在一個叫做a
的變量景图。如果是,引擎就會使用這個變量碉哑,否則引擎會繼續(xù)延著作用域鏈查找該變量挚币。如果引擎最終找到了a
變量,就會將 2 賦值給它扣典,否則引擎會拋出一個異常Uncaught ReferenceError: a is not defined
函數(shù)提升
a() // aaa => 函數(shù)a被提升妆毕,所以在聲明前可以調(diào)用函數(shù)
var a
function a () {
console.log('aaa')
}
console.log(a) // ? a() {} 函數(shù)聲明優(yōu)先級比變量聲明高
var
聲明的變量會提升,function
聲明的函數(shù)也會被提升贮尖,并且函數(shù)聲明優(yōu)先級比變量聲明優(yōu)先級高设塔,所以上面這段代碼打印 a
是個函數(shù),因為var a
聲明的變量被function
聲明的函數(shù)覆蓋了。
詞法作用域
詞法作用域就是定義在詞法階段的作用域闰蛔,也就是說作用域是在書寫代碼時函數(shù)聲明的位置來決定痕钢,與執(zhí)行過程無關(guān),JavaScript
采用的是詞法作用域序六。
相對詞法作用域另外一種叫做動態(tài)作用域任连,作用域是在執(zhí)行階段確定的,比如Bash
腳本例诀、Perl
語言等随抠。
看下面這段代碼示例:
var a = 1
function foo () {
console.log(a)
}
function bar () {
var a = 'local'
foo ()
}
bar() // 詞法作用域是:1 ;動態(tài)作用域是:‘local’
我們使用詞法作用域和動態(tài)作用域分析一下上面這段代碼執(zhí)行過程繁涂,bar
函數(shù)內(nèi)部調(diào)用 foo
函數(shù)
如果是詞法作用域拱她,調(diào)用 foo
查找變量a
會從foo
函數(shù)代碼定義的位置向外一層也就是全局作用域訪問,此時var a = 1
,結(jié)果是 1扔罪;
如果是動態(tài)作用域秉沼,調(diào)用foo
查找變量a
會從當前調(diào)用函數(shù)位置開始向往搜索,發(fā)現(xiàn)外部聲明var a = 'local'
,所以 a
的值是local
;
而在JS
引擎中上面這段代碼運行結(jié)果是 1矿酵,所以JavaScript
采用的是詞法作用域
不過唬复,this
在 JavaScript
中比較特殊,JavaScript
程序在執(zhí)行的時候才會對this
進行賦值全肮,在未執(zhí)行時不能知道this
的作用域敞咧,所以比較準確的說在JavaScript
中this
采用的是動態(tài)作用域。
修改詞法作用域: eval 和 with
eval 欺騙詞法作用域
eval
函數(shù)接收一個或多個聲明的代碼辜腺,會修改其所處的詞法作用域休建。
var a = 2
function foo (str, b) {
eval(str) // 欺騙
console.log(a, b)
}
foo('var a = 3', 1) // 3, 1
執(zhí)行 eval
函數(shù),傳入的字符串會解析成腳本執(zhí)行评疗,聲明一個變量 a
修改了 foo
函數(shù)的詞法作用域测砂,遮蔽了外部(全局)作用域中的同名變量訪問,欺騙了 foo
詞法作用域壤巷。另外,使用 eval
函數(shù)還容易受到xss
攻擊瞧毙。
with 欺騙詞法作用域
with
將一個對象的引用當作作用域來處理胧华,將對象的屬性當作作用域中的標識符來處理,如果對象中沒有該標識號宙彪,會在全局創(chuàng)建一個新的詞法作用域
with
的用法
var obj = {
a: 1,
b: 2,
c: 3
}
// 對象屬性賦值矩动,多次使用obj
obj.a = 2
obj.b = 3
obj.c = 4
// 使用 with 寫法簡潔
with(obj) {
a = 3;
b = 4;
c = 5;
}
with
的缺陷
function foo(obj) {
with(obj) {
a = 2
}
}
var obj1 = {
a: 3
}
var obj2 = {
b: 3
}
foo(obj1)
console.log(obj1.a) // 2
foo(obj2)
console.log(obj2.a) // undefined
console.log(a) // 2 —— a被泄露到了全局作用域上
with
會修改引用中屬性的值,如果引用中沒有該屬性释漆,在非嚴格模式下會在全局作用域中創(chuàng)建一個全新的詞法作用域悲没,欺騙了全局詞法作用域
除此之外,使用 eval
和 with
還會帶來性能問題男图,因為JS
引擎無法在編譯時對它們作用域進行查詢優(yōu)化示姿,這樣會導致代碼運行效率變慢甜橱,所以建議不要使用它們。
作用域鏈
作用域鏈形成是由詞法作用域和編譯時詞法環(huán)境對外部環(huán)境引用的結(jié)果栈戳,關(guān)于詞法環(huán)境外部環(huán)境的引用可以參考這篇文章【深入了解JavaScript執(zhí)行過程】
)
現(xiàn)在主要說說作用域鏈的構(gòu)成過程岂傲,開始執(zhí)行腳本時創(chuàng)建全局作用域,在全局環(huán)境調(diào)用 foo
函數(shù) 時子檀,編譯foo
函數(shù)并創(chuàng)建foo
函數(shù)作用域镊掖,foo
函數(shù)中聲明 bar
函數(shù),在調(diào)用 bar
函數(shù)會創(chuàng)建 bar
函數(shù)作用域褂痰。JavaScript
中亩进,內(nèi)部函數(shù)可以訪問外部函數(shù)的變量,這樣缩歪, bar
函數(shù)作用域 =》 foo
函數(shù)作用域 =》全局作用域 構(gòu)成了一條作用域鏈归薛。
var a = 'global'
function foo () {
var b = 'foo scoped'
function bar () {
var c = 'bar scoped'
console.log(a, b, c)
}
bar()
}
}
foo() // 'global' 'foo scoped' 'bar scoped'
閉包
談起閉包,它可是JavaScript兩個核心技術(shù)之一(異步和閉包),在面試以及實際應用當中驶冒,我們都離不開它們苟翻,甚至可以說它們是衡量js
工程師實力的一個重要指標。下面我們就羅列閉包的幾個常見問題骗污,從回答問題的角度來理解和定義閉包崇猫。
問題如下:
- 什么是閉包
- 閉包的原理是什么
- 閉包是如何使用的
- 閉包的應用場景有哪些
如果你能回答上面這些問題,說明你對閉包非常熟悉了需忿;如果腦子里比較模糊回答不上來也不用擔心诅炉,繼續(xù)往下讀,相信你會找到答案的屋厘。
什么是閉包
網(wǎng)上有很多種對閉包解釋的說法:
1涕烧、閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成
2、閉包是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)
讀起來比較抽象和拗口汗洒,用代碼來理解閉包议纯。
function foo() {
var a = 2
function bar () {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2 —— 這就是閉包的效果
函數(shù)是一等公民,可以當成數(shù)值來使用溢谤,它既可以作為函數(shù)參數(shù)瞻凤,也可以作為函數(shù)返回值。調(diào)用foo
函數(shù)返回bar
世杀,理論上來說foo
函數(shù)執(zhí)行完之后會被銷毀阀参,不過bar
函數(shù)引用著foo
的a
變量,所以執(zhí)行完foo
瞻坝,函數(shù)體會被銷毀蛛壳,但是a
被引用著不能被回收仍然保存在內(nèi)存當中,所以在外部調(diào)用bar
函數(shù)可以訪到foo
內(nèi)部函數(shù)的a
變量。這時我們給foo
起了另外一個名字叫閉包函數(shù)衙荐。
我們知道根據(jù)作用域鏈函數(shù)內(nèi)部可以訪問函數(shù)外部的變量捞挥,反過來是不行的,但是閉包可以做到赫模,這就是閉包的神奇之處
總結(jié)一下树肃,閉包本質(zhì)上是一個函數(shù),它返回另一個函數(shù)瀑罗,可以使外部函數(shù)可以訪問其他函數(shù)內(nèi)部的變量胸嘴。
閉包原理
細心的朋友可能知道答案了,閉包的原理就是詞法作用域和作用域鏈形成的結(jié)果斩祭。
如何使用閉包
為了能讓我們的程序更健壯劣像,我們往往需要將實現(xiàn)細節(jié)隱藏起來,只對外提供暴露接口摧玫,這也是面向?qū)ο笕筇匦灾环庋b性
私有變量
function foo () {
var num = 0
function bar () {
++num
return num
}
return bar
}
var add1 = foo ()
add1() // 1
add1() // 2
add1() // 3
var add2 = foo ()
add2() // 1
add2() // 2
add2() // 3
每次執(zhí)行foo
都得到相同的值耳奕,不會相互污染
function Person() {
var age = 20
var sex = 'man'
getAge () {
return age
}
setAge(value) {
age = value
}
getSex () {
return sex
}
setSex(value) {
sex = value
}
return {
getAge,
setAge,
getSex,
setSex
}
}
var zhangsan = Person()
zhangsan.getAge() // 20
zhangsan.getSex() // 男
隱藏實現(xiàn)細節(jié),對外暴露接口诬像。模擬實現(xiàn)了面向?qū)ο蟮乃枷胛萑海a也顯得健壯、易理解坏挠、可擴展可維護芍躏。
閉包的應用場景
定時器、事件監(jiān)聽器降狠、Ajax
請求对竣、跨窗口通信、Web Workers
或者任何其他的異步(或者同步)任務中榜配,只要使用了回調(diào)函數(shù)否纬,實際上就是使用閉包
閉包使用注意事項
1、閉包會使得函數(shù)中的變量都被保存在內(nèi)存中蛋褥,內(nèi)存消耗很大临燃,處理不當,容易造成內(nèi)存泄漏
2烙心、如果不是某些特定任務需要使用閉包膜廊,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包在處理速度和內(nèi)存消耗方面對腳本性能具有負面影響弃理。
總結(jié)
寫的內(nèi)容有點多溃论,梳理一下
1屎蜓、首先講了什么是作用域痘昌,作用域類型分為全局作用域、函數(shù)作用域、函數(shù)作用域
2辆苔、其次作用域工作時算灸,使用var
和functioin
聲明會出現(xiàn)變量提升和函數(shù)提升;JavaScript
是詞法作用域驻啤,eval
和 with
會欺騙詞法作用域
3菲驴、最后講了作用域鏈的原理和閉包使用介紹
引用鏈接
推薦閱讀