前端手撕源码

114 阅读34分钟

深入对象和原型链

JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

创建对象

手写 new

思路

  • 创建一个空对象(即{});
  • 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 执行构造函数方法,属性和方法被添加到this引用的对象中;
  • 如果构造函数返回的是一个对象,则返回这个对象;否则返回新创建的对象

代码


function myNew(constructor, ...args) {
  // 1.创建一个空的对象
  const obj = {};
  // 2.为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
  Object.setPrototypeOf(obj, constructor.prototype);
  // 3.执行构造函数方法,属性和方法被添加到this引用的对象中,保存执行后返回结果
  const res = constructor.call(obj, ...args);
  // 4. 如果构造函数返回的是一个对象,则返回这个对象;否则返回新创建的对象
  return res instanceof Object ? res : obj;
}


手写 Object.create()

Object.create(proto, propertiesObject)

思路

  • 创建一个临时性的构造函数
  • 将传入的对象作为这个构造函数的原型
  • 最后返回了这个临时类型的一个新实例。

代码

function myCreate(obj) {
  var args = [].slice.call(arguments, 1);

  for (let i of args) {
    Object.assign(obj, i);
  }
  function F() {}
  F.prototype = obj;
  return new F(args);
}

new,Object.create(),{}区别

Object.create 允许你创建一个对象并指定它的原型对象,而 new 关键字通常和构造函数一起使用,用于创建一个由该构造函数实例化的新对象。{} 是一种简便的方式创建一个空对象,它的原型是 Object.prototype

  • Object.create:允许你指定新对象的原型,可以创建一个继承自指定对象的新对象,只是原型指向源对象,并不会继承它的任何属性。
  • new 关键字:用于实例化构造函数,创建一个与该构造函数相关联的新对象,会继承原型对象的属性和方法。
  • {}:简单地创建一个空对象,它继承自 Object.prototype

因此,主要区别在于原型的指定和构造函数的实例化。

JavaScript 继承机制

深入了解:JavaScript常用八种继承方案 - 掘金 (juejin.cn)

原型链继承

思路

  • 在JavaScript中,原型链继承是一种基于原型链的继承方式。它通过将一个对象的原型设置为另一个对象的实例来实现继承。这种方式通过原型链使得一个对象可以访问另一个对象的属性和方法。

优点

  • 简单易懂,易于理解和实现。

缺点

  • 共享属性:所有实例在继承属性时是共享的,当一个实例修改了继承的属性,会影响到其他实例。
  • 无法传参:无法向父类构造函数传递参数,因为所有实例都是通过相同的原型对象进行创建的。
  • 不适合动态属性:原型上的属性是被所有实例共享的,如果原型上的引用类型属性被修改,会影响所有实例。

代码

// 创建一个 Animal 构造函数
function Animal() {
  this.food = ["milk", "bone"];
}

// 在 Animal 的原型上定义一个方法
Animal.prototype.walk = function () {
  console.log(this.name + " is walking.");
};

// 创建一个 Dog 构造函数,通过原型链继承 Animal
function Dog(name, breed) {
  this.name = name;
  this.breed = breed;
}

// 使用 Animal 的实例作为 Dog 的原型
Dog.prototype = new Animal();

// 在 Dog 的原型上定义一个方法
Dog.prototype.bark = function () {
  console.log("Woof!");
};

// 测试
// 创建 Dog 实例并调用方法
var myDog1 = new Dog("Buddy", "Golden Retriever");
myDog1.walk(); // 输出:Buddy is walking.
myDog1.bark(); // 输出:Woof!
myDog1.food.push("beef");// 给 Buddy 加餐牛肉
var myDog2 = new Dog("Pop", "Border Collie");
myDog2.walk(); // 输出:Pop is walking.
myDog2.bark(); // 输出:Woof!

myDog2.food.push("bacon");// 给 Pop 加餐培根
console.log(myDog1.food, myDog2.food);
// 加餐公用了
// 输出: [ 'milk', 'bone', 'beef', 'bacon' ] [ 'milk', 'bone', 'beef', 'bacon' ]

借用构造函数继承

借用构造函数继承是通过在子类构造函数中调用父类构造函数来实现继承的一种方式。

思路

  • 在子类构造函数中通过 callapply 或者 bind 方法调用父类的构造函数,从而在子类实例中创建父类的属性。

优点

  • 可以继承父类构造函数中的属性。
  • 避免了原型链继承的共享属性问题。

缺点

  • 无法继承父类原型上的方法,因为只是在子类实例中创建了父类的属性,而没有共享父类的原型。
  • 每个子类实例都会有一份父类的属性的副本,可能会造成内存浪费。

代码

// 创建一个 Animal 构造函数
function Animal(name) {
  this.name = name;
  this.food = ["milk", "bone"];
  this.eat = () => {
    console.log(this.name + " is eating.");
  };
}

// 在 Animal 的原型上定义一个方法
Animal.prototype.walk = function () {
  console.log(this.name + " is walking.");
};

// 创建一个 Dog 构造函数,通过原型链继承 Animal
function Dog(name, breed) {
  this.breed = breed;
  Animal.call(this, name);
}

// 在 Dog 的原型上定义一个方法
Dog.prototype.bark = function () {
  console.log(this.name + " Woof!");
};

// 测试
// 创建 Dog 实例并调用方法
var myDog1 = new Dog("Buddy", "Golden Retriever");
var myDog2 = new Dog("Pop", "Border Collie");
myDog1.bark(); // Buddy Woof!
myDog2.bark(); // Pop Woof!

// 可以继承父类构造函数中的属性。
myDog1.eat(); // Buddy is eating.
myDog2.eat(); // Pop is eating.

// 解决了原型链继承的共享属性问题
myDog1.food.push("beef"); // 给 Buddy 加餐牛肉
myDog2.food.push("bacon"); // 给 Pop 加餐培根
console.log(myDog1.food, myDog2.food);
// 输出:[ 'milk', 'bone', 'beef' ] [ 'milk', 'bone', 'bacon' ]

// 报错:无法继承父类原型上的方法
myDog1.walk();

组合继承

组合继承是指通过将 原型链继承借用构造函数继承 相结合,来实现对父类属性和方法的继承。

思路

  • 在子类构造函数内部通过调用父类构造函数,实现对父类属性的继承。
  • 使用 Child.prototype = new Parent() 将子类的原型指向父类的实例,从而继承父类的方法。

优点

  • 继承了父类构造函数的属性。
  • 继承了父类原型上的方法。

