JavaScript 运算符详解

在上一章动态类型中,我们探讨了类型强制和检测; 我们还介绍了几个操作符。 在本章中,我们将通过深入研究 JavaScript 语言提供的每个操作符来继续探索。 充分理解 JavaScript 的操作符会让我们觉得自己在这种有时会让人感到困惑的语言中获得了巨大的力量。 不幸的是,理解 JavaScript 没有捷径,但是当您开始探索它的操作符时,您将看到模式的出现。 例如,许多乘法运算符以类似的方式工作,逻辑运算符也是如此。 一旦您熟悉了主要操作符,您就会开始看到在复杂性的基础上存在一种优雅。

It may be useful to treat this chapter as more of a reference if you're pressed for time. Do not feel like you need to exhaustively retain every detail of every operator's behavior. 

在本章中,我们将涵盖以下主题:

现在我们已经准备好深入研究,我们需要问自己的第一个问题是:到底是什么算子?

在 JavaScript 中,操作符是一段独立的语法,它形成了一个表达式,通常用于从一组输入(称为操作数)中导出或计算逻辑或数学输出。

在这里,我们可以看到一个表达式,它包含一个操作符(+)和两个操作数(35):

3 + 5

任何算子都可以说有四个特点:

  • :操作符接受多少个操作数
  • :操作符对其操作数所做的操作以及运算结果
  • 其优先级:该运算符与其他运算符组合使用时的分组方式
  • 它的结合性:当操作符与相同优先级的操作符相邻时,操作符的行为

理解这些基本特征非常重要,因为它将极大地帮助您在 JavaScript 中使用操作符。

Arity 是指一个操作符可以接收多少个操作数(或输入)。 操作数是用于表示可以给操作符或传递给操作符的值。

如果考虑大于操作符(>),它将接收两个操作数:

a > b

在这个例子中,a是它的第一个操作数(或左操作数)。 而b是它的第二个操作数(或右端操作数)。 因为它接收两个操作数,所以大于操作符被认为是一个二元操作符。 在 JavaScript 中,我们有一元、二元和三元操作符:

// Unary operator examples (one operand)
-a
!a

// Binary operator examples (two operands)
a == b
a >= b

// Ternary operator examples (three operands)
a ? b : c

There is only one ternary operator in JavaScript, the conditional operator (a ? b : c). Since it is the only ternary operator, it is sometimes simply referred to as the ternary operator instead of its formal name.

了解给定操作符的性质是至关重要的——就像知道要传递多少个参数一样。 在编写操作时,考虑如何传达意图也很重要。 由于操作可以串联出现,所以有时会不清楚哪个操作符指向哪个操作数。 考虑这个令人困惑的表达式:

foo + + baz - - bar

为了避免在理解这样的操作时产生混淆,通常会将一元操作符移到其操作数附近,甚至使用括号来明确其意图:

foo + (+baz) - (-bar)

与代码的所有部分一样,在使用操作符时,必须小心和关心将不得不遇到、理解和维护代码的一个或多个人(包括您未来的自己)。

一个操作符的函数就是它所做的以及它的计算结果。 我们将单独讨论每一个运算符,所以除了在使用运算符时可以携带的一些基本假设之外,这里没有太多可说的。

在 JavaScript 中,每个操作符都是自己的实体,不受操作数类型的限制。 这与其他一些语言相反,在这些语言中,操作符被映射到可重写的函数,或者以某种方式附加到操作数本身。 在 JavaScript 中,操作符是它们自己的语法实体,具有不可重写的功能。 但是,它们的功能在某些情况下是可扩展的。

当使用下列任何类型的操作符时,语言将在内部尝试强制转换:

  • 算术运算符(即+*/-等)
  • 递增操作符(即++--)
  • 按位运算符(即~<<|等)
  • 计算成员访问操作符(即...[...])
  • 非严格比较运算符(即><>=<===)

为了特别覆盖这些强制机制,你可以为任何打算用作操作数的对象提供valueOf()toString()[Symbol.toPrimitive]()方法:

const a = { valueOf() { return 3; } };
const b = { valueOf() { return 5; } };

a + b; // => 8
a * b; // => 15

正如我们在前一章的转换到原语小节中所述,这些方法将根据所使用的操作符或语言结构以特定的顺序被调用。 例如,在所有算术运算符的情况下,valueOf将在toString之前尝试。

多个运算符组合使用时的操作顺序由两种机制定义:优先结合。 操作符的优先级是一个从120的数字,它定义了一系列操作符运行的顺序。 有些操作符具有相同的优先级。 结合性定义了操作相同优先级的操作符的顺序(从左到右或从右到左)。

考虑以下操作:

1 + 2 * 3 / 4 - 5;

在 JavaScript 中,这些特定的数学运算符具有以下优先级:

  • 加法运算符(+)的优先级为13
  • 乘法运算符(*)的优先级为14
  • 除法运算符(/)的优先级为14
  • 减法运算符(-)的优先级为13

它们都有从左到右结合律。 由于优先级较高的操作符优先出现,而优先级相同的操作符将根据它们的结合性出现,所以我们可以说我们的示例操作按以下顺序出现:

  1. 乘法(在优先级为14的操作符中最左边)
  2. 除法(在优先级为14的操作符中,左边下一个操作符)
  3. 加法(在优先级为13的操作符中最左边)
  4. 减法(在优先级为13的操作符中左边的下一个)

如果我们要使用括号显式地对操作进行分组,那么它看起来就像这样:

(
  1 +
  (
    (2 * 3)
    / 4
  )
) - 5;

每个运算符,甚至是非数学运算符,都有特定的优先级和关联。 例如,typeof操作符的优先级为16。 如果将它与低优先级操作符结合使用,可能会导致一个令人头痛的问题:

typeof 1 + 2; // => "number2"

由于+操作符的优先级低于typeof,JavaScript 内部会像这样运行这个操作:

(typeof 1) + 2;

因此,这导致typeof 1(即"number")与2连接(产生"number2")。 为了避免这种情况,我们必须使用自己的括号强制执行顺序:

typeof (1 + 2); // => "number"

顺便提一下,这就是为什么您可能经常看到typeof带有括号(typeof(...)),这可以使它看起来像一个正在调用的函数。 然而,它是一个操作符,括号只是用来强制执行特定的操作顺序。

You can discover the exact precedences of every operator by reading the ECMAScript specification or searching online for JavaScript operator precedences. Note that the numbers used to indicate precedence between 1 and 20 are not from the ECMAScript specification itself but are rather just a useful way of understanding precedence. 

了解每个操作符的优先级和关联不是我们应该期望我们的同事做的事情。 我们可以合理地假设他们知道一些基本数学运算符的优先级,但除此之外的知识不应被认为是有保证的。 因此,经常有必要使用圆括号来提供清晰性,即使在某些情况下它们可能不是严格必需的。 这在有大量连续运算符的复杂运算中尤其重要,如下面这个例子:

function calculateRenderedWidth(width, horizontalPadding, scale) {
  return (width + (2 * horizontalPadding)) * scale;
}

这里,括号包装(2 * horizontalPadding)在技术上是不必要的,因为乘法运算符的优先级自然高于加法运算符。 然而,提供额外的清晰度是有用的。 阅读此代码的程序员将感激花更少的认知精力来识别操作的确切顺序。 然而,与许多善意的事情一样,这可能做得太过火了。 括号既不能提供清晰性,也不能提供不同的强制操作顺序。 这种冗余的一个例子可能是将整个return表达式包装在附加的括号中:

function calculateRenderedWidth(width, horizontalPadding, scale) {
  return ((width + (2 * horizontalPadding)) * scale);
}

这在理想情况下应该避免,因为如果它做得太过了,它会给代码的读者带来额外的认知负荷。 对于这种情况,一个很好的指导是,如果你倾向于添加额外的圆括号以清晰,你可能应该将操作分成多行:

function calculateRenderedWidth(width, horizontalPadding, scale) {
  const leftAndRightPadding = 2 * horizontalPadding;
  const widthWithPadding = width + leftAndRightPadding;
  const scaledWidth = widthWithPadding * scale;
  return scaledWidth;
}

这些添加的行不仅清晰地说明了操作的顺序,而且通过将每个操作有效地分配给一个描述性变量,还提供了每个操作的目的。

