Nodejs开发进阶5-JavaScript

前面讨论了JS的执行引擎-V8,也提到了那部分内容是从JS部分中分离出来的,有了这个基础。在本章节中,笔者想要结合自己使用JavsScript的经验,从编程语言的角度,谈谈对其的一些浅薄的理解。

历史

别被它的名字骗了,其实JavaScript和Java没什么关系,Script(脚本语言)倒是真的。它的历史可以追溯到1995年时,由Brendan Eich为Netscape浏览器设计的网页脚本语言产品,最早起的名字叫LiveScript(多好的名字),后来由于Java技术大火,为了蹭这个热度,被改成了现在的JavaScript这个名字。

JS的几个重要的历史阶段包括:

  • 1995年, Netscape将其作为网页脚本语言推出
  • 1997年,建立标准化的ECMAScript,开启了跨浏览器的支持
  • 2004年,AJAX技术出现,JavaScript开始被用于构建复杂的RIA网页应用
  • 2008年,搭载V8引擎的,Chrome浏览器发布,浏览器技术发展转向
  • 2009年,Node.js项目发布,将JavaScript带到了服务器端
  • 2015年,ECMAScript6标准被正式通过,JavaScript迎来重大升级
  • 2020年后,基本建立完整的前后端生态,并被广泛应用于移动端、桌面端、物联网等各种应用和场景开发中

这里枚举其历史发展过程并不是为了简单的记录流水账,而是我们可以从这个过程中发现重要的技术脉络,从中理解这个语言平台的特性和优势。

编程语言

要理解JavaScript作为编程语言,为什么能在业界内占有一席之地,笔者认为首先要理解一些编程语言的基本底层逻辑。鉴于我们现在这个信息技术宇宙的底层技术体系其实是非常单一的,几乎所有我们能看到和接触到(其实那些接触不到的比如超级计算机也是一样)的计算机系统都是冯诺依曼架构、它们都使用二进制系统,基本运行原理也是类似的,作为普通的应用级别的开发者我们其实并不特别关注低级语言和底层系统的问题。所以,在本章节编程语言这一部分,我们讨论的主题将局限在高级编程语言这一部分。

如果有多种高级编程语言的使用经验,我们应该能够总结出来一些共性的东西:

  • 语法规则

所有高级编程语言发明的目的,都是为了提高开发和编程效率,所以它们在形式上都是接近于人类的自然语言和逻辑。特别是由于受到技术发展和沿革的影响,现实条件下,主流的编程语言都是以标准数学符号和英语为基础,制订的相关语法和规则,作为书写代码的基础。

  • 数据类型

所有编程语言都可以处理不同类型的数据(比如整数、浮点数、字符、字符串等)。将这些数据区分为不同的类型,是因为要考虑到和计算机底层的运行机制进行匹配,使程序运行和处理信息的效率较高,以及在运行时占用的资源是比较优化的。

  • 符号、变量和常量

符号、变量和常量都是软件代码中用于表示信息和数据单元的字符串。符号用于命名和标识;变量是有名字(符号表示)的存储区域,用于保存计算过程中的数值,而且这些值可能会在程序执行中发生改变;而常量是被定义成恒定不变的值,通常来源于业务功能和需求。区分变量和常量通常是因为在更底层的处理中,如编译器或者解释器会将它们区别对待以提高程序性能。

  • 操作符

变量等用于存储数据,而操作符用于执行操作。所有类型的信息处理操作,都可以简化和归类于几种有限的操作方式,在编程语言中,通常使用操作符来进行表示。常见的操作符包括算数、逻辑、比较等等。操作符的本质也是简化编程,使代码更加直观和方便,我们完全可以想象没有操作符,我们也可以只使用函数指令来编写程序(那不就是汇编嘛)。从这个意义上而言,操作符就是一种简化的指令,或者函数的表示方式。

  • 函数和模块

函数通常是一段有逻辑关系组合起来的代码(代码块),通常用于代码和处理流程的重用。将函数进一步的合理组合起来可以成为模块,是在更高层面上的抽象和复用。模块化是编程工作在软件工程上的一个重要实践方式,它通常可以大幅度的增加代码的一致性、可移植性、故障隔离等特性,提高软件开发的效率和软件质量。

  • 流程控制

