🚡 JavaScript 函数绑定
当将 对象方法 作为回调传递,例如传递给 setTimeout ,会存在一个常见的问题,丢失 this 对象。
# 引入例子:丢失的 this
🌰 例子:
let user = {
firstName: 'Simon',
sayHi() {
console.log(`Hello, ${this.firstName}!`)
}
}
setTimeout(user.sayhi, 1000) // Hello, undefined
2
3
4
5
6
7
8
传递 对象方法 给
setTimeout,但是它与对象分离开了,丢失了 上下文对象user。在 浏览器 中的setTimeout方法中,如果没有指定this上下文,此时的this = window。对于 Node.jsthis则会变为 计时器对象。
# 解决方法:包装器
使用一个 包装函数 解决。
🌰 例子:
let user = {
firstName: 'Simon',
sayHi() {
console.log(`Hello, ${this.firstName}!`)
}
}
setTimeout(function() {
user.sayHi()
}, 1000)
2
3
4
5
6
7
8
9
10
或者使用 箭头函数 形式:
setTimeout(() => user.sayHi(), 1000)
此时
user.sayHi()从外部词法环境中获取到了user对象,所以可以正常的调用。
🌰 例子 / 如果对象在 setTimeout 触发之前改变了,此时这种包装器方法就存在漏洞了:
let user = {
firstName: 'Simon',
sayHi() {
console.log(`Hello, ${this.firstName}!`)
}
}
setTimeout(() => user.sayHi(), 1000)
user = {
sayHi() {
console.log('changed user sayhi()')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 解决方法: bind
函数提供了一个 内建方法 bind ,可以绑定 this 对象。基本语法为:
let boundFunc = func.bind(context)
func.bind(context)的结果是一个 特殊的 类似于函数「外来对象」,可以像函数一样被调用,并且 透明地 将调用传递给func并且设定this = context。简单的说,就是
boundFunc的调用 是 绑定了this的func。
🌰 例子:
let user = {
firstName = 'Simon'
}
function func() {
console.log(this.firstName)
}
let funcUser = func.bind(user)
funcUser() // 'Simon'
2
3
4
5
6
7
8
9
10
funcUser将调用传递给了func同时this = user。
🌰 例子 / bind 还会传递 所有的参数:
function func(phrase) {
console.log(phrase + ', ' this.firstName);
}
let funcUser = func.bind(user)
funcUser("hello") // "hello, Simon"
2
3
4
5
6
🌰 例子 / bind 应用于 对象方法:
let user = {
firstName: 'Simon',
sayHi() {
console.log(`Hello, ${this.firstName}!`)
}
}
let sayHi = user.sayHi.bind(user)
setTimeout(sayHi, 1000)
user = {
sayHi() {
console.log('changed user sayhi()')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
即使在 不到一秒时 改变了原来的对象
user,这里bind取了方法user.sayHi将其绑定到user,所以sayHi是一个 绑定后的 方法,它可以被单独调用,也可以被传递给setTimeout。
🌰 例子 / bind 应用于 对象方法 包括方法的参数:
let user = {
firstName: 'Simon',
sayHi(phrase) {
console.log(`${phrase}, ${this.firstName}!`)
}
}
let say = user.sayHi.bind(user)
say("Hello")
say("Bye")
2
3
4
5
6
7
8
9
10
11
提示
如果对象中有很多方法,并且都打算将它们传递出去,那么可以子啊一个循环中完成所有方法的绑定:
for (let key in user) {
if (typeof user[key] === 'function') {
user[key] = user[key].bind(user)
}
}
2
3
4
5
在 JavaScript 库有提供方便批量绑定的函数,例如 loadash 中的
_.bindAll(object, methodNames)。
# 偏函数
bind 的 完整语法:
let bound = func.bind(context, [arg1], [arg2], ... )
可以看到
bind允许将 上下文 绑定为this,以及绑定函数的 初始参数。
🌰 例子:
function mul(a, b) {
return a * b
}
2
3
使用 bind 在该函数基础上创建一个 double 函数:
let double = mul.bind(null, 2)
console.log(double(3)) // 6
2
3
对
mul.bind(null, 2)的调用创建了一个新函数double,它将调用传递到mul,将null绑定为上下文,并将2绑定为第一个参数。并且,参数均被原样传递。
这种用法被称为 偏函数应用程序, 通过绑定 先有的函数 的一些参数来创建一个新的函数。上面的例子,因为没有用到上下文对象的地方,所以传入的是 null (没有也要传入参数)。
使用偏参数的好处:
- 可以在原来的函数的基础上,创建一个具有 可读性高 的名字的独立函数(例如
double,triple) 。可以只使用它们而不用每次都提供相同的参数。- 当有一个 非常通用的 函数,并且希望有一个 通用型 更低的该函数的变体,使用 偏函数 非常有用。
# 仅绑定参数 partial
当想绑定一些参数到函数,但不需要用到 上下文 this ,原生的 bind 不允许这种情况(不可以省略 context 直接跳到参数)。
partial 可以实现仅绑定参数的函数,用法:
function partial(func, argsBound) {
return function(...args) {
retrun func.call(this, ...argsBound, ...args)
}
}
2
3
4
5
🌰 例子:
let user = {
firstName: "John",
say(time, phrase) {
console.log(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
user.sayNow = partial(user.say, new Date.getHours() + ':' + new Date().getMinutes())
user.sayNow("Hello") //
2
3
4
5
6
7
8
9
10
partial(func[, arg1, arg2...])调用的结果是一个包装器 ,它调用func并具有以下内容:
- 与它获得的函数具有相同的
this(对于user.sayNow调用来说,它是user)- 然后给它
...argsBound—— 来自于partial调用的参数("10:00")- 然后给它
...args—— 给包装器的参数("Hello")
同样 lodash 库中有现成的 _.partial 实现
# 总结
- 方法
func.bind(context, ...args)返回函数func绑定了this(以及给定的一些参数)的 变体。- 通常使用
bind绑定 对象方法 的this,以便把它们传递到其他地方使用,而不丢失上下文对象。
- 通常使用
- 当绑定先有函数的一些参数,绑定后的函数为 偏函数。可以用于不想一遍一遍重复相同地传入参数时可以用
partial。
# 实例
# 二次 bind
当对一个函数应用
bind绑定上下文对象两次时:function f() { console.log(this.name); } f = f.bind( {name: "John"} ).bind( {name: "Ann" } ); f();1
2
3
4
5
6
7
点击查看
最后输出的结果是 John 。链式调用是应用于上一次方法调用后的结果。 bind( ... ) 返回的外来绑定函数仅在创建的时候记忆上下文。所以一个函数不能重绑定。
# bind 后的函数属性
当函数的属性中有一个值,
bind之后值还存在吗?function sayHi() { console.log( this.name ); } sayHi.test = 5; let bound = sayHi.bind({ name: "John" }); console.log(bound.test);1
2
3
4
5
6
7
8
9
10
点击查看
bind 的结果是另一个对象,它并没有 test 属性。
