JS中级面试题 50道及答案

一、高级概念与原理

1. 解释JavaScript的事件循环机制,包括宏任务和微任务。

答案详解:

JavaScript是单线程的,但通过事件循环(Event Loop)可以处理异步任务。事件循环的工作流程如下:

执行同步代码,这属于宏任务。

当调用栈为空时,检查微任务队列,执行所有微任务。

然后开始下一轮事件循环,执行一个宏任务(从宏任务队列中取一个),然后再执行所有微任务。

如此循环。

常见的宏任务:setTimeout, setInterval, setImmediate, I/O, UI rendering

常见的微任务:Promise.then, process.nextTick, MutationObserver

注意:process.nextTick的优先级高于Promise.then。

2. 如何实现一个Promise.all?

答案详解:
Promise.all接收一个Promise数组,返回一个新的Promise。当所有Promise都成功时,返回的结果数组与传入的Promise数组顺序一致;如果有一个失败,则返回的Promise立即失败。

实现要点:

返回一个新的Promise。

遍历传入的Promise数组,用Promise.resolve包装,确保每个都是Promise。

统计成功的个数,当个数等于数组长度时,resolve结果数组。

任何一个失败,则立即reject。

javascript 复制代码
Promise.myAll = function(promises) {
    return new Promise((resolve, reject) => {
        let results = [];
        let count = 0;
        promises.forEach((promise, index) => {
            Promise.resolve(promise).then(res => {
                results[index] = res;
                count++;
                if (count === promises.length) {
                    resolve(results);
                }
            }).catch(reject);
        });
    });
};

3. 如何实现一个Promise.race?

答案详解:
Promise.race接收一个Promise数组,返回一个新的Promise。该Promise的结果由第一个完成的Promise决定。

javascript 复制代码
Promise.myRace = function(promises) {
    return new Promise((resolve, reject) => {
        promises.forEach(promise => {
            Promise.resolve(promise).then(resolve).catch(reject);
        });
    });
};

4. 解释async/await的工作原理。

答案详解:
async/awaitGenerator函数的语法糖,基于Promise实现。async函数返回一个Promise,await后面通常是一个Promise,如果不是,会被转成Promise。async函数内部遇到await时,会暂停执行,直到await后面的Promise状态改变,然后继续执行。

async/await使得异步代码看起来像同步代码,避免了回调地狱。

5. 什么是Generator函数?如何自动执行一个Generator函数?

答案详解:
Generator函数是ES6引入的一种异步编程解决方案,可以暂停执行和恢复执行。形式上,Generator函数是一个普通函数,但是有两个特征:一是function关键字与函数名之间有一个星号;二是函数体内部使用yield表达式。

自动执行Generator函数的方法有两种:

使用co库。

自己编写一个自动执行器,原理是不断调用Generator函数的next方法,直到done为true。

javascript 复制代码
function run(gen) {
    const g = gen();
    function next(data) {
        const result = g.next(data);
        if (result.done) return result.value;
        result.value.then(data => next(data));
    }
    next();
}

注意:这里假设yield后面都是Promise。

6. 什么是函数式编程?JavaScript中哪些特性支持函数式编程?

答案详解:

函数式编程是一种编程范式,强调函数的应用和组合,避免状态改变和可变数据。JavaScript中支持函数式编程的特性包括:

  1. 高阶函数(函数可以作为参数和返回值)
  2. 闭包。
  3. 箭头函数(简洁的函数表达式)。
  4. 数组方法如map、filter、reduce。
  5. 纯函数的概念

7. 什么是纯函数?有什么优点?

答案详解:

纯函数是指对于相同的输入,总是得到相同的输出,而且没有任何可观察的副作用。优点包括:

  1. 可缓存(因为相同输入对应相同输出)。
  2. 易于测试。
  3. 易于并行处理(因为没有副作用,不依赖外部状态)。

8. 解释JavaScript中的柯里化(Currying)和部分应用(Partial Application)

答案详解:

柯里化是将一个多参数函数转换成一系列使用一个参数的函数的技术。部分应用是固定一个函数的一些参数,然后产生另一个更小参数的函数。

javascript 复制代码
// 柯里化
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 partial(fn, ...fixedArgs) {
    return function(...remainingArgs) {
        return fn.apply(this, fixedArgs.concat(remainingArgs));
    };
}

9. 什么是函数组合(Compose)?如何实现?

答案详解:

函数组合是将多个函数组合成一个新函数的过程,新函数执行时,按照从右到左的顺序依次执行传入的函数。

