如何写一个实用的bind?

作者: 谦龙 | 来源:发表于2017-05-21 21:22 被阅读36次

    前言

    这是underscore.js源码分析的第五篇,如果你对这个系列感兴趣,欢迎点击

    underscore-analysis/ watch一下,随时可以看到动态更新。

    事情要从js中的this开始说起,你是不是也经常有种无法掌控和知晓它的感觉,对于初学者来说,this简直如同回调地狱般,神乎其神,让人无法捉摸透。但是通过原生js中的bind方法,我们可以显示绑定函数的this作用域,而无需担心运行时是否会改变而不符合自己的预期。当然了下划线中的bind也是模仿它的功能同样可以达到类似的效果。

    ctx

    bind简单回顾

    我们从mdn上的介绍来回顾一下bind的使用方法。

    bind方法创建一个新的函数, 当被调用时,它的this关键字被设置为提供的值。

    语法

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

    简单地看一下这些参数的含义

    1. thisArg

    当绑定函数被调用时,该参数会作为原函数运行时的this指向,当使用new 操作符调用绑定函数时,该参数无效。

    1. arg1, arg2, ...

    当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

    绑定this作用域示例

    window.name = 'windowName'
    
    let obj = {
      name: 'qianlongo',
      showName () {
        console.log(this.name)
      }
    }
    
    obj.showName() // qianlongo
    
    let showName = obj.showName
      showName() // windowName
    
    let bindShowName = obj.showName.bind(obj)
      bindShowName() // qianlongo
    
    

    通过以上简单示例,我们知道了第一个参数的作用�就是绑定函数运行时候的this指向

    第二个参数开始起使用示例

    let sum = (num1, num2) => {
      console.log(num1 + num2)
    }
    
    let bindSum = sum.bind(null, 1)
    bindSum(2) // 3
    
    
    

    bind可以使一个函数拥有预设的初始参数。这些参数(如果有的话)作为bind的第二个参数跟在this(或其他对象)后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。

    参数的使用基本上明白了,我们再来看看使用new去调用bind之后的函数是怎么回事。

    function Person (name, sex) {
      console.log(this) // Person {}
      this.name = name
      this.sex = sex
    }
    let obj = {
      age: 100
    }
    let bindPerson = Person.bind(obj, 'qianlongo')
    
    let p = new bindPerson('boy')
    
    console.log(p) // Person {name: "qianlongo", sex: "boy"}
    
    

    有没有发现bindPerson内部的this不再是bind的第一个参数obj,此时obj已经不再起效了。

    �实际上bind的使用是有一定限制的,在一些低版本浏览器下不可用,你想不想看看下划线中是如何实现一个�兼容性好的bind呢!!!come on

    下划线中bind实现

    源码

    
     _.bind = function(func, context) {
      // 如果原生支持bind函数,就用原生的,并将对应的参数传进去
      if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
      // 如果传入的func不是一个函数类型 就抛出异常
      if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
      // 把第三个参数以后的值存起来,接下来请看executeBound
      var args = slice.call(arguments, 2);
      var bound = function() {
        return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
      };
      return bound;
    };
    
    

    executeBound实现

    var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
      // 如果调用方式不是new func的形式就直接调用sourceFunc,并且给到对应的参数即可
      if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
      // 处理new调用的形式
      var self = baseCreate(sourceFunc.prototype);
      var result = sourceFunc.apply(self, args);
      if (_.isObject(result)) return result;
      return self;
    };
    
    

    上面的源码都做了相应的注释,�我们着重来看一下executeBound的实现

    先看一下这些参数都�代表什么含义

    1. sourceFunc:原函数,待绑定函数
    2. boundFunc: 绑定后函数
    3. context:绑定后函数this指向的上下文
    4. callingContext:绑定后函数的执行上下文,通常就是 this
    5. args:绑定后的函数执行所需参数

    ok,我们来看一下第一句

    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
    
    

    这句话是为了判断绑定后的函数是以new关键字被调用还是普通的函数调用的方式,举个例子

    
    function Person () {
      if (!(this instanceof Person)) {
        return console.log('非new调用方式')
      }
    
      console.log('new 调用方式')
    }
    
    Person() // 非new调用方式
    new Person() // new 调用方式
    
    

    所以如果你希望自己写的构造�函数无论是new还是没用new都起效的话可以用下面的代码

    
    function Person (name, sex) {
      if (!(this instanceof Person)) {
        return new Person(name, sex)
      }
    
      this.name = name
      this.sex = sex
    }
    
    new Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}
    
    Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}
    
    

    我们回到executeBound的解析

    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
    
    

    callingContext是被绑定后的函数的this作用域,boundFunc就是那个被绑定后的函数,那么通过这个if判断,当为非new调用形式的时候,直接利用apply处理即可。

    但是如果是�用new调用的呢?我们看下面这段代码,别看短短的四行代码里面知识点挺多的呢!

    // 这里拿到的是一个空对象,且其继承于原函数的原型链prototype
    var self = baseCreate(sourceFunc.prototype);
    // 构造函数执行之后的返回值
    // 一般情况下是没有返回值的,也就是undefined
    // 但是有时候写构造函数的时候会显示地返回一个obj
    var result = sourceFunc.apply(self, args);
    // 所以去判断结果是不是object,如果是那么返回构造函数返回的object
    if (_.isObject(result)) return result;
    // 如果没有显示返回object,就返回�原函数执行结束后的实例
    return self;
    
    

    好,到这里,我有一个疑问,baseCreate是个什么鬼?

    var Ctor = function(){};
    
    var baseCreate = function(prototype) {
      // 如果prototype不是object类型直接返回空对象
      if (!_.isObject(prototype)) return {};
      // 如果原生支持create则用原生的
      if (nativeCreate) return nativeCreate(prototype); 
      // 将prototype赋值为Ctor构造函数的原型
      Ctor.prototype = prototype; 
      // 创建一个Ctor实例对象
      var result = new Ctor; 
      // 为了下一次使用,将原型清空
      Ctor.prototype = null; 
      // 最后将实例返回
      return result; 
    };
    
    

    是不是好简单,就是实现了原生的Object.create用来做一些继承的事情。

    �结尾

    文章很简短,知道怎么实现一个原生的bind就行。如果你对apply、call和this感兴趣,欢迎查看

    js中call、apply、bind那些事

    this-想说爱你不容易

    相关文章

      网友评论

        本文标题:如何写一个实用的bind?

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