0%

JavaScript 权威指南 03:类型、值和变量

一门语言支持的类型集、变量的工作方式都是这门编程语言最基本的特征。这篇文章讲解 JavaScript 中的类型、值和变量。

概述与定义

JavaScript 类型可以分为两类:原始类型和对象类型。JavaScript 的原始类型包括数值、字符串和布尔值​,特殊值 null 和 undefined 也是原始值,但它们不是数值、字符串或布尔值,这两个值通常被认为是各自特殊类型的唯一成员。ES6 新增了一种特殊类型 Symbol(符号)​,用于对语言进行扩展而不破坏向后兼容性。

在 JavaScript中,任何不是数值、字符串、布尔值、符号、null 和 undefined 的值都是对象。对象是属性的集合,其中每个属性都有一个名字和一个值(原始值或其他对象)​。

普通 JavaScript 对象是一个命名值的无序集合。这门语言本身也定义一种特殊对象,称为数组。JavaScript 语言包括操作数组的特殊语法,而数组本身也具有区别于普通对象的行为。

除了基本的对象和数组之外,JavaScript 还定义了其他一些有用的对象类型,例如 Set、Map、Date 等。

JavaScript 与静态语言更大的差别在于,函数和类不仅仅是语言的语法,它们本身就是可以被 JavaScript 程序操作的值。与其他 JavaScript 非原始值一样,函数和类也是特殊的对象

在内存管理方面,JavaScript 解释器会执行自动垃圾收集。这意味着 JavaScript 程序员通常不用关心对象或其他值的析构与释放。当一个值无法触达时,或者说当程序无法以任何方式引用这个值时,解释器就知道这个值已经用不到了,会自动释放它占用的内存。

JavaScript 支持面向对象的编程风格。粗略地说,这意味着不用定义全局函数去操作不同类型的值,而是由这些类型本身定义操作值的方法。

JavaScript 的原始类型是不能修改的(immutable),而对象类型是可修改的(mutable)。可修改类型的值可以改变,而数值、布尔值、符号、null 和 undefined 是不可修改的,修改它们是无意义的。

JavaScript 可以自由地转换不同类型的值,JavaScript 这种自由的值转换会影响对相等的定义。

常量和变量可以让我们在程序中使用名字来引用值。常量使用 const 声明,变量使用 let(或在较老的 JavaScript 代码中使用 var)声明。JavaScript 常量和变量是无类型的(untyped),声明并不会限定要赋何种类型的值。

数值

JavaScript 的主要数值类型 Number 用于表示整数和近似实数。JavaScript 使用 IEEE-754 标准定义的 64 位浮点格式表示数值。JavaScript 的这种数值格式可以让我们准确表示 -9007 199 254 740 992(-2^53)到9007 199 254 740 992(2^53)之间的所有整数(含首尾值)​。如果你的整数值超出了这个范围,那可能会在末尾的数字上损失一些精度。另外需要要注意,JavaScript 中的某些操作(例如数组索引和位操作)是以 32 位整数计算的。

出现在 JavaScript 程序中的数值称为数值字面量。

整数字面量

JavaScript 支持 10 进制、16 进制的整数字面量(以 0x 或 0X 开头,后跟一个十六进制数字字符串,大小写均可)。在 ES6 及之后的版本中,也可以通过二进制或八进制表示整数,分别使用前缀 0b0o(或 0B0O )​:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# node
> 10
10
> 0xa
10
> 0xA
10
> 0o12
10
> 0O12
10
> 0b1010
10
> 0B1010
10

浮点字面量

浮点字面量可以包含小数点,它们对实数使用传统语法。实数值由数值的整数部分、小数点和数值的小数部分组成。浮点字面量也可以使用指数记数法表示,即实数值后面可以跟字母e(或E)​,跟一个可选的加号或减号,再跟一个整数指数。这种记数法表示的是实数值乘以 10 的指数次幂。

1
2
3
4
5
6
7
8
9
10
> 3.14
3.14
> 100.1
100.1
> .3333
0.3333
> 6.02e10
60200000000
> 1.473823232E-32
1.473823232e-32

可以用下划线将数值字面量分隔为容易看清的数字段:

1
2
3
4
5
6
7
8
> 1_000_000_000
1000000000
> 0x89_AB_CD_EF
2309737967
> 0b0001_1101_0111
471
> 0.123_456_789
0.123456789

JavaScript 中的算术

JavaScript 程序使用语言提供的算术操作符来操作数值,包括 +、-、*、/、%(取模)、**(幂运算,ES2016)。除了这些基本的算术运算符,JavaScript 还通过 Math 对象的属性提供了一组函数和常量,以支持更复杂的数学计算:

1
2
3
4
5
6
7
8
9
10
11
12
> Math.round(.6)
1
> Math.round(.4)
0
> Math.pow(2, 10)
1024
> Math.PI
3.141592653589793
> Math.E
2.718281828459045
> Math.max(1, 2, 3)
3

JavaScript 中的算术在遇到上溢出、下溢出或被零除时不会发生错误。

  • 如果运算结果超过最大可表示数值(上溢出),结果是一个特殊的无穷值 Infinity
  • 如果运算结果低于最小可表示数值(下溢出),结果是负无穷值 -Infinity
  • 任何数加、减、乘、除无穷值结果还是无穷值(只是符号可能相反)
  • 被零除只会简单地返回无穷或负无穷。不过有一个例外:0 除以 0是没有意义的值,这个操作的结果是一个特殊的 非数值(NaN,Not a Number)
  • 无穷除无穷、负数平方根或者用无法转换为数值的非数值作为算术操作符的操作数,结果也都是NaN。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> 0 - Infinity
