美文网首页
手撕 call()、apply()、bind 源码

手撕 call()、apply()、bind 源码

作者: 酷酷的凯先生 | 来源:发表于2020-12-09 14:44 被阅读0次

    # 前言

    都知道 call、apply 与 bind 三兄弟都是用来替换函数中不想用的 this,使用方法如:

    1. call(this, ...arguments)
      => 立即执行,参数单个传入
    2. apply(this, [arguments])
      => 立即执行,参数是需放在数组里
    3. let fn = bind(this, ...arguments)
      => 绑定之后返回一个新函数但是并不立即执行,需调用的时候才执行。
      且绑定的时候可以额外传参数,执行的时候也可以额外传参数。

    # Call 与 Apply

    callapply 执行的本质是:往要绑定的 context 对象下添加该函数,然后执行,最后将属性删除。
    context 值为 nullundefined 时,非严格模式下,它将替换为 window 或者 global 全局变量。

    call()

    首选说下这个 call 的原理:call() 方法用改变函数的 this 指向,它接收多个参数,第一个参数为执行作用域,第二个及以后的参数是传递给函数的参数。

    步:把我们的 call 方法 myCall 定义到函数原型对象上

    // 全局添加一个 myCall 方法
    Function.prototype.myCall = function(context) {
        
    }
    

    步:接收一个参数 context

    // 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
    let _context = context? Object(context) : window;
    

    步:要让函数的 this 指向参数 context,则这个函数必须是 context 的一个属性或方法
    因此这里为 context 添加一个 fn 方法,并把这个函数赋给这个方法

    _context.fn = this;
    

    步:遍历 arguments 对象,把这些参数转为真正的数组,并拆分为参数传给函数并执行

    var argArr = []
    // 遍历参数,因为首项是 context,所以要从次项开始遍历才是参数
    for (let i = 1; i < arguments.length; i++) {
        argArr.push('arguments['+ i + ']');
    }
    // 或者用一下两种方法得到 argArr:
    // 1. let argArr = [...arguments].slice(1)
    // 2. let argArr = Array.from(arguments).slice(1)
    
    // 执行 _context 的 fn 方法,把 argArr 拆分
    eval("_context.fn(" + argArr + ")");  // => _context.fn(...argArr)
    

    步:要把 _context 对象中的 fn 方法移除,完整代码如下

    Function.prototype.myCall = function(context) {
      // 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
      let _context = context? Object(context) : window;
      _context.fn = this;
     
      var argArr = []
      // 遍历参数,因为首项是 context,所以要从次项开始遍历才是参数
      for (let i = 1; i < arguments.length; i++) {
        argArr.push('arguments['+ i + ']');
      }
      // 或者用一下两种方法得到 argArr:
      // 1. let argArr = [...arguments].slice(1)
      // 2. let argArr = Array.from(arguments).slice(1)
    
      // 执行 _context 的 fn 方法,把 argArr 拆分
      eval("_context.fn(" + argArr + ")");  // => _context.fn(...argArr);
      // 移除 fn 方法
      delete _context.fn;
    }
    

    apply()

    实现了 call() 方法,apply() 方法就信手拈来了。因为 apply()call() 方法的区别在于第二位参数。
    apply() 方法第二位参数也是传递给函数的参数,但是它是一个数组类型的,如下完整代码:

    Function.prototype.myApply = function(context, argArr) {
      // 参数是否存在,如果存在则转为 Object 类型,否则直接取 window 对象为默认对象
      let _context = context? Object(context) : window;
      _context.fn = this;
     
      var argList = []
      // 当这个参数数组不存在或者为空时,直接执行函数,否则把数组拆分后传递给函数并执行
      if (!argArr || argArr.length == 0) {
            _context.fn()
      } else {
        for (let i = 0; i < argArr.length; i++) {
          argList .push('argArr['+ i + ']')
        }
        // 执行 _context 的 fn 方法,把 argList  拆分
        eval("_context.fn(" + argList  + ")")
      }
      
      // 移除 fn 方法
      delete _context.fn;
    }
    

    # Bind()

    bind() 原理:bind() 依然用来改变函数 this 指向,但它不会像 call()apply() 方法会立即执行这个函数,而是返回一个 新函数 给外部,外部用一个变量去接收这个新函数并执行。
    注意:bind() 方法返回的那个函数不仅仅可以作为普通函数调用,还可以作为一个构造函数被调用。

    步:首先判断执行 myBind() 方法的是不是一个函数

    Function.prototype.myBind = function(context) {
        // 判断调用 myBind 方法的是否为函数
        if (typeof(this) !== "function") {
            throw Error("调用 myBind 方法的必须为函数")
        }
    }
    

    步:截取第一个参数
    注意:这里建立在存在 call()方法的条件下,可以直接使用 Array.slice.call()arguments 对象转为真正的数组并截取从第二项开始的参数:

    // 截取传给函数的参数
    let args = Array.prototype.slice.call(arguments, 1)
    

    步:将执行 bind() 方法的这个函数保存在一个变量中

    let _fn = this
    

    步:再创建一个新的函数变量,用来改变函数 this

    let bindFn = function() {
      // 获取_bind方法返回的函数的参数
      let newArgs = Array.prototype.slice.call(arguments)
      // 通过 apply 去改变 this 指向
      let _obj = this.constructor === _fn ? this : context
      _fn.apply(_obj, args.concat(newArgs))
    }
    

    特别注意:
    let newArgs = Array.prototype.slice.call(arguments) 这段代码不要跟
    let args = Array.prototype.slice.call(arguments,1) 这段代码搞混淆了
    newArgs 是返回的新函数的参数,而 argsmyBind () 方法接收并传递给调用它的函数的参数。

    再来看看 let _obj = this.constructor === _fn ? this : context 这段代码
    上面说到 bind() 方法返回的新函数,可以普通调用也可以构造函数方式调用,
    当为构造函数时,this 是指向实例的,因此才会做这样的处理。

    args.concat(newArgs) 是什么意思呢?
    bind() 方法的参数具有一个特性,就是函数柯里化:保留一个参数的位置,再第二次传参的时候自动把参数存入到这个位置中,而这段代码正是用来实现函数柯里化的。

    步:既然 bind() 返回的函数可以作为构造函数,那么它得继承调用它的那个函数的原型对象以及属性,这里创建一个媒介函数,用来实现寄生组合式继承:

    let ProtoFn = function(){ };
    ProtoFn.prototype = _fn.prototype;
    bindFn.prototype = new ProtoFn();
    

    完整代码如下:

    Function.prototype.myBind = function(obj) {
        // 判断调用 myBind 方法的是否为函数
        if (typeof(this) !== "function") {
            throw Error("调用_bind方法的必须为函数")
        }
        // 截取传给函数的参数
        let args = Array.prototype.slice.call(arguments, 1)
    
        // 保存这个函数,以便后续使用
        let _fn = this
    
        // 创建一个待会儿返回出去的函数,这个函数会赋到外部变量中
        let bindFn = function() {
            // 获取_bind方法返回的函数的参数
            let newArgs = Array.prototype.slice.call(arguments)
            // 通过apply去改变this指向,实现函数柯里化
            let _obj = this.constructor === _fn ? this : context
            _fn.apply(_obj, newArgs.concat(args))
        }
    
        // 创建一个中介函数,以便实现原型继承
        let ProtoFn = function(){}
        ProtoFn.prototype = _fn.prototype
        bindFn.prototype = new ProtoFn()
    
        // 返回bindFn的函数给外部
        return bindFn;
    }
    

    相关文章

      网友评论

          本文标题:手撕 call()、apply()、bind 源码

          本文链接:https://www.haomeiwen.com/subject/fiacgktx.html