前言
【JS深渊】系列是我用来对前端原生javascript语言相关技术进行的深度探究或者笔记记录,我个人是一个很爱去挖层的这么一个人,但不幸的是觉悟的太晚,但我不会放弃。决定开始写博客目的有两个,一个是有仪式性的去持续提高自己的技术能力,另一个就是希望向上再向上。不过自己能力也是有限的,如果写的不好或者出现什么纰漏又或者错误的地方,脱裤式欢迎大家的建议、指正。没错看着我,对,盯着你的屏幕,别眨眼,请记住我的这句话:
您的反馈是就是我持续进步的动力。
好了,收!biu~
正文
javascrip是一个弱类型解释性语言,依赖于浏览器JS引擎进行解析执行。里面的解析过程也十分复杂。但是稍微了解一下,对javascript语言的掌握有着很大的帮助,能够使我们在日常的开发过程中更加灵活运用js语言本身的特性。废话不多说了,开始:🤣🤣
lotoze我先来段风骚的代码
console.log("123")
console.log("456");
某某侠士:“喂,糟老头!你在逗我玩?“,哪里风骚了,不就是两句打印吗?
擦擦鼻血,整整发型。略显正经起身后:“这位侠士,别急别急,请听老头我细细道来“。先来个问题:虽然是两句简单的打印语句,但是我想问的是从在浏览器打开此html文件,针对于js代码打印输出到控制台结果,这之间发生了什么?
注:上面以及下面的所涉及的js代码均在此html文件中测试执行,html代码为:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test</title>
</head>
<body>
<script type="text/javascript">
//js代码块...
</script>
</body>
</html>
过程一:语法分析(代码通读)
这一过程,专业一点叫做语法分析,每个计算机语言都会有这一步,而且计算机语言的区分的奥妙就在语法分析、词法分析、语义分析的差异。不过这里只说语法分析,可以理解为对加载好的代码的一次的初次检验。
首先,系统,不,确切的说是js引擎线程并不会立即执行文件中的js代码,而是会先通读一遍js代码,看看是否有低级的语法错误,如果有低级语法错误,那么就立即停止并抛出错误到控制台;如果没有低级的语法错误,就会为接下来js代码的真正执行做一些准备工作。但是这样说真的对吗?一定要注意理解!
我们来看一下代码:
console.log("123");
console.log"456");
console.log("789");
我们先来假设一下:按照刚才我们所说的js代码并不会立即执行,而是会先进行通读一遍代码,如果有低级错误就抛出,并立即停止后续的代码。那么按照这种理解,最后的结果就是:直接抛出低级错误,"123"并不会输出。但是你会惊奇的发现😱😱,"123"居然正常输出了,第二行报错,后边的不执行。最终结果为:
console.log("123"); //正常打印"123"
console.log"456"); //抛出低级错误,立即停止解析执行
console.log("789");
这说明什么?
说明: js引擎在通读代码做分析时,并不是读全部的js代码,而是一行一行的去读,如果这一行代码没有低级语法错误,那么是这一行代码进入js执行的下一准备阶段,如果这一行代码有低级语法错误则会在这一行抛出错误并立即停止。注意是 这一行。
这又是为什么?为什么要一行一行的读?
因为javascript语言是解释性的语言,是解释一行执行一行。那么这里请跟lotoze去发散思考一下,解释性语言是不是都是解释一行执行一行呢?答案是肯定的。如果是先等到全部的代码解释完成再同一执行,这性能得有多慢啊,黄花菜都凉了。值得一提的是,一有错误就会立即停止,这是单线程计算机语言的特性。
过程二:预编译(执行前的准备)
预编译是js代码执行前一刻的准备过程。为js的执行提供一些必要所需,在加载好的js代码经历过js引擎的语法分析检验之后会立即进入此阶段。
首先,想要了解这一阶段我们需要先明确几个重要的概念。
-
隐式属性
顾名思义,隐式属性即为系统内部调用,不能被我们看到或使用的属性。
-
作用域
作用域分为全局作用域和函数作用域,可以说这两类出现原因是因为函数的出现,函数有着自己专属的作用域。
作用域有个比较专业一点的名字叫做执行期上下文。其实看着高大上的名字,本质就是一个对象,里面存储着js代码执行时所需要的必要“粮食”。//全局作用域 ... function test() { //局部/函数作用域 ... } -
作用域链
作用域链专业一点的说法叫做,执行期上下文的集合按照一定的规律形成的链式结构叫做作用域链。 通俗一点讲,执行期上下文穿串了,然后串太长了,成了个链子la😂😂
不要冲动,大哥!
需要注意的是,全局作用域只有一个,函数作用域每个函数并不是独一无二的(比如闭包),但每个函数都会有一个自己的作用域链。 -
函数上的[[scopes]]隐性属性
我们说,在js中的数据结构受Java的影响很大,在js中也是一切皆对象,函数也可以看做一个对象,对象可以拥有属性和方法,属性分为显式属性和隐式属性。在函数对象上都有着一个叫做[[scopes]]的隐式属性。这个属性的作用就是存储着执行期上下文的集合。换句话说,[[scopes]]就是作用域链。它是一个栈结构,可以理解为是一个数组,具有栈结构First in, last out的特性。
var b = 20; function test(argument) { var a = 10; }下面是在控制台打印出来的test函数体,可以看到[[scopes]]:
基本的概念已经了解了,但是我们进一步思考一下,我们说预编译是为js代码真正执行阶段准备过程,那么预编译到底准备了什么?准备的过程是怎样的?
*下面我来总结一下预编译的过程,预编译的执行过程分为两步:
-
全局预编译过程
- 创建Global Object对象(后面简称GO)。
- 找全局变量声明,将全局变量名做为GO对象的属性名,值赋予undfined。
- 找全局函数声明,将函数名作为GO对象的属性名,值赋予函数体。
-
函数预编译过程
- 创建Active Objct对象(后面简称AO)。
- 找局部变量或形参声明,将局部变量或形参声明作为AO对象的属性名,值赋予为undefined。
- 实参与形参相统一。
- 找局部函数声明,将函数名作为AO对象的属性名,值赋予函数体。
预编译是变量声明提升和函数声明提升的根本原因。经过预编译过程之后,代码将立即进入真正的执行阶段。
这里我们来一个的栗子,来进行理解一下预编译执行过程。
console.log(a);
console.log(b);
var a = 10;
var b = a;
function a(a) {
var a = "aaa";
console.log(a);
}
function foo(b) {
var a = 100;
var b = a;
console.log(a);
}
foo();
a("lalala");
//请问上述代码打印什么?
这个栗子如果你只知道变量声明提升和函数声明提升,想要得出正确结果真的很麻烦。但是这个栗子实质上就是js代码的预编译过程。
我们来分析一下:
当js代码加载之后,经过语法分析后,进入程序运行的前一刻的预编译阶段。这个阶段分为全局预编译阶段和函数预编译阶段。
那么我们依据这个栗子去具体分析一下预编译过程:
- 全局预编译的过程:
- 创建一个GO对象
//Go {} - 找全局变量声明,将全局变量名做为GO对象的属性名,值赋予undfined。
//GO { a: undefined, b: undefined } - 找全局函数声明,将函数名作为GO对象的属性名,值赋予函数体。.
//GO { a: a(){}, //赋值为函数体 b: undefined }
- 创建一个GO对象
- 函数预编译过程
- a函数预编译过程
- 创建Active Objct对象(后面简称AO)。
// aAO --> 表示a函数的AO对象 {} - 找局部变量或形参声明,将局部变量或形参声明作为AO对象的属性名,值赋予为undefined。
//aAO --> 表示a函数的AO对象 { a: undefined } - 实参与形参相统一。
//aAO --> 表示a函数的AO对象 { a: "lalala" } - 找局部函数声明,将函数名作为AO对象的属性名,值赋予函数体。
//aAO --> 表示a函数的AO对象 { a: a(){} }
- 创建Active Objct对象(后面简称AO)。
- foo函数预编译过程
同上,最终的foo函数的Ao对象是://aAO --> 表示foo函数的AO对象 { a: undefined, b: undefined }
- a函数预编译过程
这时预编译过程就已经完成了,生成了这个一个GO和两个AO对象,每个预编译对象(虚构的,为了理解,包括GO和AO)生成完毕的同时都会立即压入[[scopes]]栈中,通常GO先入栈,然后是当前的AO对象入栈。下一步就是真正的执行了,调用栈则会按照先进后出的方式出栈执行。 最后的结果为:
console.log(a); //a(){}
console.log(b); //undefined
var a = 10;
var b = a;
function a(a) {
var a = "aaa";
console.log(a);
}
function foo(b) {
var a = 100;
var b = a;
console.log(a);
}
foo(); //100
a("lalala"); //报错a is not a function
过程三:执行阶段(真正执行)
js的内部执行阶段,是一个非常庞杂的过程。老头我会去使用专门的篇幅来写。这里只做一个简单的介绍。
当加载的js代码经过语法分析以及预编译后,会进入js引擎线程会把js代码划分为的宏任务(包括同步任务、异步任务)、微任务。然后按照宏任务(同步任务)-->微任务-->宏任务(异步任务)的轮询中。
lotoze | 【原创】
着重说明:里面一些表情图片并非原创,只是为了读者读起来不是那么枯燥乏味。但如果原作者觉得有侵犯版权的意思,请使用下方联系方式与我联系,为了尊重原创作者的辛苦创作,我将及时处理!
当然,没事也可以联系啦😘😘欢迎交流!
求赞/求关注
写作不易,
如果您还觉得凑合,就给个赞!
如果觉得确实觉得: “老家伙,有你的啊!”就加个关注!
如果文章有任何的错误,脱裤式欢迎大家来进行批评指正!
每一个鼓励都是lotoze我持续抛头颅,撒鸡血的创作动力!
每一个批评反馈也都是lotoze我持续成长的台阶!