第 13 章 语句
目录
购买本书
(广告,请不要屏蔽。)

第 13 章 语句

本章涵盖 JavaScript 的语句:变量声明、循环、条件语句等。

声明和赋值变量

var 用于 声明 变量,创建变量并使您能够使用它。 等号运算符 (=) 用于为其赋值:

var foo;
foo = 'abc';

var 还允许您将前面的两个语句合并为一个语句

var foo = 'abc';

最后,您还可以将多个 var 语句合并为一个语句

var x, y=123, z;

第 16 章 中阅读有关变量工作原理的更多信息。

循环和条件语句的主体

复合语句 例如循环和条件语句 嵌入了一个或多个“主体”——例如,while 循环:

while («condition»)
    «statement»

对于主体 «statement»,您可以选择。您可以使用单个语句

while (x >= 0) x--;

或者您可以使用 代码块(它算作单个语句):

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

如果希望主体包含多个语句,则需要使用代码块。除非完整的复合语句可以写在一行中,否则我建议使用代码块。

循环

本节探讨 JavaScript 的循环语句。

与循环一起使用的机制

以下机制可以与所有循环一起使用:

break ⟦«label»⟧
退出循环。
continue ⟦«label»⟧
停止当前循环迭代,并立即继续下一个迭代。
标签

标签是一个标识符,后跟一个冒号。在循环前面,标签允许您即使从嵌套在其中的循环中也可以中断或继续该循环。 在代码块前面,您可以跳出该代码块。在这两种情况下,标签的名称都将成为 breakcontinue 的参数。以下是从代码块中跳出的示例:

function findEvenNumber(arr) {
    loop: { // label
        for (var i=0; i<arr.length; i++) {
            var elem = arr[i];
            if ((elem % 2) === 0) {
                console.log('Found: ' + elem);
                break loop;
            }
        }
        console.log('No even number found.');
    }
    console.log('DONE');
}

while

while 循环:

while («condition»)
    «statement»

只要 condition 成立,就执行 statement。如果 condition 始终为 true,则会得到一个无限循环

while (true) { ... }

在以下示例中,我们删除数组的所有元素并将它们记录到控制台

var arr = [ 'a', 'b', 'c' ];
while (arr.length > 0) {
    console.log(arr.shift());
}

以下是输出

a
b
c

do-while

do-while 循环

do «statement»
while («condition»);

至少执行一次 statement,然后只要 condition 成立就执行。例如

var line;
do {
    line = prompt('Enter a number:');
} while (!/^[0-9]+$/.test(line));

for

for 循环 中:

for (⟦«init»⟧; ⟦«condition»⟧; ⟦«post_iteration»⟧)
    «statement»

init 在循环之前执行一次,只要 conditiontrue,循环就会继续。您可以在 init 中使用 var 来声明变量,但这些变量的作用域始终是完整的外部函数。 post_iteration 在循环的每次迭代之后执行。考虑到所有这些,前面的循环等效于以下 while 循环

«init»;
while («condition») {
    «statement»
    «post_iteration»;
}

以下示例是 迭代数组的传统方式(其他可能性在 最佳实践:迭代数组 中进行了描述)

var arr = [ 'a', 'b', 'c' ];
for (var i=0; i<arr.length; i++) {
    console.log(arr[i]);
}

如果省略头部的所有部分,则 for 循环将变为无限循环

for (;;) {
    ...
}

for-in

for-in 循环

for («variable» in «object»)
    «statement»

迭代 object 的所有属性键,包括继承的属性键。 但是,标记为不可枚举的属性将被忽略(请参阅 属性特性和属性描述符)。以下规则适用于 for-in 循环

  • 您可以使用 var 来声明变量,但这些变量的作用域始终是完整的外部函数。
  • 可以在迭代过程中删除属性。

最佳实践:不要对数组使用 for-in

不要使用 for-in迭代数组。首先,它迭代索引,而不是值:

> var arr = [ 'a', 'b', 'c' ];
> for (var key in arr) { console.log(key); }
0
1
2

其次,它还迭代所有(非索引)属性键。以下示例说明了在向数组添加属性 foo 时会发生什么

> var arr = [ 'a', 'b', 'c' ];
> arr.foo = true;
> for (var key in arr) { console.log(key); }
0
1
2
foo

