第三章 语言基础
第三章 语言基础
- 1. 语法基础
- 1.1 区分大小写
- 1.2 标识符
- 1.3 注释
- 1.4 严格模式
- 1.5 语句的最佳实践
- 2. 关键字
- 3. 变量
- 3.1 var 关键字(定义变量,建议ES6及其之后版本别用)
- 3.2 let 关键字(定义变量,推荐使用)
- 3.3 const 关键字(定义常量,推荐使用)
- 3.4 声明风格的最佳实践
- 4. 数据类型(原始类型和复杂数据类型)
- 4.1 typeof 操作符(确定变量是何种数据类型)
- 4.2 Undefined 类型(代表假值)
- 4.3 Null 类型
- 4.4 Boolean 类型
- 4.5 Number 类型
- 4.5.1 整数
- 4.5.2 浮点数
- 4.5.3 NaN(Not a Number,不是数值的数值)
- 4.5.4 数值转换
- 4.6 String 类型
- 4.6.1 字符字面量
- 4.6.2 转为字符串(不传底数时,建议使用 String() 函数)
- 4.6.2.1 toString() 方法(返回当前值的字符串等价物)
- 4.5.2.2 String() 函数(弥补toString() 方法缺陷)
- 4.5.3 模板字面量 ``(保留换行字符,通常用于编写模板)
- 4.5.4 字符串插值(+ 或者 \`${变量名}`)
- 4.5.5 模板字面量标签函数(主要用于自定义模版字面量的各个表达式行为)
- 4.5.6 原始字符串(String.raw``)
- 4.7 Symbol 类型
- 4.7.1 符号的基本用法(Symbol() 函数)
- 4.7.2 使用全局符号注册表(Symbol.for() 方法,keyFor()用于获取对应的key)
- 4.7.3 使用符号作为属性(计算属性语法、Object.defineProperty()、Object.defineProperties())
- 4.7.4 获取对象属性数组的方法(拓展)
- 4.7.5 常用内置符号
- 4.7.6 Symbol.asyncInterator(一个方法,返回对象默认的异步迭代器)
- 4.7.7 Symbol.hasInstance(一个方法,确定一个构造器对象是否认可一个对象是它的实例)
- 4.7.8 Symbol.isConcatSpreadable(一个方法,决定对象是否应该被 Array.prototype.caoncat 打平其数组元素)
- 4.7.9 Symbol.iterator(一个方法,返回对象默认的迭代器)
- 4.7.10 Symbol.match(一个正则表达式方法,返回匹配结果数组或者null,一个参数)
- 4.7.11 Symbol.replace(一个正则表达式,该方法替换一个字符串中匹配的子串,两个参数)
- 4.7.12 Symbol.search(一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引,一个参数)
- 4.7.13 Symbo.species(一个函数值,该函数作为创建派生对象的构造函数,可以改变 instanceof 的结果)
- 4.7.14 Symbol.split(一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串)
- 4.7.15 Symbol.toPrimitive(一个方法,该方法将对象转换为相应的原始值)
- 4.7.16 Symbol.toStringTag(一个字符串,该字符串用于创建对象的默认字符串描述)
- 4.7.17 Symbol.unscopable(一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除,不推荐使用)
- 4.8 Object 类型(复杂数据类型,派生其他对象的基类,第八章会有详细介绍)
- 4.8.1 使用方式(new操作符和对象字面量)
- 4.8.2 Object 是派生其他对象的基类,拥有共同的属性和方法
- 5. 操作符
- 5.1 一元操作符
- 5.1.1 递增/递减操作符(前缀版和后缀版)
- 5.1.1.1 前缀版(--变量 或者 ++变量,变量在语句执行前改变)
- 5.1.1.2 后缀版(变量-- 或者 变量++,变量在语句执行后改变)
- 5.1.1.3 递增/递减操作符可作用于任何值
- 5.1.2 一元加和减(算数或者执行Number()函数转换)
- 5.1.2.1 一元加应用到变量前,会执行Number()一样的类型转换
- 5.1.2.2 一元减应用到变量前,会执行Number()一样的类型转换,然后取负值
- 5.2 位操作符(用到的比较少,可作为了解)
- 5.2.1 按位非(~,返回返回数值的补数,即 num = -num - )
- 5.2.2 按位与(&,将两位数的每一位对齐,两个位都是1时返回1,其他情况返回0)
- 5.2.3 按位或(|,将两位数的每一位对齐,两个位至少一位为1时返回1,其他情况返回0)
- 5.2.4 按位异或(^,将两位数的每一位对齐,两个位只有一位为1时返回1,其他情况返回0)
- 5.2.5 左移(<<,会按照指定的位数将数值的所有位向左移动)
- 5.2.6 无符号右移(>>>,会将数值的所有 32 位都向右移)
- 5.3 布尔操作符
- 5.3.1 逻辑非(!,将操作数转换为布尔值,然后再对其取反)
- 5.3.2 逻辑与(&&,操作数和返回值都不限于布尔值)
- 5.3.3 逻辑或(||,操作数和返回值都不限于布尔值)
- 5.4 乘性操作符
- 5.4.1 乘法操作符( * ,用于计算两个数值的乘积)
- 5.4.2 除法操作符( / ,用于计算第一个操作数除以第二个操作数的商)
- 5.4.3 取模操作符(%,计算余数,也叫余数操作符)
- 5.5 指数操作符
- 5.5.1 指数操作符(**)
- 5.5.2 指数赋值操作符(**=)
- 5.6 加性操作符
- 5.6.1 加法操作符(+,用于求两个数的和)
- 5.6.2 减法操作符(-)
- 5.7 关系操作符(包括小于、大于、小于等于和大于等于)
- 5.9 相等操作符(推荐使用全等和不全等,避免类型转换问题)
- 5.9.1 等于和不等于(== 和 !=)
- 5.9.2 全等和不全等(=== 和 !==)
- 5.9 条件操作符
- 5.10 赋值操作符(=)
- 5.10.1 简单写法,直接赋值
- 5.10.2 复合赋值(比如 += ,但是不推荐,影响阅读,实际也不提升性能,只是简写)
- 5.1 逗号操作符( , 用于在一条语句中执行多个操作)
- 6. 语句
- 6.1 if 语句(最佳实践为使用代码块)
- 6.2 do-while 语句(后测试循环语句,循环退出前至少执行一次体内代码)
- 6.3 while 语句(先测试循环语句,循环体可能一次都不执行)
- 6.4 for 语句(先测试循环语句,循环中的万金油,很好用)
- 6.5 for-in 语句(迭代枚举对象中的非符号键属性,不推荐迭代数组)
- 6.6 for-of 语句(迭代数组中的元素,不可迭代对象)
- 6.7 标签语句(通过 break 或 continue 语句引用,用于跳出整个大循环,看 6.8)
- 6.8 break 和 continue 语句
- 6.8.1 break 语句(打破当前整个循环)
- 6.8.2 continue 语句(停止当前语句,继续循环的下一个语句)
- 6.8.3 标签和 break 语句配合使用(跳出指定的循环)
- 6.8.4 标签和 continue 语句配合使用(跳到指定循环的下一个内部循环)
- 6.9 with 语句(是将代码作用域设置为特定的对象,不推荐使用)
- 6.10 switch 语句(与 if 语句紧密相关的一种流控制语句)
- 7. 函数
- 7.1 基础语法
- 7.2 ECMAScript 中的函数是否需要返回值都可以
- 7.3 return 后不再执行当前函数的任何语句
- 7.4 return 不带任何值时,常用于提前终止函数执行
- 7.5 函数的最佳实践(要么返回值,要么不返回值)
- 8. 小结
本篇文章主要介绍ES6(ECMAScript的第6个版本)。
1. 语法基础
1.1 区分大小写
test 和 Test是两个不同的变量。typeof 不能作为函数名,因为它是一个关键字,但是Typeof 可以。
1.2 标识符
标识符,就是变量、函数、属性或函数参数的名称。组成规则如下:
(1)由一个或多个字符组成;
(2)第一个字符必须是一个字母、下划线(_)或美元符号($);
(3)剩下的其他字符可以是字母、下划线、美元符号或数字;
(4)标识符中的字母可以是扩展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 À 和 Æ(但不推荐使用)。
标识符为多个单词组合时,驼峰大小写形式为最佳实践(即第一个单词的首字母小写,后面每个单词的首字母大写),如:myCar、doSomething。
1.3 注释
(1)单行注释以两个斜杆开头,如:
// 单行注释
(2)块注释以一个斜杠和一个星号(/)开头,以它们的反向组合(/)结尾,如:
/*这是多行
注释*/
不过,我们通常为了美观,会写成类似以下的形式:
/**
* 方法注释
* @param {参数类型} 参数一
* @param {参数类型} 参数二
* @returns 返回值
*/
1.4 严格模式
ECMAScript 5 增加了严格模式(strict mode)的概念。严格模式是一种不同的 JavaScript 解析和执行模型,ECMAScript 3 的一些不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。要对整个脚本启用严格模式,在脚本开头加上这一行:
"use strict";
虽然看起来像个没有赋值给任何变量的字符串,但它其实是一个预处理指令。任何支持的 JavaScript引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏 ECMAScript 3 语法。
也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:
function doSomething() {
"use strict";
// 函数体
}
严格模式会影响 JavaScript 执行的很多方面,因此本书在用到它时会明确指出来。所有现代浏览器都支持严格模式。
1.5 语句的最佳实践
(1)语句末尾加分号;
(2)在控制语句中使用代码块 { }。
2. 关键字
简单理解,就是语句中可能用到的用作特殊用途的单词,不能用作标识符。
3. 变量
ECMAScript 变量是松散类型的,可以保存任何类型的数据。有3个关键字可以用于声明变量:var、const 和 let。var 在所有版本都可使用,const 和 let 在 ES6 版本后才出现。
3.1 var 关键字(定义变量,建议ES6及其之后版本别用)
(1)声明的作用域范围是函数
var 关键字声明的变量会变成包含它的函数的局部变量,在函数外使用则会报错。
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message);
但是如果在函数内定义变量时省略了 var,则会创建一个全局变量,因此在函数外也能使用。但是我们不推荐这么做,因为局部中定义全局变量,不知道你是否有意为之,也容易使全局变量难以维护。
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
(2)变量提升(不推荐故意为之)
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
以上代码等价于
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。但是不建议在未声明前使用,因为容易造成混乱。
(3)可反复声明同一变量(不推荐)
但是并不推荐这么写,因为浪费时间和空间。
3.2 let 关键字(定义变量,推荐使用)
(1)声明的作用域是块
(2)不允许多次声明
let 声明过的变量,不允许在统一作用域中再次被声明。
这里应该是两个语法错误,不过因为第一个报错后,后续语句就不会再执行,因此也看不到对应的报错信息。
(3)嵌套的块中可进行二次声明
可以看到,因为 var 声明的作用域是函数,所以实际上是只声明了一次,var name = 'Matt';
等价于 name = 'Matt';
。但是 let 声明的作用域是块,所以 let age = 26;
只在块中生效,外面的 age 不受影响。
(4)暂时性死区
var 声明的变量会被提升,但是 let 声明的变量不会被提升,在 let 声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError。
(5)全局声明
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声明的变量则会)。
(6)for 循环中的 let 声明
var 声明的变量作用域在全局,let 声明的变量作用域在块中,因此 for 循环外使用let 声明的迭代变量会报错。
var 声明的变量作用域在全局,执行完循环后,异步输出时的迭代变量是同一个,并且最终的值都是5。
var 声明的变量作用域在块中,每次迭代循环的块中,都会声明一个新的迭代变量。因此循环执行后,依次输出的会是0、1、2、3、4。
3.3 const 关键字(定义常量,推荐使用)
(1)声明变量时必须初始化变量,并且不能修改;
(2)不允许重复声明;
(3)作用域是块。
(4)声明的变量是一个对象时,其实指向的是对象的引用,因此可以修改对象中的属性值,毕竟引用没变。
(5)const 不能用于声明迭代变量,但是可以声明一个不会被修改的 for 循环变量。
3.4 声明风格的最佳实践
(1)不使用 var;
(2)const 优先,let 次之;
4. 数据类型(原始类型和复杂数据类型)
ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、
String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。
1种复杂数据类型: Object(对象)。Object 是一种无序名值对的集合。
4.1 typeof 操作符(确定变量是何种数据类型)
对一个值使用 typeof 操作符会返回下列字符串之一:
(1)"undefined"表示值未定义;
(2)"boolean"表示值为布尔值;
(3)"string"表示值为字符串;
(4)"number"表示值为数值;
(5)"object"表示值为对象(而不是函数)或 null;
(6)"function"表示值为函数;
(7)"symbol"表示值为符号。
typeof null 返回的是 ‘object’,因为 null 被认为是一个对空对象的引用。
注意 严格来讲,函数在 ECMAScript 中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过 typeof 操作符来区分函数和其他对象。
4.2 Undefined 类型(代表假值)
(1) Undefined 类型只有一个值,就是 undefined;
(2)var 或者 let 声明了变量但是没有初始化时,就相当于赋值了undefined;
(3)不用显示地初始化一个变量为undefined,因为没必要;
(4)ES3 增加特殊值 undefined 的目的就是为了正式明确空对象指针(null)和未初始化变量的区别 ;
(5)连声明都未声明的变量,值不是undefined,直接使用会报错,只能执行 typeof 操作,返回的接口也是 ‘undefined’ 。严格来讲声明未初始化和未声明的两个变量,存在根本性差异,但他们都无法执行实际操作;
(6)注意,即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的同时进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。
4.3 Null 类型
(1)Null 类型同样只有一个值,即特殊值 null。
(2)逻辑上讲,null 值表示一个空对象指针,这也是给typeof 传一个 null 会返回"object"的原因;
(3)在定义将来要保存对象值的变量时,建议使用 null 初始化。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
(4)undefined 值是由 null 值派生而来的。所以二者相等,但不全等;
(5)任何时候,只要变量要保存对象,而当时又没有那个对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其与 undefined 区分开来。
4.4 Boolean 类型
(1)有两个字面值:true 和 false。这两个布尔值不同于数值,因此 true 不等于 1,false 不等于 0;
(2)Boolean 函数,可以将其他所有 ECMAScript 类型的值转为相应的布尔值形式;
(3)像 if 等流控制语句会自动执行其他类型值到布尔值的转换,比如:
let message = "Hello world!";
if (message) {
console.log("Value is true");
}
4.5 Number 类型
Number 类型使用 IEEE 754 格式表示整数和浮点值(在某些语言中也叫双精度值);
4.5.1 整数
(1)整数也可以使用八进制和十六进制字面量表示;
(2)对于八进制字面量,第一个数字必须是零(0),然后是相应的八进制数字(数值 0~7)。如果字面量中包含的数字超出了应有的范围,就会忽略前缀的零,后面的数字序列会被当成十进制数;
(3)要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(0~9 以
及 A~F)。十六进制数字中的字母大小写均可;
(4)使用八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。
4.5.2 浮点数
(1)要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不
是必须有整数,但推荐加上;
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
(2)因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小数点后面跟着 0(如 1.0),那它也会被转换为整数;
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
(3)对于非常大或非常小的数值,浮点值可以用科学记数法来表示。比如:
let floatNum = 3.125e7; // 等于 31250000
// 0.000 000 3 会被转换为 3e-7
(4)浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。
例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。因此永远不要测试某个特定的浮点值;注意 之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript所独有。其他使用相同格式的语言也有这个问题。
(5)由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。
ECMAScript 可以表示的最小数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324;
可以表示的最大数值保存在Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308;
如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无穷)值。
任何无法表示的无穷负数以-Infinity(负无穷大)表示,任何无法表示的无穷正数以 Infinity(正无穷大)表示。
使用 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以获取正、负 Infinity。没错,这两个属性包含的值分别就是-Infinity 和 Infinity。
4.5.3 NaN(Not a Number,不是数值的数值)
NaN(Not a Number,不是数值的数值),用于表示本来要返回数值的操作失败了(而不是抛出错误)。
(1)用 0 除任意数值在其他语言中通常都会导致错误,在 ECMAScript 中,0、+0 或-0 相除会返回 NaN;
(2)如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity 或-Infinity;
(3)任何涉及 NaN 的操作始终返回 NaN(如 NaN/10);
(4)NaN 不等于包括 NaN 在内的任何值:
console.log(NaN == NaN); // false
(5)isNaN()函数,任何不能转换为数值的值都会导致这个函数返回 true,否则返回false:
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.5.4 数值转换
有 3 个函数可以将非数值转换为数值:Number()、parseInt()和 parseFloat()。Number()是
转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
(1)Number() 函数规则:
(2)parseInt() 函数规则:
- parseInt() 函数专注于字符串中的前面部分是否包含数值;
- 可解析多种进制;
- 符合条件的会转为对应的十进制整数,忽略后面的小数或者字符串;
- 没有数值(包括空字符串)或者不是数值的都会转为NaN;
- parseInt() 函数第二个参数可用于指定进制数(默认为10),为避免解析错误,建议始终传第二个参数。
(3)parseFloat() 函数规则:
- parseFloat() 函数同样专注于字符串中的前面部分是否包含数值;
- 只解析十进制的数;
- 十六进制始终返回0;
- 开头的 0 会被忽略;
- 第一个小数点有效,第二个之后的小数点都无效;
- 忽略后面的字符串开始部分;
4.6 String 类型
(1)使用双引号或者单引号都行,但是对于同一个字符串,不能混用;
4.6.1 字符字面量
4.6.2 转为字符串(不传底数时,建议使用 String() 函数)
4.6.2.1 toString() 方法(返回当前值的字符串等价物)
(1)toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有toString()方法,该方法只是简单地返回自身的一个副本。)
(2)null 和 undefined 值没有 toString()方法。使用会报错。
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
(3)当数值使用该方法时,可以传入底数。
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"
4.5.2.2 String() 函数(弥补toString() 方法缺陷)
(1)如果值有 toString()方法,则调用该方法(不传参数)并返回结果;
(2)如果值是 null,返回"null";
(3)如果值是 undefined,返回"undefined"。
4.5.3 模板字面量 ``(保留换行字符,通常用于编写模板)
使用方式:将字符串写在 `` 中即可,比如:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
等价结果如下:
4.5.4 字符串插值(+ 或者 `${变量名}`)
(1)直接使用 + 号;
(2)在模板字面量中使用 ${};
可以看到,二者的效果是等价的。
4.5.5 模板字面量标签函数(主要用于自定义模版字面量的各个表达式行为)
模板字面量也支持定义标签函数(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 taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
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.5.6 原始字符串(String.raw``)
String.raw 标签函数可以获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。
// 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
也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容:
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
4.7 Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。
符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
Symbol 类型是我们前端业务开发中几乎不会用到的一种数据类型,Symbol 的很多属性在其他数据类型中都有潜在的应用。因此,了解 Symbol 类型有助于我们了解很多方法的
4.7.1 符号的基本用法(Symbol() 函数)
(1)使用 Symbol() 函数初始化;
(2)可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。
(3)这个字符串参数与符号定义或标识完全无关:
(4)Symbol()函数不能与 new 关键字一起作为构造函数使用,否则会报错。
(5)如果一定要给符号包装对象,那就使用Object() 函数:
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"
4.7.2 使用全局符号注册表(Symbol.for() 方法,keyFor()用于获取对应的key)
(1)运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,使用Symbol.for() 方法,在全局符号注册表中创建并重用符号;
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
(2)Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运
行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同
字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
(3)全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
几个转得比较特殊:所有的对象都会转为 [object Obejct],所有的数组都会转为空字符串 ’ ';
(5)直接使用Symbol() 函数创建的符号,不会放到全局注册表中,所以即使Symbol.for() 创建的符号描述和前者相同,二者也不会相等;
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
(6)还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字
符串键。如果查询的不是全局符号,则返回 undefined。如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError。
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
Symbol.keyFor(123); // TypeError: 123 is not a symbol
4.7.3 使用符号作为属性(计算属性语法、Object.defineProperty()、Object.defineProperties())
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。使用方式有3种:
(1)对象字面量属性,只能在计算属性语法中使用符号作为属性;
(2)Object.defineProperty();
(3)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.7.4 获取对象属性数组的方法(拓展)
(1)Object.getOwnPropertyNames()返回对象实例的常规属性数组;
(2)Object.getOwnPropertySymbols()返回对象实例的符号属性数组;
(3)Reflect.ownKeys()会返回两种类型的键;
(4)Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。
(5)因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果
没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
4.7.5 常用内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者
可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道
for-of 循环会在相关对象上使用 Symbol.iterator 属性,那么就可以通过在自定义对象上重新定义
Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。
这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号
的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
注意:在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,
@@iterator 指的就是 Symbol.iterator。
4.7.6 Symbol.asyncInterator(一个方法,返回对象默认的异步迭代器)
(1)符号的一个属性,是一个方法,返回对象默认的异步迭代器,由 for-wait-of 语句使用。
(2)for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator
为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API
的 AsyncGenerator:
class Foo {
async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>}
(3)技术上,这个由 Symbol.asyncIterator 函数生成的对象应该通过其 next()方法陆续返回
Promise 实例。可以通过显式地调用 next()方法返回,也可以隐式地通过异步生成器函数返回:
class Emitter {
constructor(max) {
this.max = max;
this.asyncIdx = 0;
}
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
(4)Symbol.asyncIterator 是 ES2018(也就是ES9) 规范定义的,因此只有版本非常新的浏览器
支持它。关于异步迭代和 for-await-of 循环的细节,参见附录 A。
异步迭代器对于基础不好的同学来说,可能难以理解,所以强烈建议去附录A查看实例,加深理解。
4.7.7 Symbol.hasInstance(一个方法,确定一个构造器对象是否认可一个对象是它的实例)
(1)这个符号最为一个属性,表示确定一个构造器对象是否认可一个对象是它的实例;
(2)ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系,用于确定一个对象实例的原型上是否有原型;
(3)综上,所以 instanceof 操作符和 Symbol.hasInstance 的作用其实完全相同,只是写法不同;
(4)这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。
(5)由于 instanceof操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:
class Bar {}
class Baz extends Bar {
static [Symbol.hasInstance]() {
return false;
}
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false
b 的构造函数 Baz 修改了原型链的 Symbol.hasInstance 属性,但是原型链更上一层的 Bar 的 Symbol.hasInstance 属性并未被修改,所以就出现了 爸爸不认儿子,但是爷爷认孙子的情况。
4.7.8 Symbol.isConcatSpreadable(一个方法,决定对象是否应该被 Array.prototype.caoncat 打平其数组元素)
(1)这个符号作为一个属性,表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat() 打平其数组元素”;
(2)Array.prototype.concat(),顾名思义,concat 是数组原型链上的一个方法,可以将参数加入到数组的末尾;
(3)当 数组.concat(参数) 中的参数是数组时,后面的数组会被打平一层后加入到前面的数组末尾;
(4)当 数组.concat(参数) 中的参数是非数组元素时,参数会被直接加到到前面的数组末尾;
(5)任何元素的 Symbol.isConcatSpreadable 属性都是 undefined;
(3)修改类数组对象的 Symbol.isConcatSpreadable 属性被设置为true 时,数组.concat(类数组对象),则该对象会被当成数组元素加入到末尾(就是打平);
(4)数组1.concat(数组2),当数组2的 Symbol.isConcatSpreadable 属性被设置为false时,会被当成整个元素,直接加到数组1的末尾;
(6)其他不是类数组对象的对象在 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']
4.7.9 Symbol.iterator(一个方法,返回对象默认的迭代器)
(1)这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。
(2)for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator
为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API
的 Generator:
(3)技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回:
(4)查看对应数据类型的 prototype,即可看到原型链上是否有对应的 Symbol.iterator 方法
迭代器的内容在 第7章 会有详细介绍
4.7.10 Symbol.match(一个正则表达式方法,返回匹配结果数组或者null,一个参数)
拓展:正则表达式,又称规则表达式,(Regular Expression,在代码中常简写为regex、regexp或RE),它是一种文本模式,同时也是计算机科学的一个概念,其中包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符")。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。
(1)这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”;
(2)String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值;
(3)正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数;
其实从输出也能看到,Symbol的正则表达式方法还有Symbol.matchAll、Symbol.replace等
(4)字符串使用正则表达式匹配的内容得到的结果,能得到匹配字符串、被匹配字符串,以及后者在前者中第一次出现的位置。
但是仔细一看,我懵了,返回的究竟是数组还是对象?外部用 [ ],应该是数组,但是内部的元素又有key和value,这不是对象吗?更奇怪的是,使用 instanceof 函数判断,居然表明得到的结果,既是对象,也是数组。
很有意思,于是我使用 instanceof 验证 Array 和 Object 的关系,发现 Array 居然是 Object 的实例。仔细一想,也不难发现 arr[index] 的写法不就是对象的计算属性赋值写法吗?原来,一开始一切就有迹可循,二者竟是父子关系。
(5)所以,数组是对象的实例。所以,如果返回结果使用 instanceof 验证,既是数组,也是对象,那就是数组;
(6)给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象,换句话说,一般情况下foobar.match(/bar/)
等价于foobar.match('bar')
;
(7)可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。
// 修改默认的正则表达式求值行为,判断字符串是否含有foo字符串,返回布尔值
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) {
return target.includes(this.str);
}
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false
4.7.11 Symbol.replace(一个正则表达式,该方法替换一个字符串中匹配的子串,两个参数)
(1)这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”;
(2)String.prototype.replace() 方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:
(3)给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象。
(4)可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例:
// replace方法原先只匹配第一个子串进行替换,修改后匹配所有固定子串进行替换
class FooReplacer {
static [Symbol.replace](target, replacement) {
return target.split('foo').join(replacement);
}
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));
// "barquxbaz"
// replace方法原先只匹配第一个子串进行替换,修改后匹配所有特定子串进行替换
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
拓展:
split 函数用法 :split(分隔符,返回的元素个数) 方法用于把一个字符串分割成字符串数组。
join 函数用法 :join(分隔符) 方法用于把数组中的所有元素转换一个字符串,默认分隔符为逗号
4.7.12 Symbol.search(一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引,一个参数)
(1)这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”;
(2)String.prototype.search() 方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:
console.log(RegExp.prototype[Symbol.search]);
// ƒ [Symbol.search]() { [native code] }
console.log('foobar'.search(/bar/));
// 3
(3)给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象;
(4)可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让 search()方法使用非正则表达式实例:
class FooSearcher {
static [Symbol.search](target) {
return target.indexOf('foo');
}
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1
class StringSearcher {
constructor(str) {
this.str = str;
}
[Symbol.search](target) {
return target.indexOf(this.str);
}
}
console.log('foobar'.search(new StringSearcher('foo'))); // 0
console.log('barfoo'.search(new StringSearcher('foo'))); // 3
console.log('barbaz'.search(new StringSearcher('qux'))); // -1
(5)search方法和match方法非常相似:
在字符串搜索到对应子串时,search 返回值 等于 match 返回值[‘index’];
在字符串未搜索到对应子串时,search 返回值为 -1, match 返回值为 null。
4.7.13 Symbo.species(一个函数值,该函数作为创建派生对象的构造函数,可以改变 instanceof 的结果)
(1)这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”。
(2)这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 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 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 instanceof Array); // true
console.log(baz instanceof Baz); // false
可以看到,拼接过后,baz 从 自定义的 Baz 类型变成了 Array 类型。不过目前还不知道这个函数有什么用。
4.7.14 Symbol.split(一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串)
(1)这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”;
(2)String.prototype. split() 方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数:
// ƒ [Symbol.split]() { [native code] }
console.log('foobarbaz'.split(/bar/));
// ['foo', 'baz']
(3)给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象
(4)重新定义 Symbol.split 函数以取代默认对正则表达式求值的行为,从而让 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"]
4.7.15 Symbol.toPrimitive(一个方法,该方法将对象转换为相应的原始值)
(1)这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”。
也许很多人多已经忘记原始值是什么意思了。在本章节的第4点开头有讲过。在此回顾下:
ECMAScript 有 6 种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、
String 和 Symbol。Symbol(符号)是 ECMAScript 6 新增的。
1种复杂数据类型: Object(对象)。Object 是一种无序名值对的集合。
(2)很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性上定义一个函数可以改变默认行为。
class Foo {}
let foo = new Foo();
// 数字 + 对象,默认情况下,会将二者都作为字符串进行相加处理
console.log(3 + foo); // "3[object Object]"
// 数字 - 对象,会将后者当成数字,但是因为无法计算,所以返回NaN
console.log(3 - foo); // NaN
// 对象转为字符串
console.log(String(foo)); // "[object Object]"
// 自定义对象实例的Symbol.toPrimitive属性方法,改变转变成原始值的默认行为,变成特定的原始值或者其他值
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 bar = new Bar();
console.log(3 + bar); // "3default bar"
console.log(3 - bar); // 0
console.log(String(bar)); // "string bar
4.7.16 Symbol.toStringTag(一个字符串,该字符串用于创建对象的默认字符串描述)
(1)这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString() 使用”。
(2)通过 toString()方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义:
let s = new Set();
console.log(s); // Set(0) {}
// Set是对象下的子类
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set
class Foo {}
let foo = new Foo();
console.log(foo); // Foo {}
// 对象是对象的子类,但是因为本身是对象,所以创建对象的默认字符串描述 toStringTag 为 undefined
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined
// 创建一个自定义类(对象)Bar
class Bar {
constructor() {
this[Symbol.toStringTag] = 'Bar';
}
}
let bar = new Bar();
console.log(bar); // Bar {}
// Bar是对象下的子类
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar
4.7.17 Symbol.unscopable(一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除,不推荐使用)
(1)这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。
(2)设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在 with 环境绑定中,如下例所示:
let o = { foo: 'bar' };
with (o) {
console.log(foo); // bar
}
o[Symbol.unscopables] = {
foo: true
};
with (o) {
console.log(foo); // ReferenceError
}
注意 不推荐使用 with,因此也不推荐使用 Symbol.unscopables。
4.8 Object 类型(复杂数据类型,派生其他对象的基类,第八章会有详细介绍)
ECMAScript 中的对象其实就是一组数据和功能的集合。可以说 Object 类型是唯一的复杂数据类型,其他的复杂数据类型都是由它派生出来的。
4.8.1 使用方式(new操作符和对象字面量)
(1)使用 new 操作符创建对象(不方便,少用)
开发者可以通过创建 Object 类型的实例来创建自己的对象,然后再给对象添加属性和方法:
let o = new Object();
如果没有参数,如上面的例子所示,那么完全可以省略括号(不推荐):
let o = new Object; // 合法,但不推荐
(2)直接使用对象字面量(更方便,多用)
let obj = {
name: 'Sheldon',
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name)
}
}
4.8.2 Object 是派生其他对象的基类,拥有共同的属性和方法
ECMAScript 中的 Object 是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。每个 Object 实例都有如下属性和方法:
(1)constructor:用于创建当前对象的函数。在前面的例子中,这个属性的值就是 Object()
函数;
(2)hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如 o.hasOwnProperty(“name”))或符号;
(3)isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型(第 8 章将详细介绍原型。);
(4)propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in 语句枚举。与 hasOwnProperty()一样,属性名必须是字符串;
(5)toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境;
(6)toString():返回对象的字符串表示;
(7)valueOf():返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同。
第 8 章将介绍对象间的继承机制。
注意 严格来讲,ECMA-262 中对象的行为不一定适合 JavaScript 中的其他对象。比如浏览器环境中的 BOM 和 DOM 对象,都是由宿主环境定义和提供的宿主对象。而宿主对象不受 ECMA-262 约束,所以它们可能会也可能不会继承 Object。
5. 操作符
ECMA-262 描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。
ECMAScript 中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。
在应用给对象时,操作符通常会调用 valueOf()和/或 toString()方法来取得可以计算的值。
5.1 一元操作符
5.1.1 递增/递减操作符(前缀版和后缀版)
5.1.1.1 前缀版(–变量 或者 ++变量,变量在语句执行前改变)
前缀版的递增/递减操作符,变量的值会在语句被求值之前改变,比如:
let age = 29;
let anotherAge = --age + 2;
console.log(age); // 28
console.log(anotherAge); // 30
其中,let anotherAge = --age + 2;
,相当于
age = age - 1;
let anotherAge = age + 2
5.1.1.2 后缀版(变量-- 或者 变量++,变量在语句执行后改变)
后缀版的递增/递减操作符,变量的值会在语句被求值之后才改变,比如:
let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21
其中let num3 = num1-- + num2
,相当于
num3 = num1 + num2;
num1 = num - 1;
简单归纳:
(1)前缀版的递增/递减操作符先让变量递增/递减,再执行语句;
(2)后缀版的递增/递减操作符先执行语句,后让变量递增/递减。
5.1.1.3 递增/递减操作符可作用于任何值
(1)对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值;
(2)对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成
数值;
(3)对于布尔值,如果是 false,则转换为 0 再应用改变。变量类型从布尔值变成数值;
(4)对于布尔值,如果是 true,则转换为 1 再应用改变。变量类型从布尔值变成数值;
(5)对于浮点值,加 1 或减 1;
(6)如果是对象,则调用其(第 5 章会详细介绍的)valueOf()方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN,则调用 toString()并再次应用其他规则。变量类型从对象变成数值。
5.1.2 一元加和减(算数或者执行Number()函数转换)
除了普通的数学用法,还可以用于变量的类型转换。
5.1.2.1 一元加应用到变量前,会执行Number()一样的类型转换
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 s3 = '3'
let s4 = '4'
console.log(s3 + s4); // '34',数字加字符串统一当成字符串处理
console.log(s3 + +s4); // '34',数字加字符串统一当成字符串处理
console.log(+s3 + s4); // '34',数字加字符串统一当成字符串处理
console.log(+s3 + +s4); // 7,两个数字相加还是数字
console.log(+s3 ++s4); // 报错,注意中间的+一定要和后面的+有空格,否则会被当做++处理,两个变量间没有操作符就会报错
5.1.2.2 一元减应用到变量前,会执行Number()一样的类型转换,然后取负值
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
5.2 位操作符(用到的比较少,可作为了解)
(1)操作符用于数值的底层操作,也就是操作内存中表示数据的比特(位)。
(2)ECMAScript 中的所有数值都以 IEEE 754 64 位格式存储,但位操作并不直接应用到 64 位表示,而是先把值转换为 32 位整数,再进行位操作,之后再把结果转换为 64 位。对开发者而言,就好像只有 32 位整数一样,因为 64 位整数存储格式是不可见的。既然知道了这些,就只需要考虑 32 位整数即可。
(3)有符号整数使用 32 位的前 31 位表示整数值。第 32 位表示数值的符号,如 0 表示正,1 表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。
(4)正值以真正的二进制格式存储,即 31位中的每一位都代表 2 的幂。第一位(称为第 0 位)表示 20,第二位表示 21,依此类推。如果一个位是空的,则以0填充,相当于忽略不计。比如,数值18的二进制格式为00000000000000000000000000010010,或更精简的 10010。后者是用到的 5 个有效位,决定了实际的值(如图 3-1 所示)。
(5)负值以一种称为二补数(或补码)的二进制编码存储。一个数值的二补数通过如下 3 个步骤计算得到:
【1】确定绝对值的二进制表示(如,对于-18,先确定 18 的二进制表示);
【2】找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;
【3】给结果加 1。
基于上述步骤确定-18 的二进制表示,首先从 18 的二进制表示开始:
0000 0000 0000 0000 0000 0000 0001 0010
然后,计算一补数,即反转每一位的二进制值:
那么,-18 的二进制表示就是 11111111111111111111111111101110。要注意的是,在处理有符号整数时,我们无法访问第 31 位。ECMAScript 会帮我们记录这些信息。在把负值输出为一个二进制字符串时,我们会得到一个前面加了减号的绝对值,如下所示:
let num = -18;
console.log(num.toString(2)); // "-10010"
在将-18 转换为二进制字符串时,结果得到-10010。转换过程会求得二补数,然后再以更符合逻辑
的形式表示出来。
(6)注意 默认情况下,ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 32 位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。
(7)在对 ECMAScript 中的数值应用位操作符时,后台会发生转换:64 位数值会转换为 32 位数值,然后执行位操作,最后再把结果从 32 位转换为 64 位存储起来。整个过程就像处理 32 位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值NaN 和Infinity在位操作中都会被当成 0 处理。
(8)将位操作符应用到非数值,会先使用 Number()函数将该值转换为数值(这个过程是自动的),再应用位操作,最终结果是数值。
5.2.1 按位非(~,返回返回数值的补数,即 num = -num - )
(1)按位非操作符用波浪符(~)表示
(2)它的作用是返回数值的一补数(就是当前树的相反数-1)。
(3)虽然 ~num
和 -num - 1
得到的结果相同,但位操作的速度快得多,这是因为位操作是在数值的底层表示
上完成的。
5.2.2 按位与(&,将两位数的每一位对齐,两个位都是1时返回1,其他情况返回0)
25 & 3 的按位与运算过程如下
可以看到,25 和 3 的二进制表示中,只有第 0 位上的两个数都是 1。于是结果数值的所有其他位都会以 0 填充,因此结果就是 1。
5.2.3 按位或(|,将两位数的每一位对齐,两个位至少一位为1时返回1,其他情况返回0)
按位异或用脱字符(^)表示,同样有两个操作数。
在参与计算的两个数中,有 4 位都是 1,因此它们直接对应到结果上。二进制码 11011 等于 27。
5.2.4 按位异或(^,将两位数的每一位对齐,两个位只有一位为1时返回1,其他情况返回0)
(1)按位异或用脱字符(^)表示,同样有两个操作数;
(2)按位异或与按位或的区别是,它只在一位上是 1 的时候返回 1(两位都是 1 或 0,则返回 0);
5.2.5 左移(<<,会按照指定的位数将数值的所有位向左移动)
(1)左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。
(2)在移位后,数值右端会空出 5 位。左移会以 0 填充这些空位,让结果是完整的 32 位数值。
(3)左移会保留它所操作数值的符号。比如,如果-2 左移 5 位,将得到-64,而不是正 64
5.2.6 无符号右移(>>>,会将数值的所有 32 位都向右移)
(1)无符号右移用 3 个大于号表示(>>>),会将数值的所有 32 位都向右移;
let oldValue = 64; // 等于二进制 1000000
let newValue = oldValue >>> 5; // 等于二进制 10,即十进制 2
(2)而不管符号位是什么。对正数来说,这跟有符号右移效果相同。
(3)但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大,如下面的例子所示:
let oldValue = -64; // 等于二进制 11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726
在对-64 无符号右移 5 位后,结果是 134 217 726。这是因为-64 的二进制表示是 1111111111111111111
1111111000000,无符号右移却将它当成正值,也就是 4 294 967 232。把这个值右移 5 位后,结果是
00000111111111111111111111111110,即 134 217 726。
5.3 布尔操作符
5.3.1 逻辑非(!,将操作数转换为布尔值,然后再对其取反)
(1)逻辑非操作符由一个叹号(!)表示,可应用给 ECMAScript 中的任何值。
(2)这个操作符始终返回布尔值,无论应用到的是什么数据类型。
(3)逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。换句话说,逻辑非操作符会遵循如下规则:
console.log(!false); // true
console.log(!"blue"); // false
console.log(!0); // true
console.log(!NaN); // true
console.log(!""); // true
console.log(!12345); // false
(4)逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函数 Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反,从而给出变量真正对应的布尔值。结果与对同一个值使用 Boolean()函数是一样的:
console.log(!!"blue"); // true
console.log(!!0); // false
console.log(!!NaN); // false
console.log(!!""); // false
console.log(!!12345); // true
5.3.2 逻辑与(&&,操作数和返回值都不限于布尔值)
(1)逻辑与操作符由两个和号(&&)表示,应用到两个值;
(2)逻辑与操作符可用于任何类型的操作数,不限于布尔值;
(3)逻辑与 A && B
的运算规则如下(书中说得太麻烦,总结如下):
【1】如果Boolean(A)的值为true,则返回 B;
【2】如果Boolean(A)的值为false,则返回A;
console.log(true && true); // true
console.log(false && true); // false
console.log(undefined && 123); // undefined
console.log(null && 456); // null
console.log(NaN && 234); // NaN
console.log(NaN && undefined); // NaN
console.log(123 && {a: 456}); // {a: 456}
(4)逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值
let found = null;
let result = (found && someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
按理说 someUndeclaredVariable
没有定义会报错,但是因为逻辑与操作符的短路特性,当前者的布尔值为false时,就不会继续执行后续的操作了,而是直接返回前者。
5.3.3 逻辑或(||,操作数和返回值都不限于布尔值)
(1)逻辑或操作符由两个管道符(||)表示;
(2)逻辑或操作符可用于任何类型的操作数,不限于布尔值;
(3)逻辑与 A || B
的运算规则如下(书中说得太麻烦,总结如下):
【1】如果Boolean(A)的值为true,则返回 A;
【2】如果Boolean(A)的值为false,则返回B;
console.log(true || false); // true
console.log(false|| false); // false
console.log(undefined || 123); // 123
console.log(null || 456); // 456
console.log(NaN || 234); // 234
console.log(NaN || undefined); // undefined
console.log(123 || {a: 456}); // 123
(4)同样与逻辑与类似,逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数的布尔值为
true,第二个操作数就不会再被求值了
let found = 123;
let result = (123 || someUndeclaredVariable); // 不会出错
console.log(result); // 会执行,返回123
按理说 someUndeclaredVariable
没有定义会报错,但是因为逻辑或操作符的短路特性,当前者的布尔值为true时,就不会继续执行后续的操作了,而是直接返回前者。
5.4 乘性操作符
(1)ECMAScript 定义了 3 个乘性操作符:乘法、除法和取模;
(2)如果乘性操作符有不是数值的操作数,则该操作数会在后台被使用 Number()转型函数转换为数值。这意味着空字符串会被当成 0,而布尔值 true 会被当成 1。
5.4.1 乘法操作符( * ,用于计算两个数值的乘积)
(1)如果操作数都是数值,则执行常规的乘法运算;
(2)如果相乘数值过大,不能表示乘积,则返回 Infinity 或-Infinity;
(3)如果有任一操作数是 NaN,则返回 NaN;
(4)如果是 Infinity 乘以 0,则返回 NaN;
(5)如果是 Infinity 乘以非 0的有限数值,则根据第二个操作数的符号返回 Infinity 或-Infinity;
(6)如果是 Infinity 乘以 Infinity,则返回 Infinity;
(7)如果有不是数值的操作数,则先在后台用 Number()将其转换为数值,然后再应用上述规则。
5.4.2 除法操作符( / ,用于计算第一个操作数除以第二个操作数的商)
(1)如果操作数都是数值,则执行常规的除法运算;
(2)如果相除数值过大,不能表示乘积,则返回 Infinity 或-Infinity;
(3)如果有任一操作数是 NaN,则返回 NaN;
(4)如果是 Infinity 除以 Infinity,则返回 NaN;
(5)如果是 0 除以 0,则返回 NaN;
(6)如果是非 0 的有限值除以 0,则根据第一个操作数的符号返回 Infinity 或-Infinity;
(7)如果是 Infinity 除以任何数值,则根据第二个操作数的符号返回 Infinity 或-Infinity;
(8)如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则。
5.4.3 取模操作符(%,计算余数,也叫余数操作符)
(1)取模(余数)操作符由一个百分比符号(%)表示,比如:
let result = 26 % 5; // 等于 1
(2)如果操作数是数值,则执行常规除法运算,返回余数;
(3)如果被除数是无限值,除数是有限值,则返回 NaN;
(4)如果被除数是有限值,除数是 0,则返回 NaN;
(5)如果是 Infinity 除以 Infinity,则返回 NaN;
(6)如果被除数是有限值,除数是无限值,则返回被除数;
(7)如果被除数是 0,除数不是 0,则返回 0;
(8)如果有不是数值的操作数,则先在后台用 Number()函数将其转换为数值,然后再应用上述规则;
5.5 指数操作符
5.5.1 指数操作符(**)
ECMAScript 7 新增了指数操作符,Math.pow()现在有了自己的操作符**,结果是一样的:
console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4
5.5.2 指数赋值操作符(**=)
指数操作符也有自己的指数赋值操作符**=,该操作符执行指数运算和结果的赋值操作:
let squared = 3;
squared **= 2;
console.log(squared); // 9
let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4
5.6 加性操作符
5.6.1 加法操作符(+,用于求两个数的和)
(1)加法操作符(+)用于求两个数的和,比如:
let result = 1 + 2; // 3
(2)如果有任一操作数是 NaN,则返回 NaN;
(3)如果是 Infinity 加 Infinity,则返回 Infinity;
(4)如果是-Infinity 加-Infinity,则返回-Infinity;
(5)如果是 Infinity 加-Infinity,则返回 NaN;
(6)如果两个操作数都是字符串,则将第二个字符串拼接到第一个字符串后面;
(7)如果只有一个操作数是字符串,则将另一个操作数转换为字符串,再将两个字符串拼接在一起;
(8)如果有任一操作数是对象、数值或布尔值,则调用它们的 toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于 undefined 和 null,则调用 String()函数,分别获取"undefined"和"null"。
ECMAScript 中最常犯的一个错误,就是忽略加法操作中涉及的数据类型。比如:
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"
有人会以为最终结果是"The sum of 5 and 10 is 15"
,但是"The sum of 5 and 10 is " + num1 + num2
其实从左到右,其实要分成两次计算:
第一次是"The sum of 5 and 10 is " + num1
得到了字符串"The sum of 5 and 10 is 5"
;
第二次是"The sum of 5 and 10 is 5" + num2
得到了"The sum of 5 and 10 is 510"
。
如果要得到"The sum of 5 and 10 is 15"
,可以将式子变成"The sum of 5 and 10 is " + (num1 + num2);
即可。
5.6.2 减法操作符(-)
(1)如果两个操作数都是数值,则执行数学减法运算并返回结果;
(2)如果有任一操作数是 NaN,则返回 NaN;
(3)如果是 Infinity 减 Infinity,则返回 NaN;
(4)如果是-Infinity 减-Infinity,则返回 NaN;
(5)如果是 Infinity 减-Infinity,则返回 Infinity;
(6)如果是-Infinity 减 Infinity,则返回-Infinity;
(7)如果有任一操作数是字符串、布尔值、null 或 undefined,则先在后台使用 Number()将其转换为数值,然后再根据前面的规则执行数学运算。如果转换结果是 NaN,则减法计算的结果是NaN;
(8)如果有任一操作数是对象,则调用其 valueOf()方法取得表示它的数值。如果该值是 NaN,则减法计算的结果是 NaN。如果对象没有 valueOf()方法,则调用其 toString()方法,然后再将得到的字符串转换为数值。
let result1 = 5 - true; // true 被转换为 1,所以结果是 4
let result2 = NaN - 1; // NaN
let result3 = 5 - 3; // 2
let result4 = 5 - ""; // ""被转换为 0,所以结果是 5
let result5 = 5 - "2"; // "2"被转换为 2,所以结果是 3
let result6 = 5 - null; // null 被转换为 0,所以结果是 5
5.7 关系操作符(包括小于、大于、小于等于和大于等于)
(1)如果操作数都是数值,则执行数值比较;
(2)如果操作数都是字符串,则逐个比较字符串中对应字符的编码;
(3)如果有任一操作数是数值,则将另一个操作数转换为数值,执行数值比较;
(4)如果有任一操作数是对象,则调用其 valueOf()方法,取得结果后再根据前面的规则执行比较。如果没有 valueOf()操作符,则调用 toString()方法,取得结果后再根据前面的规则执行比较;
(5)如果有任一操作数是布尔值,则将其转换为数值再执行比较;
(6)对字符串而言,关系操作符会比较字符串中对应字符的编码,而这些编码是数值。大写字母的编码都小于小写字母的编码;
(7)任何关系操作符在涉及比较 NaN 时都返回 false。
let result1 = 5 > 3; // true
let result2 = 5 < 3; // false
let result = "Brick" < "alphabet"; // true
let result = "Brick".toLowerCase() < "alphabet".toLowerCase(); // false
let result = "23" < "3"; // true,都是字符串时比较编码,字符"2"的编码是 50,而字符"3"的编码是 51
let result = "23" < 3; // false 数值和字符串比较时,字符串会被转为数字
let result = "a" < 3; // 因为"a"会转换为 NaN,所以结果是 false
let result1 = NaN < 3; // false
let result2 = NaN >= 3; // false
5.9 相等操作符(推荐使用全等和不全等,避免类型转换问题)
5.9.1 等于和不等于(== 和 !=)
(1)等于操作符用两个等于号(==)表示,如果操作数相等,则会返回 true;
(2)不等于操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回 true;
(3)这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等;
(4)如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换为 1;
(5)如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等;
(6)如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较;
(7)null 和 undefined 相等;
(8)null 和 undefined 不能转换为其他类型的值再进行比较;
(9)如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
(10)如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true。否则,两者不相等。
5.9.2 全等和不全等(=== 和 !==)
(1)全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数;
(2)全等操作符由 3 个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回 true,比如:
let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同
注意:由于相等和不相等操作符存在类型转换问题,因此推荐使用全等和不全等操作符。这样有助于在代码中保持数据类型的完整性。
5.9 条件操作符
variable = boolean_expression ? true_value : false_value;
上面的代码执行了条件赋值操作,即根据条件表达式 boolean_expression 的值决定将哪个值赋给变量 variable 。如果 boolean_expression 是 true ,则赋值 true_value ;如果boolean_expression 是 false,则赋值 false_value。
上面的语句等价于
if(boolean_expression) {
variable = true_value
} else {
variable = false_value
}
因为条件操作符比条件语句更简洁,所以如果只是简单的条件赋值操作,更建议使用条件操作符。
let max = (num1 > num2) ? num1 : num2;
在这个例子中,max 将被赋予一个最大值。这个表达式的意思是,如果 num1 大于 num2(条件表达式为 true),则将 num1 赋给 max。否则,将 num2 赋给 max。
5.10 赋值操作符(=)
5.10.1 简单写法,直接赋值
let num = 10;
5.10.2 复合赋值(比如 += ,但是不推荐,影响阅读,实际也不提升性能,只是简写)
使用乘性、加性或位操作符后跟等于号(=)表示,比如:
let num = 10;
num += 10;
等价于
let num = 10;
num = num + 10;
每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:
【1】乘后赋值(*=)
【2】除后赋值(/=)
【3】取模后赋值(%=)
【4】加后赋值(+=)
【5】减后赋值(-=)
【6】左移后赋值(<<=)
【7】右移后赋值(>>=)
【8】无符号右移后赋值(>>>=)
这些操作符仅仅是简写语法,使用它们不会提升性能。
5.1 逗号操作符( , 用于在一条语句中执行多个操作)
(1)逗号操作符可以用来在一条语句中执行多个操作,如下所示:
let num1 = 1, num2 = 2, num3 = 3;
(2)在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:
let num = (5, 1, 4, 8, 0); // num 的值为 0
怎么说呢,这个写法非常无聊,看完马上就可以就忘了,正常人都不会这么写,除非为了迷惑队友。
6. 语句
(1)ECMA-262 描述了一些语句(也称为流控制语句),而 ECMAScript 中的大部分语法都体现在语句中;
(2)语句通常使用一或多个关键字完成既定的任务。语句可以简单,也可以复杂。简单的如告诉函数退出,
复杂的如列出一堆要重复执行的指令。
6.1 if 语句(最佳实践为使用代码块)
(1)if 语句是使用最频繁的语句之一,语法如下:
if (condition) statement1 else statement2
(1)这里的条件(condition)可以是任何表达式,并且求值结果不一定是布尔值;
(2)ECMAScript 会自动调用 Boolean()函数将这个表达式的值转换为布尔值。如果条件求值为 true,则执行语句statement1;如果条件求值为 false,则执行语句 statement2;
(3)if 语句的最优实践是使用代码块,如:
if (i > 25) {
console.log("Greater than 25.");
} else if (i < 0) {
console.log("Less than 0.");
} else {
console.log("Between 0 and 25, inclusive.");
}
执行语句特别复杂,或者连续使用多个if,如果仍然使用一行代码执行,将会使代码变得难以阅读和理解。
6.2 do-while 语句(后测试循环语句,循环退出前至少执行一次体内代码)
do-while 语句是一种后测试循环语句,即循环体中的代码执行后才会对退出条件进行求值。换句
话说,循环体内的代码至少执行一次。do-while 的语法如下:
do {
statement
} while (expression);
下面是一个例子:
let i = 0;
do {
i += 2;
} while (i < 10);
在这个例子中,只要 i 小于 10,循环就会重复执行。i 从 0 开始,每次循环递增 2。
注意,后测试循环经常用于这种情形:循环体内代码在退出前至少要执行一次。
6.3 while 语句(先测试循环语句,循环体可能一次都不执行)
while 语句是一种先测试循环语句,即先检测退出条件,再执行循环体内的代码。因此,while 循
环体内的代码有可能不会执行。下面是 while 循环的语法:
while(expression) statement
这是一个例子:
let i = 0;
while (i < 10) {
i += 2;
}
在这个例子中,变量 i 从 0 开始,每次循环递增 2。只要 i 小于 10,循环就会继续。
6.4 for 语句(先测试循环语句,循环中的万金油,很好用)
(1)for 语句也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表
达式,语法如下:
for (initialization; expression; post-loop-expression) statement
例子如下:
let count = 10;
for (let i = 0; i < count; i++) {
console.log(i);
}
(2)可以看到 for 循环和 while 循环非常相似,功能几乎一致,只是将循环相关的代码封装到了一起。因此,如果使用while循环无法实现的逻辑,for循环也不能。如果只包含条件表达式,for循环实际上就变成了wihle循环:
let count = 10;
let i = 0;
for (; i < count; ) {
console.log(i);
i++;
}
(3)初始化定义的迭代器变量、条件表达式、循环后表达式都不是必须的。如果都不写,就变成了一个无限循环语句,比如:
for (;;) { // 无穷循环
console.log(111)
}
但是要小心,可能会导致部分浏览器卡死。按理说这种语句不应该写出来。
6.5 for-in 语句(迭代枚举对象中的非符号键属性,不推荐迭代数组)
(1)for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:
for (property in expression) statement
例子如下:
for (const propName in window) {
console.log(propName);
}
这个例子使用 for-in 循环显示了 BOM 对象 window 的所有属性。每次执行循环,都会给变量propName 赋予一个 window 对象的属性作为值,直到 window 的所有属性都被枚举一遍。
(2)与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const;
(3)ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序。返回顺序可能会因浏览器而异;
(4)如果 for-in 循环要迭代的变量是 null 或 undefined,则不执行循环体;但是实际上,如果给对象添加key为null或者undefined,会被自动转为字符串。
(5)也可以循环数组,但是不推荐。
严格来说,数组是对象的派生对象,因此也可以使用 for-in 语句,但是枚举的属性(数组下标,Number类型)会被自动转为 String 类型。为了避免不必要的未知错误,不推荐使用 for-in 循环数组。
6.6 for-of 语句(迭代数组中的元素,不可迭代对象)
(1)for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素,语法如下:
for (property of expression) statement
let arr = [111,222,333,444];
for(const item of arr) {
console.log(item)
}
(2)与 for 循环一样,这里控制语句中的 const 也不是必需的。但为了确保这个局部变量不被修改,推荐使用 const。
(3)for-of 循环会按照可迭代对象的 next()方法产生值的顺序迭代元素。关于可迭代对象,本书将在第 7 章详细介绍。
(4)如果尝试迭代的变量不支持迭代,则 for-of 语句会抛出错误。
(5)注意 ES2018 对 for-of 语句进行了扩展,增加了 for-await-of 循环,以支持生成期约(promise)的异步可迭代对象。相关内容将在附录 A 介绍。
6.7 标签语句(通过 break 或 continue 语句引用,用于跳出整个大循环,看 6.8)
标签语句用于给语句加标签,语法如下:
label: statement
例子如下:
start: for (let i = 0; i < count; i++) {
console.log(i);
}
在这个例子中,start 是一个标签,可以在后面通过 break 或 continue 语句引用。标签语句的典型应用场景是嵌套循环。
6.8 break 和 continue 语句
break 和 continue 语句为执行循环代码提供了更严格的控制手段。
6.8.1 break 语句(打破当前整个循环)
(1)break 语句用于立即退出循环,强制执行循环后的下一条语句;
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
break;
}
num++;
}
console.log(num); // 4
6.8.2 continue 语句(停止当前语句,继续循环的下一个语句)
(2)continue 语句也用于立即退出循环,但会再次从循环顶部开始执行;
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
continue;
}
num++;
}
console.log(num); // 8
6.8.3 标签和 break 语句配合使用(跳出指定的循环)
break 和 continue 都可以与标签语句一起使用,返回代码中特定的位置,比如:
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break outermost;
}
num++;
}
}
console.log(num); // 55
在这个例子中,outermost 标签标识的是第一个 for 语句。正常情况下,每个循环执行 10 次,意味着 num++语句会执行 100 次,而循环结束时 console.log 的结果应该是 100。
但是,break 语句带来了一个变数,即要退出到的标签。添加标签不仅让 break 退出(使用变量 j 的)内部循环,也会退出(使用变量 i 的)外部循环。当执行到 i 和 j 都等于 5 时,循环停止执行,此时 num 的值是 55。
6.8.4 标签和 continue 语句配合使用(跳到指定循环的下一个内部循环)
let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
for (let j = 0; j < 10; j++) {
if (i == 5 && j == 5) {
break continue;
}
num++;
}
}
console.log(num); // 95
这一次,continue 语句会强制循环继续执行,但不是继续执行内部循环,而是继续执行外部循环。
当 i 和 j 都等于 5 时,会执行 continue,跳到外部循环继续执行,从而导致内部循环少执行 5 次,结
果 num 等于 95。
组合使用标签语句和 break、continue 能实现复杂的逻辑,但也容易出错。注意标签要使用描述
性强的文本,而嵌套也不要太深。
6.9 with 语句(是将代码作用域设置为特定的对象,不推荐使用)
(1)with 语句的用途是将代码作用域设置为特定的对象,语法为:
with (expression) statement;
(2)使用 with 语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便
利,看下面这个例子:
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
上面代码中的每一行都用到了 location 对象。如果使用 with 语句,就可以少写一些代码:
with(location) {
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}
这里,with 语句用于连接 location 对象。这意味着在这个语句内部,每个变量首先会被认为是一个局部变量。如果没有找到该局部变量,则会搜索 location 对象,看它是否有一个同名的属性。如果有,则该变量会被求值为 location 对象的属性。
(3)严格模式不允许使用 with 语句,否则会抛出错误。
(4)警告 由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with 语句。
总结下,就是可以少写些代码,但是会影响性能,而且难以调试,非常不建议用。
6.10 switch 语句(与 if 语句紧密相关的一种流控制语句)
(1)switch 语句是与 if 语句紧密相关的一种流控制语句,语法如下:
switch (expression) {
case value1:
statement
break;
case value2:
statement
break;
case value3:
statement
break;
case value4:
statement
break;
default:
statement
(2)这里的每个 case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”
(3)break关键字会导致代码执行跳出 switch 语句。如果没有 break,则代码会继续匹配下一个条件;
(4)default关键字用于在任何条件都没有满足时指定默认执行的语句(相当于 else 语句);
(5)有了 switch 语句,开发者就用不着写类似这样的代码了:
if (i == 25) {
console.log("25");
} else if (i == 35) {
console.log("35");
} else if (i == 45) {
console.log("45");
} else {
console.log("Other");
}
可以使用 switch 代替 if else 的写法,更加明了
switch (i) {
case 25:
console.log("25");
break;
case 35:
console.log("35");
break;
case 45:
console.log("45");
break;
default:
console.log("Other");
}
(6)为避免不必要的条件判断,最好给每个条件后面都加上 break 语句。如果确实需要连续匹配几个
条件(等于或),那么推荐写个注释表明是故意忽略了 break,比如:
switch (i) {
case 25:
/*跳过*/
case 35:
console.log("25 or 35");
break;
case 45:
console.log("45");
break;
default:
console.log("Other");
}
(7)switch 语句可以用于所有数据类型(在很多语言中,它只能用于数值),因此可以使用字符串甚至对象。
其次,条件的值不需要是常量,也可以是变量或表达式。比如:
switch ("hello world") {
case "hello" + " world":
console.log("Greeting was found.");
break;
case "goodbye":
console.log("Closing was found.");
break;
default:
console.log("Unexpected message was found.");
}
(8)能够在条件判断中使用表达式,就可以在判断中加入更多逻辑:
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;
case num > 10 && num <= 20:
console.log("Between 10 and 20.");
break;
default:
console.log("More than 20.");
}
(9)执行的语句可以是多条句子,直到break出去,比如:
function say(i){
switch (i) {
case 25:
/*跳过*/
case 35:
{
console.log('hhhh');
console.log('居然可以');
console.log("25 or 35");
break;
}
case 45:
console.log("45");
break;
default:
console.log("Other");
}
}
你果然,你也可以不用块 { } 将语句包裹,这么做只是为了看着更清晰,增强可维护性。
(10)注意 switch 语句在比较每个条件的值时会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值 10)。
7. 函数
7.1 基础语法
函数对任何语言来说都是核心组件,因为它们可以封装语句,然后在任何地方、任何时间执行。ECMAScript 中的函数使用 function 关键字声明,后跟一组参数,然后是函数体。
函数的基本语法:
function functionName(arg0, arg1,...,argN) {
statements
}
例子:
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
sayHi("Sheldon", "good morning?");
7.2 ECMAScript 中的函数是否需要返回值都可以
ECMAScript 中的函数不需要指定是否返回值。但是任何函数在任何时间都可以使用 return 语句来返
回函数的值,用法是后跟要返回的值。比如:
function sum(num1, num2) {
return num1 + num2;
}
const num = sum(1, 2);
console.log(num); // 3
7.3 return 后不再执行当前函数的任何语句
函数中执行到 return 后会马上返回值,不再执行当前函数的任何语句,比如:
function sum(num1, num2) {
return num1 + num2;
console.log('这句话不会被输出')
}
const num = sum(2, 3);
console.log(num);
7.4 return 不带任何值时,常用于提前终止函数执行
return 语句也可以不带返回值。这时候,函数会立即停止执行并返回 undefined。这种用法最常用于提前终止函数执行,并不是为了返回值。比如:
function sayHi(name, message) {
return;
console.log("Hello " + name + ", " + message); // 不会执行
}
7.5 函数的最佳实践(要么返回值,要么不返回值)
注意 最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。
(6)严格模式对函数也有一些限制:
【1】函数不能以 eval 或 arguments 作为名称;
【2】函数的参数不能叫 eval 或 arguments;
【3】两个命名参数不能拥有同一个名称。
如果违反上述规则,则会导致语法错误,代码也不会执行。
8. 小结
JavaScript 的核心语言特性在 ECMA-262 中以伪语言 ECMAScript 的形式来定义。ECMAScript 包含
所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的
机制。理解 ECMAScript 及其复杂的细节是完全理解浏览器中 JavaScript 的关键。下面总结一下
ECMAScript 中的基本元素。
(1)ECMAScript 中的基本数据类型(也叫简单数据类型,原始类型)包括 Undefined、Null、Boolean、Number、String 和 Symbol;
(2)与其他语言不同,ECMAScript 不区分整数和浮点值,只有 Number 一种数值数据类型;
(3)Object 是一种复杂数据类型,它是这门语言中所有对象的基类;
(4)严格模式为这门语言中某些容易出错的部分施加了限制;
(5)ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等;
(6)这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如 if 语句、for 语句和 switch语句等;
(7)ECMAScript 中的函数与其他语言中的函数不一样:
【7.1】不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值;
【7.2】不指定返回值的函数实际上会返回特殊值 undefined;
【7.3】不包含返回值的函数,直接使用return,目的是为了提前终止语句。