浅谈js执行上下文和词法作用域

3,772 阅读10分钟

js执行上下文和词法作用域

参考 :

  1. let/const/var的声明提升
  2. 理解执行上下文和执行上栈
  3. es6规范文档
  4. es5规范文档
  5. 闭包MDN文档

执行上下文的作用

context stack -> execution context -> code

每当js引擎执行一段新的js代码时,它都会创建一个全新的执行上下文,由执行上下文来跟踪整个代码的执行情况、当前执行函数、作用域、this指向和变量映射。js引擎通过读取执行上下文就可以管理追踪该段js代码的执行情况,那么多个上下文是通过什么来管理的呢?答案是栈,多个上下文的管理是通过上下文栈来管理的。如果解析到一段跟当前上下文无关的新代码,那么js引擎就会在上下文栈里创建一个新的上下文对象,并插入栈顶的位置。当执行完上下文的时候就会在上下文栈里把这个对象踢出栈,恢复下一个栈。

执行上下文的组成

execution context -> code evaluation state + function + realm

我们来看下面这段代码,在全局情况下会有个全局上下文,放在栈底。在执行fn的时候,会在栈顶插入一个新的上下文,并且将当前执行上下文指向这里,当解析到whileFn函数时,又重复刚刚的步骤。这个时候fn上下文会 暂停 并记录执行节点,去执行whileFn上下文,当执行结束后又会 恢复 fn上下文,并且在上次暂停的地方继续执行。由此可见执行上下文跟踪代码时,会有代码执行状态(code evaluation state),用来表示执行状态是暂停、恢复等,以及指示暂停节点。除此之外执行上下文还会记录当前执行的函数(function)以及作用域(realm)

js执行代码
var global = 'global'
console.log(global)
function fn() {
  let inner = 'inner'
  let time = 0
  function whileFn() {
    while(time < 10) {
      ++time
    }
  }
  console.log(inner)
  whileFn()
  console.log(time)
}
fn()

另外可以看下这段代码及其对应的执行栈示意图

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');

词法作用域的作用和组成

lexical Environments => 标识符<-映射->实际引用 => LexicalEnvironment + VariableEnvironment

除此之外,执行上下文还会创建一个词法作用域,别看这个名词感觉很流弊的样子,其实它只是用于存储标识符和实际引用之间的映射,类似于字典。当js引擎执行到一段代码时,遇到变量名或者函数名时,就会在这个字典里进行查找并调用出里边的值。词法作用域还分为词法环境LexicalEnvironment 和变量环境VariableEnvironment ,在ES5的时候这两者还有着复杂的区别和定义,但是在ES6时,这两者的区别基本就是存储变量的声明关键字不同,前者是用来存放 let / const / function / class 等声明的标识符映射,而后者是用来存储 var 声明的标识符映射。

词法作用域的分类

lexical Environments => global environment + module environment + function environment

词法作用域可分为全局作用域、模块作用域和函数作用域。

// global
import module from './module'
console.log('global environment', this)
function() {
	console.log('function environment', this)
}

// module
console.log('module environment', this)

var 和 let、const 有什么区别

刚刚有稍微讲到这几个的区别,接下来我们结合词法作用域的概念来具体说说。这几个声明的主要区别是初始化变量的机制和存储标识符映射的环境不同:

初始化变量的机制和存储映射的环境

var声明 -> 当前执行上下文 -> 初始化词法作用域 -> 变量环境 -> 初始化var变量为undefined

js引擎在创建当前执行上下文时,会初始化词法作用域,在 变量环境 里创建 var 变量的标识符映射并初始化为undefined。var比较特殊,可以多次执行 var 声明语句赋值相同的变量,js引擎会做相应的创建 / 修改变量的操作

let / const 声明 -> 当前执行上下文 -> 初始化词法作用域 -> 词法环境 -> 创建声明的变量,禁止访问变量

js引擎在创建当前执行上下文时,会初始化词法作用域,在 词法环境 里创建 let / const / function / class 标识符映射,但是只会创建 let / const / class 变量,不做初始化操作,并且禁止访问。只有在当前执行上下文执行阶段,执行 词法绑定 时,才会初始化为对应的 value 或者undefined,此时才会允许访问变量。 let / const / class 关键字不允许在同一作用域下重复声明相同变量。

var、let、const、class和function的创建机制是,在执行上下文的创建阶段时,js引擎就会在内部创建一个词法作用域,它就像是一个标识符查找的字典。这个字典会在创建阶段就将声明的变量、函数和类都创建,但是var 声明的变量会被初始化为undefined,而 let / const / class 声明的变量不会被初始化,并且禁止访问,function 关键字的声明会直接将函数体赋值给对应的函数名。而且除了var之外,后面几个关键字声明的标识符映射是存放在词法环境里,而var声明的标识符映射是存放在变量环境里。

