【经典面试题】JS作用域、作用域链以及预编译

1,667 阅读7分钟

前言

在腾讯字节等其他大厂的面试中,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

上例中,bs都是函数内部定义的变量,他们的作用域也就仅限于函数内部,全局作用域中不会访问到。

(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的值。

在这个例子中,它们的关系示意图如下:

image.png

再看一个例子:

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]] --->函数作用域属性

用图来解析下这个过程:

image.png

image.png

三、预编译

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
// }

由四部曲我们可以解析如下:

  1. 创建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

}

四、结语

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,写作不易,持续输出的背后是无数个日夜的积累,您的点赞是持续写作的动力,感谢支持。