0%

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它让 JavaScript 可以在浏览器之外运行。本文将从概念到实践,全面介绍 Node.js,并深入分析它与浏览器 JavaScript 的差异。

阅读全文 »

Reflect 详解

【面试速答版】

Q1: Reflect 是什么?它和 Object / Function 上的方法有什么区别?

Reflect 是 ES2015(ES6)引入的一个全局内置对象,提供了一组与对象操作对应的方法。它不是构造函数(不能 new Reflect()),也不是函数对象,它的所有方法都是静态的(类似 Math)。

核心区别有三点:

  1. 返回值更合理Object.defineProperty 失败时抛出异常,Reflect.defineProperty 返回 false
  2. 参数更统一Reflect 的方法参数与对应的 Proxy handler 参数完全对齐,不用记忆零散的方法签名
  3. 行为更规范Reflect.apply 替代 Function.prototype.apply.call(fn, thisArg, args) 这种别扭写法

Q2: Reflect 解决了什么问题?

三个核心问题:

  1. 内部方法的外部化:JS 引擎内部使用 [[Get]][[Set]][[Delete]] 等内部方法操作对象。过去开发者无法直接调用这些内部方法,只能通过 obj.keydelete obj.key 等语法间接触发。Reflect 将这些内部方法暴露为可调用的函数,使元编程成为可能。

  2. 与 Proxy 配合:Proxy 的 handler 拦截方法和 Reflect 的方法是一一对应的。在 Proxy 中调用对应的 Reflect 方法可以确保行为与默认语义一致,避免手动实现带来的陷阱。

  3. 消除异常 vs false 的不一致Object.definePropertyObject.sealObject.freeze 等方法失败时抛出 TypeError,而 Reflect 对应方法返回 false,使异常处理更统一、可控。

Q3: Reflect 的常用方法有哪些?实际项目中怎么用?

13 个静态方法(按用途分类):

对象基本操作

  • Reflect.get(target, prop, receiver) — 读取属性,相当于 target[prop]
  • Reflect.set(target, prop, value, receiver) — 设置属性,返回布尔值
  • Reflect.has(target, prop) — 检查属性是否存在,相当于 prop in target
  • Reflect.deleteProperty(target, prop) — 删除属性,相当于 delete target[prop]
  • Reflect.ownKeys(target) — 返回所有自有属性键(字符串 + Symbol),相当于 Object.getOwnPropertyNames + Object.getOwnPropertySymbols

原型操作

  • Reflect.getPrototypeOf(target) — 获取原型
  • Reflect.setPrototypeOf(target, proto) — 设置原型,返回布尔值

属性描述符

  • Reflect.defineProperty(target, prop, descriptor) — 定义属性,返回布尔值
  • Reflect.getOwnPropertyDescriptor(target, prop) — 获取属性描述符

函数调用

  • Reflect.apply(target, thisArg, args) — 调用函数,相当于 target.apply(thisArg, args)
  • Reflect.construct(target, args) — 调用构造函数,相当于 new target(...args)

可扩展性

  • Reflect.isExtensible(target) — 判断是否可扩展
  • Reflect.preventExtensions(target) — 阻止扩展,返回布尔值

最常用的三个场景

  1. Proxy 中转发默认行为:在 Proxy handler 中调用 Reflect.get/set 确保默认行为一致
  2. **代替 Function.prototype.apply**:Reflect.apply(fn, thisArg, args) 更简洁
  3. 代替 delete 操作符Reflect.deleteProperty(obj, key) 返回布尔值,可安全判断是否删除成功

【深入理解版】

1. Reflect 要解决什么问题?

1.1 从一段元编程需求讲起

假设你在做一个响应式框架,需要拦截对象属性的读取和设置,以便在读取时收集依赖、在设置时触发更新。

在没有 Reflect 的时代,你可能会用 Object.defineProperty 实现属性拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Vue 2 风格的响应式(Object.defineProperty)
function reactive(obj) {
const keys = Object.keys(obj)
const deps = {} // 依赖收集

keys.forEach(key => {
deps[key] = []
let value = obj[key]

Object.defineProperty(obj, key, {
get() {
// 收集依赖
if (currentWatcher) {
deps[key].push(currentWatcher)
}
return value
},
set(newValue) {
value = newValue
// 触发更新
deps[key].forEach(watcher => watcher.update())
}
})
})

return obj
}

这个方案有几个痛点:

痛点 1:Object.defineProperty 需要遍历所有属性

1
2
3
4
5
6
// 新增属性无法被拦截
const user = reactive({ name: 'Alice' })
user.age = 25 // ❌ age 属性没有被 defineProperty 处理,没有响应式

// 删除属性也无法监听
delete user.name // ❌ 无法触发更新

Vue 3 用 Proxy 解决了这个问题。但有了 Proxy,又引出了新的问题:

痛点 2:Proxy 中如何正确恢复默认行为?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 用 Proxy 实现响应式
const user = { name: 'Alice', age: 25 }

const proxy = new Proxy(user, {
get(target, key, receiver) {
// 依赖收集...
console.log(`读取 ${String(key)}`)

// 问题来了:如何返回正确的值?
// 直觉做法是直接返回 target[key]
return target[key] // ❌ 有问题!
},

set(target, key, value, receiver) {
// 触发更新...
console.log(`设置 ${String(key)} = ${value}`)

// 直觉做法是直接赋值
target[key] = value // ⚠️ 可能有问题
return true
}
})

为什么 target[key]target[key] = value 可能有问题?因为:

  1. 丢失了 receiver(接收者):如果属性是继承来的 getter/setter,target[key] 的 this 指向 target,而不是 proxy,导致原型链上的行为异常
  2. 没有校验:如果属性是只读的,target[key] = value 在严格模式下会报错,而你却返回了 true
  3. 没有返回值set 必须返回布尔值表示成功与否,你手动返回 true 忽略了实际结果

这就引出了 Reflect 的核心价值:Reflect 的方法参数和 Proxy handler 的参数完全对齐,且行为默认正确。

1.2 Reflect 的解决思路

Reflect 将 JS 引擎内部的操作(如 [[Get]][[Set]][[HasProperty]] 等)暴露为可调用的函数。

1
2
3
4
5
6
// 内部操作 vs Reflect API
// 内部操作:obj.key → 引擎执行 target.[[Get]](key, receiver)
// Reflect:Reflect.get(target, key, receiver)

// 内部操作:obj.key = value → 引擎执行 target.[[Set]](key, value, receiver)
// Reflect:Reflect.set(target, key, value, receiver)

在 Proxy handler 中调用对应的 Reflect 方法,等价于「按默认行为执行」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 正确的做法:用 Reflect 转发
const proxy = new Proxy(user, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`)
// Reflect.get 的 receiver 参数会正确传递,保证 getter 的 this 绑定正确
return Reflect.get(target, key, receiver)
},

set(target, key, value, receiver) {
console.log(`设置 ${String(key)} = ${value}`)
// Reflect.set 返回布尔值,自动反映操作是否成功
return Reflect.set(target, key, value, receiver)
}
})

2. 核心原理与设计

2.1 内部方法映射

JS 规范将对象的基本操作定义为一系列「内部方法」,用 [[ ]] 表示。Reflect 的每个方法都对应一个内部方法:

内部方法Proxy 拦截Reflect 方法语法等价
[[Get]]getReflect.get(target, key, receiver)target[key]
[[Set]]setReflect.set(target, key, value, receiver)target[key] = value
[[HasProperty]]hasReflect.has(target, key)key in target
[[Delete]]deletePropertyReflect.deleteProperty(target, key)delete target[key]
[[Call]]applyReflect.apply(fn, thisArg, args)fn(...args)
[[Construct]]constructReflect.construct(Target, args)new Target(...args)
[[GetPrototypeOf]]getPrototypeOfReflect.getPrototypeOf(target)Object.getPrototypeOf(target)
[[SetPrototypeOf]]setPrototypeOfReflect.setPrototypeOf(target, proto)Object.setPrototypeOf(target, proto)
[[IsExtensible]]isExtensibleReflect.isExtensible(target)Object.isExtensible(target)
[[PreventExtensions]]preventExtensionsReflect.preventExtensions(target)Object.preventExtensions(target)
[[GetOwnProperty]]getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor(target, key)Object.getOwnPropertyDescriptor(target, key)
[[DefineOwnProperty]]definePropertyReflect.defineProperty(target, key, desc)Object.defineProperty(target, key, desc)
[[OwnPropertyKeys]]ownKeysReflect.ownKeys(target)Object.getOwnPropertyNames(target) + Object.getOwnPropertySymbols(target)

这种一一对应关系并非巧合——Reflect 的方法签名就是按照 Proxy handler 的参数设计的

2.2 Reflect 与 Object API 的核心差异

差异 1:异常 vs 布尔值返回

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {}
Object.defineProperty(obj, 'x', { value: 1, writable: false })

// Object.defineProperty — 重复定义会抛出异常
try {
Object.defineProperty(obj, 'x', { value: 2 }) // ❌ TypeError: Cannot redefine property
} catch (e) {
console.log('Object 方式抛异常:', e.message)
}

// Reflect.defineProperty — 返回 false,不抛异常
const success = Reflect.defineProperty(obj, 'x', { value: 2 })
console.log(success) // false,不会抛出异常

这在批量操作时非常有用——你不需要为每个操作包一层 try-catch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Object 方式:需要 try-catch
function defineProperties(obj, props) {
for (const [key, desc] of Object.entries(props)) {
try {
Object.defineProperty(obj, key, desc)
} catch (e) {
console.error(`定义属性 ${key} 失败`)
return false
}
}
return true
}

// Reflect 方式:无需 try-catch
function defineProperties(obj, props) {
for (const [key, desc] of Object.entries(props)) {
if (!Reflect.defineProperty(obj, key, desc)) {
console.error(`定义属性 ${key} 失败`)
return false
}
}
return true
}

差异 2:receiver 参数

Reflect.getReflect.setreceiver 参数是它们区别于 Object 对应操作的关键能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const parent = {
get name() {
return this._name.toUpperCase()
},
set name(val) {
this._name = val
}
}

const child = { _name: 'child' }
Object.setPrototypeOf(child, parent)

// 使用 Object / 直接属性访问
child.name // 'CHILD' — 通过原型链调用 parent 的 getter
// getter 内部 this._name 指向 child,所以返回 'CHILD'

// 但如果想强制让 this 指向另一个对象?
// Object 没有提供这样的能力

// Reflect.get 的 receiver 可以指定 this
Reflect.get(parent, 'name', { _name: 'forced' }) // 'FORCED'
// 相当于:parent.name 被调用,但 getter 内部的 this 指向 receiver 对象

这个能力在 Proxy 中至关重要

1
2
3
4
5
6
7
8
9
const target = {}
const proxy = new Proxy(target, {
get(target, key, receiver) {
// receiver 是触发这个 get 的原始对象(可能是 proxy 本身或其原型链上的对象)
// 如果直接用 target[key],getter 的 this 永远指向 target
// 如果用 Reflect.get(target, key, receiver),getter 的 this 指向 receiver
return Reflect.get(target, key, receiver)
}
})

差异 3:参数顺序更合理

1
2
3
4
5
6
7
8
9
10
11
// Object.defineProperty — target, key, descriptor
Object.defineProperty(obj, 'prop', { value: 1 })

// Reflect.defineProperty — target, key, descriptor(一致)
Reflect.defineProperty(obj, 'prop', { value: 1 })

// 但有些 Object API 的参数顺序不同:
const keys = Object.keys(obj)
const values = Object.values(obj)

// 而 Reflect 的所有方法都是 target 作为第一个参数,保持一致

2.3 Reflect.construct 与 new 的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name, age) {
this.name = name
this.age = age
}

// 传统:new 操作符
const alice = new Person('Alice', 25)

// Reflect.construct
const bob = Reflect.construct(Person, ['Bob', 30])

// Reflect.construct 比 new 多了第三个参数:newTarget
// 用于修改 constructor 的 prototype
function Child(name) {
this.name = name
}

// 用 Person 的构造函数体,但原型用 Child.prototype
const instance = Reflect.construct(Person, ['Alice', 25], Child)
console.log(instance instanceof Person) // false
console.log(instance instanceof Child) // true
console.log(instance.name) // 'Alice'
console.log(instance.age) // 25

这在实现继承代理类工厂时很有用。

3. 实际应用场景与代码示例

3.1 场景 1:Proxy 中默认行为转发(最经典用法)

这是 Reflect 最广泛的用途——在 Proxy handler 中调用对应的 Reflect 方法,确保默认行为被正确执行。

反例:手动实现转发 → 隐患

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const obj = {
_value: 10,
get value() {
return this._value * 2
}
}

const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`)
// ❌ 手动返回值,但不处理 receiver
return target[key]
}
})

const child = Object.create(proxy)
child._value = 100

console.log(child.value)
// 读取 value
// 100 * 2 = 200 ❌
// 预期是 child._value * 2 = 200,但触发的是 proxy 的 get
// 实际上 target[key] 中的 target 是 obj,所以 this._value 指向 obj._value
// 结果其实是 10 * 2 = 20,但输出显示 200???

// 仔细分析:
// 第一次 proxy.get trap 被触发,key = 'value'
// target[key] → obj.value → obj 的 getter 被调用 → this === obj → obj._value * 2 = 20
// 返回 20... 不对,怎么是 200?

实际上这里的问题更微妙:child.value 通过原型链查找,触发了 proxyget handler。handler 中 target[key] 读取 obj.value,getter 中 this 指向 objthis._value10。如果有 receiver 参数,应该返回 child._value * 2 = 200,但因为 receiver 没有被传递,所以应该得到 20

但如果使用了 Reflect.get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const proxy = new Proxy(obj, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`)
// ✅ 正确传递 receiver
return Reflect.get(target, key, receiver)
}
})

const child = Object.create(proxy)
child._value = 100

console.log(child.value)
// 读取 value
// 200 ✅
// Reflect.get 将 receiver 传递给了 getter,this 指向 child
// child._value * 2 = 200

3.2 场景 2:用 Reflect 实现更安全的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Base {
constructor(name) {
this.name = name
}

greet() {
return `Hello, ${this.name}`
}
}

class Derived extends Base {
constructor(name, age) {
// 传统写法
super(name)
this.age = age
}
}

// 用 Reflect.construct 实现动态继承
function createInstance(Constructor, args, proto) {
// 使用自定义的原型创建实例
const instance = Reflect.construct(Constructor, args, proto || Constructor)
return instance
}

// 动态指定 prototype
function SpecialProto() {}
SpecialProto.prototype.customMethod = function() {
return 'special method'
}

const obj = createInstance(Base, ['Alice'], SpecialProto)
console.log(obj.name) // 'Alice'
console.log(obj.customMethod()) // 'special method'
console.log(obj instanceof Base) // false
console.log(obj instanceof SpecialProto) // true

3.3 场景 3:用 Reflect.apply 替代 Function.prototype.apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 传统方式:Function.prototype.apply
const result1 = Math.max.apply(null, [1, 2, 3])

// ES6 方式:展开运算符
const result2 = Math.max(...[1, 2, 3])

// 但某些场景展开运算符行不通,比如:
// 1. this 上下文需要显式指定
function logger(prefix, message) {
console.log(`[${prefix}] ${message}`)
}

// 用 Function.prototype.apply — 写法别扭
Function.prototype.apply.call(logger, null, ['INFO', 'hello'])

// 用 Reflect.apply — 简洁直观
Reflect.apply(logger, null, ['INFO', 'hello'])

// 2. 需要控制 this 又不希望展开数组
const context = { count: 0 }
function increment(amount) {
this.count += amount
return this.count
}

// Reflect.apply 比 fn.apply 更安全(apply 可能被重写)
Reflect.apply(increment, context, [5])
console.log(context.count) // 5

3.4 场景 4:用 Reflect.ownKeys 获取所有键

Reflect.ownKeys 返回一个对象的所有自有属性键,包括字符串和 Symbol,等价于 Object.getOwnPropertyNamesObject.getOwnPropertySymbols 的合并结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const sym = Symbol('private')
const obj = {
name: 'Alice',
age: 25,
[sym]: 'secret'
}

// Object.keys — 只返回可枚举字符串键
console.log(Object.keys(obj)) // ['name', 'age']

// Object.getOwnPropertyNames — 返回所有字符串键(包括不可枚举)
console.log(Object.getOwnPropertyNames(obj)) // ['name', 'age']

// Object.getOwnPropertySymbols — 只返回 Symbol 键
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(private)]

// Reflect.ownKeys — 合并返回所有键
console.log(Reflect.ownKeys(obj)) // ['name', 'age', Symbol(private)]

深层拷贝时全面复制属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone(target) {
if (target === null || typeof target !== 'object') return target

const clone = Array.isArray(target) ? [] : Object.create(Reflect.getPrototypeOf(target))

// 复制所有属性(包括 Symbol 键和不可枚举属性)
for (const key of Reflect.ownKeys(target)) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, key)
if (descriptor) {
// 完整复制属性描述符
Reflect.defineProperty(clone, key, { ...descriptor, value: deepClone(descriptor.value) })
}
}

return clone
}

3.5 场景 5:用 Reflect 实现可撤销权限控制

结合 Proxy.revocable 和 Reflect,实现一个可撤销的访问代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function createAccessController(target) {
let revoked = false

const handler = {
get(target, key, receiver) {
if (revoked) throw new Error('访问已被撤销')
if (key === 'secret') throw new Error('无权访问 secret 属性')
return Reflect.get(target, key, receiver)
},

set(target, key, value, receiver) {
if (revoked) throw new Error('访问已被撤销')
if (key === 'unchangeable') {
console.log('unchangeable 是只读属性')
return false
}
return Reflect.set(target, key, value, receiver)
},

has(target, key) {
if (revoked) throw new Error('访问已被撤销')
return Reflect.has(target, key)
},

deleteProperty(target, key) {
if (revoked) throw new Error('访问已被撤销')
return Reflect.deleteProperty(target, key)
},

ownKeys(target) {
if (revoked) throw new Error('访问已被撤销')
return Reflect.ownKeys(target).filter(key => key !== 'secret')
}
}

const { proxy, revoke } = Proxy.revocable(target, handler)

return {
proxy,
revoke() {
revoked = true
revoke()
},
isRevoked: () => revoked
}
}

// 使用
const controller = createAccessController({
name: '公开数据',
secret: '这是秘密',
unchangeable: '固定值'
})

console.log(controller.proxy.name) // '公开数据'
console.log(controller.proxy.secret) // Error: 无权访问 secret 属性
controller.proxy.unchangeable = '新值' // 'unchangeable 是只读属性'

controller.revoke()
console.log(controller.proxy.name) // Error: 访问已被撤销

4. 常见误区 & 实际项目中的坑

4.1 误区:Reflect 可以替代 Object 的所有方法

错误理解

1
2
3
4
5
6
7
// 以为 Reflect 是 Object 的完全替代品
Reflect.keys(obj) // ❌ TypeError: Reflect.keys is not a function
Reflect.values(obj) // ❌ TypeError: Reflect.values is not a function
Reflect.assign(target, source) // ❌ TypeError: Reflect.assign is not a function
Reflect.entries(obj) // ❌ TypeError: Reflect.entries is not a function
Reflect.freeze(obj) // ❌ TypeError: Reflect.freeze is not a function
Reflect.seal(obj) // ❌ TypeError: Reflect.seal is not a function

为什么这些方法不存在:Reflect 只暴露 JS 引擎的「内部方法」([[Get]][[Set]][[Delete]] 等)。Object.keysObject.valuesObject.assignObject.freeze 等是由多个内部方法组合而成的高级操作,不是单一的内部操作。

关键原则:Reflect 的方法数量和 Proxy handler 的可拦截操作数量完全相同(13 个)。如果 Proxy 不能拦截它,Reflect 也不会有对应的方法。

正确认知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Reflect 有的方法(13 个):
Reflect.apply
Reflect.construct
Reflect.defineProperty
Reflect.deleteProperty
Reflect.get
Reflect.getOwnPropertyDescriptor
Reflect.getPrototypeOf
Reflect.has
Reflect.isExtensible
Reflect.ownKeys
Reflect.preventExtensions
Reflect.set
Reflect.setPrototypeOf

// 这些是和 Object 共享的(两者都有,行为略有差异):
Reflect.getPrototypeOf → Object.getPrototypeOf
Reflect.setPrototypeOf → Object.setPrototypeOf
Reflect.getOwnPropertyDescriptor → Object.getOwnPropertyDescriptor
Reflect.defineProperty → Object.defineProperty
Reflect.isExtensible → Object.isExtensible
Reflect.preventExtensions → Object.preventExtensions
Reflect.ownKeys → Object.getOwnPropertyNames + Object.getOwnPropertySymbols

// 这些是 Reflect 独有、Object 没有的:
Reflect.get (Object 没有等价方法)
Reflect.set (Object 没有等价方法)
Reflect.has (Object 没有等价方法)
Reflect.deleteProperty (Object 没有等价方法)
Reflect.apply (Function.prototype.apply,不是 Object)
Reflect.construct (new 操作符,不是 Object)

4.2 误区:Reflect.get 和 target[key] 完全等价

错误理解

1
2
3
4
const target = { x: 1 }
const val1 = target.x
const val2 = Reflect.get(target, 'x')
// 认为 val1 === val2 永远成立

为什么不全等价:当目标对象有 getter,且 getter 内部访问了 this 时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const target = {
_secret: 42,
get secret() {
return this._secret
}
}

// 直接访问
console.log(target.secret) // 42 — getter 的 this 指向 target

// Reflect.get 没有 receiver
console.log(Reflect.get(target, 'secret')) // 42 — 等价,this 指向 target

// Reflect.get 指定 receiver
const receiver = { _secret: 100 }
console.log(Reflect.get(target, 'secret', receiver)) // 100 — getter 的 this 指向 receiver

真正的等价关系

1
2
3
4
5
6
7
8
// 当没有 getter 时,两者完全等价
Reflect.get(target, key) === target[key] // true(无 getter)

// 有 getter 但 receiver 是 target 时,等价
Reflect.get(target, key, target) === target[key] // true

// 有 getter 且 receiver 是其他对象时,不等价
Reflect.get(target, key, other) !== target[key] // true,getter 的 this 指向 other

4.3 坑:Reflect.set 的返回值被忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误代码:在 Proxy set handler 中不返回布尔值
const proxy = new Proxy({}, {
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
// ❌ 没有 return!隐式返回 undefined
// Proxy 的 set handler 必须返回布尔值
// undefined 会被转型为 false,表示设置失败
}
})

proxy.x = 1
console.log(proxy.x) // undefined ❌ 设置被拒绝了!

// 在严格模式下这甚至会抛出 TypeError
'use strict'
proxy.x = 1 // TypeError: 'set' returned false for property 'x'

正确做法

1
2
3
4
5
6
const proxy = new Proxy({}, {
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
// ✅ 直接返回 Reflect.set 的结果
}
})

4.4 坑:Reflect.apply 和 Reflect.construct 的类型判断

1
2
3
4
5
6
7
8
9
// Reflect.apply 只能用于函数
Reflect.apply('not a function', null, []) // ❌ TypeError

// Reflect.construct 只能用于构造函数
Reflect.construct({}, []) // ❌ TypeError: {}.constructor is not a constructor

// 箭头函数不能作为构造函数,也不能 Reflect.construct
const arrow = () => {}
Reflect.construct(arrow, []) // ❌ TypeError: arrow is not a constructor

4.5 坑:Reflect.ownKeys 的跨平台兼容性

在某些老旧环境(IE 等)中,Reflect 不存在。对于需要兼容旧浏览器的项目,需要用 polyfill:

1
npm install core-js
1
2
3
4
import 'core-js/stable/reflect'

// 或单独引入
import 'core-js/es/reflect'

或者直接使用 Babel 的 @babel/preset-env + useBuiltIns: 'usage' 自动引入。

5. 与相关知识的关联 & 对比

5.1 Reflect vs Proxy

维度ReflectProxy
角色工具方法集对象拦截器
是否修改对象❌ 只读/写,不改行为✅ 拦截并修改行为
是否可独立使用✅ 是✅ 是
与对方的关系提供默认行为实现拦截后用 Reflect 恢复默认行为
数量13 个静态方法13 个 handler 陷阱

核心关系:Reflect 和 Proxy 是设计为一起使用的。每个 Proxy handler 的 trap 都有对应名称的 Reflect 方法。在 trap 中调用同名 Reflect 方法等于「按规范默认行为执行」。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 最佳实践:Proxy + Reflect 联合使用
const handler = {
get: Reflect.get, // 完全转发 get
set: Reflect.set, // 完全转发 set
has: Reflect.has, // 完全转发 has
deleteProperty: Reflect.deleteProperty, // 完全转发 deleteProperty

// 或者在 trap 中添加额外逻辑后再转发
get(target, key, receiver) {
track(target, key) // 额外逻辑:依赖收集
return Reflect.get(target, key, receiver) // 转发默认行为
}
}

5.2 Reflect vs Object API

操作Object APIReflect API关键差异
获取原型Object.getPrototypeOfReflect.getPrototypeOf⚠️ 行为不同:Object 会对非对象参数做类型转换,Reflect 直接抛 TypeError
设置原型Object.setPrototypeOfReflect.setPrototypeOf✅ 相同
定义属性Object.definePropertyReflect.defineProperty⚠️ Object 抛异常,Reflect 返回 false
删除属性delete target[key]Reflect.deleteProperty✅ Reflect 返回布尔值,delete 返回布尔值(但严格模式下不同)
属性描述符Object.getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor⚠️ Object 对非对象参数做类型转换,Reflect 抛 TypeError
自有键getOwnPropertyNames + getOwnPropertySymbolsReflect.ownKeys✅ Reflect 一步到位
可扩展性Object.isExtensibleReflect.isExtensible⚠️ 同上,参数处理不同
阻止扩展Object.preventExtensionsReflect.preventExtensions⚠️ 同上
属性读取target.keyReflect.get✅ Reflect 支持 receiver
属性设置target.key = valReflect.set✅ Reflect 返回布尔值,支持 receiver

关于非对象参数的行为差异

1
2
3
4
5
6
7
// Object API 会将非对象参数装箱
Object.getPrototypeOf('hello') // String.prototype
Object.getPrototypeOf(42) // Number.prototype

// Reflect API 对非对象参数直接抛 TypeError
Reflect.getPrototypeOf('hello') // ❌ TypeError: Reflect.getPrototypeOf called on non-object
Reflect.getPrototypeOf(42) // ❌ TypeError: Reflect.getPrototypeOf called on non-object

6. 现代最佳实践(2025-2026)

6.1 在 Proxy 中始终使用 Reflect 转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 误区:手动实现转发
const badHandler = {
get(target, key) {
// ❌ 手动返回值
return target[key]
},
set(target, key, value) {
// ❌ 手动赋值,没有返回值
target[key] = value
}
}

// 最佳实践:始终通过 Reflect 转发
const goodHandler = {
get(target, key, receiver) {
// 前置处理
if (typeof key === 'string' && key.startsWith('_')) {
throw new Error('私有属性不可访问')
}
// 用 Reflect 转发默认行为
return Reflect.get(target, key, receiver)
},

set(target, key, value, receiver) {
// 前置校验
if (key === 'id' && typeof value !== 'number') {
throw new TypeError('id 必须是数字')
}
// 用 Reflect 转发默认行为
return Reflect.set(target, key, value, receiver)
},

deleteProperty(target, key) {
// 保护关键属性
if (key === 'id') {
console.warn('不能删除 id 属性')
return false
}
return Reflect.deleteProperty(target, key)
},

has(target, key) {
// 隐藏私有属性
if (typeof key === 'string' && key.startsWith('_')) {
return false
}
return Reflect.has(target, key)
}
}

6.2 用 Reflect 简化参数验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Validator {
constructor(target, rules) {
return new Proxy(target, {
set(target, key, value, receiver) {
const rule = rules[key]
if (rule) {
if (rule.type && typeof value !== rule.type) {
throw new TypeError(`${String(key)} 必须是 ${rule.type} 类型`)
}
if (rule.min != null && value < rule.min) {
throw new RangeError(`${String(key)} 不能小于 ${rule.min}`)
}
if (rule.max != null && value > rule.max) {
throw new RangeError(`${String(key)} 不能大于 ${rule.max}`)
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new Error(`${String(key)} 格式不正确`)
}
}
return Reflect.set(target, key, value, receiver)
}
})
}
}

// 使用
const user = new Validator({
name: '',
age: 0,
email: ''
}, {
name: { type: 'string', minLength: 1 },
age: { type: 'number', min: 0, max: 150 },
email: { type: 'string', pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
})

user.name = 'Alice' // ✅
user.age = 25 // ✅
user.email = 'alice@example.com' // ✅
user.age = -1 // ❌ RangeError
user.email = 'invalid' // ❌ Error: 格式不正确

6.3 用 Reflect 实现观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
function createObservable(target, onChange) {
return new Proxy(target, {
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
const result = Reflect.set(target, key, value, receiver)

if (result && oldValue !== value) {
onChange({
type: 'update',
key,
oldValue,
newValue: value,
target
})
}

return result
},

deleteProperty(target, key) {
const existed = key in target
if (existed) {
const oldValue = Reflect.get(target, key)
const result = Reflect.deleteProperty(target, key)
if (result) {
onChange({
type: 'delete',
key,
oldValue,
target
})
}
return result
}
return true
},

defineProperty(target, key, descriptor) {
const result = Reflect.defineProperty(target, key, descriptor)
if (result) {
onChange({
type: 'define',
key,
descriptor,
target
})
}
return result
}
})
}

// 使用
const user = { name: 'Alice', age: 25 }
const observed = createObservable(user, (event) => {
console.log(`[变更] ${event.type} ${String(event.key)}:`,
event.oldValue, '→', event.newValue ?? '(已删除)')
})

observed.name = 'Bob' // [变更] update name: Alice → Bob
observed.age = 26 // [变更] update age: 25 → 26
delete observed.name // [变更] delete name: Bob → (已删除)

6.4 用 Reflect 实现元数据标记

利用 Reflect.definePropertyReflect.getOwnPropertyDescriptor 实现不可枚举的元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const META_KEY = Symbol('meta')

function defineMeta(target, key, meta) {
// 获取已有的元数据
let allMeta = Reflect.getOwnPropertyDescriptor(target, META_KEY)?.value
if (!allMeta) {
allMeta = {}
// 用不可枚举、不可写的方式存储
Reflect.defineProperty(target, META_KEY, {
value: allMeta,
writable: true,
enumerable: false,
configurable: false
})
}

allMeta[key] = meta
}

function getMeta(target, key) {
const allMeta = target[META_KEY]
return allMeta?.[key]
}

// 使用
const obj = {}
defineMeta(obj, 'name', { description: '用户名称', required: true })
defineMeta(obj, 'age', { description: '用户年龄', min: 0, max: 150 })

console.log(Reflect.ownKeys(obj)) // [Symbol(meta)] — meta 键不可枚举但可被 ownKeys 获取
console.log(getMeta(obj, 'name')) // { description: '用户名称', required: true }

7. 常见疑问解答(自问自答)

Q1: 为什么不需要 Reflect.keys?那我要获取可枚举键怎么办?

Reflect.keys 不存在是因为 JS 规范中没有一个对应的内部方法 [[Keys]]Object.keys 是通过 [[OwnPropertyKeys]] 获取所有自有键,再过滤出可枚举属性组合实现的。

获取可枚举键的正确方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方法 1:Object.keys(推荐)
const keys = Object.keys(obj)

// 方法 2:组合 Reflect.ownKeys + 过滤
const ownKeys = Reflect.ownKeys(obj)
const enumerableKeys = ownKeys.filter(key => {
const desc = Reflect.getOwnPropertyDescriptor(obj, key)
return desc && desc.enumerable
})

// 方法 3:for...in(包括原型链上的可枚举属性)
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
// 只处理自有属性
}
}

Q2: Reflect.apply 比 fn.apply 好在哪?直接用展开运算符不行吗?

三个优势:

  1. 防止 apply 被重写

    1
    2
    3
    4
    5
    6
    7
    8
    function malicious() { return 'hacked' }
    malicious.apply = function() { return 'apply hijacked' }

    // 如果直接用 fn.apply
    malicious.apply(null, [1, 2]) // 'apply hijacked'

    // Reflect.apply 不受影响
    Reflect.apply(malicious, null, [1, 2]) // 'hacked'
  2. 处理 null/undefined 的 thisArg:在某些严格模式下,fn.apply(null) 的 this 会指向全局对象,Reflect.apply(fn, null, []) 则始终将 null 作为 this 传递。

  3. 统一调用方式:Reflect 的 API 风格一致,所有方法都以 target 为第一个参数。

展开运算符不能完全替代 apply 的场景:

  • 需要显式控制 this 上下文
  • 参数已经是数组,不想创建新数组(...args 每次创建新数组)
  • 需要和 Proxy 的 apply trap 配合

Q3: Reflect.get 和 Reflect.set 的 receiver 参数什么时候必须传?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 场景 1:在 Proxy 的 get/set handler 中——永远必须传
const handler = {
get(target, key, receiver) {
return Reflect.get(target, key, receiver) // 必须传 receiver
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver) // 必须传 receiver
}
}

// 场景 2:对象有 getter/setter 且需要控制 this——必须传
const obj = {
get data() { return this._data }
}
Reflect.get(obj, 'data', { _data: 'custom' }) // 'custom'

// 场景 3:没有 getter/setter 的普通属性——可以不传
const obj2 = { name: 'Alice' }
Reflect.get(obj2, 'name') // 'Alice' — receiver 不影响结果

// 最佳实践:在 Proxy handler 中始终传递 receiver
// 在其他场景,如果你确定属性没有 getter/setter,可以不传

Q4: Reflect 有 polyfill 吗?如果浏览器不支持怎么办?

有。core-js 提供了完整的 Reflect polyfill:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// core-js 的 Reflect polyfill 实现思路(简化版)
if (typeof Reflect === 'undefined') {
globalThis.Reflect = {
get(target, key, receiver) {
// 基本实现:非严格模式下的属性读取
const desc = Object.getOwnPropertyDescriptor(target, key)
if (desc && desc.get) {
return desc.get.call(receiver || target)
}
return target[key]
},
set(target, key, value, receiver) {
const desc = Object.getOwnPropertyDescriptor(target, key)
if (desc) {
if (desc.writable === false) return false
if (desc.set) {
desc.set.call(receiver || target, value)
return true
}
}
target[key] = value
return true
},
// ... 其他方法类似
}
}

实际项目中:如果用 Babel + core-js,@babel/preset-envuseBuiltIns: 'usage' 会根据目标浏览器自动引入需要的 Reflect polyfill。

注意:Proxy 无法被完全 polyfill(因为它是引擎级别的能力),所以 Reflect 配合 Proxy 使用时,如果浏览器不支持 Proxy,polyfill 也没有意义。如果你不需要 Proxy,Reflect 可以独立使用,所有 13 个方法都可以被 polyfill。

Q5: Reflect.ownKeys 返回的键顺序有规范保证吗?

有。ES2015+ 规范明确规定了 Reflect.ownKeys 的返回值顺序:

  1. 整数索引(按升序)
  2. 字符串键(按创建顺序)
  3. Symbol 键(按创建顺序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {}
obj[Symbol('c')] = 3
obj['b'] = 2
obj[Symbol('a')] = 4
obj['1'] = 'first'
obj['a'] = 1

console.log(Reflect.ownKeys(obj))
// ['1', 'a', 'b', Symbol(c), Symbol(a)]
// 1. '1' — 整数索引(升序)
// 2. 'a' — 字符串键(创建顺序:b 先于 a? 不对...)
// 实际上创建顺序是:Symbol(c), b, Symbol(a), 1, a
// 按规范:先整数 '1',然后字符串按创建顺序 'b', 'a',然后 Symbol 按创建顺序 Symbol(c), Symbol(a)
// 所以结果应该是:['1', 'b', 'a', Symbol(c), Symbol(a)]

注意:这个顺序和 Object.keys 的顺序一致,也和 JSON.stringify 遍历属性的顺序一致。虽然规范保证了顺序,但实际编码中不应过度依赖,因为使用顺序通常意味着数据结构需要调整。

Q6: 为什么用 Reflect.deleteProperty 而不是 delete?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const obj = { x: 1 }

// delete 操作符
delete obj.x
// 返回 true/false,但写法是语句不是表达式

// 在表达式中使用 delete
const result = delete obj.x // 可以,但 delete 是语句上下文

// Reflect.deleteProperty
const success = Reflect.deleteProperty(obj, 'x')
// 始终返回布尔值,是纯表达式

// 在严格模式下
'use strict'
delete obj.x // 语法上允许
Reflect.deleteProperty(obj, 'x') // 语法上允许,行为一致

// 主要优势:
// 1. 函数式风格,可以在高阶函数中使用
const keys = ['a', 'b', 'c']
keys.forEach(key => Reflect.deleteProperty(obj, key))

// 2. 不会因为严格模式导致语法解析差异
// 3. 与 Proxy 的 deleteProperty trap 对应

8. 推荐学习路径

1
2
3
4
5
6
7
1. 阅读本文【面试速答版】 → 建立整体认知
2. 在浏览器控制台逐个测试 Reflect 的 13 个方法
3. 阅读 MDN 上 Reflect 的官方文档
4. 写一个简单的 Proxy + Reflect 的例子(如属性日志)
5. 手写一个 Reflect.get/Reflect.set 的 polyfill(理解内部机制)
6. 研究 Vue 3 源码中的 Reflect 使用
7. 尝试用 Proxy + Reflect 实现一个简单的响应式系统

关联知识点索引

  • Reflect 最常用的场景 → Proxy(本目录下 Proxy 相关文档)
  • 响应式原理中 Reflect 的角色 → Vue3 响应式原理
  • ES6 元编程能力 → 运算符
  • 属性描述符和对象操作 → 底层原理

事件循环(Event Loop)是 JavaScript 异步编程的核心机制,理解它是成为一名优秀前端工程师的必备技能。本文将从基础概念到浏览器和 Node.js 的实现,全面讲解事件循环。

阅读全文 »

函数是 JavaScript 的一等公民,理解函数的各种特性是掌握 JavaScript 的关键。本文将从基础到高级,全面讲解 JavaScript 函数的各种知识点。

阅读全文 »

目录

  1. 异步编程概述
  2. 回调函数
  3. Promise
  4. Generator 函数
  5. async/await
  6. 异步编程最佳实践
  7. 总结

异步编程概述

什么是异步编程

在 JavaScript 中,异步编程是一种编程模式,它允许程序在等待某些操作(如网络请求、文件读取、定时器等)完成的同时继续执行其他代码。异步编程对于构建响应式的用户界面和高效的网络应用至关重要。

JavaScript 是单线程语言,这意味着它一次只能执行一个任务。如果没有异步编程,代码会阻塞在耗时操作上,导致整个应用变得不响应。

JavaScript 异步编程的演进

JavaScript 异步编程经历了以下几个阶段的发展:

  1. 回调函数时代(早期):使用嵌套的回调函数处理异步操作
  2. Promise 时代(ES6):引入 Promise 对象,提供了更优雅的异步处理方式
  3. Generator 时代(ES6):引入 Generator 函数,可以暂停和恢复执行
  4. async/await 时代(ES2017):引入 async/await 语法糖,使异步代码看起来像同步代码
阅读全文 »

深浅拷贝

浅拷贝

浅拷贝是指创建一个新对象,该对象的属性值与原对象相同,但对于引用类型的属性,仍然共享同一个引用。

实现原理

  • 检查输入是否为对象类型
  • 根据原对象类型(数组或普通对象)创建新对象
  • 遍历原对象的自有属性,将属性值直接复制到新对象
1
2
3
4
5
6
7
8
9
10
function shallowCopy(obj) {
if (typeof obj !== 'object') return obj;
let newObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}

使用场景

  • 当对象属性都是基本类型时
  • 当需要快速创建对象副本,且不关心引用类型属性的共享问题时
  • 当对象结构简单,没有嵌套引用类型时

深拷贝

深拷贝是指创建一个新对象,该对象的所有属性(包括嵌套的引用类型属性)都是原对象的副本,不共享引用。

1. 极简版深拷贝

使用 JSON 序列化和反序列化实现深拷贝,简单易用,但有局限性。

1
2
3
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}

局限性

  • 不能处理函数、正则表达式、Date 等特殊对象
  • 不能处理循环引用
  • 会丢失 undefined 和 Symbol 类型的属性

2. 简单版深拷贝

只考虑普通对象和数组,不处理特殊对象和循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function deepClone(target) {
if (typeof target === 'object' && target !== null) {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key]);
}
}
return cloneTarget;
} else {
return target;
}
};

3. 完整版深拷贝

考虑了循环引用、特殊对象和各种数据类型的深拷贝实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 深拷贝
* @param {any} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/
function deepClone(obj = {}, map = new Map()) {
// 基本类型直接返回
if (typeof obj !== "object" || obj === null) return obj;

// 处理循环引用
if (map.get(obj)) return map.get(obj);

// 处理特殊对象
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Function) return obj;

// 初始化返回结果
let result = Array.isArray(obj) ? [] : {};

// 存储当前对象,防止循环引用
map.set(obj, result);

// 处理普通对象和数组
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key], map);
}
}

// 处理 Symbol 类型的键
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const symbolKey of symbolKeys) {
result[symbolKey] = deepClone(obj[symbolKey], map);
}

return result;
}

深拷贝的使用场景

  • 当对象包含嵌套的引用类型属性时
  • 当需要完全独立的对象副本,避免修改原对象时
  • 当对象结构复杂,需要递归处理时

性能考虑

  • 深拷贝的性能开销较大,特别是对于大型对象
  • 对于频繁操作的场景,考虑使用浅拷贝或其他优化策略
  • 对于特定场景,可以使用更高效的深拷贝库,如 lodash.cloneDeep

事件总线(发布订阅模式)

事件总线是一种实现组件间通信的设计模式,通过发布和订阅事件来实现松耦合的通信机制。

实现原理

  • 维护一个事件缓存对象,存储事件名称和对应的回调函数列表
  • 提供 on 方法注册事件监听器
  • 提供 off 方法移除事件监听器
  • 提供 emit 方法触发事件,执行对应的回调函数

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class EventEmitter {
constructor() {
// 存储事件和对应回调函数的映射
this.events = new Map();
}

/**
* 注册事件监听器
* @param {string} eventName 事件名称
* @param {Function} callback 回调函数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
on(eventName, callback) {
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}

if (!this.events.has(eventName)) {
this.events.set(eventName, []);
}

this.events.get(eventName).push(callback);
return this;
}

/**
* 注册一次性事件监听器
* @param {string} eventName 事件名称
* @param {Function} callback 回调函数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
once(eventName, callback) {
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}

const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};

// 存储原始回调,以便后续可以通过原始回调移除监听器
onceCallback.originalCallback = callback;
return this.on(eventName, onceCallback);
}

/**
* 移除事件监听器
* @param {string} eventName 事件名称
* @param {Function} callback 回调函数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
off(eventName, callback) {
if (!this.events.has(eventName)) {
return this;
}

const callbacks = this.events.get(eventName);
const index = callbacks.findIndex(fn =>
fn === callback || fn.originalCallback === callback
);

if (index > -1) {
callbacks.splice(index, 1);

// 如果没有回调了,删除事件
if (callbacks.length === 0) {
this.events.delete(eventName);
}
}

return this;
}

/**
* 触发事件
* @param {string} eventName 事件名称
* @param {...any} args 传递给回调函数的参数
* @returns {EventEmitter} 返回实例,支持链式调用
*/
emit(eventName, ...args) {
if (!this.events.has(eventName)) {
return this;
}

// 创建副本,防止回调中修改原数组导致的问题
const callbacks = [...this.events.get(eventName)];
callbacks.forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in event ${eventName} callback:`, error);
}
});

return this;
}

/**
* 清空所有事件监听器
* @returns {EventEmitter} 返回实例,支持链式调用
*/
clear() {
this.events.clear();
return this;
}

/**
* 获取指定事件的监听器数量
* @param {string} eventName 事件名称
* @returns {number} 监听器数量
*/
listenerCount(eventName) {
if (!this.events.has(eventName)) {
return 0;
}
return this.events.get(eventName).length;
}

/**
* 获取所有注册的事件名称
* @returns {Array} 事件名称数组
*/
eventNames() {
return Array.from(this.events.keys());
}
}

// 测试
const eventBus = new EventEmitter();

const fn1 = function(name, age) {
console.log(`${name} ${age}`);
};

const fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`);
};

// 注册事件
eventBus.on('greet', fn1);
eventBus.on('greet', fn2);

// 注册一次性事件
eventBus.once('greetOnce', (name) => {
console.log(`Hello ${name}, this is a once event`);
});

// 触发事件
eventBus.emit('greet', '布兰', 12);
// 输出: 布兰 12
// 输出: hello, 布兰 12

eventBus.emit('greetOnce', '布兰');
// 输出: Hello 布兰, this is a once event

// 再次触发一次性事件,不会执行
eventBus.emit('greetOnce', '布兰');

// 移除事件监听器
eventBus.off('greet', fn1);

// 再次触发事件,只有 fn2 执行
eventBus.emit('greet', '布兰', 12);
// 输出: hello, 布兰 12

// 清空所有事件
eventBus.clear();

// 再次触发事件,不会执行
eventBus.emit('greet', '布兰', 12);

事件总线的使用场景

  • 组件间通信:特别是在非父子组件之间
  • 跨模块通信:不同模块之间通过事件进行解耦
  • 事件驱动架构:基于事件的系统设计
  • 异步操作处理:通过事件通知异步操作的完成

优势

  • 松耦合:发布者和订阅者之间没有直接依赖
  • 可扩展性:可以轻松添加新的事件和监听器
  • 灵活性:支持多对多的通信模式
  • 可维护性:事件名称作为通信的契约,使代码更清晰

解析 URL 参数为对象

解析 URL 参数为对象是前端开发中常见的需求,用于获取 URL 中的查询参数并转换为可操作的对象格式。

实现原理

  • 从 URL 中提取查询字符串部分
  • 将查询字符串按 & 分割为参数数组
  • 遍历参数数组,处理每个参数的键值对
  • 对参数值进行解码和类型转换
  • 处理重复键的情况,将其转换为数组

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 解析 URL 参数为对象
* @param {string} url 完整的 URL 字符串
* @returns {Object} 解析后的参数对象
*/
function parseUrlParams(url) {
// 如果没有传入 URL,使用当前页面的 URL
if (!url) {
url = window.location.href;
}

// 提取查询字符串部分
const queryStringMatch = url.match(/\?([^#]+)/);
if (!queryStringMatch) {
return {};
}

const paramsStr = queryStringMatch[1];
const paramsArr = paramsStr.split('&');
const paramsObj = {};

paramsArr.forEach(param => {
if (!param) return; // 跳过空参数

const [key, value] = param.split('=');

// 解码键和值
const decodedKey = decodeURIComponent(key);
let decodedValue = value !== undefined ? decodeURIComponent(value) : true;

// 尝试将值转换为数字
if (typeof decodedValue === 'string' && /^\d+(\.\d+)?$/.test(decodedValue)) {
decodedValue = parseFloat(decodedValue);
}

// 处理布尔值
if (typeof decodedValue === 'string') {
if (decodedValue.toLowerCase() === 'true') {
decodedValue = true;
} else if (decodedValue.toLowerCase() === 'false') {
decodedValue = false;
} else if (decodedValue.toLowerCase() === 'null') {
decodedValue = null;
} else if (decodedValue.toLowerCase() === 'undefined') {
decodedValue = undefined;
}
}

// 处理重复键
if (paramsObj.hasOwnProperty(decodedKey)) {
// 如果已经是数组,直接 push
if (Array.isArray(paramsObj[decodedKey])) {
paramsObj[decodedKey].push(decodedValue);
} else {
// 否则转换为数组
paramsObj[decodedKey] = [paramsObj[decodedKey], decodedValue];
}
} else {
paramsObj[decodedKey] = decodedValue;
}
});

return paramsObj;
}

// 测试
const url = 'https://example.com?name=布兰&age=12&active=true&score=95.5&tags=js&tags=html&tags=css';
const params = parseUrlParams(url);
console.log(params);
// 输出: {
// name: '布兰',
// age: 12,
// active: true,
// score: 95.5,
// tags: ['js', 'html', 'css']
// }

使用场景

  • 获取 URL 中的查询参数
  • 处理用户通过 URL 传递的配置信息
  • 实现页面间的数据传递
  • 构建和解析 API 请求参数

注意事项

  • 处理 URL 编码:使用 decodeURIComponent 解码参数值
  • 处理类型转换:将数字、布尔值等字符串转换为对应类型
  • 处理重复键:将重复的参数键转换为数组
  • 处理边缘情况:如空参数、无值参数等

现代替代方案

在现代浏览器中,可以使用 URLSearchParams API 来更方便地解析 URL 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function parseUrlParamsModern(url) {
const urlObj = new URL(url || window.location.href);
const params = new URLSearchParams(urlObj.search);
const result = {};

for (const [key, value] of params.entries()) {
if (result.hasOwnProperty(key)) {
if (Array.isArray(result[key])) {
result[key].push(value);
} else {
result[key] = [result[key], value];
}
} else {
result[key] = value;
}
}

return result;
}

性能考虑

  • 对于简单的 URL 参数解析,原生实现和 URLSearchParams 都有良好的性能
  • 对于复杂的 URL 参数处理,URLSearchParams 提供了更简洁和可靠的 API
  • 在需要兼容旧浏览器的情况下,使用原生实现

字符串模板

字符串模板是一种将数据动态插入到字符串中的技术,常用于生成 HTML、邮件内容、配置文件等。

实现原理

  • 使用正则表达式匹配模板中的占位符(如 {{variable}}
  • 查找数据对象中对应的值
  • 替换模板中的占位符为实际值
  • 递归处理所有占位符,直到模板中没有占位符为止

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 渲染字符串模板
* @param {string} template 模板字符串
* @param {Object} data 数据对象
* @returns {string} 渲染后的字符串
*/
function renderTemplate(template, data) {
// 匹配 {{variable}} 格式的占位符
const reg = /\{\{([^}]+)\}\}/g;

// 使用 replace 方法处理所有匹配项
return template.replace(reg, (match, key) => {
// 去除 key 两端的空格
const trimmedKey = key.trim();

// 处理嵌套属性,如 {{user.name}}
const keys = trimmedKey.split('.');
let value = data;

// 遍历嵌套键,获取最终值
for (const k of keys) {
if (value === undefined || value === null) {
break;
}
value = value[k];
}

// 处理未定义的值
return value !== undefined && value !== null ? value : '';
});
}

// 测试
const template = '我是{{name}},年龄{{age}},性别{{sex}},邮箱{{contact.email}}';
const person = {
name: '布兰',
age: 12,
contact: {
email: 'bran@example.com'
}
};

const result = renderTemplate(template, person);
console.log(result);
// 输出: 我是布兰,年龄12,性别,邮箱bran@example.com

高级实现(支持条件和循环)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 高级模板渲染器
* @param {string} template 模板字符串
* @param {Object} data 数据对象
* @returns {string} 渲染后的字符串
*/
function advancedRender(template, data) {
// 处理条件语句 {{if condition}}...{{/if}}
template = template.replace(/\{\{if\s+([^}]+)\}\}(.*?)\{\{\/if\}\}/gs, (match, condition, content) => {
// 简单的条件表达式求值
try {
// 创建一个安全的求值环境
const evalFn = new Function('data', `return (${condition.trim()});`);
const result = evalFn(data);
return result ? content : '';
} catch (e) {
console.error('Error evaluating condition:', e);
return '';
}
});

// 处理循环语句 {{each array as item}}...{{/each}}
template = template.replace(/\{\{each\s+([^\s]+)\s+as\s+([^}]+)\}\}(.*?)\{\{\/each\}\}/gs, (match, arrayName, itemName, content) => {
const array = data[arrayName.trim()];
if (!Array.isArray(array)) {
return '';
}

return array.map((item, index) => {
// 替换循环体内的 item 变量
return content.replace(new RegExp(`\\{\\{${itemName.trim()}\\}\\}`, 'g'), item)
.replace(new RegExp(`\\{\\{${itemName.trim()}\.index\\}\\}`, 'g'), index);
}).join('');
});

// 处理普通变量
template = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const trimmedKey = key.trim();
const keys = trimmedKey.split('.');
let value = data;

for (const k of keys) {
if (value === undefined || value === null) {
break;
}
value = value[k];
}

return value !== undefined && value !== null ? value : '';
});

return template;
}

// 测试高级模板
const advancedTemplate = `
<h1>{{name}}</h1>
{{if age >= 18}}
<p>成年人</p>
{{else}}
<p>未成年人</p>
{{/if}}
<h2>兴趣爱好</h2>
{{each hobbies as hobby}}
<li>{{hobby}}</li>
{{/each}}
`;

const userData = {
name: '布兰',
age: 12,
hobbies: ['编程', '读书', '运动']
};

const advancedResult = advancedRender(advancedTemplate, userData);
console.log(advancedResult);
// 输出包含渲染后的 HTML 内容

使用场景

  • 生成动态 HTML 内容
  • 生成邮件模板
  • 生成配置文件
  • 生成报表或文档

注意事项

  • 处理未定义的值:当数据对象中没有对应键时,应提供默认值或空字符串
  • 处理嵌套属性:支持 {{user.name}} 这样的嵌套属性访问
  • 防止 XSS 攻击:如果模板内容来自用户输入,需要进行适当的转义
  • 性能考虑:对于频繁渲染的场景,考虑使用更高效的模板引擎

现代替代方案

在现代 JavaScript 中,可以使用模板字面量(Template Literals)来实现简单的字符串模板:

1
2
3
4
5
6
7
8
9
10
11
12
function renderWithTemplateLiterals(data) {
return `我是${data.name},年龄${data.age},性别${data.sex || ''}`;
}

// 测试
const person = {
name: '布兰',
age: 12
};

console.log(renderWithTemplateLiterals(person));
// 输出: 我是布兰,年龄12,性别

对于更复杂的场景,可以使用专业的模板引擎,如 Handlebars、Mustache 或 EJS 等。

图片懒加载

图片懒加载是一种优化技术,用于延迟加载页面上的图片,只有当图片进入视口时才会加载,从而提高页面加载速度和减少带宽使用。

实现原理

  • 监听滚动事件,检测图片是否进入视口
  • 当图片进入视口时,将 data-src 属性的值赋给 src 属性
  • 加载完成后从列表中移除图片,避免重复处理
  • 所有图片加载完成后移除事件监听器,减少性能消耗

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* 图片懒加载实现
* @param {string} selector - 图片选择器,默认为 'img'
* @param {Object} options - 配置选项
*/
function imgLazyLoad(selector = 'img', options = {}) {
const {
root = null,
rootMargin = '0px',
threshold = 0
} = options;

let imgList = [...document.querySelectorAll(selector)];
let length = imgList.length;

if (length === 0) return;

// 检查是否支持 Intersection Observer
if ('IntersectionObserver' in window) {
// 使用 Intersection Observer 实现
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.classList.add('lazy-loaded');
observer.unobserve(img);
length--;
if (length === 0) {
observer.disconnect();
}
}
}
});
}, {
root,
rootMargin,
threshold
});

imgList.forEach((img) => observer.observe(img));

} else {
// 传统滚动监听实现
let count = 0;

const loadImages = function() {
let deleteIndexList = [];
imgList.forEach((img, index) => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom >= 0) {
if (img.dataset.src) {
img.src = img.dataset.src;
img.classList.add('lazy-loaded');
deleteIndexList.push(index);
count++;
if (count === length) {
window.removeEventListener('scroll', loadImages);
window.removeEventListener('resize', loadImages);
window.removeEventListener('orientationchange', loadImages);
}
}
}
});
imgList = imgList.filter((_, index) => !deleteIndexList.includes(index));
};

// 添加防抖处理
const debouncedLoad = function(func, wait) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(func, wait);
};
};

const debouncedLoadImages = debouncedLoad(loadImages, 100);

// 监听滚动、 resize 和 orientationchange 事件
window.addEventListener('scroll', debouncedLoadImages);
window.addEventListener('resize', debouncedLoadImages);
window.addEventListener('orientationchange', debouncedLoadImages);

// 初始检查
loadImages();
}
}

// 测试
imgLazyLoad('img[data-src]');

使用场景

  • 长页面包含大量图片时
  • 移动端页面,减少初始加载时间和流量消耗
  • 图片库或相册页面
  • 电商网站的商品列表页

注意事项

  • 为图片设置占位符,避免页面布局跳动
  • 确保 data-src 属性包含完整的图片 URL
  • 考虑添加加载动画,提升用户体验
  • 对于关键图片,不建议使用懒加载

现代替代方案

在现代浏览器中,可以使用原生的 loading 属性来实现图片懒加载:

1
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="描述">

loading 属性支持三个值:

  • lazy:延迟加载图片,直到它进入视口
  • eager:立即加载图片
  • auto:由浏览器决定是否延迟加载

性能考虑

  • Intersection Observer 比传统的滚动监听性能更好,因为它是浏览器原生实现的
  • 防抖处理可以减少滚动事件的触发频率,提高性能
  • 图片加载完成后及时移除事件监听器,避免内存泄漏
  • 对于大量图片的页面,考虑使用虚拟滚动技术

参考:图片懒加载

函数防抖

函数防抖(Debounce)是一种优化技术,用于限制高频事件的执行频率。当事件被触发后,等待一段时间再执行回调函数,如果在这段时间内事件再次被触发,则重新计时。

实现原理

  • 维护一个计时器,记录上次触发事件的时间
  • 当事件触发时,清除之前的计时器并重新设置
  • 只有当事件停止触发一段时间后,才会执行回调函数

简单版实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 简单版防抖函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖处理后的函数
*/
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}

最终版实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 防抖函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @param {boolean} immediate - 是否立即执行
* @returns {Function} 防抖处理后的函数
*/
function debounce(func, wait, immediate) {
var timeout, result;
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function(){
result = func.apply(context, args)
}, wait);
}
return result;
};

/**
* 取消防抖
*/
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};

return debounced;
}

使用场景

  • 搜索框输入:用户输入时不立即搜索,等待用户停止输入后再执行
  • 窗口 resize 事件:调整窗口大小时,等待调整完成后再执行布局计算
  • 按钮点击:防止用户重复点击提交表单
  • 滚动事件:滚动时不立即触发计算,等待滚动停止后再执行

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 示例1:搜索框输入
const searchInput = document.getElementById('search');
const searchDebounced = debounce(function(e) {
console.log('搜索:', e.target.value);
// 执行搜索逻辑
}, 300);

searchInput.addEventListener('input', searchDebounced);

// 示例2:窗口 resize
const resizeDebounced = debounce(function() {
console.log('窗口大小:', window.innerWidth, 'x', window.innerHeight);
// 执行布局调整
}, 200);

window.addEventListener('resize', resizeDebounced);

// 示例3:按钮点击
const submitBtn = document.getElementById('submit');
const submitDebounced = debounce(function() {
console.log('提交表单');
// 执行表单提交
}, 1000, true); // 立即执行

submitBtn.addEventListener('click', submitDebounced);

函数节流

函数节流(Throttle)是一种优化技术,用于限制函数的执行频率,确保在一定时间内只执行一次。

实现原理

  • 记录上次执行函数的时间
  • 当事件触发时,检查当前时间与上次执行时间的差
  • 如果时间差大于等于指定的时间间隔,则执行函数并更新上次执行时间

时间戳实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 时间戳版节流函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 时间间隔(毫秒)
* @returns {Function} 节流处理后的函数
*/
function throttle(func, wait) {
var previous = 0;
return function() {
var now = +new Date();
var context = this;
var args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}

定时器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 定时器版节流函数
* @param {Function} func - 要执行的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 节流处理后的函数
*/
function throttle(fn, delay) {
let wait = false;
return (...args) => {
if (wait) {
return;
}
fn(...args);
wait = true;
setTimeout(() => {
wait = false;
}, delay);
};
}

最终版实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 节流函数
* @param {Function} func - 要执行的函数
* @param {number} wait - 时间间隔(毫秒)
* @param {Object} options - 配置选项
* @returns {Function} 节流处理后的函数
*/
function throttle(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};

var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
if (!timeout) context = args = null;
};

var throttled = function() {
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
};

/**
* 取消节流
*/
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}

return throttled;
}

使用场景

  • 滚动事件:滚动时定期执行某些操作,如更新滚动位置、加载更多内容
  • 鼠标移动:跟踪鼠标位置,但不需要实时更新
  • 游戏中的动画:限制动画更新频率,提高性能
  • API 请求:限制请求频率,避免过多请求导致服务器压力

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 示例1:滚动事件
const scrollThrottled = throttle(function() {
console.log('滚动位置:', window.scrollY);
// 执行滚动相关逻辑
}, 100);

window.addEventListener('scroll', scrollThrottled);

// 示例2:鼠标移动
const mouseMoveThrottled = throttle(function(e) {
console.log('鼠标位置:', e.clientX, e.clientY);
// 执行鼠标跟踪逻辑
}, 50);

document.addEventListener('mousemove', mouseMoveThrottled);

// 示例3:API 请求
const apiRequestThrottled = throttle(function(query) {
console.log('请求 API:', query);
// 执行 API 请求
}, 1000);

// 连续调用只会执行一次
apiRequestThrottled('query1');
apiRequestThrottled('query2');
apiRequestThrottled('query3');

防抖与节流的区别

  • 防抖:事件触发后等待一段时间执行,若期间再次触发则重新计时
  • 节流:事件触发后立即执行,然后在指定时间内不再执行
  • 使用场景:防抖适用于需要等待用户操作完成后再执行的场景,节流适用于需要定期执行的场景

参考:JavaScript 专题之跟着 underscore 学防抖
参考:JavaScript 专题之跟着 underscore 学节流

函数柯里化

函数柯里化(Currying)是一种函数式编程技术,将接受多个参数的函数转换为一系列接受单个参数的函数。

实现原理

  • 接收一个函数和部分参数
  • 返回一个新函数,该函数接收剩余参数
  • 当所有参数都收集完毕后,执行原始函数

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 函数柯里化
* @param {Function} fn - 要柯里化的函数
* @returns {Function} 柯里化后的函数
*/
function curry(fn) {
let judge = (...args) => {
if (args.length == fn.length) return fn(...args)
return (...arg) => judge(...args, ...arg)
}
return judge
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 示例1:基本用法
function add(a, b, c) {
return a + b + c
}

let addCurry = curry(add)
console.log(addCurry(1)(2)(3)); // 6
console.log(addCurry(1, 2)(3)); // 6
console.log(addCurry(1)(2, 3)); // 6

// 示例2:更复杂的函数
function createUser(name, age, email) {
return {
name,
age,
email
};
}

const createUserCurry = curry(createUser);
const createAdultUser = createUserCurry('_', 18);
const createAdultJohn = createAdultUser('John');

console.log(createAdultJohn('john@example.com'));
// { name: 'John', age: 18, email: 'john@example.com' }

应用场景

  • 参数复用:当某些参数在多次调用中重复时,可以通过柯里化固定这些参数
  • 延迟执行:将函数的执行推迟到所有参数都收集完毕
  • 函数组合:便于函数之间的组合和链式调用
  • 类型检查:可以在每个参数传入时进行类型检查

高级实现(支持占位符)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 支持占位符的柯里化函数
* @param {Function} fn - 要柯里化的函数
* @param {*} placeholder - 占位符,默认为 '_'
* @returns {Function} 柯里化后的函数
*/
function curryWithPlaceholder(fn, placeholder = '_') {
return function curried(...args) {
// 过滤掉占位符
const filteredArgs = args.slice(0, fn.length).filter(arg => arg !== placeholder);

// 如果参数足够,执行函数
if (filteredArgs.length >= fn.length) {
return fn(...filteredArgs);
}

// 否则返回一个新函数,继续收集参数
return function(...nextArgs) {
// 替换占位符
const combinedArgs = args.map(arg =>
arg === placeholder && nextArgs.length > 0 ? nextArgs.shift() : arg
);
return curried(...combinedArgs, ...nextArgs);
};
};
}

// 测试
const add = (a, b, c) => a + b + c;
const addCurry = curryWithPlaceholder(add);

console.log(addCurry(1)(2)(3)); // 6
console.log(addCurry('_', 2)(1)(3)); // 6
console.log(addCurry(1, '_', 3)(2)); // 6

偏函数

偏函数(Partial Application)是一种函数式编程技术,将一个函数的部分参数固定,返回一个接受剩余参数的新函数。

实现原理

  • 接收一个函数和部分参数
  • 返回一个新函数,该函数接收剩余参数
  • 当调用新函数时,将固定参数和新参数合并后传递给原始函数

基本实现

1
2
3
4
5
6
7
8
9
10
11
/**
* 偏函数
* @param {Function} fn - 要应用偏函数的函数
* @param {...any} args - 要固定的参数
* @returns {Function} 偏函数
*/
function partial(fn, ...args) {
return (...arg) => {
return fn(...args, ...arg)
}
}

支持占位符的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 支持占位符的偏函数
* @param {Function} fn - 要应用偏函数的函数
* @param {...any} args - 要固定的参数,使用 '_' 作为占位符
* @returns {Function} 偏函数
*/
function partial(fn, ...args) {
return (...arg) => {
let fullArgs = [...args];
let argIndex = 0;

// 替换占位符
for (let i = 0; i < fullArgs.length && argIndex < arg.length; i++) {
if (fullArgs[i] === '_') {
fullArgs[i] = arg[argIndex++];
}
}

// 添加剩余参数
if (argIndex < arg.length) {
fullArgs = fullArgs.concat(arg.slice(argIndex));
}

return fn(...fullArgs);
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 示例1:基本用法
function add(a, b, c) {
return a + b + c
}

let partialAdd = partial(add, 1)
console.log(partialAdd(2, 3)); // 6

// 示例2:使用占位符
function clg(a, b, c) {
console.log(a, b, c)
}

let partialClg = partial(clg, '_', 2)
partialClg(1, 3); // 依次打印:1, 2, 3

// 示例3:实际应用
function fetchData(url, method, headers, data) {
console.log(`Fetching ${url} with ${method}`, { headers, data });
// 实际的 fetch 逻辑
}

// 创建一个 GET 请求的偏函数
const get = partial(fetchData, '_', 'GET');

// 创建一个带默认 headers 的 GET 请求
const getWithHeaders = partial(get, '_', { 'Content-Type': 'application/json' });

// 使用
getWithHeaders('https://api.example.com/data');
// 等同于 fetchData('https://api.example.com/data', 'GET', { 'Content-Type': 'application/json' });

应用场景

  • 固定默认参数:为函数设置默认参数,减少重复代码
  • 简化函数调用:将复杂函数简化为更具针对性的函数
  • 函数适配:将一个函数的接口适配到另一个函数的接口
  • 参数预填充:在函数调用前预先填充一些参数

柯里化与偏函数的区别

  • 柯里化:将多参数函数转换为一系列单参数函数,每次只接收一个参数
  • 偏函数:固定函数的部分参数,返回一个接收剩余参数的新函数
  • 执行时机:柯里化在所有参数收集完毕后执行,偏函数在调用时立即执行

现代 JavaScript 中的应用

在现代 JavaScript 中,可以使用箭头函数和展开运算符来简化偏函数的创建:

1
2
3
4
5
6
7
8
9
// 使用箭头函数创建偏函数
const add = (a, b, c) => a + b + c;
const add5 = (b, c) => add(5, b, c);

// 使用 bind 方法
const add10 = add.bind(null, 10);

console.log(add5(2, 3)); // 10
console.log(add10(2, 3)); // 15

JSONP

JSONP(JSON with Padding)是一种跨域请求技术,利用 script 标签不受同源策略限制的特性来实现跨域数据获取。

实现原理

  • 创建一个 script 标签,将请求 URL 设置为其 src 属性
  • URL 中包含回调函数名称,服务器将数据包装在该回调函数中返回
  • script 标签加载完成时,浏览器会执行返回的 JavaScript 代码,调用回调函数并传入数据

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* JSONP 请求
* @param {Object} options - 配置选项
* @param {string} options.url - 请求 URL
* @param {Object} options.params - 请求参数
* @param {string} [options.callbackName] - 回调函数名称,默认为随机生成
* @param {number} [options.timeout] - 超时时间(毫秒)
* @returns {Promise} 返回 Promise 对象
*/
const jsonp = ({ url, params, callbackName = `jsonp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timeout = 5000 }) => {
return new Promise((resolve, reject) => {
// 生成完整的 URL
const generateUrl = () => {
let dataSrc = '';
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}&`;
}
}
dataSrc += `callback=${callbackName}`;
return `${url}?${dataSrc}`;
};

// 创建 script 标签
const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
scriptEle.type = 'text/javascript';
scriptEle.async = true;

// 超时处理
const timeoutId = setTimeout(() => {
reject(new Error('JSONP request timeout'));
cleanup();
}, timeout);

// 回调函数
window[callbackName] = data => {
resolve(data);
cleanup();
};

// 错误处理
scriptEle.onerror = () => {
reject(new Error('JSONP request failed'));
cleanup();
};

// 清理函数
const cleanup = () => {
clearTimeout(timeoutId);
if (scriptEle.parentNode) {
scriptEle.parentNode.removeChild(scriptEle);
}
delete window[callbackName];
};

// 添加到文档
document.body.appendChild(scriptEle);
});
};

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 示例:使用 JSONP 获取天气数据
jsonp({
url: 'https://api.example.com/weather',
params: {
city: 'Beijing',
unit: 'celsius'
},
timeout: 3000
})
.then(data => {
console.log('天气数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});

优缺点

  • 优点:兼容性好,支持所有浏览器,包括旧版本浏览器
  • 缺点:只能用于 GET 请求,存在安全风险(如 XSS 攻击),无法处理错误状态码

AJAX

AJAX(Asynchronous JavaScript and XML)是一种在不重新加载整个页面的情况下,与服务器交换数据并更新部分页面的技术。

实现原理

  • 创建 XMLHttpRequest 对象
  • 配置请求参数(方法、URL、是否异步等)
  • 发送请求
  • 监听请求状态变化,处理响应

优化实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 发送 AJAX 请求
* @param {Object} options - 配置选项
* @param {string} options.url - 请求 URL
* @param {string} [options.method] - 请求方法,默认为 'GET'
* @param {Object} [options.params] - URL 参数
* @param {Object} [options.data] - 请求体数据
* @param {Object} [options.headers] - 请求头
* @param {number} [options.timeout] - 超时时间(毫秒)
* @returns {Promise} 返回 Promise 对象
*/
const ajax = ({ url, method = 'GET', params, data, headers = {}, timeout = 10000 }) => {
return new Promise((resolve, reject) => {
// 处理 URL 参数
if (params) {
const searchParams = new URLSearchParams();
for (const key in params) {
if (params.hasOwnProperty(key)) {
searchParams.append(key, params[key]);
}
}
url += `?${searchParams.toString()}`;
}

// 创建 XMLHttpRequest 对象
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');

// 配置超时
xhr.timeout = timeout;

// 监听状态变化
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;

// 解析响应数据
let responseData;
try {
responseData = JSON.parse(xhr.responseText);
} catch (e) {
responseData = xhr.responseText;
}

// 处理响应
if (xhr.status >= 200 && xhr.status < 300) {
resolve({
data: responseData,
status: xhr.status,
statusText: xhr.statusText
});
} else {
reject({
error: new Error(`HTTP error! status: ${xhr.status}`),
status: xhr.status,
statusText: xhr.statusText,
data: responseData
});
}
};

// 错误处理
xhr.onerror = function() {
reject(new Error('Network error occurred'));
};

// 超时处理
xhr.ontimeout = function() {
reject(new Error('Request timeout'));
};

// 打开连接
xhr.open(method, url, true);

// 设置请求头
for (const key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}

// 设置默认 Content-Type
if (!headers['Content-Type'] && data) {
if (typeof data === 'object' && data !== null) {
xhr.setRequestHeader('Content-Type', 'application/json');
data = JSON.stringify(data);
} else {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
}

// 发送请求
xhr.send(data);
});
};

// 便捷方法
const get = (url, options = {}) => ajax({ ...options, url, method: 'GET' });
const post = (url, data, options = {}) => ajax({ ...options, url, method: 'POST', data });
const put = (url, data, options = {}) => ajax({ ...options, url, method: 'PUT', data });
const del = (url, options = {}) => ajax({ ...options, url, method: 'DELETE' });

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 示例1:GET 请求
get('https://api.example.com/users', {
params: { page: 1, limit: 10 },
headers: {
'Authorization': 'Bearer token123'
}
})
.then(response => {
console.log('用户列表:', response.data);
})
.catch(error => {
console.error('请求失败:', error);
});

// 示例2:POST 请求
post('https://api.example.com/users', {
name: 'John',
email: 'john@example.com'
})
.then(response => {
console.log('创建用户成功:', response.data);
})
.catch(error => {
console.error('请求失败:', error);
});

现代替代方案

在现代 JavaScript 中,fetch API 是 AJAX 的更现代替代方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 使用 fetch API 发送请求
* @param {string} url - 请求 URL
* @param {Object} options - 配置选项
* @returns {Promise} 返回 Promise 对象
*/
const fetchData = (url, options = {}) => {
const defaultOptions = {
headers: {
'Content-Type': 'application/json'
}
};

const mergedOptions = {
...defaultOptions,
...options
};

return fetch(url, mergedOptions)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
});
};

// 使用示例
fetchData('https://api.example.com/users')
.then(data => console.log('用户列表:', data))
.catch(error => console.error('请求失败:', error));

优缺点

  • 优点
    • 支持多种请求方法
    • 可以发送和接收各种数据格式
    • 支持错误处理和超时设置
    • 现代浏览器支持良好
  • 缺点
    • 旧浏览器不支持(需要 polyfill)
    • 实现相对复杂

注意事项

  • 跨域问题:需要服务器设置 CORS 头
  • 安全问题:避免发送敏感信息,使用 HTTPS
  • 性能问题:对于频繁请求,考虑使用缓存
  • 错误处理:总是处理可能的错误情况

new 运算符

new 运算符用于创建一个构造函数的实例对象,它会执行以下操作:

实现原理

  1. 创建一个新的空对象
  2. 将新对象的原型指向构造函数的 prototype 属性
  3. 将构造函数的 this 绑定到新对象上
  4. 执行构造函数代码
  5. 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 模拟实现 new 运算符
* @param {Function} Constructor - 构造函数
* @param {...any} args - 构造函数参数
* @returns {Object} 实例对象
*/
function objectFactory(Constructor, ...args) {
// 1. 创建一个新的空对象
var obj = new Object();

// 2. 将新对象的原型指向构造函数的 prototype 属性
obj.__proto__ = Constructor.prototype;

// 3. 将构造函数的 this 绑定到新对象上并执行构造函数
var ret = Constructor.apply(obj, args);

// 4. 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
return typeof ret === 'object' ? ret || obj : obj;
};

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 示例1:基本用法
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}, I'm ${this.age} years old.`);
};

let p = objectFactory(Person, '布兰', 12);
console.log(p); // { name: '布兰', age: 12 }
p.sayHello(); // Hello, my name is 布兰, I'm 12 years old.

// 示例2:构造函数返回对象
function PersonWithReturn(name, age) {
this.name = name;
return {
age: age,
greeting: `Hello, I'm ${age} years old.`
};
}

let p2 = objectFactory(PersonWithReturn, '布兰', 12);
console.log(p2); // { age: 12, greeting: 'Hello, I'm 12 years old.' }
console.log(p2.name); // undefined

// 示例3:构造函数返回 null
function PersonWithNullReturn(name, age) {
this.name = name;
this.age = age;
return null;
}

let p3 = objectFactory(PersonWithNullReturn, '布兰', 12);
console.log(p3); // { name: '布兰', age: 12 }

注意事项

  • 构造函数必须是函数,如果不是函数会抛出错误
  • 如果构造函数返回一个对象(包括数组、函数等),则返回该对象
  • 如果构造函数返回 null 或基本类型,则返回新创建的对象
  • 新对象的原型链会指向构造函数的 prototype

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

实现原理

  1. 获取左侧对象的原型
  2. 检查该原型是否等于右侧构造函数的 prototype 属性
  3. 如果是,返回 true;否则,继续向上查找原型链
  4. 如果查找到原型链末端(null)仍未找到,则返回 false

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 模拟实现 instanceof 运算符
* @param {any} left - 实例对象
* @param {Function} right - 构造函数
* @returns {boolean} 是否是指定构造函数的实例
*/
function instanceOf(left, right) {
// 获取右侧构造函数的 prototype
const prototype = right.prototype;

// 获取左侧对象的原型
let proto = Object.getPrototypeOf(left);

// 遍历原型链
while (true) {
// 如果原型为 null,说明已经到达原型链末端
if (proto === null) return false;

// 如果找到匹配的原型
if (proto === prototype) {
return true;
}

// 继续向上查找原型链
proto = Object.getPrototypeOf(proto);
}
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 示例1:基本用法
function Person() {}
function Animal() {}

const person = new Person();
const animal = new Animal();

console.log(instanceOf(person, Person)); // true
console.log(instanceOf(person, Object)); // true
console.log(instanceOf(person, Animal)); // false
console.log(instanceOf(animal, Animal)); // true
console.log(instanceOf(animal, Object)); // true

// 示例2:数组和对象
const arr = [];
const obj = {};

console.log(instanceOf(arr, Array)); // true
console.log(instanceOf(arr, Object)); // true
console.log(instanceOf(obj, Object)); // true
console.log(instanceOf(obj, Array)); // false

// 示例3:函数
function fn() {}

console.log(instanceOf(fn, Function)); // true
console.log(instanceOf(fn, Object)); // true

注意事项

  • instanceof 只能用于对象,不能用于基本类型
  • instanceof 检查的是原型链,而不是直接比较类型
  • 可以通过修改对象的原型链来改变 instanceof 的结果
  • 在多个全局环境下(如 iframe),instanceof 可能会出现意外结果,因为不同环境的构造函数是不同的对象

与 typeof 的区别

  • typeof 用于判断基本类型,返回字符串(如 ‘string’, ‘number’, ‘boolean’, ‘object’, ‘function’, ‘undefined’, ‘symbol’)
  • instanceof 用于判断对象是否是某个构造函数的实例,返回布尔值
  • typeof null 返回 ‘object’,这是一个历史遗留问题
  • typeof [] 返回 ‘object’,无法区分数组和普通对象

实际应用

  • 检查对象类型,确保传入的参数类型正确
  • 实现多态,根据对象类型执行不同的操作
  • 验证继承关系,确保对象符合预期的接口

Object.create()

Object.create() 方法创建一个新对象,使用指定的原型对象和属性来初始化。

实现原理

  1. 检查传入的原型对象是否为对象或 null
  2. 创建一个空构造函数 F
  3. 将构造函数 F 的 prototype 指向传入的原型对象
  4. 使用构造函数 F 创建一个新实例
  5. 如果提供了属性描述符对象,则使用 Object.defineProperties 添加属性
  6. 如果原型为 null,则创建一个没有原型的对象

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 模拟实现 Object.create() 方法
* @param {Object|null} proto - 新创建对象的原型对象
* @param {Object} [propertyObject] - 可选,要添加到新对象的属性描述符
* @returns {Object} 新创建的对象
*/
Object.create2 = function(proto, propertyObject = undefined) {
// 检查原型对象类型
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.');
}

// 创建一个空构造函数
function F() {}

// 将构造函数的 prototype 指向传入的原型对象
F.prototype = proto;

// 使用构造函数创建新实例
const obj = new F();

// 如果提供了属性描述符对象,添加属性
if (propertyObject !== undefined) {
Object.defineProperties(obj, propertyObject);
}

// 如果原型为 null,创建一个没有原型的对象
if (proto === null) {
obj.__proto__ = null;
}

return obj;
};

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 示例1:基本用法
const person = {
name: '布兰',
age: 12,
sayHello: function() {
console.log(`Hello, my name is ${this.name}`);
}
};

// 创建一个以 person 为原型的新对象
const student = Object.create(person, {
grade: {
value: '6th',
writable: true,
enumerable: true,
configurable: true
}
});

console.log(student.name); // 布兰(继承自原型)
console.log(student.grade); // 6th(自身属性)
student.sayHello(); // Hello, my name is 布兰

// 示例2:创建没有原型的对象
const emptyObj = Object.create(null);
console.log(emptyObj); // {} 没有原型
console.log(emptyObj.toString); // undefined

// 示例3:实现继承
function Parent(name) {
this.name = name;
}

Parent.prototype.sayName = function() {
console.log(`My name is ${this.name}`);
};

function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

// 使用 Object.create 实现继承
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
writable: true,
enumerable: false,
configurable: true
},
sayAge: {
value: function() {
console.log(`I'm ${this.age} years old`);
},
writable: true,
enumerable: true,
configurable: true
}
});

