【momo的源码基石】硬绑定apply、call、bind用法与实现及其在axios中的体现

67 阅读3分钟

前言:呜呜呜,春招开始了,但是俺的代码内功还不够。在博主的推荐下,我定下了看源码的目标,并从axios开始努力!在阅读的过程中,我发现源码对于硬绑定的封装,巧的是,硬绑定恰好是看过的《你所不知道的JavaScript(上卷)》的重点内容,因此写下此文。

硬绑定

  什么是硬绑定?在《你所不知道的JavaScript》中对this的指向做了非常全面的分析,并对多种绑定的优先级作出比较,硬绑定是其中的一种。

我所理解的硬绑定:通过显式手段对函数(对象)中的this进行强制改变,不管怎么调用(再次硬绑定除外)其中的this都指向硬绑定所带来的this。

apply(thisObj,[...argument])

使用

  apply的第一个参数是Function,但是函数的其他参数将会被封装成数组形式进行传递。

let changeObj={
  name:"changeObj",
  showName:function (){
    console.log(arguments);
    console.log("函数内name"+this.name);
  }
}

let zs={
  name:"zs"
}

changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("apply调用");
changeObj.showName.apply(zs,[1,1,["a"]])
console.log("changeObj.name"+changeObj.name);

打印:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
apply调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj

解释:开始的时候,函数内部的this指向外层的changeObj,但是经过apply处理,函数中的this指向zs

实现

/**
 * 方式一:很多实现都是用eval进行传参,但是俺觉得俺这样都行,没有发现啥问题
 * ps :我知道了,方式一这样结构赋值是es6以上的,可能兼容性问题
 */
Function.prototype.myApplyNew=function(context,args){
    // 确定上下文,如果context是null,
    context = context || window || global
    // 绑定当前函数
    context.fn = this
    if(Array.isArray(args)){
        // 传参
        let result = context.fn(...args)
        // 原来的上下文并没有该函数
        delete context.fu
        // 返回执行的结果
        return result
    }else{
        delete context.fu
        throw "要传入数组捏,小兄弟"
    }
}

// 方式二:
Function.prototype.myApplyOld=function(context,args){
    context = context || window || global
    context.fn = this
    let argArr= [],result
    console.log(args);
    for(let i=0;i<args.length;i++){
        argArr.push(`args[${i}]`)
    }
    result = eval(`context.fn(${argArr})`)
    delete context.fn
    return result
}

call(thisObj,...arguments)

使用

  call函数的第一个参数是Function将要指定的this对象,其他参数则是Function所需要的参数列表,与apply的差别仅是不需要对参数进行数组的封装。

let changeObj={
  name:"changeObj",
  showName:function (){
    console.log(argument)
    console.log("函数内name"+this.name);
  }
}

let zs={
  name:"zs"
}

changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("call调用");
changeObj.showName.call(zs,1,1,["a"])
console.log("changeObj.name"+changeObj.name)

打印输出:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
call调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj

解释:开始的时候,函数内部的this指向外层的changeObj,但是经过call处理,函数中的this指向zs

实现

根据上文中的call函数的特点,我们可以手写自己的call

Function.prototype.myCall=function(context,...args){
    // 确定上下文,如果context是null,
    context = context || window || global
    // 绑定当前函数
    context.fn = this
    // 传参
    let returnObj = context.fn(...args)
    // 原来的上下文并没有该函数
    delete context.fu
    // 返回执行的结果
    return returnObj
}

bind(thisObj,...argument)

使用

  bind和前二者有明显的区别,bind将返回一个函数进行调用,如果需要进行参数的传递,则可以直接添加参数,得到函数后,如果需要继续添加参数,可以在返回的函数中添加。

let changeObj={
  name:"changeObj",
  showName:function (){
    console.log(arguments);
    console.log("函数内name"+this.name);
  }
}

let zs={
  name:"zs"
}

changeObj.showName(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("bind调用");
changeObj.showName.bind(zs)(1,1,["a"])
console.log("changeObj.name"+changeObj.name);
console.log("第二种调用方式");
changeObj.showName.bind(zs,1,1,["a"])()
console.log("继续添加参数");
changeObj.showName.bind(zs,1,1,["a"])(1)

打印:
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namechangeObj
changeObj.namechangeObj
bind调用
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
changeObj.namechangeObj
第二种调用方式
Arguments(3) [1, 1, Array(1), callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs
继续添加参数
Arguments(4) [1, 1, Array(1), 1, callee: ƒ, Symbol(Symbol.iterator): ƒ]
函数内namezs

解释:在bind执行的时候,已经对this进行改变,和前二者不同,他返回一个硬编码的新函数进行供给调用(其上下文指向bind绑定的this

实现

Function.prototype.Mybind=function (context,...args){
    context = context || window || global
    const that = this 
    // 允许传入新增的
    return function(...addArgs){
        return that.call(context, ...[...args,...addArgs])
    }
}

Object.prototype.hasOwnProperty.call()

  其实严格来说本段只能算是call方法的一个另类运用,但是其出现在axios源码中,且在js中也是一个细节,因此抽出进行描述。

// 一般来说,当验证对象是否有某个属性的时候,可以直接对象.hasOwnProperty(属性)
let a={
  name:"zs"
}
console.log(a.hasOwnProperty("name")); // true

// 但是假如对象重写了hasOwnProperty方法,将直接从对象中获取而非原型链上,则不能有效监测
// 此时如果使用Object.prototype.hasOwnProperty.call()进行绑定和验证,才能有效监测
let a={
  name:"zs",
  hasOwnProperty:()=>false
}
console.log(a.hasOwnProperty("name")); // false
console.log(Object.hasOwnProperty.call(a,"name")); // true

axios中的call和apply

  为啥会注意到硬绑定呢?跟前文的Promise一样,也是从axios中知道的。其实axios值得我们初级程序员来说并不只有他发请求、适配器等功能,他的某些工具的实现也是值得学习的。

axios的一处call:axios工具类中的forEach()

  下面的一段代码出自axios的utils文件中的forEeach函数,该函数主要是用于遍历函数的执行,对函数中的this进行重置(指向全局对象,浏览器:window,node:global)并遍历参数进行执行。

/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate,        
 * @param {Function} fn The callback to invoke for each item
 */
function forEach(obj, fn) {
  // 判空
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

   // 进行装载
  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  // 不管是下面的哪一行call都是对函数的触发和函数中的this的清空
  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

  这个封装函数用的是真的多,但是我认为最容易理解且最经典的还是对Axios的请求方法的生成。 image.png

axios中的一处apply

  如果你用过axios.create(config),那你一定间接使用过apply。是用来把配置好的上下文对象赋值给request方法。
  这一图的代码出自axios包/lib/axios.js是入口文件 image.png   下图是bind的代码,主要是上下文对象的硬绑定和传参:

image.png

结语

上面就是apply、call、bind的复习和一些使用,果然,经典的东西够吃一壶!
智者不入爱河,寡王一路offer!
我是momo,我们下期见!

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情