跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端算法

30 道高频 JavaScript 手写实现汇总

整理 30 个高频 JavaScript 手写实现,涵盖函数防抖节流、深拷贝、原型链操作、Promise 规范、设计模式及数组处理等核心知识点。通过解析原理与代码示例,帮助开发者深入理解语言机制,适用于面试准备与日常开发优化。涉及 AJAX、JSONP、URL 解析及图片懒加载等实际应用场景。

字节跳动发布于 2026/2/26更新于 2026/9/964 浏览
30 道高频 JavaScript 手写实现汇总

JavaScript 手写实现合集

1. 手写防抖函数

原理:事件被触发 n 秒后再执行回调,n 秒内又被触发则重新计时。

// 函数防抖的实现
function debounce(fn, wait) {
  let timer = null;
  return function () {
    let context = this, args = arguments;
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 窗口滚动条的监听:滚动条触发次数过于频繁,通常只需要获取最后一次的位置

2. 手写节流函数

原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

使用时间戳实现
// 函数节流的实现
function throttle(fn, delay) {
  let curTime = Date.now();
  return function () {
    let context = this, args = arguments, nowTime = Date.now();
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}
使用标记实现
// 函数节流的实现
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

3. 手写深拷贝

使用 JSON 方法
// 使用 JSON 处理方法
const newObj = JSON.parse(JSON.stringify(obj));

局限性:

  • 函数对象会被转成 null,RegExp 会变成 {}
  • 会抛弃对象的 constructor,所有的构造函数会指向 Object
  • 对象有循环引用会报错
递归实现深拷贝
// 递归实现深拷贝
function deepCopy(obj, dep = 0) {
  if (typeof obj !== "object" || dep < 1) {
    return typeof obj === "object" ? Object.assign({}, obj) : obj;
  }
  let clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      if (obj[key] && typeof obj[key] === "object") {
        clone[key] = deepCopy(obj[key], dep - 1);
      } else {
        clone[key] = obj[key];
      }
    }
  }
  return clone;
}

这个实现简单易懂,面试中能写出这个就足够了。

4. 手写 Object.create

function create(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

面试知识点:Object.create(null) 可以创建一个不含原型链的纯净 Object 对象。

5. 手写 instanceof

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

实现思路:

  1. 首先获取类型的原型
  2. 然后获得对象的原型
  3. 循环判断对象的原型是否等于类型的原型,直到对象原型为 null
// instanceof 的实现
function myInstanceof(left, right) {
  // 获取对象原型
  let __proto__ = Object.getPrototypeOf(left),
    prototype = right.prototype;
  // 获取构造函数的 prototype
  while (__proto__) {
    if (__proto__ === prototype) {
      return true;
    }
    __proto__ = Object.getPrototypeOf(__proto__);
  }
  return false;
}

6. 手写 new 操作符

在调用 new 的过程中会发生四件事情(面试常问):

  1. 在堆中开辟一块内存创建了一个新的空对象
  2. 设置原型,将新对象的原型设置为函数的 prototype 对象
  3. 让函数的 this 指向这个对象,执行构造函数的代码
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象;如果是引用类型,就返回这个引用类型的对象
function objectFactory(constructor, ...rest) {
  let newObject = null,
    result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, ...rest);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}

7. 手写 Function.prototype.call 方法

实现步骤:

  1. 判断调用对象是否为函数
  2. 判断传入上下文对象是否存在,如果不存在,则为 window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// call 函数实现
Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

8. 手写 Function.prototype.apply 方法

实现步骤:

  1. 判断调用对象是否为函数
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
// apply 函数实现
Function.prototype.myApply = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

9. 手写 Function.prototype.bind 方法

实现步骤:

  1. 判断调用对象是否为函数
  2. 保存当前函数的引用,获取其余传入参数值
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况
// bind 函数实现
Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(this instanceof Fn ? this : context, args.concat(...arguments));
  };
};

10. 手写函数柯里化