了解每个操作符的优先级和结合性并不一定是至关重要的,但了解每个操作下的这些机制是非常有用的。 正如您所看到的,在大多数情况下,为了清晰起见,最好将操作划分为自包含的行或组,即使操作符的内部优先级或结合性不需要这样做。 最重要的是,我们必须始终考虑我们是否清楚地向代码的读者传达了我们的意图。

The average JavaScript programmer will not have an encyclopedic knowledge of the ECMAScript specification, and as such, we should not demand such knowledge to comprehend the code we have written. 

对底层操作符机制的了解为我们现在研究 JavaScript 中的各个操作符铺平了道路。 我们将从探索算术和数字运算符开始。

JavaScript 中有 8 个算术或数字运算符:

  • 加料:a + b
  • 减法:a - b
  • :a / b
  • 乘法:a * b
  • :a % b
  • 取幂:a ** b
  • 一元+:+a
  • 一元减:-a

Arithmetic and numeric operators will typically coerce their operands to numbers. The only exception is the + addition operator, which will, if passed a non-numerical operand, assume the function of string concatenation instead of addition.

所有这些操作都有一个值得事先了解的有保证的结果。 NaN的输入保证NaN的输出:

1 + NaN; // => NaN
1 / NaN; // => NaN
1 * NaN; // => NaN
-NaN;    // => NaN
+NaN;    // => NaN
// etc.

除了这个基本的假设之外,每个操作符的行为方式都略有不同,所以值得单独研究它们。

加法运算符具有双重用途:

  • 如果其中一个操作数是String,那么它将把两个操作数连接在一起
  • 如果两个操作数都不是String,则将两个操作数相加为数字

为了实现它的双重目的,+操作符首先需要识别所传递的操作数是否可以视为字符串。 String显然,原始价值显然是一个字符串,但是对于 non-primitives,+操作符将尝试你的操作数转换成他们的原始表示依靠内部ToPrimitive我们在最后一章详细过程,在转换原始部分。 如果+操作数的ToPrimitive输出是字符串,那么它将把两个操作数连接为字符串。 否则,它会把它们加成数字。

事实上,+操作符同时适用于数字相加和连接,这使得理解它非常复杂,因此,通过几个示例对我们很有帮助。

:当两个操作数都是基本数时,+操作符非常简单地将它们相加:

1 + 2; // => 3

:当两个操作数都是原始字符串时,+操作符非常简单地将它们连接在一起:

'a' + 'b'; // => "ab"

:当只有一个操作数是原始字符串时,+操作符会将另一个操作数强制转换为String,然后将两个结果字符串连接在一起:

123 + 'abc'; => "123abc"
'abc' + 123; => "abc123"

:当任意一个操作数是非原语时,+操作符将其转换为原语,然后按照通常使用新原语表示的方式进行操作。 这里有一个例子:

[123] + 123; // => "123123"

在这种情况下,JavaScript 将使用返回值[123].toString()(即"123")将[123]转换为其原始值。 因为数组的基本表示是它的String表示,所以+操作符的操作方式就像我们简单地做"123" + 123一样,而"123" + 123的计算结果是"123123"

在使用+操作符时,了解所处理的操作数尤其重要。 如果你不这样做,那么你的行动的结果可能是出乎意料的。 +操作符可能是比较复杂的操作符之一,因为它有双重目的。 大多数运算符都没有这么复杂。 我们接下来将探讨的减法运算符要简单得多。

减法运算符(-)做了它在罐头上说的事情。 它接受两个操作数,用左侧操作数减去右侧操作数:

555 - 100; // => 455

如果其中一个操作数不是数字,它将被强制为 1:

'5' - '3'; // => 2
'5' - 3;   // => 2
5 - '3';   // => 2

这也包括非原始类型:

[5] - [3]; // => 2

这里,我们看到两个数组,每个数组只有一个元素,彼此相减。 这似乎没有任何意义,直到我们回忆起数组的基本表示是它的连接元素作为字符串,即分别为"5""3":

String([5]); // => "5"
String([3]); // => "3"

然后,通过相当于以下内容的操作,将它们转换为它们的数字表示53:

Number("5"); // => 5
Number("3"); // => 3

因此,我们就得到了5减去3的直观运算,我们知道2

除法运算符很像减法运算符,接受两个操作数,并将其强制转换为数字。 它将用它的左操作数除以右操作数:

10 / 2; // => 5

这两个操作数正式地被称为被除数和除数(DIVIDEND / DIVISOR),并且总是根据浮点数学计算。 JavaScript 中不存在整数除法,这意味着除法的结果可能总是包含小数点,因此可能会出现Number.EPSILON的误差范围。

当除以 0 时要小心,因为你可能会得到NaN(0 除以 0)或Infinity(非零数除以 0):

10 / 0;  // => Infinity
10 / -0; // => -Infinity
0 / 0;   // => NaN

如果你的除数是Infinity,你的除法总是等于零(0-0),除非你的除数也是Infinity,在这种情况下,你将得到NaN:

1000 / Infinity; // => 0
-1000 / Infinity; // => -0
Infinity / Infinity; // => NaN

在你期望一个除数或被除数为 0,NaNInfinity的情况下,最好是防御,并在操作之前或之后明确地检查这些值,如下所示:

function safeDivision(a, b) {
  const result = a / b;
  if (!isFinite(result)) {
    throw Error(`Division of ${a} by ${b} is unsafe`);
  }
  return result;
}

safeDivision(1, 0); // ! Throws "Division of 1 by 0 is unsafe"
safeDivision(6, 2); // => 3

除法的边缘情况可能看起来很可怕,但在日常应用中并不经常遇到。 然而,如果我们要编写一个医疗或财务程序,那么仔细考虑我们的操作的潜在错误状态是绝对重要的。

乘法运算符的行为类似于除法运算符,除了它执行乘法这一明显的事实:

5 * 25; // => 125

必须注意强制转换的效果,以及操作数为NaNInfinity的情况。 更直观地说,将任何非零的有限值乘以Infinity总是会得到Infinity(带有适当的符号):

100 * Infinity; // => Infinity
-100 * Infinity; // => -Infinity

然而,将 0 乘以Infinity总会得到NaN:

0 * Infinity; // => NaN
-Infinity * -0; // => NaN

除了这些情况,乘法运算符的大多数用法是相当直观的。

余数算子(%),也称为模算子,与除法算子相似。 它接受两个操作数:一个被除数在左边,一个除数在右边。 它将在一个隐含的除法操作后返回余数:

10 % 5; // => 0
10 % 4; // => 2
10 % 3; // => 1
10 % 2; // => 0

如果除数为 0,则被除数为Infinity,或者其中一个操作数为NaN,则运算结果为NaN:

Infinity % Infinity; // => NaN
Infinity % 2; // => NaN
NaN % 1; // => NaN
1000 % 0; // => NaN

如果除数是Infinity,则结果等于被除数:

1000 % Infinity; // => 1000
0.03 % Infinity; // => 0.03

的模运算符是有用的在你想知道很多的情况下进入另一个号码直接,比如当希望建立平衡奇怪一个整数:

function isEvenNumber(number) {
  return number % 2 === 0;
}

isEvenNumber(0); // => true
isEvenNumber(1); // => false
isEvenNumber(2); // => true
isEvenNumber(3); // => false

与所有其他算术运算符一样,了解操作数的强制方式是很有用的。 剩余运算符的大多数用法都是直接的,所以除了它的强制行为和它对NaNInfinity的处理之外,你应该会发现它的行为是直观的。

求幂运算符(**)接受两个操作数,一个基数在左边,一个指数在右边。 它的底数是指数的几次方:

10 ** 2; // => 100
10 ** 3; // => 1,000
10 ** 4; // => 10,000

它在功能上与使用Math.pow(a, b)操作相同,但更简洁。 与其他算术运算一样,它将在内部强制其操作数为Number类型,传入任何NaNInfinity或 0 操作数都可能导致意想不到的结果,因此应该尽量避免这种情况。

值得一提的是,如果指数为零,那么无论底是多少,结果都是1。 因此,基底可以是InfinityNaN,或者其他什么,结果仍然是1:

1000 ** 0;     // => 1
0 ** 0;        // => 1
Infinity ** 0; // => 1
NaN ** 0;      // => 1

所有其他算术运算符,如果其中一个操作数是NaN,则计算结果为NaN,因此**的行为是非常独特的。 另一个独特的行为是,如果你的第一个操作数本身是一元操作,它将抛出SyntaxError:

