跳到主要内容

手写实现常见的 JavaScript 函数和概念

  1. 手写数组去重
  2. 不用 Set 如何去重
  3. 如何在原数组上去重
  4. 如何对数组里有相同内容的对象去重
  5. 如何按数组里包含的对象属性字段去重
  6. 手写深拷贝
  7. 手写处理循环引用的深拷贝
  8. 拷贝一个函数
  9. 手写一个 trim 函数
  10. 手写一个 curry 函数
  11. 写一个观察者 EventBus 含 emit、on、once、off
  12. 手写一个 instanceof
  13. 手写一个 new
  14. 手写一个 callapply
  15. call/apply 实现 bind
  16. 不用 call 或者 apply 实现 bind
  17. 手写 bind 考虑 new 一个 bind 函数的情况
  18. 手写 forEach
  19. 手写 map
  20. 手写 filter
  21. 手写 reduce
  22. 手写数组拍平 flat
  23. 手写原型继承
  24. 手写 class 继承
  25. 用 Promise 封装一个 delay 函数
  26. 实现字符串大数相加

手写数组去重

数组去重是 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;
}

方法三:利用 filterindexOf

结合 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' }]

方法三:使用 filterfindIndex

结合 filterfindIndex 方法实现对象数组的去重。

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' }
// ]

方法三:使用 filterfindIndex

结合 filterfindIndex,根据指定属性去重。

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

解释

  1. WeakMap:用于存储已经复制过的对象,键为原对象,值为克隆后的对象。
  2. 循环引用检查:在克隆前检查对象是否已存在于 WeakMap 中,如果存在,则直接返回克隆对象,避免无限递归。
  3. 数据类型处理:对不同的数据类型(如 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

注意:这种方法无法复制函数的闭包和内部状态。

方法二:使用 evaltoString

通过获取函数的字符串表示,再通过 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!'

方法三:使用 splitjoin

通过拆分和重组字符串,去除首尾空白。

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 含 emitononceoff

EventBus 是一种常见的设计模式,用于实现发布-订阅机制。以下是手写实现 EventBus 的方法,包括 emitononceoff 方法。

实现代码

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'); // 无输出

方法解释

  1. on(event, listener):订阅指定事件,将监听器添加到事件列表中。
  2. once(event, listener):订阅一次性事件,监听器在第一次触发后自动移除。
  3. off(event, listener):取消订阅指定事件的监听器。
  4. 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

方法解释

  1. 检查类型:首先检查 obj 是否为对象类型且不为 null
  2. 遍历原型链:使用 Object.getPrototypeOf 获取对象的原型,并遍历原型链。
  3. 比较原型:在遍历过程中,若发现某一层原型与构造函数的 prototype 相同,则返回 true
  4. 结束条件:若遍历到原型链末端(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

方法解释

  1. 创建新对象:使用 Object.create 创建一个新对象,该对象的原型指向构造函数的 prototype
  2. 执行构造函数:使用 constructor.apply 将构造函数的上下文绑定到新对象,并传递参数。
  3. 返回结果:如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象。

注意事项

  • 构造函数可以显式返回一个对象,覆盖默认的新对象。
  • 如果构造函数返回的是基本类型,则忽略返回值,仍返回新创建的对象。

手写一个 callapply

callapply 方法用于改变函数的执行上下文(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?

方法解释

  1. 上下文绑定:将函数的执行上下文绑定到指定的对象 context
  2. 避免属性冲突:使用 Symbol 创建一个唯一的属性名,确保不会覆盖对象已有的属性。
  3. 执行函数:调用函数,并传递参数。
  4. 清理临时属性:执行完函数后,删除临时添加的属性,避免污染对象。

注意事项

  • call 接受的是一系列参数,而 apply 接受的是一个参数数组。
  • 在严格模式下,如果 contextnullundefined,则 this 不会被绑定到全局对象。

call/apply 实现 bind

bind 方法用于创建一个新函数,并将其 this 值永久绑定到指定的对象。以下是使用 callapply 实现 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

方法解释

  1. 参数预先绑定bind 方法允许预先绑定部分参数,在调用时补充剩余参数。
  2. 处理构造函数:通过检查 this instanceof boundFunction 来判断是否通过 new 调用,如果是,则忽略绑定的 context
  3. 维护原型链:将 boundFunction.prototype 设置为原函数 fn.prototype 的副本,以确保实例能够继承原函数的原型。

注意事项

  • bind 创建的新函数具有原函数的 length 属性减去预绑定参数的数量。
  • bind 的新函数无法被 argumentscaller 属性访问,因为它们被视为已被封装。

不用 call 或者 apply 实现 bind

实现 bind 方法时,可以避免使用 callapply,而采用其他方法,如使用 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?

方法解释

  1. 上下文绑定:将函数的执行上下文绑定到指定的对象 context
  2. 避免使用 call/apply:通过将函数作为对象的属性,直接调用函数来实现上下文绑定。
  3. 执行函数:调用函数,并传递参数。
  4. 清理临时属性:执行完函数后,删除临时添加的属性,避免污染对象。

注意事项

  • 此方法同样无法处理构造函数的情况,如需支持构造函数使用,需要结合前述 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

方法解释

  1. 检查调用方式:通过 this instanceof boundFunction 判断是否通过 new 调用,如果是,则忽略绑定的 context,将 this 作为上下文。
  2. 维护原型链:使用 Object.create(fn.prototype)boundFunction.prototype 设置为原函数 fn.prototype 的副本,确保实例能够继承原函数的原型方法。
  3. 参数传递:合并预绑定参数和新传入的参数,并传递给原函数。

注意事项

  • 当通过 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

方法解释

  1. 类型检查:首先检查回调函数是否为函数类型,否则抛出错误。
  2. 处理数组:使用 Object(this) 确保 this 是一个对象,并获取数组长度。
  3. 遍历数组:使用 for 循环遍历数组元素。
  4. 检查元素存在:使用 i in arr 确保元素存在,避免遍历稀疏数组时的未定义项。
  5. 执行回调:调用回调函数,并绑定 thisArg 作为上下文,传递当前元素、索引和数组本身。

注意事项

  • forEach 不会返回任何值,仅用于执行副作用操作。
  • 无法通过 breakreturn 中断 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]

方法解释

  1. 类型检查:确保回调函数为函数类型,否则抛出错误。
  2. 处理数组:使用 Object(this) 确保 this 是一个对象,并获取数组长度。
  3. 创建结果数组:初始化一个新数组 result,长度与原数组相同。
  4. 遍历数组:使用 for 循环遍历原数组元素。
  5. 检查元素存在:使用 i in arr 确保元素存在,避免稀疏数组带来的未定义项。
  6. 执行回调:调用回调函数,传递当前元素、索引和数组本身,并将返回值赋给 result 数组的对应位置。
  7. 返回结果:返回新创建的 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]

方法解释

  1. 类型检查:确保回调函数为函数类型,否则抛出错误。
  2. 处理数组:使用 Object(this) 确保 this 是一个对象,并获取数组长度。
  3. 创建结果数组:初始化一个空数组 result 用于存储通过测试的元素。
  4. 遍历数组:使用 for 循环遍历原数组元素。
  5. 检查元素存在:使用 i in arr 确保元素存在,避免稀疏数组带来的未定义项。
  6. 执行回调:调用回调函数,传递当前元素、索引和数组本身。如果回调返回 true,则将元素添加到 result 数组中。
  7. 返回结果:返回新创建的 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

方法解释

  1. 类型检查:确保回调函数为函数类型,否则抛出错误。
  2. 处理数组:使用 Object(this) 确保 this 是一个对象,并获取数组长度。
  3. 初始化累加器
    • 如果提供了 initialValue,则将其作为初始累加器,并从索引 0 开始遍历。
    • 如果未提供 initialValue,则找到第一个存在的元素作为初始累加器,并从下一个索引开始遍历。
  4. 遍历数组:使用 for 循环遍历数组元素,从 startIndex 开始。
  5. 检查元素存在:使用 i in arr 确保元素存在,避免稀疏数组带来的未定义项。
  6. 执行回调:调用回调函数,传递累加器、当前元素、索引和数组本身,并更新累加器。
  7. 返回结果:返回最终的累加器值。

注意事项

  • 如果数组为空且未提供 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]

