字节前端面试题集合

2,428 阅读12分钟

前言

最近在牛客上收集了一些字节的面试题,下面是整理的面经。

面经一

问题一:本地存储方式的区别和应用场景

cookie

http协议是无状态的协议,会话结束了也就终止了联系,为了能在下次发送请求可以直接让服务器端知道是谁,于是cookie就诞生了。

特点:

  1. 本质上是一段存储在本地不超过4kb的小型文本
  2. 内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)

常见字段:

  • Expries 用于设置 cookie 的过期时间
Expires=Wed, 21 Oct 2015 07:28:00 GMT
  • Max-Age 用于设置在 Cookie 失效之前需要经过的秒数(优先级比Expires高)
Max-Age=604800
  • CookieSameSite属性
    • strict模式,完全禁止第三方请求携带,完全遵守同源策略
    • lax模式,get提交的时候可以携带
    • none模式,自动携带
  • domain 属性用于限制 Cookie 的作用域,只有在指定的域名下才能够使用该 Cookie。
Domain=example.com
  • path 属性则用于限制 Cookie 的生效路径,只有在指定的路径下才能够使用该 Cookie。
Path=/api
  • secure:一个布尔值,表示是否只在 HTTPS 连接时发送 Cookie。
  • http-only:一个布尔值,表示是否禁止通过 JavaScript 访问 Cookie,从而提高安全性。
  • name:Cookie 的名称,通常是一个字符串。
  • value:Cookie 的值,可以是一个字符串或其他类型的数据。

所以cookie最开始的作用并不是为了缓存而设计出来,只是借用了cookie的特性实现缓存。

怎么设置和删除?

// 设置 Cookie
document.cookie = "username=john; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";

// 读取 Cookie
function getCookie(name) {
  const cookies = document.cookie.split(';');
  for (let i = 0; i < cookies.length; i++) {
    const cookie = cookies[i].trim();
    if (cookie.startsWith(name + '=')) {
      return cookie.substring(name.length + 1);
    }
  }
  return '';
}
const username = getCookie('username');

// 删除 Cookie
// 最常用的方法就是给`cookie`设置一个过期的事件,这样`cookie`过期后会被浏览器删除
function deleteCookie(name) {
  document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
}
deleteCookie('username');

localStorage

HTML5新方法,IE8及以上浏览器都兼容。

localStorage 是一种用于在客户端(浏览器)中存储数据的 Web API,可以用于长期存储非敏感数据,例如用户的个人偏好、应用程序状态等。

特点:

  1. 持久化的本地存储,除非主动删除,否则永远不会过期。
  2. 在同一域名中,存储的信息是共享的。
  3. 当本页操作(新增、修改、删除)了localStorage的时候,本页面不会触发storage事件,但是别的页面会触发storage事件。通过 window.addEventListener('storage', listener) 方法注册一个事件监听器,其中 listener 是用于处理 storage 事件的回调函数,也就是说本页改变localStorage不会触发这个这个事件,也不会执行回调函数。
  4. 大小:5M(跟浏览器厂商有关系)。
  5. 只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题安全问题
  6. 接口封装。通过localStorage暴露在全局,并通过它的 setItem 和 getItem等方法进行操作,非常方便。

下面再看看关于localStorage的使用:

设置

localStorage.setItem('username','cfangxu');

获取

localStorage.getItem('username')

获取键名

localStorage.key(0) //获取第一个键名

删除

localStorage.removeItem('username')

一次性清除所有存储

localStorage.clear()

localStorage 也不是完美的,它有两个缺点:

  • 无法像Cookie一样设置过期时间
  • 只能存入字符串,无法直接存对象
localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'

sessionStorage

sessionStorage和 localStorage使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据。

应用场景

  1. 可以用它对表单信息进行维护,将表单信息存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。
  2. 可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用sessionStorage就再合适不过了。

IndexedDB

indexedDB是运行在浏览器中的非关系型数据库IndexDB的一些重要特性,除了拥有数据库本身的特性,比如支持事务存储二进制数据,还有这样一些特性需要格外注意:

虽然 Web Storage对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