因此,最好使用普通的 for 循环或数组方法 forEach()(请参阅 最佳实践:迭代数组)。

最佳实践:对对象使用 for-in 时要小心

for-in 循环迭代 所有(可枚举的)属性,包括继承的属性。这可能不是您想要的。让我们使用以下构造函数来说明这个问题:

function Person(name) {
    this.name = name;
}
Person.prototype.describe = function () {
    return 'Name: '+this.name;
};

Person 的实例从 Person.prototype 继承属性 describefor-in 可以看到该属性

var person = new Person('Jane');
for (var key in person) {
    console.log(key);
}

以下是输出

name
describe

通常,使用 for-in 的最佳方法是通过 hasOwnProperty() 跳过继承的属性

for (var key in person) {
    if (person.hasOwnProperty(key)) {
        console.log(key);
    }
}

以下是输出

name

最后一个需要注意的是:person 可能具有属性 hasOwnProperty,这将阻止检查工作。为了安全起见,您必须直接引用泛型方法(请参阅 泛型方法:从原型中借用方法Object.prototype.hasOwnProperty

for (var key in person) {
    if (Object.prototype.hasOwnProperty.call(person, key)) {
        console.log(key);
    }
}

还有其他更方便的方法可以迭代属性键,这些方法在 最佳实践:迭代自有属性 中进行了描述。

for each-in

此循环仅存在于 Firefox 上。 不要使用它。

条件语句

本节介绍 JavaScript 的条件语句。

if-then-else

if-then-else 语句 中:

if («condition»)
    «then_branch»
else
    «else_branch»⟧

then_branchelse_branch 可以是单个语句,也可以是语句块(请参阅 循环和条件语句的主体)。

链接 if 语句

您可以 链接多个 if 语句:

if (s1 > s2) {
    return 1;
} else if (s1 < s2) {
    return -1;
} else {
    return 0;
}

请注意,在前面的示例中,所有 else 分支都是单个语句(if 语句)。对于 else 分支仅允许使用代码块的编程语言,需要某种 else-if 分支才能进行链接。

陷阱:悬挂 else

以下示例的 else 分支称为 悬挂 分支,因为不清楚它属于两个 if 语句中的哪一个:

if («cond1») if («cond2») «stmt1» else «stmt2»

这是一个简单的规则:使用大括号。 前面的代码段等效于以下代码(其中很明显 else 属于谁):

if («cond1») {
    if («cond2») {
        «stmt1»
    } else {
        «stmt2»
    }
}

switch

switch 语句:

switch («expression») {
    case «label1_1»:
    case «label1_2»:
        ...
        «statements1»
        break;
    case «label2_1»:
    case «label2_2»:
        ...
        «statements2»
        break;
    ...
    default:
        «statements_default»
        break;⟧⟧
}

计算 expression,然后跳转到标签与结果匹配的 case 子句。 如果没有标签匹配,switch 将跳转到 default 子句(如果存在),否则不执行任何操作。

case 之后的“操作数”可以是任何表达式;它通过 ===switch 的参数进行比较。

如果未以终止语句结束子句,则执行将继续到下一个子句。最常用的终止语句是 break但是 returnthrow 也可以工作,即使它们通常离开的不仅仅是 switch 语句。

以下示例说明了如果使用 throwreturn,则不需要 break

function divide(dividend, divisor) {
    switch (divisor) {
        case 0:
            throw 'Division by zero';
        default:
            return dividend / divisor;
    }
}

在此示例中,没有 default 子句。因此,如果 fruit 与任何 case 标签都不匹配,则不会发生任何事情

function useFruit(fruit) {
    switch (fruit) {
        case 'apple':
            makeCider();
            break;
        case 'grape':
            makeWine();
            break;
        // neither apple nor grape: do nothing
    }
}

在这里,一行中有多个 case 标签

function categorizeColor(color) {
    var result;
    switch (color) {
        case 'red':
        case 'yellow':
        case 'blue':
            result = 'Primary color: '+color;
            break;
        case 'orange':
        case 'green':
        case 'violet':
            result = 'Secondary color: '+color;
            break;
        case 'black':
        case 'white':
            result = 'Not a color';
            break;
        default:
            throw 'Illegal argument: '+color;
    }
    console.log(result);
}

此示例演示了 case 之后的值可以是任意表达式

function compare(x, y) {
    switch (true) {
        case x < y:
            return -1;
        case x === y:
            return 0;
        default:
            return 1;
    }
}

前面的 switch 语句通过遍历 case 子句来查找其参数 true 的匹配项。如果其中一个 case 表达式计算结果为 true,则执行相应的 case 主体。因此,前面的代码等效于以下 if 语句

function compare(x, y) {
    if (x < y) {
        return -1;
    } else if (x === y) {
        return 0;
    } else {
        return 1;
    }
}

您通常应该首选后一种解决方案;它更易于理解。

with 语句

本节介绍 with 语句在 JavaScript 中的工作原理以及为什么不鼓励使用它。

语法和语义

with 语句的语法如下

with («object»)
    «statement»

它将 object 的属性转换为 statement 的局部变量。例如

var obj = { first: 'John' };
with (obj) {
    console.log('Hello '+first); // Hello John
}

它的预期用途是在多次访问对象时避免冗余。以下是包含冗余代码的示例

foo.bar.baz.bla   = 123;
foo.bar.baz.yadda = 'abc';

with 使其更短

with (foo.bar.baz) {
    bla   = 123;
    yadda = 'abc';
}

不推荐使用 with 语句

通常不鼓励使用 with 语句(下一节将解释原因)。例如,在严格模式下禁止使用它:

> function foo() { 'use strict'; with ({}); }
SyntaxError: strict mode code may not contain 'with' statements

避免使用 with 语句的技术

避免使用 如下代码:

// Don't do this:
with (foo.bar.baz) {
    console.log('Hello '+first+' '+last);
}

而是使用名称较短的临时变量

var b = foo.bar.baz;
console.log('Hello '+b.first+' '+b.last);

如果您不想将临时变量 b 暴露给当前作用域,则可以使用 IIFE(请参阅 通过 IIFE 引入新的作用域)

(function () {
    var b = foo.bar.baz;
    console.log('Hello '+b.first+' '+b.last);
}());

您还可以选择将要访问的对象作为 IIFE 的参数

(function (b) {
    console.log('Hello '+b.first+' '+b.last);
}(foo.bar.baz));

不推荐使用的理由

要了解为什么不推荐使用 with请查看以下示例,并注意函数的参数如何完全改变其工作方式:

function logMessage(msg, opts) {
    with (opts) {
        console.log('msg: '+msg); // (1)
    }
}

如果 opts 具有属性 msg,则第 (1) 行中的语句不再访问参数 msg。它访问属性

> logMessage('hello', {})  // parameter msg
msg: hello
> logMessage('hello', { msg: 'world' })  // property opts.msg
msg: world

with 语句会导致三个问题

性能下降
变量查找速度变慢,因为对象会临时插入到作用域链中。
代码变得更难预测

您无法通过查看标识符的语法环境(其词法上下文)来确定它引用的是什么。根据 Brendan Eich 的说法,这才是 with 被弃用的真正原因,而不是性能方面的考虑

with 违反了词法作用域,使得程序分析(例如,为了安全起见)变得困难或不可行。

压缩器(在 第 32 章 中进行了描述)无法缩短变量名
with 语句内部,您无法静态地确定一个名称是指变量还是属性。只有变量可以被压缩器重命名。

以下是一个 with 语句导致代码脆弱的例子

function foo(someArray) {
    var values = ...;  // (1)
    with (someArray) {
        values.someMethod(...);  // (2)
        ...
    }
}
foo(myData);  // (3)

您可以阻止第 (3) 行的函数调用工作,即使您没有访问数组 myData 的权限。

怎么做?通过向 Array.prototype 添加一个属性 values。例如

Array.prototype.values = function () {
    ...
};

现在,第 (2) 行的代码调用的是 someArray.values.someMethod() 而不是 values.someMethod()。原因是在 with 语句内部,values 现在指的是 someArray.values,而不是第 (1) 行的局部变量了。

这不仅仅是一个思想实验:数组方法 values() 被添加到 Firefox 中,并破坏了 TYPO3 内容管理系统。Brandon Benvie 找到了问题所在

debugger 语句

debugger 语句 的语法如下:

debugger;

如果调试器处于活动状态,则此语句充当断点;否则,它没有任何可观察到的效果。

下一页:14. 异常处理