常考的前端面试题(一)

191 阅读10分钟

0.1 + 0.2

在很多语言中,浮点数相加都会产生精度丢失,导致0.1+0.2不等于0.3

image.png

js 采用 IEEE 754规范 (双精度 64位),要弄清楚到底是怎么一回事,就需要把十进制数字转化成计算机可读的二进制数字。

十进制中的小数,比如 0.10.2,在二进制中是无法精确表示的,因为它们会转化为一个无限循环的二进制小数。以 0.1 为例:

  • 0.1 用二进制表示是 0.0001100110011001100110011001100110011001100110011...(无限循环) 相同地,0.2 也会转换为另一个无限循环的二进制数:
  • 0.2 用二进制表示是 0.001100110011001100110011001100110011001100110011...(同样是无限循环)

JavaScript 使用 IEEE 754 双精度格式来表示数字。这种格式由三个部分组成:

  • 符号位 (1位) : 表示数字的正负。
  • 指数 (11位) : 表示数字的大小(范围)。
  • 尾数 (52位) : 表示数字的精度(有效位)。

当计算机将 0.10.2 转换为二进制时,它们会被截断或者舍入到最近的可表示的值,因为尾数部分只能容纳有限的位数。这就导致了小数精度的丢失。

因为 0.10.2 无法精确表示,它们的近似值会相加。这时,结果并不是我们直观上期望的 0.3,而是一个非常接近 0.3 但并不等于 0.3 的数:

console.log(0.1 + 0.2); // 输出: 0.30000000000000004

这是因为两个近似值的和并不等于我们期望的确切值。

js事件流

js中的事件流分为三个阶段:捕获阶段,目标阶段和冒泡阶段。

捕获阶段就是从document根节点沿着DOM树向目标元素(被点击或者被触发的元素)传递;目标阶段就是事件到达元素本身;冒泡阶段就是从目标元素向上回传直到根节点。

在 JavaScript 中,我们可以为元素添加事件监听器,并指定监听器在哪个阶段触发。

添加事件监听器的语法

element.addEventListener(event, handler, useCapture);
  • event:要监听的事件类型,比如 'click'
  • handler:事件触发时的回调函数。
  • useCapture:布尔值,默认为 false,表示事件在冒泡阶段触发。如果设置为 true,表示事件在捕获阶段触发。

根据事件冒泡机制,可以实现一个高级的事件代理(Event-proxy)

事件代理(Event-proxy)的基本概念是:利用事件冒泡机制,将子元素的事件委托到父元素上。 通过在父元素上添加一个事件监听器,可以捕获所有子元素的事件,从而避免为每个子元素分别添加事件监听器。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>

        <script>
            let lis = document.querySelectorAll('li')

            lis.forEach((li) => {
                li.addEventListener('click',()=>{
                    console.log(li.innerText);
                })
            })
        </script>
    </ul>
</body>
</html>

在这个例子中,创建了一个lis去存储每一个li,通过forEach去遍历,为每一个li添加上点击事件。但是这个还不够优雅,当li很多的时候,需要的空间就会很大。

用上Event-proxy就能很优雅的解决这个问题。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>

        <script>
            let ul = document.querySelector('ul')
            ul.addEventListener('click',(e)=>{
                console.log(e.target.innerText);
            })
        </script>
    </ul>
</body>
</html>

通过给父元素ul绑定事件监听,冒泡机制为每一个li创建事件监听,当li被点击时,向上冒泡去触发ul的事件监听,打印出e.target(目标元素)的文本信息。

跨域

跨域是在浏览器中执行的网页尝试访问一个与其源(Origin)不同的资源。浏览器的同源策略 会阻止来自不同源的网页之间的请求,这种安全策略可以防止恶意网站窃取敏感数据。同源策略要求网页从相同的源请求资源。两个 URL 被认为是同源的,如果它们的协议(protocol) 、**域名(host)端口号(port)**都相同。

当我们的网页向不同源的服务器发送请求时,会被浏览器的同源策略拦截下来,实际上我们的请求是能发送的,如果要避免被浏览器拦截下来,就需要用到一些特殊的处理。

