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. setTimeout 的 this 陷阱机制与箭头函数:

    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 变量 == 冷却倒计时器。

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

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

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

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

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


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

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

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

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

第一下点击(第 0 秒):

  1. 此时 timer 是 null。

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

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

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

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

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

  1. 此时 timer 是 1。

  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 毫秒 → 执行 callback → callback 执行完后才开始下一个 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. 为什么代码里还要拼接 args 和 args2? ​

为什么 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));
//注意注意,这里需要使用
console.log(JSON.stringify(listToTree(list))); 进行一个序列化输出
主要是为了把树形对象转换成 JSON 字符串,让嵌套结构完整、明确地显示出来
比如:[{"id":1,"name":"部门A","parentId":null,"children":[...]}]
如果直接console.log(listToTree(list));
node环境运行会输出
[
  {
    id: 1,
    name: '部门A',
    parentId: null,
    children: [ [Object], [Object] ]
  }
]
所以请在测试手撕时,使用终端node环境进行,不要在浏览器进行测试,因为浏览器对这种情况可以进行一个点开展开的操作

以及,以上是二叉树情况,面试更常考以下多叉树的情况:
// 列表转树
const list = [
  { id: 1  },
  { id: 2 },
  { id: 3, parentId: 1 },
  { id: 4, parentId: 2 },
  { id: 5, parentId: 2 },
];

function listToTree(list, parentId = null) {
  //map存储id和node的映射
  const map = new Map();
  for (const item of list) {
    map.set(item.id, { ...item, children: [] });
  }
  const tree = [];
  //给当前节点找到父节点,并加入到父节点的子节点数组中
  for (const item of list) {
    const node = map.get(item.id);
    if (!node.parentId) {
      tree.push(node);
    } else {
      const parent = map.get(item.parentId);
      parent.children.push(node);
    }
  }
  return tree;
}

console.log(JSON.stringify(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面试题一次爽到底