当前位置:   article > 正文

JavaScript 权威指南第七版(GPT 重译)(二)

JavaScript 权威指南第七版(GPT 重译)(二)

第四章:表达式和运算符

本章记录了 JavaScript 表达式以及构建许多这些表达式的运算符。表达式 是 JavaScript 的短语,可以 评估 以产生一个值。在程序中直接嵌入的常量是一种非常简单的表达式。变量名也是一个简单表达式,它评估为分配给该变量的任何值。复杂表达式是由简单表达式构建的。例如,一个数组访问表达式由一个评估为数组的表达式、一个开放方括号、一个评估为整数的表达式和一个闭合方括号组成。这个新的、更复杂的表达式评估为存储在指定数组索引处的值。类似地,函数调用表达式由一个评估为函数对象的表达式和零个或多个额外表达式组成,这些额外表达式用作函数的参数。

从简单表达式中构建复杂表达式的最常见方法是使用 运算符。运算符以某种方式结合其操作数的值(通常是两个操作数中的一个)并评估为一个新值。乘法运算符 * 是一个简单的例子。表达式 x * y 评估为表达式 xy 的值的乘积。为简单起见,我们有时说一个运算符 返回 一个值,而不是“评估为”一个值。

本章记录了 JavaScript 的所有运算符,并解释了不使用运算符的表达式(如数组索引和函数调用)。如果您已经了解使用 C 风格语法的其他编程语言,您会发现大多数 JavaScript 表达式和运算符的语法已经很熟悉了。

4.1 主要表达式

最简单的表达式,称为 主要表达式,是那些独立存在的表达式——它们不包括任何更简单的表达式。JavaScript 中的主要表达式是常量或 字面值、某些语言关键字和变量引用。

字面量是直接嵌入到程序中的常量值。它们看起来像这样:

1.23         // A number literal
"hello"      // A string literal
/pattern/    // A regular expression literal
  • 1
  • 2
  • 3

JavaScript 中关于数字字面量的语法已在 §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
  • 1
  • 2
  • 3
  • 4

我们在 §3.4 和 §3.5 中学习了 truefalsenull。与其他关键字不同,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
  • 1
  • 2
  • 3

当程序中出现任何标识符时,JavaScript 假定它是一个变量、常量或全局对象的属性,并查找其值。如果不存在具有该名称的变量,则尝试评估不存在的变量会抛出 ReferenceError。

4.2 对象和数组初始化器

对象数组初始化器 是值为新创建的对象或数组的表达式。这些初始化器表达式有时被称为 对象字面量数组字面量。然而,与真正的字面量不同,它们不是主要表达式,因为它们包括一些指定属性和元素值的子表达式。数组初始化器具有稍微简单的语法,我们将从这些开始。

数组初始化器是方括号内包含的逗号分隔的表达式列表。数组初始化器的值是一个新创建的数组。这个新数组的元素被初始化为逗号分隔表达式的值:

[]         // An empty array: no expressions inside brackets means no elements
[1+2,3+4]  // A 2-element array.  First element is 3, second is 7
  • 1
  • 2

数组初始化器中的元素表达式本身可以是数组初始化器,这意味着这些表达式可以创建嵌套数组:

let matrix = [[1,2,3], [4,5,6], [7,8,9]];
  • 1

数组初始化器中的元素表达式在每次评估数组初始化器时都会被评估。这意味着数组初始化器表达式的值在每次评估时可能会有所不同。

可以通过简单地在逗号之间省略值来在数组文字中包含未定义的元素。例如,以下数组包含五个元素,包括三个未定义的元素:

let sparseArray = [1,,,,5];
  • 1

在数组初始化器中,最后一个表达式后允许有一个逗号,并且不会创建未定义的元素。然而,对于最后一个表达式之后的索引的任何数组访问表达式都将必然评估为未定义。

对象初始化器表达式类似于数组初始化器表达式,但方括号被花括号替换,每个子表达式前缀都带有属性名和冒号:

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
  • 1
  • 2
  • 3

