跳到主要内容

语言基础

任何语言的核心所描述的都是这门语言在最基本的层面上如何工作,涉及语法、操作符、数据类型 以及内置功能,在此基础之上才可以构建复杂的解决方案。如前所述,ECMA-262 以一个名为 ECMAScript 的伪语言的形式,定义了 JavaScript 的所有这些方面。

语法

区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大 10 小写。换句话说,变量 test 和变量 Test 是两个不同的变量。

标识符

所谓标识符,就是变量、函数、属性或函数参数的名称。标识符可以由一或多个下列字符组成: 字母、数字、下划线(_)和美元符号($)。标识符组成不能由数字开头

最佳实践: ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写。虽然这种写法并不是强制性的,但因为这种形式跟 ECMAScript 内置函数和对象的命名方式一致。

备注

关键字、保留字、true、false 和 null 不能作为标识符。具体查看关键字和保留字

注释

ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。

// 单行注释

/*
多行注释
*/

/**
* 多行注释的一种变体
* 常用于文档注释或结构化注释
*/

严格模式

ECMAScript 5 增加了严格模式(strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执 行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。要对 整个脚本启用严格模式,在脚本开头加上这一行:

'use strict';

虽然看起来像个没有赋值给任何变量的字符串,但它其实是一个预处理指令。任何支持的 JavaScript 引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏 ECMAScript 3 语法。

也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:

function doSomething() {
'use strict';
// 函数体
}

严格模式会影响 JavaScript 执行的很多方面,因此本书在用到它时会明确指出来。所有现代浏览器 都支持严格模式。

语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子 所示:

let sum = a + b; // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的 位置补上分号以纠正语法错误。

多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右 花括号(})标识结束:

if (test) {
test = false;
console.log(test);
}

if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中 使用代码块,即使要执行的只有一条语句,如下例所示:

// 有效,但容易导致错误,应该避免
if (test) console.log(test);

// 推荐
if (test) {
console.log(test);
}

关键字和保留字

ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,比如表示控制语句的开始和结束,或者执行特定的操作。按照规定,保留的关键字不能用作标识符或属性名。ECMA-262 第 6 版规定的所有关键字如下:

break      do       in         typeof
case else instanceof var
catch export new void
class extends return while
const finally super with
continue for switch yield
debugger function this
default if throw
delete import try

规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定 用途,但它们是保留给将来做关键字用的。

始终保留:

enum

严格模式下保留:

implements package public
interface protected static
let private

模块代码中保留:

await(已用于es8异步关键字)

变量

ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在 ECMAScript 的所有版本中都可以使用,而constlet只能在 ECMAScript 6 及更晚的版本中使用。

var 关键字

在 JavaScript 创立之初就使用 var 关键字进行变量声明,如:

var message = '赋值给变量';

// 不推荐写法
var message2;
message2 = '初始化变量不赋值,默认是undefined';
注意

虽然 "var" 仍然在 JavaScript 中广泛使用,但它有一些特性使其在现代 JavaScript 编码中相对不太推荐。这也是为什么在 ES6 版本推出constlet关键字。

声明作用域

变量使用 "var" 声明时,其作用域限制在声明所在的函数内部,而不是块级作用域(如在 "for" 循环或 "if" 语句内)。

function example() {
if (true) {
var x = 5;
}
console.log(x); // 输出 5
}
example();
function test() {
var message = 'hi'; // 局部变量
}
test();
console.log(message); // 出错!

这里,message 变量是在函数内部使用 var 定义的。函数叫 test(),调用它会创建这个变量并给 它赋值。调用之后变量随即被销毁,因此示例中的最后一行会导致错误。不过,在函数内定义变量时省 略 var 操作符,可以创建一个全局变量:

function test() {
message = 'hi'; // 全局变量
}
test();
console.log(message); // "hi"
危险

虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定 义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而 为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。

如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量(及可选的初始化):

var message = 'hi',
found = false,
age = 29;

声明提升

使用 var 时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:

function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined

之所以不会报错,是因为 ECMAScript 运行时把它看成等价于如下代码:

function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined

这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用 var 声明同一个变量也没有问题:

function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
总结

通过对 var 关键字特性的了解,想必也明白了为什么不推荐使用它来声明变量了。

let 关键字

let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。

// var 声明
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt

// let 声明
if (true) {
let age = 26;
console.log(age);
}
console.log(age);
// 26
// ReferenceError: age 没有定义

在这里,age 变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域 是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。

let 也不允许同一个块作用域中出现冗余声明。这样会导致报错:

var name;
var name;

let age;
let age; //SyntaxError;标识符age已经声明过了

当然,JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:

let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}

对声明冗余报错不会因混用letvar 而受影响。这两个关键字声明的并不是不同类型的变量, 它们只是指出变量在相关作用域如何存在。

var name;
let name; // SyntaxError

let age;
var age; // SyntaxError

暂时性死区

letvar 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。

// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;

在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。

全局声明

var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性( var 声明的变量则会)。

var name = 'Matt';
console.log(window.name); // 'Matt'

let age = 26;
console.log(window.age); // undefined

当你在全局作用域中使用 let 声明一个变量时,这个变量在页面的整个生命周期内都会存在。因此,如果你在同一个页面中重复声明相同名字的变量,就会导致 SyntaxError(语法错误)。

条件声明

在某些情况下,我们可能希望根据条件来声明一个变量。可以通过在块内使用 let 声明变量,并在条件语句内初始化它。

if (someCondition) {
let value = 'foo';
console.log(value); // foo
} else {
let value = 'bar';
console.log(value); // bar
}

在这个例子中,根据 someCondition 的值,我们在 ifelse 块中分别声明了两个 value 变量。

为什么这是一个反模式?

  • 作用域不清晰:在 ifelse 块之外无法访问 value 变量,因为它们只在各自的块作用域内有效。这可能会让代码的作用域变得复杂和不清晰。
  • 维护性差:如果有多个条件分支,每个分支中都声明了相同的变量名,这将导致代码难以维护,因为每个分支的作用域不同。
  • 可读性差:这种模式会让代码变得冗长且不易读,特别是在条件判断较多的情况下。

为了避免这种反模式,可以采用如下更清晰、更易维护的方式:

let value;
if (someCondition) {
value = 'foo';
} else {
value = 'bar';
}
console.log(value); // value 在这里始终可用

