【面試利器】原生JavaScript靈魂拷問,你能答上多少(一)

語言: CN / TW / HK

小知識,大挑戰!本文正在參與「程式設計師必備小知識」創作活動

本文已參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金。

前言

當下的前端開發中,三大框架橫行,框架的強大讓我們很難再提起對原生 JavaScript 的興趣,原生 JavaScript 所用也越來越少。

但我認為 JavaScript 作為每一個前端工程師的立身之本,不止要學會,還要學好、學精,學再多遍都不為過。

另一方面,前端面試中,越來越重視原生 JavaScript 的考察,其所佔比例也越來越高。

我抓取了牛客上今年的線上面試題和麵經,大約 500 左右道題,原生 JavaScript 的難點(閉包,eventLoop,this,手撕原生JS)考察的頻率非常高。

完整的分析我還正在趕工中,希望大家到時候可以來支援一下。

因此我決定整理JavaScript中容易忽視或者混淆的知識點,寫一系列篇文章,以靈魂拷問的方式,系統且完整的帶大家遨遊原生 JavaScript 的世界,希望能給大家帶來一些收穫。

JS型別之問——概念與檢測篇

第一問:JS中的資料型別有哪些?

  1. 基本資料型別:共有7種 js Boolean Number String undefined null Bigint Symbol SymbolES6 引入的一種新的原始值,表示獨一無二的值,主要為了解決屬性名衝突問題。

BigintES2020 新增加,是比 Number 型別的整數範圍更大。

  1. 引用資料型別:1種 js Object物件(包括普通Object、Function、Array、Date、RegExp、Math)

第二問:你真的懂typeof嗎?

  1. typeof的作用?

區分資料型別,可以返回7種資料型別:number、string、boolean、undefined、object、function ,以及 ES6 新增的 symbol 2. typeof 能正確區分資料型別嗎?

不能。對於原始型別,除 null 都可以正確判斷;對於引用型別,除 function 外,都會返回 "object" 3. typeof 注意事項 + typeof 返回值為 string 格式,注意類似這種考題: typeof(typeof(undefined)) -> "string" + typeof 未定義的變數不會報錯,返回 "undefiend" + typeof(null) -> "object": 遺留已久的 bug + typeof無法區別陣列與普通物件: typeof([]) -> "object" + typeof(NaN) -> "number" 4. 習題 js console.log(typeof(b)); console.log(typeof(undefined)); console.log(typeof(NaN)); console.log(typeof(null)); var a = '123abc'; console.log(typeof(+a)); console.log(typeof(!!a)); console.log(typeof(a + "")); console.log(typeof(typeof(null))); console.log(typeof(typeof({}))); 答案 js undefined // b未定義,返回undefined undefined number // NaN 為number型別 object number // +a 型別轉換為NaN boolean string string // typeof(null) -> "object"; typeof("object") -> "string" string

第三問:什麼是instanceof?你能模擬實現一個instanceof嗎?

  1. instanceof 判斷物件的原型鏈上是否存在建構函式的原型。只能判斷引用型別。
  2. instanceof 常用來判斷 A 是否為 B 的例項

js // A是B的例項,返回true,否則返回false // 判斷A的原型鏈上是否有B的原型 A instaceof B 3. 模擬實現 instanceof

思想:沿原型鏈往上查詢

