JS运行三部曲
JS是解释型语言,代码的执行是有个过程的:语法分析、预编译和解释执行。
所以说,解释一行,执行一行,是指“解释执行”这最后一步。 前面还有俩步骤。
语法分析
先扫描一遍代码有没有语法错误、低级错误,这个很好理解。
比如,有没有少写一个大括号之类的。
预编译
预编译就是解决程序执行顺序的。
先看个情况:
function demo(){
console.log("a");
}
demo(); // a
这是一段再普通不过的代码,问你能不能执行?没问题,肯定能执行。
如果,变一变:
demo(); // a
function demo(){
console.log("a");
}
这样还能执行么? 答案是:可以。
再看个情况:
都知道,变量没有声明,不能执行,会报错。
改一下:
var a = 123;
console.log(a); // 123
都知道,这段代码执行起来没问题。
好,变一下:
console.log(a); // undefiend
var a = 123;
还能执行么? 答案是:可以,但结果是 undefined。
两句话
不卖关子了,这是预编译机制的作用,先记住两句话
函数声明:整体提升
变量:声明提升
函数声明:整体提升
函数在预编译时,会被整体提升到代码的最上面,所以不论你把调用写在哪,都在函数声明的下面调用,所以仍然可以执行。
demo(); // a
function demo(){
console.log("a");
}
变量:声明提升
变量也会提升,但提升的只是声明,赋值不会被提升。
var a = 123;
// 等价于
var a;
a = 123;
预编译的时候,会把变量的声明 var a 提升。所以 undefined 的结果也就可以解释了。
console.log(a); // undefiend
var a = 123;
// 等价于
var a;
console.log(a); // undefined
a = 123;
记住以上两句,可以解释代码中的一部分问题了,但要理解预编译的机制,还没这么简单。
来看复杂的:
console.log(a);
function a(a){
var a = 234;
var a = function(){
}
a();
}
var a = 123;
看晕了,不急,要理解预编译,还得理解一些前奏知识点:
预编译前奏
imply global 暗示全局变量。
- 即任何变量,如果未经声明就赋值,此变量就为全局对象(window)所有。
a = 123;
console.log(a); // 123 不报错,未声明就赋值的情况,此变量就是对象 window 所有
// 等价于
window.a = 10;
console.log(window.a); // 123 window 可省略,window 就是全局
function demo(){
var a = b = 123; // 过程:123先赋值给b,后b把值赋给a,但b没有被声明,b是 window 所有
c = 100;
}
demo();
console.log(window.b); // 123 b虽在函数体内,但没有声明就赋值了,所以b是window所有
console.log(window.a); // undefined 因为 a 不是全局变量,所以这里是 undefined
console.log(window.c); // 100 c未声明就赋值了,属于window
- 一切声明的全局变量,全是 window 的属性。window 就是全局的域。
var a = 123; // 在全局声明了变量,这个变量也是 window 对象的属性
// 等价于
window.a = 123;
// 也可以理解为
window{
a:123
}
function demo(){
var b = 123;
}
demo();
console.log(b); // undefined b不是全局变量
解释执行
预编译完之后,就是解释执行。解释执行就是:解释一行,执行一行。
本文的重点还是理解预编译的过程。
预编译过程
预编译发生在执行的前一刻。 大致分为函数体预编译和全局预编译。
函数预编译
函数预编译四步曲:
1. 创建AO对象(Activation Object)
2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined。
3. 将实参值和形参统一
4. 在函数体里面找函数声明,值赋予函数体
不好理解没关系,先看示例和预编译推导过程:
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(){}
}
fn(1);
很多重复的 a ,怎么执行?这里面有“提升”和“覆盖”的问题。执行前,有个预编译的过程,预编译已经把看似的矛盾都“调和”了。下面按四部曲逐步推导。
第一步:创建AO对象
AO对象 Activation Ojbect ,学名叫:执行期上下文,可以理解为作用域。
第二步:找形参和变量声明,将变量和形参名作为AO属性名,值为:undefined
AO{
a: undefined,
b: undefined
}
第三步:将实参和形参统一
AO{
a: 1, // a 被实参 1 覆盖
b: undefined
}
第四步:在函数体内找函数声明,值赋予函数体
注意:函数声明和函数表达式是两回事;
function a(){} // 函数声明
var b = function(){} // 函数表达式
AO{
a: function a(){}, // a 被函数体覆盖
b: undefined,
d: function d(){}
}
到此,AO对象创建完成(预编译四步走完),开始执行函数。
根据 AO 对象存储的数据和执行的语句来推导结果。
function fn(a){
console.log(a); // function a(){}
var a = 123; // 变量a声明已经被提升,这里执行赋值,即 AO.a = 123;
console.log(a); // 123 a 的值从 AO 对象中取值。
function a(){} // 函数声明已经被提升,函数没有执行就跳过不看。
console.log(a); // 123 还是在AO对象中取值。
var b = function(){} // 变量b的声明已经被提升,这里执行赋值,即 AO.b = function(){};
console.log(b); // function(){} 从AO.b中取值。
function d(){}
}
fn(1);
经过以上推导,AO对象变为:
AO{
a: 123,
b: function(){},
d: function d(){}
}
4个输出结果:
// function a(){}
// 123
// 123
// function(){}
控制台输出结果(Chrome版本号:105.0.5195.127):

