判断this绑定的4个规则
this关键字是JavaScript中最复杂的机制之一,因为this既不指向函数自身也不指向函数的作用域,而是取决于函数在哪里被调用。
既然this取决于调用位置,那我们就先看看什么是调用栈和调用位置:
function baz() {
// 当前调用栈是: baz
// 因此, 当前调用位置是全局作用域
console.log('baz');
bar();
}
function bar() {
// 当前调用栈是baz -> bar
// 因此,当前调用位置在baz中
console.log('bar');
foo();
}
function foo() {
// 当前调用栈是bza -> bar -> foo
// 因此,当前调用位置在bar中
console.log('foo');
}
baz(); // baz的调用位置
在明确了函数的直接调用位置后,我们就要学习四条判断this绑定对象的规则。
1. 默认绑定
默认绑定:直接使用不带任何修饰的函数引用进行调用函数,则this都指向全局对象(在浏览器中是window
,在node中是global
),下面来看一个例子:
function foo() {
console.log(this.a);
}
var a = 2;
foo();
浏览器下: // 2
node下: //undefined
以上foo中this.a就是使用默认绑定,this等于全局对象(window、global),由于var a = 2
是在全局作用域中声明,在浏览器中这个声明相当于window.a = 2
,所以结果得到了2
。但在node中要声明全局对象要写成a=2 或者 global.a = 2
,所以以上代码在node下运行是undefined
。
在严格模式下,this将保持他进入执行上下文时的值,所以下面的this将会默认为undefined:
function foo() {
'use strict';
console.log(this.a)
}
a = 2;
foo() // Cannot read property 'a' of undefined (this 为 undefined)
在严格模式下,如果 this 没有被执行上下文(execution context)定义,那它将保持为 undefined。
不过这里有一个微妙的细节,只要foo()的执行上下文在非严格模式下时,即使在严格模式下调用,也不影响默认绑定规则。请看下面:
function foo() {
console.log(this.a)
}
a = 2;
(function(){
'use strict';
foo(); //2
})();
以上就是this的默认绑定规则,下面我们来看下一条规则
2. 隐式绑定
什么是式隐式绑定,我们先来看一段代码:
function say() {
console.log(this.name);
}
var man = {
name: '内孤',
say: say
}
man.say(); // 内孤
上面就是一个隐式绑定this的例子,通过man来调用say,使用了man的上下文引用了say()。所以在理解this上有句比较简单的话:谁去调用就指向谁
,其实默认绑定this也可以这样理解,foo()其实就是window.foo(),所以this指向window
在这我们要注意,对象属性引用链中只有上一层起作用:
function say() {
console.log(this.name);
}
var man = {
name: '内孤',
say: say
}
var he = {
name: '他是内孤',
say: man
}
he.say.say(); // 内孤
隐式绑定还是比较好理解,不过它会在我们不经意的时候失效,这就是隐式丢失
现象。下面让跟着我一起来看看什么情况下会丢失:
function say() {
console.log('我是' + this.color + '猫');
}
var whiteCat = {
color: '白',
say: say
}
var color = '黑';
var cat = whiteCat.say;
cat(); // 我是黑猫
虽然cat是whiteCat.say的一个引用,但是实际上,它引用的是say函数本身,因此此时的cat其实是一个不带任何修饰的函数调用,因此应用了默认绑定。下面再举两个常用并且容易造成隐式丢失
的代码:
// 第一种
function foo() {
console.log(this.a)
}
function doFoo(fn) {
// fn 其实引用的是foo
fn();
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops, global';
doFoo(obj.foo); // 'oops,global'
// =>
// 第二种
function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = 'oops, global';
setTimeout(obj.foo, 100); // 'oops,global'
/** setTimeout的伪代码
* function setTimeout(fn, delay) {
* fn();
* }
*/
参数传递其实就是一个隐式的赋值,因此我们传入函数时也是会被隐式赋值,所以现象和上面cat的例子是一样的结果。
3. 显式绑定
显式绑定里面有硬绑定
和API调用的"上下文"控制this
�,下面我们来分别看一下:
1)硬绑定
硬绑定就是我们经常看到的call
、apply
、bind(es5中)
三个方法,还是用一段代码来看一个究竟:
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
var bar = function() {
foo.call(obj);
}
bar(); //2
setTimeout(bar, 100) //2
global.a = 22;
// 硬绑定后就固定,不可修改
bar.apply(global) // 2
bar.bind(global) //2
2)API调用的"上下文"
在JavaScript中有几个内置函数,例如:filter
、forEach
等都有一个可选参数,在执行 callback 时的用于的 this 值。下面以forEach为例子:
var girl = {
name: '小郑'
}
function say(item) {
console.log(this.name + ' ' + item)
}
[1,2,3,4].forEach(say, girl)
//小郑 1
//小郑 2
//小郑 3
//小郑 4
这写函数实际上就是用call、apply实现的显式绑定。
4. new绑定
new应该是比较常见的操作,经常用它new各种实例函数(情人节就new girlfriend())。那我们来看一下在new的时候,会发生一些什么事情:
- 创建(或者说构造)一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(name) {
this.name = name;
}
var bar = new foo('内孤');
console.log(bar.name) // 内孤
总结
现在我们已经了解4个判断this绑定的规则,判断一个简单的情况肯定是没有什么问题。但面对复杂的情况,我们必须要了解他们之间的优先级。关于他们之间的优先级会在另外一个篇文章中介绍。
注意: es6中的箭头函数不会使用上面四条绑定规则,箭头函数是没有自己的this,它是通过继承外层函数调用的this绑定
参考资料
《你不知道的JavaScript》上卷