共计 3096 个字符,预计需要花费 8 分钟才能阅读完成。
随着对 js 一步步地深入,预编译——神奇的 js 规则出现了。预编译有函数有的预编译和全局的预编译,相信在读完这篇文章后,以后在面试官恶心你的时候,可以从容应对。
我们来举个
var a=1
console.log(a);
function foo(){
var a=1;
console.log(a);
}
foo()
这两段代码输出值都是 1,
那下面这段呢
var a = 2;
function foo(a){
var a = 1;
function a(){}
console.log(a);
}
foo(3)
这一段就要让你思考了
揭晓答案
所以,函数传参,内部声明,赋值调用应该谁先谁后呢
接下来就来解析这个魔术了
声明提升
在 js 里声明提升遵循下面两个规则
- 变量声明,声明提升
- 函数声明,整体提升
想象一下,JavaScript 引擎在执行任何代码之前,会先做一次全面的“彩排”,这就是所谓的“声明提升”(Hoisting)。在这个阶段,它会扫描整个剧本(即代码),寻找所有的变量声明和函数声明,并将它们“提”到当前作用域的最顶部,但需要注意的是,只有声明会被提升,初始化操作依然保持原位。
变量声明提升
无论是使用 var
、let
还是 const
(注意let
和const
有块级作用域,行为稍有不同),其名称会被提升至作用域顶部,值默认为undefined
。这意味着你可以在声明之前访问这些变量,尽管它们此时还未被赋值。例如上面给的代码
var a
console.log(a);// 输出 undefined
a=1
console.log(a);// 输出1
函数声明提升
整个函数体都会被提升,这意味着你可以在声明之前调用函数,而不会遇到引用错误。这种机制允许了函数的自我调用和递归调用成为可能。
foo()// 先调用函数
function foo(){
var a=1;
console.log(a);// 输出 1
}
function foo(){
var a=1;
console.log(a);// 输出 1
}
foo()// 后调用函数
在 js 中,每当运行一个程序时,就会按一下步骤解析代码
1. 创建函数的执行上下文对象 AO {Activation Object}
2. 找形参和变量声明,将形参和变量名作为 AO 的属性,值为 undefined
3. 将实参和形参统一
4. 在函数体内找函数声明,将函数名作为 AO 的属性名,值赋予函数体
我们就用下面代码作分析
function fn(a){
console.log(a);
var a =123;
console.log(a);
function a(){}
console.log(a);
var b = function(){}
console.log(b);
function d(){}
var d = a
console.log(d);
}
fn(1);
创建 AO
找形参和变量声明为 AO 的属性, 值为 undefined
AO{
a:undefined,
b:undefined,
d:undefined,
}
统一形参和实参, 函数声明提升调用函数,将 1 赋值给 a
AO 中的变量更新
AO{
a:1,
b:undefined,
d:undefined,
}
在函数体内找函数声明,将函数名作为 AO 的属性名,值赋予函数体
function fn(a){
console.log(a);
var a =123;
console.log(a);
function a(){}
console.log(a);
var b = function(){}
console.log(b);
function d(){}
var d = a
console.log(d);
}
fn(1);
AO 更新
AO{
a:function a(){},
b:undefined,
d:function d(){},
}
最后开始运行,根据 AO 中的值来按顺序输出
function fn(a){
console.log(a);// 输出 function a(){}
var a =123;
console.log(a);// 上一条代码把 123 赋值给了 a,输出 123
function a(){}
console.log(a);// 输出 123
var b = function(){}
console.log(b);//function(){}函数体赋值给了 b,输出 function(){}
function d(){}
var d = a // 把 a 的值 123 赋给 d
console.log(d);// 输出 123
}
fn(1);
最后 AO 的变化如下
AO:{
a:undefined 1 function a(){} 123,
b:undefined function b(){},
d:undefined function d(){} 123,
a:function(){}
}
这就是函数的预编译
和函数预编译比全局的预编译优先级更高,在全局作用域中,预编译同样默默进行,只是这里的主角换成了“全局执行上下文对象”GO(Global Object)。这个对象代表了整个 JavaScript 程序的顶级作用域。
其过程分三步
- 创建全局执行上下文对象 GO
- 找变量声明,变量名作为 GO 的属性名,值为 undefined
- 在全局找函数声明,函数名作为 GO 的属性名,值为函数体
了解了函数预编译,全局预编译就很容易理解了
var global = 100
function fn(){
console.log(global);
}
fu()
在全局中创建 GO 对象, 找变量声明,值为 undefined,在全局找函数声明值为函数体
GO{
global:undefined,
fu():function fun(){}
}
下一步就是创建 AO 对象,执行函数预编译,由于函数中没有变量,则为没有属性,global 从全局中找值,输出 100
GO{
global:undefined,
fu():function fun(){}
}
var global = 100
function fn(){
console.log(global);//100
}
AO{
}
fu()
如果在函数中添加 global 变量
global = 100
function fu(){
console.log(global);
global = 200
console.log(global);
var global = 300
}
fu()
console.log(global);
var global;
那么 GO 应该为
GO:{
global:undefined 100 ,
fu():function fn(){},
}
AO 应该是
AO:{
global:undefined 200 300,
}
所以最后的代码应该是
// GO:{
// global:undefined 100 ,
// fu():function fn(){},
// }
global = 100
function fu(){
console.log(global);// 输出 undefined
global = 200
console.log(global);// 输出 200
var global = 300
}
// AO:{
// global:undefined 200 300,
// }
fn()
console.log(global);// 输出 100
var global;
最后来对 js 中预编译的方法做总结分析
在 js 预编译中
系统会按照预编译的顺序,为程序开辟空间,先全局预编译入栈,后函数预编译入栈,执行完后出栈,最后完成执行,这就可以保证程序的安全性,不会进入死循环的状态
重点强调,函数执行上下文要在全局上下文中寻找参数时,必须先经过词法环境,才能到达变量环境
就如下图所示
这就是 js 中的神奇规则——预编译,在许多面试中,HR 经常会出以上毫无观感却能运行的恶心代码,只要对 js 的运行规律了如指掌,就可以从容应对这种问题,并且能够深入本质,就可以在同一批面试里超越 90% 的面试者,成为的最后赢家。