也談如何實現bind、apply、call
我們知道,JavaScript的bind、apply、call是三個非常重要的方法。bind可以返回固定this、固定引數的函式包裝;apply和call可以修改成員函式this的指向。實現bind、apply、call是前端面試的高頻問題,也能讓我們更好地理解和掌握JavaScript的函式的相關知識。本文將介紹如何自行實現bind、apply和call。
前情提要
本文先給出如下類定義和例項定義。
// Person類,每個人有姓名,有列印姓名的方法
function Person(_name) {
this.name = _name
this.sayMyName = function (postfix) {
console.log(this.name + postfix)
}
}
// 兩個例項
let alex = new Person('Alex')
let bob = new Person('Bob')
實現bind
不妨先回顧一下bind的使用方法:
let sayBobName = alex.sayMyName.bind(bob, '?')
sayBobName() // Bob?
可見:
- bind返回一個函式副本(包裝過的函式),固定this和實參。this可不指向呼叫者
- bind是函式原型上的一個方法
瞭解了這兩點,就不難寫出實現:
function MyBind(context, ...args) {
let that = this // this是呼叫者,也就是被包裝的函式(alex.sayMyName)
return function () { // 返回一個包裝過的函式
return that.call(context, ...args) // 其返回值是以context為this的執行結果
}
}
Function.prototype.bind = MyBind
實現call
在實現bind的過程中我們用到了call。那麼如何實現一個call?不妨也回顧一下call的使用方法:
alex.sayMyName.call(bob, '!') // Bob!
可見:
- bind修改當前函式的this,可使其不指向呼叫者
- 引數以...rest形式傳入
瞭解了這兩點,也不難寫出實現:
function MyCall(context, ...args) {
context._fn = this // this是呼叫者,也就是當前函式。此步給context綁上該函式
// 有個細節問題是,可能需要備份原先的context._fn,避免意外覆蓋
let ret = context._fn(..args) // 模仿context自己呼叫_fn
delete context._fn // 移除綁的函式
return ret
}
Function.prototype.call = MyCall
實現apply
apply和call的作用相同,使用方法基本類似,唯一的不同是:
- 引數以陣列形式傳入
故可寫出實現:
function MyApply(context, args) {
return MyCall(context, ...args)
}
Function.prototype.apply = MyApply
穩定的三角
從上面我們看出,apply和call可以實現bind,那麼怎麼用bind實現apply和call,從而打造這一鐵三角關係呢?
bind
/ \ 兩兩都可互相實現的三角形!
call -- apply
簡單!立即執行這個函式副本就可以了!
function AnotherMyCall(context, ...args) {
return (this.bind(context, ...args))()
}