要想改变被调用函数的上下文,可以使用 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 也会跟着变,所以不推荐