javascript 复制代码
function compose(...fns) {
    return function(x) {
        return fns.reduceRight((acc, fn) => fn(acc), x);
    };
}

// 或者使用箭头函数
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

10. 解释什么是惰性函数(Lazy Function)?

答案详解:

惰性函数是指函数执行一次后,内部会重新定义自身,以便后续调用时直接使用新的定义,从而优化性能。常用于条件判断,避免每次调用都进行判断。

javascript 复制代码
function foo() {
    if (condition) {
        foo = function() { /* 实现A */ };
    } else {
        foo = function() { /* 实现B */ };
    }
    return foo();
}

11. 解释什么是记忆函数(Memoization)?如何实现?

答案详解:

记忆函数是一种缓存技术,将函数的计算结果缓存起来,当再次以相同参数调用时,直接返回缓存的结果,避免重复计算。

javascript 复制代码
function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) {
            return cache[key];
        }
        const result = fn.apply(this, args);
        cache[key] = result;
        return result;
    };
}

12. 解释JavaScript中的迭代器(Iterator)和可迭代对象(Iterable)。

答案详解:

迭代器是一个对象,它有一个next方法,每次调用返回一个包含value和done属性的对象。可迭代对象是实现了[Symbol.iterator]方法的对象,该方法返回一个迭代器。

13. 什么是生成器(Generator)?它与迭代器有什么关系?

答案详解:

生成器是一种特殊的函数,可以返回一个生成器对象,该对象既是迭代器,也是可迭代对象。生成器函数使用function*定义,内部使用yield来产生值。

14. 解释什么是尾调用优化(Tail Call Optimization)?JavaScript中哪些情况支持?

答案详解:

尾调用是指一个函数在最后一步调用另一个函数。尾调用优化是指编译器或运行时环境将尾调用优化为跳转,从而避免额外的栈帧。ES6的严格模式下支持尾调用优化。

15. 解释什么是JavaScript中的严格模式(Strict Mode)?有哪些限制?

答案详解:

严格模式是ES5引入的一种模式,对JavaScript的语法和行为做了一些限制,使代码更安全、更优化。启用方式:在脚本或函数开头加上"use strict";。

限制包括:

  1. 变量必须声明后使用。
  2. 禁止使用with语句。
  3. 禁止this指向全局对象(在函数中)。
  4. 禁止删除变量。
  5. 函数参数不能重名。

二、对象与原型

16. 解释Object.create(null)和{}的区别。

答案详解:
Object.create(null)创建一个空对象,该对象没有原型(即__proto__为null),因此不会继承Object.prototype上的方法(如toString、hasOwnProperty等)。而{}创建的对象继承自Object.prototype

17. 如何实现一个类(Class)的私有属性和方法?

答案详解:

在ES6中,可以通过以下方式模拟私有属性和方法:

使用Symbol作为属性名,但通过Object.getOwnPropertySymbols仍然可以访问。

使用WeakMap存储私有属性。

使用闭包,在构造函数中定义变量和函数,然后通过特权方法访问。

ES2020引入了私有字段和私有方法,使用#作为前缀。

javascript 复制代码
class MyClass {
    #privateField = 10;
    #privateMethod() {
        return this.#privateField;
    }
}

18. 解释什么是属性描述符(Property Descriptor)?

答案详解:

属性描述符是一个对象,用来描述一个属性的特性。分为两种:数据描述符和存取描述符。

数据描述符包括:value, writable, enumerable, configurable

存取描述符包括:get, set, enumerable, configurable

通过Object.defineProperty可以定义属性描述符。

19. 如何实现对象的深冻结(Deep Freeze)?

答案详解:

深冻结是指冻结对象及其所有嵌套对象。

javascript 复制代码
function deepFreeze(obj) {
    Object.freeze(obj);
    for (let key in obj) {
        if (obj.hasOwnProperty(key) && typeof obj[key] === 'object' && obj[key] !== null) {
            deepFreeze(obj[key]);
        }
    }
    return obj;
}

20. 解释什么是原型污染?如何防止?

答案详解:

原型污染是指攻击者通过修改对象的原型(如Object.prototype)来影响所有对象的行为。防止方法:

  1. 使用Object.create(null)创建无原型的对象。
  2. 避免使用用户输入直接修改对象属性。
  3. 使用Object.freeze冻结Object.prototype。

21. 如何实现一个对象的深拷贝,考虑循环引用?

答案详解:

深拷贝需要递归地复制对象的所有属性。循环引用会导致递归无限循环,因此需要使用一个映射表来存储已经拷贝过的对象。