不能使用 let 进行条件式声明是件好事,因为条件声明是一种反模式,它让程序变得更难理解。如果你发现自己在使用这个模式,那一定有更好的替代方式。

for 循环中的 let 声明

let出现之前,for 循环定义的迭代变量会渗透到循环体外部:

for (var i = 0; i < 5; i++) {
// 循环逻辑
}
console.log(i); // 5

改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:

for (let i = 0; i < 5; i++) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义

在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:

for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
}
// 你可能以为会输出0、1、2、3、4
// 实际上会输出5、5、5、5、5

之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

而在使用let声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0);
}
// 会输出0、1、2、3、4

这种每次迭代声明一个独立变量实例的行为适用于所有风格的 for 循环,包括 for-infor-of 循环。

const 关键字

const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改 const 声明的变量会导致运行时错误。

const age = 26;
age = 36; // TypeError: 给常量赋值

// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 声明的作用域也是块 const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt

const 声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象, 那么修改这个对象内部的属性并不违反const的限制。

const person = {};
person.name = 'Matt'; // ok

JavaScript 引擎会为 for 循环中的let声明分别创建独立的变量实例,虽然const变量跟let变量很相似,但是不能用const来声明迭代变量(因为迭代变量会自增):

for (const i = 0; i < 10; ++i) {} // TypeError:给常量赋值

声明风格及最佳实践

ECMAScript 6 增加 letconst 从客观上为这门语言更精确地声明作用域和语义提供了更好的支持。行为怪异的 var 所造成的各种问题,已经让 JavaScript 社区为之苦恼了很多年。随着这两个新关键字的出现,新的有助于提升代码质量的最佳实践也逐渐显现。

最佳实践

  • 不使用 var
  • const 优先,let 次之

使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不 合法的赋值操作。

数据类型

ECMAScript 有 7 种简单数据类型(也称为原始类型):UndefinedNullBooleanNumberBigIntStringSymbol。Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对象)。Object 是一种无序名值对的集合。因为在 ECMAScript 中不能定义自己的数据类型,所有值都可以用上述 7 种数据类型之一来表示。只有 7 种数据类型似乎不足以表示全部数据。但 ECMAScript 的数据类型很灵活,一种数据类型可以当作多种数据类型来使用。

新类型

BigInt 是一种内置对象,它提供了一种方法来表示大于 25312^{53} - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

typeof 操作符

因为 ECMAScript 的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。typeof 操作符就是为此而生的。对一个值使用 typeof 操作符会返回下列字符串之一:

  • undefined 表示值未定义;
  • boolean 表示值为布尔值;
  • string 表示值为字符串;
  • number 表示值为数值;
  • bigint 表示值为BigInt数值。
  • object 表示值为对象(而不是函数)或 null;
  • function 表示值为函数;
  • symbol 表示值为符号。
typeof 示范
let message = 'something';
console.log(typeof message); // "string"
console.log(typeof message); // "string"

注意,因为 typeof 是一个操作符而不是函数,所以不需要参数(但可以使用参数)。

typeof 在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用 typeof null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。

Undefined 类型

Undefined 是 JavaScript 中的一个基本数据类型,表示变量未定义或未赋值。它是七种原始数据类型之一。

永远不必显式地以 undefined 来初始化。但这是不必要的,因为默认情况下,任何未经初始化的变量都会取得 undefined 值。

// 不推荐
let message = undefined;

在对未初始化的变量调用 typeof 时,返回的结果是"undefined",但对未声明的变量调用它时,返回的结果还是"undefined"。

let a;

console.log(typeof a); // undefined
console.log(typeof b); // undefined

无论是声明还是未声明,typeof 返回的都是字符串"undefined"。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异,但它们都无法执行实际操作。

undefined 是一个假值。因此,如果需要,可以用更简洁的方式检测它。

Null 类型

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因:

let car = null;
console.log(typeof car); // "object"

在定义将来要保存对象值的变量时,建议使用 null 来初始化,这样做可以使代码更具可读性,并且通过检查变量是否为 null 可以明确地知道变量是否已经被赋值了对象引用。

初始化变量为 null 并在后续代码中赋值
// 初始化变量为 null
let user = null;

// 后续代码中赋予实际的对象值
user = { name: 'Alice', age: 25 };

// 检查变量是否为 null
if (user !== null) {
console.log('User object is assigned:', user);
} else {
console.log('User object is not assigned');
}
函数参数初始化为 null
function processUser(user = null) {
if (user === null) {
console.log('No user data provided');
} else {
console.log('Processing user:', user);
}
}

// 调用函数时没有传递 user 参数
processUser();

// 调用函数时传递了 user 参数
processUser({ name: 'Bob', age: 30 });

undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下面的例子所示:

console.log(null == undefined); // true
console.log(null === undefined); // false

即使 nullundefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将 变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个 对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。

Boolean

Boolean 是 JavaScript 中的一个基本数据类型,用于表示两个值中的一个:true 或 false。这些值通常用于控制程序的逻辑流,例如条件判断、循环控制等。

虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其 他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数:

数据类型转换为true的值转换为false的值
Booleantruefalse
String非空字符串""(空字符串)
Number非零数值(包括无穷值 Infinity)0, -0, NaN
Object任意对象null
UndefinedN/A(不存在)undefined

理解以上转换非常重要,因为像 if 等流控制语句会自动执行其他类型值到布尔值的转换。

Number 类型

Number 是 JavaScript 中的一种基本数据类型,用于表示数值。JavaScript 中的 Number 类型遵循 IEEE 754 标准,可以表示双精度 64 位浮点数。Number 类型能够表示整数和浮点数,但其精度和范围是有限的。

创建 Number 值
// 字面量
let intLiteral = 42;
let floatLiteral = 3.14;

整数也可以用八进制(以 8 为基数)或十六进制(以 16 为基数)字面量表示。

创建八进制字面量,第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数,如下所示:

let octalNum1 = 070; // 八进制的 56
// let octalNum2 = 079; // 无效的八进制值,当成 79 处理
// let octalNum3 = 08; // 无效的八进制值,当成 8 处理

八进制字面量在严格模式下是无效的, JavaScript 引擎抛出语法错误。

注意

