JS手写从入门到出门

52 阅读18分钟

Object.create()

用于创建一个新对象,并指定该对象的原型

用法

Object.create(proto, [propertiesObject])
  • proto:新对象的原型(__proto__指向谁),可以是对象或者null
  • propertiesObject:用于给对象定义新属性
const person = {
  sayHi() {
    console.log(`Hi ${this.name}`);
  },
};
const dy = Object.create(person);
dy.name = 'dy';
dy.sayHi(); // Hi dy

const dy2 = Object.create(person, {
  name: {
    value: 'DY',
    enumerable: true,
    writable: true,
    configurable: true,
  },
});
dy2.sayHi(); // Hi DY

场景

  • 原型继承

    • function createDog(name) {
        const animal = {
          eat() {
            console.log('eating');
          },
        };
        const dog = Object.create(animal);
        dog.name = name;
        dog.bark = function () {
          console.log('barking');
        };
        return dog;
      }
      const d = createDog('dog01');
      d.eat();
      d.bark();
      
  • 创建一个无原型对象(纯净字典)

    • const dict = Object.create(null);
      dict['key'] = 'value';
      console.log(dict.toString); // undefined
      
    • 纯净字典:没有原型的对象,不像普通对象那样继承Object.prototype上的属性和方法

      // 普通对象
      obj --> Object.prototype --> null
      
      //纯净字典
      dict --> null
      
  • 克隆对象(浅拷贝并保留原型)

    • function clone(obj) {
        return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
      }
      

实现

  • 用临时构造函数将prototype设置为传入的proto,然后返回构造函数的实例
function createObject(obj, propertiesObject) {
  function F() {}
  F.prototype = obj;
  const newObj = new F();

  if (propertiesObject && typeof propertiesObject === 'object') {
    Object.defineProperties(newObj, propertiesObject);
  }

  return newObj;
}

与new的区别

new依赖构造函数的prototype会执行构造函数
Object.create()直接指定原型对象不执行构造函数

new

调用构造函数并创建一个带有指定原型的新对象

用法

new Constructor(arg1, arg2, ...)
  • 创建一个空对象
  • 将空对象的__proto__指向构造函数的prototype
  • 执行构造函数,把this绑定到新对象
  • 如果构造函数返回的是对象,则返回该对象,否则返回1创建的空对象
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function () {
  console.log(`Hi ${this.name}`);
};
const dy = new Person('dy', 18);
dy.sayHi();

场景

  • 创建实例对象

    • 用构造函数批量创建带有相同方法和不同数据的对象
    • function Person(name, age) {
        this.name = name;
        this.age = age;
      }
      Person.prototype.sayHi = function () {
        console.log(`Hi ${this.name}`);
      };
      const dy1 = new Person('dy1', 18);
      const dy2 = new Person('dy2', 18);
      const dy3 = new Person('dy3', 18);
      dy1.sayHi();
      dy2.sayHi();
      dy3.sayHi();
      
  • 继承实现

    • function Animal(name) {
        this.name = name;
      }
      Animal.prototype.eat = function () {
        console.log(`${this.name} is eating`);
      };
      function Dog(name) {
        Animal.call(this, name);
      }
      Dog.prototype = Object.create(Animal.prototype);
      Dog.prototype.constructor = Dog;
      const dog = new Dog('dog');
      dog.eat();
      
  • 自定义类的初始化

    • class Person {
        constructor(name) {
          this.name = name;
        }
      }
      const dy = new Person('dy');
      

实现

function myNew(Constructor, ...args) {
  const obj = {};

  Object.setPrototypeOf(obj, Constructor.prototype);

  const result = Constructor.apply(obj, args);

  return (result && (typeof result === 'object' || typeof result === 'function')) ? result : obj;
}
function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const p1 = myNew(Person, "dy");
p1.sayHi(); // Hi, I'm dy
console.log(p1 instanceof Person); // true

instanceof

用法

object instanceof Constructor
  • object:要检测的实例对象
  • Constructor:构造函数(类)
  • 检测object原型链上是否存在Contructor.prototype,不能用于基本类型的检查
function Person() {}
const p = new Person();

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true

场景

  • 类型判断

    • console.log([] instanceof Array);      // true
      console.log({} instanceof Object);     // true
      console.log(function(){} instanceof Function); // true
      
  • 继承关系检查

    • class Animal {}
      class Dog extends Animal {}
      const dog = new Dog();
      console.log(dog instanceof Dog);    // true
      console.log(dog instanceof Animal);    // true
      

实现

  • instanceof会沿着object.proto([[prototype]])链一直向上查找,看是否有和Constructor.prototype严格相等的引用
function myInstanceof(obj, Constructor) {
  // 如果obj不是对象或者为null 直接返回false
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
    return false;
  }

  let proto = Object.getPrototypeOf(obj);
  const prototype = Constructor.prototype;

  while (proto) {
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }

  return false;
}

封装类型判断方法

  • typeof:对基础类型有效,对null/array/object不够精确
  • instanceof:只能判断引用类型数据,且受原型链影响
  • Object.prototype.toString.call:能够很好区分
function type(value) {
  if (value === null) return 'null';
  if (typeof value !== 'object') return typeof value;
  return Object.prototype.toString().call(value).slice(8,-1).toLowerCase();
}

call

用法

调用函数,并指定函数执行时的this指向,同时按顺序传递参数

func.call(thisArg, arg1, arg2, ...)