javascript 复制代码
function deepClone(obj, map = new WeakMap()) {
    if (obj === null || typeof obj !== 'object') return obj;
    if (map.has(obj)) return map.get(obj);

    const clone = Array.isArray(obj) ? [] : {};
    map.set(obj, clone);

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            clone[key] = deepClone(obj[key], map);
        }
    }
    return clone;
}

22. 解释什么是ProxyReflect,以及它们的应用场景。

答案详解:

Proxy用于创建一个对象的代理,可以拦截并自定义对象的操作(如属性读取、赋值等)。Reflect是一个内置对象,提供拦截JavaScript操作的方法,与Proxy的方法一一对应。

应用场景:数据绑定、观察者模式、日志记录、验证等。

23. 如何使用Proxy实现一个简单的数据绑定?

答案详解:

javascript 复制代码
function observe(obj, callback) {
    return new Proxy(obj, {
        set(target, key, value) {
            target[key] = value;
            callback(key, value);
            return true;
        }
    });
}

const obj = observe({}, (key, value) => {
    console.log(`属性${key}被设置为${value}`);
});
obj.name = 'John'; // 输出:属性name被设置为John

三、异步编程

24. 如何取消一个Promise

答案详解:
Promise本身无法取消,但可以通过包装一个可取消的Promise。

javascript 复制代码
function cancellablePromise(promise) {
    let cancel;
    const wrappedPromise = new Promise((resolve, reject) => {
        cancel = reject;
        promise.then(resolve, reject);
    });
    wrappedPromise.cancel = cancel;
    return wrappedPromise;
}

// 使用
const p = new Promise(resolve => setTimeout(() => resolve('done'), 5000));
const cp = cancellablePromise(p);
cp.then(console.log).catch(err => console.log('取消', err));
setTimeout(() => cp.cancel('用户取消'), 2000);

25. 如何实现一个并发限制的异步调度器?

答案详解:

要求:最多同时运行n个任务,多余的任务排队等待。

javascript 复制代码
class Scheduler {
    constructor(limit) {
        this.limit = limit;
        this.running = 0;
        this.queue = [];
    }

    add(promiseCreator) {
        return new Promise((resolve) => {
            this.queue.push(() => promiseCreator().then(resolve));
            this.run();
        });
    }

    run() {
        while (this.running < this.limit && this.queue.length) {
            const task = this.queue.shift();
            this.running++;
            task().then(() => {
                this.running--;
                this.run();
            });
        }
    }
}

26. 如何实现一个sleep函数?

答案详解:

javascript 复制代码
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用
async function demo() {
    console.log('等待1秒');
    await sleep(1000);
    console.log('完成');
}

27. 解释什么是Web Worker?如何使用?

答案详解:
Web WorkerJavaScript创建多线程环境,允许在主线程之外运行脚本,避免阻塞UI。使用步骤:

  1. 创建Worker脚本文件。
  2. 在主线程中实例化Worker:new Worker('worker.js')。
  3. 通过postMessage发送消息,onmessage接收消息。

注意:Worker中无法访问DOM,且同源策略限制。

28. 解释什么是Service Worker?它有什么作用?

答案详解:
Service Worker是一种特殊的Web Worker,它充当浏览器和网络之间的代理服务器,可以拦截和处理网络请求,实现离线缓存、消息推送等功能。Service Worker在独立的线程中运行,与页面无关,即使页面关闭也可以运行。

29. 如何实现一个简单的EventEmitter

答案详解:
EventEmitter是事件发布/订阅模式的实现。

javascript 复制代码
class EventEmitter {
    constructor() {
        this.events = {};
    }

    on(event, listener) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(listener);
    }

    emit(event, ...args) {
        if (this.events[event]) {
            this.events[event].forEach(listener => listener.apply(this, args));
        }
    }

    off(event, listener) {
        if (this.events[event]) {
            this.events[event] = this.events[event].filter(l => l !== listener);
        }
    }
}

四、性能与优化

30. 解释什么是防抖(Debounce)和节流(Throttle)?如何实现?

答案详解:

防抖:事件触发后延迟执行,如果在此期间再次触发,则重新计时。

节流:在一定时间内只执行一次。

javascript 复制代码
// 防抖
function debounce(fn, delay) {
    let timer;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => fn.apply(this, args), delay);
    };
}

// 节流
function throttle(fn, delay) {
    let lastTime = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastTime >= delay) {
            fn.apply(this, args);
            lastTime = now;
        }
    };
}

