大话 JavaScript(Speaking JavaScript):第六章到第十章

2024-01-12 09:15:14 浏览数 (1)

第六章:历史 JavaScript 里程碑

原文:6. Historical JavaScript Milestones 译者:飞龙 协议:CC BY-NC-SA 4.0

JavaScript 花了很长时间才产生影响。许多与 JavaScript 相关的技术存在了一段时间,直到它们被主流发现。本节描述了从 JavaScript 的创建到今天发生的事情。在整个过程中,只提到了最受欢迎的项目,而忽略了许多项目,即使它们是第一个。例如,列出了 Dojo Toolkit,但也有较少人知道的qooxdoo,它是在同一时间创建的。还列出了 Node.js,尽管Jaxer在它之前就存在:

1997 年—动态 HTML

动态 HTML 允许您动态更改网页的内容和外观。您可以通过操作页面的文档对象模型(DOM)来实现这一点,这是一种树状数据结构。您可以做的事情包括更改内容,更改样式,显示和隐藏元素。动态 HTML 首次出现在 Internet Explorer 4 和 Netscape Navigator 4 中。

1999 年—XMLHttpRequest

此 API 允许客户端脚本向服务器发送 HTTP 或 HTTPS 请求并返回数据,通常以文本格式(XML,HTML,JSON)返回。它是在 Internet Explorer 5 中引入的。

2001 年—JSON,基于 JavaScript 的数据交换格式

2001 年,道格拉斯·克罗克福德命名并记录了 JSON(JavaScript 对象表示法),其主要思想是使用 JavaScript 语法以文本格式存储数据。 JSON 使用 JavaScript 文字来表示对象,数组,字符串,数字和布尔值以表示结构化数据。例如:

代码语言:javascript复制
{
    "first": "Jane",
    "last": "Porter",
    "married": true,
    "born": 1890,
    "friends": [ "Tarzan", "Cheeta" ]
}

多年来,JSON 已成为 XML 的受欢迎的轻量级替代品,特别是在需要表示结构化数据而不是标记时。自然地,JSON 易于通过 JavaScript 消耗(参见第二十二章)。

2004 年—Dojo Toolkit,用于大规模编程 JavaScript 的框架

Dojo Toolkit 通过提供必要的基础设施来促进大规模编程:继承库,模块系统,用于桌面式图形小部件的 API 等。

2005 年—Ajax,基于浏览器的桌面级应用程序

Ajax 是一组技术,为网页带来了与桌面应用程序相媲美的交互水平。一个令人印象深刻的例子是 2005 年 2 月推出的 Google 地图。该应用程序允许您在世界地图上平移和缩放,但只有当前可见的内容才会下载到浏览器中。在 Google 地图推出后,杰西·詹姆斯·加勒特注意到它与其他交互式网站共享某些特征。他将这些特征称为Ajax,这是异步 JavaScript 和 XML的简称。Ajax 的两个基石是在后台异步加载内容(通过XMLHttpRequest)并动态更新当前页面的结果(通过动态 HTML)。这是一个相当大的可用性改进,可以避免始终执行完整的页面重新加载。

Ajax 标志着 JavaScript 和动态 Web 应用程序的主流突破。有趣的是要注意这花了多长时间——在那时,Ajax 的成分已经可用多年。自 Ajax 诞生以来,其他数据格式变得流行(JSON 取代 XML),使用其他协议(例如,除 HTTP 外还使用 Web Sockets),并且双向通信是可能的。但基本技术仍然是相同的。然而,这个术语Ajax如今使用得少得多,大多数情况下已经被更全面的术语HTML5Web 平台(两者都意味着 JavaScript 加上浏览器 API)所取代。

2005 年—Apache CouchDB,一个以 JavaScript 为中心的数据库

大致上,CouchDB 是一个 JSON 数据库:您可以向其提供 JSON 对象,无需事先指定模式。此外,您可以通过执行 map/reduce 操作的 JavaScript 函数定义视图和索引。因此,CouchDB 非常适合 JavaScript,因为您可以直接使用本机数据。与关系数据库相比,没有映射相关的阻抗不匹配。与对象数据库相比,您避免了许多复杂性,因为只存储数据,而不是行为。CouchDB 只是几个类似的NoSQL 数据库中的一个。它们中的大多数都具有出色的 JavaScript 支持。

2006 年—jQuery,帮助 DOM 操作

浏览器 DOM 是客户端 Web 开发中最痛苦的部分之一。jQuery 通过在浏览器差异上进行抽象和提供强大的流畅式 API 来使 DOM 操作变得有趣,从而使 DOM 操作变得有趣。

2007 年—WebKit,将移动 Web 推向主流

基于 KDE 的先前工作,WebKit 是苹果于 2003 年推出的 HTML 引擎。它于 2005 年开源。随着 iPhone 于 2007 年的推出,移动 Web 突然变得主流,并与非移动 Web 相比几乎没有限制。

2008 年—V8,证明 JavaScript 可以很快

当谷歌推出其 Chrome 网络浏览器时,其中一个亮点是一个名为 V8 的快速 JavaScript 引擎。它改变了 JavaScript 速度慢的看法,并引发了与其他浏览器供应商的速度竞赛,我们至今仍在受益。V8 是开源的,可以在需要快速嵌入式语言时作为独立组件使用。

2009 年—Node.js,在服务器上实现 JavaScript

Node.js 允许您实现在负载下表现良好的服务器。为此,它使用事件驱动的非阻塞 I/O 和 JavaScript(通过 V8)。Node.js 的创始人 Ryan Dahl 提到了选择 JavaScript 的以下原因:

  • “因为它是裸露的,没有 I/O API。”[因此,Node.js 可以引入自己的非阻塞 API。]
  • “Web 开发人员已经在使用它。”[JavaScript 是一种广为人知的语言,特别是在 Web 环境中。]
  • “DOM API 是基于事件的。每个人都已经习惯了在没有线程和事件循环上运行。”[开发人员习惯于异步编码风格。]

Dahl 能够在事件驱动服务器和服务器端 JavaScript(主要是CommonJS项目)的先前工作基础上构建。

Node.js 对 JavaScript 程序员的吸引力不仅在于能够使用熟悉的语言进行编程;您可以在客户端和服务器上使用相同的语言。这意味着您可以共享更多的代码(例如,用于验证数据)并使用诸如同构 JavaScript之类的技术。同构 JavaScript 是关于在客户端或服务器上组装网页,具有许多好处:可以在服务器上呈现页面以实现更快的初始显示、SEO 以及在不支持 JavaScript 或版本过旧的浏览器上运行。但它们也可以在客户端上更新,从而实现更具响应性的用户界面。

2009 年—PhoneGap,使用 HTML5 编写本机应用程序

PhoneGap 是由一家名为 Nitobi 的公司创建的,后来被 Adobe 收购。PhoneGap 的开源基础称为 Cordova。PhoneGap 最初的使命是通过 HTML5 实现原生移动应用程序。自那时起,支持已扩展到非移动操作系统。目前支持的平台包括 Android,Bada,BlackBerry,Firefox OS,iOS,Mac OS X,Tizen,Ubuntu,Windows(桌面)和 Windows Phone。除了 HTML5 API 之外,还有专门用于访问加速计,相机和联系人等本机功能的 PhoneGap 特定 API。

2009 年—Chrome OS,使浏览器成为操作系统

对于 Chrome OS 来说,Web 平台就是本机平台。这种方法有几个优点:

  • 创建操作系统要容易得多,因为所有用户界面技术都已经存在。
  • 许多开发人员已经(大部分)知道如何为操作系统编写应用程序。
  • 管理应用程序很简单。这有助于公共场所的安装,如网吧和学校。

移动操作系统webOS的推出(起源于 Palm,现在由 LG Electronics 拥有)早于 Chrome OS 的推出,但后者更明显地体现了“浏览器作为操作系统”的理念(这就是为什么它被选为里程碑的原因)。webOS 既少,因为它非常专注于手机和平板电脑。又多,因为它内置了 Node.js,可以让您用 JavaScript 实现服务。在 Web 操作系统类别中,最近的一个新进是 Mozilla 的Firefox OS,它针对手机和平板电脑。 Mozilla 的维基提到 Web 操作系统对 Web 的好处:

我们还需要一个目标,以便确定和专注我们的努力。最近,我们看到了 pdf.js 项目[通过 HTML5 渲染 PDF,无需插件]暴露出一些需要填补的小差距,以便使“HTML5”成为 PDF 的超集。现在,我们希望迈出更大的一步,找到阻止 Web 开发人员能够构建与 iPhone,Android 和 WP7 为等同的本机应用程序的差距。

2011 年—Windows 8,一流的 HTML5 应用程序

当微软推出 Windows 8 时,它让所有人都感到惊讶,因为该操作系统广泛集成了 HTML5。在 Windows 8 中,HTML5 应用程序与通过现有技术(如.NET 和 C )实现的应用程序平等。为了证明这一点,微软用 HTML5(加上对本机 API 的调用)编写了几个重要的 Windows 8 应用程序,包括应用商店和电子邮件应用程序。


⁵ Ajax 是一个缩写词,但不是一个首字母缩写,这就是为什么它没有被写成 AJAX。

第三部分:深入 JavaScript

原文:III. JavaScript in Depth 译者:飞龙 协议:CC BY-NC-SA 4.0

这部分是 JavaScript 语言的全面参考。

第七章:JavaScript 的语法

原文:7. JavaScript’s Syntax 译者:飞龙 协议:CC BY-NC-SA 4.0

JavaScript 的语法相当简单。本章描述了需要注意的事项。

语法概述

本节让你快速了解 JavaScript 的语法是什么样子的。

以下是五种基本类型的值:

布尔值:

代码语言:javascript复制
true
false

数字:

代码语言:javascript复制
1023
7.851

字符串:

代码语言:javascript复制
'hello'
"hello"

普通对象:

代码语言:javascript复制
{
    firstName: 'Jane',
    lastName: 'Doe'
}

数组:

代码语言:javascript复制
[ 'apple', 'banana', 'cherry' ]

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

代码语言:javascript复制
// Two slashes start single-linecomments

var x;  // declaring a variable

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

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

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

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

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

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

注释

有两种注释:

通过//进行单行注释,延伸到行的其余部分。这是一个例子:

代码语言:javascript复制
var a = 0; // init

通过/* */进行多行注释,可以延伸到任意范围的文本。它们不能嵌套。以下是两个例子:

代码语言:javascript复制
/* temporarily disabled
processNext(queue);
*/

function (a /* int */, b /* str */) {
}

表达式与语句

本节讨论了 JavaScript 中一个重要的语法区别:表达式和语句之间的区别。

表达式

表达式产生一个值,并且可以在期望值的任何地方编写,例如,在函数调用的参数中或赋值的右侧。以下每一行都包含一个表达式:

代码语言:javascript复制
myvar
3   x
myfunc('a', 'b')
语句

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

无论在哪里,JavaScript 都期望一个语句,你也可以写一个表达式。这样的语句称为表达式语句。反之则不成立:你不能在 JavaScript 期望表达式的地方写一个语句。例如,if语句不能成为函数的参数。

条件语句与条件表达式

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

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

代码语言:javascript复制
var salutation;
if (male) {
    salutation = 'Mr.';
} else {
    salutation = 'Mrs.';
}

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

代码语言:javascript复制
var salutation = (male ? 'Mr.' : 'Mrs.');

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

使用模棱两可的表达式作为语句

两种表达式看起来像语句——它们在语法类别上是模棱两可的:

对象文字(表达式)看起来像块(语句):

代码语言:javascript复制
{
    foo: bar(3, 5)
}

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

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

代码语言:javascript复制
function foo() { }

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

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

  • 花括号
  • 关键字function

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

通过 eval()评估对象文字

eval在语句上下文中解析其参数。如果你想要eval返回一个对象,你必须在对象文字周围放括号:

代码语言:javascript复制
> eval('{ foo: 123 }')
123
> eval('({ foo: 123 })')
{ foo: 123 }
立即调用函数表达式

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

代码语言:javascript复制
> (function () { return 'abc' }())
'abc'

如果省略括号,您将得到语法错误,因为 JavaScript 看到一个函数声明,它不能是匿名的:

代码语言:javascript复制
> function () { return 'abc' }()
SyntaxError: function statement requires a name

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

代码语言:javascript复制
> function foo() { return 'abc' }()
SyntaxError: Unexpected token )

函数声明后面必须是一个合法的语句,而()不是。

控制流语句和块

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

代码语言:javascript复制
if (obj !== null) obj.foo();

while (x > 0) x--;

然而,任何语句都可以被替换,即包含零个或多个语句的花括号。因此,您也可以这样写:

代码语言:javascript复制
if (obj !== null) {
    obj.foo();
}

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

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

使用分号的规则

在本节中,我们将讨论 JavaScript 中分号的使用。基本规则是:

  • 通常,语句以分号终止。
  • 例外是以块结束的语句。

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

语句结束于块之后没有分号

如果以块结束,以下语句不会以分号终止:

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

以下是whiledo-while的示例:

代码语言:javascript复制
while (a > 0) {
    a--;
} // no semicolon

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

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

代码语言:javascript复制
function foo() {
    // ...
} // no semicolon

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

如果在块后添加分号,您不会得到语法错误,因为它被视为一个空语句(请参阅下一节)。

提示

这就是您需要了解的关于分号的大部分内容。如果您始终添加分号,您可能可以不阅读本节其余部分。

空语句

分号本身是一个“空语句”,什么也不做。空语句可以出现在需要语句的任何地方。它们在需要语句但不需要语句的情况下很有用。在这种情况下,通常也允许块。例如,以下两个语句是等价的:

代码语言:javascript复制
while (processNextItem() > 0);
while (processNextItem() > 0) {}

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

代码语言:javascript复制
;;;
自动分号插入

自动分号插入(ASI)的目标是使分号在行末变为可选。术语“自动分号插入”所引发的图像是 JavaScript 解析器为您插入分号(在内部,通常处理方式不同)。

换句话说,ASI 帮助解析器确定语句何时结束。通常,它以分号结束。ASI 规定,如果:

  • 行终止符(例如换行符)后面跟着一个非法标记。
  • 遇到右括号。
  • 已到达文件末尾。
示例:通过非法标记进行 ASI

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

代码语言:javascript复制
if (a < 0) a = 0
console.log(a)

0之后的console标记是非法的,并触发 ASI:

代码语言:javascript复制
if (a < 0) a = 0;
console.log(a);
示例:通过右括号进行 ASI

在以下代码中,大括号内的语句未以分号终止:

代码语言:javascript复制
function add(a,b) { return a b }

ASI 创建了前述代码的语法上正确的版本:

代码语言:javascript复制
function add(a,b) { return a b; }
陷阱:ASI 可能会意外地中断语句

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

代码语言:javascript复制
// Don't do this
return
{
    name: 'John'
};

ASI 将前面的转换为:

代码语言:javascript复制
return;
{
    name: 'John'
};

这是一个空的返回,紧接着是一个带有标签name的块,位于表达式语句'John'之前。在块之后,有一个空语句。

陷阱:ASI 可能意外地不会被触发

有时,新行中的语句以允许作为前一语句的延续的标记开头。然后,尽管看起来应该被触发,但 ASI 不会被触发。例如:

代码语言:javascript复制
func()
[ 'ul', 'ol' ].foreach(function (t) { handleTag(t) })

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

代码语言:javascript复制
func()['ol'].foreach(function (t) { handleTag(t) });

合法标识符

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

标识符的第一个字符是:

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

后续字符是:

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

合法标识符的示例:

代码语言:javascript复制
var ε = 0.0001;
var строка = '';
var _tmp;
var $foo2;

尽管这使您可以在 JavaScript 代码中使用各种人类语言,但我建议使用英语,无论是标识符还是注释。这可以确保您的代码可以被尽可能多的人理解,这很重要,考虑到如今代码可以在国际间传播。

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

arguments

break

case

catch

class

const

continue

debugger

default

delete

do

else

enum

export

extends

false

finally

for

function

if

implements

import

in

instanceof

interface

let

new

null

package

private

protected

public

return

static

super

switch

this

throw

true

try

typeof

var

void

while

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

| Infinity | | NaN | | undefined |

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

请注意,您可以使用保留字作为未引用的属性键(自 ECMAScript 5 起):