讲讲闭包

closure -> function + lexical environments

结合上面内容,我们来看看闭包是什么,我们常见的闭包写法,就是新建一个闭包函数工厂,在函数里定义变量,然后再返回一个引用这些变量的函数,如下所示:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

在makeAdder函数内会创建一个内部的词法作用域,存储传入的x变量,那么每次调用makeAdder函数就会创建一个拥有独立内部词法作用域的函数,add5函数会绑定x=5的作用域,add10绑定的是x=10的作用域。因此add5(2)和add10(2)返回的结果是不一样的。 我们可以看到,闭包是一个绑定了词法作用域的函数,通过闭包可以访问内部词法作用域定义的局部变量。这样子通过闭包我们可以实现私有方法,避免污染全局环境或者保证方法只能在内部调用,封装细节,以及避免昂贵的计算过程,缓存计算结果。参考下面代码:

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var counter1 = makeCounter();
var counter2 = makeCounter();

每次调用makeCounter函数会创建独立的闭包,在闭包里会有私有变量privateCounter和私有方法changeBy,外部调用时不需要知道这两个东西,只需要接收increment和decrement方法,不需要知道细节实现

事件回调 -> 闭包

绑定事件监听回调,其实也是一种闭包,只是没有那么明显而已。看看下面这段代码

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

这里的运行结果大家都知道了,但是运行机制就有点意思了。这是因为整个onfocus方法绑定的事件回调是一个闭包,在每个input控件里触发的onfocus回调函数里,它会在setupHelp这个函数上下文里查找item变量,由于遍历之后i都是2,所以此时item都是{'id': 'age', 'help': 'Your age (you must be over 16)'}这条记录。这就导致了每次showHelp的时候,对应的item都会绑定到同一个对象上去。这个时候的解决方法有两个,要么给onfocus绑定另一个闭包,要么就是用let关键字声明一个item,保证这个item只会存在for循环的块作用域里。

// 新的闭包
function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}
...
function setupHelp() {
...
  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

// let关键字
...
function setupHelp() {
...

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

执行上下文和词法作用域的运行机制

js引擎 -> 执行上下文(创建) -> 词法作用域 -> 词法环境 + 变量环境 -> 环境记录 + 外部引用 + this绑定 -> 执行上下文(执行) -> 执行代码

最后,我们把执行上下文、词法作用域,还有var / let / const结合起来看的话,js引擎解析代码的整体过程如下: js引擎执行一段和当前上下文无关的代码时,会创建一个对应的执行上下文用于追踪管理代码,并将当前执行上下文指向该上下文。执行上下文的创建过程中会创建对应的词法作用域,词法作用域有两种:词法环境和变量环境,分别用于存储 let / const / function / class 和 var 声明的变量。词法作用域会通过内部的环境记录来存储标识符与实际变量的映射关系,一个外部引用用于查找非当前作用域的变量时进行逐级溯源查找,以及绑定当前作用域的this指针。当创建完之后,会进入执行上下文的执行阶段,最终执行代码,获取执行结果。

举个例子,我们看看下面的代码:

const a = 20;
let b = 30;
var c = '';
function foo(a, b) {
  var d = a + b;
  return d
}
foo(a, b);

那么当js引擎解析到上述代码时,首先,会创建一个全局执行上下文用于执行代码,接着在全局上下文的创建阶段,会创建一个全局的词法作用域用于存储标识符映射,此时,var 变量会被初始化为 undefined,而 let / const 变量则是 uninitialized,并且禁止访问。如下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: uninitialized,
      b: uninitialized,
      foo: <ref. to foo function>,
    }
    outer: <null>,
    this: <global object>
  }
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined
    }
    outer: <null>,
    ThisBinding: <global object>
  }
}

接着,全局执行上下文进入执行阶段,进行词法绑定,将标识符和实际变量进行绑定。如下:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20,
      b: 30,
      foo: <ref. to foo function>,
    }
    outer: <null>,
    this: <global object>
  }
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: ''
    }
    outer: <null>,
    ThisBinding: <global object>
  }
}

在执行阶段,当执行到 foo(2, 3) 时,进入 foo 函数,会创建一个函数执行上下文,对应地创建函数词法作用域。函数词法作用域,除了存储内部声明的变量外,还会存储整个函数的实参和实参数量。函数上下文创建阶段的函数词法作用域如下:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

当 foo 函数开始执行时,会执行变量的词法绑定,此时,函数上下文执行阶段的词法作用域如下:

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      d: 60
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}