js function instance_of(Case, Constructor) { // 基本資料型別返回false // 相容一下函式物件 if ((typeof(Case) != 'object' && typeof(Case) != 'function') || Case == 'null') return false; let CaseProto = Object.getPrototypeOf(Case); while (true) { // 查到原型鏈頂端,仍未查到,返回false if (CaseProto == null) return false; // 找到相同的原型 if (CaseProto === Constructor.prototype) return true; CaseProto = Object.getPrototypeOf(CaseProto); } }

測試: js console.log(instance_of(Array, Object)) // true function User(name){ this.name = name; } const user = new User('zc'); const vipUser = Object.create(user); console.log(instance_of(vipUser, User)) // true

第四問:如何區分陣列與物件?使用instanceof判斷陣列可靠嗎?

  1. ES6 提供的新方法 Array.isArray()
  2. 如果不存在Array.isArray()呢?可以藉助Object.prototype.toString.call() 進行判斷,此方式相容性最好 js if (!Array.isArray) { Array.isArray = function(o) { return typeof(o) === 'object' && Object.prototype.toString.call(o) === '[object Array]'; } }
  3. instanceof 判斷

判斷方式 js // 如果為true,則arr為陣列 arr instanceof Array instanceof 判斷陣列型別如此之簡單,為何不推薦使用那?

instanceof 操作符的問題在於,如果網頁中存在多個 iframe ,那便會存在多個 Array 建構函式,此時判斷是否是陣列會存在問題。

更詳細的內容可以參考博文:JavaScript為啥不用instanceof檢測陣列

第五問:如何判斷一個數是否為NaN?

NaN 有個非常特殊的特性, NaN 與任何值都不相等,包括它自身 js NaN === NaN // false NaN == NaN // false 鑑於這個獨特的特性,可以手撕一個比較簡單的判斷函式 js function isNaN(x) { return x != x; }

  • 全域性函式 isNaN 方法:不推薦使用。MDN 對它的介紹是:isNaN 函式內包含一些非常有趣的規則。

但為了避免一些面試官出一些冷門題目,咱們來稍微瞭解一下 isNaN 的有趣機制:會先判斷引數是不是 Number 型別,如果不是 Number 型別會嘗試將這個引數轉換為 Number 型別,之後再去判斷是不是 NaN

舉個例子: js // 為什麼物件會帶來三種不同的結果 // 是不是很有趣 // 具體原因可以參考型別轉換篇 console.log(isNaN([])) // false console.log(isNaN([1])) // false console.log(isNaN([1, 2])) // true console.log(isNaN(null)) // false console.log(isNaN(undefined)) // true isNaN 的結果很大程度上取決於 Number() 型別轉換的結果,關於 Number 的轉換結果,後面會專門有一部分來介紹。 + Number.isNaN (推薦使用)

isNaN() 相比,Number.isNaN() 不會自行將引數轉換成數字,只有在引數是值為 NaN 的數字時,才會返回 true

第六問:如何實現一個功能完善的型別判斷函式?

Object.prototype.toString.call([value]) ,可以精準判斷資料型別,因此可以根據這個原理封裝一個自己的 type 方法。 js toString.call(()=>{}) // [object Function] toString.call({}) // [object Object] toString.call([]) // [object Array] toString.call('') // [object String] toString.call(22) // [object Number] toString.call(undefined) // [object undefined] toString.call(null) // [object null] toString.call(new Date) // [object Date] toString.call(Math) // [object Math] toString.call(window) // [object Window]

JS型別之問——型別轉換篇

第七問:toString 和 valueOf 方法有什麼區別?

  1. 基礎:這兩個方法屬於 Object 物件,是為了解決 JavaScript 值運算與顯示的問題。為了更適合自身功能,很多 JavaScript 內建物件都重寫了這兩個方法。
  2. toString(): 返回當前物件的字串形式;valueOf() : 返回該物件的原始值
  3. 各個型別下兩個方法返回值情況對比

| 型別 | valueOf | toString | | ---- | ---- | ---- | | Array[1,2,3] | 陣列本身[1, 2, 3] | 1,2,3 | | Object | 物件本身 | [object Object] | | Boolean型別 | Boolean值 | "true"或"false" | | Function | 函式本身 | function fnName(){code}| | Number | 數值 | 數值的字元換表示 | | Date | 毫米格式時間戳 | GMT格式時間字串 |

  1. 呼叫優先順序

隱式轉換時會自動呼叫 toStringvalueOf 方法,兩者優先順序如下: + 強制轉化為字串型別時,優先呼叫 toString 方法 + 強制轉換為數值型別時,優先呼叫 valueOf 方法 + 使用運算子操作符情況下,valueOf 優先順序高於 toStirng + 物件的型別轉換見下一問。

第八問:你知道物件轉換成原始值是什麼流程嗎 (ToPrimitive)?

物件轉換成原始型別,會呼叫內建的 [ToPrimitive]函式

(參考部落格: 從ECMA規範徹底理解 JavaScript 型別轉換)

  • ToPrimitive 方法接受兩個引數,一個是輸入的值 input,一個是期望轉換的型別 PreferredType
  • 如果未傳入 PreferredType 引數,讓 hint 等於 'default',後面會將 hint 修改為 'number'
  • 如果 PreferredTypehint String,讓 hint 等於 'string'
  • 如果 PreferredTypehint Number,讓 hint 等於 'number'
  • 返回 OrdinaryToPrimitive(input, hint)
  • OrdinaryToPrimitive(input, hint)
  • 如果 hint'string',那麼就將 methodNames 設定為 toString、valueOf
  • 如果 hint'number',那麼就將 methodNames 設定為 valueOf、toString

methodName 儲存的就是當前 preferredType 下的呼叫優先順序,如果全部呼叫完畢仍然未轉化為原始值,會發生報錯。

第九問:你能做出下面這個題嗎?

```js const a = {x:1}; const b = {x:2}; const obj = {}; obj[a] = 100; obj[b] = 200;

console.log(obj[a]); console.log(obj[b]); ```

有了第七問和第八問的知識,這個題目就不難了。 JavaScript 物件的鍵必須是字串,因此分別需要將物件 ab 轉換為 string 型別。具體轉換流程: js // 1.執行ToPrimitive // hint 為 string ToPrimitive(a, 'hint String') // 2.執行OrdinaryToPrimitive OrdinaryToPrimitive(a, 'string') // 3.返回methodNames methodNames = ['toString', 'valueOf'] // 4.呼叫methodNames裡方法 // 呼叫toString a.toString() // 返回[object Object] 物件 ab 轉換後的結果都是 [object Object]obj 物件上只添加了一個屬性 [object Object]

答案 js 200 200

第十問:你能理清型別轉換嗎?

首先需要知道:在JavaScript中,只有三種類型的轉換 + 轉換為Number型別: Number() / parseFloat() / parseInt() + 轉化為String型別:String() / toString() + 轉化為Boolean型別: Boolean()

因此遇到型別轉換問題,只需要弄清楚在什麼場景之下轉換成那種型別即可。

轉換為boolean

  • 顯式:Boolean 方法可以顯式將值轉換為布林型別
  • 隱式:通常在邏輯判斷或者有邏輯運算子時觸發(|| && !js Boolean(1) // 顯式型別轉換 if (1) {} // 邏輯判斷型別觸發隱式轉換 !!1 // 邏輯運算子觸發隱式轉換 1 || 'hello' // 邏輯運算子觸發隱式轉換 boolean 型別只有 truefalse 兩種值。

除值 0,-0,null,NaN,undefined,或空字串("")false 外,其餘全為 true

轉化為string

  • 顯式:String 方法可以顯式將值轉換為字串
  • 隱式:+ 運算子有一側運算元為 string 型別時

轉化為 string 型別的本質:需要轉換為string的部分呼叫自身的toString方法(null/undefined返回字串格式的null和undefined)

當被轉換值為物件時,相當於執行 ToPrimitive(input, 'hint String')

```js String([1,2,3]) // 1,2,3 String({x:1}) // [object Object]

1 + '1' // 11 1 + {} // 1[object Object] ```

轉化為number

  • 顯式:Number 方法可以顯式將值轉化為數字型別

Number 的具體規則,ES5 規範中給了一個對應的結果表

| 型別 | 結果 | | --- | --- | | undefined | NaN | | null | +0 | | Boolean | NaN | | undefined | 引數為true返回1;false返回+0 | | Number | 返回與之相等的值 | | String | 有些複雜,舉例說明 | | Object | 先執行ToPrimitive方法,在執行Number型別轉換 |

  1. String: 空字串返回 0,出現任何一個非有效數字字元,返回 NaN js console.log(Number("1 3")) // NaN console.log(Number("abc")) // NaN console.log(Number("1a")) // NaN console.log(Number("0x11")) // 17 console.log(Number("123")) // 123 console.log(Number("-123")) // -123 console.log(Number("1.2")) // 1.2

  2. 隱式:number的隱式型別轉換比較複雜,對需要隱式轉換的部分執行 Number

  3. 比較操作(<, >, <=, >=)
  4. 按位操作(| & ^ ~)
  5. 算數操作(+ - * / %) 注意:+的運算元存在字串時,為string轉換
  6. 一元 +- 操作

第十一問:== 的隱式轉換規則

  1. ==: 只需要值相等,無需型別相等;null, undefined== 下互相等且自身等
  2. == 的轉換規則:
被比較數B
Number String Boolean Object
比較數A
Number A == B A == ToNumber(B) A == ToNumber(B) A == ToPrimitive(B)
String ToNumber(A) == B A == B ToNumber(A) == ToNumber(B) ToPrimitive(B) == A
Boolean ToNumber(A) == B ToNumber(A) == ToNumber(B) ToNumber(A) == ToNumber(B) ToNumber(A) == ToPrimitive(B)
Object ToPrimitive(A) == B ToPrimitive(A) == B ToPrimitive(A) == ToPrimitive(B) A === B

在上面的表格中,ToNumber(A) 嘗試在比較前將引數 A 轉換為數字。ToPrimitive(A) 將引數 A 轉換為原始值( Primitive )。

第十二問:1 + {}{} + 1的輸出結果分別是什麼?

通過上面的學習,當物件與其他元素相加時,物件會呼叫 toPrimitive 轉化為原始值: 1. 執行 toPrimitive,未傳入 PreferredTypemethodNames[valueOf, toString] 2. 執行 ({}).valueOf,返回物件本身 {},不是原始值 3. 繼續執行 ({}).toString(),返回 "[object Object]",返回結果為原始值,轉換結束

此時 1 + {},右側為 string 型別,將 1 進行 ToString() 轉化為 "1" ,最後字串連線,結果為 "1[object Object]"

注意{} + 1 輸出的結果會和 1 + {} 一樣嗎?

{}JavaScript 中,不止可以作為物件定義,也可以作為程式碼塊的定義。js 引擎會把 {} + 1 解析成1個程式碼塊和1個+1,最終輸出結果為 1

答案 js 1[object Object] 1

第十三問:[]與{}的相加的結果是多少?

  1. [] + {}

陣列是特殊的物件,需要呼叫 toPrimitive,轉換為原始值 + 執行 toPrimitive,未傳入 PreferredTypemethodNames[valueOf, toString] + 執行 [].valueOf,返回陣列本身 + 執行 [].toString,返回空字串 ''

空物件不做贅述。

答案 js "[object Object]"

  1. [] + []

類似 1 兩個空陣列都執行 toPrimitive,返回兩個空字串。

答案 js "" 3. {} + []

類似於 {} + 1{} + [] 相當於 {}; + [],一元 + 強制將 "" 隱式轉換為0,最終結果為0

答案 js 0

  1. {} + {}

對於這個題,我先公佈一下答案,之後說一下我的疑問。

答案 js [object Object][object Object] 疑問

為什麼 JavaScript 引擎沒有將前面的 {} 解釋成程式碼塊?

友情提示:由於 {} 可以解釋為程式碼塊的形式,有些需要注意的地方,舉個栗子: + 空物件呼叫方法時:{}.toString() 會報錯 + 箭頭函式返回物件時:let getTempItem = id => { id: id, name: "Temp" } 會報錯

第十四問:你能靈活運用 parseInt 與 parseFloat 嗎

  1. parseInt:從數字類開始看,看到非數字類為止,返回原來的數。(小數點也屬於非有效數字) js parseInt('123x') -> 123 parseInt('-023x') -> -23 parseInt('1.1') -> 1 parseInt('-abc') -> NaN parseInt('x123') -> NaN
  2. parseInt(string, radix) 還有第二個引數 radix 表示要解析數字的基數,取值為 2~36 (預設值為10)
  3. parseFloatparseInt 類似,只不過它返回浮點數。從數字類開始看,看到除了第一個點以外的非數字類為截止,返回前面的數。

網紅題:['1','2','3'].map(parseInt)

這個網紅題考察的就是 parseInt 有兩個引數。 map 傳入的函式可執行三個引數: js // ele 遍歷的元素 // index 遍歷的元素索引 // arr 陣列 arr.map(function(ele, index, arr){}) ['1','2','3'].map(parseInt)相當於執行了以下三次過程: js parseInt('1', 0, ['1','2','3']) parseInt('2', 1, ['1','2','3']) parseInt('3', 2, ['1','2','3']) + parseInt('1', 0, ['1','2','3']): radix為0時,預設取10,最後返回1 + parseInt('2', 1, ['1','2','3']): radix取值為2~36,返回NaN + parseInt('3', 2, ['1','2','3']): radix取值為2,二進位制只包括0,1,返回NaN

第十五問:如何讓 if(a == 1 && a == 2) 條件成立?

valueOf 的應用 js var a = { value: 0, valueOf: function() { this.value++; return this.value; } }; console.log(a == 1 && a == 2); //true