手寫 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
的過程中會發生以上四件事情:
- 新生成了一個對象
- 鏈接到原型
- 綁定 this
- 返回新對象
根據以上幾個過程,我們也可以試著來自己實現一個 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