类型、值、变量
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