优点:

  • 储存量理论上没有上限
  • 所有操作都是异步的,相比 LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存JS的对象
  • 是个正经的数据库,意味着数据库能干的事它都能干

缺点:

  • 操作非常繁琐
  • 本身有一定门槛

关于indexedDB的使用基本使用步骤如下:

  • 打开数据库并且开始一个事务
  • 创建一个 object store
  • 构建一个请求来执行一些数据库操作,像增加或提取数据等。
  • 通过监听正确类型的 DOM 事件以等待操作完成。
  • 在操作结果上进行一些操作(可以在 request对象中找到)

关于使用indexdb的使用会比较繁琐,大家可以通过使用Godb.js库进行缓存,最大化的降低操作难度。

区别

关于cookiesessionStoragelocalStorage三者的区别主要如下:

  • 存储大小:cookie数据大小不能超过4ksessionStoragelocalStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  • 有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
  • 数据与服务器之间的交互方式,cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端; sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存

问题二:cookie的字段

参考问题一对cookie的描述

问题三:从url输入到页面显示的具体过程

  1. 用户输入url并回车

  2. 浏览器进程检查url,组装协议,构成完整的url

  3. 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程

  4. 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程

  5. 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:

    • 进行DNS解析,获取服务器ip地址,端口
    • 利用ip地址和服务器建立tcp连接
    • 构建请求头信息
    • 发送请求头信息
    • 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容
  6. 网络进程解析响应流程;

    • 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步,如果是200,则继续处理请求。
    • 200响应处理: 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行 后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。
  7. 准备渲染进程

    • 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程
  8. 传输数据、更新状态

    • 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道”
    • 渲染进程接收完数据后,向浏览器发送“确认提交”
    • 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面

问题四:渲染流程

  1. 渲染进程接受完数据之后,先把html内容转换为DOM树(document)

  2. 渲染引擎将html内容里的css内容转换为styleSheets(document.styleSheets),还会把属性值标准化,比如rem转换为px,计算DOM节点的样式,把不显示在页面的DOM去掉。设置为display:none的节点,会存在在DOM树里。

  3. 创建布局树,计算元素、节点的布局信息

  4. 对布局树进行分层,生成分层树。页面元素是按照嵌套关系组织的,生成分层树,使得渲染更加高效

  5. 对每一个图层生成绘制列表,并提交到合成线程中。绘制列表只是用来记录绘制顺序和绘制指令的列表,际上绘制操作是由渲染引擎中的合成线程来完成的。

  6. 合成线程将图层会分成图块,并在光栅化线程池中将图块转换成位图。合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512,合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图.位图(bitmap),也叫做光栅图或像素图,例如,当浏览器将网页中的 SVG 图片或 Canvas 元素渲染成屏幕上可视的元素时,会使用光栅化技术将其转换为位图。位图可以直接在屏幕上显示,而不需要进行额外的计算,因此它们是一种非常高效的图像呈现方式。栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。这就涉及到了跨进程操作。

  7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。

  8. 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的DrawQuad命令,然后根据DrawQuad命令,将其页面内容绘制到内存中,最后再将内存的绘制内容显示在屏幕上。

CSStransform属性可以用来对元素进行平移、旋转、缩放等变换。由于transform只涉及到视觉呈现的变化,而不会引起文档流的改变,因此在使用transform实现动画效果时,可以避开重排和重绘阶段,从而提高页面性能。

问题五:深拷贝浅拷贝

概念

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

JSON.parse(JSON.stringify())拷贝的缺点

  1. 无法处理特殊类型数据

JSON.stringify() 函数只能序列化 JavaScript 中的基本数据类型和数组、对象等结构化数据类型,这是JSON 数据格式所支持的数据类型,不能序列化函数、日期、正则表达式等特殊类型的数据。因此,在进行深拷贝时,如果原始对象中包含这些特殊类型的数据,拷贝后这些数据会被忽略或转换成字符串格式。

// 拷贝函数
const obj = {
  functionName: function() {
    console.log('Hello, world!');
  }
};
console.log(JSON.stringify(obj)); // 输出 '{}'

