开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。
红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否
单例内置对象:
ECMA-262对内置对象的定义是“任何由ECMAScript实现提供、与宿主环境无关,并在ECMAScript程序开始执行时就存在的对象”。这就意味着,开发者不用显式地实例化内置对象,因为它们已经实例化好了。前面我们已经接触了大部分内置对象,包括Object、Array和String。
本节介绍ECMA-262定义的另外两个单例内置对象:Global和Math。
Global:
Global对象是ECMAScript中最特别的对象,因为代码不会显式地访问它。ECMA-262规定Global对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。
事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性。本书前面介绍的函数,包括isNaN()、isFinite()、parseInt()和parseFloat(),实际上都是Global对象的方法。除了这些,Global对象上还有另外一些方法。
URI编码方法:
encodeURI() 与 encodeURIComponent():
两个方法都用于对 URI 进行编码,用 UTF-8 编码替换无效字符,使浏览器能正确处理。区别在于编码的范围不同:
| 方法 | 用途 | 编码范围 |
|---|---|---|
encodeURI() | 编码整个 URI | 不编码 URI 保留字符(:、/、?、# 等) |
encodeURIComponent() | 编码 URI 中的某个组件 | 编码所有非字母数字字符 |
1
2
3
4
5
6
7
let uri = "http://www.wiley.com/illegal value.js#start";
encodeURI(uri); // "http://www.wiley.com/illegal%20value.js#start"
// 只有空格被编码为 %20,其余保留字符不变
encodeURIComponent(uri); // "http%3A%2F%2Fwww.wiley.com%2Fillegal%20value.js%23start"
// 所有非字母字符都被编码
encodeURI() 适合处理完整的 URL,而 encodeURIComponent() 适合拼接查询参数等场景:
1
2
let query = "illegal value.js";
let url = "http://www.wiley.com/" + encodeURIComponent(query); // 只编码参数部分
实际开发中
encodeURIComponent()使用频率更高,因为编码查询参数的场景远多于编码整个 URL。
decodeURI() 与 decodeURIComponent():
与编码方法对应,用于还原被编码的字符串。它们和编码方法一一对应:
decodeURI()只解码encodeURI()编码过的字符(不会解码:、/、#等)decodeURIComponent()解码encodeURIComponent()编码过的所有字符
1
2
3
4
5
6
7
let uri = "http%3A%2F%2Fwww.wiley.com%2Fillegal%20value.js%23start";
decodeURI(uri); // "http%3A%2F%2Fwww.wiley.com%2Fillegal value.js%23start"
// 只还原了 %20 → 空格
decodeURIComponent(uri); // "http://www.wiley.com/illegal value.js#start"
// 全部还原
废弃方法escape() 与 unescape():
escape() 和 unescape() 在 ECMAScript 第 3 版就已经被废弃,被 URI 方法取代。原因在于 URI 方法能正确编码所有 Unicode 字符,而 escape() 只能正确处理 ASCII 字符。
不要在生产代码中使用
escape()和unescape(),始终使用 URI 编码方法。
eval()方法:
eval() 是 ECMAScript 中最强大的方法——它本身就是一个完整的 JavaScript 解释器,接收一个字符串参数,将其解释为实际代码并执行:
1
eval("console.log('hi')");
上面这行代码的功能与下面这行完全等价:
1
console.log("hi");
当解释器遇到 eval() 调用时,会把参数字符串当作真正的 JavaScript 语句插入到那个位置,然后立即执行。
作用域与上下文:
eval() 中执行的代码与调用所在的上下文拥有相同的作用域链。这意味着外部定义的变量可以在 eval() 内部被引用:
1
2
let msg = "hello world";
eval("console.log(msg)"); // "hello world"
变量 msg 是在 eval() 外部定义的,但 console.log() 能访问它,因为第二行代码会被替换成真正的函数调用代码。
反过来,eval() 内部定义的变量和函数也可以在外部使用:
1
2
3
4
5
eval("function sayHi() { console.log('hi'); }");
sayHi(); // "hi"
eval("let msg = 'hello world';");
console.log(msg); // "hello world"
这是因为 eval() 会把字符串内容直接插入到调用位置,就像在那行手写入一段代码一样。
通过 eval() 创建的变量和函数不会被提升,因为解析阶段它们还只是字符串,直到 eval() 真正执行时才会被创建:
1
2
3
// 下面两行等价:
eval("let msg = 'hello'");
let msg = 'hello';
严格模式下的限制:
1
2
3
4
5
6
"use strict";
eval("let msg = 'hello'");
console.log(msg); // ReferenceError ← eval 内部定义的变量无法从外部访问
eval = "hi"; // SyntaxError ← 严格模式下禁止给 eval 赋值
严格模式下,eval() 内部创建的变量和函数被隔离在它自己的作用域中,无法影响外部。同样,在严格模式下,赋值给eval也会导致错误。
eval() 的能力非常强大,但也非常危险。执行用户输入的内容时尤其危险,恶意用户可能注入代码导致网站崩溃或引发 XSS 攻击。
实际开发中几乎所有
eval()的场景都有更安全的替代方案,应尽量避免使用。
Global对象属性:
Global对象有很多属性。像undefined、NaN和Infinity等特殊值都是Global对象的属性。此外,所有原生引用类型构造函数,比如Object和Function,也都是Global对象的属性。下表列出了所有这些属性。
window对象:
ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器通过 window 对象作为 Global 对象的代理,所有全局变量和函数都会成为 window 的属性:
1
2
3
4
5
6
7
var color = "red";
function sayColor() {
console.log(window.color);
}
window.sayColor(); // "red"
注意:用
let或const声明的全局变量不会成为window的属性,只有var声明的才会。
另一种获取 Global 对象的方式是通过立即调用函数表达式(IIFE)返回 this:
1
2
3
let global = function() {
return this;
}();
原理:当函数在没有明确指定 this 值的情况下执行(既不是某个对象的方法,也没有通过 call() / apply() 调用),this 就指向 Global 对象。所以一个简单返回 this 的 IIFE 可以在任何执行上下文中获取 Global 对象。
IIFE = 把函数包在括号里 + 末尾加
(),定义即执行,常用来创建独立作用域。
在严格模式下,
this不再默认指向 Global 对象,此时返回undefined。
Math:
ECMAScript提供了Math对象作为保存数学公式、信息和计算的地方。Math对象提供了一些辅助计算的属性和方法。
Math对象上提供的计算要比直接在JavaScript中实现的快得多,因为Math对象上的计算使用了JavaScript引擎中更高效的实现和处理器指令。但使用Math计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。
Math对象属性:
Math对象有一些属性,主要用于保存数学中的一些特殊值。
这些值的含义和用法超出了本书的范畴,但都是ECMAScript规范定义的,并可以在需要时使用。
min()和max()方法:
min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数,如下面的例子所示:
1
2
3
4
5
let max = Math.max(3, 54, 32, 16);
console.log(max); // 54
let min = Math.min(3, 54, 32, 16);
console.log(min); // 3
要知道数组中的最大值或最小值,可以像下面这样使用扩展操作符:
1
2
let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...values);
舍入方法:
4 个方法用于将小数值舍入为整数:
| 方法 | 舍入方式 |
|---|---|
Math.ceil() | 始终向上舍入 |
Math.floor() | 始终向下舍入 |
Math.round() | 四舍五入 |
Math.fround() | 返回最接近的单精度(32 位)浮点值 |
1
2
3
4
5
6
7
8
9
10
11
Math.ceil(25.1); // 26 ← 25.x 一律向上
Math.ceil(25.5); // 26
Math.ceil(25.9); // 26
Math.floor(25.1); // 25 ← 25.x 一律向下
Math.floor(25.5); // 25
Math.floor(25.9); // 25
Math.round(25.1); // 25 ← < 25.5 舍去
Math.round(25.5); // 26 ← ≥ 25.5 进位
Math.round(25.9); // 26
Math.fround() 比较特殊,它不做整数舍入,而是将数值转换为 32 位单精度浮点数,精度比 JavaScript 默认的 64 位双精度低,因此可能出现精度偏差:
1
2
Math.fround(0.4); // 0.4000000059604645 ← 精度丢失
Math.fround(0.5); // 0.5 ← 恰好能精确表示
Math.round() 最常用,因为日常开发中大部分场景都是做普通的四舍五入,比如分页、金额、展示数量等。
Math.floor() 排第二,常用于取索引(下标从 0 开始)、向下取整的场景。
Math.ceil() 排第三,用于”向上取整”的场景,比如分页计算(Math.ceil(total / pageSize))。
Math.fround() 基本用不到,那是 WebGL 等底层场景才会用的。
random()方法:
返回一个 0~1 之间的随机小数,包含 0 但不包含 1。
从整数范围中随机选一个数的公式:
1
Math.floor(Math.random() * 可选总数 + 最小值)
因为 Math.random() 返回的是小数,乘以任何数都还是小数,所以要配合 Math.floor() 向下取整。
1
2
3
4
5
// 从 1~10 中选一个
let num = Math.floor(Math.random() * 10 + 1); // 10是可选总数,1是最小值
// 从 2~10 中选一个
let num = Math.floor(Math.random() * 9 + 2); // 9是可选总数(10-2+1),2是最小值
此外,也可以把方法封装成函数,每次都要算可选总数容易出错,封装一下更方便:
1
2
3
4
5
6
function selectFrom(lowerValue, upperValue) {
let choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
let num = selectFrom(2, 10); // 2~10,包含2和10
配合 selectFrom 从数组中随机挑选一个数组元素:
1
2
let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
let color = colors[selectFrom(0, colors.length - 1)];
Math.random()不适合用于加密场景,因为它产生的随机数可预测。如果需要真正的密码学安全随机数,应使用window.crypto.getRandomValues()。
其他方法:
Math对象还有很多涉及各种简单或高阶数运算的方法。讨论每种方法的具体细节或者它们的适用场景超出了本书的范畴。仅在下表简单总结。
即便这些方法都是由ECMA-262定义的,对正弦、余弦、正切等计算的实现仍然取决于浏览器,因为计算这些值的方式有很多种。结果,这些方法的精度可能因实现而异。