ECMAScript 2015 或 ES6 中的八进制值通过前缀 0o 来表示;严格模式下,前缀 0 会被视为语法错误,如果要表示 八进制值,应该使用前缀 0o。

创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(0~9 以及 A~F)。十六进制数字中的字母大小写均可。下面是几个例子:

let hexNum1 = 0xa; // 十六进制 10
let hexNum2 = 0x1f; // 十六进制 31

浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不 是必须有整数,但推荐加上。

let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = 0.1; // 有效,但不推荐

因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数。

let floatNum1 = 1; // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理

对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以 10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大 写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:

let floatNum = 3.125e7; // 等于 31250000

科学记数法也可以用于表示非常小的数值,例如 0.000 000 000 000 000 03。这个数值用科学记数法 可以表示为 3e-17。默认情况下,ECMAScript 会将小数点后至少包含 6 个零的浮点值转换为科学记数法(例如,0.000 000 3 会被转换为 3e-7)。

浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。比如下面的例子:

if (a + b == 0.3) {
// 别这么干!
console.log('You got 0.3.');
}

这里检测两个数值之和是否等于 0.3。如果两个数值分别是 0.05 和 0.25,或者 0.15 和 0.15,那没问 题。但如果是 0.1 和 0.2,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。

注意

之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。

值的范围

Number 类型取值范围:

  • 最大安全整数:Number.MAX_SAFE_INTEGER,等于 25312^{53} - 1(9007199254740991)。
  • 最大安全整数:Number.MIN_SAFE_INTEGER,等于 253+1-2^{53} + 1(-9007199254740991)。
  • 最大可表示数值:Number.MAX_VALUE,约等于 1.79103081.79 * 10^{308}
  • 最小可表示数值:Number.MIN_VALUE,约等于 5103245 * 10^{324}

如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(穷大)值。任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。

要确定一个值是不是有限大(即介于 JavaScript 能表示的 最小值和最大值之间),可以使用 isFinite() 函数,如下所示:

let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false

虽然超出有限数值范围的计算并不多见,但总归还是有可能的。因此在计算非常大或非常小的数值 时,有必要监测一下计算结果是否超出范围。

使用 Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY 也可以获 取正、负 Infinity。没错,这两个属性包含的值分别就是-Infinity 和 Infinity。

console.log(Number.NEGATIVE_INFINITY); // -Infinity
console.log(Number.POSITIVE_INFINITY); // Infinity

NaN

有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执 12 行。但在 ECMAScript 中,0、+0 或0 相除会返回 NaN:

console.log(0 / 0); // NaN
console.log(-0 / +0); // NaN

如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity-Infinity:

console.log(5 / 0); // Infinity
console.log(5 / -0); // -Infinity

NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算 时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值。例如,下面的比较操作会返回 false:

console.log(NaN == NaN); // false

为此,ECMAScript 提供了 isNaN() 函数。该函数接收一个参数,可以是任意数据类型,然后判断 这个参数是否“不是数值”。把一个值传给 isNaN() 后,该函数会尝试把它转换为数值。某些非数值的 值可以直接转换成数值,如字符串"10"或布尔值。任何不能转换为数值的值都会导致这个函数返回 true。举例如下:

console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN('10')); // false,可以转换为数值 10
console.log(isNaN('blue')); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值 1

数值转换

有 3 个函数可以将非数值转换为数值: Number()parseInt()parseFloat()Number() 是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个 函数执行的操作也不同。

Number() 函数转换规则
// 布尔值
console.log(Number(true)); // 1
console.log(Number(false)); // 0

// 数值:直接返回
console.log(Number(520)); // 520

// null
console.log(Number(null)); // 0

// undefined
console.log(Number(undefined)); // NaN

// 字符串
// 一:纯数值字符串,前缀为是0则忽略
console.log(Number('011')); // 11
console.log(Number('01.1')); // 1.1
console.log(Number('200')); // 200
// 二:有效十六进制、八进制格式
console.log(Number('0xAA')); // 170
console.log(Number('0o77')); // 63
// 三:空字符串
console.log(Number('')); // 0
// 四:如果字符串包含除上述情况之外的其他字符,则返回 NaN。
console.log(Number('xxx')); // NaN

// 对象
/**
* 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。
*/

考虑到用 Number() 函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt() 函数。parseInt() 函数更专注于字符串是否包含数值模式。

字符串最前面的空格会被 忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt() 立即返回 NaN。这意味着空字符串也会返回 NaN

console.log(Number.parseInt('')); // NaN
console.log(Number('')); // 0

如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符(忽略后面)。

console.log(Number.parseInt('01234xxx')); // 1234
console.log(Number.parseInt('01234.4567xxx')); // 1234

假设字符串中的第一个字符是数值字符,parseInt() 函数也能识别不同的整数格式(十进制、八进制、十六进制)。

// 十六进制
console.log(Number.parseInt('0xAAPP')); // 170
// 八进制: 在非严格模式下会被某些实现解释为八进制整数。
console.log(Number.parseInt('077')); // 63
// 无法使用八进制 0o 前缀
console.log(Number.parseInt('0o77')); // 0

不同的数值格式很容易混淆,因此 parseInt() 也接收第二个参数,用于指定底数(进制数)。如 果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:

console.log(Number.parseInt('0xAA', 16)); // 170

// 前缀可以省略
console.log(Number.parseInt('AA', 16)); // 170

parseFloat() 函数的工作方式跟 parseInt() 函数类似,都是从位置 0 开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。

console.log(Number.parseFloat('22.34.5')); // 22.34

parseFloat() 函数的另一个不同之处在于,它始终忽略字符串开头的零。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终会返回 0。因为 parseFloat() 只解析十进制值,因此不能指定底数。最后,如果字符串表示整数(没有小数点或者小数点后面只有一个零),则 parseFloat() 返回整数。下面是几个示例:

let num1 = parseFloat('1234blue'); // 1234,按整数解析
let num2 = parseFloat('0xA'); // 0
let num3 = parseFloat('22.5'); // 22.5
let num4 = parseFloat('22.34.5'); // 22.34
let num5 = parseFloat('0908.5'); // 908.5
let num6 = parseFloat('3.125e7'); // 31250000
提示

在 JavaScript 中,parseIntparseFloat 是一个全局函数,可以直接使用而不需要通过 Number 对象调用。这意味着你可以直接使用它们来解析字符串。事实上通过 Number 使用的函数是一个静态方法,用法没有区别。

