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

2020-12-09  本文已影响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;
}
上一篇下一篇

猜你喜欢

热点阅读