内容均摘自JavaScript高级程序设计第四版,仅用于记录学习过程。
- 虽然这一章都很基础,但是还是有很多小细节需要注意的!好好看看吧~
- 进一步细分了书里的目录,这样查询也更方便啦
第三章 语言基础
- 一.语法
- 3.1 语法
- 3.1.1 区分大小写
- 3.1.2 标识符
- 3.1.3 注释
- 3.1.4 严格模式
- 二.关键字与保留字
- 3.2 关键字与保留字
- 三.变量
- 3.3 变量
- 3.3.1 var关键字
- 1. var声明作用域
- 2. var声明提升
- 3.3.2 let声明
- 1. 暂时性死区
- 2. 全局声明
- 3. 条件声明
- 4. for循环中的let声明
- 3.3.3 const声明
- 3.3.4 声明风格及最佳实践
- 四.数据类型
- 3.4 数据类型
- 3.4.1 typeof操作符
- 3.4.2 Undefined 类型
- 3.4.3 Null 类型
- 3.4.4 Boolean 类型
- 3.4.5 Number 类型
- 1.浮点值
- 2.值的范围
- 3.NaN
- 4.数值转换
- 3.4.6 String 类型
- 1. 字符字面量
- 2. 字符串的特点
- 3. 转换为字符串
- 4. 模版字面量
- 5. 字符串插值
- 6. 模版字面量标签函数
- 7. 原始字符串
- 3.4.7 Symbol 类型
- 1. 符号的基本用法
- 2. 使用全局符号注册表
- 3. 使用符号作为属性
- 4. 常用内置符号
- 5. Symbol.asyncIterator
- 6. Symbol.iterator
- 7. Symbol.hasInstance
- 8. Symbol.isConcatSpreadable
- 9. Symbol.match
- 10. Symbol.replace
- 11. Symbol.search
- 12. Symbol.species
- 13. Symbol.split
- 14. Symbol.toPrimitive
- 15. Symbol.toStringTag
- 16. Symbol.unscopables
- 3.4.8 Object 类型
- 五.操作符
- 3.5 操作符
- 3.5.1 一元操作符
- 1.递增/递减操作符
- 2.一元加和减
- 3.5.2 位操作符
- 1. 按位非
- 2. 按位与
- 3. 按位或
- 4. 按位异或
- 5. 左移
- 6. 有符号右移
- 7. 无符号右移
- 3.5.3 布尔操作符
- 1. 逻辑非
- 2. 逻辑与
- 3. 逻辑或
- 3.5.4 乘性操作符
- 1. 乘法操作符
- 2. 除法操作符
- 3. 取模操作符
- 3.5.5 指数操作符
- 3.5.6 加性操作符
- 1. 加法操作符
- 2. 减法操作符
- 3.5.7 关系操作符
- 3.5.8 相等操作符
- 1. 等于和不等于
- 2. 全等和不全等
- 3.5.9 条件操作符
- 3.5.10 赋值操作符
- 3.5.11 逗号操作符
- 六.语句
- 3.6 语句
- 3.6.1 if语句
- 3.6.2 do-while语句
- 3.6.3 while语句
- 3.6.4 for语句
- 3.6.5 for-in语句
- 3.6.6 for-of语句
- 3.6.7 标签语句
- 3.6.8 break和continue语句
- 3.6.9 with语句
- 3.6.10 switch语句
- 七.函数
- 3.7 函数
- 八.小结
一.语法
3.1 语法
3.1.1 区分大小写
ECMAScript中一切都区分大小写。
3.1.2 标识符
标识符:变量、函数、属性或函数参数的名称。
- 第一个字符必须是字母、下划线(_)、美元符号($)
- 其他字符可以是字母、下划线、美元符号、数字
按照惯例,ECMAScript 标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写。
⚠️:关键字
、保留字
、true
、false
、null
不能作为标识符。
3.1.3 注释
// 这是单行注释
/* 这是多行
注释*/
3.1.4 严格模式
ES 5增加了严格模式(strict mode)的概念
严格模式是一种不同的JavaScript解析和执行模型,对于不安全的活动将抛出错误。
如要对整个脚本启用严格模式,在脚本开头加这一行:
"use strict";
它是一个预处理指令,任何支持的JavaScript引擎看到它都会切换到严格模式。
也可以单独指定一个函数在严格模式下执行,将这个预处理指令放到函数体开头即可:
function func(){
"use strict";
// 函数体
}
所有现代浏览器都支持严格模式。
二.关键字与保留字
3.2 关键字与保留字
- ECMA-262 描述了一组保留的关键字,这些关键字有特殊用途,保留的关键字不能用作标识符或属性名。
break
、case
、catch
、class
、const
、continue
、debugger
、default
、delete
、do
、else
、export
、extends
、finally
、for
、function
、if
、import
、in
、instanceof
、new
、return
、super
、switch
、this
、throw
、try
、typeof
、var
、void
、while
、with
、yield
- 规范中也描述了一组未来的保留字,虽然保留字在语言中没有特定 用途,但它们是保留给将来做关键字用的。
- 始终保留:
enum
- 严格模式下保留:
implements
、interface
、let
、package
、protected
、private
、public
、static
- 模块代码中保留:
await
- 始终保留:
三.变量
3.3 变量
- ECMAScript变量是松散类型的,变量可保存任何类型的数据。
var
、const
、let
3个关键字可以声明变量var
可以在ES的任何版本使用,const
、let
只能在ES 6及更晚版本中使用。
3.3.1 var关键字
var message;
这行代码定义了一个名为message的变量,可以用它保存任何类型的值。在不初始化的情况下,变量会保存一个特殊值undefined
。var message = "hi";
这里message被定义为一个保存字符串值hi的变量。
但这样初始化变量并不会将它标识为字符串类型,只是一个简单复制而已。
之后,不仅可以改变保存的值,也可以改变值的类型。var message = "hi";
message = 100; // 合法但不推荐
虽然不推荐改变变量保存值的类型,但在ECMAScript中是完全有效的。
1. var声明作用域
- 使用
var
操作符定义的变量会成为包含它的函数的局部变量,在一个函数内部定义一个变量,意味着该变量将在函数退出时被销毁。
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
- 在函数内定义变量时忽略
var
操作符,可以创建一个全局变量:
如下例:只要调用一次test()
,就会定义这个变量,并且可以在函数外部访问到。
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
- ⚠️:虽然可以通过省略
var
操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护。在严格模式下,给这样未声明的变量赋值,会导致抛出ReferenceError
- 如需定义多个变量,可在一条语句中用逗号分隔每个变量:
var message = "hi",
found = false,
age = 29;
2. var声明提升
使用var
关键字声明的变量会自动提升到函数作用域顶部
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
ECMAScript运行时把等价于如下代码:
function foo() {
var age;
console.log(age);
age = 26;
}
⚠️:变量声明会提升,变量赋值不会提升。
此外,使用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
声明的范围是函数作用域。
// var声明
if(true) {
var name = "Matt";
console.log(name); // Matt
}
console.log(name); // Matt
// let声明
if(true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
在这里,let
声明的age
变量不能在if
块外部被引用,因为它的作用域仅限于该块内部。
⚠️:块作用域是函数作用域的子集,因此适用于var
的作用域限制,同样也适用于let
。
此外,使用let
不允许在同一个块作用域中,反复声明同一个变量,会导致报错。
嵌套使用相同的变量声明标识符不会报错,这是因为在同一个块中没有重复声明:
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(语法错误)
1. 暂时性死区
let
与var
另一个重要区别:
let
声明的变量不会在块作用域中被提升。
console.log(name); // undefined
var name = "Matt"; // name被提升
console.log(age); // ReferenceError: age没有定义
let age = 26; // age不会被提升
⚠️:ES 6规定,如果区块作用域中存在let
命令(和const
命令),这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。在let
声明变量之前的执行瞬间被称为“暂时性死区”
(temporal dead zone,简称TDZ),在暂时性死区阶段引用任何后面才声明的变量都会抛出ReferenceError
2. 全局声明
let
与var
又一个区别:
let
在全局作用域中声明的变量不会成为window
对象的属性,var
声明的变量则会。
// var声明
var name = "Matt";
console.log(window.name); // "Matt"
// let声明
let age = 25;
console.log(window.age); // undefined
3. 条件声明
在用var
声明变量时,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明
但用let
声明变量时,由于let
的作用域是块,所以不可能检查前面是否已使用let
声明过同名变量。
<script>
var name = "Nicholas";
let age = 26;
</script>
<script>
// 假设脚本不确定是否已声明过同名变量,可以假设未声明过,此处可以被作为一个声明提升。
var name = "Matt";
// 如果age在这之前声明过,这里会报错
let age = 36;
⚠️:使用try/catch
语句或typeof
操作符也无法解决。因为条件块中let
声明的作用域也仅限于该块。
<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
// 假设脚本不确定是否已声明过同名变量,可以假设未声明过
if (typeof name === 'undefined') {
let name;
}
// name 被限制在 if {} 块的作用域内
// 因此这个赋值形同全局赋值
name = 'Matt';
try {
console.log(age); // 如果age未声明过,会报错
}
catch(error) {
let age;
}
// age被限制在catch{}块的作用域内
// 因此这个赋值形同全局赋值
age = 26;
</script>
因此,⚠️:对于let
,不能依赖条件声明模式。条件声明是一种反模式,它让程序变得更难理解,不能使用let
进行条件声明是件好事。
4. for循环中的let声明
let
与var
的又一不同:
for
循环中由var
关键字声明的迭代变量会渗透到循环体外部,而由let
声明的迭代变量则不会,因为其作用域仅限于for
循环块中。
// var声明
for (var i = 0; i < 5; i++) {
// 循环逻辑
}
console.log(i); // 5
// let声明
for (let i = 0; i < 5; i++) {
// 循环逻辑
}
console.log(i); // ReferenceError: i没有定义
⚠️:在使用var
的时候,有常见问题:
// var声明迭代变量
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出5、5、5、5、5
因为在退出循环时,迭代变量i保存的是导致循环退出的值:5。所以在之后执行超时逻辑时,所有的i值都为5,因而输出同一个值。
// let声明迭代变量
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0)
}
// 输出0、1、2、3、4
而用let
声明时,JavaScript引擎会为每次迭代循环声明一个新的迭代变量,所以每个setTimeout
引用的都是不同的变量实例。
⚠️:这种每次迭代声明一个独立变量实例的行为适用于所有风格的for
循环,包括for-in
和for-of
循环。
3.3.3 const声明
const
与let
唯一一个重要区别:
const
声明变量时必须同时初始化变量,且尝试修改const
声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError: 给常量赋值
⚠️:const
声明时必须初始化的限制只适用于它指向的变量,如果它引用的是一个对象,修改该对象内部的属性是不违反限制的。
const person = {};
person.name = "Matt"; // ok
虽然const
和let
很相似,但:
- 不能用
const
来声明迭代变量,因为迭代变量会自增。 - 但用
const
声明一个不会被修改的for
循环变量,是可以的。
for (const i = 0; i < 10; i++) // TypeError
/*
以下代码段每次迭代只是用const创建一个新变量,是可以的
*/
let i = 0;
for (const j = 7; i < 5; i++) {
console.log(j);
} // 7、7、7、7、7
// 这对for-of 和 for-in 循环特别有意义:
for (const key in {a : 1, b: 2}) {
console.log(key);
} // a, b
for (const value of [1,2,3,4,5]) {
console.log(value);
} // 1、2、3、4、5
3.3.4 声明风格及最佳实践
新的有助于提升代码质量的最佳实践:
- 不使用
var
限制自己只使用let
、const
,变量有了明确的作 用域、声明位置、不变的值。 const
优先,let
次之
使用const
声明可以让浏览器运行时强制保持变量不变,也可让静态代码分析工具提前发现不合法的赋值操作。只在提前知道未来会修改变量时再用let
。
四.数据类型
3.4 数据类型
ECMAScript 有6种简单数据类型(原始类型):
- Undefined
- Null
- Boolean
- Number
- String
- Symbol(符号)
1种复杂数据类型:
- Object
3.4.1 typeof操作符
typeof
操作符,来确定任意变量的数据类型
"object"
表示值为对象(而不是函数)或null
"function"
表示值为函数- …(部分省略)
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof (message)); // "string"
console.log(typeof 95); // "number"
typeof
是一个操作符而不是函数,所以不需要参数(但可以使用参数)
⚠️:typeof
在某些情况下返回的结果可能令人费解,但技术上讲是正确的。
比如:调用typeof null
返回的是"object"
,这是因为特殊值null
会被认为是一个对空对象的引用。
⚠️:严格来讲,函数在ECMAScript中被认为是对象,可是函数也有自己特殊的属性。因此有必要通过typeof
操作符来区分函数和其他对象。
3.4.2 Undefined 类型
Undefined
类型只有一个值,就是undefined
。- 当使用
var
或let
声明变量,但未初始化时,就相当于给变量赋值了undefined
值。
let message;
console.log(message == undefined); // true
// let message;此处等同于let message = undefined;
- ⚠️:值为
undefined
的变量与未定义变量不一样。
let message; // 这个变量被声明了,只是值为undefined
// let age;
console.log(message); // "undefined"
console.log(age); // 报错
- ⚠️:未声明变量只能执行一个有用操作,就是对它调用
typeof
- 对未初始化的变量调用
typeof
,返回"undefined"
,但对未声明的变量调用typeof
,还是返回"undefined"
。
let message; // 这个变量被声明了,只是值为undefined
// let age;
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"
- 无论是声明还是未声明,
typeof
返回的都是字符串undefined
,逻辑上讲这是对的,因为他们都无法执行实际操作。
3.4.3 Null 类型
Null
类型只有一个值,就是null
。- 逻辑上讲,
null
值表示一个空对象指针,因此typeof null
会返回"object"
- 定义将来要保存对象值的变量时,建议用
null
来初始化,不要用其他值。这样,只要检查此变量值是不是为null
,就可以知道它是否,之后被重新赋予了一个对象的引用。
if (car != null) {
// car是一个对象的引用
}
undefined
值是由null
值派生而来的,因此它们被定义为表面上相等。用等于操作符(==)比较null
和undefined
始终返回true
console.log(null == undefined); // true
⚠️:undefined
与null
用途完全不一样。永远不必显示地将变量值设置为undefined
,但当变量需要保存对象,而当时又没有那个对象可保存,就要用null
来填充该变量。(保持null
是空对象指针的语义,且近一步与undefined
区分)
3.4.4 Boolean 类型
Boolean
类型有两个字面值:true
、false
⚠️:true
、false
字面量是区分大小写的,因此True
、False
不是布尔值- 所有其他ECMAScript 类型的值都有相应布尔值的等价形式。将其他类型的值转换为布尔值,可以调用
Boolean()
函数
let message = "hello";
let messageAsBoolean = Boolean(message);
下表总结了其他类型的值与布尔值之间转换规则:
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
String | 非空字符串 | " "(空字符串) |
Number | 非零数值 | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
理解以上转换非常重要。
因为像if
等流控制语句会自动执行其他类型值到布尔值的转换:
let message = "hello";
if (message) {
console.log("value is true");
} // 正常输出
// 非空字符串message被自动转换为等价的布尔值true
3.4.5 Number 类型
- 对于十进制字面量。可直接写:
let intNumer = 55;
- 对于八进制字面量,第一个数字必须是0,然后是相应的八进制数字(0 - 7),若字面量中包含的数字超出了(0 - 7),则会忽略首0,后面的数字序列被当成十进制数。
⚠️:八进制字面量在严格模式下无效。
let octalNumber1 = 070; // 八进制的56
let octalNumber2 = 079; // 无效八进制值,当79处理
- 对于十六进制字面量,必须加前缀0x(区分大小写),然后是相应的十六进制数字(0 - 9 以及 A - F)
1.浮点值
- 要定义浮点值,数值中必须包含小数点
- 小数点后必须有至少一个数字
- 小数点前整数可省略,但推荐加上
- 小数点后没数字时,数值就会变成整数
(存储浮点值使用的内存空间是存储整数值的两倍,ECMAScript总是会把值转为整数) - 极大或极小的浮点值可用科学计数法表示
- 一个数值后跟一个大写或小写的字母e,再加上一个要乘的10的多少次幂
let floatNumber = 3.125e7; // 等于31250000
- 默认情况下,小数点后至少包含6个0的浮点值会被转换为科学计数法
- 浮点值计算并不精确,难以测试特定的浮点值
if (a + b == 0.3) { // 别这么干! }
2.值的范围
- ECMAScript可表示的最小值保存在
Number.MIN_VALUE
中,最大值保存在Number.MAX_VALUE
中 - 如果某个计算得到的数值超出了JavaScript 可以表示范围,那么此数值会被自动转换为一个特殊的
Infinity
(无穷)值- 任何无法表示的负数以
-Infinity
表示,任何无法表示的正数以Infinity
表示。 - 要确定一个值是不是介于JavaScript 能表示的 最小值和最大值之间(是否有限),可以使用
isFinite()
函数:let result = Number.MAX_VALUE + Number.MIN_VALUE; console.log(isFinite(result)); // false
- 任何无法表示的负数以
- ⚠️:
Number.NEGATIVE_INFINITY
和Number.POSITIVE_INFINITY
包含的值分别就是-Infinity
和Infinity
3.NaN
NaN
是一个特殊的数值,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(并不是抛出错误)
console.log(0/0); // NAN
console.log(-0/+0); // NAN
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
⚠️:
- 任何涉及
NaN
的操作始终返回NaN
,如NaN/10
- NaN不等于包含 NaN在内的任何值
console.log(NaN == NaN); // false
isNaN()
函数用于判断某个参数是否**“不是数值”**,参数可以是任意数据类型isNaN()
函数会尝试把该参数转换为数值,不能转换为数值的值会导致这个函数返回true
console.log(isNaN(NaN)); console.log(isNaN("10")); // false,可转换为数值10 console.log(isNaN("blue")); // true, 不可转换为数值
4.数值转换
有3个函数可以讲非数值转换为数值:
-
Number()
:用于任意数据类型- 布尔值:
true
->1
,false
->0
null
->0
undefined
->NaN
- 字符串:
- 包含数值字符,则转换为一个十进制数值
- 包含有效的浮点值(如"1.1"),则转换为相应浮点值
- 包含有效的十六进制格式(如"0xf"),则转换为该十六进制对应的十进制数值
- 空字符串->
0
- 若包含上述情况以外的字符,返回
NaN
- 对象:先调用
valueOf()
方法,并按照上述规则转换返回的值,若转换结果为NaN
,则调用toString()
方法,返回NaN
let num1 = Number("Hello world!"); // NaN let num2 = Number(""); // 0 let num3 = Number("000011"); // 11 let num4 = Number(true); // 1
- 布尔值:
-
parseInt()
:主要用于将字符串转换为数值,通常需要得到整数时优先使用parseInt()
函数- 从第一个非空格字符开始转换,如果第一个字符不是数值字符、加号或减号,
parseInt()
立即返回 NaN - 空字符串也会返回
NaN
(这一点跟Number()
不一样,它返回0
) - 如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符
- 比如:"1234blue"会被转换为 1234,因为"blue"会被完全忽略
- 若字符串的第一个字符是数值字符,
parseInt()
函数能识别不同的整数格式。例如字符串以"0x"开头,就会被解释成十六进制整数 parseInt()
接收第二个参数,用于指定进制数,如要解析的值是十六进制,可传入16作为第二个参数,且若提供了十六进制参数,字符串前的"0x"可省略let num1 = parseInt("0xAF",16); // 175 let num2 = parseInt("AF",16); // 175
- 从第一个非空格字符开始转换,如果第一个字符不是数值字符、加号或减号,
-
parseFloat()
:与parseInt()
类似,也是主要用于将字符串转换为数值- 解析到字符串末尾,或解析到一个无效的浮点数值字符为止
parseFloat()
始终忽略字符串开头的0,因此十六进制数值始终返回0
- 且
parseFloat()
只解析十进制值,因此不能指定进制数
let num1 = parseFloat("1234blue"); // 1234,按整数解析 let num2 = parseFloat("0xA"); // 0 let num3 = parseFloat("22.5"); // 22.5 let num4 = parseFloat("0908.5"); // 908.5 let num5 = parseFloat("3.125e7"); // 31250000
3.4.6 String 类型
字符串可使用双引号(")、 单引号(’)或反引号(`)标示
1. 字符字面量
字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,字符字面量被作为单个字符解释。
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\\ | 反斜杠 (\) |
’ | 单引号(’) |
" | 双引号(") |
` | 反引号(`) |
2. 字符串的特点
ECMAScript 中的字符串是不可变的。
要修改某个变量的字符串值,必须先销毁原始的字符串,再将包含新值的另一个字符串保存到该变量,如:
let lang = "Java";
lang = lang + "Script";
整个过程:首先分配一个足够容纳10个字符的空间,接着销毁掉原始字符串"Java"
和"Script"
3. 转换为字符串
有两种方法把一个值转换为字符串:
-
1.
toString()
方法,返回当前值的字符串等价物null
和undefined
值没有toString()
方法- 多数情况,不接收任何参数。对数值调用这个方法时,
toString()
可以接收一个参数作为进制数:即以什么进制数来输出该数值的字符串表示let num = 10; console.log(num.toString()); // "10" console.log(num.toString(2)); // "1010" console.log(num.toString(16)); // "a"
-
2.
String()
转型函数,如果你不确定一个值是不是null
或undefined
,String()
遵循如下:- 如果该值有
toString()
方法,则调用该方法并返回结果 - 如果该值为
null
,则返回null
- 如果该值为
undefined
,则返回undefined
let value1 = 10; let value2 = true; let value3 = null; let value4; console.log(String(value1)); // "10",与调用toString()相同 console.log(String(value2)); // "true",与调用toString()相同 console.log(String(value3)); // "null" console.log(String(value4)); // "undefined"
- 如果该值有
⚠️:用加号操作符(+)给一个值加上一个空字符串""也可将其转换为字符串
4. 模版字面量
ES 6新增,使用模版字面量定义字符串的能力。
模版字面量保留换行字符,可以跨行定义字符串。
模版字面量在定义模版时特别有用,比如下面这个HTML模版:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
5. 字符串插值
- 模版字面量最常用的一个特性就是支持字符串插值
- 技术上讲,模版字面量不是字符串,而是一种特殊的JavaScript 句法表达式,只是求值后得到的是字符串
- 模版字面量在定义时立即求值,并转换为字符串实例
- 字符串插值通过在
${}
中使用JavaScript表达式实现,插入的值都会使用toString()
强制转型为字符串:
let value = 5;
let exponent = second;
// 以前的字符串插值
let insertString = value + "to the " + exponent + "power is" + (value * value);
// 现在,可以用模版字面量这样实现
let insertString = `${value} to the ${exponent} power is ${value * value}`
- 嵌套的模版字符串不用转义:
console.log(`Hello, ${`World`}!`); // Hello, World!
- 插值表达式
${}
中可以调用函数和方法:
function foo(word) {
// toUpperCase()将字符串转化为大写字母
// slice(1)表示从第一个元素开始取,取到结束
return `${ word[0].toUpperCase()}${ word.slice(1) }`;
}
console.log(`${foo("hello")},${foo("world")}`); // Hello, world
- 也可以插入自己之前的值:
let value = '';
function append() {
value = `${value}abc`
console.log(value);
}
append(); // abc
append(); // abcabc
append(); // abcabcabc
6. 模版字面量标签函数
- 模版字面量也支持标签函数,通过标签函数可以自定义插值行为
- 标签函数,接收被插值记号分隔后的模版,和对每个表达式求值的结果。如:
let a = 6;
let b = 9;
function tagFunc(strings,aValue,bValue,sumValue){
console.log(strings); // ["", "+", "=", ""]
console.log(aValue); // 6
console.log(bValue); // 9
console.log(sumValue); // 15
return sumValue;
}
let tagResult = tagFunc`${a} + ${b} = ${a+b}`;
console.log(tagResult) // 15
- 第一个参数接收:被插值记号分隔后的模版,并存入数组,例如插值为
${a}
,则被分隔的模版是左边的"" 与右边的"",存入数组["",""]。例如插值为${a}+
,则被分割的模版是左边"“与右边”+",存入数组["","+"] - 可以用剩余操作符将表达式其余参数收集到一个数组中:
function tagFunc(strings, ...values) {
console.log(strings);
for(const value of values) {
console.log(value);
}
return 'foobar';
}
7. 原始字符串
- 可以使用默认的
String.raw
标签函数,获取原始的模版字面量内容,而不是转换后的字符表示。
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
⚠️:对实际换行符来说不行
3.4.7 Symbol 类型
Symbol
(符号)是ES 6 新增的数据类型
- 符号是原始值,且符号实例是唯一、不可变的
- 符号是用来创建唯一记号,进而用作非字符串形式的对象属性。
1. 符号的基本用法
- 使用
Symbol()
函数初始化符号let sym = Symbol();
- 调用
Symbol()
函数时,也可以传一个字符串参数作为对符号的描述.
⚠️:此字符串参数与符号定义或标识完全无关。
let sym1 = Symbol();
let sym2 = Symbol();
let fooSym1 = Symbol("foo");
let fooSym2 = Symbol("foo");
console.log(sym1 == sym2); // false
console.log(fooSym1 == fooSym2); // false
- 符号没有字面量语法,这是它们发挥作用的关键。
Symbol()
函数不能与new
关键字一起作为构造函数使用,这是为了避免创建符号包装对象。
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
⚠️:想使用符号包装对象,可借用Object()
函数
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof mySymbol); // "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'); // 使用Symbol()定义的
let globalSymbol = Symbol.for('foo'); // 全局注册表中定义的
console.log(localSymbol === globalSymbol); // false
- 全局注册表中的符号必须使用字符串键来创建,因此作为参数传给
Symbol.for()
的任何值都会被转换为字符串
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
- 可使用
Symbol.keyFor()
来查询全局注册表,接受符号,返回该全局符号对应的字符串键。- 如果查询的不是全局符号,则返回
undefined
- 如果查询的不是全局符号,则返回
// 创建全局符号
let s1 = Symbol.for('foo');
console.log(Symbol.keyFor(s1)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
3. 使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。
这就包括了对象字面量
属性和Object.defineProperty()
/ Object.defineProperties()
定义的属性
对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
let obj = {
[s1]: 'foo val'
};
console.log(obj); // {Symbol(foo): foo val}
Object.defineProperty(obj,s2,{value: 'bar val'});
console.log(obj); // {Symbol(foo): foo val, Symbol(bar): bar val}
4. 常用内置符号
- 常用内置符号用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。
- 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。
- 比如,我们知道
for-of
循环会在相关对象上使用Symbol.iterator
属性,那么就可以通过在自定义对象上重新定义Symbol.iterator
的值,来改变for-of
在迭代该对象时的行为。 - 这些内置符号是全局函数
Symbol
的普通字符串属性,指向一个符号的实例
⚠️:在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@
。比如, @@iterator
指的就是 Symbol.iterator
5. Symbol.asyncIterator
这个符号作为一个属性,表示“一个方法,该方法返回对象默认的 AsyncIterator”
由 for-await-of 语句使用
换句话说,这个符号表示实现异步迭代器 API 的函数。
for-await-of
循环会利用这个函数执行异步迭代
操作- 循环时,它们会调用以
Symbol.asyncIterator
为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。 - 很多时候,返回的对象是实现该 API 的
AsyncGenerator
6. Symbol.iterator
这个符号作为一个属性,表示“一个方法,该方法返回对象默认的迭代器”
由 for-of 语句使用
换句话说,这个符号表示实现迭代器 API 的函数。
for-of
循环会利用这个函数执行迭代操作- 循环时,它们会调用以
Symbol.iterator
为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象 - 很多时候,返回的对象是实现该 API 的
Generator
7. Symbol.hasInstance
这个符号作为一个属性,表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例”
由 instanceof 操作符使用
instanceof
操作符可以用来确定一个对象实例的原型链上是否有原型,instanceof
的典型使用场景如下:
function Foo () {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true
在 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
这个属性定义在Function
的原型上,因此默认在所有函数和类上都可以调用。由于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
8. Symbol.isConcatSpreadable
这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用Array.prototype.concat()打平其数组元素”。
Array.prototype.concat()
方法会根据接收到的对象类型,选择如何将一个类数组对象拼接成数组实例。
覆盖Symbol.isConcatSpreadable
的值可以修改这个行为
- 数组对象默认情况下会被打平到已有数组,
Symbol.isConcatSpreadable
的值为false
或假值 - 会导致整个对象被追加到数组末尾。
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)]
- 类数组对象默认情况下会被追加到数组末尾,
Symbol.isConcatSpreadable
的值为true
或真值 - 会导致这个类数组对象被打平到数组实例
// 类数组对象
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']
9. Symbol.match
这个符号作为一个属性,表示“一个正则表达式方法,该方法用正则表达式去匹配字符串”
由 String.prototype.match()方法使用
String.prototype.match()
方法会使用以Symbol.match
为键的函数来对正则表达式求值- 正则表达式的原型上默认有这个函数的定义, 因此所有正则表达式实例默认是
String.prototype.match()
方法的有效参数
给这个方法传入非正则表达式值会导致该值被转换为 RegExp
对象
- 要改变这种行为,让方法直接使用参数
- 则可以重新定义
Symbol.match
函数,以取代默认对正则表达式求值的行为 - 从而让
match()
方法使用非正则表达式实例。
10. Symbol.replace
这个符号作为一个属性,表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串”
由 String.prototype.replace()方法使用
String.prototype.replace()
方法会使用以Symbol.replace
为键的函数来对正则表达式求值。- 正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是
String.prototype.replace()
方法的有效参数
给这个方法传入非正则表达式值会导致该值被转换为 RegExp
对象
- 如果想改变这种行为,让方法直接使用参数
- 则可以重新定义
Symbol.replace
函数以取代默认对正则表达式求值的行为 - 从而让
replace()
方法使用非正则表达式实例
11. Symbol.search
这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引”
由 String.prototype.search()方法使用
String.prototype.search()
方法会使用以Symbol.search
为键的函数来对正则表达式求值。- 正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是
String.prototype.search()
方法的有效参数
给这个方法传入非正则表达式值会导致该值被转换为 RegExp
对象
- 如果想改变这种行为,让方法直接使用参数
- 则可以重新定义
Symbol.search
函数以取代默认对正则表达式求值的行为 - 从而让
search()
方法使用非正则表达式实例
12. Symbol.species
这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象的构造函数”
这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法
用 Symbol.species
定义静态的获取器(getter)
方法,可以覆盖新创建实例的原型定义
13. Symbol.split
这个符号作为一个属性,表示“一个正则表达式方法,该方法在匹配正则表 达式的索引位置拆分字符串”
由 String.prototype.split()方法使用
String.prototype. split()
方法会使用以Symbol.split
为键的函数来对正则表达式求值。- 正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数
给这个方法传入非正则表达式值会导致该值被转换为 RegExp
对象
- 如果想改变这种行为,让方法直接使用参数,
- 则可以重新定义
Symbol.split
函数以取代默认对正则表达式求值的行为,从而让split()
方法使用非正则表达式实例
14. Symbol.toPrimitive
这个符号作为一个属性,表示“一个方法,该方法将对象转换为相应的原始值”
由 ToPrimitive 抽象操作使用
- 很多内置操作都会尝试强制将对象转换为原始值,包括字符串、 数值和未指定的原始类型
- 对于一个自定义对象实例,通过在这个实例的
Symbol.toPrimitive
属性上定义一个函数可以改变默认行为
15. Symbol.toStringTag
这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述”
由内置方法 Object.prototype.toString()使用
- 通过
toString()
方法获取对象标识时,会检索由Symbol.toStringTag
指定的实例标识符,默认为"Object" - 内置类型已经指定了这个值,但自定义类实例还需要明确定义
16. Symbol.unscopables
这个符号作为一个属性,表示“一个对象,该对象所有的以及继承的属性, 都会从关联对象的 with 环境绑定中排除”
设置这个符号并让其映射对应属性的键值为 true
,就可以阻止该属性出现在 with
环境绑定中
⚠️:不推荐使用with
,因此也不推荐使用Symbol.unscopables
3.4.8 Object 类型
- 对象通过
new
操作符后跟对象类型的名称来创建 - 开发者可以通过创建
Object()
类型的实例来创建自己的对象,再给对象添加属性和方法:let obj = new Object();
(括号可省略,但不推荐)
- 每个
Object
实例都有如下属性和方法:constructor
:用于创建当前对象的函数,前例中这个属性值就是Object()
函数hasOwnProperty(propertyName)
:用于判断当前对象实例(不是原型)上是否存在给定的属性。⚠️:检查的属性名必须是字符串/符号isPrototypeOf(object)
:用于判断当前对象是否为另一个对象的原型propertyIsEnumerable(propertyName)
:用于判断给定属性是否可用。⚠️:检查的属性名必须是字符串toLocaleString()
:返回对象的字符串表示,该字符串反映对象所在的本地执行环境toString()
:返回对象的字符串表示valueOf()
:返回对象对应的字符串、数值、布尔值表示。通常与toString()
返回值相同
在ECMAScript中Object
是所有对象的基类,所以任何对象都有这些属性和方法。
五.操作符
3.5 操作符
在应用给对象时,操作符通常会调用valueOf()
和/或 toString()
方法来取得可以计算的值。
3.5.1 一元操作符
只操作一个值
1.递增/递减操作符
- 分前缀版和后缀版
- 前缀版:操作符位于要操作的变量前面
- 后缀版:操作符位于要操作的变量后面
- 无论是前缀版还是后缀版,变量的值都会在语句被求值后,立刻更改
- 后缀版与前缀版的主要区别在于:后缀版的递增和递减在语句被求值后才发生
let num1 = 2;
let num2 = 20;
let num3 = 2;
let num4 = --num1 + num2;
let num5 = num3-- + num2;
console.log(num4); // 21 测试前缀版
console.log(num5); // 22 测试后缀版
console.log(num1); // 1
console.log(num3); // 1
- 可作用于任何值,不限于整数,字符串、布尔值、浮点值、对象
- 对字符串:
- 有效的数值形式:转换为相应数值
- 无效的数值形式:转换为
NaN
- 对布尔值:
true
:转换为1
false
:转换为0
- 对对象:
- 调用其
valueOf()
方法取得可操作的值 - 如果是
NaN
,则调用toString()
并再次应用其他规则
- 调用其
- 对字符串:
2.一元加和减
一元加:
如果将一元加应用到非数值,则会执行与使用 Number()
转型函数一样的类型转换:
- 布尔值
false
和true
转换为0
和1
, - 字符串根据特殊规则进行解析
- 对象会调用它们的
valueOf()
和/或toString()
方法以得到可以转换的值。
let s1 = "01";
let s2 = "z";
let obj = {
valueOf() {
return -1;
}
};
s1 = +s1; // 值变为数值1
s2 = +s2; // 值变为NaN
obj = +obj; // 值变为数值-1
一元减:
- 数值使用一元减会将其变成相应的负值
- 在应用到非数值时,一元减会遵循与一元加同样的规则,先对它们进行转换,然后再取负值
3.5.2 位操作符
位操作符用于数值的底层操作,也就是操作内存中表示数据的位。
⚠️:ECMAScript中所有数值都以64 位格式存储
,但位操作不会直接应用到64位表示,而是先把值转换为32 位
整数,再进行位操作。
对开发者而言,就好像只有32 位
整数一样,因 为 64 位
整数存储格式是不可见
的
有符号整数使用32 位
的前 31 位
表示整数值,第32 位
表示数值的符号,0
表示正
,1
表示负
,这一位称为符号位
- 正值以真正的二进制格式存储,即
31 位
中的每一位都代表2 的幂
,第一位(称为第 0 位)表示 20,第二位表示 21- 比如,数值 18 的二进制格式为 00000000000000000000000000010010,后者是用到的 5 个有效位,决定了实际的值
- 负值以一种称为二补数(或补码)的二进制编码存储,一个数值的二补数通过如下 3 个步骤计算得到:
- (1) 确定
绝对值
的二进制表示(如,对于18,先确定 18 的二进制表示); - (2) 找到数值的一补数(或反码),换句话说,就是每个 0 都变成 1,每个 1 都变成 0;
- (3) 给结果加 1。
- (1) 确定
⚠️:在处理有符号整数
时,我们无法访问第 31 位
⚠️:默认情况下,ECMAScript 中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第 32 位不表示符号
,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。
⚠️:特殊值 NaN
和 Infinity
在位操作中都会被当成 0
处理。
1. 按位非
用波浪符(~)
表示,它的作用是返回数值的一补数
按位非的最终效果:对数值取反并减 1
let num1 = 25; // 二进制00000000000000000000000000011001
let num2 = ~num1; // 二进制11111111111111111111111111100110
console.log(num2); // -26
但按位非操作的速度,比对数值取反并减一的操作速度快得多,因为位操作是在数据的底层表示上完成的。
2. 按位与
用和号(&)
表示,有两个操作数
- 将两个数的每一个位对齐,对每一位执行相应的与操作
- 在两个位都是 1 时返回 1,在任何一位是 0 时返回 0
let result = 25 & 3;
console.log(result); // 1
3. 按位或
用管道符(|)
表示,有两个操作数
- 在至少一位是 1 时返回 1,两位都是 0 时返回 0
let result = 25 | 3;
console.log(result); // 27
4. 按位异或
用脱字符(^)
表示,有两个操作数
- 只在一位上是 1 的时候返回 1,两位都是 1 或 0,则返回 0
5. 左移
用两个小于号(<<)
表示,按照指定的位数将数值的所有位向左移动。
比如数值2向左移动5位,就会得到64
let oldValue = 2; // 等于二进制10
let newValue = oldValue << 5; // 等于二进制1000000,即十进制64
⚠️:在移位之后,数值右端会空出5位,左移会以0
填充这些空位,让结果是完整的32位
数值
⚠️:左移会保留该操作数的符号
6. 有符号右移
用两个大于号(>>)
表示,会将数值的所有 32 位都向右移,同时保留符号(正或负)。
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后
7. 无符号右移
用 3 个大于号(>>>)
表示
- 对于正数,无符号右移与有符号右移结果相同
- 对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补 0,而不管符号位是什么(补0直接补在符号位前面而不是后面)
let oldValue = -64; // 等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5; // 等于十进制 134217726
64 的二进制表示是 1111111111111111111 1111111000000,无符号右移却将它当成正值,也就是 4 294 967 232。把这个值右移 5 位后,结果是 00000111111111111111111111111110,即 134 217 726。
3.5.3 布尔操作符
布尔操作符一共有 3 个:逻辑非
、逻辑与
、逻辑或
。
1. 逻辑非
由一个叹号(!)
表示
无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反
遵循如下规则:
- 如果操作数是对象,则返回
false
- 如果操作数是空字符串,则返回
true
- 如果操作数是非空字符串,则返回
false
- 如果是
null
,则返回true
- 如果是
NaN
,则返回true
- 如果是
undefined
,则返回true
console.log(!""); // true
console.log(!NaN); // true
console.log(!null); // true
console.log(!undefined); // true
也可以同时使用两个叹号(!!)
,用于把任意值转换为布尔值,相当于调用了转型函数 Boolean()
2. 逻辑与
由两个和号(&&)
表示,应用到两个值
(&&)
可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则&&
并不一定会返回布尔值,而是遵循以下规则:
- 如果第一个操作数是对象,则返回第二个操作数
- 如果第二个操作数是对象,则只有第一个操作数求值为
true
才会返回该对象 - 如果两个操作数都是对象,则返回第二个操作数
- 如果有一个操作数是
null
,则返回null
- 如果有一个操作数是
NaN
,则返回NaN
- 如果有一个操作数是
undefined
,则返回undefined
⚠️:&&
是一种短路操作符,如果第一个操作数决定了结果,那么永远不会对第二个操作数求值
如果第一个操作数是false
,那么无论第二个操作数是什么值,结果也不可能等于true
下例:
let found = true;
let result = (found && undeclaredVariable); // 会报错
console.log(result); // 不会执行这一行
undeclaredVariable
没有事先声明,所以当&&
对它求值就会报错。因为found
值为true
,&&
会继续求值变量undeclaredVariable
但是如果found
值为false
,就不会报错了
let found = false;
let result = (found && someUndeclaredVariable); // 不会出错
console.log(result); // 会执行
这里的console.log
会成功执行。因为即使变量 undeclaredVariable
无定义,由于第一个操作数是false
,此时对&&
右边的操作数求值是没有意义的,所以&&
不会对它求值
⚠️:在使用&&
时,一定别忘了它的这个短路特性。
3. 逻辑或
由两个管道符(||)
表示
与&&
类似,如果有一个操作数不是布尔值,那么||
也不一定返回布尔值,而是遵循如下规则:
- 如果第一个操作数是对象,则返回第一个操作数
- 如果第一个操作数求值为
false
,则返回第二个操作数 - 如果两个操作数都是对象,则返回第一个操作数
- 如果有一个操作数是
null
,则返回null
- 如果有一个操作数是
NaN
,则返回NaN
- 如果有一个操作数是
undefined
,则返回undefined
⚠️:||
也有短路特性
第一个操作数求值为true
,第二个操作数就不会再被求值了
下例:
let found = true;
let result = (found || undeclaredVariable); // 不会出错
console.log(result); // 会执行
undeclaredVariable
没有事先声明。但是因为变量 found
的值为 true
,所以||
不会对变量undeclaredVariable
求值,而直接返回 true
。假如把found
的值改为false
,那就会报错了:
let found = false;
let result = (found || someUndeclaredVariable); // 这里会出错
console.log(result); // 不会执行这一行
利用这个行为,可以用于避免给变量赋值null
或undefined
:
let myObject = preferredObject || backupObject;
在这个例子中,变量 myObject
会被赋予两个值中的一个
3.5.4 乘性操作符
有3个乘性操作符:乘法
、除法
、取模
还是要注意,在处理非数值时,它们会包含一些自动的类型转换,该非数值的操作数会在后台被使用Number()
转型函数转换为数值
1. 乘法操作符
由一个星号(*)
表示
处理一些特殊值时:
- 有任一操作数是
NaN
,则返回NaN
Infinity
*0
,则返回 NaNInfinity
*非 0 的有限数值
,则根据第二个操作数的符号返回Infinity
或-Infinity
Infinity
*Infinity
,则返回Infinity
2. 除法操作符
由一个斜杠(/)
表示
处理一些特殊值时:
- 有任一操作数是
NaN
,则返回NaN
Infinity
/Infinity
,则返回NaN
0
/0
,则返回NaN
非 0 的有限数值
/0
,则根据第一个操作数的符号返回Infinity
或-Infinity
Infinity
/任何数值
,则根据第二个操作数的符号返回Infinity
或-Infinity
3. 取模操作符
由一个百分比符号(%)
表示
处理一些特殊值时:
无限值
%有限值
,则返回NaN
有限值
%无限值
,则返回被除数有限值
%0
,则返回NaN
Infinity
%Infinity
,则返回NaN
0
%非0值
,则返回0
3.5.5 指数操作符
由两个星号(**)
表示,和Math.pow()
效果一样
而且指数操作符也有自己的指数赋值操作符**=
let squared = 3;
squared **= 2;
console.log(squared); // 9
3.5.6 加性操作符
即加法操作符和减法操作符
1. 加法操作符
如果两个操作数都是数值:
- 有任一操作数是
NaN
,则返回NaN
Infinity
+Infinity
,则返回Infinity
-Infinity
+-Infinity
,则返回-Infinity
Infinity
+-Infinity
,则返回NaN
⚠️-0
++0
,则返回+0
不过,如果有一个操作数是字符串,则要应用如下规则:
- 两个都是字符串,则将第二个字符串拼接到第一个字符串后面
- 只有一个是字符串,则将另一个转换为字符串,再将两个字符串拼接在一起
- 如果有任一操作数是对象、数值或布尔值,则调用它们的
toString()
方法以获取字符串,然后再应用前面的关于字符串的规则
⚠️: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"
// 正确做法
let message = "The sum of 5 and 10 is " + (num1 + num2);
2. 减法操作符
与加法操作符应用规则一样
3.5.7 关系操作符
执行比较两个值的操作,包括小于(<)
、大于(>)
、小于等于(<=)
和大于等于(>=)
应用到不同数据类型时也会发生类型转换和其他行为:
- 操作数都是字符串,则逐个比较字符串中对应字符的编码
- 任一操作数是数值,则将另一个操作数转换为数值,执行数值比较
- 任一操作数是对象,则调用其
valueOf()
方法,如果没有valueOf()
操作符,则调用toString()
方法
在使用关系操作符比较两个字符串时:
- 小于意味着“字母顺序靠前”,而大于意味着“字母顺序靠后”,实际上不是这么回事
- 实际上,大写字母的编码都小于小写字母的编码
let result = "Brick" < "alphabet"; // true
这种情况就会发生- 因此,要得到按字母顺序比较的结果,就必须把两者都转换为相同的大小写形式(全大写或全小写), 然后再比较
- 另一个奇怪现象:
let result = "23" < "3"; // true
,因为两个都是字符串,所以会逐个比较它们的字符编码(字符"2"的编码是 50,而字符"3"的编码是 51),但如果有一个是数值,比较结果就正确了
let result = "23" < 3; // false
- 如果字符串不能转换成数值呢?
let result = "a" < 3; // 因为"a"会转换为NaN,所以结果是false
,这里有一个规则,即任何关系操作符在涉及比较NaN
时都返回 falselet result1 = NaN < 3; // false
let result2 = NaN >= 3; // false
在大多数比较中,如果一个值不小于另一个值,那就一定大于或等于它。但在比较 NaN 时, 无论是小于还是大于等于,比较的结果都会返回false
3.5.8 相等操作符
ECMAScript提供了两组操作符
- 第一组:
等于
和不等于
,在比较之前执行转换
- 第二组:
全等
和不全等
,在比较之前不执行转换
1. 等于和不等于
- 等于操作符用两个等于号
(==)
表示 - 不等于操作符用叹号和等于号
(!=)
表示
⚠️:这两个操作符都会先进行强制类型转换,再确定操作数是否相等。
- 如果一个操作数是对象,另一个操作数不是,则调用对象的
valueOf()
方法取得其原始值,再根据前面规则进行比较 null
和undefined
相等nul
l 和undefined
不能转换为其他类型的值再进行比较- ⚠️:有任一操作数是
NaN
,则相等
操作符返回false
,不相等
操作符返回true
- 即使两个操作数都为
NaN
,相等
操作符也返回false
NaN
不等于NaN
- 即使两个操作数都为
- 如果两个操作数都是对象,比较他们是否指向同一个对象
2. 全等和不全等
全等操作符由 3 个等于号(===)表示
不全等操作符由一个叹号和两个等于号(!==)表示
let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,数据类型不同
⚠️:null == undefined // true
null === undefined // false,数据类型不同
⚠️:推荐使用全等和不全等操作符,有助于在代码中保持数据类型的完整性。
3.5.9 条件操作符
语法:
let variable = boolean_expression ? true_value : false_value;
上面的代码执行了条件赋值操作
若boolean_expression
是 true
,则赋值true_value
到variable
若boolean_expression
是false
,则赋值false_value
到variable
3.5.10 赋值操作符
3.5.11 逗号操作符
- 逗号操作符可以用来在一条语句中执行多个操作
- 最常用在一条语句中同时声明多个变量
let num1 = 1, num2 = 2, num3 = 3;
- ⚠️:可以使用逗号操作符来辅助赋值,在赋值时使用逗号操作符分隔值,最终会返回表达式中最后一个值:
let num = (5, 1, 4, 8, 0); // num的值为0
六.语句
3.6 语句
3.6.1 if语句
语法如下:
if (condition) {
statement1
} else {
statement2
}
这里的condition
可以是任何表达式
,并且求值结果不一定是布尔值。ECMAScript 会自动调用 Boolean()
函数将这个表达式的值转换为布尔值
3.6.2 do-while语句
do-while
语句是一种后测试
循环语句,
即循环体中的代码执行后才会对退出条件进行求值
循环体内的代码至少执行一次
3.6.3 while语句
while
语句是一种先测试
循环语句,
即先检测退出条件,再执行循环体内的代码
循环体内的代码有可能不会执行
3.6.4 for语句
for
语句也是先测试
语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式
for (initialization; expression; post-loop-expression) {
statement
}
无法通过 while
循环实现的逻辑,同样也无法使用 for
循环实现。因为 for
循环只是将循环相关的代码封装在了一起而已
⚠️:初始化、条件表达式和循环后表达式都不是必需的
可创建无穷循环:
for (;;) { // 无穷循环 doSomething();
}
3.6.5 for-in语句
用于枚举对象中的非符号键属性
for (const propName in window) {
document.write(propName);
}
这个例子使用for-in
循环显示了BOM 对象 window
的所有属性。每次执行循环,都会给变量 propName
赋予一个 window
对象的属性作为值,直到 window
的所有属性都被枚举一遍。
与for
循环一样,这里控制语句中的const
也不是必需的,但为了确保这个局部变量不被修改,推荐使用const
⚠️:所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异
⚠️:如果 for-in
循环要迭代的变量是 null
或 undefined
,则不执行循环体
3.6.6 for-of语句
用于遍历可迭代对象的元素
for (const el of [2,4,6,8]) {
document.write(el);
}
在这个例子中,我们使用for-of
语句显示了一个包含 4 个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。
与for
循环一样,这里控制语句中的const
也不是必需的,但为了确保这个局部变量不被修改,推荐使用const
⚠️:for-of
循环会按照可迭代对象的next()
方法产生值的顺迭代元素
⚠️:如果尝试迭代的变量不支持迭代,则for-of
语句会抛出错误
3.6.7 标签语句
标签语句用于给语句加标签,语法如下:
label: statement
下面是个例子:
start: for (let i = 0; i < count; i++) {
console.log(i);
}
start
是一个标签,标签语句的典型应用场景是嵌套循环
3.6.8 break和continue语句
break
语句用于立即退出循环,强制执行循环后的下一条语句
continue
语句也用于立即退出循环,但会再次从循环顶部开始执行(退出的只是本层循环)
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
break;
}
num++;
}
console.log(num); // 4
当 break
语句执行后,退出循环,下一句执行的代码是console.log(num);
let num = 0;
for (let i = 1; i < 10; i++) {
if (i % 5 == 0) {
continue;
}
num++;
}
console.log(num); // 8
当i = 5
时,continue
语句执行,退出层循环,num++;
被跳过,但会执行下一次迭代,然后循环会一直执行到自然结束。
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(num);
的结果应该100。
但break
语句带来了一个变数,即要退出到的标签,break outermost
表示不仅要退出(使用j的内层循环),且退出(使用i的外层循环),所以当i
和j
都等于5时,循环停止执行。
3.6.9 with语句
with
用于将代码作用域设置为特定的对象,语法:
with (expression) statement;
使用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
对象的属性
⚠️:with
语句影响性能,且难于调试器中的代码,通常不推荐在产品代码中使用with
语句
3.6.10 switch语句
switch (expression) {
case value1:
statement
break;
case value2:
statement
break;
case value3:
statement
break;
default:
statement
}
- 每个
case
(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。” break
会导致代码执行跳出switch
语句,但是如果没有break
,代码则会继续匹配下一个条件。default
用于在任何条件都没有满足时指定默认执行的语句(相当于else
语句)
⚠️:
switch
语句可以用于所有数据类型,因此可以使用字符串甚至对象- 条件的值不需要是常量,也可以是变量或者表达式
switch ("hello world"){
case "hello" + "world":
console.log("Greetign was found.");
break;
case "goodbye":
console.log("Closing was found.");
break;
default:
console.log("Unexpected message was found.");
}
⚠️:switch
语句在比较每个条件的值时会使用全等
操作符,因此不会强制转换数据类型
七.函数
3.7 函数
function functionName(arg0, arg1,...,argN) {
statements
}
⚠️:最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来 麻烦,尤其是调试时
八.小结
下面总结一下 ECMAScript 中的基本元素:
- ECMAScript中的基本数据类型包括
Undefined
、Null
、Boolean
、Number
、String
、Symbol
- ECMAScript不区分整数和浮点值,只有
Number
着一种数值数据类型 Object
是一种复杂数据类型,它是这门语言中所有对象的基类- 严格模式为这门语言中某些容易出错的地方施加了限制
- ECMAScript中的函数与其他语言中的函数不太一样
- 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值
- 不指定返回值的函数实际上会返回特殊值
undefined