Javascript深入浅出之作用域

本来这篇文章是紧跟着 this 篇就要出来的,不想竟时隔近半年之期。从执行上下文,到作用域甚至闭包,这是一个小体系,缺了作用域以及闭包这部分,使我学习《Javascript Ninja》中设计函数式编程的的地方尤为吃力。遂补全这二章。

作用域指定了变量或者函数的可用范围

了解作用域,就是了解作用域链。

初探作用域链

代码执行 时,会创建变量对象的一条作用域链。作用域链的前端,是当前执行上下文的变量对象(活动对象),作用域链的下一个变量对象来自外部的上下文,再下一个变量对象来自再下一个外部环境…作用域链的尾部则是全局上下文的变量对象。

我们先来看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
function foo() {
var b = 2;
function bar() {
console.log(a + b);
}
bar();
}

bar 的作用域链是

1
2
3
4
5
[
VO(bar), // 作用域链的前端
VO(foo),
AO(global)
]

作用域链其实就是将自身上下文,父上下文,一直延展到全局上下文的变量对象用一条链子连接起来的。

可能我们又要好奇了,作用域链是如何把它们串起来的呢?

详解作用域链

函数创建

当函数刚创建的时候,函数就天生自带了一个属性 [[scopes]] ,它是所有父变量对象的层级链。

1
2
3
4
5
6
var a = 1;
function foo() {
var b = 2;
console.log(a, b); // 1, 2
}

foo 函数创建的时候,它就已经拥有 [[scopes]] 属性了。

1
2
3
foo.[[scopes]] = [
gloabl.VO
]

函数调用

函数被调用 时,会创建当前函数的上下文:

1
2
3
4
5
Context = {
AO: {...},
this: thisValue,
Scope: AO.concat([[scoopes]])
}

此时才会创建作用域链。

我们在 Javascript深入浅出之this 那一章中讲到的 变量提升 之所以存在,是因为在所有代码执行之前, js 引擎会进行一个 标识符解析 的过程。

标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。

我们继续来看一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 1;
function foo() {
var b = 2;
function bar() {
var c = 3;
console.log(a + b + c); // 6
}
bar();
}
foo();

全局上下文的变量对象是:

1
2
3
4
globalContext.VO = {
a: 1,
foo: <reference to function>
};

foo 创建时, foo[[scopes]] 属性为:

1
2
3
foo.[[scopes]] = [
globalContext.VO
]

foo 被调用时, foo 上下文的 活动对象 和 作用域链 为:

1
2
3
4
5
6
7
8
9
fooContext.AO = {
b: 2,
bar: <reference to function>
}
fooContext.Scope = [
fooContext.AO,
globalContext.VO
]

bar 创建时, bar[[scopes]] 属性为:

1
2
3
4
bar.[[scopes]] = [
fooContext.AO,
globalContext.VO
]

bar 被调用时, bar 上下文的 活动对象 和 作用域链 为:

1
2
3
4
5
6
7
8
9
barContext.AO = {
c: 3
}
barContext.Scope = [
barContext.AO
fooContext.AO,
globalContext.VO
]

标识符解析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a
-------------
barContext.AO // NOT FOUND
fooContext.AO // NOT FOUND
globalContext.VO // FOUND 1
b
-------------
barContext.AO // NOT FOUND
fooContext.AO // FOUND 2
c
-------------
barContext.AO // FOUND 3

我们借助 Chrome Develop Tools 看一下:(代码有很大改动)

 \[\[scopes\]\]属性以及 Scope

Some Cases

上面的例子展示的就是闭包的 [[scopes]] 特性,这里就不赘述了。

以构造函数创建的函数

1
2
3
4
5
6
7
8
function foo() {
var a = 1;
var bar = Function('console.log(a);');
bar(); // Uncaught ReferenceError: a is not defined
}
foo();

这里 bar 函数之所以无法解析找到 a ,是因为以构造函数创建的函数的 [[scopes]] 属性指向的是全局变量。

这个还有一个延伸,就是严格模式下,是不允许使用 with 语句的,但是如果把语句放到 new Function 中,是被允许的。

1
2
3
4
5
6
7
8
9
10
11
"use strict";
with({a: 2}) {} // Uncaught SyntaxError: Strict mode code may not include a with statement
function foo() {
var a = 1;
var bar = Function('with({a: 2}) {}'); // 正常
bar();
}
foo();

new Function 产生的是 global 作用域下的函数,而且默认是 non-strict

延伸作用域链

有两种方法,withtry-catch 语句。

他们都遵循:Scope = withObject|catchObject + AO|VO + [[Scope]]

这里拿 with 举例并且延伸一下。

1
2
3
4
5
6
7
8
9
10
var
a = 1,
b = 2;
with({a: 10}) {
a = b = 20;
console.log(a, b); // 20, 20
}
console.log(a, b); // 1, 20

执行 with 语句时候,作用域最前端已经是 {a: 10} 了,我们在 with 语句里面改动的 a
是在 {a: 10} 中找到了,所以,全局作用域中的 a 并没有被修改。


作用域写完了,有一些边边角角的东西没写,不然展开的话篇幅太大。下篇文章就是闭包了,闭包和作用域真的是基友到爆,所以作用域一定要好好看,好好理解,多敲代码,跑到 Chrome 里去打几个断点测一测,效果会好很多。

热评文章