代码语言:javascript复制
> var obj = { function: 'abc' };
> obj.function
'abc'

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

在数字文字上调用方法

在方法调用中,重要的是要区分浮点数点和方法调用点。因此,您不能写成1.toString();您必须使用以下替代之一:

代码语言:javascript复制
1..toString()
1 .toString()  // space before dot
(1).toString()
1.0.toString()

严格模式

ECMAScript 5 有一个严格模式,可以使 JavaScript 更清晰,减少不安全的特性,增加警告,以及更合乎逻辑的行为。正常(非严格)模式有时被称为“松散模式”。

打开严格模式

您可以通过在 JavaScript 文件中或在<script>元素内首先输入以下行来打开严格模式:

代码语言:javascript复制
'use strict';

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

您还可以按函数打开严格模式。要这样做,请像这样编写您的函数:

代码语言:javascript复制
function foo() {
    'use strict';
    ...
}

当您使用严格模式处理到处都可能破坏事物的旧代码库时,这很方便。

严格模式:推荐,但有注意事项

总的来说,严格模式启用的更改都是为了更好。因此,强烈建议您在编写新代码时使用它——只需在文件开头打开它。然而,有两个注意事项:

为现有代码启用严格模式可能会破坏它

代码可能依赖于不再可用的功能,或者可能依赖于在松散模式和严格模式中行为不同的行为。不要忘记您可以将单个严格模式函数添加到处于松散模式的文件中的选项。

小心处理包

当您连接和/或缩小文件时,您必须小心,严格模式在应该打开时没有关闭,或者反之亦然。两者都可能破坏代码。

以下部分详细解释了严格模式的特性。通常情况下,您不需要了解它们,因为您大多数情况下会因为您本不应该做的事情而得到更多的警告。

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

在严格模式下,所有变量必须明确声明。这有助于防止拼写错误。在松散模式下,对未声明的变量进行赋值会创建一个全局变量:

代码语言:javascript复制
function sloppyFunc() {
    sloppyVar = 123;
}
sloppyFunc();  // creates global variable `sloppyVar`
console.log(sloppyVar);  // 123

在严格模式下,对未声明的变量进行赋值会引发异常:

代码语言:javascript复制
function strictFunc() {
    'use strict';
    strictVar = 123;
}
strictFunc();  // ReferenceError: strictVar is not defined
严格模式中的函数

严格模式限制了与函数相关的特性。

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

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

代码语言:javascript复制
function strictFunc() {
    'use strict';
    {
        // SyntaxError:
        function nested() {
        }
    }
}

这是无用的,因为该函数是在周围函数的范围内创建的,而不是“在”块内部。

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

代码语言:javascript复制
function strictFunc() {
    'use strict';
    {
        // OK:
        var nested = function () {
        };
    }
}
函数参数的更严格规则

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

arguments 对象的属性更少

在严格模式下,arguments对象更简单:属性arguments.calleearguments.caller已被删除,您不能对变量arguments进行赋值,arguments不会跟踪参数的更改(如果参数更改,相应的数组元素不会随之更改)。arguments 的弃用特性解释了详细信息。

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

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

代码语言:javascript复制
function sloppyFunc() {
    console.log(this === window);  // true
}

在严格模式下,它是undefined

代码语言:javascript复制
function strictFunc() {
    'use strict';
    console.log(this === undefined);  // true
}

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

代码语言:javascript复制
function Point(x, y) {
    'use strict';
    this.x = x;
    this.y = y;
}

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

代码语言:javascript复制
> var pt = Point(3, 1);
TypeError: Cannot set property 'x' of undefined

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

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

在严格模式下,非法的属性操作会抛出异常。例如,试图设置只读属性的值会抛出异常,试图删除不可配置属性也会抛出异常。以下是一个例子:

代码语言:javascript复制
var str = 'abc';
function sloppyFunc() {
    str.length = 7;  // no effect, silent failure
    console.log(str.length);  // 3
}
function strictFunc() {
    'use strict';
    str.length = 7; // TypeError: Cannot assign to
                    // read-only property 'length'
}
在严格模式下,不能删除未经限定的标识符

在松散模式下,你可以像这样删除全局变量foo

代码语言:javascript复制
delete foo

在严格模式下,当你尝试删除未经限定的标识符时,你会得到一个语法错误。你仍然可以像这样删除全局变量:

代码语言:javascript复制
delete window.foo;  // browsers
delete global.foo;  // Node.js
delete this.foo;    // everywhere (in global scope)
在严格模式下,eval()更加干净

在严格模式下,eval()函数变得不那么古怪了:在评估的字符串中声明的变量不再添加到eval()周围的作用域中。详情请参阅Evaluating Code Using eval()。

在严格模式下被禁止的特性

在严格模式下,还有两个 JavaScript 特性是被禁止的:

不再允许使用with语句(参见The with Statement)。在编译时(加载代码时)会得到语法错误。

不再有八进制数:在松散模式下,以零开头的整数被解释为八进制(基数 8)。例如:

代码语言:javascript复制
> 010 === 8
true

在严格模式下,如果你使用这种文字类型,你会得到一个语法错误:

代码语言:javascript复制
    > function f() { 'use strict'; return 010 }
    SyntaxError: Octal literals are not allowed in strict mode.
    ```

* * *

⁶ 为了简化问题,我假装声明是语句。


## 第八章:值

> 原文:[8. Values](https://exploringjs.com/es5/ch08.html)
> 
> 译者:[飞龙](https://github.com/wizardforcel)
> 
> 协议:[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)


JavaScript 拥有我们所期望的大多数编程语言的值:布尔值、数字、字符串、数组等。JavaScript 中的所有正常值都有*属性*。⁷ 每个属性都有一个*键*(或*名称*)和一个*值*。你可以把属性看作记录的字段。你可以使用点(`.`)运算符来访问属性:

```js
> var obj = {}; // create an empty object
> obj.foo = 123;  // write property
123
> obj.foo  // read property
123
> 'abc'.toUpperCase()  // call method
'ABC'

JavaScript 的类型系统

本章概述了 JavaScript 的类型系统。

JavaScript 的类型

根据 ECMAScript 语言规范的第八章,JavaScript 只有六种类型:

ECMAScript 语言类型对应于由 ECMAScript 程序员直接使用 ECMAScript 语言进行操作的值。ECMAScript 语言类型包括:

  • 未定义、空值
  • 布尔值、字符串、数字和
  • 对象

因此,构造函数在技术上并没有引入新的类型,尽管它们被认为有实例。

静态与动态

在语言语义和类型系统的背景下,“静态”通常意味着“在编译时”或“在不运行程序时”,而“动态”意味着“在运行时”。

静态类型检查与动态类型检查

在静态类型语言中,变量、参数和对象的成员(JavaScript 称之为属性)在编译时就已经知道类型。编译器可以使用这些信息进行类型检查和优化编译后的代码。

即使在静态类型语言中,变量也有动态类型,即运行时变量值的类型。动态类型可以与静态类型不同。例如(Java):

代码语言:javascript复制
Object foo = "abc";

foo的静态类型是Object;它的动态类型是String

JavaScript 是动态类型的;变量的类型通常在编译时不知道。

静态类型检查与动态类型检查

如果你有类型信息,你可以检查在操作中使用的值(调用函数、应用运算符等)是否具有正确的类型。在静态类型检查的语言中,这种检查是在编译时进行的,而在动态类型检查的语言中是在运行时进行的。一种语言可以同时进行静态类型检查和动态类型检查。如果检查失败,通常会得到某种错误或异常。

JavaScript 执行一种非常有限的动态类型检查:

代码语言:javascript复制
> var foo = null;
> foo.prop
TypeError: Cannot read property 'prop' of null

然而,大多数情况下,事情会悄悄地失败或者成功。例如,如果你访问一个不存在的属性,你会得到值undefined

代码语言:javascript复制
> var bar = {};
> bar.prop
undefined
强制转换

在 JavaScript 中,处理类型不匹配的值的主要方法是将其强制转换为正确的类型。强制转换意味着隐式类型转换。大多数操作数都会强制转换:

代码语言:javascript复制
> '3' * '4'
12

JavaScript 的内置转换机制仅支持BooleanNumberStringObject类型。没有标准的方法将一个构造函数的实例转换为另一个构造函数的实例。