// 拷贝日期类型数据
// 将日期类型转换为字符串类型后再进行序列化
const obj = {
  date: new Date()
};
console.log(JSON.stringify(obj)); // 输出 '{"date":"2021-09-30T02:45:00.000Z"}'

// 正则表达式类型数据
const obj = {
  regex: /hello/g
};
console.log(JSON.stringify(obj)); // 输出 '{"regex":{}}'
  1. 不能处理循环引用

如果原始对象包含循环引用,即一个对象的某个属性值指向了该对象自身,如 { a: { b: { c: null } } } 中的 c 属性,JSON.stringify() 函数无法将其序列化为 JSON 字符串,会报错。因此,在进行深拷贝时,如果原始对象中存在循环引用,拷贝操作会产生错误。

  1. 效率较低

JSON.stringify()JSON.parse() 都需要将 JavaScript 对象或数组转换成 JSON 字符串和 JavaScript 对象或数组,这样的操作可能会比较耗时,尤其是对于大型对象或数组时,效率会更加低下。因此,如果需要频繁进行深拷贝操作,使用 JSON.parse(JSON.stringify()) 不是最优的选择。

考虑循环引用的深拷贝

function deepClone(obj, hash = new WeakMap()) {
  // 如果是null我就不进行拷贝操作
  if (obj === null) return obj; 
  // 可能是对象或者普通的值 
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  // 使用了原对象的构造方法,保留对象原型上的数据
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

可以拷贝Map、Set数据类型

function deepClone(obj, hash = new WeakMap()) {
  if (obj instanceof Date) return new Date(obj);
  // if (obj instanceof RegExp) return new RegExp(obj);
  if (obj === null || typeof obj !== 'object') return obj;
  if (hash.has(obj)) return hash.get(obj);

  let cloneObj;
  const Constructor = obj.constructor;

  switch (Constructor) {
    case Object:
      cloneObj = new Constructor();
      break;
    case Array:
      cloneObj = new Constructor();
      break;
    case Map:
      cloneObj = new Constructor();
      for (const [key, value] of obj.entries()) {
        cloneObj.set(deepClone(key, hash), deepClone(value, hash));
      }
      break;
    case Set:
      cloneObj = new Constructor();
      for (const value of obj.values()) {
        cloneObj.add(deepClone(value, hash));
      }
      break;
    default:
      cloneObj = new Constructor();
      break;
  }

  hash.set(obj, cloneObj);

  for (let prop in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, prop)) {
      cloneObj[prop] = deepClone(obj[prop], hash);
    }
  }

  return cloneObj;
}

问题六:git reset 和 git revert

问题七:水平垂直居中(如果父元素宽高自适应)

问题八: align-content 和 justify-content的区别

align-content 属性作用于多行的 Flexbox 容器,用于控制多行子元素的垂直对齐方式;而 justify-content 属性作用于单行的 Flexbox 容器,用于控制单行子元素水平对齐方式。

问题九:移动端适配的方法

  1. 使用CSS3的媒体查询(@media)来适配不同屏幕大小。

在CSS中使用@media查询可以根据屏幕大小调整不同样式。通常情况下,我们会根据屏幕宽度设置断点,并在特定宽度下应用不同的样式。

例如,在屏幕宽度小于768px时,我们可以将页面字体大小设置为16px;而在屏幕宽度大于等于768px时,页面字体大小设置为18px。

@media (min-width: 768px) {
  body {
    font-size: 18px;
  }
}

@media (max-width: 767px) {
  body {
    font-size: 16px;
  }
}
  1. 使用JavaScript根据设备像素比(devicePixelRatio)进行适配

由于不同设备的像素密度不同,如果只是简单地按照像素计算,可能会导致页面在高像素密度的设备上显示过小。而使用设备像素比(devicePixelRatio)来计算,可以根据设备像素密度调整视口的缩放比例,从而实现适配。

例如: 获取设备像素比为3,并计算出缩放比例为1/3,将元素宽度和高度都放大3倍,以达到适配高像素密度设备的目的。

var scale = 1 / window.devicePixelRatio;
document.querySelector('meta[name="viewport"]')
    .setAttribute(
    'content', 
    'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ',
    user-scalable=no'
    );
  1. 使用viewport meta标签来设置视口大小并控制页面缩放。

