作用域
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属性, 其作用域也在那时被确定下来.
- 由于逆向查找, 标识符所在位置越深, 读写越慢, 所以少用全局变量.
- 一个跨作用域的变量被引用了一次以上, 那么最好把它变成局部变量再使用. *