缺点

  • 调用了两次父类构造函数,可能会造成一些性能上的损失。
  • 子类的原型链上会包含父类的实例,可能会导致一些意想不到的问题。

代码

// 创建一个 Animal 构造函数
function Animal(name) {
  this.name = name;
  this.food = ["milk", "bone"];
  this.eat = () => {
    console.log(this.name + " is eating.");
  };
}

// 在 Animal 的原型上定义一个方法
Animal.prototype.walk = function () {
  console.log(this.name + " is walking.");
};

// 创建一个 Dog 构造函数,通过原型链继承 Animal
function Dog(name, breed) {
  this.breed = breed;

  Animal.call(this, name);
}
// 使用 Animal 的实例作为 Dog 的原型
Dog.prototype = new Animal();

// 在 Dog 的原型上定义一个方法
Dog.prototype.bark = function () {
  console.log(this.name + " Woof!");
};

// 测试
// 创建 Dog 实例并调用方法
var myDog1 = new Dog("Buddy", "Golden Retriever");
var myDog2 = new Dog("Pop", "Border Collie");
myDog1.bark(); // Buddy Woof!
myDog2.bark(); // Pop Woof!

// 可以继承父类构造函数中的属性。
myDog1.eat(); // Buddy is eating.
myDog2.eat(); // Pop is eating.

// 解决了原型链继承的共享属性问题
myDog1.food.push("beef"); // 给 Buddy 加餐牛肉
myDog2.food.push("bacon"); // 给 Pop 加餐培根
console.log(myDog1.food, myDog2.food);
// 输出:[ 'milk', 'bone', 'beef' ] [ 'milk', 'bone', 'bacon' ]

// 继承了父类原型上的方法。
myDog1.walk(); // Buddy is walking.

原型式继承

思路

  • 创建一个临时构造函数。
  • 将要用作新对象原型的对象作为临时构造函数的实例。
  • 返回这个临时构造函数的实例。

优点

  • 不需要定义构造函数,更加灵活。
  • 可以基于一个对象创建多个新对象,且这些新对象之间相互独立。

缺点

  • 如果原型对象中的属性值被修改,那么所有基于该原型的对象都会受到影响,因为它们共享同一个原型。
  • 无法传递参数,因为没有构造函数。

代码

// 原型对象
var animal = {
  type: "Animal",
  speak: function () {
    console.log("I am an animal");
  },
};

// 基于原型对象创建新对象
var dog = Object.create(animal);
dog.type = "Dog"; // 修改属性
dog.bark = function () {
  console.log("Woof!");
};

// 创建另一个基于原型的新对象
var cat = Object.create(animal);
cat.type = "Cat"; // 修改属性
cat.meow = function () {
  console.log("Meow!");
};

// 测试
console.log(dog.type); // 输出:Dog
dog.speak(); // 输出:I am an animal
dog.bark(); // 输出:Woof!

console.log(cat.type); // 输出:Cat
cat.speak(); // 输出:I am an animal
cat.meow(); // 输出:Meow!

寄生式继承

寄生式继承是一种基于已有对象创建新对象的继承方式,与 原型式继承 类似,但在新对象上增加了一些额外的属性或方法。

例如上述示例,我们可以将下列公用的属性和函数提取到函数中。

// 狗的类型不会改变,狗都会吠叫
dog.type = "Dog"; 
dog.bark = function() { 
    console.log("Woof!"); 
};

思路

  • 创建一个函数,该函数内部以某种方式增强对象。
  • 返回这个对象。

优点

  • 可以在对象上增加额外的属性或方法。
  • 不需要定义构造函数,更加灵活。

缺点

  • 与原型式继承一样,如果原型对象中的属性值被修改,那么所有基于该原型的对象都会受到影响,因为它们共享同一个原型。
  • 无法传递参数,因为没有构造函数。

代码

// 原型对象
var animal = {
  type: "Animal",
  speak: function () {
    console.log("I am an animal");
  },
};

// 寄生式继承函数
function createDog() {
  var dog = Object.create(animal); // 基于原型对象创建新对象
  dog.type = "Dog"; // 增加额外属性
  dog.bark = function () {
    console.log("Woof!");
  };
  return dog;
}

// 创建新对象
var myDog = createDog();

// 测试
console.log(myDog.type); // 输出:Dog
myDog.speak(); // 输出:I am an animal
myDog.bark(); // 输出:Woof!


寄生组合式继承

寄生组合式继承是一种通过借用构造函数来继承属性,通过原型链的混合形式来继承方法的继承方式。它结合了借用 构造函数原型链继承 的优点。

思路

  • 在子类构造函数内部通过调用父类构造函数,实现对父类属性的继承。
  • 使用 Object.create() 方法将子类的原型指向父类的原型对象,从而继承父类的方法。

优点

  • 避免了子类的原型链上包含父类的实例,避免了一些潜在的问题。

代码

// 创建一个 Animal 构造函数
function Animal(name) {
  this.name = name;
  this.food = ["milk", "bone"];
}

// 在 Animal 的原型上定义一个方法
Animal.prototype.walk = function () {
  console.log(this.name + " is walking.");
};

// 定义一个子类构造函数,通过寄生组合式继承实现对父类属性和方法的继承
function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
  this.break = function () {
    console.log(this.name + " Woof!");
  };
}
// 用 Animal 的实例作为 Dog的原型
// 解决共享同一个原型的问题
Dog.prototype = Object.create(Animal.prototype); // 原型链继承,继承父类的方法
Dog.prototype.constructor = Dog; // 修正构造函数指向

混入方式继承多个对象

混入(Mixins)是一种在对象之间复制属性的技术,可用于实现对象之间的组合和复用。通过混入方式,一个对象可以从多个其他对象中复制属性和方法。

思路

  • 定义多个对象:首先需要定义包含需要混入的属性和方法的多个源对象。
  • 目标对象定义:然后定义一个目标对象,该对象将继承多个源对象的属性和方法。
  • 混入属性和方法:使用合适的方式( 如 Object.assign() )将源对象中的属性和方法混入到目标对象中。
  • 使用目标对象:创建实例或使用目标对象,以便利用混入的属性和方法。

优点

  • 灵活性:允许在不同对象之间共享功能,从而实现更灵活的组合。
  • 复用性:可以在不同的对象之间重复使用已定义的功能,避免了重复编写相同的代码。
  • 解耦合:可以将功能模块化,并解耦对象之间的依赖,使代码更易于维护和扩展。
  • 动态性:可以在运行时动态地将功能混入到对象中,从而实现动态改变对象的行为。

