基本原理

对象的方法调用时this指向是指向该对象的,利用此原理我们可以模拟实现call\bind\apply

1. 实现call

版本v1

利用基本原理我们可以实现版本1

1
2
3
4
5
6
Function.prototype.call2 = function (ctx, ...rest) {
ctx.fn = this
let result = ctx.fn(...rest)
delete ctx.fn
return result
}

版本v2

版本1的缺点是fn属性可能已经存在于ctx中了,我们这样写可能会导致原fn属性被删除

1
2
3
4
5
6
7
Function.prototype.call2 = function (ctx, ...rest) {
let key = Symbol('function')
ctx[key] = this
let result = ctx[key](...rest)
delete ctx[key]
return result
}

版本v3

上面的版本还忽略了一个问题,当传入的ctx为null或undefined时候,this指向window;当传入的ctx为其他基本数据类型时候,会改为Object类型

1
2
3
4
5
6
7
8
9
Function.prototype.call2 = function (ctx, ...rest) {
if (ctx === null || ctx === undefined) ctx = window
if (typeof ctx !== 'object') ctx = new Object(ctx)
let key = Symbol('function')
ctx[key] = this
let result = ctx[key](...rest)
delete ctx[key]
return result
}

加分版

上面所有的版本我们都使用了es6的方法,如果不依赖es6的方法,将如何实现呢?

  • let改为var
  • Symbol使用时间戳代替,利用hasOwnProperty判断该值是否为对象的原有属性
  • 使用eval运行函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    Function.prototype.call2 = function () {
    var ctx = arguments[0]
    var key = ''
    var args = []
    if (ctx === null || ctx === undefined) ctx = window
    if (typeof ctx !== 'object') ctx = new Object(ctx)
    while (!key || ctx.hasOwnProperty(key)) {
    key = 'fn_' + new Date().getTime()
    }
    ctx[key] = this
    // 参数数组
    for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
    }
    // 字符串拼接会直接调用数组的toString方法 [1,2].toString() === '1,2'
    // 此处变为 eval('ctx[key](arguments[1], arguments[2],...)')
    var result = eval('ctx[key](' + args + ')')
    delete ctx[key]
    return result
    }

    2. 实现apply

    与模拟实现call原理一致,我们直接修改call模拟实现的加分版
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Function.prototype.apply2 = function (ctx, args) {
    var key = ''
    var argsStr = []
    if (ctx === null || ctx === undefined) ctx = window
    if (args === null || args === undefined) args = []
    if (typeof ctx !== 'object') ctx = new Object(ctx)
    if (!(args instanceof Object)) {
    throw new Error('TypeError: CreateListFromArrayLike called on non-object')
    }
    while (!key || ctx.hasOwnProperty(key)) {
    key = 'fn_' + new Date().getTime()
    }
    ctx[key] = this
    // 参数数组
    for (var i = 0, len = args.length; i < len; i++) {
    argsStr.push('args[' + i + ']')
    }
    // 字符串拼接会直接调用数组的toString方法 [1,2].toString() === '1,2',因为[[1,2],3].toString() === '1,2,3',数组参数会被拍平,所以借助argsStr
    var result = eval('ctx[key](' + argsStr + ')')
    delete ctx[key]
    return result
    }
    如果不想使用eval,可以使用new Function()根据字符串生成函数来调用
    1
    var result = eval('ctx[key](' + argsStr + ')')
    改为
    1
    var result = new Function(['ctx', 'key', 'args'], 'return ctx[key](' + argsStr + ')')(ctx, key, args)

    3. 实现bind

    v1版本,沿用之前的思想,封装函数返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    Function.prototype.bind2 = function () {
    var ctx = arguments[0]
    var args = []
    var _this = this
    if (ctx === null || ctx === undefined) ctx = window
    if (typeof ctx !== 'object') ctx = new Object(ctx)
    for (var i = 1, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    return function () {
    var key = ''
    while (!key || ctx.hasOwnProperty(key)) {
    key = 'fn_' + new Date().getTime()
    }
    ctx[key] = _this
    for (var i = 0, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    for (var i = 0, len = args.length, argsStr = []; i < len; i++) {
    argsStr.push('args[' + i + ']')
    }
    var result = eval('ctx[key](' + argsStr + ')')
    delete ctx[key]
    return result
    }
    }

    v2版本,利用之前实现的call或apply

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Function.prototype.bind2 = function () {
    var ctx = arguments[0]
    var args = []
    var self = this
    for (var i = 1, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    return function () {
    for (var i = 0, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    return self.apply2(ctx, args)
    }
    }

    v3版本,考虑生成的函数作为构造函数的情况

  • 生成的函数应该继承原函数的原型链上的属性
  • bind后的函数作为构造函数调用时,this指向new创建的实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Function.prototype.bind2 = function () {
    var ctx = arguments[0]
    var args = []
    var self = this
    for (var i = 1, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    var F = function () {
    for (var i = 0, len = arguments.length; i < len; i++) {
    args.push(arguments[i])
    }
    return self.apply2(this instanceof F ? this : ctx, args)
    }
    var FNOP = function () {}
    FNOP.prototype = self.prototype
    F.prototype = new FNOP()
    return F
    }
    【参考】

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

面试官问:能否模拟实现JS的call和apply方法

感谢您的阅读,本文由 Astar 版权所有。如若转载,请注明出处:Astar(http://example.com/2022/01/09/%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0call%E3%80%81apply%E5%92%8Cbind/
自动部署hexo博客
从常见案例探索JS正则表达式用法