前端面试题详解整理120|跨域,rem,package-lock.json TypeScript 常见的工具函数,npm三位数版本呢,

19 阅读18分钟

www.nowcoder.com/discuss/595…

base上海 哔哩哔哩 前端实习面经

投递渠道:BOSS

时间线

  • 2024-02-20 投递简历
  • 2024-02-23 一面
  • 2024-02-27 二面

一面

  1. 为什么离职
  2. 如何学习前端的
  3. TypeScript 常见的工具函数

TypeScript 是 JavaScript 的超集,它提供了静态类型检查和更丰富的面向对象编程功能。在 TypeScript 中,你可以使用各种工具函数来处理数据、进行类型转换、进行异步操作等。以下是一些常见的 TypeScript 工具函数示例:

  1. 类型判断工具函数:
    • typeof:返回变量的类型字符串。
    • instanceof:检查一个对象是否是指定类型的实例。
    • Array.isArray():检查一个对象是否是数组。
    • isNaN():检查一个值是否是 NaN。
    • Number.isInteger():检查一个值是否是整数。
const isString = (value: any): boolean => typeof value === 'string';
const isObject = (value: any): boolean => typeof value === 'object' && value !== null;
  1. 类型转换工具函数:
    • as:类型断言,用于将一个变量转换为指定的类型。
    • parseInt()parseFloat():将字符串转换为数字。
    • String()Number()Boolean():将值转换为字符串、数字、布尔值。
    • JSON.parse()JSON.stringify():将 JSON 字符串和 JavaScript 对象相互转换。
const convertToString = (value: any): string => value as string;
const convertToInt = (value: any): number => parseInt(value, 10);
  1. 数组操作工具函数:
    • Array.prototype.map():对数组的每个元素执行指定的操作,并返回操作后的新数组。
    • Array.prototype.filter():筛选数组中满足条件的元素,返回新数组。
    • Array.prototype.reduce():对数组中的每个元素执行指定的累积操作,返回累计结果。
const doubledArray = [1, 2, 3].map(num => num * 2);
const evenNumbers = [1, 2, 3, 4, 5].filter(num => num % 2 === 0);
const sum = [1, 2, 3, 4, 5].reduce((acc, curr) => acc + curr, 0);
  1. Promise 工具函数:
    • Promise.resolve():返回一个解析后带有给定值的 Promise 对象。
    • Promise.reject():返回一个带有拒绝原因的 Promise 对象。
    • Promise.all():接收一个 Promise 数组,并在所有 Promise 都解析后返回一个新的 Promise,其结果是一个值数组。
const resolvedPromise = Promise.resolve(42);
const rejectedPromise = Promise.reject(new Error('Something went wrong'));
const allPromises = Promise.all([promise1, promise2]);

这些是 TypeScript 中一些常见的工具函数示例,用于进行类型判断、类型转换、数组操作和 Promise 处理等。根据实际需求,你可以编写更多的工具函数来简化代码和提高效率。

  1. flex 布局常用属性

  2. 如何理解 Promise, 有哪些常见的方法

  3. 原型与原型链的理解

  4. npm 版本 ^~ 的区别: 前者兼容大版本, 后者兼容次版本

  5. npm 三位数版本号的含义: breaking change, feature, bug fix

  6. npm 三位数版本号采用的是语义化版本(Semantic Versioning)规范,通常称为 SemVer。每个版本号由三个数字组成:X.Y.Z,分别代表主版本号(Major)、次版本号(Minor)和修订版本号(Patch)。

  7. 主版本号(Major)

    • 当进行不兼容的 API 变更时,应该更新主版本号。这通常意味着某些功能的移除或重写,可能导致旧版本的代码无法在新版本下正常工作。主版本号的变化表示可能存在破坏性变更(breaking change)。
  8. 次版本号(Minor)

    • 当添加新功能,但是保持向后兼容时,应该更新次版本号。这意味着在新版本中引入了新的功能,但旧版本的代码仍然可以在新版本下正常工作。
  9. 修订版本号(Patch)

    • 当进行向后兼容的 bug 修复时,应该更新修订版本号。这通常意味着修复了一些 bug,但不会对现有功能造成影响。

