關(guān)于JS中的作用域中的問題思考分享
作用域
作用域,也就是我們常說的詞法作用域,說簡單點(diǎn)就是你的程序存放變量、變量值和函數(shù)的地方。根據(jù)作用范圍不同可以分為全局作用域和局部作用域,簡單說來就是,花括號(hào) {}
括起來的代碼共享一塊作用域,里面的變量都對(duì)內(nèi)或者內(nèi)部級(jí)聯(lián)的塊級(jí)作用域可見,這部分空間就是局部作用域,在 {}
之外則是全局作用域。
全局作用域
在JavaScript中,作用域是基于函數(shù)來界定的。也就是說屬于一個(gè)函數(shù)內(nèi)部的代碼,函數(shù)內(nèi)部以及內(nèi)部嵌套的代碼都可以訪問函數(shù)的變量。
function test(a){ var b = a * 2 function test2(c){ console.log(a ,b, c) } test2(b * 3) } test(4) // 4 8 24
我們不妨嘗試著來為這套代碼劃分一下作用域,上面定義了一個(gè)函數(shù)test,里面嵌套了函數(shù) test2
。圖中三個(gè)不同的顏色,對(duì)應(yīng)三個(gè)不同的作用域:
- ①對(duì)應(yīng)著全局
scope
,這里只有test2
- ②是
test2
界定的作用域,包含a、b、bar - ③是bar界定的作用域,這里只有c這個(gè)變量。
在查詢變量并作操作的時(shí)候,變量是從當(dāng)前向外查詢的。就上圖來說,就是③用到了a會(huì)依次查詢③、②、①。由于在②里查到了a,因此不會(huì)繼續(xù)查①了。這個(gè)其實(shí)就是作用域鏈的查找方式,詳細(xì)內(nèi)容我們后續(xù)介紹。
作用域中的錯(cuò)誤
這里順便講講常見的兩種error, ReferenceError
和 TypeError
。如上圖,如果在test2里使用了d,那么經(jīng)過查詢③、②、①都沒查到,那么就會(huì)報(bào)一個(gè)ReferenceError;
如果bar里使用了b,但是沒有正確引用,如b.abc(),這會(huì)導(dǎo)致TypeError
局部作用域
在局部作用域里面的變量通常是用到 with
, let
, const
with
對(duì)于with第一印象可能就是 with
關(guān)鍵字的作用在于改變作用域,但并不代表這個(gè)關(guān)鍵字不好用,至少面試的時(shí)候大概率會(huì)可以被卷起來,如果你不常用的話。
with
語句的原本用意是為逐級(jí)的對(duì)象訪問提供命名空間式的速寫方式,也就是說在指定的代碼區(qū)域,直接通過節(jié)點(diǎn)名稱調(diào)用對(duì)象。 with
通常被當(dāng)做重復(fù)引用同一個(gè)對(duì)象中的多個(gè)屬性的快捷方式,可以不需要重復(fù)引用對(duì)象本身。如下面代碼
var obj = {a: 2, b: 2, c: 2}; with (obj) { a = 5; b = 5; c = 5; } console.log(obj) // {a: 5, b: 5, c: 5}
我們快速的創(chuàng)建了一個(gè) obj
對(duì)象,為了能快速改變obj的值我們可以通過 with
的方式來進(jìn)行修改,當(dāng)然了,我們也可以通過逐行賦值的方式來進(jìn)行,代碼不夠簡潔就是了。話說回來,在這段代碼中,我們使用了 with
語句關(guān)聯(lián)了 obj
對(duì)象,這就意味著在 with
代碼塊內(nèi)部,每個(gè)變量首先被認(rèn)為是一個(gè)局部變量,如果局部變量與 obj
對(duì)象的某個(gè)屬性同名,則這個(gè)局部變量會(huì)指向 obj
對(duì)象屬性。
弊端
在上面的例子中,我們可以看到, with
可以很好地幫助我們簡化代碼。但生產(chǎn)環(huán)境中卻很少見到,事實(shí)上并不是少見多怪,主要是不推薦使用,為啥嘞?原因如下:
- 數(shù)據(jù)泄露
- 性能下降
數(shù)據(jù)泄露
function test3(obj) { with (obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 } foo(o1); console.log(o1.a) foo(o2); console.log(o2.a); console.log(a);
在運(yùn)行的過程中,我們可以看到,對(duì)于 o1.a
, o2.a
的回顯結(jié)果都不奇怪,畢竟對(duì)于 o1.a
來說a是在作用域中定義的,而 o2.a
壓根在o2中未定義,對(duì)于這個(gè)結(jié)果顯而易見,但為何 a
的值會(huì)從未定義到已賦值之間的轉(zhuǎn)變呢?這個(gè)很危險(xiǎn)的,畢竟這個(gè)時(shí)候已然出現(xiàn)數(shù)據(jù)泄露
首先,我們來分析上面的代碼。例子中創(chuàng)建了 o1
和 o2
兩個(gè)對(duì)象。其中一個(gè)有 a
屬性,另外一個(gè)沒有。 test3(obj)
函數(shù)接受一個(gè) obj
的形參,該參數(shù)是一個(gè)對(duì)象引用,并對(duì)該對(duì)象引用執(zhí)行了 with(obj){...}
。在 with 塊內(nèi)部,對(duì) a
有一個(gè)詞法引用,實(shí)際上是一個(gè) LHS引用,將 2 賦值給了它。
當(dāng)我們將 o1
傳遞進(jìn)去, a = 2
賦值操作找到了 o1.a
并將 2 賦值給它。而當(dāng) o2 傳遞進(jìn)去,o2 并沒有 a 的屬性,因此不會(huì)創(chuàng)建這個(gè)屬性, o2.a
保持 undefined
。
但為什么對(duì) o2的操作會(huì)導(dǎo)致數(shù)據(jù)的泄漏呢?
要回答這個(gè)問題則是需要了解 LHS查詢
的機(jī)制,后面有機(jī)會(huì)我們再展開來分享,基于LHS查詢的原理分析,當(dāng)我們傳遞 o2
給 with
時(shí), with
所聲明的作用域是 o2
, 從這個(gè)作用域開始對(duì) a 進(jìn)行 LHS查詢,在 o2 的作用域、foo(…) 的作用域和全局作用域中都沒有找到標(biāo)識(shí)符 a,因此在非嚴(yán)格模式下,會(huì)自動(dòng)在全局作用域創(chuàng)建一個(gè)全局變量,在嚴(yán)格模式下,會(huì)拋出 ReferenceError
異常。
性能下降
with 會(huì)在運(yùn)行時(shí)修改或創(chuàng)建新的作用域,以此來欺騙其他在開發(fā)時(shí)定義的詞法作用域。with的使用可以令代碼更具有擴(kuò)展性,雖然有數(shù)據(jù)泄漏的可能,但只要稍加注意就可以避免,除此之后,靈活運(yùn)用難道不可以創(chuàng)造出很好地功能嗎?事實(shí)上真的不能,不妨我們考察一下性能特點(diǎn)
function test4() { console.time("test4"); var obj = { a: [1, 2, 3] }; for(var i = 0; i < 100000; i++) { var v = obj.a[0]; } console.timeEnd("test4"); } test4(); function testWith() { console.time("testWith"); var obj = { a: [1, 2, 3] }; with(obj) { for(var i = 0; i < 100000; i++) { var v = a[0]; } } console.timeEnd("testWith"); } testWith();
在處理相同邏輯的代碼中,沒用 with
的運(yùn)行時(shí)間僅為 1.94 ms。而用 with
的運(yùn)用時(shí)間長達(dá) 44.13ms。
這是為什么呢?
原因是 JavaScript
引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識(shí)符。
但如果引擎在代碼中發(fā)現(xiàn)了 with
,它只能簡單地假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷都是無效的,因?yàn)闊o法知道傳遞給 with
用來創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底是什么。此時(shí)引擎的所有的優(yōu)化努力大概率都是無意義的。因此引擎會(huì)采取最簡單的做法就是完全不做任何優(yōu)化。這種情況下,設(shè)想我們代碼大量使用 with
或者 eval()
,那么運(yùn)行起來一定會(huì)變得非常慢。無論引擎多聰明,努力將這些悲觀情況的副作用限制在最小范圍內(nèi),也無法避免代碼會(huì)運(yùn)行得更慢的事實(shí)。┑( ̄Д  ̄)┍
let
在局部作用域中,關(guān)鍵字let、const倒是很常見了,先說說說let,其是ES6新增的定義變量的方法,其定義的變量僅存在于最近的{}之內(nèi)。
var test5 = true; if (test5) { let bar = test5 * 2; console.log( bar ); } console.log( bar ); // ReferenceError
const
與let一樣,唯一不同的是const定義的變量值不能修改
var test6 = true; if (test6) { var a = 2; const b = 3; a = 3; b = 4; } console.log( a ); console.log( b );
對(duì)于a來說是全局變量,而對(duì)于b的作用范圍僅僅是存在與 if
的塊內(nèi),此外從嘗試對(duì)b進(jìn)行修改的時(shí)候也會(huì)出錯(cuò),提示不能對(duì)其進(jìn)行修改
作用域鏈
在局部作用中,引用一個(gè)變量后,系統(tǒng)會(huì)自動(dòng)在當(dāng)前作用域中尋找var的聲明語句,如果找到則直接使用,否則繼續(xù)向上一級(jí)作用域中去尋找var的聲明語句,如未找到,則繼續(xù)向上級(jí)作用域中尋找…直到全局作用域中如還未找到var的聲明語句則自動(dòng)在全局作用域中聲明該變量。我們把這種鏈?zhǔn)降牟樵冴P(guān)系就稱之為"作用域鏈"。這個(gè)尋找的過程也是可以在局部作用域中可以引用全局變量的答案
代碼中的 testInner2
函數(shù)中沒有對(duì)變量a進(jìn)行賦值操作,因此由內(nèi)到外一層層尋找,發(fā)現(xiàn)在 testInner
中有 var a
的賦值操作,由此返回a的賦值,有興趣的讀者不妨把 testInner
里面的賦值操作去掉,可以發(fā)現(xiàn)函數(shù)運(yùn)行返回 a
的賦值是 yerik
。
其實(shí)作用域鏈本質(zhì)是一個(gè)對(duì)象列表,其保證了變量對(duì)象可以有序的訪問。其開始的地方是當(dāng)前代碼執(zhí)行環(huán)境的變量對(duì)象,常被稱之為“活躍對(duì)象”(AO),變量的查找會(huì)從第一個(gè)鏈的對(duì)象開始,如果對(duì)象中包含變量屬性,那么就停止查找,如果沒有就會(huì)繼續(xù)向上級(jí)作用域查找,直到找到全局對(duì)象中,如果找不到就會(huì)報(bào) ReferenceError
。
閉包
簡單的說就是一個(gè)函數(shù)內(nèi)嵌套另一個(gè)函數(shù),這就會(huì)形成一個(gè)閉包。請(qǐng)牢記這句話:“無論函數(shù)是在哪里調(diào)用,也無論函數(shù)是如何調(diào)用的,其確定的詞法作用域永遠(yuǎn)都是在函數(shù)被聲明的時(shí)候確定下來的”
function test7() { var a = 2; function test8() { console.log( a ); // 2 } test8(); } test7();
我們看到上面的函數(shù) test7
里嵌套了 test8
,這樣 test8
就形成了一個(gè)閉包。在 test8
內(nèi)可以訪問到任何屬于 test7
的作用域內(nèi)的變量。
function test7() { var a = 2; function test8() { console.log( a ); // 2 } return test8; } var test9 = test7(); test9(); // 2
在第8行,我們執(zhí)行完 test7()
后按理說垃圾回收器會(huì)釋放test7
的詞法作用域里的變量,然而沒有,當(dāng)我們運(yùn)行 test9()
的時(shí)候依然訪問到了 test7
中a的值。這是因?yàn)?,雖然 test7()
執(zhí)行完了,但是其返回了 test8
并賦給了 test9
, test8
依然保持著對(duì) test7
形成的作用域的引用。這就是依然可以訪問到 test7
中a的值的原因。再想想,“無論函數(shù)是在哪里調(diào)用,也無論函數(shù)是如何調(diào)用的,其確定的詞法作用域永遠(yuǎn)都是在函數(shù)被聲明的時(shí)候確定下來的”。
我們再來看另一個(gè)例子
function createClosure(){ var name = "yerik"; return { setStr:function(){ name = "naug"; }, getStr:function(){ return name + ":hello"; } } } var builder = new createClosure(); builder.setStr(); console.log(builder.getStr());
上面在函數(shù)中返回了兩個(gè)閉包,這兩個(gè)閉包都維持著對(duì)外部作用域的引用,因此不管在哪調(diào)用都是能夠訪問外部函數(shù)中的變量。在一個(gè)函數(shù)內(nèi)部定義的函數(shù),閉包中會(huì)將外部函數(shù)的自由對(duì)象添加到自己的作用域中,所以可以通過內(nèi)部函數(shù)訪問外部函數(shù)的屬性,這就是js模擬私有變量的一種方式。
注意:由于閉包會(huì)拓展附帶函數(shù)的作用域(內(nèi)部匿名函數(shù)攜帶外部函數(shù)的作用域),因此,閉包會(huì)比其他函數(shù)多占用些內(nèi)存空間,過度使用會(huì)導(dǎo)致內(nèi)存占用增加,這個(gè)時(shí)候如果要對(duì)性能進(jìn)行優(yōu)化可能會(huì)增加一些難度。
閉包對(duì)作用域鏈的影響
由于作用域鏈機(jī)制的影響,閉包只能取得內(nèi)部函數(shù)的最后一個(gè)值,這引起了一個(gè)副作用,如果內(nèi)部函數(shù)在一個(gè)循環(huán)中,那么變量的值始終為最后一個(gè)值。
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } console.log(data[0]) console.log(data[1]) console.log(data[2])
如果我們想要獲取循環(huán)過程的中的結(jié)果,應(yīng)該要怎么做呢?
- 返回匿名函數(shù)的賦值或者立即執(zhí)行函數(shù)
- 使用es6的let
匿名函數(shù)的賦值
var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (num) { return function(){ console.log(num); } })(i); } console.log(data[0]) console.log(data[1]) console.log(data[2])
無論上是立即執(zhí)行函數(shù)還是返回一個(gè)匿名函數(shù)賦值,原理上都是因?yàn)樽兞康陌粗祩鬟f,所以會(huì)將變量i的值賦值給實(shí)參num,在匿名函數(shù)的內(nèi)部又創(chuàng)建了一個(gè)用于訪問num的匿名函數(shù),這樣每一個(gè)函數(shù)都有一個(gè)num的副本,互不影響。
使用let
var data = []; for (let i = 0; i < 3; i++) { data[i] = (function (num) { return function(){ console.log(num); } })(i); } console.log(data[0]) console.log(data[1]) console.log(data[2])
前面我們介紹到let主要是作用域局部變量,由于其的存在,使for中的i存在于局部作用域中,而不是再全局作用域。
這個(gè)函數(shù)表執(zhí)行完畢,其中的變量會(huì)被銷毀,但是因?yàn)檫@個(gè)代碼塊中存在一個(gè)閉包,閉包的作用域鏈中引用著局部作用域,所以在閉包被調(diào)用之前,這個(gè)塊級(jí)作用域內(nèi)部的變量不會(huì)被銷毀。
這個(gè)循環(huán)本質(zhì)上就是這樣
var data = [];// 創(chuàng)建一個(gè)數(shù)組data; { // 進(jìn)入第一次循環(huán) let i = 0; // 注意:因?yàn)槭褂胠et使得for循環(huán)為局部作用域 // 此次 let i = 0 在這個(gè)局部作用域中,而不是在全局環(huán)境中 data[0] = function() { console.log(i); }; } { // 進(jìn)入第二次循環(huán) let i = 1; // 因?yàn)?let i = 1 和上面的 let i = 0 // 在不同的作用域中,所以不會(huì)相互影響 data[1] = function(){ console.log(i); }; } ...
當(dāng)我們執(zhí)行 data[1]()
的時(shí)候,相當(dāng)于是進(jìn)入了以下的執(zhí)行環(huán)境
{ let i = 1; data[1] = function(){ console.log(i); }; }
在上面這個(gè)執(zhí)行環(huán)境中,它會(huì)首先尋找該執(zhí)行環(huán)境中是否存在i,沒有找到,就沿著作用域鏈繼續(xù)向上找,在其所在的塊級(jí)作用域執(zhí)行環(huán)境中,找到i=1,于是輸出1。
到此這篇關(guān)于關(guān)于JS中的作用域中的問題思考分享的文章就介紹到這了,更多相關(guān)JS中的作用域內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序中不同頁面?zhèn)鬟f參數(shù)的操作方法
這篇文章主要介紹了微信小程序中不同頁面?zhèn)鬟f參數(shù)的操作方法,在開發(fā)項(xiàng)目中,避免不了不同頁面之間傳遞數(shù)據(jù)等,那么就需要進(jìn)行不同頁面之間的一個(gè)數(shù)據(jù)傳遞的,需要的朋友可以參考下2023-12-12JS 實(shí)現(xiàn)列表與多選框選擇附預(yù)覽動(dòng)畫
本節(jié)為大家介紹的是用JS實(shí)現(xiàn)列表與多選框選擇,并附gif演示動(dòng)畫,這個(gè)例子很詳細(xì),大家可以看看2014-10-10利用jsPDF實(shí)現(xiàn)將圖片轉(zhuǎn)為pdf
這篇文章主要為大家詳細(xì)介紹了如何利用jsPDF實(shí)現(xiàn)將圖片轉(zhuǎn)為pdf的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-08-08學(xué)習(xí)jQuey中的return false
這篇文章主要介紹了jQuey中的return false作用,以及解決jquery中的return false不起作用的方法,感興趣的小伙伴們可以參考一下2015-12-12基于Unit PNG Fix.js有時(shí)候在ie6下不正常的解決辦法
本篇文章是對(duì)Unit PNG Fix.js有時(shí)候在ie6下不正常的解決辦法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06webpack打包js文件及部署的實(shí)現(xiàn)方法
這篇文章主要介紹了webpack打包js文件的方法及webpack打包后的JS文件如何部署,需要的朋友可以參考下2017-12-12