BigInt 类型

BigInt 是一种内置对象,它提供了一种方法来表示大于 25312^{53} - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

可以通过在整数末尾添加 n 或使用 BigInt 构造函数来创建 BigInt

创建 BigInt
const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

const hugeString = BigInt('9007199254740991');
// ↪ 9007199254740991n

const hugeHex = BigInt('0x1fffffffffffff');
// ↪ 9007199254740991n

const hugeBin = BigInt(
'0b11111111111111111111111111111111111111111111111111111',
);
// ↪ 9007199254740991n

它在某些方面类似于 Number ,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

当使用 BigInt 时,带小数的运算会被取整

const expected = 4n / 2n;
// ↪ 2n

const rounded = 5n / 2n;
// ↪ 2n, not 2.5n

BigIntNumber 不是严格相等的,但是宽松相等的。

0n === 0;
// ↪ false

0n == 0;
// ↪ true

10n == 10;
// ↪ true

在 JSON 中使用

对任何 BigInt 值使用 JSON.stringify() 都会引发 TypeError,因为默认情况下 BigInt 值不会在 JSON 中序列化。

String 类型

String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、 单引号(')或反引号(`)标示。

ECMAScript 语法中表示字符串的引号没有区别。不过要注意的是,以某种引号开头,必须以它结尾,保持一致性。

字符字面量

字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符。

字面量含有
\n换行
\t制表
\b退格
\r回车
\f换页
\\反斜杠()
\'单引号(')
\"双引号(")
\`反引号(`)
\xnn以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F)
\unnnn以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F)

字符串的特点

ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

// 不可修改
let str = 'hello world';
console.log(str[1]); // e

str[1] = 'w';
console.log(str[1]);

// 先分配一个足够容纳 10 个字符的空间, 再销毁原始字符串 ‘java’
let lang = 'Java';
lang = lang + 'Script';

转换为字符串

有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的 toString() 方法。这个方法唯一的用途就是返回当前值的字符串等价物。比如:

let age = 18;
let isAdult = true;

console.log(age.toString()); // '18'
console.log(isAdult.toString()); // 'true'

toString() 方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有 toString() 方法, 该方法只是简单地返回自身的一个副本) nullundefined 值没有 toString() 方法。

默认情况下,toString() 返回数值的十进制字符串表示。toString() 接收一个底数参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示,比如:

let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

如果你不确定一个值是不是 nullundefined,可以使用 String() 转型函数,它始终会返回表示相应类型值的字符串。String() 函数遵循如下规则:

  • 如果值有 toString() 方法,则调用该方法(不传参数)并返回结果。
  • 如果值是 null,返回"null"
  • 如果值是 undefined,返回"undefined"
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;

console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"

数值和布尔值的转换结果与调用 toString() 相同。因为 nullundefined 没有 toString() 方法, 所以 String() 方法就直接返回了这两个值的字面量文本。

模板字面量

ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:

let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;

// 两者打印结果一样
console.log(myMultiLineString);
console.log(myMultiLineTemplateLiteral);
// first
// line

模板字面量在定义模板时特别有用,比如下面这个 HTML 模板:

let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;

由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。

// 这个模板字面量在换行符之后有 25 个空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47

// 这个模板字面量以一个换行符开头
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true

// 这个模板字面量没有意料之外的字符
let thirdTemplateLiteral = `first line second line`;
console.log(thirdTemplateLiteral);
// first line
// second line

字符串插值

字符串插值通过在 ${} 中使用一个 JavaScript 表达式实现:

let age = '18';
let name = 'MoFan';

console.log(`我今年${age}岁,我叫${name}`); // 我今年18岁,我叫MoFan

模板字面量标签函数

模板字面量标签函数(template literal tag functions)是 JavaScript 中的一种高级功能,允许你对模板字符串进行自定义处理。标签函数在模板字符串被解析之前执行,可以让你修改模板字符串的内容,或者执行特定的逻辑。

定义标签函数
// 标签函数是一个普通的函数,它接受模板字符串的各个部分作为参数,并返回处理后的字符串。
function tag(strings, ...values) {
console.log(strings); // 模板字符串中的静态部分
console.log(values); // 插值表达式的值
return 'Processed String';
}

const name = 'Alice';
const age = 25;
const result = tag`Name: ${name}, Age: ${age}`;
console.log(result); // 输出:Processed String
使用标签函数处理模板字符串
// 标签函数可以对模板字符串进行各种处理,例如转义特殊字符、格式化字符串等。

