面試題:箭頭函式和普通函式的區別

山頭人漢波發表於2022-04-21

去年面試的時候,五位面試官有三位問到了這個問題,可見這是一個面試常題,我都忘記自己是怎麼回答的,要我現在說:箭頭函式沒有 this 繫結,它的 this 指向父作用域

果然,記憶記不牢是有原因的,因為沒有寫文章,沒有理解真正理解它

真正的答案是什麼?

阮一峰版

  • 箭頭函式沒有自己的 this 物件,函式體內的 this 是定義時所在的物件而不是使用時所在的物件
  • 不可以當作建構函式,也就是說,不可以對箭頭函式使用 new 命令,否則會丟擲一個錯誤
  • 不可以使用 arguments 物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替
  • 不可以使用 yield 命令,因此箭頭函式不能用作 Generator 函式
  • 返回物件時必須在物件外面加上括號

尼古拉斯版

  • 沒有 this、super、arguments 和 new.target 繫結。this、super、arguments 和 new.target 的值由最近的不包含箭頭函式的作用域決定
  • 不能被 new 呼叫,箭頭函式內部沒有 [[Construct]] 方法,因此不能當作建構函式使用,使用 new 呼叫箭頭函式會丟擲錯誤
  • 沒有 prototype,既然你不能使用 new 呼叫箭頭函式,那麼 prototype 就沒有存在的理由。箭頭函式沒有 prototype 屬性
  • 不能更改 this, this 的值在函式內部不能被修改。在函式的整個生命週期內 this 的值是永恆不變的
  • 沒有 arguments 物件,既然箭頭函式沒有 arguments 繫結,你必須依賴於命名或者剩餘引數來訪問該函式的引數
  • 不允許重複的命名引數
尼古拉斯是寫《深入理解 ES6》的作者,阮一峰就不解釋了

結合起來,就是說箭頭函式和普通函式的區別在於:

  • 它不能被當作建構函式,因為它不能被new,不能被 new 的原因在於箭頭函式內部沒有 [[Construct]] 方法。又因為它不能被 new,所以也就沒有 prototype
  • 它沒有自己的 this,它的 this 由定義時所在的物件決定而不是使用時所在的物件
  • 它也沒有 arguments 物件
  • 不可以使用 yield 命令,不能用作生成器函式

我們依次說說這四點

new 從何來

先複習一下 new 呼叫建構函式會執行什麼

  1. 在記憶體中建立一個新物件
  2. 這個新物件內部的 [[prototype]] 特性被賦值為建構函式的 prototype 屬性
  3. 建構函式內部的 this 被賦值為這個新物件(this指向新物件)
  4. 執行建構函式內部的程式碼(給新物件新增屬性)
  5. 如果建構函式返回非空物件,則返回該物件;否則,返回剛建立的新物件

我們可以手寫一個 new

function new2(Constructor, ...args) {
    var obj = Object.create(null);
    obj.__proto__ = Constructor.prototype;
    var result = Constructor.apply(obj, ...args)
    return typeof result === 'object' ? result : obj
}

複習完 new,回過頭看為什麼不能呼叫 new

JavaScript 函式內部有兩個內部方法:[[Call]] 和 [[Construct]]

  • 直接呼叫時執行[[Call]] 方法,直接執行函式體
  • new 呼叫時執行 [[Construct]] 方法,建立例項物件

箭頭函式設計之初是為了設計一種更簡短的函式,沒有 [[Construct]] 方法。具體99.9%的人都不知道的箭頭函式不能當做建構函式的祕密 摘出了很多英文材料佐證這個事實

我們可以這樣說,因為它沒有[[Construct]] 內部方法,所以它不能被 new。而因為它不能被 new,所以它也沒有 prototype

prototype 的理解可以看這篇: 原型

this 誰人呼叫你

JavaScript 中的 this 是詞法作用域,與你在哪裡定義無關,而與你在哪裡呼叫有關,所以會有各種 this “妖”的問題,改變 this 有 4 種方法

  • 作為物件方法呼叫
  • 作為函式呼叫
  • 作為建構函式呼叫
  • 使用 apply 或 call 呼叫

但是箭頭函式沒有自己的 this 物件,內部的 this 就是定義時上層作用域中的this。也就是說,箭頭函式內部的 this 指向是固定的

arguments 老一輩的類陣列

arguments 是一個對應於傳遞給函式的引數的類陣列物件。arguments 物件標識所有(非箭頭)函式可用的區域性變數,可以說只要是(非箭頭)函式就自帶 arguments,它表示所有傳遞給函式的引數

什麼是類陣列物件

所謂類陣列物件,就是指可以通過索引屬性訪問元素並且擁有 length 屬性的物件

var arrLike = {
    0: 'name',
    1: 'age',
    length: 2
}

箭頭函式沒有

yield 是什麼

說 yield 之前,先了解下生成器

生成器是 ES6 新增的一個極為靈活的結構,擁有在一個函式塊內暫停和恢復程式碼執行的能力。

生成器的形式是一個函式,函式名稱前面加一個星號(*)表示它是一個生成器。只要是可以定義(非箭頭)函式的地方,就可以定義生成器

// 生成器函式宣告
function* generatorFn() {}

// 生成器函式表示式
let generatorFn = function* () {}

// 作為物件字面量方法的生成器函式
let foo = {
    * generatorFn() {}
}

// 作為類例項方法的生成器函式
class Foo {
    * generatorFn() {}
}

// 作為類靜態方法的生成器函式
class Bar {
    static * generatorFn() {}
}

標識生成器函式的星號不受兩側空格的影響

而 yield 關鍵字是可以讓生成器停止和開始執行,也是生成器最有用的地方。生成器函式在遇到 yield 關鍵字之前會正常執行。遇到這個關鍵字後,執行會停止,函式作用域的狀態會被保留。停止執行的生成器函式只能通過在生成器物件上呼叫 next() 方法來恢復執行

// umi 專案中請求介面時的例子
*fetchData({ payload }, { call, put }) {
    const resData = yield call(fetchApi, payload);
    if (resData.code === 'OK') {
        yield put({
            type: 'save',
            payload: {
                data: resData,
            },
        });
    } else {
        Toast.show(resData.resultMsg);
    }
},

因為箭頭函式不能用來定義生成器函式才不能使用 yield 關鍵字

模擬面試

面試官:對 ES6 瞭解嗎

面試者:嗯呢,專案中一直有用

面試官:你說說你平時都用哪些 ES6 的新特性

面試者:例如箭頭函式、let、const、模板字串、擴充套件運算子、Promise...

面試官:嗯嗯,箭頭函式和普通函式有什麼區別

面試者:箭頭函式不能被 new、沒有 arguments、它的 this 與在那裡定義相關、它不能用 yield 命令,返回物件時必須在物件外面加上括號

面試官:箭頭函式為什麼不能被 new

面試者:因為箭頭函式沒有 [[Construct]] 方法,在 new 時,JavaScript 內部會呼叫 [[Construct]] 方法,因為箭頭函式沒有,所以 new 時會報錯。當然,因為不能被 new ,所以箭頭函式也沒有 prototype

面試官:你剛剛說到沒有 arguments,簡單介紹下它

面試者:它是所有引數的合集,每個(非箭頭)函式自帶 arguments,其結構是類陣列物件

面試官:什麼是類陣列物件

面試者:可以通過索引訪問元素且擁有 length 屬性的物件...

面試官:我問問其他的

參考資料

本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。

相關文章