2.4 JavaScript的语法: 语句
JS是一门在执行过程中以表达式求值为核心设计的语言
语句:是代码语法分析中的核心元素,通常是单行语句,或者由一对大括号括起来的复合语句,在语法描述中,复合语句可以整体作为一个单行语句处理
这里有两个原则
- 语句由语法符号
;
来分隔 - 一些语句存在返回值
2.4.1 表达式语句
下边的代码显然是表达式
1+2+3
在后边加一个分号1+2+3;
就成为了表达式语句
JS中,许多语法和语义最终都是由”表达式语句“来实现的,例如赋值、函数调用、以及我们在语言中常见的”调用对象方法“。由于表达式运算总是有值的——这包括NaN
和undefined
因此表达式语句也总是有值
2.4.1.1 一般表达式语句
单值后边加分号就是单值表达式
evel()
会返回”最后执行到的、有返回值“的那条语句的值
JS允许空语句,但是使用的时候一定要加好注释,否则代码审查将无法清晰地理解使用该技术的意图
2.4.1.2 赋值语句和隐式的变量声明
赋值语句在JS中也是典型的表达式语句,是”赋值表达式运算“的一种效果,并且可以继续参与运算
str2 = 'this is a ' + (str = 'test string')
可以加一个分号表明这是一个表达式语句
赋值表达式具有隐式声明变量的功能,但是会声明到全局上,视为局部变量的”泄露“,严格模式不允许向未声明的变量直接赋值
2.4.1.3 函数调用语句
JS中的函数本身是一个变量/值,所以函数调用也是一个表达式
函数调用方法
// 具名函数直接调用
function a() {
// ...
}
a();
// 匿名函数通过引用来调用
b = function () {
// ...
}
b();
// 非引用匿名函数的调用方法1
(function () {
// ...
}());
// 非引用匿名函数的调用方法2
(function () {
// ...
})()
// 非引用匿名函数的调用方法3
void function() {
// ...
} ();
匿名函数调用方法1和2的运算过程是不同的
方法一中,用分组运算符使函数调用得以执行
方法二中,用分组运算符使”函数字面量“这个表达式求职,并返回一个函数自身的引用,然后通过函数调用运算符()
来操作这个函数的引用,换言之,”函数调用运算符()
“在方法一中作用于匿名函数本身,方法二中却作用于一个运算的结果值
不能不加小括号直接调用,例如
function() {
// ...
}(1,2)
// 这个是能通过语法检测的,但是并不能正确运算,因为被解释成了
function () {
// ...
};
(1,2);
类似的技巧也可以在箭头函数中使用,箭头函数通常可以在表达式中声明,并在声明后立即调用,但是需要一个阔号来将箭头函数声明作为一个操作数
> x = 1 + (y => y + 2)(5) + 10
18
2.4.2 变量声明语句
纯粹的”变量声明语句“的语义,只是声明该变量名字(而无视它的初始赋值)
var a = 1
在JS中,将该语句分为”变量声明 + 赋值语句“在两个不同阶段中处理,var a
作为变量声明在语法解析阶段就被处理,a = 1
在执行阶段处理,通过赋值向变量名a绑定具体的值,严格来说,JS中所有显式的数据声明都是按这种处理方法来实现的,包括var let const
声明、函数声明等
除此之外,具名函数(包括生成器和类)的声明,函数形式参数名的声明,以及在try catch
分支中捕获异常的那个变量名都属于显式声明的范畴,不再详述
2.4.3 分支语句
和C语言差不多,但是不幸的是,C语言的分支语句和其他语言差别很大,hhh
2.4.3.1 条件分支语句(if语句)
if (condition)
statement1
else
statement2
statement1、statement2在语法上标明是”语句“,由于”在语法描述中,复合语句可以整体作为一个单行语句处理“,因此大括号是复合语句中的语法符号,不是if语句的语法元素
if ... else if ...
这样的格式并非是”一种语法的变种“,只不过else子句中的statement2是一个新的、单行的if语句而已
2.4.3.2 多重分支语句(switch语句)
switch(x) {
case 100:
j++;
i += j;
break;
case 200:
// ...
}
关键字case
只需要标识分支的入口点,而无需标识这些分支的开始和结束,所以在case
分支中通常是直接书写多行代码而无需大括号。
一定要注意不要忘了加break;
,它之前的上一个语句最好加分号,如果需要特殊操作需要省略break;
要在旁边注释好
2.4.4 循环语句
for循环、while循环、do…while循环
循环条件都应当是有意义的,少数情况放true
表示无限循环
循环体可以使用空语句,但是除了展示技巧以外并无益处
除了这三种还有几种用于对象成员列举的循环语句,后边会说
2.4.5 流程控制:一般子句
break、continue、return
都是受上下文限制的,不能随意使用
2.4.5.1 标签声明
JS中的标签就是一个标识符,标签可以和变量名重名而互不影响,因为是一种独立的语法元素,(不是变量也不是类型)
标签不能作用于注释语句、模块导入导出语句、以及函数或者类的声明语句。因为这些语句没有执行含义。
绿宝书上对它的功能描述不多,看不太懂,这篇博客回答了功能性上的问题链接
2.4.5.2 break语句
如果在for、while
语句中使用break
,那么表明停止一个最内层的循环,如果在switch
语句中则直接跳出整个语句
尽管不太常见,但是default
分支中的break在某些情况下是有价值的,例如
default:
if(...) break;
// ...
}
break配合标签可以跳出整个标签(指示了break的作用范围)
2.4.5.3 continue子句
仅对循环语句有意义,表明跳出当前循环并转到下一次循环迭代开始运行
2.4.5.4 return子句
只能在一个函数内,同一个函数允许多个return子句,当函数被调用时,代码执行到第一个return子句则退出该函数并返回return子句所指定的值:当return子句没有指定返回值时,该函数返回undefined
当执行函数的逻辑过程中没有return 会执行到最后一个语句并返回undefined
2.4.6 流程控制:异常
结构化异常处理的语法结构如下:
try {
// tryStatements
}
catch (e) {
// catchStatements
}
finally {
// finallyStatements
};
该处理机制被分为三部分
- 触发异常,使用
throw
语句可以在任何位置触发异常,或者由引擎内部在执行过程中触发异常 - 捕获异常,使用
try...catch
语句可以(在形式上表明)捕获一个代码块中可能发生的异常,并使用expection
来指向该异常的一个引用 - 结束处理,使用
try...finally
可以无视指定代码块中发生的异常,确保finally
中的语句总是被执行
在
try
块中使用return
,finally
中的语句仍会被执行,在catch
块中使用break
,finally
中的语句仍会被执行
如果
throw
出现在finally
语句块中,那么它之后的语句也不能被执行,同理,如果出现了错误后边的语句也不能被执行,尽可能保证finally
中的代码都能安全,无异常的执行
2.5 JavaScript的语法:模块
2.5.1 模块的声明与加载
即使一个js文件没有export
语句,它也可以被其他文件作为模块导入,在这种情况下,JS引擎仍然会为它创建一个空的导出名字表。也就是说,这两种行为——使用export
语句来表明自己是模块,与普通文件作为模块加载——在ECMAScript中式没有差别的
ECMAScript约定只能在模块文件的顶层使用import/export
语句,不能用在if/for
等语句块中,或者将它们用try...catch
语句包括起来
2.5.1.1 加载模块
// 简单装载
import 'module-name';
// 命名导入
import defaultExport from 'module-name';
import { importList } from 'module-name';
// 名字空间导入
import * as aNameSpace from 'module-name';
// 默认导入的扩展形式
import defaultExport, ... from 'module-name';
如果只打算引入一个模块而无视它对外导出的内容,使用简单装载,由于模块可被多次加载并且首次加载之后就被缓存,所以也可以使用简单装载进行预装载模块
装载模块意味着模块中的顶层代码会被执行一次,由于引擎的模块装载系统会静态扫描全部模块并确定装载的次序,所以事实上模块名在import语句中出现和被依赖的次序就成为了顶层代码执行的顺序,后续的模块是基于缓存的,所以顶层的代码不会反复执行
模块可以导出一个没有名字的实体——值或者对象
// 例如:被导入模块(module-name)
export default ...;
// 当前模块
import x from "module-name";
这样就在当前模块中命名了x这个标识符。如果源模块已经声明了x这个名字的导出,可以使用以下语法
// 例如:被导入模块(module-name)
export var x = ...;
// 当前模块
import {x} from "module-name";
或者将这个名字导入成新名字y
// 当前模块
import {x as y} from "module-name";
// 也可以导入一个使用”,“分隔的名字列表
import {x, x as y, z} from "module-name";
或者将所有名字导入一个名字空间
// 得到"moudule-name"的名字空间(唯一实例)
import * as myNames from "module-name";
默认导入和名字导入是两种不同的语法风格,起因于原模块中页存在着两种不同的声明方法,为了简便ECMAScript也约定了可以将两种风格混用的语法,即“默认导入的扩展形式”,例如:
// 混用默认导入和名字导入
import defaultExport, {names...} from 'module-name';
// 混用默认导入和名字空间导入
import defaultExport, * as aNameSpace from "module-name";
2.5.1.2 声明模块
// 导出声明
export let name1, name2=..., ..., nameN; // 包括 var、count 等声明
export function FunctionName() { ... } // 包括具名的函数声明和类声明
// 导出已声明的名字,其中default将作为默认名特殊处理
export {name1, variableName as name2, name as default, ...};
// 将一个数据用默认名导出
export default ...; // 可以是任何数据、变量和声明,包括匿名函数和类声明
// 导出指定模块中的名字(聚合多个模块中的导出名)
export ... from "module-name"; // 支持导出名字、默认名或者整个名字空间
所有在JS中通过声明语法得到的名字都可以被导出,并且export
可以直接加载声明语句之前
// 用默认名导出表达式执行额结果
export deafult 1 + 2
处理这个语句的时候,语法分析阶段只是会在导出表中建立名字default
,而这个名字和值的绑定再执行阶段才会处理,这与JS处理var
是相似的
可以通过当前模块导出其他模块中定义的名字,这称为聚合
// 聚合(并导出)源模块module-name中的名字x或者v
export { x } from "module-name";
export { v as x} from "module-name";
但在使用:
// 聚合(并导出)源模块module-name中的全部名字
export * from "module-name";
这样的语法是,那些被聚合的名字不会重复出现在当前模块额导出表中,只是需要引用某个名字时,先查找自己的导出名字中是否存在,没有再通过一个内部登记项(称为REquestedModules)来索引那些源模块,以实现深度遍历
2.5.2 名字空间的特殊性
在ECMAScript中,名字空间看起来像一个普通对象,并且也可以通过对象成员来存取那些被导出的名字,不过由于名字空间对象的原型是null。所以除了导出名字之外它没有任何多余的成员名
2.5.2.1 名字空间的创建者
具体的JavaScript引擎在装载主模块并开始执行第一行用户代码之前,通过语法分析就可以得出所有模块之间的导出、导入关系。所有通过
export ... from 'module-name';
语法声明聚合的模块都被优先装载,随后是使用类似
import ... from 'module-name';
语法显式指定了导入项的模块,在所有模块依赖关系的深度遍历结束后,JS就会开始向当前模块(主模块)的执行环境中添加import
项所声明的名字,并让导出名(源模块的)与本地模块(当前模块)绑定在一起
在所有其他方式声明的(例如,使用var
声明或者函数声明)的名字创建之前,那些import导入的名字就已经被创建并且绑定了值,然而自此以后,模块依赖的维护工作就结束了,也就是说,主模块根本就没有自己的名字空间
除了这个特例,其他的模块都在他们被import
导入的时候,由JS引擎为之创建了一个对应的名字空间对象,显然,主模块没有被import导入过
2.5.2.2 名字空间中的名字是属性名
不同于导入名,名字空间的名字其实是属性名,可以像对象属性一样操作,因为名字空间本身是一个特殊对象,所以它的属性,也有一些特殊性
属性描述符显示名字是可写的,且名字空间(作为对象)未被冻结
但是删除或者更新属性描述符的操作都将直接返回false
,由于模块总是在严格模式中,所以会引发异常
如果尝试使用Object.defineProperty()
来更新属性描述符,而新的描述符相对于默认值是没有变化的,那么就不会触发异常,如果有变化就会触发异常
当然,我们仍可以列举完整的名字列表
2.5.2.3 使用上的一些特殊属性
导入名先于代码执行被创建,因此可以提前使用
console.log(x) // x是有值的
import x from 'module-name'
使用命名导入,与使用“名字空间+本地变量声明”的效果在表面上是相似的
但是存取方式是有着本质上的差别的
// 例如:被导入模块
export var x = 'good';
// 1. 导入名字空间,使用myNames.x
import * as myNames from 'module-name'
// 2. 导入名字,使用导入名x
import { x } from 'module-name'
// 3. 名字空间+本地变量声明,使用本地名字x2
var {x : x2} = myNames
// 测试
console.log(x, x2, myNames.x) // good good good
对于方法一,参见上一小节,myNames.x
是一个对象属性,但与一般对象属性的操作不同,方法2中x是当前模块中的一个本地名字,它被创建为所谓的“非可变简介绑定”,并关联到目标模块的导出项,这决定该本地名字和常量是相类似的,是不可写的,当读取x的时候发生了操作是:
- 当前模块查找到一个引用名x
- 发现它绑定到了源模块的导出名x
- 会调用源模块的内部操作返回x
所以这是一个源模块的引用,所以对x写操作会报错
但是使用方法三的时候,x2是一个本地变量,是本地环境中的值,而不是引用,所以可以被修改,因为从引用中取值的操作在之前模板赋值的时候就已经发生过了
使用名字空间的一个好处是,如果使用import导入一个名字,但是模块中没有这个名字的导出,就会报错,但是使用名字空间就可以得到undefinded
而不报错
2.6 严格模式下的语法限制
ES5开始支持严格模式,需要使用字符串序列
"use strict"
来开启,可以是一对双引号,也可以是一对单引号,代码中是一个字符串字面量,被用在一段代码的最前面,作为“指示前缀”,可以加入的地方包括
- 全局代码的开始处加入
- 在
eval
代码开始处加入 - 在函数声明代码开始处加入
- 在
new function()
所传入的body
参数快开始处加入
例如:
// 下边的函数声明表明它是一个运行在严格模式下的函数
function foo() {
"use strict";
return true;
}
除了这种显式进入严格模式的方法之外,如下情况下的代码也处于严格模式中
- 模块中
- 类声明和类表达式的整个声明块中
- 引擎或者宿主的运行参数中指定”node –use_strict”
语法限制:不能通过语法检查
执行限制:会导致运行期错误的限制
2.6.1 语法限制
总的来说,有七种语法在严格模式中被禁用
一、在对象字面量声明中存在相同的属性名,非严格模式下,声明将使用最后一个有效的声明项
二、在函数声明中,参数表中含有相同的参数名,非严格模式下,访问同名的参数时,只有最后一个是有效的
三、不能声明或者重写eval
和arguments
两个标识符,也就是说不能出现在运算符的左边,也不能用var
来声明,由于catch
以及具名函数都会隐式的声明变量名,所以也不能使用这两个作为标识符,最后这两个不能使用delete
进行删除,在非严格模式中,上述语法都是有效的,但是一些引擎中重写eval
会导致运行期异常
四、用0前缀声明的八进制字面量,非严格模式下012
将显示12
五、用delete删除显式声明的标识符、名称或者具名函数,例如
var a
delete a
非严格模式下,这个操作仅仅是无效的,但是不会报错
六、在代码中使用一些保留字,包括implements、interface、let、package、private、protected、public、static和yield
,非严格模式下是可用的
七、在代码中包含with
语句
2.6.2 执行限制
一、在严格模式下向不存在的标识符复制,将导致“引用异常”,非严格模式下会隐式声明
二、运算符处理一些不可处理的操作数,将导致“类型异常或者语法错误”
三、访问arguments.callee或者函数的caller属性将导致“类型异常”
四、以下代码的执行效果和非严格模式不一致
// 严格模式下返回传入的值
// 非严格模式下返回100
function f(x) {arguments[0] = 100; return x;}
f('abc')
2.6.3 严格模式的范围
除非在创建和启动引擎的时候将它设置为严格模式,或者通过模块来进行加载,否则用户代码只能指定一个有限范围的严格模式
2.6.3.1 有限范围的严格模式
写在文件第一行就表明该代码块是严格模式
写在函数第一行表明函数是严格模式,函数名出问题也会报错,函数之外没影响
2.6.3.2 非严格模式下的全局环境
在任何处于严格模式下的代码中,JS都允许用户代码通过两种方式将代码执行在一个非严格全局环境中
- 使用间接调用的
eval()
函数,后边会说 - 使用
new Function
方式创建的函数,后边会说