第 7 章. JavaScript 语法
目录
购买本书
(广告,请不要屏蔽。)

第 7 章. JavaScript 语法

JavaScript 的语法相当简单明了。本章将介绍需要注意的事项。

语法概述

本节将简要介绍 JavaScript 的语法。

以下是五种基本值类型:

  • 布尔值

    true
    false
  • 数字

    1023
    7.851
  • 字符串

    'hello'
    "hello"
  • 普通对象:

    {
        firstName: 'Jane',
        lastName: 'Doe'
    }
  • 数组

    [ 'apple', 'banana', 'cherry' ]

以下是一些基本语法的示例:

// Two slashes start single-linecomments

var x;  // declaring a variable

x = 3 + y;  // assigning a value to the variable `x`

foo(x, y);  // calling function `foo` with parameters `x` and `y`
obj.bar(3);  // calling method `bar` of object `obj`

// A conditional statement
if (x === 0) {  // Is `x` equal to zero?
    x = 123;
}

// Defining function `baz` with parameters `a` and `b`
function baz(a, b) {
    return a + b;
}

请注意等号的两种不同用法:

  • 单个等号 (=) 用于将值赋给变量。
  • 三个等号 (===) 用于比较两个值(请参阅相等运算符)。

注释

注释有两种

表达式与语句

本节将介绍 JavaScript 中一个重要的语法区别:表达式和语句之间的区别。

语句

粗略地说,语句执行一个动作。循环和 if 语句是语句的示例。程序基本上就是一系列语句。[8]

在 JavaScript 需要语句的地方,你也可以编写表达式。这样的语句称为表达式语句。反之则不然:你不能在 JavaScript 需要表达式的地方编写语句。例如,if 语句不能作为函数的参数。

条件语句与条件表达式

如果我们看一下两个语法类别中相似的成员:if 语句和条件运算符(一个表达式),那么语句和表达式之间的区别就会更加清晰。

以下是一个if 语句的示例:

var salutation;
if (male) {
    salutation = 'Mr.';
} else {
    salutation = 'Mrs.';
}

有一种类似的表达式,称为条件运算符。前面的语句等效于以下代码:

var salutation = (male ? 'Mr.' : 'Mrs.');

等号和分号之间的代码是一个表达式。括号不是必需的,但我发现如果将条件运算符放在括号中,则更容易阅读。

将歧义表达式用作语句

有两种表达式看起来像语句,它们在语法类别方面存在歧义:

  • 对象字面量(表达式)看起来像代码块(语句):

    {
        foo: bar(3, 5)
    }

    前面的结构要么是一个对象字面量(详细信息:对象字面量),要么是一个代码块,后面跟着标签 foo:,再后面跟着函数调用 bar(3, 5)

  • 命名函数表达式看起来像函数声明(语句):

    function foo() { }

    前面的结构要么是一个命名函数表达式,要么是一个函数声明。前者产生一个函数,后者创建一个变量并将一个函数赋给它(有关这两种函数定义的详细信息:定义函数)。

为了防止在解析过程中出现歧义,JavaScript 不允许你将对象字面量和函数表达式用作语句。也就是说,表达式语句不能以以下内容开头

  • 花括号
  • 关键字 function

如果表达式以这两个标记中的任何一个开头,则它只能出现在表达式上下文中。你可以通过例如将表达式放在括号中来满足该要求。接下来,我们将看两个需要这样做的例子。

通过 eval() 计算对象字面量

eval语句上下文中解析其参数。如果你希望 eval 返回一个对象,则必须将对象字面量放在括号中:

> eval('{ foo: 123 }')
123
> eval('({ foo: 123 })')
{ foo: 123 }

立即调用函数表达式

以下代码是一个立即调用函数表达式 (IIFE),它的主体会立即执行(你将在通过 IIFE 引入新的作用域 中了解 IIFE 的用途)

> (function () { return 'abc' }())
'abc'

如果省略括号,则会出现语法错误,因为 JavaScript 会将其视为函数声明,而函数声明不能是匿名的

