start
-
今天研究一下 bind 函数,并且手写一下。
-
阅读本文之前请熟练掌握:
- this 指向;
- call apply
- 原型以及原型链
- new 关键词
-
时间仓促,编写若有错误之处,请见谅。
1. bind 是什么?
老规矩,第一步先看MDN 官方文档
由上图可得:
-
和之前学习的
call,apply
一样,bind
也是 Function 原型上的方法。 -
再看看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: 你好
运行截图
总结:
-
bind
能改变 this 指向; -
返回的是一个函数;
-
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)
运行截图
总结
-
bind 方法的参数,除了第一项,后续的参数会传递给新生成的绑定函数 A。
-
执行绑定函数 A 的时候,参数是取 bind()的参数 和 绑定函数 A 的参数 的集合。
-
注意一下顺序:先
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 生成的函数,有些陌生的内置属性。我们对照官方文档来理解一下。
对照我上面的示例,我捋捋逻辑:
tomato.bind(obj)
会创建一个新的函数fn1
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 这个 绑定函数 会怎样?
先看下mdn
对bind
函数第一个参数thisArg
的的说明:
1. 调用绑定函数时作为 this 参数传递给目标函数的值。
2. 如果使用new运算符构造绑定函数,则忽略该值。
3. 当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。
4. 如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
个人理解
内容有点多,重点理解第二条即可!!!
-
绑定函数,就是我们 bind()生成的函数,例如我们上面演示代码的
fn1
; -
如果使用 new 运算符构造绑定函数,则忽略该值。 当 new fn1() 时,忽略掉我们 bind 的 obj 参数;
-
看下面的代码理解一下。
function tomato() {
console.log('执行tomato:', this, ...arguments)
}
var fn1 = tomato.bind('我是番茄', 1, 2)
setTimeout(fn1, 1000)
- bind 没有传参数或者 null 或 undefined 的情况,this 指向为执行作用域的 this。
捋清楚这里!!
这里建议多读几遍。
-
其实在学习 this 的时候,就知道 修改 this 指向:new 关键词优先级 > bind 优先级。
在 MND 对 bind 函数的第一个参数的 说明中有这句话 :
如果使用new运算符构造绑定函数,则忽略该值。
换成我的大白话来说就是:当我们new 绑定函数
, this 指向不再是根据 bind 的第一个参数来,而是根据 new 关键词的规则来!!! -
那么 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
*/
这里我啰嗦一下,说明一下我的逻辑:
- bind生成的函数 fn,正常情况下this指向bind()第一个参数。
- 当new fn的时候,会执行fn,且此时fn的this不在是bind的第一个参数了,而是一个新的对象。
回归到我们的手写bind的代码中,具体的体现如下:
-
this,并不是一直指向 bind的第一个参数了,要在 apply的时候做判断逻辑,如果是new的情况,this指向fn自己的this。
-
怎么判断是 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 这个函数我理解透彻了。