首頁 >web前端 >js教程 >理解 JavaScript Scoping & Hoisting(二)_javascript技巧

理解 JavaScript Scoping & Hoisting(二)_javascript技巧

WBOY
WBOY原創
2016-05-16 15:31:171236瀏覽

Scoping & Hoisting

var a = 1;

function foo() {
  if (!a) {
    var a = 2;
  }
  alert(a);
};

foo();

上面這段程式碼在執行時會產生什麼結果?

儘管對於有經驗的程式設計師來說這只是小菜一碟,不過我還是順著初學者常見的思路做一番描述:

1.建立了全域變數 a,定義其值為 1
2.創建了函數 foo
3.在 foo 的函數體內,if 語句將不會執行,因為 !a 會將變數 a 轉換成布林的假值,也就是 false
4.跳過條件分支,alert 變數 a,最終的結果應該是輸出 1

嗯,看起來無懈可擊的推理啊,但讓人驚訝的是:答案竟然是 2!為什麼?

別急,我會解釋給你聽。首先我要告訴你這不是錯誤,而是JavaScript 語言解釋器的一個(非官方的)特性,某人(Ben Cherry)把這個特性叫做:Hoisting(目前尚未有標準的翻譯,比較常見的是提升)。

聲明與定義

為了理解 Hoisting,我們先來看一個簡單的情況:

var a = 1;

你是否想過,上面這句程式碼在運作的時候到底發生了什麼事?
 你是否知道,就這句程式碼而言,「宣告變數 a」 和 「定義變數 a」這兩個說法哪一個才是正確的?
•下例叫做 「宣告變數」:

var a;

•下例叫做 「定義變數」:

var a = 1;

•聲明:是指你聲稱某樣東西的存在,例如一個變數或一個函數;但你沒有說明這樣東西到底是什麼,僅僅是告訴解釋器這樣東西存在而已;
•定義:是指你指明了某樣東西的具體實現,例如一個變數的值是多少,一個函數的函數體是什麼,確切的表達了這樣東西的意義。

總結一下:

var a;            // 這是說明
a = 1;            // 這是定義(賦值)
var a = 1;        // 合而為一:宣告變數的存在並賦值給它

重點來了:當你以為你只做了一件事情的時候(var a = 1),實際上解釋器把這件事情分解成了兩個步驟,一個是聲明(var a),另一個是定義(a = 1)。

這和 Hoisting 有何關係?

回到最開始的那個令人困惑的例子,我告訴你解釋器是如何分析你的程式碼的:

var a;
a = 1;

function foo() {
  var a;    // 关键在这里
  if (!a) {
    a = 2;
  }
  alert(a);   // 此时的 a 并非函数体外的那个全局变量
}

如程式碼所示,在進入函數體後解釋器宣告了新的變數 a,而無論 if 語句的條件為何,都將為新的變數 a 賦值為 2。你若不相信可以在函數體外面 alert(a),然後再執行 foo() 對比一下結果就知道了。

Scoping(作用域)

有人可能會問了:「為什麼不是在 if 語句內宣告變數 a?」

因為 JavaScript 沒有區塊級作用域(Block Scoping),只有函數作用域(Function Scoping),所以說不是看見一對花括號 {} 就代表產生了新的作用域,和 C 不一樣!

當解析器讀到if 語句的時候,它發現此處有一個變數宣告和賦值,於是解析器會將其宣告提升至目前作用域的頂部(這是預設行為,並且無法變更),這個行為就叫做Hoisting。

OK,大家都懂了,你懂了嗎…

懂了不代表就會用了,就拿最開始的例子來說,如果我就是想要 alert(a) 出那個 1 可咋整呢?

建立新的作用域

alert(a) 在執行的時候,會去尋找變數 a 的位置,它從目前作用域開始向上(或說向外)一直查找到頂層作用域為止,若是找不到就報 undefined。

因為在alert(a) 的同級作用域裡,我們再次聲明了本地變數a,所以它報2;所以我們可以把本地變數a 的聲明向下(或者說是向內)移動,這樣alert (a) 就找不到它了。

記住:JavaScript 只有函數作用域!

var a = 1;

function foo() {
  if (!a) {
    (function() {    // 这是上一篇说到过的 IIFE,它会创建一个新的函数作用域
      var a = 2;    // 并且该作用域在 foo() 的内部,所以 alert 访问不到
    }());        // 不过这个作用域可以访问上层作用域哦,这就叫:“闭包”
  };
  alert(a);
};

foo();

你或许在无数的 JavaScript 书籍和文章里读到过:“请始终保持作用域内所有变量的声明放置在作用域的顶部”,现在你应该明白为什么有此一说了吧?因为这样可以避免 Hoisting 特性给你带来的困扰(我不是很情愿这么说,因为 Hoisting 本身并没有什么错),也可以很明确的告诉所有阅读代码的人(包括你自己)在当前作用域内有哪些变量可以访问。但是,变量声明的提升并非 Hoisting 的全部。在 JavaScript 中,有四种方式可以让命名进入到作用域中(按优先级):

1.语言定义的命名:比如 this 或者 arguments,它们在所有作用域内都有效且优先级最高,所以在任何地方你都不能把变量命名为 this 之类的,这样是没有意义的
2.形式参数:函数定义时声明的形式参数会作为变量被 hoisting 至该函数的作用域内。所以形式参数是本地的,不是外部的或者全局的。当然你可以在执行函数的时候把外部变量传进来,但是传进来之后就是本地的了
3.函数声明:函数体内部还可以声明函数,不过它们也都是本地的了
4.变量声明:这个优先级其实还是最低的,不过它们也都是最常用的

另外,还记得之前我们讨论过 声明 和 定义 的区别吧?当时我并没有说为什么要理解这个区别,不过现在是时候了,记住:

Hosting 只提升了命名,没有提升定义

这一点和我们接下来要讲到的东西息息相关,请看:

函数声明与函数表达式的差别

先看两个例子:

function test() {
  foo();

  function foo() {
    alert("我是会出现的啦……");
  }
}

test();
function test() {
  foo();

  var foo = function() {
    alert("我不会出现的哦……");
  }
}

test();

同学,在了解了 Scoping & Hoisting 之后,你知道怎么解释这一切了吧?

在第一个例子里,函数 foo 是一个声明,既然是声明就会被提升(我特意包裹了一个外层作用域,因为全局作用域需要你的想象,不是那么直观,但是道理是一样的),所以在执行 foo() 之前,作用域就知道函数 foo 的存在了。这叫做函数声明(Function Declaration),函数声明会连通命名和函数体一起被提升至作用域顶部。

然而在第二个例子里,被提升的仅仅是变量名 foo,至于它的定义依然停留在原处。因此在执行 foo() 之前,作用域只知道 foo 的命名,不知道它到底是什么,所以执行会报错(通常会是:undefined is not a function)。这叫做函数表达式(Function Expression),函数表达式只有命名会被提升,定义的函数体则不会。

尾记:Ben Cherry 的原文解释的更加详细,只不过是英文而已。我这篇是借花献佛,主要是更浅显的解释给初学者听,若要看更多的示例,请移步原作,谢谢。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn