目录

🚡 JavaScript 函数绑定

当将 对象方法 作为回调传递,例如传递给 setTimeout ,会存在一个常见的问题,丢失 this 对象。

# 引入例子:丢失的 this

🌰 例子:

let user = {
  firstName: 'Simon',
  sayHi() {
    console.log(`Hello, ${this.firstName}`)
  }
}

setTimeout(user.sayhi, 1000) // Hello, undefined
1
2
3
4
5
6
7
8

传递 对象方法 给 setTimeout ,但是它与对象分离开了,丢失了 上下文对象 user 。在 浏览器 中的 setTimeout 方法中,如果没有指定 this 上下文,此时的 this = window 。对于 Node.js this 则会变为 计时器对象。

# 解决方法:包装器

使用一个 包装函数 解决。

🌰 例子:

let user = {
  firstName: 'Simon',
  sayHi() {
    console.log(`Hello, ${this.firstName}`)
  }
}

setTimeout(function() {
  user.sayHi()
}, 1000)
1
2
3
4
5
6
7
8
9
10

或者使用 箭头函数 形式:

setTimeout(() => user.sayHi(), 1000)
1

此时 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()')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 解决方法: bind

函数提供了一个 内建方法 bind ,可以绑定 this 对象。基本语法为:

let boundFunc = func.bind(context)
1

func.bind(context) 的结果是一个 特殊的 类似于函数「外来对象」,可以像函数一样被调用,并且 透明地 将调用传递给 func 并且设定 this = context

简单的说,就是 boundFunc 的调用 是 绑定了 thisfunc

🌰 例子:

let user = {
  firstName = 'Simon'
}

function func() {
  console.log(this.firstName) 
}

let funcUser = func.bind(user)
funcUser() // 'Simon'
1
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"
1
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()')
  }
}
1
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")
1
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)
  }
}
1
2
3
4
5

在 JavaScript 库有提供方便批量绑定的函数,例如 loadash 中的 _.bindAll(object, methodNames)

# 偏函数

bind 的 完整语法:

let bound = func.bind(context, [arg1], [arg2], ... )
1

可以看到 bind 允许将 上下文 绑定为 this ,以及绑定函数的 初始参数

🌰 例子:

function mul(a, b) {
  return a * b
}
1
2
3

使用 bind 在该函数基础上创建一个 double 函数:

let double = mul.bind(null, 2)

console.log(double(3)) // 6
1
2
3

mul.bind(null, 2) 的调用创建了一个新函数 double ,它将调用传递到 mul ,将 null 绑定为上下文,并将 2 绑定为第一个参数。并且,参数均被原样传递。

这种用法被称为 偏函数应用程序, 通过绑定 先有的函数 的一些参数来创建一个新的函数。上面的例子,因为没有用到上下文对象的地方,所以传入的是 null (没有也要传入参数)。

使用偏参数的好处:

  • 可以在原来的函数的基础上,创建一个具有 可读性高 的名字的独立函数(例如 doubletriple ) 。可以只使用它们而不用每次都提供相同的参数。
  • 当有一个 非常通用的 函数,并且希望有一个 通用型 更低的该函数的变体,使用 偏函数 非常有用。

# 仅绑定参数 partial

当想绑定一些参数到函数,但不需要用到 上下文 this ,原生的 bind 不允许这种情况(不可以省略 context 直接跳到参数)。

partial 可以实现仅绑定参数的函数,用法:

function partial(func, argsBound) {
  return function(...args) {
    retrun func.call(this, ...argsBound, ...args)
  }
}
1
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") // 
1
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 属性。

📢 上次更新: 2022/09/02, 10:18:16