JavaScript 执行上下文和作用域

77 阅读6分钟

概要

  • 执行上下文: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(后进先出)的数据结构,用来存储代码运行时的所有执行上下文。

  1. 当 JS 引擎执行 js 脚本时,会创建一个全局执行上下文,压入当前执行栈;
  2. 每当遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部;
  3. 当该函数执行结束时,执行上下文从栈中弹出,把控制权返回给之前的执行上下文;
  4. 一旦所有代码执行完毕,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");

execution-stack.png

1.3 执行上下文的组成

创建执行上下文:

  • 创建词法环境(LexicalEnvironment)
  • 创建变量环境(VariableEnvironment)
ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}
context.png

词法环境是一种保存变量(或函数)名和值的映射关系的键值对。

词法环境包含 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 的区别

varlet 和 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;