+2 ** 2;
// SyntaxError: Unary operator used immediately
// before exponentiation expression. Parenthesis
// must be used to disambiguate operator precedence

这是为了防止程序员产生歧义。 根据他们以前接触过的其他语言(或严格的数学符号),他们可能期望-2 ** 2这样的情况是4-4。 因此,JavaScript 将在这种情况下抛出,因此迫使您使用(-2) ** 2-(2 ** 2)更显式。

除了这些独特的特征,幂运算符可以被认为类似于其他二进制(两操作数)算术运算符。 和往常一样:要注意操作数的类型以及它们是如何被强制的!

一元加号运算符(+...)将其操作数转换为Number,就像它被传递给Number(...)一样:

+'42'; // => 42
+({ valueOf() { return 42; } });

如上一章所述,我们将在转换为原语部分中使用我们所珍视的内部ToPrimitive过程。 它的结果将被重新强制到Number,如果它不是Number。 因此,如果ToPrimitive返回String,则String将被转换为Number,这意味着非数字字符串将导致NaN:

+({ toString() { return 'not a number'; } }); // => NaN

当然,如果ToPrimitive中的String可以转换为Number,那么一元+操作符将计算得到:

+({ toString() { return '12345'; } }); // => 12345

当通过+强制数组时,这更真实地观察到:

+['5e3']; // => 5000

// Equivalent to:
Number(String(['5e3'])); // => 5000

一元+操作符通常用于程序员希望将一个类似数字的对象转换为Number的地方,以便他们可以将其与其他数字操作一起使用。 然而,通常更可取的是明确使用Number(...),因为它更清楚的意图是什么。

一元+运算符有时会与其他运算符混淆。 考虑一下这个场景:

number + +someObject

对于不熟悉一元加号的人或不习惯经常看到它的人来说,这段代码可能看起来像是包含了一个打印错误。 我们可以将整个一元操作包装在它自己的圆括号中,使其更清晰:

number + (+someObject)

或者我们可以使用更清晰的Number(...)函数:

number + Number(someObject)

总之,一元+运算符是Number(...)的快捷方式。 虽然在大多数情况下,我们应该更清楚地表达我们的意图,但它是有用和快速的。

一元减操作符(-...)首先将其操作数转换为Number,方法与一元+操作符相同,在最后一节中详细介绍,然后将其求反:

-55;    // => -55
-(-55); // => 55
-'55';  // => -55

它的用法相当直接和直观,尽管与一元+一样,它有助于消除二元运算符旁边有一元运算符的情况。 像这样的情况可能会令人困惑:

number - -otherNumber

在这些情况下,最好是用圆括号来表达清楚:

number - (-otherNumber)

一元减运算符通常只与字面值操作数一起直接使用,以指定一个负数。 与所有其他算术运算符一样,我们应该确保我们的意图是明确的,并且不会用冗长或令人困惑的表达式使人们感到困惑。

既然我们已经研究了算术运算符,现在可以开始研究逻辑运算符了。

逻辑运算符通常用于构建逻辑表达式,其中表达式的结果通知某些操作或不操作。 JavaScript 中有三个逻辑操作符:

  • NOT 运算符(!a)
  • AND 运算符(a && b)
  • OR 运算符(a || b)

与大多数其他操作符一样,它们可以接受各种类型,并在必要时强制执行。 通常,AND 和 OR 运算符并不总是求出Boolean值,它们都利用了一种叫做短路求值的机制,只在满足某些条件时才执行两个操作数。 在研究每个逻辑运算符时,我们将进一步了解这一点。

NOT 操作符是一元操作符。 它只接受一个操作数,并将该操作数转换为其布尔表示,然后将其反转,这样真项就变成了false,假项就变成了true:

!1;    // => false
!true; // => false
!'hi;  // => false

!0;    // => true
!'';   // => true
!true; // => false

在内部,NOT 操作符将执行以下操作:

  1. 将操作数转换为布尔值(Boolean(operand))
  2. 如果结果值为true,则返回false; 否则,返回true

作为讨论的转换为一个布尔部分在最后一章,一个典型的习语为将一个值转换成布尔表示双不是(即!!value),这有效地逆转的真实或 falsiness 两次,等于一个布尔值。 更明确和更受欢迎的习语是使用Boolean(value),因为其意图要比!!清楚得多。

由于 JavaScript 中只有 7 个假值,所以 NOT 操作符在以下 7 种情况下只能计算为true:

!false;     // => true
!'';        // => true
!null;      // => true
!undefined; // => true
!NaN;       // => true
!0n;        // => true
!0;         // => true

JavaScript 对虚假和真实的严格定义令人放心。 这意味着,即使有人构造了一个具有各种原始表示形式的对象(想象一个具有valueOf()的对象返回一个假值),所有内部布尔强制值仍然只返回false的 7 个假值,其他什么也不会返回。 这意味着我们只需要担心那七个。 )。

总的来说,逻辑 NOT 操作符的用法非常简单。 它是一种很好理解的语法,跨越编程语言,具有清晰的语义。 因此,没有太多的方式最佳实践关于它。 至少,最好避免代码中出现过多的双重否定。 double negative 指的是将命名为负的变量应用到 NOT 操作符,如下所示:

if (!isNotEnabled) {
  // ...
}

对于那些阅读您的代码的人来说,这是认知上的昂贵代价,因此很容易产生误解。 最好使用正面命名的布尔变量名,以便使用它们的任何逻辑操作都能直接理解。 在这种情况下,只需重命名变量并反转操作,如下所示:

if (isEnabled) {
  // ...
}

总的来说,逻辑 NOT 操作符在if()while()这样的布尔上下文中最有用,尽管在 double-NOT!!操作中也有惯用的用法。 从技术上讲,它是 JavaScript 中唯一保证返回Boolean值的操作符,而不管其操作数的类型是什么。 接下来,我们将探索 AND 操作符。

JavaScript 中的逻辑与操作符(&&)接受两个操作数。 如果它的左边操作数为真,那么它将求值并返回右边操作数; 否则,它将返回左侧操作数:

0 && 1; // => 0
1 && 2; // => 2

对于许多人来说,这可能是一个令人困惑的运算符,因为他们错误地假设它相当于问题a 和 B 都是真的吗? 如果 A 是真的,那么给我 B; 否则,我就选 A吧。 人们可能会假设 JavaScript 会计算两个操作数,但事实上,只有当左侧操作数为真时,它才会计算右侧操作数。 这就是所谓的短路评估。 JavaScript 不会将操作的结果值转换为Boolean:相反,它会将那个值原样返回给我们。 如果我们要自己实现这个操作,它可能看起来像这样:

function and(a, b) {
  if (a) return b;
  return a;
}

给定一个简单的操作,例如在两个值为真的情况下生成一个if(...)语句,&&操作符将以完全意料之外和预期的方式运行:

if (true && 1) {
  // Both `true` and `1` are truthy!
}

然而,&&操作符也可以用于更有趣的方式,例如当需要返回一个值,但只有在某些先决条件满足时:

function getFavoriteDrink(user) {
  return user && user.favoriteDrink;
}

这里,&&操作符是在非布尔上下文中使用的,其中没有强制其结果发生。 在这种情况下,如果它的左操作数是假的(也就是说,如果user是假的),那么它将返回那个值; 否则,它将返回右侧操作数(即user.favoriteDrink):

getFavoriteDrink({ favoriteDrink: 'Coffee' }); // => 'Coffee'
getFavoriteDrink({ favoriteDrink: null }); // => null
getFavoriteDrink(null); // => null

getFavoriteDrink函数以一种履行基本契约的方式运行,如果user对象是可用的,如果favoriteDrink属性出现在该对象上,则返回favoriteDrink,尽管它的实际功能有点混乱:

getFavoriteDrink({ favoriteDrink: 0 }); // => 0
getFavoriteDrink(0); // => 0
getFavoriteDrink(NaN); // => NaN

我们的getFavoriteDrink功能不是对用户的具体性质或favoriteDrink价值观进行任何商议; 它只是盲目地屈服于&&操作符,返回其左侧或右侧操作数。 如果我们对操作数的潜在值有信心,那么这种方法可能很好。

It's important to take the time to consider the possible ways that && will evaluate the operands you provide it with. Take into consideration the fact that it is not guaranteed to return Boolean and is not guaranteed to even evaluate the right-side operand.

