JS个人学习(7)——apply、call、bind的原理与实现

614 阅读3分钟

apply、call、bind的作用与区别

applycallbind都是用来改变函数指向的方法,区别在于:

  • applycall调用后会立即执行,而bind不会立即执行,而是会返回一个函数
  • apply的第二个参数与callbind不一样,是以数组形式传入的,而callbind则是接受多个参数

apply的实现

注意两点:

  1. 第一个参数是需要指向的对象,不传时默认为window(如果传了,就要判断是否是函数或者对象,不然要报错);
  2. 第二个参数是数组,要注意使用的方式;

实现:

Function.prototype.apply = function (context, arr) {
    // 首先判断context是否传入,如果不传则为window
    context = context || window
    // 再判断context是否是对象或者函数,如果都不是,则类型错误
    if (typeof context !== 'object' && typeof context !== 'function') {
        throw('TypeError')
    }
    // 将原函数赋值给context的fun,这样就改变了原函数的位置,从而改变它的作用域
    context.fun = this
    // 直接调用fun,注意参数arr是数组,因此先判断下arr是否传入
    arr = arr || []
    var res = context.fun(...arr)
    // 从context上删除fun属性
    delete context.fun
    return res
}

call的实现

call的实现,其实和apply的相差无几,就是传入参数的形式不一样

实现:

Function.prototype.call = function (context) {
    context = context || window
    if (typeof context !== 'object' && typeof context !== 'function') {
        throw('TypeError')
    }
    context.fun = this
    // 其他都一样,就是参数获取有变化,这里通过扩展运算符将arguments转成数组,并移除第一项
    // arguments是一个类数组,类数组转数组的方式有很多,如调用数组的各种方法:
    // Array.prototype.slice.call(arguments)、[].concat.call(...arguments)
    var args = [...arguments].slice(1)
    // 移除第一项
    var res = context.fun(...args)
    delete context.fun
    return res
}

// 这里参数其实还可以用Function.prototype.call = function (context, ...arr) { ... }这种形式来获取

bind实现

bind的实现主要是返回一个函数,以及bind支持函数柯里化的传参,其他方式与call类似

实现:

Function.prototype.bind = function (context) {
    context = context || window
    if (typeof context !== 'object' && typeof context !== 'function') {
        throw('TypeError')
    }
    // 将调用的函数赋值给fun
    var fun = this
    // 这里换一种方法将arguments转成数组
    var args = Array.prototype.slice.call(arguments)
    // 用不同的方法移除第一项
    args.shift()
    // 重新创建一个函数
    var newFun = function () {
        // newFun的参数也要获取下来,并一同传给fun执行
        var newArr = [...arguments]
        // 在新函数中通过apply方法改变函数指向并执行
        return fun.apply(context, args.concat(newArr))
    }
    return newFun
}

注意点: 这里我们实现的bind方法返回的函数是有prototype属性的,即Function.prototype.bind().prototype存在,但是原生的bind方法所返回的函数并不存在prototype属性。

这个问题在 JS个人学习(6)——原型与原型链 有说明。

总结

applycallbind这三个方法的使用及原理理解起来并不是很困难,但就是实际实现的时候需要注意几个点:

  • apply的第二个参数是数组,与其他两个方法不同;
  • bind注意返回的新方法里面也支持传参,这一步不能忘记,不然使用起来会有问题
  • arguments是一个类数组,将类数组转成数组的方式很多,比如[...arguments]Array.prototype.slice.call(arguments)[].concat.call(...arguments)
  • 要记得判断第一个参数的类型,对象或者函数,不然要抛出错误
  • applycall主要思想是将要改变指向的函数放到我们所需要指向的对象里面去执行,以此来达到改变this的目的,以下代码是我的理解:
// foo函数是我们要改变指向的函数
function foo() {
    console.log(this.name)
}
// obj是我们要将foo指向过来的对象
var obj = {
    name: 'wyt',
}
// apply跟call主要原理就是将foo函数赋值给obj里面的一个自定义属性,我们这里命名为fun
// 就相当于执行obj.fun = foo
// 此时的obj就变成了如下状态
obj = {
    name: 'wyt',
    fun: foo
    // 这里就相当于fun: function() { console.log(this.name) }
}
// 然后执行obj.fun(),this原本的指向就变成了我们想要的obj(把该传的参数放进去)
// 最后用delete删除fun,假装一切都没发生过的样子0.0