const child = new Child('布兰', 12);
child.sayName(); // My name is 布兰
child.sayAge(); // I'm 12 years old

应用场景

  • 实现继承:通过指定原型对象来实现对象间的继承关系
  • 创建特殊对象:创建没有原型的对象,避免原型链污染
  • 属性描述符控制:通过第二个参数精确控制新对象的属性
  • 原型链隔离:创建一个干净的对象,只包含指定的属性和方法

Object.assign()

Object.assign() 方法用于将所有可枚举的自有属性从一个或多个源对象复制到目标对象,返回目标对象。

实现原理

  1. 检查目标对象是否为 null 或 undefined
  2. 将目标对象转换为对象类型
  3. 遍历所有源对象
  4. 遍历每个源对象的自有可枚举属性
  5. 将属性值复制到目标对象
  6. 返回目标对象

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 模拟实现 Object.assign() 方法
* @param {Object} target - 目标对象
* @param {...Object} source - 源对象
* @returns {Object} 目标对象
*/
Object.assign2 = function(target, ...source) {
// 检查目标对象
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}

// 将目标对象转换为对象类型
let ret = Object(target);

// 遍历所有源对象
source.forEach(function(obj) {
if (obj != null) {
// 遍历源对象的自有可枚举属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key];
}
}
}
});