在 ES6 中,对象文字具有更丰富的语法(详细信息请参见§6.10)。对象文字可以嵌套。例如:

let rectangle = {
    upperLeft: { x: 2, y: 2 },
    lowerRight: { x: 4, y: 5 }
};
  • 1
  • 2
  • 3
  • 4

我们将在第六章和第七章再次看到对象和数组初始化器。

4.3 函数定义表达式

函数定义表达式 定义了一个 JavaScript 函数,这种表达式的值是新定义的函数。在某种意义上,函数定义表达式是“函数文字”的一种方式,就像对象初始化器是“对象文字”一样。函数定义表达式通常由关键字function后跟一个逗号分隔的零个或多个标识符(参数名称)的列表(在括号中)和一个 JavaScript 代码块(函数体)在花括号中组成。例如:

// This function returns the square of the value passed to it.
let square = function(x) { return x * x; };
  • 1
  • 2

函数定义表达式也可以包括函数的名称。函数也可以使用函数语句而不是函数表达式来定义。在 ES6 及更高版本中,函数表达式可以使用紧凑的新“箭头函数”语法。有关函数定义的完整详细信息请参见第八章。

4.4 属性访问表达式

属性访问表达式 评估为对象属性或数组元素的值。JavaScript 为属性访问定义了两种语法:

*`expression`* . *identifier*
*expression* [ *expression* ]
  • 1
  • 2

属性访问的第一种风格是一个表达式后跟一个句点和一个标识符。表达式指定对象,标识符指定所需属性的名称。属性访问的第二种风格在第一个表达式(对象或数组)后跟另一个方括号中的表达式。这第二个表达式指定所需属性的名称或所需数组元素的索引。以下是一些具体示例:

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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

使用任一类型的属性访问表达式时,首先评估.或``之前的表达式。如果值为nullundefined,则该表达式会抛出 TypeError,因为这是两个 JavaScript 值,不能具有属性。如果对象表达式后跟一个句点和一个标识符,则查找该标识符命名的属性的值,并成为表达式的整体值。如果对象表达式后跟另一个方括号中的表达式,则评估并转换为字符串。然后,表达式的整体值是由该字符串命名的属性的值。在任一情况下,如果命名属性不存在,则属性访问表达式的值为undefined