流程控制语句用于控制程序的执行流程,通过组合执行流程,可以使用相对简单的操作步骤,完成相对复杂的处理功能,并实现业务功能目的。常见的流程控制方式包括条件、分支、循环、跳转等等(下图)。

  • 数据结构

使用不同类型的数据结构,可以帮助开发者使用结构化的方式,组织和处理数据,目的通常是封装和解耦,简化开发和维护的工作。

除了单一的属性组合式的数据结构之外,还有更高级和复杂的数据结构,包括数组、链表、栈、队列、树等等,能将数据在集合层面上进行组织来方便进行处理,也是一个编程语言的重要特性。

  • 面向对象编程

面向对象编程(Object Oriented Programming),是一套软件工程方法论,它模拟现实世界的情况,并抽象成代码的结构和规范。主要目的还是为了提高开发效率,简化软件的移植和演进。OOP相关的概念和实务包括类、实例、接口、封装、继承、实现等等,已经相对比较成熟和完成。要实现OOP,编程语言本身也需要在语法上提供相关支持OOP编程的特性,如类和接口定义,实例化,继承等等。

  • 混合型语言

一般认为,编程语言在代码执行的层级方面,分为低级语言和高级语言;在代码执行方式方面,分为编译型语言、解释性语言(脚本语言)和指令性语言(如SQL);在方面,分为过程性语言、面向对象语言和函数式语言。JavaScript无疑属于高级语言,但可能和大家的想象不一样,笔者认为它很难被明确的归类为函数式语言和解释性语言。

使用JS编程,可以是函数型的,但JS也支持和实现类和对象,达到面向对象编程的效果。JS的精髓在于异步编程,但它也不妨碍你可以将它当作一个最普通的过程型语言来使用。从这些方面说,JS可以被称之为混合型语言。

  • 开发体系

上面,大致就是一个编程语言一般都有的基本构成和要素。但笔者认为,编程语言还可以分为狭义和广义的两种观察角度。狭义的语言就是语言系统本身就是主要是语法和配套的编译器或者运行时;广义的编程语言应该扩展理解成一个技术平台,除了语法结构,编译器和运行时之外,还应该包括相关配套的开发、调试、部署、运维等工具,和相应的开发社区或者生态系统,包括第三方库和模块,论坛,文档和知识库,主导商业实体等等。这也是一个现代化的开发体系所应当包含的要素。

本系列文章的主题主要是nodejs这样Web应用技术平台,所以我们讨论的编程语言在这里也会局限在Web应用开发的领域。根据这个限制,结合编程语言的广义化定义,我们现有的用于Web开发的技术体系主要包括:

  • JavaScript(Nodejs)
  • Java
  • DotNet
  • PHP
  • Python
  • Ruby

笔者觉得,从整体而言,这些技术平台本身应该没有绝对的优劣可言,因为他们都是在特定历史时期和应用和需求环境下的产物,都是为了解决和满足当时的应用需求,而发展的产物,每个都有其独特的起源和技术发展过程。

而且,可能在设计之初,它们的主要目标和定位有一定差异,但经过长时间的发展和演化,能够生存下来的技术体系,也会逐渐借鉴和吸取其他技术系统优秀的部分,改进和弥补自己的不足,并且紧跟甚至推动整个Web应用开发技术的前进,只不过,它们在这些方面表现出来的程度不同而已。

但不能否认,可能是JavaScript由于应用广泛而密集,最为贴近用户的实际应用需求的原因,在这些方面的表现,是比较突出的,很多现代化先进的编程理念和思维,比如异步IO、回调变量、消息驱动、群集模式等等,也是比较早的引入、实现和应用的。我们会在后续的章节中,分别展开讨论。本章节笔者想要先来重点关注JS作为一种编程语言,它的对表象化的方面,就是语法方面的一些要点。

JavaScript语法

  • 关于语法

高级编程语言已经发展了很长时间,在长期的发展过程中,各家都会互相借鉴,取长补短。所以在整体框架、常用的模式、基础功能方面,各个成熟的语言,都是趋于一致的,不会有太大的差异。可能在具体的语法、关键字、书写方式之类的会略有差异,但基础概念,其实是一样的,没有本质的区别。