柯里化就是把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。

实现思路:

  1. 判断传递的参数是否达到执行函数的 fn 个数
  2. 没有达到的话,继续返回新的函数,并且返回 curry 函数传递剩余参数
实现方式 1
function curry(fn, args) {
  // 获取函数需要的参数长度
  let length = fn.length;
  args = args || [];
  return function () {
    let subArgs = args.slice(0);
    // 拼接得到现有的所有参数
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }
    // 判断参数的长度是否已经满足函数所需参数的长度
    if (subArgs.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, subArgs);
    } else {
      // 如果不满足,递归返回柯里化的函数,等待参数的传入
      return curry.call(this, fn, subArgs);
    }
  };
}
实现方式 2
function currying(exeFunc) {
  let args = [];
  let currFunc = function (...rest) {
    args.push(...rest);
    if (rest.length <= 0) {
      return exeFunc(args);
    } else {
      return currFunc;
    }
  };
  return currFunc;
}
ES6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

11. 手写 AJAX 请求

AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建 AJAX 请求的步骤:

  1. 创建一个 XMLHttpRequest 对象
  2. 在这个对象上使用 open 方法创建一个 HTTP 请求
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数
  4. 当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成
  5. 最后调用 send 方法来向服务器发起请求
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function () {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function () {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

12. 手写 A+ 规范的 Promise

Promise 是 ES6 新增的一个对象,手写 Promise 能帮助更好地理解其原理。

var PromisePolyfill = (function () {
  // 和 reject 不同的是 resolve 需要尝试展开 thenable 对象
  function tryToResolve(value) {
    if (this === value) {
      // 主要是防止下面这种情况
      // let y = new Promise(res => setTimeout(res(y)))
      throw TypeError("Chaining cycle detected for promise!");
    }
    // 根据规范 2.32 以及 2.33 对对象或者函数尝试展开
    // 保证 S6 之前的 polyfill 也能和 ES6 的原生 promise 混用
    if (value !== null && (typeof value === "object" || typeof value === "function")) {
      try {
        // 这里记录这次 then 的值同时要被 try 包裹
        // 主要原因是 then 可能是一个 getter, 也也就是说
        // 1. value.then 可能报错
        // 2. value.then 可能产生副作用 (例如多次执行可能结果不同)
        var then = value.then;
        // 另一方面,由于无法保证 then 确实会像预期的那样只调用一个 onFullfilled / onRejected
        // 所以增加了一个 flag 来防止 resolveOrReject 被多次调用
        var thenAlreadyCalledOrThrow = false;
        if (typeof then === "function") {
          // 是 thenable 那么尝试展开
          // 并且在该 thenable 状态改变之前 this 对象的状态不变
          then.bind(value)(
            // onFullfilled
            function (value2) {
              if (thenAlreadyCalledOrThrow) return;
              thenAlreadyCalledOrThrow = true;
              tryToResolve.bind(this, value2)();
            }.bind(this),
            // onRejected
            function (reason2) {
              if (thenAlreadyCalledOrThrow) return;
              thenAlreadyCalledOrThrow = true;
              resolveOrReject.bind(this, "rejected", reason2)();
            }.bind(this)
          );
        } else {
          // 拥有 then 但是 then 不是一个函数 所以也不是 thenable
          resolveOrReject.bind(this, "resolved", value)();
        }
      } catch (e) {
        if (thenAlreadyCalledOrThrow) return;
        thenAlreadyCalledOrThrow = true;
        resolveOrReject.bind(this, "rejected", e)();
      }
    } else {
      // 基本类型 直接返回
      resolveOrReject.bind(this, "resolved", value)();
    }
  }

  function resolveOrReject(status, data) {
    if (this.status !== "pending") return;
    this.status = status;
    this.data = data;
    if (status === "resolved") {
      for (var i = 0; i < this.resolveList.length; ++i) {
        this.resolveList[i]();
      }
    } else {
      for (i = 0; i < this.rejectList.length; ++i) {
        this.rejectList[i]();
      }
    }
  }

  function Promise(executor) {
    if (!(this instanceof Promise)) {
      throw Error("Promise can not be called without new !");
    }
    if (typeof executor !== "function") {
      // 非标准 但与 Chrome 谷歌保持一致
      throw TypeError("Promise resolver " + executor + " is not a function");
    }
    this.status = "pending";
    this.resolveList = [];
    this.rejectList = [];
    try {
      executor(tryToResolve.bind(this), resolveOrReject.bind(this, "rejected"));
    } catch (e) {
      resolveOrReject.bind(this, "rejected", e)();
    }
  }

  Promise.prototype.then = function (onFullfilled, onRejected) {
    // 返回值穿透以及错误穿透,注意错误穿透用的是 throw 而不是 return,否则的话
    // 这个 then 返回的 promise 状态将变成 resolved 即接下来的 then 中的 onFullfilled
    // 会被调用,然而我们想要调用的是 onRejected
    if (typeof onFullfilled !== "function") {
      onFullfilled = function (data) {
        return data;
      };
    }
    if (typeof onRejected !== "function") {
      onRejected = function (reason) {
        throw reason;
      };
    }
    var executor = function (resolve, reject) {
      setTimeout(function () {
        try {
          // 拿到对应的 handle 函数处理 this.data
          // 并以此为依据解析这个新的 Promise
          var value =
            this.status === "resolved"
              ? onFullfilled(this.data)
              : onRejected(this.data);
          resolve(value);
        } catch (e) {
          reject(e);
        }
      }.bind(this));
    };
    // then 接受两个函数返回一个新的 Promise
    // then 自身的执行永远异步与 onFullfilled/onRejected 的执行
    if (this.status !== "pending") {
      return new Promise(executor.bind(this));
    } else {
      // pending
      return new Promise(function (resolve, reject) {
        this.resolveList.push(executor.bind(this, resolve, reject));
        this.rejectList.push(executor.bind(this, resolve, reject));
      }.bind(this));
    }
  };

  // for prmise A+ test
  Promise.deferred = Promise.defer = function () {
    var dfd = {};
    dfd.promise = new Promise(function (resolve, reject) {
      dfd.resolve = resolve;
      dfd.reject = reject;
    });
    return dfd;
  };

  // for prmise A+ test
  if (typeof module !== "undefined") {
    module.exports = Promise;
  }
  return Promise;
})();

PromisePolyfill.all = function (promises) {
  return new Promise((resolve, reject) => {
    const result = [];
    let cnt = 0;
    for (let i = 0; i < promises.length; ++i) {
      promises[i].then(
        (value) => {
          cnt++;
          result[i] = value;
          if (cnt === promises.length) resolve(result);
        },
        reject
      );
    }
  });
};

PromisePolyfill.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; ++i) {
      promises[i].then(resolve, reject);
    }
  });
};

