Skip to content

JavaScript 编程题与算法实现

导航目录

核心概念

本章节收集整理了前端开发中常见的编程题目和算法实现,包括数据结构操作、异步控制、设计模式、作用域与闭包等核心知识点。

目录


算法与数据结构

斐波那契数列

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21...(从第三项开始,每一项等于前两项之和)

循环实现(推荐)

时间复杂度:O(n),空间复杂度:O(1)

javascript
function fib(n) {
  if (n <= 1) return n;
  let prev = 0;
  let curr = 1;
  for (let i = 2; i <= n; i++) {
    const temp = curr;
    curr = prev + curr;
    prev = temp;
  }
  return curr;
}

// 或返回整个数列
function fibSequence(n) {
  if (n <= 1) return [1];
  const arr = [1, 1];
  for (let i = 2; i < n; i++) {
    arr.push(arr[i - 1] + arr[i - 2]);
  }
  return arr;
}

console.log(fib(10)); // 55
console.log(fibSequence(10)); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

递归实现(性能较差)

时间复杂度:O(2^n),存在大量重复计算

javascript
function fibRecursive(n) {
  if (n <= 1) return n;
  return fibRecursive(n - 1) + fibRecursive(n - 2);
}

// 优化:记忆化递归
function fibMemo(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n];
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

检测循环引用

javascript
// 模块依赖关系示例
const modules = {
  a: { dependencies: ["b"] },
  b: { dependencies: ["c"] },
  c: { dependencies: ["d"] },
  d: { dependencies: ["a"] }, // 形成循环:a -> b -> c -> d -> a
};

/**
 * 检测对象中是否存在循环引用
 * @param {Object} obj - 模块依赖关系对象
 * @returns {boolean} - 是否存在循环
 */
function hasCycle(obj) {
  if (!obj) return false;

  const dfs = (key, visited) => {
    if (visited.has(key)) {
      return true; // 发现循环
    }
    visited.add(key);

    const deps = obj[key]?.dependencies || [];
    for (const dep of deps) {
      if (dfs(dep, visited)) {
        return true;
      }
    }
    visited.delete(key); // 回溯
    return false;
  };

  for (const key in obj) {
    if (dfs(key, new Set())) {
      return true;
    }
  }
  return false;
}

console.log(hasCycle(modules)); // true

数组操作

求最大值/最小值

javascript
const arr = [2, 34, 54, 34, 2, 345];

// 方法1:遍历
function getMax(arr) {
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  return max;
}

// 方法2:Math.max + apply(ES5)
const max1 = Math.max.apply(null, arr);

// 方法3:Math.max + 展开运算符(ES6,推荐)
const max2 = Math.max(...arr);

// 方法4:reduce
const max3 = arr.reduce((max, curr) => Math.max(max, curr));

console.log(getMax(arr)); // 345
console.log(Math.min(...arr)); // 2

数组去重

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

// 方法1:Set(ES6,推荐)
const unique1 = [...new Set(arr)];

// 方法2:filter + indexOf
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);

// 方法3:reduce
const unique3 = arr.reduce((acc, curr) => {
  return acc.includes(curr) ? acc : [...acc, curr];
}, []);

// 方法4:Map(保持顺序,适合对象数组)
function uniqueByKey(arr, key) {
  const map = new Map();
  arr.forEach((item) => {
    const val = key ? item[key] : item;
    if (!map.has(val)) {
      map.set(val, item);
    }
  });
  return [...map.values()];
}

console.log(unique1); // [1, 2, 3, 4]

数组扁平化

javascript
const arr = [1, 2, 3, [4, 5], 6, [7, 8, [9, 10]]];

// 方法1:flat(ES2019,推荐)
const flat1 = arr.flat(Infinity);

// 方法2:toString(仅限数字数组)
const flat2 = arr.toString().split(",").map(Number);

// 方法3:reduce + 递归
function flatten(arr) {
  return arr.reduce((acc, curr) => {
    return acc.concat(Array.isArray(curr) ? flatten(curr) : curr);
  }, []);
}

