JavaScript高级程序设计——上下文环境和作用域

js-context

前言

此文章为加深对JS中重要概念进行理解,不建议没有任何JS基础的人看,只为加深对概念理解通过实际的例子,而不是看书以为自己读懂了,可能几天后就忘了,主要是为了理解核心概念,以及对重难点解释。

引言

此篇在JS红宝书里其实没有特别来讲,只是穿插在其他的章节提及而已,内容不难,只为加深印象。

执行上下文

函数表达式和函数声明

1
2
3
4
5
6
7
8
9
10
11
12
1.
console.log(a); // ReferenceError: a is not defined
// ReferenceError(引用错误)对象表明一个不存在的变量被引用。
2.
console.log(a); // undefined
var a;
3.
console.log(a); // undefined
var a = 10;
4.
var a = 10;
console.log(a); // 10

第一句报错,a未定义,很正常。第二句、第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道了a是undefined,但却不知道a是10(第三句中)。

在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明,而不是赋值。变量赋值是在赋值语句执行的时候进行的。可用下图模拟:
context

接下来的这段代码需要注意代码注释中的两个名词——“函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。

“准备工作”

1
2
3
4
5
6
7
1.
console.log(f1); //function f1()
function f1() {}
2.
console.log(f2); //undefined
var f2 = function f2() {}

看以上代码。“函数声明”时我们看到了第二种情况,而“函数表达式”时我们看到了第一种情况。

在“准备工作”中,对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。
而对待函数声明时,却把函数整个赋值了。

总结一下,在“准备工作”中完成了哪些工作:

  1. 变量、函数表达式——变量声明,默认赋值为undefined;
  2. this——赋值;
  3. 函数声明——把函数整个赋值;

这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

1
2
3
4
5
function fn(x) {
console.log(arguments); //Arguments { 0: 10, 等 2 项… }
console.log(x); //10
}
fn(10);

以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。从这里可以看出,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

另外一点不同在于,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。用一个例子说明一下:

1
2
3
4
5
6
7
8
9
10
11
12
var a = 10;
function fn() {
console.log(a); //a是自由变量
//函数创建时,就确定了a要取值的作用域
}
function bar() {
var a = 20;
fn(); //打印10不是20
}
bar(fn); //10

给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。

执行上下文栈

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。

其实这是一个压栈出栈的过程——执行上下文栈。

js-context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0
1 var a = 10, //1.进入全局上下文环境
2 fn,
3 bar = function(x) {
4 var b = 5;
5 fn(x + b); //3.进入fn函数上下文环境
6 };
7
8 fn = function(y) {
9 var c = 5;
10 console.log(y + c);
11 }
12
13 bar(10); //2.进入bar函数上下文环境

在执行代码之前,首先将创建全局上下文环境。

全局 上下文环境
a undefined
fn undefined
bar undefined
this window

然后是代码执行。代码执行到第12行之前,上下文环境中的变量都在执行过程中被赋值。

全局 上下文环境
a 10
fn function
bar function
this window

执行到第13行,调用bar函数。

跳转到bar函数内部,执行函数体语句之前,会创建一个新的执行上下文环境。
全局 | 上下文环境
—|—
b | undefined
x | 10
arguments | [10]
this | window

并将这个执行上下文环境压栈,设置为活动状态。

js-context
执行到第5行,又调用了fn函数。进入fn函数,在执行函数体语句之前,会创建fn函数的执行上下文环境,并压栈,设置为活动状态。

js-context

待第5行执行完毕,即fn函数执行完毕后,此次调用fn所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。

js-context

同理,待第13行执行完毕,即bar函数执行完毕后,调用bar函数所生成的上下文环境出栈,并且被销毁(已经用完了,就要及时销毁,释放内存)。

js-context

好了,给大家介绍了一段简短代码的执行上下文环境的变化过程,一个完整的闭环。其中上下文环境的变量赋值过程我省略了许多,因为那些并不难,一看就知道。

作用域

基础认识

“javascript没有块级作用域”。所谓“块”,就是大括号“{}”中间的语句。

比如一个if语句

1
2
3
4
5
var i = 10;
if (i > 1) {
var name = "yzh";
}
console.log(name); //yzh

for语句

1
2
3
4
for (var i = 0; i < 10; i++) {
}
console.log(i); //10

我们在编写代码的时候,不要在“块”里面声明变量,要在代码的一开始就声明好了。以避免发生歧义

1
2
3
4
5
var i;
for (i = 0; i < 10; i++) {
}
console.log(i);

你光知道“javascript没有块级作用域”是完全不够的,你需要知道的是——javascript除了全局作用域之外,只有函数可以创建的作用域。

我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式

概念

js-context

如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

例如以上代码中,三个作用域下都声明了“a”这个变量,但是他们不会有冲突。各自的作用域下,用各自的“a”。

作用域和上下文环境

js-context

如上图,我们在上文中已经介绍了,除了全局作用域之外

每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时确定。

下面我们将按照程序执行的顺序,一步一步把各个上下文环境加上

第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。

js-context

第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。

js-context

第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。

js-context

第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。

js-context

第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变为活动状态。

js-context

第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。

js-context

最后我们可以把以上这几个图片连接起来看看。

js-context

作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。

同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

自由变量

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。
例:

1
2
3
4
5
6
7
var x = 50;
function fn() {
var b = 20;
console.log(x + b);
}
fn(); //70

在调用fn()函数时,函数体中第6行。取b的值就直接可以在fn作用域中取,因为b就是在这里定义的。而取x的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
有人说过要到父作用域中取,其实有时候这种解释会产生歧义
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 50;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
(function() {
f(); //50 不是20
})();
}
show(fn); //50 不是20

不要在用以上说法了。相比而言,用这句话描述会更加贴切——要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记——其实这就是所谓的“静态作用域”。

对于本文第一段代码,在fn函数中,取自由变量x的值时,要到哪个作用域中取?——要到创建fn函数的那个作用域中取——无论fn函数将在哪里调用

上面描述的只是跨一步作用域去寻找。

如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

这个一步一步“跨”的路线,我们称之为——作用域链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var color = "blue";
function changeColor() {
var anotherColor = "red";
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//这里可以访问color,anotherColor和tempColor
}
//这里可以访问color和anotherColor,但不能访问tempColor
swapColors();
}
changeColor(); //注释后alert显示为blue
//这里只能访问color
alert("Color is now " + color); //red

以上代码共涉及3个执行环境:全局环境、changeColor()的局部环境和swapColors()的局部环境。

全局环境 changeColor()的局部环境 swapColors()的局部环境
变量color 变量anotherColor 变量tempColor
函数changeColor() 函数swapColors()

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境

最后这个例子是从书上找到的,比较经典和简单。

End

后面将总结一篇关于闭包的文章

参考文章

深入理解javascript原型和闭包——执行上下文栈

深入理解javascript原型和闭包-作用域和上下文环境

坚持原创技术分享,您的支持将鼓励我继续创作!
Fork me on GitHub