一、前言
说到this,我提出了下面几个问题:
- this存放在哪里?
- this是如何出现,又是如何消失的?
- this有什么作用?
从定义来看
this是在执行上下文创建时确定的一个在执行过程中不可更改的变量。
this的指向和上下文密不可分的,下面我们通过执行上下文来全面理解this
二、执行上下文
当 JS 引擎解析到可执行代码片段(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作,这个 “准备工作”,就叫做 "执行上下文(execution context 简称 EC)" 或者也可以叫做执行环境。
1、执行上下文类型
javascript
中有三种执行上下文类型,分别是:
-
全局执行上下文 —— 这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个 javascript 脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。
-
函数执行上下文 —— 每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)
-
Eval 函数执行上下文 —— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用 eval,所以在这里不做分析。
2、执行上下文生命周期
我们先看看执行上下文的生命周期
执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。通过执行上下文的生命周期我们可以得知,this的指向是在执行上下文创建是被确定的
3、实例分析
下面通过一段代码来具体分析一下执行上下文:
add() // call b
console.log(a) // undefined
var a = 'Hello world'
function add() {
console.log('call b')
}
代码执行前,JavaScript 引擎会创建全局执行上下文,在这一阶(创建阶段),会进行变量和函数的初始化声明,变量统一定义为 undefined(var情况下) 需要等到赋值时才会有确值,而函数则会直接定义,确定 this 值为全局对象(以浏览器为例,就是 window ),你可以参考下图:
从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。
在上下文创建阶段,引擎检查代码找出变量和函数声明,变量最初会设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。
执行上下文准备好之后,便开始执行全局代码(执行阶段),当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:
- 首先,从全局执行上下文中,取出 add 函数代码。
- 其次,对 add 函数的这段代码进行编译(函数被调用),此时会创建函数的执行上下文(通过执行上下文生命周期可知道函数中的this指向此时被确定)。
- 最后,执行代码,输出结果。
就这样,当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。
4、执行上下文总结
首先,我们需要得出一个非常重要的,并且一定要牢记于心的结论
- 函数中this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。
因此,这个特性也导致了this的多变性:🙂即当函数在不同的调用方式下都可能会导致this的值不同。
var a = 10;
var obj = {
a: 20
}
function fn() {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
三、重看 this
相信根据上文内容大家应该已经明白什么是JavaScript执行上下文
我们再来看this,其实它也存放在执行上下文中。
执行上下文包括了:变量环境、词法环境、this。如下图所示:
从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this
执行上下文主要分为三种:
1、 全局执行上下文
2、 函数执行上下文
3、 eval 执行上下文
所以对应的 this 也只有这三种:
1、 全局执行上下文中的 this
2、函数中的 this
3、 eval 中的 this(先不讲解此情况)
四、全局执行上下文中的 this
console.log(this) // window
console.log(this === window) // true
var a = 1;
function fun() {
var a = 2;
return this.a;
}
fun(); //1
我们在全局对象中调用 fun,实际上就相当于 window.fun() 的一个调用,那么就是指向 Window。
注意这里是非严格模式。严格模式下的全局对象是 undefined,函数fun中this则指向undefined,
此时就会报错误 Uncaught TypeError: Cannot read property 'name' of undefined
另外注意:开启了严格模式,只是说使得函数内的this指向undefined,它并不会改变全局中this的指向。
五、函数执行上下文中的 this
说到函数中this绑定这里就说说this绑定规则
this的绑定规则有4种:
- 默认绑定(严格/非严格模式)
- 隐式绑定
- 显式绑定
- new绑定
1、默认绑定
当函数独立调用的时候,在严格模式下它的this指向undefined,在非严格模式下,当this指向undefined的时候,自动指向全局对象(浏览器中就是window)
function foo() {
console.log( this.a ); // 2
}
var a = 2;
// 调用
foo();
// --------------------------------------
function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";
console.log( this.a );
}
var a = 2;
// 调用
foo(); // TypeError: Cannot read property 'a' of undefined
// --------------------------------------
function foo() { // 运行
console.log( this.a );
}
var a = 2;
(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";
foo(); // 2
})();
2、隐式绑定
什么是隐式绑定呢,如果函数调用时,前面存在调用它的对象,那么this就会隐式绑定到这个对象上。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
greet: function () {
console.log(this.a);
},
foo: foo
};
obj.foo(); // 2
隐式丢失
被隐式绑定的函数特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象
var a = 1;
var obj = {
a: 2,
b: function() {
return this.a;
}
}
var t = obj.b;
console.log(t());//1
这时候它又变成 window 指向了,此刻 var t = obj.b 只是一个定义,真正执行是在 t()。
3、显示绑定
显示绑定很好理解,我们希望this绑定在哪个对象上我们就用方法绑定它,具体有三种方法可以达到这个效果,需要注意的是一旦我们 显示绑定 之后我们便无法再绑定了。
- call()
- apply()
- bind()
let bar = {
myName : "dell",
}
function foo(){
this.myName = "dellyoung"
}
foo.call(bar)
console.log(bar) // {myName:"dellyoung"}
console.log(myName) // 报错myName未定义
4、new 绑定
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
咱们现在再来看一下通过new调用构造函数到底做了什么:
function New(source, ...arg) {
// 创建一个空的简单JavaScript对象(即{})
let newObj = {};
// 链接该对象(即设置该对象的构造函数)到另一个对象
Object.setPrototypeOf(newObj, source.prototype);
// 将步骤1新创建的对象作为this的上下文 ;
const resp = source.apply(newObj, arg);
// 判断该函数返回值是否是对象
if (resp && Object.prototype.toString.call(resp) === "[object Object]") {
// 如果该函数没有返回对象,则返回this。
return resp
}
// 如果该函数返回对象,那用返回的这个对象作为返回值。
return newObj
}
五、疑难填坑
1、this存放在哪里
this存放在每个执行上下文中
2、this是如何出现,又是如何消失的?
this随着执行上下文出现,当执行上下文被回收后,也随之消失
3、this有什么作用?
全局执行上下文中:this指向了window对象,方便我们来调用全局window对象。
函数执行上下文中:this指向了调用该函数的对象,减少的参数的传递,原来如果需要在函数内部操作被调用对象,
当然还需要将对象作为参数传递进去,而有了this,就不需要了,直接拿this就能操作被调用对象的属性。
4、总结this绑定
- 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
- 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
- 谁调用函数,函数的this指向谁。对象调用函数自然不必说,全局环境调用其实也可以理解为window来调用,所以当然指向了window
六、箭头函数中this指向
箭头函数不像普通函数有多个规则,它不考虑绑定的四个规则,箭头函数中没有 this,它的this是根据它外层作用域来决定的,看下面这个例子
var obj = {
count: 0,
cool: function () {
console.log(this); // obj 对象
setTimeout(() => {
console.log(this); // obj对象
this.count++;
console.log("awesome?");
}, 100);
}
}
obj.cool();
七、小试牛刀
1、默认绑定
先介绍一种最简单的绑定方式吧:默认绑定。
也就是我们常说的:在非严格模式下this
指向的是全局对象window
,而在严格模式下会绑定到undefined
。
- 第一题
var foo = 1;
function bar () {
console.log(foo);
console.log(window.foo);
var foo = 10;
console.log(foo);
}
bar();
解析:
我们知道在使用var
创建变量的时候(不在函数里),会把创建的变量绑定到window
上,所以此时foo
是window
下的属性。
而函数bar
也是window
下的属性。另外在调用bar
时,创建函数执行上下文,在这个阶段引擎检查代码找出变量和函数声明,变量最初会设置为 undefined
(var 情况下)
答案:
undefined
1
10
- 第二题
var a = 1
function foo () {
var a = 2
function inner () {
console.log(this.a)
}
inner()
}
foo()
解析:inner中,this指向的还是window。
答案:
1
- 第三题
let a = 10;
const b = 20;
function foo() {
console.log(this.a);
console.log(this.b);
}
foo();
console.log(window.a);
解析:如果把var改成了let 或者 const,变量是不会被绑定到window上的,所以此时会打印出三个undefined。
答案:
undefined
undefined
undefined
2、隐式绑定丢失问题
- 第一题
function foo () {
console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
obj.foo();
foo2();
解析:
obj.foo()中this的指向是为obj的(可以看第二部分隐式绑定),所以obj.foo()执行的时候,打印出来的是obj对象中的a,也就是1。
foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window。
答案:
1
2
- 第二题
function foo() {
console.log(this.a);
}
function doFoo(fn) {
console.log(this);
fn();
}
var obj = { a: 1, foo };
var a = 2;
var obj2 = { a: 3, doFoo };
obj2.doFoo(obj.foo);
解析:
现在调用obj2.doFoo()
函数,里面的this指向的应该是obj2,因为是obj2调用的它。
但是obj.foo()
打印出来的a依然是2,也就是window下的。
答案:
{ a:3, doFoo: f }
2
3、显示绑定问题
function foo1 (b) {
console.log(`${this.a} + ${b}`)
return this.a + b
}
var a = 1
var obj = {
a: 2
}
var foo2 = function () {
return foo1.call(obj, ...arguments)
}
var num = foo2(3)
console.log(num)
答案:
'2 + 3'
5
4、综合题
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3;
(function() {
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function() {
console.log(this.num);
},
greet: function () {
console.log(this);
setTimeout(() => {
console.log(this);
this.count++;
console.log("awesome?");
}, 100);
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
myObject.greet();
参考
1、【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理) this为何物
3、说说执行上下文吧