警告

术语强类型弱类型没有普遍有意义的定义。它们被使用,但通常是不正确的。最好使用静态类型静态类型检查等。

原始值与对象

JavaScript 在值之间做了一个相当任意的区分:

  • 原始值是布尔值,数字,字符串,nullundefined
  • 所有其他值都是对象

两者之间的主要区别在于它们的比较方式;每个对象都有唯一的身份,只有(严格)等于自己:

代码语言:javascript复制
> var obj1 = {};  // an empty object
> var obj2 = {};  // another empty object
> obj1 === obj2
false

> var obj3 = obj1;
> obj3 === obj1
true

相反,编码相同值的所有原始值被认为是相同的:

代码语言:javascript复制
> var prim1 = 123;
> var prim2 = 123;
> prim1 === prim2
true

以下两节详细解释了原始值和对象。

原始值

以下是所有的原始值(简称原始值):

  • 布尔值:truefalse(参见[第十章](ch10.html “第十章。布尔值”))
  • 数字:17361.351(参见[第十一章](ch11.html “第十一章。数字”))
  • 字符串:'abc'"abc"(参见[第十二章](ch12.html “第十二章。字符串”))
  • 两个“非值”:undefinednull(参见[undefined 和 null](ch08.html#undefined_null “undefined 和 null”))

原始值具有以下特征:

按值比较

比较“内容”:

代码语言:javascript复制
> 3 === 3
true
> 'abc' === 'abc'
true

始终不可变

属性不能被更改,添加或删除:

代码语言:javascript复制
> var str = 'abc';

> str.length = 1; // try to change property `length`
> str.length      // ⇒ no effect
3

> str.foo = 3; // try to create property `foo`
> str.foo      // ⇒ no effect, unknown property
undefined

(读取未知属性总是返回undefined。)

一组固定的类型

您不能定义自己的原始类型。

对象

所有非原始值都是对象。最常见的对象类型是:

普通对象(构造函数Object)可以通过对象字面量(参见[第十七章](ch17_split_000.html “第十七章。对象和继承”))创建:

代码语言:javascript复制
{
    firstName: 'Jane',
    lastName: 'Doe'
}

前面的对象有两个属性:属性firstName的值为'Jane',属性lastName的值为'Doe'

数组(构造函数Array)可以通过数组字面量(参见[第十八章](ch18.html “第十八章。数组”))创建:

代码语言:javascript复制
[ 'apple', 'banana', 'cherry' ]

前面的数组有三个元素,可以通过数字索引访问。例如,'apple’的索引是 0。

正则表达式(构造函数RegExp)可以通过正则表达式字面量(参见[第十九章](ch19.html “第十九章。正则表达式”))创建:

代码语言:javascript复制
/^a b $/

对象具有以下特征:

按引用比较

比较身份;每个对象都有自己的身份:

代码语言:javascript复制
> {} === {}  // two different empty objects
false

> var obj1 = {};
> var obj2 = obj1;
> obj1 === obj2
true

默认可变

通常可以自由更改,添加和删除属性(参见[点运算符(.):通过固定键访问属性](ch17_split_000.html#dot_operator “点运算符(.):通过固定键访问属性”)):

代码语言:javascript复制
> var obj = {};
> obj.foo = 123; // add property `foo`
> obj.foo
123

用户可扩展

构造函数(参见[第 3 层:构造函数-实例的工厂](ch17_split_001.html#constructors “第 3 层:构造函数-实例的工厂”)可以看作是自定义类型的实现(类似于其他语言中的类)。

undefined 和 null

JavaScript 有两个“非值”,表示缺少信息,undefinednull

  • undefined表示“没有值”(既不是原始值也不是对象)。未初始化的变量,缺少的参数和缺少的属性都具有该非值。如果没有明确返回任何内容,函数会隐式返回它。
  • null表示“没有对象”。它用作一个非值,期望一个对象(作为参数,在对象链中的成员等)。

undefinednull是唯一的值,任何属性访问都会导致异常:

代码语言:javascript复制
> function returnFoo(x) { return x.foo }

> returnFoo(true)
undefined
> returnFoo(0)
undefined

> returnFoo(null)
TypeError: Cannot read property 'foo' of null
> returnFoo(undefined)
TypeError: Cannot read property 'foo' of undefined

undefined有时也被用作指示不存在的元值。相比之下,null表示空。例如,JSON 节点访问者(请参阅通过节点访问者转换数据)返回:

  • undefined用于删除对象属性或数组元素
  • 将属性或元素设置为null
未定义和 null 的出现

在这里,我们回顾了undefinednull出现的各种情况。

未定义的出现

未初始化的变量是undefined

代码语言:javascript复制
> var foo;
> foo
undefined

缺少参数是undefined

代码语言:javascript复制
> function f(x) { return x }
> f()
undefined

如果读取不存在的属性,则会得到undefined

代码语言:javascript复制
> var obj = {}; // empty object
> obj.foo
undefined

如果没有明确返回,函数会隐式返回undefined

代码语言:javascript复制
> function f() {}
> f()
undefined

> function g() { return; }
> g()
undefined
null 的出现

null是原型链中的最后一个元素(一系列对象的链;请参阅第 2 层:对象之间的原型关系):

代码语言:javascript复制
> Object.getPrototypeOf(Object.prototype)
null

如果字符串中的正则表达式没有匹配项,则RegExp.prototype.exec()将返回null

代码语言:javascript复制
> /x/.exec('aaa')
null
检查未定义或 null

在接下来的几节中,我们将回顾如何分别检查undefinednull,或者检查它们是否存在。

检查 null

您可以通过严格相等来检查null

代码语言:javascript复制
if (x === null) ...
检查未定义

严格相等(===)是检查undefined的规范方式:

代码语言:javascript复制
if (x === undefined) ...

您还可以通过typeof运算符(typeof:对基元进行分类)来检查undefined,但通常应使用前面提到的方法。

检查未定义或 null

大多数函数允许您通过undefinednull指示缺少值。检查它们两者之一的一种方法是通过显式比较:

代码语言:javascript复制
// Does x have a value?
if (x !== undefined && x !== null) {
    ...
}
// Is x a non-value?
if (x === undefined || x === null) {
    ...
}

另一种方法是利用undefinednull都被视为false的事实(请参阅真值和假值):

代码语言:javascript复制
// Does x have a value (is it truthy)?
if (x) {
    ...
}
// Is x falsy?
if (!x) {
    ...
}
警告

false0NaN''也被视为false

未定义和 null 的历史

单个非值可以扮演undefinednull的角色。为什么 JavaScript 有两个这样的值?原因是历史性的。

JavaScript 采用了 Java 对值进行分区为基元和对象的方法。它还使用了 Java 的“不是对象”的值,null。遵循 C(但不是 Java)所设定的先例,如果强制转换为数字,则null变为 0:

代码语言:javascript复制
> Number(null)
0
> 5   null
5

请记住,JavaScript 的第一个版本没有异常处理。因此,未初始化的变量和丢失的属性等特殊情况必须通过一个值来指示。null本来是一个不错的选择,但当时 Brendan Eich 想要避免两件事:

  • 该值不应该具有引用的含义,因为它不仅仅是关于对象。
  • 该值不应该强制转换为 0,因为这样会使错误更难以发现。

因此,Eich 将undefined作为语言中的另一个非值。它强制转换为NaN

代码语言:javascript复制
> Number(undefined)
NaN
> 5   undefined
NaN
更改未定义

undefined是全局对象的一个属性(因此是全局变量;请参阅全局对象)。在 ECMAScript 3 中,读取undefined时必须采取预防措施,因为很容易意外更改其值。在 ECMAScript 5 中,这是不必要的,因为undefined是只读的。

为了防止更改undefined,有两种流行的技术(它们对于旧的 JavaScript 引擎仍然相关):

技术 1

屏蔽全局undefined(可能具有错误的值):

代码语言:javascript复制
(function (undefined) {
    if (x === undefined) ...  // safe now
}());  // don’t hand in a parameter

在上述代码中,undefined保证具有正确的值,因为它是一个参数,其值未由函数调用提供。

技术 2

与始终(正确的)undefined相比,void 0(请参阅void 运算符):

代码语言:javascript复制
if (x === void 0)  // always safe

基元的包装对象

布尔值、数字和字符串这三种原始类型都有对应的构造函数:BooleanNumberString。它们的实例(称为包装对象)包含(包装)原始值。这些构造函数可以以两种方式使用:

作为构造函数,它们创建的对象与它们包装的原始值大部分不兼容:

代码语言:javascript复制
> typeof new String('abc')
'object'
> new String('abc') === 'abc'
false

作为函数,它们将值转换为相应的原始类型(见[转换为布尔值、数字、字符串和对象的函数](ch08.html#convert_to_primitive “转换为布尔值、数字、字符串和对象的函数”))。这是推荐的转换方法:

代码语言:javascript复制
> String(123)
'123'
提示

最好的做法是避免使用包装对象。通常情况下,您不需要它们,因为对象不能做的事情原始值都可以做(除了被改变)。(这与 Java 不同,JavaScript 从中继承了原始值和对象之间的差异!)

包装对象与原始值不同

诸如'abc'之类的原始值与诸如new String('abc')之类的包装实例在根本上是不同的:

代码语言:javascript复制
> typeof 'abc'  // a primitive value
'string'
> typeof new String('abc')  // an object
'object'
> 'abc' instanceof String  // never true for primitives
false
> 'abc' === new String('abc')
false

包装实例是对象,JavaScript 中没有办法比较对象,甚至不能通过宽松相等==进行比较(见[相等运算符:=与](ch09.html#equality_operators “相等运算符:=与”)):

代码语言:javascript复制
> var a = new String('abc');
> var b = new String('abc');
> a == b
false
包装和解包原始值

包装对象的一个用例是:您想要向原始值添加属性。然后您包装原始值并向包装对象添加属性。在使用之前,您需要解包该值。

通过调用包装构造函数来包装原始值:

代码语言:javascript复制
new Boolean(true)
new Number(123)
new String('abc')

通过调用方法valueOf()来解包原始值。所有对象都有这个方法(如[转换为原始值](ch17_split_001.html#Object.prototype.valueOf “转换为原始值”)中所讨论的):

代码语言:javascript复制
> new Boolean(true).valueOf()
true
> new Number(123).valueOf()
123
> new String('abc').valueOf()
'abc'

将包装对象转换为原始值可以正确提取数字和字符串,但不能提取布尔值:

代码语言:javascript复制
> Boolean(new Boolean(false))  // does not unwrap
true
> Number(new Number(123))  // unwraps
123
> String(new String('abc'))  // unwraps
'abc'

这是在[转换为布尔值](ch10.html#toboolean “转换为布尔值”)中解释的原因。

原始值从包装对象中借用它们的方法

原始值没有自己的方法,而是从包装对象中借用它们:

代码语言:javascript复制
> 'abc'.charAt === String.prototype.charAt
true

松散模式和严格模式以不同的方式处理这种借用。在松散模式下,原始值会即时转换为包装对象:

代码语言:javascript复制
String.prototype.sloppyMethod = function () {
    console.log(typeof this); // object
    console.log(this instanceof String); // true
};
''.sloppyMethod(); // call the above method

在严格模式下,会透明地使用包装原型中的方法:

代码语言:javascript复制
String.prototype.strictMethod = function () {
    'use strict';
    console.log(typeof this); // string
    console.log(this instanceof String); // false
};
''.strictMethod(); // call the above method

类型强制

类型强制意味着将一个类型的值隐式转换为另一个类型的值。JavaScript 的大多数运算符、函数和方法都会将操作数和参数强制转换为它们需要的类型。例如,乘法运算符(*)的操作数会被强制转换为数字:

代码语言:javascript复制
> '3' * '4'
12

另一个例子,如果操作数之一是字符串,加号运算符( )会将另一个操作数转换为字符串:

代码语言:javascript复制
> 3   ' times'
'3 times'
类型强制可以隐藏错误

因此,JavaScript 很少抱怨值的类型错误。例如,程序通常会将用户输入(来自在线表单或 GUI 小部件)作为字符串接收,即使用户输入的是一个数字。如果您将一个数字作为字符串处理,您将不会收到警告,只会得到意外的结果。例如:

代码语言:javascript复制
var formData = { width: '100' };

// You think formData.width is a number
// and get unexpected results
var w = formData.width;
var outer = w   20;

// You expect outer to be 120, but it’s not
console.log(outer === 120);  // false
console.log(outer === '10020');  // true

在诸如前面的情况下,您应该尽早将其转换为适当的类型:

代码语言:javascript复制
var w = Number(formData.width);
转换为布尔值、数字、字符串和对象的函数

以下函数是将值转换为布尔值、数字、字符串或对象的首选方法:

Boolean()(见[转换为布尔值](ch10.html#toboolean “转换为布尔值”))

将一个值转换为布尔值。以下值被转换为false;它们被称为“假值”:

  • undefinednull
  • false
  • 0NaN
  • ''

所有其他值都被视为“真值”,并转换为true(包括所有对象!)。

Number()(见[转换为数字](ch11.html#tonumber “转换为数字”))

将一个值转换为数字:

  • undefined变为NaN
  • null变为0
  • false变为0true变为1
  • 字符串被解析。
  • 首先将对象转换为原始值(稍后讨论),然后将其转换为数字。

String()(参见转换为字符串)

将值转换为字符串。对于所有原始值,它都有明显的结果。例如:

代码语言:javascript复制
> String(null)
'null'
> String(123.45)
'123.45'
> String(false)
'false'

首先将对象转换为原始值(稍后讨论),然后将其转换为字符串。

Object()(参见将任何值转换为对象)

将对象转换为它们自己,将undefinednull转换为空对象,将原始值转换为包装的原始值。例如:

代码语言:javascript复制
> var obj = { foo: 123 };
> Object(obj) === obj
true

> Object(undefined)
{}
> Object('abc') instanceof String
true

请注意,Boolean()Number()String()Object()都被作为函数调用。你通常不会将它们用作构造函数。然后它们创建自己的实例(参见原始值的包装对象)。

算法:ToPrimitive()——将值转换为原始值

要将值转换为数字或字符串,首先将其转换为任意原始值,然后将其转换为最终类型(如用于转换为布尔值、数字、字符串和对象的函数中所讨论的)。

ECMAScript 规范有一个内部函数ToPrimitive()(无法从 JavaScript 中访问),它执行这种转换。了解ToPrimitive()使你能够配置对象如何转换为数字和字符串。它有以下签名:

代码语言:javascript复制
ToPrimitive(input, PreferredType?)

可选参数PreferredType指示转换的最终类型:它可以是NumberString,具体取决于ToPrimitive()的结果将被转换为数字还是字符串。

如果PreferredTypeNumber,则执行以下步骤:

  1. 如果input是原始的,就返回它(没有更多的事情要做了)。
  2. 否则,input是一个对象。调用input.valueOf()。如果结果是原始的,就返回它。
  3. 否则,调用input.toString()。如果结果是原始的,就返回它。
  4. 否则,抛出TypeError(表示无法将input转换为原始值)。

如果PreferredTypeString,则步骤 2 和 3 会交换。PreferredType也可以省略;然后它被认为是日期的String,而对于所有其他值,则被认为是Number。这就是运算符 ==调用ToPrimitive()的方式。

示例:ToPrimitive()的实际应用

valueOf()的默认实现返回this,而toString()的默认实现返回类型信息:

代码语言:javascript复制
> var empty = {};
> empty.valueOf() === empty
true
> empty.toString()
'[object Object]'

因此,Number()跳过valueOf(),将toString()的结果转换为数字;也就是说,它将'[object Object]'转换为NaN

代码语言:javascript复制
> Number({})
NaN

以下对象自定义了valueOf(),它影响Number(),但对于String()没有任何改变:

代码语言:javascript复制
> var n = { valueOf: function () { return 123 } };
> Number(n)
123
> String(n)
'[object Object]'

以下对象自定义了toString()。因为结果可以转换为数字,所以Number()可以返回一个数字:

代码语言:javascript复制
> var s = { toString: function () { return '7'; } };
> String(s)
'7'
> Number(s)
7

⁷ 从技术上讲,原始值没有自己的属性,它们从包装构造函数中借用。但这是在幕后进行的,所以你通常看不到它。

第九章 运算符

原文:9. Operators 译者:飞龙 协议:CC BY-NC-SA 4.0

本章概述了运算符。

运算符和对象

所有运算符都会强制转换(如类型强制转换中所讨论的)它们的操作数为适当的类型。大多数运算符只能处理原始值(例如,算术运算符和比较运算符)。这意味着在对它们进行任何操作之前,对象都会被转换为原始值。其中一个不幸的例子是加号运算符,许多语言用它来进行数组连接。然而,在 JavaScript 中并非如此,这个运算符会将数组转换为字符串并将它们连接起来:

代码语言:javascript复制
> [1, 2]   [3]
'1,23'
> String([1, 2])
'1,2'
> String([3])
'3'
注意

在 JavaScript 中没有办法重载或自定义运算符,甚至没有相等性。

赋值运算符

有几种使用普通赋值运算符的方法:

x = value

分配给先前声明的变量x

var x = value

将变量声明与赋值结合

obj.propKey = value

设置属性

obj['propKey'] = value

设置属性

arr[index] = value

设置数组元素⁸

赋值是一个求值为赋值的表达式。这允许您链接赋值。例如,以下语句将0分配给yx

代码语言:javascript复制
x = y = 0;
复合赋值运算符

复合赋值运算符写为op=,其中op是几个二进制运算符之一,=是赋值运算符。以下两个表达式是等价的:

代码语言:javascript复制
myvar op= value
myvar = myvar op value

换句话说,复合赋值运算符op=op应用于两个操作数,并将结果分配给第一个操作数。让我们看一个使用加法运算符( )的复合赋值的示例:

代码语言:javascript复制
> var x = 2;
> x  = 3
5
> x
5

以下都是复合赋值运算符:

  • 算术运算(参见算术运算符):*=, /=, %=, =, -=
  • 按位操作(参见二进制按位运算符):<<=, >>=, >>>=, &=, ^=, |=
  • 字符串连接(参见连接:加号( )运算符): =

相等运算符:=与

JavaScript 有两种确定两个值是否相等的方法:

  • 严格相等(===)和严格不等(!==)仅认为具有相同类型的值相等。
  • 正常(或“宽松”)相等(==)和不等(!=)在比较之前尝试转换不同类型的值,就像严格(不)相等一样。

宽松相等在两个方面存在问题。首先,它的转换方式令人困惑。其次,由于运算符如此宽容,类型错误可能会隐藏更长时间。

始终使用严格相等,避免宽松相等。只有在您想知道为什么应该避免它时,才需要了解后者。

相等是不可定制的。JavaScript 中的运算符不能被重载,也不能定制相等的工作方式。有一些操作,您经常需要影响比较——例如,Array.prototype.sort()(参见排序和反转元素(破坏性))。该方法可选择接受一个回调,该回调执行数组元素之间的所有比较。

严格相等(=, !)

具有不同类型的值永远不会严格相等。如果两个值具有相同的类型,则以下断言成立:

undefined === undefined

null === null

两个数字:

代码语言:javascript复制
x === x  // unless x is NaN
 0 === -0
NaN !== NaN  // read explanation that follows

两个布尔值,两个字符串:显而易见的结果

两个对象(包括数组和函数):x === y当且仅当xy是同一个对象时;也就是说,如果要比较不同的对象,您必须实现自己的比较算法:

代码语言:javascript复制
> var b = {}, c = {};
> b === c
false
> b === b
true

其他一切:不严格相等。

陷阱:NaN

特殊的数字值NaN(参见NaN)不等于自身:

代码语言:javascript复制
> NaN === NaN
false

因此,您需要使用其他方法来检查它,这些方法在陷阱:检查值是否为 NaN中有描述。

严格不等 (!==)

严格不等比较:

代码语言:javascript复制
x !== y

等同于严格相等比较的否定:

代码语言:javascript复制
!(x === y)
正常(宽松)相等(==, !=)

通过正常相等比较的算法工作如下。如果两个操作数具有相同的类型(六种规范类型之一——Undefined、Null、Boolean、Number、String 和 Object),则通过严格相等比较它们。

否则,如果操作数是:

  1. undefinednull,那么它们被认为是宽松相等的:
代码语言:javascript复制
    > undefined == null
    true
    ```

1.  一个字符串和一个数字,然后将字符串转换为数字,并通过严格相等比较两个操作数。

1.  一个布尔值和一个非布尔值,然后将布尔值转换为数字并进行宽松比较(再次)。

1.  一个对象和一个数字或字符串,然后尝试将对象转换为原始值(通过[算法:ToPrimitive()—将值转换为原始值](ch08.html#toprimitive "算法:ToPrimitive()—将值转换为原始值")中描述的算法)并进行宽松比较。

否则——如果上述任何情况都不适用——宽松比较的结果是`false`。

#### 宽松不等号(!=)

一个不等式比较:

```js
x != y

等同于等式比较的否定:

代码语言:javascript复制
!(x == y)
陷阱:宽松相等与转换为布尔值不同

第三步意味着相等和转换为布尔值(参见转换为布尔值)的工作方式不同。如果转换为布尔值,大于 1 的数字变为true(例如,在if语句中)。但这些数字并不宽松相等于true。注释解释了结果是如何计算的:

代码语言:javascript复制
> 2 == true  // 2 === 1
false
> 2 == false  // 2 === 0
false

> 1 == true  // 1 === 1
true
> 0 == false  // 0 === 0
true

同样,虽然空字符串等于false,但并非所有非空字符串都等于true

代码语言:javascript复制
> '' == false   // 0 === 0
true
> '1' == true  // 1 === 1
true
> '2' == true  // 2 === 1
false
> 'abc' == true  // NaN === 1
false
陷阱:宽松相等和字符串

一些宽松性可能是有用的,这取决于你的需求:

代码语言:javascript复制
> 'abc' == new String('abc')  // 'abc' == 'abc'
true
> '123' == 123  // 123 === 123
true

其他情况可能有问题,因为 JavaScript 如何将字符串转换为数字(参见转换为数字):

代码语言:javascript复制
> 'nt123r ' == 123  // usually not OK
true
> '' == 0  // 0 === 0
true
陷阱:宽松相等和对象

如果你将一个对象与一个非对象进行比较,它会被转换为原始值,这会导致奇怪的结果:

代码语言:javascript复制
> {} == '[object Object]'
true
> ['123'] == 123
true
> [] == 0
true

然而,只有两个对象是相等的,如果它们是同一个对象。这意味着你不能真正比较两个包装对象:

代码语言:javascript复制
> new Boolean(true) === new Boolean(true)
false
> new Number(123) === new Number(123)
false
> new String('abc') == new String('abc')
false
没有==的有效用例

有时你会读到关于宽松相等(==)的有效用例。本节列出了它们,并指出了更好的替代方案。

用例:检查 undefined 或 null

以下比较确保x既不是undefined也不是null

代码语言:javascript复制
if (x != null) ...

虽然这是一种简洁的写法,但它会让初学者感到困惑,而专家也无法确定它是否是打字错误。因此,如果你想检查x是否有值,请使用标准的真值检查(在真值和假值中介绍):

代码语言:javascript复制
if (x) ...

如果你想更精确,你应该对两个值进行显式检查:

代码语言:javascript复制
if (x !== undefined && x !== null) ...
用例:处理字符串中的数字

如果你不确定一个值x是一个数字还是一个数字字符串,你可以使用以下检查:

代码语言:javascript复制
if (x == 123) ...

前面的检查是为了确保x123'123'。同样,这是非常紧凑的,而且最好是明确的:

代码语言:javascript复制
if (Number(x) === 123) ...
用例:比较包装实例和原始值

宽松相等允许你比较原始值和包装原始值:

代码语言:javascript复制
> 'abc' == new String('abc')
true

有三个理由反对这种方法。首先,宽松相等在包装原始值之间不起作用:

代码语言:javascript复制
> new String('abc') == new String('abc')
false

其次,你应该无论如何避免使用包装器。第三,如果你使用它们,最好是明确的:

代码语言:javascript复制
if (wrapped.valueOf() === 'abc') ...

排序运算符

JavaScript 知道以下排序运算符:

  • 小于(<
  • 小于或等于(<=
  • 大于(>
  • 大于或等于(>=

这些运算符适用于数字和字符串:

代码语言:javascript复制
> 7 >= 5
true
> 'apple' < 'orange'
true

对于字符串来说,它们并不是非常有用,因为它们区分大小写,而且不能很好地处理重音等特性(有关详细信息,请参见比较字符串)。

算法

你评估一个比较:

代码语言:javascript复制
x < y

通过以下步骤:

  1. 确保两个操作数都是原始值。对象obj通过内部操作ToPrimitive(obj, Number)(参见算法:ToPrimitive()—将值转换为原始值)转换为原始值,该操作调用obj.valueOf()和可能的obj.toString()来实现。
  2. 如果两个操作数都是字符串,那么通过按字典顺序比较表示字符串的 JavaScript 字符的 16 位代码单元(参见第二十四章)来比较它们。
  3. 否则,将两个操作数转换为数字并进行数字比较。

其他排序运算符类似处理。

加号运算符( )

粗略地说,加号运算符检查它的操作数。 如果其中一个是字符串,则另一个也被转换为字符串,并且两者被连接:

代码语言:javascript复制
> 'foo'   3
'foo3'
> 3   'foo'
'3foo'

> 'Colors: '   [ 'red', 'green', 'blue' ]
'Colors: red,green,blue'

否则,两个操作数都转换为数字(参见转换为数字)并相加:

代码语言:javascript复制
> 3   1
4
> 3   true
4

这意味着评估的顺序很重要:

代码语言:javascript复制
> 'foo'   (1   2)
'foo3'
> ('foo'   1)   2
'foo12'
算法

你评估一个加法:

代码语言:javascript复制
value1   value2

通过以下步骤进行:

  1. 确保两个操作数都是原始值。 对象obj通过内部操作ToPrimitive(obj)(参见算法:ToPrimitive()—将值转换为原始值)转换为原始值,该操作调用obj.valueOf()和可能的obj.toString()来执行此操作。 对于日期,首先调用obj.toString()
  2. 如果任一操作数是字符串,则将两者转换为字符串并返回结果的连接。
  3. 否则,将两个操作数转换为数字,并返回结果的总和。

布尔值和数字的运算符

以下运算符只有单一类型的操作数,并且也产生该类型的结果。 它们在其他地方有所涉及。

布尔运算符:

二进制逻辑运算符(参见二进制逻辑运算符:And (&&)和 Or (||)):

代码语言:javascript复制
x && y, x || y

逻辑非(参见逻辑非(!)):

代码语言:javascript复制
!x

数字运算符:

算术运算符(参见[算术运算符](ch11.html#arithmetic_operators “算术运算符”):

代码语言:javascript复制
x   y, x - y, x * y, x / y, x % y
  x, --x, x  , x--
-x,  x

按位运算符(参见[按位运算符](ch11.html#bitwise_operators “按位运算符”):

代码语言:javascript复制
~x
x & y, x | y, x ^ y
x << y, x >> y, x >>> y

特殊运算符

在这里,我们将回顾特殊运算符,即条件、逗号和void运算符。

条件运算符(?:)

条件运算符是一个表达式:

代码语言:javascript复制
«condition» ? «if_true» : «if_false»

如果条件为true,则结果为if_true; 否则,结果为if_false。 例如:

代码语言:javascript复制
var x = (obj ? obj.prop : null);

不需要在运算符周围加括号,但这样做会使其更易于阅读。

逗号运算符
代码语言:javascript复制
«left» , «right»

逗号运算符评估两个操作数并返回“right”的结果。 粗略地说,它对表达式做了分号对语句所做的事情。

这个例子演示了第二个操作数成为运算符的结果:

代码语言:javascript复制
> 123, 'abc'
'abc'

这个例子演示了两个操作数都被评估:

代码语言:javascript复制
> var x = 0;
> var y = (x  , 10);

> x
1
> y
10

逗号运算符很令人困惑。 最好不要聪明,而是在您可以的情况下编写两个单独的语句。

void 运算符

void运算符的语法是:

代码语言:javascript复制
void «expr»

评估expr并返回undefined。 以下是一些例子:

代码语言:javascript复制
> void 0
undefined
> void (0)
undefined

> void 4 7  // same as (void 4) 7
NaN
> void (4 7)
undefined

> var x;
> x = 3
3
> void (x = 5)
undefined
> x
5

因此,如果您将void实现为一个函数,它看起来如下:

代码语言:javascript复制
function myVoid(expr) {
    return undefined;
}

void运算符与其操作数密切相关,因此根据需要使用括号。 例如,void 4 7绑定为(void 4) 7

void 用于什么?

在 ECMAScript 5 下,void很少有用。 它的主要用例是:

void 0作为undefined的同义词

后者可以更改,而前者将始终具有正确的值。 但是,在 ECMAScript 5 下,undefined相对安全,这使得这种用例不那么重要(有关详细信息,请参见更改 undefined)。

丢弃表达式的结果

在某些情况下,返回undefined而不是表达式的结果很重要。 然后可以使用void来丢弃该结果。 其中一种情况涉及javascript: URL,应该避免使用链接,但对于书签很有用。 当您访问这些 URL 之一时,许多浏览器会用 URL 的“内容”评估结果替换当前文档,但前提是结果不是undefined。 因此,如果您想要打开一个新窗口而不更改当前显示的内容,可以执行以下操作:

代码语言:javascript复制
javascript:void window.open("http://example.com/")

前缀 IIFE

IIFE 必须被解析为表达式。确保这一点的几种方法之一是用void作为前缀(参见IIFE 变体:前缀运算符)⁹

为什么 JavaScript 有 void 运算符?

根据 JavaScript 的创始人 Brendan Eich,他将其添加到语言中以帮助处理javascript:链接(前面提到的用例之一):

我在 Netscape 2 发布之前向 JS 添加了void运算符,以便轻松丢弃 javascript: URL 中的任何非 undefined 值。¹⁰

通过 typeof 和 instanceof 对值进行分类

如果你想对一个值进行分类,不幸的是你必须区分原始值和对象(参见第八章中的内容):

  • typeof 运算符区分原始值和对象,并确定原始值的类型。
  • instanceof运算符确定一个对象是否是给定构造函数的实例。有关 JavaScript 中面向对象编程的更多信息,请参阅第十七章。
typeof:对原始值进行分类

typeof 运算符:

代码语言:javascript复制
typeof «value»

返回描述value是什么类型的字符串。以下是一些例子:

代码语言:javascript复制
> typeof undefined
'undefined'
> typeof 'abc'
'string'
> typeof {}
'object'
> typeof []
'object'

typeof用于区分原始值和对象,并对原始值进行分类(instanceof无法处理原始值)。不幸的是,这个运算符的结果并不完全符合逻辑,而且只是松散地对应于 ECMAScript 规范的类型(在JavaScript 的类型中有解释):

操作数

结果

undefined,未声明的变量

'undefined'

null

'object'

布尔值

'boolean'

数值

'number'

字符串值

'string'

函数

'function'

所有其他正常值

'object'

(引擎创建的值)

JavaScript 引擎允许创建值,对于这些值,typeof返回任意字符串(与表中列出的所有结果不同)。

陷阱:typeof null

不幸的是,typeof null'object'。这被认为是一个错误(null不是内部类型 Object 的成员),但无法修复,因为这样做会破坏现有的代码。因此,你必须谨慎处理null。例如,以下函数检查value是否是一个对象:

代码语言:javascript复制
function isObject(value) {
    return (value !== null
       && (typeof value === 'object'
           || typeof value === 'function'));
}

试一试:

代码语言:javascript复制
> isObject(123)
false
> isObject(null)
false
> isObject({})
true
typeof null 的历史

第一个 JavaScript 引擎将 JavaScript 值表示为 32 位字。这样的字的最低 3 位用作类型标记,以指示该值是对象、整数、双精度、字符串还是布尔值(正如你所看到的,即使这个早期引擎已经尽可能将数字存储为整数)。

对象的类型标记为 000。为了表示值null,引擎使用了机器语言的 NULL 指针,一个所有位都为零的字。typeof检查类型标记以确定值的类型,这就是为什么它报告null是一个对象的原因。¹¹

检查变量是否存在

检查:

代码语言:javascript复制
typeof x === 'undefined'

有两种用例:

  1. 它确定x是否undefined
  2. 它确定变量x是否存在。

以下是两种用例的示例:

代码语言:javascript复制
> var foo;
> typeof foo === 'undefined'
true

> typeof undeclaredVariable === 'undefined'
true

对于第一个用例,直接与undefined进行比较通常是更好的选择。但是,对于第二个用例,这种方法行不通。

代码语言:javascript复制
> var foo;
> foo === undefined
true

> undeclaredVariable === undefined
ReferenceError: undeclaredVariable is not defined
instanceof:检查对象是否是给定构造函数的实例

instanceof运算符:

代码语言:javascript复制
«value» instanceof «Constr»

确定value是由构造函数Constr还是子构造函数创建的。以下是一些例子:

代码语言:javascript复制
> {} instanceof Object
true
> [] instanceof Array  // constructor of []
true
> [] instanceof Object  // super-constructor of []
true

预期的是,instanceof对非值undefinednull返回false

代码语言:javascript复制
> undefined instanceof Object
false
> null instanceof Object
false

但对于所有其他原始值也是false

代码语言:javascript复制
> 'abc' instanceof Object
false
> 123 instanceof Object
false

有关instanceof的详细信息,请参阅instanceof 运算符。

对象运算符

以下三个运算符适用于对象。它们在其他地方有解释:

new(参见第三层:构造函数——实例的工厂)

调用构造函数,例如,new Point(3, 5)

delete(参见删除属性)

删除属性,例如,delete obj.prop

in(参见迭代和属性检测)

检查对象是否具有给定属性,例如,'prop' in obj


⁸ 严格来说,设置数组元素是设置属性的特例。

⁹ 感谢 Brandon Benvie (@benvie),他告诉我如何使用void来进行 IIFEs。

¹⁰ 来源:en.wikipedia.org/wiki/Bookmarklet

¹¹ 感谢 Tom Schuster (@evilpies) 指引我到第一个 JavaScript 引擎的源代码。

第十章:布尔值

原文:10. Booleans 译者:飞龙 协议:CC BY-NC-SA 4.0

原始布尔类型包括值truefalse

代码语言:javascript复制
> typeof false
'boolean'
> typeof true
'boolean'

转换为布尔值

值转换为布尔值的方式如下:

转换为布尔值

undefined

false

null

false

布尔值

与输入相同(无需转换)

数字

0、NaN → false

其他数字 → true

字符串

'' → false

其他字符串 → true

对象

true(总是!)

手动转换为布尔值

任何值都可以通过三种方式转换为布尔值:

| Boolean(value) | (作为函数调用,而不是构造函数) | | value ? true : false | | | !!value | 单个“not”转换为取反的布尔值;使用两次进行非取反转换。 |

我更喜欢Boolean(),因为它更具描述性。以下是一些例子:

代码语言:javascript复制
> Boolean(undefined)
false
> Boolean(null)
false

> Boolean(0)
false
> Boolean(1)
true
> Boolean(2)
true

> Boolean('')
false
> Boolean('abc')
true
> Boolean('false')
true
真值和假值

在 JavaScript 需要布尔值的地方,你可以提供任何类型的值,它会自动转换为布尔值。因此,在 JavaScript 中有两组值:一组转换为false,而另一组转换为true。这些组被称为假值真值。根据前面的表格,以下是所有的假值:

  • undefinednull
  • 布尔值:false
  • 数字:0NaN
  • 字符串:''

所有其他值,包括所有对象,甚至是空对象、空数组和new Boolean(false),都是真值。因为undefinednull是假值,你可以使用if语句来检查变量x是否有值:

代码语言:javascript复制
if (x) {
    // x has a value
}

需要注意的是,前面的检查将所有假值解释为“没有值”,不仅仅是undefinednull。但如果你可以接受这个限制,你就可以使用一种简洁和成熟的模式。

陷阱:所有对象都是真值

所有对象都是真值:

代码语言:javascript复制
> Boolean(new Boolean(false))
true
> Boolean([])
true
> Boolean({})
true

这与对象转换为数字或字符串的方式不同,你可以通过实现valueOf()toString()方法来控制结果。

代码语言:javascript复制
> Number({ valueOf: function () { return 123 } })
123
> String({ toString: function () { return 'abc' } })
'abc'
历史:为什么对象总是真值?

由于历史原因,布尔值的转换方式不同。在 ECMAScript 1 中,决定不允许对象配置该转换(例如,通过toBoolean()方法)。其理由是布尔运算符||&&会保留其操作数的值。因此,如果你链式使用这些运算符,相同的值可能会被多次检查真值或假值。对于原始值来说,这些检查是廉价的,但如果对象能够配置它们的布尔值转换,那么对于对象来说将会很昂贵。ECMAScript 1 通过使对象始终为真值来避免这种成本。

逻辑运算符

在本节中,我们将介绍 And(&&)、Or(||)和 Not(!)逻辑运算符的基础知识。

二进制逻辑运算符:And(&&)和 Or(||)

二进制逻辑运算符有:

保持值不变

它们总是返回两个操作数中的一个,不会改变:

代码语言:javascript复制
> 'abc' || 123
'abc'
> false || 123
123

短路

如果第一个操作数已经确定了结果,则不会评估第二个操作数。例如(console.log的结果是undefined):

代码语言:javascript复制
> true || console.log('Hello')
true
> false || console.log('Hello')
Hello
undefined

这是运算符的不常见行为。通常,在调用运算符之前会评估所有操作数(就像函数一样)。

逻辑与(&&)

如果第一个操作数可以转换为false,则返回它。否则,返回第二个操作数:

代码语言:javascript复制
> true && false
false
> false && 'def'
false
> '' && 'def'
''
> 'abc' && 'def'
'def'
逻辑或(||)

如果第一个操作数可以转换为true,则返回它。否则,返回第二个操作数:

代码语言:javascript复制
> true || false
true
> true || 'def'
true
> 'abc' || 'def'
'abc'
> '' || 'def'
'def'
模式:提供默认值

有时会出现这样的情况:一个值(参数、函数的结果等)可以是非值(undefinednull)或实际值。如果要为前一种情况提供默认值,可以使用或运算符:

代码语言:javascript复制
theValue || defaultValue

前面的表达式在theValue为真值时求值为theValue,否则为defaultValue。通常的警告适用:如果theValue具有除undefinednull之外的假值,则也将返回defaultValue。让我们看看使用该模式的三个示例。

示例 1:参数的默认值

函数saveText()的参数text是可选的,如果省略了,则应该是空字符串:

代码语言:javascript复制
function saveText(text) {
    text = text || '';
    ...
}

这是||作为默认运算符的最常见用法。有关可选参数的更多信息,请参阅可选参数。

示例 2:属性的默认值

对象options可能有也可能没有属性title。如果缺少,则在设置标题时应使用值'Untitled'

代码语言:javascript复制
setTitle(options.title || 'Untitled');
示例 3:函数结果的默认值

函数countOccurrences计算regexstr中匹配的次数:

代码语言:javascript复制
function countOccurrences(regex, str) {
    // Omitted: check that /g is set for `regex`
    return (str.match(regex) || []).length;
}

问题在于match()(请参见String.prototype.match: Capture Groups or Return All Matching Substrings)要么返回一个数组,要么返回null。由于||,在后一种情况下使用了默认值。因此,您可以安全地在两种情况下访问属性length

逻辑非(!)

逻辑非运算符!将其操作数转换为布尔值,然后对其取反:

代码语言:javascript复制
> !true
false
> !43
false
> !''
true
> !{}
false

相等运算符,排序运算符

其他运算符在其他地方有所涵盖:

  • 相等运算符:===!====!=(参见相等运算符:===与==)
  • 排序运算符:>>=<<=(参见排序运算符)

布尔函数

函数Boolean可以以两种方式调用:

Boolean(value)

作为普通函数,它将value转换为原始布尔值(请参见转换为布尔值):

代码语言:javascript复制
> Boolean(0)
false
> typeof Boolean(false)  // no change
'boolean'

new Boolean(bool)

作为构造函数,它创建了Boolean的新实例(参见原始包装对象),一个将bool(在将其转换为布尔值后)包装起来的对象。例如:

代码语言:javascript复制
> typeof new Boolean(false)
'object'

前面的调用是常见的。

0 人点赞