举例来说,假设当前版本号为 1.2.3

  • 如果在下一个版本中,只是修复了一些 bug 而没有添加新功能或进行破坏性变更,那么新版本的版本号为 1.2.4,即修订版本号增加。

  • 如果在下一个版本中,添加了一些新功能,但没有进行破坏性变更,那么新版本的版本号为 1.3.0,即次版本号增加,修订版本号和主版本号归零。

  • 如果在下一个版本中,进行了破坏性的 API 变更,那么新版本的版本号为 2.0.0,即主版本号增加,次版本号和修订版本号归零。

通过遵循语义化版本规范,开发者可以更清晰地了解到每个版本的变更,以便于进行升级和维护。

  1. package-lock.json 的作用以及安装顺序: 如果与 package.json 冲突, 以 package.json 为准 package-lock.json 文件是在执行 npm installnpm ci 命令时生成的,用于锁定项目的依赖版本。它的作用是记录项目依赖的精确版本号,包括直接依赖和间接依赖,以确保在不同环境下安装相同的依赖时,安装的版本都是一致的。这样可以避免因为依赖的版本不一致导致的代码不可预测的行为,也提高了项目的可重复性和稳定性。

package-lock.json 文件的安装顺序是与 npm installnpm ci 命令的执行顺序一致的。在执行这些命令时,npm 会首先检查当前目录下是否存在 package-lock.json 文件,如果存在,会根据其中记录的依赖版本号来安装依赖;如果不存在,或者 package-lock.json 文件中的依赖版本与 package.json 文件中的依赖版本不一致,则会根据 package.json 文件中的依赖版本来安装依赖,并生成新的 package-lock.json 文件。

如果 package-lock.json 文件与 package.json 文件中的依赖版本发生冲突,那么通常情况下应该以 package.json 文件中的依赖版本为准。因为 package-lock.json 文件是由 npm 自动生成的,它的主要目的是为了确保依赖的版本一致性,但并不一定反映开发者的意图。而 package.json 文件中的依赖版本是开发者手动指定的,应该是开发者期望项目使用的依赖版本。因此,如果发生冲突,应该优先以 package.json 文件中指定的版本为准,然后通过执行 npm installnpm ci 命令重新生成 package-lock.json 文件,确保依赖版本的一致性。

  1. 跨域问题与解决方案 跨域问题是指浏览器的同源策略限制了网页从一个源加载的资源去请求另一个源的资源。同源策略要求网页的协议、域名、端口号必须完全一致,否则就会出现跨域问题。以下是常见的跨域问题和解决方案:

跨域问题:

  1. Ajax 跨域请求被拒绝:

    • 当使用 XMLHttpRequest 或 Fetch API 发起跨域请求时,浏览器会阻止该请求。
  2. Cookie、LocalStorage 和 IndexedDB 无法读取:

    • 如果网页尝试读取另一个源的 Cookie、LocalStorage 或 IndexedDB 数据,将会失败。
  3. DOM 和 JS 对象无法访问:

    • JavaScript 不能访问跨域的 DOM 对象,也无法调用跨域的 JavaScript 方法。

解决方案:

  1. CORS(跨域资源共享):

    • CORS 是一种机制,允许服务器告知浏览器是否允许通过 JavaScript 在浏览器中访问跨域资源。通过设置响应头中的 Access-Control-Allow-Origin 和其他相关的 CORS 头部,服务器可以授权浏览器访问跨域资源。
  2. JSONP(仅限 GET 请求):

    • JSONP 是一种利用 <script> 标签的跨域技术,通过动态创建 <script> 标签,指向跨域资源的 URL,并指定一个回调函数名作为参数,使得服务器返回的 JavaScript 数据作为回调函数的参数执行。
  3. 代理服务器:

    • 可以在同源的服务器上设置一个代理服务器,用来转发跨域请求。前端代码发送请求到代理服务器,再由代理服务器转发请求到目标服务器,然后将目标服务器的响应返回给前端。
  4. 跨域资源共享(CORS)代理:

    • 有些公开的 CORS 代理服务器可以帮助解决跨域问题,前端代码发送请求到 CORS 代理服务器,再由 CORS 代理服务器转发请求到目标服务器,并将目标服务器的响应返回给前端。
  5. WebSocket:

    • WebSocket 不受同源策略的限制,可以通过 WebSocket 进行跨域通信。
  6. IFrame:

    • 可以使用 iframe 来加载跨域的内容,通过 postMessage 方法实现父子页面之间的通信。
  7. 反向代理:

    • 在服务器端设置反向代理,将客户端请求发送到目标服务器,然后将目标服务器的响应返回给客户端,客户端认为是同源的。