-Infinity
> 1 + Infinity
Infinity

> 1 / 0
Infinity
> -1 / 0
-Infinity
> 0 / 0
NaN

> Infinity / -Infinity
NaN
> Math.sqrt(-1)
NaN
> 1 / "t"
NaN

JavaScript 预定义了全局常量 Infinity 和 NaN 以对应正无穷和非数值。这些值也可以通过 Number 对象的属性获取。

1
2
3
4
5
6
> Number.POSITIVE_INFINITY
Infinity
> Number.NEGATIVE_INFINITY
-Infinity
> Number.NaN
NaN

NaN 与任何值比较都不相等,也不等于自己。这意味着不同通过 x === NaN 来检查一个值是否是 NaN,而应该使用 Number.isNaN(x) 函数或者 x != x 来判断。这两个表达式当且仅当 x 与全局常量 NaN 具有相同值时才返回 true。

1
2
3
4
5
6
7
8
9
10
> x = NaN
NaN
> x === 1
false
> x === x
false
> x != x
true
> Number.isNaN(x)
true

全局函数 isNaN 与 Number.isNaN 类似,它会在参数是 NaN 时,或者在参数是无法转换为数值的非数值时返回 true。也就是说全局函数 isNaN 会在判断之前对传入的值进行类型转换,而 Number.isNaN 则不会进行转换操作,如果传入的不是数值,Number.isNaN 会直接返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
> isNaN(NaN)
true
> Number.isNaN(NaN)
true
> isNaN("1")
false
> Number.isNaN("1")
false
> isNaN("test")
true
> Number.isNaN("test")
false

负零值也有点不同寻常。它与正零值相等(即便使用JavaScript的严格相等比较)​,这意味着除了作为除数使用,几乎无法区分这两个值:

1
2
3
4
5
6
7
8
> 0 === -0
true
> 1 / 0
Infinity
> 1 / -0
-Infinity
> 1 / 0 == 1 / -0
false

二级制浮点数与舍入错误

实数是无穷的,但 JavaScript 浮点格式只能够表示其中有限个数。这意味着在通过 JavaScript 操作实数时,数值表示的经常是实际数值的近似值。

JavaScript(以及所有现代编程语言)使用的 IEEE-754 浮点表示法是一种二进制表示法,这种表示法可以精确地表示如 1/21/81/1024 等分数。我们最常用的分数是十进制分数:1/10、1/100 等等。二进制浮点表示法无法精确表示哪怕 0.1 这么简单的数。

虽然 JavaScript 数值有足够大的精度,能够非常近似地表示 0.1,但无法精确地表示。这可能导致一些问题。

1
2
3
4
5
6
7
8
> 0.3 - 0.2
0.09999999999999998
> 0.2 - 0.1
0.1
> 0.3 - 0.2 === 0.1
false
> 0.2 - 0.1 === 0.1
true

这并不是 JavaScript 独有的问题,而是所有使用二进制浮点数的编程语言共同的问题。所以对于浮点数,切记不要尝试比较它们的相等性。如果浮点近似值对你的程序而言是个问题,可以考虑使用等量整数。

通过 BigInt 表示任意精度整数

ES2020 为 JavaScript 定义了一种新的数值类型 BigInt。BigInt 这种数值类型的值是整数。之所以增加这个类型,主要是为了表示 64 位整数,这对于兼容很多其他语言和 API 是必需的。

BigInt 字面量写作一串数字后跟小写字母 n。默认情况下,基数是10,但可以通过前缀 0b0o0x 来表示二进制、八进制和十六进制 BigInt。

1
2
3
4
5
6
7
8
9
10
11
12
13
> 1234n
1234n
> 0b11111n
31n
> 0o10n
8n

> 0x8000_0000_0000_0000
9223372036854776000
> 2**63
9223372036854776000
> 0x8000_0000_0000_0000n
9223372036854775808n

可以通过 BigInt() 函数把常规的 JavaScript 数值或字符串转换为 BigInt 值:

1
2
3
4
> BigInt(1234)
1234n
> BigInt("10000000000000000000000000000000")
10000000000000000000000000000000n

BigInt 的算术运算与常规数值的算术运算类似,但是除法会丢弃余数并会向下舍入:

1
2
3
4
5
6
7
8
9
10
> 1000n + 2000n
3000n
> 10n * 20n
200n
> 20n / 2n
10n
> 20n / 3n
6n
> 20 / 3
6.666666666666667

需要注意,不能混用 BigInt 操作数和常规数值操作数。因为 BigInt 和 JavaScript 常规数值(Number)两种类型都不比另一种更通用:

  • BigInt 可以表示超大值,但只能表示整数
  • JavaScript 可以表示小数,但是却不能像 BigInt 一样表示任意精度的整数
1
2
> 1 + 2n
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

相对来说,比较操作符允许混合操作数类。

1
2
3
4
5
6
7
8
> 1 < 2n
true
> 2 > 1n
true
> 2 == 2n
true
> 2 === 2n
false

位操作符通常可以用于BigInt操作数。但 Math 对象的任何函数都不接收 BigInt 操作数。

1
2
3
4
5
6
> 1n << 3
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
> 1n << 3n
8n
> Math.Min(1n, 2n)
Uncaught TypeError: Math.Min is not a function

日期和时间