但是,不同的编程语言,在被发明和创建的时候,它们的定位和目标,还是稍有不同的,这些会体现在它们的实现细节当中。所以很难说哪种语言就比另外一种语言更好,它们都有自己的使用场景和适用范围,对于一些特定的应用而言,可能某些语言和技术,相比另外一些更加合适一些。比如Fortran主要的设计目标是科学计算,所以它的浮点、双精度等数值计算的功能在开始的时候就设计的非常强大;C语言主要用于操作系统等底层开发,和操作系统的集成和关联比较紧密;Basic作为一种脚本语言,简单易用,适合编程和学习;Java提出了字节码、虚拟机和跨平台执行的概念,同时设计严谨规范,很好的践行了面向对象编程的理念;C++、C#和ObjectiveC可能是受到Java的启示而对C进行了魔改;Pasical语言结合强大的集成开发环境和快速编译,很好的平衡了执行和开发的效率;PHP特别重视字符串操作,在早期Web动态页面的时代就能够大放异彩;Ruby On Rails创建了MVC模式,但也昙花一现;Python凭借着灵活易用和庞大的数据处理功能和模块的积累,也能在信息和数据处理领域占有一席之地。而JavaScript,就是借助了Web前端开发技术的快速发展和前后端开发一体化的需要,才能进入主流的Web应用开发技术行列的。

回到Javascript,稍微了解其技术来源和发展过程的话,就知道,这个名字是名不符实的。它确实可能是一种脚本语言(script),但其实和Java(从原理而言是编译型语言)没什么直接的关系。只是由于当时Java技术过于热门,需要蹭那个热度,或者需要取Java可以跨平台运行的寓意而已。Javascript的语法,其实更像是C,但除了基本语法之外,也有很大的差别。就笔者的使用经验和感受而言,JavaScript作为一种脚本语言发展而来的高级编程语言,相比其他的编程语言,它有一些独特的特性,这里简单列举如下。

  • 简洁

如果你有一些使用编程语言的经验,JavaScript给你的第一印象应该就是:简洁。早期的JS语言在这方面尤其明显,已经到了给人以"简陋"的感觉。但随着逐渐成为主流的编程语言,特别是通过引入ECMAScript,逐渐标准化规范化之后,在这方面已经改善了很多。但总体而言,JS还是非常简洁的。

就语法本身而言,JavaScript应该是更接近于C语言,而不是Java语言。Java语言由于需要严格的遵循面向对象的编程范式,语法略显冗繁。JS在这方面就要好很多,比如简单的类和对象的声明和建构,匿名函数,全局变量使用等等,给开发者在实际的开发工作中提供了很多的方便。

  • 灵活

除了简单之外,JS语言的语法也非常灵活。对于一般使用者而言,这些可能体现在数据类型没那么严格,函数可以作为变量,比较运算符可能不是按照预期工作,可变的参数数量和类型,异步执行的顺序难以琢磨等等。

对于刚入门或者不熟悉JS工作方式的开发者而言,这些过于灵活的特性,可能会带来一些困扰。另外一些经常诟病是性能问题,但笔者认为这其实是主要考验执行器的设计和实现水平。现代化的高级编程语言,执行的效率,和语法和形式,其实是没有太大关系的,但这些却会影响到开发者的开发体验和代码编写的便利性。这个需要一定的适应过程。

  • 强大

作为一种面向Web应用开发的编程语言,程序编写和开发效率,是JS设计和实现的一个重点考量的因素。这主要体现在JS设计和实现了很多操作方便,功能强大,规划合理,易于理解和应用的语法和规则。我们将其统称为语法糖。这方面的详细内容,我们在后面的章节中,会深入详细探讨。

语法糖

"语法糖(Syntactic Sugar)"是一种编程语言设计的概念,顾名思义,它没有太多营养(不会提供新的功能),但吃起来香甜愉悦(使用便利)。这意味着使用语法糖虽然能够更简洁、更易读,但本质上并没有改变语言的基本结构或功能。我们也不可否认,逻辑合理,优雅美观的语法糖,也确实可以提升开发的体验,在软件工程的总体上也是比较正面的。

在很大程度上,JS之所以能够做到简洁和灵活,是因为它们能够持续实现和提供语法糖,不断的优化代码编写和开发过程和体验。在JS语言中,这样的特性很多,我们这里找一些比较典型的重点讨论。

  • 复合操作符

