在JavaScript中,执行上下文和作用域链是理解代码执行流程、变量查找机制以及函数行为的核心概念。本文将深入探讨声明提升、函数中的预编译过程、全局预编译以及调用栈的工作原理,帮助您构建对JavaScript运行机制的深刻理解。
先有鸡还是先有蛋
我们先来看看以下代码,你认为会输出什么呢?
a = 2;
var a;
console.log(a);
有的人会认为是undefined,因为 var a 声明在a = 2后面,他们认为变量被重新赋值了,但真正的输出结果是2,你对了吗?接下来,我们再看到另外一段代码
consol.log(a);
var a = 2;
这里肯定有的人认为,a在使用之前没有声明,会抛出ReferenceError异常;还有人认为会和上面一样输出2,但不幸的是这俩种答案都是不对的,这里会输出undefined
那么这到底发生了什么呢?到底是蛋(声明)在前,还是鸡(赋值)在前?
声明提升(Hoisting)
首先,我们要知道什么是声明提升
声明提升是JavaScript中一个独特的行为,它涉及到变量声明和函数声明在代码执行前被“提前”到当前作用域的顶部。这一机制使得我们可以在声明之前使用变量和函数。
- 变量声明提升:使用
var声明的变量会被提升至其所在作用域的顶部,但其赋值操作不会提升。因此,在变量声明之前的代码访问该变量时,其值为undefined。 - 函数声明提升:整个函数声明(包括名称和函数体)都会被提升到作用域的顶部。这意味着你可以在函数声明之前调用该函数。
看完以上内容,我们再重新看到我们之前的代码,就可以写出他们的运行的流程了
第一段代码会以如下形式处理:
var a;
a = 2; //由于变量声明提升,var a声明会被提升到作用域的顶部,所以var a先运行
console.log(a);
再来看看第二段代码,他们会以如下形式处理:
var a;
console.log(a);
a = 2; //当你看到var a = 2时,它其实是俩个声明,var a和a = 2,然后变量声明提升,var a被提到作用域的顶部
所以在这里我们得到前面的答案,先有蛋(声明)后有鸡(赋值)
函数中的预编译
接下来我们在看到以下代码,你认为会怎么样输出呢?
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
console.log(a);
var b = function () {} // 函数表达式
console.log(b);
function d() {}
var d = a
console.log(d);
}
fn(1)
现在公布答案
[Function: a]
123
123
[Function: b]
123
你答对了吗?这里我们就需要了解当一个函数被运行时,会进行什么样的步骤了。
当一个函数被调用时,会进行以下预编译步骤:
- 创建激活对象(AO, Activation Object) :为该函数创建一个新的执行上下文,即激活对象,用于存储函数执行期间的所有局部变量、参数和内部函数。
- 初始化形参和变量声明:将函数的形参和函数内部的变量声明作为激活对象的属性,初始值均为
undefined。这一步确保了所有声明都在函数体执行前可用。 - 实参与形参绑定:如果函数调用时提供了实参,这些实参会与对应的形参进行匹配并赋值。
- 处理函数声明:在函数体中查找函数声明,并将其添加为激活对象的属性,覆盖之前可能因变量声明提升产生的同名属性。此时,函数声明的值为实际的函数体。
全局的预编译
我们依旧看一段代码
global = 100
function fn (){
console.log(global);
global = 200
console.log(global);
var global = 300
}
fn()
var global;
下面是输出的结果:
undefined
200
这里我们需要了解全局的预编译
对于全局执行上下文,预编译过程如下:
- 创建全局执行上下文对象(GO, Global Object) :在浏览器环境中,这通常是
window对象;在Node.js环境中,则是global对象。 - 变量声明:将所有使用
var声明的全局变量作为全局对象的属性,初始值为undefined。 - 函数声明:与函数内的处理类似,全局函数声明也会被添加到全局对象作为属性,其值为函数体。这意味着你可以直接通过全局对象访问这些函数。
调用栈(Call Stack)
调用栈是一种数据结构,用于跟踪函数的调用顺序和状态。在JavaScript中,每当一个函数被调用时,都会创建一个新的执行上下文并压入调用栈顶。函数执行完毕后,其执行上下文会从栈顶弹出,控制权返回到调用者。
- 工作原理:调用栈确保了函数的执行顺序,遵循“先进后出”的原则。它帮助JavaScript引擎记住当前执行的位置和上下文,以及哪些函数需要返回去继续执行。
- 应用场景:理解调用栈对于调试、特别是处理递归调用或理解某些错误(如堆栈溢出错误)至关重要。
总结
总之,声明提升、预编译过程以及调用栈共同构成了JavaScript程序执行的基础框架。通过深入理解这些机制,开发者可以更有效地编写、调试和优化代码,避免潜在的陷阱,提高程序的可读性和健壮性。
好的,这次的内容就分享到这了,如果小友觉得整的还不错的,可以留下一个小小的赞帮助俺找回自己的脑子,谢谢啦!!!