由于其短路特性,&&操作符也可用来表示控制流。 让我们考虑一个场景,如果isFeatureEnabled布尔值为真,我们希望调用renderFeature()。 按照惯例,我们可以使用if语句来这样做:

if (isFeatureEnabled) {
  renderFeature();
}

但我们也可以使用&&:

isFeatureEnabled && renderFeature();

这种用法和&&的其他非常规用法通常是不受欢迎的,因为它们可能会模糊程序员的意图,并给那些对&&在 JavaScript 中如何操作没有深入了解的代码读者造成困惑。 尽管如此,&&操作符确实很强大,应该在非常适合手头任务的情况下使用。 您应该觉得可以按照自己的意愿使用它,但要始终注意代码的典型读者可能如何看待操作,并始终考虑操作可能产生的预期值。

JavaScript 中的逻辑 OR 操作符(||)接受两个操作数。 如果它的左操作数为真,则立即返回; 否则,它将求值并返回右侧操作数:

0 || 1; // => 1
2 || 0; // => 2
3 || 4; // => 3

就像&&运算符||操作符是灵活的,它并不把它返回到Boolean,短路的方式评估,这意味着它只能评估右边操作数左侧操作数是否满足一个条件这种情况下,如果 falsy 右操作数:

true || thisWillNotExecute();
false || thisWillExecute();

通常,程序员可以假设逻辑或运算符类似于问题是 a 还是 B 是真的? 如果 A 是假的,那么给我 B; 否则,我就选 A。 如果我们要自己实现这个操作,它可能看起来像这样:

function or(a, b) {
  if (a) return a;
  return b;
}

&&一样,这意味着||可以灵活使用,提供控制流或有条件地计算特定表达式:

const nameOfUser = user.getName() || user.getSurname() || "Unknown";

因此,应该谨慎使用它,考虑代码的读者熟悉什么,并考虑所有可能的操作数和操作的结果值。

比较运算符是一个二元运算符的集合,它总是返回从两个操作数的比较中派生出来的Boolean:

  • 抽象等式(a == b)
  • 抽象不等式(a != b)
  • 严格相等(a === b)
  • 严格不等式(a !== b)
  • 大于(a > b)
  • 大于或等于(a >= b)
  • 小于(a < b)
  • 小于或等于(a <= b)
  • 实例(a instanceof b)
  • In(a in b)

每一个操作符都有一些不同的功能和强制行为所以单独研究它们是很有用的。

抽象等式(==)和不等式(!=)操作符内部依赖于相同的算法,该算法负责确定两个值是否相等。 在本节中,我们的示例将只探讨==,但请放心,!=永远只是==的对立面。

In the vast majority of cases, it is not advisable to rely on abstract equality because its mechanism can create unexpected results. Most of the time, you'll want to opt for strict equality (that is, === or !==). 

如果两个操作数,左边和右边都是相同类型的,那么机制就非常简单——操作符将检查两个操作数是否为相同的值:

100 == 100;     // => true
null == null;   // => true
'abc' == 'abc'; // => true
123n == 123n;   // => true

When both operands are of the same type, abstract equality (==) is exactly identical to strict equality (===).

由于 JavaScript 中的所有非基元类型都是相同的(Object),如果你试图比较两个非基元类型(两个对象),而它们并不引用完全相同的对象,那么抽象相等(==)将总是返回false:

[123] == [123]; // => false
/123/ == /123/; // => false
({}) == ({});   // => false

然而,如果两个操作数是不同类型的,例如,比较Number类型和String类型或Object类型和Boolean类型时,抽象相等的确切行为将取决于操作数本身。

如果其中一个操作数为Number,另一个操作数为String,则a == b操作等价于:

Number(a) === Number(b)

以下是一些例子:

123 == '123';  // => true
'123' == 123;  // => true
'1e3' == 1000; // => true

Note how, as discussed in the Conversion to a number section in the last chapter, the "1e3" string will be internally converted to the number 1000.

继续往下看,如果==操作符只有一个操作数是Boolean,那么该操作又等同于Number(a) === Number(b):

false == ''; // => true
// Explanation: Number(false) is `0` and Number('') is `0`

true == '1'; // => true
// Explanation: Number(true) is `1` and Number('1') is `1`

true == 'hello'; // => false
// Explanation: Number(true) is `1` and Number('hello') is `NaN`

false == 'hello'; // => false
// Explanation: Number(false) is `0` and Number('hello') is `NaN`

最后,如果前面的条件不满足,并且其中一个操作数是Object(不是原语),那么它将把该对象的原语表示与另一个操作数进行比较。 正如上一章所讨论的,在Conversion to a primitive部分中,将尝试调用[Symbol.toPrimitive]()valueOf()toString()方法来建立原语。 我们可以在这里看到它的作用:

