2025字节一手前端面试题~ 都是面过被问的

336 阅读7分钟

一、引言

最近一年面了很多次字节都没过,三次三面挂,分享一些我印象比较深刻的代码题,希望大家共勉

二、事件循环

1. 下方代码的输出结果是什么?

async function async1() {
  console.log('async1');
  await async2();
  console.log('async3');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
async1();
new Promise((resolve) => {
  console.log('p1');
  resolve();
  console.log('p3');
}).then(() => {
  console.log('p2');
});
setTimeout(() => {
  console.log('setTimeout');
}, 0);
console.log('script end');

运行结果为:

script start
async1
async2
p1
p3
script end
async3
p2
setTimeout

2. async/await是谁的语法糖?是怎么实现的?

  1. 生成器函数(Generator)

要理解 async/await 的实现,我们需要先了解生成器函数。生成器函数是一种特殊的函数,可以暂停执行状态,并在适当的时候恢复执行。

function* generatorFunc() {
  console.log('step 1');
  yield 1;
  console.log('step 2');
  yield 2;
  console.log('done');
}

生成器函数的特点:

    • 使用 function* 定义
    • 通过 yield 关键字暂停执行
    • 可以通过 .next() 方法恢复执行
    • 支持多次暂停和恢复
  1. 执行器模式

async/await 的实现可以简化为一个执行器模式。这个执行器负责管理生成器的生命周期,包括:

    1. 启动生成器
    2. 监听异步操作的结果
    3. 恢复生成器的执行
    4. 处理错误
  1. 手动实现 async/await

以下是一个手动实现 async/await 的执行器:

function asyncExecutor(generatorFunc) {
  return function (...args) {
    const generator = generatorFunc.apply(this, args);
    return new Promise((resolve, reject) => {
      function step(key, value) {
        try {
          const result = generator[key](value);
          if (result.done) {
            resolve(result.value);
          } else {
            Promise.resolve(result.value)
              .then((resolvedValue) => step("next", resolvedValue), 
                     (error) => step("throw", error));
          }
        } catch (error) {
          reject(error);
        }
      }
      step("next");
    });
  };
}

使用示例:

const mockAsync = asyncExecutor(function* () {
  const data1 = yield new Promise(resolve => setTimeout(() => resolve("Data1 loaded"), 1000));
  console.log(data1); // 1秒后输出
  const syncValue = yield "Immediate value";
  console.log(syncValue); // 立即输出
  const data2 = yield new Promise(resolve => setTimeout(() => resolve("Data2 loaded"), 500));
  console.log(data2); // 0.5秒后输出
  return "All done!";
});
mockAsync().then(result => console.log("Final result:", result));

3. Generator的是如何实现函数的暂停和恢复机制的?

生成器函数的暂停与恢复机制,正是协程(Coroutine) 的典型实现。协程是一种用户态的轻量级线程,能够通过让出控制权实现高效的并发执行。
生成器函数的执行流程可以分为以下几个步骤:

  1. 初始化:生成器函数被调用后,返回一个生成器对象,但函数体并未立即执行。
  2. 执行:调用 .next() 方法启动生成器的执行,直到遇到 yield 语句时暂停。
  3. 暂停yield 语句会返回当前的值,并将执行权交还给调用者。
  4. 恢复:调用 .next(value) 方法可以恢复生成器的执行,并将传入的值作为 yield 表达式的值。
  5. 终止:当生成器函数执行完毕或遇到 return 语句时,生成器的 done 标志会被设置为 true

4. 另一种事件循环考题,各位看下会输出什么?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script defer>
      setTimeout(() => {
        console.log('settimeout', document.body)
      }, 0);

      Promise.resolve().then(() => {
        console.log('promsie', document.body)
      })

      console.log('script', document.body)
    </script>
  </head>
  <body>
    hello
  </body>
</html>
script null
promsie null
settimeout hello

大家可以讨论下为什么?也是事件循环相关

三、闭包问题

1. 最后两行代码输出什么?

const createClass = () => {
  let count = 0;

  return {
    count,
    add: function() {
      count++;
      console.log(count, 'add')
    },
    clear: function() {
      count = 0;
      console.log(count, 'clear')
    }
  }
}

const classA = createClass();
const classB = createClass();

classA.add()
classA.add()
classB.add()
classA.clear()
classA.add()
classB.add()


console.log(classA.count, 'classA count')
console.log(classB.count, 'classB count')
  • 输出结果:
// 0 'classA count'
// 0 'classB count'
  • 原因:
    1. 当调用createClass时,每次都会创建一个新的count变量,所以classA和classB各自的count应该是独立的。但是,在返回的对象里,count属性是直接赋值为当时的count值,也就是0。
    2. 而add和clear方法操作的是内部变量count,不是对象的count属性。所以每次调用add或clear,修改的是闭包里的count变量,对象的count属性并没有被更新。
    3. 比如,当classA.add()被调用时,内部count增加到1,但classA.count仍然是0,因为对象的count属性在创建时就被固定为初始值0。同理,classB.add()也是修改自己的闭包count,但classB.count还是0。当调用clear时,同样只是修改闭包里的变量,而对象的属性没变。
    4. 最后,console.log输出的classA.count和classB.count都是0,因为它们没有被更新过。而方法中的console.log会显示闭包里的count值,比如classA.add两次后是1和2,clear之后变0,再add就是1。classB.add两次后是1和2。

2. 在不改变输出方式的前提下如何正确输出?你有几种方案?

const createClass = () => {
  const state = { count: 0 }; // 对象属性存储值
  return {
    get count() { return state.count; }, // 通过 getter 动态读取
    add() {
      state.count++;
      console.log(state.count, 'add');
    },
    clear() {
      state.count = 0;
      console.log(state.count, 'clear');
    }
  };
};
const createClass = () => {
  let count = 0;

  const result = {
    count,
    add: function() {
      count++;
      console.log(count, 'add')
    },
    clear: function() {
      count = 0;
      console.log(count, 'clear')
    }
  }

  Object.defineProperty(result, 'count', {
    get: () => count,
    set: (value) => { count = value } 
  })

  return result;
}

四、实现一个发布订阅模式

1. 实现一个类似EventEmitter的发布订阅模式

class EventEmitter {
    constructor() {
        this.events = {};
    }

    // 订阅事件
    on(eventName, callback, ...args) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        // 保存回调函数及其参数
        this.events[eventName].push({ callback, args });
    }

    // 取消订阅事件
    off(eventName, callback) {
        if (!this.events[eventName]) return;

        this.events[eventName] = this.events[eventName].filter(
            event => event.callback !== callback
        );
    }

    // 订阅一次性事件
    once(eventName, callback) {
        const onceWrapper = (...args) => {
            callback(...args);
            this.off(eventName, onceWrapper); // 触发后取消订阅
        };
        this.on(eventName, onceWrapper);
    }

    // 触发事件
    trigger(eventName, ...args) {
        if (!this.events[eventName]) return;

        this.events[eventName].forEach(event => {
            event.callback(...event.args, ...args); // 把原始参数和触发时的参数合并
        });
    }
}

