关于js中的this指向,必须深刻理解下面这句话:
- this指向是在执行时确定的,不是定义时确定的
- this指向是在执行时确定的,不是定义时确定的
- this指向是在执行时确定的,不是定义时确定的
在介绍this指向之前,首先来解答由上面这句话引申出来的一个问题:“定义”时确定了什么,“执行”时又确定了什么呢? 以下面的代码为例展开说明:
function f1(){
var x = 10;
f2();
}
function f2(){
console.log(x);
}
f1();
定义时 – 作用域
作用域决定代码块内“资源”的可见性。作用域在定义时就确定,并且不会改变。
在给出上面代码执行的结果之前,我们先来分析一下代码定义到执行的过程。首先,在定义时确定代码的作用域:
- 全局作用域:在全局作用域中只有两个变量f1和f2,分别指向一个函数;
- f1函数作用域:在f1函数作用域中,只有一个变量x;
- f2函数作用域:在f2函数作用域中,没有定义变量
至此,代码的作用域确定。作用域的确定,代码块内“资源”的可见性随即确定: - 全局作用域内可访问的变量:f1和f2;
- f1函数作用域内可访问的变量:x和全局作用域内的所有可见的变量;
- f2函数作用域内可访问的变量:全局作用域内的所有可见的变量
也许这时候你更加确定代码的执行结果,但是这里暂不揭晓,我们继续分析。
执行时 – 执行上下文
执行上下文是在代码执行时确定,当函数被调用时,会在执行栈中创建一个新的执行上下文,并入栈顶。 (对于执行栈和执行上下文的理解可以参考此篇文章,也可以参考我翻译以后的译文)
上述代码段中,f1在全局作用域内被调用。下面我们分析一下函数调用和执行的具体过程:
- f1函数在全局执行上下文中被调用;
- 在f1函数执行前,创建新的执行上下文并入栈顶;
- 执行f1函数。调用函数f2,再次创建新的执行上下文并入栈顶;
- 执行f2函数。由于执行上下文中作用域链是由f2–>window,首先在f2函数作用域中没有找到x,向上查找至window全局作用域,window中也没有定义x,因此执行结果是:Uncaught ReferenceError: x is not defined
相信通过以上的分析,你对作用域和执行上下文已经有所了解。总结以上两个阶段我们可以简单的描述为:
- 定义阶段:
- 确定作用域:即确定当前代码块内“资源”的可见性。
- 执行阶段:
- 确定执行上下文:作用域链、VO/AO、this的值
但是,这些和this的指向有什么关系呢?其实回顾刚刚的定义过程和执行过程,你会发现,在定义阶段根本就没有涉及到this,只有在执行阶段才有this的出现。因此,你也就明白了本文最开始的一句话:this指向是在执行时确定的,而不是在定义时确定的。
this指向
关于this指向,相信大家看到过很多描述,比如:
- 如果函数作为对象的方法时,方法中的 this 指向该对象;
- 构造函数中的this,指向new出来的新对象
- 普通函数在全局中调用,this指向window
- …
面对这么多情况,我们该怎么记忆?其实,this指向没有这么多规则,归根结底只有一句话:this总是指向当前函数所在的执行上下文 (注:箭头函数后面会单独介绍)。
结合这句话,我们仅从定义和执行两个角度再去分析常见的场景:
场景一:对象中的函数
var x = 1;
var obj = {
x: 10,
foo: function (){
var x = 100;
console.log(this.x);
}
}
obj.foo(); // 打印10
- 定义阶段:
- 全局作用域可访问的变量:x 和 obj ,其中obj的属性:x 和 foo
- 执行阶段:
- 调用obj.foo(),创建新的执行上下文
- 确定作用域链和变量对象,其中foo的作用域链为:foo-->obj-->window
- 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
- 执行obj.foo(),控制台中打印10
场景二:全局作用域中的函数
function fun1 (){
console.log(this);
}
fun1(); // 打印window
- 定义阶段:
- 全局作用域可访问的变量:fun1
- 执行阶段:
- 调用fun1(),相当于调用window.foo(),创建新的执行上下文
- 确定作用域链和变量对象,其中fun1的作用域链为:fun1 --> window
- 确定this的值,fun1所在的执行上下文为window,所以fun1中的this指向window
- 执行func1(),控制台打印window
场景三:构造函数
function Student (name,age){
this.name = name;
this.age = age;
}
Student.prototype.show = function (){
console.log(this.name);
}
var andy = new Student('andy',10);
andy.show(); // 打印andy
- 定义阶段:
- 全局作用域可访问的变量:Student、andy
- 执行阶段:
- 调用new Student('andy',10),其中new完成的操作如下所示:
- 创建空对象,即var obj = {}
- 当前空对象的原型指向构造函数的prototype对象,即obj.__proto__ = Object.create(Student.prototype)
- this指向当前对象obj
- 给当前空对象赋值,即obj.name = 'andy',obj.age = 10
- 返回当前空对象obj
- 执行andy.show(),通过原型链的方式调用Student中的show()方法,show方法当前所在的执行上下文为andy,因此控制台打印andy
场景四:回调函数
var x = 1;
var obj1 = {
x:10,
foo: function (){
setTimeout(function (){
console.log(this.x);
},1000)
}
}
obj1.foo(); // 打印1
- 定义阶段:
- 全局作用域可访问的变量:x、obj1
- 执行阶段:
- 调用obj1.foo(),创建新的执行上下文
- 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
- setTimeout函数是window的内置函数,因此setTimeout中回调函数的执行上下文为window,则回调函数中的this指向window
- 执行obj1.foo(),控制台打印1
场景五:call,bind,apply
var x = 1;
var obj2 = {
x:10,
foo: function (){
console.log(this.x);
}
}
var obj3 = {
x: 100
}
obj2.foo.call(obj3); // 打印100
call函数是改变“调用者”的执行上下文(即this指向)并立即执行“调用者”
- 定义阶段:
- 全局作用域可访问的变量:x、obj2、obj3,其中obj2的属性包括x和foo,obj3的属性包括x
- 执行阶段:
- 调用obj2.foo.call(obj3), ,将“调用者”foo的执行上下文改为call函数的第一个参数obj3
- foo的执行上下文更改为新的执行上下文obj3
- 确定作用域链和变量对象,其中foo的作用域链由为:foo --> obj2 --> window 更改为 foo --> obj3 --> window
- 确定this的值,foo所在的执行上下文为obj3,所以foo中的this指向obj3
- 执行obj2.foo.call(obj3),控制台打印100
apply和call的作用相同,只是第二个参数的数据类型是数组。bind也是改变调用者的执行上下文,不同于call和apply的地方是,函数不会立即执行。
总结以上情况,我们发现每一个this值的确定过程都涉及到了执行上下文,因此this总是指向当前函数所在的执行上下文。但是,箭头函数中的this指向也是这么确定的呢
吗?
箭头函数中的this
本文一开始我们就提到一句话,this指向是在执行时确定的,不是在定义时确定的。这句话在箭头函数中还适用吗?我们来看下面的例子:
var x = 1;
var obj = {
x: 10,
foo: () => {
console.log(this.x);
}
}
obj.foo();
我们还是按照定义和执行两个角度进行分析:
- 定义阶段:
- 全局作用域可访问的变量:x、obj
- 执行阶段:
- 调用obj.foo(),创建新的执行上下文
- 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
- 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
- 执行obj.foo(),控制台打印10
实际结果是我们分析的10吗?执行代码发现不是10,而是1。这是为什么呢?因为箭头函数中没有this,箭头函数体内的this === 最靠近箭头函数的 绑定this的 普通函数的 this值。再看下面的例子:
var x = 1;
var obj = {
x: 10,
foo: function (){
var x = 100;
setTimeout(() => {
console.log(this.x);
},1000)
}
}
obj.foo();
- 定义阶段:
- 全局作用域可访问的变量:x、obj
- 执行阶段:
- 调用obj.foo(),创建新的执行上下文
- 确定作用域链和变量对象,其中foo的作用域链为:foo --> obj --> window
- 确定this的值,foo所在的执行上下文为obj,所以foo中的this指向obj
- foo内部调用setTimeout函数,其中回调函数为箭头函数
- 执行obj.foo()
- 执行回调函数,其中this位于回调函数内部,所以沿着作用域链向上查找,发现foo中有this,并且this绑定在obj中,因此回调函数中的this也指向obj
- 打印结果为10
总结
- this指向是在执行时确定的,不是在定义时确定的;
- 箭头函数中的this指向 ===> 最靠近它的 绑定this指向的 函数中的 this的值