面试高频题(持续更新...)

245 阅读12分钟
  • 说说async、await的设计和实现
    • 设计: sync/await 是基于 Promise 的语法糖. 简化基于 Promise 的异步代码, 提高了代码可读性, 更直观的处理异步逻辑.避免了回调地域问题
    • 预备知识 了解generator
    • 实现: 底层是基于 Promise 和 生成器函数 , 当一个函数被标记为 async 时,它会隐式地返回一个 Promise 对象. async 函数内部的代码会被自动封装为一个 Promise 对象,并且会在函数执行完毕后 resolve(解决)这个 Promise。await 关键字会暂停当前 async 函数的执行,然后让出执行权给调用栈上的代码,直到 await 后面的表达式返回结果。本质上, await 会将后面的表达式, 转换成 promise, 使用它(promise)的.then 方法注册一个回调, 以便在 Promise 解决时恢复 async 函数的执行.
    • async/await/generator

  • 深拷贝需要注意哪些问题?
    • JSON.parse(JSON.stringify()) 是常见的拷贝方法, 但是它有以下缺点:

    • 无法拷贝函数function 会被忽略,因为 JSON 不支持函数。

    • 无法拷贝 undefined:对象中的 undefined 属性会被忽略。

    • 无法处理循环引用:如果对象存在循环引用,会导致 TypeError: Converting circular structure to JSON

    • 无法处理特殊对象

    • 日期 Date 对象会被转为字符串。

    • 正则 RegExp 对象会被转为空对象。

    • MapSet 等复杂数据结构无法被正确拷贝。

    • Symbol 属性会被丢弃。

    • 原型链信息丢失

    function deepCloneWithPrototype(obj, hash = new WeakMap()) {
      if (obj === null || typeof obj !== 'object') return obj;
    
      // 如果对象已经存在于 weakMap 中,直接返回它
      if (hash.has(obj)) return hash.get(obj);
    
      // 保留原型链
      const clone = Object.create(Object.getPrototypeOf(obj));
      hash.set(obj, clone);
    
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          clone[key] = deepCloneWithPrototype(obj[key], hash);
        }
      }
    
      return clone;
    }
    

    Object.assign 也可以拷贝对象,也是属于浅拷贝. 但是无法拷贝特殊对象,比如原型链上的属性、不可枚举属性、Symbol 类型的属性, 多层对象的情况下, 它也只是复制对象的引用. 且它还会覆盖属性.

    多层对象,复制引用代码说明
    const obj1 = {
     a: 1,
     b: {
       c: 2
     }
    };
    
    const obj2 = Object.assign({}, obj1);
    
    obj2.b.c = 3;
    
    console.log(obj1); // { a: 1, b: { c: 3 } }
    console.log(obj2); // { a: 1, b: { c: 3 } }
    

  • 判断数组的方法有哪些?手写一个instanceof方法
    • Array.isArray()

      const arr = [1, 2, 3];
      console.log(Array.isArray(arr)); // true
      
    • instanceof

      const arr = [1, 2, 3];
      console.log(arr instanceof Array); // true
      
    • Object.prototype.toString.call()

      const arr = [1, 2, 3];
      console.log(Object.prototype.toString.call(arr) === '\[object Array]'); // true
      
    • constructor

      const arr = [1, 2, 3];
      console.log(arr.constructor === Array); // true
      

      !!!需要注意 constructor 可以被修改

      image-20241106234938560.png

      优缺点比较:

      方法优点缺点
      Array.isArray()简单、可靠,推荐使用需要 ES5 支持(但基本已被现代浏览器支持)
      instanceof直观,适合面向对象思维不能跨 iframe 或不同的执行上下文使用
      Object.prototype.toString.call()通用性强,适合各种类型的精确检测语法相对复杂,不够简洁(难记)
      constructor使用简单可能被篡改、跨上下文时失效

      手写 (instanceof 原理)

      简单来说 instanceof 是通过检查对象的原型链来判断某个对象是否是某个构造函数的实例。所以可以通过手动遍历对象的原型链来模拟 instanceof 的实现。

      function customInstanceOf(obj, constructor) {
        // 获取目标对象的原型
        let proto = Object.getPrototypeOf(obj);
        // 循环遍历原型链
        while (proto !== null) {
          // 如果原型链中找到了 constructor.prototype,返回 true
          if (proto === constructor.prototype) {
            return true;
          }
          // 继续向上查找原型链
          proto = Object.getPrototypeOf(proto);
        }
        // 如果遍历完整个原型链,未找到 constructor.prototype,返回 false
        return false;
      }
      // 测试
      const arr = [1, 2, 3];
      console.log(customInstanceOf(arr, Array)); // true
      console.log(customInstanceOf(arr, Object)); // true
      console.log(customInstanceOf(arr, Function)); // false
      

  • 说说跨域问题
    • 首先要明确一点: 跨域请求是浏览器的 同源策略 导致的. 浏览器限制脚本内发起的跨源 HTTP 请求. 而并不是简单的前后端的接口请求问题.

    • 同源策略 指的是当浏览器执行跨源请求时,出于安全性的考虑,阻止某些类型的请求或响应

    • 协议 + 端口 + 域名, 三个中有一个不一致则会出现跨域(剩下的 path 就无所谓了)

    • 如何处理跨域问题?

      1. CORS(Cross-Origin Resource Sharing)(跨域资源共享)

      2. JSONP

        较早的跨域解决方案, 主要用于 GET 请求, 其原理是利用 <script> 标签不受同源策略限制的特性来加载跨域的 JavaScript 文件, 服务器返回的数据被包装在一个函数调用中

        例如:

        <script>
         function handleResponse(data) {
           console.log(data); // 处理跨域返回的数据
         }
        </script>
        
        <script src="https://example.com/api?callback=handleResponse"></script>
        
      3. 反向代理

        反向代理是通过配置一个中间层服务器(即代理服务器),客户端向代理服务器发送请求,代理服务器再将请求转发到目标服务器, 由于客户端和代理服务器是同源的,因此不会有跨域问题。

        例如 nginx 代理: (假设前端代码运行在 http://frontend.com网址,接口地址是api.server.com, 可以通过以下反向代理, 转发请求)

        server {
          listen 80;
          server_name frontend.com;
        
          location /api/ {
            proxy_pass http://api.server.com;
            proxy_set_header Host api.server.com;
          }
        }
        
      4. WebSocket (示例代码)

        const socket = new WebSocket("ws://example.com/socket");
        
        socket.onopen = function(event) {
          console.log("WebSocket is open now.");
        };
        
        socket.onmessage = function(event) {
          console.log("Received data: " + event.data);
        };
        
      5. 特定的跨域场景,可以通过 iframewindow.postMessage 来实现跨域通信


  • ES5怎么实现继承?讲讲对原型链的理解
    不好讲...想一下

  • require和import的区别?
    • 先总结一下

      特性require (CommonJS)import (ES6 模块)
      模块系统CommonJSES6 模块
      语法const x = require('x')import x from 'x'
      加载方式同步异步(静态)
      动态导入支持通过 import() 实现
      缓存有缓存有缓存
      执行时机运行时编译时
      环境支持Node.js现代浏览器、Node.js
      导出方式module.exportsexportexport default
    1. 模块系统

      • requireCommonJS 模块规范的实现,早期在 Node.js 中使用。
      • importES6 (ES2015) 模块系统的语法,它在现代 JavaScript(特别是前端)中得到广泛支持
    2. 语法/用法

      require

      /** require 方式 **/
      // 导出
      module.exports = { 
          foo() {
              console.log('foo'); 
          } 
      };
      // 导入
      const myModule = require('./myModule');
      myModule.foo();  // 输出 'foo'
      

      import

      /** import 方式 **/
      
      // 默认导出
      export default function() {
        console.log('default export');
      }
      // 默认导出对应的默认导入
      import myModule from './myModule'; myModule(); // 输出 'default export'
      
      /** import 方式 **/
      
      // 命名导出
      export function foo() {
        console.log('foo');
      }
      
      export const bar = 42;
      
      // 命名导出对应的导入
      import { foo, bar } from './myModule'; 
      foo(); // 输出 'foo'
      console.log(bar); // 输出 42
      
    3. 加载时机

      • require运行时加载模块,即模块是在代码执行到 require 语句时动态加载的,因此可以在程序运行的过程中根据条件加载不同的模块. 例如:
        if (condition) {
            const moduleA = require('./moduleA');
         } else {
            const moduleB = require('./moduleB');
        }
        
      • import编译时加载模块。ES6 模块在编译时就确定了模块的依赖关系,模块会在代码执行之前被加载,因此不能在运行时动态引入模块。
        if (condition) {
           import './moduleA';  // 错误!import 不能在条件语句中使用
        }
        
        但是!!! 因为 import 是静态的,JavaScript 引擎可以在代码编译阶段对模块进行依赖分析和优化。这也是 ES6 模块系统的一个重要设计目标,可以帮助实现更高效的打包和优化。
    4. 加载方式

      • require同步加载模块的 例如:
        const fs = require('fs');
        console.log(fs); // 马上可以拿到 fs 模块
        
      • import(ES6)是异步加载的 例如:
        // 动态 import 
         import('./moduleA').then(moduleA => {
             moduleA.doSomething();
          }).catch(err => {
             console.error('Failed to load moduleA', err);
          });
        
    5. 支持环境

    • require 是 Node.js 原生支持的模块系统,因此在 Node.js 环境下可以直接使用。

    • 浏览器不原生支持 CommonJS 模块系统。如果要在浏览器中使用 require,需要通过工具(如 WebpackBrowserify)进行打包,将 CommonJS 模块转换为浏览器可执行的代码。

    • import ES6 模块系统在大多数现代浏览器(如 Chrome、Firefox、Safari、Edge 等)中原生支持。可以通过 <script type="module"> 标签在浏览器中使用 ES6 模块。例如:

      <script type="module">
          import { foo } from './module.js';
          foo();
      </script>
      
    1. 灵活性
      • require 支持动态导入,因此可以在代码的任意地方根据条件加载模块。
      • require 加载的模块会被缓存,当同一个模块被多次 require 时,后续的 require 调用会直接返回缓存的模块,而不会重新执行模块代码。
      • import 是静态的,必须在模块的顶层导入,不能在代码块内条件性导入。
      • import ES6 模块也是单例的,导入的模块只会加载一次,并且是通过引用来共享模块实例。

    其他import 和 require 区别的文章


  • 手写new 操作符
    • 首先我们要了解 new 的工作原理 new 操作符的工作原理如下

    1. 创建一个新的空对象。
    2. 将新对象的原型(__proto__)链接到构造函数的原型(即 Constructor.prototype)。
    3. 使用新对象作为 this 的上下文,执行构造函数。
    4. 如果构造函数显式返回一个对象,则返回该对象;否则返回新创建的对象。
        function myNew(Constructor, ...args) {
          // 1. 创建一个新的空对象,并将其原型链接到 Constructor.prototype
          const obj = Object.create(Constructor.prototype);
    
          // 2. 执行构造函数,将新对象绑定为构造函数内的 this
          const result = Constructor.apply(obj, args);
    
          // 3. 如果构造函数返回了一个对象,则返回该对象;否则返回新对象
          return result !== null && (typeof result === 'object' || typeof result === 'function')
            ? result
            : obj;
        }
    
  • Map 和 object 的区别
    特性ObjectMap
    键的类型只能是字符串或 Symbol可以是任何类型,例如对象、数组、函数
    键的顺序键的顺序无保证(ES6 后大部分情况有序)键按照插入顺序排序
    性能针对字符串键进行了优化针对频繁增删操作和任意类型键进行了优化
    获取键的大小需要手动计算(Object.keys().length可以直接使用 map.size
    原型链存在原型链(可能会有继承属性)仅存储用户定义的键值对,无原型链
    迭代方式需要手动使用 for...inObject.keys()可以直接使用内置迭代器 for...ofmap.keys()
    序列化和克隆需要手动实现深拷贝可通过 Map 的方法轻松实现
    使用场景通常用于表示结构化数据(如对象模型)更适合用于需要频繁增删查操作的键值对集合
  • Map 和 weakMap 的区别,weakMap 的 key 为什么只能是对象

    1. 区别

    特性MapWeakMap
    键的类型可以是任意类型(对象、字符串、数字等)只能是对象(如普通对象、数组、函数等)
    值的类型可以是任意类型可以是任意类型
    键是否弱引用键是强引用键是弱引用(不会阻止垃圾回收)
    键值对的迭代支持迭代(map.keys()map.values()不支持迭代
    存储大小的获取可以使用 map.size 获取无法获取大小
    垃圾回收键是强引用,垃圾回收不会清理键是弱引用,垃圾回收会自动清理失去引用的键值对
    常见用途用于存储需要频繁访问的键值对用于存储与对象相关的临时数据,且不干扰垃圾回收

    2.weakMap 的 key 为什么只能是对象

    WeakMap 的设计目的是为了实现弱引用,而弱引用的特点是不会阻止对象被垃圾回收。要理解这一点,需要从以下几个方面分析:

    2.1 弱引用和垃圾回收机制
    • 强引用(Map 的键)

      • 如果一个对象被引用(如被设置为 Map 的键),垃圾回收机制无法回收该对象,除非显式地删除它。
      • 即使对象失去了所有其他引用,只要它仍作为 Map 的键,就会占用内存。
    • 弱引用(WeakMap 的键)

      • 如果对象只被引用为 WeakMap 的键,那么当对象失去了其他引用时,垃圾回收机制可以自动回收该对象以及对应的键值对。
      • WeakMap 不会阻止对象被垃圾回收,因此适合存储临时、私有的关联数据。
    2.2 为什么键只能是对象
    • 原始值(如字符串、数字)是不可变的

      • 原始值(如字符串、数字)是不可变的,且不是引用类型。因此,它们无法被垃圾回收。
      • 如果允许原始值作为键,WeakMap 就无法实现弱引用,因为原始值始终存在且不会被回收。
    • 对象是引用类型

      • 对象是引用类型,可以被垃圾回收机制追踪。WeakMap 的弱引用特性依赖于这一点。
      • 当对象失去了所有强引用时,垃圾回收机制可以安全地回收该对象以及 WeakMap 中与该对象关联的键值对。
    2.3 弱引用的实际意义
    • 内存优化

      • 使用 WeakMap 时,无需手动清理无用的键值对。垃圾回收机制会自动清理不再需要的键值对,从而防止内存泄漏。
    • 私有数据存储

      • WeakMap 常用于存储与对象相关的私有数据,确保数据的生命周期与对象绑定,而无需显式管理数据的清理。

  • set 是什么,有什么特点
  • weakset 是什么,有什么特点
  • symbol 是什么,有什么特点
  • Generator 是什么,有什么特点- Promise 是什么,有什么特点
  • async/await 是什么,有什么特点Reflect 是什么,有什么特点
  • Proxy 是什么,有什么特点
  • JS 加载 defer 和 async 的区别二什么是类数组,手写一个类数组 处理类数组的方法有哪些
  • vue 封装的数组方法,手写一下
  • vue data 属性值发生变化,会不会立即更新
  • vue watch 监听到属性值发生变化,是怎么处理的
  • mixin 的原理
  • extends 的原理
  • 自定义指令的原理和用法
  • 什么是高阶函数,什么是高阶组件
  • v-if v-for 为什么不能连用
  • v-for 事件代理
  • 什么是事件代理
  • 手写 v-for
  • Object.freeze 的作用
  • 手写 EventBus
  • provide inject 的原理
  • vue parent 和 children 之间的通信
  • attrsattrs 和 listeners 的作用
  • 手写 vue router
  • 手写 vuex
  • vuex 是怎么全局注册的
  • redux 和 vuex 的区别
  • composition api 和 vue2 的区别
  • 虚拟dom
  • 手写 diff
  • 如何借鉴React diff算法的思想,实现各种情况树节点的更新
  • 讲讲webpack的整个工作流程
  • 有没有用过webpack的loader解决过一些具体的场景问题?
  • 怎么让中间页携带上cookie?