JavaScript快速导览
撰写时间:2024-02-11
修订时间:2025-08-15
概述
从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 特性之旅。
数据类型
变量没有固定的数据类型
有固定的数据类型,但声明变量时,变量没有固定的数据类型。
let a = 5; // number
a = "Hello"; // string
数据类型自动转换
当数值类型与字符串类型相加时,得到一个字符串的结果。
let a = 5;
a = 5 + "Hello"; // string
字符串
可使用Unicode 来创建字符串:
let footBall = '\u26BD';
console.log(footBall); // ⚽
可通过本站制作的HTML Unicode 工具来获取更多的Unicode 符号。
还可转换Unicode 的码位,得到相应的十进制数字,然后可在HTML 中使用,如:在HTML 中显示⚽:
⚽
要构建一个字符串,不管是否多行,还是需要引用其他变量,都无比轻松:
let num = 15;
let str = `
He gave me ${num} apples.
And eventually additional 5 more.
`;
下面字符串中的控制字符因解析而被吃掉 了:
pc.log('%s', `a\rb\rc`);
查看字符串中的控制字符:
pc.log('%s', String.raw`a\rb\rc`);
数值的自由表达与转换
字面符
对于十进制的27 ,我们可以在字面符中以不同的进制来指定:
二进制。以0b或0B为前缀:
var num = 0b00011011; // 27
// or
var num = 0B00011011; // 27
八进制:以0o或0O为前缀:
var num = 0o33; // 27
// or
var num = 0O33; // 27
十六进制。以0x或0X为前缀:
var num = 0x1B; // 27
// or
var num = 0X1B; // 27
对于数据类型为BigInt 的数值,则在上面各种进制的表达式之后加上n。
var num = 12345678987654321n;
var num = 0B1011100111110011n;
var num = 0o123456712345670n;
var num = 0XFF33CC8899AA66DDn;
对于较大的数字,可以加入下划线_来分隔,以增加可读性:
var num = 12_345_678_987_654_321n;
var num = 9_8765_4321n; // chinese convention,9亿8765万4321
转化为不同进制的字符串
let num = 27;
console.log(num.toString(2)); // 11011
console.log(num.toString(8)); // 33
console.log(num.toString(16)); // 1b
字符串转换为数值
parseInt 函数只认代表数字的字符[0, 9] ,这意味着它不支持上面所谈到字面符中的0B 等前缀或_ 字符。
而在用字符串来表示二进制数值时,因太多的0 或1 ,加入0B 等前缀或_ 字符很有必要。甚至我们可以扩展功能,再加入空格符。
const { getHexAddrStrFromTypedArray, getBinStr, getHexStr } = await import('/js/esm/BinUtils.js');
let str = '0B1011_1101 1100_1110';
str = str.replaceAll(/0B|0b|_| /g, '');
pc.log(str);
let num = parseInt(str, 2);
pc.log('%d <===> %s <===> %s', num, getHexStr(num), getBinStr(num));
通过调用String .repalceAll 方法,先将表示数值的字符串中所有的前缀与分隔符都去掉,变成纯数字字符串后,再调用parseInt 函数。
应对小数点
trunc
Math .trunc 方法直接舍弃数值的小数点部分,只保留整数部分。
let num = 3.8;
pc.log(Math.trunc(num), Math.trunc(-num));
ceil
Math .ceil 方法返回数值在笛卡尔坐标系Y 轴上大于等于其值的一个最近的整数。也即,往天花板 的方向取一最接近于其值的整数。我们可以便捷地助记为向上取整 或向大取整 。
let nums = [3.2, 3.8, 5];
for (let num of nums) {
pc.log('Math.ceil(%f) = %d, Math.ceil(%f) = %d', num, Math.ceil(num), -num, Math.ceil(-num));
}
floor
Math .floor 方法返回数值在笛卡尔坐标系Y 轴上小于等于其值的一个最近的整数。也即,往地板 的方向取一最接近于其值的整数。我们可以便捷地助记为向下取整 或向小取整 。
let nums = [3.2, 3.8, 5];
for (let num of nums) {
pc.log('Math.floor(%f) = %d, Math.floor(%f) = %d', num, Math.floor(num), -num, Math.floor(-num));
}
round
Math .round 方法对数值四舍五入 。
let nums = [3.2, 3.8, 5];
for (let num of nums) {
pc.log('Math.round(%f) = %d, Math.round(%f) = %d', num, Math.round(num), -num, Math.round(-num));
}
parseInt
parseInt 函数先将参数所代表的数值转换为一个浮点数,然后再调用Math .trunc 方法舍弃结果中的小数部分。
let nums = [3.2, 3.8, 5];
for (let num of nums) {
pc.log('parseInt(%f) = %d, parseInt(%f) = %d', num, parseInt(num), -num, parseInt(-num));
}
toFixed
Number .toFixed 方法通过四舍五入 来保留指定位数的小数。返回一个字符串。
let num = 3.1239;
let str = num.toFixed(2);
pc.log(str);
一般来讲,调用Number .toFixed 方法是为了精细规整一个浮点数的字符串形式,以在界面中按特定格式来打印其值。如果只需要使用num 进行数学运算,则无需调用Number .toFixed 方法。
自由的对象创建方式
创建一个对象无比的简单。
let obj = {
name: 'mike',
age: 25
};
对象在创建后,可以自由地添加或删除属性。
obj.email = '123@263.com';
delete obj.age;
最终结果:
console.log(obj);
/*
{
name: 'mike',
email: '123@263.com';
}
*/
遍历对象属性名值:
let entries = Object.entries(obj);
for (const [key, value] of entries) {
console.log(key);
console.log(value);
}
将要创建的对象包含函数属性时,不能 使用下面的方式:
let obj = {
name: 'Mike',
function greet() { /* This is wrong! */
}
};
而应使用:
let obj = {
name: 'Mike',
greet: function() {
}
};
或者,更简练:
let obj = {
name: 'Mike',
greet() {
}
};
可以写:
let number = 2;
let msg = 'Hello';
let obj = {
number: number,
msg: msg
};
对于这种属性值为与属性名相同的变量,则可简化为:
let number = 2;
let msg = 'Hello';
let obj = { number, msg };
指针或引用
JavaScript 也存在指针的操作。当变量引用Object 类型时,即为引用关系。
let a = {
name: 'Sarkuya',
age: 25
};
let b = a;
a.name = 'Mike';
pc.log(b);
变量a 与b 均指向同一个对象,当a 改变时,b 的值也同时改变。
如果希望复制一个对象,则可调用Object 的assign 方法:
let a = {
name: 'Sarkuya',
age: 25
};
let b = Object.assign({}, a);
a.name = 'Mike';
pc.log(a);
pc.log(b);
数组
数组的创建
创建数组很直观。
let arr = [1, 2, 3, 4, 5];
数组是栈
数组是以栈的形式来组织的。
let arr = [];
arr.push(1);
arr.push(2);
console.log(arr); // [1, 2]
arr.pop();
console.log(arr); // [1]
栈的特点是先进后出(FILO ,first in, last out)。上例中,数值1 为栈底,数值2 为栈顶。Array 类的push 方法进栈,pop 方法出栈。
可调用shift 方法将栈底元素,也即第0个数组元素移出栈。
let arr = [1, 2, 3];
let bottom = arr.shift();
console.log(bottom); // 1
console.log(arr); // [2, 3]
数组的合并
concat 方法可将多个数组或元素合并进一个数组中。
let arr1 = [1, 2, 3];
let arr2 = [4, 5];
let num = 6;
let arr3 = arr1.concat(arr2, num);
console.log(arr3); // [1, 2, 3, 4, 5, 6]
使用展开操作符... ,也可以字面符的方式来合并数组元素。
let arr1 = [3, 4, 5];
let arr2 = [1, 2, ...arr1, 6, 7];
console.log(arr2); // [1, 2, 3, 4, 5, 6, 7]
上面如果不使用展开操作符... 操作符,则数组元素为数组。
let arr1 = [3, 4, 5];
let arr2 = [1, 2, arr1, 6, 7];
console.log(arr2); // [1, 2, [3, 4, 5], 6, 7]
高效的代码:数组解包
无需借助第三方变量,我们可以一行代码就实现两个变量交换数值。
let a = 10;
let b = 20;
[b, a] = [a, b];
console.log(a, b); // 20, 10
先将a 与b 置于一个数组,再通过数组解包,直接分别赋值于b 与a ,从而高效地进行数值交换。
数组字面符的使用
数组字面符也是Array 的一个实例,因此在数组字面符上面可以直接调用Array 的各种方法。
[15, 29, 32, 47, 56]
.forEach((num, index) => pc.log(`[${index}]: ${num}`));
完美支持农历换算
如果您对周易感兴趣,那您的案头将放着一本必备之书 —— 《万年历》,通过查阅的方式,以进行公历到农历的换算。但令人意料不到的是,JavaScript 悄悄给我们中国人送了一分厚礼。
今日农历是什么?
let date = new Date();
let 农历Str = new Intl.DateTimeFormat("zh-Hans-CN-u-ca-chinese").format(date);
pc.log('公历: %s', date.toLocaleString());
pc.log('农历: %s', 农历Str);
现在,您手上的那本《万年历》完全可以丢掉了。
更多细节,参见农历算法 。
switch语句
switch语句中的case会自动下坠,因此需注意调用break来防止下坠。
let num = 6;
switch(num) {
case 7:
pc.log('match');
break;
case 6:
case 8:
pc.log('close');
break;
default:
pc.log('bingo');
break;
}
当值为7 时,break语句确保不会执行到下面的代码。而当值为6 时,没有break语句,则下坠到执行值为8 时的代码。
默认情况下,各个case都处于同一作用域下,即代码相互污染。如果必要,可使用{}为每个case添加闭包。
let x = 15;
let y = 25;
let operator = 'sum';
switch(operator) {
case 'sum': {
let result = x + y;
pc.log(result);
break;
}
case 'sub': {
let result = x - y;
pc.log(result);
break;
}
default:
pc.log('bingo');
break;
}
函数
自调用函数
<script type="module">
(() => {
let a = 5;
console.log(a);
})();
</script>
上面的代码,结合了JavaScript 模块、lambda表达式及自调用函数的特点,使相关代码得以在页面内容加载完毕后自动运行。相当于:
<script>
function init() {
let a = 5;
console.log(a);
}
window.addEventListener('DOMContentLoaded', init);
</script>
内嵌函数
内嵌函数是JavaScript 的一大特色。
function aFunc() {
let a = sum(3, 5);
console.log(a);
function sum(a, b) {
return a + b;
}
}
如果某函数只在一个地方使用,而又不想与其他局部变量相互影响,则可像上面一样创建一个内嵌函数。它的好处是,对外部不可见,而又与内部的其他变量相互隔离。
因为是内部使用,甚至可以无需函数名而改写为自调用匿名函数:
function aFunc() {
let x = ((a, b) => a + b)(3, 5);
let m = 5, n = 10;
let y = (() => {
let temp = m * 2 + n;
temp++;
return temp;
})();
console.log(x); // 8
console.log(y); // 21
}
这种方式,除具备上面两个特点外,还有另外一个特点:它可访问闭包范围外的变量诸如m 。利用这种灵活性,我们可节省多少的函数参数传导?
函数指针
JavaScript 支持函数指针。
function normalFunc() {
console.log('This is a function.');
}
function invoker(func) {
func();
}
invoker(normalFunc);
invoker 的参数是一个函数指针,在其方法体中,直接调用了作为参数所传进来的函数。
回调函数
更进一步,编制非常实用的回调机制。其基本形式如下:
function dataConsumer(a, b) {
let c = a + b;
console.log(c);
}
function dataProvider(callbackFunc) {
let a = 5;
let b = 10;
callbackFunc(a, b);
}
dataProvider(dataConsumer);
回调函数的本质,是由一个数据提供者dataProvider ,负责调用作为参数所传进的函数dataConsumer ,并且将自己所持有的数据作为函数参数传递给后者。意为,我负责提供数据,并负责回调你以传送相应的数据。
Array 的forEach 方法就是基于上面的原理而编写的回调函数:
let arr = [5, 10, 15];
arr.forEach((element, index) => {
console.log(index + ': ' + element);
});
forEach 方法就类似于上面的dataProvider 函数,以参数element 及index 回调了一个匿名函数。然后,在该匿名函数中,我们就可随意使用所传入的参数。
上面的回调函数是单程的,即dataConsumer 函数处置完数据后就结束了。
更进一步,我们可以利用回调函数的返回值,实现数据提供者也数据使用者之间的双程沟通。
function dataCustomize(config) {
config.fontSize = '20';
return config;
}
function dataProvider(callbackFunc) {
let config = {};
let newConfig = callbackFunc(config);
setFontSize(newConfig.fontSize);
}
function setFontSize() {
// ...
}
dataProvider(dataCustomize);
dataProvider 函数先初始化一个config 变量,并通过回调机制传给dataCustomize 函数由其负责具体设置该对象的值。之后,dataCustomize 函数返回设置之后的值,dataProvider 接收此返回值后,又进一步根据此值来设置相应的字体大小。这就实现了双程沟通。
这种设计机制有许多好处。dataProvider 函数负责config 的生命周期,但必须考虑将设置数值的自由还给客户端。而客户端dataCustomize 函数只需根据实际需求,专注于设置相应的数值就行了,无需考虑过多的细节。双方合作得都很愉快。
Array 类提供了许多这样的双程回调函数。
let arr = [20, -5, 3, -20];
let newArr = arr.filter(element => element < 0);
console.log(newArr); // [-5, -20]
filter 方法用于筛选出数组中符合特定条件的元素。而筛选条件由回调匿名函数来定义。而对于filter 所传进的任一元素,匿名回调函数将条件设定为:如果其值小于0,则筛选出来。 filter 方法根据各个元素的返回值重新生成了一个新的数组并赋值于变量newArr 。
这就是回调机制的强大之处,filter 方法将重新生成新数组的算法隐藏起来了,但又可让客户端自由地指定各种各样的筛选条件,从而实现了固定算法与灵活配置的完美结合。
除了上面的filter 方法之外,Array 类的some , every , map , reduce 等方法均属于这种情况。
Lambda表达式
Lambda 表达式是匿名函数的一种快捷表现方式:
let sum = (a, b) => {
return a + b;
};
相当于:
function sum(a, b) {
return a + b;
}
Lambda 表达式在函数参数为函数时经常用到:
let arr = [1, 2, 3];
let newArr = arr.map((item, idx) => {
return `${idx}: ${item}`;
});
如果Lambda 表达式的参数只有1 个,可省略参数列表的括号() :
let newArr = arr.map(item => {
return item * 2;
});
如果Lambda 表达式的函数体内,只有1 条返回语句,则可省略大括号{} 以及return关键字:
let newArr = arr.map(item => item * 2);
而如果需要将参数直接传至其他方法或函数处理,则可更加简练,直接编写该方法或函数的名称即可:
let arr = [1, 2, 3];
arr.forEach(console.log);
相当于:
let arr = [1, 2, 3];
arr.forEach((item, idx, array) => {
console.log(item, idx, array);
});
结果输出:
1 - 0 - [1, 2, 3]
2 - 1 - [1, 2, 3]
3 - 2 - [1, 2, 3]
注意,因为console 的log 方法的参数为可变长参数,因此forEach 的回调函数的3 个参数全部都自动传送过去了。
如果我们只需使用其中的某个参数,可编写自定义函数:
let arr = [1, 2, 3];
function show(item) {
console.log(item);
}
arr.forEach(show); // 1, 2, 3
自定义函数show 只接受1 个参数,因此回调函数的后两个参数被自动忽略了。
因此,当Lambda 表达式浓缩为只有方法或函数名称时,需同时考虑回调函数及所调用方法、函数的参数数量、顺序,否则可能出现不易觉察的bug。
构造器
function Rect(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.offset = (xOffset, yOffset) => {
this.x += xOffset;
this.y += yOffset;
};
}
let rect = new Rect(10, 10, 50, 50);
rect.offset(20, 20);
在ECMA 6 的class 出来之前,这是JavaScript 最典型的创建类的对象的方式。Rect 称为构造函数,也称为对象rect 的构造器。我们可以用下面代码查看该构造器:
console.log(rect.constructor);
/*
* function Rect(x, y, w, h) {
* ...
* }
*/
作为对比,也可像下面一样创建实例:
let rect = {};
rect.init = function(x, y, w, h) {
this.x = x;
this.y = y;
this.width = w;
this.height = h;
};
rect.offset = function(xOffset, yOffset) {
this.x += xOffset;
this.y += yOffset;
};
rect.init(10, 10, 50, 50);
rect.offset(20, 20);
注意,作为创建对象的用例,上面在为rect 声明方法时,不能使用Lambda 的形式。
在一些图形应用中,我很喜欢使用下面便捷的伪构造器 代码:
function Rect(x, y, width, height) {
return {x:x, y:y, width:width, height:height};
}
let rect = Rect(10, 10, 50, 50);
Rect 虽不是构造器,却返回一个通过字面符构造器来创建的对象实例。
而根据自由的对象创建方式 ,则可编写以下更精简代码:
function Rect(x, y, width, height) {
return {x, y, width, height};
}
乍看之下,很难看出这里藏有构造器的应用。
自动运行代码
let a = 5;
let b = 3;
let code = `console.log(${a} + ${b});`;
Function(code)(); // 8
Function(code)()效果等同与eval(code),但比后者更安全、高效。具体参见函数类 一节。
容易犯晕的地方
aFunc(1, 2, 3); // 3 individual units
function aFunc(...args) { // rest parameter
pc.log(args); // array
pc.log(...args); // spread into 3 individual units
}
短短4 行代码,共涉及到了可变长参数 、数组、展开操作符... 等几个方面的内容。
而容易犯晕的地方在于,当...args出现在函数参数的位置,其是可变长参数 ;当其出现在函数体中,它变成了展开操作符,其结果又变回了3 个独立的个体。
Promise
Promise 是JavaScript 的原生内置对象,与间接支持该特性的其他编程语言相比,JavaScript 在处理异步线程上更为简便而强大。
fetch('../promise/examples/demo.txt')
.then(response => response.text())
.then(text => pc.log('%s', text));
现在许多Web标准直接绑定了Promise 技术。这简直就是为JavaScript 量身订制。
具体参见Promise 。
响应事件
直接在HTML 编写事件响应代码:
click on me
这种方式,需满足2 个条件。
一是onclick的文本内容必须为调用函数的方式showMessage(),而不仅仅是函数名称showMessage。
二是在页面中编写响应函数时,响应函数的代码只能放在普通的script 中,不能放在类型为module 的script 中。
<script>
function showMessage() {
console.log('Hello');
}
</script>
若在类型为module 的script 中编写响应函数的代码,可通过调用addEventListener 来添加事件监听器。
可对a 标签设定自定义的事件响应。首先编写代码:
此链接 没有默认的URL
将其href 属性的值设为javascript: void(0); ,表示当用户点击此链接时,浏览器不会自动跳转到特定的URL 。接着,为其添加事件响应代码:
document.querySelector('#the-link').addEventListener('click', () => {
...
});
这样,我们可通过普通的链接标签来实现复杂的业务逻辑。
Script
当显示一个网页时,涉及到以下几个环节:
下载网页
解析网页的DOM 元素
将DOM 元素添加至DOM 树
下载JavaScript 文件
执行JavaScript 文件
显示网页
而下载JavaScript 文件总会需要一定的时间。此时就有这样一个问题:在脚本下载完毕前、或在脚本下载完毕后但尚未执行脚本前,是否需要暂时中止其他网页元素的解析?
以上问题,由script 标签的async 或defer 属性来控制。而这两个属性,又根据脚本是普通脚本或是模块脚本,又有不同的规则及含义。
普通脚本可以带有async 或defer 属性,而模块脚本只有async 属性。
asyncdefer (图像文件摘自whatwg.org )
普通脚本
没有async 或defer 属性时:
中止网页解析,下载脚本
等待脚本下载完毕,立即运行脚本
恢复解析网页其他元素
script 标签的defer 属性有以下作用:
边下载脚本,边解析网页其他元素
脚本下载完毕后,须等待解析网页其他元素后,再运行脚本
按网页中所声明脚本的顺序来依序运行脚本
script 标签的async 属性有以下作用:
边下载脚本,边解析网页其他元素
脚本下载完毕后,中止解析网页其他元素,立即运行脚本
脚本运行完毕后,再恢复解析网页其他元素
脚本运行顺序不受其他脚本影响
模块脚本
对于模块脚本,不能使用defer 属性。
当没有async 属性时:
边下载脚本,边解析网页其他元素
脚本下载完毕,须等待解析网页其他元素后,再运行脚本
当有async 属性时:
边下载脚本,边解析网页其他元素
脚本下载完毕后,中止网页解析,立即运行脚本
脚本运行结束后,再继续解析网页其他元素