詳解 new/bind/apply/call 的模擬實現

分步實現 new/bind/apply/call 函數。git

new 的模擬實現

先看一下真正的 new 的使用方法:github

function MyClass(name, age){
  this.name = name
  this.age = age
}
var obj = new MyClass({name:'asd', age:10})
複製代碼

new 是關鍵字,調用方式是無法模仿的,只能以函數的形式實現,好比 myNew()面試

而後規定一下 myNew 接收參數的方式:算法

var obj2 = mynew(MyClass, 'asd', 10)
複製代碼

第一階段:基本實現

建立一個新對象,經過將其 __proto__ 指向構造函數的 prototype 實現繼承數組

function mynew(){
  // 新建空對象
  var obj = {}
  // 第一個參數是構造函數
  var constructor = [].shift.call(arguments)
  // 其他的參數是構造函數的參數
  var args = [].slice.call(arguments)
  // 修改原型
  obj.__proto__ = constructor.prototype
  // 修改構造函數上下文,爲 obj 賦值
  constructor.apply(obj, args)
  return obj
}
複製代碼

[].slice.call() 就是 Array.prototype.slice.call()瀏覽器

第二階段:實現返回值

構造函數也是函數,也可能有返回值。
new 有一個特性:構造函數返回值爲基本類型值時,不返回;引用類型值時,返回。閉包

只要判斷 constructor.apply() 的結果便可:app

function mynew(){
  var obj = {}
  var constructor = [].shift.call(arguments)
  var args = [].slice.call(arguments)
  obj.__proto__ = constructor.prototype
  var result = constructor.apply(obj, args)
  // 判斷結果的類型
  return (typeof result === 'object' || 'function') ? result : obj
}
複製代碼

第三階段:細節

  1. 返回值的判斷函數

    前面的代碼在判斷返回值時有問題,由於 typeof null === "object"。修改一下:post

    return (typeof result === 'object' || 'function') ? result||obj : obj
    複製代碼
  2. 建立空對象以及實現繼承的方式

    建立空對象有三種方法:

    • var obj = new Object()
    • var obj = {}
    • Object.create()

    前兩種是相同的,可是考慮到這是模擬 new,因此第一種不太合適。

    實現繼承有兩種方法:

    • var obj = Object.create(constructor.prototype)
    • obj.__proto__ = constructor.prototype

    第一種在建立對象時直接繼承。
    第二種先建立對象,再設置原型。要注意:這時不能經過 Object.create(null) 來建立對象,能夠參考這個 ISSUE

    若是使用Object.create(null),訪問不到__proto__這個原型屬性,所以在後續賦值時,__proto__被當作普通屬性進行賦值。

參考連接

JavaScript 深刻之 new 的模擬實現
面試官問:可否模擬實現 JS的new操做符

bind 的模擬實現

是用 applycall 來實現的。

注意 applycall 的區別

先大體回顧一下 bind 的用法:

name = 'global'
function test(sex, age) {
  console.log(this.name, sex, age)
  return 'return value'
}
obj = {name: 'asd'}
testBinded = test.bind(obj, 'M')
console.log(testBinded(10))
// 輸出:
// asd M 10
// return value
複製代碼

第一階段:基本實現

Function.prototype.bind2 = function () {
  // this 即將要執行 bind 的函數
  var self = this
  // 傳入的第一個參數是新的上下文
  var context = arguments[0]
  // 返回一個閉包,綁定以後的函數
  return function () {
    // 原函數可能有返回值,因此這裏返回 apply 以後的結果
    return self.apply(context)
  }
}
複製代碼

第二階段:實現參數傳遞

bind() 能夠在綁定時給原函數傳遞參數,綁定以後的函數執行時還能夠再次傳遞參數。

能夠順便學習一下柯里化

Function.prototype.bind2 = function () {
  var self = this
  // bind 時第一個參數是新的上下文
  var context = [].shift.call(arguments)
  // 其他的參數是傳遞給原函數的參數
  var args1 = [].slice.call(arguments)
  return function () {
    // bind 後的函數執行時傳入的參數
    var args2 = [].slice.call(arguments)
    // 合併參數
    return self.apply(context, args1.concat(args2))
  }
}
複製代碼