缺点

  • 命名冲突:如果不小心出现了相同名称的属性或方法,可能会导致意外的行为或错误。
  • 复杂性:当混入的对象较多时,代码可能变得复杂且难以维护。
  • 隐藏性:混入的对象可能会包含大量的属性和方法,使得对象的行为变得难以理解和预测。
  • 依赖关系不清晰:如果对象之间相互混入,可能导致对象之间的依赖关系变得模糊,使得代码难以理解。

代码

const canEat = {
  eat: function () {
    console.log("Eating");
  },
};
const canWalk = {
  walk: function () {
    console.log("Walking");
  },
};
const canDrink = {
  drink: function () {
    console.log("Drinking");
  },
};
function Animal(name) {
  this.name = name;
}

// 将多个对象的属性和方法混入到目标对象
Object.assign(Animal.prototype, canEat, canWalk, canDrink);

// 测试
const dog = new Animal("dog");
dog.eat(); // Eating
dog.walk(); // Walking
dog.drink(); // Drinking

ES6类继承 extends

思路

  • 定义父类:首先定义一个父类,其中包含要被子类继承的属性和方法。
  • 创建子类:使用 extends 关键字创建一个子类,子类可以继承父类的属性和方法。
  • 调用父类构造函数:在子类的构造函数中使用 super() 方法调用父类的构造函数,以便初始化父类的属性。
  • 扩展子类:子类可以通过扩展或重写父类的方法,以实现特定的行为。

优点

  • 清晰的语法:使用 extends 关键字能够清晰地表明类之间的继承关系,提高了代码的可读性。
  • 便于维护:类继承可以更好地遵循面向对象编程的原则,使得代码结构更加清晰,易于维护。
  • 代码复用:可以通过继承实现代码的复用,子类可以继承父类的属性和方法,避免重复编写相同的代码。
  • 灵活性:子类可以覆盖父类的方法,实现特定的行为,从而提高了代码的灵活性。

缺点

  • 过度耦合:类继承可能导致子类与父类之间的耦合过度,使得代码更加脆弱,难以修改和扩展。
  • 层次结构复杂性:当类继承关系过深或过于复杂时,会增加代码的复杂度,不易管理和维护。

代码

// 定义父类
class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(this.name + " makes a noise.");
  }
}

// 创建子类,并继承父类
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数进行初始化
    this.breed = breed;
  }

  // 覆盖父类方法
  speak() {
    console.log(this.name + " barks.");
  }

  // 子类特有方法
  fetch() {
    console.log(this.name + " fetches the ball.");
  }
}

// 创建子类实例
let dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // 输出: Buddy barks.
dog.fetch(); // 输出: Buddy fetches the ball.

super 的使用

1. 在子类构造函数中调用父类构造函数

   class Parent {
     constructor(name) {
       this.name = name;
     }
   }

   class Child extends Parent {
     constructor(name, age) {
       super(name); // 调用父类构造函数进行初始化
       this.age = age;
     }
   }

在这个例子中,super(name) 调用了父类 Parent 的构造函数,以便对继承自父类的属性进行初始化。

2. 在子类方法中调用父类方法

   class Parent {
     show() {
       console.log('Hello from parent');
     }
   }

   class Child extends Parent {
     show() {
       super.show(); // 调用父类方法
       console.log('Hello from child');
     }
   }

工具函数和函数式编程

类型判断函数

手写 type

思路

  • “根”原型(Object.prototype)下,有个toString的方法,记录着所有 数据类型(构造函数)
  • 让传入的对象,执行 “根”原型的toString方法

代码

function myType(target) {
  // 让传入的对象,执行 “根”原型的toString方法
  target = Object.prototype.toString.call(target);
  return target.slice(8, -1);
}

手写 instanceof

思路

  • 获取对象的原型__proto__
  • 判断当前构造函数是否是原型__proto__
  • 若不是沿着对象的原型链__proto__->__proto__向上查找

代码


function myInstanceof(obj, target) {
  //  如果查找的目标不是函数或者没有原型,抛出错误
  if (typeof target !== "function" || !target.prototype) {
    throw new Error("type error");
  }
  // 如果obj不是对象,返回false
  // 注意:Object(value)表示将value转成一个对象
  // new Object(value)则表示新生成一个对象,它的值是value。
  if (Object(obj) !== obj) {
    return false;
  }
  // 获取对象的原型
  // 为什么不用 obj.__proto__ 获取原型?(已弃用)
  // 使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 获取|修改原型
  let __proto__ = Object.getPrototypeOf(obj);

  // 当查找到原型链尽头跳出循环
  while (__proto__ !== null) {
    if (__proto__ === target.prototype) {
      return true;
    }
    // 沿着原型链查找
    __proto__ = getPrototypeOf(__proto__);
  }

  return false;
}

常见类型判断结果

目标类型typeofinstanceof Object
undefinedundefinedfalse
nullobjectfalse
字符串stringfalse
数字numberfalse
布尔值booleanfalse
数组objecttrue
对象objecttrue
集合setobjecttrue
映射mapobjecttrue
函数functiontrue
symbolsymbolfalse

对基本类型使用 instanceof 操作符通常会返回 false。这是因为 instanceof 用于检查对象的原型链,而基本类型不是对象,它们没有原型链,可用于判断基本类型。

数组判断 Array.isArray(arr)

拷贝

浅拷贝

浅拷贝是指创建一个新的对象,新对象中的元素是原始对象中元素的引用。

思路

  • 遍历原始对象,对于基本数据类型,直接复制其值到新对象中。
  • 对于引用类型(如列表、对象等),复制其引用到新对象中,而不是复制其内容。

代码

// 方法一
function shallowCopy(target) {
  // 判断是否是基础类型或者函数,如果是基础类型或者函数直接返回
  if (!(target instanceof Object) || typeof target === "function") {
    return target;
  }
  let res = Array.isArray(target) ? [] : {};
  // for..in; for 循环;for...of;forEach区别 ?
  for (let key in target) {
    // 为什么要加一层判断?
    // for…in 返回可枚举+原型上可枚举
    // hasOwnProperty 判断是否是对象自有属性
    if (target.hasOwnProperty(key)) {
      res[key] = target[key];
    }
  }
  return res;
}

// 方法二
function shallowCopy(target) {
  if (!(target instanceof Object) || typeof target === "function") {
    return target;
  }
  if (Array.isArray(target)) {
    return [...target];
    // 或者可以使用 target.slice(),或者 target.concat()
  } else {
    return { ...target };
    // 或者可以使用 Object.assign({}, target);
  }
}