// 方法4:栈实现(非递归)
function flattenStack(arr) {
  const result = [];
  const stack = [...arr];

  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item);
    } else {
      result.unshift(item);
    }
  }
  return result;
}

console.log(flat1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

树形结构转换

列表转树

javascript
const list = [
  { id: 1, name: "电子产品", parentId: 0 },
  { id: 2, name: "服装", parentId: 0 },
  { id: 3, name: "手机", parentId: 1 },
  { id: 4, name: "电脑", parentId: 1 },
  { id: 5, name: "T恤", parentId: 2 },
  { id: 6, name: "裤子", parentId: 2 },
];

/**
 * 将扁平列表转换为树形结构
 * @param {Array} list - 扁平列表
 * @param {Object} options - 配置选项
 * @returns {Array} - 树形结构
 */
function listToTree(list, options = {}) {
  const {
    idKey = "id",
    parentKey = "parentId",
    childrenKey = "children",
  } = options;

  const map = new Map();
  const tree = [];

  // 先构建映射表
  list.forEach((item) => {
    map.set(item[idKey], { ...item, [childrenKey]: [] });
  });

  // 构建树
  list.forEach((item) => {
    const node = map.get(item[idKey]);
    if (item[parentKey] == 0 || !map.has(item[parentKey])) {
      tree.push(node);
    } else {
      const parent = map.get(item[parentKey]);
      parent[childrenKey].push(node);
    }
  });

  return tree;
}

console.log(JSON.stringify(listToTree(list), null, 2));

树转列表

javascript
/**
 * 将树形结构转换为扁平列表
 * @param {Array} tree - 树形结构
 * @param {Object} options - 配置选项
 * @returns {Array} - 扁平列表
 */
function treeToList(tree, options = {}) {
  const { childrenKey = "children", keepChildren = false } = options;
  const list = [];

  const dfs = (nodes, depth = 0) => {
    nodes.forEach((node) => {
      const { [childrenKey]: children, ...rest } = node;
      const item = keepChildren ? node : { ...rest, depth };
      list.push(item);

      if (children?.length) {
        dfs(children, depth + 1);
      }
    });
  };

  dfs(tree);
  return list;
}

异步控制

间隔输出函数

javascript
/**
 * 创建间隔执行函数
 * @param {Function} fn - 要执行的函数
 * @param {number} repeat - 重复次数
 * @param {number} interval - 间隔时间(秒)
 * @returns {Function} - 包装后的函数
 */
function createRepeat(fn, repeat, interval) {
  return function (params) {
    let count = 0;

    const execute = () => {
      if (count < repeat) {
        fn(params);
        count++;
        setTimeout(execute, interval * 1000);
      }
    };

    execute();
  };
}

// 使用
const repeatLog = createRepeat(console.log, 3, 4);
repeatLog("helloWorld"); // 每4秒输出一次,共3次

并发请求控制

javascript
/**
 * 限制并发数的请求池
 * @param {Array} urls - 请求地址数组
 * @param {number} limit - 最大并发数
 * @param {Function} requestFn - 请求函数
 */
async function concurrentRequest(urls, limit, requestFn) {
  const results = [];
  const executing = [];

  for (const [index, url] of urls.entries()) {
    const promise = requestFn(url).then((result) => {
      results[index] = result;
      return result;
    });

    results.push(promise);

    if (urls.length >= limit) {
      executing.push(promise);
      if (executing.length >= limit) {
        await Promise.race(executing);
        executing.splice(
          executing.findIndex((p) => p === promise),
          1
        );
      }
    }
  }

  return Promise.all(results);
}

作用域与闭包

变量提升案例

javascript
// 案例1:var 变量提升
var a = 1;
function fn() {
  if (!a) {
    // 此时 a 为 undefined,!undefined 为 true
    var a = 10; // var 声明提升,但赋值不提升
  }
  console.log(a); // 10
}
fn();

// 案例2:函数提升优先级高于 var
function fn2() {
  console.log(typeof fn2); // function
  var fn2 = 3;
  function fn2() {}
}
fn2();

闭包经典案例

javascript
// 案例1:函数执行上下文
function fn1() {
  let a = 10;
  return function () {
    console.log(a); // 10,访问定义时的作用域
  };
}

let f = fn1();
function fn2() {
  let a = 20;
  f(); // 输出 10,而非 20
}
fn2();

// 案例2:循环中的闭包问题
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// 解决方案1:使用 let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

// 解决方案2:使用闭包
for (var i = 0; i < 3; i++) {
  (function (j) {
    setTimeout(() => console.log(j), 100); // 0, 1, 2
  })(i);
}

this 指向

this 指向规则总结

场景this 指向
普通函数调用window(严格模式 undefined)
对象方法调用调用该方法的对象
构造函数调用新创建的实例
call/apply/bind指定的第一个参数
箭头函数定义时上层作用域的 this
javascript
// 案例1:隐式绑定丢失
const obj = {
  name: "Alice",
  getName() {
    console.log(this.name);
  },
};

obj.getName(); // Alice
const fn = obj.getName;
fn(); // undefined(this 指向 window)

// 案例2:箭头函数的 this
const obj2 = {
  name: "Bob",
  getName: () => {
    console.log(this.name); // undefined,指向 window
  },
  getName2() {
    const arrow = () => {
      console.log(this.name); // Bob,继承外层 this
    };
    arrow();
  },
};

// 案例3:类数组中的 this
let a = [1];
a.push(function () {
  console.log(this); // [1, function],指向数组
});
a[1]();

函数式编程

函数柯里化(Curry)

javascript
/**
 * 柯里化:将多参数函数转换为单参数函数链
 * @param {Function} fn - 原函数
 * @returns {Function} - 柯里化后的函数
 */
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 使用示例
function sum(a, b, c, d) {
  return a + b + c + d;
}

const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)(4)); // 10
console.log(curriedSum(1, 2)(3, 4)); // 10
console.log(curriedSum(1)(2, 3, 4)); // 10