第三階段:實現構造函數效果

一個函數執行 bind() 後,若是使用 new 調用,即當作構造函數,那麼:

  • bind() 時傳入的上下文 context 會失效
  • 可是兩次傳入的參數 args 仍然有效

第一次看到這個的時候,想的是,bind() 已經執行完了,以後怎麼調用跟 bind() 的實現有什麼關係?

大家抓的是周樹人,跟我魯迅有什麼關係?

關係在於,bind() 返回的是閉包,函數並無執行

在前面 new 的模擬實現裏,須要經過 apply() 改變構造函數的上下文,在這裏構造函數就是 bind() 以後的函數。
可是看一下上面 bind2() 的實現,返回函數時,直接把上下文設置爲了執行bind2() 時傳入的 context,根本沒判斷這個函數是否是接受了新的上下文

因此修改的方法是,在 bind2() 中獲取 this,也就是 apply() 傳入的上下文(若是有的話),並判斷。

Function.prototype.bind2 = function () {
  var self = this
  var context = [].shift.call(arguments)
  var args1 = [].slice.call(arguments)
  var result = function () {
    var args2 = Array.prototype.slice.call(arguments)
    // 若是 this 是 result 這個函數的實例,說明 result 做爲構造函數被調用了
    var context = this instanceof result ? this : context
    return self.apply(context, args1.concat(args2))
  }
  return result
}
複製代碼

第四階段:繼承

bind 還有一些關於繼承的特性。

舉個栗子:

// 聲明一個構造函數 F1()
function F1(){}
// bind 生成構造函數 F2()
F2 = F1.bind({})
// f1 和 f2 分別是它們的實例
f1 = new F1()
f2 = new F2()
// 在 F1() 上添加原型屬性
F1.prototype.name = 'ads'

console.log(f2.name) // asd
console.log(f2.__proto__ === f1.__proto__) // true
console.log(F1.prototype) // {name: "ads", constructor: ƒ}
console.log(F2.prototype) // undefined
複製代碼

即:

  • f1f2,他們的原型對象是相同的,都是原函數的原型 F1.prototype
  • 可是 F1F2 ,他們的原型倒是不相同的,而且 F2 壓根就沒有原型

先無論第2條。
爲了實現第1條,首先想到的就是使 F2F1 有一樣的原型。也就是說 bind2 的代碼須要加上這麼一行:

result.prototype = self.prototype
複製代碼

可是存在一個問題,這樣一來能夠經過 F2.prototype 來修改原型上的屬性,而真正的 bind() 返回的函數是沒有 prototype 的,更別提經過 prototype 去修改原型上的屬性了。

怎麼辦呢?
不要忘了,如今的目的是讓 bind() 以後的函數可以訪問原函數原型對象上的屬性,實現這個目標就能夠了。

而想要訪問原函數的原型對象,沒必要非得直接基於原函數進行繼承。
由於在原型鏈上尋找屬性時是一級一級向上尋找的,就算最末端的對象與實際想要繼承的原型對象之間隔着 n 層,可是隻要它們在同一條原型鏈上,就能夠訪問到原型對象。

因此在這裏,徹底能夠新建一箇中介函數,而且繼承原函數的原型對象,而後去繼承這個新的函數。
這樣一來,bind() 以後的函數其實是經過這個中介函數把本身添加到了原函數的原型鏈上。而且由於 bind() 先後的函數原型對象不相同,因此修改時互相沒有影響。

下面是最後的代碼:

Function.prototype.bind2 = function () {
  var self = this
  var context = [].shift.call(arguments)
  var args1 = [].slice.call(arguments)
  var result = function () {
    var args2 = Array.prototype.slice.call(arguments)
    var context = this instanceof result ? this : context
    return self.apply(context, args1.concat(args2))
  }
  // 新建一個你叔
  var Agent = function () {}
  // 讓你叔也繼承原函數的原型,或者說你爺爺
  Agent.prototype = self.prototype
  // 而後你不繼承你爸了,而是繼承你叔
  result.prototype = new Agent()
  return result
}
複製代碼