return ret;
};

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 示例1:基本用法
const target = { a: 1, b: 2 };
const source1 = { b: 3, c: 4 };
const source2 = { d: 5 };

const result = Object.assign(target, source1, source2);
console.log(result); // { a: 1, b: 3, c: 4, d: 5 }
console.log(target); // { a: 1, b: 3, c: 4, d: 5 }(目标对象被修改)

// 示例2:合并对象
const user = {
name: '布兰',
age: 12
};

const address = {
city: '北京',
district: '朝阳区'
};

const contact = {
email: 'bran@example.com',
phone: '1234567890'
};

const userInfo = Object.assign({}, user, address, contact);
console.log(userInfo);
// {
// name: '布兰',
// age: 12,
// city: '北京',
// district: '朝阳区',
// email: 'bran@example.com',
// phone: '1234567890'
// }

// 示例3:克隆对象
const original = { a: 1, b: { c: 2 } };
const clone = Object.assign({}, original);
console.log(clone); // { a: 1, b: { c: 2 } }
console.log(clone.b === original.b); // true(浅拷贝)

// 示例4:默认值
function createUser(options) {
const defaultOptions = {
name: '匿名',
age: 18,
active: true
};
return Object.assign({}, defaultOptions, options);
}

const user1 = createUser({ name: '布兰', age: 12 });
console.log(user1); // { name: '布兰', age: 12, active: true }