场景

  • 改变this指向

    • function sayHi(greeting) {
        console.log(greeting + ", " + this.name);
      }
      
      const person = { name: "dy" };
      
      greet.call(person, "Hello"); 
      // Hello, dy
      
  • 借用方法

    • const person1 = {
        name: "dy",
        sayHi() {
          console.log("Hi, I'm " + this.name);
        }
      };
      
      const person2 = { name: "DY" };
      
      person1.sayHi.call(person2); 
      // Hi, I'm DY
      
  • 伪数组转数组

    • function demo() {
        // arguments 是伪数组
        const arr = Array.prototype.slice.call(arguments);
        console.log(arr);
      }
      
      demo(1, 2, 3); // [1, 2, 3]
      

实现

Function.prototype.myCall = function (context, ...args) {
  context = context || globalThis;

  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;
  const result = context[fnSymbol](...args);
  delete context[fnSymbol];
  return result;
}

apply

用法

func.apply(thisArg, [argsArray])
  • argsArray:传入函数的参数,必须是数组或者类数组对象
const person = {
  name: "dy",
};

function greet(age, city) {
  console.log(`Hello, I'm ${this.name}, ${age} years old, from ${city}`);
}

greet.apply(person, [18, "BJ"]); 
// Hello, I'm dy, 18 years old, from BJ

实现

Function.prototype.myApply = function (context, args) {
  context = context || globalThis;
  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;

  const result = args ? context[fnSymbol](...args) : context[fnSymbol]();

  delete context[fnSymbol];
  return result;
}

bind

用法

返回一个新函数,函数的this被绑定到传入的目标对象上

function greet(greeting) {
  console.log(greeting + ', ' + this.name);
}

const person = { name: 'dy' };

const bound = greet.bind(person, 'Hello');
bound(); // Hello, dy

实现

Function.prototype.myBind = function (context, ...args) {
  const self = this;
  return function (...newArgs) {
    if (this instanceof self) {
      return new self(...args, ...newArgs);
    }
    return self.apply(context, args.concat(newArgs));
  }
}

Object.setPrototypeOf

用法

用于动态设置对象的原型([[prototype]] / proto

Object.setPrototypeOf(obj, prototype);
  • obj:要修改原型的目标对象
  • prototype:新的原型(可以是null)
const animal = { type: "animal" };
const dog = { name: "Buddy" };

Object.setPrototypeOf(dog, animal);

console.log(dog.type); // animal
console.log(Object.getPrototypeOf(dog) === animal); // true

场景

  • 手动更改对象的继承关系

    • const a = {sayHi () {console.log('hi')}};
      const b = {name: "b"};
      Object.setPrototypeOf(b, a);
      b.sayHi(); // hi
      

实现

function mySetPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

Object.freeze

用法

冻结对象,使对象不能被扩展、删除或修改属性的值

只能冻结对象的第一层属性

如果属性值是对象,对象内部是可变的

const obj = {name: 'dy', age: 18};
Object.freeze(obj);

obj.name = 'DY';
delete obj.age;
obj.hobby = 'eat';

console.log(obj);    // {name: 'dy', age: 18}

const user = {
  name: "dy",
  info: { city: "ShanXi" }
};
Object.freeze(user);

user.info.city = "Beijing"; // 
console.log(user.info.city); // "Beijing"

场景

  • 常量对象

    • const CONFIG = Object.freeze({
        API_URL: "https://api.example.com",
        TIMEOUT: 5000
      });
      
  • 防止对象被修改

    • function createUser(name) {
        return Object.freeze({ name });
      }
      const u = createUser("dy");
      u.name = "DY"; //'dy'
      

实现

  • 浅冻结(同Object.freeze)

    • function shallowFreeze(obj) {
        Object.keys(obj).forEach(key => {
          Object.defineProperty(obj, key, {
            writable: false,
            configurable: false
          })
        })
      
        Object.preventExtensions(obj);
        return obj;
      }
      
  • 深冻结(对象的对象也可冻结)

    • function deepFreeze(obj) {
        Object.freeze(obj);
      
        Object.keys(obj).forEach(key => {
          if (typeof obj[key] === 'object' && obj[key] !== null) {
            deepFreeze(obj[key]);
          }
        });
      
        return obj;
      }
      

Promise.all

用法

用于将多个Promise组合成一个Promise,当所有Promise都成功时才会resolve,有一个失败就会reject

const p1 = Promise.resolve(1);
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 1000);
});
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3])
  .then(values => {
    console.log(values); // [1, 2, 3]
  })
  .catch(err => {
    console.log(err);
  });
  • 输入:一个可迭代对象(通常是数组),里面的值可以是Promise值或者普通值
  • 输出:一个新的Promise
  • 成功:所有Promise都返回fulfilled,按顺序返回结果数组
  • 失败:第一个rejected的原因会作为结果返回

注意

  • 顺序与完成时间无关

    • 结果是按照数组传入顺序而非完成时间顺序
    • Promise.all([
        Promise.resolve('a'),
        new Promise(res => setTimeout(() => res('b'), 100)),
        Promise.resolve('c')
      ]).then(console.log); // ['a', 'b', 'c']
      
  • 遇到第一个reject

    • Promise.all([
        Promise.resolve(1),
        Promise.reject('error'),
        Promise.resolve(3)
      ])
      .catch(console.error); // "error"
      
  • 普通值转换为Promise

    • Promise.all([1, 2, Promise.resolve(3)])
        .then(console.log); // [1, 2, 3]
      
  • 空数组返回立即成功的Promise

    • Promise.all([]).then(console.log); // []
      

