【JS】5行代码实现bind函数

1,467 阅读4分钟

写在前面

最近似乎快到春招了,我也在犹豫要不要投简历试试。身边很多大佬,投简历的投简历,面试的面试,很慌。但是,慌归慌,学习还得继续。听说面试可能会考到一些底层问题,例如手写Promise,手写bind。这两个问题,学习过程中碰到过,正好趁此机会好好复习一番。

本文将根据自己的实践向大家阐述bind函数的实现原理,并用简洁的代码模拟bind。假设你已经了解了call/apply,开始之前,让我们先来看看函数柯里化

函数柯里化

概念就不复述了,简洁的说,函数柯里化就是就是为函数提前传入参数(预执行)。其实就是根据需求定制参数,实现特殊效果。且看代码:

/**
 * 柯里化函数
 * @param  preArgs [需要绑定的参数列表]
 * @return 柯里化后的函数
 */
function curry(fn, ...preArgs){
    return function(...args){
        // 将外部传入的参数和预定义的参数按序拼接起来并传入目标函数
        return fn.call(null, ...preArgs, ...args)
    }
}

我们可以看到,curry函数的返回值是一个函数,这意味着我们传入curry函数的目标函数并未执行。这能实现什么特殊效果呢?

// 目标函数
function add (a, b) {
    return a + b
}
// 这里的7就是curry中的preArgs,预先绑定一个参数7
cosnt addWith_7 = curry(add, 7) 
// 由于预先传入了一个参数7,这里 7 + 1 = 8
addWith_7(1) // => 8

对目标函数add进行柯里化后,得到的是一个被提前配置了部分参数的函数(addWith_7),目标函数并未执行。并且由于提前传入了参数7,addWith_7函数只需要传入一个参数即可完成与7相加的任务。这是一种定制。

细心的小伙伴可能发现了,curry函数中fn.call(null, ...preArgs, ...args)执行目标函数时候并没有绑定作用域,可以预见,如果绑定了作用域,或许会有更多奇妙用法。

bind函数

需求

浏览器中,我们时常需要为按钮绑定事件:

// 获取按钮
let btn = document.getElementById('button')
// 事件处理函数
function print (e) {
    console.log(e)
    console.log(this)
}

为按钮绑定事件,点击按钮,打印事件对象和this

btn.addEventListener('click', print) // 打印出event对象和event.target对象

事件处理程序中this默认指向event.target。如果我想使this指向自定义作用域并输出内容,需要改变函数的作用域即this指向。能改变this指向的函数只有两个call/apply。你也许会这样想:

btn.addEventListener("click", print.call(context)) // 这显然是错误的

call/apply调用时将会执行函数,我们确实可以改变this,但同时目标函数也被执行了。而事件处理需要回调函数,只有事件触发才调用它。很明显,call/apply无法满足我们。

这时你想到了curry函数,我们稍作调整,加入context参数

// 加入context参数
function curry(context, fn, ...preArgs){
    return function(...args){
        // 将外部传入的参数和预定义的参数按序拼接起来并传入目标函数
        return fn.call(context, ...preArgs, ...args)
    }
}
// 示例作用域
let context = {
    name: "这是示例作用域"
}
// 示例函数
let print = function () {
    console.log(this.name)
}
let target = curry(context, print) // 是否传入参数根据需求,这里示例就不传参数了
target() // => '这是示例作用域'

可以看到,我们print函数作用域改变了,但是并没有立即执行,而是等到了我们执行target之后。这不正好?

btn.addEventListener('click', curry(context, print)) // 完美解决!

事实证明,这样的改造很有必要,它使得函数的调用更为灵活。

实现bind

其实改造后的curry已经可以充当bind函数了,但是为了使用方便,还是需要将他添加到Function原型上,这样可以省略一个参数,也就是目标函数。

注意!开发过程中不能随意为内置对象添加属性或方法,这可能会导致一些意想不到的覆盖和bug,极其不利于项目的维护。这里仅供学习。

Function.prototype.bind = function (context, ...preArgs) {
    // 注意箭头函数没有作用域,它的作用域即父作用域
    return (...args) => {
        // this => 调用bind的函数
        return this.call(context, ...preArgs, ...args)
    }
}

所以,上面的按钮事件绑定还可以这么写:

// 具体参数的传递可以很灵活,这里就不示例了
btn.addEventListener('click', print.bind(context))

至此我们就实现了bind函数,怎么样,是不是5行?

总结

通过函数柯里化的示例,我们可以发现,bind函数的原理实际上就是函数柯里化。通过为目标函数定制作用域和参数,来满足开发过程中的人性化要求。这样看来,bind函数也没有想的那么难不是么?

另外,函数可以柯里化,也可以反柯里化。反柯里化可以将函数从原型上分离出来单独使用,便于函数调用:

let push = Array.push.uncurry()
let obj = {}
push(obj, 1, 2)
console.log(obj) // { '0': 1, '1': 2, length: 2 }

神奇吧?掘金上有很多这类文章,感兴趣的话可以自己探索一番呀。

其他

如果有什么错误或者建议,欢迎评论区批评指正,我会虚心采纳的,祝你们面试顺利!