for..in; for 循环; for...of; forEach 区别

  • for 循环 适用于数组和固定次数的循环。
  • for…in 循环 遍历对象的可枚举属性。可枚举+原型上可枚举
  • for…of 循环 用于遍历可迭代对象的元素。
  • forEach 方法 用于遍历数组元素,为每个元素执行回调函数。不可打破
  • Object.keys() 返回可枚举的实例属性名
  • Object.getOwnPropertyNames() 可枚举+不可枚举属性

深拷贝

思路

  • 遍历原始对象或数组的属性,如果属性是对象或数组,则对其进行递归复制。对于非对象或数组的基本类型属性,直接复制其值。

代码

// 方法一
function cloneDeep(target, map = new Map()) {
  if (!(target instanceof Object) || typeof target === "function") {
    return target;
  }
  let res = Array.isArray(target) ? [] : {};
  let tmp = map.get(target);
  if (tmp) {
    // 检查map中有无克隆过的对象
    return tmp; // 有则直接返回
  }
  map.set(target, res);
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      res[key] = cloneDeep(target[key], map);
    }
  }
  return res;
}
// 方法二
function deepCopy(target) {
  return JSON.parse(JSON.stringify(target));
}

this 绑定

手写 callapplybind

思路

  • 在要调用的函数对象上创建一个临时方法,以确保在调用过程中不会覆盖现有的属性。
  • 将要调用的函数作为创建的临时方法的属性。
  • 使用临时方法调用函数,并传递指定的参数。
  • 删除临时方法,以避免对调用对象造成影响。
  • 返回调用函数的结果。

代码

// apply 传参使用数组,函数接收参数修改为 function (content, args),其余同call
Function.prototype.myCall = function (context, ...args) {
  // 如果未提供 context,则默认为全局对象
  context = context || window;
  // 为了避免 context 已有的属性被覆盖,生成一个唯一的属性名
  const uniqueID = "00" + Math.random();
  // 将函数作为 context 的一个属性
  context[uniqueID] = this;
  // 在 context 上调用函数
  const result = context[uniqueID](...args);
  // 删除添加的属性
  delete context[uniqueID];

  return result;
};

// bind
Function.prototype.myBind = function (context, ...args) {
  const fn = this; // 保存原始函数的引用

  const foo = function (...innerArgs) {
    // new.target使用详见附录
    // 判断是否有被新建,支持直接在构造函数上使用bind
    const isNew = typeof new.target !== "undefined";
    // 在指定的上下文中调用原始函数,并合并参数
    return fn.apply(isNew ? this : context, args.concat(innerArgs));
  };

  // 继承构造函数原型
  foo.prototype = this.prototype;
  return foo;
};

附录:new.target - JavaScript | MDN (mozilla.org)

防抖节流

防抖(Debouncing)和节流(Throttling)是前端开发中用于优化性能和避免过度触发事件的技术。防抖通过延迟执行函数来确保在短时间内频繁触发的事件只执行一次(连续触发时间内执行最后一次)。节流则是限制函数在一定时间间隔内执行,以确保事件处理函数不会以太快的频率连续执行(连续触发时间内执行第一次)。

防抖

思路

  • 创建一个计时器变量来存储 setTimeout 返回的 ID。
  • 当事件被触发时,清除之前的计时器。
  • 设置一个新的计时器,在指定的延迟时间后执行相应的操作。

代码

function debounce(fn, wait) {
  let timerId = null;

  return (...args) => {
    // 如有定时器,清除定时器
    if (timerId) {
      clearTimeout(timerId);
    }

    // 设置新一轮定时器
    timerId = setTimeout(() => {
      timerId = null;
      console.log(this, args, "debounce");
      // 注意:为什么要绑定this???
      // 确保在防抖函数内部使用 this 时,它指向的是预期的对象
      fn.apply(this, args);
    }, wait);
  };
}

节流

思路

  • 记录时间:需要记录上一次函数执行的时间点。
  • 比较时间:每次触发事件时,比较当前时间与上次执行时间的差值。
  • 执行与延迟:如果差值大于设定的时间间隔,则执行函数,并更新上次执行的时间点;如果小于,则不执行。

代码

function throttle(fn, wait) {
  // 初始化上次执行函数的时间戳
  let lastExecuted = 0;

  return (...args) => {
    // 获取当前时间
    const now = new Date().valueOf();
    // 如果距离上一次执行时间间隔超过等待时间,则执行函数
    if (now - lastExecuted >= wait) {
      lastExecuted = now; // 将上次执行时间设为该次执行时间
      fn.apply(this, args);
    }
  };
}

数组

扁平化

const arr = [1, 2, [3, [4, 5]]];

flat 函数

Array.prototype.flat() 函数用于将数组扁平化,即将嵌套的数组展开为一维数组。该函数接受一个参数,用来指定展开的深度。如果不提供参数,则默认展开一层嵌套。

arr.flat(Infinity) // [ 1, 2, 3, 4, 5 ]

字符串方法

// 转换后得到字符串数组
arr.toString().split(",");// [ '1', '2', '3', '4', '5' ]
arr.join().split(",");// [ '1', '2', '3', '4', '5' ]

// 得到数字数组
arr.toString().split(",").map((item)=>Number(item));
arr.join().split(",").map((item)=>Number(item));

递归拉平

// 方法一
function flatten(arr) {
  let res = [];
  arr.map((item) => {
    // 对是数组的项进行递归拉平
    if (Array.isArray(item)) {
      res = res.concat(flatten(item));
    }
    res.push(item);
  });
  return res;
}

// 方法二
function flatten(arr) {
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}

手写实现 Array.prototype.flat

const arr = [0, 1, [2, [3, [4, 5]]], [1, [4, [5, 7]]]];

// 方法一
Array.prototype.myFlat = function (count) {
  let res = [];
  this.forEach((item) => {
    if (Array.isArray(item) && count > 0) {
      // 使用--count 会出错,公用了一个 count
      // res = res.concat(item.myFlat(--count));
      res = res.concat(item.myFlat(count - 1));
    } else {
      res.push(item);
    }
  });
  return res;
};

// 方法二 
Array.prototype.myFlat = function (count) {
  let res = this;
  while (res.some((item) => Array.isArray(item)) && count) {
    res = [].concat(...res);
    count--;
  }
  return res;
};

去重

const arr = [1, 2, 3, 4, 2, 3, 4, 5];

运用 Set

// 方法一
function uniqueArray(arr) {
  return [...new Set(arr)];
}

// 方法二
function uniqueArray(arr) {
  return Array.from(new Set(arr));
}