JavaScript 为表示和操作与日期及时间相关的数据而定义了简单的 Date 类。JavaScript 的 Date是对象,但也有数值表示形式,即自 1970年1月1日 起至今的毫秒数,也叫时间戳:

1
2
3
4
5
6
7
> Date.now()
1736601447264

> tt.getTime()
1736601575200
> tt.toISOString()
'2025-01-11T13:19:35.200Z'

文本

JavaScript 中表示文本的类型是 String,即字符串:

  • 字符串是 16 位值的不可修改的有序序列
  • 字符串的 length 属性是它包含的 16 位值的个数
  • 字符串(及数组)都是基于 0 的索引
  • JavaScript 没有表示单个字符元素的专门类型。要表示一个 16 位值,使用长度为 1 的字符串即可

字符、码点和 JavaScript 字符串

JavaScript 使用 Unicode 字符集的 UTF-16 编码,因此 JavaScript 字符串是无符号 16 位值的序列

  • 最常用的 Unicode 字符的码点是 16 位的,可以使用字符串中的一个元素来表示
  • 码点超过 16 位的 Unicode 字符使用 UTF-16 规则编码为两个 16 位值的序列(称为 surrogate pairt,即为代理对)。这意味着一个长度为 2(2 个 16 位值)的 JavaScript 字符串可能表示的只是一个 Unicode 字符
1
2
3
4
5
6
7
8
9
10
11
12
13
> let euro = "€"
> euro.length
1
> euro[0]
'€'

> let love = "💙"
> love.length
2
> love[0]
'\ud83d'
> love[1]
'\udc99'

JavaScript 的字符串操作方法一般操作的是 16 位值,而不是字符。但在 ES6 中,字符串是可迭代的,如果对字符串使用 for/of... 操作符,迭代的是字符而不是 16 位值。

字符串字面量

JavaScript 字符串字面量可以通过把字符序列放到 单引号双引号反引号(ES6)中来创建。双引号字符和反引号字符可以出现在由单引号定界的字符串中,同理,由双引号或反引号定界的字符串也可以包含另外两种引号。

1
2
3
4
5
> ""
> 'testing'
> 'name="From"'
> "it's me"
> `She said "it's ok"`

JavaScript 最早的版本要求字符串字面量必须写在一行,此时会使用 + 来实现字符串拼接。ES5 开始可以通过每行末尾添加 \ 将字符串字面量拆分成多行,这个反斜杠和后面的行终结符都不属于当前字符串字面量。如果需要在单引号、双引号字符串中添加换行符,则需要使用 \n。ES6 的反引号支持跨行字符串。

如果需要将 HTML 代码与 JavaScript 混合在一起,最好 JavaScript 和 HTML 分别使用不同的引号。

字符串字面量中的转义序列

反斜杠 \ 与后面的字符组合在一起,可以在字符串中表示一个无法直接表示的字符,因此被称为转义序列:

  • \0\n\v\r\n\'\" 都是转义字符,可以表示特定的字符
  • \xnn 可以表示由 2 位十六进制数 nn 指定的 Unicode 字符
  • \unnnn 可以表示由 4 位十六进制数 nnnn 指定的 Unicode 字符
  • \u{n} 可以表示由码点 n 指定的 Unicode 字符,其中 n 是介于 0 到 10FFFF 之间的 1 到 6 个十六进制数字(ES6)

如果字符 \ 位于非有效转义序列的前面,则这个 \ 会被忽略,因此 \# 等效于 #

使用字符串

  • 拼接字符串是 JavaScript 的一个内置特性。可以使用 + 操作符进行字符串拼接。
  • 可以使用 ===!== 来比较字符串,只有当这两个字符串具有完全相同的16位值的序列时才相等。
  • 字符串也可以使用 <、<=、> 和 >= 操作符来比较
  • 使用 s.length 获取字符串的长度,即字符串包含的 16 位值的个数
  • JavaScript 也提供了丰富的操作字符串的 API
  • JavaScript 的字符串都是不可修改的,所以修改字符串的 API 其实是创建一个新的字符串,它们并不会修改原始字符串
  • 字符串也可以被当成数组,使用 [] 来访问字符串中的某个字符(十六位数值)

模版字面量

反引号字符串不仅仅是一种新的字符串字面量语法,它其实提供了模版字面量的功能。它可以包含任意的 JavaScript 表达式。反引号中字符串字面量最终值的计算,涉及对其中包含的所有表达式求值、将这些表达式的值转换为字符串,然后再把这些字符串与反引号中的字面量组合:

1
2
3
> let name = "Bill"
> `Hello ${ name }`
'Hello Bill'
  • 位于 ${} 中的内容都会被当成 JavaScript 表达式来解释,而之外的内容则当成常规字符串字面量
  • 模板字面量可以包含任意数量的表达式,可以包含任何常规字符串中可以出现的转义字符,也可以跨任意多行而无须特殊转义

模版字面量还有一个特性:如果在开头的反引号前面有一个函数名(标签)​,那么模板字面量中的文本和表达式的值将作为参数传给这个函数。​标签化模板字面量(tagged template literal) 的值就是这个函数的返回值。注意,即使标签化模板字面量的标签部分是函数,在调用这个函数时也没有圆括号。在这种非常特别的情况下,反引号字符充当开头和末尾的圆括号。

ES6 提供过了一个内置的标签函数 String.raw,这个函数返回反引号中未经处理的文本,即不会处理任何反斜杠转义:

1
2
3
4
> "\n".length
1
> String.raw`\n`.length
2

模式匹配