// 测试代码
const emitter = new EventEmitter();

const onTest = (...args) => {
    console.log('onTest:', ...args);
};

const onOnceTest = (...args) => {
    console.log('onOnceTest:', ...args);
};

emitter.on('testEvent', onTest, 'arg1', 'arg2');
emitter.once('onceEvent', onOnceTest);

emitter.trigger('testEvent', 'triggerArg'); // onTest: arg1 arg2 triggerArg
emitter.trigger('onceEvent', 'onceTriggerArg'); // onOnceTest: onceTriggerArg
emitter.trigger('onceEvent', 'won't trigger'); // 不会被触发
emitter.off('testEvent', onTest);
emitter.trigger('testEvent', 'after off'); // 不会被触发

2. 实现一个类似Rxjs的发布订阅模式

// 核心类:Observable
class Observable {
  constructor(subscribeFn) {
    this._subscribe = subscribeFn; // 保存订阅逻辑
  }

  // 订阅方法
  subscribe(observer) {
    const subscription = new Subscription();
    const safeObserver = new SafeObserver(observer, subscription);
    subscription.add(this._subscribe(safeObserver));
    return subscription;
  }

  // 静态方法:创建 Observable
  static create(subscribeFn) {
    return new Observable(subscribeFn);
  }

  // 操作符:map
  map(projectFn) {
    return new Observable(observer => {
      const subscription = this.subscribe({
        next: value => observer.next(projectFn(value)),
        error: err => observer.error(err),
        complete: () => observer.complete()
      });
      return () => subscription.unsubscribe();
    });
  }

  // 操作符:filter
  filter(predicateFn) {
    return new Observable(observer => {
      const subscription = this.subscribe({
        next: value => {
          if (predicateFn(value)) observer.next(value);
        },
        error: err => observer.error(err),
        complete: () => observer.complete()
      });
      return () => subscription.unsubscribe();
    });
  }
}

// 包装观察者,确保安全性(如取消订阅后不再调用)
class SafeObserver {
  constructor(observer, subscription) {
    this.observer = observer;
    this.subscription = subscription;
    this.isStopped = false;
  }

  next(value) {
    if (!this.isStopped && this.observer.next) {
      try {
        this.observer.next(value);
      } catch (err) {
        this.error(err);
      }
    }
  }

  error(err) {
    if (!this.isStopped) {
      this.isStopped = true;
      if (this.observer.error) {
        this.observer.error(err);
      }
      this.subscription.unsubscribe();
    }
  }

  complete() {
    if (!this.isStopped) {
      this.isStopped = true;
      if (this.observer.complete) {
        this.observer.complete();
      }
      this.subscription.unsubscribe();
    }
  }
}

// 订阅管理类
class Subscription {
  constructor() {
    this._teardowns = [];
  }

  add(teardown) {
    if (teardown) {
      this._teardowns.push(teardown);
    }
  }

  unsubscribe() {
    this._teardowns.forEach(teardown => {
      if (typeof teardown === 'function') {
        teardown();
      } else if (teardown && teardown.unsubscribe) {
        teardown.unsubscribe();
      }
    });
    this._teardowns = [];
  }
}

// -------------------- 示例用法 --------------------
// 1. 创建 Observable
const obs$ = Observable.create(observer => {
  let count = 0;
  const intervalId = setInterval(() => {
    observer.next(count++);
    if (count >= 5) {
      observer.complete();
      clearInterval(intervalId);
    }
  }, 1000);

  // 返回取消订阅逻辑
  return () => {
    console.log('清理定时器');
    clearInterval(intervalId);
  };
});

// 2. 应用操作符
const transformed$ = obs$
  .map(x => x * 2)
  .filter(x => x > 3);

// 3. 订阅
const subscription = transformed$.subscribe({
  next: value => console.log('收到数据:', value),
  error: err => console.error('错误:', err),
  complete: () => console.log('已完成')
});

// 4. 手动取消订阅(例如 3 秒后取消)
setTimeout(() => {
  subscription.unsubscribe();
  console.log('已取消订阅');
}, 3000);

五、柯里化

该函数接受一个函数作为唯一参数,并返回一个接受单个参数的函数,该函数可以重复调用

1. 至少提供最小数量的参数(由原始函数接受的参数数量决定)

function add(a, b) {
  return a + b;
}

const curriedAdd = curry(add);
console.log(curriedAdd(3)(4)); // 7
console.log(curriedAdd()(4)()(3)) // 7
console.log(curriedAdd()()()()(4)(3)) // 7

const alreadyAddedThree = curriedAdd(3);
console.log(alreadyAddedThree(4)); // 7
function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    }

    return curried.bind(this, ...args);
  };
}

