前言
在腾讯字节等其他大厂的面试中,JavaScript作用域、作用域链以及预编译是经常会被问到的问题。本文我会尽我所能用最简单的方式来解释作用域、作用域链以及预编译,希望大家有所收获!
一、作用域(Scope)
1.1、作用域定义
什么是作用域: 变量与函数的可访问范围,控制着变量与函数的可见性和生命周期。我们有时把作用域称作为运行期上下文:当函数执行时会创建一个称为执行期的上下文的内部对象,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的上下文都是独一无二的,所以多次调用一个函数会创建多个执行期上下文,当函数执行完毕,它产生的执行期上下文会被销毁。 我们看一个例子:
function foo(){
var a=1
console.log(a);//1
}
foo()
console.log(a);//ReferenceError: a is not defined
可以看到foo函数执行的时候会打印出a的值1,而在函数外打印的a会报错,原因在于最外层——全局作用域没有定义a变量(后面我们会了解到全局作用域)
再来看一段代码:
var a=1
function foo(){
console.log(a);//1
}
foo()
可以看到在输出a的时候,自己函数内部没有找到变量a,那么就在外层的全局中查找,找到了就停止查找并输出了。
所以,通俗的讲,作用域就是查找变量的地方。在某函数中找到该变量,就可以说在该函数作用域中找到了该变量;在全局中找到该变量,就可以说在全局作用域中找到了该变量。
1.2、作用域类型
(1)全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般有以下几种情况:
- 最外层函数和在最外层函数外面定义的变量,例如:
var a=1//全局变量
function foo(){
var b=2//局部变量
console.log(a);//输出1 全局变量
console.log(b);//输出2 局部变量
}
foo()
console.log(a);//输出1 全局变量
console.log(foo);//输出 [Function: foo]
console.log(b);//ReferenceError: b is not defined
- 所有末定义直接赋值的变量自动声明为拥有全局作用域,例如:
function foo(){
var a=2
b=3//全局变量
console.log(a);//2
}
foo()
console.log(b);//输出3
console.log(a);//ReferenceError: a is not defined
变量b拥有全局作用域,而变量a在函数外无法访问
- 所有window对象的属性拥有全局作用域,一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等。
(2)函数作用域
在函数内部定义的变量,拥有函数作用域。
var a=1//a 全局变量
function foo(){
var b=2 //b 局部变量
console.log(b);
}
function bar(s){
console.log(s);//形参s 局部变量
}
foo() //输出2
bar(a) //输出1
console.log(b);//ReferenceError: b is not defined
console.log(s);//ReferenceError: b is not defined
上例中,b和s都是函数内部定义的变量,他们的作用域也就仅限于函数内部,全局作用域中不会访问到。
(3)块级作用域
使用let或const声明的变量,如果被一个大括号{}括住,那么这个大括号括住的变量就形成了一个块级作用域。所声明的变量在指定块的作用域外无法被访问。
if(true){
let a=1
console.log(a);//输出1
}
console.log(a);//ReferenceError: a is not defined
可以看到,块级作用中定义的变量只在当前块中生效,这和函数作用域类似。
注意:
内存作用域是能够访问外层作用域的,反之则不可以
函数调用时会执行上下文,创建一个对象
AO:{}(表示函数作用域对象)每个函数都会有自己的函数作用域属性
[[scope]]这个隐式属性:只供给引擎访问的属性,其中存储了执行期上下文的集合
二、作用域链(Scope Chain)
指的是作用查找的线路,即[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式链接叫做作用域链。
通俗的讲,当所需要的变量在所在的作用域中查找不到的时候,它会一层一层向上查找,直到找到全局作用域还没有找到的时候,就会放弃查找。这种一层一层的关系,就是作用域链。
例如:
var a=1
function fn(){
var a=2
function fun(){
console.log(a);//2
}
fun()
}
fn()
输出a时由于fun函数内没有定义变量a,所以往上一层查找变量a,结果在上一层的fn函数内找到了变量a,输出a的值。
在这个例子中,它们的关系示意图如下:
再看一个例子:
function a(){
function b(){
var b=2
}
var a=1
b()
console.log(a);
}
var glob=100
var d=5
a()
//a 定义 a.[[scope]]--->0:GO{}
//a 执行 a.[[scope]]--->0:AO{} 1:GO{} //每次创建出来的对象都放在前面
注意:
GO:{} --->指的是全局对象
AO:{} --->包含了函数执行期上下文
[[scope]] --->函数作用域属性
用图来解析下这个过程:
三、预编译
3.1、预编译概述
一般来说,编译的步骤分为以下三部分:
- 词法分析(词法单元)
- 语法解析(抽象语法树)
- 代码生成
注意: JS编译发生在代码执行之前
首先我们得知道下声明提升:
在编译时将变量的声明,提升到当前作用域的顶端
函数声明整体提升
以下示例:
foo()
function foo(){
console.log(a);//undefined
var a=1
}
var b=2
//foo()函数执行前进行编译,编译时相当于如下
var b
function foo(){
var a
console.log(a);//undefined
a=1
}
foo()
b=2
3.2、函数执行之前的预编译(四部曲)
1.创建一个AO对象
2.找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined
3.将实参和形参统一
4.在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
案例如下:
function foo(a){
var a=1
var a=2
function b(){}
var b=a
a=function c(){}
console.log(a); //[Function: c]
c=b
console.log(c);//2
}
foo(2)
// AO:{
// a:undefined 2 1 2 function c(){}
// b:undefined function b(){} 2
// c: function c(){} 2
// }
由四部曲我们可以解析如下:
- 创建AO对象
AO{
//空对象
}
2.找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined
AO{
a:undefined
b:undefined
}
3.将实参和形参统一
AO{
a:2
b:undefined
}
4.在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
AO{
a:2
b:function b(){}
c:function c(){}
}
最后,下面是完整的预编译过程
AO:{
a:undefined --> 2 --> 1 --> 2 --> function c(){}
b:undefined --> function b(){} --> 2
c: function c(){} --> 2
}
3.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()
分析如下:
1.创建一个GO对象
GO:{
//空对象
}
2.找变量声明,将变量声明作为GO的属性名,值为undefined
GO: {
global: undefined
}
3.在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
GO: {
global: undefined
fn: function() { }
}
此外,函数fn的声明有属于自己的AO,继续使用四部曲步骤即可
AO:{
global:undefined --> 200 --> 300
}
四、结语
本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,写作不易,持续输出的背后是无数个日夜的积累,您的点赞是持续写作的动力,感谢支持。