JavaScript快速导览
撰写时间:2024-02-11
修订时间:2025-03-21
概述
从TIOBE的排行榜来看,从2004年开始,JavaScript一直排在所有编程语言的前10名。2004至2014年排在第9名,并于2014年被评为当年之最受欢迎的语言(language of the year)。2019年上升至第8名,2024年上升至第6名。
2025年伊始,2024年的年度冠军出来了:Python。
目前,排在前5名的编程语言分别为:Python, C++, Java, C, 以及C#。JavaScript以第6名紧随其后。
与前年相比,C语言从第2位下滑至第4位。当前,C#的占有率 (rating) 为4.45%,升降幅度为-2.71%,而JavaScript的占有率 (rating) 为4.20%,升降幅度为+1.43%。依此趋势,JavaScript将很快与C#易位。
随着更多Web标准的出现,JavaScript的重要性将愈发突显。
C语言高效而强大,但它一直缺乏一个简单而易用的图形库。而有Canvas支持的JavaScript则无此忧。随着ArrayBuffer的加入,不仅WebGL从此可在网页上实现,更被WebAssembly充分利用而实现了桌面应用程序到Web应用的完美移植。而2021年开始的WebGPU技术已让我们感到了不可思议。Web AI?它已经悄悄到来了。
世界变化速度开始比过山车还要更猛了。而所有的这一切,都有JavaScript在背后默默地强劲支撑。更不用说,JavaScript是Web网络的原生土著语言。
过去,JavaScript被认为只不过是一种脚本语言。但现在,越来越多的人认为,JavaScript是一种具备操控网页元素的脚本能力的编程语言。node.js的出现,本地资源管理及服务器端应用均被JavaScript收归囊下。
过去,JavaScript被认为在学习时还在睡觉;而现在,JavaScript被认为在睡觉时仍在学习。
经过较长时间的发展、演化及规范化,JavaScript有许多实用的、独具一格的、令人着迷的编程语言特性。下面开始有趣的JavaScript特性之旅。
数据类型
变量没有固定的数据类型
有固定的数据类型,但声明变量时,变量没有固定的数据类型。
数据类型自动转换
当数值类型与字符串类型相加时,得到一个字符串的结果。
字符串
可使用Unicode来创建字符串:
可通过本站制作的HTML Unicode工具来获取更多的Unicode符号。
还可转换Unicode的码位,得到相应的十进制数字,然后可在HTML中使用,如:在HTML中显示⚽
:
要构建一个字符串,不管是否多行,还是需要引用其他变量,都无比轻松:
下面字符串中的控制字符因解析而被吃掉
了:
查看字符串中的控制字符:
数值的自由表达与转换
字面符
对于十进制的27
,我们可以在字面符中以不同的进制来指定:
二进制。以0b
或0B
为前缀:
八进制:以0o
或0O
为前缀:
十六进制。以0x
或0X
为前缀:
对于数据类型为BigInt的数值,则在上面各种进制的表达式之后加上n
。
对于较大的数字,可以加入下划线_
来分隔,以增加可读性:
转化为不同进制的字符串
字符串转换为数值
parseInt函数只认代表数字的字符[0, 9]
,这意味着它不支持上面所谈到字面符中的0B
等前缀或_
字符。
而在用字符串来表示二进制数值时,因太多的0
或1
,加入0B
等前缀或_
字符很有必要。甚至我们可以扩展功能,再加入空格符。
通过调用String.repalceAll方法,先将表示数值的字符串中所有的前缀与分隔符都去掉,变成纯数字字符串后,再调用parseInt函数。
自由的对象创建方式
创建一个对象无比的简单。
对象在创建后,可以自由地添加或删除属性。
最终结果:
遍历对象属性名值:
将要创建的对象包含函数属性时,不能使用下面的方式:
而应使用:
或者,更简练:
可以写:
对于这种属性值为与属性名相同的变量,则可简化为:
指针或引用
JavaScript也存在指针的操作。当变量引用Object类型时,即为引用关系。
变量a与b均指向同一个对象,当a改变时,b的值也同时改变。
如果希望复制一个对象,则可调用Object的assign方法:
数组
数组的创建
创建数组很直观。
数组是栈
数组是以栈的形式来组织的。
栈的特点是先进后出(FILO,first in, last out)。上例中,数值1
为栈底,数值2
为栈顶。Array类的push方法进栈,pop方法出栈。
可调用shift方法将栈底元素,也即第0个数组元素移出栈。
数组的合并
concat方法可将多个数组或元素合并进一个数组中。
使用展开操作符...,也可以字面符的方式来合并数组元素。
上面如果不使用展开操作符...操作符,则数组元素为数组。
高效的代码:数组解包
无需借助第三方变量,我们可以一行代码就实现两个变量交换数值。
先将a与b置于一个数组,再通过数组解包,直接分别赋值于b与a,从而高效地进行数值交换。
数组字面符的使用
数组字面符也是Array的一个实例,因此在数组字面符上面可以直接调用Array的各种方法。
switch语句
switch
语句中的case
会自动下坠,因此需注意调用break
来防止下坠。
当值为7时,break
语句确保不会执行到下面的代码。而当值为6时,没有break
语句,则下坠到执行值为8时的代码。
默认情况下,各个case
都处于同一作用域下,即代码相互污染。如果必要,可使用{}
为每个case
添加闭包。
函数
自调用函数
上面的代码,结合了JavaScript模块、lambda表达式及自调用函数的特点,使相关代码得以在页面内容加载完毕后自动运行。相当于:
内嵌函数
内嵌函数是JavaScript的一大特色。
如果某函数只在一个地方使用,而又不想与其他局部变量相互影响,则可像上面一样创建一个内嵌函数。它的好处是,对外部不可见,而又与内部的其他变量相互隔离。
因为是内部使用,甚至可以无需函数名而改写为自调用匿名函数:
这种方式,除具备上面两个特点外,还有另外一个特点:它可访问闭包范围外的变量诸如m。利用这种灵活性,我们可节省多少的函数参数传导?
函数指针
JavaScript支持函数指针。
invoker的参数是一个函数指针,在其方法体中,直接调用了作为参数所传进来的函数。
回调函数
更进一步,编制非常实用的回调机制。其基本形式如下:
回调函数的本质,是由一个数据提供者dataProvider,负责调用作为参数所传进的函数dataConsumer,并且将自己所持有的数据作为函数参数传递给后者。意为,我负责提供数据,并负责回调你以传送相应的数据。
Array的forEach方法就是基于上面的原理而编写的回调函数:
forEach方法就类似于上面的dataProvider函数,以参数element及index回调了一个匿名函数。然后,在该匿名函数中,我们就可随意使用所传入的参数。
上面的回调函数是单程的,即dataConsumer函数处置完数据后就结束了。
更进一步,我们可以利用回调函数的返回值,实现数据提供者也数据使用者之间的双程沟通。
dataProvider函数先初始化一个config变量,并通过回调机制传给dataCustomize函数由其负责具体设置该对象的值。之后,dataCustomize函数返回设置之后的值,dataProvider接收此返回值后,又进一步根据此值来设置相应的字体大小。这就实现了双程沟通。
这种设计机制有许多好处。dataProvider函数负责config的生命周期,但必须考虑将设置数值的自由还给客户端。而客户端dataCustomize函数只需根据实际需求,专注于设置相应的数值就行了,无需考虑过多的细节。双方合作得都很愉快。
Array类提供了许多这样的双程回调函数。
filter方法用于筛选出数组中符合特定条件的元素。而筛选条件由回调匿名函数来定义。而对于filter所传进的任一元素,匿名回调函数将条件设定为:如果其值小于0,则筛选出来。
filter方法根据各个元素的返回值重新生成了一个新的数组并赋值于变量newArr。
这就是回调机制的强大之处,filter方法将重新生成新数组的算法隐藏起来了,但又可让客户端自由地指定各种各样的筛选条件,从而实现了固定算法与灵活配置的完美结合。
除了上面的filter方法之外,Array类的some, every, map, reduce等方法均属于这种情况。
Lambda表达式
Lambda表达式是匿名函数的一种快捷表现方式:
相当于:
Lambda表达式在函数参数为函数时经常用到:
如果Lambda表达式的参数只有1个,可省略参数列表的括号()
:
如果Lambda表达式的函数体内,只有1条返回语句,则可省略大括号{}
以及return
关键字:
而如果需要将参数直接传至其他方法或函数处理,则可更加简练,直接编写该方法或函数的名称即可:
相当于:
结果输出:
注意,因为console的log方法的参数为可变长参数,因此forEach的回调函数的3个参数全部都自动传送过去了。
如果我们只需使用其中的某个参数,可编写自定义函数:
自定义函数show只接受1个参数,因此回调函数的后两个参数被自动忽略了。
因此,当Lambda表达式浓缩为只有方法或函数名称时,需同时考虑回调函数及所调用方法、函数的参数数量、顺序,否则可能出现不易觉察的bug。
构造器
在ECMA 6的class出来之前,这是JavaScript最典型的创建类的对象的方式。Rect称为构造函数,也称为对象rect的构造器。我们可以用下面代码查看该构造器:
作为对比,也可像下面一样创建实例:
注意,作为创建对象的用例,上面在为rect声明方法时,不能使用Lambda的形式。
在一些图形应用中,我很喜欢使用下面便捷的伪构造器代码:
Rect虽不是构造器,却返回一个通过字面符构造器来创建的对象实例。
而根据自由的对象创建方式,则可编写以下更精简代码:
乍看之下,很难看出这里藏有构造器的应用。
自动运行代码
Function(code)()
效果等同与eval(code)
,但比后者更安全、高效。具体参见函数类一节。
容易犯晕的地方
短短4行代码,共涉及到了可变长参数、数组、展开操作符...等几个方面的内容。
而容易犯晕的地方在于,当...args
出现在函数参数的位置,其是可变长参数;当其出现在函数体中,它变成了展开操作符,其结果又变回了3个独立的个体。
解包
参见在WebGL入门教程
中的JavaScript解包。
Promise
Promise是JavaScript的原生内置对象,与间接支持该特性的其他编程语言相比,JavaScript在处理异步线程上更为简便而强大。
现在许多Web标准直接绑定了Promise技术。这简直就是为JavaScript量身订制。
具体参见Promise。
响应事件
直接在HTML编写事件响应代码:
这种方式,需满足2个条件。
一是onclick
的文本内容必须为调用函数的方式showMessage()
,而不仅仅是函数名称showMessage
。
二是在页面中编写响应函数时,响应函数的代码只能放在普通的script中,不能放在类型为module
的script中。
若在类型为module
的script中编写响应函数的代码,可通过调用addEventListener来添加事件监听器。
可对a标签设定自定义的事件响应。首先编写代码:
将其href属性的值设为javascript: void(0);
,表示当用户点击此链接时,浏览器不会自动跳转到特定的URL。接着,为其添加事件响应代码:
这样,我们可通过普通的链接标签来实现复杂的业务逻辑。
Script
当显示一个网页时,涉及到以下几个环节:
- 下载网页
- 解析网页的DOM元素
- 将DOM元素添加至DOM树
- 下载JavaScript文件
- 执行JavaScript文件
- 显示网页
而下载JavaScript文件总会需要一定的时间。此时就有这样一个问题:在脚本下载完毕前、或在脚本下载完毕后但尚未执行脚本前,是否需要暂时中止其他网页元素的解析?
以上问题,由script标签的async或defer属性来控制。而这两个属性,又根据脚本是普通脚本或是模块脚本,又有不同的规则及含义。
普通脚本可以带有async或defer属性,而模块脚本只有async属性。
普通脚本
没有async或defer属性时:
- 中止网页解析,下载脚本
- 等待脚本下载完毕,立即运行脚本
- 恢复解析网页其他元素
script标签的defer属性有以下作用:
- 边下载脚本,边解析网页其他元素
- 脚本下载完毕后,须等待解析网页其他元素后,再运行脚本
- 按网页中所声明脚本的顺序来依序运行脚本
script标签的async属性有以下作用:
- 边下载脚本,边解析网页其他元素
- 脚本下载完毕后,中止解析网页其他元素,立即运行脚本
- 脚本运行完毕后,再恢复解析网页其他元素
- 脚本运行顺序不受其他脚本影响
模块脚本
对于模块脚本,不能使用defer属性。
当没有async属性时:
- 边下载脚本,边解析网页其他元素
- 脚本下载完毕,须等待解析网页其他元素后,再运行脚本
当有async属性时:
- 边下载脚本,边解析网页其他元素
- 脚本下载完毕后,中止网页解析,立即运行脚本
- 脚本运行结束后,再继续解析网页其他元素