前言
js中的一個常見異常是ReferenceError,表示引用的變量不存在
js其實不像大多數認爲的那樣是一個腳本語言,他是一個編譯語言。代碼中包括變量和函數在內的所有聲明都會在任何代碼被執行前首先被處理
編譯語言
變量提升
console.log(a)
a = 2
console.log(a)
var a
上述代碼是不會報錯的。只不過console.log(a)的時候,打印的是undefined。由於是編譯語言,所以var a
被提升,在代碼被執行之前被處理了。在第一次執行console.log(a)
的時候,變量是存在的,只不過沒有被賦值,所以是undefined。第二次執行的時候就會打印出2了。此時的打印結果如下
➜ ~ node test.js
undefined
2
如果把代碼中變換如下
console.log(a)
a = 2
就會報錯了,出現ReferenceError的錯誤。應爲console.log(a)
運行的時候,變量a並不存在。此時的打印結果如下
➜ ~ node test.js
/Users/.../test.js:1
console.log(a)
^
ReferenceError: a is not defined
at Object.<anonymous> (/Users/.../test.js:1:13)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at Function.Module._load (internal/modules/cjs/loader.js:555:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:824:10)
at internal/main/run_main_module.js:17:11
函數提升
變量被提升的時候,可能很少人知道。但是函數提升,大家就一定都清楚了。因爲在代碼中的位置,函數調用是可以在函數。如下,此時foo()
是可以正常調用的。
foo()
function foo() {
console.log(2)
}
雖然函數是可以提升的,但是函數表達式不會提升,這點是需要注意的。例如,如下代碼就會報錯,但是錯誤並不是ReferenceError,而是TypeError。
foo()
var foo = function() {
console.log(1)
}
原因是在執行foo()
這行代碼的時候,foo變量是存在的。而此時還沒有被賦值爲函數,對於一個undefined的變量,當做函數來執行,報錯當然是TypeError了。詳細錯誤如下
/Users/.../test.js:1
foo()
^
TypeError: foo is not a function
at Object.<anonymous> (/Users/.../test.js:1:1)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at Function.Module._load (internal/modules/cjs/loader.js:555:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:824:10)
at internal/main/run_main_module.js:17:11
變量和函數同名
foo()
var foo
function foo() {
console.log(1)
}
foo = function() {
console.log(2)
}
上述代碼的輸出結果會是什麼呢?是出現異常TypeError,還是打印1,還是打印2?
首先確定可以排除的是打印2,因爲foo()
的調用是在foo = function() {console.log(2)}
之前執行的。
剩下的就判斷不了了。但是,結果是會打印1,而不會觸發TypeError這個錯誤。
這是由於函數被提升的優先級要高於變量。
塊作用域
下面的代碼,大家都會很熟悉。
for (var i=0; i<10; i++) {
console.log(i)
}
這個會從0到9的打印10個數字。但是,也會造成一個問題,就是之後的代碼中也是可以調用變量i的。在之後執行命令console.log(i)
就會打印出10了。這個是不是不是我們想要的。因爲在for循環中,我們希望的是i僅僅在這個for循環之中使用的,這個在JavaScript之中是可以簡單有效的解決的。
在ES6之中,提供了除了var之外聲明變量的方式,有let和const。let關鍵字可以將變量綁定到所在的任意作用域中(通常是{…}內部)。換句話說,let爲其聲明的變量的隱飾的劫持在了所在的塊作用域之中。for循環頭部的let聲明還會有一個特殊的行爲,這個行爲爲指出變量在循環的過程中不止被聲明一次,每次迭代都會聲明。聲明後的每個迭代都會使用上一個迭代結束時的值來初始化這個值
for (let i=0; i<5; i++) {
console.log(i)
}
console.log(i)
輸出如下
➜ ~ node test.js
0
1
2
3
4
/Users/.../test.js:4
console.log(i)
^
ReferenceError: i is not defined
at Object.<anonymous> (/Users/.../test.js:4:13)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at Function.Module._load (internal/modules/cjs/loader.js:555:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:824:10)
at internal/main/run_main_module.js:17:11
除了會順序輸出0到4的五個數外,還會報錯ReferenceError。這個就是let的作用。
題外話
python的作用域也挺有意思的,下面是做python開發經常用來的面試題(可是我從來沒遇到過)。
a = [lambda : x for x in range(3)]
for func in a:
print func()
2
2
2
三個輸出竟然都是2,這是爲什麼呢?是由於三個函數在執行的時候,由於函數內部沒有變量x,所以通過legb原則逐層向上搜索變量x,而在執行的時候的x爲2,所以輸出的都是2。
那麼如何修改爲輸出爲0,1,2呢?可以給匿名函數傳遞個默認參數
a = [lambda x = x: x for x in range(3)]
for func in a:
print func()
0
1
2
JavaScript之中,也會遇到相似的問題。
for (var i=0; i<3; i++) {
setTimeout( function time() {
console.log(i)
}, 1000)
}
一秒之後的輸出結果是3個3。那麼如何修改爲分別輸出0,1,2呢?
-
通過和python相似的方法
for (var i=0; i<3; i++) { (function (i){setTimeout( function time() { console.log(i) }, 1000)})(i) }
-
通過JavaScript的塊作用域方法
for (let i=0; i<3; i++) { setTimeout(function() {console.log(i)}, 1000); }
上述兩個方法都是可以正確的輸出的
➜ ~ node test.js
0
1
2