// 实际应用:创建配置函数
const addTax = curry((tax, price) => price * (1 + tax));
const addVAT = addTax(0.2); // 创建 20% 增值税函数
console.log(addVAT(100)); // 120

反柯里化(Uncurry)

javascript
/**
 * 反柯里化:借用其他对象的方法
 */
Function.prototype.uncurry = function () {
  const self = this;
  return function (context, ...args) {
    return self.apply(context, args);
  };
};

// 使用示例
const push = Array.prototype.push.uncurry();
const obj = { length: 0 };
push(obj, 1);
push(obj, 2);
console.log(obj); // { '0': 1, '1': 2, length: 2 }

// 类型判断
const toString = Object.prototype.toString.uncurry();
console.log(toString([])); // [object Array]
console.log(toString({})); // [object Object]
console.log(toString(123)); // [object Number]

设计模式

发布订阅模式(Pub/Sub)

javascript
/**
 * 发布订阅模式:解耦的事件系统
 */
class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);

    // 返回取消订阅函数
    return () => this.off(event, callback);
  }

  // 取消订阅
  off(event, callback) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter((cb) => cb !== callback);
  }

  // 发布
  emit(event, ...args) {
    if (!this.events[event]) return;
    this.events[event].forEach((callback) => {
      try {
        callback(...args);
      } catch (error) {
        console.error(`Error in event ${event}:`, error);
      }
    });
  }

  // 一次性订阅
  once(event, callback) {
    const onceCallback = (...args) => {
      callback(...args);
      this.off(event, onceCallback);
    };
    this.on(event, onceCallback);
  }
}

// 使用
const emitter = new EventEmitter();
const unsubscribe = emitter.on("message", (data) => {
  console.log("收到消息:", data);
});

emitter.emit("message", "Hello World"); // 收到消息: Hello World
unsubscribe(); // 取消订阅

观察者模式(Observer)

javascript
/**
 * 观察者模式:被观察者主动通知观察者
 */
// 被观察者(Subject)
class Subject {
  constructor(name) {
    this.name = name;
    this.observers = [];
    this.state = "";
  }

  // 添加观察者
  attach(observer) {
    this.observers.push(observer);
  }

  // 移除观察者
  detach(observer) {
    this.observers = this.observers.filter((obs) => obs !== observer);
  }

