• JS之手写bind原理


    由于最近的工作比较忙,所以导致自己也很长一段时间没写博客了,另一方面则是暂时不知道有啥可写的,不过在最近我看到了一篇关于手写bind原理的文章,和自己之前简单手写的bind完全不是一个level,想着一方面这也是面试经常会问到的一个问题,于是我深挖了一下这部分的知识点,带着大家一起学习一下:

    不知道大家一开始听到手写bind函数是不是和我一样的不屑,这不是有手就行(doge)?然后直接写了类似下面的代码:

    //大致步骤基本是:
    //1、在原型函数上定义一个bind函数,并接受一个要绑定的对象参数
    //2、通过apply将函数绑定在传过来的对象参数上
    //3、返回这个绑定后的新函数
    function bind(fn, obj, ...arr) {
    	return fn.apply(obj, arr)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果只是普通的面试这样基本没问题了,但是各位毕竟作为一名优秀的开发人员,这样还远远不够,因为我们还要考虑原型、函数柯里化和new方法,所以带着这几个问题继续将我们写的代码进行优化:

    1、绑定在原型上的方法
    众所周知,我们在调用某个内置的api时,例如xxx.bind,由于 function xxx 的原型链 指向的是 Function.prototype , 因此我们在调用 xxx.bind 的时候,调用的是 Function.prototype 上的方法,所以我们直接在函数原型链上构建一个我们的_bind方法,Function.prototype._bind = function() {}

    2、改变this的指向
    这可以说是 bind 最核心的特性了,就是改变 this 的指向,并且返回一个函数。而改变 this , 我们可以通过已知的 apply 和 call 来实现,这里我们就暂且使用 apply 来进行模拟。首先通过 self 来保存当前 this,也就是传入的函数。因为我们知道 this 具有 隐式绑定的规则(摘自 《你不知道的JavaScript(上)》2.2.2)

    Function.prototype._bind = function(thisObj) {
    //这里的this指向的就是调用_bind的myname函数
       const self = this
       return function() { 
           self.apply(thisObj)
        }
    }
    
    var obj = {a:1}
    function myname() {
      console.log(this.a)	//输出a的值为1
    }
    myname._bind(obj)()	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3、支持柯里化
    首先我们要清除函数柯里化的作用有:
    ① 接收比较固定的参数,其他的参数由返回的函数接收使用,提高参数的复用能力,提高函数的适用性。
    ② 延迟执行。(用于被要求求值的时候,再一次性求值)
    为了帮助新手便于理解什么是柯里化,这里我举一个具体的例子:

    function fn(x) {
    	return function (y) {
    		return x + y
    	}
    }
    var fn1 = fn(1)
    fn1(2) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    从上面的这个例子不难发现,柯里化使用了闭包,当我们执行 fn1 的时候,函数内使用了外层函数的 x, 从而形成了闭包。
    而我们的 bind 函数也是类似,我们通过先获取当前外部函数的 arguments ,并且去除了绑定的对象,保存成变量 args,最后 return 的方法,再一次获取当前函数传进来的 arguments参数, 最终用 finalArgs 进行了一次合并。(可能看这段话有点难以理解,那看完下面代码你便会恍然大悟)

    Function.prototype._bind = function(thisObj) {
       const self = this
        // 将第一个绑定的对象参数去除,留下其他参数
        const args = [...arguments].slice(1)
        return function (){
            // 考虑二次调用继续传参的情况
            const finalArgs = [...args, ...arguments]
            self.apply(thisObj, finalArgs)
        }
    }
    
     var obj = { i: 1} 
     function myFun(a, b, c) {
         console.log(this.i + a + b + c)
     }
     var myFun1 = myFun._bind(obj, 1, 2)
     myFun1(3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4、考虑new的调用
    我们的方法,通过 bind 绑定之后,依然是可以通过 new 来进行实例化的, new 的优先级会高于 bind(摘自 《你不知道的JavaScript(上)》2.3 优先级)。根据书中提出的关于判断this总结的几条规则是:
    ① 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象
    ② 函数是否通过call/apply绑定 ( 显示绑定 ) ? 如果是的话this绑定的就是指定对象
    ③ 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话this绑定的是那个上下文对象
    ④ 如果都不是的话,使用默认绑定,严格模式下绑定到undefined,否则绑定到全局对象下(即window)
    举个例子帮大家理解一下第一点:

    function foo(something){
         this.a = something
     }
     var obj1 = {}
     var bar = foo.bind(obj1)
     bar(2)
     console.log(obj1.a);    //2
    
     // 我们会发现new bar(3)并没有像我们预计的那样把obj1.a修改为3,相反new修改了硬绑定到obj1的this指向
     // 因为使用了new绑定,我们得到了一个名字为baz的新对象,并且它的值为3
     var baz = new bar(3)
     console.log(obj1.a);    //2
     console.log(baz.a);     //3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    理解以后我们用原生的bind和我们的_bind来做对比。

    原生bind:

    var obj = { i: 1}
    function myFun(a, b, c) {
        // 此处用new方法,this指向的是当前函数 myFun,i的值为undefined
        console.log(this.i + a + b + c)
    }
    var myFun1 = myFun.bind(obj, 1, 2)
    new myFun1(3); // NAN
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    _bind:

    var obj = { i: 1}
    function myFun(a, b, c) {
        console.log(this.i + a + b + c)
    }
    // 此处调用_bind方法,this指向的是 obj,i的值为1
    var myFun1 = myFun._bind(obj, 1, 2)
    new myFun1(3);	//7
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所以我们对_bind函数进行修改,判断是否为new调用的,这里用到new.target,正好是用来检测构造方法是否是通过 new 运算符来被调用的。所以我们对_bind函数进行改造,改造后的_bind如下:

    Function.prototype._bind = function(thisObj) {
        const self = this
        const args = [...arguments].slice(1)
        return function () {
            const finalArgs = [...args, ...arguments];
            //判断是否为new调用,如果是的话,则不能让构造函数成功绑定上thisObj,所以这里绑定的this是一个空的对象,this = {}
            //则result的结果是undefined,最终加出来的结果也就是NaN
            if(new.target !== undefined) {
                var result = self.apply(this, finalArgs)
                if(result instanceof Object) {return reuslt}
                return this
            }else{
                self.apply(thisObj, finalArgs)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5、 保留函数原型

    Function.prototype._bind = function (thisObj) {
      // 判断是否为函数调用
    if (typeof this !== 'function' || Object.prototype.toString.call(this) !== '[object Function]') {
       throw new TypeError(this + ' must be a function')
     }
       const self = this
       const args = [...arguments].slice(1)
       var bound = function () {
           var finalArgs = [...args, ...arguments]
           // new.target 用来检测是否是被 new 调用
           if (new.target !== undefined) {
               // 说明是用new来调用的
               var result = self.apply(this, finalArgs)
               if (result instanceof Object) {return result}
               return this; 
           } else {
               return self.apply(thisArg, finalArgs)
            } 
       }
       if (self.prototype) {
           // 为什么使用了 Object.create? 因为我们要防止,bound.prototype 的修改而导致self.prototype 被修改。
           // 不要写成 bound.prototype = self.prototype; 这样可能会导致原函数的原型被修改。 
           bound.prototype = Object.create(self.prototype); 
           bound.prototype.constructor = self; 
       }
       return bound
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    参考文章:https://baijiahao.baidu.com/s?id=1700147115251557647&wfr=spider&for=pc
    以上就是一个比较完整的 bind 实现了,如果你想了解更多细节的实践,可以查看(也是 MDN 推荐的)
    https://github.com/Raynos/function-bind

  • 相关阅读:
    Selenium 三种等待方式详解 (强制等待、隐式等待、显示等待)
    Vue3的7种和Vue2的12种组件通信
    “酷暑”结束,程序员待在大型互联网公司能一直安逸下去?
    《前端运维》一、Linux基础--05Shell运算符
    Fortify-设置中文语言
    DNS、ICMP和NAT
    设计一个基于 GraphQL 的 Node.js 工单系统
    Vue中使用el-upload+XLSX实现解析excel文件为json数据
    应用聚类算法,预测中国足球在亚洲处于什么水平
    【JavaWeb】JSP基本语法、指令、九大内置对象、四大作用域
  • 原文地址:https://blog.csdn.net/weixin_48956280/article/details/126467632