const user2 = createUser({ active: false });
console.log(user2); // { name: '匿名', age: 18, active: false }

注意事项

  • 浅拷贝Object.assign() 只进行浅拷贝,对于嵌套对象,只会复制引用
  • 目标对象修改:目标对象会被修改,返回的是修改后的目标对象
  • 可枚举属性:只复制可枚举的自有属性
  • 属性覆盖:如果多个源对象有相同的属性,后面的会覆盖前面的
  • 类型转换:目标对象会被转换为对象类型,如果是基本类型会被包装

应用场景

  • 对象合并:将多个对象合并为一个
  • 对象克隆:创建对象的浅拷贝
  • 默认值设置:为函数参数设置默认值
  • 属性复制:将一个对象的属性复制到另一个对象
  • 配置合并:合并默认配置和用户配置

JSON.stringify()

JSON.stringify() 方法将 JavaScript 对象或值转换为 JSON 字符串。

实现原理

  1. 处理基本类型:根据类型进行相应的转换
  2. 处理对象类型:
    • 处理 null、Date、RegExp 等特殊对象
    • 处理数组:递归处理每个元素
    • 处理普通对象:递归处理每个属性
  3. 处理循环引用:检测并抛出错误
  4. 处理 toJSON 方法:如果对象有 toJSON 方法,使用其返回值

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* 模拟实现 JSON.stringify() 方法
* @param {any} data - 要序列化的数据
* @param {Function|Array} [replacer] - 可选,用于转换结果的函数或数组
* @param {string|number} [space] - 可选,用于缩进的字符串或数字
* @returns {string|undefined} 序列化后的 JSON 字符串
*/
function jsonStringify(data, replacer, space) {
// 处理 replacer 函数
function getReplacedValue(key, value) {
if (typeof replacer === 'function') {
return replacer(key, value);
} else if (Array.isArray(replacer)) {
return replacer.includes(key) ? value : undefined;
}
return value;
}

// 处理缩进
function getIndent(level) {
if (space === undefined) return '';
if (typeof space === 'number') {
return ' '.repeat(Math.min(space, 10));
} else if (typeof space === 'string') {
return space.substring(0, 10);
}
return '';
}

// 检测循环引用
const seen = new WeakSet();

function stringify(data, key, level = 0) {
const dataType = typeof data;
const replacedValue = getReplacedValue(key, data);

// 如果 replacer 返回 undefined,忽略该值
if (replacedValue === undefined) {
return undefined;
}

// 处理基本类型
if (dataType !== 'object' || data === null) {
if (replacedValue === null) {
return 'null';
} else if (typeof replacedValue === 'boolean') {
return String(replacedValue);
} else if (typeof replacedValue === 'number') {
return isNaN(replacedValue) || !isFinite(replacedValue) ? 'null' : String(replacedValue);
} else if (typeof replacedValue === 'string') {
// 转义字符串
return '"' + replacedValue.replace(/[\"\\\b\f\n\r\t]/g, function (char) {
const escapeMap = {
'"': '\\"',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t'
};
return escapeMap[char];
}) + '"';
} else if (typeof replacedValue === 'symbol' || typeof replacedValue === 'function' || typeof replacedValue === 'undefined') {
return undefined;
}
return String(replacedValue);
}

// 处理循环引用
if (seen.has(replacedValue)) {
throw new TypeError('Converting circular structure to JSON');
}

seen.add(replacedValue);

try {
// 处理 toJSON 方法
if (typeof replacedValue.toJSON === 'function') {
return stringify(replacedValue.toJSON(), key, level);
}

// 处理数组
if (Array.isArray(replacedValue)) {
const indent = getIndent(level);
const nextIndent = getIndent(level + 1);
const items = replacedValue.map((item, index) => {
const itemStr = stringify(item, String(index), level + 1);
return itemStr === undefined ? 'null' : nextIndent + itemStr;
});
return indent + '[' + (items.length ? '\n' + items.join(',\n') + '\n' + indent : '') + ']';
}

// 处理普通对象
const indent = getIndent(level);
const nextIndent = getIndent(level + 1);
const keys = Object.keys(replacedValue);
const properties = keys.map(key => {
const valueStr = stringify(replacedValue[key], key, level + 1);
if (valueStr === undefined) return undefined;
return nextIndent + '"' + key + '": ' + valueStr;
}).filter(Boolean);

return indent + '{' + (properties.length ? '\n' + properties.join(',\n') + '\n' + indent : '') + '}';
} finally {
seen.delete(replacedValue);
}
}

const result = stringify(data, '');
return result === undefined ? undefined : String(result);
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 示例1:基本类型
console.log(jsonStringify(1)); // "1"
console.log(jsonStringify('hello')); // "hello"
console.log(jsonStringify(true)); // "true"
console.log(jsonStringify(null)); // "null"
console.log(jsonStringify(undefined)); // undefined
console.log(jsonStringify(NaN)); // "null"
console.log(jsonStringify(Infinity)); // "null"

// 示例2:对象和数组
const obj = {
name: '布兰',
age: 12,
hobbies: ['编程', '读书'],
address: {
city: '北京'
},
birth: new Date('2010-01-01'),
regex: /\d+/,
sayHello: function() {
console.log('Hello');
},
undefinedProp: undefined,
symbolProp: Symbol('test')
};

console.log(jsonStringify(obj));
// {"name":"布兰","age":12,"hobbies":["编程","读书"],"address":{"city":"北京"},"birth":"2009-12-31T16:00:00.000Z","regex":{}}

// 示例3:使用 replacer 函数
console.log(jsonStringify(obj, function(key, value) {
if (key === 'age') return value + 1;
if (key === 'address') return undefined;
return value;
}));
// {"name":"布兰","age":13,"hobbies":["编程","读书"],"birth":"2009-12-31T16:00:00.000Z","regex":{}}

// 示例4:使用 replacer 数组
console.log(jsonStringify(obj, ['name', 'age', 'hobbies']));
// {"name":"布兰","age":12,"hobbies":["编程","读书"]}

// 示例5:使用 space
console.log(jsonStringify(obj, null, 2));
// {
// "name": "布兰",
// "age": 12,
// "hobbies": [
// "编程",
// "读书"
// ],
// "address": {
// "city": "北京"
// },
// "birth": "2009-12-31T16:00:00.000Z",
// "regex": {}
// }

注意事项

  • 循环引用:会抛出 TypeError 错误
  • 特殊值处理:NaN、Infinity、-Infinity 会被转换为 “null”
  • 函数和Symbol:会被忽略(对象中)或返回 undefined(单独序列化时)
  • undefined:会被忽略(对象中)或返回 undefined(单独序列化时)
  • Date对象:会调用 toJSON() 方法,转换为 ISO 日期字符串
  • RegExp对象:会被转换为 {}
  • toJSON方法:如果对象有 toJSON() 方法,会使用其返回值

JSON.parse()

JSON.parse() 方法将 JSON 字符串转换为 JavaScript 对象。

实现原理

  1. 验证 JSON 字符串格式
  2. 将 JSON 字符串转换为 JavaScript 对象
  3. 处理reviver函数(如果提供)

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 模拟实现 JSON.parse() 方法
* @param {string} jsonString - 要解析的 JSON 字符串
* @param {Function} [reviver] - 可选,用于转换结果的函数
* @returns {any} 解析后的 JavaScript 值
*/
function jsonParse(jsonString, reviver) {
// 验证输入
if (typeof jsonString !== 'string') {
throw new TypeError('JSON.parse() expects a string argument');
}

// 移除首尾空白
jsonString = jsonString.trim();

if (jsonString === '') {
throw new SyntaxError('Unexpected end of JSON input');
}

// 简单的 JSON 格式验证
const rx_one = /^[\],:{}\s]*$/;
const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
const rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
const rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (!rx_one.test(
jsonString.replace(rx_two, "@")
.replace(rx_three, "]")
.replace(rx_four, "")
)) {
throw new SyntaxError('Unexpected token in JSON at position');
}

// 使用 new Function 实现解析
const obj = (new Function('return ' + jsonString))();

// 处理 reviver 函数
if (typeof reviver === 'function') {
function walk(key, value) {
if (value && typeof value === 'object') {
for (const k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
const newVal = walk(k, value[k]);
if (newVal !== undefined) {
value[k] = newVal;
} else {
delete value[k];
}
}
}
}
return reviver(key, value);
}
return walk('', obj);
}

