详解执行上下文、执行栈、this 指向
本文目录:
├── 一、什么是执行上下文
├── 二、执行上下文的类型(3种)
│ ├── 1、全局执行上下文
│ ├── 2、函数执行上下文
│ ├── 3、Eval 全局执行上下文(不讨论)
├── 三、执行上下文的生命周期(3个阶段)
│ ├── 1、创建阶段
│ │ ├── (1)、生成变量对象
│ │ ├── (2)、建立作用域链
│ │ ├── (3)、确定 this 指向
│ ├── 2、执行阶段
│ ├── 3、回收阶段
├── 四、执行上下文栈
一、什么是执行上下文
可能小伙伴们会觉得这个专业名词肯定夹杂着很多高深莫测的知识,但其实可以理解成通俗易懂的两句话。
- JavaScript 中运行的任何代码都是在执行上下文中运行的。
- 当前 JavaScript 代码被执行和解析时所在环境(抽象概念)。
是不是觉得很泛,但事实就是如此。
二、执行上下文的类型(3种)
1、全局执行上下文
不在任何函数中的代码都是全局执行上下文(巨大无比),它做两件事:
- 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。
- 将
this
指针指向全局对象。一个程序中有且只有一个全局对象。
2、函数执行上下文
每个函数都拥有自己的执行上下文,只有在函数被调用的时候该执行上下文才会被创建。一个程序中可以有任意个函数执行上下文。
3、Eval 全局执行上下文(不讨论)
三、执行上下文的生命周期(3个阶段)
1、创建阶段
(1)、生成变量对象:
- 建立
arguments
对象 - 检测当前上下文变量声明和函数声明。变量提升,即将当前上下文环境中的变量先声明,并赋值为
undefind
。待代码执行时才会有正确的赋值。当存在同名情况,函数声明的优先级会高于变量声明。
(2)、建立作用域链
在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量
(3)、确定 this 指向
this 的值是在执行的时候才能确认,定义的时候不能确认! (是不是一脸懵逼?看下下面的事例你就能明白)
// 事例一
function foo() {
console.log(this);
}
foo(); // 这里输出的 this -> window
// 事例二
function foo() {
console.log(this);
}
var obj = {fn: fn}
fn.foo(); // 这里输出的 this -> obj
// 事例三
function foo() {
console.log(this);
}
var obj = {fn: fn}
var aa = obj.fn;
aa(); // 这里输出的 this -> window
// 事例四
var obj4 = {};
var fn = function () {
console.log(this);
}
fn.call(obj4); // 这里输出的 this -> obj4
解释下上面的例子:
- 事例一:
foo()
为构造函数。 - 事例二:
foo()
作为一个对象的属性,作为一个对象的属性被调用,this
指向obj
。 - 事例三:
foo()
被赋值到另一个变量中,并没有作为一个对象的属性被调用,this
指向window
。 - 事例四:函数被
call
、apply
调用时,this
指向传入的第一个属性对象。
看到了吧,相同的代码在不同的情况下 this
取值是不同的,所以现在是不是理解 this 的值是在执行的时候才能确认,定义的时候不能确认! 这句话的含义。
2、执行阶段
变量赋值,函数引用,代码执行。
3、回收阶段
从执行上下文栈(下文会做解释)出栈,等待回收。
四、执行上下文栈
在执行 js 代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?
这里就是 执行上下文栈-出栈入栈 来管理。下面列出一些关键点和代码事例来讲解,细看之后你就能明白,因为并不难。
- 执行上下文栈可以类似于一个存储函数调用的栈结构,遵循先进后出的原则。
- JavaScript 执行在单线程上,所有代码都是排队执行。
- 处于活动状态的执行上下文环境只有一个,且位于栈顶。
- 函数执行上下文执行完毕就会出栈,等待垃圾回收。
好了我们来看代码吧
1 var a = 10; // 1、进入全局上下文环境
2 var fn = function(d) {
3 var b = 2;
4 console.log(d + b);
5 }
6 var bar = function(x) {
7 var c = 3;
8 fn(c + x); // 3、进入 fn 函数上下文执行环境
9 }
10 bar(4); // 2、进入 bar 函数上下文执行环境
(1)、在代码执行前,首先创建全局上下文环境,声明变量,将全局执行上下文
入栈,并将全局执行上下文
设置为活动状态。
全局:
变量 | 值 |
---|---|
a | undefined |
fn | undefined |
bar | undefined |
this | window |
(2)、在执行到第10行之前,上下文栈还是只有全局执行上下文
,但上下文变量都在执行过程中被赋值。
全局:
变量 | 值 |
---|---|
a | 10 |
fn | function |
bar | function |
this | window |
(3)、在执行到第10行时,调用 bar
函数,跳转到函数体内部,在执行函数体语句之前会创建一个新的 bar 函数执行上下文
,并将此压入栈顶,设置为活动状态,全局执行上下文
就不再是活动状态了。
bar:
变量 | 值 |
---|---|
c | undefined |
x | 4 |
arguments | [4] |
this | window |
(4)、执行到第8行时,调用 fn
函数,跳转到函数体内部,在执行函数体语句之前会创建一个新的 fn 函数执行上下文
,并将此压入栈顶,设置为活动状态,bar 函数执行上下文
就不再是活动状态了。
fn:
变量 | 值 |
---|---|
b | undefined |
d | 7 |
arguments | [7] |
this | window |
(5)、待 fn
函数执行完,fn 函数上下文环境
出栈,并及时销毁,bar 函数上下文环境
重新回到活动状态。
(6)、待 bar
函数执行完,bar 函数上下文环境
出栈,并及时销毁,全局上下文环境
重新回到活动状态。
(7)、待浏览器关闭,全局上下文环境
出栈,并销毁。
一整个理想的流程就完成了。是不是不会太难?如果遇到闭包的情况呢,要另外说明哈。