跳到主要内容

类型、值、变量

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 任何值都能转为布尔值。undefinednull±0NaN"" 都会转为 false(和 false 一同称为假性值 falsy ),而其余的值为真性值,即转为 true

null & undefined

nullundefined 都表示某个值不存在,null 更接近于表示引用的空指向。两者相等,但不全等。

typeof null 为 "object" 。

符号

符号 Symbol 是 ES6 新增的一种原始类型,用作非字符串的属性名。

符号没有字面量语法,创建符号需要调用 Symbol() 函数,且这个函数永远不会返回相同的值,即使是相同的实参。所以使用符号可以为对象安全地添加新属性,无需担心重名问题。

虽然相同参数的两个符号不等,但是 toString() 方法的结果一般都是一致的:

Symbol("hello").toString() // => "Symbol(hello)"

为了与别的代码共享符号值,可以使用 Symbol.for() ,这个函数行为类似字典,给定一个参数,如果存在对应的符号值则返回,否则创建符号并返回。

全局对象

JS 解释器启动后,都会创建一个新的全局对象,并为其设置一些初始属性,比如:

  • undefinedInfinityNaN 等全局常量;

  • isNaN()parseInt()eval() 等全局函数;

  • Date()RegExp()String()Object()Array() 等构造函数;

  • MathJSON 等全局对象。

这些全局属性及函数并非保留字,但应当视为保留字。

在 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 - MDNparseFloat - MDNNumber - MDN

字符串隐式转换为数值时会识别进制前缀。

对象转换

  • 所有对象转为布尔类型均为 true

  • 转为字符串类型时,会先尝试使用对象的 toString() 方法,若该方法不存在或者返回的不是原始值,则尝试 valueOf() 方法,若仍然不是原始值,则报错 TypeError 。

    这称为 偏字符串 算法;

  • 转为数值类型时则与转为字符串的操作相反,先使用 valueOf() 再尝试 toString()

    这称为 偏数值 算法;

  • 还有 无偏好 算法,这取决于对象类型,如果是 Date 类型则偏字符串,其余类型则偏数值。

一般对象的 toString() 方法默认会返回 "[object 类型名]"

一般的操作符,比如 +==!= 会先对对象进行无偏好转换。而 <<=>>= 则会进行偏数值转换。这意味着 Date 对象会转为数值进行比较,而非无偏好下的字符串。

变量声明 & 赋值

ES6 之前,变量是通过 var 声明,之后则可以用 letconst

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 // ! 报错

letconst 声明的变量具有块级作用域。在同一作用域中重复声明会报错。

声明在任何代码块外部的称为全局变量,具有全局作用域。

用 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

参考

JavaScript 数据类型和数据结构 - MDN

Number - MDN

String - MDN