前端面试重要代码内容之 js / ts¶
总结于2025.10
这些是本人在面试的时候总结的需要熟悉的 js / ts 相关知识,加*号的是个人认为比较重要的。因为一开始是总结给自己看的,可能部分写法比较草率。如果你认为其中有错误或需要补充,请联系我~
发布者模式*¶
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
}
off(event, listener) {
if (!this.events[event]) return;
const index = this.events[event].indexOf(listener);
if (index > -1) {
this.events[event].splice(index, 1);
}
}
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(...args));
}
once(event, listener) {
const onceListener = (...args) => {
listener(...args);
this.off(event, onceListener);
};
this.on(event, onceListener);
}
}
// 测试代码
const emitter = new EventEmitter();
function responseToEvent(msg) {
console.log(msg);
}
emitter.on('event1', responseToEvent);
emitter.emit('event1', 'Event 1 triggered!'); // 输出: Event 1 triggered!
emitter.off('event1', responseToEvent);
emitter.emit('event1', 'Event 1 triggered again!'); // 无输出
emitter.once('event2', responseToEvent);
emitter.emit('event2', 'Event 2 triggered!'); // 输出: Event 2 triggered!
emitter.emit('event2', 'Event 2 triggered again!'); // 无输出
数组扁平化¶
function flattenObject(obj) {
const result = {};
function flatten(current, path) {
if (typeof current !== 'object' || current === null) {
// 当前值不是对象或数组(即是叶子节点),将当前路径与值存入结果
if (path !== '') {
result[path] = current;
}
return;
}
if (Array.isArray(current)) {
// 处理数组
for (let i = 0; i < current.length; i++) {
const newPath = path ? `${path}[${i}]` : `[${i}]`;
flatten(current[i], newPath);
}
} else {
// 处理对象
const keys = Object.keys(current);
for (const key of keys) {
const newPath = path ? `${path}.${key}` : key;
flatten(current[key], newPath);
}
// 遍历对象代码等价于
// for (const [key, value] of Object.entries(current)) {
// const newPath = path ? `${path}.${key}` : key;
// flatten(value, newPath);
// }
}
}
flatten(obj, '');
return result;
}
const obj3 = {
a: {
b: 5,
c: [
{ d: 8 },
{ e: 9 }
],
f: {
g: 10
}
}
};
console.log(flattenObject(obj3));
类型判断¶
let arr = new Array();
console.log(typeof arr); // "object"
console.log(Object.prototype.toString(arr)); // "[object Object]"
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(arr instanceof Array); // true
console.log(Array.isArray(arr)); // true
let map = new Map();
console.log(map instanceof Map); // true
console.log(map instanceof Object); // true
// for of 可以遍历数组、字符串、Set、Map 等可迭代对象(需要可迭代)
// for in 可以遍历对象的可枚举属性(对象的键)—— 会遍历原型链属性 顺序不保证
let myArr = [1, 2, 3];
for (let value of myArr)
{
console.log(value); // 1 2 3
}
for (let value in myArr) // for in 遍历数组 遍历的是索引 一般不用
{
console.log(value); // 0 1 2
}
let myMap = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of myMap)
{
console.log(key, value); // a 1 b 2
}
for (let key of myMap.keys())
{
console.log(key); // a b
}
for (let value of myMap.values())
{
console.log(value); // 1 2
}
for (let entry of myMap.entries())
{
console.log(entry); // ['a', 1] ['b', 2]
}
myMap.forEach((value, key) => {
console.log(key, value); // a 1 b 2
});
let myObj = { x: 10, y: 20 };
for (let key in myObj)
{
console.log(key, myObj[key]); // x 10 y 20 // 没打出原型链属性是因为原型链上属性是不可枚举的
}
let myStr = "";
myStr += "he";
console.log(myStr); // he
myStr += 'l';
console.log(myStr); // hel
myStr += 'lo';
console.log(myStr); // hello
继承使用(es5)¶
// 1. 原型链继承
function Parent1() {
this.name = 'parent1';
this.play = [1, 2, 3];
}
Parent1.prototype.getName = function() {
return this.name;
}
function Child1() {
this.type = 'child1';
}
// 继承了Parent1
Child1.prototype = new Parent1();
Child1.prototype.constructor = Child1;
var child1 = new Child1();
console.log(child1.getName());
// 缺点:
// 1. 引用类型的属性被所有实例共享
var child = new Child1();
child.play.push(4);
console.log(child1.play); // [1, 2, 3, 4]
// 2. 在创建Child的实例时,不能向Parent传参
// 2. 借用构造函数继承
function Parent2(name) {
this.name = name;
this.play = [1, 2, 3];
}
Parent2.prototype.getName = function() {
return this.name;
}
function Child2(name) {
this.type = 'child2';
// 继承了Parent2
Parent2.call(this, name); // 执行父类构造函数并绑定子类的this
}
var child = new Child2('child');
console.log(child.name); // child3
console.log(child.play); // [1, 2, 3]
// 缺点:只能继承父类的实例属性和方法,不能继承原型属性和方法
// 3. 组合继承
function Parent3(name) {
this.name = name;
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function() {
return this.name;
}
function Child3(name) {
this.type = 'child3';
Parent3.call(this, name); // 继承实例属性
}
// 继承原型属性和方法
Child3.prototype = new Parent3();
Child3.prototype.constructor = Child3;
// 缺点:调用了两次Parent3构造函数
// 4. 寄生组合继承
function Parent4(name) {
this.name = name;
this.play = [1, 2, 3];
}
Parent4.prototype.getName = function() {
return this.name;
}
function Child4(name) {
this.type = 'child4';
Parent4.call(this, name); // 继承实例属性
}
// 创建一个没有实例属性的中间类
function F() {}
F.prototype = Parent4.prototype;
Child4.prototype = new F(); // 继承原型属性和方法
Child4.prototype.constructor = Child4;
var child4 = new Child4('child4');
console.log(child4.name);
节流(throttle)实现*¶
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime > delay) {
fn.apply(this, args);
lastTime = now;
}
}
}
window.addEventListener('resize', throttle(() => {
console.log('Resize event handler called at most once every 300ms.');
}, 300));
防抖(debounce)实现*¶
function debounce(fn, delay)
{
let timer = null;
return function (...args)
{
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
}
// 带immediate的防抖
function debounce(fn, delay, immediate = false) {
let timer = null;
return function (...args) {
const context = this;
if (immediate && !timer) {
// 如果是立即执行模式,且当前没有定时器,则立即执行
fn.apply(context, args);
}
// 清除之前的定时器
if (timer) clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
// 如果不是立即执行模式,或者已经执行过一次立即执行了,这里再执行
if (!immediate) {
fn.apply(context, args);
}
// 重置 timer,为下一次可能的立即执行做准备
timer = null;
}, delay);
};
}
window.addEventListener('resize', debounce(() => {
console.log('Resize event handler called after 300ms of no resize events.');
}, 300, true));
同步代码执行顺序相关*¶
// 1
console.log("start");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
(async function foo() {
console.log("async 1");
await asyncFunction();
console.log("async2");
})().then(console.log("foo.then"));
async function asyncFunction() {
console.log("asyncFunction");
setTimeout(() => {
console.log("setTimeout2");
}, 0);
new Promise((res) => {
console.log("promise1");
res("promise2");
}).then(console.log);
}
console.log("end");
// 这段代码输出顺序是:start - async 1 - asyncFunction - promise1 - foo.then - end - promise2 - async2 - setTimeout1 - setTimeout2
// 2
console.log("start");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
(async function foo() {
console.log("async 1");
await asyncFunction();
console.log("async2");
})().then(()=>console.log("foo.then"));
// 注意这里有区别
async function asyncFunction() {
console.log("asyncFunction");
setTimeout(() => {
console.log("setTimeout2");
}, 0);
new Promise((res) => {
console.log("promise1");
res("promise2");
}).then(console.log);
}
console.log("end");
// 这段代码输出顺序是:start - async 1 - asyncFunction - promise1 - end - promise2 - async2 - foo.then - setTimeout1 - setTimeout2
// 3
async function async1() {
console.log('async1 start');
await async2(); // 加到微任务队列中 其实是等于 async2().then(() => {console.log('async1 end)})
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 这段代码输出顺序是:script start - async1 start - async2 - promise1 - script end - async1 end - promise2 - setTimeout
// 4
async function async1() {
console.log('async1 start');
// await async2(); // 等同于 async2().then(() => {})
async2().then(() => {console.log('then')});
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 这段代码输出顺序是:script start - async1 start - async2 - async1 end - promise1 - script end - then - promise2 - setTimeout
map 使用¶
// 两数之和,返回的是下标
var twoSum = function (nums, target) {
let map = new Map();
for(var i=0;i<nums.length;i++)
{
let dif=target-nums[i];
if(map.has(dif))
{
return [map.get(dif),i];
}
map.set(nums[i],i);
}
};
// 返回格式是[1,2]
数组转成树¶
const arr = [
{ id: 2, title: '部门2', pid: 1 },
{ id: 3, title: '部门3', pid: 1 },
{ id: 1, title: '部门1', pid: 0 },
{ id: 4, title: '部门4', pid: 3 },
{ id: 5, title: '部门5', pid: 4 },
]
const ArrToTree = (arr, rootId = 0) => {
const result = [];
const map = new Map();
arr.forEach(item => {
map.set(item.id, { ...item, children: [] });
})
arr.forEach(item => {
const node = map.get(item.id);
if (item.pid === rootId) {
result.push(node);
}
else {
const parent = map.get(item.pid);
if (parent)
{
parent.children.push(node);
}
}
})
return result;
}
const tree = ArrToTree(arr);
console.log(JSON.stringify(tree, null, 2));
在原型链上实现 call, apply 和 bind¶
Function.prototype.call = function (context, ...args)
{
context = context || window;
const fnSymbol = Symbol();
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
return result;
}
Function.prototype.apply = function (context, args)
{
context = context || window;
const fnSymbol = Symbol();
context[fnSymbol] = this;
let result;
if (!args)
{
result = context[fnSymbol]();
}
else
{
result = context[fnSymbol](...args);
}
delete context[fnSymbol];
return result;
}
Function.prototype.bind = function (context, ...args)
{
const fn = this;
return function F(...innerArgs)
{
// new 调用
if (this instanceof F)
{
return new fn(...args, ...innerArgs);
}
return fn.apply(context, [...args, ...innerArgs]);
}
}
使用数组的 join 方法实现字符串重复¶
let str = "ab";
function repeatStringJoin(s, n) {
return new Array(n + 1).join(s);
}
console.log(repeatStringJoin(str, 2)); // "abab"
// join 方法会在数组的每个元素之间插入指定的字符串——可以用于字符串重复
// 不使用fill,会返回[ 'ab', 'ab', 'ab' ]
this 指向¶
const person = {
name: "Alice",
age: 30,
greet() { // 方法里的this指向调用它的对象
return (`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}
console.log(person.greet());
const anotherPerson = {
name: "Bob",
age: 25
}
// 指向另一个对象的greet方法
anotherPerson.greet = person.greet; // 方法的复用
console.log(anotherPerson.greet()); // Hello, my name is Bob and I am 25 years old.
// this指向调用它的对象
// 箭头函数的this指向定义时所在的作用域
const obj = {
value: 42,
regularFunction: function() {
console.log('Regular Function this.value:', this.value);
},
arrowFunction: () => {
console.log('Arrow Function this.value:', this.value);
}
};
obj.regularFunction(); // 输出: 42
obj.arrowFunction(); // 输出: undefined (或全局对象的value,如果存在)
var 作用范围¶
var a = 10;
function set() {
var a = 20;
var b = 30;
console.log(a); // 20
console.log(b); // 30
a = 10; // 没有这两句外面的a也还是10,因为这里的a是局部变量
console.log(a); // 10
}
set();
console.log(a); // 10
// console.log(b); // ReferenceError: b is not defined
await 常数的机制¶
var a = 0;
var b = async () => {
a += await 10;
console.log(a);
};
b();
a += 5;
console.log(a);
// 输出结果
// 5
// 10
// a+=10的时候 a=0已经被保存
浏览器 api 使用¶
/*
编写一段 JS,在 first 和 last 的位置别插入一个新的 li 元素
<ul class="list">
<!-- first -->
<li><a hre="#link1">link1</a></li>
<li><a hre="#link2">link2</a></li>
<li><a hre="#link3">link3</a></li>
<!-- last -->
</ul>
*/
// 获取 ul 元素
const list = document.querySelector('.list');
// 创建要在 first 位置插入的 li 元素
const newFirstLi = document.createElement('li');
const newFirstLink = document.createElement('a');
newFirstLink.href = '#newlink0'; // 你可以修改链接
newFirstLink.textContent = 'new link 0';
newFirstLi.appendChild(newFirstLink);
// 创建要在 last 位置插入的 li 元素
const newLastLi = document.createElement('li');
const newLastLink = document.createElement('a');
newLastLink.href = '#newlink4'; // 你可以修改链接
newLastLink.textContent = 'new link 4';
newLastLi.appendChild(newLastLink);
// 在第一个 li 之前插入(即列表最前面)
if (list.firstChild) {
list.insertBefore(newFirstLi, list.firstChild);
} else {
// 如果列表为空,直接 append
list.appendChild(newFirstLi);
}
// 在最后一个 li 之后插入(即列表最后面)
const lastLi = list.lastElementChild;
if (lastLi) {
list.insertBefore(newLastLi, null); // 等同于 list.appendChild(newLastLi)
// 或者更直观的写法:
// list.appendChild(newLastLi);
} else {
// 如果没有 li,直接添加到 ul 中
list.appendChild(newLastLi);
}
简单实现响应式函数¶
// 来简单实现一个响应式函数: 能对一个对象内的所有key添加响应式特征, 要求最终的输出如下方代码所示
const reactive = (obj) => {
for (let key in obj) { // key和value是键值对
console.log(key, obj[key]);
let val = obj[key];
if (typeof val === 'object' && val !== null) {
// console.log(obj[key]);
reactive(obj[key])
}
let internalVal = val;
// console.log('val:', val);
Object.defineProperty(obj, key, {
// 直接在对象上定义新属性或修改现有属性并返回该对象
// 第一个参数obj:要在其上定义属性的对象
// 第二个参数key(prop):要定义或修改的属性的名称
// 第三个参数:要定义或修改的属性描述符对象
get() {
return val;
},
set(newVal) {
if (newVal === internalVal) return;
internalVal = newVal;
console.log(`SET key=${key} val=${newVal}`);
}
})
}
}
const data = {
a: 1,
b: 2,
c: {
c1: {
af: 999
},
c2: 4
}
}
reactive(data)
data.a = 5 // SET key=a val=5
data.b = 7 // SET key=b val=7
data.b = 7
data.c.c2 = 4
data.c.c1.af = 121 //SET key=af val=121
data.a = 5
/*Object.defineProperty的作用
是 ES5 提供的一个 API,用于 精确地添加或修改对象的某个属性,并可以定义该属性的 特性(如可枚举、可配置、可写,以及 getter/setter)。
在实现响应式系统(如 Vue 2.x)中,通过 getter 拦截读取(可以在这里做依赖收集),通过 setter 拦截写入(可以在这里通知视图更新)。
它是实现“数据劫持”的基础。*/
Promise 链式调用*¶
class Task{
constructor()
{
// 返回一个已完成状态的Promise,每次调用,都在后面then一个新任务
this.queue = Promise.resolve();
}
log(msg)
{
this.queue = this.queue.then(() => {
console.log(msg);
})
return this;
}
wait(duration)
{
this.queue = this.queue.then(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration * 1000);
})
})
return this;
}
}
// 测试代码
const task = new Task();
task.log('Task 1 started')
.wait(2)
.log('Task 1 completed after 2 seconds')
.wait(1)
.log('Task 2 started after 1 second wait')
.wait(3)
.log('Task 2 completed after 3 seconds wait');
实现 PromiseAll*¶
function promiseAll(promises) {
return new Promise((resolve, reject) => {
// 如果输入不是数组,直接返回一个已解决的 Promise
if (!Array.isArray(promises)) {
return reject(new TypeError('Arguments must be an array'));
}
const results = [];
let completedCount = 0;
const promiseCount = promises.length;
// 如果传入空数组,直接返回一个已解决的 Promise
if (promiseCount === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
// 确保每个元素都是 Promise,如果不是则用 Promise.resolve 包装
Promise.resolve(promise)
.then((result) => {
results[index] = result;
completedCount++;
// 当所有 Promise 都完成时,解决返回的 Promise
if (completedCount === promiseCount) {
resolve(results);
}
})
.catch((error) => {
// 任何一个 Promise 被拒绝,立即拒绝返回的 Promise
reject(error);
});
});
});
}
// 测试用例
const promise1 = Promise.resolve(1);
const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 100));
const promise3 = Promise.resolve(3);
promiseAll([promise1, promise2, promise3])
.then((results) => {
console.log(results); // [1, 2, 3]
})
.catch((error) => {
console.error(error);
});
// 测试拒绝情况
const promise4 = Promise.reject('Error occurred');
promiseAll([promise1, promise2, promise4])
.then((results) => {
console.log(results);
})
.catch((error) => {
console.error(error); // "Error occurred"
});
ts keyof 使用¶
// 获取对象类型的所有 key 的联合类型
type Person = {
name: string;
age: number;
gender: string;
};
type PersonKeys = keyof Person;
// 等价于:'name' | 'age' | 'gender'
// 使用示例
const key: PersonKeys = 'name'; // 正确
// const key2: PersonKeys = 'height'; // 报错,'height' 不是 Person 的 key
// 1. 类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key]; // 安全地获取属性值
}
const person = { name: "Alice", age: 25 };
const name2 = getProperty(person, "name"); // 正确
// const unknown = getProperty(person, "unknownKey"); // 编译时报错
ts MyReturnType实现¶
function foo(): number {
return 1;
}
type FooReturnType = ReturnType<typeof foo>; // number
type MyReturnType<F extends (...args: any[]) => any> = F extends (...args: any[]) => infer R ? R : never;
ts 泛型¶
// 使用泛型的例子:保持类型安全+灵活性
// 1. 泛型函数
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString"); // 明确指定类型为 string
console.log(output1.toUpperCase()); // 正常,有类型提示
let output2 = identity(100); // 自动推断为 number 类型
// console.log(output2.toUpperCase()); // 报错,因为 number 没有 toUpperCase 方法
// 2. 泛型接口
interface Box<T> {
value: T;
}
let numberBox: Box<number> = { value: 123 };
let stringBox: Box<string> = { value: "hello" };
// 3. 泛型类
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("TypeScript");
// textStorage.addItem(100); // 报错,只能添加 string
const numStorage = new DataStorage<number>();
numStorage.addItem(100);
// 使用extends约束泛型
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // 字符串有 length 属性
logLength([1, 2, 3]); // 数组也有 length
// logLength(123); // 报错,number 没有 length 属性