轻松get——js预编译,作用域以及作用域链

预编译 作用域 作用域链

前言

对于很多即将要面试的毕业生来说,对预编译不同环境下是如何进行的这个问题可能模糊不清,作用域作用域链的概念也可能有些抽象,可是这些也是很多面试官会问到的,为拿下offer不可疏忽,这篇文章带你轻松拿下!😍😍😍

正文

一、预编译

我们知道,编译发生在代码执行,编译过程主要是以下三步:
1.词法分析(词法单元) 2.语法解析 (抽象语法树) 3.代码生成

预编译的执行情况有以下三类:

1、发生在代码执行前

1)声明提升

在编译时,将变量的声明提升到当前作用域顶端(这针对于变量提升,值不会提升)

举个例子:

 console.log(a)   //打印结果:undefined
 var a=1
 var a            //变量a的声明提升
 console.log(a);  //打印结果:undefined
 a=1              //值不会提升

这两种方式等同,打印结果均为undefined,与Refference:b is not defined不同。 补充一下:
undefined是变量已声明,但未赋值。
ReferenceError: xxx is not defined 是xxx未声明,使用了不存在的变量。

2)整体提升

这针对于函数提升,包括函数体整个提升,举个例子:

fun();
function fun(){
   var a=1
   console.log(a)  //打印结果为1
}
function fun(){    //函数整体提升
   var a=1
   console.log(a)  //打印结果为1
}
fun();

这两种方式等同,打印结果均为1,预编译使函数fun整体提升。

2、发生在函数执行之前(四部曲)(函数体内预编译)

1)创建一个AO对象
2)找形参变量声明,将变量声明和形参作为AO的属性名,值为undefined
3)将实参形参统一。
4)在函数体内函数声明,将函数名作为AO对象的属性名,值赋予函数体。

举个例子:

function fun(a){
    var a=1
    var a=2
    function b(){}
    var b=a
    a=function c(){}
    console.log(a);
    c=b
    console.log(c);
}
fun(2)

// AO:{
//     a:undefined 2,    1 2 function c(){}
//     b:undefined function b(){},     2
//     c:function c(){} ,    2
// }

函数体内预编译过程:
第一步,看到fun()的那一刻开始创建AO对象。 AO:{}
第二步,找形参和变量声明,并把值赋为undefined。 AO:{a:undefined b:undefined}
第三步,将实参和形参统一。 AO:{a:undefined 2 b:undefined}
第四步,将函数体内找函数声明。 AO:{a:undefined 2 b:undefined function b(){} c:function c(){}}

函数体内预编译结束,进入执行阶段:
第一步,a赋值为1。 AO:{a:undefined 2,1 b:undefined function b(){} c:function c(){}}
第二步,a赋值为2。 AO:{a:undefined 2,1 2 b:undefined function b(){} c:function c(){}}
第三步,a赋值给b。 AO:{a:undefined 2,1 2 b:undefined function b(){},2 c:function c(){}}
第四步,function c(){}赋给a,打印a结果为function c(){}。 AO:{a:undefined 2,1 2 function c(){} b:undefined function b(){},2 c:function c(){}}
第五步,b赋值给c,打印c结果为2。 AO:{a:undefined 2,1 2 function c(){} b:undefined function b(){},2 c:function c(){},2}

3、发生在全局(全局预编译)

1)创建一个GO对象
2)找变量声明,将变量声明作为GO的属性名,值为undefined
3)在全局函数声明,将函数名作为GO对象的属性名,值赋予函数体

举个例子:

global=100
function fn(){
  console.log(global);//打印结果:undefined
  global=200
  console.log(global);//打印结果:200
  var global=300
}
fn()
var global

// GO:{
//     global:undefined 100
//     fn:function fn(){}
// }
// AO:{
//     global:undefined,    200
// }

全局预编译过程:
第一步,开始全局下的预编译,创建GO对象。 GO:{}
第二步,找变量声明,并把值赋为undefined。 GO:{global:undefined}
第三步,在全局找函数声明。 GO:{global:undefined fn:function fn(){}}

