JS 模拟实现 call、apply、bind 方法

270 阅读3分钟

记录一个知识点,共勉。

一、call 方法的模拟实现

1. call 的简单使用

在使用 call 方法时注意以下两点:

  1. foo 函数本身执行了
  2. call 改变了函数 foo 中 this 的指向,该例指向到 obj
var obj = { value: 1 }
function foo() {
    console.log(this.value)
}
foo.call(obj)  // 1

2. 模拟实现原理

var obj = {
    value: 1,
    foo: function() {
        console.log(this.value)
    }
}
// 此时调用 obj.foo 也可以打印出 value 的值

因而得到模拟的原理:

1. 给对象 obj 赋值函数 foo

2. 调用这个函数

3. 调用完后删除这个函数

obj.fn = foo
obj.fn()
delete obj.fn

3. 第一步

把上面模拟的原理写到自定义的 call 方法中

Function.prototype.call2 = function (context) {
    context.fn = this
    context.fn()
    delete context.fn
}

// 使用
var obj = {
    value: 1
}
function foo() {
    console.log(this.value)
}
foo.call2(obj) // 1

4. 第二步

传参实现:call 方法可接收不定量参数

1. 在函数内部使用 arguments 可得到参数类数组

2. 使用 eval 执行函数解决参数传递问题,内部会自动调用 Array.toString()

Function.prototype.call2 = function (context) {
    context.fn = this
    const args = []
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args + ')')
    delete context.fn
}

// 使用
var obj = {
    value: 1
}
function foo(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
foo.call2(obj, 'Tom', 18)
// Tom
// 18
// 1

5. 第三步

1. call 方法第一个参数可以传 null,此时 this 指向 window

2. 第一个参数也可以传常量,使用 Object 包一层

Function.prototype.call2 = function (context) {
    // 判断 context 是否是对象,为 null 时指向 window
    var context = typeof contenxt === 'object' ? context || window : new Object(context)
    context.fn = this
    const args = []
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args + ')')
    delete context.fn
}

6. 第四步

call 方法可以拥有返回值

Function.prototype.call2 = function (context) {
    var context = context || window
    context.fn = this
    const args = []
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var res = eval('context.fn(' + args + ')')
    delete context.fn
    return res
}

var obj = {
    value: 1
}
function foo(name, age) {
    return {
        name, age, val: this.value
    }
}
console.log(foo.call(obj, 'Tom', 18))
// Object {
//    name: 'Tom',
//    age: 18,
//    value: 1,
// }

二、apply 方法的模拟实现

apply 的实现与 call 类似,唯一区别是 apply 接收的参数以数组的方式传入

Function.prototype.apply2 = function (context, arr) {
    var context = context || window
    context.fn = this
    var res
    if (!arr) {
        res = context.fn()
    } else {
        var args = []
        for(var i = 1, len = arguments.length; i < len; i++) {
            args.push('arguments[' + i + ']');
        }
        res = eval('context.fn(' + args + ')')
    }
    delete context.fn
    return res
}

三、bind 的模拟实现

1. bind 的使用

function fn(name, age) {
    console.log(this.value, name, age)
}
const obj = { value: 1 }
const resFn = fn.bind(obj, 'tom')
resFn(18) // 1, 'tom', 18

bind 方法使用的一些特征:
1. fn 绑定 obj 的 this,可以传入参数
2. 执行结果返回一个函数,这个函数执行的时候还可以传入参数
3. 返回的函数还可以当做构造函数使用 new 调用,传入的参数仍然生效

2. 第一步:使用 apply 做最基础的实现

Object.prototype.bind2 = function(context) {
    // 缓存一份当前函数的 this,点调用,this 指向点前面的函数 
    const self = this 
    // 取到使用 bind 的时候传入的参数 
    const args = Array.prototype.slice.call(arguments, 1) 
    // 返回一个函数 
    return self.apply(context, args)
}

3. 第二步:实现返回的函数调用时,还可以传入参数

Object.prototype.bind2 = function(context) {
    const self = this
    const args = Array.prototype.slice.call(arguments, 1)
    return function () {
        // 取到函数调用时传入的参数
        const _args = Array.prototype.slice.call(arguments)
        return self.apply(context, args.concat(_args))
    }
}

4. 第三步:使用 new 操作符调用返回参数

当做构造函数使用 new 操作符调用,这时原来的 this 失效,指向构造函数实例

Object.prototype.bind2 = function(context) {
    const self = this
    const args = Array.prototype.slice.call(arguments, 1)
    const resFn = function () {
        const _args = Array.prototype.slice.call(arguments)
        // 作为构造函数使用 new 调用时,this instanceof resFn 为 true
        return self.apply(this instanceof resFn ? this : context || this, args.concat(_args)) 
    } 
    // 再使用寄生式继承,把原型链上的属性继承上 
    const voidFn = function () {} 
    voidFn.prototype = this.prototype
    resFn.prototype = new voidFn() 
    return resFn 
}

5. 第四步:调用 bind 的必须是函数,加上判断

Object.prototype.bind2 = function(context) {
    if (typeof this !== 'function') {
        throw new Error('报错啦~')
    }
    const self = this
    const args = Array.prototype.slice.call(arguments, 1)
    const resFn = function () {
        const _args = Array.prototype.slice.call(arguments)
        return self.apply(this instanceof resFn ? this : context || this, args.concat(_args))
    }
    const voidFn = function () {}
    voidFn.prototype = this.prototype
    resFn.prototype = new voidFn()
    return resFn
}

四、最后

这里只是简单的模拟实现,其中还有很多细节可以完善,例如对参数的类型判断,写法的优化等