function escapeHTML(strings, ...values) {
return strings.reduce((result, str, i) => {
let value = values[i - 1];
if (value) {
value = String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
return result + value + str;
});
}

const userInput = '<script>alert("XSS")</script>';
const escapedString = escapeHTML`User input: ${userInput}`;
console.log(escapedString); // 输出:User input: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

模板字面量标签函数在 JavaScript 中具有多种重要用途,它们提供了一种灵活的方式来处理和格式化字符串。以下是一些常见的用处和具体示例:

  • 安全性(防止XSS攻击): 模板字面量标签函数可以用于转义HTML,以防止XSS(跨站脚本)攻击。
  • 国际化(i18n): 标签函数可以用来实现字符串的国际化和本地化,方便多语言支持。
  • 字符串格式化: 标签函数可以用于格式化货币、日期等复杂格式。

原始字符串

在JavaScript中,原始字符串(raw strings)是使用 String.raw 标签函数创建的字符串,它保留了字符串中所有的转义字符。原始字符串在处理文件路径、正则表达式模式或任何需要精确控制转义字符的情况时特别有用。

String.raw 标签函数与模板字面量一起使用时,可以创建一个原始字符串,它不会对反斜杠进行转义处理。

处理文件路径
const filePath = String.raw`C:\Users\Alice\Documents\file.txt`;
console.log(filePath); // 输出:C:\Users\Alice\Documents\file.txt
处理正则表达式模式
const regexPattern = String.raw`\d{3}-\d{2}-\d{4}`;
console.log(regexPattern); // 输出:\d{3}-\d{2}-\d{4}

String.raw 标签函数的工作原理

String.raw 是一个标签函数,它接受模板字面量的静态字符串部分和插值表达式的值,并返回一个处理后的字符串。与其他标签函数不同的是,String.raw 不会对反斜杠进行转义处理。

const name = 'Alice';
const rawString = String.raw`Hello\n${name}!`;
console.log(rawString); // 输出:Hello\nAlice!
自定义 raw 标签函数
function customRaw(strings, ...values) {
return strings.raw.reduce(
(result, str, i) => result + str + (values[i] || ''),
'',
);
}

const rawString = customRaw`Hello\n${name}!`;
console.log(rawString); // 输出:Hello\nAlice!

在这个示例中,自定义标签函数 customRaw 模仿了 String.raw 的行为,保留了原始字符串中的转义字符。

Symbol 类型

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

符号的基本用法

符号需要使用 Symbol() 函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回 symbol

let sym = Symbol();
console.log(typeof sym); // symbol

调用 Symbol() 函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通 过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');

// 尽管对符号的描述一致,但实例依然不相等
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol() 实例并将其 用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

最重要的是,Symbol() 函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符 号包装对象,像使用 BooleanStringNumber 那样,它们都支持构造函数且可用于初始化包含原 始值的包装对象:

let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"

let myString = new String();
console.log(typeof myString); // "object"

let myNumber = new Number();
console.log(typeof myNumber); // "object"

let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor

// 如果你确实想使用符号包装对象,可以借用 Object()函数:
let mySymbolA = Symbol();
let myWrappedSymbol = Object(mySymbolA);
console.log(typeof myWrappedSymbol); // "object"

使用全局符号注册表

如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册 表中创建并重用符号。为此,需要使用 Symbol.for() 方法:

let fooGlobalSymbolA = Symbol.for('foo');
let fooGlobalSymbolB = Symbol.for('foo');
console.log(typeof fooGlobalSymbolA); // symbol
console.log(fooGlobalSymbolA === fooGlobalSymbolB); // true

Symbol.for() 对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号

console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不等同:

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for() 的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

Symbol.keyFor()

使用 Symbol.keyFor() 来查询全局注册表,这个方法接收符号实例,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

//如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol

使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和 Object.defineProperty()Object.defineProperties() 定义的属性。对象字面量只能在计算属 性语法中使用符号作为属性。

let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');

let o = {
[s1]: 'foo val',
};
// 这样也可以:o[s1] = 'foo val';
console.log(o);
// {Symbol(foo): foo val}

Object.defineProperty(o, s2, { value: 'bar val' });
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val}

Object.defineProperties(o, {
[s3]: { value: 'baz val' },
[s4]: { value: 'qux val' },
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}

类似于 Object.getOwnPropertyNames() 返回对象实例的常规属性数组,Object.getOwnPropertySymbols() 返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors() 会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys() 会返回两种类型的键:

let s1 = Symbol('foo'),
s2 = Symbol('bar');

let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val',
};

console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]

console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]

console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}

console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]

因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果 没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:

let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val',
};

console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}

let barSymbol = Object.getOwnPropertySymbols(o).find(symbol =>
symbol.toString().match(/bar/),
);

console.log(barSymbol);
// Symbol(bar)

常用内置符号

ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者 可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。

这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道 for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义 Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。

这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号 的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。

备注

在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如, @@iterator 指的就是 Symbol.iterator

Symbol.iterator

Symbol.iterator 被用作定义对象的默认迭代器。实现 Symbol.iterator 方法的对象可以被 for...of 循环遍历。

const myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
},
};

for (const value of myIterable) {
console.log(value); // 输出:1, 2, 3
}
Symbol.asyncIterator

Symbol.asyncIterator 被用作定义对象的默认异步迭代器。实现 Symbol.asyncIterator 方法的对象可以被 for await...of 循环遍历。

const myAsyncIterable = {
[Symbol.asyncIterator]: async function* () {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
},
};

(async () => {
for await (const value of myAsyncIterable) {
console.log(value); // 输出:1, 2, 3
}
})();
Symbol.toStringTag

Symbol.toStringTag 被用来创建对象的自定义描述,该描述可以通过 Object.prototype.toString 方法获取。

const myObject = {
[Symbol.toStringTag]: 'MyCustomObject',
};

console.log(Object.prototype.toString.call(myObject)); // 输出:[object MyCustomObject]
Symbol.hasInstance

Symbol.hasInstance 方法被用来判断某个对象是否是某个构造函数的实例,instanceof 运算符会调用这个方法。

class MyClass {
static [Symbol.hasInstance](instance) {
return instance.special === true;
}
}

const obj1 = { special: true };
const obj2 = { special: false };

console.log(obj1 instanceof MyClass); // 输出:true
console.log(obj2 instanceof MyClass); // 输出:false
Symbol.isConcatSpreadable

Symbol.isConcatSpreadable 是一个布尔属性,用于决定对象在 Array.prototype.concat 方法中是否可展开。

let arr1 = [1, 2];
let arr2 = [3, 4];
arr2[Symbol.isConcatSpreadable] = false;

let result = arr1.concat(arr2);
console.log(result); // 输出:[1, 2, [3, 4]]
Symbol.species

Symbol.species 被用来创建派生对象的构造函数。

class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}

let a = new MyArray(1, 2, 3);
let mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // 输出:false
console.log(mapped instanceof Array); // 输出:true
Symbol.match

Symbol.match 被用来定义当字符串与正则表达式匹配时调用的方法。String.prototype.match 会调用这个方法。

class MyMatcher {
constructor(value) {
this.value = value;
}

[Symbol.match](str) {
return str.includes(this.value) ? [this.value] : null;
}
}

const matcher = new MyMatcher('hello');
console.log('hello world'.match(matcher)); // 输出:['hello']
console.log('goodbye world'.match(matcher)); // 输出:null
Symbol.replace

Symbol.replace 被用来定义当字符串执行替换操作时调用的方法。String.prototype.replace 会调用这个方法。

class MyReplacer {
constructor(replaceValue) {
this.replaceValue = replaceValue;
}

[Symbol.replace](str, newSubStr) {
return str.split(this.replaceValue).join(newSubStr);
}
}

const replacer = new MyReplacer('world');
console.log('hello world'.replace(replacer, 'universe')); // 输出:hello universe
Symbol.search

Symbol.search 被用来定义当字符串执行搜索操作时调用的方法。String.prototype.search 会调用这个方法。

