细聊浏览器中js的执行机制(一)--作用域链

293 阅读4分钟

前言

前面我们了解了JS的预编译,本文我们来深入了解浏览器中JS的执行机制

正文

变量提升

  1. js引擎在执行js的过程中把变量声明部分和函数声明部分提升到代码头部,默认赋值为undefined

  2. 声明提升时发生在编译阶段

  3. 执行上下文中放变量环境和词法环境,变量环境中放var的声明变量和函数声明,词法环境中放let和const声明的变量

当引擎执行下列代码时,会先进行编译,编译就会创建上下文对象

showName()
console.log(myName);
var myName ='Alo'
function showName(){
    console.log('函数showName被执行');
}

在执行这段代码之前,JavaScript引擎会进行预编译阶段,将变量和函数声明提升到顶部。因此,代码实际上会被重构为:

var myName; // 变量声明被提升,但赋值操作没有
function showName() {
    console.log('函数showName被执行');
}

showName(); // 调用函数
console.log(myName); // 打印变量myName,此时myName的值为undefined
myName = 'Alo'; // 赋值操作

我们用图解析:

image.png

调用栈

在了解调用栈之前,我们先解释一下栈是个什么东西

栈的特点就是先进后出

栈在JS代码中表示为var stack = [] 欸我们发现,这不是个数组吗?

确实,但是我们要把它当成栈来用

JavaScript中的数组可以通过以下方法来模拟栈的行为:

  • Push:使用数组的push()方法来在末尾添加元素,这相当于在栈顶添加元素。
  • Pop:使用数组的pop()方法来移除最后一个元素,并返回它,这也符合栈的Pop操作。

调用栈的步骤

调用栈是js引擎用来追踪函数调用关系的

我们用下面这段代码进行解释:

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)

下面这个红框就是调用栈:

8f8d92d6a64c16e96dbf8392123ff75.jpg

第一步

创建全局的执行上下文:

其中左边的框为变量环境,右边为词法环境

82aa528484cae47053264bb196bdda6.jpg

第二步

js执行全局的代码

var a = 2 a变为2

函数先不看,最后是addAll的调用

0a0c14beb6ba17ac433c2cbc7d99cec.jpg

第三步

创建addAll执行上下文

def4a92d7de6fbe6cb18011929d72da.jpg

第四步 addAll的调用

发现有add函数调用

5bc546b9e6e57bae782632046b55865.jpg

add执行上下文中,声明环境和词法环境都没有内容

这就是完整的调用栈了

为什么使用栈

栈上的数据(如局部变量)在函数调用时自动分配和释放,这可以减少程序员管理内存的负担

当add执行完了,其执行上下文对象就会被销毁,当全部代码执行完后,全局执行上下文也会出栈

栈溢出

调用栈超出内存限制

比如下面的代码

function a() {
  var num = 1
  return num + b()
}
function b() {
  var num = 2
  return num + c()
}
function c() {

  var num = 3

  return num + d()
}
// .....有n多个调用
a()

要执行a 就要等b执行,要执行b,就要等c执行...这种写法就叫做回调地狱,会导致爆栈

作用域链

同样,我们先看个例子:

function bar() {
console.log(myName);
}
function foo() {
var myName = 'Alo';
bar();
}
var myName = 'Zack';
foo()

这段代码的执行结果是Alo还是Zack呢?

执行结果实际是Zack,我们来分布解析:

第一步

全局编译:

700634ffebea9a0cce168a1eac98755.jpg

第二步

创建foo执行上下文:

bcd50fd1c7c0dce1f09ca42aa074d2c.jpg

第三步 创建bar执行上下文:

9daa570572aeb9d9a773188ef4c934e.jpg

我们只看这个图,得出的输出结果好像就是Alo,其实不然,我们这里要引入一个outer的概念

outer

outer属性其实就是作用域链

每个执行上下文的变量环境当中都会包含一个outer,指向的是它的外层作用域(由于全局作用域就是最外层了,所以它的outer为null)

在上面那段代码中,bar里的outer其实指的是全局,而不是foo

作用域链并不是在调用栈中从上向下查找,而是当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里

词法作用域:在函数定义时所在的作用域,大白话讲就是你把那个单词写在哪里面就属于哪

image.png

所以关键就是理解outer这个概念,这样我们就能明白为什么输出的是Zack而不是Alo啦

结语

以上就是本篇文章全部内容,希望能帮助读者深入理解JavaScript执行机制,包括变量提升、调用栈、作用域链以及栈溢出等核心概念。感谢您的阅读!