.identifier语法是两种属性访问选项中更简单的一种,但请注意,只有当要访问的属性具有合法标识符名称,并且在编写程序时知道名称时才能使用。如果属性名称包含空格或标点符号,或者是数字(对于数组),则必须使用方括号表示法。当属性名称不是静态的,而是计算结果时,也使用方括号(参见[§6.3.1 中的示例)。

对象及其属性在第六章中有详细介绍,数组及其元素在第七章中有介绍。

4.4.1 条件属性访问

ES2020 添加了两种新的属性访问表达式:

*`expression`* ?. *identifier*
*expression* ?.[ *expression* ]
  • 1
  • 2

在 JavaScript 中,值nullundefined是唯一没有属性的两个值。在使用.[]的常规属性访问表达式中,如果左侧的表达式评估为nullundefined,则会收到 TypeError。您可以使用?.?.[]语法来防止此类错误。

考虑表达式a?.b。如果anullundefined,那么该表达式将评估为undefined,而不会尝试访问属性b。如果a是其他值,则a?.b将评估为a.b的评估结果(如果a没有名为b的属性,则该值将再次为undefined)。

这种形式的属性访问表达式有时被称为“可选链”,因为它也适用于像这样的更长的“链式”属性访问表达式:

let a = { b: null };
a.b?.c.d   // => undefined
  • 1
  • 2

a是一个对象,因此a.b是一个有效的属性访问表达式。但是a.b的值是null,所以a.b.c会抛出 TypeError。通过使用?.而不是.,我们避免了 TypeError,a.b?.c评估为undefined。这意味着(a.b?.c).d将抛出 TypeError,因为该表达式尝试访问值undefined的属性。但是——这是“可选链”非常重要的一部分——a.b?.c.d(不带括号)简单地评估为undefined,不会抛出错误。这是因为使用?.的属性访问是“短路”的:如果?.左侧的子表达式评估为nullundefined,则整个表达式立即评估为undefined,而不会进一步尝试访问属性。

当然,如果a.b是一个对象,并且该对象没有名为c的属性,则a.b?.c.d将再次抛出 TypeError,我们将需要使用另一种条件属性访问:

let a = { b: {} };
a.b?.c?.d  // => undefined
  • 1
  • 2

使用?.[]而不是[]也可以进行条件属性访问。在表达式a?.[b][c]中,如果a的值为nullundefined,则整个表达式立即评估为undefined,并且子表达式bc甚至不会被评估。如果其中任何一个表达式具有副作用,则如果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: can't index undefined.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

使用?.?.[]进行条件属性访问是 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.
  • 1
  • 2
  • 3

当调用表达式被评估时,首先评估函数表达式,然后评估参数表达式以生成参数值列表。如果函数表达式的值不是函数,则会抛出 TypeError。接下来,按顺序将参数值分配给函数定义时指定的参数名,然后执行函数体。如果函数使用return语句返回一个值,则该值成为调用表达式的值。否则,调用表达式的值为undefined。有关函数调用的完整详细信息,包括当参数表达式的数量与函数定义中的参数数量不匹配时会发生什么的解释,请参阅第八章。

每个调用表达式都包括一对括号和开括号前的表达式。如果该表达式是一个属性访问表达式,则调用被称为方法调用。在方法调用中,作为属性访问主题的对象或数组在执行函数体时成为this关键字的值。这使得面向对象编程范式成为可能,其中函数(当以这种方式使用时我们称之为“方法”)在其所属对象上操作。详细信息请参阅第九章。

4.5.1 条件调用

在 ES2020 中,你也可以使用?.()而不是()来调用函数。通常当你调用一个函数时,如果括号左侧的表达式为nullundefined或任何其他非函数值,将抛出 TypeError。使用新的?.()调用语法,如果?.左侧的表达式评估为nullundefined,那么整个调用表达式将评估为undefined,不会抛出异常。

数组对象有一个sort()方法,可以选择性地传递一个函数参数,该函数定义了数组元素的期望排序顺序。在 ES2020 之前,如果你想编写一个像sort()这样的方法,它接受一个可选的函数参数,你通常会使用一个if语句来检查函数参数在if体中调用之前是否已定义:

function square(x, log) { // The second argument is an optional function
    if (log) {            // If the optional function is passed
        log(x);           // Invoke it
    }
    return x * x;         // Return the square of the argument
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然而,使用 ES2020 的这种条件调用语法,你可以简单地使用?.()编写函数调用,只有在实际有值可调用时才会发生调用:

function square(x, log) { // The second argument is an optional function
    log?.(x);             // Call the function if there is one
    return x * x;         // Return the square of the argument
}
  • 1
  • 2
  • 3
  • 4

但请注意,?.()仅检查左侧是否为nullundefined。它不验证该值实际上是否为函数。因此,在这个例子中,如果你向square()函数传递两个数字,它仍会抛出异常。

类似于条件属性访问表达式(§4.4.1),带有?.()的函数调用是短路的:如果?.左侧的值为nullundefined,则括号内的参数表达式都不会被评估:

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

带有?.()的条件调用表达式对方法和函数同样有效。但是因为方法调用还涉及属性访问,所以值得花点时间确保你理解以下表达式之间的区别:

o.m()     // Regular property access, regular invocation
o?.m()    // Conditional property access, regular invocation
o.m?.()   // Regular property access, conditional invocation
  • 1
  • 2
  • 3

在第一个表达式中,o必须是一个具有属性m且该属性的值必须是一个函数的对象。在第二个表达式中,如果onullundefined,则表达式评估为undefined。但如果o有任何其他值,则它必须具有一个值为函数的属性m。在第三个表达式中,o不能为nullundefined。如果它没有属性m,或者该属性的值为null,则整个表达式评估为undefined

使用?.()进行条件调用是 JavaScript 的最新功能之一。截至 2020 年初,这种新语法在大多数主要浏览器的当前或测试版本中得到支持。

4.6 对象创建表达式

对象创建表达式创建一个新对象,并调用一个函数(称为构造函数)来初始化该对象的属性。对象创建表达式类似于调用表达式,只是它们以关键字new为前缀:

new Object()
new Point(2,3)
  • 1
  • 2

如果在对象创建表达式中未传递参数给构造函数,则可以省略空括号对:

new Object
new Date
  • 1
  • 2

对象创建表达式的值是新创建的对象。构造函数在第九章中有更详细的解释。

4.7 运算符概述

运算符用于 JavaScript 的算术表达式,比较表达式,逻辑表达式,赋值表达式等。表 4-1 总结了这些运算符,并作为一个方便的参考。

请注意,大多数运算符由标点字符表示,如+=。但是,有些运算符由关键字表示,如deleteinstanceof。关键字运算符是常规运算符,就像用标点符号表示的那些一样;它们只是具有不太简洁的语法。

表 4-1 按运算符优先级进行组织。列出的运算符比最后列出的运算符具有更高的优先级。由水平线分隔的运算符具有不同的优先级级别。标记为 A 的列给出了运算符的结合性,可以是 L(从左到右)或 R(从右到左),列 N 指定了操作数的数量。标记为 Types 的列列出了操作数的预期类型和(在→符号之后)运算符的结果类型。表后面的子章节解释了优先级,结合性和操作数类型的概念。这些运算符本身在讨论之后分别进行了文档化。

表 4-1. JavaScript 运算符

运算符操作AN类型
++前置或后置递增R1lval→num
--前置或后置递减R1lval→num
-取反数R1num→num
+转换为数字R1any→num
~反转位R1int→int
!反转布尔值R1bool→bool
delete删除属性R1lval→bool
typeof确定操作数的类型R1any→str
void返回未定义的值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→bool
instanceof测试对象类L2obj,func→bool
in测试属性是否存在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;
  • 1

乘法运算符*的优先级高于加法运算符+,因此先执行乘法。此外,赋值运算符=的优先级最低,因此在右侧所有操作完成后执行赋值。

可以通过显式使用括号来覆盖运算符的优先级。要求在上一个示例中首先执行加法,写成:

w = (x + y)*z;
  • 1

注意,属性访问和调用表达式的优先级高于表 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
  • 1
  • 2
  • 3
  • 4

尽管typeof是优先级最高的运算符之一,但typeof操作是在属性访问、数组索引和函数调用的结果上执行的,所有这些操作的优先级都高于运算符。

实际上,如果您对运算符的优先级有任何疑问,最简单的方法是使用括号使评估顺序明确。重要的规则是:乘法和除法在加法和减法之前执行。赋值的优先级非常低,几乎总是最后执行。

当新的运算符添加到 JavaScript 时,它们并不总是自然地适应这个优先级方案。??运算符(§4.13.2)在表中显示为比||&&低优先级,但实际上,它相对于这些运算符的优先级没有定义,并且 ES2020 要求您在混合??||&&时明确使用括号。同样,新的**乘幂运算符相对于一元否定运算符没有明确定义的优先级,当将否定与乘幂结合时,必须使用括号。

4.7.5 运算符结合性

在表 4-1 中,标记为 A 的列指定了运算符的结合性。L 值指定左到右的结合性,R 值指定右到左的结合性。运算符的结合性指定了相同优先级操作的执行顺序。左到右的结合性意味着操作从左到右执行。例如,减法运算符具有左到右的结合性,因此:

w = x - y - z;
  • 1

等同于:

w = ((x - y) - z);
  • 1

另一方面,以下表达式:

y = a ** b ** c;
x = ~-y;
w = x = y = z;
q = a?b:c?d:e?f:g;
  • 1
  • 2
  • 3
  • 4

等同于:

y = (a ** (b ** c));
x = ~(-y);
w = (x = (y = z));
q = a?b:(c?d:(e?f:g));
  • 1
  • 2
  • 3
  • 4

因为乘幂、一元、赋值和三元条件运算符具有从右到左的结合性。

4.7.6 评估顺序

运算符的优先级和结合性指定复杂表达式中操作的执行顺序,但它们不指定子表达式的评估顺序。JavaScript 总是严格按照从左到右的顺序评估表达式。例如,在表达式w = x + y * z中,首先评估子表达式w,然后是xyz。然后将yz的值相乘,加上x的值,并将结果赋给表达式w指定的变量或属性。添加括号可以改变乘法、加法和赋值的相对顺序,但不能改变从左到右的评估顺序。

评估顺序只有在正在评估的任何表达式具有影响另一个表达式值的副作用时才会有所不同。如果表达式x增加了一个被表达式z使用的变量,那么评估xz之前的事实就很重要。

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"
  • 1
  • 2
  • 3

当两个操作数的值都是数字,或者都是字符串时,+ 运算符的作用是显而易见的。然而,在任何其他情况下,都需要进行类型转换,并且要执行的操作取决于所执行的转换。+ 的转换规则优先考虑字符串连接:如果其中一个操作数是字符串或可转换为字符串的对象,则另一个操作数将被转换为字符串并执行连接。只有当两个操作数都不像字符串时才执行加法。

技术上,+ 运算符的行为如下:

  • 如果其操作数值中的任一值为对象,则它将使用 §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
  • 3
  • 4
  • 5
  • 6
  • 7

最后,重要的是要注意,当 + 运算符与字符串和数字一起使用时,它可能不是结合的。也就是说,结果可能取决于操作执行的顺序。

例如:

1 + 2 + " blind mice"    // => "3 blind mice"
1 + (2 + " blind mice")  // => "12 blind mice"
  • 1
  • 2

第一行没有括号,+ 运算符具有从左到右的结合性,因此先将两个数字相加,然后将它们的和与字符串连接起来。在第二行中,括号改变了操作顺序:数字 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
  • 1
  • 2

注意表达式 x++ 不总是等同于 x=x+1++ 运算符永远不会执行字符串连接:它总是将其操作数转换为数字并递增。如果 x 是字符串“1”,++x 是数字 2,但 x+1 是字符串“11”。

还要注意,由于 JavaScript 的自动分号插入,您不能在后增量运算符和其前面的操作数之间插入换行符。如果这样做,JavaScript 将把操作数视为一个独立的完整语句,并在其前插入一个分号。

这个运算符,在其前增量和后增量形式中,最常用于递增控制 for 循环的计数器(§5.4.3)。

递减--

-- 运算符期望一个左值操作数。它将操作数的值转换为数字,减去 1,并将减少后的值重新赋给操作数。与 ++ 运算符一样,-- 的返回值取决于其相对于操作数的位置。当在操作数之前使用时,它减少并返回减少后的值。当在操作数之后使用时,它减少操作数但返回未减少的值。在操作数之后使用时,不允许换行符。

4.8.3 位运算符

位运算符对数字的二进制表示中的位进行低级别操作。虽然它们不执行传统的算术运算,但在这里被归类为算术运算符,因为它们对数字操作并返回一个数字值。这四个运算符对操作数的各个位执行布尔代数运算,表现得好像每个操作数中的每个位都是一个布尔值(1=true,0=false)。另外三个位运算符用于左移和右移位。这些运算符在 JavaScript 编程中并不常用,如果你不熟悉整数的二进制表示,包括负整数的二进制补码表示,那么你可能可以跳过这一部分。

位运算符期望整数操作数,并表现得好像这些值被表示为 32 位整数而不是 64 位浮点值。这些运算符将它们的操作数转换为数字,如果需要的话,然后通过丢弃任何小数部分和超过第 32 位的任何位来将数值值强制转换为 32 位整数。移位运算符需要一个右侧操作数,介于 0 和 31 之间。在将此操作数转换为无符号 32 位整数后,它们会丢弃超过第 5 位的任何位,从而得到适当范围内的数字。令人惊讶的是,当这些位运算符的操作数时,NaNInfinity-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 的关系运算符。这些运算符测试两个值之间的关系(如“相等”,“小于”或“属性”),并根据该关系是否存在返回truefalse。关系表达式始终评估为布尔值,并且该值通常用于控制程序执行在ifwhilefor语句中的流程(参见第五章)。接下来的小节记录了相等和不等运算符,比较运算符以及 JavaScript 的另外两个关系运算符ininstanceof

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
  • 1

此表达式求值为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.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后,请注意<=(小于或等于)和>=(大于或等于)运算符不依赖于相等或严格相等运算符来确定两个值是否“相等”。相反,小于或等于运算符简单地定义为“不大于”,大于或等于运算符定义为“不小于”。唯一的例外是当任一操作数是(或转换为)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 method

let 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 3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4.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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

注意所有对象都是Object的实例。instanceof在判断一个对象是否是某个类的实例时会考虑“超类”。如果instanceof的左操作数不是对象,则返回false。如果右操作数不是对象类,则抛出TypeError

要理解instanceof运算符的工作原理,您必须了解“原型链”。这是 JavaScript 的继承机制,描述在§6.3.2 中。要评估表达式o instanceof f,JavaScript 会评估f.prototype,然后在o的原型链中查找该值。如果找到,则of的实例(或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
  • 1

关系表达式始终评估为truefalse,因此在这种情况下,&&运算符本身返回truefalse。关系运算符的优先级高于&&(和||),因此可以安全地写出不带括号的表达式。

但是&&不要求其操作数是布尔值。回想一下,所有 JavaScript 值都是“真值”或“假值”。(有关详细信息,请参阅§3.4。假值包括falsenullundefined0-0NaN""。所有其他值,包括所有对象,都是真值。)&&的第二个级别可以理解为真值和假值的布尔 AND 运算符。如果两个操作数都是真值,则运算符返回真值。否则,一个或两个操作数必须是假值,运算符返回假值。在 JavaScript 中,任何期望布尔值的表达式或语句都可以使用真值或假值,因此&&并不总是返回truefalse不会造成实际问题。

请注意,此描述指出该运算符返回“真值”或“假值”,但没有指定该值是什么。为此,我们需要在第三个最终级别描述&&。该运算符首先评估其第一个操作数,即左侧的表达式。如果左侧的值为假,整个表达式的值也必须为假,因此&&只返回左侧的值,甚至不评估右侧的表达式。

另一方面,如果左侧的值为真值,则表达式的整体值取决于右侧的值。如果右侧的值为真值,则整体值必须为真值,如果右侧的值为假值,则整体值必须为假值。因此,当左侧的值为真值时,&&运算符评估并返回右侧的值:

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 don't evaluate p.x
  • 1
  • 2
  • 3
  • 4

重要的是要理解 && 可能会或可能不会评估其右侧操作数。在这个代码示例中,变量 p 被设置为 null,并且表达式 p.x 如果被评估,将导致 TypeError。但是代码以一种惯用的方式使用 &&,以便仅在 p 为真值时才评估 p.x,而不是 nullundefined

&& 的行为有时被称为短路,你可能会看到故意利用这种行为有条件地执行代码的代码。例如,下面两行 JavaScript 代码具有等效的效果:

if (a === b) stop();   // Invoke stop() only if a === b
(a === b) && stop();   // This does the same thing
  • 1
  • 2

一般来说,当你在 && 的右侧写一个具有副作用(赋值、递增、递减或函数调用)的表达式时,你必须小心。这些副作用是否发生取决于左侧的值。

尽管这个运算符实际上的工作方式有些复杂,但它最常用作一个简单的布尔代数运算符,适用于真值和假值。

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;
  • 1
  • 2
  • 3

请注意,如果 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
}
  • 1
  • 2
  • 3
  • 4
  • 5

然而,在 ES6 及以后,这个技巧不再需要,因为默认参数值可以直接写在函数定义中:function copy(o, p={}) { ... }

4.10.3 逻辑 NOT (!)

! 运算符是一个一元运算符;它放在单个操作数之前。它的目的是反转其操作数的布尔值。例如,如果 x 是真值,!x 评估为 false。如果 x 是假值,则 !xtrue

&&|| 运算符不同,! 运算符在反转转换其操作数为布尔值(使用 第三章 中描述的规则)之前。这意味着 ! 总是返回 truefalse,你可以通过两次应用这个运算符将任何值 x 转换为其等效的布尔值:!!x(参见 §3.9.2)。

作为一元运算符,! 具有高优先级并且紧密绑定。如果你想反转类似 p && q 的表达式的值,你需要使用括号:!(p && q)。值得注意的是,我们可以使用 JavaScript 语法表达布尔代数的两个定律:

// DeMorgan's Laws
!(p && q) === (!p || !q)  // => true: for all values of p and q
!(p || q) === (!p && !q)  // => true: for all values of p and q
  • 1
  • 2
  • 3

4.11 赋值表达式

JavaScript 使用 = 运算符将一个值分配给一个变量或属性。例如:

i = 0;     // Set the variable i to 0.
o.x = 1;   // Set the property x of object o to 1.
  • 1
  • 2

= 运算符期望其左侧操作数是一个 lvalue:一个变量或对象属性(或数组元素)。它期望其右侧操作数是任何类型的任意值。赋值表达式的值是右侧操作数的值。作为副作用,= 运算符将右侧的值分配给左侧的变量或属性,以便将来对变量或属性的引用评估为该值。

虽然赋值表达式通常相当简单,但有时您可能会看到赋值表达式的值作为更大表达式的一部分使用。例如,您可以使用以下代码在同一表达式中赋值和测试一个值:

(a = b) === 0
  • 1

如果这样做,请确保您清楚====运算符之间的区别!请注意,=的优先级非常低,当赋值的值要在更大的表达式中使用时,通常需要括号。

赋值运算符具有从右到左的结合性,这意味着当表达式中出现多个赋值运算符时,它们将从右到左进行评估。因此,您可以编写如下代码将单个值分配给多个变量:

i = j = k = 0;       // Initialize 3 variables to 0
  • 1

4.11.1 带操作符的赋值

除了正常的=赋值运算符外,JavaScript 还支持许多其他赋值运算符,通过将赋值与其他操作结合起来提供快捷方式。例如,+=运算符执行加法和赋值。以下表达式:

total += salesTax;
  • 1

等同于这个:

total = total + salesTax;
  • 1

正如您所期望的那样,+=运算符适用于数字或字符串。对于数字操作数,它执行加法和赋值;对于字符串操作数,它执行连接和赋值。

类似的运算符包括-=*=&=等。表 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 ** b
<<=a <<= ba = a << b
>>=a >>= ba = a >> b
>>>=a >>>= ba = a >>> b
&=a &= ba = a & b
&#124;=a &#124;= ba = a &#124; b
^=a ^= ba = a ^ b

在大多数情况下,表达式:

a op= b
  • 1

其中op是一个运算符,等价于表达式:

a = a op b
  • 1

在第一行中,表达式a被评估一次。在第二行中,它被评估两次。这两种情况只有在a包含函数调用或增量运算符等副作用时才会有所不同。例如,以下两个赋值是不同的:

data[i++] *= 2;
data[i++] = data[i++] * 2;
  • 1
  • 2

4.12 评估表达式

与许多解释性语言一样,JavaScript 有解释 JavaScript 源代码字符串并对其进行评估以生成值的能力。JavaScript 使用全局函数eval()来实现这一点:

eval("3+2")    // => 5
  • 1

动态评估源代码字符串是一种强大的语言特性,在实践中几乎从不需要。如果您发现自己使用eval(),您应该仔细考虑是否真的需要使用它。特别是,eval()可能存在安全漏洞,您绝不应将任何源自用户输入的字符串传递给eval()。由于 JavaScript 这样复杂的语言,没有办法对用户输入进行清理以使其安全用于eval()。由于这些安全问题,一些 Web 服务器使用 HTTP 的“内容安全策略”头部来禁用整个网站的eval()

接下来的小节将解释eval()的基本用法,并解释两个对优化器影响较小的受限版本。

4.12.1 eval()

eval()期望一个参数。如果传递的值不是字符串,则它只是返回该值。如果传递一个字符串,则它尝试将字符串解析为 JavaScript 代码,如果失败则抛出 SyntaxError。如果成功解析字符串,则评估代码并返回字符串中最后一个表达式或语句的值,如果最后一个表达式或语句没有值,则返回undefined。如果评估的字符串引发异常,则该异常从调用eval()传播出来。

eval()的关键之处(在这种情况下调用)是它使用调用它的代码的变量环境。也就是说,它查找变量的值,并以与局部代码相同的方式定义新变量和函数。如果一个函数定义了一个局部变量x,然后调用eval("x"),它将获得局部变量的值。如果它调用eval("x=1"),它会改变局部变量的值。如果函数调用eval("var y = 3;"),它会声明一个新的局部变量y。另一方面,如果被评估的字符串使用letconst,则声明的变量或常量将局部于评估,并不会在调用环境中定义。

类似地,函数可以使用以下代码声明一个局部函数:

eval("function f() { return x+1; }");
  • 1

如果你从顶层代码调用eval(),它当然会操作全局变量和全局函数。

请注意,传递给eval()的代码字符串必须在语法上是合理的:你不能使用它来将代码片段粘贴到函数中。例如,写eval("return;")是没有意义的,因为return只在函数内部合法,而被评估的字符串使用与调用函数相同的变量环境并不使其成为该函数的一部分。如果你的字符串作为独立脚本是合理的(即使是非常简短的像x=0),那么它是可以传递给eval()的。否则,eval()会抛出 SyntaxError。

4.12.2 全局 eval()

正是eval()改变局部变量的能力让 JavaScript 优化器感到困扰。然而,作为一种解决方法,解释器只是对调用eval()的任何函数进行较少的优化。但是,如果一个脚本定义了eval()的别名,然后通过另一个名称调用该函数,JavaScript 规范声明,当eval()被任何名称调用时,除了“eval”之外,它应该评估字符串,就像它是顶层全局代码一样。被评估的代码可以定义新的全局变量或全局函数,并且可以设置全局变量,但不会使用或修改调用函数的局部变量,因此不会干扰局部优化。

“直接 eval”是使用确切的、未限定名称“eval”调用eval()函数的表达式(开始感觉像是一个保留字)。直接调用eval()使用调用上下文的变量环境。任何其他调用——间接调用——使用全局对象作为其变量环境,不能读取、写入或定义局部变量或函数。(直接和间接调用只能使用var定义新变量。在评估的字符串中使用letconst会创建仅在评估中局部的变量和常量,不会改变调用或全局环境。)

以下代码演示:

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 eval
    let x = "local";              // Define a local variable
    eval("x += 'changed';");      // Direct eval sets local variable
    return x;                     // Return changed local variable
}
function g() {                    // This function does a global eval
    let y = "local";              // A local variable
    geval("y += 'changed';");     // Indirect eval sets global variable
    return 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":
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

请注意,进行全局 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 条件运算符 (?
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/295358

推荐阅读
相关标签