第二章 词法结构
词法结构
词法结构是一门语言最基础的规则,是最低级的语法,本章主要讲解以下内容:
- JavaScript的大小写与空格和换行符
- 注释
- 标识符和保留字
- Unicode编码
- 可选分号
JavaScript的大小写与空格和换行符
JavaScript严格区分大小写,这不必多说,就像a和A是两个不同的变量,除了这个特性,还需要知道的是,JavaScript很大程度上会忽略换行符,所以你可以在代码中随意使用换行符和空格,此空格的Unicode编码为\u0020,难道空格还有不同的编码吗?有的,在Unicode中有三种不同的空格,他们分别为:
- 不间断空格\u00A0,主要用在office中,让一个单词在结尾处不会换行显示,快捷键ctrl+shift+space ;
- 半角空格(英文符号)\u0020,代码中常用的;
- 全角空格(中文符号)\u3000,中文文章中使用;
除了常规的英文空格(\u0020)以外,JavaScript还将制表符、ASCII控制符、Unicode间格识别为空格,并且将换行符、回车符识别为终止符,所以你可在JavaScript中不写 “;” 通过回车符和换行符来表示语句的结束。但是需要注意,这个“;”在某些情况下是必须要加的,后面会讲JavaScript对不加分号的代码是如何处理的。
注释
在JavaScript中主要有两种注释方法
- //:双斜杠,用于一行注释。
- /**/: 用于多行注释。
说到注释,让我想起了函数的函数注释,用于描述函数的参数和返回值,以方便代码编辑器解析给用户更好的提示作用,像下面这样
/**
* 求两个数的和
* @param {number} a 第一个数
* @param {number} b 第二个数
* @returns {number} 两数之和
*/
效果像这样
当然上面这个为题外话,你只要知道JavaScript提供了两种注释即可。
比较有意思的是,多行注释可以出现在任意地方,你甚至可以像下面这样写:
let a = /*多行注释可以写在任意位置*/ 1 // 参见于《你不知道的JavaScript下卷》
字面量
字面量值的是出现在程序中最基本的最直接的数据值比如: 1、 "hello world"、 {}、 []、 true、 null、 都是字面量
标识符与保留字
标识符就是一个名字或者变量名,在JavaScript中标识符必须以字母、下划线(_)或者美元($)开头。
JavaScript为语言自身保留了些标识符,有如下一些:
保留字 | 描述 | 备注 |
---|---|---|
as | JavaScript中暂无语义 TypeSript中用于声明类型 | |
const | 声明常量 | |
export | JavaScript模块 导出 | |
get | 属性的获取方法 | |
null | 空 可用于清空指针 便于垃圾回收 | |
target | 暂无语义 | |
void | 空 用于声明函数的返回值 | 因为window对象身上有undefined属性,为了安全 一般用void(0)来代替undefined |
await | 异步等待 | |
continue | 跳过循环 | |
extends | 继承 | |
if | if语句 | |
of | 循环语句 | |
this | 对象指针 | |
while | 循环语句 | |
async | 包装函数为Promise | |
debugger | 断点 | |
false | 布尔值 | |
import | 模块导入 | |
return | 函数返回值 | |
throw | 抛出异常 | |
with | 用于创建临时作用域 使用with语句代码难优化应当废弃 | |
break | 跳出语句 | |
default | 默认,常见于switch语句中 | |
finally | 最终,常见于catch语句中 | |
in | 遍历属性 | |
set | 属性获取方法,与get应为一对 | |
true | 布尔值 | |
yield | 生成器语句 | |
case | switch语句 | |
delete | 删除属性 | |
for | 循环 | |
instanceof | 查看某对象是否由某类创建 | instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可 |
static | 静态属性 用于声明类的静态属性,也可称之为类方法 | 静态方法是作为构造函数而非原型对象的属性定义的 |
try | 异常 | |
catch | 异常捕获 | |
do | 常见do while语句 | |
from | 常见于模块导入 | |
let | 块作用域变量声明 | |
super | 父类引用 | |
typeof | 类型判断 | |
class | 声明类 | |
else | 常见if else | |
function | 声明函数 | |
new | 创建对象 | |
switch | switch语句 | |
var | 声明变量(不推荐) |
上面这些保留字严格意义上来说都是不应该被使用的(对象作属性名除外),但是部分可以在没有语法歧义的时候是可以被安全使用的,如(form、 of、 get、set ),另外像from、set、target这样的安全且常见的依然可是作变量。 另外、下面这些关键字也最好不要做变量名:
关键字 |
---|
enum |
implements |
interface |
package |
private |
protected |
public |
eval |
arguments |
这些关键字大多在TypeScript具有语义,而eval和arguments由于历史原因,不建议做变量名。
Unicode
Unicode字符范围从0x0000到0xffff,包含了可能见到的所有语言的字符,这组字符称之为“基于多语言平面(BMP)”在BMP之外,还能扩展到0X10FFFF,这些字符称之为“星型符号(astral symbol)”(BMP之外的16个平面)。
在ES6之前,可以通过Unicode转义符指定JavaScript字符串为Unicode字符:
let snoman = "\u2603"
console.log(snoman) //☃
不难看出,这种方式只支持4个16进制的字符形式,所以此方式只支持BMP字符集,而要用其表示astral字符集,则需要将两个计算出来的Unicode转义字符解释为单个astral字符,像下面这样:
let gclef = "\uD834\uDD1E"
console.log(gclef) // 𝄞
在ES6之后,有了将Unicode转义的新方式,称之为“Unicdoe 码点转义(code point escaping)”,支持在其中包含 任意多个16进制字符 像这样:
let gclef = "\u{1D11E}"
console.log(gclef) // 𝄞
不难看出,上面我们使用了两个Unicode编码来表示了一个字符:“𝄞”,但是如果要问你作为字符,那么他的长度(length)是多少呢,没了解过的还真不好回答,其实,从专业角度上来说,像这样我们视觉上看到的渲染出来的单个字符更精确的叫法应该叫 字素 ,而字符更多是在程序意义上处理的,JavaScript字符串运算是不能识别出astral符号的,只会单独处理每个BMP,没错,上面我们使用了两个Unicode编码对来渲染成了一个字素“𝄞”,而JavaScript会认为这是两个BMP字符,所以,“𝄞”的长度应该是2:
let g = "\uD834\uDD1E"
console.log(g,g.length) // 𝄞 2
那么问题来了,如何处理这种情况呢?总不能以后我算字符长度都算不对吧?一种方法是使用ES6字符迭代器,利用其 迭代器能够精确识别Unicode的特点(后面章节会讲),即迭代器能够将astral符号作为单个值输出,利用这一点我们就能够精确识别其长度了,像这样:
let g = "\uD834\uDD1E"
console.log([...g].length) // 1
另一种方法是使用Array.from()方法,其实他们本质一样的。
你以为这就完了吗?别急,还有好戏。
有一组Unicode会修改前面相邻字符的音标,称之为 组合音标符号(Combine Diacritical Mark),比如:
let e2 = "e\u0301"
console.log(e2) // é 好像给e加了2声音标
这道没啥,很好理解,离谱就离谱在在astral中存在一种字符和他的字素长得一模一样:
let astral_e2 = "\u{e9}" // 也可以写成 “\xE9”
console.log(astral_e2) // é
但是由于两者的编码不同,对应的二进制不同,所以JavaScript并不认为他俩相同,
let e1 = "\u00E9"
let e2 = "e\u0301"
console.log(e1,e2,e1 === e2) // é é false
问题就来了,由于上面的e2变量指向的“é”是由字符e和组合音标符号组合而成,所以使用字符串迭代器会被认为是两个字符,即“é”的长度为2:
console.log([...e2].length) // 2
console.log([...e1].length) // 1
可是字素明明就是相同,却因为编码方式不一样从而导致字符长度不一样,这个时候怎么办呢。
ES6为字符串提供了一个normalize()方法,这个方法会对字符串进行 “Unicode标准化”,可见Unicode官网对此的描述。总的来说就是会将多个Unicode序列合并为一个能够用一个Unicode识别的序列,例如:
let e2 = "e\u0301" // é
console.log(e2.normalize().length) // 此方法将"e\u0301" 标准化为 “\xE9” 所以长度为1
但是这样也有问题,比如用多个组合符号修改了单个字符:
let e2 = "e\u0301\u0330"
console.log(e2) // ḛ́
console.log(e2.normalize().length) // 2
而normalize()方法并不能处理这样的情况,所以要真正获取真实的字符长度,目前基本是无解的。
如果你想了解更多相关知识,可参见“字素族界限算法”
可选的分号
JavaScript可使用“;”来作为分隔符,如果两条语句分别写在两行,一般来说都是可以省略他们之间的分号的(可用换行符充当分号)。 但是也有例外,在介绍例外前,你有必要知道JavaScript在无分号的时候解释器是如何处理的: JavaScript并非任何时候都把换行符当作分号处理,只是在不隐式添加分号就无法解析代码的时候才这么做,即JavaScript只在下一个非空格字符无法被解释为当前语句的一部分的时候才把换行符当分号处理。
比如:
let a // 此处如果不自定加分号 则会解析失败,所以解释器隐式加了分号
a // 此处JavaScript加不加分号都能解析,但是他会贪婪的尽量多解释语句,即他能解析更长的语句a = 3
=
2 // 此处不隐式添加分号会解析失败
console.log(a)
被解释为:
let a; a = 3; console.log(a)
上面示例相信很清楚的解释了这个过程,不过还有些情况你必须要加分号,才能正确被解析:
let x = a + f
(c+b).toString()
上面代码会被解释器理解为:
let x = a+f(c+b).toString
很明显与代码作者意图不同,所以此情况需要添加合适的分号:
let x = a + f ; // 需要添加分号
(c+b).toString()
另外,JavaScript对换行符的解释有三种例外:
(1) 涉及return、throw、yield、break、continue语句时,如果这些边师傅后面有换行符,则会把这个换行符解释为分号,例如:
return
true;
会被解释为:
return ; true;
(2) 涉及++和--操作符时,这些操作符既可以放在表达式前,也可以放在表达式后,若要想把这些操作符作为后置操作符,则它们必须和操作的表达式在同一行:
++
a //错误
++a //正确
(3) 涉及箭头函数时,箭头=>必须和参数在同一行
(...)
=>{...} // 错误
()=>{} //正确
欢迎关注、点赞、评论,你的每一次互动都是我输出更多优质文章的动力!
转载自:https://juejin.cn/post/7270061728896876602