手撕代码

2024/10/7 前端

# 前言

记录一些手撕代码题目。

# new

function myNew(constructor, ...args) {
  // 1. 创建一个空对象
  const obj = {};

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

  // 3. 将构造函数的this绑定到空对象上,并执行构造函数中的代码
  const result = constructor.apply(obj, args);

  // 4. 如果构造函数返回一个对象,则返回该对象;否则,返回步骤1创建的空对象
  return (typeof result === 'object' && result !== null) ? result : obj;
}

function Person(name, age) {
  this.name = name
  this.age = age
}

const p = myNew(Person, "zhangsan", 18)
console.log(p)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 防抖debounce

function debounce(func, delay) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, arguments); // 使用apply来确保this指向正确
    }, delay);
  };
}

const test = (...args) => {
  console.log('click test button', args)
}

document.querySelector("#test").addEventListener('click', () => {
  debounce(test, 1000)(1, 2, 3)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

防抖和节流也算是闭包的经典应用了。因为防抖是一个工具类的函数,我们需要让他可以适配所有函数,那么它本身也是一个函数,这样才能接收参数,所以在防抖函数体中返回一个函数。

这其中涉及到一个this指向问题。如果是按照上述写法,那么this是指向调用debounce返回函数的对象,这没问题。

但是如果使用了箭头函数,则需要切记一点:箭头函数没有自己的this,他拿的是外层环境的this,并且在函数定义时就已经确定了,而不是等到调用时才确定。

一般情况下也不会涉及与this有关的操作,如果有的话则需要确定好this的指向问题,这种情况最好就直接不要使用箭头函数。

# 节流throttle

function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

function handleScroll() {
  console.log('页面滚动');
}

window.addEventListener('scroll', throttle(handleScroll, 100));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

防抖和节流的都是用来限制事件的调用次数的,但是他们限制的形式不同。防抖是在一定时间内多次点击,只有最后一次生效,而节流是限制两次事件触发的时间间隔。

# 柯里化

柯里化(Currying)是一种将使用多个参数的函数转换成一系列使用一个或多个参数的函数的技术。这种技术是由数学家哈斯凯尔·柯里(Haskell Curry)命名的。柯里化的主要目的是将复杂函数的调用分解成几个步骤,使得每一步都返回一个新的函数,直到最终执行。

柯里化的优点:

  • 参数复用:可以复用已经提供的参数。
  • 延迟计算:只有在所有参数都提供之后,函数才会被执行。
  • 函数重用:可以创建新的函数,这些函数具有预设的参数。
function curry(func) {
  return function carried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args)
    } else {
      return function (...args2) {
        return carried.apply(this, args.concat(args2))
      }
    }
  }
}

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

应用场景:

  • 数据校验:在数据校验时,可以创建一个通用的校验函数,然后通过柯里化来固定校验规则,使得校验逻辑更加清晰和易于管理。
  • 函数式编程:在函数式编程中,柯里化经常用于创建高阶函数和部分应用,例如使用 map、filter 等高阶函数时,可以通过柯里化来传递函数和参数。

# 数组扁平化

ES2019引入的新方法Array.prototype.flat(depth),数组的原始层级是0,参数depth指的是需要扁平化的层级,默认是1,即往下展开一层。

本次用递归来实现这个函数。

const myFlat = (arr, depth = 1) => {
  if (depth === 0) return arr
  const res = []
  for (const item of arr) {
    if (Array.isArray(item)) {
      res.push(...myFlat(item, depth - 1))
    } else {
      res.push(item)
    }
  }
  return res
}

const arr = [1,2, [3, 4, [5, 6], 7], 8]
console.log(myFlat(arr, 2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

此外还可以用数组的reduceconcat方法来进行简写:

const myFlat = (arr, depth = 1) => {
  if (depth === 0) return arr
  return arr.reduce((pre, cnt) => {
    return pre.concat(Array.isArray(cnt) ? myFlat(cnt, depth - 1) : cnt)
  }, [])
}
const arr = [1,2, [3, 4, [5, 6], 7], 8]
console.log(myFlat(arr, Infinity))
1
2
3
4
5
6
7
8

# 深拷贝

手写深拷贝需要涉及的方面太多,这里写一个简单的实现方法,存在一定的漏洞,例如循环引用、日期、函数类型可能拷贝结果不准确甚至报错。

注意:使用for...in来遍历对象时,会把原型对象上的key也遍历出来。可以使用Object.keys来获取对象的key数组,则不会出现上述情况,或者使用函数Object.hasOwnProperty来进行判断。

const deepClone = (origin) => {
  const obj = {}
  for(const key of Object.keys(origin)) {
    // 只考虑基本对象类型
    if (origin[key] && Object.prototype.toString.call(origin[key]) === '[object Object]'){
      obj[key] = deepClone(origin[key])
    } else {
      obj[key] = origin[key]
    }
  }
  return obj
}
1
2
3
4
5
6
7
8
9
10
11
12

以下是AI写的深拷贝,考虑的全面很多:

function deepClone(obj, hash = new WeakMap()) {
  // 如果是基本数据类型,直接返回
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  // 如果是日期类型,创建一个新的日期对象
  if (obj instanceof Date) {
    return new Date(obj);
  }

  // 如果是正则类型,创建一个新的正则对象
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }

  // 如果循环引用,则直接返回已拷贝的对象
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 获取对象的构造函数,并根据构造函数来创建新对象
  const cloneObj = new obj.constructor();
  // 缓存原对象和拷贝的对象,避免循环引用导致的问题
  hash.set(obj, cloneObj);

  // 递归拷贝对象中的每个属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { // 只拷贝对象自身的属性
      cloneObj[key] = deepClone(obj[key], hash); // 递归拷贝
    }
  }
  
  // 如果是数组,拷贝数组的特殊属性
  if (Array.isArray(obj)) {
    cloneObj.length = obj.length;
  }

  return cloneObj;
}

// 使用示例
const original = {
  number: 1,
  bool: false,
  string: 'string',
  symbol: Symbol('sym'),
  obj: {
    nested: 'object',
  },
  array: [1, 2, 3],
  date: new Date(),
  regex: /regex/g,
  func: function() {
    console.log('function');
  },
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  buffer: Buffer.from([1, 2, 3]),
};

const copied = deepClone(original);
console.log(copied);
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

# Promise.all

Last Updated: 2024/10/20 08:22:07