每个函数的this是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。
调用位置
怎么找调用位置?
通过分析调用栈(就是为了到达当前执行位置所调用的所有函数),来找到当前正在执行的函数的前一个调用,这个调用就是调用位置。
1 | function baz(){ |
绑定规则
必须先找到调用位置,然后判断需要应用下面四条规则中的哪一条。
1.默认绑定
最常用的函数调用类型:独立函数调用。这条规则是无法应用其他规则时的默认规则。
1 | function foo(){ |
如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this
会绑定到undefined
。
一个重要的细节:在严格模式下调用(非运行)foo()
则不影响默认绑定。
2.隐式绑定
调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,隐式绑定规则会把函数调用中的this
绑定到这个上下文对象。
1 | function foo(){ |
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
1 | function foo(){ |
2.1隐式丢失
被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认规则。
1 | function foo() { |
bar引用的是foo函数本身,因此bar()
是一个不带任何修饰的函数调用。
传入回调函数时,几乎都会隐式丢失(如下两种)。
1 | function foo() { |
3.显示绑定
call(...)
和apply(...)
称为显示绑定。
如果传入一个原始值当作this
的绑定对象,这个原始值会被转换成它的对象形式(new String(..)
、new Boolean(..)
、new Number(..)
)。则通常称为装箱。
3.1 硬绑定
硬绑定是一种显示的强制绑定,是一个函数。
硬绑定可以解决隐式丢失问题。
硬绑定的典型应用场景是创建一个包裹函数,负责接收参数并返回值。
1 | function foo(something){ |
另一种使用方法是,创建一个可以重复使用的辅助函数。
1 | function foo(something){ |
ES5内置bind方法Function.prototype.bind
。
3.2 API调用的“上下文”
一些函数提供一个可选的参数,通常称为“上下文”(context),其作用和bind(..)
一样,确保你的回调函数使用指定的this
。
4.new绑定
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面操作:
- 创建(或者说构造)一个全新的对象。
- 这个对象会被执行
[[prototype]]
连接。 - 这个对象会绑定到函数调用的
this
。 - 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个对象。
优先级
如果某个调用位置可以应用多条规则该怎么办?
先比较隐式绑定和显示绑定,谁的优先级高–>
1 | function foo(){ |
–>显示绑定高于隐式绑定。
再比较new绑定和隐式绑定的优先级–>
1 | function foo(something){ |
–>new绑定比隐式绑定优先级高。
最后比较new绑定和显示绑定–>
1 | function foo(something){ |
–>书上解释很多,结论一句话:硬绑定函数被new
调用,会使用新创建的this
(即新创建的对象)替换硬绑定的this
。
在new
中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用new
进行初始化时就可以只传入其余的参数。bind(..)
的功能之一是可以把除了第一个参数之外的其他参数都传给下层函数。
1 | function foo(p1,p2) { |
判断this
- 函数是否在
new
中调用(new
绑定)?如果是,this
绑定的是新创建的对象。 - 函数是否通过
call
、apply
(显示绑定)或者硬绑定调用?如果是,this
绑定的是指定的对象。 - 函数是否在某个上下文对象中调用(隐式绑定)?如果是,
this
绑定的是那个上下文对象。 - 如果都不是,使用默认绑定。如果在严格模式下,就绑定到
undefined
,否则绑定到全局对象。
绑定例外
1.被忽略的this
把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则,绑定全局对象。
那么什么情况下会传入null
?
解构时
1 | function foo(a,b){ |
使用null
来忽略this绑定可能产生一些副作用,可能会用默认规则把this绑定到全局对象。
更安全的this
创建一个空的非委托的对象Object.create(null)
。Object.create(null)
和{}
很像,但是并不会创建Object.prototype
这个委托,所以它比{}
“更空”。
2.间接引用
(有意或无意)创建一个函数的“间接引用”时,调用这个函数会应用默认绑定规则。
1 | function foo(){ |
赋值表达式p.foo=o.foo
的返回值是目标函数的引用,因此调用位置是foo()
而不是p.foo()
和o.foo()
。
注意: 对于默认绑定,函数体处于严格模式下,this
会绑定到undefined
;调用位置是否处于严格模式,则无影响。
3.软绑定
原理:给默认绑定指定一个全局对象和undefined
以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this
的能力。
也就是四种规则中,默认绑定不再是全局对象或者undefined,而是软绑定那个值,同时其他3种规则不变。
以下是软绑定的实现
1 | if(!Function.prototype.softBind){ |
以下是软绑定功能例子
1 | function foo(){ |
this词法
箭头函数不使用this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
。
1 | function foo(){ |
foo内部创建的箭头函数会捕获调用时foo()的this,且箭头函数的绑定无法被修改。
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
1 | function foo(){ |
小结
如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面四条规则来判断this的绑定对象。
- 由
new
调用?绑定到新创建的对象。 - 由
call
或者apply
(或者bind
)调用?绑定到指定的对象。 - 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到
undefined
,否则绑定到全局对象。
安全的忽略this绑定,使用φ=Object.create(null)
。
ES6的箭头函数不会使用四条规则,而是根据当前的词法作用域来决定this
。具体说,箭头函数会继承外层函数调用的this
绑定,这其实和self=this
机制一样。