概要
执行上下文:JavaScript 执行一段代码时的运行环境。- 分类:执行上下文分为全局执行上下文、函数执行上下文和 eval 函数执行上下文。
- 组成:执行上下文由变量环境和词法环境组成。
- 变量环境和词法环境结构相同,均包含环境记录、outer 指针、this 对象。其中,环境记录存储变量和函数声明。
执行栈:是一种后进先出的数据结构,用来存储代码运行时的所有执行上下文。作用域链:当一段代码使用了一个变量,会首先在自身执行上下文中查找该变量,如果没有找到,继续到上一级执行上下文中查找,直到全局执行上下文,这个查找的链条就称为作用域链。作用域:指一个变量和函数的可访问范围。- JavaScript 采用的静态作用域,又称词法作用域,函数定义的位置决定了函数的作用域,和函数是怎么调用的没有关系。
1. 执行上下文和作用域
Understanding Execution Context and Execution Stack in Javascript
[译] 理解 JavaScript 中的执行上下文和执行栈
1.1 执行上下文的分类
执行上下文是 JavaScript 执行一段代码时的运行环境。有三种执行上下文:
- 全局执行上下文: 任何不在函数内部的代码都在全局上下文中,一个程序中只会有一个全局执行上下文。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。。
- 函数执行上下文: 每当一个函数被调用时, 都会为该函数创建一个新的上下文。
- eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。
1.2 执行栈
执行栈,即调用栈,是一种 LIFO(后进先出)的数据结构,用来存储代码运行时的所有执行上下文。
- 当 JS 引擎执行 js 脚本时,会创建一个全局执行上下文,压入当前执行栈;
- 每当遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部;
- 当该函数执行结束时,执行上下文从栈中弹出,把控制权返回给之前的执行上下文;
- 一旦所有代码执行完毕,JS 引擎从当前栈中移除全局执行上下文。
let a = "Hello World!";
function first() {
console.log("Inside first function");
second();
console.log("Again inside first function");
}
function second() {
console.log("Inside second function");
}
first();
console.log("Inside Global Execution Context");
1.3 执行上下文的组成
创建执行上下文:
- 创建词法环境(LexicalEnvironment)
- 创建变量环境(VariableEnvironment)
ExecutionContext = {
LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
VariableEnvironment = <ref. to VariableEnvironment in memory>,
}
词法环境是一种保存变量(或函数)名和值的映射关系的键值对。
词法环境包含 3 个组成部分:
- 环境记录:环境记录是词法环境中存储变量和函数声明的地方。
- 声明性环境记录(Declarative environment record),函数代码的词法环境包含声明性环境记录。存储变量和函数声明,也包含一个 arguments 对象。
- 对象环境记录(Object environment record),全局代码的词法环境包含客观环境记录。除了变量和函数声明之外,对象环境记录还存储全局绑定对象(浏览器中的 window 对象)。
- outer:指向外部的词法环境
- this
变量环境也是一种词法环境,也包含词法环境一样的 3 个组成部分。
在 ES6 中,词法环境用于存储函数声明和 let/const 声明的变量,而变量环境仅用于存储 var 声明的变量。
1.4 作用域链
当一段代码使用了一个变量,会首先在自身执行上下文中查找该变量,如果没有找到,继续到上一级执行上下文中查找,直到全局执行上下文,这个查找的链条就称为作用域链。
作用域链保存在函数的 [[Scopes]] 属性中。
1.5 作用域
作用域,指一个变量和函数的可访问范围,即可见性和生命周期。
- 全局作用域:存在于全局作用域的变量,可以在任意位置被访问。
- 函数作用域:存在于函数最外层的变量,可以在函数内任意位置被访问。
- 块级作用域:由最近的一对包含花括号{}界定, let 和 const 声明的变量具有块级作用域。
JavaScript 采用的静态作用域,又称词法作用域,函数定义的位置决定了函数的作用域,和函数是怎么调用的没有关系。
2. var、let 和 const 的区别
2.1 var、let 和 const 的区别
| var | let 和 const |
|---|---|
| 不存在块级作用域 | 具有块级作用域 |
| 存在变量提升,var 声明的变量会给全局对象 window/global 添加属性 | 不存在变量提升,在声明之前不可以使用,存在暂时性死区 |
| 重复声明时,后声明的变量会覆盖之前的变量 | 不可以重复声明 |
| 声明时,可以不用设置初始值 | let 用来声明变量,const 用于声明常量,必须在声明时进行初始化,且不可更改 |
| var 声明的内层变量可能覆盖外层变量,用来计数的循环变量泄露为全局变量,循环时产生的闭包可能会出现怪异行为 | const 声明的变量是不允许改变指针的指向,但可以改变指针所指对象的属性值 |
2.2 ES5 如何实现 let、const
(function () {
var a = 1;
console.log(a); // 1
})();
console.log(a); // Uncaught ReferenceError: a is not defined
function _const(key, value) {
window[key] = value;
Object.defineProperty(window, key, {
enumerable: false, // 能否通过delete删除,能否修改属性特性
configurable: false, // 能否通过for-in返回属性
get: function () {
return value;
},
set: function (newValue) {
if (newValue !== value) {
throw TypeError("只读变量,不可修改");
} else {
return value;
}
},
});
}
_const("a", 10);
console.log(a); // 10
delete a;
console.log(a); // 10
for (let item in window) {
if (item === "a") {
// 不可枚举,所以不执行
console.log(window[item]);
}
}
a = 20; // Uncaught TypeError: 只读变量,不可修改
2.3 如何使得 const 定义的对象的属性也不能被修改?
- 递归 defineProperty
- Proxy
2.4 变量或函数提升
var 声明或函数声明,会被拿到当前作用域的顶部,位于作用域中所有代码之前,叫做“提升”。
- var 变量被提升后,变量默认值为 undefined;
- 函数被提升后,可以在函数声明之前调用;
- 如果存在同名的变量和函数,函数声明会覆盖变量声明。
提升带来的问题:变量容易在不被察觉的情况下被覆盖掉;本应销毁的变量没有被销毁。
解决方案:let 和 const 声明的变量,由于存在“暂时性死区”,不能在声明之前使用。
3. 代码题
3.1 静态作用域
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
function F1() {
var a = 100;
return function () {
console.log(a);
};
}
function F2(f1) {
var a = 200;
f1();
}
var f1 = F1();
F2(f1); // 100
3.2 函数作用域
var str = "window";
(function () {
var str = "function";
console.log(str);
})();
// function
console.log(str); // window
(function () {
var str = "function";
console.log(str);
})();
// function
console.log(str); // Uncaught ReferenceError: str is not defined
var name = "window";
function fn() {
if (typeof name === "undefined") {
var name = "function";
console.log(name);
} else {
console.log(name);
}
}
fn(); // function
3.3 变量提升
showName();
console.log(name);
var name = "yc";
function showName() {
console.log("函数showName被执行");
console.log(name);
}
// 函数showName被执行
// undefined
// undefined
console.log(a); // ƒ a() {}
function a() {}
var a = 1;