js浅析预解析(es6之前)

本文详细讲解了JavaScript代码执行过程中的预解析,重点剖析了变量对象、全局对象和函数作用域中的变量声明与查找机制,以及闭包的概念。通过实例演示了变量声明、预解析步骤和作用域链的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

此文想深入理解的话可以看看汤姆大叔的博客:
https://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html
非常详细,看懂了这里面的所有文章,理解作用域、闭包、变量对象、原型都不是问题。

js大致运行过程

1.语法分析:
js代码在执行之前,会通篇扫码一遍代码,有没有低级的语法错误,比如: 中文的括号,如果有错误,就一行代码都不执行

console.log(1111);
console.log(22)//中文的分号
console.log(5555);

我们看到第二行代码是中文符号,结果导致代码一行都不执行
在这里插入图片描述
2. 预解析
就是本章要讲的内容。

3.解释执行
解释一行执行一行,如果有错误下面的代码不执行,到第三步代码才开始执行,也就是说代码执行之前还要经历前两个步。

变量对象

在讲预解析前先简单理解一下变量对象的概念,在全局或函数中,会有一个对象VO,它会保存全局或函数中所有的声明的标识符,也就是说在全局或函数中声明的变量都会当做变量对象的属性来创建,变量的读取访问的操作都在VO中进行。
例子:
变量对象就像标识符的仓库一样,保存着所有标识符的值。

var a = 20,
	b = function (){};
//在全局中声明的变量和函数都当保存在VO里。
VO{
	a: 20,
	b: function(){}
}
//访问a标识符时,会在VO中查找
console.log(a);
//修改a时会在VO中修改
a = 111;
//对应的VO
VO{
	a: 111,
	b: function(){}
}

全局中的变量对象
在js中,全局对象GO就作为变量对象VO,所有的全局变量都被全局对象保存,在执行全局代码前,会创建全局对象GO,全局对象的生命周期是整个网页,当关闭网页时,全局对象就会被销毁,它保存着所有全局的标识符

window对象
js引擎会将该对象作为GO的初始化属性而创建,因此在全局中可以直接访问它,代表的是浏览器窗口,在网站中window就是GO,换句话说window引用了GO对象,因此你访问window.a其实就是访问GO.a;

例如:
js引擎创建GO对象后会初始化window属性并指向GO自身。

GO{
	window: GO
}

还有一个情况就是,变量没有声明就赋值不会报错,也会作为全局对象window的属性创建

例子:
以下代码a没有声明就赋值,会作为全局变量创建,相当于window.a = 20。

function fn(){
	a = 20;
}

window{
	a: 20;
}

在访问全局对象的标识符属性时会忽略掉对象名,因为全局对象是不能直接通过名称被访问的不过可以通过间接的方式访问,例如this、window。

var a = 20;
//忽略了对象名其实就是global.a,相当于在global里找a
console.log(a);
//由于window执行GO,因此window.a === global.a
console.log(window.a);
//在全局中,this指向全局对象
console.log(this.a);

函数中的变量对象:
在函数执行前会创建一个AO对象(Activation Object),它不能被访问,它的声明周期是整个函数,当时执行结束时,AO被销毁。

AO{}

关于变量
在es5中,只有var才能声明变量,不声明直接赋值实际上是在全局对象上创建了一个普通的属性,并不是声明了一个变量。

a = 10;

这只是给全局对象创建了一个属性,不算是变量,它并不符合emascipt规范中变量的概念,它能成为全局对象的属性是因为VO===GO

变量属性和普通属性的区别

console.log(a); //undefined
console.log(b); //b没有声明

b = 10;
var a = 20; //代码执行阶段被修改

因为a是使用var声明的,因此在执行上下文创建阶段就把声明的变量保存进来了。而b没有声明,因此它不算是一个变量,所以在执行上下文阶段就没有b,b只能在执行阶段被执行。

VO = {
	a: undefined
}

关于这个区别,还有就是变量属性有一个特性,不能被delete操作符删除(attribute:DontDelete)。

a = 10;
console.log(delete a); //true
console.log(window a); //undefined
var b = 20;
console.log(delete b); //false
console.log(window b); //20

预解析

