轻松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);
来张图片理解理解:
当我们在终端打印,结果是4:
我们可以看到打印的是a+b的值,这就说明函数fun()内能访问到全局变量b的值,也就是说,内层作用域是能访问外层作用域的。
那么外层作用域能访问内层作用域吗? 答案是:不能!,大家可以尝试一下在fun()函数中定义一个变量并在函数外部打印这个变量,发现并不能访问到。
2、块级作用域
ES6给我们带来了块级作用域,块级作用域由{}包括,创建方式由let和const声明,举个例子:
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
可以看到函数体中用var和let声明变量会导致结果的不同,这是因为ES6产生之前语句是没有作用域的,ES6的到来可以产生了块级作用域,这样让外部无法访问到内部块级作用域。
const和let的共同点是都能为花括号{}带来块级作用域,不同点是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的值了。
还是抽象?图示法呈上!
最后
以上就是js预编译,作用域以及作用域链的介绍,看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~ 文章可能有一些错误,欢迎评论指出,也欢迎一起讨论。
转载自:https://juejin.cn/post/7217057805782270009