场景

  • 同时请求多个接口,合并结果

    • 页面需要多个接口数据才能渲染
    • Promise.all([
        fetch('/api/user').then(res => console.log(res)),
        fetch('/api/orders').then(res => console.log(res)),
        fetch('/api/notifications').then(res => console.log(res))
      ]).then(([user, orders, notifications]) => {
        console.log('全部数据已获取', { user, orders, notifications });
      });
      
  • 批量预加载资源

    • const preloadImage = src => new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = resolve;
        img.onerror = reject;
        img.src = src;
      });
      
      Promise.all([
        preloadImage('/img/1.png'),
        preloadImage('/img/2.png'),
        preloadImage('/img/3.png')
      ]).then(() => console.log('所有图片已预加载'));
      
  • 多条校验一次完成

    • Promise.all([
        checkUsername('dy'),
        checkEmail('dy.com')
      ]).then(([usernameOk, emailOk]) => {
        if (usernameOk && emailOk) console.log('注册信息可用');
      });
      

实现

  • 创建一个新的Promise

  • 遍历传入的可迭代对象

    • 用Promise.resolve统一转换成Promise
    • Promise成功,存储到结果数组对应的位置
    • Promise失败立即reject
  • 当计数器等于传入Promise的数量,resolve结果数组

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('need array'));
    }

    const results = [];
    let count = 0;

    if (promises.length === 0) {
      return resolve([]);
    }

    promises.forEach((p, index) => {
      //遍历传入的所有 Promise(或类 Promise 对象)
      Promise.resolve(p)
        .then(value => {
          //使用 Promise.resolve() 将 p 转换为 Promise(处理非 Promise 值)
          results[index] = value; // 将结果按原始顺序存储在 results 数组中对应位置
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
}

Promise.race

用法

Promise.race(iterable)
  • 第一个有结果(resolve,reject)的Promise决定Promise.race的最终状态和返回值
  • 不关心其他Promise是否还在执行
  • 结果可能是成功或者失败,取决于第一个完成的Promise

场景

  • 网络请求超时控制

    • function fetchWithTimeout(url, ms) {
        const timeout = new Promise((_, reject) => {
          setTimeout(() => reject(new Error('timeout'), ms));
        });
      
        return Promise.race([fetch(url), timeout]);
      }
      
      fetchWithTimeout('/api/user', 3000)
        .then(res => console.log(res))
        .catch(err => {
          console.error(err);
        });
      
  • 取最快结果(CDN/镜像抢先)

    • const cdn1 = fetch('https://cdn1.example.com/file');
      const cdn2 = fetch('https://cdn2.example.com/file');
      
      Promise.race([cdn1, cdn2])
        .then(res => console.log('最快的CDN返回:', res))
        .catch(console.error);
      
  • 用户交互等待

    • 兼容用户主动与超时自动
    • function waitForUserAction(ms) {
        const clickPromise = new Promise(resolve =>
          document.addEventListener('click', () => resolve('clicked'), { once: true })
        );
        const timeoutPromise = new Promise(resolve =>
          setTimeout(() => resolve('timeout'), ms)
        );
        return Promise.race([clickPromise, timeoutPromise]);
      }
      
      waitForUserAction(5000).then(console.log);
      

实现

  • 接收一个可迭代对象作为参数
  • 遍历其中每个元素,用Promise.resolve转成Promise
  • 对每个Promise注册then和catch
  • 谁先调用resolve/reject,直接把结果传给Promise.race的resolve/reject并结束流程
Promise.myRace = function (promises) {
  return new Promise((resolve, reject) => {
    for (const p of promises) {
      Promise.resolve(p)
        .then(resolve)
        .catch(reject);
    }
  })
}

Promise.myRace([
  new Promise(res => setTimeout(() => res('A'), 1000)),
  new Promise(res => setTimeout(() => res('B'), 500))
]).then(console.log); // 'B' 

Promise.settled

用法

用来并行执行多个Promise,不论成功或失败,都会在全部完成后返回一个“汇总数组”

Promise.allSettled(iterable)
  • 返回一个新的Promise,状态总是fulfilled,值是一个数组,每个元素对应输入的Promise的最终状态
const p1 = Promise.resolve(100);
const p2 = Promise.reject("出错了");
const p3 = new Promise(resolve => setTimeout(() => resolve("完成"), 500));

Promise.allSettled([p1, p2, p3]).then(results => {
  console.log(results);
});


/**
[
  { status: "fulfilled", value: 100 },
  { status: "rejected", reason: "出错了" },
  { status: "fulfilled", value: "完成" }
]
*/

场景

  • 批量接口请求

    • 页面需要请求多个接口,即使某个失败,也要正常展示其余内容
    • 保证流程完整性
    • const apis = [fetch("/user"), fetch("/orders"), fetch("/messages")];
      Promise.allSettled(apis).then(results => {
        results.forEach(r => {
          if (r.status === "fulfilled") {
            console.log("成功:", r.value);
          } else {
            console.error("失败:", r.reason);
          }
        });
      });
      

实现

  • 把每个都包装成“永远成功”
Promise.myAllSettled = function myAllSettled(promises) {
  return Promise.all(
    promises.map(p => {
      return Promise.resolve(p).then(
        value => ({ status: 'fulfilled', value }),
        reason => ({ status: 'rejected', reason })
      );
      //.catch(reason => ({ status: 'rejected', reason }));
    })
  );
};
Promise.myAllSettled([p1, p2, p3]).then(results => {
  console.log(results);
});

Promise.any

用法

只要有一个Promise成功,就返回。如果所有的Promise都报错才会报错

Promise.any(iterable);
  • 一个可迭代对象

  • 返回:一个新的Promise

    • 第一个成功的结果--->fulfilled,返回该结果。只要有一个成功,立即返回
    • 如果全部失败--->rejected,返回一个AggregateError,里面包含所有错误的原因
const p1 = Promise.reject("失败1");
const p2 = new Promise(res => setTimeout(() => res("成功2"), 100));
const p3 = new Promise(res => setTimeout(() => res("成功3"), 200));

Promise.any([p1, p2, p3]).then(result => {
  console.log(result); // "成功2"
});
const p1 = Promise.reject("错误1");
const p2 = Promise.reject("错误2");

Promise.any([p1, p2])
  .then(result => console.log("成功:", result))
  .catch(err => {
    console.log("全部失败:", err);
    console.log(err.errors); // ["错误1", "错误2"]
  });

实现

Promise.myAny = function (promises) {
  return new Promise((resolve, reject) => {
    let rejections = [];
    let pending = promises.length;
    if (pending === 0) {
      return reject(new AggregateError('No promises resolved', []));
    }
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        value => resolve(value),
        reson => {
          rejections[i] = reson;
          pending--;
          if (pending === 0) {
            reject(
              new AggregateError(rejections, 'All promises were rejected')
            );
          }
        }
      );
    });
  });
};