预解析发生在全局和函数中,声明语句在代码执行前就提前得到解析,所以在代码执行环节会忽略掉声明语句,这个过程其实就是变量对象初始化的过程,就是在代码执行前会做一些准备工作,例如全局对象window初始化阶段会将Math、String、Date、parseInt等作为自身属性创建,将变量声明和函数声明做为全局对象的属性创建。

作用: 解决标识符命名冲突的问题,谁会覆盖掉谁。

预解析的4个步骤(以函数为例)
1.创建AO对象

2.找所有的形参作为AO对象的属性名,如果没有传参数的话值赋予undefined。

3.找所有的函数声明作为AO对象的属性名,值赋为函数值,如果变量对象已经存在同名的属性,则函数声明会完全替换掉。

4.找所有的变量声明做为AO对象的属性名,值赋为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(){}
}
fn(1);

以上预解析的阶段:
1.创建AO对象

AO{
	
}

2.找所有的形参作为AO对象的属性名,如果没有传参数的话值赋予undefined如果有传值的话,值为传递的值。

以上代码中,a是形参,传递给它的值是1

AO{
	a: 1,
}

3.找所有的函数声明作为AO对象的属性名,值赋为函数体,如果变量对象已经存在同名的属性,则函数声明会完全替换掉。

以上代码中,function a(){} 和function d(){}是函数声明,但是AO中已经存在属性a,因此function a(){}会替代AO中的属性a并赋予函数值。

AO{
	a: function a(){},
	d: function d(){}
}

4.找所有的变量声明做AO对象的属性名,值赋为undefined,如果AO中已经存在同名属性了,则变量声明不会干扰已存在的属性

以上代码中var a和 var b是函数声明,但是AO中已经存在属性a了,因此var a会被忽略掉,var a不会干扰AO中a的属性值

AO{
	a: function a(){},
	d: function d(){},
	b: undefined
}

代码执行阶段:
函数fn中的代码,在函数中访问标识符时会在AO中查找结果。

//打印 function a
console.log(a);
/*
var a在预解析阶段就得到解析,var a会被忽略
因此这句是赋值语句a = 123,AO中a改成123
AO{
	a: 123,
	d: function d(){},
	b: undefined
}
*/
var a = 123;  
// 打印123
console.log(a);  
//已经在预解析阶段就解析,这句会被忽略
function a(){} 
//打印123
console.log(a);
/*
AO中的b被改成匿名函数
AO{
	a: 123,
	d: function d(){},
	b: function(){}
}
*/
var b = function(){} 
//打印function(){}
console.log(b);
function d(){}

以上代码最后的执行结果是: function a(){} 123 123 function(){}

全局中也有预解析,只不过全局的预解析比函数的预解析少了一个第二步不用找形参。
例子:

<script>
var a = 123;
function a(){
    
}   
//在GO中找标识符
console.log(a);
</script>

以上代码预解析过程:
1.创建GO对象

GO{

}

2.找所有的函数声明作为AO对象的属性名,值赋为函数值

GO{
	a:function a(){}
}

3.找所有的变量声明做为AO对象的属性名,值赋为undefined,如果AO中已经存在同名属性了,则变量声明不会干扰已存在的执行属性

以上代码var a是变量声明,但是GO中已经有a这个属性,因此var a不会干扰GO中a的属性值。

深入理解作用域变量的查找过程:

几层嵌套关系,我自己有就用我自己的,我没有的在往上找

var a = 123;
function fn(test){
    var a = 234;
	function fn2(){
		var a = 345;
		console.log(a);
	}
	fn2();
}
test(1);

上面对应的变量对象是:

GO{
	a: 123
	fn: function(){}	
}
fn.AO{
	a: 234,
	fn2: function(){}
}
fn2.AO{
	var a = 345
}

最后打印的结果是: 345

链式关系就近原则: 当在函数作用域中操作一个变量时,会先从自己的作用域中查找有没有这个标识符,如果有,就使用这个标识符的值,如果没有就去上一层的作用域作用域找这个标识符,直到找到全局作用域为止,如果全局作用域依然没有这个标识符,则抛出错误: test is not defined,这种链式查找关系我们称为作用域链

航母的AO和GO实际上就可以理解为作用域,这个GO和AO会形成链式关系,这个链式关系的访问顺序是可进的来,先当前的AO有没有,有就用,如果没有找上一层的AO,直到找到GO为止,如果GO里没有,则抛出错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值