原文引用 大专栏 https://www.dazhuanlan.com/2019/08/26/5d6332d1a19b5/
我们真的了解 Javascript 的内部工作原理吗?
下面几个问题可以检验一下。
Questions
Q1
1234 |
if (!("a" in window)) { var a = 1;}alert(a); |
Q2
12345 |
var a = 1, b = function (x) { x && a(--x); };alert(a); |
Q3
12345 |
function a(x) { return x * 2;}var a;alert(a);//function a(x) {return x * 2;} |
处理上下文代码
要想理解上面问题的执行结果的原因,需要先理解 Javascript 是如何处理执行上下文代码。
处理执行上下文代码分为两个阶段:
- 进入执行上下文
- 执行代码
进入执行上下文
先了解一个概念变量对象(Variable object),它是一个与执行上下文有关的特殊的对象,它包括:
- 变量声明(var)
- 函数声明(FD)
- 函数形参在上下文中的声明
一般用 VO = { } 表示变量对象。举个例子:
12345 |
var a = 10;function test(x) { var b = 20;};test(30); |
上述代码对应的变量对象(VO)则如下所示:
1234567891011 |
// 全局上下文中的变量对象VO(globalContext) = { a: 10, test: }; // “test”函数上下文中的变量对象VO(test functionContext) = { x: 30, b: 20}; |
当进入执行上下文时,变量对象(VO)会依次被下列属性填充:
-
函数的形参
—— 其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined。 -
函数声明(FD)
—— 其属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则替换它的值。 -
变量声明(var)
—— 其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
举个例子:
1234567 |
function test(a, b) { var c = 10; function d() {} var e = function _e() {}; (function x() {});}test(10); |
当以10为参数进入“test”函数上下文的时候,对应的AO如下所示:
1234567 |
AO(test) = { a: 10, b: undefined, c: undefined, d: <reference to FunctionDeclaration "d">, e: undefined}; |
注意,上面的AO并不包含函数“x”。这是因为这里的“x”并不是函数声明而是函数表达式(FunctionExpression),函数表达式不会对VO造成影响。 尽管函数“_e”也是函数表达式,然而,由于它被赋值给了变量“e”,因此它可以通过“e”来访问到。
这样,第一阶段进入执行上下文就结束了,接下来是第二阶段执行代码。
执行代码
这时的变量对象(VO)的属性已经被填充好了(大多数值仍未undefined),在执行代码阶段,变量对象(VO)会被修改为如下形式:
12 |
AO['c'] = 10;AO['e'] = <reference to FunctionExpression "_e">; |
看一个更典型的例子:
123456 |
alert(x); // function x(){} var x = 10;alert(x); // 10 x = 20;function x() {};alert(x); // 20 |
根据规则,在进入上下文的时候,VO会首先被填充函数声明; 同一阶段,还有变量声明“x”,但是,正如此前提到的,变量声明是在函数形参和函数声明之后,并且,变量声明不会对已经存在的同样名字的函数形参和函数声明发生冲突, 因此,在进入执行上下文的阶段,VO填充为如下形式:
123456 |
VO = {};VO['x'] = <reference to FunctionDeclaration "x">;// 发现var x = 10;// 如果函数“x”还未定义,则 "x" 为undefined, 但是在我们的例子中已经有了函数声明“x”;// 所以变量声明并不会影响同名的函数值;VO['x'] = <reference to FunctionDeclaration "x">; |
随后,在执行代码阶段,VO被修改为如下所示:
12 |
VO['x'] = 10;VO['x'] = 20; |
再看一个例子:
1234567 |
if (true) { var a = 1;} else { var b = 2;}alert(a); // 1alert(b); // undefined, but not "b is not defined" |
在进入执行上下文阶段,变量存储在VO中,因此,尽管else的代码块永远都不会执行到,但“b”却仍然在VO中,值为undefined,而不是报错。
回顾开始的问题
Q1
1234 |
if (!("a" in window)) { var a = 1;}alert(a);//undefined |
-
进入执行上下文: 创建VO,并填充变量声明 a,VO如下所示:
123
VO(global) = { a: undefined}
-
执行代码: 进入 if语句,发现条件判断 “a” in window 为 true。于是就不会进入if代码块,直接执行alert语句,因此,最终为undefined。
Q2
12345 |
var a = 1, b = function a(x) { x && a(--x); };alert(a);//1 |
-
进入执行上下文: 注意, b = function a(){},这里的 function a并非函数声明,因为整个这个句话属于赋值语句(assignment statement),所以,这里的 function a会被看作是函数表达式。 函数表达式是不会对VO造成影响的。所以,这个时候VO中其实只有 a和x(函数形参):
1234
VO(global) = {a: undefined,b: undefined}
-
执行代码: 这个时候a的值修改为1:
1234
VO(global) = {x: undefined,a: 1}
Q3
12345 |
function a(x) { return x * 2;}var a;alert(a);//function a(x) {return x * 2;} |
-
进入执行上下文: 根据变量对象(VO)介绍的,填充VO的顺序是: 函数的形参 -> 函数声明 -> 变量声明。变量a在函数a后面,那么,变量a遇到函数a怎么办呢?还是根据变量对象中介绍的,当变量声明遇到VO中已经有同名的时候,不会影响已经存在的属性。因此,VO如下所示::
123
VO(global) = {a: <reference to FunctionExpression "x">}
-
执行代码: 没变化,最终的结果是:函数a。
参考链接: