JS進階知識點和常考面試題

手寫 call、apply 及 bind 函數

涉及面試題:call、apply 及 bind 函數內部實現是怎么樣的?

首先從以下幾點來考慮如何實現這幾個函數

  • 不傳入第一個參數,那么上下文默認為 window
  • 改變了 this 指向,讓新的對象可以執行該函數,并能接受參數
    那么我們先來實現 call
Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  delete context.fn
  return result
}

以下是對實現的分析:

  • 首先 context 為可選參數,如果不傳的話默認上下文為 window
  • 接下來給 context 創建一個 fn 屬性,并將值設置為需要調用的函數
  • 因為 call 可以傳入多個參數作為調用函數的參數,所以需要將參數剝離出來
  • 然后調用函數并將對象上的函數刪除

以上就是實現 call 的思路,apply 的實現也類似,區別在于對參數的處理,所以就不一一分析思路了

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 處理參數和 call 有區別
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

bind 的實現對比其他兩個函數略微地復雜了一點,因為 bind 需要返回一個函數,需要判斷一些邊界問題,以下是 bind 的實現

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一個函數
  return function F() {
    // 因為返回了一個函數,我們可以 new F(),所以需要判斷
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

以下是對實現的分析:

  • 前幾步和之前的實現差不多,就不贅述了
  • bind 返回了一個函數,對于函數來說有兩種方式調用,一種是直接調用,一種是通過 new 的方式,我們先來說直接調用的方式
  • 對于直接調用來說,這里選擇了 apply 的方式實現,但是對于參數需要注意以下情況:因為 bind 可以實現類似這樣的代碼 f.bind(obj, 1)(2),所以我們需要將兩邊的參數拼接起來,于是就有了這樣的實現 args.concat(...arguments)
  • 最后來說通過 new的方式,在之前的章節中我們學習過如何判斷 this,對于 new 的情況來說,不會被任何方式改變 this,所以對于這種情況我們需要忽略傳入的 this

new

涉及面試題:new 的原理是什么?通過 new 的方式創建對象和通過字面量創建有什么區別?

在調用 new 的過程中會發生以上四件事情:

  1. 新生成了一個對象
  2. 鏈接到原型
  3. 綁定 this
  4. 返回新對象

根據以上幾個過程,我們也可以試著來自己實現一個 new

function create() {
  let obj = {}
  let Con = [].shift.call(arguments)
  obj.__proto__ = Con.prototype
  let result = Con.apply(obj, arguments)
  return result instanceof Object ? result : obj
}

以下是對實現的分析:

  • 創建一個空對象
  • 獲取構造函數
  • 設置空對象的原型
  • 綁定 this 并執行構造函數
  • 確保返回值為對象

對于對象來說,其實都是通過 new 產生的,無論是 function Foo() 還是 let a = { b : 1 } 。

對于創建一個對象來說,更推薦使用字面量的方式創建對象(無論性能上還是可讀性)。因為你使用new Object()的方式創建對象需要通過作用域鏈一層層找到 Object,但是你使用字面量的方式就沒這個問題

function Foo() {}
// function 就是個語法糖
// 內部等同于 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()

instanceof 的原理

涉及面試題:instanceof 的原理是什么?

instanceof 可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype。

我們也可以試著實現一下 instanceof

function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}

以下是對實現的分析:

  • 首先獲取類型的原型
  • 然后獲得對象的原型
  • 然后一直循環判斷對象的原型是否等于類型的原型,直到對象原型為 null,因為原型鏈最終為 null

為什么 0.1 + 0.2 != 0.3

涉及面試題:為什么 0.1 + 0.2 != 0.3?如何解決這個問題?

先說原因,因為 JS 采用 IEEE 754 雙精度版本(64位),并且只要采用 IEEE 754 的語言都有該問題。

我們都知道計算機是通過二進制來存儲東西的,那么 0.1 在二進制中會表示為

// (0011) 表示循環
0.1 = 2^-4 * 1.10011(0011)

我們可以發現,0.1 在二進制中是無限循環的一些數字,其實不只是 0.1,其實很多十進制小數用二進制表示都是無限循環的。這樣其實沒什么問題,但是 JS 采用的浮點數標準卻會裁剪掉我們的數字。

IEEE 754 雙精度版本(64位)將 64 位分為了三段

  • 第一位用來表示符號
  • 接下去的 11 位用來表示指數
  • 其他的位數用來表示有效位,也就是用二進制表示 0.1 中的 10011(0011)

那么這些循環的數字被裁剪了,就會出現精度丟失的問題,也就造成了 0.1 不再是 0.1 了,而是變成了 ````0.100000000000000002``

0.100000000000000002 === 0.1 // true

那么同樣的,0.2 在二進制也是無限循環的,被裁剪后也失去了精度變成了 0.200000000000000002

0.200000000000000002 === 0.2 // true

所以這兩者相加不等于 0.3 而是 0.300000000000000004

0.1 + 0.2 === 0.30000000000000004 // true

那么可能你又會有一個疑問,既然 0.1 不是 0.1,那為什么 console.log(0.1) 卻是正確的呢?

因為在輸入內容的時候,二進制被轉換為了十進制,十進制又被轉換為了字符串,在這個轉換的過程中發生了取近似值的過程,所以打印出來的其實是一個近似值,你也可以通過以下代碼來驗證

console.log(0.100000000000000002) // 0.1

解決辦法

parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。