柯里化函数

用法

把一个接受多个参数的函数,转化为一系列接受一个参数的函数

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

实现

  • 普通版

    • 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));
            };
          }
        };
      }
      

防抖(Debounce)

在事件被触发后,延迟一定时间执行回调,如果在延迟期间再次触发,则重新计时

  • 减少频繁触发,尤其是高频事件(scroll,resize,input)
  • 保证函数只在最后一次触发后执行

实现

  • 输入搜索防抖

    • function search(query) {
        console.log(query);
      }
      
      const debounce = (fn, delay) => {
        let timer = null;
        return function (...args) {
          clearTimeout(timer);
          timer = setTimeout(() => fn.apply(this, args), delay);
        };
      };
      
      const debouncedSearch = debounce(search, 500);
      
      document.querySelector('#input').addEventListener('input', event => {
        debouncedSearch(event.target.value);
      });
      
  • 第一次立即执行

    • const debounce = (fn, delay) => {
        let timer = null;
        return function (...args) {
          if(!timer) fn.apply(this, args); // 第一次立即执行
          clearTimeout(timer);
          timer = setTimeout(() => fn.apply(this, args), delay);
        };
      };
      

节流(throttle)

让一个函数在一定时间内最多执行一次

实现

  • 时间戳版

    • 立即执行一次,后续在规定时间间隔内不再触发
    • 第一次立即执行,但最后一次可能丢失
    • function throttle(fn, delay) {
        let lastTime = 0;
        return function (...args) {
          const now = Date.now();
          if (now - lastTime >= delay) {
            lastTime = now;
            return fn.apply(this, args);
          }
        };
      }
      
      window.addEventListener(
        'scroll',
        throttle(function () {
          console.log('scrolling...');
        }, 1000)
      );
      
  • 定时器版

    • 等到一定时间后再执行(固定间隔执行)
    • 第一次不会立即执行,最后一次不会丢失
    • function throttle(fn, delay) {
        let timer = null;
        return function (...args) {
          if (!timer) {
            timer = setTimeout(() => {
              fn.apply(this, args);
              timer = null;
            }, delay);
          }
        };
      }
      
      document.getElementById('btn').onclick = throttle(() => {
        console.log("按钮点击:", Date.now());
      }, 2000);
      
  • 时间戳+定时器

    • function throttle(fn, delay) {
        let lastTime = 0;
        let timer = null;
        return function (...args) {
          const now = Date.now();
          const remaining = delay - (now - lastTime);
          if (remaining <= 0) {
            if (timer) {
              clearTimeout(timer);
              timer = null;
            }
            fn.apply(this, args);
            lastTime = now;
          } else if (!timer) {
            timer = setTimeout(() => {
              fn.apply(this, args);
              lastTime = Date.now();
              timer = null;
            }, remaining);
          }
        };
      }
      

防抖升级版:竞争触发

不等待(不像传统防抖延迟发请求),每次输入都立即发请求,但只让“该用的结果生效”,避免“旧请求慢返回把新结果覆盖”的问题

策略

  • 最新优先(takeLatest)

    • 只让最后一次触发的结果生效
    • 搜索联想、级联选择、表格筛选 --- 每次输入都请求,但UI只展示最后一次输入对应的数据
  • 最快优先(fastesWins)

    • 谁先返回就用谁

    • 多个镜像/CDN/数据源同时请求,先到先用

0.1+0.2

现象

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

原因

JS中数字(除BigInt)都遵循双精度浮点数标准,像0.1,0.2这样的十进制小数,无法用二进制浮点数表示

  • 0.1 转换为二进制是无限循环小数 0.0001100110011...(无限)
  • 0.2也是无限循环
  • 存储时会被截断,导致 微小误差

当 0.1+0.2 时,误差累加,结果就变成了 0.30000000000000004。

解决方案

  • 误差容忍

使用Number.EPSILON判断两个数

function equal(a, b) {
  return Math.abs(a - b) < Number.EPSILON;
}

console.log(equal(0.1 + 0.2, 0.3)); // true
  • 四舍五入
let sum = 0.1 + 0.2;
console.log(Number(sum.toFixed(10))); // 0.3
  • 整数化后计算
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3

大数加法

bigInt

const a = 12345678901234567890n;
const b = 98765432109876543210n;
console.log(a + b); // 111111111011111111100n

实现

  • 把数字当字符串处理,每一位手动相加

    • function addBigInt(a, b) {
        a = a.toString();
        b = b.toString();
      
        const maxLength = Math.max(a.length, b.length);
        a = a.padStart(maxLength, '0');
        b = b.padStart(maxLength, '0');
      
        let carry = 0;
        let res = [];
      
        for (let i = maxLength - 1; i >= 0; i--) {
          const sum = parseInt(a[i]) + parseInt(b[i]) + carry;
          res.push(sum % 10);
          carry = Math.floor(sum / 10);
        }
        
        if (carry) {
          res.push(carry);
        }
        return res.reverse().join('');
      }
      