运用 filter

function uniqueArray(arr) {
  return arr.filter((value, index, self) => {
    return self.indexOf(value) === index;
  });
}

运用 reduce

function uniqueArray(arr) {
  return arr.reduce((pre, value) => {
    if (!pre.includes(value)) {
      pre.push(value);
    }
    return pre;
  }, []);
}

运用 map

function uniqueArray(arr) {
  let map = {};
  let res = [];
  arr.map((item) => {
    if (!map[item]) {
      map[item] = true;
      res.push(item);
    }
  });
  return res;
}

实现push方法

思路

  • 接受需要添加的元素
  • 依次添加到数组末尾
  • 返回数组添加后的长度

代码

Array.prototype.myPush = function (...args) {
  args.forEach((item) => {
    this[this.length] = item;
  });
  return this.length;
};

实现map方法

思路

  • 接受一个回调函数,回调函数传参:
    • 当前处理元素
    • 处理元素的索引
    • 调用了回调函数的数组本身
  • 依次为数组元素执行回调函数并保存返回结果
  • 返回执行结果数组

代码

Array.prototype.myMap = function (fn) {
  const res = [];
  for (let i = 0, len = this.length; i < len; i++) {
    res.push(fn(this[i], i, this));
  }
  return res;
};

实现filter方法

思路

  • 接受一个回调函数,回调函数传参同map
  • 依次为数组元素执行回调函数,依据回调函数的执行结果进行判断,为真则存入当前元素
  • 返回执行结果数组

代码

Array.prototype.myFilter = function (fn) {
  const res = [];
  for (let i = 0, len = this.length; i < len; i++) {
    const validate = fn(this[i], i, this);
    if (validate) {
      res.push(this[i]);
    }
  }
  return res;
};

实现reduce方法

思路

  • 接受一个回调函数,一个初始化值(若不传则取数组第一个值),回调函数传参
    • 上次执行结果
    • 当前处理元素
    • 处理元素的索引
    • 调用了回调函数的数组本身
  • 依次为数组元素执行回调函数,回调函数返回结果传入下一次调用
  • 返回回调函数遍历整个数组后的结果

代码

Array.prototype.myReduce = function (fn, pre) {
  for (let i = 0, len = this.length; i < len; i++) {
    per = fn(pre ?? this[0], this[i], i, this);
  }
  return pre;
};

函数式编程

函数式编程入门教程 - 阮一峰的网络日志 (ruanyifeng.com)

柯里化

函数柯里化是指将接受多个参数的函数转化为一系列接受单一参数的函数的过程。这种转化允许你部分应用函数,延迟接收其余参数,或者生成更专门化的新函数。

fn.length 返回函数所需参数个数

思路

  • 创建一个函数,该函数接受一个参数并返回一个函数
  • 返回的函数接受下一个参数,并返回一个新函数
  • 最终返回的函数会依次收集所有参数并执行原始函数

代码

function curry(fn, args = []) {
  // 创建一个函数,该函数接受一个参数并返回一个函数。
  return function () {
    // 返回的函数接受下一个参数
    let newArgs = [...args, ...arguments];
    // 当参数数量未到达函数执行需要参数个数,返回一个新的函数
    if (newArgs.length < fn.length) {
      return curry(fn, newArgs);
    }
    // 在接收最后一个参数后执行原始函数
    return fn(...newArgs);
  };
}

反柯里化

函数反柯里化 - 掘金 (juejin.cn)

function unCurrying(fn){
    return function(){
        var args = [].slice.call(arguments);
        var that = args.shift();
        return fn.apply(that, args);
    }
}

函数组合

思路

  • 创建一个函数,给定需要组合的函数
  • 从右到左,执行函数,上次函数的返回作为下次函数的输入

「前端进阶」彻底弄懂函数组合 - 掘金 (juejin.cn)

代码

function compose(...fns) {
  return (arg) =>
    fns.reduceRight((pre, fn) => {
      return fn(pre);
    }, arg);
}

函数管道

同函数组合,只是修改了执行顺序为从左到右

function pipe(...fns) {
  return (arg) =>
    fns.reduce((pre, fn) => {
      return fn(pre);
    }, arg);
}

异步编程

Promise 实现及其方法

实现 Promise

思路

  • 创建一个 Promise 类,接受一个执行器函数作为参数。
    • 执行器接受两个参数:resolve (成功执行的方法) 和 reject (失败执行的方法)。
  • 在 Promise 类中创建参数
    • status 标识运行状态(pendingfulfilledrejected),运行状态一旦改变不可逆转
    • value 保存成功后的值
    • reason 保存失败的原因
    • onFulfilledCallback 保存成功后回调函数
    • onRejectedCallback 保存失败后回调函数
  • 在 Promise 类中创建 resolvereject 两个方法。
    • 提供给执行器根据执行结果来改变 Promise 的状态,并且保存相应的值。
  • 在 Promise 类中创建 then 方法
    • 接受两个参数:onFulfilled(成功的回调函数) 和 onRejected(失败的回调函数) 。
    • 当 Promise 的状态变为 pending 时,在 Promise 中保存onFulfilledonRejected
    • 当 Promise 的状态变为 fulfilled 时,执行 onFulfilled
    • 当 Promise 的状态变为 rejected 时,执行 onRejected
  • 在 Promise 类中创建 catch 方法
    • 它接受一个参数:onRejected
    • 当 Promise 的状态变为 rejected 时,执行 onRejected
  • 在 Promise 类中,确保 thencatch 方法返回一个新的 Promise,以支持 Promise 链式调用。

代码

const PENDING = "pending",
  FULFILLED = "fulfilled",
  REJECTED = "rejected";

class myPromise {
  // 创建一个 Promise 类,接受一个执行器函数作为参数
  constructor(executor) {
    // 需要一个状态标识执行结果
    this.status = PENDING;
    // 保存成功结果
    this.value = null;
    // 保存失败结果
    this.reason = null;
    // 由于一个promise可以有多个then,所以存在同一个数组内
    // 保存成功后回调函数
    this.onFulfilledCallback = [];
    // 保存失败后回调函数
    this.onRejectedCallback = [];
    executor(this.resolve, this.reject);
  }

  resolve = (val) => {
    // 状态一经改变不可修改,只有pending状态下可以修改
    if (this.status === PENDING) {
      // 修改状态为成功
      this.status = FULFILLED;
      // 保存成功的结果
      this.value = val;
      // 依次执行回调函数
      this.onFulfilledCallback.forEach((callback) => {
        callback(val);
      });
    }
  };