以上解决方案各有优缺点,需要根据具体的情况和需求选择合适的方案。

  1. rem 理解 rem 是一种相对长度单位,它代表相对于根元素(即 <html> 元素)的字体大小(font-size)的倍数。与 em 单位类似,但不同之处在于 rem 是相对于根元素而言,而 em 则是相对于父元素的字体大小。

使用方法:

  1. 设置根元素的字体大小:

    • 通过在 CSS 中设置 <html> 元素的 font-size 属性来确定 rem 单位的基准大小。通常将其设置为 16px 是一种常见的做法。
    html {
        font-size: 16px;
    }
    
  2. 使用 rem 单位:

    • 一旦确定了根元素的字体大小,你可以在 CSS 中使用 rem 单位来定义其他元素的尺寸。
    .box {
        width: 10rem; /* 相当于 160px */
        height: 5rem; /* 相当于 80px */
        font-size: 1.5rem; /* 相当于 24px */
    }
    

优势:

  1. 相对性:

    • 使用 rem 单位可以确保页面元素的尺寸是相对于根元素的字体大小而言的,这样可以简化样式表的编写,并且使得页面在不同屏幕尺寸和分辨率下更具灵活性和一致性。
  2. 适配性:

    • 由于 rem 单位是相对于根元素的,因此可以轻松地进行响应式设计和移动端适配,只需在根元素的字体大小上做出相应调整即可。

注意事项:

  1. IE8及以下版本不支持:

    • 如果需要兼容较旧的浏览器,需要提供备用方案。
  2. 字体大小不可过小:

    • 由于 rem 单位是相对于根元素的字体大小,如果根元素的字体大小设置过小,会导致 rem 单位的尺寸也很小,可能影响到页面元素的可读性和可点击性。因此,通常建议将根元素的字体大小设置为一个较合适的基准值。

总的来说,rem 单位是一种非常便捷和灵活的相对长度单位,特别适用于构建响应式和移动优先的网页设计。 3. iframe 的作用与使用场景, 如何与父级通信, 如果遇到跨域问题如何解决 iframe 的作用与使用场景:

  1. 作用:

    • iframe(Inline Frame)是 HTML 中的一种元素,用于在当前 HTML 文档中嵌入另一个 HTML 文档。通过 iframe,可以在页面中嵌入其他网页、媒体内容或者其他外部资源。
  2. 使用场景:

    • 嵌入外部内容: 最常见的使用场景是在网页中嵌入其他网页或第三方内容,比如嵌入地图、视频、广告等。
    • 分割页面内容: 可以将页面划分为多个区域,每个区域独立加载不同的内容。
    • 加载第三方组件: 比如嵌入社交分享按钮、评论框等第三方组件。
    • 实现异步加载: 可以在 iframe 中加载异步内容,避免阻塞主页面的加载。

与父级通信:

在 iframe 内部的页面(子页面)可以通过 window.parent 对象与父级页面通信,而父级页面也可以通过 iframe 元素的 contentWindow 属性与子页面通信。常见的通信方式有以下几种:

  1. postMessage:

    • 子页面可以使用 window.parent.postMessage() 方法向父级页面发送消息,父级页面可以通过监听 message 事件来接收消息。反之,父级页面也可以使用 iframe.contentWindow.postMessage() 方法向子页面发送消息。
  2. Hash(哈希):

    • 子页面可以通过修改 URL 的哈希值来传递信息,父级页面可以通过监听 hashchange 事件来获取哈希值的变化。但这种方法只能传递简单的字符串信息,且受浏览器限制,无法跨域通信。
  3. Window.name:

    • 子页面可以通过修改 window.name 属性来传递信息,父级页面可以读取子页面的 window.name 属性来获取信息。但同样受到浏览器限制,只适用于同域通信。

跨域问题的解决:

如果 iframe 的源与父级页面的源不同,就会面临跨域问题。解决跨域问题的方法有以下几种:

  1. CORS(跨域资源共享):

    • 在服务器端配置 CORS 头部,允许跨域请求。
  2. postMessage:

    • 使用 postMessage 方法进行跨域通信,但需要在目标页面和父级页面都进行安全校验。
  3. 代理页面:

    • 在同源的情况下,通过代理页面来转发请求,绕过跨域限制。
  4. JSONP(仅限 GET 请求):

    • 如果允许,可以使用 JSONP 来进行跨域请求。
  5. 修改 document.domain

    • 如果两个页面的域名具有相同的父域名,可以通过设置 document.domain 来实现跨域通信。这种方法需要在两个页面中都设置相同的 document.domain
  6. 普通函数与箭头函数的区别, 箭头函数的 this 指向 普通函数与箭头函数之间有几个关键的区别,其中最重要的一个是箭头函数的 this 指向不同于普通函数。

普通函数与箭头函数的区别:

  1. 语法:

    • 普通函数使用 function 关键字来声明,例如:function add(a, b) { return a + b; }
    • 箭头函数使用箭头符号 => 来声明,例如:const add = (a, b) => a + b;
  2. this 的指向:

    • 在普通函数中,this 的值是在运行时确定的,取决于函数是如何调用的。如果函数被作为方法调用,则 this 指向调用该方法的对象;如果函数被单独调用,则 this 指向全局对象(在浏览器环境下通常是 window)。
    • 在箭头函数中,this 的值是在定义时确定的,指向定义时所在的词法作用域的 this 值,而不是运行时的 this。因此,箭头函数没有自己的 this,它会捕获上下文的 this 值。
  3. arguments 对象:

    • 普通函数中可以使用 arguments 对象来获取传递给函数的参数列表。
    • 箭头函数没有自己的 arguments 对象,但可以使用 ES6 的剩余参数语法(...args)来获取参数列表。
  4. 构造函数:

    • 普通函数可以作为构造函数来创建对象实例,通过 new 关键字调用时,函数内部的 this 指向新创建的对象实例。
    • 箭头函数不能用作构造函数,使用 new 关键字调用时会抛出错误。

箭头函数的 this 指向:

箭头函数没有自己的 this 绑定,而是继承自父作用域的 this 值。这意味着在箭头函数内部,this 的值是在定义时确定的,而不是在运行时。箭头函数的 this 值由包含它的非箭头函数决定,通常是最近的非箭头函数。

这个特性使得箭头函数特别适合于在回调函数中使用,因为它们可以捕获到所在上下文的 this 值,而不会受到回调函数调用方式的影响。这样可以避免了在回调函数中常见的 thatselfbind 等方式来绑定 this。 5. 对于闭包的理解 6. 实习业务介绍 7. 项目难点 8. 项目中遇到的性能优化是如何做的 9. 前端性能优化有哪些方向

前端性能优化是提高网站性能和用户体验的关键,可以从多个方向进行优化。以下是一些常见的前端性能优化方向:

  1. 减少 HTTP 请求:

    • 合并和压缩 CSS 和 JavaScript 文件。
    • 使用 CSS 精灵图来减少图片请求。
    • 使用字体图标代替图片,减少字体文件的请求。
    • 使用 HTTP/2 或者 HTTP/3 协议来减少请求的数量。
  2. 优化资源加载:

    • 使用异步加载(defer 和 async)来延迟非关键资源的加载。
    • 使用预加载和预渲染来提前加载关键资源。
    • 使用懒加载来延迟加载图片和其他资源。
  3. 优化图片:

    • 使用适当的图片格式(比如 WebP、JPEG XR)来减小图片大小。
    • 压缩图片,减小文件大小。
    • 使用图片占位符和渐进式加载来提高页面加载速度。
  4. 减少页面体积:

    • 移除不必要的代码和注释。
    • 优化 HTML、CSS 和 JavaScript 代码结构,减小文件大小。
    • 使用代码分割和按需加载来减少页面体积。
  5. 使用缓存:

    • 使用浏览器缓存(强缓存和协商缓存)来减少服务器请求。
    • 使用 Service Worker 来实现离线缓存和资源预取。
  6. 优化渲染性能:

    • 减少页面重排和重绘,避免使用不必要的 CSS 属性。
    • 使用 CSS 动画和过渡代替 JavaScript 动画,提高动画性能。
    • 使用 requestAnimationFrame 来优化动画性能。
  7. 优化 JavaScript 执行性能:

    • 减少 JavaScript 的执行时间,避免长时间运行的 JavaScript 代码。
    • 使用事件委托和事件冒泡来减少事件处理器的数量。
    • 使用 Web Worker 来执行后台任务,避免阻塞主线程。
  8. 优化网络请求:

    • 使用 CDN 加速静态资源的加载。
    • 使用 DNS 预解析来提前解析域名。
    • 减少重定向和跳转,提高页面加载速度。
  9. 移动端优化:

    • 使用响应式设计和弹性布局来适配不同屏幕尺寸。
    • 使用移动端专属的 CSS 和 JavaScript 框架,提高移动端性能。
  10. 监控和优化:

    • 使用性能分析工具(比如 Chrome 开发者工具、Lighthouse)来监控页面性能。
    • 定期检查和优化页面,确保页面性能保持在一个合理的水平。
  11. 竞态问题如何解决, 点击 TabA, 数据未返回又点击 TabB, 数据返回了, 如何保证数据的正确性