大数减法

function subBigInt(a, b) {
  a = a.toString();
  b = b.toString();

  a = a.replace(/^0+/, '') || '0';
  b = b.replace(/^0+/, '') || '0';

  let sign = '';
  if (a.length < b.length || (a.length === b.length && a < b)) {
    [a, b] = [b, a];
    sign = '-';
  }

  const maxLength = Math.max(a.length, b.length);
  a = a.padStart(maxLength, '0');
  b = b.padStart(maxLength, '0');

  let borrow = 0;
  let res = [];

  for (let i = maxLength - 1; i >= 0; i--) {
    let x = parseInt(a[i]) - borrow;
    const y = parseInt(b[i]);
    if (x < y) {
      x += 10;
      borrow = 1;
    } else {
      borrow = 0;
    }
    res.push(x - y);
  }
  res = res.reverse().join('').replace(/^0+/, '') || '0';
  return sign + res;
}

setTimeout递归setInterval

  • 基本款

    • function run() {
        console.log('run', new Date().toLocaleTimeString());
      
        setTimeout(run, 1000);
      }
      setTimeout(run, 1000);
      
  • 带终止条件

    • let count = 0;
      
      function run() {
        console.log('第', ++count, '次执行');
      
        if (count < 5) {
          setTimeout(run, 1000); // 只执行5次
        }
      }
      
      setTimeout(run, 1000);
      

分隔千分位

  • 非正则版

    • function formatNumber(n) {
        let numStr = n.toString();
      
        let sign = '';
        if (numStr[0] === '-') {
          sign = '-';
          numStr = numStr.slice(1);
        }
      
        let [intPart, decimalPart] = numStr.split('.');
      
        let result = '';
        let count = 0;
      
        for (let i = intPart.length - 1; i >= 0; i--) {
          result = intPart[i] + result;
          count++;
          if (count % 3 === 0 && i !== 0) {
            result = ',' + result;
          }
        }
      
        return sign + (decimalPart ? `${result}.${decimalPart}` : result);
      }
      
  • 正则版

    • function formatNumber(n) {
        let numStr = n.toString();
      
        let sign = '';
        if (numStr[0] === '-') {
          sign = '-';
          numStr = numStr.slice(1);
        }
      
        let [intPart, decimalPart] = numStr.split('.');
      
        intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
      
        return sign + (decimalPart ? `${intPart}.${decimalPart}` : intPart);
      }
      

深拷贝与浅拷贝

浅拷贝

只复制对象的第一层属性,如果属性是引用类型,只复制它的地址

const obj = {a: 1, b: {c: 2}};
const copy = {...obj};
copy.a = 100;
copy.b.c = 200;

console.log(obj);   // { a: 1, b: { c: 200 } }

a 是基本类型

  • 浅拷贝时复制了值 1 本身
  • copy.a = 100 修改的是 copy 对象中的值
  • 原对象 obj.a 仍然保持为 1

b 是引用类型

  • 浅拷贝时只复制了引用地址,没有创建新对象
  • copy.b 和 obj.b 指向同一个对象 { c: 2 }
  • copy.b.c = 200 实际上修改了共同引用的对象
  • 因此 obj.b.c 也变成了 200

常用方法

  • Object.assign({}, obj)

  • {...obj}

  • Array.prototype.slice()/contact()

深拷贝

不仅复制对象的第一层,还会递归地复制所有子对象,新对象与旧对象完全独立

const obj = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(obj));

copy.b.c = 200;

console.log(obj);   // { a: 1, b: { c: 2 } }

常用方法

  • 使用JSON.stringfy()将对象序列化,再将其通过JSON.parse()反序列化

    • 会丢失undefined、function、symbol等属性
    • 如果有正则表达式、Error会得到空对象
    • 无法处理循环引用
const deepCopy = JSON.parse(JSON.stringify(obj));

实现

function deepClone(target, map = new WeakMap()) {
  if (target === null || typeof target !== 'object') {
    return target; // 基础类型直接返回
  }

  if (map.has(target)) {
    return map.get(target); // 循环引用处理
  }

  let clone = Array.isArray(target) ? [] : {};
  map.set(target, clone);

  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      clone[key] = deepClone(target[key], map); // 递归深拷贝
    }
  }
  return clone;
}

const obj = { a: 1, b: { c: 2 } };
const copy = deepClone(obj);

copy.a = 100;
copy.b.c = 200; 

console.log(obj); // { a: 1, b: { c: 200 } }
console.log(copy); // { a: 100, b: { c: 200 } } 

数组乱序(洗牌)

实现

function shuffle(arr) {
  const res = arr.slice();
  for (let i = res.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [res[i], res[j]] = [res[j], res[i]];
  }
  return res;
}

数组判断相等

严格顺序比较(元素和顺序都必须相同)

function arraysEqual(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  return arr1.every((item,index) => value === arr2[index]);
}

console.log(arraysEqual([1, 2, 3], [1, 2, 3])); // true
console.log(arraysEqual([1, 2, 3], [3, 2, 1])); // false

无序比较(元素相同但顺序可以不同)

function arraysEqual(arr1, arr2) {
  if (arr1.length !== arr2.length) return false;
  return arr1.slice().sort().every((item, index) => arr2.slice().sort()[index]);
}

深度比较(多维数组/对象)

function arraysEqual(arr1, arr2) {
  return JSON.stringfy(arr1) === JSON.stringfy(arr2);
}