  reject = (res) => {
    if (this.status === PENDING) {
      // 修改状态为失败
      this.status = REJECTED;
      // 保存失败的结果
      this.reason = res;
      // 依次执行回调函数
      this.onRejectedCallback.forEach((callback) => {
        callback(val);
      });
    }
  };

  then = (onFulfilled, onRejected) => {
    switch (this.status) {
      // 当 Promise 的状态变为 `pending` 时,在 Promise 中保存`onFulfilled` 和 `onRejected`
      case PENDING: {
        this.onFulfilledCallback.push(onFulfilled);
        this.onRejectedCallback.push(onRejected);
        break;
      }
      // 当 Promise 的状态变为 `fulfilled` 时,执行 `onFulfilled`;
      case FULFILLED: {
        onFulfilled?.(this.value);
        break;
      }
      // 当 Promise 的状态变为 `rejected` 时,执行 `onRejected`;
      case REJECTED: {
        onRejected?.(this.reason);
        break;
      }
    }
  };
}

实现 Promise.all

特点

  1. 接受一个 Promise 可迭代对象 (例如 Array 或 String) 作为输入,并返回一个 Promise
// 测试用例
let promise1 = new Promise((resolve) => setTimeout(resolve, 100, "one"));
let promise2 = new Promise((resolve) => setTimeout(resolve, 200, "two"));
let promise3 = new Promise((resolve) => setTimeout(resolve, 50, "three"));

// 可接受一个promise数组
Promise.all([promise1, promise2, promise3]).then((results) => {
  console.log(results); // 输出:['one', 'two', 'three']
});
// 可接受一个字符串
Promise.all("123456").then((results) => {
  console.log(results); // 输出:['1', '2', '3', '4', '5', '6']
});

  1. 当所有输入的 Promise 都被兑现时,返回的 Promise 也将被兑现(即使传入的是一个空的可迭代对象),并返回一个包含所有兑现值的数组
Promise.all([]).then((value) => {
  console.log(value); // []
});
  1. 如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因。
// 测试用例
let promise1 = new Promise((resolve) => setTimeout(resolve, 100, "one"));
let promise2 = new Promise((resolve) => setTimeout(resolve, 200, "two"));
let promise3 = new Promise((resolve) => setTimeout(resolve, 50, "three"));
let rejectedPromise1 = new Promise((resolve, reject) =>
  setTimeout(reject, 150, "Rejected1!")
);
let rejectedPromise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 2000, "Rejected2!")
);

Promise.all([promise1, promise2, rejectedPromise1, promise3, rejectedPromise2])
  .then((results) => {
    console.log(results); // 该语句不执行
  })
  .catch((reason) => {
    console.error(reason); // 输出:Rejected1!
  });
  1. 如果传入非promise对象,则将该对象作为resolve成功值
// 可接受一个混合数组
Promise.all([
  promise1,
  promise2,
  promise3,
  "123456",
  1111,
  true,
  () => true,
  {},
  [],
]).then((value) => {
  console.log(value); 
  // ['one','two','three','123456',1111,true,[Function (anonymous)],{},[]]
});

思路

  • 创建一个新的 Promise 对象,并返回它
  • 在 Promise.all 函数中,接受一个包含多个 Promise 的可迭代对象(通常是数组)作为输入参数。
  • 遍历传入的可迭代对象,并对每个元素进行处理。
  • 在内部创建一个空数组用于保存每个 Promise 的解决值。
  • 对传入的每个 Promise 调用 .then 方法,将其解决值存入空数组。
  • 一旦所有 Promise 都解决了,将解决值数组作为参数来解决新创建的 Promise。
  • 如果任何一个 Promise 被拒绝(rejected),则立即将新创建的 Promise 拒绝,并返回拒绝的原因。

代码

// 接受一个 Promise 可迭代对象(例如 Array 或 String)作为输入,并返回一个 Promise
function myPromiseAll(arg) {
  return new Promise((resolve, reject) => {
    // 如果参数不是可迭代对象
    if (typeof arg[Symbol.iterator] !== "function") {
      reject("type error");
    }
    // 如果传参是字符串,返回字符串分割后结果
    if (typeof arg === "string") {
      resolve(arg.split(""));
    }
    // 保存每个 Promise 的解决值
    const res = [];
    // 如果传入空数组,直接返回
    if (arg.length === 0) {
      resolve(res);
    }

    // 成功时执行的函数,保存结果到数组中,并判断是否跳出
    const onResolve = (val, index) => {
      res[index] = val;
      count++;
      // 当所有输入的 Promise 都被兑现时,返回一个包含所有兑现值的数组。
      if (count === arg.length) {
        resolve(res);
      }
    };

    // 标识成功个数
    let count = 0;
    // 遍历传入的 Promise 数组,并对每个 Promise 进行处理
    arg.forEach((item, index) => {
      // 用 Promise.resolve 对每个元素进行包裹,兼容非 Promise 类型
      Promise.resolve(item)
        .then((val) => {
          onResolve(val, index);
        })
        .catch((reason) => {
          // 任何一个 Promise 被拒绝(rejected),返回拒绝的原因
          reject(reason);
        });
    });
  });
}

实现 Promise.race

思路

  • 大致同 Promise.all,返回最早敲定的结果

代码

function promiseRace(arg) {
  return new Promise((resolve, reject) => {
    // 如果参数不是可迭代对象
    if (typeof arg[Symbol.iterator] !== "function") {
      reject("type error");
    }
    // 如果传参是字符串,返回字符串第一位
    if (typeof arg === "string") {
      resolve(arg[0]);
    }

    arg.forEach((item, index) => {
      // 用 Promise.resolve 对每个元素进行包裹,兼容非 Promise 类型
      Promise.resolve(item)
        .then((val) => {
          resolve(val);
        })
        .catch((reason) => {
          reject(reason);
        });
    });
  });
}

实现 Promise.retry

思路

  • 创建一个能够执行指定操作的 Promise,并设置重试次数和重试间隔。
  • 如果操作成功,则返回成功的 Promise;
  • 如果失败,根据重试次数进行重试,并在重试间隔之后再次尝试执行操作。
Promise.myRetry = function (fn, maxAttempts, interval) {
  return new Promise((resolve, reject) => {
    function attemptOperation(attempt) {
      console.log("attempt:", attempt);
      fn().then(
        (res) => {
          return resolve(res);
        },
        (err) => {
          if (attempt < maxAttempts) {
            setTimeout(() => attemptOperation(attempt + 1), interval);
          } else {
            reject(err);
          }
        }
      );
    }
    attemptOperation(1);
  });
};