13. 手写千位分隔符

千位分隔符是处理数字格式化常用的方式。

function parseToMoney(num) {
  num = parseFloat(num.toFixed(3));
  let [integer, decimal] = String.prototype.split.call(num, ".");
  integer = integer.replace(/\d(?=(\d{3})+$)/g, "$&,");
  return integer + "." + (decimal ? decimal : "");
}

14. 手写观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。Vue 双向绑定就是使用了观察者模式。

class Subject {
  constructor() {
    this.ObserverList = [];
  }
  add(observer) {
    this.ObserverList.push(observer);
  }
  remove(observer) {
    this.ObserverList = this.ObserverList.filter((item) => item !== observer);
  }
  notify(...arg) {
    this.ObserverList.forEach((cb) => cb.update(...arg));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  update() {
    console.log(this.name);
  }
}

15. 发布/订阅模式

发布/订阅模式:基于一个主题/事件通道,希望接收通知的对象(称为 subscriber)通过自定义事件订阅主题,被激活事件的对象(称为 publisher)通过发布主题事件的方式被通知。Vue2 中的全局事件总线就是使用这种设计模式。

/**
 * 发布/订阅模式组件
 */
// 定义发布/订阅类
class Pubsub {
  constructor() {
    this.topics = {};
    this.subUid = -1;
  }
  // 发布事件
  publish(topic, args) {
    if (!this.topics[topic]) return false;
    let subscribers = this.topics[topic];
    let len = subscribers ? subscribers.length : 0;
    while (len--) {
      subscribers[len].func(topic, args);
    }
    return this;
  }
  // 订阅事件
  subscribe(topic, func) {
    if (!this.topics[topic]) this.topics[topic] = [];
    let token = (++this.subUid).toString();
    this.topics[topic].push({
      token: token,
      func: func,
    });
    return token;
  }
  // 取消订阅
  unsubscribe(token) {
    for (let m in this.topics) {
      if (this.topics[m]) {
        for (let i = 0; i < this.topics[m].length; i++) {
          if (this.topics[m][i].token == token) {
            this.topics[m].splice(i, 1);
            return token;
          }
        }
      }
    }
    return this;
  }
}

16. 使用 setTimeout 实现 setInterval

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去。只有当执行栈为空的时候,才能去从事件队列中取出事件执行。

针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件。

function mySetInterval(fn, timeout) {
  setTimeout(() => {
    fn();
    mySetInterval(fn, timeout);
  }, timeout);
}

17. 将 JS 对象转化为树形结构

转换前:

source = [
  { id: 1, pid: 0, name: "body" },
  { id: 2, pid: 1, name: "title" },
  { id: 3, pid: 2, name: "div" },
];

转换为:

tree = [
  {
    id: 1,
    pid: 0,
    name: "body",
    children: [
      {
        id: 2,
        pid: 1,
        name: "title",
        children: [
          {
            id: 3,
            pid: 2,
            name: "div",
          },
        ],
      },
    ],
  },
];

实现方法:

function jsonToTree(data) {
  // 初始化结果数组,并判断输入数据的格式
  let result = [];
  if (!Array.isArray(data)) {
    return result;
  }
  // 使用 map,将当前对象的 id 与当前对象对应存储起来
  let map = {};
  data.forEach((item) => {
    map[item.id] = item;
  });
  data.forEach((item) => {
    let parent = map[item.pid];
    if (parent) {
      (parent.children || (parent.children = [])).push(item);
    } else {
      result.push(item);
    }
  });
  return result;
}

18. 实现 sleep 函数

使用 Promise 封装 setTimeout
function timeout(delay) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
}
使用时间戳阻塞线程
function sleep(time, fn) {
  let curDate = Date.now();
  while (Date.now() - curDate < time); // eslint-disable-line no-empty
  fn();
}

