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: 你好

运行截图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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. 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)

运行截图

在这里插入图片描述

思考

  • 首先,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方法

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. 绑定函数,就是我们 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. 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
*/

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
}

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. 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
}

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()

最终版本

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
}

其他

今天遇到一个面试题,这种情况

var obj1 = {
  name: 1
}

var obj2 = {
  name: 2
}

function t() {
  console.log(this.name)
}

t.bind(obj1).call(obj2) // 1
t.bind(obj1).apply(obj2) // 1
t.bind(obj1).bind(obj2)() // 1

var fn = t.bind(obj1)
new fn() // 1

我的理解是:bind返回的函数 类似于 obj1.t , obj2.obj1.t this指向还是obj1

end

  • 对个别知识掌握不熟练,写起来就有点难受。
  • 其实难点就在于 new fn 的处理。
  • 写到这里,基本 bind 这个函数我理解透彻了。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lazy_tomato

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值