class MySearcher {
constructor(value) {
this.value = value;
}

[Symbol.search](str) {
return str.indexOf(this.value);
}
}

const searcher = new MySearcher('world');
console.log('hello world'.search(searcher)); // 输出:6
console.log('goodbye world'.search(searcher)); // 输出:8
Symbol.split

Symbol.split 被用来定义当字符串执行分割操作时调用的方法。String.prototype.split 会调用这个方法。

class MySplitter {
constructor(separator) {
this.separator = separator;
}

[Symbol.split](str) {
return str.split(this.separator);
}
}

const splitter = new MySplitter(' ');
console.log('hello world'.split(splitter)); // 输出:['hello', 'world']
console.log('one two three'.split(splitter)); // 输出:['one', 'two', 'three']
Symbol.toPrimitive

Symbol.toPrimitive 被用来将对象转换为相应的原始值。

let obj = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 42;
}
if (hint === 'string') {
return 'hello';
}
return true;
},
};

console.log(+obj); // 输出:42
console.log(`${obj}`); // 输出:hello
console.log(obj + ''); // 输出:true
Symbol.unscopables

Symbol.unscopables 是一个对象,它指定了在 with 语句中不可用的属性名称。

let obj = {
foo: 1,
bar: 2,
[Symbol.unscopables]: {
bar: true,
},
};

with (obj) {
console.log(foo); // 输出:1
console.log(bar); // ReferenceError: bar is not defined
}

Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称 来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:

let o = new Object();

// new Object 虽然不传递参数,可以省略括号,但不推荐这样做

ECMAScript 中的 Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生 的对象上同样存在。

每个 Object 实例都有如下属性和方法。

  • constructor: 用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object() 函数。
  • hasOwnProperty(propertyName): 用于判断当前对象实例(不是原型)上是否存在给定的属性。
  • isPrototypeOf(object): 用于判断当前对象是否为另一个对象的原型。
  • propertyIsEnumerable(propertyName): 用于判断给定的属性是否可以使用for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
  • toLocaleString(): 返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
  • toString(): 返回对象的字符串表示。
  • valueOf(): 返回对象对应的字符串、数值或布尔值表示。通常与 toString() 的返回值相同。
备注

因为在 ECMAScript 中 Object 是所有对象的基类,所以任何对象都有这些属性和方法。

操作符

ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、 数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf()toString() 方法来取得可以计算的值。

一元操作符

只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是 ECMAScript 中最简单的操作符。

递增/递减操作符

递增和递减操作符直接照搬自 C 语言,但有两个版本:前缀版和后缀版。顾名思义,前缀版就是位 于要操作的变量前头,后缀版就是位于要操作的变量后头。

前缀版
let age = 18;
++age;
console.log(age); // 19

--age;
console.log(age); // 18

// 副作用
// 非独立的表达式中,存在副作用:前缀版会优先进行递增或递减运算,在进行其他计算
let num = 20;
let sum = ++num + 10;
console.log(sum); // 31

sum = --num + 10;
console.log(sum); // 30

// 等同
num = num + 1;
sum = num + 10;

num = num - 1;
sum = num + 10;
后缀版
let age = 18;
age++;
console.log(age); // 19

age--;
console.log(age); // 18

// 副作用
// 非独立的表达式中,存在副作用:后缀版会优先完成表达式计算,再进行递增或递减运算
let num = 20;
let sum = 10 + num++;
console.log(sum); // 30

sum = 10 + num--;
console.log(sum); // 31

// 等同
sum = num + 10;
num = num + 1;

sum = num + 10;
num = num - 1;

一元加和减

一元加和减操作符对大多数开发者来说并不陌生,它们在 ECMAScript 中跟在高中数学中的用途一样。

let age = 18;
age = age + 1;
// 等同
age += 1;

age = age - 1;
//等同
age -= 1;

一元加和减操作符可以对数据进行隐式转换。

let s1 = '01';
let s2 = '1.1';
let s3 = 'z';
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
},
};

console.log(+s1); // 1
console.log(+s2); // 1.1
console.log(+s3); // NaN
console.log(+b); // 0
console.log(+f); // 1.1
console.log(+o); // -1

// 使用一元减和一元加效果类似,不过会在数值前加上 - 号
console.log(-s1); // -1
console.log(-o); // 1

一元加由一个加号(+)表示,放在变量前头,对数值没有任何影响。一元减由一个减号(-)表示,放在变量前头,主要用于把数值变成负值。

位操作符

布尔操作符

对于编程语言来说,布尔操作符跟相等操作符几乎同样重要。如果没有能力测试两个值的关系,那么像 if-else 和循环这样的语句也没什么用了。布尔操作符一共有 3 个:逻辑非 !、逻辑与 && 和逻辑或 ||

逻辑非

逻辑非操作符由一个叹号(!)表示,可应用给 ECMAScript 中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。

// 操作数是对象
console.log(!{}); // false

// 操作数是空字符串
console.log(!''); // true

// 操作数是非空字符串
console.log(!'message'); // false

// 操作数是数值 0
console.log(!0); // true

// 操作数是数值 非0
console.log(!1212132); // false
console.log(!Infinity); // false

// 操作数是 null
console.log(!null); // true

// 操作数是 NaN
console.log(!NaN); // true

// 操作数是 undefined
console.log(!undefined); // true

逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函数 Boolean()。无论操作数是什么类型

逻辑与

逻辑与操作符由两个和号(&&)表示,应用到两个值。逻辑与操作符遵循如下真值表:

第一个操作数第二个操作数结果
truetruetrue
falsetruefalse
truefalsefalse
falsefalsefalse

逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值,而是遵循如下规则。

  • 如果第一个操作数是对象,则返回第二个操作数。
  • 如果第二个操作数是对象,则只有第一个操作数求值为 true 才会返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数。
  • 如果有一个操作数是 null,则返回 null。
  • 如果有一个操作数是 NaN,则返回 NaN。
  • 如果有一个操作数是 undefined,则返回 undefined。

逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。

let found = false;
let result = found && someUndeclaredVariable; // 不会出错
console.log(result); // 会执行