2. 接受可变数量参数的函数

function multiply(...numbers) {
  return numbers.reduce((a, b) => a * b, 1);
}
const curriedMultiply = curry(multiply);
const multiplyByThree = curriedMultiply(3);
console.log(+multiplyByThree); // 3
console.log(+multiplyByThree(4)); // 12

const multiplyByFifteen = multiplyByThree(5);
console.log(+multiplyByFifteen); // 15
console.log(+multiplyByFifteen(2)); // 30

console.log(+curriedMultiply(1)(2)(3)(4)); // 24
console.log(+curriedMultiply(1, 2, 3, 4)); // 24
function curry(func) {
  return function curried(...args) {
    const fn = curried.bind(this, ...args);
    // 
    fn[Symbol.toPrimitive] = () => func.apply(this, args);
    return fn;
  };
}

function multiply(...numbers) {
  return numbers.reduce((a, b) => a * b, 1);
}
const curriedMultiply = curry(multiply);
const multiplyByThree = curriedMultiply(3);
console.log(+multiplyByThree); // 3
console.log(+multiplyByThree(4)); // 12

const multiplyByFifteen = multiplyByThree(5);
console.log(+multiplyByFifteen); // 15
console.log(+multiplyByFifteen(2)); // 30

console.log(+curriedMultiply(1)(2)(3)(4)); // 24
console.log(+curriedMultiply(1, 2, 3, 4)); // 24

六、最长无重复子串

最经典的算法题,被问了起码3次,leetcode原题

// 给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。

// 示例 1:

// 输入: s = "abcabcbb"
// 输出: 3 
// 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
// 示例 2:

// 输入: s = "bbbbb"
// 输出: 1
// 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
// 示例 3:

// 输入: s = "pwwkew"
// 输出: 3
// 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
//      请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。


function maxLengthOfSubstring(str) {
  let set = new Set();
  let left = 0;
  let maxLength = 0;

  // 滑动窗口
  for (let right = 0; right < str.length; right++) {
    // 重复了滑动左边界
    while (set.has(str[right])) { 
        set.delete(str[left])
        left++;      
    }
    // 不重复了添加到集合里 下次用
    set.add(s[right])
    maxLength = Math.max(maxLength, right - left + 1)
  }

  return maxLength;
}

未完待续