手写 call、apply和bind方法

95 阅读3分钟

1.手写 call 方法

手写 call 方法之前,我们得了解 call 的特征:

  1. call() -> 相当于调用了前面的函数
  2. this -> call 方法把前面函数的 this 指向了 call 方法的第一个参数
  3. call 从第二个参数开始就传进了函数里面

接下来我们来手写一下:

    Function.prototype.myCall = function (ctx) {
        // call 第一个值一定要是一个引用值,普通值没有意义,所以我们把普通值也转换为一个对象
        ctx = ctx ? Object(ctx) : window
        // 一个函数谁调用它,函数的 this 指向就指向调用者
        // 保存调用者,也就是那个函数 // 示例:test.myCall({}, 1, 2)
        ctx.originFn = this

        // 保存参数的数组
        var args = []

        // 拿到 myCall 的第二个参数开始到结束的所有参数作为 test 的实参列表
        for (var i = 1; i < arguments.length; i++) {
          // 传 arguments[i] 字符串进去,然后后面传进 eval , eval 函数会将传入的字符串当做 JavaScript 代码进行执行
          args.push('arguments[' + i + ']')

          // 第二种是错误示范,方便大家理解为什么要用 eval
          // args.push(arguments[i])
        }
        // 执行函数,并把接受的参数传进去
        // 字符串 + 数组 数组会调用 toString() 转换成字符串
        var ret = eval('ctx.originFn(' + args + ')')

        // 这样会变成 ctx.originFn('zhangsan,lisi')
        // var ret = ctx.originFn('' + args)
        // 调用完后我们要删除掉这个函数(引用)
        delete ctx.originFn

        // 返回这个函数的返回值
        return ret
      }

      -------------------------------------------------------------------

      // 测试
      function test() {
        console.log(this, arguments)
      }
      test.myCall(
        {
          a: 1,
          b: 2,
        },
        'zhangsan',
        'lisi'
      )

2. 手写 apply 方法

apply 方法和 call 方法差不多,接下来我们还是先来了解一下 apply 的特征:

  1. apply() -> 相当于调用了前面的函数
  2. 第二个参数只接收一个数组,后面多余的参数忽略
  3. 第二个参数传原始值报错,如果传 object,null,undefined,Function 不报错,但 arguments 的 length 为 0

由于判断的类型比较多,我们来自己封装一个 typeOf 函数

    function typeOf(value) {
        // 如果值为 null 返回 null
        if (value === null) {
          return 'null'
        }
    
    // ({}).toString.call(value) -> [object Object] 其实就是对象索引取值
        return typeof value === 'object'
          ? {
              '[object Object]': 'Object',
              '[object Array]': 'Array',
              '[object Number]': 'Number',
              '[object String]': 'String',
              '[object Boolean]': 'Boolean',
            }[{}.toString.call(value)]
          : typeof value
        }

接下来我们来手写 apply:

     Function.prototype.myApply = function (ctx, args) {
        // apply 第一个值一定要是一个引用值,普通值没有意义,所以我们把普通值也转换为一个对象
        ctx = ctx ? Object(ctx) : window
        // 一个函数谁调用它,函数的 this 指向就指向调用者
        // 保存调用者,也就是那个函数 // 示例:test.myApply({}, [1,2])
        ctx.originFn = this

        // 如果是原始值我们就报错,注意这里用的是 js 原生的 typeof
        if (typeof agrs !== 'object' && typeof args !== 'function') {
          throw new TypeError('CreateListFromArrayLike called on non-object')
        }

        // 如果没有参数或者参数是 object,null,undefined,Function 我们就直接执行函数
        // 这里用的是我们自己封装的 typeOf 
        if (!args || typeOf(args) !== 'Array') {
          return ctx.originFn()
        }

        var ret = eval('ctx.originFn(' + args + ')')
        // 调用完后我们要删除掉这个函数(引用)
        delete ctx.originFn

        // 返回这个函数的返回值
        return ret
        
        ---------------------------------------------------------
        
        // 测试
        function test() {
          console.log(this, arguments)
        }
        test.myApply(
          {
            a: 1,
            b: 2,
          },
          function () {}
        )
  }

手写 bind 方法

bind 方法会比前两个稍微难一点,所以拿到最后来讲,还是老样子,先了解 bind 方法的特征:

  1. bind() -> bind方法执行了,但是前面的函数不执行
  2. bind 会返回一个新的函数
  3. bind 的第一个参数会改变前面函数的 this 指向
  4. bind 从第二个参数开始会依次传进 test 方法里,返回的函数可以继续接着传参数,比如 test 接收两个参数,bind 方法传入一个参数,返回的函数调用接着再传一个
  5. new (bind方法返回的函数),this指向为 test 实例,实例应该继承原型上的属性和方法

了解完之后我们手写下 bind 方法:

    Function.prototype.myBind = function (ctx) {
        // 拿到 this 指向,也就是调用者
        var originFn = this,
          // bind 传递的参数,删掉一个参数,拿到后面的参数(第一个参数是 ctx 改变 this 指向我们不需要,我们只需要它后面的参数)
          args = [].slice.call(arguments, 1),
          _tempFn = function () {}

        var newFn = function () {
          // 新函数传递的参数 
          // 示例:const fn = test.bind('张三'); fn('李四');fn就是新函数,传进去的参数就是李四
          var newArgs = [].slice.call(arguments)
          // 如果 new bind方法返回的函数,我们就判断一下,如果是 new 的,那么就是 instanceof bind方法返回的函数,如果为 true 返回 this,否则就为 bind 方法第一个参数作为 this 指向
          // 示例:new fn()   fn 是什么,不就是我们的返回的 newFn 吗,所以 instanceof new Fn 判断它有没有 new,如果 new 了就直接返回 this,否则为 bind 方法第一个参数 ctx
          return originFn.apply(
            this instanceof newFn ? this : ctx,
            args.concat(newArgs)
          )
        }

        // 直接赋值共用一个原型不太好
        // newFn.prototype = this.prototype
        
        // 这里大家自己理解一下吧,我不知道怎么描述
        _tempFn.prototype = this.prototype
        newFn.prototype = new _tempFn()
        
        // 返回新函数
        return newFn
  }