return obj;
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例1:基本用法
const jsonStr = '{"name":"布兰","age":12,"hobbies":["编程","读书"]}';
const obj = jsonParse(jsonStr);
console.log(obj); // { name: '布兰', age: 12, hobbies: [ '编程', '读书' ] }

// 示例2:使用 reviver 函数
const jsonStr2 = '{"name":"布兰","age":12,"birth":"2010-01-01"}';
const obj2 = jsonParse(jsonStr2, function(key, value) {
if (key === 'age') return value + 1;
if (key === 'birth') return new Date(value);
return value;
});
console.log(obj2); // { name: '布兰', age: 13, birth: 2010-01-01T00:00:00.000Z }

// 示例3:解析数组
const jsonArrayStr = '[1, 2, 3, "hello", true, null]';
const array = jsonParse(jsonArrayStr);
console.log(array); // [ 1, 2, 3, 'hello', true, null ]

注意事项

  • 格式验证:JSON 字符串必须符合严格的 JSON 格式
  • 安全问题:使用 eval 或 new Function 解析 JSON 存在安全风险,应确保输入的 JSON 字符串来自可信来源
  • reviver函数:可以用来转换解析后的值,如将日期字符串转换为 Date 对象
  • 错误处理:格式不正确的 JSON 字符串会抛出 SyntaxError 错误

