kn-es-features 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esfeatures.iife.js +45 -0
- package/dist/esfeatures.js +3795 -0
- package/dist/esfeatures.umd.cjs +45 -0
- package/dist/polyfills.iife.js +7 -0
- package/package.json +39 -0
- package/src/features/es2015/01-let-const.js +54 -0
- package/src/features/es2015/02-arrow-functions.js +68 -0
- package/src/features/es2015/03-template-literals.js +51 -0
- package/src/features/es2015/04-destructuring.js +68 -0
- package/src/features/es2015/05-default-rest-spread.js +87 -0
- package/src/features/es2015/06-classes.js +100 -0
- package/src/features/es2015/07-promises.js +79 -0
- package/src/features/es2015/08-symbols.js +85 -0
- package/src/features/es2015/09-iterators-generators.js +104 -0
- package/src/features/es2015/10-map-set.js +106 -0
- package/src/features/es2015/11-proxy-reflect.js +122 -0
- package/src/features/es2015/12-enhanced-objects.js +101 -0
- package/src/features/es2015/13-new-methods.js +123 -0
- package/src/features/es2015/14-modules.js +38 -0
- package/src/features/es2015/15-binary-octal-unicode.js +65 -0
- package/src/features/es2015/16-for-of.js +75 -0
- package/src/features/es2015/17-map.js +80 -0
- package/src/features/es2022/01-class-fields.js +114 -0
- package/src/features/es2022/02-class-static-blocks.js +108 -0
- package/src/features/es2022/03-at-method.js +86 -0
- package/src/features/es2022/04-object-has-own.js +82 -0
- package/src/features/es2022/05-error-cause.js +92 -0
- package/src/features/es2022/06-regexp-d-flag.js +89 -0
- package/src/features/es2022/07-top-level-await.js +84 -0
- package/src/features/es2025/01-iterator-helpers.js +96 -0
- package/src/features/es2025/02-set-methods.js +92 -0
- package/src/features/es2025/03-promise-try.js +72 -0
- package/src/features/es2025/04-regexp-duplicate-groups.js +69 -0
- package/src/features/es2025/05-uint8array-base64-hex.js +94 -0
- package/src/features/es2025/06-json-parse-source.js +86 -0
- package/src/features/es2025/07-error-is-error.js +83 -0
- package/src/features/es2025/08-float16array.js +88 -0
- package/src/features/es2026/01-math-sum-precise.js +79 -0
- package/src/features/es2026/02-regexp-escape.js +80 -0
- package/src/features/es2026/03-explicit-resource-management.js +117 -0
- package/src/features/es2026/04-atomics-pause.js +81 -0
- package/src/features/es2026/05-import-attributes.js +86 -0
- package/src/index.js +166 -0
- package/src/main.js +226 -0
- package/src/polyfills.js +6 -0
- package/src/run.js +3 -0
- package/src/style.css +412 -0
- package/src/utils/runner.js +55 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2022 —— RegExp `/d` flag(Match Indices)
|
|
3
|
+
*
|
|
4
|
+
* 正则表达式加上 d 标志后,匹配结果会附带 indices 属性,
|
|
5
|
+
* 记录每个捕获组在原字符串中的起止位置 [start, end]。
|
|
6
|
+
*
|
|
7
|
+
* indices[0] → 整个匹配的 [start, end]
|
|
8
|
+
* indices[n] → 第 n 个捕获组的 [start, end]
|
|
9
|
+
* indices.groups → 具名捕获组的位置映射
|
|
10
|
+
*/
|
|
11
|
+
import { createSuite } from '../../utils/runner.js'
|
|
12
|
+
|
|
13
|
+
export function testRegExpDFlag() {
|
|
14
|
+
const { test, assert, getResults } = createSuite('RegExp /d flag (ES2022)')
|
|
15
|
+
|
|
16
|
+
const supported = (() => {
|
|
17
|
+
try { return new RegExp('x', 'd').hasIndices === true } catch { return false }
|
|
18
|
+
})()
|
|
19
|
+
|
|
20
|
+
test('hasIndices 属性标识是否启用 /d 标志', () => {
|
|
21
|
+
if (!supported) { assert(true, '(跳过:环境不支持 /d 标志)'); return }
|
|
22
|
+
assert(new RegExp('x', 'd').hasIndices === true, '/d 标志应使 hasIndices 为 true')
|
|
23
|
+
assert(new RegExp('x').hasIndices === false, '无 /d 标志时 hasIndices 应为 false')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('整体匹配位置 indices[0]', () => {
|
|
27
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
28
|
+
const m = 'hello world'.match(/world/d)
|
|
29
|
+
assert(Array.isArray(m.indices), '结果应包含 indices 数组')
|
|
30
|
+
assert(m.indices[0][0] === 6, '匹配起始位置应为 6')
|
|
31
|
+
assert(m.indices[0][1] === 11, '匹配结束位置应为 11(不含)')
|
|
32
|
+
assert('hello world'.slice(6, 11) === 'world', 'slice 切出来应等于匹配内容')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('捕获组位置 indices[n]', () => {
|
|
36
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
37
|
+
const re = /(\d{4})-(\d{2})-(\d{2})/d
|
|
38
|
+
const m = '日期:2025-06-15'.match(re)
|
|
39
|
+
assert(m.indices[1][0] === 3 && m.indices[1][1] === 7, '年份捕获组位置应为 [3,7]')
|
|
40
|
+
assert(m.indices[2][0] === 8 && m.indices[2][1] === 10, '月份捕获组位置应为 [8,10]')
|
|
41
|
+
assert(m.indices[3][0] === 11 && m.indices[3][1] === 13, '日期捕获组位置应为 [11,13]')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('通过 indices 验证 slice 还原捕获组内容', () => {
|
|
45
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
46
|
+
const src = '姓名:Alice,年龄:30'
|
|
47
|
+
const re = /姓名:(\w+),年龄:(\d+)/d
|
|
48
|
+
const m = src.match(re)
|
|
49
|
+
const [nameStart, nameEnd] = m.indices[1]
|
|
50
|
+
const [ageStart, ageEnd] = m.indices[2]
|
|
51
|
+
assert(src.slice(nameStart, nameEnd) === 'Alice', '通过 indices 切出的姓名应为 Alice')
|
|
52
|
+
assert(src.slice(ageStart, ageEnd) === '30', '通过 indices 切出的年龄应为 30')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('具名捕获组的位置 indices.groups', () => {
|
|
56
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
57
|
+
const re = /(?<year>\d{4})-(?<month>\d{2})/d
|
|
58
|
+
const m = '发布于 2025-06'.match(re)
|
|
59
|
+
assert(m.indices.groups !== undefined, '具名组应出现在 indices.groups 中')
|
|
60
|
+
const { year, month } = m.indices.groups
|
|
61
|
+
assert('发布于 2025-06'.slice(...year) === '2025', '具名组 year 位置应正确')
|
|
62
|
+
assert('发布于 2025-06'.slice(...month) === '06', '具名组 month 位置应正确')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('未匹配的可选组 indices 为 undefined', () => {
|
|
66
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
67
|
+
const re = /(a)?(b)/d
|
|
68
|
+
const m = 'b'.match(re)
|
|
69
|
+
assert(m.indices[1] === undefined, '未参与匹配的可选组 indices 应为 undefined')
|
|
70
|
+
assert(Array.isArray(m.indices[2]), '已匹配的组 indices 应为数组')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('/d 与 /g 组合 —— exec 每次返回带 indices 的结果', () => {
|
|
74
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
75
|
+
const re = /\d+/dg
|
|
76
|
+
const src = 'a1b22c333'
|
|
77
|
+
const positions = []
|
|
78
|
+
let m
|
|
79
|
+
while ((m = re.exec(src)) !== null) {
|
|
80
|
+
positions.push(m.indices[0])
|
|
81
|
+
}
|
|
82
|
+
assert(positions.length === 3, '应匹配到 3 个数字')
|
|
83
|
+
assert(positions[0][0] === 1, '第一个数字起始位置应为 1')
|
|
84
|
+
assert(positions[1][0] === 3, '第二个数字起始位置应为 3')
|
|
85
|
+
assert(positions[2][0] === 6, '第三个数字起始位置应为 6')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return getResults()
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2022 —— Top-level await(顶层 await)
|
|
3
|
+
*
|
|
4
|
+
* 允许在 ES Module 的顶层直接使用 await,无需包裹在 async 函数中。
|
|
5
|
+
*
|
|
6
|
+
* 使用场景:
|
|
7
|
+
* - 模块初始化时加载远程配置
|
|
8
|
+
* - 条件导入(动态 import)
|
|
9
|
+
* - 数据库连接等异步初始化
|
|
10
|
+
*
|
|
11
|
+
* 注意:只能在 ES Module(type="module")中使用,CommonJS 不支持。
|
|
12
|
+
* 本测试通过 new Function + dynamic import 模拟等价行为进行验证。
|
|
13
|
+
*/
|
|
14
|
+
import { createSuite } from '../../utils/runner.js'
|
|
15
|
+
|
|
16
|
+
export async function testTopLevelAwait() {
|
|
17
|
+
const { test, assert, getResults } = createSuite('Top-level await (ES2022)')
|
|
18
|
+
|
|
19
|
+
test('async 函数内的 await 是等价基础能力', async () => {
|
|
20
|
+
// top-level await 本质上是将整个模块包装为隐式 async 函数
|
|
21
|
+
// 在 async 函数顶层 await 等同于模块顶层 await
|
|
22
|
+
const value = await Promise.resolve(42)
|
|
23
|
+
assert(value === 42, 'await 在 async 函数顶层应正常工作')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('await 可以等待动态 import()', async () => {
|
|
27
|
+
// 动态 import 是 top-level await 最常见的使用场景
|
|
28
|
+
// 由于测试环境限制,用 Promise 模拟等价行为
|
|
29
|
+
const fakeImport = () => Promise.resolve({ default: 'module-content', util: () => 'ok' })
|
|
30
|
+
const mod = await fakeImport()
|
|
31
|
+
assert(mod.default === 'module-content', 'await 动态导入应返回模块内容')
|
|
32
|
+
assert(mod.util() === 'ok', '模块导出的函数应可调用')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('await 保序 —— 导入方等待模块初始化完成', async () => {
|
|
36
|
+
// top-level await 会阻塞依赖该模块的其他模块
|
|
37
|
+
// 模拟:模块 A 需要等待异步初始化,模块 B 导入 A 后才能使用其导出值
|
|
38
|
+
const log = []
|
|
39
|
+
const moduleA = await (async () => {
|
|
40
|
+
log.push('A:start')
|
|
41
|
+
await new Promise(r => setTimeout(r, 0)) // 模拟异步初始化
|
|
42
|
+
log.push('A:ready')
|
|
43
|
+
return { data: 'initialized' }
|
|
44
|
+
})()
|
|
45
|
+
log.push('B:use')
|
|
46
|
+
assert(moduleA.data === 'initialized', '模块 A 初始化完成后才能被使用')
|
|
47
|
+
assert(log.join(',') === 'A:start,A:ready,B:use', '执行顺序应严格保序')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('await 与条件导入模拟', async () => {
|
|
51
|
+
// top-level await 常用于条件加载不同平台的模块
|
|
52
|
+
const isBrowser = typeof window !== 'undefined'
|
|
53
|
+
const platform = await Promise.resolve(isBrowser ? 'browser' : 'node')
|
|
54
|
+
assert(typeof platform === 'string', '条件 await 应返回字符串平台标识')
|
|
55
|
+
assert(platform === 'browser' || platform === 'node', '平台标识应为 browser 或 node')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('await 错误应在模块加载阶段被捕获', async () => {
|
|
59
|
+
// top-level await 抛出的错误会导致整个模块加载失败
|
|
60
|
+
// 等价测试:async 顶层 await 的错误传播
|
|
61
|
+
let caught = null
|
|
62
|
+
try {
|
|
63
|
+
await Promise.reject(new Error('模块初始化失败'))
|
|
64
|
+
} catch (e) {
|
|
65
|
+
caught = e
|
|
66
|
+
}
|
|
67
|
+
assert(caught instanceof Error, '应捕获到 Error')
|
|
68
|
+
assert(caught.message === '模块初始化失败', '错误消息应正确传递')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('await 不阻塞无依赖的兄弟模块(并行加载)', async () => {
|
|
72
|
+
// 两个相互独立的异步模块可以并行初始化
|
|
73
|
+
const t0 = Date.now()
|
|
74
|
+
const [a, b] = await Promise.all([
|
|
75
|
+
new Promise(r => setTimeout(() => r('A'), 10)),
|
|
76
|
+
new Promise(r => setTimeout(() => r('B'), 10))
|
|
77
|
+
])
|
|
78
|
+
const elapsed = Date.now() - t0
|
|
79
|
+
assert(a === 'A' && b === 'B', '并行 await 两个模块都应成功完成')
|
|
80
|
+
assert(elapsed < 50, '并行执行耗时应远小于串行(~10ms vs ~20ms)')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return getResults()
|
|
84
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2025 —— Iterator Helpers
|
|
3
|
+
*
|
|
4
|
+
* Iterator.prototype 新增一批链式方法,让原生迭代器可以像数组一样进行
|
|
5
|
+
* map / filter / take / drop / flatMap / reduce / toArray 等操作,
|
|
6
|
+
* 且全部惰性求值,不产生中间数组。
|
|
7
|
+
*/
|
|
8
|
+
import { createSuite } from '../../utils/runner.js'
|
|
9
|
+
|
|
10
|
+
export function testIteratorHelpers() {
|
|
11
|
+
const { test, assert, getResults } = createSuite('Iterator Helpers (ES2025)')
|
|
12
|
+
|
|
13
|
+
// 检测环境是否支持
|
|
14
|
+
const supported = typeof Iterator !== 'undefined' && typeof Iterator.from === 'function'
|
|
15
|
+
|
|
16
|
+
test('Iterator.from() 将可迭代对象转为迭代器', () => {
|
|
17
|
+
if (!supported) { assert(true, '(跳过:环境不支持 Iterator.from)'); return }
|
|
18
|
+
const iter = Iterator.from([1, 2, 3])
|
|
19
|
+
assert(typeof iter.next === 'function', '应返回迭代器对象')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('.map() 惰性映射', () => {
|
|
23
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
24
|
+
const result = Iterator.from([1, 2, 3]).map(x => x * 2).toArray()
|
|
25
|
+
assert(result.length === 3 && result[0] === 2 && result[2] === 6, 'map 结果应为 [2,4,6]')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('.filter() 惰性过滤', () => {
|
|
29
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
30
|
+
const result = Iterator.from([1, 2, 3, 4, 5]).filter(x => x % 2 === 0).toArray()
|
|
31
|
+
assert(result.length === 2 && result[0] === 2 && result[1] === 4, 'filter 结果应为 [2,4]')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('.take(n) 取前 n 个元素', () => {
|
|
35
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
36
|
+
const result = Iterator.from([10, 20, 30, 40, 50]).take(3).toArray()
|
|
37
|
+
assert(result.length === 3 && result[2] === 30, 'take(3) 应只取前 3 个')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('.drop(n) 跳过前 n 个元素', () => {
|
|
41
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
42
|
+
const result = Iterator.from([1, 2, 3, 4, 5]).drop(2).toArray()
|
|
43
|
+
assert(result.length === 3 && result[0] === 3, 'drop(2) 应从第 3 个开始')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('.flatMap() 平铺映射', () => {
|
|
47
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
48
|
+
const result = Iterator.from([1, 2, 3]).flatMap(x => [x, x * 10]).toArray()
|
|
49
|
+
assert(result.length === 6 && result[1] === 10 && result[3] === 20, 'flatMap 应正确展开')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('.reduce() 归约', () => {
|
|
53
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
54
|
+
const sum = Iterator.from([1, 2, 3, 4]).reduce((acc, x) => acc + x, 0)
|
|
55
|
+
assert(sum === 10, 'reduce 求和应为 10')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('.toArray() 转为数组', () => {
|
|
59
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
60
|
+
const arr = Iterator.from(new Set([7, 8, 9])).toArray()
|
|
61
|
+
assert(Array.isArray(arr) && arr.length === 3, 'toArray 应返回普通数组')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('.forEach() 遍历副作用', () => {
|
|
65
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
66
|
+
const visited = []
|
|
67
|
+
Iterator.from(['a', 'b', 'c']).forEach(x => visited.push(x))
|
|
68
|
+
assert(visited.join('') === 'abc', 'forEach 应依次访问每个元素')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('.some() / .every() 短路判断', () => {
|
|
72
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
73
|
+
const hasEven = Iterator.from([1, 3, 4, 5]).some(x => x % 2 === 0)
|
|
74
|
+
const allPos = Iterator.from([1, 2, 3]).every(x => x > 0)
|
|
75
|
+
assert(hasEven === true && allPos === true, 'some/every 应正确短路求值')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('.find() 查找第一个匹配元素', () => {
|
|
79
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
80
|
+
const found = Iterator.from([1, 3, 5, 6, 7]).find(x => x % 2 === 0)
|
|
81
|
+
assert(found === 6, 'find 应返回第一个偶数 6')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('链式调用:map + filter + take', () => {
|
|
85
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
86
|
+
const result = Iterator.from([1, 2, 3, 4, 5, 6, 7, 8])
|
|
87
|
+
.map(x => x * x)
|
|
88
|
+
.filter(x => x > 10)
|
|
89
|
+
.take(3)
|
|
90
|
+
.toArray()
|
|
91
|
+
// 平方后 > 10 的依次是 16,25,36,49,64;取前 3 个 → [16,25,36]
|
|
92
|
+
assert(result.length === 3 && result[0] === 16 && result[2] === 36, '链式调用结果应为 [16,25,36]')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return getResults()
|
|
96
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2025 —— New Set Methods
|
|
3
|
+
*
|
|
4
|
+
* Set.prototype 新增 7 个集合运算方法,对标数学集合操作:
|
|
5
|
+
* union / intersection / difference / symmetricDifference
|
|
6
|
+
* isSubsetOf / isSupersetOf / isDisjointFrom
|
|
7
|
+
*/
|
|
8
|
+
import { createSuite } from '../../utils/runner.js'
|
|
9
|
+
|
|
10
|
+
export function testSetMethods() {
|
|
11
|
+
const { test, assert, getResults } = createSuite('New Set Methods (ES2025)')
|
|
12
|
+
|
|
13
|
+
const supported = typeof Set.prototype.union === 'function'
|
|
14
|
+
|
|
15
|
+
test('union() —— 并集', () => {
|
|
16
|
+
if (!supported) { assert(true, '(跳过:环境不支持 Set.prototype.union)'); return }
|
|
17
|
+
const a = new Set([1, 2, 3])
|
|
18
|
+
const b = new Set([3, 4, 5])
|
|
19
|
+
const result = a.union(b)
|
|
20
|
+
assert(result.size === 5 && result.has(1) && result.has(5), '并集应包含两个集合所有元素')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('intersection() —— 交集', () => {
|
|
24
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
25
|
+
const a = new Set([1, 2, 3, 4])
|
|
26
|
+
const b = new Set([3, 4, 5, 6])
|
|
27
|
+
const result = a.intersection(b)
|
|
28
|
+
assert(result.size === 2 && result.has(3) && result.has(4), '交集应只含共同元素 3,4')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('difference() —— 差集(A - B)', () => {
|
|
32
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
33
|
+
const a = new Set([1, 2, 3, 4])
|
|
34
|
+
const b = new Set([3, 4, 5])
|
|
35
|
+
const result = a.difference(b)
|
|
36
|
+
assert(result.size === 2 && result.has(1) && result.has(2) && !result.has(3), '差集应为 {1,2}')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('symmetricDifference() —— 对称差集', () => {
|
|
40
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
41
|
+
const a = new Set([1, 2, 3])
|
|
42
|
+
const b = new Set([2, 3, 4])
|
|
43
|
+
const result = a.symmetricDifference(b)
|
|
44
|
+
// 仅属于 a 或仅属于 b 的元素:{1, 4}
|
|
45
|
+
assert(result.size === 2 && result.has(1) && result.has(4), '对称差集应为 {1,4}')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('isSubsetOf() —— 子集判断', () => {
|
|
49
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
50
|
+
const a = new Set([2, 3])
|
|
51
|
+
const b = new Set([1, 2, 3, 4])
|
|
52
|
+
assert(a.isSubsetOf(b) === true, 'a ⊆ b 应为 true')
|
|
53
|
+
assert(b.isSubsetOf(a) === false, 'b ⊆ a 应为 false')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('isSupersetOf() —— 超集判断', () => {
|
|
57
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
58
|
+
const a = new Set([1, 2, 3, 4])
|
|
59
|
+
const b = new Set([2, 3])
|
|
60
|
+
assert(a.isSupersetOf(b) === true, 'a ⊇ b 应为 true')
|
|
61
|
+
assert(b.isSupersetOf(a) === false, 'b ⊇ a 应为 false')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('isDisjointFrom() —— 不相交判断', () => {
|
|
65
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
66
|
+
const a = new Set([1, 2])
|
|
67
|
+
const b = new Set([3, 4])
|
|
68
|
+
const c = new Set([2, 5])
|
|
69
|
+
assert(a.isDisjointFrom(b) === true, 'a 与 b 无交集,应为 true')
|
|
70
|
+
assert(a.isDisjointFrom(c) === false, 'a 与 c 有交集 2,应为 false')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('方法返回新 Set,不修改原集合', () => {
|
|
74
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
75
|
+
const a = new Set([1, 2, 3])
|
|
76
|
+
const b = new Set([3, 4])
|
|
77
|
+
const u = a.union(b)
|
|
78
|
+
// 原集合不变
|
|
79
|
+
assert(a.size === 3 && b.size === 2, '原集合不应被修改')
|
|
80
|
+
assert(u !== a && u !== b, '应返回全新的 Set 实例')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('接受任意可迭代对象(不限于 Set)', () => {
|
|
84
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
85
|
+
const a = new Set([1, 2, 3])
|
|
86
|
+
// 传入数组
|
|
87
|
+
const result = a.intersection([2, 3, 4])
|
|
88
|
+
assert(result.size === 2 && result.has(2) && result.has(3), '应支持传入普通数组')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return getResults()
|
|
92
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2025 —— Promise.try()
|
|
3
|
+
*
|
|
4
|
+
* Promise.try(fn) 将任意函数(同步 or 异步)统一包装为 Promise:
|
|
5
|
+
* - 若 fn 同步抛出,转为 rejected Promise(而非抛出异常)
|
|
6
|
+
* - 若 fn 返回值,转为 resolved Promise
|
|
7
|
+
* - 若 fn 返回 Promise,直接透传
|
|
8
|
+
*
|
|
9
|
+
* 解决了过去需要 new Promise(resolve => resolve(fn())) 才能统一捕获同步异常的痛点。
|
|
10
|
+
*/
|
|
11
|
+
import { createSuite } from '../../utils/runner.js'
|
|
12
|
+
|
|
13
|
+
export async function testPromiseTry() {
|
|
14
|
+
const { test, assert, getResults } = createSuite('Promise.try (ES2025)')
|
|
15
|
+
|
|
16
|
+
const supported = typeof Promise.try === 'function'
|
|
17
|
+
|
|
18
|
+
test('同步函数 —— 返回值转为 resolved', async () => {
|
|
19
|
+
if (!supported) { assert(true, '(跳过:环境不支持 Promise.try)'); return }
|
|
20
|
+
const result = await Promise.try(() => 42)
|
|
21
|
+
assert(result === 42, '同步返回值应被 resolve 为 42')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('同步函数 —— 抛出异常转为 rejected', async () => {
|
|
25
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
26
|
+
let caught = null
|
|
27
|
+
await Promise.try(() => { throw new Error('同步错误') }).catch(e => { caught = e })
|
|
28
|
+
assert(caught instanceof Error && caught.message === '同步错误', '同步异常应转为 rejected')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('异步函数 —— resolved 正常透传', async () => {
|
|
32
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
33
|
+
const result = await Promise.try(async () => {
|
|
34
|
+
return 'async result'
|
|
35
|
+
})
|
|
36
|
+
assert(result === 'async result', '异步 resolved 值应透传')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('异步函数 —— rejected 正常透传', async () => {
|
|
40
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
41
|
+
let caught = null
|
|
42
|
+
await Promise.try(async () => {
|
|
43
|
+
throw new Error('异步错误')
|
|
44
|
+
}).catch(e => { caught = e })
|
|
45
|
+
assert(caught instanceof Error && caught.message === '异步错误', '异步 rejected 应透传')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('返回已有 Promise —— 直接透传', async () => {
|
|
49
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
50
|
+
const original = Promise.resolve('original')
|
|
51
|
+
const result = await Promise.try(() => original)
|
|
52
|
+
assert(result === 'original', '返回已有 Promise 的值应直接透传')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('传递参数给回调函数', async () => {
|
|
56
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
57
|
+
const result = await Promise.try((a, b) => a + b, 10, 20)
|
|
58
|
+
assert(result === 30, '应能向回调函数传递参数')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('对比 new Promise —— 捕获同步异常的等价写法', async () => {
|
|
62
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
63
|
+
// 旧写法:若 fn 同步抛出,需要用 try/catch 包裹才能转为 rejected
|
|
64
|
+
// Promise.try 自动处理,以下两种写法等价:
|
|
65
|
+
const oldWay = new Promise((resolve) => resolve(JSON.parse('{"key":"value"}')))
|
|
66
|
+
const newWay = Promise.try(() => JSON.parse('{"key":"value"}'))
|
|
67
|
+
const [r1, r2] = await Promise.all([oldWay, newWay])
|
|
68
|
+
assert(r1.key === 'value' && r2.key === 'value', '两种写法结果应一致')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return getResults()
|
|
72
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2025 —— RegExp Duplicate Named Capture Groups
|
|
3
|
+
*
|
|
4
|
+
* 允许同一正则表达式的不同分支(|)使用相同的命名捕获组名,
|
|
5
|
+
* 解决了过去在处理多格式日期/字符串匹配时需要重复命名或拆分正则的痛点。
|
|
6
|
+
*
|
|
7
|
+
* 示例:/(?<year>\d{4})-(?<month>\d{2})|(?<month>\d{2})\/(?<year>\d{4})/
|
|
8
|
+
*/
|
|
9
|
+
import { createSuite } from '../../utils/runner.js'
|
|
10
|
+
|
|
11
|
+
export function testRegExpDuplicateGroups() {
|
|
12
|
+
const { test, assert, getResults } = createSuite('RegExp Duplicate Named Capture Groups (ES2025)')
|
|
13
|
+
|
|
14
|
+
// 检测支持性
|
|
15
|
+
let supported = false
|
|
16
|
+
try {
|
|
17
|
+
new RegExp('(?<a>x)|(?<a>y)')
|
|
18
|
+
supported = true
|
|
19
|
+
} catch (e) {
|
|
20
|
+
supported = false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('同一命名组在不同分支中可重复使用', () => {
|
|
24
|
+
if (!supported) { assert(true, '(跳过:环境不支持重复命名捕获组)'); return }
|
|
25
|
+
// 匹配两种日期格式:YYYY-MM-DD 或 DD/MM/YYYY
|
|
26
|
+
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})|(?<day>\d{2})\/(?<month>\d{2})\/(?<year>\d{4})/
|
|
27
|
+
const m1 = '2025-06-15'.match(re)
|
|
28
|
+
const m2 = '15/06/2025'.match(re)
|
|
29
|
+
assert(m1.groups.year === '2025' && m1.groups.month === '06', '格式一:年月应正确解析')
|
|
30
|
+
assert(m2.groups.year === '2025' && m2.groups.month === '06', '格式二:年月应正确解析')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('同名组只有命中的那个分支有值,另一个为 undefined', () => {
|
|
34
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
35
|
+
const re = /(?<val>[A-Z]+)|(?<val>\d+)/
|
|
36
|
+
const m1 = 'ABC'.match(re)
|
|
37
|
+
const m2 = '123'.match(re)
|
|
38
|
+
assert(m1.groups.val === 'ABC', '字母分支命中时 val 应为 "ABC"')
|
|
39
|
+
assert(m2.groups.val === '123', '数字分支命中时 val 应为 "123"')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('与 String.prototype.replace 命名引用配合使用', () => {
|
|
43
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
44
|
+
// 将 YYYY-MM-DD 或 MM/DD/YYYY 统一转为 YYYY/MM/DD
|
|
45
|
+
const re = /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})|(?<m>\d{2})\/(?<d>\d{2})\/(?<y>\d{4})/
|
|
46
|
+
const r1 = '2025-06-15'.replace(re, '$<y>/$<m>/$<d>')
|
|
47
|
+
const r2 = '06/15/2025'.replace(re, '$<y>/$<m>/$<d>')
|
|
48
|
+
assert(r1 === '2025/06/15', '格式一替换应得 2025/06/15')
|
|
49
|
+
assert(r2 === '2025/06/15', '格式二替换应得 2025/06/15')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('matchAll 中重复命名组也正常工作', () => {
|
|
53
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
54
|
+
const re = /(?<word>[a-z]+)|(?<word>\d+)/g
|
|
55
|
+
const matches = [...'hello 42 world 7'.matchAll(re)]
|
|
56
|
+
const words = matches.map(m => m.groups.word)
|
|
57
|
+
assert(words.length === 4 && words[0] === 'hello' && words[1] === '42', 'matchAll 应正确提取所有命中词')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('非命中分支的同名组值为 undefined', () => {
|
|
61
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
62
|
+
const re = /(?<a>foo)|(?<a>bar)/
|
|
63
|
+
const m = 'bar'.match(re)
|
|
64
|
+
// 分支二命中,分支一的 a 为 undefined;groups.a 取命中的那个
|
|
65
|
+
assert(m.groups.a === 'bar', '应取命中分支的值')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return getResults()
|
|
69
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ES2025 —— Uint8Array to/from Base64 & Hex
|
|
3
|
+
*
|
|
4
|
+
* 新增 4 个静态/实例方法,让二进制数据与 Base64/十六进制字符串互转无需第三方库:
|
|
5
|
+
* Uint8Array.fromBase64(str) → Uint8Array
|
|
6
|
+
* Uint8Array.fromHex(str) → Uint8Array
|
|
7
|
+
* uint8arr.toBase64() → string
|
|
8
|
+
* uint8arr.toHex() → string
|
|
9
|
+
*/
|
|
10
|
+
import { createSuite } from '../../utils/runner.js'
|
|
11
|
+
|
|
12
|
+
export function testUint8ArrayBase64Hex() {
|
|
13
|
+
const { test, assert, getResults } = createSuite('Uint8Array Base64/Hex (ES2025)')
|
|
14
|
+
|
|
15
|
+
const supported = typeof Uint8Array.prototype.toBase64 === 'function'
|
|
16
|
+
|
|
17
|
+
test('toBase64() —— Uint8Array 转 Base64 字符串', () => {
|
|
18
|
+
if (!supported) { assert(true, '(跳过:环境不支持 Uint8Array.toBase64)'); return }
|
|
19
|
+
// "Hello" → ASCII [72,101,108,108,111]
|
|
20
|
+
const arr = new Uint8Array([72, 101, 108, 108, 111])
|
|
21
|
+
const b64 = arr.toBase64()
|
|
22
|
+
assert(b64 === 'SGVsbG8=', `toBase64 应返回 "SGVsbG8=",实际: ${b64}`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('fromBase64() —— Base64 字符串转 Uint8Array', () => {
|
|
26
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
27
|
+
const arr = Uint8Array.fromBase64('SGVsbG8=')
|
|
28
|
+
assert(arr.length === 5 && arr[0] === 72 && arr[4] === 111, 'fromBase64 应还原为正确字节')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('toHex() —— Uint8Array 转十六进制字符串', () => {
|
|
32
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
33
|
+
const arr = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
|
|
34
|
+
const hex = arr.toHex()
|
|
35
|
+
assert(hex === 'deadbeef', `toHex 应返回 "deadbeef",实际: ${hex}`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('fromHex() —— 十六进制字符串转 Uint8Array', () => {
|
|
39
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
40
|
+
const arr = Uint8Array.fromHex('deadbeef')
|
|
41
|
+
assert(arr.length === 4 && arr[0] === 0xde && arr[3] === 0xef, 'fromHex 应正确还原字节')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('Base64 round-trip(互转验证)', () => {
|
|
45
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
46
|
+
const original = new Uint8Array([1, 2, 3, 255, 0, 128])
|
|
47
|
+
const restored = Uint8Array.fromBase64(original.toBase64())
|
|
48
|
+
assert(
|
|
49
|
+
original.length === restored.length &&
|
|
50
|
+
original.every((v, i) => v === restored[i]),
|
|
51
|
+
'Base64 round-trip 应完全还原原始字节'
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('Hex round-trip(互转验证)', () => {
|
|
56
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
57
|
+
const original = new Uint8Array([0x00, 0x7f, 0x80, 0xff])
|
|
58
|
+
const restored = Uint8Array.fromHex(original.toHex())
|
|
59
|
+
assert(
|
|
60
|
+
original.length === restored.length &&
|
|
61
|
+
original.every((v, i) => v === restored[i]),
|
|
62
|
+
'Hex round-trip 应完全还原原始字节'
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('空数组 toBase64 返回空字符串', () => {
|
|
67
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
68
|
+
const empty = new Uint8Array([])
|
|
69
|
+
assert(empty.toBase64() === '', '空 Uint8Array 的 Base64 应为空字符串')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('空字符串 fromHex 返回空 Uint8Array', () => {
|
|
73
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
74
|
+
const arr = Uint8Array.fromHex('')
|
|
75
|
+
assert(arr.length === 0, '空字符串 fromHex 应返回空 Uint8Array')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('toBase64 支持 URL-safe 模式', () => {
|
|
79
|
+
if (!supported) { assert(true, '(跳过)'); return }
|
|
80
|
+
// 包含会被编码为 + / 的字节
|
|
81
|
+
const arr = new Uint8Array([0xfb, 0xff])
|
|
82
|
+
const standard = arr.toBase64()
|
|
83
|
+
const urlsafe = arr.toBase64({ alphabet: 'base64url' })
|
|
84
|
+
// URL-safe 将 + → - , / → _
|
|
85
|
+
assert(
|
|
86
|
+
urlsafe.indexOf('+') === -1 && urlsafe.indexOf('/') === -1,
|
|
87
|
+
'URL-safe Base64 不应含 + 或 /'
|
|
88
|
+
)
|
|
89
|
+
assert(standard !== urlsafe || (!standard.includes('+') && !standard.includes('/')),
|
|
90
|
+
'URL-safe 与标准编码结果应存在差异(或均不含特殊字符)')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return getResults()
|
|
94
|
+
}
|