likes
comments
collection
share

JavaScript执行前的秘书——预编译

作者站长头像
站长
· 阅读数 29

前言

随着对js一步步地深入,预编译——神奇的js规则出现了。预编译有函数有的预编译和全局的预编译,相信在读完这篇文章后,以后在面试官恶心你的时候,可以从容应对。

我们来举个JavaScript执行前的秘书——预编译

var a=1
console.log(a);
function foo(){
    var a=1;
    console.log(a);
}
foo()

这两段代码输出值都是1, 那下面这段呢

var a = 2;
function foo(a){
    var a = 1;
    function a(){}
    console.log(a);
}
foo(3)

这一段就要让你思考了 揭晓答案

JavaScript执行前的秘书——预编译

所以,函数传参,内部声明,赋值调用应该谁先谁后呢

接下来就来解析这个魔术了

JavaScrip 中的预编译

声明提升

在js里声明提升遵循下面两个规则

  • 变量声明,声明提升
  • 函数声明,整体提升

想象一下,JavaScript引擎在执行任何代码之前,会先做一次全面的“彩排”,这就是所谓的“声明提升”(Hoisting)。在这个阶段,它会扫描整个剧本(即代码),寻找所有的变量声明和函数声明,并将它们“提”到当前作用域的最顶部,但需要注意的是,只有声明会被提升,初始化操作依然保持原位。

变量声明提升

无论是使用varlet还是const(注意letconst有块级作用域,行为稍有不同),其名称会被提升至作用域顶部,值默认为undefined。这意味着你可以在声明之前访问这些变量,尽管它们此时还未被赋值。例如上面给的代码

var a
console.log(a);//输出undefined
a=1
console.log(a);//输出1

函数声明提升

整个函数体都会被提升,这意味着你可以在声明之前调用函数,而不会遇到引用错误。这种机制允许了函数的自我调用和递归调用成为可能。

foo()//先调用函数

function foo(){
    var a=1;
    console.log(a);//输出1
}
function foo(){
    var a=1;
    console.log(a);//输出1
}
foo()//后调用函数

函数中的预编译

在js中,每当运行一个程序时,就会按一下步骤解析代码

1.创建函数的执行上下文对象 AO {Activation Object} 2.找形参和变量声明,将形参和变量名作为AO的属性,值为undefined 3.将实参和形参统一 4.在函数体内找函数声明,将函数名作为AO的属性名,值赋予函数体

我们就用下面代码作分析

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);

创建AO 找形参和变量声明为AO的属性,值为undefined

AO{
    a:undefined,
    b:undefined,
    d:undefined,
}

统一形参和实参,函数声明提升调用函数,将1赋值给a AO中的变量更新

AO{
    a:1,
    b:undefined,
    d:undefined,
}

在函数体内找函数声明,将函数名作为AO的属性名,值赋予函数体

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);

AO更新

AO{
    a:function a(){},
    b:undefined,
    d:function d(){},
}

最后开始运行,根据AO中的值来按顺序输出

function fn(a){
    console.log(a);//输出function a(){}
    var a =123;
    console.log(a);//上一条代码把123赋值给了a,输出123
    function a(){}
    console.log(a);//输出123
    var b = function(){}
    console.log(b);//function(){}函数体赋值给了b,输出function(){}
    function d(){}
    var d = a     //把a的值123赋给d
    console.log(d);//输出123
}
fn(1);

最后AO的变化如下

AO:{
    a:undefined 1  function a(){} 123,
    b:undefined function b(){},
    d:undefined function d(){} 123,
    a:function(){}
}

这就是函数的预编译

全局的预编译

和函数预编译比全局的预编译优先级更高,在全局作用域中,预编译同样默默进行,只是这里的主角换成了“全局执行上下文对象”GO(Global Object)。这个对象代表了整个JavaScript程序的顶级作用域。

其过程分三步

  1. 创建全局执行上下文对象 GO
  2. 找变量声明,变量名作为GO的属性名,值为undefined
  3. 在全局找函数声明,函数名作为GO的属性名,值为函数体

了解了函数预编译,全局预编译就很容易理解了

var global = 100
function fn(){
    console.log(global);
}
fu()

在全局中创建GO对象,找变量声明,值为undefined,在全局找函数声明值为函数体

GO{
    global:undefined,
    fu():function fun(){}
}

下一步就是创建AO对象,执行函数预编译,由于函数中没有变量,则为没有属性,global从全局中找值,输出100

GO{
    global:undefined
    fu():function fun(){}
}
var global = 100
function fn(){
    console.log(global);//100
}
AO{

}
fu()

如果在函数中添加global变量

global = 100
function fu(){
    console.log(global);
     global = 200
    console.log(global);
    var global = 300
 }
 fu()
 console.log(global);
 var global;

那么GO应该为

GO:{
    global:undefined 100 ,
    fu():function fn(){},
}

AO应该是

AO:{
    global:undefined 200 300,
}

所以最后的代码应该是

// GO:{
//     global:undefined 100 ,
//     fu():function fn(){},
// }
global = 100
function fu(){
    console.log(global);//输出undefined
    global = 200
    console.log(global);//输出200
    var global = 300
}
// AO:{
//     global:undefined 200 300,
// }
fn()
console.log(global);//输出100
var global;

调用栈

最后来对js中预编译的方法做总结分析 在js预编译中 系统会按照预编译的顺序,为程序开辟空间,先全局预编译入栈,后函数预编译入栈,执行完后出栈,最后完成执行,这就可以保证程序的安全性,不会进入死循环的状态 重点强调,函数执行上下文要在全局上下文中寻找参数时,必须先经过词法环境,才能到达变量环境 就如下图所示

JavaScript执行前的秘书——预编译

JavaScript执行前的秘书——预编译

结语

这就是js中的神奇规则——预编译,在许多面试中,HR经常会出以上毫无观感却能运行的恶心代码,只要对js的运行规律了如指掌,就可以从容应对这种问题,并且能够深入本质,就可以在同一批面试里超越90%的面试者,成为的最后赢家。

转载自:https://juejin.cn/post/7372404987701985320
评论
请登录