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