19. 手写 ES6 的 flat(数组扁平化)

数组扁平化就是将 [1, [2, [3]] 这种多层的数组拍平成一层 [1, 2, 3]。

// 数组扁平化示例
let arr = [
  [22, [1, 2, [5, 8], [9, 2]], [2, 7, 1, [9, 0, [1, 6]]]],
  [2],
];
方法 1: reduce + concat
function flatten(arr, dep) {
  return dep > 1
    ? arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatten(cur, dep - 1) : cur), [])
    : Array.prototype.slice.call(arr);
}
方法 2: some + concat + 解构
function flatten2(arr, dep = 0) {
  if (dep < 1) {
    return arr.slice();
  }
  while (dep && arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
    dep--;
  }
  return arr;
}
方法 3: push + apply
function flatten3(arr, dep = 0) {
  if (dep < 1) {
    return Array.prototype.slice.call(arr);
  }
  let result = [];
  arr.forEach((element) =>
    Array.isArray(element) ? result.push.apply(result, flatten3(element, dep - 1)) : result.push(element)
  );
  return result;
}
方法 4: toString (仅适用于数字)
function flatten4(arr) {
  return arr.toString().split(",").map((item) => +item);
}
方法 5: 栈实现
function flatten5(arr, dep = 0) {
  let stack = [...arr];
  let result = [];
  while (stack.length) {
    let data = stack.pop();
    if (Array.isArray(data)) {
      stack.push(...data);
    } else {
      result.push(data);
    }
  }
  return result.reverse();
}