JavaScript 定义了一种被称为正则表达式(或 RegExp)的数据类型,用于描述和匹配文本中的字符串模式。RegExp 不是JavaScript中的基础类型,但具有类似数值和字符串的字面量语法。

  • 一对斜杠之间的文本构成正则表达式字面量。这对斜杠中的第二个后面也可以跟一个或多个字母,用于修改模式的含义
  • RegExp 对象定义了一些有用的方法,而字符串也有接收 RegExp 参数的方法
1
2
3
> /^HTML/
> /[1-9][0-9]*/
> /\bjavascript\b/i
1
2
3
4
5
6
7
8
> let text ="HTML, CSS, JavaScript"
> let pattern = /^HTML/
> pattern.test(text)
true
> text.search(pattern)
0
> text.replace(pattern, "HTML5")
'HTML5, CSS, JavaScript'

布尔值

  • 布尔类型只有两个值:truefalse,布尔值在 JavaScript 中常用于控制结构
  • JavaScript 的任何值都可以转换为布尔值。
    • undefinednull0-0 NaN""(空字符串)都转换为 false
    • 其他值包括所有对象和数组都转换为 true
  • 布尔值有一个 toString() 方法,可用于将自己转换为字符串 truefalse。除此之外,布尔值再没有其他有用的方法了
  • && 布尔与运算:两个操作数都为真,结果才为真;任意一个操作数为假,结果为假
  • || 布尔或运算:任意一个操作数为真,结果为真;两个操作数都为假,结果为假
  • ! 布尔非运算,对布尔值取反
1
2
3
4
> (!!0).toString()
'false'
> (!!"0").toString()
'true'

null 与 undefined

null 是一个语言关键字,其为一个特殊值,用于表示某个值不存在。对 null 执行 typeof 会返回字符串 'object',表明可以将 null 看成一种特殊对象。但在实践中,null 通常被当作它自己类型的唯一成员,可以用来表示数值、字符串以及对象“没有值”​。

JavaScript 中的 undefined 也表示值不存在,但 undefined 表示一种更深层次的不存在。对 undefined 应用 typeof 操作符会返回 undefined,表示这个值是该特殊类型的唯一成员。

  • 变量的值未初始化时就是 undefined
  • 在查询不存在的对象属性或数组元素时也会得到 undefined
  • 没有明确返回值的函数返回的值是 undefined
  • 没有传值的函数参数的值也是 undefined
1
2
3
4
> typeof null
'object'
> typeof undefined
'undefined'

可以用 undefined 表示一种系统级别、意料之外或类似错误的没有值,可以用 null 表示程序级别、正常或意料之中的没有值

符号

符号(Symbol)是 ES6 新增的一种原始类型,用作非字符串的属性名。要理解符号,需要了解 JavaScript 的基础类型 Object 是一个属性的无序集合,其中每个属性都有一个名字和一个值。属性名通常是(在 ES6 之前一直必须是)字符串。但在 ES6 和之后的版本中,符号也可以作为属性名

1
2
3
4
5
6
7
8
9
10
> let strname = "string name"
> let symname = Symbol("propname")
> typeof strname
'string'
> typeof symname
'symbol'
> o[strname] = 1
> o[symname] = 2
> o
{ 'string name': 1, [Symbol(propname)]: 2 }

Symbol 类型没有字面量语法,要获取一个 Symbol 值,需要调用 Symbol() 函数。这个函数永远不会返回相同的值,即使每次传入的参数都一样,这意味着可以将 调用Symbol()取得的符号值 安全地用于为对象添加新属性,而无须担心可能重写已有的同名属性。

实践中,符号通常用作一种语言扩展机制。有时候我们选择任何特定的字符串作为标准的新增属性名,都有可能破坏已有的代码。因此符号名应运而生。Symbol.iterator 是一个符号值,可用作一个方法名,让对象变得可迭代。

Symbol() 函数可选地接收一个字符串参数,返回唯一的符号值。如果提供了字符串参数,那么调用返回符号值的 toString() 方法得到的结果中会包含该字符串

有时我们希望定义一些可以与其他代码共享的符号值。例如,我们定义了某种扩展,希望别人的代码也可以使用,就像前面提到的 Symbol.iterator 机制一样。JavaScript 定义了一个全局符号注册表。Symbol.for() 函数接收一个字符串参数,返回一个与该字符串关联的符号值。

  • 如果没有符号与该字符串关联,则会创建并返回一个新符号
  • 否则,就会返回已有的符号

因此 Symbol.for() 函数与 Symbol() 函数完全不同:

  • Symbol() 函数永远都不会返回相同值
  • Symbol.for() 在以相同的字符串调用时始终返回相同的值
  • 通过 Symbol.keyFor() 函数可以查询全局符号注册表中某个符号的字符串键
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> let s = Symbol("test")
> let t = Symbol("test")
> s === t
false

> let s1 = Symbol.for("test")
> let t1 = Symbol.for("test")
> s1 === t1
true

> Symbol.keyFor(s1)
'test'
> Symbol.keyFor(t1)
'test'

全局对象

全局对象的属性是全局性定义的标识符,可以在 JavaScript 程序的任何地方使用。JavaScript 解释器启动后(或每次浏览器加载新的页面时),都会创建一个新的全局对象并为其添加一组初始的属性,定义了:

  • undefined、Infinity、NaN 等全局常量
  • isNaN()、parseInt() 等全局函数
  • Date()、String()、RegExp() 等全局构造函数
  • Math 和 JSON 等全局对象

