Function.prototype.bind()

Function.prototype.bind()

要想改变被调用函数的上下文,可以使用 callapply 方法,但如果重复使用就会很不方便,因为每次都要把上下文对象作为参数传递,而且还会使代码变得不直观,针对这种情况,我们可以使用 bind 方法来永久地绑定函数的上下文,使其无论被谁调用,上下文都是固定的

基本语法

bind 方法的定义见 ECMAScript 15.3.4.5 Function.prototype.bind() ,使用方式如下

1
fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数, 它的参数是 bind() 的其他参数和其原本的参数,bind 返回的绑定函数也能使用 new 操作符创建对象(这种行为就像把原函数当成构造器),提供的 this 值被忽略,同时调用时的参数被提供给模拟函数,bind 方法与 callapply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数

还可以写成 fn.bind(obj, arg1)(arg2)

一句话概括就是,该方法创建一个新函数,称为绑定函数,绑定函数会以创建它时传入 bind 方法的第一个参数作为 this,传入 bind 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var someuser = {
name: 'abc',
func: function () {
console.log(this.name)
}
}

var foo = {
name: 'foobar'
}

foo.func = someuser.func
foo.func() // 输出 foobar

foo.func1 = someuser.func.bind(someuser)
foo.func1() // 输出 abc

func = someuser.func.bind(foo)
func() // 输出 foobar

func2 = func
func2() // 输出 foobar

上面代码直接将 foo.func 赋值为 someuser.func,调用 foo.func() 时,this 指针为 foo,所以输出结果是 foobarfoo.func1 使用了 bind 方法,将 someuser 作为 this 指针绑定到 someuser.func,调用 foo.func1() 时,this 指针为 someuser,所以输出结果是 abc

全局函数 func 同样使用了 bind 方法,将 foo 作为 this 指针绑定到 someuser.func,调用 func() 时,this 指针为 foo,所以输出结果是 foobar,而 func2 直接将绑定过的 func 赋值过来,与 func 行为完全相同

使用 bind 绑定参数表

bind 方法还有一个重要的功能,那就是绑定参数表,如下例所示

1
2
3
4
5
6
7
8
9
10
11
var person = {
name: 'foo',
says: function (act, obj) {
console.log(this.name + ' ' + act + ' ' + obj)
}
}

person.says('loves', 'bar') // 输出 foo loves bar

fooLoves = person.says.bind(person, 'loves')
fooLoves('you') // 输出 foo loves you

可以看到,fooLovesthis 指针绑定到了 person,并将第一个参数绑定到 loves,之后在调用 fooLoves 的时候,只需传入第三个参数,这个特性可以用于创建一个函数的捷径,之后我们可以通过这个捷径调用,以便在代码多处调用时省略重复输入相同的参数,也就是下面会说到的 currying

理解 bind

尽管 bind 很优美,还是有一些令人迷惑的地方,例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var someuser = {
name: 'abc',
func: function () {
console.log(this.name)
}
}

var foo = {
name: 'foobar'
}

func = someuser.func.bind(foo)
func() // 输出 foobar

func2 = func.bind(someuser)
func2() // 输出 foobar

全局函数 func 通过 someuser.func.bindthis 指针绑定到了 foo,调用 func() 输出了 foobar ,我们试图将 func2 赋值为已绑定的 func 重新通过 bindthis 指针绑定到 someuser 的结果, 而调用 func2 时却发现输出值仍为 foobar, 即 this 指针还是停留在 foo 对象上,这是为什么呢?要想解释这个现象,我们必须了解 bind 方法的原理,让我们看一个 bind 方法的简化版本(不支持绑定参数表)

1
2
3
someuser.func.bind = function(self) { 
return this.call(self)
}

假设上面函数是 someuser.funcbind 方法的实现,函数体内 this 指向的是 someuser.func,因为函数也是对象,所以 this.call(self) 的作用就是以 self 作为 this 指针调用 someuser.func