20. 手写 Object.assign

Object.assign 用于浅拷贝对象属性。

Object.myAssign = function (target, ...source) {
  if (target == null) {
    throw new TypeError("Cannot convert undefined or null to object");
  }
  let ret = Object(target);
  source.forEach(function (obj) {
    if (obj != null) {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key];
        }
      }
    }
  });
  return ret;
};

21. 实现每隔一秒打印 1,2,3,4

var 作用域问题会导致一直输出最后一个数字。

使用闭包实现
for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  })(i);
}
使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, i * 1000);
}

22. JS 解析 URL Params 为对象

let url = "http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled";
parseParam(url);
/* 结果
{
  user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true // 未指定值得 key 约定为 true
}
*/
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split("&"); // 将字符串以 & 分割后存到数组中
  let paramsObj = {}; // 将 params 存到对象中
  paramsArr.forEach((param) => {
    if (/=/.test(param)) {
      // 处理有 value 的参数
      let [key, val] = param.split("=");
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
      if (paramsObj.hasOwnProperty(key)) {
        // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else {
        // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else {
      // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  });
  return paramsObj;
}

23. 实现一个 JSON.stringify

特性说明:

  • Boolean | Number | String 类型会自动转换成对应的原始值
  • undefined、任意函数以及 symbol,会被忽略(非数组对象)或者被转换成 null(数组中)
  • 不可枚举的属性会被忽略
  • 循环引用属性会被忽略
  • Function 会变成 null,RegExp 对象会变成 {}
function jsonStringify(obj) {
  let type = typeof obj;
  if (type !== "object") {
    if (/string|undefined|function/.test(type)) {
      obj = '"' + obj + '"';
    }
    return String(obj);
  } else {
    let json = [];
    let arr = Array.isArray(obj);
    for (let k in obj) {
      let v = obj[k];
      let type = typeof v;
      if (/string|undefined|function/.test(type)) {
        v = '"' + v + '"';
      } else if (type === "object") {
        v = jsonStringify(v);
      }
      json.push((arr ? "" : '"' + k + '":') + String(v));
    }
    return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
  }
}

24. 实现 JSON.parse

let json = '{"name":"cxk", "age":25}';
let obj = eval("(" + json + ")");

使用 eval 实现简单但很容易被 XSS 攻击。

25. 手写 JS 的继承

继承有很多方式,这里只归纳最优的两种:寄生组合式和 ES6 的 class 继承。

寄生组合式继承
function inheritPrototype(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  let prototype = Object.create(superType.prototype);
  // constructor 属性为子类构造函数
  prototype.constructor = subType;
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = prototype;
}
ES6 class 的 extends
class Rectangle {
  // constructor
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}
const rectangle = new Rectangle(40, 20);
console.log(rectangle.area); // 输出 800

// 继承
class Square extends Rectangle {
  constructor(len) {
    // 子类没有 this,必须先调用 super
    super(len, len);
    // 如果子类中存在构造函数,则需要在使用"this"之前首先调用 super()
    this.name = "SquareIng";
  }
  get area() {
    return this.height * this.width;
  }
}
const square = new Square(20);
console.log(square.area); // 输出 400

extends 继承的核心代码如下,其实和寄生组合式继承方式一样:

function _inherits(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  // 增强对象,弥补因重写原型而失去的默认的 constructor 属性
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  });
  if (superType) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subType, superType)
      : (subType.__proto__ = superType);
  }
}

26. 判断对象是否存在循环引用

