跳转至

前端面试重要代码内容之 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 属性