  // 状态变更,通知所有观察者
  setState(state) {
    this.state = state;
    console.log(`${this.name} 状态变更为: ${state}`);
    this.notify();
  }

  notify() {
    this.observers.forEach((observer) => {
      observer.update(this.state);
    });
  }
}

// 观察者(Observer)
class Observer {
  constructor(name) {
    this.name = name;
  }

  update(state) {
    console.log(`${this.name} 收到通知: ${state}`);
  }
}

// 使用
const subject = new Subject("天气系统");
const observer1 = new Observer("小明");
const observer2 = new Observer("小红");

subject.attach(observer1);
subject.attach(observer2);
subject.setState("下雨了");
// 天气系统 状态变更为: 下雨了
// 小明 收到通知: 下雨了
// 小红 收到通知: 下雨了

手写实现

深拷贝

javascript
/**
 * 深拷贝:完整复制对象,处理循环引用
 * @param {*} data - 要拷贝的数据
 * @param {WeakMap} weakMap - 用于处理循环引用
 * @returns {*} - 拷贝后的数据
 */
function deepClone(data, weakMap = new WeakMap()) {
  // 基本类型直接返回
  if (data === null || typeof data !== "object") {
    return data;
  }

  // 处理函数
  if (typeof data === "function") {
    return data;
  }

  // 处理日期
  if (data instanceof Date) {
    return new Date(data.getTime());
  }

  // 处理正则
  if (data instanceof RegExp) {
    return new RegExp(data.source, data.flags);
  }

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

  // 处理 Map
  if (data instanceof Map) {
    const cloneMap = new Map();
    weakMap.set(data, cloneMap);
    data.forEach((value, key) => {
      cloneMap.set(deepClone(key, weakMap), deepClone(value, weakMap));
    });
    return cloneMap;
  }

  // 处理 Set
  if (data instanceof Set) {
    const cloneSet = new Set();
    weakMap.set(data, cloneSet);
    data.forEach((value) => {
      cloneSet.add(deepClone(value, weakMap));
    });
    return cloneSet;
  }

  // 处理对象和数组
  const clone = Array.isArray(data) ? [] : {};
  weakMap.set(data, clone);

  for (const key in data) {
    if (Object.prototype.hasOwnProperty.call(data, key)) {
      clone[key] = deepClone(data[key], weakMap);
    }
  }

  return clone;
}

// 测试
const obj = {
  a: 1,
  b: { c: 2 },
  d: [1, 2, 3],
  e: new Date(),
  f: /abc/g,
};
obj.circular = obj; // 循环引用

const cloned = deepClone(obj);
console.log(cloned.b === obj.b); // false
console.log(cloned.circular === cloned); // true

节流与防抖

javascript
/**
 * 节流(Throttle):固定时间内只执行一次
 * 适用场景:滚动、resize、鼠标移动
 */
function throttle(fn, delay) {
  let lastTime = 0;
  let timer = null;

  return function (...args) {
    const now = Date.now();

    if (now - lastTime >= delay) {
      // 立即执行
      fn.apply(this, args);
      lastTime = now;
    } else {
      // 延迟执行最后一次
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(this, args);
        lastTime = Date.now();
      }, delay - (now - lastTime));
    }
  };
}

/**
 * 防抖(Debounce):延迟执行,期间再次触发则重新计时
 * 适用场景:搜索输入、表单验证
 */
function debounce(fn, delay, immediate = false) {
  let timer = null;

  return function (...args) {
    const callNow = immediate && !timer;

    clearTimeout(timer);

    timer = setTimeout(() => {
      timer = null;
      if (!immediate) {
        fn.apply(this, args);
      }
    }, delay);

    if (callNow) {
      fn.apply(this, args);
    }
  };
}

// 使用
window.addEventListener(
  "scroll",
  throttle(() => {
    console.log("scroll");
  }, 200)
);

const searchInput = debounce((value) => {
  console.log("搜索:", value);
}, 500);

手写 Promise

