类型、值、变量
JS 中类型分为 原始类型 和 对象类型 。
原始类型有:number、string、boolean、symbol、null、undefined、bigint 。
JS 解释器会执行自动垃圾收集,开发者不需要关心值和对象的析构和释放,解释器会知道什么值已经用不到了,然后释放它占用的内存。
对象类型是可修改的,而原始类型是 不可修改 的( immutable )。
数值
JS 用 Number 表示整数和近似实数。
使用 IEEE 754 标准 定义的 64 位双精度浮点型表示数值,可以准确表示 [-253, 253] 区间的所有整数,超出这个范围则会损失精度。
64 位中最高位为符号位,其次 11 位是指数位,低位则为 52 位尾数。事实上,尽管 ±253 能准确对应一个整数,但不能算作安全整数,因为其尾数已经损失了一位精度。
数值字面量
整数字面量 中,可以用 0x or 0X 开头来表示十六进制。在 ES6 之后版本中,可以用 0b or 0B 表示二进制,0o or 0O 表示八进制。
浮点字面量 遵循这样的语法形式:[digits][.digits][(E|e)[(+|-)]digits] 。
ES2021 中可以用 数字分隔符 _ 来进行数字的位数分组,提高可读性。如 1_0000_0000 、1_234_567 。
数值算术
支持 +、-、*、/、%,以及 ES2016 的 ** 幂运算。Math 对象提供了一些数学函数及常量。
数值操作中出现 上溢出 会得到 ±Infinity 。对无穷值的加减乘除操作皆为无穷。而 下溢出 会得到 ±0 。正负零值相等,即便使用严格相等比较。非零数被零除会被判为无穷,零除以零会得到非数值 NaN 。NaN 与任何值均不等,包括自身,判断非数值应当用 Number.isNaN() 函数。
由于精度问题,谨慎比较两个浮点数的等于关系。
BigInt
ES2020 中定义了 BigInt 数值类型来表示任意精度整数。字面量以 n 结尾,比如 9000n 。
支持基本的算术和比较,但注意 0 === 0n 结果为 false 。同时,Math 对象不支持大数值。
可以用 BigInt() 将数值或字符串转为大数值。
字符串
JS 用 String 表示字符串。JS 使用 UTF-16 编码,所以字符串是无符号 16 位值的不可修改的有序序列,每个值表示一个 Unicode 字符。字符串的 length 属性则为 16 位值的个数,索引也是基于该值。
常见的 Unicode 字符的 码点( codepoint )为 16 位。若超过 16 位,则用两个 16 位值表示,称为 代理对 。意味着长度为 2 的字符串可能只表示一个 Unicode 字符。但在 ES6 中,字符串是可迭代的,迭代的是字符而不是 16 位值。
字符串字面量
可以用 ' 或 " 包裹字符串,ES6 中可以用反引号 ` 。字符串中若出现相同的引号则需要用 \ 转义。
早版本的 JS 要求字符串字面量必须写在一行,分行则需要用 + 。在 ES5 中可以用 \ 进行分行。在 ES6 中,反引号 ` 支持多行字面量。
转义序列
字符串中可以用的转义序列有:
反斜杠
\\NUL 字符
\0、退格符\b水平制表符
\t、换行符\n、垂直制表符\v、进纸符\f、回车符\r引号
\'、\"、\`两位 16 位值指定的 Unicode 字符
\xnn、四位\unnnn、码点 n 指定\u{n}此外如果字符前有反斜杠,则这个反斜杠会被忽略。
使用字符串
字符串支持 + 进行拼接,以及比较操作。只有当两个字符的 16 位值序列一致时才会相等以及全等。
使用 length 属性获得字符串长度,即 16 位值的个数。其余 API 可以见书上 P35 ,或 String - MDN :
获取子串
substring()、slice()、split()搜索
indexOf()、lastIndexOf()、【ES6】startsWith()、endsWith()、includes()访问字符
charAt()、charCodeAt()、【ES6】codePointAt()匹配
match()、matchAll()、search()、replace()、replaceAll()大小写
toLowerCase()、toUpperCase()归一化
【ES6】
normalize()填充
【ES2017】
padStart()、padEnd()去除空格
trim()、【ES2019】trimStart()、trimEnd()拼接
concat()、【ES6】repeat()
模板字符串
ES6 中,可以用反引号包裹字面量内容。同时可以在字符串内容中通过 ${表达式} 进行插值,比如:
let name = "Talaxy"
let greeting = `Hello ${name}.`
可以多行书写字面量,行末可以用反斜杠取消换行:
let text = `\
Hello Talaxy.
` // => "Hello Talaxy.\n"
可以在模板字面量前标注一个函数名,模板字面量中的文本以及表达式的值将作为参数传给这个函数。
布尔值
只有两个值 true or false 。支持 &&、||、! 运算。
JS 任何值都能转为布尔值。undefined、null、±0、NaN、"" 都会转为 false(和 false 一同称为假性值 falsy ),而其余的值为真性值,即转为 true 。
null & undefined
null 和 undefined 都表示某个值不存在,null 更接近于表示引用的空指向。两者相等,但不全等。
typeof null 为 "object" 。
符号
符号 Symbol 是 ES6 新增的一种原始类型,用作非字符串的属性名。
符号没有字面量语法,创建符号需要调用 Symbol() 函数,且这个函数永远不会返回相同的值,即使是相同的实参。所以使用符号可以为对象安全地添加新属性,无需担心重名问题。
虽然相同参数的两个符号不等,但是 toString() 方法的结果一般都是一致的:
Symbol("hello").toString() // => "Symbol(hello)"
为了与别的代码共享符号值,可以使用 Symbol.for() ,这个函数行为类似字典,给定一个参数,如果存在对应的符号值则返回,否则创建符号并返回。
全局对象
JS 解释器启动后,都会创建一个新的全局对象,并为其设置一些初始属性,比如:
undefined、Infinity、NaN等全局常量;isNaN()、parseInt()、eval()等全局函数;Date()、RegExp()、String()、Object()、Array()等构造函数;Math、JSON等全局对象。
这些全局属性及函数并非保留字,但应当视为保留字。
在 Node 中,全局对象中有个属性为 global ,指向全局对象自身;在浏览器中,Window 对象为全局对象,其也有个 window 属性指向全局对象自身。但最终在 ES2020 中,定义了 globalThis 作为任何上下文引用全局对象的标准方式。
原始值 & 对象引用
JS 的原始值与对象有个本质区别是,原始值不可修改 。在比较两值时,原始值是纯粹比较值,而对象值是按引用比较的。两个对象值只有当引用同一个底层对象时,才是相等的。
类型转换
JS 会尝试将值传为想要的类型,具体的转换表见书上 P44 。
对于数组,JS 会调用其 join() 方法来转为字符串,通常是用 , 进行拼接。
若对两个不同类型的值进行严格相等 === 操作,则会判否。而简单的等于 == 会先尝试转换类型再比较。
显式转换
有的时候需要我们自己去进行类型转换,即显示转换,来保证代码清晰。比如可以使用 Boolean()、Number()、String() 函数。
这些函数也可以当作构造函数使用,但是你会得到一个封装的对象,这是 JS 的历史遗存,现在已经不需要用到了。
我们也可以手动触发 JS 的隐式转换来达到目的,比如:
x + ""  // => String(x)
+x      // => Number(x)
x - 0   // => Number(x)
!!x     // => Boolean(x)
对于二元加号
+,若有一个操作数为字符串,则将另一个操作数转为字符串;对于二元减号
-,会尝试将两个操作数转为数值;对于一元加减号,会尝试将操作数转为数值;
对于一元叹号
!,会将操作数转为布尔值然后再取反。
数值类型的 toString() 方法可接收一个参数,指定其转换的基数,默认为 10 。
数值也有三种记数方法,均采用四舍五入:
toFixed()指定小数点后位数toExponential()使用科学计数法,小数点前只有一位,指定小数点后的位数toPrecision()指定有效数字个数
如果将字符串传给 Number() ,会尝试十进制的整数或浮点数字面量进行解析,不允许出现无关字符(开头和末尾可以为空白符)。而全局函数 parseInt() 和 parseFloat() 则灵活一些,除了会识别进制前缀,它们会尽可能解析数字字符,直到无关字符出现。可以给 parseInt() 传递第二个参数来指定基数。更多可见 parseInt - MDN、parseFloat - MDN、Number - MDN 。
字符串隐式转换为数值时会识别进制前缀。
对象转换
所有对象转为布尔类型均为
true;转为字符串类型时,会先尝试使用对象的
toString()方法,若该方法不存在或者返回的不是原始值,则尝试valueOf()方法,若仍然不是原始值,则报错 TypeError 。这称为 偏字符串 算法;
转为数值类型时则与转为字符串的操作相反,先使用
valueOf()再尝试toString()。这称为 偏数值 算法;
还有 无偏好 算法,这取决于对象类型,如果是 Date 类型则偏字符串,其余类型则偏数值。
一般对象的
toString()方法默认会返回"[object 类型名]"
一般的操作符,比如 +、==、!= 会先对对象进行无偏好转换。而 <、<=、>、>= 则会进行偏数值转换。这意味着 Date 对象会转为数值进行比较,而非无偏好下的字符串。
变量声明 & 赋值
ES6 之前,变量是通过 var 声明,之后则可以用 let 和 const 。
let & const 声明
一条 let 语句可以声明多个变量,声明的时候最好赋予初始值:
let i                     // => undefined
let message = "hello"
let i = 0, j = 0, k = 0
let x = 2, y = x * x      // 可以使用之前的变量
使用 const 来定义常量,定义时必须赋值,且之后不允许修改值:
const PI = 3.14159
PI = 1              // ! 报错
let 和 const 声明的变量具有块级作用域。在同一作用域中重复声明会报错。
声明在任何代码块外部的称为全局变量,具有全局作用域。
用 var 声明变量
var 的语法和 let 相同,但也有重要的区别:
var声明的变量不具有块级作用域,仅限于函数作用域;对于全局作用域中的变量,
var的声明会实现为全局对象的属性;var在同作用域里可以多次对同一变量名进行声明;var声明具有 提升 效果,即声明会提升到所在函数的顶部,但是 初始化仍在原始位置完成 。
在非严格模式下,若将一个值赋值给一个未被任何方式声明的变量名,则会创建一个新全局变量。
解构赋值
ES6 中,左值可以通过模拟数组或对象字面量来指定一个或多个变量:
let [x, y] = [1, 2, 3]  // x = 1, y = 2
可以这样遍历对象的键值对:
let o = { x: 1, y: 2 }
for (const [name, value] of Object.entries(o)) {
    console.log(name, value)
}
左值中的变量个数不一定与右边不一致时,多余的变量会赋值为 undefined ,或者可以提供默认值:
let o = { x: 1 }
let { x, y = 2 } = o      // x = 1, y = 2
可以通过逗号直接跳过元素:
let [,,x] = [1, 2, 3, 4]  // x = 3
用剩余符号 ... 收集剩余的元素:
let [x, ...y] = [1, 2, 3, 4]  // x = 1, y = [2, 3, 4]
支持嵌套:
let [a, [b, c]] = [1, [2, 3], 4]  // a = 1, b = 2, c = 3
右值只要为可迭代对象就行,比如上面的 Object.entries() ,以及字符串:
let [first] = "Hello"   // first = 'H'
对于对象,可以直接列出属性名,或者设置别名:
let o = { x: 1, y: 2 }
let { x, y } = o        // x = 1, y = 2
let { x: a, y: b } = o  // a = 1, b = 2
对象与数组之间可以相互嵌套(但是不利于可读性):
let points = { p1: [1, 2], p2: [3, 4] }
// ↓ x1 = 1, y1 = 2, x2 = 3, y2 = 4
let { p1: [x1, y1], p2: [x2, y2] } = points