Skip to content

类型、值和变量

1. 概述

JavaScript 类型可以分为原始类型和对象类型。

  • 原始类型:数值(number、BigInt)、字符串、布尔值、null、undefined、Symbol。
  • 对象类型:非原始类型的值都是对象。

注意

  1. 在 JavaScript 中只有 null 和 undefined 是不能调用方法的值。
  2. 对象类型可以修改,原始类型不能修改。

2. 数值

2.1 整数字面量

十进制数可以直接写成数字序列,十六进制字面量以 0x/0X 开头,二进制以 0b/0B 开头,八进制以 0o/0O 开头。

javascript
let a = 10 // 十进制
let b = 0xff // 十六进制
let c = 0b1101 // 二进制
let d = 0o17 // 八进制

2.2 浮点字面量

浮点字面量可以使用科学计数法表示,即实数值后跟一个 e/E ,再跟一个整数。

javascript
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 预定义了全局常量 InfinityNaN,也可以通过 Number 的属性获取。
  • NaN 与任何值都不相等,包括自己。
  • -0 === 0 为 true。
javascript
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 无法精确表示。

javascript
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,但可以通过前缀 0b0o0x 来表示二进制、八进制和十六进制 BigInt。

可以用 BigInt() 函数把常规 JavaScript 数值或字符串转换为 BigInt 值。

BigInt 运算与常规数值运算类似,但不能把 BigInt 类型与常规数值混一起计算。

javascript
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 使用字符串

javascript
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 表达式,这些表达式要放在 ${} 中。

标签化模板字面量:在反引号前面有个函数名(标签),则模板字面量中的文本和表达式值将作为参数传给这个函数,函数返回值就是标签化模板字面量的值。

javascript
let s = "hello,"
let a = s.concat`wolld,${s}` // s.concat(['wolld,', 'hello,'])

注意

即使标签化模板字面量的标签部分是函数,在调用这个函数时也没有圆括号。

3.5 模式匹配

正则表达式用于描述和匹配文本中的字符串模式,不是基本类型。

一对斜杠之间的文本构成正则表达式字面量。

4. 布尔值

布尔类型只有两个值:truefalse

布尔值有一个 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() 方法会返回传入的参数。
javascript
let s = Symbol('sym_x')
s.toString() // Symbol(sym_x)
  • Symbol.for() 函数接收一个字符串参数,返回一个与该字符串关联的 Symbol 值,如果没有符号与该字符串关联,则会创建并返回一个新符号;否则,就会返回已有的符号。
  • Symbol() 永远不会返回相同的值,而在以相同的字符串调用时Symbol.for() 始终返回相同的值。

7. 全局对象

JavaScript 解释器启动后,会创建一个新的全局对象并为其添加一组初始属性:

  • 添加 undefinedInfinityNaN 这样的全局常量。
  • isNaN()parseInt()eval() 这样的全局函数。
  • Date()RegExp()String()Object()Array() 这样的构造函数。
  • MathJSON 这样的全局对象。
  • Node 程序中可以通过 global 引用全局对象,浏览器中通过 window

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

  • 原始值(undefined、null、布尔值、数值和字符串)不可修改,对于字符串来说,所有修改字符串的方法都是返回一个新的字符串,对象可以修改。
  • 原始值按值进行比较,对象不是按值进行比较,两个对象即使属性和值完全相同,它们也不相等。
  • 对象也被称为引用类型,两个对象值当且仅当引用同一个底层对象,它们才是相等的。
  • 把对象复制给一个变量,其实是在赋值引用,并不会创建对象的新副本,如果想创建对象或数组的新副本,必须显式复制对象的属性或数组的元素。

9. 类型转换

JavaScript 会根据需要的类型自动进行类型转换。

转为字符串转为数值转为布尔值
undefined"undefined"NaNfalse
null"null"0false
true"true"1
false"false"0
""(空字符串)0fasle
"1.2"(非空、数值)1.2true
"one"(非空、非数值)NaNtrue
0/-0"0"false
1(有限、非零)"1"true
Infinity"Infinity"true
-Infinity"-Infinity"true
NaN"NaN"fasle
[](空数组)""0true

9.1 转换与相等

  • ===:严格相等操作符,如果两个值不是同一种类型,就不会判断相等。
  • ==:比较灵活,有的情况会先类型转换再比较。

9.2 显式转换

  • 使用 Number()Boolean()String() 函数可以执行显示转换。
  • nullundefined 之外的所有值都有 toString() 方法,这个方法返回的结果通常与 String() 函数返回的结果相同。
  • number 类型的 toString() 方法接收一个可选参数,用于指定一个基数,默认为 10,也可以按其它基数来转换数值。
  • toFixed():把数值转换为字符串时指定小数点后位数。
  • 全局函数 parseInt()parseFloat() 可以把字符串转为数值,前者只解析整数,后者解析整数和浮点数,二者都会跳过开头的空格,尽量多地解析数字,如果第一个字符不是有效数字字面量,返回 NaN
javascript
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 方法会返回被包装的原始值。
javascript
({x: 1, y: 2}).toString() // [object Object]

