一、引言
最近一年面了很多次字节都没过,三次三面挂,分享一些我印象比较深刻的代码题,希望大家共勉
二、事件循环
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是谁的语法糖?是怎么实现的?
- 生成器函数(Generator)
要理解 async/await 的实现,我们需要先了解生成器函数。生成器函数是一种特殊的函数,可以暂停执行状态,并在适当的时候恢复执行。
function* generatorFunc() {
console.log('step 1');
yield 1;
console.log('step 2');
yield 2;
console.log('done');
}
生成器函数的特点:
-
- 使用
function*定义 - 通过
yield关键字暂停执行 - 可以通过
.next()方法恢复执行 - 支持多次暂停和恢复
- 使用
- 执行器模式
async/await 的实现可以简化为一个执行器模式。这个执行器负责管理生成器的生命周期,包括:
-
- 启动生成器
- 监听异步操作的结果
- 恢复生成器的执行
- 处理错误
- 手动实现 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) 的典型实现。协程是一种用户态的轻量级线程,能够通过让出控制权实现高效的并发执行。
生成器函数的执行流程可以分为以下几个步骤:
- 初始化:生成器函数被调用后,返回一个生成器对象,但函数体并未立即执行。
- 执行:调用
.next()方法启动生成器的执行,直到遇到yield语句时暂停。 - 暂停:
yield语句会返回当前的值,并将执行权交还给调用者。 - 恢复:调用
.next(value)方法可以恢复生成器的执行,并将传入的值作为yield表达式的值。 - 终止:当生成器函数执行完毕或遇到
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'
- 原因:
-
- 当调用createClass时,每次都会创建一个新的count变量,所以classA和classB各自的count应该是独立的。但是,在返回的对象里,count属性是直接赋值为当时的count值,也就是0。
- 而add和clear方法操作的是内部变量count,不是对象的count属性。所以每次调用add或clear,修改的是闭包里的count变量,对象的count属性并没有被更新。
- 比如,当classA.add()被调用时,内部count增加到1,但classA.count仍然是0,因为对象的count属性在创建时就被固定为初始值0。同理,classB.add()也是修改自己的闭包count,但classB.count还是0。当调用clear时,同样只是修改闭包里的变量,而对象的属性没变。
- 最后,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;
}
未完待续