序言
盡管通常將JavaScript歸類為“動態”或“解釋執行”語言,但實際上它是一門編程語言,但與傳統的編譯語言不同,它不是提前編譯的,編譯結果也不能在分布式系統中進行移植,盡管如此,JavaScript引擎進行編譯的步驟和傳統的編譯語言非常相似,在某些環節可能比預想的復雜,
傳統的編譯語言通常會在一段源代碼執行之前經歷三個步驟,統稱為“編譯”,這三個步驟分別是分詞/詞法分析(Tokenizing/Lexing)、決議/語法分析(Parsing)、代碼生成,但對于JavaScript來說,大部分情況下編譯發生在代碼執行前的幾微秒,所以JavaScript引擎不會有大量的時間用來進行優化,他只會在這幾微秒內用盡各種辦法(比如JIT,可以延遲編譯甚至實施重編譯)來保證性能最佳,
一、理解作用域
首先要知道以下三個概念:
- 引擎:從頭到尾負責整個JavaScript程式的編譯及其執行程序
- 編譯器:負責語法分析及代碼生成
- 作用域:負責收集并維護所有宣告的識別符號(變數)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些識別符號的訪問權限,
以 var a = 2 為例,首先這段代碼是分為兩個步驟執行的,第一補是宣告var = a,編譯器會在當前作用域中查找是否存在一個名為 a 的變數,若存在則忽略該宣告,若不存在,則會在當前作用域進行宣告,第二步是賦值,在運行時引擎會在作用域中查找該變數,然后將 2 賦值給 a
如果查找的目的是對變數進行賦值,那么就會使用LHS查詢,如果目的是獲取變數的值,就會使用RHS查詢,賦值運算子會導致LHS查詢, = 等號運算子或呼叫函式是傳入引數的操作都會導致關聯作用域的賦值操作,
思考一下下面一段代碼有幾處LHS和RHS查詢:
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
答案是LHS查詢有三處,分別是(1) c = …;c被賦值,(2)a = 2;隱式變數分配,也就是foo(2)傳入foo(a),相當于a = 2,(3)b = …;b被賦值,
LRH查詢有四處,分別是(1)函式foo(a)獲取a的值,(2)b = a 獲取a的值,(3)(4)return a + b 獲取一次a獲取一次b
二、詞法作用域
大部分標準語言編譯器的第一個作業階段叫做詞法化,簡單地說,詞法作用域就是定義在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變數和塊作用域寫在哪里來決定的,因此當詞法分析器處理代碼時會保持作用域不變(大部分情況下是這樣的),
三、函式作用域
函式作用域的含義是指,屬于這個函式的全部變數都可以在整個函式的范圍內使用及復用(事實上在嵌套的作用域中也可以使用),
1.匿名函式和具名函式
顧名思義,匿名函式是沒有名稱識別符號的函式(比如回呼函式),具名函式是有名稱識別符號的函式,
//匿名函式
setTimeout(function(){
console.log("I wait 1000ms")
},1000)
//具名函式
function hello(){
console.log("Hello World")
}
2. 立即執行函式(IIFE)
var a = 2
(funcrion IIFE(global){
var a = 3;
console.log(a); //3
console.log(global.a); //2
})(window);
console.log(a); //2
上面這個函式就是立即執行函式,函式別包含在一對( )括號內部,因此成了一個運算式,通過在末尾加上另外一個( )可以立即執行這個函式,第一個( )將函式變成了運算式,第二個( )執行了這個函式,并且第二個( )可以對第一個( ) 里的函式進行傳參,
四、塊作用域
定義:塊作用域是一個用來對之前最小授權原則進行拓展的工具,將代碼從在函式中隱藏資訊擴展為在塊中隱藏資訊,
對于大家常用的for回圈來說,為什么要把一個只在for回圈內部使用的變數 i,污染到整個函式作用域中呢?
除了for回圈之外,ES3規范中規定 try/catch 的 catch 分句也會創建一個塊作用域,其中宣告的變數僅在 catch 內部有效,
1. let
ES6引入了新的關鍵字let,提供了var以外的另一種變數宣告方式,
let關鍵字可以將變數系結到所在的任意作用域中(通常是{,,,}內部),換句話說,let為其宣告的變數隱式得劫持了所在的塊作用域,
2. 垃圾收集
考慮以下代碼:
function process(data) {
//do something
}
var someData = { .. }
process( someData )
var btn = document.getElementById("my_button")
btn.addEventListeer("click", function click(evt){
console.log("button clicked")
}, false)
click 函式的點擊回呼并不需要 someData 變數,理論上這意味著當process()執行完后,在記憶體中占用大量空間的資料結構就可以被垃圾回收了,但是,由于click函式形成了一個覆寫整個作用域,JavaScript引擎極有可能依然保存著這個結構(取決于具體實作),
塊作用域可以打消這種顧慮,可以讓引擎清楚地知道沒有必要繼續保存someData 了,
function process(data) {
//do something
}
//在這個塊中定義的內容完事可以銷毀!
{
let someData = { .. }
process( someData )
}
var btn = document.getElementById("my_button")
btn.addEventListeer("click", function click(evt){
console.log("button clicked")
}, false)
3. let回圈
for回圈頭部的let不僅將 i 系結到了for回圈的塊中,事實上它將其重新系結到了回圈的每一個迭代中,確保使用上一個回圈迭代結束時的值重新進行賦值,
每個迭代進行重新系結會在后面討論閉包時進行說明,
4. const
const也是ES6和let一塊引進來用來創建作用域變數的,與其他兩個不同的是它宣告的值都是固定的,也就是只能用來宣告常量,宣告之后任何試圖修改值的操作都會引起錯誤,
五、提升
思考下面兩段代碼
a = 2
var a
console.log(a)
console.log(a)
var a = 2
我們先來說一下兩段代碼的輸出結果,第一段輸出 2,第二段輸出的是undefined,
為什么會是這樣的呢?正確的思考思路是,包括變數和函式在內的所有宣告都會在任何代碼被執行前首先被處理,也就是也就是引擎會在解釋JavaScript代碼之前首先對其進行編譯編譯階段中的一部分作業就是找到所有的宣告,并用合適的作用域將他們關聯起來,也就是之前說的詞法作用域的機制,
也就導致了var a永遠是在前面的,所以第一段代碼被成功賦值,第二段代碼為賦值而拋出undefined而非未宣告的referenceError,
函式的宣告與函式運算式
函式的宣告是以function foo () { … }的方式宣告的函式
函式運算式是 var foo = function () { … }
foo() //函式的宣告會被提升
function foo(){
console.log(a) //undefined
var a = 2
}
foo() // 不是referenceError,而是TypeError,函式運算式不會被提升
var foo = function(){ .. }
1. 函式優先
函式宣告和變數宣告都會被提升,但是函式會被首先提升,然后才是變數,重復的var宣告會被忽略掉,出現在后面的函式宣告會覆寫前面的,
foo() //3
function foo() {
console.log(1)
}
var foo = function() {
console.log(2)
}
function foo() {
console.log(3)
}
無論作用域中的宣告出現在什么地方,都將在代碼本身被執行前首先進行處理,可以將這個程序想象成所有的宣告(變數和函式)都會被“移動”到各自作用域的最頂端,這個程序被稱為提升,
六、作用域閉包
1. 什么是閉包
函式在當前語法作用域之外執行,也可以函式記住并訪問所在的詞法作用域
我們來看一段代碼,清晰地展示了閉包:
function foo() {
var a = 2
function bar() {
console.log( a )
}
return bar
}
var baz = foo()
baz() //2 --朋友,這就是閉包
函式bar()的詞法作用域能夠訪問foo()的內部作用域,然后我們將bar()函式本身當作一個值型別進行傳遞,在這個例子中,我們將bar所參考的函式物件本身當作回傳值,
在foo()執行后,其回傳值(也就是內部的bar()函式)賦值給變數baz并呼叫baz(),實際上只是通過不同的識別符號參考呼叫了內部的函式bar(),
bar() 顯然可以被正常執行,但是在這個例子中,它在自己定義的詞法作用域以外的地方執行,
在foo()執行后,通常會期待fo()的整個內部作用城都被銷毀,因為我們知道引擎有垃圾回收器用來釋放不再使用的記憶體空間,由于看上去foo()的內容不會再被使用,所以很自然地會考慮對其進行回收,
而閉包的“神奇”之處正是可以阻止這件事情的發生,事實上內部作用域依然存在,因此沒有被回收,誰在使用這個內部作用域?原來是bar()本身在使用,
拜bar()所宣告的位置所賜,它擁有涵蓋foo() 內部作用域的閉包,使得該作用域能夠一直存活,以供bar()在之后任何時間進行參考,
bar()依然持有對該作用域的參考,而這個參考就叫作閉包,
2. 回圈和閉包
要說明閉包,for回圈是最常見的例子,
for (var i = 0; i <= 5; i++) {
setTimeout( function timer() {
console.log( i )
}, i * 1000)
}
相信大家的預期效果都是分別輸出數字1-5,每秒一次,每次一個,
但實際上,這段代碼在運行時會以每秒一次的頻率輸出五次6,
這是為什么呢?
很顯然,當for回圈5次后,i = 6,然后跳出回圈,這時候延遲函式的回呼才會開始執行,所以才會輸出五個6來,
缺陷是我們試圖假設回圈中的每個迭代在運行時都會給自己“捕獲”一個 i 的副本,但是根據作用域的作業原理,實際情況是盡管回圈中的五個函式是在各個迭代中分別定義的,但是它們都被粉筆仔一個共享的全域作用域中,因此實際上只有一個 i,
那么我們要怎么解決呢?可以使用前面說到過的IIFE立即執行一個函式來創建作用域,我們來試一下
for (var i = 0; i <= 5; i++) {
(function() {
setTimeout( function timer() {
console.log( i )
}, i * 1000)
})()
}
你以為這樣就可以了嗎?其實不然,顯然我們擁有了更多的詞法作用域,但是這個作用域沒有起到對 i 的封閉作用,很顯然 i 的賦值是在作用域外的,那么我們再改進一下
for (var i = 0; i <= 5; i++) {
(function() {
var j = i
setTimeout( function timer() {
console.log( j )
}, j * 1000)
})()
}
這樣!他就能正常地作業了!也許還能再改進一下
for (var i = 0; i <= 5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j )
}, j * 1000)
})(i)
}
還記得我們前面說過的塊作用域嗎?除了創建一個新的作用域外,我們還可以通過let劫持塊作用域,并且在這個塊作用域中宣告一個變數,本質上是將一個塊轉換成一個可以被關閉的作用域,
代碼如下:
for (var i = 0; i <= 5; i++) {
let j = i //閉包的塊作用域!
setTimeout( function timer() {
console.log( j )
}, j * 1000)
}
或許我們還能再進行優化
for (let i = 0; i <= 5; i++) {
setTimeout( function timer() {
console.log( i )
}, i * 1000)
}
這樣我們就大功告成了!
3. 模塊
相信寫過一點專案的同學都會知道吧,就是我們的js檔案不會都寫在一塊,而是在一個模塊對其暴露再在另一個需要的模塊進行匯入,最后將主要模塊集合在一個js檔案進行管理,這樣會使我們的代碼脈絡更加清晰以及更好地分工合作,
4. 未來的模塊機制
ES6中為模塊增加了一級語法支持,在通過模塊系統進行加載時,ES6會將檔案當作獨立的模塊來處理,每個模塊都可以匯入其他模塊或特定的API成員,同樣也可以匯出自己的API成員,
ES6的模塊沒有“行內”格式,必須被定義在獨立的檔案中(一個檔案一個模塊),瀏覽器或者引擎有一個默認的“模塊加載器”(可以被多載,但這遠超出我們的討論范圍)可以在匯入模塊時同步地加載模塊檔案,
考慮以下代碼:
bar.js
function hello(who) {
return "Let me intriduce:" + who
}
export hello
foo.js
//僅從"bar"模塊匯入hello()
import hello from "bar"
var hungry = "hippo"
function awesome() {
console.log(
hello( hungry ).toUpperCase()
)
}
export awesome
baz.js
//匯入完整的"foo"和"bar"模塊
module foo from "foo"
module bar from "bar"
console.log(
bar.hello("rhino")
) //Let me intriduce: rhino
foo.awecome() //Let me intriduce: hippo
import 可以講一個模塊中的一個或多個API匯入到當前作用域中,并分別系結在一個變數上(在我們的例子里是hello),module會將整個模塊的API匯入并系結到一個變數上(在我們的例子里是foo 和 bar),export會將當前模塊的一個識別符號(變數,函式)匯出為公共API,這些操作可以在模塊定義中根據需要使用任意多次,
本文所有用例,部分話語參考自 Scope and Closures, Kyle Simpson 著(O’Reilly,2014),著作權所有,978-1-491-33558-8,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/273357.html
標籤:其他
下一篇:前端,H5,展示Heic格式圖片
