🚡 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
属性。