引言
历经两个多月断断续续地读完了JavaScript的经典书籍红宝书 -《JavaScript高级程序设计(第4版)
详细地读完一遍后发觉整本书知识点全而泛,乍一想每一章的细节,还是略显模糊。
于是督促自己计划编写每一章的着重点再次加深印象和理解,顺便记录自己的所学所想所悟。方便自身利用电脑的快速搜索关键词来进行快速定位和学习,也希望能帮助到有需要的同学们哈。
若是想要系统仔细的学习,当然还是看原书比较好,我也是强烈推荐的噢!这里内容只当个人复习和总结。
提示: 一些个人主观认为不重要或不流行的章节将进行删减
3. 语言基础
ECMA-262 以一个名为 ECMAScript 伪语言的形式,定义了 JavaScript 的语法、操作符、数据类型和内置功能等。到2017年底,大多数主流浏览器几乎或全部实现了这一版的规范,为此,本章内容主要基于 ECMAScript 第6版。
3.1 语法
ECMAScript 的语法很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl。
3.1.1 区分大小写
在 ECMAScript 中一切都区分大小写,无论是变量、函数名还是操作符。类似地,typeof 不能作为函数名(因为是一个关键字),但 Typeof 是一个完全有效地函数名。
3.1.2 标识符
标识符就是变量、函数、属性或函数参数的名称。可以由一或多个下列字符组成:
- 第一个字符必须是一个字母、下划线(_)或美元符号($)。
- 剩下的其他字符可以是字母、下划线、美元符号或数字。
标识符中的字母可以是扩展 ASCII 中的字母,也可以是 Unicode 的字母字符(但不推荐使用)。
ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写。如: firstSecond、myCar。
关键字、保留字、true、false 和 null 不能作为标识符。
3.1.3 注释
ECMAScript 采用 C 语言风格的注释,包括单行注释和块注释。
-
// 单行注释 -
/* 这是多行 注释 */
3.1.4 严格模式
ECMAScript 5 增加了严格模式的概念。ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。
在脚本文件的开头加上这一行即可声明: "use strict"; (一个预处理指令)
也可以单独指定一个函数在严格模式下执行,把预处理指令放到函数体开头即可:
function doSomething() {
"use strict";
// 函数体
}
严格模式会影响 JavaScript 执行的很多方面且所有现代浏览器都支持严格模式。
3.1.5 语句
-
分号结尾
-
let sum = a + b // 无分号有效,不推荐 -
let diff = a - b; // 加分号有效,推荐- 让解析器确定语句在哪里结尾,有助于防止省略造成的问题。
- 解析器会尝试在合适的位置补上分号以纠正语法错误,有助于在某些情况下提升性能。
-
-
代码块: 由一个左花括号({)标识开始,一个右花括号(})标识结束。
-
if (test) // 单条语句有效,但不推荐 console.log(test); -
if (test) { // 推荐 console.log(test); }- 在控制语句中使用代码块可以让内容更清晰,减少出错的可能性。
-
3.2 关键字与保留字
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
-
3.3 变量
ECMAScript 变量是松散类型,是指变量可以用于保存任何类型的数据。有 3 个关键字可以声明变量:var、const 和 let。其中 var 在ECMAScript 所有版本中均可使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。
3.3.1 var 关键字
var message; // 定义了名为 message 的变量,可以用它保存任何类型的值。(不初始化的情况下,变量会保存一个特殊值 undefined)
var message = "hi";
message = 100; // 合法,但不推荐改变数据值的类型
var messgae = "hi",
found = false,
age = 29; // 如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量
3.3.1.1 var 声明作用域
使用 var 操作符定义的变量会成为包含它的函数的局部变量。
function test() {
var message = "h1"; // 局部变量
}
test();
console.log(message); // 出错
3.3.1.2 var 声明提升
var 关键字声明的变量会自动提升到函数作用域顶部:
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
// 之所以不会出错,是因为等效于如下
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
// 因此,可以反复多次使用 var 声明同一个变量也没有问题,后者将覆盖前者。
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
3.3.2 let 声明
let 关键字声明与 var 作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
// 函数作用域
function foo() {
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
}
foo();
// 块作用域
function foo() {
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age没有定义
}
let 也不允许用一个块作用域中出现重复声明。
var name;
var name; // 不会报错
let age;
let age; // SyntaxError: 标识符 age 已经声明过了
var name;
let name; // SyntacError
3.3.2.1 暂时性死区
let 声明的变量不会在作用域中被提升。在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出错误。
console.log(name); // undefined
var name = 'Matt';
console.log(age); // ReferenceError: age 没有定义
let age = 26;
3.3.2.2 全局声明
var name = 'Matt'; // var 声明的变量会成为 window 对象的属性
console.log(window.name); // 'Matt'
let age = 26; // let 声明的变量不会
console.log(window.age); // undefined
let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。
3.3.2.4 for 循环中的 let 声明
像类似于 for、for-in 和 for-of 循环的块作用域,var 和 let 声明变量通常会导致结果大为不同。
// var 是函数作用域,因此所有 i 一直为同一个变量
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
} // 5、5、5、5、5
// let 是块作用域,每次迭代循环都会声明一个新的迭代变量
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
} // 0、1、2、3、4
3.3.3 const 声明
const 声明与 let 声明基本一致,区分其重要区别如下:
// 必须同时初始化变量
const age; // SyntaxError
// 不能重复赋值
const age = 26;
age = 36; // TypeError
const 声明的限制只适用于它指向的变量的引用,若 const 变量引用的是一个对象,那么是可以修改这个对象内部的属性。
const person = {};
person.name = 'Matt';
3.3.4 声明风格及最佳实践
- 不使用 var
- 有了 let 和 const,开发者应该限制自己只使用 let 和 const 有助于提升代码质量。
- 有明确的作用域
- 声明位置
- 不变的值
- 有了 let 和 const,开发者应该限制自己只使用 let 和 const 有助于提升代码质量。
- const 优先,let 次之
- 开发者应优先使用 const 来声明变量,只会提前知道未来会有修改时,再使用 let。
- 让浏览器运行时强制保持变量不变
- 让静态代码分析工具提前发现不合法的赋值操作
- 更能迅速发现因意外赋值导致的非预期行为
- 开发者应优先使用 const 来声明变量,只会提前知道未来会有修改时,再使用 let。
3.4 数据类型
简单的数据类型(也称为原始类型)
- ES6之前
- Undefined、Null、Number、String、Boolean
- ES6新增
- Symbol(符号)
- ES10新增
- BigInt
引用数据类型: Object(对象)是一种无序名值对的集合。
3.4.1 typeof 操作符
因为 ECMAScript 的类型系统是松散,使用 typeof 操作符返回下列字符串之一
- "undefined" 表示值未定义
- "boolean" 表示值为布尔值
- "string" 表示值为字符串
- "number" 表示值为数值
- "object" 表示值为对象或 null
- "function" 表示值为函数
- "symbol" 表示值为符号
console.log(typeof "abc"); // "string"
console.log(typeof 123); // "number"
- typeof 是一个操作符而不是函数,所以不需要参数。
- 调用 typeof null 返回的是 "object",这是因为特殊值 null 被认为是一个对空对象的引用。
- 严格来说,函数在 ECMAScript 中被认为是对象,并非一种数据类型。不过函数也有自己的特殊属性,因此有必要通过 typeof 操作符来区分函数和其他对象。
3.4.2 Undefined 类型
Undefined 数据类型只有一个特殊值: undefined。
当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋于了 undefined 值。
let message; // 这个变量被声明,被赋于了 undefined 值
if (message) {
// 这个块不会执行
}
if (!message) {
// 这个块会执行
}
if (age) { // 未声明变量
// 这里会报错
}
3.4.3 Null 类型
Null 数据类型只有一个特殊值: null。
逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 "object" 的原因。因此定义将来要保存对象值的变量时,建议使用 null 来初始化。
let cat = null;
console.log(typeof cat); // "object"
- undefined 值是由 null 值派生而来的,因此 console.log(null == undefined); // true
3.4.4 Boolean 类型
Boolean 数据类型有两个字面值: true 和 false(区分大写)。
在所有其他 ECMAScript 类型的值都有相应布尔值的等价形式,调用特定的 Boolean() 转型函数或 !! 隐形转换。
| 数据类型 | 转换为 true 的值 | 转换为 false 的值 |
|---|---|---|
| Boolean | true | false |
| String | 非空字符串 | " "(空字符串) |
| Number | 非零数值 | 0、NaN |
| Object | 任意对象 | null |
| Undefined | N/A(无) | undefined |
3.4.5 Number 类型
Number 类型使用 IEEE 754 格式表示整数和浮点数(在某些语言中也叫双精度值)。
-
十进制整数:直接写出即可
- let intNum = 55;
-
八进制整数:第一个数字必须是零(0)+ 相应的八进制数字(0~7)。否则将当成十进制处理。
-
let octalNum1 = 070; // 八进制 56 let octalNum2 = 079; // 无效八进制值,当成 79 处理
-
-
十六进制整数:前缀 0x(区分大小写)+ 相应十六进制数字(0~9 以及 A~F)不分大小写。
-
let hexNum1 = 0xA; // 十六进制 10 let hexNum2 = 0x1f; // 十六进制 31 let hexNum3 = 0xg; // 无效十六进制,报错
-
由于 JavaScript 保存数值的方式,实际中可能存在正零(+0)和负零(-0),在所有情况下都被认为是等同的。
console.log(-0 == 0) // true console.log(-0 === 0) // true
3.4.5.1 浮点值
定义浮点值,数值中必须包含小数点。
-
let floatNum1 = 0.1; let floatNum2 = .1; // 有效,但不推荐
因为存储浮点值使用的内存空间是存储整数值的两倍,估 ECMAScript 总是想方设法转换为整数:
-
let floatNum1 = 1.; // 整数 1 处理 let floatNum2 = 10.0; // 整数 10 处理
浮点数的精确值最高可达 17 位小数,但在算术计算中因机器码的浮点操作,往往会造成微小的舍入错误。
如 0.1 + 0.2 得不到 0.3,而是0.300 000 000 000 000 04。故不要测试某个特定的浮点值
3.4.5.2 值的范围
-
ECMAScript 可以表示的最小的数值:Number.MIN_VALUE 为 5e-324。
-
最大的数值:Number.MAX_VALUE 为 1.797 693 134 862 315 7e+308。
-
若某个计算超出了 JavaScript 可以表示的范围,将会自动转换为特殊的 Infinity 和 -Infinity,该值将不能再进一步用于任何计算。
-
要确定一个值是不是有限大(介于 JavaScript 能表示的范围中),使用 isFinite() 函数
-
let InfinityNum = Number.MAX_VALUE + Number.MAX_VALUE; console.log(isFinite(InfinityNum)); // 如果参数是 NaN,正无穷大或者负无穷大,会返回 false,其他返回 true。
-
3.4.5.3 NaN
NaN:不是数值(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)如
-
0、+0 或 -0 相除会返回 NaN
-
console.log(0/0); // NaN console.log(-0/+0); // NaN
-
-
若分子非 0 值,分母为 0 或 -0 则返回无穷值
-
console.log(5/0); // Infinity console.log(5/-0); // -Infinity
-
NaN 不等于包括 NaN 在内的任何值
console.log(NaN == NaN) // false
console.log(NaN === NaN) // false
// 可使用 isNaN() 函数接收任意类型的一个参数,将进行隐式转换返回 Boolean 值。
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10是数值
console.log(isNaN("10")); // false,可以转换为数值 10
console.log(isNaN(true)); // false,可以转换为数值 1
console.log(isNaN("blue")); // true,不可以转换为数值
3.4.5.4 数值转换
Number()、parseInt() 和 parseFloat(),可以将非数值转换为数值。
-
Number() 函数基于如下规则执行转换。
-
布尔值,true 转换为 1,false 转换为 0。
-
数值,直接返回。
-
null,返回 0.
-
undefined,返回 NaN。
-
字符串,应用如下规则。
-
字符串为数值字符(包含前缀有 +、- 情况),转换为一个十进制数值。
-
Number("1"); // 1 Number("0123"); // 123
-
-
字符串包含有效浮点值格式,则会转换为相应的浮点值。
-
Number("0.1"); // 0.1 Number("00.123"); // 0.123
-
-
字符串包含有效的十六进制格式数值,则会转换为与该进制对应的十进制整数数值。
-
Number("0xf"); // 15
-
-
字符串为空字符串,则返回 0。
-
字符串包含除上述情况之外的其他字符,则返回 NaN。
-
-
对象,调用 valueof() 方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用 toString() 方法,再按照转换字符串规则转换。
-
一元加操作符与 Number() 函数遵循相同的转换规则。
-
-
parseInt() 函数在需要得到整数时可以优先使用。第一个参数接收一个字符串,第二个参数接收指定底数(进制数)。
-
字符串前面的空格会被忽略,从第一个非空格字符开始转换。
-
字符串第一个非空格字符不是数值字符、加号或减号,将立即返回 NaN。
-
字符串第一个非空格字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
-
字符串为不同进制格式的数值时,也可以被该函数解释为对应进制格式整数。
-
let num1 = parseInt(" 123"); // 123 let num2 = parseInt("1234blue"); // 1234 let num3 = parseInt(""); // NaN let num4 = parseInt("0xA"); // 10,解释为十六进制整数 let num5 = parseInt("22.5"); // 22 let num6 = parseInt("70"); // 70 -
通过第二个参数,可以极大扩展转换后获得的结果类型
-
let num1 = parseInt("AF", 16); // 175 let num2 = parseInt("AF"); // NaN,未指定底数不能省略前缀。 let num3 = parseInt("10", 2); // 2, 按二进制解析。 let num4 = parseInt("10", 8); // 8, 按八进制解析。 let num5 = parseInt("10", 10); // 10, 默认按十进制解析。 let num6 = parseInt("10", 16); // 16, 按十六进制解析。
-
-
-
parseFloat() 函数与上述 parseInt() 函数类似。主要区别如下
-
可以解析小数点,但仅有第一次出现的小数点有效。
-
只解析十进制值,因此不能指定底数。
-
若字符串数值为整数,JavaScript 为节省空间内存将自动转为整数。
-
let num1 = parseFloat(" 1.23"); // 1.23 let num2 = parseFloat("01.234blue"); // 1.234 let num3 = parseFloat(""); // NaN let num4 = parseFloat("0xA"); // 0,无法解析进制数。 let num5 = parseFloat("22.34.5"); // 22.34,第二个小数点当作普通字符处理。 let num6 = parseFloat("3.125e7"); // 31250000
-
3.4.6 String 类型
String 数据类型表示零或多个 16 位 Unicode 字符序列,字符串可以如下三种形式表示:
let str1 = "Lindada";
let str2 = 'Lindada';
let str3 = `Lindada`;
ECMAScript 语法中表示三种形式的字符串对字符串的解释与某些语言不同,呈现的结果是相同的。
3.4.6.1 字符字面量
字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表示:
| 字面量 | 含义 |
|---|---|
| \n | 换行 |
| \t | 制表 |
| \b | 退格 |
| \r | 回车 |
| \f | 换页 |
| \\ | 反斜杠(\) |
| \' | 单引号(') |
| \" | 双引号(") |
| \` | 反引号(`) |
| \xnn | 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如 \x4 等于 "A" |
| \unnnn | 以十六进制编码 nnnn 表示的 Unicode 字符,例如 \u03a3 等于希腊字符"Σ" |
字符串的长度可以通过其 length 属性获取,但如果字符串中包含双字节字符,那么 length 属性返回的值可能不是准确的字符数,后边第 5 章将具体讨论。
3.4.6.2 字符串的特点
ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,值便无法修改了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。
let lang = "Java";
lang += "Script";
/**
* 变量 lang 一开始为 "Java" 字符串,随后 lang 变量被修改定义为 "JavaScript" 字符串。
* 这意味着修改的整个过程首先会先分配一个足够容纳 10 个字符的空间,填充上 "Java" 和 "Script",最后销毁原始的字符串 "Java" 和 "Script"。
* 所有处理均在后台发生完成。
*/
3.4.6.3 转换为字符串
有两种方式把一个值转换为字符串,toString() 方法和 String 转型函数。
-
toString():返回当前值的字符串等价物。
-
let age = 11; let age2Str = age.toString(); // "11" let found = true; let found2Str = found.toString(); // "true"- 几乎所有数据类型的值均有 toString() 方法,但除了 null 和 undefined 值没有 toString() 方法。
-
一般情况下该方法不接收任何参数,不过在对数值类型数据调用该方法时,可以传入底数(进制数)来进行转换。
-
let num = 10; num.toString(); // "10" num.toString(2); // "1010" num.toString(8); // "12" num.toString(10); // "10",默认以 10 为底。 num.toString(16); // "a"
-
-
-
String():当不确定一个值是否具有 toString() 方法时(null 或 undefined),可以使用 String() 转型函数。
-
函数遵循如下规则:
- 如果值有 toString() 方法,则直接调用该方法(不传参数)并返回结果。
- 如果值是 null,返回 "null"。
- 如果值是 undefined,返回 "undefined"。
-
如下例子:
-
String(10); // "10" String(true); // "true" String(null); // "null" String(undefined); // "undefined"
-
-
用加号操作符给一个值加上 ""(空字符串)也可以将其转换为字符串。
3.4.6.4 模版字面量
ECMAScript 6 新增模版字面量定义字符串的能力,保留换行字符,可以跨行定义字符串:
// 结果一致
let multiLineStr = 'first line\nsecond line';
let multiLineTemplateLiteral = `first line
second line`;
// frist line
// second line
multiLineStr === multiLineTemplateLiteral; // true
模版字面量在定义模版时特别有用,比如下面这个 HTML 模版:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>
由于模版字面量会保持反引号内部的空格,所以格式正确的模版字符串应不带有缩进。
let multiLineTemplateLiteral = `first line
second line`;
console.log(multiLineTemplateLiteral.length); // 38
3.4.6.5 字符串插值
技术上讲,模版字面量不是字符串,而是一种特殊的 JavaScript 语法表达式。在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
-
字符串插值通过在 ${} 中使用一个 JavaScript 表达式实现:
-
let value = 5; let str = 'second'; // 以前,实现字符串插值。 let interpolatedStr = value + ' to the ' + str + 'power is ' + (value * value); // 5 to the second power is 25 // 现在,使用模版字面量 let interpolatedTemplateLiteral = `${value} to the ${str} power is ${value * value}`; // 5 to the second power is 25
-
-
所有插入的值都会使用 toString() 强制转型为字符串。
-
在插值表达式中可以调用函数和方法:
-
function capitalize(word) { return `${word[0].toUpperCase()} ${word.slice(1)}`; } console.log(`${capitalize('hello')}, ${capitalize('world')}!`); // Hello, World!
-
3.4.6.6 模版字面量标签函数
模版字面量支持定义标签函数,标签函数会接收被插值记号分隔后的模版和对每个表达式求值的结果:
let a = 6;
let b = 9;
function simpleTag(strings, aVal, bVal, sumVal) {
console.log(strings);
console.log(aVal);
console.log(bVal);
console.log(sumVal);
return 'value';
}
let untaggedRes = `${a} + ${b} = ${a + b}`;
let taggedRes = simpleTag`${a} + ${b} = ${a + b}`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedRes); // "6 + 9 = 15"
console.log(taggedRes); // "value"
若不确定标签函数中的参数,可以使用剩余操作符(...):
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings[0])
return strings[0] +
expressions.map((v, i) => {
console.log(`${v}${strings[i + 1]}`)
return `${v}${strings[i + 1]}`})
.join('');
}
let untaggedRes = `${a} + ${b} = ${a + b}`;
let taggedRes = simpleTag`${a} + ${b} = ${a + b}`;
// ""
// "6 + "
// "9 = "
// "15"
console.log(untaggedRes); // "6 + 9 = 15"
console.log(taggedRes); // "6 + 9 = 15"
3.4.6.7 原始字符串
使用模版字面量也可以直接获取原始的模版字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。为此可以使用默认的 String.raw 标签函数:
// Unicode 示例
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
也可通过标签函数的第一个参数,即字符串数组的 .raw 属性取得每个字符串的原始内容:
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters:');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${'and'}\n`;
// Actual characters:
// ©
// (换行符)
// Escaped characters:
// \u00A9
// \n
3.4.7 Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号的实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,进而用作非字符串形式的对象属性,不会发生属性冲突的危险。
3.4.7.1 符号的基本用法
符号需要使用 Symbol() 函数初始化,可以传入一个字符串参数对符号的描述,可通过这个描述来调试代码,但描述与符号定义或标识完全无关:
// 定义 Symbol
let sym = Symbol();
console.log(typeof sym); // symbol
let sym1 = Symbol();
let sym2 = Symbol();
console.log(sym1 === sym2); // false
let sym3 = Symbol('foo');
let sym4 = Symbol('foo');
console.log(sym3 === sym4); // false
Symbol() 函数不能用作构造函数,与 new 关键字使用,为避免创建符号包装对象。
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
// 若确实需要符号包装对象,可以借助 Object() 函数
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"
3.4.7.2 使用全局符号注册表
-
Symbol.for() 方法:运行时的不同部分需要共享和重用符号的实例,那么可以使用一个符号串作为键。
-
let globalSymbol = Symbol.for('foo'); // 后续使用相同字符串的调用会检查注册表,但会对应该符号的实例 let otherGlobalSymbol = Symbol.for('foo'); // 即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也并不相同 let localSymbol = Symbol('foo'); console.log(typeof fooGlobalSymbol); // symbol console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true console.log(fooGlobalSymbol === localSymbol); // false
-
-
全局注册表中的符号必须使用字符串键来创建,因此传入 Symbol.for() 的值都会被转换为字符串。
-
还可以使用 Symbol.keyFor() 来查询全局注册表
-
// 创建全局符号 let s = Symbol.for('foo'); console.log(Symbol.keyFor(s)); // foo // 创建普通符号,不是全局符号则返回 undefined let s2 = Symbol('foo'); console.log(Symbol.keyFor(s2)); // undefined // 传入的不是符号,则抛出错误 TypeError Symbol.keyFor(123); // TypeError: 123 is not a symbol
-
3.4.7.3 使用符号作为属性
-
凡事可以使用字符串或数值作为属性的地方,都可以使用符号替代。包括对象字面量属性和 Object.defineProperty() / Object.definedProperties() 定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
-
let s1 = Symbol('foo'), s2 = Symbol('bar'); let o = { [s1]: 'foo val' }; let o[s1] = 'foo val'; Object.defineProperty(o, s1, {value: 'foo val'}); Object.defineProperties(o, { [s1]: {value: 'foo value'}, [s2]: {value: 'bar value'} })
-
-
返回对象实例的属性方法
-
let s1 = Symbol('foo'), s2 = Symbol('bar'); let o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' }; Object.getOwnPropertySymbols(o); // [Symbol(foo), Symbol(bar)] Object.getOwnPropertyNames(o); // ["bar", "qux"] Object.getOwnPropertyDescriptors(o); // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} Reflect.ownKeys(o); // ["baz", "qux", Symbol(foo), Symbol(bar)]
-
3.4.7.4 常用内置符号
ECMAScript 6 引入了一批常用内置符号,用于暴露语言内部行为,开发者可以世界访问、重写或模拟这些行为。内置符号也就是全局函数 Symbol 的普通字符串属性,指向一个符号实例。
在提到 ECMAScript 规范时,会引用符号在规范中的名称,前缀为 @@。比如,@@iterator 指的就是 Symbol.iterator。
-
Symbol.hasInstance:一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用。
-
在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系
-
function Foo() {} let f = new Foo(); // 作用一致 console.log(f instanceof Foo); // true console.log(Foo[Symbol.hasInstance](f)); // true
-
-
可以在继承的类上通过静态方法重新定义这个函数。
-
class F {} class C extends F { static [Symbol.hasInstance]() { return false; } } let c = new C(); // 默认行为 console.log(c instanceof F); // true console.log(F[Symbol.hasInstance](c)); // true // 通过重写内置符号定制行为 console.log(c instanceof C); // false console.log(C[Symbol.hasInstance](c)); // false
-
-
-
Symbol.isConcatSpreadable:一个布尔值,如果是 true,则意味着对象应该使用 Array.prototype.concat() 打平其数组元素。
-
ES6 中的 Array.prototype.concat() 方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。下列覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为:
- 数组对象
- 默认情况下会被打平到已有的数组。
- false 或假值会导致整个对象被追加到数组末尾。
- 类数组对象
- 默认情况会被追加到数组末尾。
- true 或真值会导致这个类数组对象被打平到数组实例。
- 其他不是类数组对象的对象
- true 或真值将被忽略。
- 数组对象
-
代码演示上述情况:
-
// 数组对象 let initial = ['foo']; let array = ['bar']; console.log(array[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(array)); // ['foo', 'bar'] array.[Symbol.isConcatSpreadable] = false; // 改变内置符号 console.log(initial.concat(array)); // ['foo', Array(1)] -
// 类数组对象 let initial = ['foo']; let arrayLikeObject = { length: 1, 0: 'baz' }; console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(arrayLikeObject)); // ['foo', {...}] arrayLikeObject.[Symbol.isConcatSpreadable] = true; // 改变内置符号 console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz'] -
// 类数组对象 let initial = ['foo']; let otherObject = new Set().add('qux') console.log(otherObject[Symbol.isConcatSpreadable]); // undefined console.log(initial.concat(otherObject)); // ['foo', Set(1)] otherObject.[Symbol.isConcatSpreadable] = true; // 改变内置符号 console.log(initial.concat(otherObject)); // ['foo']
-
-
[].concat() 方法默认是将数组打平并拼接到数组末尾,可以使用内置符号来进行特殊需要设置。
-
-
Symbol.asyncIterator:一个方法,该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用。
-
这个由 Symbol.asyncIterator 函数生成的对象可以隐式通过异步生成器函数返回:
-
class Emitter { constructor(max) { this.max = max; this.asyncIdx = 0; } // 通过符号隐式修改 for-await-of 的默认行为 async *[Symbol.asyncIterator](){ while(this.asyncIdx < this.max) { yield new Promise((resolve) => resolve(this.asyncIdx++)); } } } async function asyncCount() { let emitter = new Emitter(5); for await(const x of emitter) { console.log(x); } } asyncCount(); // 0 // 1 // 2 // 3 // 4
-
-
-
Symbol.iterator:一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用
-
这个由 Symbol.iterator 函数生成的对象可以隐式通过生成器函数返回:
-
class Emitter { constructor(max) { this.max = max; this.Idx = 0; } // 通过符号隐式修改 for-await-of 的默认行为 async *[Symbol.iterator](){ while(this.Idx < this.max) { yield this.Idx++; } } } function count() { let emitter = new Emitter(5); for (const x of emitter) { console.log(x); } } count(); // 0 // 1 // 2 // 3 // 4 -
迭代器后续相关内容将在第 7 章详细介绍。
-
-
-
Symbol.match:一个正则表达式方法,该方法用正则表达式去匹配字符串。由 prototype.match() 方法使用。
-
String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。
-
Symbol.match 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:
-
class FooMatcher { static [Symbol.match](target) { return target.includes('foo'); } } console.log('foobar'.match(FooMatcher)); // true console.log('barbaz'.match(FooMatcher)); // false class StringMatcher { constructor(str) { this.str = str } [Symbol.match](target) { // 重置 String.prototype.match() 方法 return target.includes(this.str); } } console.log('foobar'.match(new StringMatcher('foo'))); // true console.log('barbaz'.match(new StringMatcher('qux'))); // false
-
-
-
Symbol.replace:一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace() 方法使用。
-
String.prototype.replace() 方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。
-
Symbol.replace 函数接收两个参数,即调用 replace() 方法字符串实例和替换字符串。返回的值没有限制:
-
class FooReplacer { static [Symbol.replace](target, replacement) { return target.split('foo').join(replacement); } } console.log('barfoobaz'.replace(FooReplacer, 'qux')); // "barquxbaz" class StringReplacer { constructor(str) { this.str = str; } [Symbol.replace](target, replacement) { return target.split(this.str).join(replacement); } } console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // "barquxbaz"
-
-
-
Symbol.search:一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search() 方法使用。
-
String.prototype.search()方法会使用以 Symbol.search 为键的函数来对正则表达式求值。
-
Symbol.search 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:
-
class FooSearcher { static [Symbol.search](target) { return target.indexOf('foo'); } } console.log('foobar'.replace(FooSearcher)); // 0 console.log('barfoo'.replace(FooSearcher)); // 3 console.log('barbaz'.replace(FooSearcher)); // -1 class StringSearcher { constructor(str) { this.str = str; } [Symbol.search](target) { return target.indexOf(this.str); } } console.log('foobar'.replace(new FooSearcher('foo'))); // 0 console.log('barfoo'.replace(new FooSearcher('foo'))); // 3 console.log('barbaz'.replace(new FooSearcher('qux'))); // -1
-
-
-
Symbol.species:一个函数值,该函数作为创建派生对象的构造函数。
-
这个属性在内置类型中最常用,用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:
-
class Bar extends Array {} class Baz extends Array { static get [Symbol.species]() { return Array; } } let bar = new Bar(); console.log(bar instanceof Array); // true console.log(bar instanceof Bar); // true bar = bar.concat('bar'); console.log(bar); // Bar(1) [ 'bar' ] console.log(bar instanceof Array); // true console.log(bar instanceof Bar); // true let baz = new Baz(); console.log(baz instanceof Array); // true console.log(baz instanceof Baz); // true baz = baz.concat('baz'); console.log(baz); // [ 'bar' ] console.log(baz instanceof Array); // true // Symbol.species 内置重置 console.log(baz instanceof Baz); // false
-
-
-
Symbol.split:一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split() 方法使用。
-
String.prototype.split() 方法会使用以 Symbol.split 为键的函数来对正则表达式求值。
-
Symbol.split 函数接收一个参数,就是调用 match() 方法的字符串实例。返回的值没有限制:
-
class FooSplitter { static [Symbol.split](target) { return target.split('foo'); } } console.log('barfoobaz'.split(FooSplitter)); // ["bar", "baz"] class StringSplitter{ constructor(str) { this.str = str; } [Symbol.split](target) { return target.split(this.str); } } console.log('barfoobaz'.split(new StringSplitter('foo'))); // ["bar", "baz"]
-
-
-
Symbol.toPrimitive:一个方法,该方法将对象转为相对应的原始值,由 ToPrimitive 抽象操作使用。
-
根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值:
-
class Foo {} class Bar { constructor() { this[Symbol.toPrimitive] = function(hint) { switch (hint) { case 'number': return 3; case 'string': return 'string bar'; case 'default': default: return 'default bar'; } } } } let foo = new Foo(); console.log(3 + foo); // "3[object Object]" console.log(3 - foo); // NaN console.log(String(foo)); // "[object Object]" let bar = new Bar(); console.log(3 + bar); // "3default bar" console.log(3 - bar); // 0 console.log(String(bar)); // "string bar" console.log(Number(bar)); // 3
-
-
-
Symbol.toStringTag:一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString() 使用。
-
let s = new Set(); class Foo {} let foo = new Foo(); class Bar { constructor() { this[Symbol.toStringTag] = 'Bar'; } } let bar = new Bar(); console.log(s.toString()); // [object Set] console.log(s[Symbol.toStringTag]); // Set console.log(foo.toString()); // [object Object] console.log(foo[Symbol.toStringTag]); // Object console.log(bar.toString()); // [object Bar] console.log(bar[Symbol.toStringTag]); // Bar
-
3.4.8 Object 类型
ECMAScript 中的对象其实就是一组数据和功能的集合。
let o = new Object();
let o2 = new Object; // 合法,但不推荐
Object 是派生其他对象的基类,Object 类型的所有属性和方法在派生的对象上同样存在,每个 Object 实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。
- hasOwnProperty():用于判断当前对象实例(不是原型)上是否有存在给定的属性。传入字符串参数。
- isPrototypeof():用于判断当前对象是否为另一个对象的原型。传入字符串参数。
- propertyIsEnumerable():用于判断给定的属性是否可以使用 for-in 语句枚举。传入字符串参数。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值、布尔值表示。通常与 toString() 的返回值相同。
严格来说浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。
3.5 操作符
ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。在应用给对象时,操作符通常会调用 valueof() 或 toString() 方法来取得可以计算的值
3.5.1 一元操作符
-
递增/递减操作符:直接照搬自 C 语言,且可作用于任何值,整数、字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则:
-
字符串
- 有效的数值形式,转换为数值再应用改变。变量类型从字符串变成数值。
- 无效的数值形式,将变量值设置为 NaN。变量类型从字符串变成数值。
-
布尔值
- false,转换为 0 再应用改变。变量类型从布尔值变成数值。
- true,转换为 1 再应用改变。变量类型从布尔值变成数值。
-
浮点值:直接加 1 或减 1。
-
对象:调用其 valueof() 方法取得可以操作的值,对得到的值应用上述规则。如果是 NaN,则调用 toString() 并再次应用其他规则。变量类型从对象变为数值。
-
// 演示上述规则 let s1 = "2"; let s2 = "z"; let b = false; let f = 1.1; let o = { valueof() { return -1; } } s1++; // 值变成数值 3 s2++; // 值变成 NaN b++; // 值变成数值 1 f--; // 值变成 0.10000000000000009(因为浮点数不精确) o--; // 值变成 -2
-
-
一元加和减:它们在 ECMAScript 中跟高中数学中的用途一样,可用于数据类型转换。
-
一元加由一个加号(+)表示,放在变量前头,对数值没有影响。
-
// 演示上述规则 let s1 = "01"; let s2 = "1.1"; let s3 = "z"; let b = false; let f = 1.1; let o = { valueof() { return -1; } } s1 = +s1; // 值变成数值 1 s2 = +s2; // 值变成 1.1 s3 = +s3; // 值变成 NaN b = +b; // 值变成数值 0 f = +f; // 值变成 1.1 o = +o; // 值变成 -1
-
-
一元减由一个减号(-)表示,放在变量前头将其变成相应的负值
-
// 演示上述规则 let s1 = "01"; let s2 = "1.1"; let s3 = "z"; let b = false; let f = 1.1; let o = { valueof() { return -1; } } s1 = -s1; // 值变成数值 -1 s2 = -s2; // 值变成 -1.1 s3 = -s3; // 值变成 NaN b = -b; // 值变成数值 0 f = -f; // 值变成 -1.1 o = -o; // 值变成 1
-
-
3.5.2 位操作符
ECMAScript 中的所有数值都是以 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先将值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。因此在进行位操作符时,只需要考虑 32 位整数即可。
-
有符号整数前 31 位表示整数值,第 32 位表示数值的符号。0 表示正,1 表示负。
-
无符号整数 32 位均表示整数值,因此比有符号整数的范围更大。
默认情况下,ECMAScript 中的所有整数都表示为有符号数。
3.5.2.1 按位非
用 ~ 表示,其作用返回数值的二进制的取反,是二进制数学操作符之一。
let num1 = 25; // 二进制 00000000 00000000 00000000 00011001
let num2 = ~num1; // 二进制 11111111 11111111 11111111 11100110
console.log(num2); // -26
// 按位非,即是对数值的二进制取反。
// 最终效果对十进制数取反并减 1,如下操作。
let num3 = 25;
let num4 = -num3 - 1;
console.log(num4); // 26
尽管两者返回的结果一样,但位操作符的速度快得多。这是因为位操作是在数值的底层表示上完成的。
3.5.2.2 按位与
用 & 表示,有两个操作数,若两个操作数二进制每一对应相同位上均为 1 则返回 1,否则返回 0。
let result = 25 & 3; // 1
/**
* 25 = 0000 0000 0000 0000 0000 0000 0001 1001
* 3 = 0000 0000 0000 0000 0000 0000 0000 0011
* AND = 0000 0000 0000 0000 0000 0000 0000 0001
*/
let result2 = 25 & 20; // 16
/**
* 25 = 0000 0000 0000 0000 0000 0000 0001 1001
* 20 = 0000 0000 0000 0000 0000 0000 0001 0100
* AND = 0000 0000 0000 0000 0000 0000 0001 0000
*/
3.5.2.3 按位或
用 | 表示,有两个操作符,返回两个操作数二进制每一对应相同位上有 1 则返回 1,否则返回 0。
let result = 25 & 3; // 27
/**
* 25 = 0000 0000 0000 0000 0000 0000 0001 1001
* 3 = 0000 0000 0000 0000 0000 0000 0000 0011
* AND = 0000 0000 0000 0000 0000 0000 0001 1011
*/
3.5.2.4 按位异或
用 ^ 表示,有两个操作符,返回两个操作数二进制每一对应相同位上数值不同则返回 1,否则返回 0。
let result = 25 & 3; // 26
/**
* 25 = 0000 0000 0000 0000 0000 0000 0001 1001
* 3 = 0000 0000 0000 0000 0000 0000 0000 0011
* AND = 0000 0000 0000 0000 0000 0000 0001 1010
*/
3.5.2.5 左移
用 << 表示,按照指定位数将数值的所有位向左移动,以 0 填充空位(保留 32 位中最左侧的符号位)。
let oldValue = 2; // 二进制 10
let newValue = oldValue << 5; // 二进制 1000000 => 64
// 左移会保留符号位
let oldValue2 = -2; // 二进制 -10
let newValue2 = oldValue2 << 5; // 二进制 -1000000 => -64
3.5.2.6 有符号右移
用 >> 表示,按照指定位数将数值的所有位向右移动,同时保留符号(正或负),(保留 32 位中最左侧的符号位)。
let oldValue = 64; // 二进制 1000000
let newValue = oldValue >> 5; // 二进制 10 => 2
// 左移会保留符号位
let oldValue2 = -64; // 二进制 -1000000
let newValue2 = oldValue2 >> 5; // 二进制 -10 => -2
3.5.2.7 无符号右移
用 >>> 表示,按照指定位数将数值的所有位向右移动,不保留符号,可表示的范围变大。
-
正数:无符号右移与有符号右移结果相同。
-
let oldValue = 64; // 二进制 1000000 let newValue = oldValue >> 5; // 二进制 10 => 2
-
-
负数:无符号右移会给空位补 0(不保留符号位),因此结果将变得非常之大。
-
let oldValue = -64; // 二进制 11111111 11111111 11111111 11000000,无符号的负数是其绝对值的二补数。 let newValue = oldValue >>> 5; // 二进制 00000111 11111111 11111111 11111110,十进制 134217726。
-
3.5.3 布尔操作符
布尔操作符一共有 3 个:逻辑非、逻辑与和逻辑或。
-
! 逻辑非:该操作符首先将操作数转换为布尔值,然后再对其取反。
- 对象,则返回 false。
- 空字符串,则返回 true。
- 非空字符串,则返回 false。
- 数值 0,则返回 true。
- 非 0 数值(包括 Infinity),则返回 false。
- null,则返回 true。
- NaN,则返回 true。
- undefined,则返回 true。
console.log(!false); // true console.log(!"blue"); // false console.log(!0); // true console.log(!NaN); // true console.log(!""); // true console.log(!12345); // false同时使用两个逻辑非 !! 相当于调用了转型函数 Boolean()。
-
&& 逻辑与:该操作符应用到两个值,且遵循如下规则:
-
第一个操作数 第二个操作数 结果 true true true true false false false true false false false false 当第一个操作符为 true 时,将会作用到第二个操作符。反之不会作用到第二个操作符,因此可以当作一种短路操作符。
let found = true; let result = (found && undeclaredVariable); // 作用到未定义的 undeclaredVariable,这里会出错 console.log(result); // 不会执行这一行 let found2 = false; let result2 = (found2 && undeclaredVariable); // 不会作用到未定义的 undeclaredVariable。 console.log(result2); // 会执行这一行,打印 false。
-
-
|| 逻辑或:该操作符应用到两个值,且遵循如下规则:
-
第一个操作数 第二个操作数 结果 true true true true false true false true true false false false 与逻辑与类似,逻辑或也具有段路的特性。当第一个操作数求值为 true,第二个操作数就不会再被求值。
let found = false; let result = (found || undeclaredVariable); // 作用到未定义的 undeclaredVariable,这里会出错 console.log(result); // 不会执行这一行 let found2 = true; let result2 = (found2 && undeclaredVariable); // 不会作用到未定义的 undeclaredVariable。 console.log(result2); // 会执行这一行,打印 false。 // 利用这种行为,可以避免给变量赋值 null 或 undefined。 let myObject = preferredObject || backupObject // backupObject 可以被看作为备用的值。
-
3.5.6 加性操作符
加性操作符,即加法和减法操作符,在 ECMAScript 中,这两个操作符在后台会发生不同数据类型的转换。
-
+ 加法操作符,用于求两个数的和,并应用如下转换规则:
- 两个操作数均为数值
- 任一操作数为 NaN,则返回 NaN。
- Infinity 加 Infinity,则返回 Infinity。
- -Infinity 加 -Infinity,则返回 -Infinity。
- Infinity 加 -Infinity,则返回 NaN。
- -0 加 +0,则返回 +0。
- 两个操作数不均为数值
- 均为字符串,则将第二个字符串拼接到第一个字符串后面。
- 任一操作数为字符串,则将另一个操作数转换为字符串,拼接起来。
- 任一操作数为其他数据类型,则调用它们的 toString() 方法,再拼接起来。
// 上述常见错误:字符串与 num1 加法操作为字符串,再与 num2 加法操作仍为字符串。 let num1 = 5; let num2 = 10; let message = "The sum of 5 and 10 is " + num1 + num2; console.log(message); // "The sum of 5 and 10 is 510" // 解决:优先相加数值,再拼接为字符串。 let num1 = 5; let num2 = 10; let message = "The sum of 5 and 10 is " + (num1 + num2); console.log(message); // "The sum of 5 and 10 is 15" - 两个操作数均为数值
-
- 减法操作符,用于求两个数的差,并应用如下转换规则:
-
两个操作数均为数值
- 任一操作数为 NaN,则返回 NaN。
- Infinity 减 Infinity,则返回 NaN。
- -Infinity 减 -Infinity,则返回 NaN。
- Infinity 减 -Infinity,则返回 Infinity。
- +0 减 -0,则返回 -0。
- -0 减 -0,则返回 +0。
-
任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number() 将其转换为数值,再根据应用上边规则。
-
任一操作数是对象
- 有valueOf()方法,则调用其 valueOf() 方法取得表示它的数值。
- 无valueOf()方法,则调用其 toString() 方法,然后再将得到的字符串转换为数值。
// 演示上述规则 let res1 = 5 - true; // 4 let res2 = NaN - 1; //NaN let res3 = 5 - 3; // 2 let res4 = 5 - ""; // 5 let res5 = 5 - "2"; // 3 let res6 = 5 - null; // 5
-
3.5.7 关系操作符
关系操作符执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=),返回布尔值。应用到不同数据类型时也会发生类型转换和其他行为,如下规则:
- 都是数值,则执行数值比较。
- 都是字符串,则逐个比较字符串中对应字符的编码。
- 任一操作数是数值,则将另一个操作数转换为数值,执行数值比较。
- 有任一操作数是布尔值,则将其转换为数值再执行比较。
- 任一操作数是对象
- 若有 valueOf() 方法,取得结果后根据前面规则执行比较。
- 若无 valueOf() 方法,则调用 toString() 方法,再根据前面规则执行比较。
// 常见关系操作符错误
// 1. 大写字母编码小于小写字母编码
let resultError1 = "Brick" < "alphabet"; // true
let resultGreat1 = "Brick".toLowerCase() < "alphabet".toLowerCase(); // false
// 2. 数值字符串,字符编码不一致。
let resultError2 = "23" < "3"; // true
let resultGreat2 = "23" < 3; // false
// 3. 若关系操作符涉及到返回结果为 NaN,则均返回 false。
let result1 = NaN < 3; // false
let result2 = NaN >= 3; // false
3.5.8 相等操作符
ECMAScript 提供了两组操作符。第一组是等于和不等于,在比较之前执行转换。第二组是全等和不全等,在比较之前不执行转换。
3.5.8.1 等于和不等于
等于操作符用(==)表示,不等于操作符用(!=)表示。这两个操作符都会先进行强制类型转换再确定操作数是否相等。
遵循如下转换规则:
- 任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1。
- 一个操作数是字符串,另一个是数值,则将字符串转换为数值,再比较是否相等。
- 一个操作数是对象,另一个不是,则调用对象的 valueOf() 取得原始值,再根据前面规则进行比较。
遵循如下比较规则:
- null 和 undefined 相等。
- null 和 undefined 不能转换为其他类型的值再进行比较。
- 任一操作数为 NaN,则均为不相等。按照规则, NaN 不等于 NaN。
- 两个操作符均为对象,则比较是否为同一对象,是则相等,反之则不相等。
下表列举一些特殊情况及比较结果
| 表达式 | 结果 |
|---|---|
| null == undefined | true |
| "NaN" == NaN | false |
| 5 == NaN | false |
| NaN == NaN | false |
| NaN != NaN | true |
| false == 0 | true |
| true == 1 | true |
| true == 2 | false |
| undefined == 0 | false |
| null == 0 | false |
| "5" == 5 | true |
3.5.8.2 全等和不全等
全等操作符用(===)表示,不全等操作符用(!==)表示。这两个操作符都不会进行强制类型转换,便确定操作数是否相等。
let res1 = ("55" == 55); // true,转换后相等。
let res2 = ("55" === 55); // false,数据类型不同。
let res3 = ("55" != 55); // false,转换后相等。
let res4 = ("55" !== 55); // true,数据类型不同。
let res5 = (null == undefined); // true,两个值类似。
let res6 = (null !== undefined); // false,数据类型不同。
由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。有助于在代码中保持数据类型的完整性。
3.5.9 条件操作符
// boolean_expression 为 true 返回 true_value 赋予 variable,反之亦然。
variable = boolean_expression ? true_value : false_value;
3.5.11 逗号操作符
// 逗号操作符可以用来在一条语句中执行多个操作。
let num1 = 1, num2 = 2, num3 = 3;
// 利用逗号操作符在赋值多个值时,最终会返回表达式中对后一个值。
let num2 = (5, 1, 4, 8, 0); // 0
3.6 语句
ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中。
3.6.2 do-while 语句
do-while 语句是一种后测试循环语句,用于循环体内代码在退出前至少要执行一次。
let i = 0;
do {
i += 2;
console.log(i);
} while (i < 10);
// 2、4、6、8、10
3.6.5 for-in 语句
for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。如果要迭代的变量是 null 或 undefined,则不执行循环体。
const obj = {
name: "lindada",
age: 21,
job: null
}
for (const propName in obj) { // const 确保这个局部变量不被修改。
console.log(propName) // 返回的属性顺序不一定一致。
}
// name、age、job
3.6.6 for-of 语句
for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素。
for(const el of [2, 4, 6, 8]) {
console.log(el);
}
// 2、4、6、8
for-of 循环会按照可迭代对象的 next() 方法产生值的顺序迭代元素,关于可迭代对象,第 7 章详细介绍。
ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约(promise)的异步可迭代对象。
3.6.7 标签语句
标签语句用于给语句加标签
start: for (let i = 0; i < count; i++) {
console.log(i);
}
// start 是 for 语句的标签,可以通过 break 或 continue 语句引用
// 在嵌套循环中十分有用。
3.6.8 break 和 continue 语句
为执行循环代码提供了更严格的控制手段。break 语句用于立即跳出循环,强制执行循环后的下一语句。continue 语句也用于立即跳出循环,但会再次从循环顶部开始执行。
// break 和 continue 与标签语句来控制外部嵌套循环的语句
let num1 = 0;
outermost1:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost1;
}
num1++;
}
}
console.log(num1); // 55
// 当 i 和 j 均为 5 时,break 跳出结束外部标签 outermost 循环语句,结束循环。
let num2 = 0;
outermost2:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
continue outermost2;
}
num2++;
}
}
console.log(num2); // 95
// 当 i 和 j 均为 5 时,continue 跳出结束外部标签 outermost 循环语句,进行下一次循环。
3.6.10 switch 语句
switch 语句是与 if 语句紧密相关的一种流控制语句,如下两个语句等效:
const i = 25;
if (i === 25) {
console.log("25");
} else if (i == 35) {
console.log("35");
} else {
console.log("Other");
}
// 等效于
switch (i) {
case 25:
console.log("25");
break;
case 35:
console.log("35");
break;
default:
console.log("Other");
}
每个 case 条件后面都需加上 break 语句,如果确实需要连续匹配几个条件,那么推荐写注释表明情况:
const i = 25;
switch (i) {
case 25:
/*跳过*/
case 35:
console.log("25 or 35");
default:
conosle.log(Other);
}
ECMAScript 为 switch 语句赋予了一些独有的特性,可用于所有数据类型(不限于数值),case 条件的值不仅为常量,可以是变量或表达式。
switch 语句在比较每个条件的值时,会使用全等操作符,因此不会强制转换数据类型。
let num = 25;
switch (true) {
case num < 0:
console.log("Less than 0");
break;
case num >= 0 && num <= 10:
console.log("Between 0 and 10");
break;
default:
console.log("More than 20");
}
3.7 函数
ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。(第 10 章会更详细地介绍函数。)
function sayHi(name, message) {
console.log("Hello" + name + ", " + message);
}
// 通过函数名来调用函数
sayHi("lindada", "how are you today?"); // "Hello lindada, how are you today?"
- 除了 return 语句之外没有任何特殊声明表明该函数有返回值,如下:
function sum(num1, num2) {
return num1 + num2;
}
const result = sum(5, 10); // 15
- 只要碰到 return 语句,函数就会立即停止执行并退出。
function sum2(num1, num2) {
return num1 + num2;
console.log("can't do it!"); // 不会执行
}
- return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined,这种用法最常用于提前终止函数执行。
function sayHi(name, message) {
return; // 终止返回 undefined
console.log("Hello" + name + ", " + message); // 不会执行
}
严格模式对函数也有一些限制,若违反如下规则,则导致语法错误:
- 函数不能以 eval 或 arguments 座位名称;
- 函数的参数不能叫做 eval 或 arguments;
- 两个函数的参数不能叫同一个名称。
3.8 小结
JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下 ECMAScript 中的基本元素。
- ECMAScript中的基本数据类型
- ES6 之前
- Undefined、Null、Boolean、Number、String
- ES6 之后 ES10 之前
- 新增 Symbol
- ES10 之后
- 新增 BigInt
- ES6 之前
- ECMAScript 不区分整数和浮点数,只有 Number 一种数值数据类型。
- Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
- 严格模式为这门语言中某些容易出错的部分施加了限制。
- ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符。
- 流控制语句大多是从其他语言中借鉴惹来的。
ECMAScript 中的函数特点
- 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
- 不指定返回值的函数实际上会返回特殊值 undefined。