0%

一、ts 类型

1. 基本类型:string, number, boolean

Note: The type names String, Number, and Boolean (starting with capital letters) are legal, but refer to some special built-in types that will very rarely appear in your code. Always use string, number, or boolean for types.

阅读全文 »

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

阅读全文 »

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

阅读全文 »

Promise

(一)含义

Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。Promise 可以说是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

阅读全文 »

目录

  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');
阅读全文 »

深浅拷贝

浅拷贝

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

实现原理

  • 检查输入是否为对象类型
  • 根据原对象类型(数组或普通对象)创建新对象
  • 遍历原对象的自有属性,将属性值直接复制到新对象
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

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

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

JavaScript Date objects represent a single moment in time in a platform-independent format. Date objects contain a Number that represents milliseconds since 1 January 1970 UTC.

阅读全文 »