全局对象的初始属性并不是保留字,但它们应该都被当成保留字

在 Node 中,全局对象有一个名为 global 的属性,其值为全局对象本身,因此在 Node 程序中始终可以通过 global 来引用全局对象。

在浏览器中,Window 对象对浏览器窗口中的所有 JavaScript 代码而言,充当了全局对象的角色。这个全局的 Window 对象有一个自引用的 window 属性,可以引用全局对象。Window 对象定义了核心全局属性,也定义了其他一些特定于浏览器和客户端 JavaScript 的全局值。

ES2020 最终定义了 globalThis 作为在任何上下文中引用全局对象的标准方式。2020年初,所有现代浏览器和 Node 都实现了这个特性。

不可修改的原始值与可修改的对象引用

JavaScript 中的原始类型的值(undeifned、null、boolean、number 和 string)是不可修改的。尤其是字符串,所有看起来返回一个修改后字符串的字符串方法,实际上返回的都是一个新字符串。原始值是按值比较的,即两个值只有在它们的值相同的时候才是相同的(当且仅当这两个字符串长度相同并且每个索引的字符也相同时,JavaScript 才认为它们相等)。

对象不同于原始值,对象是可修改的,即它们的值可以改变。

1
2
3
> let o = { x : 1 }
> o.x = 2
> o.y = 3

对象不是按值比较的,两个不同的对象即使拥有完全相同的属性和值,它们也不相等。同样,两个不同的数组,即使每个元素都相同,它们也不相等。

1
2
3
4
5
6
7
8
9
10
> let o = { x : 1 }
> o.x = 2
> o.y = 3

> let oo = { x: 2, y: 3 }
> o === oo
false

> [] === []
false

对象有时候被称作引用类型(reference type),以区别于 JavaScript 的原始类型。基于这一术语,对象值就是引用,对象是按引用比较的。换句话说,两个对象值当且仅当它们引用同一个底层对象时,才是相等的

1
2
3
4
5
6
7
> let a = []
> b = a
> b[0] = 1
> a[0]
1
> a === b
true

把对象(或数组)赋值给一个变量,其实是在赋值引用,并不会创建对象的新副本。如果想创建对象或数组的新副本,必须显式复制对象的属性或数组的元素。

1
2
3
4
5
6
> let a1 = [1, 2, 3]
> let b1 = Array.from(a1)
> b1
[ 1, 2, 3 ]
> a1 === b1
false

所以如果想要比较两个对象或数组的内容是否相等,必须比较它们的属性或者元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function equalArray(a, b) {
if (a === b) {
return true;
}

if (a.length != b.length) {
return false;
}

for (let i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
return false;
}
}

return true;
}

console.log(equalArray([1, 2, 3], [1, 2, 3])) // => true
console.log(equalArray([1, 2, 3], [])) // => false

类型转换

JavaScript 对待自己所需值的类型非常灵活

  • 如果 JavaScript 需要布尔值,会将其他类型的值转换为 true 或者 false
  • 如果 JavaScript 想要字符串,它就会把你提供的任何值都转换为字符串
  • 如果 JavaScript 想要数值,它也会尝试把你给的值转换为一个数值,如果无法进行有意义的转换就转换为NaN​

下表展示了 JavaScript 中类型之间的转换关系:

转换为字符串 转换为数值 转换为布尔值
undefiend “undefined” NaN false
null “null” 0 false
true “true” 1
false “false” 0
“” 0 false
“1.2” 1.2 true
“one” NaN true
0 “0” false
-0 “0” false
1 “1” true
Infinity “Infinity” true
-Infinity “-Infinity” true
NaN “NaN” false
{}(任何对象) 后文介绍 后文介绍 true
[](空数组) “” 0 true
[9](一个数值元素) “9” 9 true
[‘a’](任何其他数组) 使用 join 方法 NaN true
Function{} 后文介绍 NaN true

转换与相等

JavaScript有两个操作符用于测试两个值是否相等:

  • 一个是严格相等操作符 ===,如果两个值不是同一种类型,那么这个操作符就不会判定它们相等
  • 由于 JavaScript 在类型转换上很灵活,所以它也定义了 == 操作符,这个操作符判定相等的标准相当灵活
1
2
3
4
5
6
7
8
9
10
> null === undefined
false
> "0" === 0
false
> "0" == 0
true
> 0 == false
true
> "0" == false
true

一个值可以转换为另一个值并不意味着这两个值是相等的。JavaScript 操作符和语句期待不同类型的值,因此会执行以这些类型为目标类型的转换。例如,if 语句将 undefined 转换为 false,但 == 操作符永远不会将其操作数转换为布尔值。

显式转换

执行显示类型转换的最简单方法就是使用 Boolean()、Number() 和 String() 函数。除 null 和 undefined 之外的所有值都有 toString() 方法,这个方法返回的结果通常与 String() 函数返回的结果相同。

1
2
3
4
5
6
> Number("3")
3
> String(false)
'false'
> Boolean([])
true

另外,Boolean()、Number() 和 String() 函数也可以被当作构造函数通过 new 关键字来使用。如果你这样使用它们,那会得到一个与原始布尔值、数值和字符串值类似的 包装 对象。这种包装对象是早期 JavaScript 的历史遗存,已经没有必要再使用它们了。

某些JavaScript操作符会执行隐式类型转换,有时候可以利用这一点完成类型转换:

  • 如果 + 操作符有一个操作数是字符串,那它会把另一个操作数转换为字符串
  • 一元操作符 + 会把自己的操作数转换为数值
  • 而一元操作符 ! 会把自己的操作数转换为布尔值,然后再取反

