大厂前端面试题 var x = 100;consolelo

前言

大家面试的时候,会不会碰到这么一道题目。

1
2
3
4
5
php复制代码var global = 100;
function fn(){
console.log(global);
}
fn();

面试官问,这段代码打印的是什么?心中暗喜,这不简简单单打印的就是100嘛,这面试官问的也特简单了吧,于是脱口而出100,然后面试官继续问,为什么是100?然后你仔细一思索,于是这么回答到:因为在全局域中定义了一个变量global,函数作用域能访问到全局作用域的变量,所以打印的是100。于是面试官说,你还是回家等消息吧。然后你百思不得其解,我回答的没错呀,为什么面试官叫我回家呢?你回答的确实没错,但是还是那句话,你回答的还不够优秀,没有答到面试官的心窝窝里面去。

接下来就由蘑菇头来解答一下这道简单但是其实并不简单的面试题。

首先上述回答一点问题都没有,但是我们要知道,大厂考这么简单的一道题,那么他想考察的知识点肯定不会这么简单,你以为他想考察的是作用域,其实,面试官真正想要考察的是编译原理。他想知道你对这么一段代码的理解程度。我们从编译原理的角度来分析这里为什么打印的是100。

预编译

我们先清楚一个概念,什么是预编译。代码在执行前需要进行编译操作,用于确定变量的作用域,提高运行效率。预编译的过程包括词法分析、语法分析、语义分析、代码生成等步骤,用于确定各种代码之间的关联。

所以上面这段代码会执行一次预编译的过程。在此之前,我们先了解一下v8是怎么理解一个函数的,比如下面这个函数

1
2
3
scss复制代码function foo(){//函数的声明
}
foo();//函数的调用

foo函数,在JavaScript中它是一个对象,按道理来说对象身上就会有一些属性和方法,所以foo函数身上也会有一些属性和方法如:

1
2
3
4
lua复制代码foo.name //"foo";
foo.length //0; 表示函数参数的长度
foo.prototype //undefined; 表示函数的原型
foo.[[scope]] //函数的作用域属性 我们无法访问 v8引擎内部属性 -隐式属性

这个作用域属性也就是我们现在要聊的。每个函数都会有一个作用域属性,在 JavaScript 中,函数的作用域属性是指在函数创建时,除了开辟内存和赋值外,系统还会为该函数设置一个作用域。当前函数的作用域等于当前函数创建时所在的上下文。这个作用域也叫函数AO对象,AO 对象(Activation Object)是函数执行前的一瞬间生成的一个对象。它主要用于存储函数的上下文信息,包括函数的参数、局部变量、内部函数等,当函数调用结束后,AO对象会被销毁,当再次调用时,AO对象会再次创建。里面会用键值对的形式存储有效标识符。在全局作用域下会创建GO对象。如果您还不了解作用域的话可以移步这篇文章:作用域你真的了解吗? - 掘金 (juejin.cn)

代码是一行一行执行的,当v8扫描到函数的声明时,并不会进行编译,当碰到函数的调用时,v8才会进行编译,他会先找到这个函数的声明,并且创建AO对象。

OK,当我们了解这些之后,我们可以完整的分析一下上述代码v8是如何理解的。

1
2
3
4
5
php复制代码var global = 100;
function fn(){
console.log(global);
}
fn();

首先,v8会先创建GO对象,然后在全局作用域下找所有的变量声明,将变量名当做key,值为undefined存入GO对象中,所以现在GO对象中有一个global属性,然后在全局中找函数声明,将函数名作为key,值为函数体存入,现在GO对象中有global和fn。执行语句,将global的值更新为100,然后开始执行函数体fn(),在执行fn()函数体之前,会进行编译,v8会创建AO对象,然后在函数作用域下找所有的形参和变量名,将形参和变量名作为key,值为undefined存入AO对象中,然后就是形参和实参统一,将AO对象形参key的值更新为实参的值,然后在函数体内找函数声明,将函数名作为AO的属性名值为该函数体,并且该AO对象指向会指向GO对象。这个函数编译完成,执行fn,打印global,会先在当前作用域AO中查找,如果没有找到,则会在上一级作用域中查找,直到全局作用域中查找。如果全局作用域中也没有找到,则会报错,。在全局中找到一个global值为100,所以打印100。如果该函数体fn内还有函数执行,会再次执行上述过程,在执行语句调用前编译并且创建新的AO对象并且指向上一个AO对象也就是fn的作用域。

1
2
3
4
5
6
7
8
javascript复制代码//GO AO 对象更新过程
GO:{
global:undefined --> 100,
fn:function(){}
}
AO:{
//找不到global变量,去上一级GO作用域找
}

当我们知道V8的编译原理之后,如果碰到以下问题,那我们就能神挡杀神,佛挡杀佛。

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码function fn(a){
console.log(a);//function a(){}
var a = 123;
console.log(a);//123
function a(){}
console.log(a);//123
var b = function(){}
console.log(b);//function(){}
function c(){}
var c = a;
console.log(c);//123
}
fn(1);

问打印的是什么?把自己想像成v8,看看他是怎么理解的。

1
2
3
4
5
6
7
8
9
javascript复制代码//赋值过程
GO{
fn:fn(){}
}
AO{
a:undefined --> 1 --> function a(){} -->123
b:undefined --> function(){}
c:undefined -->function c(){} -->123
}

总结

今天我们学习了v8的编译过程,主要可以分成两种:

1、发生在全局

a.创建GO对象

b.找变量声明,将变量名key和值undefined存入GO对象中

c.在全局找函数声明,将函数名作为GO的属性名,值为该函数体

d.执行函数体,函数体中使用变量时,会先在当前作用域中查找,如果没有找到,则会在上一级作用域中查找,直到全局作用域中查找。如果全局作用域中也没有找到,则会报错。

全局代码先编译,然后执行,在执行过程中可能会碰到其他函数调用,函数调用时,会先编译该函数,然后执行。

2、发生在函数体内

a.创建函数作用域AO对象,找形参和变量名,将变量名key和值undefined存入AO对象中

b.形参和实参统一

c.在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%