在移动端开发中,通常会设置viewport的width为设备宽度,并禁止用户缩放页面。这样可以保证页面展示效果的稳定性。

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  1. 使用rem或者em作为单位进行适配,同时配合根据不同尺寸屏幕动态计算字体大小等方法。

rem和em都是相对长度单位,可以根据页面根元素(html)的字体大小进行调整。我们可以将页面字体大小设置为基准大小,然后根据不同屏幕尺寸动态调整字体大小。

html {
  font-size: 16px; /* 假设基准像素为16px */
}

@media (max-width: 767px) {
  html {
    font-size: 14px;
  }
}

问题十: 可以用flex能完成移动端适配吗

可以,flex 布局可以方便地实现响应式设计。

流程

使用flex布局进行移动端适配的具体流程可以分为以下几步:

  1. 设置 meta 标签,控制页面缩放和视口大小。通常建议设置viewport的 width=device-width,initial-scale=1.0,user-scalable=no。
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
  1. 确定设计稿尺寸,并根据设计稿尺寸计算出基准像素。比如,设计稿宽度为 750px,则基准像素为 1/75,即 10px。

  2. 在根元素(html)中设置 font-size,用于控制 rem 的大小。一般情况下可以设置为基准像素大小。同时,建议设置一个最小值,避免在屏幕过小时字体过小。

/* 假设基准像素为10px */
html {
  font-size: 10px;
  min-width: 320px; /* 避免字体过小 */
}
  1. 使用 rem 作为长度单位,并根据视觉稿中的尺寸换算成 rem。比如,设计稿中某一元素的宽度为 100px,则应设置其宽度为 10rem。
/* 假设设计稿中某一元素的宽度为100px */
.element {
  width: 10rem;
}
  1. 使用 flex 布局,并根据子元素所占比例进行设置。使用 flex-wrap 属性设置是否换行,使用 align-items 和 justify-content 控制子元素在交叉轴和主轴上的对齐方式。
.container {
  display: flex;
  flex-wrap: wrap;
}

.item {
  flex: 1; /* 自适应宽度 */
  min-width: 100px; /* 设置最小宽度 */
  margin-right: 10px; /* 设置间距 */
  margin-bottom: 10px;
}

在实际开发中,还要注意屏幕尺寸、像素密度、字体大小等因素,选择合适的方案实现移动端适配。

问题十一:怎么在高像素设备上显示 1px

  • transform的scaleY属性
  • meta viewport 设置scale的值
  • SVG
  • Canvas

