1、执行上下文与调用栈
JavaScript代码执行过程分为两个阶段:代码编译阶段和代码执行阶段。 编译阶段由编译器完成,将代码编译为可执行代码,这个阶段会确定作用域规则;执行阶段由JS引擎完成,主要任务是执行可执行代码,这个阶段会创建执行上下文。
可执行代码主要分为全局代码和函数代码。
-
执行上下文:每个函数调用都会创建一个新的执行上下文。执行上下文是JavaScript代码执行时的环境,有三个重要组成部分
- 变量环境:用于存储该上下文中声明的所有变量和函数。
- 词法环境:用于确定当前上下文中可以访问的外部变量。
- this值:确定当前上下文中this的指向。
- 调用栈:是一种数据结构,用于管理执行上下文。当JS开始执行可执行代码时,最先遇见的是全局代码,因此初始化的时候就会在栈中压入全局代码,它会在最底层,等函数调用时会将新的执行上下文推入栈顶,执行完毕后该上下文被弹出栈,最后弹出全局执行上下文。
function first() {console.log("first function"); second();
}
function second() {console.log("second function");
}
first();
代码执行过程:
- 创建全局执行上下文并压入调用栈;
- first()函数被调用,创建一个新的执行上下文并将其压入调用栈顶部;
- second()函数在first()函数内部被调用,因此创建一个新的执行上下文并压入调用栈顶部;
- second()执行完毕后,它的执行上下文从调用栈中弹出;
- first()继续执行直到完成,它的执行上下文也从调用栈中弹出;
- 最终,只有全局执行上下文还在调用栈中,直到整个脚本执行结束。
2.变量提升和暂时性死区
- 变量提升:在JS中,用var关键字声明的变量会经历“变量提升”的过程。即无论在代码中的哪里使用了var声明变量,这个变量的声明都会被移动到当前作用域的最顶端。但是只有变量的声明部分被提升了,赋值(初始化)没有被提升。因此,在声明之前访问的变量值都为undefined。
- 函数提升:在JS中,函数提升是指无论函数声明在代码的哪个位置,都会被移动到当前作用域的最顶端,相当于可以在实际声明函数之前就调用它。这个特性适用于函数声明,但不直接适用于函数表达式。
sayHello(); // 正常输出Hello! 相当于sayHello函数声明被调到了最顶端
function sayHello() {alert("Hello!")
}
myFunction(); // 报错 var声明的变量只会将变量提升,其初始值为undefined
var myFunction = function sayHello() {alert("Hello!")
}
- 暂时性死区:ES6引入了let和const来声明变量和常量,和var不同的是,在声明这些变量之前访问都会报错(引用错误)。因为从块级作用域的顶端到变量声明之间的区域被称为“暂时性死区”,在这一区域中访问变量就会报错。
console.log('a') // 报错
let a = 10;
console.log('b') // 报错
const b = 10;
3.块级作用域
在ES6之前,作用域只有两种:全局作用域和函数作用域。
- 块级作用域:ES6中新增了块级作用域,只有let和const声明的变量具有块级作用域,即它们只在声明它们的块(如if语句、for循环等)内有效。
- 块级作用域的生命周期:
- 创建:当程序执行到带有let 或const声明的代码块时,该块级作用域被创建;
- 活动:只要代码还在该块内执行,这个作用域就处于活动状态,声明的变量可以被当前作用域内的代码访问;
- 销毁:一旦控制流离开该代码块(即执行了最后一个花括号}后),该块级作用域就“结束”了。
4.词法作用域(outer属性)和作用域链
- 词法作用域:也叫静态作用域,是编程语言中确定变量和函数可访问范围的一种机制。它基于书写位置来确定作用域,而不是函数的调用位置或执行时的上下文。相当于函数的作用域是在定义时确定的,而不是调用时,所以函数内部可以访问其外部作用域中的变量,而外部不能访问函数内部的变量。
- 外层作用域链:函数可以通过outer属性访问其外层作用域。
- 作用域链:它描述了一个函数被执行时,查找变量的过程。每个执行上下文都有自己的变量对象,这个变量对象会链接起来形成一个链条,从当前执行上下文开始向上追溯到全局执行上下文。如果当前上下文中找不到某个变量就会一直往上找,直到找到改变量或者到达全局执行上下文为止。
function bar() {console.log(myName, 'bar'); // lisi
}
function foo() {var myName = 'zhangsan';bar();console.log(myName, 'foo'); // zhangsan
}
var myName = 'lisi';
foo();
由于bar是在全局作用域中定义的,即使是在foo()中被调用,outer指向的还是全局作用域,不会经过foo()。意味着如果在全局没有定义myName,会报错myName is not defined。
总结
- 执行上下文:管理代码的执行环境。
- 词法环境与环境变量:存储该上下文中声明的变量和函数,用于确认当前上下文中可以访问的外部变量。
- 调用栈:管理执行上下文的数据结构,推入全局执行上下文->推入函数执行上下文->函数执行完毕后弹出->推入新的函数执行上下文->函数执行完毕后弹出->...->弹出全局执行上下文。
- 变量提升:var声明的变量会被提升到当前作用域所在的顶部。
- 暂时性死区:let和const声明的变量在声明之前不可以访问是因为块的顶端到声明变量的区域之间存在暂时性死区。
- 块级作用域:let和const声明的变量具有块级作用域。
- 词法作用域:函数的作用域在定义时确定,而非调用或执行时。
- 作用域链:变量查找从当前作用域逐级向上查找,直到找到变量或到达全局执行上下文为止。