广义上的复合操作符,就是将基本的程序操作符组合使用,来达到更强和简化的语义表达能力,可以使代码和程序更加直观和简洁。这一部分,很多也不是JS独有的特性,里面一些思维和处理方式,也是来自于C语言和其他语言的启发。特别是对于入门的开发者而言,相比普通的程序代码和书写方式,在代码简化和方便性方面,应该有更多的体会和感受。而且,这些特性还在不断发展当中。这里简单列举几条:

js 复制代码
// 整数递增或递减后
let i = 0;
i++;i--;--i;++i;

// 引用赋值, 支持 +,-,x,/,&,|, <<, >>
i += 2; // i = i+2; // 算数运算
i |= 4; // i = i|4; // 位运算
i <<= 2; // i = i << 2; // 位移运算

// 三元运算 ?:
let j = i > 5 ? 1 : 2;

// 短路运算符
let a1 = false && "hello"; // false
let a2 = a1 && "world";  // false
a2 && doSomething();  // 条件执行

a1 = false || "hello"; // hello 通常用于设置默认值
a2 = a1 || "world";  // hello

// 默认值 可以使用 || 或者 ??
result = a ?? b 
result = a || b 
// 支持多个判断值,|| 返回第一个"真"值, ?? 返回第一个非undefined值,如0

// 可选链,安全访问属性(不抛出错误)
p12 = a1?.p1?.p2; 


// 取整,简化Math.floor或者Math.ceil
let a = 0 | 100 / 3;

// 逻辑相等和严格相等
b1 = a1 == a2;
b2 = a2 === a2; 
  • 构建和解构

在JS中,可以对变量或者方法进行简洁和快速的架构和解构,支持对象和数组,也支持变量和方法,应用场景广泛,操作非常方便,这里简单试举几例:

js 复制代码
// 构建对象,可以直接使用变量
let name = "John";
let person = { id:1, name};

// 解构对象
let {id, name} = persion;

// 构建数组
let aa = [1,2,3,4,5];

// 解构数组
let [a1,a2] = aa;

// 快速复制对象和数组(浅复制)
let person2 = { age: 20, ...person };
let ab = [0, ...aa, 7,8,9];

// require使用
let { createHash, createHmac } = requre("crypto");
  • 函数变量

作为一个老开发者(用过C、VB、Pasical、Java和PHP),笔者在第一次看到,可以将函数作为变量和参数,在另一个函数使用时,有种被惊呆的感觉。但现在看起来当然理所当然,函数式编程,回调方法的前提,不就是需要函数作为一个参数变量,传递给主函数嘛。

所以,在JS中,定义一个方法,既可以使用function关键字,也可以使用const关键字:

js 复制代码
const f1 = ()=>{};
function f2(){};

在JS中,使用function定义的函数和箭头函数变量,几乎是等效的。但它们之间有一个重要的差异,就是如果使用箭头函数,代码块中的this逻辑变量将不能正常使用。

这里顺便提一下,作为一个比较新的,也是可以被看成是函数式编程的语言go,它声明函数的关键字更加简洁:func。笔者觉得JS可以借鉴一下 :)。

  • 匿名和箭头函数

函数定义和编写的本意是功能模块划分和可复用性。但很多模块化的函数,其实并不需要或者没有机会在被其他的场景调用,这时定义一个符号化的方法,就显得有点多余。尤其是对于JS这种函数式编程的语言,可能会大量使用回调函数,所以是有必要支持匿名函数的。

由于是匿名函数,定义时会使用一个比较特殊的语法就是使用箭头和代码块来进行定义,但可以不给这个函数命名。匿名函数的使用,总是以回调函数或者之间执行的方式,这时可以不需要给这个函数命名。比如,常见的延迟执行方法,就可以通过定义一个匿名函数(箭头定义)的方式来操作:

js 复制代码
setTimeout(()=>{ console.log(Date.now()) }, 2000);

// 简化参数

const multi = i=> i * (i-1);

let b = multi(3); // 6

上面的一些用法示例,也可以看到,这些箭头函数的编写和应用方式,如可忽略的单参数括号,省略return声明,都在很大程度上体现了JS语言简洁、灵活的特点。

  • 可变参数和默认参数值