31. 解释什么是虚拟DOM?它的优点是什么?

答案详解:

虚拟DOM是一个JavaScript对象,用来描述真实DOM。当状态变化时,生成新的虚拟DOM,然后与旧的虚拟DOM进行对比(diff算法),找出差异,然后只更新真实DOM中需要修改的部分。

优点:

  1. 提高性能,避免直接操作真实DOM带来的重排重绘。
  2. 跨平台,虚拟DOM可以转换为其他平台的UI。

32. 如何实现一个简单的虚拟DOM

答案详解:

虚拟DOM实现包括三个步骤:创建虚拟DOM树、比较两棵虚拟DOM树的差异、将差异应用到真实DOM

由于实现较为复杂,这里只给出简化版本。

javascript 复制代码
// 创建虚拟DOM
function createElement(tag, props, children) {
    return { tag, props, children };
}

// 渲染虚拟DOM到真实DOM
function render(vnode) {
    if (typeof vnode === 'string') {
        return document.createTextNode(vnode);
    }
    const el = document.createElement(vnode.tag);
    for (let key in vnode.props) {
        el.setAttribute(key, vnode.props[key]);
    }
    vnode.children.forEach(child => {
        el.appendChild(render(child));
    });
    return el;
}

33. 解释什么是时间分片(Time Slicing)?

答案详解:

时间分片是一种将长任务拆分成多个小任务,在每一帧中执行一部分,以避免阻塞主线程的技术。通常使用requestAnimationFrame或setTimeout来实现。

34. 如何优化JavaScript代码的性能?

答案详解:

  1. 避免全局查找,将全局变量缓存到局部。
  2. 避免使用with语句。
  3. 减少重排和重绘。
  4. 使用事件委托。
  5. 使用防抖和节流。
  6. 使用Web Worker处理耗时任务。
  7. 使用虚拟DOM。
  8. 使用时间分片。

五、浏览器相关

35. 解释什么是同源策略(Same-origin Policy)?如何解决跨域问题?

答案详解:

同源策略要求协议、域名、端口都相同。跨域解决方案:

JSONP:利用script标签不受同源策略限制,但只支持GET请求。

CORS:服务器设置Access-Control-Allow-Origin响应头。

代理服务器:同源策略是浏览器的限制,服务器之间没有限制。

postMessage:用于窗口间通信。

WebSocket:不受同源策略限制。

36. 解释什么是CORS(跨域资源共享)?

答案详解:
CORS是一种机制,允许服务器声明哪些源可以访问资源。浏览器在发送跨域请求时,会自动添加Origin头,服务器返回的响应头中需要包含Access-Control-Allow-Origin,如果匹配,则浏览器允许跨域。

对于非简单请求(如PUT、DELETE或Content-Type为application/json),浏览器会先发送预检请求(OPTIONS),服务器响应通过后,才会发送实际请求。

37. 解释什么是XSS攻击?如何防范?

答案详解:
XSS(跨站脚本攻击)是指攻击者将恶意脚本注入到网页中,当用户浏览网页时,脚本执行,从而窃取用户信息。防范措施:

  1. 对用户输入进行转义(如将<转义为<)。
  2. 使用CSP(内容安全策略)。
  3. 设置HttpOnly Cookie,防止脚本访问Cookie

38. 解释什么是CSRF攻击?如何防范?

答案详解:

CSRF(跨站请求伪造)是指攻击者诱导用户访问一个页面,该页面自动发送请求到用户已登录的网站,利用用户的登录状态进行非法操作。防范措施:

  1. 使用CSRF Token,服务器生成Token,客户端在请求中携带,服务器验证。
  2. 验证Referer头。
  3. 使用SameSite Cookie属性。

39. 解释什么是点击劫持(Clickjacking)?如何防范?

答案详解:

点击劫持是指攻击者将一个透明的iframe覆盖在正常网页上,诱导用户点击,从而触发恶意操作。防范措施:

设置X-Frame-Options响应头,禁止页面被嵌入到iframe中。

使用CSPframe-ancestors指令。

模块化与工程化

40. 解释CommonJSES6 Module的区别。

答案详解:

CommonJS是同步加载,用于服务器端(Node.js)ES6 Module是异步加载,用于浏览器。
CommonJS输出的是值的拷贝,ES6 Module输出的是值的引用。
CommonJS使用require()加载,module.exports导出;ES6 Module使用importexport

41. 解释什么是Tree Shaking?如何实现?

