前言
在学习this指向前,需要了解两个概念:词法作用域和动态作用域。词法作用域顾名思义,就是定义在代码书写阶段的作用域,也就是说我们编写代码时,声明的变量所处的作用域就已经确定下来了。词法作用域关注函数的声明位置,而动态作用域则是在程序运行时动态确定的作用域。它并不在意声明,而是看在哪里被调用,也就是说我们需要分析调用栈来确定作用域。js的中的作用域是词法作用域,但是this的机制跟动态作用域很类似。
绑定规则
1. 默认规则
默认绑定规则是最基础的规则,也就是将this指向全局对象。(如果代码运行在严格模式中,则不能绑定到全局对象上而是会绑定到undefined)
var name = 'Jack'
function getName() {
console.log(this.name) // 'Jack'
}
getName()
2. 隐式绑定
隐式绑定规则需要关注函数被调用时是否存在上下文对象。当函数调用时存在上下文对象,函数内的this会绑定到这个上下文对象。观察下面代码,思考代码运行后三处的打印结果:
var name = 'Jack'
function getName() {
console.log(this.name)
}
var obj = {
name: 'Tom',
getName: getName
}
// 1.
obj.getName()
var obj2 = {
name: 'Mike',
obj: obj,
getName: obj.getName
}
// 2.
obj2.getName()
// 3.
obj2.obj.getName()
//4.
var fn = obj.getName
fn()
先看第1处调用,obj对象的属性getName拥有对在全局作用域中声明的函数getName的引用。因为函数调用时存在obj这个上下文对象,所以输出结果为‘Tom’。
第2处调用,obj2对象的属性getName拥有对obj.getName属性值的引用,也就是在全局作用域中声明的函数getName的引用。因为函数调用时存在obj2这个上下文对象,所以输出结果为‘Mike’。
第3处是一个对象属性引用链,只有最后一个调用位置起决定作用,所以我们直接看obj.getName(),因为函数调用时存在obj这个上下文对象,所以输出结果为‘Tom’。
第4处,是一个容易出错的场景,注意到我们分析几点都在强调“obj对象的属性getName拥有对在全局作用域中声明的函数getName的引用”,所以这里将obj.getName赋值给全局变量fn,其实是fn拥有了对全局声明的函数getName的引用。执行fn()时不存在调用的上下文对象,this应用的是默认规则,指向全局对象或undefined。
3. 显示绑定
不同于隐式绑定需要分析调用的上下文对象,显示绑定是指通过一定方式明确指定我们想要this绑定的上下文对象。最常见的call,apply,bind强制this绑定到指定的上下文中,也有一些api支持传入上下文,比如forEach的第二个接收参数
array.forEach(function(currentValue, index, arr), thisValue)
这里就call举个例子:
var num = 2
function getNum () {
console.log(this.num)
}
var obj = {
num: 1
}
getNum.call(obj) // 1
4. new绑定
首先来看下new的过程都做了哪些事:
- 创建一个全新的对象
- 新对象被执行Prototype连接
- 函数中的this被绑定到新对象上
- 如果函数没有返回对象,那么new表达式中的函数会自动返回这个对象 思考下面代码的输出:
function person (name) {
this.name = name
}
var tom = new person('Tom')
console.log(tom.name) // Tom
这里使用new来调用person函数,new会创建一个对象并将person中的this绑定到这个新对象上。
总结
有时我们遇到的this问题比较复杂,下面按照规则的优先级从高到低总结了this的判断方法:
- 判断是否为new绑定
- 是否存在显示绑定(call,apply等)
- 是否存在引用绑定,即上下文对象调用
- 都不是则采用默认绑定
注意,我们上面讲述的场景不适用于箭头函数(()=>{}),箭头函数会继承外层最接近它的第一个非箭头函数的函数的this绑定。