手写代码系列
将数字以逗号进行千分位分割
let num = 1232456789.98765;
let formattedNum = num.toLocaleString('en-US');
console.log(formattedNum); // 1,232,456,789.988
上述代码如何输出保留两位小数的字符串?
let num = 1232456789.98765;
let formattedNum = Number(num.toFixed(2)).toLocaleString('en-US');
console.log(formattedNum); // 1,232,456,789.99
防抖与节流
防抖
function debounce(fn, wait) {
let timer;
return function (...params) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, params), wait);
};
}
节流
function throttle(fn, limit) {
let isThrottle;
return function (...params) {
if (!isThrottle) {
fn.apply(this, params);
isThrottle = true;
setTimeout(() => (isThrottle = false), limit);
}
};
}
浅拷贝与深拷贝
浅拷贝
浅拷贝是指创建一个新对象,并将原始对象的属性直接复制到新对象中。如果原始对象的属性是基本类型(如字符串、数字、布尔值等),那么这些值会被直接复制;但如果属性是引用类型(如对象、数组等),那么这些引用会被复制,也就是说新对象的属性会指向原始对象的属性所指向的同一块内存。
function shallowCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj; // 如果不是对象,则直接返回
}
let copy;
if (Array.isArray(obj)) {
copy = []; // 创建一个新数组
} else {
copy = {}; // 创建一个新对象
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = obj[key]; // 直接复制属性
}
}
return copy;
}
// 测试
const original = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: 10001,
},
hobbies: ['reading', 'traveling'],
};
const shallowCopied = shallowCopy(original);
console.log(shallowCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ] }
shallowCopied.address.city = 'San Francisco';
shallowCopied.hobbies.push('swimming');
console.log(original); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ] }
console.log(shallowCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ] }
深拷贝
深拷贝是指创建一个新对象,并递归地复制原始对象及其所有属性。无论是基本类型还是引用类型,都会创建一个新的实例,从而确保新对象与原始对象没有任何共享的引用。
在实现深拷贝时,需要考虑以下边界情况:
- 循环引用:
- 如果原始对象中存在循环引用,递归拷贝会导致无限递归。
- 解决方案是使用一个 Map 来跟踪已经拷贝过的对象,避免重复拷贝相同的对象。
- 特殊类型的处理:
- 需要考虑特殊类型的对象,如日期、正则表达式、函数等。
- 对于这些类型,可以直接拷贝它们的值或创建新的实例。
- 原型链:
- 深拷贝通常只复制对象自身的属性,不会复制原型链上的属性。
- 如果需要复制原型链上的属性,需要额外处理。
- 错误处理:
- 需要考虑非对象类型的处理,例如
null、undefined等。
- 需要考虑非对象类型的处理,例如
function deepCopy(obj, seen = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
return obj; // 如果不是对象,则直接返回
}
if (seen.has(obj)) {
return seen.get(obj); // 如果已经拷贝过,则直接返回已拷贝的对象
}
let copy;
if (Array.isArray(obj)) {
copy = []; // 创建一个新数组
} else if (obj instanceof Date) {
copy = new Date(obj); // 处理日期对象
} else if (obj instanceof RegExp) {
copy = new RegExp(obj); // 处理正则表达式对象
} else {
copy = {}; // 创建一个新对象
}
seen.set(obj, copy); // 标记已经拷贝过的对象
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], seen); // 递归深拷贝属性
}
}
return copy;
}
// 测试
const original = {
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: 10001,
},
hobbies: ['reading', 'traveling'],
nested: {
more: {
info: 'deeply nested object',
},
},
date: new Date(),
regex: /test/,
loop: null,
};
// 创建循环引用
original.loop = original;
const deepCopied = deepCopy(original);
console.log(deepCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ], nested: { more: { info: 'deeply nested object' } }, date: [Date], regex: [RegExp], loop: [Circular] }
deepCopied.address.city = 'San Francisco';
deepCopied.hobbies.push('swimming');
deepCopied.nested.more.info = 'changed';
deepCopied.date.setDate(deepCopied.date.getDate() + 1);
deepCopied.regex.lastIndex = 1;
console.log(original); // 输出:{ name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: [ 'reading', 'traveling' ], nested: { more: { info: 'deeply nested object' } }, date: [Date], regex: [RegExp], loop: [Circular] }
console.log(deepCopied); // 输出:{ name: 'Alice', age: 25, address: { city: 'San Francisco', zip: 10001 }, hobbies: [ 'reading', 'traveling', 'swimming' ], nested: { more: { info: 'changed' } }, date: [Date], regex: [RegExp], loop: [Circular] }
模拟实现一个new操作符
function _new(constructor, ...args) {
// Step 1: 创建一个新的空对象
const newObj = Object.create(constructor.prototype);
// Step 2: 调用构造函数并传入参数
const result = constructor.apply(newObj, args);
// Step 3: 判断返回值,如果是对象或函数,则返回,否则返回新创建的对象
return (typeof result === 'object' && result !== null) || typeof result === 'function' ? result : newObj;
}
// 示例构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
};
// 使用手写的 new 操作符
const person = _new(Person, 'Bob', 25);
person.greet(); // 输出: Hello, my name is Bob, and I am 25 years old.
函数柯里化
函数柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术。下面是一个简单的函数柯里化的实现示例:
function curry(fn) {
const arity = fn.length; // 获取原函数需要的参数数量
return function curried(...args) {
// 如果提供的参数数量不足,则返回一个新的函数
if (args.length < arity) {
return function(...moreArgs) {
return curried(...args, ...moreArgs);
};
}
// 参数数量足够时,调用原函数
return fn(...args);
};
}
// 示例使用
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
// 调用
console.log(curriedSum(1)(2)(3)); // 输出: 6
console.log(curriedSum(1)(2, 3)); // 输出: 6
console.log(curriedSum(1, 2)(3)); // 输出: 6
console.log(curriedSum(1, 2, 3)); // 输出: 6
实现数组任意层级的flat函数
可以使用ES10的flat函数,第二个参数传入Infinity :
let arr= [1, [2, [3, [4]], 5]];
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5]
还可以使用递归的办法:
function deepFlat(arr) {
// 检查是否已经是扁平数组
if (!Array.isArray(arr) || !arr.some(item => Array.isArray(item))) {
return arr.slice(); // 如果不是数组或已经是扁平数组,直接返回
}
return arr.reduce((acc, val) => {
// 如果当前项是数组,则递归调用 deepFlat 函数
return acc.concat(Array.isArray(val) ? deepFlat(val) : val);
}, []);
}
// 示例
const nestedArray = [1, 2, [3, 4, [5, 6]], 7, [8, [9, [10]]]];
const flatArray = deepFlat(nestedArray);
console.log(flatArray); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
使用Promise来封装AJAX请求
function ajax(url, options = {}) {
return new Promise((resolve, reject) => {
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();
// 设置默认选项
const defaultOptions = {
method: 'GET', // 默认使用 GET 方法
headers: {
'Content-Type': 'application/json',
},
};
// 合并默认选项和传入的选项
const finalOptions = { ...defaultOptions, ...options };
// 初始化请求
xhr.open(finalOptions.method, url, true);
// 设置请求头
Object.keys(finalOptions.headers).forEach(key => {
xhr.setRequestHeader(key, finalOptions.headers[key]);
});
// 发送请求
xhr.send(finalOptions.body);
// 监听请求状态变化
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) { // 请求已完成
if (xhr.status >= 200 && xhr.status < 300) { // 成功响应
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (e) {
reject(e);
}
} else { // 失败响应
reject(new Error(`Request failed with status code ${xhr.status}`));
}
}
};
// 错误处理
xhr.onerror = function () {
reject(new Error('Network error'));
};
});
}
// 示例使用
ajax('/api/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));