console.log(arraysEqual([1, [2, 3]], [1, [2, 3]])); // true
console.log(arraysEqual([1, [2, 3]], [1, [3, 2]])); // false
  • 使用递归判断
function arraysEqual(x, y) {
  if (x === y) return true;
  if (Array.isArray(x) && Array.isArray(y)) {
    if (x.length !== y.length) return false;
    return x.every((item, index) => arraysEqual(item, y[index]));
  }
  
  if (x && y && typeof x === 'object' && typeof y ==== 'object') {
    const keyX = Object.keys(x);
    const keyY = Object.keys(y);
    if (keyX.length !== keyY.length) return false;
    return keyX.every(key => arraysEqual(x[key], y[key]));
  }
  return false;
}

console.log(arraysEqual([1, [2, 3]], [1, [2, 3]])); // true
console.log(arraysEqual({ a: 1, b: [2, 3] }, { a: 1, b: [2, 3] })); // true

多维数组求和

 // 一维
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((acc, cur) => arr + cur, 0);

// 扁平化
const arr1 = [1, [2, 3], [4, [5]]];
const sum1 = arr1.flat(Infinity).reduce((acc, cur) => arr + cur, 0);

//递归
const arr2 = [1, [2, 3], [4, [5]]];
function deepSum(arr) {
  let sum = 0;
  for (let a of arr) {
    if (Array.isArray(a)) {
      sum += deepSum(a);
    } else {
      sum += a;
    }
  }
  return sum;
}

// 递归
const arr3 = [1, [2, 3], [4, [5]]];
const sum3 = arr3.reduce((acc, cur) =>{
  return acc + (Array.isArray(cur) ? sum3(cur) : cur)
}, 0)

数组去重

  • Set

    • const arr = [1, 2, 2, 3, 3, 4];
      const unique = [...new Set(arr)];
      console.log(unique); // [1, 2, 3, 4] 
      
  • Filter

    • const arr = [1, 2, 2, 3, 3, 4];
      const unique = arr.filter((item, index) => arr.indexOf(item) === index);
      console.log(unique); // [1, 2, 3, 4] 
      

数组扁平化

const arr = [1, [2, [3, [4, [5]]]]];
console.log(arr.flat(Infinity));

function flatten(arr) {
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? flatten(val) : val);
  }, []);
}
console.log(flatten(arr));

function flatten1(arr) {
  const res = [];
  for (let a in arr) {
    if (Array.isArray(a)) {
      res = res.concat(flatten1(a));
    } else {
      res.push(a);
    }
  }
  return res;
}

function flatten2(arr) {
  while(arr.some(Array.isArray)) {
    arr = [].concat(...arr);
  }
  return arr;
}

数组push

function myPush(arr, ...items) {
  let len = arr.length;
  for (let i = 0; i < items.length; i++) {
    arr[len] = items[i];
    len++;
  }
}
const arr = [1, 2, 3];
const newLength = myPush(arr, 4, 5);
console.log(arr); // [1, 2, 3, 4, 5]
console.log(arr.length);


Array.prototype.myPush = function (...items) {
  let len = this.length;
  for (let i = 0; i < items.length; i++) {
    this[len] = items[i];
    len++;
  }
};
const arr = [1, 2, 3];
arr.myPush(4, 5);
console.log(arr); // [1, 2, 3, 4, 5]

数组filter

稀疏数组:某些索引上没有元素或者没有赋值

[1,,3]

function myFilter(arr, callback, thisArg) {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    // 跳过稀疏数组中没有值的地方
    if (i in arr) {
      const item = arr[i];
      if (callback.call(thisArg, item, i, arr)) {
        res.push(item);
      }
    }
  }
  return res;
}

const arr = [1, 2, 3, 4, 5];
const filtered = myFilter(arr, x => x > 2);
console.log(filtered); // [3, 4, 5]

数组map

Array.prototype.myMap = function (callback, thisArg) {
  if (typeof callback !== 'function') {
    throw new TypeError(callback + 'is not a function');
  }

  const arr = this;
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    // 跳过稀疏数组中没有值的地方
    if (i in arr) {
      result[i] = callback.call(thisArg, arr[i], i, arr);
    }
  }
  return result;
};

const arr = [1, 2, 3];
const newArr = arr.myMap((item, index) => item * 2);
console.log(newArr); // [2, 4, 6] 

数组reduce

Array.prototype.myReduce = function (callback, initialValue) {
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }

  const arr = this;
  let accumulator;
  let startIndex;

  if (arguments.length > 2) {
    accumulator = initialValue;
    startIndex = 0;
  } else {
    let i = 0;
    while (i < arr.length && !(i in arr)) {
      i++;
    }
    if (i >= arr.length) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = arr[i];
    startIndex = i + 1;
  }

  for (let i = startIndex; i < arr.length; i++) {
    if (i in arr) {
      accumulator = callback(accumulator, arr[i], i, arr);
    }
  }

  return accumulator;
};

const arr = [1, 2, 3, 4];
const sum = arr.myReduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10

