手写实现常见的 JavaScript 函数和概念
- 手写数组去重
- 不用 Set 如何去重
- 如何在原数组上去重
- 如何对数组里有相同内容的对象去重
- 如何按数组里包含的对象属性字段去重
- 手写深拷贝
- 手写处理循环引用的深拷贝
- 拷贝一个函数
- 手写一个
trim
函数 - 手写一个
curry
函数 - 写一个观察者 EventBus 含 emit、on、once、off
- 手写一个
instanceof
- 手写一个
new
- 手写一个
call
和apply
- 用
call
/apply
实现bind
- 不用
call
或者apply
实现bind
- 手写
bind
考虑new
一个bind
函数的情况 - 手写
forEach
- 手写
map
- 手写
filter
- 手写
reduce
- 手写数组拍平
flat
- 手写原型继承
- 手写
class
继承 - 用 Promise 封装一个
delay
函数 - 实现字符串大数相加
手写数组去重
数组去重是 JavaScript 中常见的操作之一。手写实现数组去重可以帮助我们理解数组操作的底层机制。
方法一:使用双重循环
通过遍历数组,逐一比较元素,剔除重复项。
function unique(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
let isDuplicate = false;
for (let j = 0; j < result.length; j++) {
if (arr[i] === result[j]) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
result.push(arr[i]);
}
}
return result;
}
// 示例
const array = [1, 2, 2, 3, 4, 4, 5];
console.log(unique(array)); // 输出: [1, 2, 3, 4, 5]
方法二:使用对象键值
利用对象的键唯一性,实现数组去重。
function unique(arr) {
const obj = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
obj[arr[i]] = true;
result.push(arr[i]);
}
}
return result;
}
// 示例
const array = [1, 2, 2, 3, 4, 4, 5];
console.log(unique(array)); // 输出: [1, 2, 3, 4, 5]
方法三:利用 indexOf
通过 indexOf
检查元素是否已存在于结果数组中。
function unique(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
if (result.indexOf(arr[i]) === -1) {
result.push(arr[i]);
}
}
return result;
}
// 示例
const array = [1, 2, 2, 3, 4, 4, 5];
console.log(unique(array)); // 输出: [1, 2, 3, 4, 5]
不用 Set 如何去重
虽然 Set
提供了简便的去重方法,但在某些情况下,我们可能需要不使用 Set
来实现数组去重。以下介绍几种常见的方法。
方法一:使用双重循环
与前述的手写数组去重方法类似,通过双重循环检查重复。
function unique(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
let isDuplicate = false;
for (let j = 0; j < result.length; j++) {
if (arr[i] === result[j]) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
result.push(arr[i]);
}
}
return result;
}
方法二:使用对象键值
利用对象的键唯一性进行去重。
function unique(arr) {
const obj = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
obj[arr[i]] = true;
result.push(arr[i]);
}
}
return result;
}
方法三:利用 filter
和 indexOf
结合 filter
方法和 indexOf
实现去重。
function unique(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
方法四:排序后去重
先对数组进行排序,再去除相邻的重复项。
function unique(arr) {
arr.sort();
const result = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i + 1]) {
result.push(arr[i]);
}
}
return result;
}
如何在原数组上去重
有时我们需要在不创建新数组的情况下,对原数组进行去重操作。这可以通过遍历和删除重复项来实现。
function uniqueInPlace(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = arr.length - 1; j > i; j--) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
}
}
}
return arr;
}
// 示例
const array = [1, 2, 2, 3, 4, 4, 5];
uniqueInPlace(array);
console.log(array); // 输出: [1, 2, 3, 4, 5]
注意:在原数组上进行操作时,应谨慎使用 splice
,以避免意外删除或影响性能,特别是对于大型数组。
如何对数组里有相同内容的对象去重
数组中包含对象时,直接使用前述方法无法正确去重,因为对象是引用类型。我们需要根据对象的属性值来判断重复。
方法一:基于 JSON 字符串
将对象转换为 JSON 字符串,利用字符串的唯一性进行去重。
function uniqueObjects(arr) {
const seen = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
const key = JSON.stringify(arr[i]);
if (!seen[key]) {
seen[key] = true;
result.push(arr[i]);
}
}
return result;
}
// 示例
const array = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
];
console.log(uniqueObjects(array));
// 输出: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
方法二:基于指定属性
如果只需要根据特定属性来判断对象的唯一性,可以如下实现:
function uniqueByProperty(arr, prop) {
const seen = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!seen[arr[i][prop]]) {
seen[arr[i][prop]] = true;
result.push(arr[i]);
}
}
return result;
}
// 示例
const array = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Charlie' },
];
console.log(uniqueByProperty(array, 'id'));
// 输出: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
方法三:使用 filter
和 findIndex
结合 filter
和 findIndex
方法实现对象数组的去重。
function uniqueObjects(arr) {
return arr.filter((item, index, self) => index === self.findIndex((t) => t.id === item.id && t.name === item.name));
}
// 示例
const array = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
];
console.log(uniqueObjects(array));
// 输出: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
如何按数组里包含的对象属性字段去重?
在对象数组中,根据特定的属性字段进行去重,可以提升性能和灵活性。以下是实现方法。
示例需求
假设我们有一个用户数组,需要根据 id
字段进行去重。
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Charlie' },
{ id: 3, name: 'Dave' },
];
方法一:使用对象键值
利用对象的键唯一性,根据指定属性去重。
function uniqueByKey(arr, key) {
const seen = {};
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!seen[arr[i][key]]) {
seen[arr[i][key]] = true;
result.push(arr[i]);
}
}
return result;
}
// 示例
console.log(uniqueByKey(users, 'id'));
// 输出: [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' },
// { id: 3, name: 'Dave' }
// ]
方法二:使用 Map
Map
允许使用任意类型的键,适用于更复杂的去重需求。
function uniqueByKey(arr, key) {
const map = new Map();
arr.forEach((item) => {
if (!map.has(item[key])) {
map.set(item[key], item);
}
});
return Array.from(map.values());
}
// 示例
console.log(uniqueByKey(users, 'id'));
// 输出: [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' },
// { id: 3, name: 'Dave' }
// ]
方法三:使用 filter
和 findIndex
结合 filter
和 findIndex
,根据指定属性去重。
function uniqueByKey(arr, key) {
return arr.filter((item, index, self) => index === self.findIndex((t) => t[key] === item[key]));
}
// 示例
console.log(uniqueByKey(users, 'id'));
// 输出: [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' },
// { id: 3, name: 'Dave' }
// ]
手写深拷贝
深拷贝是指创建一个对象的完整副本,副本与原对象互不影响。手写深拷贝可以帮助理解对象的引用和复制。
简单实现
递归遍历对象或数组,复制每一个属性或元素。
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 处理 Array
if (Array.isArray(obj)) {
const arrCopy = [];
for (let i = 0; i < obj.length; i++) {
arrCopy[i] = deepClone(obj[i]);
}
return arrCopy;
}
// 处理 Object
const objCopy = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
objCopy[key] = deepClone(obj[key]);
}
}
return objCopy;
}
// 示例
const original = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'gaming'],
birthDate: new Date(),
};
const copy = deepClone(original);
copy.hobbies.push('coding');
console.log(original.hobbies); // 输出: ['reading', 'gaming']
console.log(copy.hobbies); // 输出: ['reading', 'gaming', 'coding']
支持更多数据类型
为了处理更多的数据类型(如 RegExp
, Map
, Set
等),可以扩展深拷贝函数。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 处理循环引用
let clone;
if (obj instanceof Date) {
clone = new Date(obj);
} else if (obj instanceof RegExp) {
clone = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
clone = new Map();
obj.forEach((value, key) => {
clone.set(key, deepClone(value, hash));
});
} else if (obj instanceof Set) {
clone = new Set();
obj.forEach((value) => {
clone.add(deepClone(value, hash));
});
} else if (Array.isArray(obj)) {
clone = [];
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], hash);
}
} else {
clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
}
hash.set(obj, clone);
return clone;
}
手写处理循环引用的深拷贝
在对象中存在循环引用时,简单的深拷贝方法会导致无限递归。以下方法通过使用 WeakMap
来记录已复制的对象,避免循环引用问题。
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj); // 处理循环引用
let clone;
if (obj instanceof Date) {
clone = new Date(obj);
} else if (obj instanceof RegExp) {
clone = new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
clone = new Map();
obj.forEach((value, key) => {
clone.set(key, deepClone(value, hash));
});
} else if (obj instanceof Set) {
clone = new Set();
obj.forEach((value) => {
clone.add(deepClone(value, hash));
});
} else if (Array.isArray(obj)) {
clone = [];
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i], hash);
}
} else {
clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], hash);
}
}
}
hash.set(obj, clone);
return clone;
}
// 示例
const obj = { name: 'Alice' };
obj.self = obj; // 创建循环引用
const clonedObj = deepClone(obj);
console.log(clonedObj); // 输出: { name: 'Alice', self: [Circular] }
console.log(clonedObj.self === clonedObj); // 输出: true
解释:
- WeakMap:用于存储已经复制过的对象,键为原对象,值为克隆后的对象。
- 循环引用检查:在克隆前检查对象是否已存在于
WeakMap
中,如果存在,则直接返回克隆对象,避免无限递归。 - 数据类型处理:对不同的数据类型(如
Date
,RegExp
,Map
,Set
,Array
,Object
)进行相应的克隆处理。
拷贝一个函数
在 JavaScript 中,函数也是对象,可以进行拷贝。然而,函数的拷贝涉及到上下文和闭包等复杂问题。以下是基本的函数拷贝方法。
方法一:使用 bind
bind
可以创建一个新的函数,绑定特定的 this
值和参数。
function cloneFunction(func) {
return func.bind({});
}
// 示例
function greet(name) {
return `Hello, ${name}`;
}
const clonedGreet = cloneFunction(greet);
console.log(clonedGreet('Alice')); // 输出: Hello, Alice
注意:这种方法无法复制函数的闭包和内部状态。
方法二:使用 eval
和 toString
通过获取函数的字符串表示,再通过 eval
创建新函数。
function cloneFunction(func) {
const funcStr = func.toString();
return eval(`(${funcStr})`);
}
// 示例
function greet(name) {
return `Hello, ${name}`;
}
const clonedGreet = cloneFunction(greet);
console.log(clonedGreet('Bob')); // 输出: Hello, Bob
注意:使用 eval
存在安全风险,不推荐在生产环境中使用。
方法三:使用 new Function
通过解析函数的参数和体,创建新函数。
function cloneFunction(func) {
const funcStr = func.toString();
const paramStart = funcStr.indexOf('(') + 1;
const paramEnd = funcStr.indexOf(')');
const params = funcStr.substring(paramStart, paramEnd);
const bodyStart = funcStr.indexOf('{') + 1;
const bodyEnd = funcStr.lastIndexOf('}');
const body = funcStr.substring(bodyStart, bodyEnd);
return new Function(params, body);
}
// 示例
function greet(name) {
return `Hello, ${name}`;
}
const clonedGreet = cloneFunction(greet);
console.log(clonedGreet('Charlie')); // 输出: Hello, Charlie
注意:这种方法同样无法复制函数的闭包和内部状态,且存在安全风险。
方法四:浅拷贝函数属性
如果函数包含自定义属性,可以手动复制这些属性。
function cloneFunction(func) {
const cloned = func.bind({});
for (let key in func) {
if (func.hasOwnProperty(key)) {
cloned[key] = func[key];
}
}
return cloned;
}
// 示例
function greet(name) {
return `Hello, ${name}`;
}
greet.language = 'English';
const clonedGreet = cloneFunction(greet);
console.log(clonedGreet('Dave')); // 输出: Hello, Dave
console.log(clonedGreet.language); // 输出: English
总结:拷贝函数在 JavaScript 中具有一定的局限性,尤其是无法复制函数的闭包和内部状态。在实际开发中,应谨慎使用函数拷贝,避免引入潜在的问题。
手写一个 trim
函数
trim
方法用于删除字符串两端的空白字符。以下是手写实现 trim
的方法。
方法一:使用正则表达式
利用正则表达式匹配并替换空白字符。
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
// 示例
const string = ' Hello, World! ';
console.log(trim(string)); // 输出: 'Hello, World!'
方法二:使用 charAt
和循环
通过遍历字符串,找到第一个和最后一个非空白字符的位置。
function trim(str) {
let start = 0;
let end = str.length - 1;
while (start <= end && /\s/.test(str.charAt(start))) {
start++;
}
while (end >= start && /\s/.test(str.charAt(end))) {
end--;
}
return str.substring(start, end + 1);
}
// 示例
const string = ' Hello, World! ';
console.log(trim(string)); // 输出: 'Hello, World!'
方法三:使用 split
和 join
通过拆分和重组字符串,去除首尾空白。
function trim(str) {
return str
.split('')
.reduce(
(acc, char, index) => {
if (!acc.startFound && /\S/.test(char)) {
acc.startFound = true;
}
if (acc.startFound) {
acc.result += char;
}
return acc;
},
{ startFound: false, result: '' }
)
.trimEnd();
}
// 辅助函数
String.prototype.trimEnd = function () {
let end = this.length - 1;
while (end >= 0 && /\s/.test(this.charAt(end))) {
end--;
}
return this.substring(0, end + 1);
};
// 示例
const string = ' Hello, World! ';
console.log(trim(string)); // 输出: 'Hello, World!'
注意:上述方法三较为复杂,推荐使用前两种方法,尤其是正则表达式方法,简洁且高效。
手写一个 curry
函数
curry
(柯里化)是将一个接受多个参数的函数转化为一系列接受单一参数的函数的技术。以下是手写实现 curry
的方法。
基本实现
通过递归收集参数,直到达到原函数的参数长度,再执行函数。
function curry(fn) {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) {
return fn.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 示例
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出: 6
console.log(curriedAdd(1, 2)(3)); // 输出: 6
console.log(curriedAdd(1)(2, 3)); // 输出: 6
支持不定数量参数
为了支持不定数量的参数,可以调整 curry
函数,使其在每次调用时检查是否已经满足条件。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 示例同上
ES6 实现
利用 ES6 的箭头函数和扩展运算符,实现更加简洁的 curry
函数。
const curry = (fn) => {
const curried = (...args) => (args.length >= fn.length ? fn(...args) : (...nextArgs) => curried(...args, ...nextArgs));
return curried;
};
// 示例同上
高级实现:自动收集参数
支持函数参数的动态收集,无需预先知道函数的长度。
function curry(fn) {
const curried = (...args) => {
return fn.length <= args.length ? fn(...args) : (...nextArgs) => curried(...args, ...nextArgs);
};
return curried;
}
// 示例同上
注意:柯里化的应用场景广泛,特别适用于函数式编程和提高代码复用性。
写一个观察者 EventBus 含 emit
、on
、once
、off
EventBus
是一种常见的设计模式,用于实现发布-订阅机制。以下是手写实现 EventBus
的方法,包括 emit
、on
、once
和 off
方法。
实现代码
class EventBus {
constructor() {
this.events = {};
}
// 订阅事件
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
// 订阅一次性事件
once(event, listener) {
const onceListener = (...args) => {
listener.apply(this, args);
this.off(event, onceListener);
};
this.on(event, onceListener);
}
// 取消订阅
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter((l) => l !== listener);
}
// 触发事件
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach((listener) => {
listener.apply(this, args);
});
}
}
// 示例
const bus = new EventBus();
function greet(name) {
console.log(`Hello, ${name}`);
}
function farewell(name) {
console.log(`Goodbye, ${name}`);
}
bus.on('greet', greet);
bus.once('farewell', farewell);
bus.emit('greet', 'Alice'); // 输出: Hello, Alice
bus.emit('farewell', 'Bob'); // 输出: Goodbye, Bob
bus.emit('farewell', 'Charlie'); // 无输出
方法解释
on(event, listener)
:订阅指定事件,将监听器添加到事件列表中。once(event, listener)
:订阅一次性事件,监听器在第一次触发后自动移除。off(event, listener)
:取消订阅指定事件的监听器。emit(event, ...args)
:触发指定事件,调用所有相关的监听器,并传递参数。
进阶功能
可以添加更多功能,如事件优先级、异步触发等,根据需求进行扩展。
手写一个 instanceof
instanceof
运算符用于检测一个对象是否属于某个构造函数的实例。以下是手写实现 instanceof
的方法。
实现代码
function myInstanceof(obj, constructor) {
if (typeof obj !== 'object' || obj === null) return false;
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 示例
class Person {}
const alice = new Person();
console.log(myInstanceof(alice, Person)); // 输出: true
console.log(myInstanceof(alice, Object)); // 输出: true
console.log(myInstanceof({}, Person)); // 输出: false
方法解释
- 检查类型:首先检查
obj
是否为对象类型且不为null
。 - 遍历原型链:使用
Object.getPrototypeOf
获取对象的原型,并遍历原型链。 - 比较原型:在遍历过程中,若发现某一层原型与构造函数的
prototype
相同,则返回true
。 - 结束条件:若遍历到原型链末端(
null
)仍未找到匹配,则返回false
。
注意事项
instanceof
只能用于对象,不能用于基本类型(如number
,string
)。- 在多重继承或复杂原型链的情况下,
instanceof
依然能正确工作。
手写一个 new
new
运算符用于创建构造函数的实例。手写实现 new
的过程,有助于理解构造函数、原型链和上下文绑定。
实现代码
function myNew(constructor, ...args) {
// 创建一个新的对象,继承自构造函数的原型
const obj = Object.create(constructor.prototype);
// 执行构造函数,将新对象作为上下文
const result = constructor.apply(obj, args);
// 如果构造函数返回对象,则返回该对象,否则返回新创建的对象
return result instanceof Object ? result : obj;
}
// 示例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
const alice = myNew(Person, 'Alice', 30);
alice.sayHello(); // 输出: Hello, I'm Alice
console.log(alice instanceof Person); // 输出: true
方法解释
- 创建新对象:使用
Object.create
创建一个新对象,该对象的原型指向构造函数的prototype
。 - 执行构造函数:使用
constructor.apply
将构造函数的上下文绑定到新对象,并传递参数。 - 返回结果:如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象。
注意事项
- 构造函数可以显式返回一个对象,覆盖默认的新对象。
- 如果构造函数返回的是基本类型,则忽略返回值,仍返回新创建的对象。
手写一个 call
和 apply
call
和 apply
方法用于改变函数的执行上下文(this
值)。以下是手写实现这两个方法的步骤。
手写实现 call
Function.prototype.myCall = function (context, ...args) {
// 如果 context 为 null 或 undefined,则绑定到全局对象
context = context || globalThis;
// 创建一个唯一的属性名,避免覆盖原有属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行函数
const result = context[fnSymbol](...args);
// 删除临时属性
delete context[fnSymbol];
return result;
};
// 示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
greet.myCall(person, 'Hello', '!'); // 输出: Hello, Alice!
手写实现 apply
Function.prototype.myApply = function (context, args) {
// 如果 context 为 null 或 undefined,则绑定到全局对象
context = context || globalThis;
// 创建一个唯一的属性名,避免覆盖原有属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行函数
let result;
if (Array.isArray(args) || args instanceof Arguments) {
result = context[fnSymbol](...args);
} else {
result = context[fnSymbol]();
}
// 删除临时属性
delete context[fnSymbol];
return result;
};
// 示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Bob' };
greet.myApply(person, ['Hi', '?']); // 输出: Hi, Bob?
方法解释
- 上下文绑定:将函数的执行上下文绑定到指定的对象
context
。 - 避免属性冲突:使用
Symbol
创建一个唯一的属性名,确保不会覆盖对象已有的属性。 - 执行函数:调用函数,并传递参数。
- 清理临时属性:执行完函数后,删除临时添加的属性,避免污染对象。
注意事项
call
接受的是一系列参数,而apply
接受的是一个参数数组。- 在严格模式下,如果
context
为null
或undefined
,则this
不会被绑定到全局对象。
用 call
/apply
实现 bind
bind
方法用于创建一个新函数,并将其 this
值永久绑定到指定的对象。以下是使用 call
或 apply
实现 bind
的方法。
手写实现 bind
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function boundFunction(...newArgs) {
return fn.call(context, ...args, ...newArgs);
};
};
// 示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Charlie' };
const greetCharlie = greet.myBind(person, 'Hello');
greetCharlie('!'); // 输出: Hello, Charlie!
支持构造函数
为了使 bind
支持作为构造函数使用,需要处理 new
操作符的情况。
Function.prototype.myBind = function (context, ...args) {
const fn = this;
function boundFunction(...newArgs) {
// 如果通过 new 调用,则忽略绑定的 context
const isNew = this instanceof boundFunction;
const finalContext = isNew ? this : context;
return fn.apply(finalContext, [...args, ...newArgs]);
}
// 维护原型链
boundFunction.prototype = Object.create(fn.prototype);
return boundFunction;
};
// 示例
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.myBind(null, 'Dave');
const dave = new BoundPerson(25);
console.log(dave.name); // 输出: Dave
console.log(dave.age); // 输出: 25
console.log(dave instanceof Person); // 输出: true
方法解释
- 参数预先绑定:
bind
方法允许预先绑定部分参数,在调用时补充剩余参数。 - 处理构造函数:通过检查
this instanceof boundFunction
来判断是否通过new
调用,如果是,则忽略绑定的context
。 - 维护原型链:将
boundFunction.prototype
设置为原函数fn.prototype
的副本,以确保实例能够继承原函数的原型。
注意事项
bind
创建的新函数具有原函数的length
属性减去预绑定参数的数量。bind
的新函数无法被arguments
或caller
属性访问,因为它们被视为已被封装。
不用 call
或者 apply
实现 bind
实现 bind
方法时,可以避免使用 call
或 apply
,而采用其他方法,如使用 Function.prototype
来调用原函数。
实现代码
Function.prototype.myBind = function (context, ...args) {
const fn = this;
return function boundFunction(...newArgs) {
// 创建一个唯一的属性名,避免覆盖原有属性
const fnSymbol = Symbol();
context[fnSymbol] = fn;
// 执行函数
const result = context[fnSymbol](...args, ...newArgs);
// 删除临时属性
delete context[fnSymbol];
return result;
};
};
// 示例
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Eve' };
const greetEve = greet.myBind(person, 'Hi');
greetEve('?'); // 输出: Hi, Eve?
方法解释
- 上下文绑定:将函数的执行上下文绑定到指定的对象
context
。 - 避免使用
call
/apply
:通过将函数作为对象的属性,直接调用函数来实现上下文绑定。 - 执行函数:调用函数,并传递参数。
- 清理临时属性:执行完函数后,删除临时添加的属性,避免污染对象。
注意事项
- 此方法同样无法处理构造函数的情况,如需支持构造函数使用,需要结合前述
bind
实现方法。 - 使用
Symbol
确保属性名的唯一性,避免与对象已有属性冲突。
手写 bind
考虑 new
一个 bind
函数的情况
在实现 bind
方法时,需要考虑通过 new
操作符创建实例的情况,确保新实例能够继承原函数的原型。
实现代码
Function.prototype.myBind = function (context, ...args) {
const fn = this;
function boundFunction(...newArgs) {
// 如果通过 new 调用,则忽略绑定的 context
const isNew = this instanceof boundFunction;
const finalContext = isNew ? this : context;
return fn.apply(finalContext, [...args, ...newArgs]);
}
// 维护原型链
boundFunction.prototype = Object.create(fn.prototype);
return boundFunction;
};
// 示例
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
const BoundPerson = Person.myBind(null, 'Frank');
const frank = new BoundPerson(40);
frank.sayName(); // 输出: My name is Frank
console.log(frank instanceof Person); // 输出: true
方法解释
- 检查调用方式:通过
this instanceof boundFunction
判断是否通过new
调用,如果是,则忽略绑定的context
,将this
作为上下文。 - 维护原型链:使用
Object.create(fn.prototype)
将boundFunction.prototype
设置为原函数fn.prototype
的副本,确保实例能够继承原函数的原型方法。 - 参数传递:合并预绑定参数和新传入的参数,并传递给原函数。
注意事项
- 当通过
new
调用绑定函数时,this
指向新创建的实例,而不是绑定的context
。 - 维护原型链是确保实例能够继承原函数的原型属性和方法的关键步骤。
手写 forEach
Array.prototype.forEach
方法用于遍历数组,并对每个元素执行指定的回调函数。以下是手写实现 forEach
的方法。
实现代码
Array.prototype.myForEach = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = Object(this);
const len = arr.length >>> 0; // 转换为无符号整数
for (let i = 0; i < len; i++) {
if (i in arr) {
callback.call(thisArg, arr[i], i, arr);
}
}
};
// 示例
const array = [1, 2, 3];
array.myForEach(function (element, index, arr) {
console.log(`Element: ${element}, Index: ${index}`);
});
// 输出:
// Element: 1, Index: 0
// Element: 2, Index: 1
// Element: 3, Index: 2
方法解释
- 类型检查:首先检查回调函数是否为函数类型,否则抛出错误。
- 处理数组:使用
Object(this)
确保this
是一个对象,并获取数组长度。 - 遍历数组:使用
for
循环遍历数组元素。 - 检查元素存在:使用
i in arr
确保元素存在,避免遍历稀疏数组时的未定义项。 - 执行回调:调用回调函数,并绑定
thisArg
作为上下文,传递当前元素、索引和数组本身。
注意事项
forEach
不会返回任何值,仅用于执行副作用操作。- 无法通过
break
或return
中断forEach
循环。
手写 map
Array.prototype.map
方法用于创建一个新数组,结果是对原数组的每个元素调用指定函数后的返回值。以下是手写实现 map
的方法。
实现代码
Array.prototype.myMap = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = Object(this);
const len = arr.length >>> 0;
const result = new Array(len);
for (let i = 0; i < len; i++) {
if (i in arr) {
result[i] = callback.call(thisArg, arr[i], i, arr);
}
}
return result;
};
// 示例
const array = [1, 2, 3];
const doubled = array.myMap(function (element) {
return element * 2;
});
console.log(doubled); // 输出: [2, 4, 6]
方法解释
- 类型检查:确保回调函数为函数类型,否则抛出错误。
- 处理数组:使用
Object(this)
确保this
是一个对象,并获取数组长度。 - 创建结果数组:初始化一个新数组
result
,长度与原数组相同。 - 遍历数组:使用
for
循环遍历原数组元素。 - 检查元素存在:使用
i in arr
确保元素存在,避免稀疏数组带来的未定义项。 - 执行回调:调用回调函数,传递当前元素、索引和数组本身,并将返回值赋给
result
数组的对应位置。 - 返回结果:返回新创建的
result
数组。
注意事项
map
会跳过稀疏数组中的未定义项。- 新数组的长度与原数组相同,即使某些元素被跳过。
手写 filter
Array.prototype.filter
方法用于创建一个新数组,包含所有通过指定函数测试的元素。以下是手写实现 filter
的方法。
实现代码
Array.prototype.myFilter = function (callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = Object(this);
const len = arr.length >>> 0;
const result = [];
for (let i = 0; i < len; i++) {
if (i in arr) {
const value = arr[i];
if (callback.call(thisArg, value, i, arr)) {
result.push(value);
}
}
}
return result;
};
// 示例
const array = [1, 2, 3, 4, 5];
const even = array.myFilter(function (element) {
return element % 2 === 0;
});
console.log(even); // 输出: [2, 4]
方法解释
- 类型检查:确保回调函数为函数类型,否则抛出错误。
- 处理数组:使用
Object(this)
确保this
是一个对象,并获取数组长度。 - 创建结果数组:初始化一个空数组
result
用于存储通过测试的元素。 - 遍历数组:使用
for
循环遍历原数组元素。 - 检查元素存在:使用
i in arr
确保元素存在,避免稀疏数组带来的未定义项。 - 执行回调:调用回调函数,传递当前元素、索引和数组本身。如果回调返回
true
,则将元素添加到result
数组中。 - 返回结果:返回新创建的
result
数组。
注意事项
filter
不会改变原数组。- 新数组中只包含通过测试的元素,元素的顺序与原数组相同。
手写 reduce
Array.prototype.reduce
方法用于对数组中的元素进行累计计算,最终得到一个单一的结果。以下是手写实现 reduce
的方法。
实现代码
Array.prototype.myReduce = function (callback, initialValue) {
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
const arr = Object(this);
const len = arr.length >>> 0;
let accumulator = initialValue;
let startIndex = 0;
// 如果没有提供初始值,则使用第一个元素作为累加器
if (accumulator === undefined) {
for (let i = 0; i < len; i++) {
if (i in arr) {
accumulator = arr[i];
startIndex = i + 1;
break;
}
}
if (accumulator === undefined) {
throw new TypeError('Reduce of empty array with no initial value');
}
}
for (let i = startIndex; i < len; i++) {
if (i in arr) {
accumulator = callback(accumulator, arr[i], i, arr);
}
}
return accumulator;
};
// 示例
const array = [1, 2, 3, 4];
const sum = array.myReduce(function (acc, curr) {
return acc + curr;
}, 0);
console.log(sum); // 输出: 10
方法解释
- 类型检查:确保回调函数为函数类型,否则抛出错误。
- 处理数组:使用
Object(this)
确保this
是一个对象,并获取数组长度。 - 初始化累加器:
- 如果提供了
initialValue
,则将其作为初始累加器,并从索引0
开始遍历。 - 如果未提供
initialValue
,则找到第一个存在的元素作为初始累加器,并从下一个索引开始遍历。
- 如果提供了
- 遍历数组:使用
for
循环遍历数组元素,从startIndex
开始。 - 检查元素存在:使用
i in arr
确保元素存在,避免稀疏数组带来的未定义项。 - 执行回调:调用回调函数,传递累加器、当前元素、索引和数组本身,并更新累加器。
- 返回结果:返回最终的累加器值。
注意事项
- 如果数组为空且未提供
initialValue
,会抛出错误。 reduce
可以用于实现复杂的数据转换和聚合操作,如求和、扁平化数组等。
手写数组拍平 flat
Array.prototype.flat
方法用于将嵌套的数组拍平成一维数组。以下是手写实现 flat
的方法。
实现代码
Array.prototype.myFlat = function (depth = 1) {
const result = [];
const flatten = (arr, currentDepth) => {
for (let item of arr) {
if (Array.isArray(item) && currentDepth > 0) {
flatten(item, currentDepth - 1);
} else {
result.push(item);
}
}
};
flatten(this, depth);
return result;
};
// 示例
const array = [1, [2, [3, [4]], 5]];
console.log(array.myFlat()); // 输出: [1, 2, [3, [4]], 5]
console.log(array.myFlat(2)); // 输出: [1, 2, 3, [4], 5]
console.log(array.myFlat(Infinity)); // 输出: [1, 2, 3, 4, 5]
方法解释
- 参数处理:接受一个可选参数
depth
,表示拍平的深度,默认为1
。如果depth
为Infinity
,则完全拍平数组。 - 递归拍平:
- 定义一个内部函数
flatten
,接受当前数组和当前深度。 - 遍历数组元素,如果元素是数组且当前深度大于
0
,则递归调用flatten
,并将深度减1
。 - 否则,将元素添加到结果数组中。
- 定义一个内部函数
- 返回结果:完成递归后,返回拍平后的结果数组。
方法二:使用栈迭代
通过使用栈(数组)来实现非递归的拍平方法。
Array.prototype.myFlat = function (depth = 1) {
const stack = [...this];
const result = [];
while (stack.length) {
const next = stack.shift();
if (Array.isArray(next) && depth > 0) {
stack.unshift(...next);
depth--;
} else {
result.push(next);
}
}
return result;
};
// 示例同上
注意事项
flat
方法不会改变原数组,而是返回一个新的数组。- 稀疏数组中的空位会被忽略。
手写原型继承
原型继承是 JavaScript 实现继承的一种方式。以下是手写实现原型继承的方法,包括原型链继承、构造函数继承、组合继承和寄生组合继承。
方法一:原型链继承
通过让子类的原型指向父类的实例,实现继承。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`);
};
function Child(name, age) {
this.age = age;
}
Child.prototype = new Parent('Default');
Child.prototype.constructor = Child;
// 示例
const child = new Child('Alice', 30);
child.sayName(); // 输出: Name: Default
console.log(child.age); // 输出: 30
缺点:
- 所有子类实例共享父类的引用类型属性,可能导致意外修改。
- 无法实现多继承。
方法二:构造函数继承
在子类构造函数中调用父类构造函数,使用 call
或 apply
绑定 this
。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 示例
const child = new Child('Bob', 25);
child.sayName(); // 输出: Name: Bob
console.log(child.age); // 输出: 25
缺点:
- 无法继承父类原型上的方法。
- 无法实现方法共享,每个子类实例都有父类方法的副本。
方法三:组合继承
结合原型链继承和构造函数继承的优点,解决各自的缺点。
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
Child.prototype = Object.create(Parent.prototype); // 原型链继承
Child.prototype.constructor = Child;
// 示例
const child = new Child('Charlie', 28);
child.sayName(); // 输出: Name: Charlie
console.log(child.age); // 输出: 28
优点:
- 既继承了父类的实例属性,又继承了父类的原型方法。
- 每个子类实例拥有独立的父类属性,避免共享引用类型属性的问题。
方法四:寄生组合继承
优化组合继承,避免在子类原型中多次调用父类构造函数。
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建父类原型的副本
prototype.constructor = child; // 修正构造函数指向
child.prototype = prototype; // 绑定子类原型
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
// 示例
const child = new Child('Dave', 22);
child.sayName(); // 输出: Name: Dave
console.log(child.age); // 输出: 22
优点:
- 只调用一次父类构造函数,提升性能。
- 避免了原型链继承的引用类型属性共享问题。
- 实现了父类原型方法的共享。
总结
不同的继承方法各有优缺点,选择合适的继承方式取决于具体需求。在现代 JavaScript 中,推荐使用 ES6 的 class
和 extends
语法来实现继承,语法更加简洁且易于理解。
手写 class
继承
ES6 引入了 class
和 extends
关键字,使得 JavaScript 的面向对象编程更加直观。以下是手写实现 class
继承的方法,结合 ES5 的原型继承机制。
示例代码
// 父类
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(`Name: ${this.name}`);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log(`Age: ${this.age}`);
}
}
// 示例
const child = new Child('Eve', 20);
child.sayName(); // 输出: Name: Eve
child.sayAge(); // 输出: Age: 20
console.log(child instanceof Parent); // 输出: true
手写实现 ES6 class
继承
通过 ES5 的函数和原型机制,模拟 ES6 class
和 extends
的功能。
// 父类构造函数
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(`Name: ${this.name}`);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数
this.age = age;
}
// 继承父类原型
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 子类方法
Child.prototype.sayAge = function () {
console.log(`Age: ${this.age}`);
};
// 示例
const child = new Child('Frank', 18);
child.sayName(); // 输出: Name: Frank
child.sayAge(); // 输出: Age: 18
console.log(child instanceof Parent); // 输出: true
方法解释
- 父类构造函数:定义父类的属性和方法。
- 子类构造函数:调用
Parent.call(this, name)
,继承父类的实例属性。 - 继承父类原型:通过
Object.create(Parent.prototype)
继承父类的原型方法,并修正子类的constructor
指向。 - 子类方法:在子类原型上定义子类特有的方法。
注意事项
- 使用
super
调用父类构造函数,确保this
被正确初始化。 - 修正子类的
constructor
指向,以保持原型链的正确性。 - ES6
class
支持更多的面向对象特性,如静态方法、getter/setter 等,可以通过进一步手写实现来学习。
用 Promise 封装一个 delay
函数
delay
函数用于在指定时间后执行某个操作,常用于异步编程。以下是使用 Promise
封装 delay
函数的实现。
实现代码
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// 示例
console.log('Start');
delay(2000).then(() => {
console.log('Executed after 2 seconds');
});
console.log('End');
// 输出:
// Start
// End
// (2秒后)
// Executed after 2 seconds
方法解释
- 创建 Promise:
delay
函数返回一个新的Promise
对象。 - 设置定时器:使用
setTimeout
在指定的毫秒数后调用resolve
,从而完成Promise
。 - 使用
then
:调用delay
函数后,可以使用.then
来执行延迟后的操作。
进阶实现:支持取消
为 delay
函数添加取消功能,允许在延迟期间取消操作。
function delay(ms) {
let timeoutId;
const promise = new Promise((resolve, reject) => {
timeoutId = setTimeout(resolve, ms);
});
return {
promise,
cancel: () => clearTimeout(timeoutId),
};
}
// 示例
const { promise, cancel } = delay(5000);
promise.then(() => {
console.log('Executed after 5 seconds');
});
// 取消延迟
setTimeout(() => {
cancel();
console.log('Delay canceled');
}, 2000);
// 输出:
// Delay canceled
注意:取消功能需要额外的逻辑处理,如使用 AbortController
或其他方式来通知取消状态。
实现字符串大数相加
在 JavaScript 中,Number 类型无法精确表示大于 2^53 - 1
的整数。以下是实现字符串表示的大数相加的方法,确保精度不丢失。
实现代码
function addBigNumbers(num1, num2) {
let carry = 0;
let result = '';
let i = num1.length - 1;
let j = num2.length - 1;
while (i >= 0 || j >= 0 || carry) {
const digit1 = i >= 0 ? parseInt(num1[i], 10) : 0;
const digit2 = j >= 0 ? parseInt(num2[j], 10) : 0;
const sum = digit1 + digit2 + carry;
carry = Math.floor(sum / 10);
const digit = sum % 10;
result = digit.toString() + result;
i--;
j--;
}
return result;
}
// 示例
const num1 = '123456789123456789';
const num2 = '987654321987654321';
console.log(addBigNumbers(num1, num2)); // 输出: '1111111111111111110'
方法解释
-
初始化变量:
carry
:进位值,初始为0
。result
:结果字符串,初始为空。i
,j
:分别指向num1
和num2
的末尾索引。
-
循环相加:
- 遍历两个字符串,从末尾开始逐位相加。
- 若某个字符串已遍历完,则该位数视为
0
。 - 计算当前位的和,并更新进位值。
- 将当前位的结果添加到
result
字符串前端。
-
处理最后的进位:
- 如果最后有进位,需将其添加到
result
前端。
- 如果最后有进位,需将其添加到
-
返回结果:完成所有位数的相加后,返回
result
字符串。
优化与扩展
- 输入校验:检查输入是否为有效的非负整数字符串。
- 支持负数:扩展算法以支持负数的相加和相减。
- 性能优化:对于非常大的数,可以优化字符串操作,减少性能开销。
示例
const a = '9999999999999999999999999999';
const b = '1';
console.log(addBigNumbers(a, b)); // 输出: '10000000000000000000000000000'