当前位置: 首页 > news >正文

微信公众号网站建设mvc5网站开发

微信公众号网站建设,mvc5网站开发,几百块做网站,产品设计培训第四章#xff1a;表达式和运算符 本章记录了 JavaScript 表达式以及构建许多这些表达式的运算符。表达式 是 JavaScript 的短语#xff0c;可以 评估 以产生一个值。在程序中直接嵌入的常量是一种非常简单的表达式。变量名也是一个简单表达式#xff0c;它评估为分配给该变…第四章表达式和运算符 本章记录了 JavaScript 表达式以及构建许多这些表达式的运算符。表达式 是 JavaScript 的短语可以 评估 以产生一个值。在程序中直接嵌入的常量是一种非常简单的表达式。变量名也是一个简单表达式它评估为分配给该变量的任何值。复杂表达式是由简单表达式构建的。例如一个数组访问表达式由一个评估为数组的表达式、一个开放方括号、一个评估为整数的表达式和一个闭合方括号组成。这个新的、更复杂的表达式评估为存储在指定数组索引处的值。类似地函数调用表达式由一个评估为函数对象的表达式和零个或多个额外表达式组成这些额外表达式用作函数的参数。 从简单表达式中构建复杂表达式的最常见方法是使用 运算符。运算符以某种方式结合其操作数的值通常是两个操作数中的一个并评估为一个新值。乘法运算符 * 是一个简单的例子。表达式 x * y 评估为表达式 x 和 y 的值的乘积。为简单起见我们有时说一个运算符 返回 一个值而不是“评估为”一个值。 本章记录了 JavaScript 的所有运算符并解释了不使用运算符的表达式如数组索引和函数调用。如果您已经了解使用 C 风格语法的其他编程语言您会发现大多数 JavaScript 表达式和运算符的语法已经很熟悉了。 4.1 主要表达式 最简单的表达式称为 主要表达式是那些独立存在的表达式——它们不包括任何更简单的表达式。JavaScript 中的主要表达式是常量或 字面值、某些语言关键字和变量引用。 字面量是直接嵌入到程序中的常量值。它们看起来像这样 1.23 // A number literal hello // A string literal /pattern/ // A regular expression literalJavaScript 中关于数字字面量的语法已在 §3.2 中介绍过。字符串字面量在 §3.3 中有文档记录。正则表达式字面量语法在 §3.3.5 中介绍过并将在 §11.3 中详细记录。 JavaScript 的一些保留字是主要表达式 true // Evalutes to the boolean true value false // Evaluates to the boolean false value null // Evaluates to the null value this // Evaluates to the current object我们在 §3.4 和 §3.5 中学习了 true、false 和 null。与其他关键字不同this 不是一个常量——它在程序中的不同位置评估为不同的值。this 关键字用于面向对象编程。在方法体内this 评估为调用该方法的对象。查看 §4.5、第八章特别是 §8.2.2和 第九章 了解更多关于 this 的内容。 最后第三种主要表达式是对变量、常量或全局对象属性的引用 i // Evaluates to the value of the variable i. sum // Evaluates to the value of the variable sum. undefined // The value of the undefined property of the global object当程序中出现任何标识符时JavaScript 假定它是一个变量、常量或全局对象的属性并查找其值。如果不存在具有该名称的变量则尝试评估不存在的变量会抛出 ReferenceError。 4.2 对象和数组初始化器 对象 和 数组初始化器 是值为新创建的对象或数组的表达式。这些初始化器表达式有时被称为 对象字面量 和 数组字面量。然而与真正的字面量不同它们不是主要表达式因为它们包括一些指定属性和元素值的子表达式。数组初始化器具有稍微简单的语法我们将从这些开始。 数组初始化器是方括号内包含的逗号分隔的表达式列表。数组初始化器的值是一个新创建的数组。这个新数组的元素被初始化为逗号分隔表达式的值 [] // An empty array: no expressions inside brackets means no elements [12,34] // A 2-element array. First element is 3, second is 7数组初始化器中的元素表达式本身可以是数组初始化器这意味着这些表达式可以创建嵌套数组 let matrix [[1,2,3], [4,5,6], [7,8,9]];数组初始化器中的元素表达式在每次评估数组初始化器时都会被评估。这意味着数组初始化器表达式的值在每次评估时可能会有所不同。 可以通过简单地在逗号之间省略值来在数组文字中包含未定义的元素。例如以下数组包含五个元素包括三个未定义的元素 let sparseArray [1,,,,5];在数组初始化器中最后一个表达式后允许有一个逗号并且不会创建未定义的元素。然而对于最后一个表达式之后的索引的任何数组访问表达式都将必然评估为未定义。 对象初始化器表达式类似于数组初始化器表达式但方括号被花括号替换每个子表达式前缀都带有属性名和冒号 let p { x: 2.3, y: -1.2 }; // An object with 2 properties let q {}; // An empty object with no properties q.x 2.3; q.y -1.2; // Now q has the same properties as p在 ES6 中对象文字具有更丰富的语法详细信息请参见§6.10。对象文字可以嵌套。例如 let rectangle {upperLeft: { x: 2, y: 2 },lowerRight: { x: 4, y: 5 } };我们将在第六章和第七章再次看到对象和数组初始化器。 4.3 函数定义表达式 函数定义表达式 定义了一个 JavaScript 函数这种表达式的值是新定义的函数。在某种意义上函数定义表达式是“函数文字”的一种方式就像对象初始化器是“对象文字”一样。函数定义表达式通常由关键字function后跟一个逗号分隔的零个或多个标识符参数名称的列表在括号中和一个 JavaScript 代码块函数体在花括号中组成。例如 // This function returns the square of the value passed to it. let square function(x) { return x * x; };函数定义表达式也可以包括函数的名称。函数也可以使用函数语句而不是函数表达式来定义。在 ES6 及更高版本中函数表达式可以使用紧凑的新“箭头函数”语法。有关函数定义的完整详细信息请参见第八章。 4.4 属性访问表达式 属性访问表达式 评估为对象属性或数组元素的值。JavaScript 为属性访问定义了两种语法 *expression* . *identifier* *expression* [ *expression* ]属性访问的第一种风格是一个表达式后跟一个句点和一个标识符。表达式指定对象标识符指定所需属性的名称。属性访问的第二种风格在第一个表达式对象或数组后跟另一个方括号中的表达式。这第二个表达式指定所需属性的名称或所需数组元素的索引。以下是一些具体示例 let o {x: 1, y: {z: 3}}; // An example object let a [o, 4, [5, 6]]; // An example array that contains the object o.x // 1: property x of expression o o.y.z // 3: property z of expression o.y o[x] // 1: property x of object o a[1] // 4: element at index 1 of expression a a[2][1] // 6: element at index 1 of expression a[2] a[0].x // 1: property x of expression a[0]使用任一类型的属性访问表达式时首先评估.或之前的表达式。如果值为null或undefined则该表达式会抛出 TypeError因为这是两个 JavaScript 值不能具有属性。如果对象表达式后跟一个句点和一个标识符则查找该标识符命名的属性的值并成为表达式的整体值。如果对象表达式后跟另一个方括号中的表达式则评估并转换为字符串。然后表达式的整体值是由该字符串命名的属性的值。在任一情况下如果命名属性不存在则属性访问表达式的值为undefined。 .identifier语法是两种属性访问选项中更简单的一种但请注意只有当要访问的属性具有合法标识符名称并且在编写程序时知道名称时才能使用。如果属性名称包含空格或标点符号或者是数字对于数组则必须使用方括号表示法。当属性名称不是静态的而是计算结果时也使用方括号参见[§6.3.1 中的示例。 对象及其属性在第六章中有详细介绍数组及其元素在第七章中有介绍。 4.4.1 条件属性访问 ES2020 添加了两种新的属性访问表达式 *expression* ?. *identifier* *expression* ?.[ *expression* ]在 JavaScript 中值null和undefined是唯一没有属性的两个值。在使用.或[]的常规属性访问表达式中如果左侧的表达式评估为null或undefined则会收到 TypeError。您可以使用?.和?.[]语法来防止此类错误。 考虑表达式a?.b。如果a是null或undefined那么该表达式将评估为undefined而不会尝试访问属性b。如果a是其他值则a?.b将评估为a.b的评估结果如果a没有名为b的属性则该值将再次为undefined。 这种形式的属性访问表达式有时被称为“可选链”因为它也适用于像这样的更长的“链式”属性访问表达式 let a { b: null }; a.b?.c.d // undefineda是一个对象因此a.b是一个有效的属性访问表达式。但是a.b的值是null所以a.b.c会抛出 TypeError。通过使用?.而不是.我们避免了 TypeErrora.b?.c评估为undefined。这意味着(a.b?.c).d将抛出 TypeError因为该表达式尝试访问值undefined的属性。但是——这是“可选链”非常重要的一部分——a.b?.c.d不带括号简单地评估为undefined不会抛出错误。这是因为使用?.的属性访问是“短路”的如果?.左侧的子表达式评估为null或undefined则整个表达式立即评估为undefined而不会进一步尝试访问属性。 当然如果a.b是一个对象并且该对象没有名为c的属性则a.b?.c.d将再次抛出 TypeError我们将需要使用另一种条件属性访问 let a { b: {} }; a.b?.c?.d // undefined使用?.[]而不是[]也可以进行条件属性访问。在表达式a?.[b][c]中如果a的值为null或undefined则整个表达式立即评估为undefined并且子表达式b和c甚至不会被评估。如果其中任何一个表达式具有副作用则如果a未定义则副作用不会发生 let a; // Oops, we forgot to initialize this variable! let index 0; try {a[index]; // Throws TypeError } catch(e) {index // 1: increment occurs before TypeError is thrown } a?.[index] // undefined: because a is undefined index // 1: not incremented because ?.[] short-circuits a[index] // !TypeError: cant index undefined.使用?.和?.[]进行条件属性访问是 JavaScript 的最新功能之一。截至 2020 年初这种新语法在大多数主要浏览器的当前或测试版本中得到支持。 4.5 调用表达式 调用表达式是 JavaScript 用于调用或执行函数或方法的语法。它以标识要调用的函数的函数表达式开头。函数表达式后跟一个开括号一个逗号分隔的零个或多个参数表达式列表以及一个闭括号。一些示例 f(0) // f is the function expression; 0 is the argument expression. Math.max(x,y,z) // Math.max is the function; x, y, and z are the arguments. a.sort() // a.sort is the function; there are no arguments.当调用表达式被评估时首先评估函数表达式然后评估参数表达式以生成参数值列表。如果函数表达式的值不是函数则会抛出 TypeError。接下来按顺序将参数值分配给函数定义时指定的参数名然后执行函数体。如果函数使用return语句返回一个值则该值成为调用表达式的值。否则调用表达式的值为undefined。有关函数调用的完整详细信息包括当参数表达式的数量与函数定义中的参数数量不匹配时会发生什么的解释请参阅第八章。 每个调用表达式都包括一对括号和开括号前的表达式。如果该表达式是一个属性访问表达式则调用被称为方法调用。在方法调用中作为属性访问主题的对象或数组在执行函数体时成为this关键字的值。这使得面向对象编程范式成为可能其中函数当以这种方式使用时我们称之为“方法”在其所属对象上操作。详细信息请参阅第九章。 4.5.1 条件调用 在 ES2020 中你也可以使用?.()而不是()来调用函数。通常当你调用一个函数时如果括号左侧的表达式为null或undefined或任何其他非函数值将抛出 TypeError。使用新的?.()调用语法如果?.左侧的表达式评估为null或undefined那么整个调用表达式将评估为undefined不会抛出异常。 数组对象有一个sort()方法可以选择性地传递一个函数参数该函数定义了数组元素的期望排序顺序。在 ES2020 之前如果你想编写一个像sort()这样的方法它接受一个可选的函数参数你通常会使用一个if语句来检查函数参数在if体中调用之前是否已定义 function square(x, log) { // The second argument is an optional functionif (log) { // If the optional function is passedlog(x); // Invoke it}return x * x; // Return the square of the argument }然而使用 ES2020 的这种条件调用语法你可以简单地使用?.()编写函数调用只有在实际有值可调用时才会发生调用 function square(x, log) { // The second argument is an optional functionlog?.(x); // Call the function if there is onereturn x * x; // Return the square of the argument }但请注意?.()仅检查左侧是否为null或undefined。它不验证该值实际上是否为函数。因此在这个例子中如果你向square()函数传递两个数字它仍会抛出异常。 类似于条件属性访问表达式§4.4.1带有?.()的函数调用是短路的如果?.左侧的值为null或undefined则括号内的参数表达式都不会被评估 let f null, x 0; try {f(x); // Throws TypeError because f is null } catch(e) {x // 1: x gets incremented before the exception is thrown } f?.(x) // undefined: f is null, but no exception thrown x // 1: increment is skipped because of short-circuiting带有?.()的条件调用表达式对方法和函数同样有效。但是因为方法调用还涉及属性访问所以值得花点时间确保你理解以下表达式之间的区别 o.m() // Regular property access, regular invocation o?.m() // Conditional property access, regular invocation o.m?.() // Regular property access, conditional invocation在第一个表达式中o必须是一个具有属性m且该属性的值必须是一个函数的对象。在第二个表达式中如果o为null或undefined则表达式评估为undefined。但如果o有任何其他值则它必须具有一个值为函数的属性m。在第三个表达式中o不能为null或undefined。如果它没有属性m或者该属性的值为null则整个表达式评估为undefined。 使用?.()进行条件调用是 JavaScript 的最新功能之一。截至 2020 年初这种新语法在大多数主要浏览器的当前或测试版本中得到支持。 4.6 对象创建表达式 对象创建表达式创建一个新对象并调用一个函数称为构造函数来初始化该对象的属性。对象创建表达式类似于调用表达式只是它们以关键字new为前缀 new Object() new Point(2,3)如果在对象创建表达式中未传递参数给构造函数则可以省略空括号对 new Object new Date对象创建表达式的值是新创建的对象。构造函数在第九章中有更详细的解释。 4.7 运算符概述 运算符用于 JavaScript 的算术表达式比较表达式逻辑表达式赋值表达式等。表 4-1 总结了这些运算符并作为一个方便的参考。 请注意大多数运算符由标点字符表示如和。但是有些运算符由关键字表示如delete和instanceof。关键字运算符是常规运算符就像用标点符号表示的那些一样它们只是具有不太简洁的语法。 表 4-1 按运算符优先级进行组织。列出的运算符比最后列出的运算符具有更高的优先级。由水平线分隔的运算符具有不同的优先级级别。标记为 A 的列给出了运算符的结合性可以是 L从左到右或 R从右到左列 N 指定了操作数的数量。标记为 Types 的列列出了操作数的预期类型和在→符号之后运算符的结果类型。表后面的子章节解释了优先级结合性和操作数类型的概念。这些运算符本身在讨论之后分别进行了文档化。 表 4-1. JavaScript 运算符 运算符操作AN类型前置或后置递增R1lval→num--前置或后置递减R1lval→num-取反数R1num→num转换为数字R1any→num~反转位R1int→int!反转布尔值R1bool→booldelete删除属性R1lval→booltypeof确定操作数的类型R1any→strvoid返回未定义的值R1any→undef**指数R2num,num→num*, /, %乘法除法取余L2num,num→num, -加法减法L2num,num→num连接字符串L2str,str→str左移L2int,int→int右移并用符号扩展L2int,int→int右移并用零扩展L2int,int→int, ,, 按数字顺序比较L2num,num→bool, ,, 按字母顺序比较L2str,str→boolinstanceof测试对象类L2obj,func→boolin测试属性是否存在L2any,obj→bool测试非严格相等性L2any,any→bool!测试非严格不等式L2any,any→bool测试严格相等性L2any,any→bool!测试严格不等式L2any,any→bool计算按位与L2int,int→int^计算按位异或L2int,int→int#124;计算按位或L2int,int→int计算逻辑与L2any,any→any#124;#124;计算逻辑或L2any,any→any??选择第一个定义的操作数L2any,any→any?:选择第二或第三个操作数R3bool,any,any→any分配给变量或属性R2lval,any→any**, *, /, %,运算并赋值R2lval,any→any, -, , ^, #124;,, , ,丢弃第一个操作数返回第二个L2any,any→any 4.7.1 操作数的数量 运算符可以根据它们期望的操作数数量它们的arity进行分类。大多数 JavaScript 运算符如 * 乘法运算符都是将两个表达式组合成单个更复杂表达式的二元运算符。也就是说它们期望两个操作数。JavaScript 还支持许多一元运算符它们将单个表达式转换为单个更复杂表达式。表达式 −x 中的 − 运算符是一个一元运算符它对操作数 x 执行否定操作。最后JavaScript 支持一个三元运算符条件运算符 ?:它将三个表达式组合成单个表达式。 4.7.2 操作数和结果类型 一些运算符适用于任何类型的值但大多数期望它们的操作数是特定类型的并且大多数运算符返回或计算为特定类型的值。表 4-1 中的类型列指定了运算符的操作数类型箭头前和结果类型箭头后。 JavaScript 运算符通常根据需要转换操作数的类型参见 §3.9。乘法运算符 * 需要数字操作数但表达式 3 * 5 是合法的因为 JavaScript 可以将操作数转换为数字。这个表达式的值是数字 15而不是字符串“15”当然。还要记住每个 JavaScript 值都是“真值”或“假值”因此期望布尔操作数的运算符将使用任何类型的操作数。 一些运算符的行为取决于与它们一起使用的操作数的类型。最值得注意的是 运算符添加数字操作数但连接字符串操作数。类似地诸如 的比较运算符根据操作数的类型以数字或字母顺序执行比较。各个运算符的描述解释了它们的类型依赖性并指定它们执行的类型转换。 注意赋值运算符和 表 4-1 中列出的其他一些运算符期望类型为 lval 的操作数。lvalue 是一个历史术语意思是“一个可以合法出现在赋值表达式左侧的表达式”。在 JavaScript 中变量、对象的属性和数组的元素都是 lvalues。 4.7.3 运算符副作用 评估简单表达式如 2 * 3 不会影响程序的状态程序执行的任何未来计算也不会受到该评估的影响。然而一些表达式具有副作用它们的评估可能会影响未来评估的结果。赋值运算符是最明显的例子如果将一个值赋给变量或属性那么使用该变量或属性的任何表达式的值都会发生变化。 和 -- 递增和递减运算符也类似因为它们执行隐式赋值。delete 运算符也具有副作用删除属性就像但不完全相同于将 undefined 赋给属性。 没有其他 JavaScript 运算符会产生副作用但是如果函数调用和对象创建表达式中使用的任何运算符具有副作用则会产生副作用。 4.7.4 运算符优先级 表 4-1 中列出的运算符按照从高优先级到低优先级的顺序排列水平线将同一优先级的运算符分组。运算符优先级控制操作执行的顺序。优先级较高的运算符在表的顶部附近在优先级较低的运算符在表的底部附近之前执行。 考虑以下表达式 w x y*z;乘法运算符*的优先级高于加法运算符因此先执行乘法。此外赋值运算符的优先级最低因此在右侧所有操作完成后执行赋值。 可以通过显式使用括号来覆盖运算符的优先级。要求在上一个示例中首先执行加法写成 w (x y)*z;注意属性访问和调用表达式的优先级高于表 4-1 中列出的任何运算符。考虑以下表达式 // my is an object with a property named functions whose value is an // array of functions. We invoke function number x, passing it argument // y, and then we ask for the type of the value returned. typeof my.functionsx尽管typeof是优先级最高的运算符之一但typeof操作是在属性访问、数组索引和函数调用的结果上执行的所有这些操作的优先级都高于运算符。 实际上如果您对运算符的优先级有任何疑问最简单的方法是使用括号使评估顺序明确。重要的规则是乘法和除法在加法和减法之前执行。赋值的优先级非常低几乎总是最后执行。 当新的运算符添加到 JavaScript 时它们并不总是自然地适应这个优先级方案。??运算符(§4.13.2)在表中显示为比||和低优先级但实际上它相对于这些运算符的优先级没有定义并且 ES2020 要求您在混合??与||或时明确使用括号。同样新的**乘幂运算符相对于一元否定运算符没有明确定义的优先级当将否定与乘幂结合时必须使用括号。 4.7.5 运算符结合性 在表 4-1 中标记为 A 的列指定了运算符的结合性。L 值指定左到右的结合性R 值指定右到左的结合性。运算符的结合性指定了相同优先级操作的执行顺序。左到右的结合性意味着操作从左到右执行。例如减法运算符具有左到右的结合性因此 w x - y - z;等同于 w ((x - y) - z);另一方面以下表达式 y a ** b ** c; x ~-y; w x y z; q a?b:c?d:e?f:g;等同于 y (a ** (b ** c)); x ~(-y); w (x (y z)); q a?b:(c?d:(e?f:g));因为乘幂、一元、赋值和三元条件运算符具有从右到左的结合性。 4.7.6 评估顺序 运算符的优先级和结合性指定复杂表达式中操作的执行顺序但它们不指定子表达式的评估顺序。JavaScript 总是严格按照从左到右的顺序评估表达式。例如在表达式w x y * z中首先评估子表达式w然后是x、y和z。然后将y和z的值相乘加上x的值并将结果赋给表达式w指定的变量或属性。添加括号可以改变乘法、加法和赋值的相对顺序但不能改变从左到右的评估顺序。 评估顺序只有在正在评估的任何表达式具有影响另一个表达式值的副作用时才会有所不同。如果表达式x增加了一个被表达式z使用的变量那么评估x在z之前的事实就很重要。 4.8 算术表达式 本节涵盖对操作数执行算术或其他数值操作的运算符。乘幂、乘法、除法和减法运算符是直接的并且首先进行讨论。加法运算符有自己的子节因为它还可以执行字符串连接并且具有一些不寻常的类型转换规则。一元运算符和位运算符也有自己的子节。 这些算术运算符中的大多数除非另有说明如下可以与 BigInt参见 §3.2.5操作数或常规数字一起使用只要不混合这两种类型。 基本算术运算符包括 **指数运算*乘法/除法%取模除法后的余数加法和 -减法。正如前面所述我们将在单独的章节讨论 运算符。其他五个基本运算符只是评估它们的操作数必要时将值转换为数字然后计算幂、乘积、商、余数或差。无法转换为数字的非数字操作数将转换为 NaN 值。如果任一操作数为或转换为NaN则操作的结果几乎总是为 NaN。 ** 运算符的优先级高于 */ 和 %这些运算符的优先级又高于 和 -。与其他运算符不同** 从右到左工作因此 2**2**3 等同于 2**8而不是 4**3。表达式 -3**2 存在自然的歧义。根据一元减号和指数运算符的相对优先级该表达式可能表示 (-3)**2 或 -(3**2)。不同的语言处理方式不同而 JavaScript 简单地使得在这种情况下省略括号成为语法错误强制您编写一个明确的表达式。** 是 JavaScript 最新的算术运算符它是在 ES2016 版本中添加到语言中的。然而Math.pow() 函数自最早版本的 JavaScript 就已经可用并且执行的操作与 ** 运算符完全相同。 / 运算符将其第一个操作数除以第二个操作数。如果您习惯于区分整数和浮点数的编程语言当您将一个整数除以另一个整数时您可能期望得到一个整数结果。然而在 JavaScript 中所有数字都是浮点数因此所有除法操作都具有浮点结果5/2 的结果为 2.5而不是 2。除以零会产生正无穷大或负无穷大而 0/0 的结果为 NaN这两种情况都不会引发错误。 % 运算符计算第一个操作数对第二个操作数的模。换句话说它返回第一个操作数除以第二个操作数的整数除法后的余数。结果的符号与第一个操作数的符号相同。例如5 % 2 的结果为 1-5 % 2 的结果为 -1。 尽管取模运算符通常用于整数操作数但它也适用于浮点值。例如6.5 % 2.1 的结果为 0.2。 4.8.1 运算符 二元 运算符添加数字操作数或连接字符串操作数 1 2 // 3 hello there // hello there 1 2 // 12当两个操作数的值都是数字或者都是字符串时 运算符的作用是显而易见的。然而在任何其他情况下都需要进行类型转换并且要执行的操作取决于所执行的转换。 的转换规则优先考虑字符串连接如果其中一个操作数是字符串或可转换为字符串的对象则另一个操作数将被转换为字符串并执行连接。只有当两个操作数都不像字符串时才执行加法。 技术上 运算符的行为如下 如果其操作数值中的任一值为对象则它将使用 §3.9.3 中描述的对象转换为原始值算法将其转换为原始值。日期对象通过其 toString() 方法转换而所有其他对象通过 valueOf() 转换如果该方法返回原始值。然而大多数对象没有有用的 valueOf() 方法因此它们也通过 toString() 转换。 在对象转换为原始值之后如果其中一个操作数是字符串则另一个操作数将被转换为字符串并执行连接。 否则两个操作数将被转换为数字或 NaN然后执行加法。 以下是一些示例 1 2 // 3: addition 1 2 // 12: concatenation 1 2 // 12: concatenation after number-to-string 1 {} // 1[object Object]: concatenation after object-to-string true true // 2: addition after boolean-to-number 2 null // 2: addition after null converts to 0 2 undefined // NaN: addition after undefined converts to NaN最后重要的是要注意当 运算符与字符串和数字一起使用时它可能不是结合的。也就是说结果可能取决于操作执行的顺序。 例如 1 2 blind mice // 3 blind mice 1 (2 blind mice) // 12 blind mice第一行没有括号 运算符具有从左到右的结合性因此先将两个数字相加然后将它们的和与字符串连接起来。在第二行中括号改变了操作顺序数字 2 与字符串连接以产生一个新字符串。然后数字 1 与新字符串连接以产生最终结果。 4.8.2 一元算术运算符 一元运算符修改单个操作数的值以产生一个新值。在 JavaScript 中所有一元运算符都具有高优先级并且都是右结合的。本节描述的算术一元运算符、-、 和 --都将其单个操作数转换为数字如果需要的话。请注意标点字符 和 - 既用作一元运算符又用作二元运算符。 以下是一元算术运算符 一元加 一元加运算符将其操作数转换为数字或 NaN并返回该转换后的值。当与已经是数字的操作数一起使用时它不会执行任何操作。由于 BigInt 值无法转换为常规数字因此不能使用此运算符。 一元减- 当 - 作为一元运算符使用时它将其操作数转换为数字如果需要的话然后改变结果的符号。 递增 运算符递增即加 1其单个操作数该操作数必须是左值变量、数组元素或对象的属性。该运算符将其操作数转换为数字将 1 添加到该数字并将递增后的值重新赋给变量、元素或属性。 运算符的返回值取决于其相对于操作数的位置。当在操作数之前使用时称为前增量运算符它递增操作数并计算该操作数的递增值。当在操作数之后使用时称为后增量运算符它递增其操作数但计算该操作数的未递增值。考虑以下两行代码之间的区别 let i 1, j i; // i and j are both 2 let n 1, m n; // n is 2, m is 1注意表达式 x 不总是等同于 xx1。 运算符永远不会执行字符串连接它总是将其操作数转换为数字并递增。如果 x 是字符串“1”x 是数字 2但 x1 是字符串“11”。 还要注意由于 JavaScript 的自动分号插入您不能在后增量运算符和其前面的操作数之间插入换行符。如果这样做JavaScript 将把操作数视为一个独立的完整语句并在其前插入一个分号。 这个运算符在其前增量和后增量形式中最常用于递增控制 for 循环的计数器§5.4.3。 递减-- -- 运算符期望一个左值操作数。它将操作数的值转换为数字减去 1并将减少后的值重新赋给操作数。与 运算符一样-- 的返回值取决于其相对于操作数的位置。当在操作数之前使用时它减少并返回减少后的值。当在操作数之后使用时它减少操作数但返回未减少的值。在操作数之后使用时不允许换行符。 4.8.3 位运算符 位运算符对数字的二进制表示中的位进行低级别操作。虽然它们不执行传统的算术运算但在这里被归类为算术运算符因为它们对数字操作并返回一个数字值。这四个运算符对操作数的各个位执行布尔代数运算表现得好像每个操作数中的每个位都是一个布尔值1true0false。另外三个位运算符用于左移和右移位。这些运算符在 JavaScript 编程中并不常用如果你不熟悉整数的二进制表示包括负整数的二进制补码表示那么你可能可以跳过这一部分。 位运算符期望整数操作数并表现得好像这些值被表示为 32 位整数而不是 64 位浮点值。这些运算符将它们的操作数转换为数字如果需要的话然后通过丢弃任何小数部分和超过第 32 位的任何位来将数值值强制转换为 32 位整数。移位运算符需要一个右侧操作数介于 0 和 31 之间。在将此操作数转换为无符号 32 位整数后它们会丢弃超过第 5 位的任何位从而得到适当范围内的数字。令人惊讶的是当这些位运算符的操作数时NaN、Infinity 和 -Infinity 都会转换为 0。 所有这些位运算符除了 都可以与常规数字操作数或 BigInt参见 §3.2.5操作数一起使用。 位与 () 运算符对其整数参数的每个位执行布尔与操作。只有在两个操作数中相应的位都设置时结果中才设置一个位。例如0x1234 0x00FF 的计算结果为 0x0034。 位或 (|) | 运算符对其整数参数的每个位执行布尔或操作。如果相应的位在一个或两个操作数中的一个或两个中设置则结果中设置一个位。例如0x1234 | 0x00FF 的计算结果为 0x12FF。 位异或 (^) ^ 运算符对其整数参数的每个位执行布尔异或操作。异或意味着操作数一为 true 或操作数二为 true但不是两者都为 true。如果在这个操作的结果中设置了一个相应的位则表示两个操作数中的一个但不是两个中设置了一个位。例如0xFF00 ^ 0xF0F0 的计算结果为 0x0FF0。 位非 (~) ~ 运算符是一个一元运算符出现在其单个整数操作数之前。它通过反转操作数中的所有位来运行。由于 JavaScript 中有符号整数的表示方式将 ~ 运算符应用于一个值等同于改变其符号并减去 1。例如~0x0F 的计算结果为 0xFFFFFFF0或者 −16。 左移 () 运算符将其第一个操作数中的所有位向左移动指定的位数该位数应为介于 0 和 31 之间的整数。例如在操作 a 1 中a 的第一位个位变为第二位十位a 的第二位变为第三位依此类推。新的第一位使用零第 32 位的值丢失。将一个值左移一位等同于乘以 2将两个位置左移等同于乘以 4依此类推。例如7 2 的计算结果为 28。 带符号右移 () 运算符将其第一个操作数中的所有位向右移动指定的位数一个介于 0 和 31 之间的整数。向右移动的位将丢失。左侧填充的位取决于原始操作数的符号位以保留结果的符号。如果第一个操作数是正数则结果的高位为零如果第一个操作数是负数则结果的高位为一。向右移动一个正值相当于除以 2舍弃余数向右移动两个位置相当于整数除以 4依此类推。例如7 1 的结果为 3但请注意−7 1 的结果为−4。 零填充右移 () 运算符与 运算符类似只是左侧移入的位始终为零不管第一个操作数的符号如何。当您希望将有符号的 32 位值视为无符号整数时这很有用。例如−1 4 的结果为−1但−1 4 的结果为0x0FFFFFFF。这是 JavaScript 按位运算符中唯一不能与 BigInt 值一起使用的运算符。BigInt 不通过设置高位来表示负数而是通过特定的二进制补码表示。 4.9 关系表达式 本节描述了 JavaScript 的关系运算符。这些运算符测试两个值之间的关系如“相等”“小于”或“属性”并根据该关系是否存在返回true或false。关系表达式始终评估为布尔值并且该值通常用于控制程序执行在ifwhile和for语句中的流程参见第五章。接下来的小节记录了相等和不等运算符比较运算符以及 JavaScript 的另外两个关系运算符in和instanceof。 4.9.1 相等和不等运算符 和 运算符检查两个值是否相同使用两种不同的相同定义。这两个运算符接受任何类型的操作数并且如果它们的操作数相同则返回true如果它们不同则返回false。 运算符被称为严格相等运算符有时称为身份运算符它使用严格的相同定义来检查其两个操作数是否“相同”。 运算符被称为相等运算符它使用更宽松的相同定义来检查其两个操作数是否“相等”允许类型转换。 ! 和 ! 运算符测试 和 运算符的确刚好相反。! 不等运算符如果两个值根据相等则返回false否则返回true。! 运算符如果两个值严格相等则返回false否则返回true。正如您将在§4.10 中看到的! 运算符计算布尔非操作。这使得很容易记住! 和 ! 代表“不等于”和“不严格相等于”。 如§3.8 中所述JavaScript 对象通过引用而不是值进行比较。对象等于自身但不等于任何其他对象。如果两个不同的对象具有相同数量的属性具有相同名称和值则它们仍然不相等。同样具有相同顺序的相同元素的两个数组也不相等。 严格相等 严格相等运算符评估其操作数然后按照以下方式比较两个值不执行任何类型转换 如果两个值具有不同的类型则它们不相等。 如果两个值都是null或两个值都是undefined它们是相等的。 如果两个值都是布尔值true或都是布尔值false它们是相等的。 如果一个或两个值是NaN它们不相等。这很令人惊讶但NaN值永远不等于任何其他值包括它自己要检查值x是否为NaN请使用x ! x或全局的isNaN()函数。 如果两个值都是数字且具有相同的值则它们是相等的。如果一个值是0另一个是-0它们也是相等的。 如果两个值都是字符串且包含完全相同的 16 位值参见§3.3 中的侧边栏且位置相同则它们是相等的。如果字符串在长度或内容上有所不同则它们不相等。两个字符串可能具有相同的含义和相同的视觉外观但仍然使用不同的 16 位值序列进行编码。JavaScript 不执行 Unicode 规范化因此这样的一对字符串不被认为等于或运算符。 如果两个值引用相同的对象、数组或函数则它们是相等的。如果它们引用不同的对象则它们不相等即使两个对象具有相同的属性。 带类型转换的相等性 相等运算符类似于严格相等运算符但它不那么严格。如果两个操作数的值不是相同类型则它尝试一些类型转换并再次尝试比较 如果两个值具有相同的类型请按照前面描述的严格相等性进行测试。如果它们严格相等则它们是相等的。如果它们不严格相等则它们不相等。 如果两个值的类型不同运算符可能仍然认为它们相等。它使用以下规则和类型转换来检查相等性 如果一个值是null另一个是undefined它们是相等的。 如果一个值是数字另一个是字符串则将字符串转换为数字然后使用转换后的值再次尝试比较。 如果任一值为true则将其转换为 1然后再次尝试比较。如果任一值为false则将其转换为 0然后再次尝试比较。 如果一个值是对象另一个是数字或字符串则使用§3.9.3 中描述的算法将对象转换为原始值然后再次尝试比较。对象通过其toString()方法或valueOf()方法转换为原始值。核心 JavaScript 的内置类在执行toString()转换之前尝试valueOf()转换但 Date 类除外它执行toString()转换。 任何其他值的组合都不相等。 作为相等性测试的一个例子考虑比较 1 true // true此表达式求值为true表示这些外观非常不同的值实际上是相等的。布尔值true首先转换为数字 1然后再次进行比较。接下来字符串1转换为数字 1。由于现在两个值相同比较返回true。 4.9.2 比较运算符 这些比较运算符测试它们的两个操作数的相对顺序数字或字母 小于 () 运算符在其第一个操作数小于第二个操作数时求值为true否则求值为false。 大于 () 运算符在其第一个操作数大于第二个操作数时求值为true否则求值为false。 小于或等于 () 运算符在其第一个操作数小于或等于第二个操作数时求值为true否则求值为false。 大于或等于 () 运算符在其第一个操作数大于或等于第二个操作数时求值为true否则求值为false。 这些比较运算符的操作数可以是任何类型。但是比较只能在数字和字符串上执行因此不是数字或字符串的操作数将被转换。 比较和转换如下进行 如果任一操作数评估为对象则将该对象转换为原始值如§3.9.3 末尾所述如果其valueOf()方法返回原始值则使用该值。否则使用其toString()方法的返回值。 如果在任何必要的对象到原始值转换后两个操作数都是字符串则比较这两个字符串使用字母顺序其中“字母顺序”由组成字符串的 16 位 Unicode 值的数值顺序定义。 如果在对象到原始值转换后至少有一个操作数不是字符串则两个操作数都将转换为数字并进行数值比较。0和-0被视为相等。Infinity大于除自身以外的任何数字而-Infinity小于除自身以外的任何数字。如果任一操作数是或转换为NaN则比较运算符始终返回false。尽管算术运算符不允许 BigInt 值与常规数字混合使用但比较运算符允许数字和 BigInt 之间的比较。 请记住JavaScript 字符串是 16 位整数值的序列并且字符串比较只是对两个字符串中的值进行数值比较。Unicode 定义的数值编码顺序可能与任何特定语言或区域设置中使用的传统排序顺序不匹配。特别注意字符串比较区分大小写所有大写 ASCII 字母都“小于”所有小写 ASCII 字母。如果您没有预期此规则可能导致令人困惑的结果。例如根据运算符字符串“Zoo”在字符串“aardvark”之前。 对于更强大的字符串比较算法请尝试String.localeCompare()方法该方法还考虑了特定区域设置的字母顺序定义。对于不区分大小写的比较您可以使用String.toLowerCase()或String.toUpperCase()将字符串转换为全小写或全大写。而且为了使用更通用且更好本地化的字符串比较工具请使用§11.7.3 中描述的 Intl.Collator 类。 运算符和比较运算符对数字和字符串操作数的行为不同。偏向于字符串如果任一操作数是字符串则执行连接操作。比较运算符偏向于数字只有在两个操作数都是字符串时才执行字符串比较 1 2 // 3: addition. 1 2 // 12: concatenation. 1 2 // 12: 2 is converted to 2. 11 3 // false: numeric comparison. 11 3 // true: string comparison. 11 3 // false: numeric comparison, 11 converted to 11. one 3 // false: numeric comparison, one converted to NaN.最后请注意小于或等于和大于或等于运算符不依赖于相等或严格相等运算符来确定两个值是否“相等”。相反小于或等于运算符简单地定义为“不大于”大于或等于运算符定义为“不小于”。唯一的例外是当任一操作数是或转换为NaN时此时所有四个比较运算符都返回false。 4.9.3 in 运算符 in运算符期望左侧操作数是一个字符串、符号或可转换为字符串的值。它期望右侧操作数是一个对象。如果左侧值是右侧对象的属性名称则评估为true。例如 let point {x: 1, y: 1}; // Define an object x in point // true: object has property named x z in point // false: object has no z property. toString in point // true: object inherits toString methodlet data [7,8,9]; // An array with elements (indices) 0, 1, and 2 0 in data // true: array has an element 0 1 in data // true: numbers are converted to strings 3 in data // false: no element 34.9.4 instanceof 运算符 instanceof运算符期望左侧操作数是一个对象右侧操作数标识对象类。如果左侧对象是右侧类的实例则运算符评估为true否则评估为false。第九章解释了在 JavaScript 中对象类由初始化它们的构造函数定义。因此instanceof的右侧操作数应该是一个函数。以下是示例 let d new Date(); // Create a new object with the Date() constructor d instanceof Date // true: d was created with Date() d instanceof Object // true: all objects are instances of Object d instanceof Number // false: d is not a Number object let a [1, 2, 3]; // Create an array with array literal syntax a instanceof Array // true: a is an array a instanceof Object // true: all arrays are objects a instanceof RegExp // false: arrays are not regular expressions注意所有对象都是Object的实例。instanceof在判断一个对象是否是某个类的实例时会考虑“超类”。如果instanceof的左操作数不是对象则返回false。如果右操作数不是对象类则抛出TypeError。 要理解instanceof运算符的工作原理您必须了解“原型链”。这是 JavaScript 的继承机制描述在§6.3.2 中。要评估表达式o instanceof fJavaScript 会评估f.prototype然后在o的原型链中查找该值。如果找到则o是f的实例或f的子类运算符返回true。如果f.prototype不是o的原型链中的值之一则o不是f的实例instanceof返回false。 4.10 逻辑表达式 逻辑运算符、||和!执行布尔代数通常与关系运算符结合使用将两个关系表达式组合成一个更复杂的表达式。这些运算符在接下来的小节中描述。为了完全理解它们您可能需要回顾§3.4 中介绍的“真值”和“假值”概念。 4.10.1 逻辑 AND 运算符可以在三个不同级别理解。在最简单的级别上当与布尔操作数一起使用时对这两个值执行布尔 AND 操作仅当其第一个操作数和第二个操作数都为true时才返回true。如果其中一个或两个操作数为false则返回false。 经常用作连接两个关系表达式的连接词 x 0 y 0 // true if, and only if, x and y are both 0关系表达式始终评估为true或false因此在这种情况下运算符本身返回true或false。关系运算符的优先级高于和||因此可以安全地写出不带括号的表达式。 但是不要求其操作数是布尔值。回想一下所有 JavaScript 值都是“真值”或“假值”。有关详细信息请参阅§3.4。假值包括false、null、undefined、0、-0、NaN和。所有其他值包括所有对象都是真值。的第二个级别可以理解为真值和假值的布尔 AND 运算符。如果两个操作数都是真值则运算符返回真值。否则一个或两个操作数必须是假值运算符返回假值。在 JavaScript 中任何期望布尔值的表达式或语句都可以使用真值或假值因此并不总是返回true或false不会造成实际问题。 请注意此描述指出该运算符返回“真值”或“假值”但没有指定该值是什么。为此我们需要在第三个最终级别描述。该运算符首先评估其第一个操作数即左侧的表达式。如果左侧的值为假整个表达式的值也必须为假因此只返回左侧的值甚至不评估右侧的表达式。 另一方面如果左侧的值为真值则表达式的整体值取决于右侧的值。如果右侧的值为真值则整体值必须为真值如果右侧的值为假值则整体值必须为假值。因此当左侧的值为真值时运算符评估并返回右侧的值 let o {x: 1}; let p null; o o.x // 1: o is truthy, so return value of o.x p p.x // null: p is falsy, so return it and dont evaluate p.x重要的是要理解 可能会或可能不会评估其右侧操作数。在这个代码示例中变量 p 被设置为 null并且表达式 p.x 如果被评估将导致 TypeError。但是代码以一种惯用的方式使用 以便仅在 p 为真值时才评估 p.x而不是 null 或 undefined。 的行为有时被称为短路你可能会看到故意利用这种行为有条件地执行代码的代码。例如下面两行 JavaScript 代码具有等效的效果 if (a b) stop(); // Invoke stop() only if a b (a b) stop(); // This does the same thing一般来说当你在 的右侧写一个具有副作用赋值、递增、递减或函数调用的表达式时你必须小心。这些副作用是否发生取决于左侧的值。 尽管这个运算符实际上的工作方式有些复杂但它最常用作一个简单的布尔代数运算符适用于真值和假值。 4.10.2 逻辑 OR (||) || 运算符对其两个操作数执行布尔 OR 操作。如果一个或两个操作数为真值则返回真值。如果两个操作数都为假值则返回假值。 尽管 || 运算符通常被简单地用作布尔 OR 运算符但它和 运算符一样具有更复杂的行为。它首先评估其第一个操作数即左侧的表达式。如果这个第一个操作数的值为真值它会短路并返回该真值而不会评估右侧的表达式。另一方面如果第一个操作数的值为假值则 || 评估其第二个操作数并返回该表达式的值。 与 运算符一样你应该避免包含副作用的右侧操作数除非你故意想要利用右侧表达式可能不会被评估的事实。 这个运算符的一个惯用用法是在一组备选项中选择第一个真值 // If maxWidth is truthy, use that. Otherwise, look for a value in // the preferences object. If that is not truthy, use a hardcoded constant. let max maxWidth || preferences.maxWidth || 500;请注意如果 0 是 maxWidth 的合法值则此代码将无法正常工作因为 0 是一个假值。参见 ?? 运算符§4.13.2以获取替代方案。 在 ES6 之前这种习惯通常用于函数中为参数提供默认值 // Copy the properties of o to p, and return p function copy(o, p) {p p || {}; // If no object passed for p, use a newly created object.// function body goes here }然而在 ES6 及以后这个技巧不再需要因为默认参数值可以直接写在函数定义中function copy(o, p{}) { ... }。 4.10.3 逻辑 NOT (!) ! 运算符是一个一元运算符它放在单个操作数之前。它的目的是反转其操作数的布尔值。例如如果 x 是真值!x 评估为 false。如果 x 是假值则 !x 是 true。 与 和 || 运算符不同! 运算符在反转转换其操作数为布尔值使用 第三章 中描述的规则之前。这意味着 ! 总是返回 true 或 false你可以通过两次应用这个运算符将任何值 x 转换为其等效的布尔值!!x参见 §3.9.2。 作为一元运算符! 具有高优先级并且紧密绑定。如果你想反转类似 p q 的表达式的值你需要使用括号!(p q)。值得注意的是我们可以使用 JavaScript 语法表达布尔代数的两个定律 // DeMorgans Laws !(p q) (!p || !q) // true: for all values of p and q !(p || q) (!p !q) // true: for all values of p and q4.11 赋值表达式 JavaScript 使用 运算符将一个值分配给一个变量或属性。例如 i 0; // Set the variable i to 0. o.x 1; // Set the property x of object o to 1.运算符期望其左侧操作数是一个 lvalue一个变量或对象属性或数组元素。它期望其右侧操作数是任何类型的任意值。赋值表达式的值是右侧操作数的值。作为副作用 运算符将右侧的值分配给左侧的变量或属性以便将来对变量或属性的引用评估为该值。 虽然赋值表达式通常相当简单但有时您可能会看到赋值表达式的值作为更大表达式的一部分使用。例如您可以使用以下代码在同一表达式中赋值和测试一个值 (a b) 0如果这样做请确保您清楚和运算符之间的区别请注意的优先级非常低当赋值的值要在更大的表达式中使用时通常需要括号。 赋值运算符具有从右到左的结合性这意味着当表达式中出现多个赋值运算符时它们将从右到左进行评估。因此您可以编写如下代码将单个值分配给多个变量 i j k 0; // Initialize 3 variables to 04.11.1 带操作符的赋值 除了正常的赋值运算符外JavaScript 还支持许多其他赋值运算符通过将赋值与其他操作结合起来提供快捷方式。例如运算符执行加法和赋值。以下表达式 total salesTax;等同于这个 total total salesTax;正如您所期望的那样运算符适用于数字或字符串。对于数字操作数它执行加法和赋值对于字符串操作数它执行连接和赋值。 类似的运算符包括-、*、等。表 4-2 列出了它们全部。 表 4-2. 赋值运算符 运算符示例等价a ba a b-a - ba a - b*a * ba a * b/a / ba a / b%a % ba a % b**a ** ba a ** ba ba a ba ba a ba ba a ba ba a b#124;a #124; ba a #124; b^a ^ ba a ^ b 在大多数情况下表达式 a op b其中op是一个运算符等价于表达式 a a op b在第一行中表达式a被评估一次。在第二行中它被评估两次。这两种情况只有在a包含函数调用或增量运算符等副作用时才会有所不同。例如以下两个赋值是不同的 data[i] * 2; data[i] data[i] * 2;4.12 评估表达式 与许多解释性语言一样JavaScript 有解释 JavaScript 源代码字符串并对其进行评估以生成值的能力。JavaScript 使用全局函数eval()来实现这一点 eval(32) // 5动态评估源代码字符串是一种强大的语言特性在实践中几乎从不需要。如果您发现自己使用eval()您应该仔细考虑是否真的需要使用它。特别是eval()可能存在安全漏洞您绝不应将任何源自用户输入的字符串传递给eval()。由于 JavaScript 这样复杂的语言没有办法对用户输入进行清理以使其安全用于eval()。由于这些安全问题一些 Web 服务器使用 HTTP 的“内容安全策略”头部来禁用整个网站的eval()。 接下来的小节将解释eval()的基本用法并解释两个对优化器影响较小的受限版本。 4.12.1 eval() eval()期望一个参数。如果传递的值不是字符串则它只是返回该值。如果传递一个字符串则它尝试将字符串解析为 JavaScript 代码如果失败则抛出 SyntaxError。如果成功解析字符串则评估代码并返回字符串中最后一个表达式或语句的值如果最后一个表达式或语句没有值则返回undefined。如果评估的字符串引发异常则该异常从调用eval()传播出来。 eval()的关键之处在这种情况下调用是它使用调用它的代码的变量环境。也就是说它查找变量的值并以与局部代码相同的方式定义新变量和函数。如果一个函数定义了一个局部变量x然后调用eval(x)它将获得局部变量的值。如果它调用eval(x1)它会改变局部变量的值。如果函数调用eval(var y 3;)它会声明一个新的局部变量y。另一方面如果被评估的字符串使用let或const则声明的变量或常量将局部于评估并不会在调用环境中定义。 类似地函数可以使用以下代码声明一个局部函数 eval(function f() { return x1; });如果你从顶层代码调用eval()它当然会操作全局变量和全局函数。 请注意传递给eval()的代码字符串必须在语法上是合理的你不能使用它来将代码片段粘贴到函数中。例如写eval(return;)是没有意义的因为return只在函数内部合法而被评估的字符串使用与调用函数相同的变量环境并不使其成为该函数的一部分。如果你的字符串作为独立脚本是合理的即使是非常简短的像x0那么它是可以传递给eval()的。否则eval()会抛出 SyntaxError。 4.12.2 全局 eval() 正是eval()改变局部变量的能力让 JavaScript 优化器感到困扰。然而作为一种解决方法解释器只是对调用eval()的任何函数进行较少的优化。但是如果一个脚本定义了eval()的别名然后通过另一个名称调用该函数JavaScript 规范声明当eval()被任何名称调用时除了“eval”之外它应该评估字符串就像它是顶层全局代码一样。被评估的代码可以定义新的全局变量或全局函数并且可以设置全局变量但不会使用或修改调用函数的局部变量因此不会干扰局部优化。 “直接 eval”是使用确切的、未限定名称“eval”调用eval()函数的表达式开始感觉像是一个保留字。直接调用eval()使用调用上下文的变量环境。任何其他调用——间接调用——使用全局对象作为其变量环境不能读取、写入或定义局部变量或函数。直接和间接调用只能使用var定义新变量。在评估的字符串中使用let和const会创建仅在评估中局部的变量和常量不会改变调用或全局环境。 以下代码演示 const geval eval; // Using another name does a global eval let x global, y global; // Two global variables function f() { // This function does a local evallet x local; // Define a local variableeval(x changed;); // Direct eval sets local variablereturn x; // Return changed local variable } function g() { // This function does a global evallet y local; // A local variablegeval(y changed;); // Indirect eval sets global variablereturn y; // Return unchanged local variable } console.log(f(), x); // Local variable changed: prints localchanged global: console.log(g(), y); // Global variable changed: prints local globalchanged:请注意进行全局 eval 的能力不仅仅是为了优化器的需要实际上这是一个非常有用的功能允许你执行字符串代码就像它们是独立的顶层脚本一样。正如本节开头所述真正需要评估代码字符串是罕见的。但是如果你确实发现有必要你更可能想要进行全局 eval 而不是局部 eval。 4.12.3 严格 eval() 严格模式参见§5.6.3对eval()函数的行为甚至对标识符“eval”的使用施加了进一步的限制。当从严格模式代码中调用eval()或者当要评估的代码字符串本身以“use strict”指令开头时eval()会使用私有变量环境进行局部评估。这意味着在严格模式下被评估的代码可以查询和设置局部变量但不能在局部范围内定义新变量或函数。 此外严格模式使 eval() 更像是一个运算符有效地将“eval”变成了一个保留字。你不能用新值覆盖 eval() 函数。你也不能声明一个名为“eval”的变量、函数、函数参数或 catch 块参数。 4.13 其他运算符 JavaScript 支持许多其他杂项运算符详细描述在以下章节。 4.13.1 条件运算符 (? 条件运算符是 JavaScript 中唯一的三元运算符有时实际上被称为三元运算符。这个运算符有时被写为 ?:尽管在代码中看起来并不完全是这样。因为这个运算符有三个操作数第一个在 ? 前面第二个在 ? 和 : 之间第三个在 : 后面。使用方法如下 x 0 ? x : -x // The absolute value of x条件运算符的操作数可以是任何类型。第一个操作数被评估并解释为布尔值。如果第一个操作数的值为真值则评估第二个操作数并返回其值。否则如果第一个操作数为假值则评估第三个操作数并返回其值。第二个和第三个操作数中只有一个被评估永远不会同时评估两个。 虽然可以使用 if 语句 (§5.3.1) 实现类似的结果但 ?: 运算符通常提供了一个便捷的快捷方式。以下是一个典型的用法检查变量是否已定义并具有有意义的真值如果是则使用它否则提供默认值 greeting hello (username ? username : there);这等同于以下 if 语句但更简洁 greeting hello ; if (username) {greeting username; } else {greeting there; }4.13.2 第一个定义的 (??) 第一个定义运算符 ?? 的值为其第一个定义的操作数如果其左操作数不是 null 且不是 undefined则返回该值。否则返回右操作数的值。与 和 || 运算符一样?? 是短路运算只有在第一个操作数评估为 null 或 undefined 时才评估第二个操作数。如果表达式 a 没有副作用那么表达式 a ?? b 等效于 (a ! null a ! undefined) ? a : b当你想选择第一个定义的操作数而不是第一个真值操作数时?? 是 || (§4.10.2) 的一个有用替代。虽然 || 名义上是一个逻辑 OR 运算符但它也被习惯性地用来选择第一个非假值操作数例如以下代码 // If maxWidth is truthy, use that. Otherwise, look for a value in // the preferences object. If that is not truthy, use a hardcoded constant. let max maxWidth || preferences.maxWidth || 500;这种习惯用法的问题在于零、空字符串和 false 都是假值在某些情况下可能是完全有效的值。在这个代码示例中如果 maxWidth 是零则该值将被忽略。但如果我们将 || 运算符改为 ??我们最终得到一个零是有效值的表达式 // If maxWidth is defined, use that. Otherwise, look for a value in // the preferences object. If that is not defined, use a hardcoded constant. let max maxWidth ?? preferences.maxWidth ?? 500;以下是更多示例展示了当第一个操作数为假值时 ?? 的工作原理。如果该操作数为假值但已定义则 ?? 返回它。只有当第一个操作数为“nullish”即 null 或 undefined时该运算符才会评估并返回第二个操作数 let options { timeout: 0, title: , verbose: false, n: null }; options.timeout ?? 1000 // 0: as defined in the object options.title ?? Untitled // : as defined in the object options.verbose ?? true // false: as defined in the object options.quiet ?? false // false: property is not defined options.n ?? 10 // 10: property is null请注意如果我们使用 || 而不是 ??这里的 timeout、title 和 verbose 表达式将具有不同的值。 ?? 运算符类似于 和 || 运算符但它的优先级既不高于它们也不低于它们。如果你在一个表达式中使用它与这些运算符之一你必须使用显式括号来指定你想要先执行哪个操作 (a ?? b) || c // ?? first, then || a ?? (b || c) // || first, then ?? a ?? b || c // SyntaxError: parentheses are required?? 运算符由 ES2020 定义在 2020 年初所有主要浏览器的当前版本或 beta 版本都新支持该运算符。这个运算符正式称为“nullish coalescing”运算符但我避免使用这个术语因为这个运算符选择其操作数之一但在我看来并没有以任何方式“合并”它们。 4.13.3 typeof 运算符 typeof 是一个一元运算符放置在其单个操作数之前该操作数可以是任何类型。它的值是一个指定操作数类型的字符串。Table 4-3 指定了typeof 运算符对任何 JavaScript 值的值。 表 4-3。typeof 运算符返回的值 xtypeof xundefinedundefinednullobjecttrue 或 falseboolean任何数字或 NaNnumber任何 BigIntbigint任何字符串string任何符号symbol任何函数function任何非函数对象object 您可能会在表达式中使用typeof 运算符如下所示 // If the value is a string, wrap it in quotes, otherwise, convert (typeof value string) ? value : value.toString()注意如果操作数值为nulltypeof 返回“object”。如果要区分null 和对象您必须明确测试这种特殊情况的值。 尽管 JavaScript 函数是一种对象但typeof 运算符认为函数与其他对象有足够大的不同因此它们有自己的返回值。 因为对于除函数之外的所有对象和数组值typeof 都会评估为“object”所以它只有在区分对象和其他原始类型时才有用。为了区分一个类的对象与另一个类的对象您必须使用其他技术如instanceof 运算符参见§4.9.4、class 属性参见§14.4.3或constructor 属性参见§9.2.2 和§14.3。 4.13.4 delete 运算符 delete 是一个一元运算符试图删除指定为其操作数的对象属性或数组元素。与赋值、递增和递减运算符一样delete 通常用于其属性删除副作用而不是用于其返回的值。一些例子 let o { x: 1, y: 2}; // Start with an object delete o.x; // Delete one of its properties x in o // false: the property does not exist anymorelet a [1,2,3]; // Start with an array delete a[2]; // Delete the last element of the array 2 in a // false: array element 2 doesnt exist anymore a.length // 3: note that array length doesnt change, though请注意删除的属性或数组元素不仅仅被设置为undefined 值。当删除属性时该属性将不再存在。尝试读取不存在的属性会返回undefined但您可以使用in 运算符§4.9.3测试属性的实际存在性。删除数组元素会在数组中留下一个“空洞”并且不会更改数组的长度。结果数组是稀疏的§7.3。 delete 期望其操作数为左值。如果它不是左值则运算符不起作用并返回true。否则delete 会尝试删除指定的左值。如果成功删除指定的左值则delete 返回true。然而并非所有属性都可以被删除不可配置的属性§14.1不受删除的影响。 在严格模式下如果其操作数是未经限定的标识符如变量、函数或函数参数则delete 会引发 SyntaxError它仅在操作数为属性访问表达式时起作用§4.4。严格模式还指定如果要删除任何不可配置的即不可删除的属性则delete 会引发 TypeError。在严格模式之外这些情况不会发生异常delete 简单地返回false表示无法删除操作数。 以下是delete 运算符的一些示例用法 let o {x: 1, y: 2}; delete o.x; // Delete one of the object properties; returns true. typeof o.x; // Property does not exist; returns undefined. delete o.x; // Delete a nonexistent property; returns true. delete 1; // This makes no sense, but it just returns true. // Cant delete a variable; returns false, or SyntaxError in strict mode. delete o; // Undeletable property: returns false, or TypeError in strict mode. delete Object.prototype;我们将在§6.4 中再次看到delete 运算符。 4.13.5 await 运算符 await在 ES2017 中引入作为使 JavaScript 中的异步编程更自然的一种方式。您需要阅读第十三章以了解此运算符。简而言之await期望一个 Promise 对象表示异步计算作为其唯一操作数并使您的程序表现得好像正在等待异步计算完成但实际上不会阻塞并且不会阻止其他异步操作同时进行。await运算符的值是 Promise 对象的完成值。重要的是await只在使用async关键字声明的函数内部合法。再次查看第十三章获取完整详情。 4.13.6 void 运算符 void是一个一元运算符出现在其单个操作数之前该操作数可以是任何类型。这个运算符是不寻常且很少使用的它评估其操作数然后丢弃值并返回undefined。由于操作数值被丢弃只有在操作数具有副作用时使用void运算符才有意义。 void运算符如此隐晦以至于很难想出其使用的实际示例。一个情况是当您想要定义一个什么都不返回但也使用箭头函数快捷语法的函数时参见§8.1.3其中函数体是一个被评估并返回的单个表达式。如果您仅仅为了其副作用而评估表达式并且不想返回其值那么最简单的方法是在函数体周围使用大括号。但是作为替代方案在这种情况下您也可以使用void运算符 let counter 0; const increment () void counter; increment() // undefined counter // 14.13.7 逗号运算符, 逗号运算符是一个二元运算符其操作数可以是任何类型。它评估其左操作数评估其右操作数然后返回右操作数的值。因此以下行 i0, j1, k2;评估为 2基本上等同于 i 0; j 1; k 2;左侧表达式始终被评估但其值被丢弃这意味着只有在左侧表达式具有副作用时才有意义使用逗号运算符。逗号运算符通常使用的唯一情况是在具有多个循环变量的for循环§5.4.3中 // The first comma below is part of the syntax of the let statement // The second comma is the comma operator: it lets us squeeze 2 // expressions (i and j--) into a statement (the for loop) that expects 1. for(let i0,j10; i j; i,j--) {console.log(ij); }4.14 总结 本章涵盖了各种主题并且这里有很多参考资料您可能希望在未来继续学习 JavaScript 时重新阅读。然而需要记住的一些关键点是 表达式是 JavaScript 程序的短语。 任何表达式都可以评估为 JavaScript 值。 表达式除了产生一个值外还可能具有副作用如变量赋值。 简单表达式如文字变量引用和属性访问可以与运算符结合以产生更大的表达式。 JavaScript 定义了用于算术比较布尔逻辑赋值和位操作的运算符以及一些其他运算符包括三元条件运算符。 JavaScript 运算符用于添加数字和连接字符串。 逻辑运算符和||具有特殊的“短路”行为有时只评估它们的一个参数。常见的 JavaScript 习语要求您了解这些运算符的特殊行为。 第五章语句 第四章将表达式描述为 JavaScript 短语。按照这个类比语句是 JavaScript 句子或命令。就像英语句子用句号终止并用句号分隔开一样JavaScript 语句用分号终止§2.6。表达式被评估以产生一个值但语句被执行以使某事发生。 使某事发生的一种方法是评估具有副作用的表达式。具有副作用的表达式如赋值和函数调用可以独立作为语句存在当以这种方式使用时被称为表达式语句。另一类语句是声明语句它声明新变量并定义新函数。 JavaScript 程序只不过是一系列要执行的语句。默认情况下JavaScript 解释器按照它们编写的顺序一个接一个地执行这些语句。改变这种默认执行顺序的另一种方法是使用 JavaScript 中的一些语句或控制结构 条件语句 诸如if和switch这样的语句根据表达式的值使 JavaScript 解释器执行或跳过其他语句 循环 诸如while和for这样重复执行其他语句的语句 跳转 诸如break、return和throw这样的语句会导致解释器跳转到程序的另一个部分 接下来的章节描述了 JavaScript 中的各种语句并解释了它们的语法。表 5-1 在本章末尾总结了语法。JavaScript 程序只不过是一系列语句用分号分隔开因此一旦熟悉了 JavaScript 的语句就可以开始编写 JavaScript 程序。 5.1 表达式语句 JavaScript 中最简单的语句是具有副作用的表达式。这种语句在第四章中有所展示。赋值语句是表达式语句的一个主要类别。例如 greeting Hello name; i * 3;递增和递减运算符和--与赋值语句相关。它们具有改变变量值的副作用就像执行了一个赋值一样 counter;delete 运算符的重要副作用是删除对象属性。因此它几乎总是作为语句使用而不是作为更大表达式的一部分 delete o.x;函数调用是另一种重要的表达式语句。例如 console.log(debugMessage); displaySpinner(); // A hypothetical function to display a spinner in a web app.这些函数调用是表达式但它们具有影响主机环境或程序状态的副作用并且在这里被用作语句。如果一个函数没有任何副作用那么调用它就没有意义除非它是更大表达式或赋值语句的一部分。例如你不会仅仅计算余弦值然后丢弃结果 Math.cos(x);但你可能会计算值并将其赋给一个变量以备将来使用 cx Math.cos(x);请注意这些示例中的每行代码都以分号结束。 5.2 复合语句和空语句 就像逗号运算符§4.13.7将多个表达式组合成一个单一表达式一样语句块将多个语句组合成一个复合语句。语句块只是一系列语句被花括号包围起来。因此以下行作为单个语句并可以在 JavaScript 需要单个语句的任何地方使用 {x Math.PI;cx Math.cos(x);console.log(cos(π) cx); }关于这个语句块有几点需要注意。首先它不以分号结束。块内的原始语句以分号结束但块本身不以分号结束。其次块内的行相对于包围它们的花括号缩进。这是可选的但它使代码更易于阅读和理解。 就像表达式经常包含子表达式一样许多 JavaScript 语句包含子语句。形式上JavaScript 语法通常允许单个子语句。例如while循环语法包括一个作为循环体的单个语句。使用语句块您可以在这个单个允许的子语句中放置任意数量的语句。 复合语句允许您在 JavaScript 语法期望单个语句的地方使用多个语句。空语句则相反它允许您在期望一个语句的地方不包含任何语句。空语句如下所示 ;当执行空语句时JavaScript 解释器不会采取任何操作。空语句偶尔在您想要创建一个空循环体的循环时很有用。考虑以下for循环for循环将在§5.4.3 中介绍 // Initialize an array a for(let i 0; i a.length; a[i] 0) ;在这个循环中所有工作都由表达式a[i] 0完成不需要循环体。然而JavaScript 语法要求循环体作为一个语句因此使用了一个空语句——只是一个裸分号。 请注意在for循环、while循环或if语句的右括号后意外包含分号可能导致难以检测的令人沮丧的错误。例如以下代码可能不会按照作者的意图执行 if ((a 0) || (b 0)); // Oops! This line does nothing...o null; // and this line is always executed.当您有意使用空语句时最好以一种清晰表明您是有意这样做的方式对代码进行注释。例如 for(let i 0; i a.length; a[i] 0) /* empty */ ;5.3 条件语句 条件语句根据指定表达式的值执行或跳过其他语句。这些语句是您代码的决策点有时也被称为“分支”。如果想象一个 JavaScript 解释器沿着代码路径执行条件语句是代码分支成两个或多个路径的地方解释器必须选择要遵循的路径。 以下小节解释了 JavaScript 的基本条件语句if/else并介绍了更复杂的多路分支语句switch。 5.3.1 if if语句是允许 JavaScript 做出决策的基本控制语句更准确地说是有条件地执行语句。该语句有两种形式。第一种是 if (*expression*)*statement*在这种形式中expression被评估。如果结果值为真值将执行statement。如果expression为假值则不执行statement。有关真值和假值的定义请参见§3.4。例如 if (username null) // If username is null or undefined,username John Doe; // define it或者类似地 // If username is null, undefined, false, 0, , or NaN, give it a new value if (!username) username John Doe;请注意围绕expression的括号是if语句语法的必需部分。 JavaScript 语法要求在if关键字和括号表达式之后有一个语句但您可以使用语句块将多个语句组合成一个。因此if语句也可能如下所示 if (!address) {address ;message Please specify a mailing address.; }第二种形式的if语句引入了一个else子句当expression为false时执行。其语法如下 if (*expression*)*statement1* else*statement2*该语句形式在expression为真值时执行statement1在expression为假值时执行statement2。例如 if (n 1)console.log(You have 1 new message.); elseconsole.log(You have ${n} new messages.);当您有嵌套的带有else子句的if语句时需要谨慎确保else子句与适当的if语句配对。考虑以下行 i j 1; k 2; if (i j)if (j k)console.log(i equals k); elseconsole.log(i doesnt equal j); // WRONG!!在这个例子中内部的if语句形成了外部if语句语法允许的单个语句。不幸的是不清楚除了缩进给出的提示外else与哪个if配对。而且在这个例子中缩进是错误的因为 JavaScript 解释器实际上将前一个例子解释为 if (i j) {if (j k)console.log(i equals k);elseconsole.log(i doesnt equal j); // OOPS! }JavaScript与大多数编程语言一样的规则是默认情况下else子句是最近的if语句的一部分。为了使这个例子不那么模棱两可更容易阅读、理解、维护和调试您应该使用花括号 if (i j) {if (j k) {console.log(i equals k);} } else { // What a difference the location of a curly brace makes!console.log(i doesnt equal j); }许多程序员习惯将 if 和 else 语句的主体以及其他复合语句如 while 循环放在花括号中即使主体只包含一个语句。始终如此可以防止刚才显示的问题我建议你采用这种做法。在这本印刷书中我非常重视保持示例代码的垂直紧凑性并且并不总是遵循自己在这个问题上的建议。 5.3.2 else if if/else 语句评估一个表达式并根据结果执行两个代码块中的一个。但是当你需要执行多个代码块中的一个时怎么办一种方法是使用 else if 语句。else if 实际上不是一个 JavaScript 语句而只是一个经常使用的编程习惯当使用重复的 if/else 语句时会出现 if (n 1) {// Execute code block #1 } else if (n 2) {// Execute code block #2 } else if (n 3) {// Execute code block #3 } else {// If all else fails, execute block #4 }这段代码没有什么特别之处。它只是一系列 if 语句每个后续的 if 都是前一个语句的 else 子句的一部分。使用 else if 习惯比在其语法上等效的完全嵌套形式中编写这些语句更可取也更易读 if (n 1) {// Execute code block #1 } else {if (n 2) {// Execute code block #2}else {if (n 3) {// Execute code block #3}else {// If all else fails, execute block #4}} }5.3.3 switch if 语句会导致程序执行流程的分支你可以使用 else if 习惯来执行多路分支。然而当所有分支都依赖于相同表达式的值时这并不是最佳解决方案。在这种情况下多次在多个 if 语句中评估该表达式是浪费的。 switch 语句正好处理这种情况。switch 关键字后跟着括号中的表达式和花括号中的代码块 switch(*expression*) {*statements* }然而switch 语句的完整语法比这更复杂。代码块中的各个位置都用 case 关键字标记后跟一个表达式和一个冒号。当 switch 执行时它计算表达式的值然后寻找一个 case 标签其表达式的值与之相同相同性由 运算符确定。如果找到一个匹配值的 case它会从标记为 case 的语句开始执行代码块。如果找不到具有匹配值的 case它会寻找一个标记为 default: 的语句。如果没有 default: 标签switch 语句会跳过整个代码块。 switch 是一个很难解释的语句通过一个例子它的操作会变得更加清晰。下面的 switch 语句等同于前一节中展示的重复的 if/else 语句 switch(n) { case 1: // Start here if n 1// Execute code block #1.break; // Stop here case 2: // Start here if n 2// Execute code block #2.break; // Stop here case 3: // Start here if n 3// Execute code block #3.break; // Stop here default: // If all else fails...// Execute code block #4.break; // Stop here }注意这段代码中每个 case 结尾使用的 break 关键字。break 语句会在本章后面描述它会导致解释器跳出或“中断”switch 语句并继续执行后面的语句。switch 语句中的 case 子句只指定所需代码的起始点它们不指定任何结束点。在没有 break 语句的情况下switch 语句会从与其表达式值匹配的 case 标签开始执行其代码块并继续执行语句直到达到代码块的末尾。在极少数情况下编写“穿透”从一个 case 标签到下一个的代码是有用的但 99% 的情况下你应该小心地用 break 语句结束每个 case。然而在函数内部使用 switch 时你可以使用 return 语句代替 break 语句。两者都用于终止 switch 语句并防止执行穿透到下一个 case。 这里是 switch 语句的一个更加现实的例子它根据值的类型将值转换为字符串 function convert(x) {switch(typeof x) {case number: // Convert the number to a hexadecimal integerreturn x.toString(16);case string: // Return the string enclosed in quotesreturn x ;default: // Convert any other type in the usual wayreturn String(x);} }请注意在前两个示例中case关键字分别后跟数字和字符串字面量。这是switch语句在实践中最常用的方式但请注意ECMAScript 标准允许每个case后跟任意表达式。 switch语句首先评估跟在switch关键字后面的表达式然后按照它们出现的顺序评估case表达式直到找到匹配的值。匹配的情况是使用身份运算符确定的而不是相等运算符因此表达式必须在没有任何类型转换的情况下匹配。 因为并非每次执行switch语句时都会评估所有case表达式所以应避免使用包含函数调用或赋值等副作用的case表达式。最安全的做法是将case表达式限制为常量表达式。 如前所述如果没有case表达式与switch表达式匹配switch语句将从标记为default:的语句处开始执行其主体。如果没有default:标签则switch语句将完全跳过其主体。请注意在所示示例中default:标签出现在switch主体的末尾跟在所有case标签后面。这是一个逻辑和常见的位置但实际上它可以出现在语句主体的任何位置。 5.4 循环 要理解条件语句我们可以想象 JavaScript 解释器通过源代码的分支路径。循环语句是将该路径弯回自身以重复代码部分的语句。JavaScript 有五个循环语句while、do/while、for、for/of及其for/await变体和for/in。以下各小节依次解释每个循环语句。循环的一个常见用途是遍历数组元素。§7.6 详细讨论了这种循环并涵盖了 Array 类定义的特殊循环方法。 5.4.1 while 就像if语句是 JavaScript 的基本条件语句一样while语句是 JavaScript 的基本循环语句。它的语法如下 while (*expression*)*statement*要执行while语句解释器首先评估expression。如果表达式的值为假值则解释器跳过作为循环体的statement并继续执行程序中的下一条语句。另一方面如果expression为真值则解释器执行statement并重复跳回循环的顶部并再次评估expression。另一种说法是解释器在expression为真值时重复执行statement。请注意您可以使用while(true)语法创建一个无限循环。 通常您不希望 JavaScript 一遍又一遍地执行完全相同的操作。在几乎每个循环中一个或多个变量会随着循环的每次迭代而改变。由于变量会改变执行statement的操作可能每次循环时都不同。此外如果涉及到expression中的变化变量那么表达式的值可能每次循环时都不同。这很重要否则一开始为真值的表达式永远不会改变循环永远不会结束以下是一个打印从 0 到 9 的数字的while循环示例 let count 0; while(count 10) {console.log(count);count; }正如你所看到的变量count从 0 开始并且在循环体运行每次后递增。一旦循环执行了 10 次表达式变为false即变量count不再小于 10while语句结束解释器可以继续执行程序中的下一条语句。许多循环都有像count这样的计数变量。变量名i、j和k通常用作循环计数器但如果使用更具描述性的名称可以使代码更易于理解。 5.4.2 do/while do/while循环类似于while循环不同之处在于循环表达式在循环底部测试而不是在顶部测试。这意味着循环体始终至少执行一次。语法是 do*statement* while (*expression*);do/while循环比其while表亲更少使用——实际上很少有确定要执行至少一次循环的情况。以下是do/while循环的示例 function printArray(a) {let len a.length, i 0;if (len 0) {console.log(Empty Array);} else {do {console.log(a[i]);} while(i len);} }do/while循环和普通的while循环之间有一些语法上的差异。首先do循环需要do关键字标记循环开始和while关键字标记结束并引入循环条件。此外do循环必须始终以分号结尾。如果循环体用大括号括起来则while循环不需要分号。 5.4.3 for for语句提供了一个循环结构通常比while语句更方便。for语句简化了遵循常见模式的循环。大多数循环都有某种计数变量。该变量在循环开始之前初始化并在每次循环迭代之前进行测试。最后在循环体结束之前计数变量会递增或以其他方式更新然后再次测试该变量。在这种循环中初始化、测试和更新是循环变量的三个关键操作。for语句将这三个操作编码为表达式并将这些表达式作为循环语法的显式部分 for(*initialize* ; *test* ; *increment*)*statement*initialize、test和increment是三个用分号分隔的表达式负责初始化、测试和递增循环变量。将它们都放在循环的第一行中可以轻松理解for循环正在做什么并防止遗漏初始化或递增循环变量等错误。 解释for循环如何工作的最简单方法是展示等效的while循环² *initialize*; while(*test*) {*statement**increment*; }换句话说initialize表达式在循环开始之前只计算一次。为了有用此表达式必须具有副作用通常是赋值。JavaScript 还允许initialize是一个变量声明语句这样您可以同时声明和初始化循环计数器。test表达式在每次迭代之前进行评估并控制循环体是否执行。如果test评估为真值则执行循环体的statement。最后评估increment表达式。同样这必须是具有副作用的表达式才能有效。通常它是一个赋值表达式或者使用或--运算符。 我们可以使用以下for循环打印从 0 到 9 的数字。将其与前一节中显示的等效while循环进行对比 for(let count 0; count 10; count) {console.log(count); }当然循环可能比这个简单示例复杂得多有时多个变量在循环的每次迭代中都会发生变化。这种情况是 JavaScript 中唯一常用逗号运算符的地方它提供了一种将多个初始化和递增表达式组合成适合在for循环中使用的单个表达式的方法 let i, j, sum 0; for(i 0, j 10 ; i 10 ; i, j--) {sum i * j; }到目前为止我们所有的循环示例中循环变量都是数字。这是很常见的但并非必须的。以下代码使用for循环遍历一个链表数据结构并返回列表中的最后一个对象即第一个没有next属性的对象 function tail(o) { // Return the tail of linked list ofor(; o.next; o o.next) /* empty */ ; // Traverse while o.next is truthyreturn o; }注意这段代码没有初始化表达式。for循环中的三个表达式中的任何一个都可以省略但两个分号是必需的。如果省略测试表达式则循环将永远重复for(;;)就像while(true)一样是写无限循环的另一种方式。 5.4.4 for/of ES6 定义了一种新的循环语句for/of。这种新类型的循环使用for关键字但是与常规的for循环完全不同。它也与我们将在§5.4.5 中描述的旧的for/in循环完全不同。 for/of循环适用于可迭代对象。我们将在第十二章中详细解释对象何时被视为可迭代但在本章中只需知道数组、字符串、集合和映射是可迭代的它们代表一个序列或一组元素您可以使用for/of循环进行循环或迭代。 例如这里是我们如何使用for/of循环遍历一个数字数组的元素并计算它们的总和 let data [1, 2, 3, 4, 5, 6, 7, 8, 9], sum 0; for(let element of data) {sum element; } sum // 45表面上语法看起来像是常规的for循环for关键字后面跟着包含有关循环应该执行的详细信息的括号。在这种情况下括号包含一个变量声明或者对于已经声明的变量只是变量的名称后面跟着of关键字和一个求值为可迭代对象的表达式就像这种情况下的data数组一样。与所有循环一样for/of循环的主体跟在括号后面通常在花括号内。 在刚才显示的代码中循环体会针对data数组的每个元素运行一次。在执行循环体之前数组的下一个元素会被分配给元素变量。数组元素按顺序从第一个到最后一个进行迭代。 数组是“实时”迭代的——在迭代过程中进行的更改可能会影响迭代的结果。如果我们在循环体内添加data.push(sum);这行代码那么我们将创建一个无限循环因为迭代永远无法到达数组的最后一个元素。 使用对象进行for/of循环 对象默认情况下不可迭代。尝试在常规对象上使用for/of会在运行时引发 TypeError let o { x: 1, y: 2, z: 3 }; for(let element of o) { // Throws TypeError because o is not iterableconsole.log(element); }如果要遍历对象的属性可以使用for/in循环在§5.4.5 中介绍或者使用for/of与Object.keys()方法 let o { x: 1, y: 2, z: 3 }; let keys ; for(let k of Object.keys(o)) {keys k; } keys // xyz这是因为Object.keys()返回一个对象的属性名称数组数组可以使用for/of进行迭代。还要注意与上面的数组示例不同对象的键的这种迭代不是实时的——在循环体中对对象o进行的更改不会影响迭代。如果您不关心对象的键也可以像这样迭代它们对应的值 let sum 0; for(let v of Object.values(o)) {sum v; } sum // 6如果您对对象属性的键和值都感兴趣可以使用for/of与Object.entries()和解构赋值 let pairs ; for(let [k, v] of Object.entries(o)) {pairs k v; } pairs // x1y2z3Object.entries()返回一个数组其中每个内部数组表示对象的一个属性的键/值对。在这个代码示例中我们使用解构赋值来将这些内部数组解包成两个单独的变量。 使用字符串进行for/of循环 在 ES6 中字符串是逐个字符可迭代的 let frequency {}; for(let letter of mississippi) {if (frequency[letter]) {frequency[letter];} else {frequency[letter] 1;} } frequency // {m: 1, i: 4, s: 4, p: 2}请注意字符串是按 Unicode 代码点迭代的而不是按 UTF-16 字符。字符串“I ❤ ”的.length为 5因为两个表情符号字符分别需要两个 UTF-16 字符来表示。但如果您使用for/of迭代该字符串循环体将运行三次分别为每个代码点“I”、“❤”和“”。 使用 Set 和 Map 进行 for/of 内置的 ES6 Set 和 Map 类是可迭代的。当您使用 for/of 迭代 Set 时循环体会为集合的每个元素运行一次。您可以使用以下代码打印文本字符串中的唯一单词 let text Na na na na na na na na Batman!; let wordSet new Set(text.split( )); let unique []; for(let word of wordSet) {unique.push(word); } unique // [Na, na, Batman!]Map 是一个有趣的情况因为 Map 对象的迭代器不会迭代 Map 键或 Map 值而是键/值对。在每次迭代中迭代器返回一个数组其第一个元素是键第二个元素是相应的值。给定一个 Map m您可以像这样迭代并解构其键/值对 let m new Map([[1, one]]); for(let [key, value] of m) {key // 1value // one }使用 for/await 进行异步迭代 ES2018 引入了一种新类型的迭代器称为异步迭代器以及与之配套的 for/of 循环的变体称为 for/await 循环可与异步迭代器一起使用。 您需要阅读第十二章和第十三章才能理解 for/await 循环但以下是代码示例 // Read chunks from an asynchronously iterable stream and print them out async function printStream(stream) {for await (let chunk of stream) {console.log(chunk);} }5.4.5 for/in for/in 循环看起来很像 for/of 循环只是将 of 关键字更改为 in。在 of 之后for/of 循环需要一个可迭代对象而 for/in 循环在 in 之后可以使用任何对象。for/of 循环是 ES6 中的新功能但 for/in 从 JavaScript 最初就存在这就是为什么它具有更自然的语法。 for/in 语句循环遍历指定对象的属性名称。语法如下 for (*variable* in *object*)*statement*variable 通常命名一个变量但它也可以是一个变量声明或任何适合作为赋值表达式左侧的内容。object 是一个求值为对象的表达式。通常情况下statement 是作为循环主体的语句或语句块。 您可能会像这样使用 for/in 循环 for(let p in o) { // Assign property names of o to variable pconsole.log(o[p]); // Print the value of each property }要执行 for/in 语句JavaScript 解释器首先评估 object 表达式。如果它评估为 null 或 undefined解释器将跳过循环并继续执行下一条语句。解释器现在会为对象的每个可枚举属性执行循环体。然而在每次迭代之前解释器会评估 variable 表达式并将属性的名称一个字符串值赋给它。 请注意在 for/in 循环中的 variable 可以是任意表达式只要它评估为适合赋值左侧的内容。这个表达式在每次循环时都会被评估这意味着它可能每次评估的结果都不同。例如您可以使用以下代码将所有对象属性的名称复制到数组中 let o { x: 1, y: 2, z: 3 }; let a [], i 0; for(a[i] in o) /* empty */;JavaScript 数组只是一种特殊类型的对象数组索引是可以用 for/in 循环枚举的对象属性。例如以下代码后面加上这行代码将枚举数组索引 0、1 和 2 for(let i in a) console.log(i);我发现在我的代码中常见的错误来源是意外使用数组时使用 for/in 而不是 for/of。在处理数组时您几乎总是希望使用 for/of 而不是 for/in。 for/in 循环实际上并不枚举对象的所有属性。它不会枚举名称为符号的属性。对于名称为字符串的属性它只循环遍历可枚举属性参见§14.1。核心 JavaScript 定义的各种内置方法都不可枚举。例如所有对象都有一个 toString() 方法但 for/in 循环不会枚举这个 toString 属性。除了内置方法许多内置对象的其他属性也是不可枚举的。默认情况下您代码定义的所有属性和方法都是可枚举的您可以使用§14.1 中解释的技术使它们变为不可枚举。 可枚举的继承属性参见§6.3.2也会被for/in循环枚举。这意味着如果您使用for/in循环并且还使用定义了所有对象都继承的属性的代码那么您的循环可能不会按您的预期方式运行。因此许多程序员更喜欢使用Object.keys()的for/of循环而不是for/in循环。 如果for/in循环的主体删除尚未枚举的属性则该属性将不会被枚举。如果循环的主体在对象上定义了新属性则这些属性可能会被枚举也可能不会被枚举。有关for/in枚举对象属性的顺序的更多信息请参见§6.6.1。 5.5 跳转 另一类 JavaScript 语句是跳转语句。顾名思义这些语句会导致 JavaScript 解释器跳转到源代码中的新位置。break语句使解释器跳转到循环或其他语句的末尾。continue使解释器跳过循环体的其余部分并跳回到循环的顶部开始新的迭代。JavaScript 允许对语句进行命名或标记break和continue可以标识目标循环或其他语句标签。 return语句使解释器从函数调用跳回到调用它的代码并提供调用的值。throw语句是一种临时从生成器函数返回的方式。throw语句引发异常并设计用于与try/catch/finally语句一起工作后者建立了一个异常处理代码块。这是一种复杂的跳转语句当抛出异常时解释器会跳转到最近的封闭异常处理程序该处理程序可能在同一函数中或在调用函数的调用堆栈中。 关于这些跳转语句的详细信息在接下来的章节中。 5.5.1 标记语句 任何语句都可以通过在其前面加上标识符和冒号来标记 *identifier*: *statement*通过给语句加上标签您为其赋予一个名称以便在程序的其他地方引用它。您可以为任何语句加上标签尽管只有为具有主体的语句加上标签才有用例如循环和条件语句。通过给循环命名您可以在循环体内使用break和continue语句来退出循环或直接跳转到循环的顶部开始下一次迭代。break和continue是唯一使用语句标签的 JavaScript 语句它们在以下子节中介绍。这里是一个带有标签的while循环和使用标签的continue语句的示例。 mainloop: while(token ! null) {// Code omitted...continue mainloop; // Jump to the next iteration of the named loop// More code omitted... }用于标记语句的标识符可以是任何合法的 JavaScript 标识符不能是保留字。标签的命名空间与变量和函数的命名空间不同因此您可以将相同的标识符用作语句标签和变量或函数名称。语句标签仅在其适用的语句内部定义当然也包括其子语句。语句不能具有包含它的语句相同的标签但是只要一个语句不嵌套在另一个语句内两个语句可以具有相同的标签。标记的语句本身也可以被标记。实际上这意味着任何语句可以具有多个标签。 5.5.2 break 单独使用的break语句会导致最内层的循环或switch语句立即退出。其语法很简单 break;因为它导致循环或switch退出所以这种形式的break语句只有在出现在这些语句内部时才合法。 您已经看到了switch语句中break语句的示例。在循环中当不再需要完成循环时通常会提前退出。当循环具有复杂的终止条件时通常更容易使用break语句实现其中一些条件而不是尝试在单个循环表达式中表达所有条件。以下代码搜索数组元素以找到特定值。当它在数组中找到所需的内容时循环以正常方式终止如果在数组中找到所需的内容则使用break语句终止 for(let i 0; i a.length; i) {if (a[i] target) break; }JavaScript 还允许在break关键字后面跟着一个语句标签只是标识符没有冒号 break *labelname*;当break与标签一起使用时它会跳转到具有指定标签的结束语句或终止该结束语句。如果没有具有指定标签的结束语句则以这种形式使用break语句是语法错误。使用这种形式的break语句时命名的语句不必是循环或switchbreak可以“跳出”任何包含语句。这个语句甚至可以是一个仅用于使用标签命名块的大括号组成的语句块。 在break关键字和labelname之间不允许换行。这是由于 JavaScript 自动插入省略的分号如果在break关键字和后面的标签之间放置换行符JavaScript 会认为您想使用简单的、无标签的语句形式并将换行符视为分号。参见§2.6。 当您想要跳出不是最近的循环或switch的语句时您需要带标签的break语句。以下代码演示了 let matrix getData(); // Get a 2D array of numbers from somewhere // Now sum all the numbers in the matrix. let sum 0, success false; // Start with a labeled statement that we can break out of if errors occur computeSum: if (matrix) {for(let x 0; x matrix.length; x) {let row matrix[x];if (!row) break computeSum;for(let y 0; y row.length; y) {let cell row[y];if (isNaN(cell)) break computeSum;sum cell;}}success true; } // The break statements jump here. If we arrive here with success false // then there was something wrong with the matrix we were given. // Otherwise, sum contains the sum of all cells of the matrix.最后请注意break语句无论是否带有标签都不能跨越函数边界转移控制。例如您不能给函数定义语句加上标签然后在函数内部使用该标签。 5.5.3 continue continue语句类似于break语句。但是continue不是退出循环而是在下一次迭代时重新开始循环。continue语句的语法与break语句一样简单 continue;continue语句也可以与标签一起使用 continue *labelname*;continue语句无论是带标签还是不带标签只能在循环体内使用。在其他任何地方使用它都会导致语法错误。 当执行continue语句时将终止当前循环的迭代并开始下一次迭代。对于不同类型的循环这意味着不同的事情 在while循环中循环开始时测试循环开头的指定表达式如果为true则从顶部执行循环体。 在do/while循环中执行跳转到循环底部然后再次测试循环条件然后重新开始循环。 在for循环中将评估增量表达式并再次测试测试表达式以确定是否应进行另一次迭代。 在for/of或for/in循环中循环将重新开始下一个迭代值或下一个属性名将被赋给指定的变量。 请注意while和for循环中continue语句的行为差异while循环直接返回到其条件但for循环首先评估其增量表达式然后返回到其条件。之前我们考虑了for循环的行为以等效的while循环来描述。然而由于continue语句对这两种循环的行为不同因此仅使用while循环无法完全模拟for循环。 以下示例显示了在发生错误时使用未标记的continue语句跳过当前迭代的其余部分的情况 for(let i 0; i data.length; i) {if (!data[i]) continue; // Cant proceed with undefined datatotal data[i]; }与break语句类似continue语句可以在嵌套循环中的标记形式中使用当要重新启动的循环不是直接包围的循环时。同样与break语句一样continue语句和其labelname之间不允许换行。 5.5.4 return 请记住函数调用是表达式所有表达式都有值。函数内部的return语句指定了该函数调用的值。下面是return语句的语法 return *expression*;return语句只能出现在函数体内部。在其他任何地方出现都会导致语法错误。当执行return语句时包含它的函数将expression的值返回给调用者。例如 function square(x) { return x*x; } // A function that has a return statement square(2) // 4没有return语句时函数调用会依次执行函数体中的每个语句直到到达函数末尾然后返回给调用者。在这种情况下调用表达式评估为undefined。return语句通常出现在函数中的最后一个语句但不一定非得是最后一个当执行return语句时函数返回给调用者即使函数体中还有其他语句。 return语句也可以在没有expression的情况下使用使函数返回undefined给调用者。例如 function displayObject(o) {// Return immediately if the argument is null or undefined.if (!o) return;// Rest of function goes here... }由于 JavaScript 的自动分号插入§2.6你不能在return关键字和其后的表达式之间插入换行符。 5.5.5 yield yield语句与return语句非常相似但仅在 ES6 生成器函数参见§12.3中使用用于生成值序列中的下一个值而不实际返回 // A generator function that yields a range of integers function* range(from, to) {for(let i from; i to; i) {yield i;} }要理解yield你必须理解迭代器和生成器这将在第十二章中介绍。然而为了完整起见这里包括了yield。严格来说yield是一个运算符而不是语句如§12.4.2 中所解释的。 5.5.6 throw 异常是指示发生了某种异常情况或错误的信号。抛出异常是指示发生了这样的错误或异常情况。捕获异常是处理它 - 采取必要或适当的措施来从异常中恢复。在 JavaScript 中每当发生运行时错误或程序明确使用throw语句抛出异常时都会抛出异常。异常可以通过try/catch/finally语句捕获下一节将对此进行描述。 throw语句的语法如下 throw *expression*;expression可能会评估为任何类型的值。你可以抛出一个代表错误代码的数字或者包含人类可读错误消息的字符串。当 JavaScript 解释器本身抛出错误时会使用 Error 类及其子类你也可以使用它们。一个 Error 对象有一个name属性指定错误类型一个message属性保存传递给构造函数的字符串。下面是一个示例函数当使用无效参数调用时会抛出一个 Error 对象 function factorial(x) {// If the input argument is invalid, throw an exception!if (x 0) throw new Error(x must not be negative);// Otherwise, compute a value and return normallylet f;for(f 1; x 1; f * x, x--) /* empty */ ;return f; } factorial(4) // 24当抛出异常时JavaScript 解释器立即停止正常程序执行并跳转到最近的异常处理程序。异常处理程序使用try/catch/finally语句的catch子句编写下一节将对其进行描述。如果抛出异常的代码块没有关联的catch子句解释器将检查下一个最高级别的封闭代码块看看它是否有与之关联的异常处理程序。这将一直持续下去直到找到处理程序。如果在一个不包含try/catch/finally语句来处理异常的函数中抛出异常异常将传播到调用该函数的代码。通过这种方式异常通过 JavaScript 方法的词法结构向上传播并沿着调用堆栈向上传播。如果从未找到异常处理程序异常将被视为错误并报告给用户。 5.5.7 try/catch/finally try/catch/finally语句是 JavaScript 的异常处理机制。该语句的try子句简单地定义了要处理异常的代码块。try块后面是一个catch子句当try块内部发生异常时将调用一组语句。catch子句后面是一个finally块其中包含清理代码无论try块中发生了什么都保证会执行。catch和finally块都是可选的但try块必须至少伴随其中一个。try、catch和finally块都以大括号开始和结束。这些大括号是语法的必要部分即使一个子句只包含一个语句也不能省略。 以下代码示例说明了try/catch/finally语句的语法和目的 try {// Normally, this code runs from the top of the block to the bottom// without problems. But it can sometimes throw an exception,// either directly, with a throw statement, or indirectly, by calling// a method that throws an exception. } catch(e) {// The statements in this block are executed if, and only if, the try// block throws an exception. These statements can use the local variable// e to refer to the Error object or other value that was thrown.// This block may handle the exception somehow, may ignore the// exception by doing nothing, or may rethrow the exception with throw. } finally {// This block contains statements that are always executed, regardless of// what happens in the try block. They are executed whether the try// block terminates:// 1) normally, after reaching the bottom of the block// 2) because of a break, continue, or return statement// 3) with an exception that is handled by a catch clause above// 4) with an uncaught exception that is still propagating }请注意catch关键字通常后面跟着一个括号中的标识符。这个标识符类似于函数参数。当捕获到异常时与异常相关联的值例如一个 Error 对象将被分配给这个参数。与catch子句关联的标识符具有块作用域——它只在catch块内定义。 这里是try/catch语句的一个实际例子。它使用了前一节中定义的factorial()方法以及客户端 JavaScript 方法prompt()和alert()来进行输入和输出 try {// Ask the user to enter a numberlet n Number(prompt(Please enter a positive integer, ));// Compute the factorial of the number, assuming the input is validlet f factorial(n);// Display the resultalert(n ! f); } catch(ex) { // If the users input was not valid, we end up herealert(ex); // Tell the user what the error is }这个例子是一个没有finally子句的try/catch语句。虽然finally不像catch那样经常使用但它也是有用的。然而它的行为需要额外的解释。如果try块的任何部分被执行finally子句将被执行。它通常用于在try子句中的代码执行完毕后进行清理。 在正常情况下JavaScript 解释器执行完try块后然后继续执行finally块执行任何必要的清理工作。如果解释器因为return、continue或break语句而离开try块那么在解释器跳转到新目的地之前将执行finally块。 如果在try块中发生异常并且有一个关联的catch块来处理异常解释器首先执行catch块然后执行finally块。如果没有本地catch块来处理异常解释器首先执行finally块然后跳转到最近的包含catch子句。 如果finally块本身导致使用return、continue、break或throw语句跳转或通过调用抛出异常的方法解释器会放弃任何待处理的跳转并执行新的跳转。例如如果finally子句抛出异常那个异常会替换正在被抛出的任何异常。如果finally子句发出return语句方法会正常返回即使已经抛出异常但尚未处理。 try和finally可以在没有catch子句的情况下一起使用。在这种情况下finally块只是保证会被执行的清理代码无论try块中发生了什么。请记住我们无法完全用while循环模拟for循环因为continue语句对这两种循环的行为是不同的。如果我们添加一个try/finally语句我们可以编写一个像for循环一样工作并正确处理continue语句的while循环 // Simulate for(*initialize* ; *test* ;*increment* ) body; *initialize* ; while( *test* ) {try { *body* ; }finally { *increment* ; } }但是请注意包含break语句的body在while循环中的行为略有不同导致在退出之前额外增加一次递增与在for循环中的行为不同因此即使有finally子句也无法完全用while模拟for循环。 5.6 其他语句 本节描述了剩余的三个 JavaScript 语句——with、debugger和use strict。 5.6.1 with with语句会将指定对象的属性作为作用域内的变量运行一段代码块。它的语法如下 with (*object*)*statement*这个语句创建一个临时作用域将object的属性作为变量然后在该作用域内执行statement。 with语句在严格模式下是被禁止的参见§5.6.3在非严格模式下应被视为已弃用尽量避免使用。使用with的 JavaScript 代码很难优化并且可能比不使用with语句编写的等效代码运行得慢得多。 with语句的常见用法是使得在深度嵌套的对象层次结构中更容易工作。例如在客户端 JavaScript 中你可能需要输入这样的表达式来访问 HTML 表单的元素 document.forms[0].address.value如果你需要多次编写这样的表达式你可以使用with语句将表单对象的属性视为变量处理 with(document.forms[0]) {// Access form elements directly here. For example:name.value ;address.value ;email.value ; }这样可以减少你需要输入的内容你不再需要在每个表单属性名称前加上document.forms[0]。当然避免使用with语句并像这样编写前面的代码同样简单 let f document.forms[0]; f.name.value ; f.address.value ; f.email.value ;请注意如果在with语句的主体中使用const、let或var声明变量或常量它会创建一个普通变量而不会在指定对象中定义一个新属性。 5.6.2 debugger debugger语句通常不会执行任何操作。然而如果一个调试器程序可用且正在运行那么实现可能但不是必须执行某种调试操作。实际上这个语句就像一个断点JavaScript 代码的执行会停止你可以使用调试器打印变量的值检查调用堆栈等。例如假设你在函数f()中遇到异常因为它被使用未定义的参数调用而你无法弄清楚这个调用是从哪里来的。为了帮助你调试这个问题你可以修改f()使其如下所示开始 function f(o) {if (o undefined) debugger; // Temporary line for debugging purposes... // The rest of the function goes here. }现在当没有参数调用f()时执行会停止你可以使用调试器检查调用堆栈并找出这个错误调用是从哪里来的。 请注意仅仅拥有一个调试器是不够的debugger语句不会为你启动调试器。然而如果你正在使用一个网页浏览器并且打开了开发者工具控制台这个语句会导致断点。 5.6.3 “use strict” use strict是 ES5 中引入的指令。 指令不是语句但足够接近以至于在此处记录了use strict。 use strict指令和常规语句之间有两个重要区别 它不包括任何语言关键字该指令只是一个表达式语句由一个特殊的字符串文字单引号或双引号组成。 它只能出现在脚本的开头或函数体的开头在任何真实语句出现之前。 use strict指令的目的是指示随后的代码在脚本或函数中是严格代码。 如果脚本有use strict指令则脚本的顶级非函数代码是严格代码。 如果函数体在严格代码中定义或具有use strict指令则函数体是严格代码。 如果从严格代码调用eval()方法则传递给eval()的代码是严格代码或者如果代码字符串包含use strict指令。 除了明确声明为严格的代码外class体第九章中的任何代码或 ES6 模块§10.3中的任何代码都自动成为严格代码。 这意味着如果所有 JavaScript 代码都编写为模块则所有代码都自动成为严格代码您将永远不需要使用显式的use strict指令。 严格模式下执行严格模式。 严格模式是语言的受限子集修复了重要的语言缺陷并提供了更强的错误检查和增强的安全性。 由于严格模式不是默认设置仍然使用语言的不足遗留功能的旧 JavaScript 代码将继续正确运行。 严格模式和非严格模式之间的区别如下前三个特别重要 在严格模式下不允许使用with语句。 在严格模式下所有变量必须声明如果将值分配给未声明的变量、函数、函数参数、catch子句参数或全局对象的属性则会抛出 ReferenceError。在非严格模式下这将通过向全局对象添加新属性来隐式声明全局变量。 在严格模式下作为函数调用的函数而不是作为方法的this值为undefined。在非严格模式下作为函数调用的函数始终将全局对象作为其this值传递。此外在严格模式下当使用call()或apply()§8.7.4调用函数时this值正好是传递给call()或apply()的第一个参数的值。在非严格模式下null和undefined值将替换为全局对象非对象值将转换为对象。 在严格模式下对不可写属性的赋值和尝试在不可扩展对象上创建新属性会抛出 TypeError。在非严格模式下这些尝试会静默失败。 在严格模式下传递给eval()的代码不能在调用者的范围内声明变量或定义函数就像在非严格模式下那样。 相反变量和函数定义存在于为eval()创建的新作用域中。 当eval()返回时此作用域将被丢弃。 在严格模式下函数中的 Arguments 对象§8.3.3保存传递给函数的值的静态副本。 在非严格模式下Arguments 对象具有“神奇”的行为其中数组的元素和命名函数参数都指向相同的值。 在严格模式下如果delete运算符后跟未经限定的标识符如变量、函数或函数参数则会抛出 SyntaxError。在非严格模式下这样的delete表达式不起作用并计算为false。 在严格模式下尝试删除不可配置属性会抛出 TypeError。 在非严格模式下尝试失败delete表达式的值为false。 在严格模式下对象字面量定义具有相同名称的两个或更多属性是语法错误。在非严格模式下不会发生错误。 在严格模式下函数声明具有两个或更多具有相同名称的参数是语法错误。在非严格模式下不会发生错误。 在严格模式下不允许使用八进制整数字面量以 0 开头且后面不跟 x。在非严格模式下一些实现允许八进制字面量。 在严格模式下标识符eval和arguments被视为关键字不允许更改它们的值。不能为这些标识符分配值将它们声明为变量将它们用作函数名称将它们用作函数参数名称或将它们用作catch块的标识符。 在严格模式下限制了检查调用堆栈的能力。在严格模式函数内arguments.caller和arguments.callee都会抛出 TypeError。严格模式函数还具有caller和arguments属性当读取时会抛出 TypeError。一些实现在非严格函数上定义这些非标准属性。 5.7 声明 关键字const、let、var、function、class、import和export在技术上不是语句但它们看起来很像语句因此本书非正式地将它们称为语句因此它们在本章中值得一提。 这些关键字更准确地描述为声明而不是语句。我们在本章开头说过语句“让某事发生”。声明用于定义新值并为其赋予我们可以用来引用这些值的名称。它们本身并没有做太多事情但通过为值提供名称它们在重要意义上定义了程序中其他语句的含义。 当程序运行时程序的表达式正在被评估程序的语句正在被执行。程序中的声明不会像语句一样“运行”相反它们定义了程序本身的结构。可以粗略地将声明视为在代码开始运行之前处理的程序部分。 JavaScript 声明用于定义常量、变量、函数和类并用于在模块之间导入和导出值。下一小节将给出所有这些声明的示例。它们在本书的其他地方都有更详细的介绍。 5.7.1 const、let 和 var const、let和var声明在§3.10 中有介绍。在 ES6 及更高版本中const声明常量let声明变量。在 ES6 之前var关键字是声明变量的唯一方式没有办法声明常量。使用var声明的变量的作用域是包含函数而不是包含块。这可能导致错误并且在现代 JavaScript 中没有理由使用var而不是let。 const TAU 2*Math.PI; let radius 3; var circumference TAU * radius;5.7.2 function function声明用于定义函数在第八章中有详细介绍。我们还在§4.3 中看到function那里它被用作函数表达式的一部分而不是函数声明。函数声明如下所示 function area(radius) {return Math.PI * radius * radius; }函数声明创建一个函数对象并将其分配给指定的名称—在这个例子中是area。 在程序的其他地方我们可以通过使用这个名称引用函数—并运行其中的代码。 JavaScript 代码块中的函数声明在代码运行之前被处理并且函数名称在整个代码块中绑定到函数对象。 我们说函数声明被“提升”因为它就好像它们都被移动到它们所在的作用域的顶部一样。 结果是调用函数的代码可以存在于程序中在声明函数的代码之前。 §12.3 描述了一种特殊类型的函数称为生成器。 生成器声明使用function关键字但后面跟着一个星号。 §13.3 描述了异步函数也是使用function关键字声明的但前面加上async关键字。 5.7.3 类 在 ES6 及更高版本中class声明创建一个新的类并为其赋予一个我们可以用来引用它的名称。 类在第九章中有详细描述。 一个简单的类声明可能如下所示 class Circle {constructor(radius) { this.r radius; }area() { return Math.PI * this.r * this.r; }circumference() { return 2 * Math.PI * this.r; } }与函数不同类声明不会被提升你不能在类声明之前的代码中使用以这种方式声明的类。 5.7.4 导入和导出 import和export声明一起使用使得在 JavaScript 代码的一个模块中定义的值可以在另一个模块中使用。 模块是具有自己全局命名空间的 JavaScript 代码文件完全独立于所有其他模块。 一个值如函数或类在一个模块中定义后只有通过export导出并在另一个模块中使用import导入才能在另一个模块中使用。 模块是第十章的主题import和export在§10.3 中有详细介绍。 import指令用于从另一个 JavaScript 代码文件中导入一个或多个值并在当前模块中为它们命名。 import指令有几种不同的形式。 以下是一些示例 import Circle from ./geometry/circle.js; import { PI, TAU } from ./geometry/constants.js; import { magnitude as hypotenuse } from ./vectors/utils.js;JavaScript 模块中的值是私有的除非它们已经被明确导出否则不能被导入到其他模块中。 export指令可以实现这一点它声明当前模块中定义的一个或多个值被导出因此可以被其他模块导入。 export指令比import指令有更多的变体。 这是其中之一 // geometry/constants.js const PI Math.PI; const TAU 2 * PI; export { PI, TAU };export关键字有时用作其他声明的修饰符从而形成一种复合声明同时定义一个常量、变量、函数或类并将其导出。 当一个模块只导出一个值时通常使用特殊形式export default export const TAU 2 * Math.PI; export function magnitude(x,y) { return Math.sqrt(x*x y*y); } export default class Circle { /* class definition omitted here */ }5.8 JavaScript 语句总结 本章介绍了 JavaScript 语言的每个语句总结在表 5-1 中。 表 5-1. JavaScript 语句语法 语句目的break退出最内层循环或switch或从命名封闭语句中退出case在switch语句中标记一个语句class声明一个类const声明和初始化一个或多个常量continue开始最内层循环或命名循环的下一次迭代debugger调试器断点default标记switch语句中的默认语句do/whilewhile循环的替代方案export声明可以被其他模块导入的值for一个易于使用的循环for/await异步迭代异步迭代器的值for/in枚举对象的属性名称for/of枚举可迭代对象如数组的值function声明一个函数if/else根据条件执行一个语句或另一个import声明在其他模块中定义的值的名称label为break和continue给语句命名let声明并初始化一个或多个块作用域变量新语法return从函数中返回一个值switch多路分支到case或default:标签throw抛出异常try/catch/finally处理异常和代码清理“use strict”将严格模式限制应用于脚本或函数var声明并初始化一个或多个变量旧语法while基本的循环结构with扩展作用域链已弃用且在严格模式下禁止使用yield提供一个要迭代的值仅在生成器函数中使用 ¹ case表达式在运行时评估的事实使得 JavaScript 的switch语句与 C、C和 Java 的switch语句有很大不同且效率较低。在那些语言中case表达式必须是相同类型的编译时常量并且switch语句通常可以编译为高效的跳转表。 ² 当我们考虑在§5.5.3 中的continue语句时我们会发现这个while循环并不是for循环的精确等价。
http://www.hkea.cn/news/14475735/