实现控制并发

思路

  • 按照最大执行数初始化执行promise队列
  • 获取最快完成任务进行替换

代码

function limitConcurrence(promiseList, limit) {
  // 创建保存结果容器
  const res = new Array(promiseList.length);
  //  复制promise队列
  const sequence = promiseList.concat(promiseList);
  // 按照限制个数初始化运行容器
  let execute = sequence
    .splice(0, Math.min(limit, promiseList.length)) // 避免出现显示个数大于数组长度情况
    .map((promise, index) => {
      // 运行promise函数并保存结果
      return promise().then((value) => {
        // 保存执行结果
        res[index] = value;
        // 返回下标是为了知道数组中是哪一项最先完成
        return index;
      });
    });

  // 遍历剩余未执行的promise队列,并处理最快完成项进行替换
  return sequence
    .reduce((pCollect, promise, promiseIndex) => {
      return pCollect.then(() =>
        // 用race获取最快完成promise下标,并进行替换处理
        Promise.race(execute).then((fastIndex) => {
          execute[fastIndex] = promise().then((value) => {
            // 保存值到对应数组
            res[promiseIndex + limit] = value;
            return index; // 继续返回完成最快下标
          });
        })
      );
    }, Promise.resolve())
    .then(() => Promise.all(execute).then(() => res));
}

实现控制动态并发

type taskCallbacks = () => Promise<any>;
type ITask = {
  id: number; // 任务 ID 标识
  taskCallbacks: taskCallbacks; // 运行的回调函数
};

/**动态并发池 */
export class PromisePoolDynamic<T> {
  /**最大并发数量 **/
  private limit: number;
  /**当前正在跑的数量 **/
  private runningCount: number;
  /**等待队列 **/
  private queue: ITask[];

  /** 动态并发池 - 构造函数
  * @param maxConcurrency 最大并发数量
  **/
  constructor(maxConcurrency: number) {
    this.limit = maxConcurrency;
    this.runningCount = 0;
    this.queue = [];
  }
  /** 判断是否在等待队列,避免重复添加
  * @param id 任务ID标识
  **/
  taskOnWait(id) {
    return this.queue.find((item) => item.id === id);
  }

  /** 添加任务 **/
  addTask(args: ITask) {
    const { id, taskCallbacks } = args;
    if (this.taskOnWait(id)) {
      return;
    }
    if (this.runningCount < this.limit) {
      // 并发数量没满则运行
      this.runTask({ id, taskCallbacks });
    } else {
      // 并发数量满则加入等待队列
      this.queue.push({ id, taskCallbacks });
    }
  }
  /** 运行任务 **/
  private runTask(args: ITask) {
    const { id, taskCallbacks } = args;
    this.runningCount++; // 当前并发数++
    taskCallbacks()
      .then((result) => {
        this.runningCount--;
        this.checkQueue();
      })
      .catch((error) => {
        this.queue = this.queue.filter((item) => item.id !== id);
        this.runningCount--;
        this.checkQueue();
      });
  }

  /** 运行完成后,检查队列,看看是否有在等待的,有就取出第一个来运行 **/
  private checkQueue() {
    if (this.queue.length > 0 && this.runningCount < this.limit) {
      const nextTask = this.queue.shift()!;
      this.runTask(nextTask);
    }
  }
}

// 测试
const promisePoolDynamic = new PromisePoolDynamic(2);
promisePoolDynamic.addTask({id,taskCallbacks});

排序

冒泡排序

通过多次遍历要排序的数列,每次遍历时依次比较相邻的两个数,如果它们的顺序错误就交换它们的位置。通过多次遍历,将最大(或最小)的数移动到数列的末尾。

时间复杂度O(n)~O(n^2)空间复杂度O(1)稳定

function bubbleSort(arr) {
  const len = arr.length;
  var flag = false;
  for (let i = 0; i < len - 1; i++) {
    // 初始化标记
    flag = false;
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j + 1] < arr[j]) {
        // 利用解构赋值交换数组元素
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        flag = true;
      }
    }
    // 如果没有交换,则表示队列有序
    if (!flag) {
      break;
    }
  }
  return arr;
}

选择排序

每次从未排序的部分选择最小(或最大)的元素,放到已排序部分的末尾,直到所有元素都排序完成。

时间复杂度O(n^2)空间复杂度O(1)不稳定

function selectionSort(arr) {
  var len = arr.length;
  for (var i = 0; i < len - 1; i++) {
    var minIndex = i;
    for (var j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (minIndex !== i) {
      var temp = arr[i];
      arr[i] = arr[minIndex];
      arr[minIndex] = temp;
    }
  }
  return arr;
}

插入排序

将未排序的部分依次插入到已排序部分的合适位置,直到所有元素都排序完成。

时间复杂度O(n)~O(n^2)空间复杂度O(1)稳定

function insertionSort(arr) {
  var len = arr.length;
  for (var i = 1; i < len; i++) {
    var key = arr[i],
      j = i - 1;

    while (j >= 0 && arr[j] > key) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = key;
  }
  return arr;
}

希尔排序

将整个待排序的序列分割成若干个子序列分别进行直接插入排序,待整个序列中的元素基本有序时,再对整个序列进行一次直接插入排序。 时间复杂度O(n log^2 n)~O(n^2)空间复杂度O(1)不稳定

function shellSort(arr) {
  var len = arr.length;
  for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < len; i++) {
      let j = i;
      let temp = arr[i];
      while (j - gap >= 0 && temp < arr[j - gap]) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = temp;
    }
  }
  return arr;
}

归并排序

  1. 分解:将数组递归地分成较小的数组,直到每个小数组只有一个元素。
  2. 合并:将两个有序的小数组合并成一个有序的大数组,重复此过程直到整个数组有序。

时间复杂度O(nlogn)空间复杂度O(n)稳定

function mergeSort(arr) {
  const len = arr.length;
  if (len <= 1) {
    return arr;
  }
  const middle = Math.floor(len / 2);
  const left = arr.slice(0, middle);
  const right = arr.slice(middle);

  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  let leftIndex = 0,
    rightIndex = 0;
  const arr = [],
    leftLen = left.length,
    rightLen = right.length;
  while (leftIndex < leftLen && rightIndex < rightLen) {
    if (left[leftIndex] < right[rightIndex]) {
      arr.push(left[leftIndex++]);
    } else {
      arr.push(right[rightIndex++]);
    }
  }
  return arr.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

快速排序

  1. 选择一个基准元素(通常是数组中的第一个元素)。
  2. 将数组中小于基准元素的值移到基准元素的左边,大于基准元素的值移到基准元素的右边,基准元素则位于最终排序的位置。
  3. 对基准元素左右两边的子数组分别重复步骤 1 和 2,直到整个数组有序。 时间复杂度O(nlogn)~O(n^2)空间复杂度O(n)不稳定
function quickSort(arr) {
  const len = arr.length;
  if (len < 1) {
    return arr;
  }
  const left = [],
    right = [],
    key = arr[0];

  for (let i = 1; i < len; i++) {
    if (arr[i] < key) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), key, ...quickSort(right)];
}

