面试题大杂烩(三)🤡=>数组与对象扁平化

545 阅读11分钟

前言

开卷,有点疲惫了

面试题:

从输入url到页面展示这段时间里发生了什么?

  1. URL输入与解析: 一般输入url的是域名,所以需要先解析成:确定协议(如HTTP或HTTPS)、主机名、端口号(如果有指定)、路径及查询参数。
  2. DNS查询=>逐级向上的过程
  • 浏览器检查本地缓存是否有该域名对应的IP地址。
  • 如果没有找到,浏览器会向操作系统发出DNS查询请求以获取对应域名的IP地址。
  • 操作系统也会首先检查自身的DNS缓存,如果未命中,则会向配置的DNS服务器发送查询请求。
  • DNS服务器返回域名对应的IP地址给浏览器。
  1. 建立TCP连接: 一旦获得了目标服务器的IP地址,浏览器将尝试与服务器建立TCP连接。如果是HTTPS请求,还会进行SSL/TLS握手来确保通信的安全性。(这时候可以讲讲三次握手、四次挥手)
  2. 发送HTTP/HTTPS请求
  • 建立连接后,浏览器构造HTTP请求(包含请求行、头部信息和可能的请求体)并发送给服务器。
  • 请求可以是GET、POST等方法,取决于用户交互或网页加载的需求。 (可以聊聊GET和POST的区别)
  1. 服务器处理请求并返回HTTP报文
  • HTTP报文也分成三份,状态码 ,响应报头响应报文
  • 从服务器请求的HTML,CSS,JS文件就放在响应报文中
  1. 浏览器接收响应
  • 浏览器接收到服务器的响应后,开始解析HTML文档内容。
  • 在解析过程中,如果遇到外部资源引用(如CSS文件、JavaScript脚本、图片等),浏览器会发起额外的请求来获取这些资源。
  1. 构建DOM树和CSSOM树
  • 浏览器基于HTML内容构建DOM树。
  • 同时,基于CSS样式表构建CSSOM树。
  1. 布局和绘制
  • 结合DOM树和CSSOM树,浏览器创建渲染树,并计算每个节点在屏幕上的确切位置和大小(布局阶段)。
  • 根据渲染树的信息,浏览器将页面绘制出来(绘制阶段)。

9 JavaScript执行

  • 页面加载过程中,若存在JavaScript代码,浏览器会在适当的时候执行这些代码,这可能会修改DOM结构或应用新的样式规则,从而影响最终的页面展示。

那如果js一直堵塞,想要渲染动画怎么处理?

  • 当时回答了使用Web Workers,Web Workers允许你在后台线程中运行脚本,从而避免长时间运行的任务阻塞主线程。但是Web Workers不能直接访问DOM,一般渲染动画都需要操作dom
  • 后面想到了可以用css去渲染,回答了可以用到transform 的一些属性 (实际上是用@keyframes来制作动画)

面试官又问 CSS 动画为什么不被 JS 阻塞?

虽然JS是单线程,但是浏览器是多线程:

  • 主线程:负责处理大部分网页内容,包括解析 HTML、执行 JavaScript、计算样式以及布局等。
  • 合成器线程:专门负责页面的绘制和合成(compositing)。当动画仅涉及 transform 和 opacity 属性时,这些属性的变化可以由合成器线程独立处理,无需经过主线程重新计算布局或样式。

如何减少白屏时间?

  1. 首先我回答了可以使用SSR 也就是 服务端渲染(Server-Side Rendering, SSR),静态资源可以在服务器端编译好,在发给浏览器从而让用户更快看到内容,同时也可以改善SEO。
  2. 使用骨架屏(Skeleton Screen), 在主要内容加载完成之前,显示一个简单的占位符布局(即骨架屏),给用户一种页面正在快速加载的感觉。
  3. 路由懒加载: 主要的作用在于打包的时候,可以减少压缩的体积
  4. 组件懒加载:是在需要的时候才动态加载组件,而不是在应用启动时就全部加载。这样可以减少初始加载时间,并且只在实际需要显示某个组件时才加载相关的JavaScript代码。
  5. 异步加载非关键资源
  • 异步加载JavaScript:使用<script async><script defer>属性,避免阻塞页面渲染。
  • 懒加载图像和其他媒体:仅当用户滚动到相应部分时才加载图像或其他媒体文件。

v-for 使用时key 的作用是什么?

当时我主要从虚拟DOM去讲起,可以避免一些不必要的更新,当Vue执行虚拟DOM的diff算法时,它会利用key来匹配新旧虚拟DOM树中的元素。这使得Vue可以快速定位到哪些元素发生了变化,从而只更新必要的部分,而不是重新渲染整个列表。

作用:

1. 提高更新效率