> function () { return 'abc' }()
SyntaxError: function statement requires a name

如果添加名称,也会出现语法错误,因为函数声明不能立即调用

> function foo() { return 'abc' }()
SyntaxError: Unexpected token )

函数声明后面的任何内容都必须是合法的语句,而 () 不是。

控制流语句和代码块

对于控制流语句,主体是单个语句。以下是两个示例:

if (obj !== null) obj.foo();

while (x > 0) x--;

但是,任何语句都可以始终替换为代码块,即花括号中包含零个或多个语句。因此,你也可以这样写:

if (obj !== null) {
    obj.foo();
}

while (x > 0) {
    x--;
}

我更喜欢后一种形式的控制流语句。将其标准化意味着单语句主体和多语句主体之间没有区别。因此,你的代码看起来更加一致,并且在一条语句和多条语句之间切换更容易。

使用分号的规则

在本节中,我们将研究分号在 JavaScript 中的用法。基本规则是:

  • 通常,语句以分号结尾。
  • 以代码块结尾的语句除外。

分号在 JavaScript 中是可选的。缺少的分号会通过所谓的自动分号插入 (ASI) 添加;请参阅自动分号插入)。但是,该功能并不总是按预期工作,这就是为什么你应该始终包含分号的原因。

以代码块结尾的语句后不加分号

