聽說會做這道題的人後來都進了頭條?
寫在前面
在面試的過程中,相信好多朋友都經歷過一些百思不得其姐的題目,或難題,或怪題,或偏題。今天咱們一起來看一道相對偏、但其實又很基礎的面試題。這道題是我的一個哥們兒,在半年前去面試位元組跳動廣州分公司的時候遇到的。他說當時不會做,回來後和分享的(其實是請教嘿嘿嘿~)。
知識預備
作為專業的切圖仔,我們對於 Function.prototype.call
方法肯定不陌生(後面簡寫成 Function#call
) ,它的作用是用來指定 this
物件的,或者也可以通俗一點說,是用來讓一個物件 借用
另外一個物件的某個方法的。比如陣列有個方法叫做 Array#push
, 然後我們自己發明了一個假陣列(是指有 length
push
方法來新增元素,可能會寫出以下程式碼:
const arrayLike = {
length: 0
}
[].push.call(arrayLike, 1)
console.log(arrayLike); // {0: 1, lenght: 1}
第 4 行中就用到了 Funtion#call
方法。因為它能讓一個不擁有某個方法的物件,去借用其他物件的方法來呼叫 ,所以在很多庫和框架原始碼中的出鏡率都是非常高的。
引出問題
OK, Function#call
方法的介紹完畢。下面就來看看今日頭條的面試官小哥當初到底問了個什麼問題,竟然讓我的哥們兒百思不得其姐?
題目是這樣的:
以下程式碼是可以正常執行的:
const arrayLike = {
length: 0
}
[].push.call(arrayLike, 1);
console.log(arrayLike); // {0: 1, lenght: 1}
但是把如果把 [].push.call
方法賦值到變數後再呼叫 ,在 chrome 卻會報錯:
const arrayLike = {
length: 0
}
const call = [].push.call;
call(arrayLike, 1);
console.log(arrayLike);
請問這是什麼原因呢?
把 Function#call
啊!call is not a function
? 這是在逗我嘛?於是趕緊用 typeof
操作符來驗證一下到底是不是 function
此時此刻真想罵人了,有木有? 呼叫 call()
會報錯 call is not a function
, 而使用 typeof
檢測 call
的型別卻是 function
! 你說氣不氣人?
解決問題
冷靜冷靜!面試遇到不懂的問題一定要冷靜,如果緊張就真的不知道怎麼辦,只能涼拌了。那冷靜又能怎麼辦呢?冷靜之後, 就要嘗試從從實現原理或原始碼來尋找思路。
下面就根據 Function#call
基本用法,來自己實現一個叫做 Function#myCall
的方法。
Function.prototype.myCall = function(context, ...args) {
context = context || {};
context.fn = this;
const res = context.fn(...args);
delete context.fn;
return res;
}
下面來驗證一下 myCall
的功能:
const arrayLike = {
length: 0
}
[].push.myCall(arrayLike, 1)
console.log(arrayLike); // {0: 1, lenght: 1}
OK, 是可以正常工作了。
然後,如果把 [].push.myCall
賦值到變數再呼叫會怎樣呢?會不會也跟原生的 call
一樣報錯?如下:
const arrayLike = {
length: 0
}
const myCall = [].push.myCall;
myCall(arrayLike, 1);
console.log(arrayLike);
在 chrome 中執行結果如下:
錯誤資訊是 context.fn is not a function
, 從原始碼中我們可以看到 context.fn
實際上就是 this
, 因為原始碼中有 context.fn = this
的賦值語句。 那這個 this
到底是什麼呢?如果在瀏覽器端就是 window
, 如果在 node 端就是 global
,因為他們確實都不是 function
型別。所以呼叫就會報錯。
好聽,到此大家應該已經明白了為什麼 Functio#call
不能先賦值給變數後再呼叫了。
其他環境
但是再來看另外一個問題,chrome 的錯誤資訊 call is not function
, 其實是非常不友好的錯誤資訊,甚至是自相矛盾的。因為他說 call
變數的值不是 function
, 而用 typeof
檢測卻是 function
。所以這道題目的本身其實並不難,而是被 chrome
的極不友好的錯誤資訊給誤導了。
下面讓我們來看看其他瀏覽器或 node 端的錯誤資訊:
IE11 瀏覽器
edge 瀏覽器:
firefox 瀏覽器
node 伺服器
對比以上不同的瀏覽器,會發現,node 伺服器 和 chrome 瀏覽器的錯誤資訊是一樣的,都是 call is not a function
, 這是因為他們都用了 v8 直譯器的原因。而 IE11 和 edge 的錯誤資訊也基本一樣,都是 this is not a Function object
。 而 firefox 的錯誤資訊是 Function.prototype.call called on incompatible undefined
,這是什麼意思呢??我也不知道,於是悄悄的點選了錯誤資訊右邊的 [詳細瞭解]
,然後打開了 mdn 上關於 X.prototype.y called on incompatible type 的詳情頁面, 裡面有對這種錯誤的詳細介紹,並列舉出了比如 Funtion#call
, Function#apply
, Function#bind
等, 使用不當都會發生這樣的錯誤。
來個總結
優雅的報錯也是很難的, 關於 v8 引擎的 issue 6513 就是專門做這件事的。這個 issue 裡有人提議:
var c = Function.prototype.call; c();
Uncaught TypeError: c is not a function
Should be something like:
c is called with undefined as a context which is not a function
或許改成提議那樣確實會優雅一些。
另外,如果我們在平時工作中或面試中,遇到一些百思不得其姐的問題,不要憑空去猜測,要透過本質和原理去分析問題的根源。
還有後話
標題只是開玩笑的啦,如果你會做這道題目不一定能進頭條,哈哈哈。但是如果不會做,就說明基本功不夠紮實,就很有可能過不了面試啦