您好,登錄后才能下訂單哦!
這篇文章主要介紹“詳解JavaScript作用域”,在日常操作中,相信很多人在詳解JavaScript作用域問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”詳解JavaScript作用域”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
在學習作用域之前先簡單了解一下JavaScript的編譯、執行過程。
JavaScript被稱之為解釋性語言,與Java等這類編譯語言區別在于:JavaScript代碼寫好了就可以直接立即執行,Java則需要相對較長時間的編譯過程才可生成可執行的機器碼。
但其實JavaScript也是有編譯過程的,JavaScript使用的是一種即時編譯的方式(JIT)。 JIT會把JavaScript代碼中會多次運行的代碼給編譯器編譯,生成編譯后的代碼并保存起來,在下次使用時使用編譯好的代碼。這其實是JavaScript運行環境采用的一種優化解決方案。 如果不這么做,大量重復的代碼都會在運行前重復編譯,這將極大的影響性能與運行效率。
JavaScript引擎也會對JavaScript代碼在運行前進去預編譯,在預編譯的過程中會定義一套規則用來存儲變量,對象,函數等,方便在之后的運行調用。這套規則就是作用域。
JavaScript引擎在編譯過種中要對代碼進行詞法分析、語法分析、代碼生成、性能優化等等一系列工作。JIT就是這一過程中用來優化的一部分。
var a = 1;
這行代碼在運行前編譯器都會做哪些事情?
編譯器會把這行代碼分成 var a
和 a = 1
,兩個部分。
首先編譯器會在相同作用域內查詢是否已經存在一個叫 a
的變量,如是存在,編譯器會忽略聲明a
,繼續下一步編譯;如果不存在,則在當前作用域聲明一個變量,命名為a
。
然后編譯器會為引擎生成運行時的代碼,這些代碼中包含處理a = 1
的部分,引擎在處理a = 1
的時候,同樣也會查詢作用域中是否存在a
變量(會逐級向上一個作用域查找), 存在則賦值為2,不存在則拋出異常(嚴格模式下,如非嚴格模式則會隱式創建一個全局變量a
;LHS
)。
LHS 和 RHS 的含義是“賦值操作的左側與右側”,不過要注意并不單指“=”和左側與右側。 賦值操作還有其它的形式,因此可以理解為:LHS-賦值操作的目標是誰? RHS-誰是賦值操作的源頭。
a = 1;
是對a
LHS查詢,a是賦值操作的目標,為a賦值為1. 如LHS查詢失敗,非嚴格模式下會隱式創建一個全局變量,嚴格模式下會拋出ReferenceError: a is not defined
;
console.log(a)
是對a
RHS查詢,a是賦值的源頭;如果在作用域鏈中沒有查詢到a
,同樣也會拋出ReferenceError: a is not defined
;
作用域是存儲變量的一套規則,當代碼運行時可能并不只是在一個作用域查詢變量。 當一個作用域中包含另一個作用域的時候,就會存在作用域嵌套的情況。所以當內部的作用域無法找到某個變量的時候,引擎會在當前作用域的外層嵌套中繼續查詢;直到查到變量或者達到最外層的作用域為止。這就是作用域鏈接。
var name = "rewa"; function sayHi(){ console.log("hello,"+name); } sayHi(); // hello,rewa
如上述代碼,sayHi
函數作用域中并沒有變量name
;卻能正常引用。就是因為引擎在上一層作用域找到并使用了變量name
;
var name = "rewa"; function sayHi(){ var name = "fang"; // 添加的代碼 console.log("hello,"+name); } sayHi(); // hello,fang
當sayHi
作用域中已經找到變量name
時,引擎會停止向上層作用域查找,這叫作“遮蔽效應”,內部變量遮蔽外部作用域變量。
作用域有兩種主要的工作模型。一種是最為最為普遍的,被大多數編程語言采用的詞法作用域
; 還有一種叫動態作用域
。
詞法作用域就是在寫代碼時將變量和塊作用域寫在哪里作用域就在哪里,定義在詞法階段的作用域。JavaScript就是采用的詞法作用域。
詞法:就是組成代碼塊的字符串。比如:
var a = 1;
這行代碼中,var
、a
、=
、2
、;
還有這中間的空格
都是詞法單元。
編譯器的第一個工作就是詞法化,會把代碼分解成一個一個詞法單元;具體編譯器在詞法化階段都做了哪些工作遵守哪些規則,根據不同編程語言而不同。JavaScript是怎么樣的規則我特么也不清楚,等我研究清楚了;再來做一個筆記。
簡單的說,詞法作用域就是你寫代碼的時候,把變量a
寫在函數b
中,那么編譯器編譯時b
的作用域中就會包含有a
變量,編譯器會保持詞法作用域不變。(也會有特殊情況)
如下代碼:
var a = 1; function foo(){ var b = a + 2; function bar(){ var c = b + 3; console.log(a,b,c) } bar(); } foo(); // 1,3,6
這段代碼編譯后的作用域與你編寫時的詞法作用域是一致的。
全局作用域: 變量a
, 函數 foo
函數foo()
創建的作用域:變量b
,函數bar
函數bar()
創建的作用域:變量c
代碼寫在哪作用域就在哪。
了解詞法作用域需要注意以下幾點:
無論函數在哪里被調用,如何被調用,函數的詞法作用域都只由函數被聲明時所處的位置決定。
詞法作用域查詢只會查找一級標識符,比如上述代碼中的變量a,b,c
。如果訪問foo.bar.baz
,詞法作用域只會查詢foo
。找到這個變量后,再訪問屬性bar
,再到baz
。
存在使詞法作用域編譯后不一致的方法,但會導致性能下降。
代碼如下:
var a = 1; function foo(str){ eval(str); console.log(a); } foo('var a = 2;'); // 2
var a = 1;
會在函數foo
中運行,變量a
將包含作用域。 eval(...)
函數接受一個字符串,并將字符串當作代碼運行;就相當于把代碼寫在這個位置。
eval
在嚴格模式下會拋出異常:
var a = 1; function foo(str){ "use strict" eval(str); console.log(a); // ReferenceError: a is not defined } foo('var a = 2;');
默認情況下,如果eval()
中有包含聲明,就會對所處的詞法作用域進行修改;在嚴格模式下,eval()
在運行時有其自己的詞法作用域,那么將無法修改所在的作用域,如上述代碼。
var obj = { a:1, b:2, c:3 } obj.a = 11; obj.b = 22; obj.c = 33; // with 也可以達到同樣的效果 with(obj){ a=111; b=222; c=333; } //這樣 obj 被修改為: { a:111, b:222, c:333 }
with()
接受一個參數,在這里是obj
;此時with
中作用域是obj
, 可以訪問obj
中的屬性。 這種方式賦值就變得簡潔很多。
with可以為一個對象創建一個作用域,對象的屬性會定義為這個作用域中的變量;不過with
中的通過var
聲明的變量并不會成為這個作用域的成員,而是被聲明到with所在的作用域中。這不正常了,代碼使用with
會變得很不容易控制。比如:
with(obj){ a=111; b=222; c=333; d=444; } console.log(obj.d); // undefined console.log(d); // 444
原來以為會添加在obj
中的屬性d
,卻被添加到了全局作用域中;這就可能與開發編寫時的預期結果不符;也不符合詞法作用域的規則。
所以eval
與with
都已經被禁止了,也不推薦使用。
這種不可預估詞法作用域的特性,也帶了一個嚴重的性能問題。 JavaScript引擎在編譯階段會進行性能優化。其中有一些優化依賴代碼的詞法,對詞法進行靜態分析,并預先確定所有變量與函數的定義位置,才能在執行過程中快速找到變量。
如果引擎在代碼中發現了eval
或with
,它無法在詞法分析階段明確知道eval(...)
接生什么代碼;也無法知道傳遞給with用來創建新詞法作用域的對象內容是什么。 那么優化未知的代碼和詞法作用域是沒有意義的,引擎將放棄優化這一部分。
如果在代碼中頻繁使用eval
或with
,程序運行起來將會非常慢。
函數內部的變量和函數定義都可以封裝起來,外部無法訪問封裝在函數內部的變量標識符。
如下代碼:
function foo(){ var a = 1; function sayHi(){ console.log('Hello!') } } console.log(a); // ReferenceError:a is not defined sayHi(); //ReferenceError: sayHi is not defined
在函數外部訪問其內部的變量與函數會拋出異常。
這樣函數就可以行成一個相對獨立的作用域,可以用函數來封裝一個相對獨立的功能。 把業務代碼隱藏在函數內部實現,對外暴露接口;只要傳入不同的參數就可以輸入對應的結果。 所以很多情況下函數可以用來模擬Java語言中類的實現。
例如:
function shoot(who,score){ //這里面可以包含更多邏輯 function one(){ console.log(who + '罰籃命中!到得' +score+ '分!'); } function dunk(){ console.log(who + '扣籃,獲得' +score+ '分!'); } function three(){ console.log(who + '命中了一個' +score+ '分球!'); } switch(score){ case 1: one(); break; case 2: dunk(); break; case 3: three(); break; } } shoot('Kobe',3); // Kobe投中了一個3分球!' shoot('Lebron',2); // Lebron扣籃,獲得2分!' shoot('Shaq',1); // Shaq罰籃命中!到得1分!'
函數內部隱藏變量與函數的定義可以避免污染全局命名空間;比如當全局作用域中也有one
dunk
three
這些函數,并且內部實現不同;代碼邏輯就會混亂。 而在上面的代碼中,函數中定義的函數會遮蔽外部作用域的函數定義,只會調用到當前函數作用域中的同名函數。
但是即使如此,大量的函數聲明同樣也會污染全局全名空間。 當下流行的模塊化就是解決這一問題的方案之一。不過在模塊化出來之前,大多數情況可以使用立即執行函數(IIFE)來解決。 代碼如下:
(function(){ var name = 'kobe'; console.log(name); })();
當函數執行結束后,name
變量會被垃圾回收; 且不會與外部的任何作用域產生沖突,因為整個函數都執行在一個立即執行函數中。它是一個塊作用域,且本身也沒有在作用域下創建任何標識符。
立即執行函數也可以接受參數,用來函數內部引用:
(function(name){ console.log(name); })('kobe');
JavaScript中除了函數作用域,還有其它塊作用域。比如with也是塊作用域;上面有過介紹
with
。 還有一個容易被忽略的塊作用域 try/catch
。
try{ undefined(); //拋出異常 } catch(err){ console.log(err); // 正常執行 } console.log(err); //ReferenceError: err is not defined
err
只能在catch
中訪問,在外部的引用會拋出異常。
對于塊作用域,ES6
中我們可以用let
聲明實現這種需求。
if(true){ let a = 1; console.log(a); //1 } console.log(a); //ReferenceError: a is not defined
if(){}
并不是塊作用域,但上述代碼中let
可以讓a
變量成為僅if(){...}
中的變量,外部不可訪問。
這是不是像極了try/catch
, 可let
是ES6
的標準;在ES6
之前實現類似塊作用域效果的方法可沒這么輕松。 現在一般我們在編寫ES6
代碼,想要運行在所有瀏覽器上需要通過轉譯。而轉譯器也會把類似let的聲明,轉為 try/catch
的形式。
{ let a = 1; console.log(a); // 1 } console.log(a); //ReferenceError: a is not defined
轉為:
try{ throw 1; }catch(a){ console.log(a); //1 } console.log(a); //ReferenceError: a is not defined
還有可能轉譯為:
{ let _a = 1; // 把{}中的 a 轉為_a console.log(_a); } console.log(a);
到此,關于“詳解JavaScript作用域”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。