要想改变被调用函数的上下文,可以使用 call
或 apply
方法,但如果重复使用就会很不方便,因为每次都要把上下文对象作为参数传递,而且还会使代码变得不直观,针对这种情况,我们可以使用 bind
方法来永久地绑定函数的上下文,使其无论被谁调用,上下文都是固定的
基本语法
bind
方法的定义见 ECMAScript 15.3.4.5 Function.prototype.bind() ,使用方式如下
1 | fun.bind(thisArg[, arg1[, arg2[, ...]]]) |
bind()
方法会创建一个新函数,当这个新函数被调用时,它的 this
值是传递给 bind()
的第一个参数, 它的参数是 bind()
的其他参数和其原本的参数,bind
返回的绑定函数也能使用 new
操作符创建对象(这种行为就像把原函数当成构造器),提供的 this
值被忽略,同时调用时的参数被提供给模拟函数,bind
方法与 call
、apply
最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数
还可以写成
fn.bind(obj, arg1)(arg2)
一句话概括就是,该方法创建一个新函数,称为绑定函数,绑定函数会以创建它时传入 bind
方法的第一个参数作为 this
,传入 bind
方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数
1 | var someuser = { |
上面代码直接将 foo.func
赋值为 someuser.func
,调用 foo.func()
时,this
指针为 foo
,所以输出结果是 foobar
,foo.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 | var person = { |
可以看到,fooLoves
将 this
指针绑定到了 person
,并将第一个参数绑定到 loves
,之后在调用 fooLoves
的时候,只需传入第三个参数,这个特性可以用于创建一个函数的捷径,之后我们可以通过这个捷径调用,以便在代码多处调用时省略重复输入相同的参数,也就是下面会说到的 currying
理解 bind
尽管 bind
很优美,还是有一些令人迷惑的地方,例如下面的代码
1 | var someuser = { |
全局函数 func
通过 someuser.func.bind
将 this
指针绑定到了 foo
,调用 func()
输出了 foobar
,我们试图将 func2
赋值为已绑定的 func
重新通过 bind
将 this
指针绑定到 someuser
的结果, 而调用 func2
时却发现输出值仍为 foobar
, 即 this
指针还是停留在 foo
对象上,这是为什么呢?要想解释这个现象,我们必须了解 bind
方法的原理,让我们看一个 bind
方法的简化版本(不支持绑定参数表)
1 | someuser.func.bind = function(self) { |
假设上面函数是 someuser.func
的 bind
方法的实现,函数体内 this
指向的是 someuser.func
,因为函数也是对象,所以 this.call(self)
的作用就是以 self
作为 this
指针调用 someuser.func
1 | // 将 func = someuser.func.bind(foo) 展开 |
从上面展开过程我们可以看出,func2
实际上是以 someuser
作为 func
的 this
指针调用了 func
,而 func
根本没有使用 this
指针,所以第二次 bind
是没有效果的
bind 与 currying
比如我们有一个函数
1 | function add(a, b, c) { |
add
函数的作用是把参数 a, b, c
进行拼接(或者说相加),但是有的时候不需要一次把这个函数都调用完成,而是调用一次把前两个参数传完了以后,然后得到了这样的一个函数,再去调用,并且每次传入第三个值
1 | // 由于我们不需要改变它的 this,所以随便传入一个 undefined/null ,但是我们提供了额外的参数 100 |
然后我们拿到这样一个 bind
函数以后,相当于这个 100
就会固定赋值给第一个参数,也就是这里的 a
参数, 然后在调用的时候传入 1
和 2
参数,1
和 2
就会分别给 b
和 c
,所以,最后的结果为 103
像这样的使用方式,我们就可以称之为函数的柯里化应用(关于柯里化的详细内容,在闭包章节当中会详细介绍,可以参考 函数的柯里化)
bind 与 new
1 | function foo() { |
我们声明了一个全局变量 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 | function foo(c) { |
先来看一个简单的实现方式
1 | Function.prototype.bind = Function.prototype.bind || function (context) { |
然后再来看一个较为完善的实现,采用了 ES6
的方式来进行实现,避免了使用 slice
去截取参数等一系列操作
1 | Function.prototype.myBind = function (oThis, ...args) { |
再来对比看一下完整的实现方式,摘选自 MDN
1 | if (!Function.prototype.bind) { |
比较好理解的地方都直接标记在注释当中了,现在就剩下比较饶的两点,一个一个来看,简单来说就是分为以下两种情况,第一种就是直接调用的情况
- 正常调用的时候,即
func()
,此时func()
中的this
是指向window
的,所以this instanceof fNOP
为false
- 此时执行的是
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
了以后,this
和bind()
后绑定的参数没有关系了)
关于 this 指向的是一个空对象
当一个函数被作为一个构造函数来使用(使用 new
关键字),它的 this
与即将被创建的新对象绑定(见 构造函数中的 this),当构造器返回的默认值是一个 this
引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回 this
(不指定,则默认为一个空对象)
1 | function foo() { |
关于 fNOP.prototype = this.prototype
在之前的代码中有这么一段
1 | fNOP = function () { }, |
之所以会拷贝一个 fNOP
的 prototype
给 fBound
,由于是拷贝所以修改 fBound
的 prototype
不会影响到 fNOP
的 prototype
,其实这两种方法是等价的
1 | fNOP.prototype = this.prototype |
如果直接使用 fBound.prototype = this.prototype
的话,那么在改变 func
的 prototype
的时候,foo
的 prototype
也会跟着变,所以不推荐