第2章 词法结构
编程语言的词法结构是一套基础性规则,用来描述如何使用这门语言来编写程序。作为语法的基础,它规定了诸如变量名是什么样的、怎么写注释,以及程序语句之间如何分隔等规则。
2.1 字符集
JavaScript 程序是用 Unicode字符集编写的。Unicode 是 ASCII 和 Latin-1 的超集,并支持地球上几乎所有在用的语言。ECMAScript 3要求JavaScript 的实现必须支持 Unicode 2.1 及后续版本,ECMAScript 5则要求支持Unicode 3及后续版本。
var hzh1 = "黄子涵";
var hzh2 = "황자함";
var hzh3 = "жёлтая оболочка";
var hzh4 = "หวง";
var hzh5 = "هوانغ زي هان";
var hzh6 = "हुआन जिहानchina. kgm";
var hzh7 = "হুয়াং জিহান";
console.log(hzh1);
console.log(hzh2);
console.log(hzh3);
console.log(hzh4);
console.log(hzh5);
console.log(hzh6);
console.log(hzh7);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
黄子涵
황자함
жёлтая оболочка
หวง
هوانغ زي هان
हुआन जिहानchina. kgm
হুয়াং জিহান
[Done] exited with code=0 in 0.547 seconds
2.1.1 区分大小写
JavaScript 是区分大小写的语言。也就是说,关键字、变量、函数名和所有的标识符( identifier )都必须采取一致的大小写形式。比如,关键字“while”必须写成 “while”,而不能写成,“While”或者“WHILE”。同样,"online”、“Online”、 “OnLine”和“ONLINE”是4个不同的变最名。
var hzh = 1;
var hzH = 2;
var hZh = 3;
var Hzh = 4;
var HZh = 5;
var HZH = 6;
console.log("hzh = " + hzh);
console.log("hzH = " + hzH);
console.log("hZh = " + hZh);
console.log("Hzh = " + Hzh);
console.log("HZh = " + HZh);
console.log("HZH = " + HZH);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh = 1
hzH = 2
hZh = 3
Hzh = 4
HZh = 5
HZH = 6
[Done] exited with code=0 in 0.219 seconds
但需要注意的是,HTML 并不区分大小写(尽管 XHTML 区分大小写)。由于它和客户端 JavaScript 联系紧密,因此这点区别很容易混淆。许多客户端 JavaScript 对象和属性与它们所表示的 HTML 标签和属性同名。在 HTML 中,这些标签和属性名可以使用大写也可以是小写,而在 JavaScript 中则必须是小写。例如,在 HTML 中设置事件处理程序时,onclick 属性可以写成 onClick,但在 JavaScript 代码(或者 XHTML 文档)中,必须使用小写的 onclick。
2.1.2 空格、换行符和格式控制符
JavaScript 会忽略程序中标识(token)之间的空格。多数情况下,JavaScript 同样会忽略换行符。由于可以在代码中随意使用空格和换行,因此可以釆用整齐、一致的缩进来形成统一的编码风格,从而提高代码的可读性。
除了可以识别普通的空格符(\u0020),JavaScript还可以识别如下这些表示空格的字符:水平制表符(\u0009)、垂直制表符(\u000B),换页符(\u000C)、不中断空白(\u00A0)、字节序标记(\uFEFF),以及在Unicode中所有Zs类别的字符。JavaScript将如下字符识别为行结束符:换行符(\u000A),回车符(\u000D),行分隔符(\u2028),段分隔符(\u2029)。回车符加换行符在一起被解析为一个单行结束符。
console.log("以下这些是行结束符:");
console.log("黄子涵\u000A是帅哥!");
console.log("");
console.log("黄子涵\u000D是靓仔!");
[Running] node "e:\HMV\JavaScript\JavaScript.js"
以下这些是行结束符:
黄子涵
是帅哥!
黄子涵
是靓仔!
[Done] exited with code=0 in 0.174 seconds
console.log("测试一下单行结束符:");
console.log("黄子涵\u000A\u000D是帅哥!");
console.log("*************************");
console.log("黄子涵\u000A\u000D是靓仔!");
[Running] node "e:\HMV\JavaScript\JavaScript.js"
测试一下单行结束符:
黄子涵
是帅哥!
*************************
黄子涵
是靓仔!
[Done] exited with code=0 in 0.172 seconds
Unicode格式控制字符(Cf类),比如“从右至左书写标记”(\u200F)和“从左至右书写标记”(\u200E),控制着文本的视觉显示,这对于一些非英语文本的正确显示来说是至关重要的,这些字符可以用在 JavaScirpt 的注释、字符串直接量和正则表达式直接量中,但不能用在标识符(比如,变量名)中。但有个例外,零宽连接符(\u200D)和零宽非连接符(\uFEFF)是可以出现在标识符中的,但不能作为标识符的首字符。字节序标记格式控制符(\uFEFF)被当成了空格来对待。
2.1.3 Unicode转义序列
在有些计算机硬件和软件里,无法显示或输入 Unicode 字符全集。为了支持那些使用老旧技术的程序员,JavaScript 定义了一种特殊序列,使用 6 个 ASCII 字符来代表任意 16 位 Unicode 内码。这些 Unicode 转义序列均以
\u
为前缀,其后跟随4个十六进制数(使用数字以及大写或小写的字母A ~ F表示)。这种 Unicode 转义写法可以用在 JavaScript 字符串直接量、正则表达式直接量和标识符中(关键字除外)。例如,字符 e 的 Unicode 转义写法为\u00E9
,如下两个 JavaScript 字符串是完全一样的:
console.log("Unicode转义序列:");
console.log("caf\u00E9");
[Running] node "e:\HMV\JavaScript\JavaScript.js"
Unicode转义序列:
café
[Done] exited with code=0 in 0.179 seconds
Unicode 转义写法也可以出现在注释中,但由于 JavaScript 会将注释忽略,它们只是被当成上下文中的 ASCII 字符处理,而且并不会被解析为其对应的 Unicode 字符。
2.1.4 标准化
Unicode 允许使用多种方法对同一个字符进行编码。比如,字符“e”可以使用Unicode字符
\u00E9
表示,也可以使用普通的ASCII字符e跟随一个语调符\u0301
。在文本编辑器中,这两种编码的显示结果一模一样,但它们的二进制编码表示是不一样的,在计算机里也不相等。
console.log("多种方法对同一个字符进行编码:");
console.log("\u00E9");
console.log("e\u0301");
console.log("看看它们是不是严格相等:");
console.log("\u00E9" === "e\u0301");
[Running] node "e:\HMV\JavaScript\tempCodeRunnerFile.js"
多种方法对同一个字符进行编码:
é
é
看看它们是不是严格相等:
false
[Done] exited with code=0 in 0.669 seconds
Unicode 标准为所有字符定义了一个首选的编码格式,并给出了一个标准化的处理方式将文本转换为一种适合比较的标准格式,JavaScript 会认为它正在解析的程序代码已经是这种标准格式,不会再对其标识符、字符串或正则表达式作标准化处理。
2.2 注释
JavaScript 支持两种格式的注释。在行尾“//”之后的文本都会被 JavaScript 当做注释忽略掉的。此外,“
/*
”和“*/
”之间的文本也会当做注释,这种注释可以跨行书写,但不能有嵌套的注释。下面都是合法的JavaScript注释:
console.log("注释:");
// 这里是单行注释
console.log("上面注释被忽略就只显示:黄子涵是帅哥!");
/* 这样是一段注释 */ // 这里是另一段注释
console.log("上面注释被忽略就只显示:黄子涵是靓仔!");
/*
* 这又是一段注释
* 这里的注释可以连写多行
*/
console.log("上面注释被忽略就只显示:黄子涵真厉害!");
[Running] node "e:\HMV\JavaScript\JavaScript.js"
注释:
上面注释被忽略就只显示:黄子涵是帅哥!
上面注释被忽略就只显示:黄子涵是靓仔!
上面注释被忽略就只显示:黄子涵真厉害!
[Done] exited with code=0 in 0.248 seconds
2.3 直接量
就是程序中直接使用的数据值。下面列出的都是直接量:
12 //数字
1.2 //小数
"hello world" //字符串文本
"hello world" //另一个字符串
true //布尔值
false //另一个布尔值
/javascript/gi //正则表达式直接量(用做模式匹配)
null //空
更多复杂的表达方式可以写成数组或对象直接量,例如:
{ x:l, y:2 } //对象
[1,2,3,4,5] //数组
2.4 标识符和保留字
标识符就是一个名字。在 JavaScript 中,标识符用来对变量和函数进行命名,或者用做 JavaScript 代码中某些循环语句中的跳转位置的标记。JavaScript标识符必须以字母、下划线(
_
)或美元符($
)开始。后续的字符可以是字母、数字、下划线或美元符(数字是不允许作为首字符出现的,以便JavaScript可以轻易区分开标识符和数字)。下面是合法的标识符:
var i = 1;
var my_variable_name = 2;
var V13 = 3;
var _dummy = 4;
var $str = 5;
console.log("看能不能把上面的变量输出:");
console.log("i = " + i);
console.log("imy_variable_name = " + my_variable_name);
console.log("V13 = " + V13);
console.log("_dummy = " + _dummy);
console.log("$str = " + $str);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
看能不能把上面的变量输出:
i = 1
imy_variable_name = 2
V13 = 3
_dummy = 4
$str = 5
[Done] exited with code=0 in 0.296 seconds
出于可移植性和易于书写的考虑,通常我们只使用 ASCII 字母和数字来书写标识符。然而需要注意的是,JavaScript 允许标识符中出现 Unicode 字符全集中的字母和数字。(从技术上讲,ECMAScript 标准也允许在标识符的首字符后面出现 Unicode 字符集中的Mn类、Me类和Pc类)。由此,程序员也可以使用非英语语言或数学符号来书写标识符:
var si = true;
var n = 3.14;
和其他任何编程语言一样,JavaScript保留了一些标识符为自己所用。这些“保留字”不能用做普通的标识符,下面会讲到。
保留字
JavaScript把一些标识符拿出来用做自己的关键字。因此,就不能再在程序中把这些关键字用做标识符了:
break delete function return typeof
case do if switch var
catch else in this void
continue false instanceof throw while
debugger finally new true with
default for null try
var do = "黄子涵";
console.log(do);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:1
var do = "黄子涵";
^^
SyntaxError: Unexpected token 'do'
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.677 seconds
JavaScript同样保留了一些关键字,这些关键字在当前的语言版本中并没有使用,但在未来版本中可能会用到。ECMAScript 5保留了这些关键字:
class const enum export extends import super
此外,下面这些关键字在普通的JavaScript代码中是合法的,但是在严格模式下是保留字:
implements let private public yield interface package protected static
严格模式同样对下面的标识符的使用做了严格限制,它们并不完全是保留字,但不能用 做变量名、函数名或参数名:
arguments eval
'use strict'
var eval = "黄子涵";
console.log(eval);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:2
var eval = "黄子涵";
^^^^
SyntaxError: Unexpected eval or arguments in strict mode
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 1.128 seconds
ECMAScript 3将 Java 的所有关键字都列为自己的保留字,尽管这些保留字在ECMAScript 5 中放宽了限制,但如果你希望代码能在基于 ECMAScript 3 实现的解释器上运行的话,应当避免使用这些关键字作为标识符:
abstract double goto native static
boolean enum implements package super
byte export import private synchronized
char extends int protected throws
class final interface public transient
const float long short volatile
JavaScript预定义了很多全局变量和函数,应当避免把它们的名字用做变量名和函数名:
arguments encodeURI Infinity Number RegExp
Array encodeURIComponent isFinite Object String
Boolean Error isNaN parseFloat SyntaxError
Date eval JSON parseInt TypeError
decodeURI EvalError Math RangeError undefined
decodeURIComponent Function NaN ReferenceError URIError
JavaScript 的具体实现可能定义独有的全局变量和函数,每一种特定的JavaScript 运行环境(客户端、服务器端等)都有自己的一个全局属性列表,这一点是需要牢记的。
2.5 可选的分号
和其他许多编程语言一样,JavaScript 使用分号(;)将语句分隔开。这对增强代码的可读性和整洁性是非常重要的:缺少分隔符,一条语句的结束就成了下一条语句的开始,反之亦然。在 JavaScript 中,如果语句各自独占一行,通常可以省略语句之间的分号(程序结尾或右花括号“}”之前的分号也可以省略)。许多 JavaScript 程序员使用分号来明确标记语句的结束,即使在并不完全需要分号的时候也是如此。另一种风格就是,在任何可以省略分号的地方都将其省略,只有在不得不用的时候才使用分号。不管采用哪种编程风格,关于 JavaScript 中可选分号的问题有几个细节需要注意。
考虑如下代码,因为两条语句用两行书写,第一个分号是可以省略掉的:
a = 3;
b = 4;
var hzh1 = 1
var hzh2 = 2;
console.log('将上面的变量输出:');
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
将上面的变量输出:
hzh1 = 1
hzh2 = 2
[Done] exited with code=0 in 0.537 seconds
如果按照如下格式书写,第一个分号则不能省略掉:
a = 3; b = 4;
var hzh1 = 1 var hzh2 = 2;
console.log('将上面的变量输出:');
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:1
var hzh1 = 1 var hzh2 = 2;
^^^
SyntaxError: Unexpected token 'var'
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47
[Done] exited with code=1 in 0.182 seconds
var hzh1 = 1; var hzh2 = 2;
console.log('将上面的变量输出:');
console.log("hzh1 = " + hzh1);
console.log("hzh2 = " + hzh2);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
将上面的变量输出:
hzh1 = 1
hzh2 = 2
[Done] exited with code=0 in 0.173 seconds
需要注意的是,JavaScript 并不是在所有换行处都填补分号:只有在缺少了分号就无法正确解析代码的时候,JavaScript 才会填补分号。换句话讲(类似下面代码中的两处异常),如果当前语句和随后的非空格字符不能当成一个整体来解析的话,JavaScript 就在当前语句行结束处填补分号。看一下如下代码:
var a
a
3
console.log(a)
JavaScript 将其解析为:
var a; a = 3; console.log(a);
JavaScript 给第一行换行处添加了分号,因为如果没有分号,JavaScript 就无法解析代码
var a a
。第二个a可以单独当做一条语句“a;
”,但 JavaScript 并没有给第二行结尾填补分号,因为它可以和第三行内容一起解析成“a=3;
”。
var a
a
3
console.log("a = " + a);
console.log("");
var b; b = 3; console.log("b = " + b);
console.log("上面两种情况JS是不是解析成一样?");
console.log(a === b);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
a = undefined
b = 3
上面两种情况JS是不是解析成一样?
false
[Done] exited with code=0 in 0.179 seconds
这些语句的分隔规则会导致一些意想不到的情形,这段代码写成了两行,看起来是两条独立的语句:
var y = x + f
(a+b).toString()
但第二行的圆括号却和第一行的f组成了一个函数调用,JavaScript会把这段代码看做:
var y = x + f(a+b).toString();
而这段代码的本意并不是这样。为了能让上述代码解析为两条不同的语句,必须手动填写行尾的显式分号。
通常来讲,如果一条语句以“
(
”、"[
”、“/
”、“+
”或“-
”开始,那么它极有可能和前一条语句合在一起解析。以“/
”、“+
”和开始的语句并不常见,而以“(
”和开始的语句则非常常见,至少在一些JavaScript编码风格中是很普遍的。有些程序员喜欢保守地在语句前加上一个分号,这样哪怕之前的语句被修改了、分号被误删除了,当前语句还是会正确地解析:
var x = 0 //这里省略了分号
;[x,x+i,x+2].forEach(console.log) //前面的分号保证了正确地语句解析
var i = 1;
var x = 0 //这里省略了分号
;[x,x+i,x+2].forEach(console.log) //前面的分号保证了正确地语句解析
[Running] node "e:\HMV\JavaScript\JavaScript.js"
0 0 [ 0, 1, 2 ]
1 1 [ 0, 1, 2 ]
2 2 [ 0, 1, 2 ]
[Done] exited with code=0 in 0.2 seconds
如果当前语句和下一行语句无法合并解析,JavaScript 则在第一行后填补分号,这是通用规则,但有两个例外。第一个例外是在涉及 return、break 和 continue 语句的场景中。如果这三个关键字后紧跟着换行,JavaScript 则会在换行处填补分号。例如,这段代码:
return
true;
JavaScript会解析成:
return; true;
而代码的本意是这样:
return true;
也就是说,在 return, break 和 continue 和随后的表达式之间不能有换行。如果添加了换行,程序则只有在极特殊的情况下才会报错,而且程序的调试非常不方便。第二个例外是在涉及“++”和“--”运算符的时候。这些运算符可以作为表达式的前缀,也可以当做表达式的后缀。如果将其用做后缀表达式,它和表达式应当 在同一行。否则,行尾将填补分号,同时“++”或"--”将会作为下一行代码的前缀 操作符并与之一起解析,例如,这段代码:
X
++
y
这段代码将解析为“
x; ++y
”,而不是“x++; y
"。
var X = 1;
var y = 2;
X
++
y
console.log("X = " + X);
console.log("y = " + y);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
X = 1
y = 3
[Done] exited with code=0 in 0.209 seconds
var X = 1;
var y = 2;
X; ++y
console.log("X = " + X);
console.log("y = " + y);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
X = 1
y = 3
[Done] exited with code=0 in 0.179 seconds