当前位置: 首页 > 后端技术 > Node.js

JavaScript是如何工作的?

时间:2023-04-03 10:25:27 Node.js

JavaScript是什么?让我们确认一下JavaScript的定义:JavaScript是一种解释型动态语言。解释型语言相对于编译型语言存在。源代码不是直接编译成目标代码,而是转换成中间代码,然后由解释器解释并运行中间代码。主流的编程语言包括编译型(如C++)、解释型(如JavaScript)和半解释半编译型(如Java)。代码如何工作?首先,让我们了解代码的工作原理。我们知道代码是由CPU来执行的,而现在的CPU并不能直接执行if...else等语句,只能执行二进制指令。但是二进制指令对人类来说真的很不友好:我们很难快速准确地判断一条二进制指令1000010010101001代表什么?于是科学家发明了汇编语言。汇编语言汇编语言实际上是二进制指令的助记符。假设10101010代表读取内存的操作,内存地址为10101111,寄存器地址为11111010,那么完整的操作10101010101011111111010代表读取某个内存地址的值并加载到寄存器中,汇编语言不会改变这种运行方式,它只是二进制指令的映射:LD:10101010id:10101111R:11111010这样,上面的指令就可以表示为LDidR,大大增强了代码的可读性。但这还不够友好,CPU只能执行三地址表达式,与人类的思维方式和语言模式相去甚远。于是伟大的科学家又发明了高级语言。高级语言“代码是写给人看的,不是给机器看的,只是给计算机执行的。”高级语言之所以被称为“高级”,是因为它们更符合我们的思维和阅读习惯。if...else语句看起来比1010101010舒服多了。但是计算机不能直接执行高级语言,所以需要将高级语言转换成汇编语言/机器指令才能执行。这个过程就是编译。JavaScript需要编译吗?JavaScript无疑是一门高级语言,所以必须先编译后才能执行。但是为什么我们称它为解释型语言呢?它和编译型语言、半解释型和半编译型语言有什么区别?让我们从编译开始。在编译之前,我们已经了解了编译的概念。再来说说平台:同样的C++代码,在Windows上会编译成.obj文件,但在Linux上会生成.o文件。两者不能共同使用。这是因为一个可执行文件除了需要代码外,还需要操作系统API、内存、线程、进程等系统资源,不同的操作系统有不同的实现。比如大家熟悉的I/O多路复用(事件驱动的灵魂),在Windows上的实现方案是IOCP方案,在Linux上是epoll。因此,对于不同的平台,编译语言需要单独编译甚至编写,生成的可执行文件格式也不尽相同。跨平台Java更进一步,引入字节码实现跨平台运行:.java文件无论在什么操作系统下编译成.class文件(这是字节码文件,一种中间形式的目标代码).然后Java为不同的系统提供了不同的Java虚拟机来解释和执行字节码文件。解释和执行不会生成目标代码,但最终会转换成汇编/二进制指令供计算机执行。如果我们完全独立编写一个简单的操作系统,它能运行Java吗?显然不是,因为这个系统没有对应的JVM。因此,Java的跨平台和其他任何语言的跨平台都是有局限性的。Java中使用半解释半编译的好处是大大提高了开发效率,但相应地降低了代码的执行效率。毕竟虚拟机是有性能损耗的。解释和执行JavaScript更进一步。它被完全解释和执行,或称为即时编译。它将没有中间代码生成,也没有目标代码生成。这个过程通常由主机环境(例如浏览器、Node.js)负责。编译过程现在我们已经确认,即使是解释型和执行型语言也需要编译。那么代码是如何编译的呢?让我们来看看。词法分析词法分析会将语句分解为词法单元,即Token。functionsquare(n){returnn*n;}这个函数会被词法分析器识别为function,square,(,n,),{,return,,n,*,n,}并标记它们,表示是否这是一个变量或操作。语法分析的过程会将Token转化为抽象语法树(AST):{type:'function',id:{type:'id'name:'square'},params:[{type:'id',name:'n'}]...}优化和代码生成在这一步中,编译器会做一些优化工作,比如删除冗余操作,删除不用的赋值,合并一些变量等,最后生成目标代码。由于即时编译语言的编译通常发生在执行前几微秒,编译器无暇做太多的优化工作。这也是早期JavaScript与编译型语言相比性能较弱的原因之一。但就目前而言,得益于V8引擎(相对于早期的JavaScript引擎转换成字节码或解释执行,Node.js可以使用V8提供的JS2C工具将JavaScript翻译成C++代码),JavaScript与其他语言的性能差距是不再微不足道。链接和加载目标代码基本上不能独立运行。一个应用程序通常由多个部分(模块)组成。例如,C++中的简单输出需要导入标准库iostream:#includeusingnamespacestd;intmain(){cout<<"HappyHacking!\n";return0;}编译器需要链接目标代码(库)的多个副本来生成可执行文件。至此,我们已经简单了解了编译过程。但其实编译比我们说的要复杂得多,这里就不展开了。什么是动态语言,动态类型?我们还知道JavaScript是一种动态语言。那么什么是动态语言呢?一般来说,这是指代码可以在运行时根据某些条件改变其结构的语言。例如,可以在运行时引入(eval)JavaScript的新函数、对象,甚至代码;再比如Objective-C,它也可以在运行时修改对象,但是不能动态创建类,也没有eval方法。Objective-C是一种动态语言吗?所以我觉得动态语言是一个度的问题,大家不必太纠结于这个概念,可以多关注它的应用。APP常用的热更新功能是基于动态语言特性实现的。JavaScript是一种动态类型语言,那么什么是动态类型呢?动态类型的定义很明确:数据类型不是在编译阶段就确定的,而是在运行时确定的。那么TypeScript是一种什么类型的语言呢?它有静态类型检查,它是一种静态语言吗?它实际上只是JavaScript的一种方言。TypeScript最后还是要翻译成JavaScript来执行(tsc),就像我们用babel把ES6代码翻译成ES5一样。这个过程严格来说不是编译。TypeScript最大的优势是静态类型检查和类型推断,这是JavaScript严重缺失的能力。但实际上,如果我们忽略IDE给出的错误信息,强行运行TS代码,我们还是有机会运行成功的。Error刚才我们提到了报错,不妨展开说说错误。一般来说,错误分为以下几类:编译时错误、链接时错误和运行时错误。运行时错误能否严格对应编译过程?编译时错误编译时错误分为:语法错误varstr='s;这是一个典型的语法错误,这种代码不能生成AST,在词法分析阶段会报错。通常我们这样写代码的时候,IDE会报错。这是IDE的优化工作,跟词法分析有关。类型错误编译器检查我们声明的变量和函数的类型。我们在JavaScript中非常熟悉的TypeError:undefinedisnotobject就是这样的错误。链接时错误链接阶段发生的异常。这种情况在JavaScript中比较少见,在编译型语言中比较常见。运行时错误这是最难排除的错误。例如:intdivider(inta,intb){returna/b;}以上代码在编辑、编译和链接阶段都没有问题,也可以正常生成可执行文件。但是一旦这样使用divider(1,0),就会报错,这是典型的运行时错误。一般来说,运行时错误是由程序不够健壮引起的。JavaScript最常见的十大错误:下图是某错误处理平台收集到的前10个JavaScript错误,其中TypeError7个,ReferenceError1个:显然,我们可以在早期使用TypeScript来处理这8种问题编码。结论现在我们了解了JavaScript的工作原理。但是知道这一点是否有助于我们编写更好的代码?答案是肯定的。更不用说TypeScript可以帮助我们改进类型检查和类型推断。JavaScript的scope和this也与编译过程密切相关;而目前主流的小程序框架都可以支持一套代码,多平台。相信看完这篇文章,你终于对这些技术背后的原理有了一个大概的了解。快乐黑客!顺便给大家推荐一下Fundebug,非常好用的BUG监控工具~