• javascript复习之旅 9.2初识到手写 javascript 中的 bind 函数


    start

    • 今天研究一下 bind 函数,并且手写一下。

    • 阅读本文之前请熟练掌握:

      1. this 指向;
      2. call apply
      3. 原型以及原型链
      4. new 关键词
    • 时间仓促,编写若有错误之处,请见谅。

    1. bind 是什么?

    老规矩,第一步先看MDN 官方文档

    在这里插入图片描述

    由上图可得:

    1. 和之前学习的call,apply一样, bind 也是 Function 原型上的方法。

    2. 再看看MDN官方介绍:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

    2. bind 基本使用

    2.1 浅试一下 bind

    代码

    // 1. 定义一个对象 obj
    var obj = {
      name: '你好',
    }
    
    // 2. 定义一个函数tomato
    function tomato() {
      // 3. 函数执行的时候打印它的this上的name属性
      console.log('执行tomato:', this.name)
    }
    
    // 4.正常情况下 执行tomato
    tomato() // 执行tomato:
    
    // 5. 调用bind
    var fn1 = tomato.bind(obj)
    
    // 6. 打印一下fn1
    console.dir(fn1) // ƒ bound tomato()
    
    // 7. 执行一下 fn1
    fn1() // 执行tomato: 你好
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行截图

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a56w31Kv-1656679178638)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67d4843686064492a8f0426680bb7a15~tplv-k3u1fbpfcp-watermark.image?)]

    总结:

    1. bind 能改变 this 指向;

    2. 返回的是一个函数;

    3. A.bind() 并不会执行函数 A 本身;

    2.2 bind 函数传参的情况

    代码

    // 1. 声明一个对象
    var obj = {
      name: '你好',
    }
    
    // 2. 声明一个函数
    function tomato() {
      console.log('执行tomato:', this.name, ...arguments)
    }
    
    // 3. 执行一下这个函数
    tomato()
    
    // 4. 在bind函数中传参
    var fn1 = tomato.bind(obj, 1, 2, 3, 4, '我是bind前面的参数')
    
    // 5. 查看一下fn1
    console.dir(fn1)
    
    // 6. 调用 fn1 不传参数
    fn1()
    
    // 7. 调用 fn1 传参数
    fn1('我是bind后面的参数', 6)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行截图
    在这里插入图片描述

    总结

    1. bind 方法的参数,除了第一项,后续的参数会传递给新生成的绑定函数 A。

    2. 执行绑定函数 A 的时候,参数是取 bind()的参数 和 绑定函数 A 的参数 的集合。

    3. 注意一下顺序:先 bind()的参数绑定函数A的参数

    2.3 研究一下 bind 方法 生成的函数

    使用 chrome 浏览器的控制台,打印 bind 生成的函数fn1,这个函数带有 bound 这个单词(2.2 章节的运行截图就有)。有点意思,我们去了解了解它。

    代码

    // 1. 定义一个对象 obj
    var obj = {
      name: '你好',
    }
    
    // 2. 定义一个函数tomato
    function tomato() {
      // 3. 函数执行的时候打印它的this上的name属性
      console.log('执行tomato:', this.name)
    }
    
    // 4. 调用bind
    var fn1 = tomato.bind(obj)
    
    // 5. 打印一下fn1
    console.log(fn1)
    
    console.dir(fn1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行截图

    在这里插入图片描述

    思考

    • 首先,console.log 这个由 bind 生成的函数,右下角会有一个蓝色的i图标,鼠标移动上去后,会提示这么一句话:Function was resolved from bound funciton。(后面的中文是我意译的)

    • 其次,console.dir 这个由 bind 生成的函数,有些陌生的内置属性。我们对照官方文档来理解一下。

    123

    对照我上面的示例,我捋捋逻辑:

    1. tomato.bind(obj) 会创建一个新的函数fn1
    2. fn1上有四个内置属性
      [[Prototype]]
      // 隐式原型属性 __proto__
      
      [[TargetFunction]]
      // 英译:目标函数;
      // mdn解释:包装的函数对象;
      // 我理解的,就是我们调用bind的函数tomato
      
      [[BoundThis]]
      // 英译:绑定的this;
      // mdn解释:在调用包装函数时始终作为this值传递的值;
      // 我理解的,就是我们调用bind传入的第一个参数 obj
      
      [[BoundArgs]]
      // Bound arguments的简写
      // 英译:绑定的参数;
      // mdn解释:列表,在对包装函数做任何调用都会优先用列表元素填充参数列表;
      // 我理解的,就是我们调用bind传入的第一个参数之后的参数列表
    
      // [[Call]]
      // 这里我的chrome浏览器控制台没有打印, 我感觉就是Function原型上的call方法
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.4 和 new 关键词的爱恨纠葛

    思考

    call apply 有些不同,bind 返回的是一个绑定函数

    如果 new 这个 绑定函数 会怎样?

    先看下mdnbind函数第一个参数thisArg的的说明:

    1. 调用绑定函数时作为 this 参数传递给目标函数的值。
    2. 如果使用new运算符构造绑定函数,则忽略该值。
    3. 当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。
    4. 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
    
    • 1
    • 2
    • 3
    • 4

    个人理解

    内容有点多,重点理解第二条即可!!!

    1. 绑定函数,就是我们 bind()生成的函数,例如我们上面演示代码的fn1;

    2. 如果使用 new 运算符构造绑定函数,则忽略该值。 当 new fn1() 时,忽略掉我们 bind 的 obj 参数;

    3. 看下面的代码理解一下。

    function tomato() {
      console.log('执行tomato:', this, ...arguments)
    }
    
    var fn1 = tomato.bind('我是番茄', 1, 2)
    setTimeout(fn1, 1000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    1. bind 没有传参数或者 null 或 undefined 的情况,this 指向为执行作用域的 this。

    捋清楚这里!!

    这里建议多读几遍。

    1. 其实在学习 this 的时候,就知道 修改 this 指向:new 关键词优先级 > bind 优先级。

      在 MND 对 bind 函数的第一个参数的 说明中有这句话 : 如果使用new运算符构造绑定函数,则忽略该值。 换成我的大白话来说就是:当我们 new 绑定函数, this 指向不再是根据 bind 的第一个参数来,而是根据 new 关键词的规则来!!!

    2. 那么 new 做什么的? (建议掌握 new 这个关键词,再看后续)

      new Foo()发生了什么

      • new 创建了一个对象 A。
      • 对象 A 的隐式原型指向函数的显式原型(Foo.prototype)
      • 运行函数 && 函数的 this 指向这个对象 A
      • new foo() 返回结果 ,函数有返回对象,结果=》这个对象; 函数没有返回对象,结果=》new 出来的对象 A。

    代码验证

    var obj = {
      name: '我是obj的name',
    }
    
    function tomato() {
      console.log('执行tomato', this)
    }
    
    var fn = tomato.bind(obj, 1, 2, 3)
    
    console.log(fn.prototype, tomato.prototype, fn.prototype === tomato.prototype)
    // ① 正常调用 绑定函数fn
    // fn()  // 执行tomato { name: '我是obj的name' }
    
    // ② 使用new 触发绑定函数fn
    new fn() // 执行tomato tomato {}
    /*
    new fn()
    
    1. 创建了一个空对象A
    2. 空对象 `A.__proto__` 指向 `fn.prototype`
    3. 执行fn ,fn的this指向对象A
    4. 返回一个对象
    
    所以 new fn()的时候,打印 this 的输出是 `{}`。这个`{}`是,new fn创建的对象A
    */
    
    • 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

    3. 开始手写 bind

    1. 基础的功能: 修改this;接收参数;返回函数;

    // 1.和call类似,在Function原型上,创建一个名为 myBind的函数
    Function.prototype.myBind = function (content) {
      // 3. 拿到调用 myBind 的函数
      var that = this
    
      // 5. 拿到bind的参数
      let args = Array.prototype.slice.call(arguments, 1)
    
      var fn = function () {
        // 6.拿到调用 绑定函数 传递的参数
        let args2 = Array.prototype.slice.call(arguments)
    
        // 4.调用 绑定函数 的时候,修改this指向改为content,且执行that函数
        that.apply(content, args.concat(args2))
      }
      // 2.返回一个函数
      return fn
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2. 处理 new 关键词

    Function.prototype.myBind = function (content) {
      var that = this
    
      let args = Array.prototype.slice.call(arguments, 1)
    
      var fn = function () {
        let args2 = Array.prototype.slice.call(arguments)
    
        // that.apply(content, args.concat(args2))
        // 处理 new fn的情况
        that.apply(this instanceof fn ? this : content, args.concat(args2))
      }
      return fn
    }
    
    var obj = {
      name: 'woshi OBJ',
    }
    
    function tomato() {
      console.log('番茄', this)
    }
    
    tomato.myBind(obj, 1, 2, 3)
    /**
     * 如何处理 `new 绑定函数` 的情况呢? 参照上面的代码
     *
     * 1. new 运算符对bind函数的影响,无非就是影响 绑定函数的this ==> 也就是上面示例的  函数fn中的this  ==> 修改apply的第一个参数即可
     * 2. 而new优先级大于bind  ==> 在new fn的时候 fn 的 this 不再指向 content,而是遵循new的逻辑,指向 fn 自己的 this 即可。==> 然后fn 自己的 this 就是 new生成的新obj
     * 3. 怎么判断是 new fn的情况呢? ==>   new fn, 函数fn自己的this指向的是一个new生成的新对象 ==> 这个新对象的隐式原型指向fn的显式原型==> `新生成的obj  instanceof fn` true ==> this
     */
    
    • 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
    • 28
    • 29
    • 30
    • 31

    这里我啰嗦一下,说明一下我的逻辑:

    1. bind生成的函数 fn,正常情况下this指向bind()第一个参数。
    2. 当new fn的时候,会执行fn,且此时fn的this不在是bind的第一个参数了,而是一个新的对象。

    回归到我们的手写bind的代码中,具体的体现如下:

    1. this,并不是一直指向 bind的第一个参数了,要在 apply的时候做判断逻辑,如果是new的情况,this指向fn自己的this。

    2. 怎么判断是 new fn的情况呢? new fn的时候,fn的this指向 新生成的对象,判断fn在不在这个新生成对象原型链上即可

    3. 处理返回值和异常

    Function.prototype.myBind = function (content) {
      // 2.异常情况处理
      if (typeof this !== 'function') {
        throw new Error(
          'Function.prototype.myBind - what is trying to be bound is not callable'
        )
      }
      var that = this
    
      let args = Array.prototype.slice.call(arguments, 1)
    
      var fn = function () {
        let args2 = Array.prototype.slice.call(arguments)
        // 1.返回值
        return that.apply(this instanceof fn ? this : content, args.concat(args2))
      }
      return fn
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    4. 原型上的逻辑优化

    Function.prototype.myBind = function (content) {
      if (typeof this !== 'function') {
        throw new Error(
          'Function.prototype.myBind - what is trying to be bound is not callable'
        )
      }
    
      var that = this
    
      let args = Array.prototype.slice.call(arguments, 1)
    
      var fn = function () {
        let args2 = Array.prototype.slice.call(arguments)
        return that.apply(this instanceof fn ? this : content, args.concat(args2))
      }
    
      // 为了解决我们new fn生成的对象没有tomato的属性和方法。  让fn.prototype = that.prototyp
      // fn.prototype = that.prototype
    
      // 但是这样有一个缺点,那就是我们修改fn的原型就修改了that的原型,我们换个中转函数jump处理一下。
      // 怎么处理的? 啰嗦一句,很简单就是让输出的fn的显式原型等于 jump的实例。
    
      function jump() {}
    
      jump.prototype = that.prototype
    
      fn.prototype = new jump()
      return fn
    }
    
    function tomato() {
      console.log('tomato执行了')
    }
    
    tomato.prototype.say = function () {
      console.log('tomato原型上的say方法')
    }
    
    var fn = tomato.bind({}, 1, 2, 3)
    var obj1 = new fn()
    obj1.say() // tomato原型上的say方法
    
    var myFn = tomato.myBind({}, 1, 2, 3)
    
    myFn.prototype.say = function () {
      console.log('替换say方法')
    }
    var obj2 = new myFn()
    obj2.say() // obj2.say is not a function
    
    new tomato().say()
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    最终版本

    Function.prototype.myBind = function (content) {
      if (typeof this !== 'function') {
        throw new Error(
          'Function.prototype.myBind - what is trying to be bound is not callable'
        )
      }
    
      var that = this
    
      let args = Array.prototype.slice.call(arguments, 1)
    
      var fn = function () {
        let args2 = Array.prototype.slice.call(arguments)
        return that.apply(this instanceof fn ? this : content, args.concat(args2))
      }
      function jump() {}
    
      jump.prototype = that.prototype
    
      fn.prototype = new jump()
      return fn
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    end

    • 对个别知识掌握不熟练,写起来就有点难受。
    • 其实难点就在于 new fn 的处理。
    • 写到这里,基本 bind 这个函数我理解透彻了。
  • 相关阅读:
    GO语言网络编程(并发编程)定时器
    docker 部署node项目
    词向量的维度大概多少才够?
    ES8311 - 音频编解码芯片调试
    【毕业设计推荐】基于MATLAB的水果分级系统设计与实现
    uniapp小程序定位;解决调试可以,发布不行的问题
    Spire.PDF for .NET【文档操作】演示:更改 PDF 版本
    JavaScript-修炼之路第七层
    Mybatis快速入门
    【Rust】快速教程——一直在单行显示打印、输入、文件读写
  • 原文地址:https://blog.csdn.net/wswq2505655377/article/details/125565433