竞态问题指的是在并发执行的情况下,由于执行顺序不确定而导致的结果不确定性。在你描述的情况下,用户在点击 TabA 后,数据未返回就又点击了 TabB,而此时数据返回了,需要保证数据的正确性。以下是一种常见的解决方案:

  1. 取消之前的请求

    • 在用户切换 TabA 和 TabB 时,首先取消之前发出的请求,以确保不会因为请求顺序不一致而导致数据混乱。
    • 可以使用一个变量来保存当前正在进行的请求,并在发起新的请求时先取消旧的请求。
  2. 标记请求

    • 在发起请求时,可以为每个请求添加一个唯一的标识符(比如一个序号或者一个随机生成的字符串)。
    • 当数据返回后,检查返回的数据是否与当前 Tab 相关联,只有与当前 Tab 相关联的数据才会被使用。
  3. 使用 Promise 对象

    • 使用 Promise 对象来处理异步请求,可以更容易地控制请求的顺序和并发。
    • 当用户切换 Tab 时,可以创建一个新的 Promise 对象,并在切换完成后再处理返回的数据。
  4. 防止重复处理

    • 在处理返回的数据时,可以添加一些逻辑来防止重复处理数据。比如使用一个标志位来表示数据是否已经被处理过,如果已经处理过则不再重复处理。

下面是一个简单的示例,演示了如何使用 Promise 对象来解决竞态问题:

let currentRequest = null;

function fetchData(tabId) {
    // 取消之前的请求
    if (currentRequest) {
        currentRequest.cancel(); // 假设有一个取消请求的方法
    }

    // 创建一个新的 Promise 对象
    const promise = new Promise((resolve, reject) => {
        currentRequest = makeRequest(tabId, resolve, reject);
    });

    // 返回 Promise 对象
    return promise;
}

function makeRequest(tabId, resolve, reject) {
    // 发起异步请求
    const request = sendRequest(tabId);

    // 请求成功时,调用 resolve 方法
    request.then((data) => {
        resolve(data);
    }).catch((error) => {
        reject(error);
    });

    // 返回请求对象
    return request;
}

// 示例:模拟发送请求的方法
function sendRequest(tabId) {
    // 假设这里是发送异步请求的过程
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const responseData = {
                tabId: tabId,
                data: "data for tab " + tabId
            };
            resolve(responseData);
        }, Math.random() * 2000); // 模拟异步请求的延迟时间
    });
}

// 示例:点击 TabA 和 TabB 时分别发起请求
function handleTabClick(tabId) {
    fetchData(tabId)
        .then((data) => {
            // 检查返回的数据是否与当前 Tab 相关联
            if (data.tabId === tabId) {
                console.log("Received data for tab " + tabId + ": " + data.data);
            }
        })
        .catch((error) => {
            console.error("Error fetching data:", error);
        });
}

// 示例:模拟用户点击 TabA 和 TabB
handleTabClick("TabA");
handleTabClick("TabB");

在这个示例中,fetchData() 函数用于发起异步请求,并返回一个 Promise 对象。在用户点击不同的 Tab 时,会调用 handleTabClick() 函数发起请求,并在数据返回后处理返回的数据,同时检查返回的数据是否与当前 Tab 相关联,以保证数据的正确性。