AO对象,在预编译和执行的过程中,a、b两个属性(变量)的值反复变化。
AO{
a: undefined, // ==> 1 (实参赋值) ==> function a(){} (函数体覆盖) ==> 123 (语句执行:赋值覆盖)
b: undefined // ==> function(){} (语句执行:赋值函数体)
}
通过 a 变量在预编译过程中的变化,总结取值的优先级:函数体 > 实参赋值 > 初始赋值
AO对象贯穿了函数预编译和执行的整个生命周期。
练习一
function demo(a, b) {
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b() { }
function d() { }
console.log(b);
}
demo(1);
第一步:建AO。只要求输出 a 和 b ,那就只写 a 和 b 。
第二步:找变量形参。
AO{
a:undefined,
b:undefined
}
第三步:实参形参统一。
AO{
a:1, // 1 a有实参,赋值实参,覆盖原数据
b:undefined
}
第四步:赋值函数体。
AO{
a:1, // a 没有
b:function b(){} // b有,赋值函数体,覆盖原数据
}
预编译结束,形始执行。
function demo(a, b) {
console.log(a); // 1,从AO对象中取值 AO.a = 1
c = 0;
var c;
a = 3; // 给 a 赋值了, AO.a = 3
b = 2; // 给 b 赋值了, AO.b = 2
console.log(b); // 2, 从AO对象中取值
function b() { } // 函数提升,没执行跳过
function d() { }
console.log(b); // 2, 从AO对象中取值
}
demo(1);
此时的AO对象:
输出结果:
练习二
function demo(a, b) {
console.log(a);
console.log(b);
var b = 234;
console.log(b);
a = 123;
console.log(a);
function a() { }
var a;
b = 456;
var b = function () { }
console.log(a);
console.log(b);
}
demo(1);
预编译过程:在一堆重复的变量名中快速完成赋值。
心算AO:提取变量形参,首先找函数体赋值,其次是实参赋值,最后是undefined
AO{
a:function a(){},
b:undefined
}
执行过程:边执行,边给AO中的属性(变量)赋值。
function demo(a, b) {
console.log(a); // function a(){}
console.log(b); // undefined
var b = 234; // AO.b = 234
console.log(b); // 234
a = 123; // AO.a = 123
console.log(a); // 123
function a() { }
var a;
b = 456; // AO.b = 456
var b = function () { } // AO.b = function (){}
console.log(a); // 123
console.log(b); // function (){}
}
demo(1);
AO结果:
AO{
a:123,
b:function(){}
}
执行结果:
// function a(){}
// undefined
// 234
// 123
// 123
// function(){}
全局预编译
跟函数体内的预编译比,全局预编译没有实参和形参。
- 创建GO对象(Global Object)
- 找变量声明,将变量作为GO属性名,值为undefined。
- 找函数声明,值赋予函数体
先看一个:
console.log(a); // undefined
var a = 123;
再看一个:
console.log(a); // function a(){}
var a = 123;
function a(){}
掌握了基本的提升和优先级后,再写GO对象就简单了。
还记得前面说过的 window 全局对象么? 和这里的GO对象是啥关系?
他俩是一个东西,两名字,存储变量名和值的。
来几个练习吧。
练习一
console.log(test);
function test(test){
console.log(test);
var test = 234;
console.log(test);
function test(){}
}
test(1);
var test = 123;
预编译全局先写GO对象。
GO{
test : function test(){
//.....
}
}
全局就这一个变量,开始执行。
console.log(test); // function test(test){//...} 全局的GO.test 有值
function test(test){
// 代码省略
}
test(1);
var test = 123;
继续往下执行,在函数体内部,预编译还要创建个AO对象。
AO{
test : function test(){}
}
console.log(test); // function test(test){//...} 全局的GO.test 有值
function test(test){
console.log(test); // function test(){} 先找AO.test 有值
var test = 234; // AO 对象更新,AO.test = 234
console.log(test); // 234 从AO中取得
function test(){}
}
test(1);
var test = 123;
执行结束,GO和AO结果:
GO{
test : function test(){//.....}
}
AO{
test : 234
}
执行结果:
// function test(test){//...}
// function test(){}
// 234
练习二
global = 100;
function fn(){
console.log(global);
global = 200;
console.log(global);
var global = 300;
}
fn();
var global;
预编译GO对象:
执行函数 fn();
进入函数体,预编译AO对象:
执行函数体:
global = 100;
function fn(){
console.log(global); // undefined 函数体AO.global 有值;
global = 200; // 更新 AO.global = 200
console.log(global); // 200 从AO中取值
var global = 300;
}
fn();
var global;
执行结果:
// undefined 注意:函数体AO有值,就不会在GO里取,所以这里是undefined 而不是100
// 200
练习三
function test(){
console.log(b);
if(a){
var b = 100;
}
console.log(b);
c = 234;
console.log(c);
}
var a;
test();
a = 10;
console.log(c);
全局预编译GO对象:
执行函数,在执行函数体代码前,函数体内预编译AO对象:
GO{
a:undefined,
c:undefined
}
AO{
b:undefined
}
执行函数体和全局代码:
function test(){
console.log(b); // undefined AO.b
if(a){ // a == undefined 代码不执行
var b = 100;
}
console.log(b); // undefined AO.b
c = 234; // 函数体内c未声明,放到GO对象中
console.log(c); // 234 从GO.c取值
}
var a;
test();
a = 10; // GO对象中的a赋新值,GO.a=10
console.log(c); // 234 从GO.c 取值
执行结束,GO和AO结果:
GO{
a:10,
c:234
}
AO{
b:undefined
}
执行结果:
// undefined
// undefined
// 234
// 234
练习四
据说是某大厂前端面试题
function bar() {
return foo;
foo = 10;
function foo() {}
var foo = 11;
}
console.log(bar()); // function foo(){}
console.log(bar()); // 11
function bar() {
foo = 10;
function foo(){}
var foo = 11;
return foo;
}
练习五
var x = 1, y = z = 0;
function add(n) {
return n = n + 1;
}
y = add(x);
function add(n) {
return n = n + 3;
}
z = add(x);
console.log(x); // 1
console.log(y); // 4
console.log(z); // 4
函数提升后,同名函数覆盖, 所以求 n=n+3; 结果,144。