为什么要知道作用域链
作用域链
的概念包含了 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 执行时的作用域链不确定。
总结
- 尽量使用局部变量
- 如果需要引用全局变量,例如 window,把 window 前缀写上
- 尽量避免使用 with 语句
- 在必要的时候才使用 eval
虽然现代浏览器解析速度很快,但是这些基本的小技巧还是可以对应用程序有一定的帮助的。当然在应用足够复杂的时候才可能需要优化。
参考
- Javascript 高级程序设计
- 高性能 Javascript