# JavaScript 相关问题
# ES6
# 变量的解构赋值
# 函数参数的解构赋值
// 函数参数的解构也可以使用默认值
// move 函数的参数是对象,通过对这个对象解构,得到变量x和y的值。若解构失败,x和y就等于默认值
function move({x: 0, y: 0} = {}) {
console.log(x, y)
}
move({x: 3, y: 8}) // 3, 8
move({x: 3}) // 3, 0
move() // 0, 0
// 为函数的参数指定默认值,并非为变量x和y指定默认值
function move({x, y} = {x: 0, y: 0}) {
console.log(x, y)
}
move({x: 3, y: 8}) // 3, 8
move({x: 3}) // 3, undefined
move({}) // undefined, undefined
move() // 0, 0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 用途
- 交换变量的值 [x, y] = [y, x]
let x = 1
let y = 2
;[x, y] = [y, x]
2
3
4
- 从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3]
}
let [a, b, c] = example()
// 返回一个对象
function example = {
return {
foo: 1,
bar: 2
}
}
let { foo, bar } = example()
2
3
4
5
6
7
8
9
10
11
12
13
14
- 函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
2
3
4
5
6
7
- 提取 JSON 数据
let jsonData = {
id: 42,
status: 'OK',
data: [867, 5309]
}
let { id, status, data: number } = jsonData
console.log(id, status, number)
// 42, "OK", [867, 5309]
2
3
4
5
6
7
8
9
10
- 函数参数的默认值
jQuery.ajax = function (
url,
{
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true
// ... more config
} = {}
) {
// ... do stuff
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 遍历 Map 结构
const map = new Map()
map.set('first', 'hello')
map.set('second', 'world')
for (let [key, value] of map) {
console.log(key, value)
}
// first, hello
// second, world
// 遍历 key
for (let [key] of map) {
console.log(key)
}
// 遍历 value
for (let [, value] of map) {
console.log(value)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 输入模块的指定方法
const { sourceMap, souceNode } = require('xxx-lib')
# 字符串的扩展
字符的 Unicode 表示法
字符串的遍历器接口
TIP
ES6 为字符串提供了遍历器接口, 字符串可以被 for ... of 遍历。
遍历器可以识别大于 0xFFFF 的码点,传统的 for 循环无法识别这样的码点。
直接输入 U+2028 和 U+2029
JavaScript 字符串允许直接输入字符,以及字符的转义形式
WARNING
JavaScript 规定有 5 个字符, 不能在字符串里直接使用,只能使用转义形式 即字符串里不能直接包含反斜杠,一定要转义要写成 \ 或者 \u005c
- U + 005C: 反斜杠(reverse solidus)
- U + 000D: 回车(carrige return)
- U + 2028: 行分隔符(line separator)
- U + 2029: 段分隔符(paragraph separator)
- U + 000A: 换行符(line feed)
ES2019 允许直接在字符串里输入 U+2028(行分隔符)和 U+2029(段分隔符)
const PS = eval("'\u2029'")
1WARNING
模板字符串现在允许直接输入 U + 2028: 行分隔符(line separator)和 U + 2029: 段分隔符(paragraph separator)这两个字符。
正则表达式不允许直接输入这两个字符。
JSON.stringify() 的改造
TIP
为了确保 JSON.stringify() 返回的是合法的 UTF-8 字符, ES2019 改变了其行为。
如果遇到 0xD800 和 0xDFFF 之间的单码点,或者不存在的配对形式,它会返回转义字符,留给应用自己处理接下来如何处理
JSON.stringify('\u{D834}') // ""\\uD834"" JSON.stringify('\uDF06\uD834') // ""\\udf06\\d834""
1
2模板字符串
模板字符串是增强的字符串,用反引号(`)标识 它可以当作普通字符串使用, 也可以用来定义多行字符串 或者在字符串中嵌入变量
实例:模板编译
标签模板
函数调用的一种特殊方式。“标签”指的就是函数,紧跟在后边的模板字符串就是参数
let a = 5 let b = 10 tag`Hello ${a + b} World ${a * b}` // 等同于 tag(['Hello ', ' World', ''], 15, 50)
1
2
3
4
5WARNING
模板处理函数的第一个参数还有一个 raw 属性
console.log`123` // ["123", raw: Array[1]]
1TIP
应用
- 过滤 HTML 字符串,防止用户输入恶意内容
- 多语言处理(国际化处理)
- 在 js 中嵌入 其他语言(通过 jsx 函数将 DOM 字符串转换为 React 对象)
模板字符串的限制
WARNING
模板字符串默认会将字符产 转义,导致无法嵌入其他语言。
# 字符串新增方法
String.fromCodePoint()
TIP
用于从 Unicode 码点返回对应的字符串
解决了 String.fromCharCode() 不能识别大于 0xFFFF 的码点问题
String.raw()
TIP
返回一个斜杠(即斜杠前边再加一个斜杠)都被转义的字符串。
通常用于模板字符串的处理。本质是一个正常的函数,只是专用于模板字符的 标签函数。
若写成函数形式,它的第一个参数应该是一个具有 raw 属性的对象,且 raw 属性的值应该是一个数组,对应模板字符串解析后的值。
作为函数,String.raw() 的代码实现基本如下
String.raw = function (strings, ...values) { let output = '' let index for (index = 0; index < values.length; index++) { output += strings.raw[index] + values[index] } output += strings.raw[index] return output }
1
2
3
4
5
6
7
8
9
10
11实例方法:codePointAt()
TIP
解决了 String.charCodeAt() 无法处理 4 字节存储的字符。
codePointAt() 方法返回的是十进制的值,可以使用 toString() 转换。
使用 for...of 或者 扩展运算符(...)可以正确识别 32 位的 UTF-16 的字符。
测试一个字符是由两个字节还是四个字节组成的最简单方法
function is32Bit(c) = { return c.codePointAt(0).toString(16) > 0xFFFF }
1
2
3实例方法:normalize()
用来将字符的不同表示方法统一为同样的形式。
WARNING
normalize() 目前不能识别三个或三个以上的字符的合成。这种情况下,只能使用正则表达式,通过 Unicode 编码区间判断。
实例方法:includes(), startsWith(), endsWith()
用来判断一个字符串是否包含在另一个字符串中。ES5 提供了 indexOf()方法
TIP
includes():返回布尔值,表示是否找到了参数字符串
startsWith():返回布尔值,表示参数字符串是否在原字符串头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
都支持第二个参数 n。includes()和 startsWith()的第二个参数表示从 n 到字符串结束。endsWith 表示针对前 n 个字符串
实例方法:repeat()
返回一个字符串,表示将字符串重复 n 次。
实例方法:padStart(), padEnd()
实例方法:trimStart(), trimEnd()
实例方法:matchAll()
实例方法:replaceAll()
实例方法:at()
实例方法:toWellFormed()
# 事件循环
事件循环又叫消息循环,是浏览器渲染主线程的工作方式
在 Chrome 源码中,它开启了一个不会结束的 for 循环,每次循环会从 消息队列中取出第一个任务执行,而其他线程只需要在合适的时间将任务加入到队尾。
过去把消息队列简单的分为 宏任务队列 和 微任务队列。这种说法目前无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列中,不同类型的任务可以属于不同的队列。
不同任务队列有不同的优先级(任务没有优先级,任务队列有优先级),在一次事件循环中,由浏览器自行决定取哪一个队列的任务。 例如任务队列有微任务队列、延时队列、交互队列等
但是,浏览器必须有一个
微队列
, 微队列的任务一定具有做高的优先级,必须优先调度执行。
# 任务具有优先级吗?
任务没有优先级,在消息对列中先进先出。但是消息队列是有优先级的。 根据 W3C 官方的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在同一个队列中
- 不同类型的任务可以分属于不同的队列
- 浏览器必须准备好一个微队列,微队列中的任务优先于其他任务执行。链接 (opens new window)
在目前的 chrome 的实现中,至少包含下面的队列:
延时队列:用于存放计时器达到后返回的回调任务。优先级【中】
交互队列:用于存放用户操作后产生的事件处理任务。优先级【高】
微队列:用户存放需要最快执行的任务, 优先级【最高】
添加任务到微队列的主要方式是使用 Promise、MutationObserver
例如
// 把一个函数添加到微任务队列 Promsie.resovle().then(fn)
1
2
# JS 中的计时器能做到精确计时吗?
不行。
- 计算机没有原子钟 (opens new window),无法做到精确计时
- 操作系统过的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的计时函数,也就携带了偏差
- 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的时间,这样在计时时间少于 4 毫秒时又带来了偏差
- 受事件循环的影响,计时器的回调函数只能在渲染主线程 空闲时运行,因此又带来了偏差
# 什么是异步?
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务:setTimeout、setInterval
- 网络通信完成后需要执行的任务:XHR、Fetch
- 用户操作后需要处理的任务:addEventListener
如果让渲染主线程等待这些任务的时机到达,就会导致主线程长期处于“阻塞”状态,从而导致浏览器“卡死”。
渲染主线程承担着极其重要的工作,无论如何都不能阻塞! 因此浏览器选择异步来解决这个问题。
使用异步的方式,浏览器主线程永不阻塞。
# 如何理解 JS 的异步
JS 是一门单线程语言,这是因为它 运行在浏览器的渲染主线程中,而渲染主线程只有一个。
而渲染主线程承担着诸多工作:解析html、css、布局、分层、js执行等任务。
如果使用 同步 的方式,就极有可能导致主线程产生 阻塞,从而导致消息队列中的其他任务无法执行。这样一来,一方面会导致繁忙的主线程白白消耗时间,另一方面导致页面无法及时更新,给用户造成 卡死 现象。
所以浏览器采用异步的方式来避免。
具体做法就是当某些任务发生时,例如计时器、网络、时间监听,主线程就会把任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,则将事先传递的回调函数 包装成 任务。加入到消息队列的队尾,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。