9.3.6 对象到原始值转换算法

  • 偏字符串算法首先尝试 toString() 方法,如果该方法有定义且返回原始值,则使用该原始值;如果 toString() 方法不存在,或存在但返回对象,则尝试 valueOf() 方法;如果 valueOf() 存在且返回原始值,就使用该原始值,否则转换失败,报 TypeError。
  • 偏数值算法与偏字符串算法类似,只不过先尝试 valueOf() 方法,再尝试 toString() 方法。
  • 无偏好算法取决于被转换对象的类。如果是 Date 对象,则使用偏字符串算法,否则使用偏数值算法。
javascript
/** 
 * 使用偏数值算法,首先调用 valueOf() 得到数组对象本身,然后调用 toString() 
 * 将 [] 转为 "",[99] 转为 "99",再将原始值转为数值类型,分别得到 0 和 99
*/
Number([]) // 0
Number([99]) // 99

10. 变量声明与赋值

10.1 使用 let 和 const

  • 在现代 JavaScript 中,通过 let 声明变量,通过 const 声明常量。
  • 声明变量时不指定初始值,则变量值为 undefined
  • const 在声明时必须初始化,且对于字面量常量,应该全部字母大写。
javascript
let i;
let a, b;
const PI = 3.14;

TIP

forfor...infor...of 循环语句都包含一个循环变量,在循环的每次迭代中都会取得一个新值,可以在循环语法中声明这个循环变量,也是 let 的常见使用场景。

javascript
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 变量与常量作用域

  • 通过 letconst 声明的变量和常量具有块级作用域,通俗地说,如果变量和常量声明在一对花括号中,则它们只在花括号内有效。
  • 在 Node 和 客户端 JS 模块中,一个文件就是一个模块,在模块顶层声明的变量或常量,仅在模块作用域内有效,不是全局变量;对于传统客户端 JS(浏览器环境),全局变量的作用域是 HTML 文档,如果有 <script> 标签声明了一个全局变量或常量,该变量或常量在同一个文档的任何 <script> 标签中都有定义。
  • 作为 forfor...offor...in 循环的一部分声明的变量或常量,作用域是循环体,即使实际上位于花括号外部。

10.1.2 重复声明

同一个作用域中不能用 letconst 声明同名的变量或常量,可以在嵌套的作用域中声明,但不推荐。

javascript
const x = 1;
if (x == 1) {
  let x = 2; // 不推荐声明外部作用域同名的变量
  console.log(x) // 2
}

10.2 使用 var 的变量声明

在 ES6 之前,只能用 var 声明变量,不能声明常量。

varlet 的区别:

  • var 声明的变量不具有块作用域,只有函数作用域。
  • var 可以多次声明同名变量。
  • var 有作用域提升特性,letconst 没有,即在作用域内使用 var 声明变量时,声明会被提升到作用域顶部,但初始化仍在代码所在位置。
javascript
function test1() {
  var a = 1
  if (a == 2) {
    var b = 2
  }
  console.log(b)
}
test1() // undefined
// 看起来条件语句不执行,b应该未声明
// 实际上函数内的 var 均会声明且提升,只是没有赋值
  • 浏览器环境中,在任何代码块外部使用 var,会声明为一个全局变量,且会被实现为全局对象的属性(该全局对象可以通过 globalThis 或 window 引用);通过 let 会声明为全局变量,但不会成为全局对象上的属性。
  • Node 环境中,在任何代码块外使用 varlet ,都不会声明为全局变量,也不会成为全局对象上的属性(该全局对象可以通过 globalThis 或 global 引用)。
  • 如果不使用 varlet 声明变量而直接初始化变量,该变量会自动提升为全局变量 ,同时作为属性添加到全局对象上,在严格模式下会报错。
javascript
let a = 1
if (a = 1) {
  b = 2
}
console.log(b) // 2
console.log(globalThis.b) // 2

10.3 解构赋值

解构赋值是 ES6 之后提出一种复合声明与赋值的语法,可以从数组或对象中提取多个值赋值给左侧的变量列表。

10.3.1 数组解构

javascript
let [x, y] = [1, 2]
[x, y] = [x + 1, y + 1]
[x, y] = [y, x]
[x, y] // [3, 2]

在各种 for 循环中可以使用解构赋值。

javascript
let o = { x: 1, y: 2}
for (const [name, value] of Object.entries(o)) {
  console.log(name, value) // 打印 'x 1' 和 'y 2'
}

数组解构赋值左侧变量的个数不一定与右侧数组元素个数相同。左侧多余的变量会被设置为 undefined,而右侧多余的值会被忽略。左侧变量列表可以包含额外的逗号以跳过某些值。

javascript
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

把剩余的值收集到一个变量中,可以在左侧最后一个变量前加上 ...

javascript
let [x, ...y] = [1, 2, 3, 4] // y == [2, 3, 4]

解构赋值可用于嵌套数组。

javascript
let [a, [b, c]] = [1, [2, 2.5], 3] // a == 1, b == 2, c == 2.5

数组解构的右侧不要求必须是数组,可以是任何可迭代对象。

javascript
let [first, ...rest] = 'hello' 
// first == 'h', rest == ['e', 'l', 'l', 'o']

10.3.2 对象解构

右侧为对象,左侧是一个包含在花括号内的逗号分隔的变量列表,看起来像对象字面量。

javascript
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

如果想要变量名跟属性名不一样,可以通过冒号给变量起别名。

javascript
// 将 Math 的 cos 属性赋值给 cosine 变量,tan 属性给 tangent 变量
let { cos: cosine, tan: tangent } = Math

与数组解构一样,通过 ... 可以将剩余的键值对收集到左侧的变量中:

javascript
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 }