在Java中,要实现同一个方法,但接收不同数量参数的需求,可能需要用不同的参数重复定义同一个方法,然后在内部调用的方式来实现。JS直接支持可变参数。

在JS中,方法的参数值,不仅可以是动态的,也可以从后向前指定默认值,这样可以简化一些代码的编写和调用方式。这两种使用方式,可以参见下面的示例代码:

js 复制代码
const isum = (...ilist)=>{
    return ilist.reduce((c,v)=>c+v,0);
};
console.log("Sum1:", isum(1,3,5,8));

const isum2 = (i1,i2=10)=>{
    return i1+i2;
};
console.log("Sum2:", isum2(1));
console.log("Sum3:", isum2(1,3));

示例表明,可变参数,可以将一个方法的参数,转换称为一个参数数值,传入方法进行处理。

  • 数组方法

数组操作,是现代Web应用中,一个非常重要的应用场景。在常规的数组操作模式之上,JS还提供了一整套方便进行数组操作的方法,来简化和方便此类操作的程序编写,如map、filter、reduce、some、any...等等。它们的通用应用模式和设计的巧妙之处是可以使用一个回调方法来遍历和操作处理数组元素,从而实现可方便的自定义和扩展功能,这样就提供了很大的灵活性和方便性。而且,从另一个角度而言,将数组这种数据结构抽象出来,作为一个整体,用抽象一致的方式来进行处理,笔者认为,这种编程思维和认知的层级,显然提升了一个层次。

除了这些数组方法之外,JS还通过Array类,可以帮助快速创建和初始化数组,特别适合于预构造一些常见的数据数值模型。另外,哪怕是对于传统的数组操作方式,JS也进行了改进和优化,比如构造方法,合并操作,负值索引等等,极大的方便了编码操作。

下面我们通过几个简单的代码来直观的了解在JS代码中数组方法的应用方式:

js 复制代码
// 数组创建和初始化
let a1 = Array(100).fill();
console.log(a1);

// map映射方法,生成 1~100的数组
let a2 = a1.map((v,i)=>i+1);
console.log(a2);

// 过滤器, 5的倍数
let a3 = a2.filter( v=> v % 5 == 0);
console.log(a3);

// 过滤后求和
let a4 = a3.reduce((c,v)=>c+v,0);
console.log(a4);

// 随机排序和再排序(倒序)
let a5 = a3.sort(v=> 0.5 - Math.random());
console.log(a5);

let a6 = a5.sort((a,b) =>b-a);
console.log(a6);

// 切片和负索引
let a7 = a6.slice(-8,-4);
console.log(a7);

// 构建合并
let a8 = [...a6.slice(0,4),...a6.slice(-4)];
console.log(a8);

最后需要注意,和传统的for循环相比,类似map迭代处理的方式,因为需要将数组作为一个整体来处理,是不能在中间中断的。所以需要开发者根据实际的需求和场景,选择合适的技术实现方案。

  • 链式操作

在一些流程化、关联性的操作中,JS可以使用链式操作,来简化代码的编写,同时使操作逻辑更加清晰直观。这里简单举几个例子方便读者理解:

js 复制代码
// 摘要计算
let hash = crypto.createHash("SHA256").update("China中国").digest("hex");

// 数组处理
let sum =  Array(100).fill().map((v,i)=>i+1).reduce((c,v)=>c+v);

笔者理解,要实现链式操作,其核心是其相关的实现,方法和函数的返回值是当前对象,并且在当前对象中,需要保存当前操作步骤的状态(Context,上下文),并且这些方法,是可以按照逻辑关系,进行灵活的组合的。

链式操作可以以更贴近人类思维习惯的方式来组织代码,具有更好的表达力,更加紧凑和简洁,并且容易编写、修改和移植。

  • 字符串操作

Web应用开发的一个重要的需求和特点,就是会涉及大量的字符串操作,就特别要求相关的编程语言在这方面有所着重(笔者觉得早期的PHP就是因为在这方面特别突出而快速的发展起来的)。而JS早期作为前端脚本语言,主要的处理内容和场景也是字符串,现在作为全栈编程语言,在这方面也不会落后。

最常见的字符串操作包括字符串的连接、分割,内容检索,正则表达式支持,字符串和数组、对象的相互转换,模板字符串处理等等,这些方面JS的支持都是比较完善的,下面简单例举几条:

js 复制代码
// 字符串分隔、切片和拼接
let slist = "北上广深".split("");
let s = slist.sort().join(",");
let l2 = slict(-2);

// 模板字符串
const name = 'John';
const greeting1 = 'Hello, ' + name + '!'; // 传统拼接
const greeting2 = `Hello, ${name}!`; // 模板字符串
  • 柯里化

JS提供函数柯里化的支持。除了Python有这个概念之外,笔者不确定其他的语言是否也支持。

按照其定义,柯里化(Currying,原意是卷边)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。这个描述刚接触的人有点不好理解,我们看一个示例:

js 复制代码
const add = (x, y)=> x+y;
  
// 柯里化后的函数
const curriedAdd = (x)=> (y)=> add(x,y);
// function curriedAdd(x) {
//     return function(y) {
//       return add(x, y);
//     };
// };

// 调用柯里化后的函数
console.log(curriedAdd(2)(3)); // 5
console.log(curriedAdd(5)(6)); // 11

根据一般的说法。柯里化可以简化函数的的调用,清晰调用步骤和逻辑,以及方便的使用函数链,提高代码的复用性和可读性。但笔者对此有一定的保留意见。柯里化看起来很酷,但笔者觉得却把问题搞复杂了。函数的目的就是隐藏细节,JS提供的链式调用、可变参数和参数默认值,已经可以方便的处理绝大多数的情况。柯里化并没有改善程序结构和可读性,只是转换了一下表述方式,反而由于比较奇怪的结构,提高了理解成本。

JSON

也许很多JS的开发者没有意识到,JSON其实也是JS平台的一个核心技术和优势。使用JS语言处理JSON数据,显得非常自然、流畅和高效,更重要的是,它还可以提供在前端浏览器环境、后端Nodejs环境、文本传输过程和文件存储中的一致性。

这里刚好有一个机会,让我们来讨论一下这个重要而又出色的技术。和JS语言编程观一样,JSON的结构和设计非常简单,用几个图就可以清晰直观的表达(来自json.org)。

首先,JSON的基本结构,就只有Object对象和Array数组两种形式,对象的形式如下图所示:

而数组的结构如下图所示:

无论是对象还是数组,其值的类型包括如下图所示:

JSON的值,只有七种类型,包括对象、数组、字符串string、数字number、真true、假false和空。JSON还详细定义了这些类型的结构和形式,都有相应的示意图,很容易理解,这里就不再赘述。

在js中,使用JSON也是非常简单和自然的,js的对象,和JSON数据,几乎可以通用和无缝转换。只有当需要进行序列化的时候(通常用于在网络和异构系统间传输数据),才进行转换。所使用的代码也很简单:

js 复制代码
// 声明和创建JS对象
let obj = { id:1, name: "John"};

// 转换为json字符串
let strjson = JSON.stringify(obj);

// JSON字符串转换为js对象
let obj2 = JSON.parse(strjson);

有趣的是,虽然JS原始支持JSON和String之间的相互转换,但有很多性能测试表明,JS的原生转换性能却不是最好的。这里笔者觉得需要澄清一下。因为作为一个基础技术特性,JS的原生方法,需要考虑到最大的兼容性和可靠性。我们看到的很多高性能的JSON序列化解构方法,很多其实是有条件的,就是需要先明确需要解析对象的结构,这时它就可以做预先的优化和处理,来达到一个很高的性能。

所以,这里的认知和结论是很清楚的。通用场景,就用系统原生的序列化和解析方法;特定场景和固定数据结构的场景,可以考虑引入特别的解析库和方法。

全局变量和函数

从一个侧面,我们也可以感觉到JS的设计目标就是对于开发工作的易用和高效,比如一些全局变量和函数,不需要引用或者声明,就可以直接使用,其实是违反了一些严格的编程规范的,并且有一定的命名冲突风险或者学习使用台阶。但其实,任何语言和编程环境,其实都有保留关键字,没有哪个环境是可以做到完全没有限制的,所以,这里面关键就是平衡和妥协。现在看起来,JS还算是做的不错的。

当然,这些很多特性,本质上而言,其实是由其执行环境提供的,但广义上也可以看成是JS的一种语言特性。我们常用的一些全局变量和函数包括: setTimeout/setInterval,Math,Array,Object..等等,在nodejs环境中,还包括如Buffer、Process等等。

