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.
Files changed (48) hide show
  1. package/dist/esfeatures.iife.js +45 -0
  2. package/dist/esfeatures.js +3795 -0
  3. package/dist/esfeatures.umd.cjs +45 -0
  4. package/dist/polyfills.iife.js +7 -0
  5. package/package.json +39 -0
  6. package/src/features/es2015/01-let-const.js +54 -0
  7. package/src/features/es2015/02-arrow-functions.js +68 -0
  8. package/src/features/es2015/03-template-literals.js +51 -0
  9. package/src/features/es2015/04-destructuring.js +68 -0
  10. package/src/features/es2015/05-default-rest-spread.js +87 -0
  11. package/src/features/es2015/06-classes.js +100 -0
  12. package/src/features/es2015/07-promises.js +79 -0
  13. package/src/features/es2015/08-symbols.js +85 -0
  14. package/src/features/es2015/09-iterators-generators.js +104 -0
  15. package/src/features/es2015/10-map-set.js +106 -0
  16. package/src/features/es2015/11-proxy-reflect.js +122 -0
  17. package/src/features/es2015/12-enhanced-objects.js +101 -0
  18. package/src/features/es2015/13-new-methods.js +123 -0
  19. package/src/features/es2015/14-modules.js +38 -0
  20. package/src/features/es2015/15-binary-octal-unicode.js +65 -0
  21. package/src/features/es2015/16-for-of.js +75 -0
  22. package/src/features/es2015/17-map.js +80 -0
  23. package/src/features/es2022/01-class-fields.js +114 -0
  24. package/src/features/es2022/02-class-static-blocks.js +108 -0
  25. package/src/features/es2022/03-at-method.js +86 -0
  26. package/src/features/es2022/04-object-has-own.js +82 -0
  27. package/src/features/es2022/05-error-cause.js +92 -0
  28. package/src/features/es2022/06-regexp-d-flag.js +89 -0
  29. package/src/features/es2022/07-top-level-await.js +84 -0
  30. package/src/features/es2025/01-iterator-helpers.js +96 -0
  31. package/src/features/es2025/02-set-methods.js +92 -0
  32. package/src/features/es2025/03-promise-try.js +72 -0
  33. package/src/features/es2025/04-regexp-duplicate-groups.js +69 -0
  34. package/src/features/es2025/05-uint8array-base64-hex.js +94 -0
  35. package/src/features/es2025/06-json-parse-source.js +86 -0
  36. package/src/features/es2025/07-error-is-error.js +83 -0
  37. package/src/features/es2025/08-float16array.js +88 -0
  38. package/src/features/es2026/01-math-sum-precise.js +79 -0
  39. package/src/features/es2026/02-regexp-escape.js +80 -0
  40. package/src/features/es2026/03-explicit-resource-management.js +117 -0
  41. package/src/features/es2026/04-atomics-pause.js +81 -0
  42. package/src/features/es2026/05-import-attributes.js +86 -0
  43. package/src/index.js +166 -0
  44. package/src/main.js +226 -0
  45. package/src/polyfills.js +6 -0
  46. package/src/run.js +3 -0
  47. package/src/style.css +412 -0
  48. package/src/utils/runner.js +55 -0