至於 F2.prototype 應該爲 undefined 這一點該怎麼搞呢?看下一部分。

MDN 提供的 Polyfill

MDN 提供了一個 bind() 的墊片,這裏就再也不貼代碼了,戳連接本身看。

後面緊跟着也說明了這個兼容方案的不足之處。
實際上也就是上面手動實現的方案的不足。

參考連接

JavaScript 深刻之 bind 的模擬實現
Polyfill - MDN

apply 和 call 的模擬實現

apply()call() 只是接收參數的方式不同。
這裏以 apply() 爲例實現一下。call() 的模擬實現能夠參考《JavaScript 深刻之 call 和 apply 的模擬實現》

先回顧一下 apply 的效果:

name = 'global'
function test(age, sex) {
  console.log(this.name, age, sex)
  return 'return value'
}
console.log(test.apply({name: 'asd'}, [1, 'M']))
// 輸出:
// asd 1 M
// return value
複製代碼

第一階段:基本實現

首先,apply() 在給定的上下文中當即執行了一個函數。

而說到「在給定的上下文中執行」,讓人不得不想到把函數做爲對象的方法來執行:

obj = {
  name: 'asd',
  showName() {
    console.log(this.name)
  }
}
obj.showName()
複製代碼

那麼第一步能夠這樣實現一下:

Function.prototype.apply2 = function () {
  // 新的上下文,是一個對象
  var context = arguments[0]
  // 把原函數添加爲這個對象的方法
  context.fn = this
  // 執行,而且函數可能有返回值
  return context.fn()
}
複製代碼

可是這樣有兩個問題:

  1. 原對象被修改了,增長了一個叫 fn 的方法
  2. 若是原對象裏原本就有一個鍵叫 fn 呢?

增長了,只要刪掉就行了;而重名的狀況,能夠用 Symbol 解決。

雖然Symbol 是 ES6 的內容,可是不要在乎這些細節!
call 還從 ES3 開始就有了呢,又不是從底層重寫,意思意思就行...

Function.prototype.apply2 = function () {
  var context = arguments[0]
  // 生成一個惟一的 key,就不會與原對象中其餘的 key 衝突了
  var symbol = Symbol()
  context[symbol] = this
  var result = context[symbol]()
  // 最後刪掉
  delete context[symbol]
  return result
}
複製代碼

第二階段:實現參數傳遞

apply() 接受兩個參數,第一個參數爲新的上下文,第二個是由傳遞給原函數的參數組成的數組。

獲取參數很簡單,第二個參數就是 arguments[1]
重點在於,函數接收參數的時候通常是以逗號爲分隔符,每一個變量挨個放上去的,而不是直接接受一個數組。

能夠想到這麼兩種實現方式:

  1. eval()
  2. 展開運算符

eval() 接受一個字符串,並把字符串做爲 JS 來運行:

eval("console.log('asd')") // asd
複製代碼

你覺得它是字符串,實際上是我 JS 噠!

那麼在這裏就改寫成了:

Function.prototype.apply2 = function () {
  var context = arguments[0]
  var args_arr = arguments[1]
  var symbol = Symbol()
  context[symbol] = this
  // 1. 使用 eval()
  // 處理參數,字符串須要加上雙引號
  var args_string = ''
  args_arr.forEach((val) => {
    if (typeof val === 'string') args_string += '"' + val + '",'
    else args_string += val + ','
  })
  var result = eval('context[symbol](' + args_string + ')')
  // 2. 或者使用展開運算符
  // var result = context.symbol(...args_arr)
  delete context[symbol]
  return result
}
複製代碼

其實首先想到的是柯里化
可是回頭一想要實現柯里化好像用到了 apply,那這裏就不合適了。

第三階段:細節

第一個參數也能夠是 null,瀏覽器環境下指向 window。只要改一行:

var context = arguments[0] || window
複製代碼

參考連接

JavaScript 深刻之 call 和 apply 的模擬實現

打個廣告

個人其餘文章:

超詳細的10種排序算法原理及 JS 實現》
《免費爲網站添加 SSL 證書》
《深刻 JavaScript 經常使用的8種繼承方案》

相關文章
相關標籤/搜索