类型、值和变量
1. 概述
JavaScript 类型可以分为原始类型和对象类型。
- 原始类型:数值(number、BigInt)、字符串、布尔值、null、undefined、Symbol。
- 对象类型:非原始类型的值都是对象。
注意
- 在 JavaScript 中只有 null 和 undefined 是不能调用方法的值。
- 对象类型可以修改,原始类型不能修改。
2. 数值
2.1 整数字面量
十进制数可以直接写成数字序列,十六进制字面量以 0x/0X
开头,二进制以 0b/0B
开头,八进制以 0o/0O
开头。
let a = 10 // 十进制
let b = 0xff // 十六进制
let c = 0b1101 // 二进制
let d = 0o17 // 八进制
2.2 浮点字面量
浮点字面量可以使用科学计数法表示,即实数值后跟一个 e/E
,再跟一个整数。
let a = 3.14
let b = 1.4738223E-32 // 1.4738223 * 10 ^ -32
let c = 123_333
let d = 0xff_dd
在较新的 JavaScript 标准里,可以用下划线将数值字面量分隔,目的是更容易识别。
2.3 JavaScript 中的算术
- 数值操作的结果超过最大可表示数时,结果是一个特殊的无穷值
Infinity
,类似有负无穷-Infinity
。 - 任何数与无穷值运算结果还是无穷值。
- 被零除会返回无穷,但
0/0
例外,返回 NaN,表示非数值。 - JavaScript 预定义了全局常量
Infinity
和NaN
,也可以通过 Number 的属性获取。 NaN
与任何值都不相等,包括自己。-0 === 0
为 true。
let a = Infinity
let b = Number.POSITIVE_INFINITY // Infinity
console.log(a === b) // true
let c = Number.MAX_VALUE * 2 // Infinity
let d = Number.NEGATIVE_INFINITY // -Infinity
2.4 二进制浮点数与舍入错误
实数值有无限个,JavaScript 的实数只能表示其中有限个,即实数通常是实际数值的近似值。
所有现代编程语言使用的浮点表示法是二进制表示法,可以精确表示 1/2、1/8 等分数,但十进制分数如 0.1 无法精确表示。
let x = .3 - .2
let y = .2 - .1
console.log(x === y); // false
console.log(x === 0.1); // false
console.log(y === 0.1); // true
2.5 通过 BigInt 表示任意精度整数
BigInt 字面量可以写作一串数字后跟小写字母 n
,默认情况下,基数是 10,但可以通过前缀 0b
、0o
和 0x
来表示二进制、八进制和十六进制 BigInt。
可以用 BigInt()
函数把常规 JavaScript 数值或字符串转换为 BigInt 值。
BigInt 运算与常规数值运算类似,但不能把 BigInt 类型与常规数值混一起计算。
let a = 123n
let b = a + 1n // 124n
let c = a + 1 // 报错
let d = BigInt('1000') // 1000n
let e = BigInt('100a') // 报错
3. 文本
JavaScript 字符串使用 Unicode 字符集的 UTF-16 编码,即字符串是 16 位无符号序列。
JavaScript 字符串的 length
属性表示字符串的 UTF-16 码元长度。
大部分 Unicode 字符可以用 1 个码元表示,刚好可以对应字符串中一个元素,但有的 Unicode 字符需要 2 个码元表示,意味着一个长度为 2 的字符串可能只是表示一个 Unicode 字符。
3.1 字符串字面量
字符串需要放到一对匹配的双引号或单引号或反引号中。
3.2 字符串字面量中的转义序列
\n
:换行符。\t
:水平制表符。\r
:回车符。
3.3 使用字符串
let s = "hello, world";
// 取得字符串的一部分
s.substring(1, 4) // ell
s.slice(1, 4) // ell
s.slice(-3) // rld 最后三个字符
s.split(',')
// 搜索字符串
s.indexOf("l") // 2
s.indexOf("l", 3) // 3: 位置3后第一个"l"出现的位置
s.indexOf("xx") // -1: s并不包含子串"xx"
s.lastIndexOf("l") // 10: 最后一个字母l出现的问题
// ES6之后的布尔值搜索函数
s.startsWith("Hell") // true: 字符串以这些字符开头
s.endsWith("!") // false: 不是以它结尾的
s.includes("or") // true: s 包含子串"or"
// 创建字符串的修改版本
s.replace("l", "y"); // "Heylo, world": 替换第一个出现的位置
s.toLowerCase() // "hello, world"
s.toUpperCase() // "HELLO, WORLD"
// 访问字符串中的个别(16位值)字符
s.charAt(0) // "H";第一个字符
s.charCodeAt(0) // 72: 指定位置的16位数值
s.codePointAt(0) // 72: 适合码点大于16位的情况
// 删除空格字符
" test ".trim() // "test": 删除开头和末尾的空格
" test ".trimStart() // "test ": 删除开头的空格
" test ".trimEnd() // " test": 删除右侧空格
// 未分类字符串方法
s.concat('!') // "hello, world!": 可以用+操作符替代
"<>".repeat(5) // "<><><><><>": 拼接n次
说明
JavaScript中的字符串是不可修改的,像 replace()
和 toUpperCase()
这样的方法都返回新字符串,它们并不会修改调用它们的字符串。
3.4 模板字符串
字符串放到一对反引号中,并且可以包含任意数量 JavaScript 表达式,这些表达式要放在 ${}
中。
标签化模板字面量:在反引号前面有个函数名(标签),则模板字面量中的文本和表达式值将作为参数传给这个函数,函数返回值就是标签化模板字面量的值。
let s = "hello,"
let a = s.concat`wolld,${s}` // s.concat(['wolld,', 'hello,'])
注意
即使标签化模板字面量的标签部分是函数,在调用这个函数时也没有圆括号。
3.5 模式匹配
正则表达式用于描述和匹配文本中的字符串模式,不是基本类型。
一对斜杠之间的文本构成正则表达式字面量。
4. 布尔值
布尔类型只有两个值:true 或 false。
布尔值有一个 toString()
方法,可以将自己转换为字符串 "true" 或 "false"。
5. null 与 undefined
- 对
null
使用typeof
操作符返回 "object",表明可以将null
看成一种特殊对象,表示没有对象。 - 对
undefined
使用typeof
操作符返回 "undefined"。 - 两者都没有属性或方法。
6. 符号 Symbol
Symbol
是 ES6 新增的一种原始类型,用作非字符串的属性名。Symbol
类型没有字面量语法,必须调用Symbol()
函数才能得到 Symbol 值,这个函数 永远不会返回相同的值,即使传入相同的字符串参数。- 可以将
Symbol()
取得的符号值安全地用于为对象添加新属性,而无需担心可能重写已有的同名属性。 Symbol.iterator
是一个符号值,可用于方法名,让对象变得可迭代。Symbol()
函数可以传入字符串参数,Symbol 值调用toString()
方法会返回传入的参数。
let s = Symbol('sym_x')
s.toString() // Symbol(sym_x)
Symbol.for()
函数接收一个字符串参数,返回一个与该字符串关联的 Symbol 值,如果没有符号与该字符串关联,则会创建并返回一个新符号;否则,就会返回已有的符号。Symbol()
永远不会返回相同的值,而在以相同的字符串调用时Symbol.for()
始终返回相同的值。
7. 全局对象
JavaScript 解释器启动后,会创建一个新的全局对象并为其添加一组初始属性:
- 添加
undefined
、Infinity
和NaN
这样的全局常量。 isNaN()
、parseInt()
和eval()
这样的全局函数。Date()
、RegExp()
、String()
、Object()
、Array()
这样的构造函数。Math
、JSON
这样的全局对象。- Node 程序中可以通过
global
引用全局对象,浏览器中通过window
。
8. 不可修改的原始值与可修改的对象引用
- 原始值(undefined、null、布尔值、数值和字符串)不可修改,对于字符串来说,所有修改字符串的方法都是返回一个新的字符串,对象可以修改。
- 原始值按值进行比较,对象不是按值进行比较,两个对象即使属性和值完全相同,它们也不相等。
- 对象也被称为引用类型,两个对象值当且仅当引用同一个底层对象,它们才是相等的。
- 把对象复制给一个变量,其实是在赋值引用,并不会创建对象的新副本,如果想创建对象或数组的新副本,必须显式复制对象的属性或数组的元素。
9. 类型转换
JavaScript 会根据需要的类型自动进行类型转换。
值 | 转为字符串 | 转为数值 | 转为布尔值 |
---|---|---|---|
undefined | "undefined" | NaN | false |
null | "null" | 0 | false |
true | "true" | 1 | |
false | "false" | 0 | |
""(空字符串) | 0 | fasle | |
"1.2"(非空、数值) | 1.2 | true | |
"one"(非空、非数值) | NaN | true | |
0/-0 | "0" | false | |
1(有限、非零) | "1" | true | |
Infinity | "Infinity" | true | |
-Infinity | "-Infinity" | true | |
NaN | "NaN" | fasle | |
[](空数组) | "" | 0 | true |
9.1 转换与相等
===
:严格相等操作符,如果两个值不是同一种类型,就不会判断相等。==
:比较灵活,有的情况会先类型转换再比较。
9.2 显式转换
- 使用
Number()
、Boolean()
和String()
函数可以执行显示转换。 - 除
null
和undefined
之外的所有值都有toString()
方法,这个方法返回的结果通常与String()
函数返回的结果相同。 number
类型的toString()
方法接收一个可选参数,用于指定一个基数,默认为 10,也可以按其它基数来转换数值。toFixed()
:把数值转换为字符串时指定小数点后位数。- 全局函数
parseInt()
和parseFloat()
可以把字符串转为数值,前者只解析整数,后者解析整数和浮点数,二者都会跳过开头的空格,尽量多地解析数字,如果第一个字符不是有效数字字面量,返回NaN
。
let n = 17;
let binary = "0b" + n.toString(2)
let octal = "0o" + n.toString(8)
let hex = "0x" + n.toString(16)
console.log(binary);
console.log(octal);
console.log(hex);
9.3 对象到原始值转换
9.3.1 对象转换为布尔值
对象到布尔值的转换很简单:所有对象都转为 true。
9.3.2 对象转为字符串
对象转为字符串发生在把对象传给接收字符串参数的内置函数如 String()
,或者将对象插入字符串模板字面量中。
过程:首先使用偏字符串算法将对象转为一个原始值,然后将原始值转为字符串。
9.3.3 对象转为数值
发生在把对象传给接收数值参数的内置函数和方法,以及一些操作符的隐式转换中。
过程:首先使用偏数值算法将对象转为一个原始值,然后将原始值转为数值。
9.3.4 操作符转换特例
+
操作符可以执行数值加法和字符串拼接,如果一个操作数是对象,则会使用无偏好算法将对象转为原始值,如果两个都是原始值,则会先检查类型。如果一个操作数是字符串,则把另一个也转为字符串并拼接;否则,都转为数值并相加。
==
和 !=
:如果一个操作数是对象,另一个操作数是原始值,则会使用无偏好算法把操作数转为原始值再比较。
9.3.5 toString() 和 valueOf()
- 所有对象都会继承两个在对象到原始值转换时用到的方法:
toString()
和valueOf()
。 toString()
的任务是返回对象的字符串表示,默认情况下返回 "[object Object]",很多类重写了该方法,比如 Array 类的toString()
方法会将数组的每个元素转为字符串,然后使用逗号拼接。valueOf()
默认情况下返回对象本身,而非返回原始值,很多类重写了该方法,如 String、Number 和 Boolean 这样的包装类定义的valueOf
方法会返回被包装的原始值。
({x: 1, y: 2}).toString() // [object Object]
9.3.6 对象到原始值转换算法
- 偏字符串算法首先尝试
toString()
方法,如果该方法有定义且返回原始值,则使用该原始值;如果toString()
方法不存在,或存在但返回对象,则尝试valueOf()
方法;如果valueOf()
存在且返回原始值,就使用该原始值,否则转换失败,报 TypeError。 - 偏数值算法与偏字符串算法类似,只不过先尝试
valueOf()
方法,再尝试toString()
方法。 - 无偏好算法取决于被转换对象的类。如果是
Date
对象,则使用偏字符串算法,否则使用偏数值算法。
/**
* 使用偏数值算法,首先调用 valueOf() 得到数组对象本身,然后调用 toString()
* 将 [] 转为 "",[99] 转为 "99",再将原始值转为数值类型,分别得到 0 和 99
*/
Number([]) // 0
Number([99]) // 99
10. 变量声明与赋值
10.1 使用 let 和 const
- 在现代 JavaScript 中,通过
let
声明变量,通过const
声明常量。 - 声明变量时不指定初始值,则变量值为
undefined
。 const
在声明时必须初始化,且对于字面量常量,应该全部字母大写。
let i;
let a, b;
const PI = 3.14;
TIP
for
、for...in
、for...of
循环语句都包含一个循环变量,在循环的每次迭代中都会取得一个新值,可以在循环语法中声明这个循环变量,也是 let
的常见使用场景。
for (let i = 0; i < data.length; i++) {
console.log(data[i])
}
for (let datum of data) {
console.log(datum)
}
for (let key in object) {
console.log(key)
}
特殊情况
也可以使用 const
声明这些循环“变量”,只要保证在循环体内不重新赋值。
10.1.1 变量与常量作用域
- 通过
let
和const
声明的变量和常量具有块级作用域,通俗地说,如果变量和常量声明在一对花括号中,则它们只在花括号内有效。 - 在 Node 和 客户端 JS 模块中,一个文件就是一个模块,在模块顶层声明的变量或常量,仅在模块作用域内有效,不是全局变量;对于传统客户端 JS(浏览器环境),全局变量的作用域是 HTML 文档,如果有
<script>
标签声明了一个全局变量或常量,该变量或常量在同一个文档的任何<script>
标签中都有定义。 - 作为
for
、for...of
、for...in
循环的一部分声明的变量或常量,作用域是循环体,即使实际上位于花括号外部。
10.1.2 重复声明
同一个作用域中不能用 let
或 const
声明同名的变量或常量,可以在嵌套的作用域中声明,但不推荐。
const x = 1;
if (x == 1) {
let x = 2; // 不推荐声明外部作用域同名的变量
console.log(x) // 2
}
10.2 使用 var 的变量声明
在 ES6 之前,只能用 var
声明变量,不能声明常量。
var
与 let
的区别:
var
声明的变量不具有块作用域,只有函数作用域。var
可以多次声明同名变量。var
有作用域提升特性,let
和const
没有,即在作用域内使用var
声明变量时,声明会被提升到作用域顶部,但初始化仍在代码所在位置。
function test1() {
var a = 1
if (a == 2) {
var b = 2
}
console.log(b)
}
test1() // undefined
// 看起来条件语句不执行,b应该未声明
// 实际上函数内的 var 均会声明且提升,只是没有赋值
- 浏览器环境中,在任何代码块外部使用
var
,会声明为一个全局变量,且会被实现为全局对象的属性(该全局对象可以通过 globalThis 或 window 引用);通过let
会声明为全局变量,但不会成为全局对象上的属性。 - Node 环境中,在任何代码块外使用
var
或let
,都不会声明为全局变量,也不会成为全局对象上的属性(该全局对象可以通过 globalThis 或 global 引用)。 - 如果不使用
var
或let
声明变量而直接初始化变量,该变量会自动提升为全局变量 ,同时作为属性添加到全局对象上,在严格模式下会报错。
let a = 1
if (a = 1) {
b = 2
}
console.log(b) // 2
console.log(globalThis.b) // 2
10.3 解构赋值
解构赋值是 ES6 之后提出一种复合声明与赋值的语法,可以从数组或对象中提取多个值赋值给左侧的变量列表。
10.3.1 数组解构
let [x, y] = [1, 2]
[x, y] = [x + 1, y + 1]
[x, y] = [y, x]
[x, y] // [3, 2]
在各种 for
循环中可以使用解构赋值。
let o = { x: 1, y: 2}
for (const [name, value] of Object.entries(o)) {
console.log(name, value) // 打印 'x 1' 和 'y 2'
}
数组解构赋值左侧变量的个数不一定与右侧数组元素个数相同。左侧多余的变量会被设置为 undefined
,而右侧多余的值会被忽略。左侧变量列表可以包含额外的逗号以跳过某些值。
let [x, y] = [1] // x == 1, y == undefined
[x, y] = [1, 2, 3] // x == 1, y == 2
[, x, , y] = [1, 2, 3, 4] // x == 2, y == 4
把剩余的值收集到一个变量中,可以在左侧最后一个变量前加上 ...
。
let [x, ...y] = [1, 2, 3, 4] // y == [2, 3, 4]
解构赋值可用于嵌套数组。
let [a, [b, c]] = [1, [2, 2.5], 3] // a == 1, b == 2, c == 2.5
数组解构的右侧不要求必须是数组,可以是任何可迭代对象。
let [first, ...rest] = 'hello'
// first == 'h', rest == ['e', 'l', 'l', 'o']
10.3.2 对象解构
右侧为对象,左侧是一个包含在花括号内的逗号分隔的变量列表,看起来像对象字面量。
let transparent = { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
let { r, g, b } = transparent // r == 0.0 g == 0.0 b == 0.0
左侧变量名需要与右侧对象的属性名对应,如果左侧变量不是右侧的属性,则赋值为 undefined
。
如果想要变量名跟属性名不一样,可以通过冒号给变量起别名。
// 将 Math 的 cos 属性赋值给 cosine 变量,tan 属性给 tangent 变量
let { cos: cosine, tan: tangent } = Math
与数组解构一样,通过 ...
可以将剩余的键值对收集到左侧的变量中:
let transparent = { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
let { r, g, ...rest } = transparent
// r == 0.0 g == 0.0 rest == { b: 0.0, a: 1.0 }