当数据项发生变化(如添加、删除或重新排序)时,Vue 使用 key 来追踪每个节点的身份,以便尽可能地复用现有的 DOM 元素,而不是每次都重新创建它们。这可以显著提高列表更新的性能。

2. 确保组件状态正确

如果列表中的项目是动态组件或包含局部状态的组件,key 可以确保当数据变化时,正确的组件实例被销毁和重建。没有 key,Vue 可能会复用组件实例,导致状态混乱。

3. 避免不必要的重排

在不使用 key 的情况下,Vue 默认会尝试最小化对 DOM 的变动,但这可能导致不必要的重排(reflow)和重绘(repaint)。通过提供唯一的 key,Vue 能够更精确地决定哪些元素需要被更新,从而减少不必要的操作。

又问那key 对于v-if的作用呢?

 强制重新渲染

  • 当切换 v-if 的条件时,默认情况下 Vue 会尝试复用现有的 DOM 元素以提高性能。这意味着如果条件从 false 变为 true,Vue 可能会保留之前的 DOM 状态而不是完全重新创建一个新的。
  • 如果你希望在条件变化时强制重新渲染(即销毁旧的实例并创建新的),可以通过给元素添加唯一的 key 来实现。当 key 发生变化时,Vue 会认为这是一个全新的元素,从而触发完整的生命周期钩子(如 mounteddestroyed 等)和相应的DOM操作。

手写题:

对象扁平化

给出flattenObject具体实现
const nestedObj = {
    a: 1,
    b: {
        c: 2,
        d: { e: 3    }
    }
};
const flattenedObj = flattenObject(nestedObj);
console.log(flattenedObj); 
// 输出: { 'a': 1, 'b.c': 2, 'b.d.e': 3 }

实现:

function flattenObject(obj, parentKey = '', result = {}) {
    // 遍历对象 obj 的所有可枚举属性
    for (const key in obj) {
        // 检查属性是否是对象自身的属性,而非原型链上的属性
        if (obj.hasOwnProperty(key)) {
            // 构建新的键名,如果 parentKey 存在,使用点号连接 parentKey 和当前 key
            const newKey = parentKey ? `${parentKey}.${key}` : key;

            // 检查当前属性值是否为非 null 的对象,并且不是数组
            if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
                // 递归调用 flattenObject 处理嵌套对象,更新 parentKey 和 result
                flattenObject(obj[key], newKey, result);
            } else {
                // 如果是基本类型值,直接将新键和对应的值添加到 result 对象中
                result[newKey] = obj[key];
            }
        }
    }
    // 返回扁平化后的对象
    return result;
}
console.log( flattenObject(nestedObj)) //{ a: 1, 'b.c': 2, 'b.d.e': 3 }

解析:

  1. hasOwnProperty 方法确保只处理对象本身的属性,而不是其原型链上的属性。
  2. 创建 newKey,如果 parentKey 存在,则通过点号连接它与当前 key,否则直接使用 key 作为新的键名。
  3. 检查当前属性值是否为非 null 的对象并且不是数组。如果是,则递归调用 flattenObject 函数,继续处理该嵌套对象,更新 parentKey 为 newKey 并将结果存储在同一个 result 对象中。

数组扁平化:

题目描述已有多级嵌套数组 : [1, [2, [3, [4, 5]]], 6,7] 将其扁平化处理 输出: [1,2,3,4,5,6,7] 实现: 只需要按照上面的模板我们来改改就好了



const nestedObj = [1, [2, [3, [4, 5]]], 6, 7];

function flattenObject(obj, result = []) {
    if (!Array.isArray(obj)) {
        result.push(obj);
        return result;
    }
    for (const item of obj) {
        if (Array.isArray(item)) {
            flattenObject(item, result);
        } else {
            result.push(item);
        }
    }
    return result;
}

console.log(flattenObject(nestedObj));// [ 1, 2, 3, 4, 5, 6, 7]

高级写法: 我去搜了大佬的文章来理解,见此篇==>>>juejin.cn/post/711876…

function flatten(arr) {
  while (arr.some(item=> Array.isArray(item))) {
    console.log(...arr)
    arr = [].concat(...arr)
    console.log(arr)
  }
  return arr
}
console.log(flatten(arr));
  1. 介绍一下some函数方法 .some() 方法在 JavaScript 中用于测试数组中的至少一个元素是否满足提供的函数实现的条件。它是一个数组方法,返回一个布尔值:
  • 如果找到一个数组元素满足提供的测试函数,则返回 true 并停止遍历数组。
  • 如果没有找到这样的元素,则返回 false

案例


const numbers = [2, 4, 6, 8, 10, 12];
const anyOver10 = numbers.some(function(num) {
    return num > 10;
});
console.log(anyOver10); // 输出 true 因为存在大于10的数字12

  1. 直接执行这个代码时arr = [].concat(...arr) 等价于arr = [].concat(1, [2, [3, [4, 5]]], 6, 7);因为扩展运算符 ...arr 把原数组打散成了一个个参数传给 .concat()。然后 .concat() 会把这些参数合并成一个新数组:
[1, 2, [3, [4, 5]], 6, 7]
  1. 讲讲执行流程:

第一次迭代

  • arr.some(item => Array.isArray(item)) :
    • item 遍历到的第一个值是 1,不是数组。
    • 接着 item 是 [2, [3, [4, 5]]],这是数组,所以 .some() 返回 true,进入循环体。
  • console.log(...arr) :
    • 输出原始数组中的所有元素:1 [2, [3, [4, 5]]] 6 7
  • arr = [].concat(...arr) :
    • 将 arr 展开一层,结果为 [1, 2, [3, [4, 5]], 6, 7]

第二次迭代

  • arr.some(item => Array.isArray(item)) :
    • 现在 arr 已经变成了 [1, 2, [3, [4, 5]], 6, 7]
    • item 遍历到第一个非数组值 1,继续。
    • 下一个 item 是 2,继续。
    • 当 item 是 [3, [4, 5]] 时,.some() 返回 true,因为这是一个数组,再次进入循环体。
  • console.log(...arr) :
    • 输出展开后的一层数组:1 2 [3, [4, 5]] 6 7
  • arr = [].concat(...arr) :
    • 再次将 arr 展开一层,结果为 [1, 2, 3, [4, 5], 6, 7]

第三次迭代

  • arr.some(item => Array.isArray(item))
    • 现在 arr 变成了 [1, 2, 3, [4, 5], 6, 7]
    • item 遍历到 [4, 5],这是一个数组,因此 .some() 返回 true,再次进入循环体。
  • console.log(...arr) :
    • 输出:1 2 3 [4, 5] 6 7
  • arr = [].concat(...arr) :
    • 最终展开为 [1, 2, 3, 4, 5, 6, 7]

总结: 如果面试真来了这题,我们先可以写第一个或者简单的方法,等面试问你能不能优化一下,或者你写完第一个方法,假装思考一下,再写第二种方法.

实现嵌套对象数组去重

实现嵌套对象数组去重:给出removeDuplicates

const nestedArr = [
    { a: 1, b: { c: 2 } },
    { a: 1, b: { c: 2 } },
    { a: 3, b: { d: 4 } }
];
const uniqueArr = removeDuplicates(nestedArr);

实现:

function deepEqual(a, b) {
    if (a === b) return true;
    if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
        return false;
    }
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);
    if (keysA.length !== keysB.length) return false;

    for (let key of keysA) {
        if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
            return false;
        }
    }
    return true;
}

function removeDuplicates(arr) {
    const result = [];
    for (const item of arr) {
        if (!result.some(existing => deepEqual(item, existing))) {
            result.push(item);
        }
    }
    return result;
}
const nestedArr = [
    { a: 1, b: { c: 2 } },
    { a: 1, b: { c: 2 } },
    { a: 3, b: { d: 4 } }
];

const uniqueArr = removeDuplicates(nestedArr);
console.log(uniqueArr);
// 输出: [ { a: 1, b: { c: 2 } }, { a: 3, b: { d: 4 } } ]

流程讲解:

  1. 当你调用 removeDuplicates(nestedArr) 时:
  • nestedArr 被遍历,每个元素都是一个对象。
  • 对于每个对象,使用 some 方法和 deepEqual 函数检查这个对象是否已经在 result 数组中存在。
  • 如果不存在,则将该对象添加到 result 中。
  • 最终,result 数组包含了所有唯一的对象,没有重复。

2.调用 deepEqual 时:

  • 首先检查两个变量是否是同一个引用(即指向内存中的同一块地址)。如果是,则直接返回 true,因为它们肯定是相等的。
  • 检查 a 和 b 是否都是对象类型,并且都不是 null。如果其中任何一个不是对象(例如数字、字符串、布尔值)或为 null,则直接返回 false,因为这种情况下只能通过严格相等(===)来判断,而前面已经排除了这种情况。
  • 使用 Object.keys() 方法获取对象 a 和 b 的所有可枚举属性名(键),并分别存储在 keysA 和 keysB 中。这一步是为了后续遍历每个对象的属性进行逐个比较做准备。
  • 如果两个对象的键数量不同,则它们肯定不相等,因此直接返回 false。这是为了快速排除明显不相等的情况。
  • 对于 a 中的每一个键,首先检查该键是否也存在于 b 中(使用 includes 方法)。如果不存在,或者虽然存在但是对应的值不相等(通过递归调用 deepEqual 来判断),则返回 false
  • 如果通过了上述所有的检查,说明两个对象在结构和内容上都是完全相同的,所以最终返回 true