所以经常看到这样的代码其实是执行类型转换:

  • x + "" // => String(x)
  • +x // => Number(x)
  • x - 0 // => Number(x)
  • !!x // => Boolean(x)

Number类定义的一些类型转换方法,可以提供更多的精细控制能力:

  • toString()方法接收一个可选的参数,用于指定一个基数或底数(不指定默认为 10)
  • toFixed()把数值转换为字符串时可以指定小数点后面的位数
  • toExponential() 使用指数记数法将数值转换为字符串,结果是小数点前保留 1 位,小数点后保留指定位数
  • toPrecision() 按照指定的有效数字个数将数值转换为字符串。如果有效数字个数不足以显示数值的整数部分,它会使用指数记数法
  • 以上三种方法必要时都会舍去末尾的数字或者补零
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> let n = 17
> n.toString(8)
'21'
> n.toString(16)
'11'

> let m = 123456.789
> m.toFixed(0)
'123457'
> m.toFixed(2)
'123456.79'
> m.toFixed(5)
'123456.78900'

> m.toExponential(1)
'1.2e+5'
> m.toExponential(3)
'1.235e+5'

> m.toPrecision(4)
'1.235e+5'
> m.toPrecision(7)
'123456.8'
> m.toPrecision(10)
'123456.7890'

字符串转换为数值时:

  • 如果把字符串传给 Number() 转换函数,它会尝试把字符串当成整数或浮点数字面量来解析。这个函数只能处理基数为 10 的整数,不允许首尾出现非空格的无关字符
  • parseInt()parseFloat() 函数则更灵活一些。parseInt() 只解析整数,而 parseFloat() 既解析整数也解析浮点数
  • 如果字符串以 0x0X 开头,parseInt() 会将其解析为十六进制数值
  • parseInt() 接收可选的第二个参数,用于指定要解析数值的底(基)数,合法的值是 2 到 36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> Number(" 10")
10
> Number("x10")
NaN
> Number("10x")
NaN

> parseInt("0o10")
0
> parseInt("0x10")
16
> parseInt("Y10")
NaN
> parseInt("10Y")
10

> parseInt("11", 2)
3
> parseInt("11", 16)
17

对象到原始值的转换

接下来介绍 JavaScript 将对象转换为原始值时要遵循的复杂规则,这些规则冗长、晦涩。JavaScript 对象到原始值转换的复杂性,主要原因在于某些对象类型有不止一种原始值的表示。JavaScript 规范定义了对象到原始值转换的 3 种基本算法。

  • 偏字符串:该算法返回原始值,而且只要可能就返回字符串
  • 偏数值:该算法返回原始值,而且只要可能就返回数值
  • 无偏好:该算法不倾向于任何原始值类型,而是由类定义自己的转换规则

JavaScript 内置类型除了 Date 类都实现了偏数值算法,Date 类实现了偏字符串算法。接下来我们先介绍哪些情况下会使用到这些算法

对象转换为布尔值

  • 所有对象都转换为true
  • 这个转换不需要使用前面介绍的对象到原始值的转换算法,而是直接适用于所有对象
  • 包括空数组,甚至包括 new Boolean(false) 这样的包装对象
1
2
3
> let t = new Boolean(false)
> !!t
true

对象转换为字符串

  • JavaScript 首先使用偏字符串算法将它转换为一个原始值,然后将得到的原始值再转换为字符串
  • 当把对象传递给接受字符串参数的内置函数时、或者调用 String() 进行转换时、或者将对象插入到模版字面量中时,就会发生这种转换

对象转换为数值

  • JavaScript 首先使用偏数值算法将它转换为一个原始值,然后将得到的原始值再转换为数值
  • 接收数值参数的内置 JavaScript 函数和方法都以这种方式将对象转换为数值,希望接受数值参数的操作符也是按照这种方式把对象转换为数值(以下操作符转换特例除外):

接下来再介绍一些操作符转换特例:

  • JavaScript中的 + 操作符可以执行数值加法和字符串拼接:
    • 如果操作数是对象,那 JavaScript 会使用无偏好算法将对象转换为原始值
    • 一旦两个操作数都是原始值,则会先检查它们的类型。如果有一个参数是字符串,则把另一个原始值也转换为字符串并拼接两个字符串
    • 否则,把两个参数都转换为数值并把它们相加
1
2
3
4
> "1" + 1
'11'
> 1 + "1"
'11'
  • ==!= 操作符以允许类型转换的宽松方式执行相等和不相等测试

    • 如果一个操作数是对象,另一个操作数是原始值,则这两个操作符会使用无偏好算法将对象转换为原始值,然后再比较两个原始值
  • 关系操作符 <、<=、> 和 >= 用于比较操作数,它们既可以比较数值,也可以比较字符串

    • 如果操作数中有一个是对象,则会使用偏数值算法将对象转换为原始值。不过要注意,与对象到数值转换不同,这个偏数值算法返回的原始值不会再被转换为数值

所有对象都会继承 toString()toValue() 这两个方法,它们在对象到原始值转换时会被用到。

  • toString() 方法返回对象的字符串表示,默认情况下,该方法不会返回有意义的值。很多类都定义了自己特有的 toString() 版本
