【前端面经 | 京东】2024 面试复盘第二弹——京东(部门A)

602 阅读8分钟

总结

京东部门A的面试 总结起来就一个字 —— 爽!

因为当时这个面试是我第一次面试,所以准备并不充分,而且面试问题时也没有记录全,有点小遗憾。不过确实是特别爽,给我问兴奋了。

没有手撕代码,所有的八股文都是根据简历上的项目进行扩展询问的,基本上回答完上一个,下一个就蒙圈的那种。也是那种面试的每个问题,复盘起来都要进行深度学习的那种,所以当时很幸运,开局遇到能力这么强的面试官。而且面试官人也超级好,会引导你进行思考,简直是太nice了!而且后面询问有什么建议,真的很认真很认真的在回答,非常感谢!

面试

1 若此时已经使用百度地图 API 封装了这些功能,如果需要使用百度地图、高德地图、腾讯地图进行多地图开发,如何封装组件?

链接:多地图(百度、高德、腾讯地图)API 组件封装 - 掘金 (juejin.cn)

  1. 提问依据:通过百度地图 API 封装了更新、展示、移除地图撒点等功能
  2. 实现方法:在封装多地图(百度地图、高德地图、腾讯地图)组件时,可以定义一个接口(interface)或一个抽象类(abstract),然后为每个地图 API 实现这个接口或抽象类,这样我的应用程序就可以通过调用统一的接口与不同的地图 API 进行交互,而不需要关心底层的逻辑实现的具体细节

2 为什么 defineProperty 不可以监听数组下标的变化 为什么实际应用中还可以监听到数组的增删?

1. 为什么 defineProperty 不可以监听数组下标的变化?

当通过arr[0]修改元素时 不会触发 Object.definePProperty()的 setter,因为Object.defineProperty() 是针对对象的属性进行设置的,当修改数组的一个元素时 其实是在修改数组对象的一个内部状态 而不是修改一个通过 Object.defineProperty() 定义的属性

2. 为什么实际应用中还可以监听到数组的增删?

虽然 Object.defineProperty() 无法直接监听数组元素的变化,但可以通过数组实例上定义的方法实现增减操作。例如,Vue 通过重写数组的 push、pop、shift、unshift、splice、sort、reverse(七个变异方法)来触发视图更新

3 路由模式 hash 和 history 是基于什么开发的?

1. hash 模式:

基于 URL 中的 hash(#号及其后面部分)来实现路由,当 hash 值变化时 浏览器不会向服务器发送请求 而是通过监听 hashChange 事件 监听 hash 的变化 再通过 dom 操作来更新视图

2. history 模式:

基于 HTML5 History API 的 pushState() 和 replaceState() 方法来修改 URL

4 history 服务端配置有哪些?(回退路由是前端的)

  1. 目的:确保所有的前端路由请求都能正确返回前端应用的入口文件(index.html)而不是返回 404

  2. Apache 服务端配置:

    使用.htaccess 文件 进行以下配置 该配置会确保当请求的 URL 不是真实存在的文件或目录时 将请求重定向到 index.html

    // 在项目的根目录下创建名为.htaccess的文件
       <IfModule mod_rewrite.c>
          RewriteEngine On
          RewriteBase /
          RewriteRule ^index\.html$ - [L]
          RewriteCond %{REQUEST_FILENAME} !-f
          RewriteCond %{REQUEST_FILENAME} !-d
          RewriteRule . /index.html [L]
       </IfModule>
    
  3. Nginx 服务器配置

    修改 nginx.conf 文件 按以下顺序返回文件:首先返回请求的 url 对应文件 若不存在则返回 url 对应目录 都不存在则返回 index.html

    // 在Nginx的配置文件(如nginx.conf)中,为前端应用所在的location块添加以下配置:
    ocation / {
       try_files $uri $uri/ /index.html;
    }
    
  4. Node.js 服务器------使用 Express.js

    配置一个通配符路由来捕获所有未定义的路由请求,并返回 index.html

    // 捕获所有GET请求,并将它们重定向到index.html
    const express = require("express");
    const path = require("path");
    const app = express();
    
    app.use(express.static(path.join(__dirname, "public"))); // 静态资源目录
    
    app.get("*", (req, res) => {
      res.sendFile(path.join(__dirname, "public", "index.html"));
    });
    
    app.listen(3000, () => {
      console.log("Server is running on port 3000");
    });
    

5 浏览器的渲染流程是怎样的?

  1. DNS 查询:浏览器首先检查 URL,以确定要请求的域名所对应的 IP 地址。
  2. 建立 TCP 链接
  3. 发送 HTTP 请求
  4. 等待服务器响应
  5. 渲染内容:
    1. 处理 HTML 构建 DOM 树
    2. 处理 CSS 构建 CSSOM
    3. 构建渲染树(只包含需要显示的节点和这些节点的样式信息(不包括如<head>、display:none 等不可见元素)
    4. 布局绘制
  6. 分层、分块与光栅化
    1. 分层:浏览器会将渲染树的节点进行分层处理 层中某一节点发生改变时 只需要重绘和合成该层就可以
    2. 分块:合成线程将每个图层分为多个小区域(块) 以便并行处理
    3. 光栅化:将图层转换为位图形式的图像(合成线程交给 GPU 进程进行光栅化操作)
  7. 合成显示:GPU 将光栅化后的位图图像合成到屏幕上

6 DNS 解析前会做什么?

DNS 解析前主要会进行域名注册、选择 DNS 服务商、准备 IP 地址、确保域名和服务器状态正常,并理解 DNS 解析方式及服务器类型,以准备开始设置域名与 IP 地址之间的映射关系。

  1. 域名注册:通过“域名注册服务提供商”进行注册和获取
  2. 选择 DNS 服务商:用来管理你的域名解析(阿里云--云解析 DNS 服务)
  3. 准备 IP 地址:获取应用资源(如网站、应用服务等)的服务器 IP 地址。
    1. ECS、独享虚机类:通过登录该产品的控制台 查看到分配的 IP 地址
    2. 网站空间:联系网站空间商获取解析地址
  4. 检查域名和服务器状态:确保域名已经进行了实名认证,并且如果服务器位于中国,还需要确保已经完成了 ICP 备案
  5. 理解 DNS 解析方式:对于网站应用,常见的 DNS 解析方式有 A 记录方式和 CNAME 记录方式
    1. A 记录方式:直接将域名映射到 IP 地址
    2. CNAME 记录方式:允许你将域名指向另一个域名(别名),再由该别名提供 IP 地址服务
  6. 检查其他应用需求:若使用邮箱服务等,还需 MX 记录、CNAME 记录或 TXT 记录等
  7. 理解 DNS 服务器类型:
    1. 主域名服务器:负责维护区域的所有域名信息,
    2. 辅助域名服务器:作为备份
    3. 缓存域名服务器:存储查询结果的缓存
    4. 转发域名服务器:负责非本地域名的查询请求。
  8. 准备解析设置:在以上步骤完成后,登录你的 DNS 服务商的控制台,开始设置解析。这通常包括添加 A 记录、CNAME 记录等,以建立域名和 IP 地址之间的映射关系。

7 为什么会有预检请求,预检请求主要做什么 预检请求只有 post 吗?

  1. 目的:确保跨域请求的安全性 当一个来自不同源的请求 浏览器会先发送一个预检请求(通常为 options 请求) 确认服务器是否允许这样的请求
  2. 作用:向服务器检查实际请求是否可以安全发送
    1. 检查请求的方法(如 PUT、DELETE 等)和请求头(如自定义的头部信息)是否得到服务器的允许
    2. 服务器在收到预检请求后,会检查其 CORS 策略和请求内容,然后决定是否允许该实际请求
  3. 预检请求并不是只有 POST 预检请求的触发条件:
    1. 请求方法只能是 get post head
    2. 请求头包含了自定义的头部信息(Authorization、Content-Type 等)

8 为什么只能同源请求?

  1. 作用:浏览器为了保护用户数据安全 防止恶意网站窃取数据而实施的一种安全策略
  2. 通过限制来自不同源的“文档”或“脚本”对当前文档的读写 该源指协议、域名、端口号均一致
  3. 目的:
    1. 防止跨站脚本攻击(XSS)
    2. 保护隐私:防止一个网站读取另一个网站的数据
    3. 避免数据篡改:避免一个网站修改另一个网站的数据

9 vite 为什么比 webpack 快?

  1. vite 是基于浏览器原生的 ESM(ES Modules)开发的 而 webpack 是通过打包来模拟 ESM
  2. vite 允许在服务器端按需编译和提供源码 避免了全量打包
  3. Webpack 在开发过程中会进行完整的依赖分析和打包 导致了启动速度相对较慢

9 vue 的性能优化有哪些?

  1. 编码优化(综合)

    1. v-for 给每个元素绑定事件->用事件委托
    2. v-for 循环时加上 key,提高 diff 算法的效率
    3. v-for 不要于 v-if 同级使用,避免 v-if 重复执行
    4. 合理使用 v-if 和 v-show,频繁的显示隐藏用 v-show
    5. 把响应式的数据放在 data 中 不要把所有数据都放在 data 中:因为 data 中的数据都会增加 getter、setter 会收集对应的 watcher 消耗性能
    6. 合理使用 computed 和 watch:computed 只有相关响应式数据改变时才会重新计算;watch 适用于执行异步或开销较大的操作
    7. 防抖和节流:优化高频率触发的事件处理函数
  2. 加载优化

    1. 按需加载:使用第三方插件实现按需加载 如滚动到可视区域动态加载
    2. 图片懒加载: 减少不必要的网络请求
    3. 组件懒加载:合理使用路由懒加载、异步组件,尽量拆分组件 提高复用性和维护性, keep-alive 缓存组件
  3. SEO 优化

    1. 预渲染:使用预渲染插件(如 prerender-spa-plugin)将页面提前渲染成静态 HTML 提高首次加载速度(缺点:不能实时 适合内容变化不频繁的场景)
    2. 服务端渲染(SSR):使用服务端渲染技术,将 Vue 组件在服务端渲染成 HTML 字符串后发送给客户端,提高页面加载速度和可访问性。
  4. 用户体验优化(骨架屏)

    1. 利用 vue-content-loader 插件
    2. 利用 Suspense 组件:#fallback 为异步请求中的处理 UI ,即常见的 loading 效果。
    <Suspense>
    <!-- 包含异步的组件 -->
    <ASycComponent></ASycComponent>
    
    <templete #fallback>
    <!-- 骨架屏组件 -->
      <Skeleton>
    </templete>
    
    </Suspense>
    
  5. 打包优化

    1. 使用 CDN:通过 CDN 加载第三方模块 减少应用体积 提高加载速度
    2. 多线程打包:使用 happpack 等工具 进行多线程打包 提高打包速度
    3. 抽离公共文件:使用 splitChunks 等工具 抽离公共文件 减少重复加载
  6. 缓存和压缩

    1. 客户端缓存:利用浏览器的缓存机制 如缓存控制头(Cache-Control) 减少不必要的网络请求
    2. 服务端缓存:在服务端设置缓存策略 如使用 Redis、Memcached 等缓存工具
    3. 服务端压缩(Gzip):使用 Gzip 或 Brotli 等算法对服务端响应进行压缩 减少传输体积

10 原型链和原型对象是什么?

之所以问道原型链 是因为当时响应式中数组为何在不能通过索引进行监听 但数组的增删还是可以被监听到时,面试官进行的引导。

  1. prototype 原型对象:

    1. 本质:只有(构造)函数有这个属性, 本质是一个对象 包含了一些属性和方法 ,可以 prototype.__proto__

    2. 用途:他是为构造函数的实例共享属性和方法的,所有实例中引用的原型都是同一个对象

    3. 注意:使用 prototype 可以把方法(或属性)挂载在原型上,确保这些方法或属性在内存中只被存储一份 存在 Person 构造函数,你希望所有 Person 的实例都能够访问一个名为 greet 的方法。就可以将 greet 方法添加到 Person.prototype 上,而不是在每个实例上单独创建它。这样,所有 Person 的实例都将共享同一个 greet 方法的引用,而不是每个实例都有自己的 greet 方法副本。

      function Person(name, age) {
        this.name = name;
        this.age = age;
      }
      
      // 将 greet 方法挂载在 Person.prototype 上
      Person.prototype.greet = function () {
        console.log(`Hello ${this.name} ${this.age}`);
      };
      
      // 创建 Person 实例
      let john = new Person("John", 30);
      let jane = new Person("Jane", 25);
      
      // 调用 greet 方法
      john.greet(); // 输出: Hello John 30
      jane.greet(); // 输出: Hello Jane 25
      // john.greet 和 jane.greet 引用的是同一个 greet 方法
      console.log(john.greet === jane.greet); // 输出: true本。
      
  2. proto 原型链:

    1. 所有对象都有这个属性,总是指向对应(构造)函数的原型对象——prototype

    2. __proto__总是指向 prototype ,指向的是地址 所以是同一个数据堆

    3. 原型链的终点:Object.prototype

      let obj = { a: 1 }; // 相当于 new Object({a:1})
      // 1.1 obj是对象 所以是__proto__上查找
      console.log(obj.__proto__); // Object
      console.log(obj.__proto__.__proto__); // null 说明obj.__proto__是最后一层了,找到底了
      // 1.2  对象(obj)的原型(__proto__)总是指向这个对象的构造函数(Object)的原型对象(prototype)
      console.log(obj.__proto__ === Object.prototype); // true
      
      // 2 新建构造函数
      function Vue() {
        this.name = "小红";
      }
      const app = new Vue();
      // 2.1 app对象的__proto__一定指向的是app的构造函数Vue的prototype
      console.log(app.__proto__ === Vue.prototype); // true
      console.log(
        (app.__proto__.__proto__ === Vue.prototype.__proto__) ===
          Object.prototype
      ); // true 显示false需要1===2 2===3 1===3
      console.log(app.__proto__.__proto__.__proto__); // null
      console.log(Vue.__proto__ === Function.prototype); // true 构造函数自身的原型链
      
  3. 原型链的查找规则:

    1. 一个实例对象在调用属性和方法时,会依次从实例本身、构造函数原型、原型的原型上去查找。 对象访问属性:在自身属性查找,找不到就去 __proto__原型链上查找,直到找不到为止(显示 null-原型链的顶端)-返回 undifined
    2. 注:原型链总是指向原型对象的 也就是他们都是指向同一个地址同一个堆
  4. 验证某个对象是否从某个函数里面来:constructor

    function Vue() {
      this.name = "小红";
    }
    const app = new Vue();
    console.log(Vue.__proto__.constructor); // ƒ Function() { [native code] }
    console.log(Vue.__proto__.constructor === Function); // true
    // 判断app是否从Vue这个构造函数里面来
    console.log(app.__proto__.constructor === Vue); // true
    
  5. 判断某个属性是自身有的还是原型链上的:hasOwnProperty

    function Own() {
      this.age = 18;
    }
    const own = new Own();
    for (let key in own) {
      // 判断对象的属性是不是他本身上的
      if (own.hasOwnProperty(key)) {
        console.log(key);
      }
    }
    

11 vue的响应式原理是什么?

通过数据劫持和发布订阅者模式来实现,vue 会接收一个模板和 data,对 data 进行递归遍历给每个数据绑定 get set 并添加一个数组(用于收集这个 data 属性依赖的 dom 节点集合,并提供更新方法),如果 get 被出发了就会向数组里添加一个 watcher,set 被触发了,重新赋值,调用数组的方法去通知每一个使用该属性的 watcher,更新对应的 dom 元素

vue 接收一个模板和 data 参数:

  1. 首先将 data 中的数据进行递归遍历,对每个属性执行 Object.defineProperty,定义 get 和 set 函数。并为每个属性添加一个 dep 数组。当 get 执行时,会为调用的 dom 节点创建一个 watcher 存放在该数组中。当 set 执行时,重新赋值,并调用 dep 数组的 notify 方法,通知所有使用了该属性 watcher,并更新对应 dom 的内容。
  2. 将模板加载到内存中,递归模板中的元素,检测到元素有 v-开头的命令或者双大括号的指令,就会从 data 中取对应的值去修改模板内容,这个时候就将该 dom 元素添加到了该属性的 dep 数组中。这就实现了数据驱动视图。在处理 v-model 指令的时候,为该 dom 添加 input 事件(或 change),输入时就去修改对应的属性的值,实现了页面驱动数据。
  3. 将模板与数据进行绑定后,将模板添加到真实 dom 树中。

实现步骤:

  1. 接收一个模板(Compiler 类用于解析模板中的 vue 指令)将 data 中的数据进行递归遍历,定义 getter 和 setter 属性
  2. compile 模板解析指令,实现数据首次渲染到页面:
  3. observe 使用 defineRecative 函数对 data 做 defineProperty 处理,拦截 data 中的每一个数据的 get 和 set
  4. 把每个指令对应的节点绑定上新函数,添加订阅者,如果数据变化,收到通知,更新视图
  5. watcher 订阅者是 observer 和 compile 之间的通信桥梁,作用: 1. 在自身实例化的时候在订阅器内添加自己 2. 自身要有 update()方法 3. 等待属性变动时,调用自身的 update 方法,触发 compile 回调
  6. MVVM 作为数据绑定的入口,整合了 observe、compile、watcher 三者,通过 observe 来监听自己的数据变化,通过 compile 解析指令模板,最后利用 watcher 把 observer 和 compile 联系起来,最终达到数据更新视图更新,视图更新数据更新

12 跨域方法有哪些?

三个允许跨域加载的资源标签:image link script

  1. JSONP :JSON with Padding

    1. 支持:需要对方服务器的支持
    2. 原理:利用 script 标签没有跨域限制的特点,通过动态生成 script 标签加载跨域资源
    3. 缺点:只支持 get 请求,不支持 post 请求
    4. 安全性问题:由于恶意服务器可以返回恶意 js 代码()存在窃取用户 cookie 或跨站脚本攻击(xss)
  2. CORS :跨来源资源共享 Cross-Origin Resource Sharing

    1. 支持:需要浏览器和服务器同时支持
    2. 原理:允许浏览器向跨源服务器发出 XMLHttpRequest 请求,克服 AJAX 只能同源使用的限制
      1. 预检请求:浏览器发送 options 请求到服务器 询问是否允许跨域
      2. 服务器响应:在响应预检请求时 会在 http 头部包含一些 cors 字段(例:哪些源的页面可以加载页面 允许哪些 http 请求方法等)
      3. 实际请求:浏览器发送真正 htp 请求
      4. 处理响应:服务器在响应中再次包含 CORS 相关的 HTTP 头部 以便浏览器判断是否可以加载这些资源
    3. 特点:默认允许所有的跨域请求,若限制指定地址需添加[origin]配置项
    // node.js express
    const app = express(); // 创建express实例
    const cors = require("cors"); // 引入cors模块
    
    // 1. 允许全部跨域请求
    app.use(cors());
    
    // 2. 允许指定地址跨域
    app.use(
      cors({
        origin: ["http://domain1.com", "http://domain2.com"],
      })
    );
    

其他问题

1 未来三五年有哪些规划?

2 入行的契机是什么?你是怎么学习的?