call,apply,bind的模拟实现

208 阅读3分钟

call

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

  • call 改变了this的指向,并执行函数
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

// 可以看成如下实现
/**
将函数设为对象的属性
执行该函数
删除该函数
**/
var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1
delete foo.bar

Function.prototype.call = function(context) {
    // 用this获取调用call的函数
    context.fn = this
    context.fn()
    delete context.fn
}
  • call 函数还能给定参数执行函数
    • 思路: Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里

    • 把参数数组放到要执行的函数的参数里面去,用 eval 方法拼成一个函数

    • eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。

    // 此时的arguments为:
    // arguments = {
    //      0: foo,
    //      1: 'kevin',
    //      2: 18,
    //      length: 3
    // }
    // 因为arguments是类数组对象,所以可以用for循环
Function.prototype.call = function(context) {
    context.fn = this
    let arr = []
    for(let i = 1, i< arguments.length, i++) {
        arr.push(`arguments[${i}]`)
    }
    // `${arr}` = arr.toString() , [1,2,3].toString() = '1,2,3'
    eval(`context.fn(${arr})`)
    // 或: context.fn(...arr)
    delete context.fn
}

  • this 参数可以传 null,当为 null 的时候,视为指向 window
  • 函数是可以有返回值的!
Function.prototype.call = function(context) {
    context = context || window
    context.fn = this
    let arr = []
    for(let i = 1, i< arguments.length, i++) {
        arr.push(`arguments[${i}]`)
    }
    let result = eval(`context.fn(${arr})`)
    delete context.fn
    return result
}

apply

  • apply 实现同call,注意arguments不同
Function.prototype.apply = function(context,arr) {
    context = context || window
    context.fn = this
    let result
    if(!arr){
        result = context.fn()
    } else {
        let args = []
        for(let i = 1, i< arr.length, i++) {
            args.push(`arr[${i}]`)
        }
        result = eval(`context.fn(${args})`)
    }
    delete context.fn
    return result
}

bind

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

  • 返回一个函数
  • 可以传入参数
    • 思考:bind传入参数类似于call
    • Array.prototype.slice.call()将类数组转为数组
Function.prototype.bind = function(context) {
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    return function () {
       that.apply(context, args)
    }
}
  • 返回函数可以有返回值
  • 返回函数也可以传入参数
Function.prototype.bind = function(context) {
    let that = this
    // 这里的arguments是通过bind传入的参数
    let args = Array.prototype.slice.call(arguments, 1)
    return function () {
        // 这里的arguments是返回函数传入的参数
        let bindArgs = Array.prototype.slice.call(arguments)
        // 使用return, 函数可以有返回值
        return that.apply(context, args.concat(bindArgs))
        // 或 return that.apply(context, [...args,...bindArgs])
    }
}
  • 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
    • 思考: 当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值失效
Function.prototype.bind = function(context) {
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    let funBind = function () {
        let bindArgs = Array.prototype.slice.call(arguments)
        // 这里的this,当返回的函数为构造函数的时候会指向funBind
        // 当返回的函数作为普通函数的时候会指向window
        // this指向返回函数的this,可以使用绑定函数的属性值
        return that.apply(
            this instanceof funBind? this : context, 
            args.concat(bindArgs)
        )
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    funBind.prototype = this.prototype
    return funBind
}

funBind.prototype = this.prototype的情况下修改funBind.prototype的时候也会修改到绑定函数的prototype,所以用一个空函数来轮转

Function.prototype.bind = function(context) {
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    let funNo = function () {}
    let funBind = function () {
        let bindArgs = Array.prototype.slice.call(arguments)
        return that.apply(
            this instanceof funNo? this : context, 
            args.concat(bindArgs)
        )
    }
    funNo.prototype = this.prototype
    funBind.prototype = new funNo()
    return funBind
}
  • 防止调用bind的不是函数
Function.prototype.bind = function(context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
    
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    let funNo = function () {}
    let funBind = function () {
        let bindArgs = Array.prototype.slice.call(arguments)
        return that.apply(
            this instanceof funNo? this : context, 
            args.concat(bindArgs)
        )
    }
    funNo.prototype = this.prototype
    funBind.prototype = new funNo()
    return funBind
}

参考文章:

JavaScript深入之bind的模拟实现

JavaScript深入之call和apply的模拟实现