new Number(1) == 1; // => true
new Number().valueOf(); // => 1
({ valueOf() { return 555; }) == 555; // => true

由于其复杂的强制行为,最好避免使用抽象等式不等式。 任何读过充斥着这些操作符的代码的人都无法对程序的条件和控制流有很好的信心,因为存在太多的奇怪的边情况,抽象的等式可能会起作用。

If you find yourself wanting to use abstract equality, for example, when one operand is a number and another is a string, consider whether it might be clearer and less error-prone to use a combination of stricter checks or to explicitly cast your values for clarity; for example, instead of aNumber == aNumericString, you could do aNumber === Number(aNumericString).

JavaScript 中的严格相等(===)和严格相等(!==)操作符是干净代码的主要组成部分。 与抽象的等价类不同,它们在处理操作数时提供了确定性和简单性。

===操作符只有在两个操作数相同的情况下才返回true:

1 === 1;       // => true
null === null; // => true
'hi' === 'hi'; // => true

唯一的例外是当其中一个操作数是NaN时,它将返回false:

NaN === NaN; // => false

严格相等的情况下不会发生内部强制,所以即使你有两个原语,例如,它们可以被强制为相同的数字,它们仍然会被认为是不相等的:

'123' === 123; // => false

在非原语的情况下,两个操作数必须指向完全相同的对象:

const me = { name: 'James' };
me === me; // => true
me !== me; // => false

即使对象具有相同的结构或共享其他特征,如果它不是对相同对象的引用,它将返回false。 我们可以通过比较一个值为3的包装的Number实例和数字文字3来说明这一点:

new Number(3) === 3; // => false

在这种情况下,抽象相等运算符(==)的值将为真。 您可能认为将new Number(3)强制转换为3更好,但最好是显式地设置操作数,使它们在比较之前具有所需的类型。 因此,在String的例子中,我们希望将一个数值与Number进行比较,最好先通过Number()显式强制转换它:

Number('123') === 123; // => true

使用严格相等而不是抽象相等总是可取的。 它为每一个操作的结果提供了更多的确定性和可靠性,并允许您从抽象平等所带来的无数强制行为中解放您的思想。

大于(>),【T7 小于号】(<),【显示】大于或等于(>=),和小于或等于(<=)运算符都在类似的方式进行操作。 它们遵循一种类似于抽象相等的算法,尽管值的强制方式略有不同。

首先要注意的是,这些操作符的所有操作数将首先被强制转换为它们的原语表示。 接下来,如果它们的原始表示都是字符串,那么它们将按字典顺序进行比较。 如果它们的原始表示不都是字符串,那么它们将被强制转换为数字,然后进行比较。 这意味着即使只有一个操作数是字符串,它们也会进行数值比较。

当两个操作数都是字符串时,就会进行字典序比较,并且涉及到每个字符串的逐个字符比较。 一般来说,大于的字符串是那些在字典中出现较晚的字符串。 因此,banana在字典上要大于apple

正如我们在第 6 章Primitive and embedded Types中所发现的,JavaScript 使用 UTF-16 编码字符串,因此每个代码单元都是一个 16 位整数。 从65(U+0041)到122(U+007A)的 UTF-16 编码单位如下:

ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz

后面出现的字符用更大的 UTF-16 整数表示。 要比较任意两个给定的代码单元,JavaScript 只需比较它们的整数值。 在比较BA的情况下,可能是这样的:

const intA = 'A'.charCodeAt(0); // => 65
const intB = 'B'.charCodeAt(0); // => 66
intB > intA; // => true

每个操作数字符串中的每个字符都必须进行比较。 为了做到这一点,JavaScript 将逐个代码单元。 在每个字符串的每个索引处,如果代码单位不同,则较大的代码单位将被认为更大,因此该字符串将被认为比另一个字符串更大:

"AAA" > "AAB"
"AAB" > "AAC"

如果一个操作数等于另一个操作数的前缀,那么它总是被认为小于,如下所示:

'coff' < 'coffee'; // => true

正如您可能已经注意到的,小写英文字母比大写字母占用更高的 UTF-16 整数。 这意味着大写字母被认为比小写字母小,因此会以字典顺序出现在它之前:

'A' < 'a'; // => true
'Z' < 'z'; // => true
'Adam' < 'adam'; // => true

您还会注意到,从9196的代码单元包含了标点字符`[]^_``。 这也会影响我们的字典比较:

'[' < ']'; // => true
'_' < 'a'; // => true

Unicode 倾向于按照一种方式来安排,即任何给定语言的字符都将按字典顺序自然地排序,因此,一种语言字母表中的第一个符号用比后面的符号更低的 16 位整数表示。 例如,我们可以看到,泰国语中表示鸡的单词("ไก่")在字典上比表示蛋的单词("ไข่")小,因为在泰国字母表中,字符出现在之前:

'ไก่' < 'ไข่'; // => true ("chicken" comes before "egg")
'ก'.charCodeAt(0); // => 3585
'ข'.charCodeAt(0); // => 3586

Unicode 的自然顺序可能并不总是产生合理的字典顺序。 正如我们在前一章学到的,复杂符号可以通过将多个代码单元组合成字符对、代理对(创建码点)甚至字素簇来表示。 这可能会造成各种各样的困难。 一个例子将是以下情况给定的符号,在这种情况下,与弯曲的大写拉丁字母 a,可以表示通过唯一的 Unicode 码点U+00C2或通过结合大写字母"A"与【显示】(U+0041)结合 T 字符 ACCEN(U+0302)。 在象征和语义上,它们是相同的:**

'Â'; // => Â
'A\u0302'; // => Â

然而,由于U+00C2(decimal:194)在技术上大于U+0041(decimal:65),因此在词典的比较中,它会被认为大于,即使它们在符号和语义上是相同的:

'Â' > 'A\u0302'; // => true

有成千上万的潜在差异需要注意,因此,如果您发现自己需要按字典顺序进行比较,请注意 JavaScript 的大于小于操作符将受到 Unicode 固有顺序的限制。

使用 JavaScript 的 greater-than 和 less 操作符进行数值比较相当直观。 如前所述,操作数将首先被强制为其原始表示形式,然后再次显式地强制为数字。 对于两个操作数都是数字的情况,结果是完全直观的:

123 < 456; // => true

对于NaNInfinity,可以得出以下结论:

Infinity > 123; // => true
Infinity >= Infinity; // => true
Infinity > Infinity; // => false

NaN >= NaN; // => false
NaN > 3; // => false
NaN < 3; // => false

如果一个操作数的原始表示不是Number,那么在比较之前,它将被强制为Number。 如果你不小心通过Array``>作为操作数,那么它首先会迫使其原始表示,对数组,String所有个人强制元素与一个逗号,然后试图强迫【5】:

// Therefore this:
[123] < 456;

// Is equivalent to this:
Number(String([123])) < 456

由于可能发生复杂的强制操作,最好将相同类型的操作数传递给><>=<=

JavaScript 中的instanceof操作符允许你检测一个对象是否是构造函数的实例:

const component = new Component();
component instanceof Component; 

该操作将沿着其左侧操作数的[[Prototype]]链查找特定的constructor函数。 然后它将检查这个构造函数是否等于右边的操作数。

由于它爬上了[[Prototype]]链,它可以安全地使用多个继承:

class Super {}
class Child extends Super {}

new Super() instanceof Super; // => true
new Child() instanceof Child; // => true
new Child() instanceof Super; // => true

如果右侧操作数不是函数(也就是说,不能作为构造函数调用),那么TypeError将被抛出:

1 instanceof {}; // => TypeError: Right-hand side of 'instanceof' is not callable

instanceof操作符有时在识别本机类型(例如对象是否为数组)时很有用:

[1, 2, 3] instanceof Array; // => true

然而,这种用法已经被Array.isArray()所取代,Array.isArray()通常更值得信任,因为它将在少数情况下正常工作,当Array已经从另一个本机上下文(如浏览器中的框架)传递给你。

如果在一个对象中可以找到一个属性,in操作符将返回true:

'foo' in { foo: 123 }; // => true

左侧操作数将被强制为其原始表示,如果不是Symbol,则将被强制为String。 在这里,我们可以看到左侧操作数Array将如何被强制为以逗号分隔的内容序列化(由于Array.prototype.toString,数组被强制为原语的本机和默认方式):

const object = {
  'Array,coerced,into,String': 123
};

['Array', 'coerced', 'into', 'String'] in object; // => true

所有看似数值属性名称在 JavaScript 中被存储为字符串,所以访问someArray[0]=someArray["0"],因此询问是否一个对象有一个数值属性与in还将考虑同样0"0":

'0' in [1]; // => true
0 in { '0': 'foo' }; // => true

当确定一个属性是否在给定对象中时,in操作符将遍历整个[[Prototype]]链,因此在链的所有级别返回true所有可访问的方法和属性:

'map' in [];     // => true
'forEach' in []; // => true
'concat' in [];  // => true

这意味着如果你想区分的概念有一个财产属性本身,您应该使用hasOwnProperty,一个方法继承自Object.prototype这只会检查对象本身:

['wow'].hasOwnProperty('map'); // => false
['wow'].hasOwnProperty(0);     // => true
['wow'].hasOwnProperty('0');   // => true

总的来说,如果你确信你期望使用的属性名和对象的[[Prototype]]链提供的属性没有冲突,最好只使用in。 即使您只是使用普通对象,您仍然需要担心本机原型。 如果以任何方式(例如通过实用程序库)对其进行了修改,那么您对in操作的结果将不再具有高度的信任,因此应该使用hasOwnProperty

在较旧的库代码中,您甚至可能发现一些代码选择不依赖于hasOwnProperty查询对象,担心它可能已被覆盖。 相反,它会选择直接使用Object.prototype.hasOwnProperty方法,并将该对象作为其执行上下文来调用它:

function cautiousHasOwnProperty(object, property) {
  return Object.prototype.hasOwnProperty.call(object, property);
}

不过,这可能过于谨慎了。 在大多数代码库和环境中,信任和使用继承的hasOwnProperty是足够安全的。 如果你已经考虑到风险,in操作器通常是足够安全的。

赋值操作符将右操作数的值赋给左操作数,并返回新赋值的值。 赋值操作的左侧操作数必须始终是可赋值且有效的标识符或属性。 这方面的例子包括:

value = 1;
value.property = 1;
value['property'] = 1;

另外可以使用*destructuring 任务,*使你要申报的左侧操作数作为一个 object-literal-like 或者数组类结构,指定您希望指定标识符和您希望被分配的值:

[name, hobby] = ['Pikachu', 'Eating Ketchup'];
name;  // => "Pikachu"
hobby: // => "Eating Ketchup"

我们将进一步探讨解构赋值。 现在,重要的是要知道,它与常规标识符(foo=...)和属性访问器(foo.baz = ...foo[baz] = ...)一起,可以用作赋值操作符的左操作数。

在技术上有大量的赋值操作符,因为 JavaScript 将常规操作符与基本赋值操作符结合在一起,在通常需要更改现有变量或属性所引用的值的情况下,可以创建更简洁的赋值操作。 JavaScript 中的赋值操作符如下:

  • 直接分配:=
  • :+=
  • :-=
  • 乘法分配:*=
  • :/=
  • :%=
  • 位左移赋值:<<=
  • 位右移赋值:>>=
  • Bitwise unsigned right-shift 赋值
  • 按位赋值:&=
  • 按位异或赋值:^=
  • :|=

除了直接赋值=操作符外,所有赋值操作符都将执行=前面的操作符所指示的操作。 因此,在+=的情况下,+操作符将应用于左右操作数,其结果将被赋值给左边操作数。 所以,考虑下面的陈述:

value += 5

它将等价于:

value = value + 5

对于所有其他组合类型的赋值操作符,情况也是如此。 如我们所知,如果任意一个操作符是字符串,则加法操作符将连接其操作数。 如果指数操作数为 0(2 ** 0 === 1),则求指数运算符(**)的值始终为1。 我们可以依靠这个和其他现有的知识来了解这些操作符在与赋值结合时是如何工作的。 因此,我们不需要单独研究所有这些赋值运算符变量。

赋值通常发生在单行上下文中。 通常会看到一个赋值语句以分号结束:

someValue = someOtherValue;

但是赋值运算符中没有隐含的要求。 事实上,您可以将赋值嵌入语言中任何表达式的任何位置。 下面的语法是完全合法的,例如:

processStep(nextValue += currentValue);

这是执行加法和赋值,然后将结果值传递给processStep函数。 它完全等价于以下代码:

nextValue += currentValue;
processStep(nextValue);

注意这里是如何将nextValue传递给processStep的。 赋值运算表达式的结果总是被赋值的值:

let a;
(a = 1); // => 1
(a += 2); // => 3
(a *= 2); // => 6

forwhile循环的上下文中看到赋值是很常见的:

for (let i = 0, l = arr.length; i < l; i += 1) { }
//       \___/  \____________/         \____/
//         |          |                  |
//    Assignment  Assignment       Additive Assignment

这种分配模式和其他分配模式完全没问题,因为它们被广泛使用,已经成为 JavaScript 的惯用方法。 但在大多数其他情况下,最好不要在其他语言构造中嵌入赋值。 对于某些人来说,像fn(a += b)这样的代码可能不是很直观,因为它可能不清楚实际传递给fn()的值是什么。

In regard to clean code, the only question we need to ask ourselves when assigning values is whether the reader of our code (including us!) will find it obvious that assignment is occurring and whether they'll understand what is being assigned.

这四个操作符在技术上属于分配的范围,但它们足够独特,可以保证它们自己的部分:

  • 后缀自增操作符(value++)
  • 后缀自减运算符(value--)
  • 前缀自增运算符(++value)
  • 前缀自减运算符(--value)

这些将简单地增加或减少值1。 它们通常出现在forwhile循环等迭代上下文中。 它们被认为是加法和减法赋值(即value += 1value -= 1)的简洁替代品。 然而,它们也有一些独特的特点值得我们关注。

前缀自增和自减操作符允许你对任何给定值进行自增或自减操作,并计算出新增加的值:

let n = 0;

++n; // => 1 (the newly incremented value)
n;   // => 1 (the newly incremented value)

--n; // => 0 (the newly decremented value)
n;   // => 0 (the newly decremented value)

++n在技术上相当于以下附加赋值:

n += Number(n);

注意如何将当前的n转换为Number。 这是自增操作符和自减操作符的本质:它们严格地对数字进行操作。 因此,如果nString,则不能成功强制,则n的新值增加或减少为NaN:

let n = 'foo';
++n; // => NaN
n;   // => NaN

在这里,我们可以观察到,由于将foo强制转换为Number失败了,所以尝试对它进行增量也失败了,返回NaN

自增和自减操作符的后缀变量与前缀变量是相同的,除了一个事实:后缀变量将被计算为旧值,而不是新增加/减少的值:

let n = 0;

n++; // => 0 (the old value)
n;   // => 1 (the newly incremented value)

n--; // => 1 (the old value)
n;   // => 0 (the newly decremented value)

这是至关重要的,如果不是有意使用,可能会导致不希望出现的错误。 自增和自减操作符通常用于与这种差异无关的上下文中。 例如,当在for (_;_;_)语句的最后一个表达式中使用时,返回值在任何地方都没有使用,所以我们可以看到以下两种方法之间没有区别:

for (let i = 0; i < array.length; i++) { ...}
for (let i = 0; i < array.length; ++i) { ...}

然而,在其他情况下,评估价值绝对是关键。 例如,在下面的while循环中,每次迭代都会计算++i < array.length表达式,这意味着新增加的值将与array.length进行比较。 如果我们将其交换为i++ < array.length,那么您将在加 1 之前比较该值,这意味着它将减少 1,因此我们将获得额外的(不必要的!)迭代。 你可以在这里观察到区别:

const array = ['a', 'b', 'c'];

let i = -1;
while (++i < array.length) { console.log(i); } Logs: 0, 1, 2

let x = -1;
while (x++ < array.length) { console.log(x); } // Logs: 0, 1, 2, 3

这是相当罕见的情况,特别是在该语言中使用了更现代的迭代技术。 但是自增和自减操作符在其他上下文中仍然非常流行,因此了解它们的前缀和后缀变体之间的区别是很有用的。

如上所述,赋值操作符(... =)的左操作数可以指定为解构对象或数组模式,如下所示:

let position = { x: 123, y: 456 };
let { x, y } = position;
x; // => 123
y; // => 456

这些模式通常看起来像ObjectArray字面量,因为它们分别以{}[]开始和结束。 然而,它们略有不同。

在解构对象模式中,当您希望声明希望赋值的标识符或属性时,必须将其作为对象字面量中的值来放置。 是,{ foo: bar }通常意味着分配barfoo,在【显示】destructuring 模式,这意味着分配的价值foo【病人标识符】,bar。 这是逆转。 如果你想要访问的属性的名称与你想要在本地范围内赋值的名称匹配,你可以使用更短的语法{ foo },如下所示:

let message = { body: 'Dear Customer...' };

// Accessing `body` and assigning to a different name (`theBody`): 
const { body: theBody } = message;
theBody; // => "Dear Customer..."

// Accessing `body` and assigning to the same name (`body`):
const { body } = message;
body; // => "Dear Customer..."

对于数组,通常用来指定值(即,[here, here, and here])的语法槽被用来指定要赋值的标识符,因此序列中的每个标识符都与数组中相同的索引元素相关:

let [a, b, c] = [1, 2, 3];
a; // => 1
b; // => 2
c; // => 3

您还可以使用 rest 操作符(...foo)来指示 JavaScript 将属性的rest分配给给定的标识符。 下面是一个在解构数组模式中使用它的例子:

let [a, b, c, ...others] = [1, 2, 3, 4, 5, 6, 7];
others; // => [4, 5, 6, 7];

下面是一个在解构对象模式中使用它的例子:

let { name, ...otherThings } = {
 name: 'James', hobby: 'JS', location: 'Europe'
};
name; // => "James"
otherThings; // => { hobby: "JS", location: "Europe" }

Only destructure your assignments when it provides genuine increased readability and simplicity.

对于包含多层层次的对象结构,也可以进行解构:

let city = {
  suburb: {
    inhabitants: ['alice', 'steve', 'claire']
  }
};

如果我们希望提取inhabitants数组并将其赋值给同名的变量,那么我们可以做以下操作:

let { suburb: { inhabitants } } = city;
inhabitants; // => ["alice", ...]

析构阵列图案可以嵌入析构对象图案,反之亦然:

let {
  suburb: {
    inhabitants: [firstInhabitant, ...otherInhabitants]
  }
} = city;

firstInhabitant; // => "alice"
otherInhabitants: // => ["steve", "claire"]

析构赋值(Destructuring 赋值)

let firstInhabitant = city.suburb.inhabitants[0];

然而,它应该保留使用,因为对于那些必须阅读您的代码的人来说,它有时会使事情过于复杂。 当它第一次写的时候可能看起来很直观,解构赋值是出了名的难以理清。 考虑一下下面的陈述:

const [{someProperty:{someOtherProperty:[{foo:baz}]}}] = X;

理清这一问题的认知成本很高。 也许,用传统的方式来表达这种逻辑会更直观:

const baz = X[0].someProperty.someOtherProperty[0].foo;

总的来说,析构赋值是 JavaScript 语言的一个令人兴奋和有用的特性,但应该谨慎使用,考虑到它可能造成的混淆。

在 JavaScript 中访问属性可以使用以下两种操作符之一:

  • 直接属性访问:obj.property
  • :obj[property]

直接访问属性的语法是一个句点字符,左边的操作数是你想要访问的对象,右边的操作数是你想要访问的属性名。**

const street = {
  name: 'Marshal St.'
};

street.name; // => "Marshal St."

右侧操作数必须是有效的 JavaScript 标识符,因此,不能以数字开头,不能包含空格,而且通常不能包含 JavaScript 规范中其他地方存在的任何标点字符。 但是,您可以使用所谓的奇异 Unicode 字符来命名属性,例如π(PI):

const myMathConstants = { π: Math.PI };
myMathConstants.π; // => 3.14...

这是一种非常规的做法,通常只在新颖的场合使用。 然而,它可能在嵌入问题领域的代码中真正有用,这些问题领域中存在具有现有含义的合法奇异符号(数学物理,等等)。

在不能通过直接属性访问直接访问属性的情况下,可以计算想要访问的属性名,用方括号分隔:

someObject["somePropertyName"]

这是一个*right-si*任何表达式的操作数,这意味着你可以自由地计算一些值,然后被强迫一个字符串(如果尚未字符串)和用作属性名来访问对象:

someObject[ computeSomethingHere() ]

通常,这用于访问包含字符的属性名,这些字符使它们成为无效标识符,因此与直接属性访问操作符一起使用是非法的。 这将包括数字属性名(比如在数组中找到的那些),带有空格的名称,或语言中其他地方存在的带有标点符号或关键字的名称:

object[1];
object['a property name with whitespace'];
object['{[property.name.with.odd.punctuation]}'];

在没有其他选择的情况下,最好只依赖计算属性访问。 如果存在直接访问属性的可能性(即object.property),那么您应该选择它。 同样地,如果您正在决定一个对象可能包含哪些属性,最好使用语言中有效标识符的名称,以便能够轻松地直接访问它们。

还有一些剩余的操作符和语法我们还没有探索,它们不属于任何其他操作符类别:

  • delete operator:delete VALUE
  • :void VALUE
  • 新操作符:new VALUE
  • :... VALUE
  • :(VALUE)
  • 逗号运算符:VALUE, VALUE, ...

delete操作符可用于从对象中删除属性,因为它的唯一操作数通常采用属性访问器的形式,如下所示:

delete object.property;
delete object[property];

只有认为可配置的属性才能以这种方式删除。 默认情况下,所有添加的属性都是可配置的,因此可以删除:

const foo = { baz: 123; };

foo.baz;        // => 123
delete foo.baz; // => true
foo.baz;        // => undefined
'baz' in foo;   // => undefined

然而,如果属性是通过defineProperty添加的,configurable设置为false,那么它将不能被删除:

const foo = {};
Object.defineProperty(foo, 'baz', {
  value: 123,
  configurable: false
});

foo.baz; // => 123
delete foo.baz; // => false
foo.baz; // => 123
'baz' in foo; // => true

如您所见,delete操作符计算结果为truefalse,这取决于属性是否已成功删除。 删除成功后,不仅将属性设置为undefinednull,而且将从对象中完全删除,以便通过in检查其是否存在将返回false

delete操作符在技术上可用于删除任何变量(或所谓的环境记录绑定内部),但尝试这样做被认为是一种不推荐使用的行为,并将在严格模式下产生SyntaxError:

'use strict';
let foo = 1;
delete foo; // ! SyntaxError

delete操作符在历史上一直是 JavaScript 实现之间不一致的问题,尤其是在不同的浏览器之间。 因此,建议只使用常规方法删除对象上的属性。

不管操作数是多少,void运算符都将求值为undefined。 它的操作数可以是任何有效的引用或表达式:

void 1; // => undefined
void null; // => undefined
void [1, 2, 3]; // => undefined

现在它没有很多用途,尽管void 0有时被用作undefined的习惯用法,要么是为了简洁,要么是为了避免在undefined是一个不可信的可变值的遗留环境中出现问题。

new操作符用于从构造函数形成一个实例。 它的右侧操作数必须是一个有效的构造函数,可以由语言(例如new String())提供,也可以由我们自己提供:

function Thing() {} 
new Thing(); // => Instance of Thing

通过,我们真正的意思是一个对象,有一个[[Prototype]]等于构造函数的prototype属性,并且一直传递到构造函数为this绑定,以便构造函数可以完全以达到其目的。 请注意,无论我们是通过类定义还是常规语法定义构造函数,都可以对生成的实例做出相同的断言:

// Conventional Constructor Definition:
function Example1() {
  this.value = 123;
}

Example1.prototype.constructor === Example1; // => true
Object.getPrototypeOf(new Example1()) === Example1.prototype; // => true
new Example1().value === 123; // => true

// Class Definition:
class Example2 {
  constructor() { this.value = 123; }
}

Example2.prototype.constructor === Example2; // => true
Object.getPrototypeOf(new Example2()) === Example2.prototype; // => true
new Example2().value === 123; // => true

new操作符只关心它的右边操作数是可构造的。 这意味着它不能是由箭头函数构成的函数,如下面这个例子:

const Thing = () => {};
new Thing(); // ! TypeError: Thing is not a constructor

只要使用函数表达式或声明定义了构造函数,它就可以正常工作。 如果你愿意,你甚至可以实例化一个匿名内联构造函数:

const thing = new (function() {
  this.name = 'Anonymous';
});

thing.name; // => "Anonymous"

new操作符在形式上并不需要调用括号。 只有当你向构造函数传递参数时,它们才需要被包含:

// Both equivalent:
new Thing;
new Thing();

然而,当你希望实例化一些东西,然后立即访问一个属性或方法时,你需要通过提供调用括号和,然后访问属性来消除歧义; 否则,您将收到TypeError:

function Component() {
  this.width = 200;
  this.height = 200;
}

new Component().width; // => 200
new Component.width; // => ! TypeError: Component.width is not a constructor
(new Component).width; // => 200

new操作符的用法通常非常简单。 从语义上讲,它被理解为与实例的构造相关,因此在理想情况下应该只用于实现此目的。 因此,还假定new右侧操作数引用的任何内容都以大写字母开头的名称标识,并且是名词。 这些命名约定表明它是一个构造函数,为希望使用它的程序员提供了有用的提示。 下面是一些好的和坏的构造函数名的例子:

// Bad (non-idiomatic) names for Constructors:
new dropdownComponent;
new the_dropdown_component;
new componentDropdown;
new CreateDropdownComponent;

// Good (idiomatic) names for Constructors:
new Dropdown;
new DropdownComponent;

构造函数的正确命名至关重要。 它使我们的程序员同伴立即意识到特定抽象的契约实现了什么。 如果我们将构造函数命名为看起来像普通函数,那么我们的同事可能会尝试不正确地调用它,从而导致可能的错误。 因此,它利用一个名字的完美意义交流的能力合同,正如前面所讨论的章节命名(第五章,命名事物很难)。

扩展语法(也称为rest 语法)由三个点和一个操作数表达式(...expression)组成。 它允许在需要多个参数或多个数组元素的地方展开表达式。 从技术上讲,它存在于语言的五个不同领域:

  • 数组字面值中,形式为array = [a, b, c, ...otherArray]
  • 对象字面值中,形式为object = {a, b, c, ...otherObject}
  • 中函数参数列表为,形式为function(a, b,  c, ...otherArguments) {}
  • destructuring array patterns中,形式[a,  b, c, ...others] = array
  • ,在{a, b, c, ,,,otherProps} = object的形式

上下文中的函数参数列表,传播语法必须最后一个参数,表明你希望所有参数传递给函数从那时起收集到一个单一的数组的名称表示:

function addPersonWithHobbies(name, ...hobbies) {
  name; // => "Kirk"
  hobbies; // => ["Collecting Antiques", "Playing Chess", "Drinking"]
}

addPersonWithHobbies(
 'Kirk',
 'Collecting Antiques',
 'Playing Chess',
 'Drinking'
);

如果您试图在其他参数中使用它,那么您将收到SyntaxError:

function doThings(a, ...things, c, d, e) {}
// ! SyntaxError: Rest parameter must be last formal parameter

数组文字析构数组模式的上下文中,扩展语法同样用于表示所引用的值应该展开。 我们最好将其视为两个对立面,解构重建:

// Deconstruction:
let [a, b, c, ...otherLetters] = ['a', 'b', 'c', 'd', 'e', 'f'];
a; // => "a"
b; // => "b"
c; // => "c"
otherLetters; // => ["d", "e", "f"]

// Reconstruction:
let reconstructedArray = [a, b, c, ...otherLetters];
reconstructedArray; // => ["a", "b", "c", "d", "e", "f"]

当在数组文字析构数组模式上下文中使用时,扩展语法必须指向可迭代值。 这并不一定是一个数组。 例如,字符串是可迭代的,所以下面的方法也适用:

let [...characters] = 'Hello';
characters; // => ["H", "e", "l", "l", "o"]

对象上下文中的文字 d或*estructuring 对象模式,语法传播同样是用来传播任何给定对象的所有属性为接收对象。 再一次,我们可以将其视为解构重建的过程:*

// Deconstruction:
const {name, ...attributes} = {
  name: 'Nissan Skyline',
  engineSize: '2500cc',
  year: 2009
};
name; // => "Nissan Skyline"
attributes; // => { engineSize: "2500cc", year: 2009 }

// Reconstruction:
const skyline = {name, ...attributes};
skyline; // => { name: "Nissan Skyline", engineSize: "2500cc", year: 2009 }

在此上下文中使用时,扩展语法的右侧值必须是一个对象或可以包装为对象的原语(例如,NumberString)。 这意味着 JavaScript 中除了nullundefined以外的所有值都是允许的,我们知道,这两个值都不能被包装成对象:

let {...stuff} = null; // => TypeError

因此,当您确信该值是一个对象时,最好只在对象上下文中使用spread 语法

总之,正如我们所看到的,扩展语法在各种不同的情况下都非常有用。 它的主要优点是减少了提取和指定值所需的语法量。

逗号操作符(a, b)接受一个左右操作数,并且总是计算其右操作数。 它有时不被认为是操作符,因为它在技术上并不操作其操作数。 它也很罕见。

逗号操作符不应混淆我们使用逗号来分隔参数,当宣布或调用一个函数(例如fn(a,b,c)),逗号使用数组在创建文本和对象文字(例如[a, b, c]),或逗号时使用声明变量(例如let a, b, c;)。 逗号运算符与所有这些都不同。

最常见的是在for(;;)循环的迭代语句部分:

for (let i = 0; i < length; i++, x++, y++) {
  // ...
}

请注意,在第三条语句中(在常规的for(;;)语句的每次迭代结束时)出现了三个递增操作,并且它们之间都用逗号分隔。 在这个上下文中,使用逗号仅仅是为了确保在单个语句的上下文中,所有这些单独的操作都将发生,而彼此无关。 在常规的for(;;)语句之外的代码中,你可能只会有这些单独的行和语句,就像这样:

i++;
x++;
y++;

然而,由于for(;;)语法的限制,它们必须全部存在于一个单数语句中,因此逗号操作符是必要的。

在这个上下文中,逗号操作符计算为其右侧操作数的事实并不重要,但在其他上下文中,它可能很重要:

const processThings = () => (firstThing(), secondThing());

这里,当调用processThings时,将首先调用firstThing,然后调用secondThing,并返回secondThing返回的任何内容。 因此,它相当于以下内容:

const processThings = () => {
  firstThing();
  return secondThing();
};

即使在这样的场景中,也很少看到使用逗号操作符,因为它可能会不必要地模糊本可以更清楚表达的代码。 知道它的存在和它的行为是有用的,但我们不应该期望它是一个日常操作符。

分组或圆括号,使用正则括号((...))来实现。 不应该将其与其他使用括号的语法片段相混淆,例如函数调用(fn(...))。

分组括号可以被视为一个操作符,就像我们已经学习过的所有其他操作符一样。 它们接受一个操作数(任何形式的表达式),并将求值为其中的任何值:

(1);             // => 1
([1, 2, 3]);     // => [1, 2, 3]
(false && true); // => false
((1 + 2) * 3);   // => 9
(()=>{});        // => (A function)

由于它只是评估其内容,您可能想知道组的目的是什么。 前面,我们讨论了运算符优先级和结合性的概念。 有时,如果你在使用一系列操作符,并希望强制执行特定的操作顺序,那么唯一的方法是将它们包装在一个组中,当与其他操作符组合使用时,具有最高的优先级:

// The order of operations is dictated
// by each operator's precedence:
1 + 2 * 3 - 5; 

// Here, we are forcing the order:
(1 + 2) * (3 - 5);

当操作的顺序不是您想要的,或者代码的读者可能不清楚时,使用组是明智的。 例如,有时很常见的做法是将函数返回的项包装在一个组中,以提供美观的包容性和清晰度:

function getComponentWidth(component) {
  return (
    component.getInnerWidth() +
    component.getLeftPadding() +
    component.getRightPadding()
  );
}

另一个明显的解决方案可能是仅仅缩进你想要包含的项,但这个问题是 JavaScriptreturn语句不知道在它自己的行之外寻找它必须返回的表达式或值的开始:

// WARNING: this won't work
return
  component.getInnerWidth() +
  component.getLeftPadding() +
  component.getRightPadding();

当解析器发现在同一行中没有值或表达式时,前面代码中的return语句实际上以分号结束。 这被称为自动分号插入(ASI),它的存在意味着我们经常需要使用组来让解析器清楚地知道我们的意图:

// Clear to humans; clear to the parser:
return (
  component.getInnerWidth() +
  component.getLeftPadding() +
  component.getRightPadding()
);

总而言之,分组对于包含和重新排序操作来说是一个有用的工具,而且它是一种提高表达式清晰度和可读性的廉价而简单的方法。

JavaScript 有 7 个按位运算符。 术语按位在这里表示对二进制数进行操作。 这些操作符很少被使用,但了解这些操作符还是很有用的:

  • Bitwise unsigned 右移操作符:>>>
  • 位左移操作符:<<
  • 位右移操作符:>>
  • 按位或:|
  • :&
  • :^
  • Bitwise NOT:~(一元操作符)

Bitwise operations are incredibly rare in JavaScript since you're usually dealing with higher-level sequences of bits like strings or numbers. However, it's worth having at least a cursory understanding of bitwise operations so that if you encounter the need, you can cope.

JavaScript 中的所有按位运算符都将首先将其操作数(或单个操作数,在按位 NOT~的情况下)强制为 32 位整数表示。 这意味着,在内部,像250这样的数字将表现如下:

00000000 00000000 00000000 11111010

最后八位,在这个例子中是250,包含了关于这个数字的所有信息:

1 1 1 1 1 0 1 0
+ + + + + + + +
| | | | | | | +---> 0 * 001 = 000
| | | | | | +-----> 1 * 002 = 002
| | | | | +-------> 0 * 004 = 000
| | | | +---------> 1 * 008 = 008
| | | +-----------> 1 * 016 = 016 
| | +-------------> 1 * 032 = 032
| +---------------> 1 * 064 = 064
+-----------------> 1 * 128 = 128
=================================
                        SUM = 250

把所有的位加在一起将得到一个十进制整数250

每个可用的按位运算符将对这些位进行操作并派生一个新值。 例如,一个按位的 AND 操作将为上同时为的每一对位产生一个1位值:

const a = 250;  // 11111010
const b = 20;   // 00010100
a & b; // => 16 // 00010000

我们可以看到,在25020中,从右数第五位(即16)是的值,因此 and 操作将只保留该位。

位运算符只能在执行二进制运算时使用。 除此之外,应该避免使用按位运算符(例如,用于副作用),因为它极大地限制了代码的清晰性和可理解性。

在一段时间内,JavaScript 中使用的按位运算符(如~|)并不少见,因为它们在简洁地推导整数底(如~34.6789 === 34)时很流行。 不用说,这种方法虽然很聪明、很自我,但却会产生难以读懂和不熟悉的代码。 使用更明确的技术仍然是可取的。 对于地板,使用Math.floor()是理想的。

在本章中,我们详细介绍了 JavaScript 中可用的操作符。 总的来说,过去的三章让我们对 JavaScript 语法有了非常深刻的基础理解,使我们在构造表达式时感到非常舒服。

在下一章中,我们将继续通过将我们现有的类型和操作符知识应用到声明和控制流的场景中来探索该语言。 我们将探讨如何使用更大的语言结构来编写干净的代码,并讨论这些结构中存在的许多陷阱和特性。***

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

深入拆解Tomcat & Jetty -〔李号双〕

玩转webpack -〔程柳锋〕

网络编程实战 -〔盛延敏〕

JavaScript核心原理解析 -〔周爱民〕

Linux内核技术实战课 -〔邵亚方〕

Go 并发编程实战课 -〔晁岳攀(鸟窝)〕

超级访谈:对话毕玄 -〔毕玄〕

大型Android系统重构实战 -〔黄俊彬〕

结构沟通力 -〔李忠秋〕