REPL

为了进一步降低JS编程语言特性的学习、验证、探险、测试JS代码的成本,并结合JS语言的脚本化和实时执行的特点,设计者们在nodejs命令行和浏览器环境中实现和集成了REPL。可以将其简单的理解成为一个"PlayGround,游乐场"。使用这个游乐场,开发者可以使用基本上最简便的方式,来探索JS的语言特性,并且验证自己片段性的代码构想。使用类似命令行的交互工作方式,快速的编写和调用函数,输入数据,并检查输出结果,不需要设置执行环境,不需要编辑和保存代码文件,不需要编译执行,及其快速和便利。

当然,很多脚本化语言包括Python也有类似的特性。但传统的编译型语言,相对而言,要实现相同的工作流程,就麻烦很多。以C或者Java为例,基本上就是要编写一个完整的原始代码,然后使用编译器编辑和执行,这一般都需要IDE(Intergreate Develop Environment,集成开发环境)工具的支持。两者的差异,就像摩托车和大卡车。这大概就是很多开发者喜欢JS的一个重要原因吧。

在本系列文章中,笔者专门有一个章节阐述这个功能和特点和应用方式,读者可以在那里获得更详细的信息。

执行原理

关于JS的执行原理方面很多细节的地方,笔者会在本系列中V8相关的部分深入探讨,这里先简单的列举一些基本框架和要点,让读者有一个基本认知和概念。

作为一个"脚本型"的编程语言,源程序代码是不能直接执行的,它必须通过一个"执行器"程序加载,转移后,才能在操作系统环境中执行,就是所谓的"执行引擎,Engine"。比如常见的,在Chrome浏览器和Nodejs环境中,就使用V8作为其JS引擎,但我们应该也能看出来,V8也只是一个JS引擎技术的具体实现,由于JS程序其实就是文本文件,JS语言的规范本身也是开放的,所以理论上任何人都可以开发一个JS执行引擎来执行JS代码和程序(比如Apple的Safari浏览器就使用JavaScripCore引擎)。

JS引擎一般使用上图所示的流程进行工作。对于原始的JS代码文件,JS引擎读取后进行解析,并按照JS语言规范,生成AST(抽象语法树);然后使用解释器,将AST编译成为字节码,这个字节码就是可以在操作系统中执行的了。

传统的JS引擎大体上就是这个流程,当时的执行环境,也被限制在浏览器的环境当中,只是作为浏览器的一个功能部件存在。但如果这样的话,就不会有现在这样强大而丰富的JS生态了。这一切的开端,应该就是V8提出的JS执行的优化方案(图中虚线这一部分)。V8可以在JS程序运行的过程中,自动的采集字节码的执行指标和状态,并根据这些数据,对解释代码执行进行优化编译,生成优化代码。相关配套的,V8还引入了如JIT、内联缓存、改进的GC、指令优化、编译器优化等很多优化的技术和方案。最终得到了大幅度的性能改善。装备V8引擎的Chrome浏览器,在性能表现上,大幅度领先当时(2009年)主流的IE浏览器和一些传统技术的浏览器产品(下图)。

这促成了第二次浏览器战争和技术革命。以及以浏览器技术革命引起的互联网应用的快速发展和繁荣。

负面特性

如果站在相对中立的位置,来观察和审视这些软件和程序的技术方案,包括编程语言,我们可能会看到一些JS语言的相对负面或者有问题的方面。正确的认识和理解这些问题,对于我们更好和正确的使用JS语言和开发系统,有很重要的意义。所以,笔者这里尝试澄清和讨论一下,一家之言,仅供参考。如果读者对此有疑问,特别是不同的观点和看法,欢迎提出讨论,大家一起进步提高。

在正式开始之前,笔者想要先阐述一下自己对这些问题在整体上的一个理解。就是JS的设计和开发,不是简单单纯的看成一个编程语言,而是试图从一个整体和长期的角度来规划和构思的。所有的软件开发系统,都是为系统或者应用软件的开发而服务的。而所有软件和应用程序其实都是有一个完整的生命周期的,包括从需求-建模-架构-编码-测试-部署-运行-维护-迁移-演进-退役等多个状态和阶段,一个完善的开发系统,对这些状态和环节,都应该有比较好的支持,另外,还需要保持在整个软件生命周期中的较高的效率和成本控制。这里面就会产生很多取舍和平衡。最终我们看到的各种开发系统、工具等的特性,很多都是这些平衡和妥协的结果。