数组转树

  • 递归

    • const arr = [
        { id: 1, parentId: 0, name: 'A' },
        { id: 2, parentId: 1, name: 'B' },
        { id: 3, parentId: 1, name: 'C' },
        { id: 4, parentId: 2, name: 'D' },
        { id: 5, parentId: 0, name: 'E' },
      ];
      
      function buildTree(arr, parentId = 0) {
        return arr
          .filter(item => item.parentId === parentId)
          .map(item => ({
            ...item,
            children: buildTree(arr, item.id),
          }));
      }
      
      const tree = buildTree(arr);
      console.log(JSON.stringify(tree, null, 2));
      
  • map

    • function buildTree(arr) {
        const map = new Map();
        const tree = [];
      
        arr.forEach(item => map.set(item.id, { ...item, children: [] }));
      
        arr.forEach(item => {
          const node = map.get(item.id);
          if (item.parentId === 0) {
            tree.push(node);
          } else {
            const parent = map.get(item.parentId);
            if (parent) {
              parent.children.push(node);
            }
          }
        });
      
        return tree;
      }
      const tree = buildTree(arr);
      console.log(JSON.stringify(tree, null, 2));
      

对象迭代器

用法

JS中for..of、扩展运算符...、数组解构

实现

const myIterable = {
  data: ['a','b','c'],
  [Symbol.iterator]() {
    let index = 0;
    const arr = this.data;

    return {
      next() {
        if (index < arr.length) {
          return { value: arr[index++], done: false };
        } else {
          return { value: undefined, done: true}
        }
      }
    }
  }
}

for (const v of myIterable) {
  console.log(v); // a b c
}
console.log([...myIterable]); // ['a','b','c'] 
const obj = {
  name: 'dy',
  age: 18,
  city: 'bj',
  [Symbol.iterator]() {
    const entrues = Object.entries(this);
    let index = 0;
    return {
      next() {
        if (index < entrues.length) {
          return { value: entrues[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};

for (const [key, value] of obj) {
  console.log(key, value);
}

JSONP

原理

  • 一种跨域请求数据的方式,基于
  • 服务器返回的不是纯JSON,而是被一个回调函数包裹的js代码
  • 前端通过定义一个全局函数作为回调,在

实现

function jsonp(url, params = {}, callbackName = 'callback') {
  return new Promise((resolve, reject) => {
    const cbName =
      'jsonp_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
    params[callbackName] = cbName;

    const queryString = Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&');
      
    const script = document.createElement('script');
    script.src = `${url}?${queryString}`;

    window[cbName] = data => {
      resolve(data);
      document.body.removeChild(script);
      delete window[cbName];
    };

    script.onerror = () => {
      reject(new Error(`JSONP request to ${url} failed`));
      document.body.removeChild(script);
      delete window[cbName]; 
    }
  });
}

封装ajax

function ajax(options) {
  const {
    url,
    method = 'GET',
    data = null,
    async = true,
    headers = {},
    timeout = 0,
  } = options;

  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    let finalUrl = url;
    if (method.toUpperCase() === 'GET' && data) {
      const params = new URLSearchParams(data).toString();
      finalUrl += (url.includes('?') ? '&' : '?') + params;
    }

    xhr.open(method, finalUrl, async);

    for (let key in headers) {
      xhr.setRequestHeader(key, headers[key]);
    }

    if (timeout) {
      xhr.timeout = timeout;
    }

    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          try {
            resolve(JSON.parse(xhr.responseText));
          } catch (e) {
            resolve(xhr.responseText);
          }
        } else {
          reject(new Error(`Request failed with status ${xhr.status}`));
        }
      }
    };

    xhr.ontimeout = () => reject(new Error('Request timed out'));
    xhr.onerror = () => reject(new Error('Network error'));

    if (method.toUpperCase() === 'POST') {
      if (!(data instanceof FormData)) {
        xhr.setRequestHeader(
          'Content-Type',
          'application/x-www-form-urlencoded'
        );
        xhr.send(new URLSearchParams(data).toString());
      } else {
        xhr.send(data);
      }
    } else {
      xhr.send();
    }
  });
}

双向绑定

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input type="text" id="input" />
      <p id="text"></p>
    </div>

    <script>
      const data = {};

      Object.defineProperty(data, 'message', {
        get() {
          return this._message;
        },
        set(val) {
          this._message = val;
          document.getElementById('text').textContent = val;
          document.getElementById('input').value = val;
        },
      });

      const input = document.getElementById('input');
      input.addEventListener('input', function (e) {
        data.message = e.target.value;
      });

      data.message = 'Hello, 双向绑定!';
    </script>
  </body>
</html>
  • proxy版
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <input type="text" id="input2" />
      <p id="text2"></p>
    </div>

    <script>
      const input2 = document.getElementById('input2');
      const text2 = document.getElementById('text2');

      const state = {
        message: '',
      };

      const data2 = new Proxy(state, {
        set(target, key, value) {
          target[key] = value;
          if (key === 'message') {
            text2.textContent = value;
            input2.value = value;
          }
          return true;
        },
        get(target, key) {
          return target[key];
        },
      });

      input2.addEventListener('input', e => {
        data2.message = e.target.value;
      });
      data2.message = 'Hello';
    </script>
  </body>
</html>

发布订阅

发布-订阅模式(Publish–Subscribe Pattern)是一种常见的设计模式,常用于模块解耦。

  • 订阅者(Subscriber):对某个事件感兴趣,注册回调函数。
  • 发布者(Publisher):当事件发生时,发布(触发)消息通知所有订阅者。
class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  emit(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(...args));
    }
  }

  off(eventName, callback) {
    if (!this.events[eventName]) return;
    this.events[eventName] = this.events[eventName].filter(
      cb => cb !== callback
    );
  }

  once(eventName, callback) {
    const onceWrapper = (...args) => {
      callback(...args);
      this.off(eventName, onceWrapper);
    };
    this.on(eventName, onceWrapper);
  }
}