javascript
class MyPromise {
  constructor(executor) {
    this.state = "pending";
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === "pending") {
        this.state = "fulfilled";
        this.value = value;
        this.onFulfilledCallbacks.forEach((fn) => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === "pending") {
        this.state = "rejected";
        this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) => value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) => {
            throw reason;
          };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === "fulfilled") {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else if (this.state === "rejected") {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error);
          }
        });
      } else {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (error) {
              reject(error);
            }
          });
        });
      }
    });

    return promise2;
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let completed = 0;

      promises.forEach((promise, index) => {
        MyPromise.resolve(promise).then((value) => {
          results[index] = value;
          completed++;
          if (completed === promises.length) {
            resolve(results);
          }
        }, reject);
      });
    });
  }
}

// 辅助函数
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    reject(new TypeError("Chaining cycle detected for promise"));
    return;
  }

  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    let called = false;
    try {
      const then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (y) => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (error) {
      if (called) return;
      reject(error);
    }
  } else {
    resolve(x);
  }
}

手写 call、apply、bind

javascript
/**
 * 手写 call
 */
Function.prototype.myCall = function (context, ...args) {
  context = context ? Object(context) : window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  const result = context[fnSymbol](...args);
  delete context[fnSymbol];
  return result;
};

/**
 * 手写 apply
 */
Function.prototype.myApply = function (context, args = []) {
  context = context ? Object(context) : window;
  const fnSymbol = Symbol("fn");
  context[fnSymbol] = this;
  const result = context[fnSymbol](...args);
  delete context[fnSymbol];
  return result;
};

/**
 * 手写 bind
 */
Function.prototype.myBind = function (context, ...args1) {
  const fn = this;
  return function (...args2) {
    return fn.apply(context, [...args1, ...args2]);
  };
};

// 使用
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

const person = { name: "Alice" };
greet.myCall(person, "Hello", "!"); // Hello, Alice!
greet.myApply(person, ["Hi", "?"]); // Hi, Alice?
const boundGreet = greet.myBind(person, "Hey");
boundGreet("..."); // Hey, Alice...

经典面试题

让 a == 1 && a == 2 && a == 3 成立

javascript
// 方法1:重写 valueOf 或 toString
let a = {
  value: 1,
  valueOf() {
    return this.value++;
  },
};

if (a == 1 && a == 2 && a == 3) {
  console.log("成立!");
}

// 方法2:使用 Proxy
let index = 1;
a = new Proxy(
  {},
  {
    get() {
      return index++;
    },
  }
);

if (a == 1 && a == 2 && a == 3) {
  console.log("成立!");
}

对象深度比较

javascript
/**
 * 深度比较两个对象是否相等
 */
function isEqual(obj1, obj2) {
  // 基本类型直接比较
  if (obj1 === obj2) return true;

  // 类型不同
  if (typeof obj1 !== typeof obj2) return false;

  // null 和 undefined
  if (obj1 == null || obj2 == null) return obj1 === obj2;

  // 日期
  if (obj1 instanceof Date && obj2 instanceof Date) {
    return obj1.getTime() === obj2.getTime();
  }

  // 正则
  if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
    return obj1.toString() === obj2.toString();
  }

  // 数组
  if (Array.isArray(obj1) && Array.isArray(obj2)) {
    if (obj1.length !== obj2.length) return false;
    return obj1.every((item, index) => isEqual(item, obj2[index]));
  }

  // 对象
  if (typeof obj1 === "object" && typeof obj2 === "object") {
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);

    if (keys1.length !== keys2.length) return false;

    return keys1.every(
      (key) => keys2.includes(key) && isEqual(obj1[key], obj2[key])
    );
  }

  return false;
}

// 测试
console.log(isEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })); // true
console.log(isEqual([1, 2, 3], [1, 2, 3])); // true

最佳实践总结

  1. 优先使用 ES6+ 语法:展开运算符、解构赋值、箭头函数等
  2. 注意 this 指向:箭头函数没有自己的 this,普通函数的 this 取决于调用方式
  3. 避免 var:使用 let 和 const,避免变量提升带来的问题
  4. 处理异步:使用 async/await 替代回调地狱
  5. 函数式编程:善用 map、filter、reduce 等高阶函数
  6. 性能优化:大数据量操作考虑时间复杂度和空间复杂度