/*
变量 found 的值是 true,逻辑与操作符会继续求值变量 someUndeclaredVariable。
但是由于 someUndeclaredVariable 没有定义,不能对它应用逻辑与操作符,因此就报错了。
*/
let found = true;
let result = found && someUndeclaredVariable; // 这里会出错
console.log(result); // 不会执行这一行

逻辑或

逻辑或操作符由两个管道符(||)表示, 逻辑或操作符遵循如下真值表:

第一个操作数第二个操作数结果
truetruetrue
falsetruetrue
truefalsetrue
falsefalsefalse

与逻辑与类似,如果有一个操作数不是布尔值,那么逻辑或操作符也不一定返回布尔值。它遵循如下规则:

  • 如果第一个操作数是对象,则返回第一个操作数。
  • 如果第一个操作数求值为 false,则返回第二个操作数。
  • 如果两个操作数都是对象,则返回第一个操作数。
  • 如果两个操作数都是 null,则返回 null。
  • 如果两个操作数都是 NaN,则返回 NaN。
  • 如果两个操作数都是 undefined,则返回 undefined。

同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为 true,第二个操作数就不会再被求值了。

let found = true;
let result = found || someUndeclaredVariable; // 不会出错
console.log(result); // 会执行

乘性操作符

ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模。这些操作符跟它们在 Java、C 语言及 Perl 中对应的操作符作用一样,但在处理非数值时,它们也会包含一些隐式类型转换。如果乘性操作符有 不是数值的操作数,则该操作数会在后台被使用 Number() 转型函数转换为数值。这意味着空字符串会 被当成 0,而布尔值 true 会被当成 1。

乘法操作符

乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。

let result = 34 * 56;

乘法操作符在处理特殊值时也有一些特殊的行为:

  • 如果操作数都是数值,则执行常规的乘法运算,即两个正值相乘是正值,两个负值相乘也是正值,正负符号不同的值相乘得到负值。如果 ECMAScript 不能表示乘积,则返回 Infinity 或 -Infinity。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 乘以 0,则返回 NaN。
  • 如果是 Infinity 乘以非 0 的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。
  • 如果是 Infinity 乘以 Infinity,则返回 Infinity。
  • 如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则。

除法操作符

除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商,比如:

let result = 66 / 11;

除法操作符针对特殊值也有一些特殊的行为:

  • 如果操作数都是数值,则执行常规的除法运算,即两个正值相除是正值,两个负值相除也是正值,符号不同的值相除得到负值。如果 ECMAScript 不能表示商,则返回 Infinity 或-Infinity。
  • 如果有任一操作数是 NaN,则返回 NaN。
  • 如果是 Infinity 除以 Infinity,则返回 NaN。
  • 如果是 0 除以 0,则返回 NaN。
  • 如果是 Infinity 除以任何数值,则根据第二个操作数的符号返回 Infinity 或-Infinity。 6
  • 如果是非 0 的有限值除以 0,则根据第一个操作数的符号返回 Infinity 或-Infinity。
  • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。

取模操作符

取模(求余数)操作符由一个百分比符号(%)表示,比如:

let result = 26 % 5; // 等于 1

取模操作符对特殊值也有一些特殊的行为:

  • 如果操作数是数值,则执行常规除法运算,返回余数。
  • 如果被除数是无限值,除数是有限值,则返回 NaN。
  • 如果被除数是有限值,除数是 0,则返回 NaN。
  • 如果被除数是 0,除数不是 0,则返回 0。
  • 如果是 Infinity 除以 Infinity,则返回 NaN。
  • 如果被除数是有限值,除数是无限值,则返回被除数。
  • 如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。

指数操作符

ECMAScript 7 新增了指数操作符,Math.pow() 现在有了自己的操作符 **,结果是一样的:

console.log(Math.pow(3, 2)); // 9
console.log(3 ** 2);

不仅如此,指数操作符也有自己的指数赋值操作符**=,该操作符执行指数运算和结果的赋值操作:

let squared = 3;
squared **= 2;
console.log(squared); // 9

加性操作符

加法操作符

加法操作符(+)用于求两个数的和。

let result = 1 + 2;

减法操作符

减法操作符(-)也是使用很频繁的一种操作符,比如:

let result = 2 - 1;

关系操作符

关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值,如下所示:

let result1 = 5 > 3; // true
let result2 = 5 < 3; // false

相等操作符

等于和不等于

ECMAScript 中的等于操作符用两个等于号(==)表示,不等于操作符用叹号和等于号(!=)表示。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。

下表总结了一些特殊情况及比较的结果:

表达式结果
null == undefinedtrue
"NaN" == NaNfalse
5 == NaNfalse
NaN == NaNfalse
NaN != NaNtrue
false == 0true
true == 1true
true == 2false
undefined == 0false
null == 0false
"5" == 5true

全等和不全等

全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。

全等操作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true。

console.log('5' === 5); // false
console.log(false === 0); // false
推荐

由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。 这样有助于在代码中保持数据类型的完整性。

条件操作符

条件操作符,也称为三元操作符,是 JavaScript 中唯一的三元运算符(接受三个操作数)。它的语法非常简洁,用于基于一个条件表达式来选择两个值之一。

语法
let variable = boolean_expression ? true_value : false_value;

根据条件表达式 boolean_expression 的值决定将哪个值赋给变量 variable 。 如果 boolean_expression 是 true , 则赋值 true_value; 如果 boolean_expression 是 false,则赋值 false_value。

示例
let num1 = 10;
let num2 = 20;
let max = num1 > num2 ? num1 : num2;
console.log(max); // 20

赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,如下所示:

let num = 10;

复合赋值使用乘性、加性或位操作符后跟等于号(=)表示,这些赋值操作符是类似如下常见赋值 操作的简写形式:

let num = 10;

num = num + 10;
// 等价
num += 10;

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:

  • 乘后赋值(*=)
  • 除后赋值(/=)
  • 取模后赋值(%=)
  • 加后赋值(+=)
  • 减后赋值(-=)
  • 左移后赋值(<<=)
  • 右移后赋值(>>=)
  • 无符号右移后赋值(>>>=)

逗号操作符

逗号操作符可以用来在一条语句中执行多个操作,如下所示:

在一条语句中同时声明多个变量是逗号操作符最常用的场景。

let num1 = 1,
num2 = 2,
num3 = 3;

使用逗号操作符来辅助赋值。在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:

let num = (5, 1, 4, 8, 0); // num的值为0
备注