请求并发

  • Promise.all实现

    • 无法控制并发数,一次性全发
    • function request(url, delay) {
        return new Promise(resolve => {
          setTimeout(() => {
            console.log(`请求${url}成功`);
            resolve(url);
          }, delay);
        });
      }
      
      const urls = [
        request('api/1', 1000),
        request('api/2', 2000),
        request('api/3', 1500),
      ];
      
      Promise.all(urls)
        .then(results => {
          console.log('全部完成', results);
        })
        .catch(err => console.error('出错', err));
      
  • 限制并发数量

    • function request(tasks, limit = 3) {
        const results = [];
        let index = 0;
        let active = 0;
      
        return new Promise((resolve, reject) => {
          function next() {
            if (index === tasks.length && active === 0) {
              resolve(results);
              return;
            }
          }
      
          while (active < limit && index < tasks.length) {
            const currentIndex = index;
            const task = tasks[currentIndex];
            index++;
            active++;
      
            task()
              .then(res => {
                results[currentIndex] = res;
              })
              .catch(err => {
                results[currentIndex] = err;
              })
              .finally(() => {
                active--;
                next();
              });
          }
          next();
        });
      }
      
    • async function fetchWithLimit(tasks, limit = 3) {
        const pool = new Set();
        const results = [];
      
        for (const task of tasks) {
          const p = task().then(result => {
            pool.delete(p);
            return result;
          });
          pool.add(p);
          
          if (pool.size >= limit) {
            await Promise.race(pool);
          }
          results.push(p);
        }
      
        return Promise.all(results);
      }
      

Case

题目 — 实现有并发限制的 Promise 池

请你实现一个函数 promisePool(tasks, limit),用于并发执行一组返回 Promise 的函数(任务),限制最多同时运行 limit 个任务。要求如下:

要求

  1. tasks 是一个函数数组,每个函数调用后返回一个 Promise(例如:() => fetch(url))。
  2. 最多同时运行 limit 个任务(limit 为正整数)。
  3. 返回值是一个 Promise,当所有任务都完成(全部 resolve)时,返回 Promise resolve,值为和 tasks 顺序对应的结果数组。
  4. 若任一任务 reject,则 promisePool 应立即 reject(并取消/不再启动后续任务),reject 的原因为该任务的 error。
  5. 保留任务的顺序 —— 返回数组中第 i 项是第 i 个任务的结果(或按上面第4点直接 reject)。
const wait =
  (t, val, shouldReject = false) =>
  () =>
    new Promise((resolve, reject) =>
      setTimeout(() => (shouldReject ? reject(val) : resolve(val)), t)
    );

const tasks = [wait(300, 'A'), wait(200, 'B'), wait(100, 'C'), wait(400, 'D')];

promisePool(tasks, 2).then(console.log).catch(console.error);
// 运行顺序示意(并发最多2个):启动 tasks[0] & tasks[1],随后当某个完成则启动 tasks[2],以此类推。
// 最终输出(resolve): ['A', 'B', 'C', 'D']

function promisePool(tasks, limit) {
  return new Promise((resolve, reject) => {
    let n = tasks.length;
    let result = new Array(n);
    let started = 0; // 已经启动的task数
    let finished = 0; //已完成的task数
    let active = 0; // 正在运行的task数
    let stop = false; // 是否有reject

    function nextTask() {
      if (n === finished) {
        return resolve(result);
      }

      while (!skytop && started < n && active < limit) {
        let idx = started++;
        active++;
        let p;
        try {
          p = tasks[idx]();
        } catch (e) {
          stop = true;
          return reject(e);
        }

        Promise.resolve(p)
          .then(val => {
            result[idx] = val;
            active--;
            finished++;
            nextTask();
          })
          .catch(e => {
            if (!stop) {
              stop = true;
              return reject(e);
            }
          });
      }
    }
    nextTask();
  });
}

交替亮灯

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .lights {
        display: flex;
        gap: 20px;
        margin-top: 40px;
        justify-content: center;
      }
      .lamp {
        width: 60px;
        height: 60px;
        border-radius: 50%;
        background: #333;
        transition: background 0.3s, transform 0.2s;
      }
      .lamp.active {
        background: gold;
        transform: scale(1.1);
      }
    </style>
  </head>
  <body>
    <div class="lights">
      <div class="lamp"></div>
      <div class="lamp"></div>
      <div class="lamp"></div>
    </div>

    <script>
      const lamps = document.querySelectorAll('.lamp');

      const delay = ms => new Promise(res => setTimeout(res, ms));

      function setActive(index) {
        lamps.forEach((lamp, i) =>
          lamp.classList.toggle('active', i === index)
        );
      }

      async function run() {
        let index = 0;
        while (true) {
          setActive(index);
          await delay(500);
          index = (index + 1) % lamps.length;
        }
      }

      run();
    </script>
  </body>
</html>

哈希路由

利用URL的#来实现前端路由,#后面的内容(hash)不会被浏览器发送到服务器

class HashRouter {
  constructor() {
    this.routes = {};
    window.addEventListener('hashchange', this.load.bind(this));
    window.addEventListener('load', this.load.bind(this));
  }

  register(path, callback) {
    this.routes[path] = callback || function () {};
  }

  load() {
    const path = location.hash.slice(1) || '/';
    if (this.routes[path]) {
      this.routes[path]();
    } else {
      console.log('404');
    }
  }
}

判断对象是否循环引用

循环引用:对象间相互引用形成一个环

const obj = {};

obj.self = obj; // obj 自己引用自己

function hasCircularReference(obj, seen = new WeakSet()) {
  if (obj && typeof obj === 'object') {
    if (seen.has(obj)) {
      return true;
    }
    seen.add(obj);
    for (const key in obj) {
      if (Object.hasOwn(obj, key)) {
        if (hasCircularReference(obj[key], seen)) {
          return true;
        }
      }
    }
  }

  return false;
}