循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用 JSON.stringify() 对该类对象进行序列化,就会报错:Converting circular structure to JSON。

const isCycleObject = (obj, parent) => {
  const parentArr = parent || [obj];
  for (let i in obj) {
    if (typeof obj[i] === "object") {
      let flag = false;
      parentArr.forEach((pObj) => {
        if (pObj === obj[i]) {
          flag = true;
        }
      });
      if (flag) return true;
      flag = isCycleObject(obj[i], [...parentArr, obj[i]]);
      if (flag) return true;
    }
  }
  return false;
};

27. 数组的去重

数组去重也是常用的处理数组面试题。

双重循环
function unique(array) {
  // res 用来存储结果
  var res = [];
  for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
    for (var j = 0, resLen = res.length; j < resLen; j++) {
      if (array[i] === res[j]) {
        break;
      }
    }
    // 如果 array[i] 是唯一的,那么执行完循环,j 等于 resLen
    if (j === resLen) {
      res.push(array[i]);
    }
  }
  return res;
}
ES6 Set
function unique(arr) {
  return [...new Set(arr)];
}
filter + indexOf
function unique_(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}
reduce + includes
function unique__(arr) {
  return arr.reduce((pre, cur) => (pre.includes(cur) ? pre : pre.concat(cur)), []);
}
Map 实现
function unique___(arr) {
  let mp = new Map();
  arr.forEach((element) => {
    if (!mp.has(element)) {
      mp.set(element, 1);
    }
  });
  return [...mp.keys()];
}
排序后去重
function unique(array) {
  var res = [];
  var sortedArray = array.slice().sort();
  var seen;
  for (var i = 0, len = sortedArray.length; i < len; i++) {
    // 如果是第一个元素或者相邻的元素不相同
    if (!i || seen !== sortedArray[i]) {
      res.push(sortedArray[i]);
    }
    seen = sortedArray[i];
  }
  return res;
}

28. 图片懒加载

图片懒加载就是鼠标滑动到哪里,图片加载到哪里。总的来说,一般页面打开,会同时加载页面所有的图片,如果页面的图片请求太多会造成很卡很慢的现象,为了避免这一现象,利用懒加载图片的方法,提高性能。

let imgList = [...document.querySelectorAll("img")];
let length = imgList.length;
const imgLazyLoad = (function () {
  let count = 0;
  return function () {
    let deleteIndexList = [];
    imgList.forEach((img, index) => {
      let rect = img.getBoundingClientRect();
      if (rect.top < window.innerHeight) {
        img.src = img.dataset.src;
        deleteIndexList.push(index);
        count++;
        if (count === length) {
          document.removeEventListener("scroll", imgLazyLoad);
        }
      }
    });
    imgList = imgList.filter((img, index) => !deleteIndexList.includes(index));
  };
})();
// 加上防抖处理
document.addEventListener("scroll", debounce(imgLazyLoad, 200));

29. 洗牌算法

打乱数组在开发场景中也是很常用的功能。

