当代码在JS中运行时,执行代码的环境非常重要,并将概括为以下几点:
全局代码——第一次执行代码的默认环境。
函数代码——当执行流进入函数体时。
(…) —— 我们当作执行上下文,是当前代码执行的一个环境与范围。

换句话说,当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当一个程序调用函数时,会发生什么?

以下几个步骤:

  • js创建一个新的执行上下文,我们叫做本地执行上下文
  • 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  • 新的执行上下文被推送到执行堆栈中。可以将执行堆栈看作时一种保存程序在其执行中的位置的容器。

函数什么时候结束?当它遇到一个return语句或一个结束括号 } 。

当一个函数结束时,会发生以下情况:

  • 这个本地执行上下文从执行堆栈中弹出。
  • 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有return语句,则返回undefined。
  • 这个本地执行上下文被销毁,销毁很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么称为本地执行上下文中自有的变量。

JS 执行上下文与执行上下文栈_前端

一、变量提升与函数提升

  1. 变量声明提升
  • 通过var 关键字定义(声明)的变量,在定义语句之前就可以访问到
  • 值:undefined
  1. 函数声明提升
  • 通过function声明的函数,在之前就可以被直接调用
  • 值:函数定义(对象)
  1. 引出一个问题:变量提升与函数提升是如何产生的?
/*
面试题 : 输出 undefined
*/
var a = 3
function fn () {
console.log(a)
var a = 4 //变量提升
}
fn() //undefined
'--------------------------------------------'
console.log(b) //undefined 变量提升
fn2() //可调用 函数提升
// fn3() //不能 变量提升
var b = 3
function fn2() { console.log('fn2()') }
var fn3 = function () { console.log('fn3()') }

二、执行上下文

  1. 代码分类(位置)
  • 全局代码
  • 函数(局部)代码
  1. 全局执行上下文
  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理:
  • var 定义的全局变量==>undefined,添加为window属性
  • function 声明的全局函数==> 赋值(fun),添加为window的方法
  • this ==> 赋值(window)
  • 开始执行全局代码
  1. 函数执行上下文
  • 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
  • 对局部数据进行预处理
  • 形参变量–> 赋值(实参) -->添加为执行上下文的属性
  • arguments --> 赋值(实参列表),添加为执行上下文的属性
  • var 定义的局部变量 --> undefined,添加为执行上下文的属性
  • function 声明的函数 --> 赋值(fun),添加为执行上下文的方法
  • this --> 赋值(调用函数的对象)
  • 开始执行函数体代码

三、执行上下文栈

  1. 在全局代码执行前,JS引擎就会创建一个栈来管理所有的执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)–> 所以栈底百分百是window
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当所以的代码执行完后,栈中只剩下window
  6. 上下文栈数==函数调用数 + 1
//1. 进入全局执行上下文
var a = 10
var bar = function (x) {
var b = 5
foo(x + b) //3. 进入foo执行上下文
}
var foo = function (y) {
var c = 5
console.log(a + c + y)
}
bar(10) //2. 进入bar函数执行上下文

JS 执行上下文与执行上下文栈_javascript_02


此处用一个动态图展示:

JS 执行上下文与执行上下文栈_javascript_03


举个栗子:

//栗子
<!--
1. 依次输出什么?
gb: undefined
fb: 1
fb: 2
fb: 3
fe: 3
fe: 2
fe: 1
ge: 1
2. 整个过程中产生了几个执行上下文? 5
-->
<script type="text/javascript">
console.log('gb: '+ i)
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i) //出栈 所以会 3 2 1这样的结果
}
console.log('ge: ' + i)
</script>

四、相关面试题

函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值覆盖。

/*
测试题1: 先执行变量提升, 再执行函数提升
*/
function a() {}
var a
console.log(typeof a) // 'function'

/*
测试题2:
1.首先,所有的全局变量都是window的属性,语句var b = 1; 等价于window.b = 1;
可以使用 console.log(b in window); 来检测全局变量是否声明。
2.第二,所有的变量声明都在范围作用域的顶部:
console.log(b in window)
var b;
此时,尽管声明是在console.log() 之后,log 打印的依然是true,这是因为js引擎
首先会扫描所有的变量声明,然后将这些变量声明移动到顶部,最终的代码效果是这样的:

var b;
console.log(b in window)
这样看起来就很容易理解为什么log结果是true了。
3. 第三,你需要理解题目的意思是,变量声明被提前了,
但变量赋值没有,因为这行代码包括了变量声明和变量赋值。

你可以将语句拆分为如下代码:

var b; // 声明
b = 1; // 初始化赋值
当变量声明和赋值在一起用的时候,js引擎会自动将它分为两部以便将变量声明提前,
不将赋值的步骤提前是因为他有可能影响,代码执行出不可预期的结果。

所以,知道了这些概念之后,重新回头看一下题目的代码,其实就等价于:
var b;
if(!(b in window)){
b = 1;
}
console.log(b);
这样,题目的意思就非常清楚了: 首先声明b, 然后判断b是否在window中存在,如果不存在就赋值为1,
很明显,b永远在window里存在,这个赋值语句永远不会执行,所以结果是undefined

“提升” 这个词显得有些迷惑,你可以理解为:预编译。
*/
console.log(b in window); // true
if (!(b in window)) {
var b = 1
}
console.log(b) // undefined(说明b在window中,但是没有赋值)--变量提升

/*
测试题3:
*/
var c = 1

function c(c) {
console.log(c)
var c = 3 //与此行无关
}
console.log(c);
c(2) // 报错 c is not a function