1
2
3
4
5
6
> {x:1, y:2}.toString()
'[object Object]'
> [1, 2, 3].toString()
'1,2,3'
> /\d+/.toString()
'/\\d+/'
  • valueOf() 方法并没有明确的任务定义,主要认为它可以将对象转换为代表对象的原始值(如果的确存在这样一个原始值)。valueOf() 方法默认返回对象本身

现在我们可以大致介绍前面 3 个对象到原始值的转换算法的实现了:

  • 偏字符串算法:

    • 首先尝试 toString() 方法。如果这个方法有定义且返回原始值,则 JavaScript 使用该原始值(即使这个值不是字符串)​
    • 如果 toString() 不存在,或者存在但返回对象,则 JavaScript 尝试 valueOf() 方法。如果这个方法存在且返回原始值,则 JavaScript 使用该值。
    • 否则,转换失败,报 TypeError
  • 偏数值算法:

    • 与偏字符串算法类似,只不过是先尝试 valueOf() 方法,再尝试 toString() 方法
  • 无偏好算法:

    • 无偏好算法取决于被转换对象的类
    • 如果是一个 Date 对象,则 JavaScript 使用偏字符串算法
    • 如果是其他类型的对象,则 JavaScript 使用偏数值算法

了解这些转换规则,我们就能理解为什么单个数值元素的数组可以转换为数值了:

1
2
3
4
5
6
7
8
> String([1])
'1'
> String([1, 2])
'1,2'
> Number([1])
1
> Number([1, 2])
NaN

变量声明与赋值

当把名字和值进行绑定时,我们通会说把值赋值给变量。变量意味着可以为其赋予新值,即变量所关联的值在程序运行期间可能会变化。如果把一个值永久地赋给一个名字,那么成该名字为常量而不是变量。

在 JavaScript 中使用变量或常量前,必须先声明它

  • 在 ES6 及之后的版本中,这是通过 let 和 const 关键字来完成的
  • 在ES6之前,变量是通过 var 声明的

使用 let 和 const 声明

在现代 JavaScript(ES6及之后)中,变量是通过 let 关键字声明的,可以使用一条 let 语句声明多个变量:

1
2
3
let i;
let sum;
let j, x;

声明变量的同时(如果可能)也为其赋予一个初始值是个好的编程习惯。如果在 let 语句中不为变量指定初始值,变量也会被声明,但在被赋值之前它的值是undefined

1
2
3
4
5
6
7
8
9
> let i = 1
> let s = "hello"
> let jh
> i
1
> s
'hello'
> jh
undefined

要声明常量而非变量,则要使用 const 而非 let。

  • const 与 let 类似,区别在于 const 必须在声明时初始化常量
  • 常量的值是不能改变的,尝试给常量重新赋值会抛出 TypeError
  • 声明常量的一个常见(但并非普遍性)的约定是全部字母大写
1
2
3
4
5
6
7
8
const C = 100

> const y;
const y;
^
Uncaught SyntaxError: Missing initializer in const declaration
> C = 200
Uncaught TypeError: Assignment to constant variable.

JavaScript 允许在循环语法中声明循环变量,这也是 let 另一个常见的使用场景:

1
2
3
for (let i = 0, len = data.length; i < length; i++) {
console.log(data[i])
}

虽然看起来有点怪,但也可以使用 const 声明 for/in 和 for/of 中的这些循环“变量”​,只要保证在循环体内不给它重新赋值即可。此时,const 声明的只是一次循环迭代期间的常量值。

1
2
3
for (const i of data) {
console.log(i);
}

变量与常量作用域

通过 let 和 const 声明的变量和常量具有块作用域。这意味着它们只在 let 和 const 语句所在的代码块中有定义。

  • JavaScript 类和函数的函数体是代码块,if/else 语句的语句体、while 和 for 循环的循环体都是代码块
  • 粗略地讲,如果变量或常量声明在一对花括号中,那这对花括号就限定了该变量或常量有定义的代码区域
  • 在声明变量或常量的 let 或 const 语句之前的代码行中引用这些变量或常量也是不合法的
  • 作为for、for/in 或 for/of 循环的一部分声明的变量和常量,以循环体作为它们的作用域,即使它们实际上位于花括号外部
  • 如果声明位于顶级,在任何代码块外部,则称其为全局变量或常量,具有全局作用域
    • 在 Node 和客户端 JavaScript 模块中​,全局变量的作用域是定义它们的文件
    • 在传统客户端 JavaScript 中,全局变量的作用域是定义它们的 HTML 文档

重复声明

在同一个作用域中使用多个 let 或 const 声明同一个名字是语法错误。在嵌套作用域中声明同名变量是合法的(尽管实践中最好不要这么做):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let i = 0;

{
i = 20;
console.log(i); // => 20
}

{
let i = 30;
console.log(i); // => 30
}

{
// Cannot access 'i' before initialization
i = 40;
let i = 50;
console.log(i);
}

声明与类型

JavaScript 的变量声明与值的类型无关。JavaScript 变量可以保存任何类型的值。

1
2
> let i = 10
> i = "test"

使用 var 的变量声明

在 ES6 之前的 JavaScript 中,声明变量的唯一方式是使用 var 关键字,无法声明常量。var 的语法与 let 的语法相同:

1
2
3
4
5
6
> var x = 20
> var xx
> x
20
> xx
undefined