现代实现

在现代 JavaScript 中,JSON.parse() 是浏览器原生实现的,性能更好且更安全。但了解其实现原理有助于理解 JSON 格式和 JavaScript 对象之间的转换过程。

参考:实现 JSON.stringify
参考:JSON.parse 三种实现方式

函数原型方法

call

使用一个指定的 this 值和一个或多个参数来调用一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.call2 = function (context) {
var context = context || window; // 参数为 null,默认指向 window
const fnSymbol = Symbol('fn');
context.fnSymbol = this; // 使用 Symbol,防止 context 里有同名属性
var args = [];
// arguments 是类数组对象,可以采用 for 循环
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
} // 执行后 args 格式为 ['arguments[1]', 'arguments[2]']

var result = eval('context.fnSymbol(' + args +')');
// 这里采用 eval 方法进行拼接。args 会自动调用 Array.toString()
// context.fn(arguments[1],arguments[2])
delete context.fnSymbol; // 函数执行完后须将添加的属性删除
return result; // 函数可能有返回值
}

1
2
3
4
5
6
7
8
9
10
11
12
// 似乎更简洁
Function.prototype.call3 = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
delete context.fn;
return result;
}

apply

apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.apply2 = function (context, arr) {
var context = context || window;
const fnSymbol = Symbol('fn');
context.fnSymbol = this;

var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fnSymbol(' + args + ')')
}

delete context.fnSymbol
return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 似乎更简洁
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
}

bind

bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

  • bind() 除了 this 外,还可传入多个参数;
  • bing 创建的新函数可能传入多个参数;
  • 新函数可能被当做构造函数调用;
  • 函数可能有返回值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.bind2 = function (context) {
var self = this;
// 获取 bind2 函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
// fbound.prototype = this.prototype 会使我们在修改 fbound.prototype 时,也会影响到绑定函数的 prototype。因此我们可以通过一个空函数来进行中转:
var fNOP = function () {};

var fBound = function () {
// 这个时候的 arguments 是指 bind 返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}

// 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。
// 当作为普通函数时,this 指向 window,self 指向绑定函数,此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
1
2
3
4
5
6
7
8
9
10
11
12
// 似乎更简洁
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
var _this = this;
var args = Array.prototype.slice.call(arguments, 1);
return function () {
var bindArgs = Array.prototype.slice.call(arguments);
_this.apply(context, args.concat(bindArgs));
}
}

数组原型方法

数组原型方法是 JavaScript 中常用的数组操作方法,下面我们来实现一些核心的数组原型方法。

forEach

forEach 方法对数组的每个元素执行一次提供的函数。

实现原理

  1. 检查调用对象是否为 null 或 undefined
  2. 检查回调函数是否为函数
  3. 将调用对象转换为对象
  4. 获取数组长度(确保为正整数)
  5. 遍历数组,对每个存在的元素调用回调函数

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 模拟实现 Array.prototype.forEach 方法
* @param {Function} callback - 回调函数
* @param {any} [thisArg] - 回调函数的 this 值
*/
Array.prototype.forEach2 = function(callback, thisArg) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 检查回调函数
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度(无符号右移 0 位确保为正整数)
const len = O.length >>> 0;

let k = 0;

// 遍历数组
while (k < len) {
// 检查索引是否存在
if (k in O) {
// 调用回调函数,传入当前元素、索引和数组
callback.call(thisArg, O[k], k, O);
}
k++;
}
};

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const arr = [1, 2, 3, 4, 5];
arr.forEach2((item, index, array) => {
console.log(`Item ${index}: ${item}`);
});
// 输出:
// Item 0: 1
// Item 1: 2
// Item 2: 3
// Item 3: 4
// Item 4: 5

// 使用 thisArg
const obj = { multiplier: 2 };
arr.forEach2(function(item) {
console.log(item * this.multiplier);
}, obj);
// 输出:
// 2
// 4
// 6
// 8
// 10

map

map 方法创建一个新数组,其结果是该数组中的每个元素都调用一次提供的函数后的返回值。

实现原理

  1. 检查调用对象和回调函数
  2. 创建一个新数组
  3. 遍历原数组,对每个元素调用回调函数,并将结果存入新数组
  4. 返回新数组

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 模拟实现 Array.prototype.map 方法
* @param {Function} callback - 回调函数
* @param {any} [thisArg] - 回调函数的 this 值
* @returns {Array} 新数组
*/
Array.prototype.map2 = function(callback, thisArg) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 检查回调函数
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度
const len = O.length >>> 0;

// 创建新数组
const res = new Array(len);

let k = 0;

// 遍历数组
while (k < len) {
if (k in O) {
// 调用回调函数并将结果存入新数组
res[k] = callback.call(thisArg, O[k], k, O);
}
k++;
}

return res;
};

使用示例

1
2
3
4
5
6
const arr = [1, 2, 3, 4, 5];
const doubled = arr.map2(item => item * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const squared = arr.map2(item => item * item);
console.log(squared); // [1, 4, 9, 16, 25]

filter

filter 方法创建一个新数组,包含通过所提供函数实现的测试的所有元素。

实现原理

  1. 检查调用对象和回调函数
  2. 创建一个空数组
  3. 遍历原数组,对每个元素调用回调函数
  4. 如果回调函数返回 true,则将该元素添加到新数组
  5. 返回新数组

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 模拟实现 Array.prototype.filter 方法
* @param {Function} callback - 回调函数
* @param {any} [thisArg] - 回调函数的 this 值
* @returns {Array} 新数组
*/
Array.prototype.filter2 = function(callback, thisArg) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 检查回调函数
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度
const len = O.length >>> 0;

// 创建新数组
const res = [];

let k = 0;

// 遍历数组
while (k < len) {
if (k in O) {
// 调用回调函数,如果返回 true 则添加到新数组
if (callback.call(thisArg, O[k], k, O)) {
res.push(O[k]);
}
}
k++;
}

return res;
};

使用示例

1
2
3
4
5
6
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = arr.filter2(item => item % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

const greaterThan5 = arr.filter2(item => item > 5);
console.log(greaterThan5); // [6, 7, 8, 9, 10]

some

some 方法测试数组中是否至少有一个元素通过了由提供的函数实现的测试。

实现原理

  1. 检查调用对象和回调函数
  2. 遍历数组,对每个元素调用回调函数
  3. 如果回调函数返回 true,则立即返回 true
  4. 如果遍历完所有元素都没有返回 true,则返回 false

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 模拟实现 Array.prototype.some 方法
* @param {Function} callback - 回调函数
* @param {any} [thisArg] - 回调函数的 this 值
* @returns {boolean} 是否至少有一个元素通过测试
*/
Array.prototype.some2 = function(callback, thisArg) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 检查回调函数
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度
const len = O.length >>> 0;

let k = 0;

// 遍历数组
while (k < len) {
if (k in O) {
// 调用回调函数,如果返回 true 则立即返回 true
if (callback.call(thisArg, O[k], k, O)) {
return true;
}
}
k++;
}

// 没有元素通过测试
return false;
};

使用示例

1
2
3
4
5
6
const arr = [1, 2, 3, 4, 5];
const hasEven = arr.some2(item => item % 2 === 0);
console.log(hasEven); // true

const hasGreaterThan10 = arr.some2(item => item > 10);
console.log(hasGreaterThan10); // false

reduce

reduce 方法对数组中的每个元素执行一个由您提供的 reducer 函数,将其结果汇总为单个返回值。

实现原理

  1. 检查调用对象和回调函数
  2. 处理初始值
  3. 遍历数组,对每个元素调用回调函数,更新累加器
  4. 返回累加器

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 模拟实现 Array.prototype.reduce 方法
* @param {Function} callback - 回调函数
* @param {any} [initialValue] - 初始值
* @returns {any} 累加结果
*/
Array.prototype.reduce2 = function(callback, initialValue) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 检查回调函数
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度
const len = O.length >>> 0;

let k = 0;
let acc;

// 处理初始值
if (arguments.length > 1) {
acc = initialValue;
} else {
// 没传入初始值时,取数组中第一个非 empty 的值
while (k < len && !(k in O)) {
k++;
}

// 如果数组为空且没有初始值,抛出错误
if (k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}

acc = O[k++];
}

// 遍历数组
while (k < len) {
if (k in O) {
// 调用回调函数,更新累加器
acc = callback(acc, O[k], k, O);
}
k++;
}

return acc;
};

使用示例

1
2
3
4
5
6
7
8
9
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce2((acc, item) => acc + item, 0);
console.log(sum); // 15

const product = arr.reduce2((acc, item) => acc * item, 1);
console.log(product); // 120

const max = arr.reduce2((acc, item) => Math.max(acc, item));
console.log(max); // 5

flat

flat 方法创建一个新数组,所有子数组元素递归地拼接到新数组中。

实现原理

  1. 检查调用对象
  2. 处理深度参数
  3. 递归遍历数组,将元素添加到结果数组中
  4. 如果元素是数组,则继续递归

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 模拟实现 Array.prototype.flat 方法
* @param {number} [depth=1] - 嵌套数组的深度
* @returns {Array} 扁平化后的数组
*/
Array.prototype.flat2 = function(depth = 1) {
// 检查调用对象
if (this == null) {
throw new TypeError('this is null or not defined');
}

// 将调用对象转换为对象
const O = Object(this);

// 获取数组长度
const len = O.length >>> 0;

// 处理深度参数
const depthNum = Number(depth);
const actualDepth = depthNum > 0 ? Math.floor(depthNum) : 0;

// 结果数组
const result = [];

// 递归扁平化
function flatten(array, currentDepth) {
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (Array.isArray(item) && currentDepth < actualDepth) {
// 递归扁平化子数组
flatten(item, currentDepth + 1);
} else {
// 添加非数组元素
result.push(item);
}
}
}

// 开始扁平化
flatten(O, 0);

return result;
};

// 深度扁平化实现
function flattenDeep(arr) {
return arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []
);
}

使用示例

1
2
3
4
5
6
7
8
9
const arr1 = [1, 2, 3, [4, 5, [6, 7]]];
const flatOnce = arr1.flat2();
console.log(flatOnce); // [1, 2, 3, 4, 5, [6, 7]]

const flatTwice = arr1.flat2(2);
console.log(flatTwice); // [1, 2, 3, 4, 5, 6, 7]

const flatDeep = flattenDeep(arr1);
console.log(flatDeep); // [1, 2, 3, 4, 5, 6, 7]

性能考虑

  • forEach、map、filter:时间复杂度为 O(n),其中 n 是数组长度
  • some:时间复杂度最好为 O(1)(找到符合条件的元素),最坏为 O(n)
  • reduce:时间复杂度为 O(n)
  • flat:时间复杂度为 O(n),其中 n 是所有层级元素的总数

注意事项

  • 所有方法都不会修改原数组(除了回调函数可能修改)
  • 所有方法都会跳过稀疏数组中的空槽位
  • 回调函数接收三个参数:当前元素、索引、原数组
  • 可以通过 thisArg 参数设置回调函数的 this 值
  • 对于大型数组,应考虑性能影响,避免在回调函数中执行复杂操作

参考:forEach#polyfill

目录

  1. 创建对象的方式
  2. 继承
  3. TypeScript 类与继承
  4. 实际应用示例
  5. 最佳实践
  6. 总结

创建对象的方式

工厂模式

1
2
3
4
5
6
7
8
9
function createPerson(name) {
const o = new Object();
o.name = name;
o.getName = function () {
console.log(this.name);
};
return o;
}
const person1 = createPerson('kevin');
阅读全文 »

过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差。然而,随着互联网的发展,这种需求却与日俱增,比如,下面这些情况都需要用到相交检测:

  • 图片懒加载——当图片滚动到可见时才进行加载
  • 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
  • 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
  • 在用户看见某个区域时执行任务或播放动画
阅读全文 »

用户体验设计秉承着以用户为中心的思想,以用户需求为目标。设计过程注重以用户为中心,用户体验的概念从开发的最早期就开始进入整个流程,并贯穿始终。良好的用户体验设计,是产品每一个环节共同努力的结果。

除去一些很难一蹴而就的,本文将就页面展示交互细节可访问性三个方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。通过本文,你将能收获到:

  • 了解到一些小细节是如何影响用户体验的

  • 了解到如何在尽量小的开发改动下,提升页面的用户体验

  • 了解到一些优秀的交互设计细节

  • 了解基本的无障碍功能及页面可访问性的含义

  • 了解基本的提升页面可访问性的方法

    阅读全文 »