【理解】一道 JS 面試題

最近在一個前端學習羣裏,有人拋出了這麼一道 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

函數聲明式 vs 函數表達式

如下表示形式的是函數聲明式,簡單說就是 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 綁定在函數內部的做用域下

背後的緣由

說了這麼多,好像還沒說的真正的緣由。是的,前面的內容只是鋪墊,有了上面的內容,才能更好理解背後的緣由。

解釋前先說緣由:

  • 函數表達式的函數名是不可修改的(ImmutableBinding)。但若是你真修改了,在非嚴格模式下會靜默失敗,在嚴格模式下會報錯(Uncaught TypeError: Assignment to constant variable)。
  • 函數聲明式的函數名是可修改的(MutableBinding)。

緣由出自《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 面試題

相關文章
相關標籤/搜索