Skip to content

面试-手撕

作者:青见春山
发表于:2026-07-21
字数统计:9312 字
预计阅读32分钟

参考链接

大部分题目来源牛客链接

高频

1. 防抖

Image

1.1 普通防抖

在规定时间内,如果事件被重复触发,只执行最后一次。比如搜索框输入。

JavaScript
function debounce(fn, delay) {
  // 1. 创建一个闭包变量timer,用于存储定时器ID
  let timer = null

  // 2. 返回一个新的函数(闭包)
  return function (...args) {
    // 3. 如果已经有定时器,清除它(取消之前的调用)
    if (timer) clearTimeout(timer)

    // 4. 设置新的定时器
    timer = setTimeout(() => {
      // 5. 在延迟时间后执行原函数
      // 使用apply确保正确的this上下文,并传递所有参数
      fn.apply(this, args)
    }, delay)
  }
}

防抖(Debounce)的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。 可以把它想象成进电梯,只要一直有人进(触发事件),电梯门就不会关(不执行真正的函数),直到一定时间内没人进了才关门。

1. 整体实现逻辑

  • debounce 函数接收两个参数:需要防抖的真实函数 fn 和延迟时间 delay

  • 它返回一个新的函数。我们绑定到DOM事件(如点击、滚动)上的,实际上是这个返回的新函数

  • 每次触发事件时,都会检查 timer 是否存在。如果存在,说明上一次触发还在等待执行中,于是通过 clearTimeout(timer) 取消掉上一次的计时。

  • 然后开启一个新的定时器重新倒计时。只有在 delay 期间没有新的触发,定时器里的 fn.apply(this, args) 才会被真正执行。


2. 闭包(Closure)的概念与应用

概念:闭包是指那些能够访问自由变量的函数。其实就是函数里面嵌套函数,内部函数可以访问外部函数的变量。

在这段代码中的体现

JavaScript
function debounce(fn,delay){
    let timer = null; // 外部函数的局部变量
    return function(...args){
        // 内部函数使用了外部的 timer
        if(timer) clearTimeout(timer);
        timer = setTimeout(...);
    }
}

内部返回的匿名函数访问并修改了外部 debounce 函数内的 timer 变量。当 debounce 执行完毕后,timer 并没有被垃圾回收机制销毁,因为它一直被返回的内部函数引用着。

3. 使用闭包的好处

  1. 维持状态(状态持久化)

防抖函数需要知道上一次触发的定时器是谁,以便在下一次触发时取消它。闭包让 timer 变量常驻内存,每次调用返回的函数时,都能操作同一个 timer

  1. 避免全局变量污染(封装私有变量)

如果没有闭包,我们必须把 timer 定义在全局作用域中,才能让多次触发都访问到同一个定时器。但这会导致全局变量泛滥,并且如果页面上有多个按钮使用了防抖,它们的定时器状态会互相覆盖、互相干扰。通过闭包,每次调用 debounce() 生成的 timer 都是独立且私有的。


4. this 指向问题

这段代码在处理 this 时非常巧妙,主要解决的是确保真正的函数 fn 执行时,上下文能和绑定事件时的上下文保持一致

拆解分析:

  1. 事件触发时的 this

    JavaScript
    return function(...args){ ... }

    这个返回的内部函数才是真正绑定给 DOM 元素的事件处理函数。当 DOM 触发事件(比如 button.onclick)时,这个内部函数的 this 会自动指向触发事件的 DOM 元素(即 button)。

  2. setTimeoutthis 陷阱机制与箭头函数

    JavaScript
    timer = setTimeout(() => { ... }, delay);

    如果是普通的 function() {} 传入 setTimeout,在非严格模式下,它内部的 this 默认会指向 window

    这里使用了箭头函数 () => {}。箭头函数没有自己的 this,它会捕获其所在上下文(也就是外层的返回函数 function(...args))的 this。这就成功地把 DOM 元素的 this 传递到了定时器内部。

  3. 使用 apply 绑定 this 和参数

    JavaScript
    fn.apply(this, args)

    如果不加 apply 直接调用 fn(...args),即使我们在箭头函数里,fn 内部的 this 依然可能是全局对象(因为是直接调用)。

    通过 apply(this, args),我们显式地让开发者传入的原函数 fn,使用前面捕获到的、正确的 DOM 元素 this,同时把收集到的事件参数 ...args(如 MouseEvent 对象)完整地传递给原函数。

    1.2 加可立即执行及取消

JavaScript
function debounce(fn, delay, immediate = false) {
  let timer = null

  return function (...args) {
  // 关键点1:每次调用时,先检查并清除之前的定时器
  // 这是防抖的核心:取消之前的调用,重新计时
    if (timer) clearTimeout(timer)

    if (immediate) {
     // 关键点2:判断是否可以立即执行
     // callNow为true的条件:timer为null(即没有正在进行的定时器)
      // 这意味着:
      // - 第一次调用时,timer为null → callNow = true
       // - 冷却期结束后,timer为null → callNow = true
        // - 在冷却期内,timer不为null → callNow = false
      const callNow = !timer
      // 关键点3:设置定时器,用于重置timer(冷却期)
      // 注意:这个定时器的作用不是执行函数,而是控制冷却期
      // 在delay毫秒后,将timer设为null,表示冷却期结束
      timer = setTimeout(() => {
        timer = null
      }, delay)
 // 关键点4:在冷却期开始时就执行函数
  // 只有满足callNow条件(即冷却期已结束)才执行
      if (callNow) fn.apply(this, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, delay)
    }
  }
}

加取消和立即执行的完整实现:

JavaScript
/**
 * 防抖函数(支持取消)
 * @param {Function} fn 要执行的函数
 * @param {number} delay 延迟时间
 * @param {boolean} immediate 是否立即执行
 */
function debounce(fn, delay, immediate = false) {
  let timer = null

  // 创建要返回的函数
  function debounced(...args) {
    if (timer) clearTimeout(timer)

    if (immediate) {
      const callNow = !timer

      timer = setTimeout(() => {
        timer = null
      }, delay)

      if (callNow) fn.apply(this, args)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null  // 执行后清除timer
      }, delay)
    }
  }

  // 添加 cancel 方法,用于手动取消
  debounced.cancel = function() {
    if (timer) {
      clearTimeout(timer)
      timer = null
    }
  }

  return debounced
}

在“立即执行”模式下:

  • timer 变量 == 冷却倒计时器

  • 如果 timernull,说明技能处于就绪状态(没有在冷却中)

  • 如果 timer 有值(非 null),说明技能正在冷却中

1. 立即执行的条件是什么?如果有 timer 代表什么?

JavaScript
// 看是否满足立即执行的条件
const callNow = !timer;
  • 立即执行的唯一条件:就是当前绝对没有定时器timernullundefined)。!null 会变成 true,所以 callNowtrue,允许放技能。

  • 如果有 timer 代表什么?:代表你前不久刚刚按过了按钮,现在正处于"冷却期"内。这时候 !timer 就是 false,所以本次触发被拦截,不能再次触发函数。


2. 为什么需要下面这个看似无用的定时器?

JavaScript
timer = setTimeout(() => {
    timer = null
}, delay)

这个定时器的唯一作用,就是“控制冷却什么时候结束”!它本身不负责执行原函数。

我们来看看你疯狂点击按钮时的完整运行流程:

第一下点击(第 0 秒):

  1. 此时 timernull

  2. 遇到 if (timer),因为是 null,跳过。

  3. callNow = !timer 算出来是 true(这就是初次执行的原因)。

  4. 开启冷却倒计时:timer = setTimeout(..., 2000)。(原本是 null 的 timer,现在变成了一个数字比如 1)

  5. if(callNow) 成立,立即执行 fn.apply(...)(技能放出来了!)

