JS 的作用域链以及函数执行过程

6 min read
# 记录# 技术
View

为什么要知道作用域链

作用域链的概念包含了 JS 函数被执行的过程,是关于如何理解 this 的至关重要的知识,以及对在编写代码时有意识地做效率优化有帮助。

什么是作用域链

每一个 Javascript 函数都表示为 Function 对象的实例,Function 对象和其他对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问,仅供 JavaScript 引擎存取的内部属性,其中一个内部属性是 [[scope]],由 ECMA-262 标准第三版定义。 [[scopr]] 中包含了一个函数被创建的作用域中对象的集合,这个集合被成为函数的作用域链

高性能 JavaScript 第二章

作用域链内部属性:

函数执行过程

以下是一段代码片段例子

// 找出元素在数组中的 index
function getIndex(arr, item) {
  return arr.indexOf(arr);
}

// 执行 getIndex
getIndex([1,2,3,4], 2)

创建执行上下文(excision context)

在 getIndex 被执行的时候,会创建一个执行上下文(excision context)的内部对象。执行上下文对象是唯一的,并且每次调用同一个函数时执行上下文都不同。当函数执行完毕后,执行上下文会被销毁。

执行上下文初始化自身的值

每一个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建时,它的作用域链初始化为当前运行函数的 [[scope]] 属性中的对象。这些值按照他们出现的顺序,被复制到执行上下文作用域链中。

创建 activation object

等这个过程完成了,又会创建一个 activation object,它主要是作为函数运行时的变量对象,包含了所有局部变量、命名参数、参数集合以及 this。这个对象会被推到作用域链的最顶层。

解析标识符和关键字

在函数的执行中,每遇到一个变量,都会经历一次标识符解析过程,用以决定从哪里获取或存储数据。该过程检索执行上下文作用域链,查找同名的标识符。如果一直都找不到标识符,则标识符被视为未定义。每个标识符都将经历这样的过程。

new 操作

作用域链是一个函数对象的内部属性,如果使用 new 构建一个函数的实例,实例自身并没有 [[scope]],其作用域链就是原型上的 [[scope]]

更改作用域链

有 2 个方法可以更改函数的作用域链

with 语句

function testWith() {
  with(document) {
    let div = createElement('div');
    console.log(div)
  }
}

这个例子的说,函数 testWith 在执行的过程中,把其执行上下文的作用域链的顶端设置为 document,而不是自身的,所以对性能有一定的损耗,目前情况也很少用这个语句

try catch 语句

在 try catch 语句中,try 块内运行的代码如果遇到错误,会跳转到 catch 块中,然后把异常对象推入一个变量对象并置于作用域首位。

try {
  let a = {};
  JSON.parse(a)
} catch (e) {
  console.log(e)
}

在 catch 语句中会将修改作用域链,当 catch 语句执行完毕后,作用域链又会回到原本的状态。

动态作用域链

使用 eval

function exec() {
  let name = 'yourName'
  console.log(name)
  eval('name = "Hahaha";')
  console.log(name)
}

eval 执行时的作用域链不确定。

总结

  1. 尽量使用局部变量
  2. 如果需要引用全局变量,例如 window,把 window 前缀写上
  3. 尽量避免使用 with 语句
  4. 在必要的时候才使用 eval

虽然现代浏览器解析速度很快,但是这些基本的小技巧还是可以对应用程序有一定的帮助的。当然在应用足够复杂的时候才可能需要优化。

参考

  • Javascript 高级程序设计
  • 高性能 Javascript
Table of Contents