1. 语法
很大程度上借鉴了 C 语言和其他类 C 语言,如 Java 和 Perl.
1.1 区分大小写
1.2 标识符
第一个字符必须是一个字母、下划线(_)或美元符号($);
剩下的其他字符可以是字母、下划线、美元符号或数字。
推荐使用驼峰写法
1.3 注释
//单行注释
/这是
多行注释/
1.4 严格模式
要对整个脚本启用严格模式,在脚本开头加上这一行:
“use strict”;
也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
function doSomething() {
“use strict”;
// 函数体
}
1.5 语句
语句末尾的分号不是必需的,但是推荐加上.
2. 变量
变量是松散类型的,意思是变量可以用于保存任何类型的数据。有 3 个关键字可以声明变量:var、const 和 let。其中,var 在
ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。
2.1 var关键字
弱类型语言,同一个变量可以进行不同类型值的赋值
var message = "hi";message = 100; // 合法,但不推荐
不初始化的情况下,变量会保存一个特殊值 undefined.
1.作用域
使用 var 操作符定义的变量会成为包含它的函数的局部变量.
不过,在函数内定义变量时省略 var 操作符,可以创建一个全局变量:
function test() { var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!function test1() { message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
注意: 虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略 var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError。
2. 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
2.2 let 声明
let 跟 var 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。
if (true) { var name = 'Matt'; console.log(name); // Matt
}
console.log(name); // Matt
if (true) { let age = 26; console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
age 变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样也适用于 let。
let 也不允许同一个块作用域中出现冗余声明。这样会导致报错:
var name;
var name;
let age;
let age; // SyntaxError;标识符 age 已经声明过了
JavaScript 引擎会记录用于变量声明的标识符及其所在的块作用域,因此嵌套使用相同的标识符不会报错,而这是因为同一个块中没有重复声明:
var name = 'Nicholas';
console.log(name); // 'Nicholas'
if (true) { var name = 'Matt'; console.log(name); // 'Matt'
}
let age = 30;
console.log(age); // 30
if (true) { let age = 26; console.log(age); // 26
}
对声明冗余报错不会因混用 let 和 var 而受影响。这两个关键字声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
1. 暂时性死区
let 与 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。
// name 会被提升
console.log(name); // undefined
var name = 'Matt';
// age 不会被提升
console.log(age); // ReferenceError:age 没有定义
let age = 26;
在解析代码时,JavaScript 引擎也会注意出现在块后面的 let 声明,只不过在此之前不能以任何方式来引用未声明的变量。在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。
2. 全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。因此,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。
3. 条件声明
在使用 var 声明变量时,由于声明会被提升,JavaScript 引擎会自动将多余的声明在作用域顶部合并为一个声明。因为 let 的作用域是块,所以不可能检查前面是否已经使用 let 声明过同名变量,同时也就不可能在已经声明的情况下声明它。
<script> var name = 'Nicholas'; let age = 26;
</script>
<script> // 假设脚本不确定页面中是否已经声明了同名变量// 那它可以假设还没有声明过var name = 'Matt'; // 这里没问题,因为可以被作为一个提升声明来处理// 不需要检查之前是否声明过同名变量let age = 36; // 如果 age 之前声明过,这里会报错
</script>
4. for 循环中的 let 声明
在 let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) { // 循环逻辑
}
console.log(i); // 5
改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部.
在使用 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
2.3 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
2.4 声明风格及最佳实践
1. 不使用 var
限制自己只使用 let 和 const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。
2. const 优先,let 次之
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,只在提前知道未来会有修改时,再使用 let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。
3. 数据类型
ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、
String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。还有一种复杂数据类型叫 Object(对
象)。Object 是一种无序名值对的集合。因为在 ECMAScript 中不能定义自己的数据类型,所有值都可以用上述 7 种数据类型之一来表示。只有 7 种数据类型似乎不足以表示全部数据。但 ECMAScript 的数据类型很灵活,一种数据类型可以当作多种数据类型来使用。
3.1 typeof 操作符
因为 ECMAScript 的类型系统是松散的,所以需要一种手段来确定任意变量的数据类型。typeof操作符就是为此而生的。对一个值使用 typeof 操作符会返回下列字符串之一:
"undefined"表示值未定义;"boolean"表示值为布尔值;"string"表示值为字符串;"number"表示值为数值;"object"表示值为对象(而不是函数)或 null;"function"表示值为函数;"symbol"表示值为符号。
注意typeof在某些情况下返回的结果可能会让人费解,但技术上讲还是正确的。比如,调用typeof
null 返回的是"object"。这是因为特殊值 null 被认为是一个对空对象的引用。
严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过 typeof 操作符来区分函数和其他对象。
3.2 Undefined 类型
Undefined 类型只有一个值,就是特殊值 undefined。当使用 var 或 let 声明了变量但没有初始化时,就相当于给变量赋予了 undefined 值
在对未初始化的变量调用 typeof 时,返回的结果是"undefined",但对未声明的变量调用它时,返回的结果还是"undefined",这就有点让人看不懂了
let message; // 这个变量被声明了,只是值为 undefined
// 确保没有声明过这个变量
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"
无论是声明还是未声明,typeof 返回的都是字符串"undefined"。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异,但它们都无法执行实际操作。
3.3 Null 类型
Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因
在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用.
undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等.
3.4 Boolean 类型
Boolean(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true 和 false。
这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0。
注意,布尔值字面量 true 和 false 是区分大小写的,因此 True 和 False(及其他大小混写形式)是有效的标识符,但不是布尔值。
虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean()转型函数:
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
下表总结了不同类型与布尔值之间的转换规则。
理解以上转换非常重要,因为像 if 等流控制语句会自动执行其他类型值到布尔值的转换
let message = "Hello world!";
if (message) { console.log("Value is true");
}
3.5 Number 类型
ECMAScript 中最有意思的数据类型或许就是 Number 了。Number 类型使用 IEEE 754 格式表示整
数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。
最基本的数值字面量格式是十进制整数,直接写出来即可.
整数也可以用八进制(以 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
1. 浮点值
要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不
是必须有整数,但推荐加上。下面是几个例子:
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以
10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:
let floatNum = 3.125e7; // 等于 31250000
浮点值的精确度最高可达 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,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。
2. 值的范围
由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。ECMAScript 可以表示的最小
数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e324;可以表示的最大数值保存在Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的Infinity(无穷)值。任何无法表示的负数以-Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正无穷大)表示。
如果计算返回正 Infinity 或负 Infinity,则该值将不能再进一步用于任何计算。这是因为
Infinity 没有可用于计算的数值表示形式。要确定一个值是不是有限大(即介于 JavaScript 能表示的
最小值和最大值之间),可以使用 isFinite()函数,如下所示:
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
虽然超出有限数值范围的计算并不多见,但总归还是有可能的。因此在计算非常大或非常小的数值时,有必要监测一下计算结果是否超出范围。
3. NaN
有一个特殊的数值叫 NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作
失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执行。但在 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
4. 数值转换
3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个函数执行的操作也不同。
Number()函数基于如下规则执行转换:
- . 布尔值,true 转换为 1,false 转换为 0。
- . 数值,直接返回。
- . null,返回 0。
- . undefined,返回 NaN。
- . 字符串,应用以下规则:
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。
因此,Number(“1”)返回 1,Number(“123”)返回 123,Number(“011”)返回 11(忽略前面的零)。 - 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
- 如果是空字符串(不包含字符),则返回 0。
- 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
- . 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。
考虑到用 Number()函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用 parseInt()函数。parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符
是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,
"1234blue"会被转换为 1234,因为"blue"会被完全忽略。类似地,"22.5"会被转换为 22,因为小数
点不是有效的整数字符。
假设字符串中的第一个字符是数值字符,parseInt()函数也能识别不同的整数格式(十进制、八进制、十六进制)。换句话说,如果字符串以"0x"开头,就会被解释为十六进制整数。如果字符串以"0"开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
不同的数值格式很容易混淆,因此 parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入 16 作为第二个参数,以便正确解析:
let num = parseInt("0xAF", 16); // 175
事实上,如果提供了十六进制参数,那么字符串前面的"0x"可以省掉
因为不传底数参数相当于让 parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。
parseFloat()函数的工作方式跟 parseInt()函数类似,都是从位置 0 开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"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
3.6 String 类型
String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。字符串可以使用双引号(")、单引号(')或反引号(`)标示.
ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改
某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量.
有两种方式把一个值转换为字符串。
首先是使用几乎所有值都有的 toString()方法。
其次如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。
1. 模板字面量
ECMAScript 6 新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineString = 'first line\nsecond line';
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineString);
// first line
// second line"
console.log(myMultiLineTemplateLiteral);
// first line
// second line
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起
来可能会缩进不当:
2. 字符串插值
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,只不过求值后得到的
是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
字符串插值通过在${}中使用一个 JavaScript 表达式实现:
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
所有插入的值都会使用 toString()强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值。嵌套的模板字符串无须转义。
3. 模板字面量标签函数
模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数
会接收被插值记号分隔后的模板和对每个表达式求值的结果。
标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数
接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) { console.log(strings); console.log(aValExpression); console.log(bValExpression); console.log(sumExpression); return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个
数组中
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) { console.log(strings); for(const expression of expressions) { console.log(expression); } return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
4. 原始字符串
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转
换后的字符表示。为此,可以使用默认的 String.raw 标签函数:
// Unicode 示例
// \u00A9 是版权符号
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"
// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(`first line
second line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line
3.7 Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。
符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
1. 符号的基本用法
符号需要使用 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 关键字一起作为构造函数使用。这样做是为了避免创建符
号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原
始值的包装对象:
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 mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"
2. 使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册
表中创建并重用符号。为此,需要使用 Symbol.for()方法:
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
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()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 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
3. 使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和
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}
4. 常用内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道
for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义
Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号
的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。
5. Symbol.asyncIterator
根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的AsyncIterator。由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。
for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator
为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的 AsyncGenerator:
class Foo { async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
6. Symbol.hasInstance
根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否
认可一个对象是它的实例。由 instanceof 操作符使用”。instanceof 操作符可以用来确定一个对象
实例的原型链上是否有原型。
ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol. hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true
7. Symbol.isConcatSpreadable
根据 ECMAScript 规范,这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素”. ES6 中的 Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为。
数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 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 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 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']
8. Symbol.iterator
根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。
9. Symbol.match
根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式
去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。
10. Symbol.replace
根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。
11. Symbol.search
根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”。String.prototype.search()方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。
12. Symbol.species
根据 ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义。
13. Symbol.split
根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表
达式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype.
split()方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有
这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数。
14. Symbol.toPrimitive
根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性上定义一个函数可以改变默认行为。
15. Symbol.toStringTag
根据 ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。
16. Symbol.unscopables
根据 ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在 with 环境绑定中。
3.8 Object 类型
ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称来创建。开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:
let o = new Object();
这个语法类似 Java,但 ECMAScript 只要求在给构造函数提供参数时使用括号。如果没有参数,如上面的例子所示,那么完全可以省略括号(不推荐):
let o = new Object; // 合法,但不推荐
Object 的实例本身并不是很有用,但理解与它相关的概念非常重要。类似 Java 中的 java.lang. Object,ECMAScript 中的 Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。
每个 Object 实例都有如下属性和方法。
- constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object()
函数。 - hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属
性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号。 - isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。
注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。