相关文章:

  • 凡科可以做游戏网站吗宝塔面板怎么做自己的网站
  • 做网站的需求清单计算机培训机构哪个最好
  • 潍坊网站建设 中公福州金山网站建设
  • 网站开发公司名单中文设计网站
  • 网站开发用什么系统比较好代做毕业设计网站
  • 网站建设招标范文做网站的组要具备哪些素质
  • 深圳电器网站建设百度百科官网首页
  • 做美图 网站有哪些上海免费注册公司官网
  • 网站建设的背景及意义中国电力建设协会网站
  • 做悬浮导航的网站dedecms 做影网站
  • 自己做网站花钱吗沈阳公司网站设计公司
  • 推荐网站建设服务英文网站开发公司哪家好
  • 网页设计与网站建设考试题目网页制作基础教程直播
  • 网站开发与软件开发免费网址生成app
  • 天津模板建站定制网站如何自己制作游戏软件
  • 小程序可以做网站吗做营销型网站
  • 鹰潭公司做网站专业网页设计软件
  • 做彩票平台网站吗新建网站怎么优化
  • 专做蓝领招聘网站有哪些服务比较好的网页传奇
  • 用wordpress做的站点淄博网上商城制作
  • 手机微信一体网站建设东莞网站建设排行
  • 怎么建设免费网站网站301做下
  • flash网站建设个人简介辽宁建设厅新网站
  • 通过网站做外贸竞价推广账户竞价托管收费
  • 网站做实名验证网易企业邮箱登录入口手机
  • 中山网站建设sipocmsseo教程从零开始
  • 国外网页设计分享网站wordpress 谷歌搜索
  • 大良营销网站建设市场wordpress 房产
  • 网络公司企业网站模板网站淘客宝怎么做
  • 如何不花钱开发网站做家纺网站哪家好