JS 回顾 1
约 9591 字大约 32 分钟
2026-01-21
数值
精度
JavaScript 内部,所有数字都是以 64 位浮点数 形式储存,即使整数也是如此,所以,1 与 1.0 是相同的,是同一个数。
“64 位浮点数”说白了就是:JavaScript 的 Number 用 64 个二进制位来存一个数字,并且采用 IEEE 754 的“双精度(double)浮点数”格式。所以它既能表示整数,也能表示小数,但“底层存法”本质上都是同一种,42.0 和 42 当然就是同一个数,JS 里不存在 int 这种独立类型(ES2020 才有 BigInt)More
JS 中 所有 number 类型的值(不管你写的是 42 还是 42.5 ,都用 IEEE 754 双精度(64-bit float) 来表示/存储/计算,所以“整数”和“小数”在 JS 的 Number 世界里 是同一种底层格式
Number.isInteger(42) // true
Number.isInteger(42.5) // false
typeof 42 // "number"
typeof 42.5 // "number"IEEE 754 双精度把这 64 位分成 3 段:
- 1 位:符号位(正/负)
- 11 位:指数(决定数量级有多大)
- 52 位:尾数/有效数字(决定“精度”)
解读
64 位被硬切成三段
格式固定:
- 第 1 位:符号位
S(sign)- 接下来 11 位:指数位
E(exponent)- 最后 52 位:尾数/小数位
F(fraction / mantissa)[S][EEEEEEEEEEE][FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF] 1 11 52数字 在内存里就是一串 0 和 1,而且 布局这个样子 ↑,在底层(CPU / JS 引擎)里,最终都会被编码成这样一串 64 个二进制位。
例子 1:
1.0的 64 位长啥样?
1.0的双精度二进制是:S = 0 E = 01111111111 F = 0000000000000000000000000000000000000000000000000000“拼回”成数字要进行 规格化数,规则是:值=(−1)S×(1.F)×2(E−1023)
(-1)^S:符号,S = 0 就是正
(1.F):有效数字,注意前面有个 隐含的 1(也就是“1.”开头)
E-1023:指数要减去一个固定偏移量 1023(叫 bias)套到 1.0 上:
- S = 0 → 正数
- E =
01111111111这个二进制等于 1023,所以E-1023 = 0- F 全 0 →
1.F = 1.0000...就是 1所以:
1.0=(+1)×1×20
例子 2:
-2.5的 64 位
-2.5的三段是:S = 1 E = 10000000000 F = 0100000000000000000000000000000000000000000000000000S = 1 → 负数
E = 10000000000这个二进制等于 1024,所以指数是:E−1023=1,也就是乘以2^1(放大 2 倍)
1.F的意思是:固定从 1 开头,然后 F 是小数部分的二进制位:1.F = 1.0100000000...1.F=1+0.25=1.25
−2.5=(−1)×1.25×21=−2.5
符号位 决定负号
指数位 决定乘以
2^几(把整体放大/缩小)尾数位 决定“精细的具体数值”(1.25 这个细节)
为什么
0.1会不精确:
0.1的三段是:S = 0 E = 01111111011 F = 1001100110011001100110011001100110011001100110011010会发现 F 出现了
1001 1001 1001...这 种循环样子这就是因为 0.1 用二进制表示是无限循环小数,而尾数只有 52 位,只能截断,于是就“差一点点”。
由于无法存储无限位数,计算机(如使用标准的 64 位双精度浮点数 IEEE 754)只能存储其近似值。
“双精度”(double precision)就是:用 64 位(8 字节)来存一个浮点数的标准格式(IEEE 754 里的一种)。
它叫“双 精度”,是相对“单 精度”来说的:
- 单精度(float32):32 位(4 字节)
- 双精度(float64 / double):64 位(8 字节)
因为位数越多,能存的“细节”越多:
- 双精度能提供大约 15~17 位十进制有效数字 的精度
- 单精度大约只有 6~9 位十进制有效数字
因为有效数字只有 52 位(实际精度约等于 53 位二进制有效位),这会带来两个非常经典的后果:
后果 1:很多小数“存不准”,像 0.1、0.2 在二进制里是无限循环小数,只能截断近似,所以会出现:
0.1 + 0.2 === 0.3 // false后果 2:整数也不是无限精确(有“安全整数”上限)
因为精度有限,能保证不丢精度的最大整数是 2^53 - 1(也就是 Number.MAX_SAFE_INTEGER = 9007199254740991),超过这个范围,有些整数就会“跳格子”,相邻两个整数可能被存成同一个值。
JS 的 Number 最多只能保证 53 个二进制有效位
原因:尾数 52 位,隐含的最高位 1,总共 53 位二进制有效数字
安全整数范围,能被精确表示的整数范围:
-(2^53 - 1) 到 (2^53 - 1)常量:
Number.MAX_SAFE_INTEGER === 9007199254740991
Number.MIN_SAFE_INTEGER === -9007199254740991超过 2^53 整数会开始“跳数”,相邻整数可能会被表示成 同一个值
Math.pow(2, 53)
// 9007199254740992
Math.pow(2, 53) + 1
// 9007199254740992
Math.pow(2, 53) + 2
// 9007199254740994
Math.pow(2, 53) + 3
// 9007199254740996
Math.pow(2, 53) + 4
// 9007199254740996不是每个数都错,而是“无法保证正确”
超过 53 位二进制精度后,多出来的有效数字会被截断,十进制表现为:尾部数字被“抹平”
9007199254740992111
// 实际存成
9007199254740992000不是算错,是存不下
一个非常实用的经验法则:JavaScript 可以精确处理:15 位以内的十进制整数,16 位及以上:不保证
不要用 Number 存:超大 ID,精确的金额(分以下),银行账号 / 身份证 / 哈希值
替代方案:BigInt,字符串,专用高精度库(如 decimal.js)
ES2020(不是 ES6)新增了一个专门处理超大整数的类型:BigInt。
Number:IEEE 754 双精度,整数安全范围到±(2^53 - 1)BigInt:可以表示 任意大 的整数(只受内存限制),不会丢精度,BigInt 只能表示整数
1n 里的这个 n 是 BigInt 的字面量后缀,意思很简单:告诉 JavaScript:这个数字不是 Number,而是 BigInt。
1 // Number 类型(双精度浮点数)
1n // BigInt 类型(任意精度整数)
typeof 1 // "number"
typeof 1n // "bigint"
9007199254740993 // 已经丢精度了(Number)
9007199254740993n // 精确(BigInt)JS 默认把 所有数字字面量都当成 Number,这个 n 就是 显式声明:这是个超大整数,请用 BigInt 来存,它只对“整数字面量”有效
// 合法
0n
123n
-999n
// 不合法
1.2n // BigInt 不能有小数
1e3n // 科学计数法也不行BigInt 和 Number 不能直接混算,要么都用 BigInt,要么显式转换
Number(1n) + 1
BigInt(1) + 1n总结
JavaScript 的 Number 不适合表示精确金额
错误不是“存的时候才发生”,而是“计算过程中就已经发生”
只要使用了 Number,每一步运算都可能发生精度丢失
绝对禁止:
let balance = 1234.56; // Number 存金额
balance += 0.1;原因:十进制小数在 Number 中无法精确表示,连续加减会累积误差,结果在 业务上不可接受
正确做法 1:整数化 + BigInt(强烈推荐)
金额统一换算为最小货币单位(如“分”)进行存储和计算
// 存储(单位:分)
let balance = 123456n; // ¥1234.56
// 交易
balance += 1000n; // +¥10.00
balance -= 255n; // -¥2.55展示给用户时再格式化
function formatCNY(amountInFen) {
return `${amountInFen / 100n}.${(amountInFen % 100n).toString().padStart(2, "0")}`;
}所有整数运算完全精确,不受 2^53 - 1 限制,可审计、可追责
正确做法 2:字符串 / 高精度库(跨币种 / 利率)
适用场景:
- 利率、汇率
- 多位小数
- 需要四舍五入规则、银行家舍入等
推荐方案:decimal.js,big.js
示意:
new Decimal("0.1").plus("0.2").equals("0.3"); // true后端金额字段数据库用 BIGINT(分),或 DECIMAL(20, 4)(明确精度),禁止浮点类型存钱,API 传输用 字符串 或 整数
64 位浮点数的指数部分的长度是 11 个二进制位,意味着指数部分的最大值是 2047(2 的 11 次方减 1)。
也就是说,64 位浮点数的指数部分的值最大为 2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为 21024 到 2−1023(开区间),超出这个范围的数无法表示。
如果一个数大于等于 2 的 1024 次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回 Infinity。
Math.pow(2, 1024) // Infinity如果一个数小于等于 2 的-1075 次方(指数部分最小值-1023,再加上小数部分的 52 位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回 0。
Math.pow(2, -1075) // 0JavaScript 提供 Number 对象的 MAX_VALUE 和 MIN_VALUE 属性,返回可以表示的具体的最大值和最小值。
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324Number.MAX_VALUE Number 能表示的最大有限值(最大“量级”),大约是 1.7976931348623157e+308,再大就会变成 Infinity
Number.MAX_SAFE_INTEGER 能被 Number 精确表示的最大整数,等于 2^53 - 1,也就是 9007199254740991,超过它以后:不是一定立刻错,但“相邻整数分不清”,整数运算不再可靠。
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2标准里没有 BigInt.MAX_VALUE / MIN_VALUE / MAX_SAFE_INTEGER 这类常量。原因很直接:BigInt 设计目标就是“任意精度整数”,不是固定 64 位/128 位那种。
BigInt 有极限,但不是“固定上限常量”,而是 实现与资源限制 决定的:
- 内存:数越大,占用的字节越多
- 时间:加减乘除的成本会随位数增加(大数乘法/除法更明显)
- 引擎/平台限制:不同 JS 引擎可能有自己的内部上限或防御性限制
所以工程上可以理解为:理论上无限,实践上受内存/性能约束。
BigInt 不支持 Infinity / NaN
超出能力时,通常是 内存不足 或引擎抛错,而不是得到一个“无穷大 BigInt”。
表示
科学计数法:
a × 10^n(十进制科学计数法),其中 1 ≤ |a| < 10
- 1234000 = 1.234 × 10^6
- 0.00056 = 5.6 × 10^-4
- 42 = 4.2 × 10^1
JS 的“科学计数法”,通常就是 指数表示法(exponential notation):用 e / E 表示“乘以 10 的多少次方”。
1.23e5读作:1.23 × 10^57.5e-3读作:7.5 × 10^-3
JS 代码里的 1e3:是 × 10^3(十进制表示法)
JS 内部存储:依然会转成 IEEE 754 的 2^n 来存(这是底层实现)
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。
- 十进制:没有前导 0 的数值。
- 八进制:有前缀
0o或0O的数值,或者有前导 0、且只用到 0-7 的八个阿拉伯数字的数值。 - 十六进制:有前缀
0x或0X的数值。 - 二进制:有前缀
0b或0B的数值。
默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制。
通常来说,有前导 0 的数值会被视为八进制,但是如果前导 0 后面有数字 8 和 9,则该数值被视为十进制。
前导 0 表示八进制,处理时很容易造成混乱,ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法。
NaN 是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
NaN 不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于 Number,使用 typeof 运算符可以看得很清楚。
NaN 不等于任何值,包括它本身,NaN 在布尔运算时被当作 false,NaN 与任何数(包括它自己)的运算,得到的都是 NaN。但是,ES6 引入指数运算符(**)后,NaN ** 0 // 1
Infinity 表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非 0 数值除以 0,得到 Infinity。
方法
parseInt()
parseInt 方法用于将字符串转为 整数,如果字符串头部有空格,空格会被自动去除,如果 parseInt 的参数不是字符串,则会先转为字符串再转换。
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
如果字符串的第一个字符不能转化为数字,返回 NaN。
parseInt 方法还可以接受第二个参数(2 到 36 之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt 的第二个参数为 10,即默认是十进制转十进制。
parseFloat()
parseFloat 方法用于将一个字符串转为浮点数。
如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。
如果参数不是字符串,则会先转为字符串再转换。
isNaN()
isNaN 方法可以用来判断一个值是否为 NaN。
isNaN(NaN) // true
isNaN(123) // falseisNaN 只对数值有效,如果传入其他值,会被先转成数值。
比如,传入字符串的时候,字符串会被先转成 NaN,所以最后返回 true,这一点要特别引起注意。
也就是说,isNaN 为 true 的值,有可能不是 NaN,而是一个字符串。
判断 NaN 更可靠的方法是,利用 NaN 为唯一不等于自身的值的这个特点,进行判断:
function myIsNaN(value) {
return value !== value;
}isFinite()
isFinite 方法返回一个布尔值,表示某个值是否为正常的数值。
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true字符串
反斜杠(\)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
JavaScript 使用 Unicode 字符集,JavaScript 引擎内部,所有字符都用 Unicode 表示。
Unicode 是一套标准:规定每个字符对应一个编号(码点 code point),比如:
A是U+0041,中是U+4E2D,😀 是U+1F600,这一步只是在说:“这个字符编号是多少”,还没说怎么存。它规定了一个巨大的“字符表”,每个字符都有一个唯一编号,叫 码点(code point),码点常写成
U+XXXX这种形式,比如 雪人:U+2603(☃),JS 中\u2603"会得到 ☃UTF-16 是一种编码方式:把 Unicode 码点编码成 16 位一组的单元(码元 code unit)。
- 如果码点在
U+0000 ~ U+FFFF(叫 BMP,基本多文种平面),UTF-16 用 1 个 16 位码元 就能表示。- 如果码点更大(比如很多 emoji 在
U+10000以上),UTF-16 要用 2 个 16 位码元(代理对 surrogate pair)来表示。
中 (U+4E2D)→ UTF-16:1 个码元
😀 (U+1F600)→ UTF-16:2 个码元
"😀".length常常是 2:因为length数的是 UTF-16 码元,不是你眼睛看到的“字符个数”。
JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成 \uxxxx 的形式,其中 xxxx 代表该字符的 Unicode 码点。比如,\u00A9 代表版权符号。
解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示,输出给用户的时候,所有字符都会转成字面形式。
var f\u006F\u006F = 'abc';
foo // "abc"在代码中,"\u2603" 叫 Unicode 转义序列,表示“码点/码元对应的字符”。
关键点:
- 老形式:
\uXXXX只能写 4 位十六进制,只能覆盖U+0000 ~ U+FFFF(BMP)。 - ES6 新形式:
\u{...}叫 码点转义(code point escaping),可以写到0x10FFFF,所以能直接写增补平面字符,比如:\u{1D11E}(𝄞)。
在 ES6 之前如果你想写 BMP 外字符,只能用 代理对 两段拼起来:"\uD834\uDD1E" 才能得到同一个 𝄞。
Unicode 也可以用于代码混淆,叫 Unicode escape / string escape 形式的混淆,把源码里原本的可读字符(字母、关键字、字符串内容等)替换成一堆 \uXXXX / \xNN 这种转义序列,看起来像“全变成 Unicode 了”。
console.log("hi");
// 等价于
\u0063\u006F\u006E\u0073\u006F\u006C\u0065.\u006C\u006F\u0067("\u0068\u0069");JavaScript 的字符串 基于 Unicode 语义,但 内部和许多旧 API 按 UTF-16 的 16 位码元(2 字节)工作。
可以理解为:Unicode 负责“这个字符是什么(编号是多少)”,UTF-16 负责“JS 字符串把它怎么存、很多老 API 怎么数”。
UTF-16 的两种情况:
码点在 U+0000 ~ U+FFFF(BMP) → 使用 1 个 UTF-16 码元(16 位 / 2 字节)
码点在 U+10000 ~ U+10FFFF(增补平面)→ 使用 2 个 UTF-16 码元(代理对,共 32 位 / 4 字节)
- 高代理:
0xD800 ~ 0xDBFF - 低代理:
0xDC00 ~ 0xDFFF
𝌆 的 Unicode 码点是 U+1D306,UTF-16 表示为:0xD834 0xDF06
JavaScript 并不是不支持 UTF-16,而是 JS 把“字符串长度、索引、charAt、charCodeAt”等概念,固定定义为“16 位码元”级别。
原因是:JS 诞生时,Unicode 只到 U+FFFF,当时 1 个字符 = 2 字节 是成立的,后来 Unicode 扩展了,但 JS 的字符串模型已经定型
'𝌆'的 length 是 2,JS 能显示这个字符,但在字符串 API 眼里,它是 两个码元 ≈ 两个“字符单位”。
所以:JavaScript 的 length 不是“字符数”,而是 UTF-16 码元数
对于 U+10000 ~ U+10FFFF 的字符 JS 永远认为它们“长度是 2”
Base64 转码
早期计算机通信里,数据 ≠ 文字,ASCII 最早定义了 0~127:
32 ~ 126:可打印字符(字母、数字、标点)
0 ~ 31:控制字符,比如:换行、回车、响铃、暂停传输,设计给“设备/协议用的”,不是给人看的
这些字符在屏幕上 没有形状,放进文本里容易 破坏格式或传输协议,所以它们叫:不可打印字符。
ASCII 本质是 “7 位字符编码表”(0~127),它的时代背景是:英文世界,电传机 / 终端,极端省空间,所以只能表示英文,没有中文、emoji,但 非常稳定、所有系统都认识
Base64 是一种“把任意二进制数据,安全地伪装成纯文本”的编码方式,将 任意二进制数据 转换成由 0–9、A–Z、a–z、+、/ 这 64 个可打印字符 组成的文本。
它的目标只有一个:避免出现“特殊字符”,让数据能顺利通过“只认文本”的通道,不是加密,也不压缩。
二进制数据里可能包含控制字符,它们会破坏文本协议,Base64 可以把这些数据“伪装成安全文本”
很多地方 只能传文本,不能传原始二进制,比如:JSON,HTML,URL,HTTP header,但你偏偏想传图片,文件,压缩包,音频,加密后的字节流,这些都是 二进制数据。
解决方案:把二进制 → Base64 → 文本 → 传输 → 再还原
任何数据都可以都能转 Base64 ,任意数据(字节) → Base64 → 可打印字符,所以完全可以 PDF → Base64,PDF → Base64,ZIP → Base64,data:image/png;base64,... 就是典型例子
JS 的 btoa / atob 不支持中文,因为 它们是“按字节(Latin1 / ASCII)工作的”,不是按 Unicode 字符工作的,JS 字符串是 Unicode,btoa 只接受 0~255 的字节序列,中文字符早就超出这个范围
btoa() // ASCII / Latin1 → Base64
atob() // Base64 → ASCII / Latin1JS 中正确处理中文的方式
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}对象
引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说 指向同一个内存地址,修改其中一个变量,会影响到其他所有变量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2如果取消某一个变量对于原对象的引用,不会影响到另一个变量
var o1 = {};
var o2 = o1;
o1 = 1;
o2 // {}也就是说需要分清 是在改同一个对象,还是把变量指向了另一个值。
o1.a = 1:修改对象本身(mutate),对象的引用不变o1 = 1:替换变量指向(rebind),o1不再指向原对象
o1 = {} 这种“直接赋值一个新对象”本质就是把 o1 的引用换掉了
删除
delete 命令用于删除对象的属性,删除成功后返回 true
delete obj.p // true注意:删除一个不存在的属性,delete 不报错,而且返回 true
还有一种情况,delete 命令会返回 false,那就是该属性存在,且不得删除
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false注意:delete 命令只能删除对象本身的属性,无法删除继承的属性,即使 delete 返回 true,该属性依然可能读取到值
也就是说 delete 只影响“自己那层”,原型链上的属性是删不到的,除非你去原型对象上删
in
in 运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回 true,否则返回 false。
它的左边是一个字符串,表示属性名,右边是一个对象。
注意:它不能识别哪些属性是对象自身的,哪些属性是继承的,可以使用对象的 hasOwnProperty 方法判断是否为对象自身的属性
for...in 循环用来遍历一个对象的全部属性,它遍历的是对象所有 可遍历(enumerable)的属性,会跳过不可遍历的属性,它不仅遍历对象自身的属性,还遍历继承的属性。
const proto = { p: 1 };
Object.defineProperty(proto, 'hidden', { value: 2, enumerable: false });
const obj = Object.create(proto);
obj.a = 10;
for (const k in obj) console.log(k);
// a
// p ← 原型上的、且 enumerable: true 的也出来了
// hidden 不会出来(因为 enumerable: false)用 for...in,但过滤自有属性
for (const k in obj) {
if (Object.hasOwn(obj, k)) {
// 只处理自有属性
}
}Object.hasOwn(obj, prop) (推荐) ES2022 新增的静态方法,不走原型链方法调用,更稳,不会被对象里同名属性“覆盖/污染”
obj.hasOwnProperty(prop)(老方法)是从 Object.prototype 继承来的方法,可能会被覆盖,导致报错或结果不可靠:
const obj = { hasOwnProperty: 123, x: 1 };
obj.hasOwnProperty("x"); // TypeError: obj.hasOwnProperty is not a function所以更稳的老写法是:
Object.prototype.hasOwnProperty.call(obj, "x")函数
声明函数的方式可以是 Function 构造函数
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}可以传递任意数量的参数给 Function 构造函数,只有最后一个参数会被当做函数体,如果只有一个参数,该参数就是函数体。
JS 在执行代码前会先做一轮“创建阶段”(建立作用域里的绑定),这轮里不光有变量绑定(var),还有 函数声明绑定,所以就出现了“函数提升”。
foo(); // 能调用
function foo() {
console.log("hi");
}原因:function foo(){} 这种 函数声明 在创建阶段就把 foo 绑定好了,绑定的值就是函数本体。
但是函数表达式不会像上面那样提升
bar(); // TypeError: bar is not a function
var bar = function () {};var bar 这个“变量名”会提升(初始值是 undefined),但 = function(){} 这个赋值不会提升到前面执行,所以调用时 bar 还是 undefined。
箭头函数本身没有“函数声明提升”,因为箭头函数几乎总是以 函数表达式 的形式出现(赋值给变量/属性),而不是 function foo(){} 那种“函数声明”。
函数的 name 属性返回函数的名字,函数的 length 属性返回函数预期传入的参数个数,即函数定义之中的参数个数,函数的 toString() 方法返回一个字符串,内容是函数的源码。
对于原生的函数,toString() 方法返回 function (){[native code]}。
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数,这就是 arguments 对象的由来,arguments 对象包含了函数运行时的所有参数,arguments[0] 就是第一个参数,以此类推,这个对象只有在函数体内部,才可以使用
注意:虽然 arguments 很像数组,但它是一个对象。数组专有的方法(比如 slice 和 forEach),不能在 arguments 对象上直接使用。
可以将 arguments 转为真正的数组:
const args = Array.prototype.slice.call(arguments);arguments 对象带有一个 callee 属性,返回它所对应的原函数,可以通过 arguments.callee,达到调用函数自身的目的,但这个属性在严格模式里面是禁用的,因此不建议使用。
闭包
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999可以把闭包简单理解成“定义在一个函数内部的函数”,
闭包最大的特点,就是它可以“记住”诞生的环境,比如 f2 记住了它诞生的环境 f1,所以从 f2 可以得到 f1 的内部变量,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取外层函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
闭包使得内部变量记住上一次调用时的运算结果:
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7start 是函数 createIncrementor 的内部变量。通过闭包,start 的状态被保留了,每一次调用都是在上一次调用的基础上进行计算,从中可以看到,闭包 inc 使得函数 createIncrementor 的内部环境,一直存在,所以,闭包可以看作是函数内部作用域的一个接口。
闭包能够返回外层函数的内部变量,原因是闭包(上例的 inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。
只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。
注意:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大,因此不能滥用闭包,否则会造成网页的性能问题。
闭包用完需要进行释放,核心原则就是 让引用链断掉:不再有任何变量/数据结构指向那个闭包函数对象,这样它捕获的那套环境也就变成“不可达”,GC 才能回收。
最常见做法,是把持有它的引用置空,比如 inc = null;,这会让闭包函数对象不可达(前提:没有别的地方也引用它),它携带的环境(start)也会随之可回收(GC 何时回收不确定,但条件满足了)。
数组
本质上,数组属于一种特殊的对象,typeof 运算符会返回数组的类型是 object。
清空数组的一个有效方法,就是将 length 属性设为 0
for...in 在遍历数组的时候 不仅会遍历数组所有的数字键,还会遍历非数字键。
var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo如果一个对象的所有键名都是正整数或零,并且有 length 属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function典型的“类似数组的对象”是函数的 arguments 对象,以及大多数 DOM 元素集,还有字符串。
// arguments 对象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM 元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false
// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false数组的 slice 方法可以将“类似数组的对象”变成真正的数组
var arr = Array.prototype.slice.call(arrayLike);“类似数组的对象”还有一个办法可以使用数组的方法,就是通过 call() 把数组的方法放到对象上面
const arrayLike = {
0: 'Tony',
1: 'Jeney',
2: 'Katy',
length: '3'
}
Array.prototype.forEach.call(arrayLike, ((item, index) => {
console.log(item + " ~ " + index);
}));
/*
Tony ~ 0
Jeney ~ 1
Katy ~ 2
*/字符串也是类似数组的对象,所以也可以用 Array.prototype.forEach.call 遍历。
Array.prototype.forEach.call('abc', ((item,index) => {
console.log(item + " ~ " + index);
}))
/*
a ~ 0
b ~ 1
c ~ 2
*/注意:这种方法比直接使用数组原生的 forEach 要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的 forEach 方法
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c运算
如果运算的是对象,必须先转成原始类型的值,然后再相加
var obj = { p: 1 };
obj + 2 // "[object Object] 2"对象 obj 转成原始类型的值是 [object Object]
对象转成原始类型的值会自动调用对象的 valueOf 方法,对象的 valueOf 方法总是返回对象自身,这时再自动调用对象的 toString 方法,而对象的 toString 方法默认返回 [object Object]
obj.valueOf().toString() // "[object Object]"字符串按照字典顺序进行比较,'cat' > 'dog' // false,JavaScript 引擎内部首先比较首字符的 Unicode 码点,如果相等,再比较第二个字符的 Unicode 码点,以此类推,所以:如果两个操作数最终都是字符串 → 按字符编码逐个比较
两个对象 用 == 比的是“是不是同一个引用”,不是比它们转字符串/转数字后的内容
严格相等 对于原始类型(primitive)是比“值”,原始类型:number / string / boolean / bigint / symbol / null / undefined
引用类型(object,包括数组、函数、普通对象、Date、Map、Set…)是比“引用身份”(是不是同一个对象)
({a:1}) === ({a:1}) // false (长得一样,但不是同一个对象)
[] === [] // false
(function(){}) === (function(){}) // false
const x = {a:1};
const y = x;
x === y // true (同一个引用)JS 虽然数值内部是 64 位浮点数,但 做位运算时会把操作数转成“32 位带符号整数”再算,结果也会是 32 位带符号整数。
JS 的“数值存储”是 IEEE-754 double(64 位), 但 位运算 时,JS 会临时把数值 强制转换成 32 位整数 再计算,存的是 64 位,算的时候进了 32 位小黑屋。
变成 32 位是因为 ECMAScript 规范明确规定:所有位运算符,在运算前都会对操作数执行 ToInt32(或 ToUint32)转换,这是一个 历史包袱 + 性能折中 的问题。
如果真的需要 64 位位运算,可以使用 BigInt(现代、正确)
但是,只在位运算那一挂(以及少数“借用位运算做技巧”的写法)才会强制进 32 位 int,/ * % + - 这些 普通算术运算,全程都在 IEEE-754 double(64 位浮点) 里算,不会自动“缩水成 32 位”。
转换
Number 函数将字符串转为数值,要比 parseInt 函数严格很多,基本上,只要有一个字符无法转成数值,整个字符串就会被转为 NaN。
String 函数可以将任意类型的值转化成字符串,参数如果是对象,返回一个类型字符串 "[object Object]",如果是数组,返回该数组的字符串形式。
String 方法背后的转换规则,与 Number 方法基本相同,只是互换了 valueOf 方法和 toString 方法的执行顺序。
Boolean() 函数可以将任意类型的值转为布尔值。
错误
JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。
JavaScript 原生提供 Error 构造函数,所有抛出的错误都是这个构造函数的实例。
var err = new Error('出错了');
err.message // "出错了"生成一个实例对象 err。Error() 构造函数接受一个参数,表示错误提示
抛出(throw)Error 实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。
语言标准提到 Error 实例对象必须有 message 属性,大多数 JavaScript 引擎,对 Error 实例还提供 name 和 stack 属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有。
Error 实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他 6 种错误对象,也就是说,存在 Error 的 6 个派生对象。
SyntaxError 对象是解析代码时发生的语法错误
ReferenceError 对象是引用一个不存在的变量时发生的错误
RangeError 对象是一个值超出有效范围时发生的错误,主要有几种情况,一是数组长度为负数,二是 Number 对象的方法参数超出范围,以及函数堆栈超过最大值。
TypeError 对象是变量或参数不是预期类型时发生的错误。
URIError 对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及 encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape() 和 unescape() 这六个函数。
eval 函数没有被正确执行时,会抛出 EvalError 错误。
除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象:
function UserError(message) {
this.message = message || '默认信息';
this.name = 'UserError';
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
new UserError('这是自定义的错误!');但这是“老派”自定义错误(函数构造器 + 手动挂原型),现在更推荐用 class extends Error
class UserError extends Error {
constructor(message = '默认信息', options) {
super(message, options); // message 会写到 this.message
this.name = 'UserError';
}
}
throw new UserError('参数不合法');throw 语句的作用是手动中断程序执行,抛出一个错误,throw 抛出的错误就是它的参数,通常是一个 Error 对象的实例
try...catch 结构允许在最后添加一个 finally 代码块,表示不管是否出现错误,都必需在最后运行的语句。
try {
} catch (error) {
} finally {
}console
console 支持格式占位符,方法将依次用后面的参数替换占位符,然后再进行输出。
console.log(' %s + %s = %s', 1, 1, 2)
// 1 + 1 = 2console.log 方法的第一个参数有三个占位符(%s),第二、三、四个参数会在显示时,依次替换掉这个三个占位符。
console.log 方法支持以下占位符,不同类型的数据必须使用对应的占位符。
%s字符串%d整数%i整数%f浮点数%o对象的链接%cCSS 格式字符串
使用 %c 占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染。
console.log(
'%cThis text is styled!',
'color: red; background: yellow; font-size: 24px;'
)%c 可以区分区域,靠“分段 + 多个 %c”来做的,即 每出现一次 %c,后面就要跟一个对应的 CSS 字符串参数;样式会从那个 %c 开始作用到下一段文字(直到下一个 %c 改掉样式)。
console.log('%c蓝色字体%c红色字体',' color: blue; font-size: 20px;','color: red; font-size: 16px;');小技巧:
const parts = [
['ERROR', 'color:red;font-weight:bold;'],
[': ', ''],
['user not found', 'color:orange;'],
];
console.log(
parts.map(() => '%c%s').join(''),
...parts.flatMap(([text, style]) => [style, text])
);console 对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义 console.log 方法。
['log', 'info', 'warn', 'error'].forEach(function(method) {
console[method] = console[method].bind(
console,
new Date().toISOString()
);
});
console.log("出错了!");
// 2014-05-18T09:00.000Z 出错了!对于某些复合类型的数据,console.table 方法可以将其转为表格显示。
var languages = [
{ name: "JavaScript", fileExtension: ".js" },
{ name: "TypeScript", fileExtension: ".ts" },
{ name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);count 方法用于计数,输出它被调用了多少次,该方法可以接受一个字符串作为参数,作为标签,对执行次数进行分类。
console.dir 用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示。
console.log({f1: 'foo', f2: 'bar'})
// Object {f1: "foo", f2: "bar"}
console.dir({f1: 'foo', f2: 'bar'})
// Object
// f1: "foo"
// f2: "bar"
// __proto__: Object该方法对于 DOM 输出,会显示 DOM 对象的所有属性:console.dir(document.body)
console.dirxml 主要用于以目录树的形式,显示 DOM 节点,如果参数不是 DOM 节点,而是普通的 JavaScript 对象,console.dirxml 等同于 console.dir。
console.assert 方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。
它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为 false,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果。
console.assert(false, '判断条件不成立')
// Assertion failed: 判断条件不成立
// 相当于
try {
if (!false) {
throw new Error('判断条件不成立');
}
} catch(e) {
console.error(e);
}console.time(),console.timeEnd() 这两个方法用于计时,可以算出一个操作所花费的准确时间。
console.time('Array initialize');
var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
array[i] = new Object();
};
console.timeEnd('Array initialize');
// Array initialize: 1914.481mstime 方法表示计时开始,timeEnd 方法表示计时结束,它们的参数是计时器的名称,调用 timeEnd 方法之后,控制台会显示“计时器名称: 所耗费的时间”。
console.group 和 console.groupEnd 这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。
console.group('一级分组');
console.log('一级分组的内容');
console.group('二级分组');
console.log('二级分组的内容');
console.groupEnd(); // 二级分组结束
console.groupEnd(); // 一级分组结束console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。
console.trace方法显示当前执行的代码在堆栈中的调用路径。
console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear方法将不起作用。