答案详解:
Tree Shaking是指通过静态分析,消除JavaScript上下文中未引用的代码(死代码)。实现条件:使用ES6 Module(静态导入导出),然后使用工具如Webpack、Rollup进行打包。

42. 解释什么是代码分割(Code Splitting)?如何实现?

答案详解:

代码分割是将代码分割成多个小块,然后按需加载,减少初始加载时间。实现方式:

使用Webpack的入口点分割、动态导入(import())或SplitChunksPlugin

使用React.lazySuspense实现组件懒加载。

六、框架与库

43. 解释什么是高阶组件(Higher-Order Component)?

答案详解:

高阶组件是React中的一种模式,它是一个函数,接收一个组件作为参数,返回一个新的组件。用于复用组件逻辑。

44. 解释什么是React Hooks?它解决了什么问题?

答案详解:
React Hooks是React 16.8引入的特性,允许在函数组件中使用状态和其他React特性。解决了类组件中状态逻辑难以复用、复杂组件难以理解等问题。

45. 解释什么是Vue的响应式原理?

答案详解:
Vue2使用Object.defineProperty对数据对象的属性进行劫持,当数据变化时,触发setter,通知依赖进行更新。Vue3使用Proxy实现,可以拦截对象的各种操作。

46. 解释什么是单向数据流和双向数据绑定?

答案详解:
单向数据流 :数据从上向下传递,子组件不能直接修改父组件的数据,需要通过事件通知父组件修改。如React。
双向数据绑定:数据的变化会自动更新到视图,视图的变化也会自动更新数据。如Vue的v-model。

八、算法与数据结构

47. 实现一个函数,将数组转换成树形结构。

答案详解:

假设数组中的每个元素都有id和parentId,根节点的parentId为0。

javascript 复制代码
function arrayToTree(arr) {
    const map = {};
    const tree = [];
    arr.forEach(item => {
        map[item.id] = { ...item, children: [] };
    });
    arr.forEach(item => {
        if (item.parentId === 0) {
            tree.push(map[item.id]);
        } else {
            map[item.parentId].children.push(map[item.id]);
        }
    });
    return tree;
}

48. 实现一个函数,深度优先遍历DOM树。

答案详解:

javascript 复制代码
function dfs(node, callback) {
    callback(node);
    node = node.firstElementChild;
    while (node) {
        dfs(node, callback);
        node = node.nextElementSibling;
    }
}

49. 实现一个函数,广度优先遍历DOM树。

答案详解:

javascript 复制代码
function bfs(root, callback) {
    const queue = [root];
    while (queue.length) {
        const node = queue.shift();
        callback(node);
        let child = node.firstElementChild;
        while (child) {
            queue.push(child);
            child = child.nextElementSibling;
        }
    }
}

50. 实现一个函数,解析URL的查询参数。

答案详解:

javascript 复制代码
function parseQuery(url) {
    const query = {};
    const queryString = url.split('?')[1];
    if (queryString) {
        queryString.split('&').forEach(pair => {
            const [key, value] = pair.split('=');
            query[decodeURIComponent(key)] = decodeURIComponent(value || '');
        });
    }
    return query;
}
相关推荐
LawrenceLan6 小时前
Flutter 零基础入门(十一):空安全(Null Safety)基础
开发语言·flutter·dart
txinyu的博客6 小时前
解析业务层的key冲突问题
开发语言·c++·分布式
码不停蹄Zzz6 小时前
C语言第1章
c语言·开发语言
行者967 小时前
Flutter跨平台开发在OpenHarmony上的评分组件实现与优化
开发语言·flutter·harmonyos·鸿蒙
阿蒙Amon7 小时前
C#每日面试题-Array和ArrayList的区别
java·开发语言·c#
SmartRadio7 小时前
ESP32添加修改蓝牙名称和获取蓝牙连接状态的AT命令-完整UART BLE服务功能后的完整`main.c`代码
c语言·开发语言·c++·esp32·ble
且去填词8 小时前
Go 语言的“反叛”——为什么少即是多?
开发语言·后端·面试·go
知乎的哥廷根数学学派8 小时前
基于生成对抗U-Net混合架构的隧道衬砌缺陷地质雷达数据智能反演与成像方法(以模拟信号为例,Pytorch)
开发语言·人工智能·pytorch·python·深度学习·机器学习
yeziyfx9 小时前
kotlin中 ?:的用法
android·开发语言·kotlin
charlie1145141919 小时前
嵌入式的现代C++教程——constexpr与设计技巧
开发语言·c++·笔记·单片机·学习·算法·嵌入式