方法解释

  1. 参数处理:接受一个可选参数 depth,表示拍平的深度,默认为 1。如果 depthInfinity,则完全拍平数组。
  2. 递归拍平
    • 定义一个内部函数 flatten,接受当前数组和当前深度。
    • 遍历数组元素,如果元素是数组且当前深度大于 0,则递归调用 flatten,并将深度减 1
    • 否则,将元素添加到结果数组中。
  3. 返回结果:完成递归后,返回拍平后的结果数组。

方法二:使用栈迭代

通过使用栈(数组)来实现非递归的拍平方法。

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

缺点

  • 所有子类实例共享父类的引用类型属性,可能导致意外修改。
  • 无法实现多继承。

方法二:构造函数继承

在子类构造函数中调用父类构造函数,使用 callapply 绑定 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 的 classextends 语法来实现继承,语法更加简洁且易于理解。


手写 class 继承

ES6 引入了 classextends 关键字,使得 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 classextends 的功能。

// 父类构造函数
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

方法解释

  1. 父类构造函数:定义父类的属性和方法。
  2. 子类构造函数:调用 Parent.call(this, name),继承父类的实例属性。
  3. 继承父类原型:通过 Object.create(Parent.prototype) 继承父类的原型方法,并修正子类的 constructor 指向。
  4. 子类方法:在子类原型上定义子类特有的方法。

注意事项

  • 使用 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

方法解释

  1. 创建 Promisedelay 函数返回一个新的 Promise 对象。
  2. 设置定时器:使用 setTimeout 在指定的毫秒数后调用 resolve,从而完成 Promise
  3. 使用 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'

方法解释

  1. 初始化变量

    • carry:进位值,初始为 0
    • result:结果字符串,初始为空。
    • i, j:分别指向 num1num2 的末尾索引。
  2. 循环相加

    • 遍历两个字符串,从末尾开始逐位相加。
    • 若某个字符串已遍历完,则该位数视为 0
    • 计算当前位的和,并更新进位值。
    • 将当前位的结果添加到 result 字符串前端。
  3. 处理最后的进位

    • 如果最后有进位,需将其添加到 result 前端。
  4. 返回结果:完成所有位数的相加后,返回 result 字符串。

优化与扩展

  • 输入校验:检查输入是否为有效的非负整数字符串。
  • 支持负数:扩展算法以支持负数的相加和相减。
  • 性能优化:对于非常大的数,可以优化字符串操作,减少性能开销。

示例

const a = '9999999999999999999999999999';
const b = '1';
console.log(addBigNumbers(a, b)); // 输出: '10000000000000000000000000000'