以下提供了这六种手段去解决跨域问题。

1.jsonp

jsonp属于是一种早期的解决跨域的方法,通过 <script> 标签来加载一个跨域的 JavaScript 文件。JSONP 利用了 <script> 标签不受同源策略限制的特点。

JSONP 的基本原理是:服务器返回的数据是一个 JavaScript 函数调用而不是纯数据,客户端定义一个回调函数,服务器将数据作为参数传递给这个回调函数。

例如:

客户端请求:

<script src="https://anotherdomain.com/api/data?callback=myCallback"></script>

服务器响应:

myCallback({ "name": "John", "age": 30 });

在这种情况下,myCallback 是客户端定义的一个函数,服务器返回的数据作为参数传递给它。

注意: JSONP 只支持 GET 请求,因此不适合传递敏感数据。

2.cors

cors属于是最常用的一种解决跨域的方法。它允许服务器在响应头中指定哪些源可以访问资源。

3.webscoket

WebSocket 协议本身并没有直接解决跨域问题的机制,但它可以在一定程度上绕过传统的 HTTP 跨域限制。这主要是因为 WebSocket 的握手过程使用了 HTTP,但在连接建立后,通信不再使用 HTTP 请求,因此不受浏览器的同源策略限制。

4.nginx

通过nginx反向代理,绕开浏览器的同源策略,反向代理的核心功能是将客户端的请求转发到后端服务器,并将后端服务器的响应结果返回给客户端。这样,客户端并不直接和后端服务器通信,而是通过 Nginx 作为中间层来实现数据的传递。

5.postMessage

window.postMessage 通过发送跨域消息的方式,实现两个窗口(如 iframe 与父页面)之间的安全通信。

6.domain

document.domain 适用于在同一主域(如 example.com)下的不同子域之间的跨域访问。需要将两个页面的 document.domain 都设置为相同的主域。

浏览器存储

localStorage(本地存储):是HTML5新更新的存储API,可以持久化存储,存储在浏览器中,只有5MB左右,以键值对的形式存储。

sessionStorage(会话存储):也是HTML5新更新的存储API,在会话结束后清除,也只有5MB左右,以键值对的形式存储。

indexedDB:indexedDB 是浏览器提供的一个本地数据库,允许你以结构化方式存储大量数据,并进行索引、搜索等操作。其存储空间取决于你的设备的存储空间,硬盘有多大,它就能有多大。

cookie:cookie 是一种在浏览器中存储少量文本数据的方式,通常用于在浏览器和服务器之间交换数据。

image.png

输入url到页面渲染

  • 网络请求

    DNS解析:需要把域名解析成IP地址。

    这里又涉及到浏览器先向本地缓存查找是否有缓存好的IP地址,如果没找到,再去本地的DNS服务器发起查询请求,如果还没找到就去网站后缀查找,例如 juejin.cn .cn后缀就去中国找,如果是.com后缀就是全球找。

    建立TCP连接:

    这里涉及到TCP三次握手

    三次握手的步骤

  1. 第一次握手:客户端发送 SYN 包

    • 客户端(Client)服务器(Server) 发送一个 SYN(同步序列编号,Synchronize Sequence Number) 标志的数据包,表示请求建立连接。
    • 这个数据包包含一个初始的序列号 Seq = x,表示客户端希望与服务器建立连接。
    Client -> Server: [SYN, Seq=x]
    
  2. 第二次握手:服务器发送 SYN-ACK 包

    • 服务器(Server) 接收到客户端的 SYN 请求后,返回一个 SYN-ACK(同步-确认,Synchronize-Acknowledgment) 标志的数据包。
    • 服务器将其自身的初始序列号 Seq = y 发送给客户端,同时对客户端的序列号 x 进行确认,设置 ACK = x + 1
    • 这一步表示服务器收到了客户端的请求,并同意建立连接。
    Server -> Client: [SYN, Seq=y, ACK=x+1]
    
  3. 第三次握手:客户端发送 ACK 包

    • 客户端(Client) 接收到服务器的 SYN-ACK 包后,向 服务器(Server) 发送一个 ACK(确认,Acknowledgment) 标志的数据包。
    • 这个数据包中,客户端对服务器的序列号 y 进行确认,设置 ACK = y + 1,并使用序列号 Seq = x + 1
    • 这一步表示客户端和服务器都已确认连接的建立。
    Client -> Server: [ACK, Seq=x+1, ACK=y+1]
    

