call,apply,bind 详解

159 阅读5分钟

    在JavaScript中,call,apply, bind 方法都能够帮助我们改变函数执行时this的指向,将一个对象的方法交给另一个对象来执行,接下来让我们详细整理一下三者的区别和实现方法

1. call()

call()  方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数,其语法如下;

function.call(thisArg, arg1, arg2, ...)

thisArg 即改变后 function 函数运行时this的指向对象,需要注意的是在非严格模式下,该参数若设置为nullundefiendthis会被自动替换为全局对象 Window, 而在严格模式下则不会发生自动替换

  function o() {
      console.log(this)
    }
    o.call(null) //Window
    o.call()//Window
    -------------------------------------------------------------------------------------------
    "use strict";
     function o() {
      console.log(this)
    }
    o.call(null) //null
    o.call()//undefiend

arg1, arg2, ... 是可选的参数,它们将作为实参在 function 方法执行时的传入

 const obj = {
      name: 'newThis'
    } 
    const num = 1111
    function o(a) {
      console.log(this)  
      console.log(a)
    }
    o.call(obj,num)//打印 obj 1111
    //num作为o方法调用时的实参传入

call() 方法的返回值为 function 函数的返回值,若该函数没有返回值,则返回 undefiend

注意当我们执行 function.call()时, function 方法会被直接调用

根据上面的需求,接下来让我们来实现一个自己的 call 方法,重点主要有两个:

  1. function 方法绑定给新的 this

  2. 执行 function 方法,并将其结果返回

Function.prototype.myCall = function (context, ...args) {
      if (typeof this !== 'function') {
        throw new TypeError('error')
      } //如果调用myCall方法的并非是一个function对象,则抛出异常
      const isStrict = (function () { return this === undefined })()//判断当前环境是否开启严格模式
      if (!isStrict) {
        context = context || window //非严格模式下当context为null或者undefined时将其赋值为window
        const type = typeof context//当context不是Object类型时,使用包装类将基本数据类型转换为对象
        if (type === 'number') {
          context = new Number(context)
        } else if (type === 'string') {
          context = new String(context)
        } else if (type === 'boolean') {
          context = new Boolean(context)
        }
      } else if (context === undefined || context === null) {
        //严格模式若context为null或者undefined则直接执行当前function,此时该function的this为undefined
        return this(...args)
      }
      const k = Symbol('k')//使用Symbol确保不会发生命名冲突
      context[k] = this// 相当于 context.k = this 即将function方法添加到context所指向的对象上
      const result = context[k](...args)//执行function,并接收其返回值
      delete context[k]//删除添加到context所指对象上的function,避免污染context所指向的对象
      return result//将function执行的结果作为myCall的返回值
    }

2. apply()

apply() 方法与 call()区别在于第二个参数的形式不同,需要将 function 方法执行时所需要的参数合并为一个数组或者伪数组对象传入,其语法如下:

function.apply(thisArg, argsArray)

thisArgcall 方法的使用相同,表示 function 方法执行时的 this指向

argsArray 一个类数组对象,用于指定调用 function 时的参数,或者如果不需要向函数提供参数,则为 undefiend 或者 null

const obj = {
      name: 'newThis'
    }
    const arr = [1,2]
    function o(a,b) {
      console.log(this)
      console.log(a,b)
    }
    o.apply(obj, arr)// obj 1 2
    //将o方法执行时所需要的参数合并为一个数组传入

apply() 方法的实现与 call() 方法类似,只需要在调用 function 时注意处理参数数组

 Function.prototype.myApply = function (context, argsArr) {
      if (typeof this !== 'function') {
        throw new TypeError('error')
      }
      const isStrict = (function () { return this === undefined })()
      if (!isStrict) {
        context = context || window
        const type = typeof context
        if (type === 'number') {
          context = new Number(context)
        } else if (type === 'string') {
          context = new String(context)
        } else if (type === 'boolean') {
          context = new Boolean(context)
        }
      } else if (context === undefined || context === null) {
        return this(...argsArr)
      }
      const k = Symbol('k')
      context[k] = this
      const result = argsArr ? context[k](...argsArr) : context[k]()//对没有参数的情况进行处理
      delete context[k]
      return result
    }

3. bind()

在实际的使用场景中,有时候我们并不希望 function 方法立即执行,而是在某些条件满足的情况下再进行执行,这时显然 call()apply() 方法都不适用,bind() 方法能很好的解决这一问题

bind()  方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用其语法如下:

function.bind(thisArg[, arg1[, arg2[, ...]]])

其参数传递方式跟call方法一致,但和applycall 稍微有所不同,bind 是一个闭包结构,返回的是一个函数,可以在需要的时候选择执行这个函数

对于bind的实现,最基础的方式直接在返回的函数中调用 call方法即可:

Function.prototype.myBind = function (context, ...args) {
      if (typeof this !== 'function') {
        throw new TypeError('error')
      }
      context = context || window // 判断上下文是否传入,默认window
      const _this = this // 保存this

      // 返回闭包
      return function (...innerArgs) {

        return _this.call(context, ...args, ...innerArgs)//注意bind在调用时可以继续传参
      }
    }

但是这种方式存在一些问题,当返回的函数与 new 配合作为构造函数使用时的情况发生时,构造函数的 this指向其生成的实例对象,此时会造成我们传递的 context 失效,需要进行处理:

 Function.prototype.myBind = function (context,...args) {
      if (typeof this !== 'function') {
        throw new TypeError('error')
      }
      context = context || window // 判断上下文是否传入,默认window
      const _this = this // 保存this


      // 返回闭包
      return function fn(...innerArgs) {

        if (this instanceof fn) {//若使用new关键字返回函数的this指向一个新的fn对象,否则this指向window
          return new _this(...args, ...innerArgs)
        }
        return _this.call(context, ...args, ...innerArgs)
      }
    }

上面的写法使用了大量ES6语法,采用ES5可以进行如下实现:

  Function.prototype.myBind = function (context) {
      if (typeof this !== 'function') {
        throw new TypeError('error')
      }
      var _this = this
      var args = Array.prototype.slice.call(arguments, 1)//arguments是伪数组不能直接调用Array的方法
      var fn = function () {
        var innerArgs = Array.prototype.slice.call(arguments)
        return _this.apply(this instanceof fn ? this : context, args.concat(innerArgs))
      }
      fn.prototype = Object.create(_this.prototype)//改变fn的原型对象,让fn的实例能够继承绑定函数原型中的值
      return fn
    }

参考

手写源码系列 - call + bind + apply

JavaScript深入之bind的模拟实现