淺談javascript
今天我就詳細整理一下js作用域的知識來療療我被捶打的經歷!! 首先我們要清楚計算機不能識別程式語言,計算機唯一能識別的就是二進制資料。在計算機通訊中我們電腦輸入的指令其實就是一串串二進制的資料(100010001100)。這時候就需要將javascript程式碼進行編譯和轉譯。JS是一種解釋型語言,這意味著它通常不是在編譯成機器碼之後再執行,而是在程式碼被載入時直接由瀏覽器或其他JavaScript引擎(如Node.js)解釋並立即執行。java是交給伺服器執行,前端(javascript)程式碼給客戶端(瀏覽器)用。瀏覽器能讀的懂我們寫的js程式碼。我們以google瀏覽器為例子,如果把瀏覽器理解為一家企業的話這家企業就會有許多部門:如人力資源部門、程式設計部門、x銷售部門......其中有一個js執行引擎部門。我們以Google瀏覽器的執行引擎為例-v8引擎。
詞法分析
var a = 2
這段程式碼中我們定義了一個值賦值為2,在v8引擎裡首先會進行詞法分析。以這個例子為例會分析出詞法單元:var,a,=,2。
解析
詞法分析結束後就會將詞法單元轉化為一個逐級巢狀的程式語法結構樹 --抽象語法樹
生成程式碼
之後會重新生成一份程式碼。var a=2 此時v8會讀取我們最後生成出來的程式碼。
我們以下面的程式碼來感受一下程式碼
function foo(a){ console.log(a+b) } var b=2 foo(1)
執行結果為3,我們把程式碼修改一下看看又會有怎樣的效果呢?
function foo(a){ console.log(a+b); } function bar(){ var b=2 } bar() foo(1)
程式碼直接報錯了!!
有效識別符號
function foo(a){ var b=2 console.log(a+b) } a,b是foo的有效識別符號
我們也可以這樣寫程式碼
function foo(a){ var b=2 function bar(c){ console.log(a+b+c) } bar(3) } foo(1)
首先呢a,b,bar是foo的有效識別符號,c為bar的有效識別符號。列印出來的結果為6。從這裏我們可以看到這麼一個問題foo的呼叫帶來了bar的宣告,foo要是不呼叫v8都不知道有bar這個東西,所以bar是foo的有效識別符號是foo域裏邊的東西。那麼我們可以說foo的域是bar的外層域。所以從效果來看內層領域能訪問外層領域。
作用域
內層作用域是可以訪問外層作用域的。反之外層作用域是不能訪問內層作用域的
var a=1 function foo(){ console.log(a); } foo()
a宣告在全域性作用域,因此可以列印出a=1
function foo(){ var a=1 } foo() console.log(a);
而這段程式碼中a是宣告在foo裏邊的,屬於內層作用域。而console.log(a)是在外層作用域中。因此會報錯。
我們看下這段程式碼!!
var b; function foo(a){ b=a+bar(a*2) console.log(b*3) } function bar(a){ return a-1 } foo(2)
列印結果為15如何讓這段程式碼更加優雅呢? bar是為foo裏邊的邏輯來提供服務的,也可以稱為foo裏邊的邏輯走到某一步突然,需要一個輔助函式(bar)來乾點啥事情。
function foo(a){ var b; function bar(a){ return a-1 } b=a+bar(a*2) console.log(b*3); } foo(2)
這樣就封裝的很徹底,不零散。foo要實現的功能要用到的元素隱藏在foo的內部並沒有放到外部。沒有影響到全域性。我們試想一下假如企業多人開發寫一個專案,我要實現一些功能在全域性裏邊瘋狂定義一些變數,這時候同事也要實現一些功能也在全域性定義一些變數,這時候可能會出現定義的變數名會出現重名,會有覆蓋情況。Bug~也就隨之出現。
var a=1 function foo(){ var a=2 console.log(a); } foo()
列印結果為2,訪問foo自己本身形成的作用域,不會訪問外部的。 我們來剖析一下,作用域分為兩種一種是全域性的一種是函式自己本身形成的。這份程式碼在v8執行的時候,v8看見這份程式碼了,v8會維護出來一個呼叫棧(先進後出),把程式碼放到呼叫棧裏邊整理。v8從上往下執行的時候第一個碰到的域就是全域性作用域(全域性上下文),之後呢foo呼叫了就形成了函式作用域,這時候就又會來一個foo執行上下文入棧,然後在foo裏邊找有效識別符號,後邊就開始console.log(a)的執行。console.log會到foo的執行上下文裏邊找a的值,找到了就不會找了,如果沒找到的話就會繼續往下找,到全域性執行上下文裏邊找(這個過程跟棧先進後出的特點重合)。這就是內層作用域能訪問外層作用域的原因。
自執行函式
自執行函式是不需要呼叫就能自動觸發的函式
var a=2 (function(){ var a=3 console.log(a); })() console.log(a);
列印為3,2 或者
!function(){ }()
塊級作用域
首先呢我們看看下邊的程式碼
console.log(a); var a=1
var宣告的變數會變數宣告提升console.log列印出來的為undefined,於是es6打造了一個新的關鍵字為let,用let定義一個變數效果跟var基本一致。
console.log(a); let a=1
這樣就會直接報錯,變數宣告提升的問題就直接幹掉了。順著這條路我們可以整理出: 1.var 存在宣告提升,let不存在 2.let 會和{}形成塊級作用域 3.var 可以重複宣告變數let 不可以
我們可以看以下程式碼來了解下let和{}形成塊級作用域的實際效果
{ let x = 5; if (true) { let x = 10; // 這裏的x是另一個塊級作用域內的變數 console.log(x); // 輸出 10 } console.log(x); // 輸出 5 } console.log(x); // 報錯:ReferenceError: x is not defined
由此可見let和{}會形成塊級作用域,此時由於外層不能訪問內層作用域因此報了錯誤。 我們接下來聊聊let造成的暫時性死區問題
let a=1 if(1){ console.log(a); let a=2 }
出現直接報錯的情況,這就是let獨有的語法暫時性死區,按理來說會列印出1,但就是因為有暫時性死區這個語法會出現報錯。if語句形成的作用域中已經申明瞭let a=2,這時候就會觸發暫時性死區機制導致console.log不能訪問外層作用域申明的a=1,因此直接報錯。
我們接下來來聊聊兩種特殊的情況。
欺騙詞法作用域
function foo(str,a){ eval(str) console.log(a,b)//1,2 } var b=2 foo('var b=3',1)
列印出的結果為 1,3 eval(str)就相當於//var b=3 eval()將原本不屬於這裏的程式碼變得就像是天生定義在這裏一樣
var obj={ a:1, b:2, c:3 } obj.a=2 obj.b=3 obj.c=4 console.log(obj)
當物件裏邊的屬性很多的時候這樣改其實很麻煩,我們可以用with()語句來改
with(obj){ a=3 b=4 c=5 } console.log(obj)
列印為{a:3,b:4,c:5}
function foo(obj){ with(obj){ a=2 } } var o1={ a:3 } var o2={ b:3 } foo(o1) console.log(o1); foo(o2) console.log(o2); console.log(a)
列印出來結果為{a:2},{b:3},2。由此我們可以看出with(){}用於修改一個物件的屬性值,但如果修改的 屬性在原物件中不存在,那麼該屬性就會被洩露到全域性。