分步實現 new/bind/apply/call 函數。git
先看一下真正的 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
}
複製代碼
返回值的判斷函數
前面的代碼在判斷返回值時有問題,由於 typeof null === "object"
。修改一下:post
return (typeof result === 'object' || 'function') ? result||obj : obj
複製代碼
建立空對象以及實現繼承的方式
建立空對象有三種方法:
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操做符
是用 apply
或 call
來實現的。
注意
apply
和call
的區別
先大體回顧一下 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
複製代碼
即:
f1
與 f2
,他們的原型對象是相同的,都是原函數的原型 F1.prototype
F1
與 F2
,他們的原型倒是不相同的,而且 F2
壓根就沒有原型先無論第2條。
爲了實現第1條,首先想到的就是使 F2
與 F1
有一樣的原型。也就是說 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 提供了一個 bind()
的墊片,這裏就再也不貼代碼了,戳連接本身看。
後面緊跟着也說明了這個兼容方案的不足之處。
實際上也就是上面手動實現的方案的不足。
JavaScript 深刻之 bind 的模擬實現
Polyfill - MDN
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()
}
複製代碼
可是這樣有兩個問題:
fn
的方法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]
。
重點在於,函數接收參數的時候通常是以逗號爲分隔符,每一個變量挨個放上去的,而不是直接接受一個數組。
能夠想到這麼兩種實現方式:
eval()
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種繼承方案》