在刘慈欣的科幻小说《三体》中,人类科学家遭遇了一个前所未有的困境:来自三体世界的智子施加干扰,使得高能物理实验的结果变得混乱不堪,仿佛整个物理法则被悄然篡改,让原本铁律般的科学定律变得不再可信。这种状况引发了科学界的巨大恐慌,甚至有人宣称“物理学不存在了”。—— 那假如一段简单如 Hello world 的代码也出现了意料之外的运行结果,你是否还会相信代码世界的秩序和逻辑?
当然,至少到目前为止,小说中的情节并没有变为现实。所以让我们睁大眼睛,看以下 JavaScript 代码及其运行结果中,是哪里出现了问题。
一段“不可思议”的代码
let а = 1;
(() => {
let a = 1;
а -= 10;
console.log('local a =', a);
})();
console.log('global a =', а);
乍一看,这段代码可能被用来演示 JavaScript 的变量作用域:变量 a
被声明了两次,一次在全局作用域中,另一次在匿名函数的局部作用域中。这两个变量虽然名字相同,但是它们存在于不同的作用域中,是相互独立的。在函数作用域内部对变量 a
进行的修改不会影响全局作用域中的变量 a
。有编程基础的朋友应该可以指出这段代码的输出应是:
local a = -9
global a = 1
但假如你现在得知,这段代码的执行结果实际上是:
local a = 1
global a = -9
这是否出乎你的意料?不过幸运的是,这是一个可以复现的问题,你可以尝试执行这段代码,验证我所言非虚。
假如眼睛欺骗了你
如果你没有注意到全局作用域的 а
实际上和函数作用域的 a
是两个不同的字符,那么你可能已经落入了这个陷阱:代码中混入了 西里尔小写字母 а(U+0430)。它看起来与我们常用的 拉丁文小写字母 a(U+0061)极为相似,但在计算机的眼中,它们是两个不同的字符。
名称 | 字符 | Unicode 码点 |
---|---|---|
Cyrillic Small Letter A | а | U+0430 |
Latin Small Letter A | a | U+0061 |
在允许使用非 ASCII 字符作为变量名的 JavaScript 中,也对应着两个不同的变量名。因此,在匿名函数中,实际上既可以访问到全局变量 а
,也可以访问到局部变量 a
。
const a = 1;
const а = 2;
console.log(a); // 1
console.log(а); // 2
所以谜底是?
为了便于阅读,以下内容会对字母 a
进行染色。
- 西里尔小写字母 а 会用 红色 表示
- 拉丁文小写字母 a 会用 蓝色 表示
如果你还没能搞清楚状况,我们可以一起睁大眼睛,逐行阅读这段代码:
- 第 1 行
let а = 1;
:声明了一个名为 а 的全局变量,并对其赋初始值1
。 第 2~6 行是一个立即执行函数表达式(IIFE),函数内声明的变量属于函数内的局部作用域
- 第 3 行
let a = 1;
:声明了一个名为 a 的局部变量,并对其赋初始值1
。 - 第 4 行
а -= 10;
:将全局变量 а 的值减少10
(注意,由于变量名不同,所以实际上修改的不是局部变量 a) - 第 5 行
console.log('local a =', a);
:将局部变量 a 的值打印出来
- 第 3 行
- 第 7 行
console.log('global a =', а);
:将全局变量 а 的值打印出来
我们可以进行一下字符替换,将不常用的西里尔小写字母 а 替换为 alpha
来提高这段代码的可读性。替换后的代码如下,它的流程和逻辑与原始代码一致。相信此时大家都能正确地指出它的输出了。
let a = 1;
(() => {
let alpha = 1;
a -= 10;
console.log('local a =', alpha);
})();
console.log('global a =', a);
有请 "Sherlock VSCode"
如果我们将这段“不可思议”的代码用 VSCode 打开,VSCode 会对其中易混淆的非 ASCII 字符进行标注和提示。
字符 U+0430 "а" 可能会与 ASCII 字符 U+0061 "a" 混淆,后者在源代码中更为常见。
让一切回归既有秩序
将西里尔小写字母 а 替换为拉丁文小写字母 a 后,我们可以得到最初所预期的执行结果。
let a = 1;
(() => {
let a = 1;
a -= 10;
console.log('local a =', a);
})();
console.log('global a =', a);
它会输出:
local a = -9
global a = 1
谢天谢地,又是无 bug 的一天😆。
试了一下,控制台
'a' === 'а' //false
突然想到古早一张图,this is Javascript