1
2
3
4
5
6
7
8
9
// 将 func = someuser.func.bind(foo) 展开
func = function() {
return someuser.func.call(foo)
}

// 再将 func2 = func.bind(someuser) 展开
func2 = function() {
return func.call(someuser)
}

从上面展开过程我们可以看出,func2 实际上是以 someuser 作为 functhis 指针调用了 func,而 func 根本没有使用 this 指针,所以第二次 bind 是没有效果的

bind 与 currying

比如我们有一个函数

1
2
3
function add(a, b, c) {
return a + b + c
}

add 函数的作用是把参数 a, b, c 进行拼接(或者说相加),但是有的时候不需要一次把这个函数都调用完成,而是调用一次把前两个参数传完了以后,然后得到了这样的一个函数,再去调用,并且每次传入第三个值

1
2
3
4
// 由于我们不需要改变它的 this,所以随便传入一个 undefined/null ,但是我们提供了额外的参数 100
var func = add.bind(null, 100)

func(1, 2) // 103

然后我们拿到这样一个 bind 函数以后,相当于这个 100 就会固定赋值给第一个参数,也就是这里的 a 参数, 然后在调用的时候传入 12 参数,12 就会分别给 bc,所以,最后的结果为 103

像这样的使用方式,我们就可以称之为函数的柯里化应用(关于柯里化的详细内容,在闭包章节当中会详细介绍,可以参考 函数的柯里化

bind 与 new

1
2
3
4
5
6
7
8
9
function foo() {
this.b = 100
return this.a
}

var func = foo.bind({ a: 1 })

func() // 1
new func() // {b: 100}

我们声明了一个全局变量 b,并且把它的值赋为 100,然后返回全局变量 a,这样我们直接调用的话,那么 this 就会指向 bind 这样的一个参数,所以 return this.a 就会返回 1,如果使用了 new,那么针对于 ruturn,如果不是对象,将会把 this 做为返回值,并且 this 会被初始化为默认的一个空对象,这个对象的原型为 foo.prototype

所以说,我们用 new 去调用的话,这种情况下,即使我们使用了 bind 方法,但是这个 this 依然会指向没有 bind 的时候所指向的(正常状态),这样一个空对象的 b 属性会被赋值为 100,然后整个这个对象会做为返回值返回,所以就会忽略这样一个 return

bind 实现

最后我们再来尝试一下手动的实现 bind 方法,通过上文的了解我们可以发现,其实要实现 bind 就是实现以下功能

  • 一个是绑定函数里面的 this(或者说改变函数里面的 this 指向)
  • 另一个就是把函数拆分为不同的子函数,即柯里化功能
  • 还有就是在 new 调用的时候,忽略掉 bind 的作用(通过 instanceof 判断函数是否通过 new 调用,来决定绑定的 Context

其实在使用 bind 的时候,无非就分为两种情况,一种是直接调用,另一种就是 new 调用,如下所示

1
2
3
4
5
6
7
8
9
10
function foo(c) {
this.b = 100
console.log(c)
return this.a
}

var func = foo.bind({a: 1}, 20)

func() // 20 1
new func() // 20 {b: 100}

先来看一个简单的实现方式

1
2
3
4
5
6
7
8
9
10
Function.prototype.bind = Function.prototype.bind || function (context) {
// 保存 this
const self = this
// 保存第一部分参数(拆分)
const args = Array.prototype.slice.call(arguments, 1)
return function () {
// 合并参数(实现 currying 功能)
return self.apply(context, args.concat(Array.prototype.slice.call(arguments)))
}
}

然后再来看一个较为完善的实现,采用了 ES6 的方式来进行实现,避免了使用 slice 去截取参数等一系列操作

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myBind = function (oThis, ...args) {
const thisFn = this
let fToBind = function (...params) {
// 判断是否通过 new 调用,如果是 new 调用就绑定到 this 上,否则就绑定到传入的 oThis 上
const context = this instanceof fToBind ? this : Object(oThis)
// 老规矩,利用 apply 绑定 this 指向并且传递参数
return thisFn.apply(context, ...args, ...params)
}
fToBind.prototype = Object.create(thisFn.prototype)
return fToBind
}

再来对比看一下完整的实现方式,摘选自 MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if (!Function.prototype.bind) {

// 传入的 oThis 就是 foo.bind({a: 1}, 20) 中传入的对象 {a: 1}
Function.prototype.bind = function (oThis) {

if (typeof this !== "function") {
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable")
}

// 调用数组方法,剔除掉第一个参数,得到剩余参数
var aArgs = Array.prototype.slice.call(arguments, 1),

// 这里的 this 指向函数对象 foo
fToBind = this,

fNOP = function () { },

// 最后返回的就是这个对象,其实就是 fToBind.apply()
// 在不调用 func 的情况下,func 本质就是返回的 apply() 函数
fBound = function () {

// 这里的 this 指的是调用 bind 后 func 的执行环境
// 第一点,见最后
return fToBind.apply(this instanceof fNOP
? this
: oThis,

// 这里的 arguments 和上面的不一样,这里是 func() 的执行环境
// 比如 foo.bind({a: 1}, 20) 中剩余的参数 20 和调用 func(b, c) 时的参数 b 和 c
// 这里就利用 aArgs.concat() 连接了起来,实现了柯里化的功能
// 单单执行 bind 的时候是空数组(类数组对象),因为 arguments 是指向 实参 对象的引用
// 只有在函数调用的时候,传入了实参以后,才会有 arguments 对象
aArgs.concat(Array.prototype.slice.call(arguments)))
}

// 第二点,见最后
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()

return fBound
}
}

比较好理解的地方都直接标记在注释当中了,现在就剩下比较饶的两点,一个一个来看,简单来说就是分为以下两种情况,第一种就是直接调用的情况

  • 正常调用的时候,即 func(),此时 func() 中的 this 是指向 window 的,所以 this instanceof fNOPfalse
  • 此时执行的是 fToBind.apply(oThis, ...),这里的 oThis 也就是传入 bind() 的第一个参数对象 {a: 1}
  • 所以这时 foo() 函数中的 this 就可以指向这个参数对象({a: 1}
  • bind() 后剩余的参数(比如 bind({a: 1}, 20) 中的 20)和 fBound 的一些自己的参数,这个就是通过最后的 aArgs.concat() 拼接完成的

第二种就是 new 调用的情况

  • 当在对 func() 使用 new 的时候,本质上 func() 就是作为构造函数在使用了,所以此时的 this 指向的是一个空对象(见最后)
  • 这时的 this instanceof fNOP 就为 true 了,而此时执行的也就是 fToBind.apply(this, ...)
  • 所以这时的 this 就作为 foo() 函数中调用的 this,也就不再指向 bind() 后的参数对象了,而是作为函数体内正常的 this 使用
  • 这也就忽略掉 bind 的作用了(即 new 了以后,thisbind() 后绑定的参数没有关系了)

关于 this 指向的是一个空对象

当一个函数被作为一个构造函数来使用(使用 new 关键字),它的 this 与即将被创建的新对象绑定(见 构造函数中的 this),当构造器返回的默认值是一个 this 引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回 this(不指定,则默认为一个空对象)

1
2
3
4
5
function foo() { 
console.log(this.__proto__ === foo.prototype) // true
}

new foo()

关于 fNOP.prototype = this.prototype

在之前的代码中有这么一段

1
2
3
4
5
6
fNOP = function () { },

// ...

fNOP.prototype = this.prototype
fBound.prototype = new fNOP()

之所以会拷贝一个 fNOPprototypefBound,由于是拷贝所以修改 fBoundprototype 不会影响到 fNOPprototype,其实这两种方法是等价的

1
2
3
4
5
6
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()

// ==> 两者是相等的

fBound.prototype = Object.creat(this.prototype)

如果直接使用 fBound.prototype = this.prototype 的话,那么在改变 funcprototype 的时候,fooprototype 也会跟着变,所以不推荐

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×