预编译
- 首先下面这段代码的执行是一个怎样的结果呢?
showName();
console.log(MyName);
var MyName = '小陈同学'
function showName() {
console.log('函数showName被执行');
}
在这段代码中我们声明了一个变量MyName和一个函数showName,调用函数,打印MyName
因为在函数编译的过程中会存在变量的声明提示,默认赋值为undefined,方法的整体提升
所以这段代码又可以看做成以下代码
var MyName = undefined
function showName() {
console.log('函数showName被执行');
showName();
console.log(MyName);
MyName = '小陈同学'
}
所以执行结果显而易见
接下来我们再细致的分析一下
在编译过程中,首先会创建一个上下文对象
当执行showName时,又会创建一个showName的执行上下文,这里就形成了一个调用栈
调用栈
调用栈是js引擎用来维护函数调用关系的一个数据结构
这段代码会出现什么结果?
function fn(){
fn()
}
fn()
答案是
超出了最大的栈内存调用大小,发生了栈溢出,这里我们可以知道,每当一个函数调用的时候就会发生栈帧的创建,以及压栈操作
这里我们明白了这个道理后,我们继续分析一下代码
var a = 2
function add(b, c) {
return b + c
}
function addAll(b, c) {
var d = 10
var result = add(b, c)
return a + result + d
}
addAll(3, 6)
首先会创建全局上下文
第二步会创建addAll的执行上下文
第三步会创建add的执行上下文
这便是这段代码在引擎里面维护的一个执行上下文调用栈
前面一直没有用到词法变量,现在我们要引入词法变量了,来分析一下下面的代码
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a);
console.log(b);
}
console.log(b);
console.log(c);
console.log(d);
}
foo()
首先会创建一个全局的上下文,这里包括了fanction foo,比较简单
接下会创建foo的执行全局上下文,这里我们重点分析一下
首先会进行一个预编译
预编译执行后开始执行代码
- 在函数内,定义了变量
a
为 1(使用var
),b
为 2(使用let
)。 - 然后在一个代码块中,又重新定义了
let b = 3
,此时在这个代码块内的b
是 3 ,而不是外面的 2 ;同时定义了var c = 4
和let d = 5
。 - 在代码块内打印
a
为 1 ,打印b
为 3 。 - 之后在函数内再次打印
b
为 2 (因为外面的b
没有被修改)。 - 由于
c
是在代码块内用var
定义的,所以在函数内可以访问到,打印c
为 4 。 - 但是
d
是在代码块内用let
定义的,在代码块外无法访问,会报错说d
未定义。
那么这里我提一个问题
代码块中的console.log(b);
为什么打印的是3而不是2?
你肯定会说,因为你使用了{}啊,这只是表层
其实是因为,在查找变量的时候,首先会去词法环境查找数据,然后再去变量环境查找,并且在词法环境里面维护了一个栈的结构,从上往下找
这里就出来了另一个问题,既然是首先会去词法环境查找数据,然后再去变量环境查找,那为什么
这个打印的b不是3呢?
这里面就是作用域链在起作用了
- 作用域链
这里举个代码例子,分析一下这段代码执行的结果
function bar(params) {
console.log(myName);
}
function foo(params) {
var myName = 'Tom'
bar();
}
var myName = 'Jerry';
foo();
结果为
为什么结果是Jerry呢?
不是在foo里面var myName = 'Tom’吗?
首先我们分析一下这段代码的调用栈
-
首先,程序开始执行,遇到
foo
函数的调用,进入foo
函数,此时调用栈中添加了foo
-
在
foo
函数内部,定义了变量myName
为'Tom'
,然后调用bar
函数,此时调用栈中添加了bar
在foo
之上 -
进入
bar
函数后,它尝试打印myName
,由于bar
函数内部没有定义myName
,它会沿着作用域链向上查找。首先在bar
自身的作用域内未找到,然后到foo
函数的作用域内也未找到(尽管foo
内部有定义,但不是同一个作用域),最后到全局作用域中找到了定义的myName
为'Jerry'
,所以打印出'Jerry'
-
当
bar
函数执行完毕后,从调用栈中弹出bar
,回到foo
函数继续执行,直到foo
函数执行完毕,再从调用栈中弹出foo
其实在每个上下文中都有一个outer属性,他通过去关联上一个作用域,来形成作用域链
那么它是一局什么规则呢?
词法作用域在哪里,outer就指向哪里,词法作用域的意思是在函数定义时所在的作用域
这也就是为什么打印的是Jerry,因为bar定义在全局,所以会去全局找myName
那么接下来这段代码呢?
function foo(params) {
var myName = 'Tom'
function bar(params) {
console.log(myName);
}
bar();
}
var myName = 'Jerry';
foo();
- 首先,执行
foo
函数。在foo
函数内部定义了变量myName
为'Tom'
,然后定义了内部函数bar
。 - 当调用
bar
函数时,它要打印myName
。由于bar
函数内部没有定义myName
,它会沿着作用域链查找。 - 首先在
bar
自身的局部作用域中找不到,然后会在其直接外层,也就是foo
函数的作用域中找到myName
为'Tom'
,所以最终会打印出'Tom'
。而全局定义的myName
为'Jerry'
,在这里并不会被bar
函数访问到
总结
本文深入探讨JavaScript的执行机制,从预编译,引擎的调用栈,作用域链等方面分析,相信看到这里你一定对JavaScript的执行机制有了更加深刻的理解