第二下疯狂点击(第 0.5 秒时):

  1. 此时 timer1

  2. 遇到 if (timer) clearTimeout(timer),把上一次的倒计时强行砸碎取消掉(但是 timer 变量里仍然存着 1 这个数字)!

  3. callNow = !timer 算出来是 false(因为 timer 是 1,说明技能还在冷却)。

  4. 重新开启新的冷却倒计时timer = setTimeout(..., 2000)。(原本是 1 的 timer,变成了新定时器的 ID 比如 2。这意味着:只要你手欠又点了一次,2秒的冷却时间就要重新开始算!

  5. if(callNow) 不成立,不执行函数(技能放不出来)

...只要一直以低于 2 秒的频率疯狂点击,步骤 2 到步骤 5 就会一直循环。虽然函数放不出来,但惩罚就是冷却时间不断被刷新

终于,你停手了(等了完整的 2.001 秒):

就在这 2 秒内没有人去捣乱(没人触发 clearTimeout),那个最后创建的定时器终于走完了!

此时,它执行了内部唯一的代码:

JavaScript
timer = null;

意味着:冷却彻底结束,系统回到初始状态。下一次点击,又可以立即执行了。

2. 节流

Image

时间戳实现及定时器实现

JavaScript
/**
 * 节流函数 - 时间戳实现
 * 在延迟时间内只执行第一次调用
 * @param {Function} fn 要执行的函数
 * @param {number} delay 延迟时间(毫秒)
 * @returns {Function} 节流后的函数
 */
function throttle(fn, delay) {
  // 记录上一次执行的时间戳
  let lastTime = 0

  // 返回节流后的函数
  return function (...args) {
    // 获取当前时间戳
    const now = Date.now()

    // 判断是否达到了执行间隔
    if (now - lastTime >= delay) {
      // 更新上一次执行时间
      lastTime = now
      // 执行原函数,保持 this 上下文和参数
      fn.apply(this, args)
    }
    // 如果没有达到间隔时间,则什么都不做
  }
}

/**
 * 节流函数 - 定时器实现
 * 在延迟时间内只执行最后一次调用
 * @param {Function} fn 要执行的函数
 * @param {number} delay 延迟时间(毫秒)
 * @returns {Function} 节流后的函数
 */
function throttle(fn, delay) {
  // 定时器标识
  let timer = null

  // 返回节流后的函数
  return function (...args) {
    // 如果没有正在等待执行的定时器
    if (!timer) {
      // 设置定时器,延迟执行原函数
      timer = setTimeout(() => {
        // 执行原函数,保持 this 上下文和参数
        fn.apply(this, args)
        // 执行完毕后清除定时器,允许下一次调用
        timer = null
      }, delay)
    }
    // 如果有定时器在等待,则忽略本次调用
  }
}

https://juejin.cn/post/7492402669392773147 讲解防抖节流的文章

3.Promise四种静态方法

四个静态方法对比

  • all:全部成功 → 成功;有一个失败 → 失败

  • race:谁先完成 → 就跟谁(不管成功/失败)

  • any:谁先成功 → 跟谁;全部失败 → 才失败

  • allSettled:全部完成 → 返回结果(永不失败)

3.1实现Promise.all

Promise.all 的实现思路:
返回一个新的 Promise
遍历传入数组,用 Promise.resolve 统一处理每一项
每个 Promise 成功后,将结果按索引存入结果数组
使用计数器记录完成数量
当所有 Promise 都成功时,resolve 返回结果数组
如果有任意一个 Promise 失败,直接 reject

JavaScript
/**
 * 实现 Promise.all 方法
 * 接收一个 Promise 数组,返回一个新的 Promise
 * 所有 Promise 成功时,返回结果数组
 * 任何一个 Promise 失败时,立即 reject
 * @param {Array} promises Promise 数组
 * @returns {Promise} 新的 Promise 对象
 */
Promise.myAll = function (promises) {
  // 返回一个新的 Promise 实例
  return new Promise((resolve, reject) => {
    // 存储每个 Promise 的结果,保持传入顺序
    const result = []
    // 计数器,记录已完成的 Promise 数量
    let count = 0

    // 遍历传入的 Promise 数组
    promises.forEach((p, index) => {
      // 使用 Promise.resolve 确保传入的值可以被正确处理
      // 如果是普通值,会直接 resolve;如果是 Promise,则等待其完成
      Promise.resolve(p).then(res => {
        // 将结果存入对应位置,保证输出顺序与输入顺序一致
        result[index] = res
        // 完成计数加1
        count++

        // 当所有 Promise 都完成时
        if (count === promises.length) {
          // 返回结果数组
          resolve(result)
        }
      }).catch( err=>{
           reject(err)
      }
     ) // 任何一个 Promise 失败,立即 reject
    })
  })
}

3.2 实现Promise.race

JavaScript
/**
 * 实现 Promise.race 方法
 * 接收一个 Promise 数组,返回一个新的 Promise
 * 哪个 Promise 最先改变状态(成功或失败),就采用它的结果
 * @param {Array} promises Promise 数组
 * @returns {Promise} 新的 Promise 对象
 */
Promise.myRace = function (promises) {
  // 返回一个新的 Promise 实例
  return new Promise((resolve, reject) => {
    // 遍历传入的 Promise 数组
    promises.forEach(p => {
      // 使用 Promise.resolve 确保传入的值可以被正确处理
      // 如果是普通值,会直接 resolve;如果是 Promise,则等待其完成
      Promise.resolve(p)
        .then(res => {
         resolve(res)
         )  // 任何一个 Promise 成功,就用它的结果 resolve
        .catch(err=>{
           reject(err)
      })  // 任何一个 Promise 失败,就用它的原因 reject
    })
    // 注意:这里没有使用计数器,因为 race 只关心第一个完成的结果
    // 后续完成的 Promise 会被忽略(因为 Promise 状态一旦改变就不能再变)
  })
}

3.3 实现Promise.Any

JavaScript
Promise.myAny = function(promises) {
  return new Promise((resolve, reject) => {
    const errors = [];      // 存储所有错误信息
    let count = 0;          // 记录失败的 Promise 数量

    promises.forEach((p, index) => {
      // 将每个值转为 Promise
      Promise.resolve(p)
        .then(resolve)      // 任何一个成功,直接 resolve
        .catch(err => {     // 失败时收集错误
          errors[index] = err;    // 记录错误,保持顺序
          count++;                // 失败计数加1
          // 如果所有 Promise 都失败了,才 reject
          if (count === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  });
};

3.4 Promise.myAllSettled

JavaScript
Promise.myAllSettled = function(promises) {
  return new Promise(resolve => {
    const results = [];   // 存储所有结果
    let count = 0;        // 记录已完成的 Promise 数量

    promises.forEach((p, index) => {
      Promise.resolve(p)
        .then(value => {
          results[index] = { status: 'fulfilled', value };
          count++;
          if (count === promises.length) resolve(results);
        })
        .catch(reason => {
          results[index] = { status: 'rejected', reason };
          count++;
          if (count === promises.length) resolve(results);
        });
    });
  });
};

4.深拷贝:实现支持对象、数组、循环引用的深拷贝函数。

JavaScript
/**
 * 深拷贝函数
 * 递归地拷贝一个对象的所有属性,处理循环引用问题
 * @param {*} obj 要拷贝的对象
 * @param {WeakMap} map 用于存储已拷贝对象的映射表,防止循环引用
 * @returns {*} 拷贝后的新对象
 */
function deepClone(obj, map = new WeakMap()) {
  // 1. 基础类型处理:如果是 null 或不是对象,直接返回
  // 这包括:string、number、boolean、null、undefined、symbol、function 等
  if (obj === null || typeof obj !== 'object') return obj

  if(obj instanceof RegExp) return new RegExp(obj)
  if(obj instanceof Date) return new Date(obj)

  // 2. 循环引用检查:如果已经拷贝过这个对象,直接返回之前拷贝的结果
  // 防止无限递归,例如:obj.self = obj
  if (map.has(obj)) return map.get(obj)

  // 3. 创建新对象:根据原对象是数组还是普通对象来创建
  const clone = Array.isArray(obj) ? [] : {}

  // 4. 将原对象和克隆对象的对应关系存入 map
  // 必须在递归前存储,这样后续遇到循环引用时才能返回
  map.set(obj, clone)

  // 5. 遍历原对象的所有可枚举属性
  // 注意:这里使用 for...in 会遍历原型链上的可枚举属性
  for (let key in obj) {
    // 只拷贝对象自身的属性,不拷贝原型链上的属性
    if (obj.hasOwnProperty(key)) {
      // 递归拷贝每个属性的值
      clone[key] = deepClone(obj[key], map)
    }
  }

  // 6. 返回克隆后的新对象
  return clone
}

JSON实现深拷贝的局限性

Image

循环引用问题

Image

WeakMap解释

WeakMap 是 JavaScript 中的一种特殊的数据结构,它的键必须是对象,并且是弱引用的。

弱引用的特点:

  • 不会阻止垃圾回收

  • 如果一个对象只有 WeakMap 在引用它,这个对象可以被垃圾回收

  • 当对象被回收后,它在 WeakMap 中的键值对会自动被移除

注意:此处WeakMap可以直接先用Map,如果问到优化再说WeakMap,别给自己挖坑。
用 Map 会导致被拷贝的对象永远无法被垃圾回收 ,造成内存泄漏

5. 实现 sleep 函数

因为setTimeout是通过回调函数来实现定时任务的,所以在多任务的场景下就会出现回调嵌套:

JavaScript
console.time('runTime:');
setTimeout(() => {
    console.log('1');
    setTimeout(() => {
        console.log('2')
        setTimeout(() => {
            console.log('3')
            console.timeEnd('runTime:');
        }, 2000);
    }, 3000);
}, 2000);
// 结果:1 2 3 runTime:: 7017.87890625 ms

上面的方式存在回调嵌套的问题,我们希望可以利用sleep函数更方便优雅地实现上面的例子。

HTML
<!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>
    <script>
      //使程序暂停一定时间,起到延时的效果
      function sleep(time) {
        return new Promise((resolve) =>
            setTimeout(() => {
                resolve();
            }, time));
      }

      async function run() {
        console.time("runTime:");
        console.log("1");
        await sleep(2000);
        console.log("2");
        await sleep(1000);
        console.log("3");
        console.timeEnd("runTime:");
      }

      run();
      console.log("a");

      // 1
      // a
      // 2
      // 3
      // runTime:: 3009.984ms
    </script>
  </body>
</html>

6. setTimeout 实现 setInterval

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能实现间隔一段时间执行的效果。

举个例子:

  • 假设 delay 是 1000ms,但 callback 执行时间很长(比如 2000ms)。

  • 第一次 callback 执行时,第二次 callback 已经在 1000ms 时被加入队列了。

  • 等到第一次执行完(2000ms 时),发现队列里已经堆积了后续的 callback 事件,它们会连续执行(每隔 0ms 就执行一个),而不是真正的“每隔 1000ms 执行一次”。

结果就是:执行时像连续触发,失去了间隔控制。

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

为什么能解决?

因为 setTimeout 只在 上一次 callback 执行完成后 才开始计时 delay 毫秒,再安排下一次执行。

  • 第一次执行:等待 delay 毫秒 → 执行 callbackcallback 执行完后才开始下一个 setTimeout

  • 如果 callback 耗时 2000ms,那么下一次的定时器是从这个 2000ms 结束点再等 delay 毫秒才触发。

  • 两次 callback 实际执行的间隔 = callback 运行时间 + delay,大于等于 delay,永远不会出现堆积连续运行的情况。

实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果

JavaScript
function mySetinterval(fn,delay,...args){
            let timer = null
            const settimeout = () =>{
                timer = setTimeout(() => {
                    fn(...args)
                    settimeout()
                },delay)
            }
            settimeout()
            return function clear(){
                clearTimeout(timer)
            }
 }

const fn = () => console.log(Date.now())
const clear = mySetinterval(fn,1000)
setTimeout(() => {
            clear()
    },10000)

7. 异步循环打印 1,2,3

JavaScript
async function printAsync() {
  console.log(1);
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log(2);
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log(3);
}

printAsync();
// 输出:1 (等待1秒) 2 (等待1秒) 3

function print(count = 1) {
    console.log(count);
    if (count < 3) {
        setTimeout(() => print(count + 1), 1000);
    }
}
print();

8. 循环打印红、黄、绿

JavaScript
function printcolor(color, delay) {
        return new Promise((resolve) => {
          setTimeout(() => {
            console.log(color);
            resolve();
          }, delay);
        });
      }
async function printcolors() {
        while (true) {
          await printcolor("red", 1000);
          await printcolor("yellow", 2000);
          await printcolor("green", 3000);
        }
      }
printcolors()

9.数组扁平化:用递归或reduce实现多层级数组扁平化。

Image

JavaScript
/**
 * 数组扁平化函数 - forEach版本(支持depth深度控制)
 * 将多维数组按指定深度扁平化
 * @param {Array} arr - 需要扁平化的多维数组
 * @param {number} depth - 扁平化深度,默认为1,Infinity表示无限深度
 * @returns {Array} - 扁平化后的数组
 */
function flatten(arr, depth = 1) {
  // 如果深度为0或负数,直接返回原数组的浅拷贝
  if (depth <= 0) return [...arr]

  const result = []

  arr.forEach(item => {
    if (Array.isArray(item) && depth > 0) {
      // 递归扁平化子数组,深度减1
      // 使用扩展运算符将递归结果展开后添加到result中
      result.push(...flatten(item, depth - 1))
    } else {
      // 如果当前元素不是数组,或已达到指定深度,直接添加
      result.push(item)
    }
  })

  return result
}

/**
 * 数组扁平化函数 - reduce版本(支持depth深度控制)
 * 使用reduce方法实现更简洁的数组扁平化,支持指定深度
 * @param {Array} arr - 需要扁平化的多维数组
 * @param {number} depth - 扁平化深度,默认为1,Infinity表示无限深度
 * @returns {Array} - 扁平化后的数组
 */
function flatten(arr, depth = 1) {
  // 如果深度为0或负数,直接返回原数组的浅拷贝
  if (depth <= 0) return [...arr]

  return arr.reduce((acc, cur) => {
    return acc.concat(
      // 如果当前元素是数组且还有深度剩余,递归扁平化(深度减1)
      // 否则直接使用当前元素
      Array.isArray(cur) && depth > 0
        ? flatten(cur, depth - 1)
        : cur
    )
  }, [])
}
JavaScript
// 先转成字符串,再变回数组
function flat4(array){
    //[1,[2,3]].toString()  =>  1,2,3
    return array.toString().split(',').map(item => parseInt(item))
}

function flat5(array){
    return array.join(',').split(',').map(item => Number(item))
}

10.函数柯里化:实现 add(1)(2)(3) 的累加效果,理解闭包与参数收集。

1. 通俗理解:什么是柯里化?

Image

你可以把普通函数想象成全款买房

你要买套房(执行 sum(x, y, z)),你必须一次性凑齐首付 x、贷款 y、税费 z,把这笔钱一次性交出去,交易才能完成。

sum(10, 20, 30) 👉 一起交钱,拿房。

把这个过程柯里化,就变成了分期付款(或者攒首付):

你现在只有 10 万(只传一个参数 10),开发商说不行,钱不够不能给你房。但是他给你开了一张收据(返回了一个新函数)。

等你又攒了 20 万(传入 20),你拿着之前的收据去,开发商还是说不够,于是又给你换了一张新的收据(再返回一个新函数,并记住了你一共交了 30 万)。

最后你又拿了 30 万(传入 30),开发商一看,10+20+30=60,总共需要的钱凑齐了!于是当场交房(真正执行了原函数计算结果)。

总结一下概念

柯里化就是一个“收集参数”的过程。只要参数没凑齐,它就一直返回新函数让你继续传参;一旦参数凑齐了,它就把之前收集到的所有参数一把塞给原函数去执行。

2. 代码

柯里化是把一个接收多个参数的函数,转换成一系列接收单个参数的函数的过程。

函数名.length 代表什么?当你定义一个函数时,它的 .length 属性会自动记录这个函数在定义时声明的参数个数(即形参的个数)。

JavaScript
function sum(x,y,z) {
    return x + y + z
}

function myCurrying(fn) {
    // 判断当前已经接收的参数的个数,和函数本身需要接收的参数是否一致
    function curried(...args) {
        // 1.当已经传入的参数 大于等于 需要的参数时,就执行函数
        if(args.length >= fn.length){
            // 如果调用函数时指定了this,要将其绑定上去
            return fn.apply(this, args)
        }
        else{
            // 没有达到个数时,需要返回一个新的函数,继续来接收参数
            return function(...args2) {
                //return curried.apply(this, [...args, ...args2])
                // 接收到参数后,需要递归调用 curried 来检查函数的个数是否达到
                return curried.apply(this, args.concat(args2))
            }
        }
    }
    return curried
}

var curryAdd = myCurrying(sum)

curryAdd(10,20,30)
curryAdd(10,20)(30)
curryAdd(10)(20)(30)

函数的调用顺序是:
myCurrying(sum)

curried(10)

匿名函数(20)

匿名函数(30)

curried(10,20,30)

sum(...)

核心逻辑就是:比较 “已经收到的参数个数” 和 “原函数需要的参数个数”

curryAdd(10)(20)(30) 的调用过程:

初始状态

当我们执行 var curryAdd = myCurrying(sum) 时,sum.length 是 3(它需要3个参数)。

返回了内部的 curried 函数。所以此时 curryAdd 就等于 curried

第一步:传入 10 👉 curryAdd(10)

  1. 进入 curried 函数,此时参数 args = [10]

  2. 判断 args.length (1个) >= fn.length (3个) 吗?不成立。

  3. 走进 else返回了一个匿名函数。你可以把这个匿名函数看作是“带有记忆的收据”,它利用闭包记住了当前的 args = [10]

第二步:传入 20 👉 匿名函数(20)

  1. 上一步返回了匿名函数,紧接着后面有个 (20),说明马上调用了这个匿名函数。

  2. 进入匿名函数,此时新传入的参数 args2 = [20]

  3. 执行 curried.apply(this, args.concat(args2))。这是什么意思?

就是把之前记住的 [10] 和新传入的 [20] 拼起来,变成了 [10, 20],然后再次调用 curried 函数去检查。

  1. 再次进入 curried,此时参数 args = [10, 20]

  2. 判断长度 >= fn.length 吗? 2 >= 3 不成立。

  3. 再次走进 else,由于还没凑齐,又返回了一个新的匿名函数,并记住了现在的状态是 [10, 20]

第三步:传入 30 👉 新匿名函数(30)

  1. 接着调用新匿名函数,传入 (30)

  2. 进入匿名函数,新参数 args2 = [30]

  3. 再次拼接之前记住的参数:[10, 20] 加上 [30],变成 [10, 20, 30],再次调用 curried

  4. 第三次进入 curried,此时参数 args = [10, 20, 30]

  5. 判断 args.length (3个) >= fn.length (3个) 吗?成立!

  6. 终于凑齐了!走进 if:直接执行 return fn.apply(this, args)

  7. 也就是执行了 sum(10, 20, 30),算出了 60。整个过程结束。


3. 为什么代码里还要拼接 argsargs2

为什么 curryAdd(10, 20)(30) 也是可以工作的?

如果一开始传了 10, 20,那么第一次调用时 args 就是 [10, 20]

返回匿名函数后,再传 (30) 也就是 args2 = [30]

拼接时 args.concat(args2) 就直接变成了 [10, 20, 30]。再传给 curried 检查时,顺利凑齐,执行 sum

所以 ...args2 这个匿名函数的唯一使命就是:接收新来的参数,把它和旧的参数拼到一起,然后交给 curried 这个黑脸长官去检查"人够了没"。够了就干活(if),不够就继续等下一批(else 返回新函数)。

11. 手写instanceof

instanceof 的核心作用是:判断某个对象是否是某个构造函数创建的实例

比如 [] instanceof Array[] 是不是构造函数Array实例化出来的。

它本质上做的事情只有一件:

看构造函数的 prototype 是否存在于对象的原型链上。

也就是说,从实例化出来的对象出发,查找原型链,看看能不能找到构造函数的prototype,因为如果是这个构造函数实例化出来的,这个对象的原型链上就会有这个构造函数的prototye。

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
          const _instanceof = (target, Fn) => {
            //先得到实例对象的__proto__
            let __proto__ = Object.getPrototypeOf(target)
            //得到构造函数的原型prototype
            let prototype = Fn.prototype
            while(true){
                if(__proto__ == null) return false //原型链一直到末尾null都没找到
                if(__proto__ == prototype) return true //匹配成果
                //沿着原型链往上找,更新__proto__
                __proto__ = Object.getPrototypeOf(__proto__)
            }

}
    </script>
</body>

</html>

12. 手写new操作符

在 JavaScript 中,new 做了 4 件事

  1. 创建一个新的空对象
  2. 让这个对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数,并把 this 指向新对象
  4. 如果构造函数返回对象,则返回该对象,否则返回新对象

JS 的 new 有特殊规则:如果构造函数返回 对象 → 返回这个对象;返回 基本类型 → 忽略,返回实例。

JavaScript
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>


        <script type="text/javascript">
            const _new = function(con, ...args) {
                //new出来的是一个新的对象,所以先定义一个空对象
                let obj = {}
                //指定新对象的__proto__指向构造函数的原型
                obj.__proto__ = con.prototype
                //Object.setPrototypeOf(obj, con.prototype)
                // 调用构造函数,修改this指向为新对象,给新对象加入属性和方法
                //用result接收构造函数的返回值
                let result = con.apply(obj,args)
                //规则:如果构造函数有返回值,那么new返回这个返回值,否则返回新对象
                return result instanceof Object ? result : obj
            }
        </script>
    </body>
</html>

13. 手写 call、apply、bind 函数

call(thisArg, ...args)

call 的本质:把函数变成对象的方法调用。

核心步骤:

  1. 创建临时属性
  2. 挂到对象上
  3. 通过对象调用函数
  4. 删除临时属性

伪代码:

JavaScript
obj.fn = fn
obj.fn()
delete obj.fn

牛客已pass

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        //函数本质也是对象,基础自Function.prototype
        Function.prototype._call = function(thisArgs,...args){
        //先获取要执行的函数,谁调用call,此时this就是谁
        let func = this
        //thisArgs决定函数调用时,this指向谁
        //规则是null/undefined this指向全局对象 基本数据类型要转object
        thisArgs = thisArgs ? Object(thisArgs) : window
        //使用symbol创建独一无二的key,防止和原有普通属性冲突
        let fn = Symbol()
        thisArgs[fn] = this
        //接收返回值
        let result = thisArgs[fn](...args)
        //最后删除属性值
        delete thisArgs[fn]
        //返回最终结果
        return result
        }
    </script>
</body>

</html>

apply(thisArg, argsArray)

JavaScript
Function.prototype._apply = function(thisArg,argArray){
    // 1.获取需要执行的函数
    let fn = this

    // 2.将 thisArg 转成对象类型(防止它传入的是非对象类型,例如123数字)
    thisArg = thisArg ? Object(thisArg) : window
    // 判断一些边界情况
    argArray = argArray || []

    // 3.使用 thisArg 调用函数,绑定 this
    //thisArg.fn = fn 使用symbol,防止属性覆盖
       const key = Symbol()
       thisArg[key] = fn
    // 将传递过来的数组(可迭代对象)拆分,传给函数
    let result = thisArg[key](...argArray)
    delete thisArg[key]

    // 4.返回结果
    return result
}

bind(thisArg, ...args)

JavaScript
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            // 补全代码
            Function.prototype._bind = function(target, ...arguments1) {
                //保存调用bind的函数,以便后续调用
                let fn = this
                //返回一个新函数
                return function(){
                    //用 call 执行原函数,并把 this 指向 target。
                    return fn.call(target,...arguments1)
                }
}
        </script>
    </body>
</html>

14. 手写Ajax请求

整个过程:

  1. 创建 Promise
  2. 创建 XMLHttpRequest 对象
  3. 配置请求(open)
  4. 监听请求状态变化(onreadystatechange)
  5. 发送请求(send)
  6. 请求成功 → resolve
  7. 请求失败 → reject
JavaScript
function myAjax(url,method = 'GET') {
        //创建一个promise,因为ajax是异步的,函数无法直接return结果,promise可以在未来返回结果
        return new Promise((resolve, reject) => {
          //创建xhr对象
          const xhr = new XMLHttpRequest();
          //初始化请求,设置请求方式,url和异步
          xhr.open(method, url, true);
          //监听请求的状态变化
          xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
              if (xhr.status === 200) {
                resolve(JSON.parse(xhr.responseText));
              } else {
                reject(xhr.status);
              }
            }
          };
          // 必须发送请求
          xhr.send(null);
        });
      }

15. 手写数组去重

必会标绿

JavaScript
      // 原地去重:方法一 (双层 for 循环 + splice, O(n^2) 时间, O(1) 空间)
      const uniqueInPlaceSplice = (arr) => {
        for(let i = 0; i < arr.length; i++) {
            for(let j = i + 1; j < arr.length; j++) {
                if(arr[i] === arr[j]) {
                    arr.splice(j, 1);
                    j--; // 因为数组长度减少了,j 需要后退一步,否则会漏掉相邻的重复项
                }
            }
        }
        return arr; // 返回原数组的引用
      }



// 1.Set + 数组复制
fuction unique1(array){
    // Array.from(),对一个可迭代对象进行浅拷贝
    return Array.from(new Set(array))
}

// 2.Set + 扩展运算符浅拷贝
function unique2(array){
    // ... 扩展运算符
    return [...new Set(array)]
}

// 3.filter,判断是不是首次出现,如果不是就过滤掉
function unique3(array){
    return array.filter((item,index) => {
        return array.indexOf(item) === index
        //indexOf()返回 **这个元素第一次出现的位置**
    })
}

// 4.创建一个新数组,如果之前没加入就加入
function unique4(array){
    let res = []
    array.forEach(item => {
        if(!res.includes(item)){
            res.push(item)
        }
    })
    return res
}

进阶:如果数组内有数组和对象,应该怎么去重(此时对象的地址不同,用Set去不了重)

JavaScript
function unique(arr){
  const map = new Map()
  return arr.filter(item=>{
    const key = JSON.stringify(item)
    if(map.has(key)) return false
    map.set(key,true)
    return true
  })
}

16. 手写数组乱序

HTML
<!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>
    <script>
        function randomArray(arr){
            // return arr.sort(() => Math.random() - 0.5)
            // 洗牌算法,从后往前遍历,依次确定当前遍历i位置的值
            for(let i = arr.length -1; i >0 ; i--){
                const j = Math.floor(Math.random() * (i +1)) ; //注意这个分号
                //random 范围是【0,1) 取i+1 再向下取整,是为了让i这个值也可以被选到
                [arr[i],arr[j]] = [arr[j], arr[i]] //直接交换,时间复杂度o(1)
            }
            return arr
        }

        console.log(randomArray([1,2,3,4,5,6,7,8,9]))
    </script>
</body>
</html>

真正的问题是 **JavaScript 自动分号插入(ASI)**。
如果前一行被解释器误解析,可能会导致下一行在解析时出现问题。
 解决方法很简单:**在解构赋值前加分号。**
正确写法
function randomArray(arr){
    for(let i = arr.length -1; i >0 ; i--){
        const j = Math.floor(Math.random() * (i +1));
        ;[arr[i],arr[j]] = [arr[j], arr[i]]
    }
    return arr
}
关键是这一行:
;[arr[i],arr[j]] = [arr[j], arr[i]]
前面的 ; 是为了避免 JS 把它和上一行连在一起解析。



// 方法1: sort + Math.random()
function shuffle1(arr){
    return arr.sort(() => Math.random() - 0.5);//
}

// 方法2:时间复杂度 O(n^2)
// 随机拿出一个数(并在原数组中删除),放到新数组中
function randomSortArray(arr) {
    let backArr = [];
    while (arr.length) {
        let index = parseInt(Math.random() * arr.length);
        backArr.push(arr[index]);
        arr.splice(index, 1);
    }
    return backArr;
}

// 方法3:时间复杂度 O(n)
// 随机选一个放在最后,交换
function randomSortArray2(arr) {
    let lenNum = arr.length - 1;
    for (let i = 0; i < lenNum; i++) {
        let index = parseInt(Math.random() * (lenNum + 1 - i));
        [a[index],a[lenNum - i]] = [a[lenNum - i],a[index]]
    }
    return arr;
}

17. 手撕快排

PS: 常见的排序算法,像冒泡,选择,插入排序这些最好也背一下,堆排序归并排序能写则写。万一考到了呢,要是写不出就直接回去等通知了

HTML
快速排序是一种基于分治思想的排序算法。
首先选择一个基准元素,然后通过分区操作,使得小于基准的元素放在左边,大于基准的元素放在右边。
接着递归地对左右两个子数组继续进行快速排序,直到数组长度为1。
平均时间复杂度是 O(n log n),最坏是 O(n²)。

选一个基准值(pivot),把数组分成
 左边:比它小
 右边:比它大
 然后递归排序左右两边。

先把基准值保存下来,相当于留下一个坑,然后从右边找到小于基准的元素填到左坑,再从左边找到大于基准的元素填到右坑,不断重复,直到左右指针相遇,最后把基准值填回去,这样就完成了分区。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
         const quicksort = (arr, start = 0, end = arr.length - 1) => {
            if(start >= end) return arr // 务必注意是大于等于,返回 arr 方便第一次调用的返回值打印
            let left = start
            let right = end
            const base = arr[left]
            //务必注意外层还有一个循环
           while(left<right){
             //从右往左
            while(left < right && arr[right] >= base) right--
            arr[left] = arr[right] //base已保存,不用担心覆盖
            //从左往右
            while(left < right && arr[left] <= base) left++
            arr[right] = arr[left]
           }
            arr[left] = base

            quicksort(arr,start,left-1)
            quicksort(arr,left+1,end)

            return arr
        }
        console.log(quicksort([1,6,4,5,37,89,0]))

    </script>
</body>

</html>

18. 手写 JSONP

JSONP原理:

  1. 利用script标签可以跨域加载JS的特点
  2. 前端创建script请求接口
  3. 传递callback函数名
  4. 服务器返回 callback(data)
  5. 浏览器执行函数并拿到数据

特点:只支持GET,通过函数调用传递数据。

JavaScript
// 动态的加载js文件
function addScript(src) {
  const script = document.createElement('script');
  script.src = src;
  script.type = "text/javascript";
  document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 设置一个全局的callback函数来接收回调结果
function handleRes(res) {
  console.log(res);
}

// 接口返回的数据格式,加载完js脚本后会自动执行回调函数
handleRes({a: 1, b: 2});

19. 手写寄生组合继承

HTML
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            // 补全代码
            function Human(name) {
                this.name = name
                this.kingdom = 'animal'
                this.color = ['yellow', 'white', 'brown', 'black']
            }

            //给human原型加getname方法
            Human.prototype.getName = function(){
                return this.name
            }

            function Chinese(name,age) {
               // 构造函数继承父类实例方法和属性
               Human.call(this,name)
               //添加age属性
               this.age = age
                this.color = 'yellow'
            }

            //寄生组合继承,创建一个空对象并指定原型指向 这个新对象的 constructor 默认指向 Human
            Chinese.prototype = Object.create(Human.prototype)
            //每个函数的 原型对象(prototype)默认都有一个 constructor 属性,指向它自身:
            Chinese.prototype.constructor = Chinese

            //注意要在这里才添加子类方法,不然会被上面的继承覆盖
            Chinese.prototype.getAge = function(){
                return this.age
            }
        </script>
    </body>
</html>

20. 手写浅拷贝

HTML
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            const _shallowClone = target => {
                // 补全代码
                //先判断边界,如果不是对象直接返回,注意null类型是对象
                if(typeof target !== 'object' || target == null){
                    return target
                }
                //如果是数组,要开一个新数组
                const newObj = Array.isArray(target) ? [] : {}
                //得到target的每一个属性
                const keys = Object.getOwnPropertyNames(target)
                const symbols = Object.getOwnPropertySymbols(target);
                //对属性进行遍历复制到新对象里
                [...keys,...symbols].forEach(key => {
                    newObj[key] = target[key]
                })
                return newObj
            }
        </script>
    </body>
</html>

21. 数组/字符串操作题

可以自己找些基础的练一下,就不一一列举了

本人没练过这个

22. 手写二分查找

(这个感觉跟力扣差不多,本人没看过

JavaScript
//  迭代版
function search(nums, target) {
  // write code here
    if(nums.length === 0)    return -1
    let left = 0,right = nums.length - 1
        // 注意这里的边界,有等号
    while(left <= right){
        let mid = Math.floor((left + right) / 2)
        if(nums[mid] < target)    left = mid + 1
        else if(nums[mid] > target)    right = mid - 1
        else    return mid
    }
    return -1
}
// 递归版
function binary_search(arr, low, high, key) {
    if (low > high) {
        return -1;
    }
    var mid = parseInt((high + low) / 2);
    if (arr[mid] == key) {
        return mid;
    } else if (arr[mid] > key) {
        high = mid - 1;
        return binary_search(arr, low, high, key);
    } else if (arr[mid] < key) {
        low = mid + 1;
        return binary_search(arr, low, high, key);
    }
};

23. CSS水平垂直居中

flex:

css
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

grid:

css
.container {
  display: grid;
  place-items: center;
}

margin(只能水平居中):

css
.item {
  margin: 0 auto;
}

absolute+transform:

css
.parent {
  position: relative;
}
.son {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

文字居中:

css
.text {
  height: 100px;
  line-height: 100px;
  text-align: center;
}

vertical-align 常用值:top / middle / bottom / baseline。若想用 vertical-align: middle 实现单行文字垂直居中,需配合 line-height 等于容器高度,且元素为 inline 或 inline-block。

24. CSS画三角形

css
.div {
  width: 0px;
  height: 0px;
  border: 5px solid transparent;
  border-top: 5px solid red; /* 这里记得加宽度和实线 */
}

25. CSS实现两栏和三栏布局

flex,grid,圣杯,双飞翼

26. 手写发布-订阅模式 || Event Bus

HTML
<!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>
    <script>
        class EventEmitter{
            constructor(){
                this.events = {}
            }

            //订阅事件
            on(eventName,fn){
                if(!this.events[eventName]){
                     this.events[eventName] = []
                }
                this.events[eventName].push(fn)
            }

            //发布事件
            emit(eventName,...args){
                const fns = this.events[eventName]
                if(!fns) return
                // 【满分细节1】浅拷贝数组,防止在遍历过程中被 once 或 off 修改导致跳过后面的回调
                const fnsToTigger = [...fns];
                fnsToTigger.forEach(fn => {
                    fn.apply(this,args)
                });
            }
            //取消订阅
            off(eventName,fn){
                const fns = this.events[eventName]
                if(!fns) return
                // 【满分细节2】兼容 once 的解绑(识别 originalFn)
                const index = fns.findIndex(item => item === fn || item.originalFn === fn)
                // 【满分细节3】找不到该函数时不误删(indexOf为-1时splice会删掉最后一位)
                if(index !== -1){
                    fns.splice(index,1)
                }
            }
            //一次订阅
            once(eventName,fn){
                const wrapper = (...args) => {
                    fn.apply(this,args)
                    this.off(eventName, wrapper)
                }
                // 【满分细节2配套】将旧函数挂在 wrapper 上
                wrapper.originalFn = fn
                this.on(eventName,wrapper)
            }
        }
    </script>
</body>
</html>

简单版

HTML
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            class EventEmitter {
                // 补全代码
                constructor(){
                    //初始化对象,用于存储事件
                    this.events = {}
                }
                //订阅事件
                on(eventName,fn){
                    if(!this.events[eventName]){
                        //如果事件中心对于某事件没有,那么开辟新数组进行存储
                        this.events[eventName] = []
                    }
                    this.events[eventName].push(fn)
                }
                //发布事件
                emit(eventName,...args){
                    const fns = this.events[eventName]
                    if(!fns) return
                    //遍历每一个订阅者,进行函数调用
                    fns.forEach(fn =>{
                        fn(...args)
                    })
                }
            }
        </script>
    </body>
</html>

中频

1. 手写事件委托(牛客⭐)

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <ul>
            <li>.</li>
            <li>.</li>
            <li>.</li>
        </ul>
    <!-- 填写标签 -->
    <script type="text/javascript">
       // 事件委托就是利用事件冒泡,把子元素的事件交给父元素统一处理,通过 event.target 判断触发事件的具体元素。
        // 填写JavaScript
        document.querySelector('ul').onclick = event => {
            // 获取真正被点击的元素
        const target = event.target

        // 判断是不是li
        if(target.tagName === 'LI'){
            target.innerHTML += '.'
        }

}
    </script>
</body>

</html>

2. DOM操作相关题目

(这个不知道是啥

3. 手写对象扁平化

JavaScript
<!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>
    <script>
      /**
       * 对象扁平化(使用 Object.keys)
       * @param {Object} obj 需要扁平化的对象
       * @param {String} prefix 前缀
       * @param {Object} result 结果对象
       * @returns {Object} 扁平化后的对象
       */
      function flattenObject(obj, prefix = "", result = {}) {
        // 处理基本类型和 null
        if (obj === null || typeof obj !== "object") {
          if (prefix) {
            result[prefix] = obj;
          }
          return result;
        }

        // 使用 Object.keys 遍历对象自身可枚举属性
        Object.keys(obj).forEach((key) => {
          const newKey = prefix ? `${prefix}.${key}` : key;
          const value = obj[key];

          if (value !== null && typeof value === "object") {
            flattenObject(value, newKey, result);
          } else {
            result[newKey] = value;
          }
        });

        return result;
      }

      const nestedObject = {
        name: "张三",
        age: 25,
        address: {
          province: "广东省",
          city: "深圳市",
          district: "南山区",
          detail: {
            street: "科技园路",
            number: 100,
          },
        },
        hobbies: ["阅读", "编程", "运动"],
        isActive: true,
      };

      const flattened = flattenObject(nestedObject);
      console.log(flattened);
    </script>
  </body>
</html>

4. 手写数组常见方法 Array.filter/map/fill/reduce

Array.map(牛客⭐)

遍历数组,对每个元素执行回调函数 Fn,把回调函数的返回值放入新数组,返回新数组。

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        //
        Array.prototype._map = function(Fn) {
            const res = [] //map最终要返回一个新数组
            //遍历数组每一项,执行回调函数
            //this执行调用map的数组
            for(let i = 0; i<this.length; i++){
                //将回调函数对每一项执行的结果加入新数组,注意map回调有三个参数
                res.push(Fn(this[i],i,this))
            }
            return res
}

    </script>
</body>

</html>

Array.filter(牛客⭐)

遍历数组,把每一项传给回调函数 Fn,如果返回 true 就把该元素放进新数组。

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
         Array.prototype._filter = function(Fn) {
            const res = []   // 存放结果

            for(let i = 0; i < this.length; i++){
                if(Fn(this[i], i, this)){  // 执行回调
                    res.push(this[i])      // 满足条件加入新数组
                }
            }

            return res
        }

    </script>
</body>

</html>

Array.reduce(牛客⭐)

HTML
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
        Array.prototype._reduce = function(fn,initialvalue){
            let arr = this //调用reduce的数组
            let acc, startIndex
            //判断初始值,这决定cur从数组哪一项开始
            if(initialvalue !== undefined){
                acc = initialvalue // 初始累计值为传入初始值
                startIndex = 0
            }else{
                acc = arr[0]  //如果没有初始值,那么累加值从数组第一项开始
                startIndex = 1 //注意
            }
            for(let i = startIndex; i<arr.length; i++){
               // 把这次计算的结果重新赋值给 acc,作为下一次循环的累计值。
                acc = fn(acc, arr[i], i, arr)
            }
            return acc
        }
    </script>
</body>

</html>

5. 手写列表转树

JavaScript
<!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>
    <script>
      function listToTree(list, parentId = null) {
        const map = new Map();
        const tree = [];

        // 建立 id -> node 映射
        for (const item of list) {
          map.set(item.id, { ...item, children: [] });
        }

        // 构建树结构
        for (const item of list) {
          const node = map.get(item.id);
          if (item.parentId === parentId) {
            tree.push(node);
          } else {
            const parent = map.get(item.parentId);
            parent?.children.push(node);
          }
        }

        return tree;
      }
      const list = [
        { id: 1, name: "部门A", parentId: null },
        { id: 2, name: "部门B", parentId: 1 },
        { id: 3, name: "部门C", parentId: 1 },
        { id: 4, name: "部门D", parentId: 2 },
        { id: 5, name: "部门E", parentId: 3 },
      ];

      console.log(listToTree(list));
    </script>
  </body>
</html>

⭐ 日期时间格式化

https://juejin.cn/post/7238199999732711481

HTML
<!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>
    <script>
        let date = new Date()
        function formatDate(date,format){
            const year = date.getFullYear()
            const month = String(date.getMonth()+1).padStart(2,'0')
            const day = String(date.getDate()).padStart(2,'0')
            const hour = String(date.getHours()).padStart(2,'0')
            const minute = String(date.getMinutes()).padStart(2,'0')
            const second = String(date.getSeconds()).padStart(2,'0')

            return format.replace('YYYY',year) //用第二个参数的结果替换第一个参数
                         .replace('MM',month)
                         .replace('DD',day)
                         .replace('hh',hour)
                         .replace('mm',minute)
                         .replace('ss',second)
        }
        console.log(formatDate(date,'YYYY-MM-DD hh:mm:ss'))
        console.log(formatDate(date,'YYYY/MM/DD'))
        console.log(formatDate(date,'DD-MM-YYYY'))
    </script>
</body>
</html>

7. 数字千分位

JavaScript
<!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>
    <script>
      function formatNumber(num) {
        // 第1步:将数字转成字符串,方便遍历
        // 例如:1234567.89 -> "1234567.89"
        const str = num.toString();

        // 第2步:用小数点分割,得到整数部分和小数部分
        // 例如:["1234567", "89"]
        const [integer, decimal] = str.split(".");

        // 第3步:判断是否为负数(整数部分是否以"-"开头)
        // 例如:-1234567 -> integer = "-1234567"
        const isNegative = integer.startsWith("-");

        // 第4步:如果是负数,去掉负号,只处理数字部分
        // 例如:-1234567 -> digits = "1234567"
        let digits = isNegative ? integer.slice(1) : integer;

        // 第5步:result 用来存放最终结果,count 用来计数(每3位加逗号)
        let result = "";
        let count = 0;

        // 第6步:从右向左遍历数字字符串(关键!)
        // 例如:"1234567" 从 '7' 开始往前遍历
        for (let i = digits.length - 1; i >= 0; i--) {
          count++; // 计数器+1,表示已经处理了1位数字

          // 把当前数字放到 result 的前面(因为是从右往左遍历)
          // 例如:第一次循环 digits[i]='7', result='7'
          result = digits[i] + result;

          // 每3位加一个逗号,但注意:
          // 1. count % 3 === 0:已经处理了3的倍数位
          // 2. i !== 0:不是最后一位(避免在数字最前面加逗号)
          if (count % 3 === 0 && i !== 0) {
            result = "," + result; // 在结果前面加逗号
          }
        }

        // 第7步:如果是负数,把负号拼回去
        result = (isNegative ? "-" : "") + result;

        // 第8步:如果有小数部分,就拼上小数点和小数
        return decimal ? `${result}.${decimal}` : result;
      }

      console.log(formatNumber(1234567)); // "1,234,567"
      console.log(formatNumber(-1234567.89)); // "-1,234,567.89"
      console.log(formatNumber(123)); // "123"
      console.log(formatNumber(1234)); // "1,234"
    </script>
  </body>
</html>

8. URL参数解析 合法的url

(这个本人不会)

Image

JavaScript
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
           // const _isUrl = url => {
                // 补全代码
               // const reg = /^https?:\/\/[^\s]+$/;
                //return reg.test(url);
              //  return url.startsWith('https')
           // }
           const _isUrl = url => {
    return /^((http|https):\/\/)?(([A-Za-z0-9]+-[A-Za-z0-9]+|[A-Za-z0-9]+)\.)+([A-Za-z]+)(:\d+)?(\/.*)?(\?.*)?(#.*)?$/.test(url)
}

        </script>
    </body>
</html>

9. 手写观察者模式(牛客⭐)

JavaScript
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            // 补全代码
            class Observerd {
                constructor(name){
                    this.name = name
                    this.state = '走路'
                    this.observers = []  //初始观察者为空
                }
                //保存观察者
                setObserver(observer){
                    this.observers.push(observer)
                }
                //修改状态并且通知观察者
                setState(state){
                    this.state = state
                    this.observers.forEach(observer =>{
                        observer.update(this)
                        //注意这里要传入this也就是被观察者给观察者
                    })
                }
            }

            class Observer {
                update(observerd){
                    console.log(`${observerd.name}正在${observerd.state}`)
                }
            }
        </script>
    </body>
</html>

10. 手写 Promise(进阶)

本人不会

低频

1. 手写组合函数

compose 把多个函数组合成一个,从右向左执行,前一个函数的返回值是后一个函数的参数。

JavaScript
<!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>
    <script>
      function compose(...fns) {
        return function (x) {
          let result = x;
          for (let i = fns.length - 1; i >= 0; i--) {
            result = fns[i](result);
          }
          return result;
        };
      }

      // 基础测试
      const add1 = (x) => x + 1;
      const double = (x) => x * 2;
      const square = (x) => x * x;

      const composed = compose(square, double, add1);
      console.log(composed(5)); // 144

      // 执行过程:
      // 1. add1(5) = 6
      // 2. double(6) = 12
      // 3. square(12) = 144
    </script>
  </body>
</html>

2. 手写Object.create()(牛客⭐)

Object.create(proto) 用来:

创建一个新对象,并指定它的原型。

实现 Object.create 本质三步:

  1. 创建空函数
  2. prototype 指向目标对象
  3. new 这个函数
JavaScript
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
        function _objectCreate(proto){
            //先创建一个空构造函数
            function F(){}
            //指定F的prototype为传进来的proto
            F.prototype = proto
            //最后new这个构造函数,此时相当于创建了一个新的对象,并且对象的原型是传进来的proto
             return new F()
        }
    </script>
</body>

</html>

3. 手写Object.is()

用来解决 === 的两个特殊问题:NaN 和 +0/-0。

  • NaN === NaN → false
  • +0 === -0 → true
JavaScript
function objectIs(x, y) {
  // 1. NaN 判断
  if (x !== x && y !== y) return true;

  // 2. +0 和 -0 判断
  if (x === 0 && y === 0) return 1 / x === 1 / y;

  // 3. 其他情况
  return x === y;
}

关键点:

  • NaN:利用 NaN 不等于自身的特性

  • +0/-0:利用 1/0 得到 Infinity 和 -Infinity 来区分

  • 其他情况直接用 ===

4. 手写Object.freeze()(牛客⭐)

JavaScript
<!DOCTYPE html>
<html>
    <head>
        <meta charset=utf-8>
    </head>
    <body>

        <script type="text/javascript">
            const _objectFreeze = object => {
                // 补全代码
                //得到string和symbol类型的key值
                const keys = Object.getOwnPropertyNames(object);
                const symbols = Object.getOwnPropertySymbols(object);
                //遍历属性每一项
                [...keys,...symbols].forEach(key => {
                    //定义每一个属性不可修改删除
                    Object.defineProperty(object,key,{
                        writable:false, //属性值不可改
                         configurable:false //属性不可删及改操作符
                    })
                })
                //并且要禁止属性扩展增加
                Object.preventExtensions(object)
            }
        </script>
    </body>
</html>

5. 手写JSON.stringify()

这个太多了本人不会

JavaScript
<!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>
    <script>
      function jsonStringify(data) {
        // ========== 1. 处理 null ==========
        // null 在 JSON 中是 'null'
        if (data === null) {
          return "null";
        }

        // ========== 2. 处理布尔值 ==========
        if (typeof data === "boolean") {
          return String(data); // 'true' 或 'false'
        }

        // ========== 3. 处理数字 ==========
        if (typeof data === "number") {
          // NaN、Infinity、-Infinity 在 JSON 中转为 null
          if (isNaN(data) || !isFinite(data)) {
            return "null";
          }
          return String(data); // 普通数字转为字符串
        }

        // ========== 4. 处理字符串 ==========
        if (typeof data === "string") {
          return `"${data}"`; // 加上双引号
        }

        // ========== 5. 处理 undefined、函数、Symbol ==========
        // 这些类型在 JSON.stringify 中返回 undefined
        if (
          typeof data === "undefined" ||
          typeof data === "function" ||
          typeof data === "symbol"
        ) {
          return undefined;
        }

        // ========== 6. 处理数组 ==========
        if (Array.isArray(data)) {
          const result = [];
          for (let i = 0; i < data.length; i++) {
            const value = data[i];
            // 数组中的 undefined、函数、Symbol 转为 null
            if (
              value === undefined ||
              typeof value === "function" ||
              typeof value === "symbol"
            ) {
              result.push("null");
            } else {
              result.push(jsonStringify(value));
            }
          }
          return `[${result.join(",")}]`;
        }

        // ========== 7. 处理普通对象 ==========
        if (typeof data === "object") {
          const result = [];
          for (let key in data) {
            // 只处理对象自身的属性(不处理原型链)
            if (data.hasOwnProperty(key)) {
              const value = data[key];
              // 跳过值为 undefined、函数、Symbol 的属性
              if (
                value !== undefined &&
                typeof value !== "function" &&
                typeof value !== "symbol"
              ) {
                result.push(`"${key}":${jsonStringify(value)}`);
              }
            }
          }
          return `{${result.join(",")}}`;
        }

        // 其他类型返回 undefined
        return undefined;
      }

      // 基本类型
      console.log(jsonStringify(null)); // "null"
      console.log(jsonStringify(true)); // "true"
      console.log(jsonStringify(123)); // "123"
      console.log(jsonStringify("hello")); // '"hello"'
      console.log(jsonStringify(NaN)); // "null"
      console.log(jsonStringify(Infinity)); // "null"

      // 数组
      console.log(jsonStringify([1, 2, 3])); // "[1,2,3]"
      console.log(jsonStringify([1, null, 3])); // "[1,null,3]"
      console.log(jsonStringify([1, undefined, 3])); // "[1,null,3]"
      console.log(jsonStringify([1, function () {}, 3])); // "[1,null,3]"

      // 对象
      console.log(jsonStringify({ a: 1, b: 2 })); // '{"a":1,"b":2}'
      console.log(jsonStringify({ a: 1, b: undefined })); // '{"a":1}'
      console.log(jsonStringify({ a: 1, b: function () {} })); // '{"a":1}'

      // 嵌套结构
      console.log(
        jsonStringify({
          name: "张三",
          age: 25,
          hobbies: ["篮球", "足球"],
          address: {
            city: "北京",
            code: 100000,
          },
        }),
      );
      // '{"name":"张三","age":25,"hobbies":["篮球","足球"],"address":{"city":"北京","code":100000}}'

      // 循环引用(需要额外处理)
      const obj = { a: 1 };
      obj.self = obj;
      // jsonStringify(obj); // 会栈溢出,需要处理循环引用
    </script>
  </body>
</html>

6. 大数相加

JS 中 Number 有精度限制,超过 2^53-1 会丢失精度,所以大数要用字符串处理 大概16位

JavaScript
function bigNumberAdd(a, b) {
  // 1. 转为字符串,方便逐位处理
  let str1 = a.toString();
  let str2 = b.toString();

  // 2. 初始化指针和进位
  let i = str1.length - 1;  // 指向 str1 的最后一位
  let j = str2.length - 1;  // 指向 str2 的最后一位
  let carry = 0;            // 进位,初始为0
  let result = '';          // 存储结果

  // 3. 从右向左逐位相加
  // 循环条件:还有数字没处理,或者还有进位
  while (i >= 0 || j >= 0 || carry > 0) {
    // 获取当前位的数字(没有则取0)
    const num1 = i >= 0 ? Number(str1[i]) : 0;
    const num2 = j >= 0 ? Number(str2[j]) : 0;

    // 当前位的和(包括进位)
    const sum = num1 + num2 + carry;

    // 当前位的结果:sum % 10
    const currentDigit = sum % 10;

    // 新的进位:Math.floor(sum / 10)
    carry = Math.floor(sum / 10);

    // 将当前位拼接到结果前面
    result = currentDigit + result;

    // 指针左移
    i--;
    j--;
  }

  // 4. 返回结果
  return result;
}

// 测试
console.log(bigNumberAdd('123', '456'));        // "579"
console.log(bigNumberAdd('999', '1'));          // "1000"
console.log(bigNumberAdd('123456789', '987654321')); // "1111111110"
console.log(bigNumberAdd('9007199254740992', '1'));   // "9007199254740993"

7. ⭐下划线转驼峰

HTML
<!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>
    <script>
        function toCamel(str){
            return str.replace(/_([a-z])/g, (match,letter) => letter.toUpperCase())
            //match是匹配到的完整内容,比如 _a
            //letter是捕获组()中匹配到的内容,比如a
            //直接把_a整体替换成a的大小写形式A

        }
        console.log(toCamel('hello_world')) // 输出: helloWorld
    </script>
</body>
</html>

8. CSS清除浮动

https://juejin.cn/post/7522686990870757386

进阶

Promise 并发控制、串行执行相关的题目

更多 Promise 的面试题在这里:要就来45道Promise面试题一次爽到底