在这个例子中,num 将被赋值为 0,因为 0 是表达式中最后一项。逗号操作符的这种使用场景并不 多见,但这种行为的确存在。

语句

ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,复杂的如列出一堆要重复执行的指令。

if 语句

if 语句是使用最频繁的语句之一。

语法
if (condition) {
// 代码逻辑
} else {
// 代码逻辑
}

do-while 语句

do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句话说,循环体内的代码至少执行一次。

语法
do {
statement;
} while (expression);

while 语句

while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循 环体内的代码有可能不会执行。

语法
while (expression) {
statement;
}

for 语句

for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式。

for (初始化; 条件表达式; 循环后表达式) {
statement;
}

// 示例
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

for (;;) {
// 无穷循环 doSomething();
}

如果只包含条件表达式,那么 for 循环实际上就变成了 while 循环:

let count = 10;
let i = 0;
for (; i < count; ) {
console.log(i);
i++;
}

这种多功能性使得 for 语句在这门语言中使用非常广泛。

for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性:

语法
for (property in expression) {
statement;
}

// 示例
for (const propName in window) {
document.write(propName);
}

ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序。

for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素:

语法
for (property of expression) {
statement;
}

// 示例
for (const el of [2, 4, 6, 8]) {
console.log(el);
}

for-await-of 语句

for-await-of 语句是 JavaScript 中的一种循环结构,用于遍历异步可迭代对象。它在 for-of 语句的基础上扩展,允许在循环中等待每一个异步操作的完成。for-await-of 主要用于处理返回 Promise 对象的异步迭代器。

语法
for await (const variable of iterable) {
// 代码块
}
  • variable:在每次迭代时,将从可迭代对象中提取的值赋给这个变量。
  • iterable:一个异步可迭代对象,比如一个实现了 Symbol.asyncIterator 方法的对象。

注意事项:

  1. for-await-of 只能用于异步函数或模块的顶层(使用 async 关键字的函数)。
  2. 被迭代的对象必须是异步可迭代对象,即必须实现 [Symbol.asyncIterator] 方法。
  3. 使用 for-await-of 循环可以简化对异步操作的处理,不需要手动处理每一个异步操作的完成状态。

标签语句

在 JavaScript 中,标签语句(Label Statement)是一种语句,可以与 breakcontinue 语句结合使用,以控制程序的执行流程。标签语句通过在语句前添加标签来标识特定的代码块。

// labelName:任意有效的标识符,作为标签的名称。
// statement:带有标签的语句,可以是任意的 JavaScript 语句。
labelName: statement;

标签语句主要用于与 break 和 continue 语句一起使用,以控制多层循环或代码块的跳出。

outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
console.log(`i = ${i}, j = ${j}`);
if (i === 1 && j === 1) {
break outerLoop;
}
}
}
console.log('Done');
// 输出:
// i = 0, j = 0
// i = 0, j = 1
// i = 0, j = 2
// i = 1, j = 0
// i = 1, j = 1
// Done

标签语句的典型应用场景是嵌套循环。通过理解和合理使用标签语句,可以有效地控制代码的执行流程,特别是在处理复杂的嵌套结构时。

break 和 continue

breakcontinue 语句为执行循环代码提供了更严格的控制手段。其中,break 语句用于立即退出循环,强制执行循环后的下一条语句。而 continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。

let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
break;
}
num++;
}
console.log(num); // 循环被完整执行了 4 次

let num2 = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
continue;
}
num2++;
}
console.log(num2); // 循环被完整执行了 8 次

with 语句

with 语句是 JavaScript 中的一种语句,用于扩展语句的作用域链,使其包含某个对象的属性。它通常用于简化代码书写,使得在访问对象的属性时不需要反复引用对象本身。然而,由于其对性能和代码可读性的影响,with 语句在现代 JavaScript 开发中不推荐使用,并且在严格模式下已经被禁用

使用 with 语句可以简化对对象属性的多次访问:

let obj = {
a: 1,
b: 2,
c: 3,
};

with (obj) {
console.log(a); // 输出: 1
console.log(b); // 输出: 2
console.log(c); // 输出: 3
}
危险

由于 with 语句影响性能且难于调试其中的代码,通常不推荐在开发中使用 with 语句。

switch 语句

switch 语句是与 if 语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript 中 switch 语句跟 C 语言中 switch 语句的语法非常相似,如下所示:

switch (expression) {
case value1:
break;
case value2:
statement;
break;
case value3:
statement;
break;
case value4:
statement;
break;
default:
statement;
}

这里的每个 case(条件/分支)相当于: 如果表达式等于后面的值,则执行下面的语句。break 关键字会导致代码执行跳出 switch 语句。如果没有 break,则代码会继续匹配下一个条件。default 关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句)。

为避免不必要的条件判断,最好给每个条件后面都加上 break 语句。如果确实需要连续匹配几个条件,那么推荐写个注释表明是故意忽略了 break,如下所示:

switch (i) {
case 25:
/*跳过*/
case 35:
console.log('25 or 35');
break;
case 45:
console.log('45');
break;
default:
console.log('Other');
}

虽然 switch 语句是从其他语言借鉴过来的,但 ECMAScript 为它赋予了一些独有的特性。首先,switch 语句可以用于所有数据类型(在很多语言中,它只能用于数值),因此可以使用字符串甚至对象。 其次,条件的值不需要是常量,也可以是变量或表达式。

注意

switch语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型。

函数

函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。

可以通过函数名来调用函数,要传给函数的参数放在括号里(如果有多个参数,则用逗号隔开)。

语法
function functionName(arg0, arg1, ...arg2) {
statements;
}

functionName(1, 2, 3);

ECMAScript 中的函数不需要指定是否返回值。任何函数在任何时间都可以使用 return 语句来返 回函数的值,用法是后跟要返回的值。

function sum(num1, num2) {
return num1 + num2;
}

要注意的是,只要碰到 return 语句,函数就会立即停止执行并退出。因此,return 语句后面的 代码不会被执行。

return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined。这种用法最常 用于提前终止函数执行,并不是为了返回值。

function sayHi(name, message) {
return;
console.log('Hello ' + name + ', ' + message); // 不会执行s
}

console.log(sayHi()); // undefined
最佳实践

最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来 麻烦,尤其是调试时。