以JS为例,看起来有很多不严谨,不合理,和影响运行效率的地方,很多就是基于易于理解,易于使用,易于迁移和演进的考虑。系统应用的性能和效率,不仅仅体现在运行的时候,很多时候,也体现在开发过程,部署过程,甚至Bug修复和错误恢复,特性的快速增加或者修改等不那么明显的场合。

  • 严谨性

可能对于Java或者C#这种"学院派"的编程语言而言,JS显得太不严谨了。有很多方面的体现,比如早期的全局变量、过于灵活的变量作用域、数值比较、缺乏模块系统等等。对于初学者可能感受不大,但对于从其他系统迁移而来的开发者就需要一个适应的过程。

笔者的理解,这可能和它的草根出身有关。这个话题,其实体现了一个编程语言设计的"价值观"和"方法论"。毕竟,所有的编程语言,都是为了有针对性的应用程序的编写场景设计的,是为了解决实际问题,而不是为了构建或证明某种软件开发理论而存在的。JS原本就是为了在浏览器里面支持页面和数据简单操作的小工具,不需要多复杂和强大的功能,当然是越简单越灵活越好。

当然现在情况不同了,JS已经成为了一个主流的Web应用开发体系。其实在这方面已经有了一些改观。它的策略是,在不放弃简洁灵活的基本特性的前提下,通过渐进式的规范化和体系化,使JS逐渐的发展成为一个正规的,工业级的应用开发编程语言和体系。但在这个过程中,基于兼容性的考虑,不可避免的需要保留一些过去的特性,这个应该是可以理解的。

  • 执行效率

本质上而言,脚本式编程语言的代码,都不是操作系统可以直接执行的原生可执行代码,它们的执行,通常需要加载、解释执行。而如果是编译型语言,编译后是可以直接生成操作系统原生程序码的。不需要解释执行,也不需要外部解释器,程序自己就可以执行,效率最高。

关于这个问题,笔者是这样考虑的。在计算机技术发展的初期,系统的性能比较弱的阶段,这个问题确实比较突出。相对软件业务执行的效率而言,软件编译,或者解释的代价是比较高昂的。但现在的计算机系统,对于系统和低层基础操作的效率是非常高的,这样,解释或者编译,在整个应用的准备阶段的时长占比就比较小,这也是我们有时候会感觉到Java调试运行基本可以做到"实时"感觉的原因。所以,从这个角度而言,这个问题在现在的技术条件下,由于编译过程对性能的影响,已经不是一个主要或者突出的问题,不必过于纠结。开发者应该更加关注业务架构、流程、数据等方面的优化空间。

  • 类型系统

一般认为JS是一种弱类型的编程语言,这会带来一些性能和安全方面的问题。从计算机程序运行的原理来看,对于动态数据类型的变量,执行系统需要对执行过程进行适配性的检查和处理,这会增加一些处理过程,当然会影响程序整体的性能。

前面已经简单的提到了造成这种情况是由于JS语言的定位、历史和对易用性的追求等多个因素造成的。在大多数情况下,其实编码时稍微注意一下编程规范,这个问题并不严重。现在的改善方案包括使用TS语言(微软提出的类型化JS语言规范),以及在程序中,尽量不要频繁的更改变量的数据类型。

小结和感想

这个章节,笔者其实是觉得写得的非常痛苦的。因为JS这个主题,课题过于庞大,而篇幅又有限。内容和材料的选择,边界和延申的限定,都有很多的问题。所以,这里的内容,很大程度上是作者自己基于认知和思考的结果,它的体系、结构和逻辑不一定非常丰富完善,但主要表现了笔者在这些方面的观点和思考,以及自己觉得有必要进行表达和传递方面。

相关推荐
uzong3 小时前
技术故障复盘模版
后端
GetcharZp3 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程4 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研4 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi4 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip5 小时前
vite和webpack打包结构控制
前端·javascript
阿华的代码王国5 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy5 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack6 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
烛阴6 小时前
前端必会:如何创建一个可随时取消的定时器
前端·javascript·typescript