全局预编译结束,进入执行阶段:
第一步,global赋为100。 GO:{global:undefined 100 fn:function fn(){}}
第二步,看到fn(),对fn函数进行函数体内预编译,创建AO对象。 AO:{}
第三步,找形参和变量声明,并把值赋为undefined。AO:{global:undefined}
第四步,将实参和形参统一,在函数体内找函数声明,这里都找不到。 AO:{global:undefined}
第五步,函数体内预编译结束,执行函数体,打印global为undefined之后给global赋为200。 AO:{global:undefined,200}
第六步,打印global值为200,将300赋值给global。 AO:{global:undefined,200 300}

二、作用域

作用域:作用域也叫运行期上下文,当函数执行时会创建一个称为执行期上下文的内部对象,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的上下文都是独一无二的,所以多次调用一个函数会多次创建多个执行期上下文,当函数执行完毕,它产生的执行期上下文会被销毁

作用域分为三种:函数作用域,全局作用域,块级作用域

1、函数作用域 和 全局作用域

举个例子:

function fun(a){   
    console.log(a+b);
}
var b=2
fun(2);

来张图片理解理解:

轻松get——js预编译,作用域以及作用域链

当我们在终端打印,结果是4:

轻松get——js预编译,作用域以及作用域链
我们可以看到打印的是a+b的值,这就说明函数fun()内能访问到全局变量b的值,也就是说,内层作用域是能访问外层作用域的

那么外层作用域能访问内层作用域吗?
答案是:不能!,大家可以尝试一下在fun()函数中定义一个变量并在函数外部打印这个变量,发现并不能访问到。

2、块级作用域

ES6给我们带来了块级作用域,块级作用域由{}包括,创建方式由letconst声明,举个例子:

if(1){
    var a=1  
}
console.log(a);//打印结果为:1
if(1){
    let a=1  
}
console.log(a);//打印结果为:a is not defined 
if(1){
    const a=1  
}
console.log(a);//打印结果为:a is not defined    

可以看到函数体中用varlet声明变量会导致结果的不同,这是因为ES6产生之前语句是没有作用域的,ES6的到来可以产生了块级作用域,这样让外部无法访问到内部块级作用域

constlet共同点是都能为花括号{}带来块级作用域,不同点是const声明的是常量,是不允许被改变的。

三、作用域链

作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链

[[scope]]:只提供给引擎访问的属性,其中存储了执行期上下文的集合

有点抽象,举个例子:

function a(){
    var a=1
    console.log(a);
}
var glob=100
a()
//a 定义   a.[[scope]]--> 0:GO:{}
//a 执行   a.[[scope]]--> 0:AO:{} 1:GO:{}

我们知道,当我们定义一个函数时,这个函数就拥有了scope属性

这个例子中,当函数a被定义时就有了scope属性,它记录的是a的执行上下文,而函数a中有内容,也就有了全局执行上下文的对象了,我们把它称为GO(Global Object)。当函数a被执行时,就带来了函数a自己的执行上下文的创建,我们把它称为AO(Activation Object)。类似的原理,AO来到了GO的位置,GO向后挪了一位,这也就形成了一个集合

当要访问变量a时,就可以顺着函数a的作用域链开始查找,先从AO中寻找,找不到再一层一层向外寻找。这样就能解释执行打印语句时为什么能访问到a的值了。

还是抽象?图示法呈上!

轻松get——js预编译,作用域以及作用域链

最后

以上就是js预编译,作用域以及作用域链的介绍,看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
文章可能有一些错误,欢迎评论指出,也欢迎一起讨论。

原文链接:https://juejin.cn/post/7217057805782270009 作者:zt_ever

(0)
上一篇 2023年4月2日 下午4:26
下一篇 2023年4月2日 下午4:36

相关推荐

发表评论

登录后才能评论