[轉(zhuǎn)載請注明來自: http://blog.csdn.net/sunxing007]
下面的程序是什么結(jié)果?
- var foo = 1;
- function bar() {
- if (!foo) {
- var foo = 10;
- }
- alert(foo);
- }
- bar();
結(jié)果是10;
那么下面這個呢?
- var a = 1;
- function b() {
- a = 10;
- return;
- function a() {}
- }
- b();
- alert(a);
結(jié)果是1.
嚇你一跳吧?發(fā)生了什么事情?這可能是陌生的,危險的,迷惑的,同樣事實上也是非常有用和印象深刻的javascript語言特性。對于這種表現(xiàn)行為,我不知道有沒有一個標(biāo)準(zhǔn)的稱呼,但是我喜歡這個術(shù)語:“Hoisting (變量提升)”。這篇文章將對這種機制做一個拋磚引玉式的講解,但是,首先讓我們對javascript的作用域有一些必要的理解。
Javascript的作用域
對于Javascript初學(xué)者來說,一個最迷惑的地方就是作用域;事實上,不光是初學(xué)者。我就見過一些有經(jīng)驗的javascript程序員,但他們對scope理解不深。javascript作用域之所以迷惑,是因為它程序語法本身長的像C家族的語言,像下面的C程序:
- #include <stdio.h>
- int main() {
- int x = 1;
- printf("%d, ", x); // 1
- if (1) {
- int x = 2;
- printf("%d, ", x); // 2
- }
- printf("%d\n", x); // 1
- }
輸出結(jié)果是1 2 1,這是因為C家族的語言有塊作用域,當(dāng)程序控制走進一個塊,比如if塊,只作用于該塊的變量可以被聲明,而不會影響塊外面的作用域。但是在Javascript里面,這樣不行??纯聪旅娴拇a:
- var x = 1;
- console.log(x); // 1
- if (true) {
- var x = 2;
- console.log(x); // 2
- }
- console.log(x); // 2
結(jié)果會是1 2 2。因為javascript是函數(shù)作用域。這是和c家族語言最大的不同。該程序里面的if并不會創(chuàng)建新的作用域。
對于很多C,c++,java程序員來說,這不是他們期望和歡迎的。幸運的是,基于javascript函數(shù)的靈活性,這里有可變通的地方。如果你必須創(chuàng)建臨時的作用域,可以像下面這樣:
- function foo() {
- var x = 1;
- if (x) {
- (function () {
- var x = 2;
- // some other code
- }());
- }
- // x is still 1.
- }
這種方法很靈活,可以用在任何你想創(chuàng)建臨時的作用域的地方。不光是塊內(nèi)。但是,我強烈推薦你花點時間理解javascript的作用域。它很有用,是我最喜歡的javascript特性之一。如果你理解了作用域,那么變量提升就對你顯得更有意義。
變量聲明,命名,和提升
在javascript,變量有4種基本方式進入作用域:
- 1 語言內(nèi)置:所有的作用域里都有this和arguments;(譯者注:經(jīng)過測試arguments在全局作用域是不可見的)
- 2 形式參數(shù):函數(shù)的形式參數(shù)會作為函數(shù)體作用域的一部分;
- 3 函數(shù)聲明:像這種形式:function foo(){};
函數(shù)聲明和變量聲明總是會被解釋器悄悄地被“提升”到方法體的最頂部。這個意思是,像下面的代碼:
- function foo() {
- bar();
- var x = 1;
- }
實際上會被解釋成:
- function foo() {
- var x;
- bar();
- x = 1;
- }
無論定義該變量的塊是否能被執(zhí)行。下面的兩個函數(shù)實際上是一回事:
- function foo() {
- if (false) {
- var x = 1;
- }
- return;
- var y = 1;
- }
- function foo() {
- var x, y;
- if (false) {
- x = 1;
- }
- return;
- y = 1;
- }
請注意,變量賦值并沒有被提升,只是聲明被提升了。但是,函數(shù)的聲明有點不一樣,函數(shù)體也會一同被提升。但是請注意,函數(shù)的聲明有兩種方式:
- function test() {
- foo(); // TypeError "foo is not a function"
- bar(); // "this will run!"
- var foo = function () { // 變量指向函數(shù)表達式
- alert("this won't run!");
- }
- function bar() { // 函數(shù)聲明 函數(shù)名為bar
- alert("this will run!");
- }
- }
- test();
這個例子里面,只有函數(shù)式的聲明才會連同函數(shù)體一起被提升。foo的聲明會被提升,但是它指向的函數(shù)體只會在執(zhí)行的時候才被賦值。
上面的東西涵蓋了提升的一些基本知識,它們看起來也沒有那么迷惑。但是,在一些特殊場景,還是有一定的復(fù)雜度的。
變量解析順序
最需要牢記在心的是變量解析順序。記得我前面給出的命名進入作用域的4種方式嗎?變量解析的順序就是我列出來的順序。一般來說,如果一個名稱已經(jīng)被定義,則不會被其他相同名稱的屬性覆蓋。這是說,(譯者沒理解這句,所以先做刪除樣式) 函數(shù)的聲明比變量的聲明具有高的優(yōu)先級。這并不是說給那個變量賦值不管用,而是聲明不會被忽略了。
(譯者注: 關(guān)于函數(shù)的聲明比變量的聲明具有高的優(yōu)先級,下面的程序能幫助你理解)
- <script>
- function a(){
- }
- var a;
- alert(a);//打印出a的函數(shù)體
- </script>
-
- <script>
-
- var a;
- function a(){
- }
- alert(a);//打印出a的函數(shù)體
- </script>
- //但是要注意區(qū)分和下面兩個寫法的區(qū)別:
- <script>
- var a=1;
- function a(){
- }
- alert(a);//打印出1
- </script>
-
- <script>
- function a(){
- }
-
- var a=1;
-
- alert(a);//打印出1
- </script>
這里有3個例外:
1 內(nèi)置的名稱arguments表現(xiàn)得很奇怪,他看起來應(yīng)該是聲明在函數(shù)形式參數(shù)之后,但是卻在函數(shù)聲明之前。這是說,如果形參里面有arguments,它會比內(nèi)置的那個有優(yōu)先級。這是很不好的特性,所以要杜絕在形參里面使用arguments;
2 在任何地方定義this變量都會出語法錯誤,這是個好特性;
3 如果多個形式參數(shù)擁有相同的名稱,最后的那個具有優(yōu)先級,即便實際運行的時候它的值是undefined;
命名函數(shù)
你可以給一個函數(shù)一個名字。如果這樣的話,它就不是一個函數(shù)聲明,同時,函數(shù)體定義里面的指定的函數(shù)名(
如果有的話,如下面的spam, 譯者注)將不會被提升, 而是被忽略。這里一些代碼幫助你理解:
- foo(); // TypeError "foo is not a function"
- bar(); // valid
- baz(); // TypeError "baz is not a function"
- spam(); // ReferenceError "spam is not defined"
-
- var foo = function () {}; // foo指向匿名函數(shù)
- function bar() {}; // 函數(shù)聲明
- var baz = function spam() {}; // 命名函數(shù),只有baz被提升,spam不會被提升。
-
- foo(); // valid
- bar(); // valid
- baz(); // valid
- spam(); // ReferenceError "spam is not defined"
怎么寫代碼
現(xiàn)在你理解了作用域和變量提升,那么這對于javascript編碼意味著什么?最重要的一點是,總是用var定義你的變量。而且我強烈推薦,對于一個名稱,在一個作用域里面永遠只有一次var聲明。如果你這么做,你就不會遇到作用域和變量提升問題。
語言規(guī)范怎么說
我發(fā)現(xiàn)ECMAScript參考文檔總是很有用。下面是我找到的關(guān)于作用域和變量提升的部分:
如果變量在函數(shù)體類聲明,則它是函數(shù)作用域。否則,它是全局作用域(作為global的屬性)。變量將會在執(zhí)行進入作用域的時候被創(chuàng)建。塊不會定義新的作用域,只有函數(shù)聲明和程序(譯者以為,就是全局性質(zhì)的代碼執(zhí)行)才會創(chuàng)造新的作用域。變量在創(chuàng)建的時候會被初始化為undefined。如果變量聲明語句里面帶有賦值操作,則賦值操作只有被執(zhí)行到的時候才會發(fā)生,而不是創(chuàng)建的時候。
我期待這篇文章會對那些對javascript比較迷惑的程序員帶來一絲光明。我自己也盡最大的可能去避免帶來更多的迷惑。如果我說錯了什么,或者忽略了什么,請告知。
譯者補充
xu281828044提醒了我發(fā)現(xiàn)了IE下全局作用域下命名函數(shù)的提升問題:
我翻譯文章的時候是這么測試的:
- <script>
- functiont(){
- spam();
- var baz = function spam() {alert('this is spam')};
- }
- t();
- </script>
這種寫法, 即非全局作用域下的命名函數(shù)的提升,在ie和ff下表現(xiàn)是一致的. 我改成:
- <script>
- spam();
- var baz = function spam() {alert('this is spam')};
- </script>
則ie下是可以執(zhí)行spam的,ff下不可以. 說明不同瀏覽器在處理這個細節(jié)上是有差別的.
這個問題還引導(dǎo)我思考了另2個問題,1:對于全局作用于范圍的變量,var與不var是有區(qū)別的. 沒有var的寫法,其變量不會被提升。比如下面兩個程序,第二個會報錯:
- <script>
- alert(a);
- var a=1;
- </script>
- <script>
- alert(a);
- a=1;
- </script>
2: eval中創(chuàng)建的局部變量是不會被提升的(它也沒辦法做到).
- <script>
- var a = 1;
- function t(){
- alert(a);
- eval('var a = 2');
- alert(a);
- }
- t();
- alert(a);
- </script>
原文地址:http://www./JavaScript-Scoping-and-Hoisting.html
轉(zhuǎn)載請注明來自: http://blog.csdn.net/sunxing007
|