apply、bind、call的用法及实现原理

参考资料

[1] 《JavaScript高级程序设计》

[2] js手动实现bind方法,超详细思路分析! --- 听风是风

PS:下面是我个人的总结,有些所以然都略过了,想理解得更透彻,建议看参考资料。

三者的用法及区别

const obj = { name: 'jack' }
function printMessage(age, sex) {
    console.log(`my name is ${this.name}, my age is ${age}, my sex is ${sex}.`);
}
printMessage.apply(obj,[12,'男']) // 数组传参,直接执行
printMessage.bind(obj,12,'男')() // 直接传参,返回函数
printMessage.call(obj,12,'男') // 直接传参,直接执行

下面用通过模拟apply()、bind()、call()的实现来理解它们的原理。

apply()原理

实现apply()的关键点是如何将第二个参数(装着参数的数组)拆开来放到调用函数时的括号里。解决办法就是用eval()函数来执行函数。

模拟实现:

Function.prototype.apply2 = function(ctx, argArr) {
    // 如果ctx为空,则指向window
    ctx = ctx || window
    // 给ctx加上一个临时变量fn,指向当前函数
    ctx.fn = this
    // 处理一下参数(因为函数不能传入数组作为参数列表,所以要自己拆开来)
    const args2 = []
    for (var i in argArr) {
        args2.push('argArr['+i+']')
    }
    // 执行函数,获得结果
    const result = eval('ctx.fn('+args2+')')
    // 清除临时变量
    delete ctx.fn
    // 返回结果
    return result;
}

测试代码:

const obj = { x: 1 }
function test(y, z) {
    console.log(this.x, y, z)
}
test.apply2(obj, [2, 3])

输出:

1 2 3

call()原理

原来想按a、b、c的顺序写apply()、bind()、call(),但由于call()和apply()的原理比较类似,且bind()比较复杂,因此call()放在这里了。

模拟实现:

Function.prototype.call2 = function(ctx) {
    // 如果ctx为空,则指向window
    ctx = ctx || window;
    // 给ctx加上一个临时变量fn,指向当前函数
    ctx.fn = this;
    // 处理一下参数
    const args = []
    const args2 = []
    for(let i=1; i<arguments.length; i++) {
        args.push(arguments[i])
        args2.push('args['+(i-1)+']')
    }
    // 执行获得结果
    const result = eval('ctx.fn('+args2+')')
    // 删除临时变量
    delete ctx.fn
    // 返回结果
    return result
}

测试代码:

const obj = { x: 1 }
function test(y, z) {
    console.log(this.x, y, z)
}
test.call2(obj, 2, 3)

输出结果:

1 2 3

bind()原理

先给出bind()的使用方法(有三个要点,因此实现起来比较复杂):

const obj = { x: 1 }
function Test(y, z) {
    console.log(this.x, y, z)
}
Test.prototype.w = 4

// 返回的是一个函数。
// 绑定时可以只传入一部分参数,在调用返回的函数时传入剩下的函数(这叫做函数柯里化)。
const Bound = Test.bind(obj, 2)
Bound(3)

// 用bound创建实例时,Test()内部的this指向的是Bound的实例,而不是之前绑定的对象obj。
const bound = new Bound(3)

// 用Bound创建的实例能够通过原型链访问Test.prototype。
console.log(bound.w)

模拟实现:

Function.prototype.bind2 = function(ctx) {
    // 如果ctx为空,则指向window
    ctx = ctx || window
    // 处理一下参数
    const args = []
    for(let i=1; i<arguments.length; i++) {
        args.push(arguments[i])
    }
    // 创建一个函数
    const fn = this // 因为返回的函数中this指向的是调用者,因此这里要用一个新变量暂存当前函数
    function fbound() {
        // 第一个参数用来处理当返回的函数被当做构造函数的情况
        // 第二个参数将在返回的参数中新传入的参数与调用bind2()时传入的参数合并
        fn.apply2(
            this instanceof fn ? this : ctx,
            args.concat(Array.prototype.slice.call2(arguments))
        )
    }
    // 通过fbound创建的实例,我们希望它也是当前函数fn()的实例
    // 因此我们用prototype来让fbound继承自fn
    // 但又不想修改fbound.prototype直接修改fn.prototype
    // 因此创建一个中间对象
    function FnProto () {}
    FnProto.prototype = fn.prototype
    fbound.prototype = new FnProto()
    
    return fbound
}

测试代码:

const obj = { x: 1 }
function Test(y, z) {
    console.log(this.x, y, z)
}
Test.prototype.w = 4
// 返回的是一个函数。
// 绑定时可以只传入一部分参数,在调用返回的函数时传入剩下的函数(这叫做函数柯里化)。
const Bound = Test.bind2(obj, 2)
Bound(3)

// 用bound创建实例时,Test()内部的this指向的是Bound的实例,而不是之前绑定的对象obj。
const bound = new Bound(3)

// 用Bound创建的实例能够通过原型链访问Test.prototype。
console.log(bound.w)

输出结果:

1 2 3 // Bound(3)输出
undefined 2 3 // new Bound(3)输出
4 // console.log(bound.w)输出
上一篇:cplex教学 | 分支定界法(branch and bound)解带时间窗的车辆路径规划问题(附代码及详细注释)


下一篇:干货 | 10分钟教你用branch and bound(分支定界)算法求解TSP旅行商问题