以下语句如果以代码块结尾,则不以分号结尾:

  • 循环:forwhile(但不包括 do-while
  • 分支:ifswitchtry
  • 函数声明(但不包括函数表达式)

以下是 whiledo-while 的示例

while (a > 0) {
    a--;
} // no semicolon

do {
    a--;
} while (a > 0);

以下是函数声明与函数表达式的示例。后者后面跟着一个分号,因为它出现在 var 声明(确实 以分号结尾)的内部

function foo() {
    // ...
} // no semicolon

var foo = function () {
    // ...
};

注意

如果在代码块后添加分号,则不会出现语法错误,因为它被视为空语句(请参阅下一节)。

提示

这就是你需要了解的有关分号的大部分内容。如果你始终添加分号,则可能无需阅读本节的其余部分。

空语句

单独一个分号是一个空语句,什么也不做。空语句可以出现在任何需要语句的地方。当需要语句但不需要执行任何操作时,它们很有用。在这种情况下,通常也允许使用代码块。例如,以下两条语句是等效的:

while (processNextItem() > 0);
while (processNextItem() > 0) {}

假设函数 processNextItem 返回剩余项目的数量。以下由三个空语句组成的程序在语法上也是正确的

;;;

自动分号插入

自动分号插入 (ASI) 的目标是使分号在行尾成为可选的。自动分号插入一词所引发的图像是 JavaScript 解析器为你插入分号(在内部,处理方式通常不同)。

换句话说,ASI 帮助解析器确定语句何时结束。通常,它以分号结尾。ASI 规定,如果出现以下情况,语句也会结束

  • 行终止符(例如,换行符)后跟一个非法标记。
  • 遇到右花括号。
  • 已到达文件末尾。

示例:通过非法标记进行 ASI

以下代码包含一个行终止符,后跟一个非法标记:

if (a < 0) a = 0
console.log(a)

标记 console0 之后是非法的,会触发 ASI

if (a < 0) a = 0;
console.log(a);

示例:通过右花括号进行 ASI

以下代码中,花括号内的语句不以分号结尾:

function add(a,b) { return a+b }

ASI 创建了前面代码的语法正确版本

function add(a,b) { return a+b; }

陷阱:ASI 可能会意外地拆分语句

如果关键字 return 后面有一个行终止符,也会触发 ASI。例如:

// Don't do this
return
{
    name: 'John'
};

ASI 将前面的代码转换为

return;
{
    name: 'John'
};

这是一个空的 return 语句,后面跟着一个代码块,代码块中带有标签 name,标签前面是表达式语句 'John'。代码块后面是一个空语句。

陷阱:ASI 可能意外地没有被触发

有时,新行中的语句以一个标记开头,该标记允许作为前一条语句的延续。在这种情况下,即使看起来应该触发 ASI,也不会触发它。例如:

func()
[ 'ul', 'ol' ].forEach(function (t) { handleTag(t) })

第二行中的方括号被解释为对 func() 返回的结果进行索引。方括号内的逗号被解释为逗号运算符(在本例中返回 'ol';请参阅逗号运算符)。因此,JavaScript 将前面的代码视为:

func()['ol'].forEach(function (t) { handleTag(t) });

合法标识符

标识符用于命名事物,并在 JavaScript 中以各种语法角色出现。例如,变量的名称和未加引号的属性键必须是有效的标识符。标识符区分大小写。

标识符的第一个字符可以是以下任意一个

  • 任何 Unicode 字母,包括拉丁字母(如 D)、希腊字母(如 λ)和西里尔字母(如 Д)
  • 美元符号 ($)
  • 下划线 (_)

后续字符可以是

  • 任何合法的第一个字符
  • Unicode 类别“十进制数字 (Nd)”中的任何 Unicode 数字;这包括欧洲数字(如 7)和印度数字(如 ٣)
  • 各种其他 Unicode 符号和标点符号

合法标识符示例

var ε = 0.0001;
var строка = '';
var _tmp;
var $foo2;

尽管这使你能够在 JavaScript 代码中使用各种人类语言,但我建议坚持使用英语,包括标识符和注释。这可以确保你的代码能够被尽可能多的人理解,考虑到如今代码可以在国际范围内传播,这一点非常重要。

以下标识符保留字——它们是语法的一部分,不能用作变量名(包括函数名和参数名):

arguments

break

case

catch

class

const

continue

debugger

default

delete

do

else

enum

export

extends

false

finally

for

function

if

implements

import

in

instanceof

interface

let

new

null

package

private

protected

public

return

static

super

switch

this

throw

true

try

typeof

var

void

while

以下三个标识符不是保留字,但您应该将它们视为保留字

Infinity

NaN

undefined

最后,您还应该避免使用标准全局变量的名称(请参阅第 23 章)。您可以将它们用于局部变量而不会破坏任何内容,但您的代码仍然会变得混乱。

请注意,您可以使用保留字作为未加引号的属性键(从 ECMAScript 5 开始)

> var obj = { function: 'abc' };
> obj.function
'abc'

您可以在 Mathias Bynens 的博客文章“有效的 JavaScript 变量名”中查找标识符的精确规则。

对数字字面量调用方法

使用方法调用时,区分浮点和方法调用点非常重要。因此,您不能编写1.toString();您必须使用以下替代方法之一:

1..toString()
1 .toString()  // space before dot
(1).toString()
1.0.toString()

严格模式

ECMAScript 5 有一种严格模式,它可以生成更简洁的 JavaScript,减少不安全的功能,提供更多警告,并使行为更加合乎逻辑。正常(非严格)模式有时称为“草率模式”。

开启严格模式

您可以在 JavaScript 文件或<script> 元素内部输入以下行来开启严格模式:

'use strict';

请注意,不支持 ECMAScript 5 的 JavaScript 引擎将忽略前面的语句,因为以这种方式编写字符串(作为表达式语句;请参阅语句)通常不会执行任何操作。

您还可以为每个函数开启严格模式。为此,请像这样编写您的函数

function foo() {
    'use strict';
    ...
}

当您使用的是旧版代码库,并且在任何地方开启严格模式都可能会破坏代码时,这非常方便。

总的来说,严格模式启用的更改都是为了改进代码。因此,强烈建议您将其用于您编写的代码——只需在文件开头将其打开即可。但是,有两点需要注意:

为现有代码启用严格模式可能会破坏它
代码可能依赖于不再可用的功能,或者它可能依赖于在草率模式下与严格模式下不同的行为。不要忘记,您可以选择将单个严格模式函数添加到处于草率模式的文件中。
打包时要小心
当您连接和/或缩小文件时,您必须小心不要在应该打开严格模式的地方关闭它,反之亦然。这两种情况都可能会破坏代码。

以下部分将更详细地解释严格模式的功能。您通常不需要了解它们,因为您通常会收到更多关于您不应该做的事情的警告。

变量必须在严格模式下声明

所有变量都必须在严格模式下显式声明。这有助于防止拼错。在草率模式下,分配给未声明的变量会创建一个全局变量:

function sloppyFunc() {
    sloppyVar = 123;
}
sloppyFunc();  // creates global variable `sloppyVar`
console.log(sloppyVar);  // 123

在严格模式下,分配给未声明的变量会引发异常

function strictFunc() {
    'use strict';
    strictVar = 123;
}
strictFunc();  // ReferenceError: strictVar is not defined

严格模式下的函数

严格模式限制了与函数相关的功能。

函数必须在作用域的顶层声明

在严格模式下,所有函数都必须在作用域的顶层声明(全局作用域或直接在函数内部)。这意味着您不能将函数声明放在块内。如果您这样做,您将收到一个描述性的SyntaxError。例如,V8 会告诉您:“在严格模式代码中,函数只能在顶层或直接在另一个函数内部声明”:

function strictFunc() {
    'use strict';
    if (true) {
        // SyntaxError:
        function nested() {
        }
    }
}

这无论如何都没有用,因为函数是在周围函数的作用域中创建的,而不是在块的“内部”。

如果要解决此限制,可以通过变量声明和函数表达式在块内创建函数

function strictFunc() {
    'use strict';
    if (true) {
        // OK:
        var nested = function () {
        };
    }
}

更严格的函数参数规则

函数参数的规则不那么宽松:禁止两次使用相同的参数名,以及与参数同名的局部变量。

this 在非方法函数中是未定义的

在草率模式下,非方法函数中this 的值是全局对象(浏览器中的window;请参阅全局对象):

function sloppyFunc() {
    console.log(this === window);  // true
}

在严格模式下,它是undefined

function strictFunc() {
    'use strict';
    console.log(this === undefined);  // true
}

这对构造函数很有用。例如,以下构造函数Point处于严格模式:

function Point(x, y) {
    'use strict';
    this.x = x;
    this.y = y;
}

由于严格模式,当您不小心忘记new 并将其作为函数调用时,您会收到警告

> var pt = Point(3, 1);
TypeError: Cannot set property 'x' of undefined

在草率模式下,您不会收到警告,并且会创建全局变量xy。有关详细信息,请参阅实现构造函数的技巧

在严格模式下,设置和删除不可变属性将失败并引发异常

在严格模式下,非法操作属性会引发异常。例如,尝试设置只读属性的值会引发异常,尝试删除不可配置属性也会引发异常。以下是前者的示例:

var str = 'abc';
function sloppyFunc() {
    str.length = 7;  // no effect, silent failure
    console.log(str.length);  // 3
}
function strictFunc() {
    'use strict';
    str.length = 7; // TypeError: Cannot assign to
                    // read-only property 'length'
}

在严格模式下,不能删除非限定标识符

在草率模式下,您可以像这样删除全局变量foo

delete foo

在严格模式下,只要您尝试删除非限定标识符,就会收到语法错误。您仍然可以像这样删除全局变量

delete window.foo;  // browsers
delete global.foo;  // Node.js
delete this.foo;    // everywhere (in global scope)

严格模式下禁止的功能

严格模式下禁止使用另外两个 JavaScript 功能

  • 不再允许使用with 语句(请参阅with 语句)。您会在编译时(加载代码时)收到语法错误。
  • 不再使用八进制数:在草率模式下,以零开头的整数将被解释为八进制(以 8 为基数)。例如

    > 010 === 8
    true

    在严格模式下,如果使用这种字面量,则会出现语法错误:

    > function f() { 'use strict'; return 010 }
    SyntaxError: Octal literals are not allowed in strict mode.



[8] 为简单起见,我假设声明是语句。

下一页:8. 值