堆排序

  1. 构建最大堆(对于升序排序)或最小堆(对于降序排序)。
  2. 将堆顶元素(最大元素或最小元素)与堆的最后一个元素交换。
  3. 修复剩余元素的堆性质。
  4. 重复步骤 2 和 3,直到整个数组有序。

时间复杂度O(nlogn)空间复杂度O(1)不稳定

function heapSort(arr) {
  buildMaxHeap(arr);
  for (let i = arr.length - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    maxHeapify(arr, 0, i);
  }
  return arr;
}

function buildMaxHeap(arr) {
  const length = arr.length;
  for (let i = Math.floor(length / 2); i >= 0; i--) {
    maxHeapify(arr, i, length);
  }
}

function maxHeapify(arr, i, length) {
  let left = i * 2 + 1,
    right = i * 2 + 2,
    largest = i;
  if (left < length && arr[left] > arr[largest]) {
    largest = left;
  }
  if (right < length && arr[right] > arr[largest]) {
    largest = right;
  }
  if (largest !== i) {
    [arr[largest], arr[i]] = [arr[i], arr[largest]];
    maxHeapify(arr, largest, length);
  }
}

计数排序

  1. 找出待排序的数组中最大和最小的元素。
  2. 统计数组中每个值为 i 的元素出现的次数,存入数组 C 的第 i 项,其中 C 的长度为最大元素值加一。
  3. 对所有的计数累加(从 C 中的第一个元素开始,每一项和前一项相加)。
  4. 反向填充目标数组:将每个元素 i 放在新数组的第 C[i] 项,每放一个元素就将 C[i] 减去 1。

时间复杂度O(n + k)空间复杂度O(k)稳定

function countingSort(arr) {
  const max = Math.max(...arr);
  const min = Math.min(...arr);
  const count = new Array(max - min + 1).fill(0);
  const output = [];

  for (let i = 0; i < arr.length; i++) {
    count[arr[i] - min]++;
  }

  for (let i = 0; i < count.length; i++) {
    while (count[i] > 0) {
      output.push(i + min);
      count[i]--;
    }
  }

  return output;
}

桶排序

  1. 将元素分散到不同的桶中。
  2. 对每个桶中的元素进行单独排序。
  3. 将所有桶中的元素按顺序合并。

时间复杂度O(n + k) ~ O(n^2)空间复杂度O(n + k)稳定

function bucketSort(arr, bucketSize) {
  if (arr.length === 0) {
    return arr;
  }

  var i;
  var minValue = arr[0];
  var maxValue = arr[0];
  for (i = 1; i < arr.length; i++) {
    if (arr[i] < minValue) {
      minValue = arr[i]; // 输入数据的最小值
    } else if (arr[i] > maxValue) {
      maxValue = arr[i]; // 输入数据的最大值
    }
  }

  //桶的初始化
  var DEFAULT_BUCKET_SIZE = 5; // 设置桶的默认数量为5
  bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
  var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  var buckets = new Array(bucketCount);
  for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
  }

  //利用映射函数将数据分配到各个桶中
  for (i = 0; i < arr.length; i++) {
    buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
  }

  arr.length = 0;
  for (i = 0; i < buckets.length; i++) {
    insertionSort(buckets[i]); // 对每个桶进行排序,这里使用了插入排序
    for (var j = 0; j < buckets[i].length; j++) {
      arr.push(buckets[i][j]);
    }
  }

  return arr;
}

基数排序

将待排序的整数按照位数切割成数字,然后按每个位数分别进行排序。通常是从低位到高位进行排序,可以用桶排序作为每一位的排序方法。缺点:不适用负数

时间复杂度O(k * n)空间复杂度O(n + k)稳定

// 获取数字的指定位数上的数字
function getDigit(num, i) {
  return Math.floor(Math.abs(num) / Math.pow(10, i)) % 10;
}

// 获取数字的位数
function digitCount(num) {
  if (num === 0) {
    return 1;
  }
  return Math.floor(Math.log10(Math.abs(num))) + 1;
}

// 获取数组中最大值的位数
function mostDigits(arr) {
  let maxDigits = 0;
  arr.forEach((value) => {
    maxDigits = Math.max(digitCount(value), maxDigits);
  });
  return maxDigits;
}

// 基数排序
function radixSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const maxDigitCount = mostDigits(arr);
  for (let k = 0; k < maxDigitCount; k++) {
    let digitBuckets = Array.from({ length: 10 }, () => []);
    for (let i = 0; i < arr.length; i++) {
      let digit = getDigit(arr[i], k);
      digitBuckets[digit].push(arr[i]);
    }
    arr = [].concat(...digitBuckets);
  }
  return arr;
}

正则

千分位分割

// 指定小数点位数,并添加千分位分隔符
function fixedNumber(number, precision = 2) {
  if (precision > 0) {
    return Number(number)
      .toFixed(precision)
      .replace(/(\d)(?=(\d{3})+\.)/g, "$&,");
  }
  return Number(number)
    .toFixed(0)
    .replace(/(\d)(?=(\d{3})+$)/g, "$&,");
}

下划线 驼峰转换

const str = "abc_dfr_sdf";
// 下划线转驼峰
str.replace(/\_(\w)/g, (_, s) => s.toUpperCase());
// 驼峰转下划线
str.replace(/([A-Z])/g, "_$1");4
// 转小写字母下划线
str.replace(/([A-Z])/g, (_, s) => "_" + s.toLowerCase());

搜索高亮

function highlight(text, keyword, className) {
  if (keyword) {
    const highlightStr = ($1) => `<span class=${className}>${$1}</span>`;
    const reg = new RegExp(keyword, "gi");
    return text?.replace(reg, highlightStr);
  }
  return text;
}

邮箱校验

/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

url校验

/((http|https):\/\/)?(www\.)?([a-zA-Z0-9-]+\.)([a-zA-Z]{2,})([a-zA-Z0-9._~:/?#[\]@!$&'()*+,;=-]*)/g