作用域

js中变量的作用域, 只有全局作用域和函数作用域, 不支持类C语言中的块级作用域(一个花括号即一个作用域.)

全局作用域没什么好讲的, 全局变量有全局作用域. 下面主要讲函数作用域.

表现

定义在函数内部的变量和参数, 在函数外部不可见, 在函数内部任何位置的变量则在函数内任何位置都可见.

实现

js通过作用域链Scope Chain来实现作用域, 与C不同, js的作用域链不是基于栈而是基于列表的.

1 当定义一个函数时, 将该时刻的作用域链连接到这个函数对象的[[scope]]属性. [[scope]]是一个内部属性, 仅供js引擎访问, 它包含了函数被创建的作用域中的对象的集合.

2 当调用这个函数时, 会创建一个叫执行时上下文(execution context)的内部对象, 它定义了函数执行时的环境. 每个执行时上下文都会有自己的作用域链用于标识符解析, 这个作用域链先是被初始化当前执行函数的[[scope]]中所包含的所有对象, 然后再将一个新创建的叫活动对象(activation object)的对象推入到作用域链的最前端. 活动对象中包含了函数的所有局部变量, 命名参数, 参数集合和this.

当执行时上下文被销毁, 活动对象也随之销毁.

3 在发生标识符解析(或称变量解析variable resolution)的时候, 就逆向查询当前作用域链的每一个活动对象的属性. 如果找到同名的就返回, 找不到, 那就是这个标识符没有被定义, 抛出ReferenceError.

用实例表述

定义时刻

  • 创建[[scope]]属性, 收集保存作用域内的被创建的对象.
  • 把[[scope]]属性链接到作用域链
  • 设置[[scope]]属性指向的活动对象(本例为全局活动对象window activation object)
function fun(arg1, arg2){
  var mood = arg1 + arg2;  // 调试点1
  var moha = makeABigNews();
  moha();  // 调试点4
}

function makeABigNews(){ // 调试点2
  var mood = 'angry!';
  var result = function(){ // 调试点3
    console.log('I am ' + mood);
  }
  return result;
}

调用时刻

  • 创建activeObj(假设名)活动对象, 同时创建arguments属性.
  • 以保存为属性的形式, 将函数的每一个形参局部变量添加到活动对象中

    比如activeObj.arg1, activeObj.arg2, activeObj.prop

  • 把实参赋给形参
  • 把activeObj活动对象作为作用域链最前端
  • 把fun函数的[[scope]]属性所指向的活动对象(假设在浏览器中的话, 本例为window activation object)也加入到作用域链
// 调试点0
fun('excited', '!'); // I am angry!

图表展示流

1 定义时 定义时作用域链

2 执行时 执行时作用域链

调试追踪

还以上面的代码为例, 配合chrome-devtool, 查看作用域链的变化, 讲讲为什么最终输出I am angry!.

调试点的编号也指明了调试时解释跳转的顺序.

1 当调用fun函数的时候, 其作用域链是由{window activation object}->{activeObj}组成的.

// 刚刚进入fun函数, 即源码处的调试点1
[[scope chain]] = [
{
     arguments: ['excited', '!'],
     arg1: 'excited',
     arg2: '!',
     mood: undefined,
     moha: undefined
}, {
     window activation object
}]

2 当调用进入makeABigNews的函数体的时候, 此时makeABigNews的作用域链为

// 对应调试点2
[[scope chain]] = [
{
     mood: undefined,
     result: undefined
}, {
     window activation object
}]

3 在定义result函数的时候, result函数的scope为

// 对应调试点3
[[scope chain]] = [
{
     mood: 'angry!',
     result: undefined
}, {
     window activation object
}]

4 从makeABigNews函数返回以后, 在fun函数中调用result的时候, 发生了标识符解析, 而此时的作用域链为

// 对应调试点4
[[scope chain]] = [
{
     result call object
}, {
     mood: 'angry!',
     result: undefined
}, {
     window activation object
}]

可以看到, 这时候并不包含fun的活动对象, 所以返回的是makeABigNews活动对象中的mood属性.

特例

通过构造器创建的函数是访问不到外层的局部变量的.

function outer() {
    var i = 1;
    var func = new Function("console.log(typeof i);");
    func(); // undefined
}
outer();

总结

  • 在定义函数时, 就决定了函数的scope属性, 其作用域也在那时被确定下来.
  • 由于逆向查找, 标识符所在位置越深, 读写越慢, 所以少用全局变量.
  • 一个跨作用域的变量被引用了一次以上, 那么最好把它变成局部变量再使用. *

References