最近在一個前端學習羣裏,有人拋出了這麼一道 JS 面試題。javascript
var foo = 1; (function foo(){ foo = 100; console.log(foo); }()) console.log(foo);
我一看,這不很簡單嗎?IIFE 局部的 foo
原本指向函數自己,但後來被修改爲 100 了,因此局部的 foo
打印 100。全局的 foo
仍是保留原來的值,因此全局的 foo
打印 1。html
而後我複製代碼到控制檯運行,發現先打印函數體 foo(){...}
,而後再打印 1
。前端
我猜測的第一個打印結果錯了,一番查找資料終於搞懂了,因而有了這篇文章。java
如下表示形式的是函數聲明式,簡單說就是 function
前面沒有任何運算符,其實就下面一種形式。git
function name() { ... }
如下表示形式的是函數表達式,有多種形式。github
var fun = function name() { ... } // 函數前帶有 + - * / () && || 等運算符號 (function name(){ ... }()) // 又或者 +function name(){ ... }()
能認出函數聲明式與函數表達式後,咱們來看看二者有什麼區別。面試
稍微瞭解 JS 的都知道變量提高(variable hoisting),除此以外還有函數提高(function hoisting),也就是說下面的代碼是正常運行的。express
foo(); // running function foo() { console.log('running'); }
可是函數提高只對函數聲明式有效,對函數表達式不生效,下面的代碼就會報錯。ide
foo(); // Uncaught TypeError: foo is not a function var foo = function () { console.log('running'); }
區別一:函數聲明式會提高函數定義,而函數表達式不提高函數定義。這一區別只是想給你們複習知識點,並非本文的重點。函數
先看看下面函數的表示形式,記住它有助於接下來的說明。
function BindingIdentifier (FormalParameters) { FunctionBody }
函數聲明式和函數表達式的另一個關鍵區別是,看函數名(BindingIdentifier)綁定到哪一個做用域下。
先看下 ECMAScript 是怎麼描述這一區別的。
The BindingIdentifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the BindingIdentifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.
上面說 BindingIdentifier(函數的引用) 能夠用於在函數表達式內遞歸調用自身。並且函數表達式的 BindingIdentifier 只綁定在該函數內部,不污染外部的做用域,外部做用域也沒法訪問到 BindingIdentifier。
區別二:函數聲明式的 BindingIdentifier 綁定在聲明時的做用域下,函數表達式的 BindingIdentifier 綁定在函數內部的做用域下。
說了這麼多,好像還沒說的真正的緣由。是的,前面的內容只是鋪墊,有了上面的內容,才能更好理解背後的緣由。
解釋前先說緣由:
緣由出自《You-Dont-Know-JS》的一個 issue,這一 issue 已被做者歸入第二版(second edition)的編寫中。
The production FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody } is evaluated as follows: ... Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument. ...
調用 CreateImmutableBinding
建立 Immutable's 函數名。
For each FunctionDeclaration f in code, in source text order do ... If funcAlreadyDeclared is false, call env’s CreateMutableBinding concrete method passing fn and configurableBindings as the arguments. ...
調用 CreateMutableBinding
建立 Mutable's 函數名。
固然也能夠從 ECMAScript 規範中找到緣由:Runtime Semantics: Evaluation。
至於語言爲何要這麼規定,我也沒想明白,若是有知道的同窗能夠分享一下。
那回頭再分析下一開始的示例,從每一行註釋能夠幫助理解背後的緣由。
var foo = 1; // 在外部做用域聲明foo=1 // IIFE是典型的函數表達式 (function foo(){ // 函數名foo,引用函數自身,綁定在函數內部,不污染外部做用域 foo = 100; // 這裏修改了foo,但規範規定不能修改,但不會報錯 console.log(foo); // 仍是引用函數自身 }()) console.log(foo); // 外部做用域一直是1
一樣的代碼,當函數運行在嚴格模式下,報錯提示說:「不能賦值給常量」。也就是說函數表達式的函數名被定義成常量,沒法再修改了。
var foo = 1; (function foo(){ 'use strict'; // 嚴格模式 foo = 100; // Uncaught TypeError: Assignment to constant variable console.log(foo); }()) console.log(foo);
爲了幫助對比理解,下面給出了函數聲明式的示例及解釋,下面的代碼不管在非嚴格模式仍是嚴格模式下都打印100,也就是說函數聲明式的函數名能夠被修改。
// foo是函數聲明式 function foo(){ // 函數名foo,引用函數自身,綁定在聲明時的做用域下 foo = 100; // 修改了foo,函數聲明式內能夠從新修改函數名 console.log(foo); // 100 } foo();
若是在函數表達式內使用 var foo = 100;
來從新聲明變量,那這個變量就不是不可修改的(ImmutableBinding),因此內部的 foo 打印 100。
var foo = 1; (function foo(){ var foo = 100; // 從新聲明變量 console.log(foo); // 100 }()) console.log(foo); // 1
經過上面的分析解釋,但願你能夠掌握這道面試題,觸類旁通。
若是你喜歡這篇文章,請關注我,我會持續輸出更多原創且高質量的內容。
原文連接:【理解】一道 JS 面試題