@@ -0,0 +1,75 @@
1
+ import { createSuite } from '../../utils/runner.js'
2
+
3
+ export function testForOf() {
4
+ const { test, assert, getResults } = createSuite('for...of 与迭代协议')
5
+
6
+ test('for...of 迭代数组', () => {
7
+ const result = []
8
+ for (const x of [1, 2, 3]) result.push(x)
9
+ assert(result.join(',') === '1,2,3', 'for...of 迭代数组')
10
+ })
11
+
12
+ test('for...of 迭代字符串', () => {
13
+ const chars = []
14
+ for (const c of 'abc') chars.push(c)
15
+ assert(chars.join('') === 'abc', 'for...of 迭代字符串')
16
+ })
17
+
18
+ test('for...of 迭代 Set', () => {
19
+ const result = []
20
+ for (const x of new Set([1, 2, 3])) result.push(x)
21
+ assert(result.join(',') === '1,2,3', 'for...of 迭代 Set')
22
+ })
23
+
24
+ test('for...of 迭代 Map', () => {
25
+ const result = []
26
+ for (const [k, v] of new Map([['a', 1], ['b', 2]])) {
27
+ result.push(`${k}=${v}`)
28
+ }
29
+ assert(result.join(',') === 'a=1,b=2', 'for...of 迭代 Map')
30
+ })
31
+
32
+ test('for...of 迭代 arguments', () => {
33
+ function fn() {
34
+ const result = []
35
+ for (const x of arguments) result.push(x)
36
+ return result
37
+ }
38
+ assert(fn(1, 2, 3).join(',') === '1,2,3', 'for...of 迭代 arguments')
39
+ })
40
+
41
+ test('for...of 与 break / continue', () => {
42
+ const result = []
43
+ for (const x of [1, 2, 3, 4, 5]) {
44
+ if (x === 3) continue
45
+ if (x === 5) break
46
+ result.push(x)
47
+ }
48
+ assert(result.join(',') === '1,2,4', 'for...of 支持 break 和 continue')
49
+ })
50
+
51
+ test('for...of 迭代 NodeList(DOM 环境跳过)', () => {
52
+ // 非 DOM 环境,仅验证迭代协议通用性
53
+ const iterable = {
54
+ [Symbol.iterator]() {
55
+ let n = 0
56
+ return { next() { return n < 3 ? { value: n++, done: false } : { done: true } } }
57
+ }
58
+ }
59
+ const result = []
60
+ for (const x of iterable) result.push(x)
61
+ assert(result.join(',') === '0,1,2', '自定义可迭代对象')
62
+ })
63
+
64
+ test('Array entries / keys / values 迭代', () => {
65
+ const arr = ['a', 'b', 'c']
66
+ const entries = [...arr.entries()]
67
+ const keys = [...arr.keys()]
68
+ const values = [...arr.values()]
69
+ assert(entries[1][0] === 1 && entries[1][1] === 'b', 'entries 迭代')
70
+ assert(keys.join(',') === '0,1,2', 'keys 迭代')
71
+ assert(values.join(',') === 'a,b,c', 'values 迭代')
72
+ })
73
+
74
+ return getResults()
75
+ }
@@ -0,0 +1,80 @@
1
+ import { createSuite } from '../../utils/runner.js'
2
+
3
+ // // Map.prototype.getOrInsert / getOrInsertComputed 是 Stage 4 提案(ES2025)
4
+ // // 旧版浏览器(如 Chrome 40)不支持,注入 polyfill 以演示其行为
5
+ // function ensureGetOrInsert() {
6
+ // if (!Map.prototype.getOrInsert) {
7
+ // Map.prototype.getOrInsert = function (key, defaultValue) {
8
+ // if (!this.has(key)) this.set(key, defaultValue)
9
+ // return this.get(key)
10
+ // }
11
+ // }
12
+ // if (!Map.prototype.getOrInsertComputed) {
13
+ // Map.prototype.getOrInsertComputed = function (key, callbackFn) {
14
+ // if (!this.has(key)) this.set(key, callbackFn(key))
15
+ // return this.get(key)
16
+ // }
17
+ // }
18
+ // }
19
+
20
+ export function testMapGetOrInsert() {
21
+ // ensureGetOrInsert()
22
+
23
+ const { test, assert, getResults } = createSuite('map getOrInsert')
24
+
25
+ test('getOrInsert - key 已存在返回原值', () => {
26
+ const map = new Map([['bar', 'foo']])
27
+ const result = map.getOrInsert('bar', 'default')
28
+ assert(result === 'foo', 'key 已存在应返回原值 foo')
29
+ })
30
+
31
+ test('getOrInsert - key 不存在时插入默认值', () => {
32
+ const map = new Map()
33
+ const result = map.getOrInsert('newKey', 42)
34
+ assert(result === 42, '应返回插入的默认值 42')
35
+ assert(map.get('newKey') === 42, 'key 已被写入 map')
36
+ })
37
+
38
+ test('getOrInsert - 不覆盖已有值', () => {
39
+ const map = new Map([['x', 100]])
40
+ map.getOrInsert('x', 999)
41
+ assert(map.get('x') === 100, '已有值不应被覆盖')
42
+ })
43
+
44
+ test('getOrInsert - 词频统计经典用法', () => {
45
+ const words = ['apple', 'banana', 'apple', 'apple', 'banana']
46
+ const freq = new Map()
47
+ for (const w of words) {
48
+ freq.getOrInsert(w, 0)
49
+ freq.set(w, freq.get(w) + 1)
50
+ }
51
+ assert(freq.get('apple') === 3, 'apple 出现 3 次')
52
+ assert(freq.get('banana') === 2, 'banana 出现 2 次')
53
+ })
54
+
55
+ test('getOrInsertComputed - key 不存在时用回调计算', () => {
56
+ const map = new Map()
57
+ const result = map.getOrInsertComputed('user:1', (k) => ({ id: k, score: 0 }))
58
+ assert(result.id === 'user:1' && result.score === 0, '回调以 key 为参数生成默认值')
59
+ })
60
+
61
+ test('getOrInsertComputed - key 已存在时不调用回调', () => {
62
+ const map = new Map([['x', 'original']])
63
+ let called = false
64
+ const result = map.getOrInsertComputed('x', () => { called = true; return 'new' })
65
+ assert(!called, 'key 已存在,回调不应被调用')
66
+ assert(result === 'original', '应返回原有值')
67
+ })
68
+
69
+ test('getOrInsertComputed - 构建邻接表', () => {
70
+ const edges = [[1, 2], [1, 3], [2, 3]]
71
+ const graph = new Map()
72
+ for (const [from, to] of edges) {
73
+ graph.getOrInsertComputed(from, () => []).push(to)
74
+ }
75
+ assert(graph.get(1).join(',') === '2,3', '节点 1 的邻居为 [2,3]')
76
+ assert(graph.get(2).join(',') === '3', '节点 2 的邻居为 [3]')
77
+ })
78
+
79
+ return getResults()
80
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * ES2022 —— Class Fields
3
+ *
4
+ * 正式将以下语法纳入规范(ES2022 之前是提案):
5
+ * - 公共实例字段(public instance fields)
6
+ * - 私有实例字段(private fields) #field
7
+ * - 私有实例方法(private methods) #method()
8
+ * - 静态公共字段(static fields)
9
+ * - 静态私有字段(static private fields)
10
+ * - 静态私有方法
11
+ * - 私有字段存在性判断(in 操作符)
12
+ */
13
+ import { createSuite } from '../../utils/runner.js'
14
+
15
+ export function testClassFields() {
16
+ const { test, assert, getResults } = createSuite('Class Fields (ES2022)')
17
+
18
+ test('公共实例字段 —— 声明并初始化', () => {
19
+ class Point {
20
+ x = 0
21
+ y = 0
22
+ constructor(x, y) { this.x = x; this.y = y }
23
+ }
24
+ const p = new Point(3, 4)
25
+ assert(p.x === 3 && p.y === 4, '公共字段应可读写')
26
+ })
27
+
28
+ test('公共字段默认值在 constructor 之前初始化', () => {
29
+ class Counter {
30
+ count = 10
31
+ constructor() { this.count += 5 }
32
+ }
33
+ assert(new Counter().count === 15, '默认值应先于 constructor 体初始化')
34
+ })
35
+
36
+ test('私有实例字段 #field —— 外部不可访问', () => {
37
+ class BankAccount {
38
+ #balance = 0
39
+ deposit(n) { this.#balance += n }
40
+ get balance() { return this.#balance }
41
+ }
42
+ const acc = new BankAccount()
43
+ acc.deposit(100)
44
+ assert(acc.balance === 100, '私有字段应通过访问器正确读取')
45
+ assert(!('#balance' in acc), '私有字段不应出现在普通属性枚举中')
46
+
47
+ let threw = false
48
+ try { eval('acc.#balance') } catch (e) { threw = true }
49
+ assert(threw, '外部直接访问私有字段应抛出语法错误')
50
+ })
51
+
52
+ test('私有实例方法 #method()', () => {
53
+ class Formatter {
54
+ #prefix = '[LOG]'
55
+ #wrap(msg) { return `${this.#prefix} ${msg}` }
56
+ format(msg) { return this.#wrap(msg) }
57
+ }
58
+ assert(new Formatter().format('hello') === '[LOG] hello', '私有方法应正常调用')
59
+ })
60
+
61
+ test('静态公共字段', () => {
62
+ class Config {
63
+ static version = '1.0.0'
64
+ static maxRetry = 3
65
+ }
66
+ assert(Config.version === '1.0.0', '静态公共字段应可通过类名访问')
67
+ assert(Config.maxRetry === 3, '静态字段应保持初始值')
68
+ })
69
+
70
+ test('静态私有字段 —— 追踪实例数量', () => {
71
+ class Registry {
72
+ static #count = 0
73
+ constructor() { Registry.#count++ }
74
+ static getCount() { return Registry.#count }
75
+ }
76
+ new Registry(); new Registry(); new Registry()
77
+ assert(Registry.getCount() === 3, '静态私有字段应跨实例共享')
78
+ })
79
+
80
+ test('静态私有方法', () => {
81
+ class MathUtils {
82
+ static #clamp(v, min, max) { return Math.min(Math.max(v, min), max) }
83
+ static normalize(v) { return MathUtils.#clamp(v, 0, 1) }
84
+ }
85
+ assert(MathUtils.normalize(-0.5) === 0, '静态私有方法:低于下界应返回 0')
86
+ assert(MathUtils.normalize(1.5) === 1, '静态私有方法:超出上界应返回 1')
87
+ assert(MathUtils.normalize(0.5) === 0.5, '静态私有方法:正常值应原样返回')
88
+ })
89
+
90
+ test('私有字段存在性:`#field in obj`', () => {
91
+ class Node {
92
+ #value
93
+ constructor(v) { this.#value = v }
94
+ static isNode(obj) { return #value in obj }
95
+ }
96
+ assert(Node.isNode(new Node(1)) === true, '实例应包含私有字段')
97
+ assert(Node.isNode({}) === false, '普通对象不应包含私有字段')
98
+ })
99
+
100
+ test('私有字段不参与继承 —— 子类无法访问父类私有字段', () => {
101
+ class Animal {
102
+ #name
103
+ constructor(name) { this.#name = name }
104
+ getName() { return this.#name }
105
+ }
106
+ class Dog extends Animal {
107
+ bark() { return `${this.getName()} says woof` }
108
+ }
109
+ const d = new Dog('Rex')
110
+ assert(d.bark() === 'Rex says woof', '子类可通过父类公共方法访问私有字段')
111
+ })
112
+
113
+ return getResults()
114
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ES2022 —— Class Static Initialization Blocks
3
+ *
4
+ * `static { ... }` 块在类定义时执行一次,用于:
5
+ * - 复杂的静态字段初始化(需要 try/catch、条件判断等)
6
+ * - 访问私有字段来初始化外部资源
7
+ * - 替代类外部的静态初始化代码
8
+ *
9
+ * 执行顺序:父类静态块 → 子类静态块,每个类只执行一次。
10
+ */
11
+ import { createSuite } from '../../utils/runner.js'
12
+
13
+ export function testClassStaticBlocks() {
14
+ const { test, assert, getResults } = createSuite('Class Static Blocks (ES2022)')
15
+
16
+ test('基本静态块 —— 初始化静态字段', () => {
17
+ class AppConfig {
18
+ static host
19
+ static port
20
+ static {
21
+ AppConfig.host = 'localhost'
22
+ AppConfig.port = 8080
23
+ }
24
+ }
25
+ assert(AppConfig.host === 'localhost', '静态块应初始化 host')
26
+ assert(AppConfig.port === 8080, '静态块应初始化 port')
27
+ })
28
+
29
+ test('静态块中可使用 try/catch 处理初始化失败', () => {
30
+ class SafeConfig {
31
+ static value
32
+ static {
33
+ try {
34
+ // 模拟可能失败的操作
35
+ const raw = '{"timeout":3000}'
36
+ SafeConfig.value = JSON.parse(raw).timeout
37
+ } catch {
38
+ SafeConfig.value = 1000 // 默认值
39
+ }
40
+ }
41
+ }
42
+ assert(SafeConfig.value === 3000, '静态块中 try/catch 初始化应正确执行')
43
+ })
44
+
45
+ test('静态块访问同类私有字段', () => {
46
+ // 常见模式:将私有字段的 setter 暴露给类外的受信任代码
47
+ let setSecret
48
+ class SecureStore {
49
+ static #secret = 'initial'
50
+ static {
51
+ // 将私有字段的修改能力暴露给模块内部
52
+ setSecret = (val) => { SecureStore.#secret = val }
53
+ }
54
+ static getSecret() { return SecureStore.#secret }
55
+ }
56
+ setSecret('updated')
57
+ assert(SecureStore.getSecret() === 'updated', '静态块应能操作同类私有字段')
58
+ })
59
+
60
+ test('多个静态块按声明顺序执行', () => {
61
+ const log = []
62
+ class Sequence {
63
+ static a
64
+ static { log.push(1); Sequence.a = 10 }
65
+ static b
66
+ static { log.push(2); Sequence.b = Sequence.a * 2 }
67
+ }
68
+ assert(log[0] === 1 && log[1] === 2, '多个静态块应按顺序执行')
69
+ assert(Sequence.b === 20, '第二个静态块应能读取第一个块的结果')
70
+ })
71
+
72
+ test('静态块与静态字段声明交替执行', () => {
73
+ const order = []
74
+ class Mixed {
75
+ static x = (order.push('field-x'), 1)
76
+ static { order.push('block-1') }
77
+ static y = (order.push('field-y'), 2)
78
+ static { order.push('block-2') }
79
+ }
80
+ assert(order.join(',') === 'field-x,block-1,field-y,block-2', '字段与静态块应按声明顺序交替初始化')
81
+ assert(Mixed.x === 1 && Mixed.y === 2, '所有字段应正确赋值')
82
+ })
83
+
84
+ test('子类静态块在父类之后执行', () => {
85
+ const order = []
86
+ class Base {
87
+ static { order.push('base') }
88
+ }
89
+ class Child extends Base {
90
+ static { order.push('child') }
91
+ }
92
+ assert(order[0] === 'base' && order[1] === 'child', '父类静态块应先于子类执行')
93
+ })
94
+
95
+ test('静态块中 this 指向类本身', () => {
96
+ class Widget {
97
+ static name2 = 'Widget'
98
+ static instance
99
+ static {
100
+ // this 指向 Widget 类
101
+ this.instance = { type: this.name2 }
102
+ }
103
+ }
104
+ assert(Widget.instance.type === 'Widget', '静态块内 this 应指向类本身')
105
+ })
106
+
107
+ return getResults()
108
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ES2022 —— Array.prototype.at() / String.prototype.at()
3
+ * TypedArray.prototype.at()
4
+ *
5
+ * .at(index) 支持负索引,解决了 arr[arr.length - 1] 这种冗长写法:
6
+ * arr.at(0) → 第一个元素
7
+ * arr.at(-1) → 最后一个元素(等价于 arr[arr.length - 1])
8
+ * arr.at(-2) → 倒数第二个元素
9
+ *
10
+ * 同样适用于 String、TypedArray(Int8Array、Uint8Array 等)。
11
+ */
12
+ import { createSuite } from '../../utils/runner.js'
13
+
14
+ export function testAtMethod() {
15
+ const { test, assert, getResults } = createSuite('Array/String.at() (ES2022)')
16
+
17
+ // ── Array.prototype.at ─────────────────────────────────────────────
18
+
19
+ test('Array.at() —— 正索引与 arr[i] 等价', () => {
20
+ const arr = [10, 20, 30, 40, 50]
21
+ assert(arr.at(0) === 10, 'at(0) 应返回第一个元素')
22
+ assert(arr.at(2) === 30, 'at(2) 应返回第三个元素')
23
+ assert(arr.at(4) === 50, 'at(4) 应返回最后一个元素')
24
+ })
25
+
26
+ test('Array.at() —— 负索引从末尾倒数', () => {
27
+ const arr = [10, 20, 30, 40, 50]
28
+ assert(arr.at(-1) === 50, 'at(-1) 应返回最后一个元素')
29
+ assert(arr.at(-2) === 40, 'at(-2) 应返回倒数第二个元素')
30
+ assert(arr.at(-5) === 10, 'at(-5) 应返回第一个元素')
31
+ })
32
+
33
+ test('Array.at() —— 越界返回 undefined', () => {
34
+ const arr = [1, 2, 3]
35
+ assert(arr.at(5) === undefined, '正向越界应返回 undefined')
36
+ assert(arr.at(-4) === undefined, '负向越界应返回 undefined')
37
+ })
38
+
39
+ test('Array.at() —— 空数组始终返回 undefined', () => {
40
+ assert([].at(0) === undefined, '空数组 at(0) 应返回 undefined')
41
+ assert([].at(-1) === undefined, '空数组 at(-1) 应返回 undefined')
42
+ })
43
+
44
+ test('对比旧写法:arr.at(-1) vs arr[arr.length - 1]', () => {
45
+ const data = [3, 1, 4, 1, 5, 9, 2, 6]
46
+ const last = data.at(-1)
47
+ const last2 = data[data.length - 1]
48
+ assert(last === last2, 'at(-1) 应与 arr[length-1] 等价')
49
+ })
50
+
51
+ // ── String.prototype.at ────────────────────────────────────────────
52
+
53
+ test('String.at() —— 正索引', () => {
54
+ const s = 'hello'
55
+ assert(s.at(0) === 'h', 'at(0) 应返回首字符')
56
+ assert(s.at(4) === 'o', 'at(4) 应返回末字符')
57
+ })
58
+
59
+ test('String.at() —— 负索引', () => {
60
+ const s = 'hello'
61
+ assert(s.at(-1) === 'o', 'at(-1) 应返回最后一个字符')
62
+ assert(s.at(-3) === 'l', 'at(-3) 应返回倒数第三个字符')
63
+ })
64
+
65
+ test('String.at() —— 越界返回 undefined', () => {
66
+ assert('abc'.at(10) === undefined, '正向越界应返回 undefined')
67
+ assert('abc'.at(-10) === undefined, '负向越界应返回 undefined')
68
+ })
69
+
70
+ // ── TypedArray.prototype.at ────────────────────────────────────────
71
+
72
+ test('TypedArray.at() —— 支持负索引', () => {
73
+ const ta = new Int32Array([100, 200, 300, 400])
74
+ assert(ta.at(0) === 100, 'TypedArray.at(0) 应返回第一个元素')
75
+ assert(ta.at(-1) === 400, 'TypedArray.at(-1) 应返回最后一个元素')
76
+ assert(ta.at(-2) === 300, 'TypedArray.at(-2) 应返回倒数第二个元素')
77
+ })
78
+
79
+ test('at() 不修改原数组', () => {
80
+ const arr = [1, 2, 3]
81
+ arr.at(-1)
82
+ assert(arr.length === 3 && arr[2] === 3, 'at() 不应修改原数组')
83
+ })
84
+
85
+ return getResults()
86
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ES2022 —— Object.hasOwn()
3
+ *
4
+ * Object.hasOwn(obj, key) 是 Object.prototype.hasOwnProperty.call(obj, key) 的简洁替代。
5
+ *
6
+ * 为什么需要它?
7
+ * 1. Object.create(null) 创建的对象没有原型链,调用 .hasOwnProperty() 会报错
8
+ * 2. 对象可能覆盖了自身的 hasOwnProperty 方法,导致判断不可靠
9
+ * 3. Object.hasOwn 基于规范内部操作,始终安全可靠
10
+ */
11
+ import { createSuite } from '../../utils/runner.js'
12
+
13
+ export function testObjectHasOwn() {
14
+ const { test, assert, getResults } = createSuite('Object.hasOwn() (ES2022)')
15
+
16
+ test('基本用法 —— 自有属性返回 true', () => {
17
+ const obj = { name: 'Alice', age: 30 }
18
+ assert(Object.hasOwn(obj, 'name') === true, 'name 是自有属性,应返回 true')
19
+ assert(Object.hasOwn(obj, 'age') === true, 'age 是自有属性,应返回 true')
20
+ })
21
+
22
+ test('不存在的属性返回 false', () => {
23
+ const obj = { a: 1 }
24
+ assert(Object.hasOwn(obj, 'b') === false, '不存在的属性应返回 false')
25
+ assert(Object.hasOwn(obj, 'toString') === false, '原型链上的属性应返回 false')
26
+ })
27
+
28
+ test('继承属性返回 false —— 只检查自有属性', () => {
29
+ class Animal { constructor(name) { this.name = name } }
30
+ class Dog extends Animal {}
31
+ const d = new Dog('Rex')
32
+ assert(Object.hasOwn(d, 'name') === true, 'name 是实例自有属性')
33
+ assert(Object.hasOwn(d, 'constructor') === false, 'constructor 在原型上,不是自有属性')
34
+ })
35
+
36
+ test('null 原型对象 —— hasOwnProperty 不可用时的安全方案', () => {
37
+ // Object.create(null) 的对象没有 hasOwnProperty 方法
38
+ const dict = Object.create(null)
39
+ dict.key = 'value'
40
+
41
+ let threw = false
42
+ try { dict.hasOwnProperty('key') } catch (e) { threw = true }
43
+ assert(threw, 'null 原型对象调用 hasOwnProperty 应抛出错误')
44
+
45
+ // Object.hasOwn 不依赖原型链,安全可用
46
+ assert(Object.hasOwn(dict, 'key') === true, 'hasOwn 对 null 原型对象应正常工作')
47
+ assert(Object.hasOwn(dict, 'other') === false, '不存在的键应返回 false')
48
+ })
49
+
50
+ test('对象覆盖 hasOwnProperty —— hasOwn 不受影响', () => {
51
+ const malicious = {
52
+ hasOwnProperty() { return false }, // 伪造返回值
53
+ secret: 42
54
+ }
55
+ // 旧方式被欺骗
56
+ assert(malicious.hasOwnProperty('secret') === false, '被覆盖的 hasOwnProperty 返回错误结果')
57
+ // Object.hasOwn 不调用实例方法,始终可靠
58
+ assert(Object.hasOwn(malicious, 'secret') === true, 'hasOwn 不受覆盖影响,结果正确')
59
+ })
60
+
61
+ test('值为 undefined 的属性仍返回 true', () => {
62
+ const obj = { key: undefined }
63
+ assert(Object.hasOwn(obj, 'key') === true, '值为 undefined 的自有属性应返回 true')
64
+ })
65
+
66
+ test('Symbol 作为键', () => {
67
+ const sym = Symbol('id')
68
+ const obj = { [sym]: 123 }
69
+ assert(Object.hasOwn(obj, sym) === true, 'Symbol 键的自有属性应返回 true')
70
+ assert(Object.hasOwn(obj, Symbol('id')) === false, '不同的 Symbol 实例应返回 false')
71
+ })
72
+
73
+ test('数组的索引属性', () => {
74
+ const arr = [10, 20, 30]
75
+ assert(Object.hasOwn(arr, 0) === true, '数组索引 0 是自有属性')
76
+ assert(Object.hasOwn(arr, '1') === true, '字符串索引 "1" 也是自有属性')
77
+ assert(Object.hasOwn(arr, 'length') === true, 'length 是数组的自有属性')
78
+ assert(Object.hasOwn(arr, 3) === false, '越界索引应返回 false')
79
+ })
80
+
81
+ return getResults()
82
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * ES2022 —— Error Cause
3
+ *
4
+ * Error 构造函数接受第二个参数 options,其中 cause 属性用于链式传递原始错误:
5
+ * new Error('高层描述', { cause: originalError })
6
+ *
7
+ * 所有内置 Error 子类(TypeError、RangeError 等)同样支持。
8
+ * 主要用途:在错误封装时保留根因,方便调试和日志溯源。
9
+ */
10
+ import { createSuite } from '../../utils/runner.js'
11
+
12
+ export function testErrorCause() {
13
+ const { test, assert, getResults } = createSuite('Error Cause (ES2022)')
14
+
15
+ test('基本用法 —— cause 属性保存原始错误', () => {
16
+ const original = new TypeError('原始类型错误')
17
+ const wrapped = new Error('操作失败', { cause: original })
18
+ assert(wrapped.message === '操作失败', '外层错误消息应正确')
19
+ assert(wrapped.cause === original, 'cause 应指向原始错误')
20
+ assert(wrapped.cause instanceof TypeError, 'cause 类型应保留')
21
+ })
22
+
23
+ test('所有内置子类均支持 cause', () => {
24
+ const root = new Error('根因')
25
+ const types = [
26
+ new TypeError('类型错误', { cause: root }),
27
+ new RangeError('范围错误', { cause: root }),
28
+ new SyntaxError('语法错误', { cause: root }),
29
+ ]
30
+ types.forEach(e => {
31
+ assert(e.cause === root, `${e.constructor.name} 的 cause 应指向根因`)
32
+ })
33
+ })
34
+
35
+ test('错误链可以多层嵌套', () => {
36
+ const level1 = new Error('数据库连接失败')
37
+ const level2 = new Error('查询用户失败', { cause: level1 })
38
+ const level3 = new Error('登录接口报错', { cause: level2 })
39
+
40
+ assert(level3.cause === level2, '第三层 cause 应指向第二层')
41
+ assert(level3.cause.cause === level1, '第二层 cause 应指向第一层')
42
+ assert(level3.cause.cause.message === '数据库连接失败', '根因消息应可追溯')
43
+ })
44
+
45
+ test('不传 cause 时属性为 undefined', () => {
46
+ const e = new Error('普通错误')
47
+ assert(e.cause === undefined, '未传 cause 时属性应为 undefined')
48
+ })
49
+
50
+ test('cause 可以是任意值(不限于 Error)', () => {
51
+ const e1 = new Error('原因是字符串', { cause: '网络超时' })
52
+ const e2 = new Error('原因是数字', { cause: 404 })
53
+ const e3 = new Error('原因是对象', { cause: { code: 'ENOENT' } })
54
+ assert(e1.cause === '网络超时', 'cause 可以是字符串')
55
+ assert(e2.cause === 404, 'cause 可以是数字')
56
+ assert(e3.cause.code === 'ENOENT', 'cause 可以是对象')
57
+ })
58
+
59
+ test('实际应用:fetch 封装中传递原始网络错误', async () => {
60
+ async function fetchUser(id) {
61
+ try {
62
+ // 模拟请求失败
63
+ throw new Error('fetch failed: 500')
64
+ } catch (networkErr) {
65
+ throw new Error(`获取用户 ${id} 失败`, { cause: networkErr })
66
+ }
67
+ }
68
+
69
+ let caught = null
70
+ try { await fetchUser(42) } catch (e) { caught = e }
71
+
72
+ assert(caught !== null, '应捕获到封装后的错误')
73
+ assert(caught.message.includes('42'), '外层错误消息应包含用户 id')
74
+ assert(caught.cause instanceof Error, 'cause 应是原始 Error')
75
+ assert(caught.cause.message.includes('500'), '原始错误消息应保留')
76
+ })
77
+
78
+ test('自定义 Error 子类也支持 cause', () => {
79
+ class AppError extends Error {
80
+ constructor(msg, options) {
81
+ super(msg, options)
82
+ this.name = 'AppError'
83
+ }
84
+ }
85
+ const root = new TypeError('底层失败')
86
+ const e = new AppError('应用层失败', { cause: root })
87
+ assert(e.name === 'AppError', '子类名称应正确')
88
+ assert(e.cause === root, '自定义子类应支持 cause')
89
+ })
90
+
91
+ return getResults()
92
+ }