当服务器收到客户端的 ACK 包时,连接建立完成,客户端和服务器之间可以开始数据传输。

三次握手的示意图

Client                        Server
   | --- SYN, Seq=x --->       |
   | <--- SYN, Seq=y, ACK=x+1--|
   | --- ACK, Seq=x+1, ACK=y+1--> 
   |___________________________|
          Connection Established

发送HTTP请求: (如果是HTTPS还需要多加一个TLS进行加密,HTTPS = HTTP + TLS)

-   建立好连接后,浏览器发送HTTP请求。
-   请求头包含URL、HTTP方法(GET、POST等)、User-Agent等信息。

服务器处理请求:

-   服务器接收到请求,解析请求头和请求体。
-   执行相应的业务逻辑,生成响应内容。
-   返回HTTP响应,包含状态码(如200 OK)、响应头和响应体。

接收HTTP响应:

-   浏览器接收到服务器的响应。
-   解析响应头,获取状态码和响应体。
-   如果状态码是200 OK,则继续处理响应体。
  • 渲染页面

    1.浏览器解析html文件 DOM树

    2.解析CSS文件 CSSOM树

    3.DOM树和CSSOM合并成渲染树

    4.计算布局layout(重排或者回流)

    5.绘制页面(重绘)

这里有一个考点:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        let box = document.getElementById('box')
        box.style.width = box.offsetWidth + 1 + 'px'
        box.style.width = '100px'
    </script>

</body>
</html>

问:这有几次重排重绘?答案为1次。

  • 浏览器的优化策略 将多次回流行为存入任务队列,当队列达到一定的阈值或者时间后会一次性全部执行

  • 所有的offsetWidth offsetHeight offsetTop offsetLeft offset... clientWidth clientHeight clientWidth client... scrollTop .... 以上这些会强制执行一次

预加载图片和懒加载图片

预加载图片(Preloading Images)是一种提前加载图片的技术,用于在用户需要图片之前就将其加载到浏览器的缓存中,以便在需要时能快速显示。

方法一:使用 JavaScript 预加载图片

const preloadImages = (imageUrls) => {
  imageUrls.forEach((url) => {
    const img = new Image();  // 创建一个新的 Image 对象
    img.src = url;  // 设置图片的 src 属性,浏览器会自动加载
  });
};

// 预加载图片
preloadImages([
  'https://example.com/image1.jpg',
  'https://example.com/image2.jpg'
]);

方法二:使用 <link> 标签进行预加载

<head> 标签中加入如下代码:

<link rel="preload" href="https://example.com/image1.jpg" as="image">

这种方式可以告诉浏览器提前加载这些资源。

懒加载图片(Lazy Loading Images)是一种在用户即将看到图片时才加载的技术,通常用于优化长页面或图片较多的页面,减少初始加载的内容,提高页面的首次加载速度。

方法一:使用原生 loading="lazy" 属性

HTML5 提供了一个 loading 属性,可以直接在 <img> 标签中使用:

<img src="https://example.com/image.jpg" alt="Example Image" loading="lazy">

这种方法非常简单易用,且被大多数现代浏览器所支持。

方法二:使用 JavaScript 实现懒加载

使用 IntersectionObserver API,可以监听图片是否进入视口,并在需要时加载:

document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = document.querySelectorAll("img[data-src]");
  
  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;  // 将真实的图片地址赋给 src
        observer.unobserve(img);  // 停止观察该图片
      }
    });
  });

  lazyImages.forEach(img => observer.observe(img));
});

image.png