问题十二:flexible 适配的原理

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 假设设计稿宽度为750px,在750px屏幕下,1rem=100px */
    html {
      font-size: 100px;
    }

    /* 其他CSS样式 */
    .box {
      width: 6rem;
      height: 3rem;
      background-color: #f00;
      margin: 1rem auto;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <!-- flexible核心代码,设置根元素的font-size值 -->
  <script>
     (function () {
      var width = document.documentElement.clientWidth // 获取屏幕宽度
      var ratio = 750 / width // 计算出设计稿的比例
      var fontSize = 100 * ratio // 将比例设置为根元素的font-size值
      document.documentElement.style.fontSize = fontSize + 'px'
      window.onresize = function () {
        var width = document.documentElement.clientWidth // 监听屏幕尺寸变化
        var ratio = 750 / width
        var fontSize = 100 * ratio
        document.documentElement.style.fontSize = fontSize + 'px'
      }
    })()
  </script>
</body>
</html>

问题十三:react

问题十四:手写 new 函数

function myNew(constructor, ...args) {
  // 创建一个空对象,并将它的原型指向构造函数的 prototype 属性
  // 它的作用是以指定对象为原型创建一个新的对象,新对象会继承原型对象的所有属性和方法
  const obj = Object.create(constructor.prototype);
  // 调用构造函数,并将 this 绑定到新创建的对象上
  const result = constructor.apply(obj, args);
  // 如果构造函数返回了一个对象,则直接返回该对象;否则返回新创建的对象
  return (typeof result === 'object' && result !== null) ? result : obj;
} 

问题十五:场景代码题

function find(obj, str) {
  var arr = str.split('.'); // 将传入的字符串按照"."切割成数组
  var pointer = obj; // 初始化指针变量为传入的对象本身
  for (var i = 0; i < arr.length; i++) {
    var key = arr[i];
    if (pointer.hasOwnProperty(key)) { // 判断当前指针变量所指的对象中是否包含该属性
      pointer = pointer[key]; // 将指针变量指向该属性对应的对象
    } else {
      return undefined; // 如果不存在该属性,返回undefined
    }
  }
  return pointer; // 循环结束时,指针变量所指的对象即为所要查找的属性值
}

// 示例
var obj = {a:{b:{c:1}}};
console.log(find(obj,'a.b.c')); // 1
console.log(find(obj,'a.d.c')); // undefined

问题十六

剑指 Offer II 105. 岛屿的最大面积 - 力扣(LeetCode)

面经二

问题一: 三次握手为什么是三次,两次不行吗?

对应到 TCP 的三次握手,也是需要确认: 请求能力、接受能力、对方请求能力和对方接受能力

一是三次握手可以确认服务器端有没有发送能力、对方接受能力。

二是当只需要两次握手就可以建立连接时,对于发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手,服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。

所以两次握手会请求超时导致的脏连接,带来了连接资源的浪费。无法确认服务器端有没有发送能力、对方接受能力。而三次握手可以防止超时,同时保证信息对等

问题二: 四次挥手为什么是四次,三次不行吗?

  • 第四次挥手,主动关闭端等待2MSL是必须的
  • 所以也就是说把服务端将ACKFIN的发送合并为一次挥手
  • 这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端
  • 从而让客户端不断的重发FIN

问题三: 强缓存和协商缓存

问题四: 跨域

问题五: 箭头函数和普通函数的区别

  1. 语法不同:箭头函数使用箭头(=>)而不是关键字function来声明,这使得它们的语法更加紧凑、简洁。
  2. this 值不同:箭头函数中的this值不同于普通函数在相同情况下的this值。箭头函数的this值由外层作用域(即定义时的作用域)决定,而不是函数被调用时的作用域。这意味着箭头函数中的this指向永远不会改变,而普通函数的this值可能会随着调用方式的不同而改变。
  3. 没有自己的arguments对象:箭头函数不能使用自己的arguments对象,但是可以通过扩展运算符...来访问传递给函数的参数。
  4. 不能作为构造函数:箭头函数不能用作构造函数,因为它们没有自己的this值和原型对象。如果尝试使用new关键字实例化一个箭头函数,将会抛出TypeError错误。
  5. 没有 prototype 属性:箭头函数没有prototype属性,因为它们不能用作构造函数,因此也就不需要原型对象。普通函数则具有原型对象,并且可以在此原型对象上添加方法和属性。
  6. 不能使用 yield 关键字:箭头函数不能使用yield关键字来定义生成器函数,但是普通函数可以。

问题六: 虚拟 DOM 和 diff 算法

了解 diff 算法可以去看看图理解双端diff算法 - 掘金 (juejin.cn)

  1. 什么是虚拟 DOM

虚拟 DOM 是指将页面结构转化成一个JavaScript对象,在内存中维护一份虚拟 DOM 树,通过对比前后两个虚拟 DOM 树的差异,最小化地更新真实 DOM。

  1. 为什么使用虚拟 DOM

由于真实 DOM 操作非常昂贵,并且直接影响到应用程序的性能和用户体验。因此,虚拟 DOM 提供了一种更高效的方式来操作 DOM,它可以利用 JavaScript 引擎更高效地更新 DOM,并且可以避免频繁的重绘、回流等操作,从而提高应用程序的性能。

  1. 什么是 diff 算法

diff 算法是比较两个虚拟 DOM 树的差异,找出需要更新的节点,并将这些节点更新到真实 DOM 上的算法。Vue.js 内部使用的是优化算法,可以快速地确定需要更新的节点,以最小化更新的时间和效果。

  1. diff 算法如何工作

当数据改变时,Vue.js 首先创建一个新的虚拟 DOM 树,并将其与之前的虚拟 DOM 树进行比较。diff 算法是一种递归算法,它会逐层遍历节点,比较两个虚拟 DOM 树中的每个节点,找出需要更新的节点。在比较节点时,Vue.js 会使用一些启发式算法来优化比较性能。

  1. diff 算法的优化

Vue.js 内部使用了大量的优化策略来提高 diff 算法的性能,其中最重要的包括:

  • 只对同级组件进行比较
  • 根据节点的关键属性进行比较
  • 对列表中的节点采用就地复用策略

这些优化策略可以显著提高 Vue.js 应用程序的性能,并使其在渲染大型数据集时更具可扩展性。

问题七:判断链表是否有环,不能用本地IDE,自己定义数据结构

// 定义链表节点类
class Node {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

// 定义链表类
class LinkedList {
  constructor() {
    this.head = null;
  }

  // 尾部添加新节点
  append(val) {
    const newNode = new Node(val);
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
  }

  // 判断链表是否有环
  hasCycle() {
    if (!this.head || !this.head.next) {
      return false;
    }
    let slow = this.head;
    let fast = this.head.next;
    while (slow !== fast) {
      if (!fast || !fast.next) {
        return false;
      }
      slow = slow.next;
      fast = fast.next.next;
    }
    return true;
  }
}

// 测试代码
const list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.append(4);
list.append(5);
console.log(list.hasCycle());  // 输出 false

// 创建一个有环的链表
const node3 = new Node(3);
const node2 = new Node(2);
const node1 = new Node(1);
node1.next = node2;
node2.next = node3;
node3.next = node1;
const list2 = new LinkedList();
list2.head = node1;
console.log(list2.hasCycle());  // 输出 true

// 判断链表是否有环(使用哈希表)
function hasCycle(head) {
  const hashSet = new Set();
  let current = head;
  while (current) {
    if (hashSet.has(current)) {
      return true;
    }
    hashSet.add(current);
    current = current.next;
  }
  return false;
}

// 测试代码
const node3 = {val: 3, next: null};
const node2 = {val: 2, next: node3};
const node1 = {val: 1, next: node2};
node3.next = node1;
console.log(hasCycle(node1));  // 输出 true

const node5 = {val: 5, next: null};
const node4 = {val: 4, next: node5};
console.log(hasCycle(node4));  // 输出 false

// 判断链表是否有环(暴力解法)
function hasCycle(head) {
  let current = head;
  while (current) {
    let checkNode = current.next;
    while (checkNode) {
      if (current === checkNode) {
        return true;
      }
      checkNode = checkNode.next;
    }
    current = current.next;
  }
  return false;
}

// 测试代码
const node3 = {val: 3, next: null};
const node2 = {val: 2, next: node3};
const node1 = {val: 1, next: node2};
node3.next = node1;
console.log(hasCycle(node1));  // 输出 true

const node5 = {val: 5, next: null};
const node4 = {val: 4, next: node5};
console.log(hasCycle(node4));  // 输出 false

问题八:最长递增子序列

Question 617 - 力扣

问题九:在 es5 中如何实现 const

var MY_CONST = {};
Object.defineProperty(MY_CONST, "MY_CONSTANT", {
  value: "my constant value",
  writable: false,// writable 属性设置为 false 表示该常量不可写
  enumerable: true,// enumerable 属性设置为 true 表示该常量可以被枚举
  configurable: false // configurable 属性设置为 false 表示该常量不可再被重新配置或删除
});

console.log(MY_CONST.MY_CONSTANT); // 输出 "my constant value"
MY_CONST.MY_CONSTANT = "new value"; // 没有报错,但不会更改 MY_CONSTANT 的值
console.log(MY_CONST.MY_CONSTANT); // 输出 "my constant value"

问题十: object.defineProproty 和 proxy 的区别

Object.defineProproty

该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

语法

Object.defineProperty(obj, prop, descriptor)

参数

obj: 要在其上定义属性的对象。

prop:  要定义或修改的属性的名称。

descriptor: 将被定义或修改的属性的描述符。

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符

这就意味着你可以:

Object.defineProperty({}, "num", {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
});

也可以:

var value = 1;
Object.defineProperty({}, "num", {
    get : function(){
      return value;
    },
    set : function(newValue){
      value = newValue;
    },
    enumerable : true,
    configurable : true
});

两者均具有以下两种键值

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false

enumerable

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false

数据描述符同时具有以下可选键值

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined

writable

当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false

存取描述符同时具有以下可选键值

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined

descriptor这个字段是必须的,如果不进行任何配置,你可以这样:

var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined

Proxy

下面是 Proxy 的几个主要特点:

  1. 拦截器(handler):Proxy 对象的第一个参数是一个目标对象,第二个参数是一个拦截器对象,其中拦截器对象包含一些方法,用来拦截底层操作。
  2. 可代理的操作:Proxy 可以代理目标对象的各种操作,例如读取属性、写入属性、函数调用等。
  3. 自定义处理逻辑:在拦截器对象中,我们可以定义自己的处理逻辑,并在底层操作被触发时执行相应的处理逻辑。

下面通过代码示例来进一步说明 Proxy 的用法:

const person = {
  name: 'Tom',
  age: 18
};

const handler = {
  get(target, key) {
    console.log(`get ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`set ${key} to ${value}`);
    target[key] = value;
    return true;
  }
};

const proxyPerson = new Proxy(person, handler);
proxyPerson.name; // 触发 getter
proxyPerson.age = 20; // 触发 setter

下面的例子是拦截第一个字符为下划线的属性名,不让它被 for of 遍历。

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

问题十一:用对象实现 Map 数据类型?

function Map() {
  this.data = {};
}

Map.prototype.set = function(key, value) {
  this.data[JSON.stringify(key)] = value;
};

Map.prototype.get = function(key) {
  return this.data[JSON.stringify(key)];
};

Map.prototype.delete = function(key) {
  delete this.data[JSON.stringify(key)];
};

Map.prototype.clear = function() {
  this.data = {};
};

Map.prototype.has = function(key) {
  return this.data.hasOwnProperty(JSON.stringify(key));
};

Map.prototype.forEach = function(callback) {
  for (let prop in this.data) {
    callback(JSON.parse(prop), this.data[prop]);
  }
};

Map.prototype.keys = function() {
  let keys = [];

  for (let prop in this.data) {
    keys.push(JSON.parse(prop));
  }

  return keys;
};

Map.prototype.values = function() {
  let values = [];

  for (let prop in this.data) {
    values.push(this.data[prop]);
  }

  return values;
};

Map.prototype.entries = function() {
  let entries = [];

  for (let prop in this.data) {
    entries.push([JSON.parse(prop), this.data[prop]]);
  }

  return entries;
};

问题十二:算法题1

实现一个定时器函数myTimer(fn, a, b),
让fn执行,
第一次执行是a毫秒后,
第二次执行是a+b毫秒后,
第三次是a+2b毫秒,
第N次执行是a+Nb毫秒后

要求:
1、白板手撕
2、myTimer要有返回值,并且返回值是一个函数,调用该函数,可以让myTimer停掉

解答:

function myTimer(fn, a, b) {
  let timerId;
  let count = 0;

  function schedule() {
    const delay = a + count * b;
    timerId = setTimeout(() => {
      fn();
      count++;
      schedule();
    }, delay);
  }

  schedule();

  return function() {
    clearTimeout(timerId);
  }
}

问题十三:算法题2

写一个构造函数Foo,该函数每个实例为一个对象,形如{id:N},其中N表示第N次调用得到的。
要求:
1、不能使用全局变量
2、直接调用Foo()也会返回实例化的对象
3、实例化的对象必须是Foo的实例

解答:

const Foo = (function() {
  let count = 0;

  function Foo() {
    if (!(this instanceof Foo)) {
      return new Foo();
    }
    count++;
    this.id = count;
  }

  return Foo;
})();

const foo1 = new Foo();
console.log(foo1); // { id: 1 }

const foo2 = new Foo();
console.log(foo2); // { id: 2 }

const foo3 = Foo();
console.log(foo3); // { id: 3 }