虽然 var 和 let 有相同的语法,但它们也有重要的区别。

  • 使用 var 声明的变量不具有块作用域。这种变量的作用域仅限于包含函数的函数体,无论它们在函数中嵌套的层次有多深
  • 如果在函数体外部使用 var,则会声明一个全局变量,通过 var 声明的全局变量与通过 let 声明的全局变量有一个重要区别
    • 通过 var 声明的全局变量被实现为全局对象的属性,且这个属性不能使用 delete 操作符删除
    • 通过 let 和 const 声明的全局变量和常量不是全局对象的属性
  • 与通过 let 声明的变量不同,使用 var 在相同作用域内多次声明同名变量是合法的(var 变量都是具有函数作用域),而且这种重新声明也很常见
1
2
3
4
5
6
7
function test2() {
var i = 1;
{
var i = 2;
}
console.log(i); // => 2
}
  • var 声明的一个最不同寻常的特性是作用域提升(hoisting)。在使用 var 声明变量时,该声明会被提高(或提升)到包含函数的顶部,但变量的初始化仍然在代码所在位置完成,只有变量的定义转移到了函数顶部。
    • 因此对使用 var 声明的变量,可以在包含函数内部的任何地方使用而不会报错
    • 如果初始化代码尚未运行,则变量的值可能是 undefined,但在初始化之前是可以使用变量而不报错的
    • 这会成为一个 bug 来源,也是 let 要纠正的一个最重要的错误特性。如果使用 let 声明了一个变量,但试图在 let 语句运行前使用该变量则会导致错误,而不是得到 undefined 值
1
2
3
4
5
6
7
8
9
10
11
function test3() {
console.log(i) // => undefined

{
var i;
}

i = 10;

console.log(i) // => 10
}

另外,关于使用未声明的变量,也有注意事项。

  • 在严格模式下​,如果试图使用未声明的变量,那代码运行时会触发引用错误
  • 在严格模式外部,如果将一个值赋给尚未使用 let、const 或 var 声明的名字,则会创建一个新全局变量。且无论这个赋值语句在函数或代码块中被嵌套了多少次,都会创建一个全局变量。这很容易导致 bug,因此总是推荐使用严格模式
  • 但是以这种意外方式创建的全局变量类似于使用 var 声明的全局变量,都是定义全局对象的属性。但是这些属性可以通过的 delete 删除

解构赋值

ES6 实现了一种复合声明与赋值语法,叫作解构赋值(destructuring assignment)。

  • 等号右手端的值是数组或对象(​结构化 的值)​
  • 左边通过模拟数组或对象字面量语法指定一个或多个变量
  • 解构赋值会从右侧的值中提取出一个或多个值,并保存到左侧列出的变量中
  • 解构赋值可能最常用于在 const、let 或 var 声明语句中初始化变量,但也可以在常规赋值表达式中使用。解构也可以在定义函数参数时使用
  • 解构赋值让使用返回数组的函数变得异常便捷
  • 也可以在这个 for 循环上下文中使用变量解构赋值
  • 解构赋值左侧变量的个数不一定与右侧数组中元素的个数相同。左侧多余的变量会被设置为 undefined,而右侧多余的值会被忽略。左侧的变量列表可以包含额外的逗号,以跳过右侧的某些值
  • 在解构赋值时,如果你想把所有未使用或剩余的值收集到一个变量中,可以在左侧最后一个变量名前面加上 ...
  • 解构赋值可用于嵌套数组。此时,赋值的左侧看起来也应该像一个嵌套的数组字面量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> let [x, y] = [1, 2]
> x
1
> y
2
> [x, y] = [y, x]
> x
2
> y
1
> [,x,,,y] = [1, 2, 3, 4, 5]
> x
2
> y
5
> [x, ...y] = [1, 2, 3, 4, 5]
> x
1
> y
[ 2, 3, 4, 5 ]

> let [a, [b, c], d] = [1, [2, 3], 4]
1
2
3
4
5
let o = {name:"Mike", age:20};

for (let [key, value] of Object.entries(o)) {
console.log(key, value);
}

数组解构的一个强大特性是它并不要求必须是数组!实际上,赋值的右侧可以是任何可迭代对象​,任何可以在 for/of 循环中使用的对象也可以被解构:

1
2
3
4
5
> let [x, ...y] = "hello"
> x
'h'
> y
[ 'e', 'l', 'l', 'o' ]

解构赋值在右侧是对象值的情况下也可以执行。此时,赋值的左侧看起来就像一个对象字面量,即一个包含在花括号内的逗号分隔的变量名列表

  • 赋值右侧对象中那些没有提到名字的属性都被忽略了。如果赋值的左侧包含一个不是右侧对象属性的变量名,该变量将被赋值 undefined。
  • 左侧中变量的名称不一定要与结构对象的属性名称一致(当然这样做更好,保持简单、易于理解)。对象解构赋值左侧的每个标识符都可以是一个冒号分隔的标识符对,其中第一个标识符是要解构其值的属性名,第二个标识符是要把值赋给它的变量名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> let color = {r: 0, g: 100, b:255};
> let {r, g, b} = color
> r
0
> g
100
> b
255

> let {name, age, country} = {name:"mike", age:20, heigth:180}
> name
'mike'
> age
20
> country
undefined

> let {name:n, age:a} = {name:"jack", age:21}
> n
'jack'
> a
21

在使用嵌套对象、对象的数组,或数组的对象时,解构赋值甚至会变得更复杂,但都是合法的。

1
2
3
4
5
6
7
8
9
> let [{x: x1, y:y1}, {x:x2, y:y2}] = [{x:10, y:20}, {x:30, y:40}]
> x1
10
> y1
20
> x2
30
> y2
40

当然复杂的结构语法可能会导致代码难于理解,有时候还不如使用传统代码,这样更易于理解。