function shuffle(arr) {
  for (let i = 0; i < arr.length; i++) {
    let randomIndex = i + Math.floor(Math.random() * (arr.length - i));
    [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
  }
  return arr;
}

30. 手写 JSONP

JSONP 的特殊之处在于前端会传递一个 callback 参数给后端,后端返回数据时会将这个 callback 参数的值作为函数名来包裹住 JSON 数据,最终返给前端的就是一段 JS 代码,这样就巧妙地解决了跨域问题。优点是兼容性好,但只能用于 GET 请求,而且需要服务端支持。

const jsonp = ({ url, params, callbackName }) => {
  const generateUrl = () => {
    let dataSrc = "";
    for (let key in params) {
      if (params.hasOwnProperty(key)) {
        dataSrc += `${key}=${params[key]}&`;
      }
    }
    dataSrc += `callback=${callbackName}`;
    return `${url}?${dataSrc}`;
  };
  return new Promise((resolve, reject) => {
    const scriptEle = document.createElement("script");
    scriptEle.src = generateUrl();
    document.body.appendChild(scriptEle);
    window[callbackName] = (data) => {
      resolve(data);
      document.removeChild(scriptEle);
    };
  });
};

目录

  1. JavaScript 手写实现合集
  2. 1. 手写防抖函数
  3. 2. 手写节流函数
  4. 使用时间戳实现
  5. 使用标记实现
  6. 3. 手写深拷贝
  7. 使用 JSON 方法
  8. 递归实现深拷贝
  9. 4. 手写 Object.create
  10. 5. 手写 instanceof
  11. 6. 手写 new 操作符
  12. 7. 手写 Function.prototype.call 方法
  13. 8. 手写 Function.prototype.apply 方法
  14. 9. 手写 Function.prototype.bind 方法
  15. 10. 手写函数柯里化
  16. 实现方式 1
  17. 实现方式 2
  18. ES6 实现
  19. 11. 手写 AJAX 请求
  20. 12. 手写 A+ 规范的 Promise
  21. 13. 手写千位分隔符
  22. 14. 手写观察者模式
  23. 15. 发布/订阅模式
  24. 16. 使用 setTimeout 实现 setInterval
  25. 17. 将 JS 对象转化为树形结构
  26. 18. 实现 sleep 函数
  27. 使用 Promise 封装 setTimeout
  28. 使用时间戳阻塞线程
  29. 19. 手写 ES6 的 flat(数组扁平化)
  30. 方法 1: reduce + concat
  31. 方法 2: some + concat + 解构
  32. 方法 3: push + apply
  33. 方法 4: toString (仅适用于数字)
  34. 方法 5: 栈实现
  35. 20. 手写 Object.assign
  36. 21. 实现每隔一秒打印 1,2,3,4
  37. 使用闭包实现
  38. 使用 let 块级作用域
  39. 22. JS 解析 URL Params 为对象
  40. 23. 实现一个 JSON.stringify
  41. 24. 实现 JSON.parse
  42. 25. 手写 JS 的继承
  43. 寄生组合式继承
  44. ES6 class 的 extends
  45. 26. 判断对象是否存在循环引用
  46. 27. 数组的去重
  47. 双重循环
  48. ES6 Set
  49. filter + indexOf
  50. reduce + includes
  51. Map 实现
  52. 排序后去重
  53. 28. 图片懒加载
  54. 29. 洗牌算法
  55. 30. 手写 JSONP

更多推荐文章

查看全部
  • Windows 下配置 WSL2 复现 Linux 项目:安装、报错修复与环境初始化
  • WebLaTeX:基于 VSCode 的在线 LaTeX 编辑器
  • OpenClaw 开源 AI 智能体:架构、功能与实战部署
  • VSCode Copilot 无法连接网络问题解决方案
  • 绿联 NAS 配置 WebDAV 公网访问并使用 RaiDrive 挂载
  • Prompt Engineering 提示工程从入门到精通指南
  • 前端 Canvas 基础与实战应用
  • 昇腾 NPU 部署 Llama 2 模型的性能测试与优化实践
  • STM32 单片机智能家居安防控制系统
  • PyTorch 深度学习实战:图像分类任务详解
  • Web 前端 JavaScript 基本语法
  • Spring AI:Java 生态原生 AI 框架入门指南
  • 小厂架构师实战:AI Agent 辅助 Bug 定位与修复
  • 《Agent Runtime 工程化》第五章 上下文工程:5.1 token budget 是产品预算
  • 医疗连续体机器人模块化控制界面设计与 Python 库应用研究
  • 鸿蒙金融理财全栈项目:上线运维、用户反馈与持续迭代
  • Linux 库的制作与原理
  • OpenClaw Web Search 工具配置与渠道选择指南
  • AI Agent 设计模式实战:ReAct 架构详解
  • Python 七大学习路线详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online