2.4 JavaScript的语法: 语句

JS是一门在执行过程中以表达式求值为核心设计的语言

语句:是代码语法分析中的核心元素,通常是单行语句,或者由一对大括号括起来的复合语句,在语法描述中,复合语句可以整体作为一个单行语句处理

这里有两个原则

  • 语句由语法符号;来分隔
  • 一些语句存在返回值

2.4.1 表达式语句

下边的代码显然是表达式

1+2+3

在后边加一个分号1+2+3;就成为了表达式语句

JS中,许多语法和语义最终都是由”表达式语句“来实现的,例如赋值、函数调用、以及我们在语言中常见的”调用对象方法“。由于表达式运算总是有值的——这包括NaNundefined因此表达式语句也总是有值

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块中使用returnfinally中的语句仍会被执行,在catch块中使用breakfinally中的语句仍会被执行

如果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 语法限制

总的来说,有七种语法在严格模式中被禁用

一、在对象字面量声明中存在相同的属性名,非严格模式下,声明将使用最后一个有效的声明项

二、在函数声明中,参数表中含有相同的参数名,非严格模式下,访问同名的参数时,只有最后一个是有效的

三、不能声明或者重写evalarguments两个标识符,也就是说不能出现在运算符的左边,也不能用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方式创建的函数,后边会说


语言   js      js 基础 绿宝书

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!