春招面试万字整理,全程拷打,干货满满(3)

623 阅读25分钟

Map、Set 和 Object 区别

特性/对比项MapSetObject
数据结构键值对集合唯一值的集合键值对集合(键通常是字符串或符号)
键类型任意类型(包括对象、函数等)不适用字符串或符号(数字会被隐式转换为字符串形式的键。)
值唯一性键唯一,值可重复所有值都是唯一的允许重复值
插入顺序保持插入顺序保持插入顺序ES6之后保持插入顺序(数字键除外)
查询大小.size 属性.size 属性需要手动计算属性数量
性能更适合频繁增删操作适合存储唯一值动态增删键值对效率较低
迭代支持直接支持迭代 (forEachfor...of)直接支持迭代 (forEachfor...of)需要使用 Object.keys()Object.values(), 或 Object.entries()

浏览器沙盒

浏览器沙盒(Browser Sandbox)是一种安全机制,旨在限制网页内容对用户系统的访问权限,从而保护用户免受恶意软件、网络钓鱼和其他在线威胁的侵害。通过将网页内容和脚本运行在一个隔离的环境中,即使这些内容存在安全隐患,也能有效防止它们对系统造成损害。

沙盒的主要功能和特性包括:

  1. 隔离性:沙盒技术确保了每个标签页或每个网页都在其自己的独立环境中运行,这减少了不同网页之间以及网页与操作系统之间的潜在交互,降低了恶意代码利用漏洞进行攻击的风险。
  2. 权限控制:沙盒可以严格控制网页能够访问的资源类型和数量。例如,它可以阻止网页未经允许就访问用户的文件系统、摄像头或麦克风等硬件设备。
  3. 安全性增强:通过限制网页执行某些操作的能力,沙盒有效地增加了额外的安全层,使得即便浏览器本身存在漏洞,攻击者也难以利用这些漏洞执行有害操作。
  4. 崩溃隔离:如果某个网页或插件导致浏览器崩溃,沙盒可以确保这种崩溃不会影响到其他正在运行的网页或其他浏览器组件,提高了整体稳定性和用户体验。

Promise的静态方法

  • Promise.resolve

    返回一个以给定值为成功状态的Promise对象

  • Promise.reject

    返回一个以给定值为失败状态的Promise对象

  • Promise.all

    全部成功才成功,只要有一个失败就失败。Promise.all 返回的结果数组会按照传入的 Promise 顺序排列,而不是按照完成的顺序。

  • Promise.race

    Promise.race 会返回第一个被解决(无论是成功还是失败)的 Promise 的结果。 Promise.race 通常用于需要快速响应的场景。

  • Promise.allSettled

    Promise.allSettled 是 ES2020 引入的新方法。它接受一个 Promise 实例数组作为参数,并返回一个新的 Promise。这个新的 Promise 会在所有传入的 Promise 都完成(无论是成功还是失败)时被解决,返回一个包含所有结果的数组。Promise.allSettled 非常适合用于需要处理多个异步任务,并且希望获取所有任务的结果(无论成功还是失败)的场景。Promise.allSettled 返回的结果数组中的每个元素都是一个对象,包含 status 和 value(成功) 或 reason(失败) 属性。

  • Promise.any

    Promise.any 是 ES2021 引入的新方法。它接受一个 Promise 实例数组作为参数,并返回一个新的 Promise。这个新的 Promise 会在第一个成功的 Promise 完成时被解决,返回该 Promise 的结果。如果所有 Promise 都失败,Promise.any 会返回一个 AggregateErrorPromise.any 非常适合用于需要获取第一个成功结果的场景。如果所有 Promise 都失败,Promise.any 会返回一个 AggregateError,包含所有失败的原因。

虚拟列表

虚拟列表是一种用于优化长列表渲染的技术,特别适用于那些包含大量数据项的应用场景。通过只渲染当前可见区域的数据项,可以显著减少DOM元素的数量,从而提升应用的性能和响应速度。

image.png

webSocket 断开之后怎么办

断网重连

  1. 监听连接状态:首先,您需要监听WebSocket的onclose事件来检测连接何时关闭。这可以帮助您了解是否由于网络问题导致了连接中断。

  2. 设置重连尝试逻辑

    • 在检测到连接关闭后,可以立即尝试重新连接,或者等待一段时间后再尝试。
    • 使用指数退避算法(例如首次等待2秒,然后4秒、8秒等)增加每次重试前的等待时间,以避免对服务器造成过大压力。
    • 设置最大重试次数限制,防止在无法恢复的情况下无限重试。
  3. 成功重连后的处理:一旦成功重连,可能需要重新发送在断开期间未能成功发送的消息,或者同步客户端与服务器之间的状态。

let ws = null;
// 是否正在尝试重新连接
let isReconnecting = false;
// 重连尝试次数
let reconnectAttempts = 0;
// 最大重连次数
const MAX_RECONNECT_ATTEMPTS = 5;

/**
 * 初始化WebSocket连接
 */
function initWebSocket() {
    // 替换为你的WebSocket服务器地址
    const wsUrl = 'ws://yourserver.com/socket';
    ws = new WebSocket(wsUrl);

    // 当WebSocket连接成功打开时触发
    ws.onopen = function(event) {
        console.log('WebSocket连接已打开', event);
        reconnectAttempts = 0; // 成功连接后重置重连计数器
    };

    // 当收到消息时触发
    ws.onmessage = function(event) {
        console.log('收到消息:', event.data);
    };

    // 当WebSocket连接关闭时触发
    ws.onclose = function(event) {
        console.log('WebSocket连接关闭:', event);
        // 如果不是主动重连且未达到最大重试次数,则进行重连
        if (!isReconnecting && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
            setTimeout(() => { // 等待一段时间后尝试重新连接
                isReconnecting = true;
                reconnectAttempts++;
                initWebSocket(); // 尝试重新初始化WebSocket连接
            }, 2000); // 等待2秒
        } else {
            console.error('达到最大重连次数,停止尝试');
        }
    };

    // 当发生错误时触发
    ws.onerror = function(event) {
        console.error('WebSocket错误:', event);
    };
}

// 页面加载完成后初始化WebSocket
window.onload = function() {
    initWebSocket();
};

自动心跳

  1. 目的:心跳机制的主要目的是保持客户端和服务器之间的连接活跃,避免由于长时间没有数据传输而导致的连接被中间设备(如防火墙或代理服务器)自动断开。

  2. 实现方法

    • 客户端定期向服务器发送“ping”消息,服务器接收到后回复一个“pong”响应。
    • 如果客户端在设定的时间间隔内没有收到“pong”响应,则认为连接已失效,并触发重连逻辑。
    • 同样,服务器也可以主动发送“ping”,客户端回应“pong”来确保双向通信的有效性。
  3. 注意事项

    • 选择合适的心跳间隔非常重要。如果间隔太短,会增加不必要的网络流量;如果间隔太长,则可能不足以维持连接的活跃状态。
    • 确保心跳消息尽可能简单,以减少带宽消耗和服务器负载。
// 心跳定时器
let heartbeatTimer = null;
// 心跳间隔时间(毫秒),比如30秒
const HEARTBEAT_INTERVAL = 30000;


// 开始心跳检测
function startHeartbeat() {
    if (heartbeatTimer) return; // 如果已经有定时器在运行,则不创建新的定时器
    sendPing(); // 立即发送一次ping
    // 设置定期发送ping消息的定时器
    heartbeatTimer = setInterval(sendPing, HEARTBEAT_INTERVAL);
}

// 停止心跳检测
function stopHeartbeat() {
    if (heartbeatTimer) { // 如果存在定时器,则清除它
        clearInterval(heartbeatTimer);
        heartbeatTimer = null;
    }
}

// 发送ping消息
function sendPing() {
    if (ws.readyState === WebSocket.OPEN) { // 检查WebSocket是否处于打开状态
        ws.send('ping'); // 实际应用中可能需要发送特定格式的消息
    } else {
        console.error('WebSocket连接未打开,无法发送ping');
    }
}

// 在WebSocket连接成功建立时启动心跳
ws.onopen = function(event) {
    console.log('WebSocket连接已打开', event);
    reconnectAttempts = 0; // 成功连接后重置重连计数器
    startHeartbeat(); // 启动心跳检测
};

// 在WebSocket连接关闭时停止心跳
ws.onclose = function(event) {
    console.log('WebSocket连接关闭:', event);
    stopHeartbeat(); // 连接关闭时停止心跳检测
    // ... 继续之前的重连逻辑 ...
};

通过结合使用断网重连和心跳机制,可以大大提高WebSocket应用的稳定性和可靠性,确保即使在网络条件不佳的情况下也能提供良好的用户体验。

响应式失效(toRefs 和 toRef)

  • 范围toRefs适用于整个响应式对象,将其所有属性转化为响应式引用;而toRef仅针对单一属性。
  • 目标:当你想要解构一个响应式对象并保留每个属性的响应性时,应使用toRefs。如果你只想专注于某个特定属性,则使用toRef更合适。

排序算法

image.png

冒泡排序

通过重复遍历列表,比较相邻元素并交换它们的位置来排序列表。每次遍历后,最大的元素会“冒泡”到列表的末尾。

function bubbleSort(arr) {
    let n = arr.length;
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换
            }
        }
    }
    return arr;
}

选择排序

从未排序部分中找到最小(或最大)元素,将其与未排序部分的第一个元素交换位置。

function selectionSort(arr) {
    const n = arr.length;
    for (let i = 0; i < n; i++) {
        let minIndex = i;
        for (let j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex !== i) {
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; // 交换
        }
    }
    return arr;
}

插入排序

构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

function insertionSort(arr) {
    for (let i = 1; i < arr.length; i++) {
        let current = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > current) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = current;
    }
    return arr;
}

希尔排序

基于插入排序的改进版,通过使用增量间隔逐步减少的方式来排序数组。

function shellSort(arr) {
    let n = arr.length;
    for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
        for (let i = gap; i < n; i++) {
            let temp = arr[i];
            let j;
            for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
                arr[j] = arr[j - gap];
            }
            arr[j] = temp;
        }
    }
    return arr;
}

归并排序

采用分治法策略,首先递归分解数组直到每个子数组只有一个元素,然后合并这些子数组形成最终的有序数组。

function mergeSort(arr) {
    if (arr.length <= 1) return arr;

    const middle = Math.floor(arr.length / 2);
    const left = arr.slice(0, middle);
    const right = arr.slice(middle);

    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
    let result = [];
    let leftIndex = 0;
    let rightIndex = 0;

    while (leftIndex < left.length && rightIndex < right.length) {
        if (left[leftIndex] < right[rightIndex]) {
            result.push(left[leftIndex++]);
        } else {
            result.push(right[rightIndex++]);
        }
    }

    return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

快速排序

采用分治法策略,选定一个“基准”,将数组分为两部分,使得一部分的所有元素都小于另一部分的所有元素,然后递归地对这两部分进行排序。

function quickSort(arr) {
    if (arr.length <= 1) return arr;

    const pivotIndex = Math.floor(arr.length / 2);
    const pivot = arr[pivotIndex];
    const left = [];
    const right = [];

    for (let i = 0; i < arr.length; i++) {
        if (i === pivotIndex) continue;
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }

    return [...quickSort(left), pivot, ...quickSort(right)];
}

堆排序

利用堆这种数据结构设计的排序算法,首先将待排序序列构造成一个大顶堆,然后依次取出堆顶元素(最大值),再调整剩余元素重新成为堆。

function heapSort(arr) {
    buildMaxHeap(arr);
    for (let i = arr.length - 1; i > 0; i--) {
        [arr[0], arr[i]] = [arr[i], arr[0]];
        maxHeapify(arr, 0, i);
    }
    return arr;
}

function buildMaxHeap(arr) {
    const n = arr.length;
    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
        maxHeapify(arr, i, n);
    }
}

function maxHeapify(arr, i, heapSize) {
    const left = 2 * i + 1;
    const right = 2 * i + 2;
    let largest = i;

    if (left < heapSize && arr[left] > arr[largest]) largest = left;
    if (right < heapSize && arr[right] > arr[largest]) largest = right;

    if (largest !== i) {
        [arr[i], arr[largest]] = [arr[largest], arr[i]];
        maxHeapify(arr, largest, heapSize);
    }
}

计数排序

适用于一定范围内的整数排序,通过统计每个整数出现的次数来确定各个数字的位置。注意: 此函数仅适用于非负整数

function countingSort(arr) {
    const max = Math.max(...arr);
    const count = new Array(max + 1).fill(0);
    const output = new Array(arr.length);

    for (const num of arr) {
        count[num]++;
    }

    for (let i = 1; i < count.length; i++) {
        count[i] += count[i - 1];
    }

    for (let i = arr.length - 1; i >= 0; i--) {
        output[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }

    return output;
}

桶排序

将数组分到有限数量的桶里,每个桶内再进行排序(通常使用快速排序或其他排序算法)。

function bucketSort(arr, bucketSize = 5) {
    if (arr.length === 0) return arr;

    const minValue = Math.min(...arr);
    const maxValue = Math.max(...arr);
    const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
    const buckets = Array.from({ length: bucketCount }, () => []);

    arr.forEach(val => {
        const bucketIndex = Math.floor((val - minValue) / bucketSize);
        buckets[bucketIndex].push(val);
    });

    return buckets.reduce((prev, curr) => prev.concat(curr.sort((a, b) => a - b)), []);
}

基数排序

按位排序,从最低有效位开始逐位对所有数字进行排序。

function radixSort(arr) {
    const maxDigitLength = getMaxDigits(arr);
    for (let digitPlace = 0; digitPlace < maxDigitLength; digitPlace++) {
        const buckets = Array.from({ length: 10 }, () => []);
        arr.forEach(num => {
            const digit = getDigit(num, digitPlace);
            buckets[digit].push(num);
        });
        arr = [].concat(...buckets);
    }
    return arr;

    function getDigit(num, place) {
        return Math.floor(Math.abs(num) / Math.pow(10, place)) % 10;
    }

    function getMaxDigits(nums) {
        let maxDigits = 0;
        nums.forEach(num => {
            maxDigits = Math.max(maxDigits, num.toString().length);
        });
        return maxDigits;
    }
}

部分方法还有一些能够优化的地方,可以去看看这位大佬整理的文章。

重温前端10大排序算法(长文建议收藏)通过相邻元素的比较和交换,使得每一趟循环都能找到未有序数组的最大值或最小值。 - 掘金

webpack 和 vite 区别

特性WebpackVite
启动速度较慢,需打包整个项目才能启动开发服务器。非常快,基于ESM按需加载,启动迅速。
热更新(HMR)支持,但随项目规模增大可能变慢。支持,且通常比Webpack更快。
配置复杂度复杂,高度可定制,适合复杂项目。简单,默认配置满足多数需求,易于上手。
构建效率生产环境构建优化好,但首次构建时间长。生产环境使用Rollup高效构建;开发环境快速响应。
适用场景大型、复杂项目,需要高级定制化构建流程。追求开发体验和速度的新项目,特别是现代框架。
核心理念一切皆模块,通过loader和plugin扩展功能。利用浏览器ESM支持,实现极速冷启动和编译。

路由传参

query

query参数是通过URL的查询字符串传递的数据,形式为?key=value&key2=value2

  • 数据直接显示在URL中,因此可以通过浏览器地址栏直接访问。
  • 刷新页面不会丢失参数。
  • 适合用于需要共享链接并且希望接收方能够直接看到参数的情况。
// 跳转并传递query参数
this.$router.push({ path: '/user', query: { id: '123', name: 'Zhang' }})

// http://yourdomain.com/user?id=123&name=Zhang
// 获取query参数
console.log(this.$route.query.id) // 输出: '123'

params

params参数是嵌入到路由路径中的动态段,例如:id

  • 参数不直接显示在URL的查询字符串部分,而是作为路径的一部分。
  • 如果刷新页面,且路由配置中没有设置永久重定向或其他保持参数的策略,参数可能会丢失。
  • 更加美观,适合于那些不需要直接通过URL分享的状态信息。
  • 使用 path 属性时,不能使用 params
// 定义路由时使用动态段
const routes = [{ path: '/user/:id', component: User }]

// 跳转并传递params参数
this.$router.push({ name: 'user', params: { id: '123' }})

// 获取params参数
console.log(this.$route.params.id) // 输出: '123'

props

props是一种将数据作为属性传递给组件的方式。当与路由结合使用时,可以更灵活地向目标组件传递数据。

  • 提供了更高的灵活性,允许你选择如何处理路由参数(如直接作为props传递、转换后再传递等)。
  • 可以使组件更加独立,因为它们不再直接依赖于$route对象。
// 路由
const routes = [
  { path: '/user/:id', component: User, props: true }
]

// 接收:id参数
export default {
  props: ['id'],
  // ...
}

html5 新特性

  • 语义化标签:header nav main footer section aside article

    image.png 好处:内容语义化 + 代码语义化

    语义化是指根据内容使用合适的标签,就是用标签做正确的事 对机器友好,文字表现力丰富,有利于搜索引擎爬虫爬取有效信息,有利于seo(搜索引擎优化),支持读屏软件,可以根据文章自动生成目录

    对开发者友好,增强了可读性,结构更加清晰,便于团队的开发与维护

  • 音频和视频:audio video

    • audio

      <audio controls>
        <source src="example.mp3" type="audio/mpeg">
        您的浏览器不支持 audio 元素。
      </audio>
      

      属性

      • autoplay:自动播放音频(注意:某些移动浏览器可能出于用户体验考虑限制此属性的效果)。
      • loop:循环播放音频。
      • muted:静音播放。
      • preload:规定是否/如何预加载音频文件(可选值:autometadatanone)。
    • video

      <video width="320" height="240" controls>
          <source src="example.mp4" type="video/mp4">
          <source src="example.ogg" type="video/ogg">
          您的浏览器不支持 video 标签。
      </video>
      

      除了与<audio>标签共享的一些属性还提供了一些特有的属性:

      • poster:指定视频加载前显示的图像。

      • defaultMuted:设置默认静音状态。

  • 表单

    • 表单类型:number date time email ...
    • 表单属性:placeholder(提示信息) required(必填) pattern(正则) min max autofocus(页面加载时自动获取焦点)等
  • 存储方式

    • localStorage:永久保存,除非手动删除
    • sessionStorage:关闭浏览器后清空
  • dom获取
    document.querySelector/all

  • canvas画布 可以通过JavaScript进行绘制图形、动画、图表等的强大工具

  • Geolocation
    允许用户与网站或应用分享他们地理位置的信息。

    if ("geolocation" in navigator) {
        console.log("Geolocation is supported!");
    } else {
        console.log("Geolocation is not supported.");
    }      
    // 获取用户的当前位置
    navigator.geolocation.getCurrentPosition(function(position) {
        console.log("Latitude is :", position.coords.latitude);
        console.log("Longitude is :", position.coords.longitude);
    });
    
  • 全双工通信协议Websoket
    WebSocket 提供了一种在浏览器和服务器之间进行实时双向通信的机制,适用于实时聊天、实时数据更新等场景。

// server.js
// 服务器
const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });
server.on('connection', (socket) => {
    console.log('New client connected');
    // 当收到消息时
    socket.on('message', (message) => {
        console.log(`Received message => ${message}`);
        // 向客户端发送消息
        socket.send('Message received');
    });
    // 当断开连接时
    socket.on('close', () => {
        console.log('Client disconnected');
    });
});
console.log('WebSocket server is running on ws://localhost:8080');
// 客户端
const socket = new WebSocket('ws://localhost:8080');
 socket.onopen = function() {
     console.log('Connected to the server');
 };
 socket.onmessage = function(event) {
     console.log(`Message from server: ${event.data}`);
 };
 socket.onclose = function() {
     console.log('Disconnected from server');
 };
 function sendMessage() {
     console.log('Sending message to server');
     socket.send('Hello, Server!');
 }
  • Web Worker 在浏览器中创建后台线程,执行 JavaScript 代码而不影响页面的响应性能。

Web Worker

  • Web Worker

    允许在 js 主线程之外开辟新的 Worker 线程。利用 js 操作多线程的能力

  • 在我们有大量运算任务时,可以把运算任务交给 Worker 线程去处理 当 Worker 线程计算完成,再把结果返回给 js 主线程 js 主线程只用专注处理业务逻辑,不用耗费过多时间去处理大量复杂计算 减少了阻塞时间,也提高了运行效率,页面流畅度和用户体验自然而然也提高了

  • Worker 在另一个全局上下文中运行,与当前的 window 不同!

    Worker 线程是在浏览器环境中被唤起的

    在 Worker 线程上下文中, window, parent, document 等属性都不可用

  • 但是可以使用 self 来代替 window

    可以使用 self.postMessage() 方法来向 js 主线程发送消息

    可以使用 self.onmessage 来监听 js 主线程发送的消息

  • 主线程与 worker 线程之间的数据传递是传值而不是传地址。所以你会发现,即使你传递的是一个Object,并且被直接传递回来,接收到的也不是原来的那个值了

  • 监听错误信息

    error // 当worker内部出现错误时触发

    messageerror // 当 message 事件接收到无法被反序列化的参数时触发

  • 关闭

    myWorker.terminate() // 主线程终止 worker 线程

    self.close() // worker 线程终止自身

    • 无论谁关闭 worker,worker 线程当前的 Event Loop 中的任务会继续执行。但下一个 Event Loop 不会继续执行
    • 主线程关闭,worker 线程会继续执行,但是主线程不会收到消息
    • worker 线程关闭,Event Loop 执行完之前 主线程还能收到消息
  • importScripts() 在 worker 线程中导入其他 js 文件

  • const myWorker = new Worker(aURL, options);

    • options
      • type: 'classic' | 'module' // 表示 worker 线程的类型,默认为 classic
      • credentials: 'omit' | 'same-origin' | 'include' // 表示 worker 线程的凭证,默认为 omit
      • name: string // 表示 worker 线程的名称,默认为空字符串
  • postMessage() 主线程和 worker 线程之间传递数据 可以是由结构化克隆算法处理的任何值或 JavaScript 对象,包括循环引用。 不能处理:

    • Error 以及 Function 对象;
    • DOM 节点
    • 对象的某些特定参数不会被保留
    • RegExp 对象的 lastIndex 字段不会被保留
    • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write
    • 原形链上的属性也不会被追踪以及复制。
// main.html
<!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>
    const myWorker = new Worker('./worker.js', {
      type: 'module', // 指定 worker.js 的类型
      name: 'myWorker', // 给worker线程命名
    }); // 创建worker

    // const obj = { name: '小明'}

    // 利用self判断是否在主线程
    // if (self === window) {
    //     console.log('代码运行在主线程');
    //   } else {
    //     console.log('代码运行在一个工作线程');
    // }

    myWorker.addEventListener('message', e => { // 接收消息
      console.log(e.data); // worker线程发送的消息
      // myWorker.terminate(); // 关闭 worker
    });
    // 这种写法也可以
    // myWorker.onmessage = e => { // 接收消息
    //    console.log(e.data);
    // };

    myWorker.postMessage('Greeting from Main.js'); // 向 worker 线程发送消息,对应 worker 线程中的 e.data
    // myWorker.postMessage(obj);

    // myWorker.terminate(); // 终止worker线程
  </script>
</body>
</html>
// worker.js(worker线程)
// self.addEventListener('message', e => { // 接收到消息
//   console.log(self);
//   console.log(e.data); // Greeting from Main.js,主线程发送的消息
//   self.postMessage('Greeting from Worker.js'); // 向主线程发送消息
//   // self.postMessage(e.data)
//   self.close();   // 关闭worker线程
// });


// 主线程关闭
// self.addEventListener('message', e => {
//   postMessage('Greeting from Worker');
//   setTimeout(() => {
//     console.log('setTimeout run');
//     postMessage('Greeting from SetTimeout');
//   });
//   Promise.resolve().then(() => {
//     console.log('Promise run');
//     postMessage('Greeting from Promise');
//   })
//   for (let i = 0; i < 1001; i++) {
//     if (i === 1000) {
//       console.log('Loop run');
//       postMessage('Greeting from Loop');
//     }
//   }
// });

// worker线程关闭
// setTimeout 宏任务 会放在下一个EventLoop中执行 下一个EventLoop 关闭worker,不执行
// for循环为当前宏任务中的一部分 Promise.resolve().then 微任务
self.addEventListener('message', e => {

  postMessage('Greeting from Worker');

  self.close(); // 关闭 worker

  setTimeout(() => {
    console.log('setTimeout run');
    postMessage('Greeting from SetTimeout');
  });

  Promise.resolve().then(() => {
    console.log('Promise run');
    postMessage('Greeting from Promise');
  })

  for (let i = 0; i < 3; i++) {
    if (i === 2) {
      console.log('Loop run');
      postMessage('Greeting from Loop');
    }
  }

});

// importScripts 导入其他js文件
importScripts('./utils.js');

// import add from './utils.js';
// console.log(self);
// console.log(add(1, 2));
// export default self;
// utils.js
// worker线程引用其他js文件
// const add = (a, b) => a + b;
export default function add(a, b){
  return a + b;
}

作用域 和 作用域链

作用域

作用域是定义变量可访问范围的一套规则,决定了程序中哪里可以访问或使用这些变量。 分为词法作用域和动态作用域。

查找规则分为:

  • 词法作用域(Lexical Scope) :在代码编写时确定,由代码嵌套结构决定。变量查找是从当前作用域向上查找,直到全局作用域。
  • 动态作用域(Dynamic Scope) :变量查找是在函数调用时根据调用栈来决定,而不是代码结构。比如this。

作用域的范围分为:

  • 全局作用域

    在代码的最外层定义的变量拥有全局作用域,它们可以在代码的任何地方被访问。在浏览器环境下,全局作用域通常绑定到window对象上。在node环境下,全局作用域通常绑定到global对象上

    注意: 使用letconst声明的变量不会挂载到window对象上。这一改变是ES6引入块级作用域的一个重要特性,旨在解决变量声明提升和变量污染问题。解决方案是TDZ。

  • 函数作用域 在函数内部定义的变量拥有函数作用域,它们只在该函数内部及嵌套的函数内部可见。

  • 块级作用域(ES6引入) 使用letconst声明的变量具有块级作用域,这意味着它们只在定义它们的代码块(如if语句或for循环)内部可见。

暂时性死区(TDZ, Temporal Dead Zone)  在使用letconst声明变量之前,该变量在其声明之前的区域被称为暂时性死区,任何访问该变量的尝试都将导致错误。

作用域链

  • 当JavaScript引擎需要查找一个变量的值时,它会遵循一定的规则去查找,这个查找路径就是作用域链。作用域链是由当前执行环境的作用域与其所有外部作用域组成的链式结构。

  • 每个函数在创建时,都会生成一个新的执行上下文,这个执行上下文中会包含一个指向其外部作用域的引用,这样就形成了一个链式结构。

image.png

  • 查找变量时,引擎首先会在当前作用域查找,如果找不到,就会向上一级作用域继续查找,直到全局作用域。如果在全局作用域也未找到,则认为该变量未定义。

  • 作用域链确保了函数可以访问其自身作用域以及包含它的所有外部作用域中的变量,但外部作用域无法访问函数内部的变量,体现了JavaScript的词法作用域特性。

原型 和 原型链

原型(Prototype)

每个JavaScript对象都有一个内部属性,称为[[Prototype]],它指向另一个对象,即该对象的原型,用于存放所有实例共享的属性和方法,当一个函数作为构造函数创建实例时,这个实例的内部将有一个隐式链接(proto)指向构造函数的prototype对象,这就是原型链的基础。

原型链(Prototype Chain)

原型链是JavaScript实现继承的基础,形成了一种链式的继承结构。当试图访问对象的一个属性时,如果没有在该对象本身找到此属性,则会在其原型对象中继续寻找,依次向上直到原型链的末端。如果在整个原型链中都没有找到该属性,则返回undefined

image.png

new 操作符(原理 + 手写)

在 JavaScript 中,new 是一个关键字,用于创建用户定义对象的实例或内置对象的实例。它主要用于面向对象编程(OOP)中,通过构造函数来创建新对象。

工作过程

  1. 创建新对象
  2. 设置原型链
  3. 绑定this值
  4. 执行构造函数
  5. 返回值处理
    • 如果构造函数没有明确返回一个对象(或者返回nullundefined),那么new操作符会自动返回新创建的对象。

    • 如果构造函数返回了一个对象,那么这个返回的对象将会替代步骤1中创建的新对象成为最终的结果。如果返回的是非对象值(如数字、字符串等),则忽略返回值,仍然返回新创建的对象。

手写 new

function Person(name, age) {
    this.name = name;
    this.age = age;
    return {
        name:this.name,
        age:this.age,
        tag:'haha'
    }
}

Person.prototype.sayName = function () {
    console.log(this.name);
}
// 手写new
function objectFactory(fn, ...args) {
    // arguments 所有参数 类数组
    // 1. 空对象创建
    // const obj = new Object();  
    const obj = {};
    // 2. 设置原型链
    obj.__proto__ = fn.prototype;
    // 3. 绑定this值 + 4. 执行构造函数
    const result = fn.apply(obj, args);  // 返回值
    // 5. 返回值处理
    // 如果返回对象 返回对象     
    // 如果返回简单数据类型或者没有  就返回 obj   可以是null(空对象)
    return typeof result === "object" ? result : obj;
}

// 检测 
let awei = objectFactory(Person, 'awei', 20)
// console.log(awei.name);
// awei.sayName();
console.log(awei);

setTimeout/setInterval 不准的原因

setTimeout/setInterval

setTimeout()方法用于在指定的毫秒数后调用函数或计算表达式。 setInterval()方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。

返回一个 定时器ID(数字),可以将这个ID传递给clearTimeout()clearInterval()来关闭定时器。

setTimeout(functionRef, delay, param1, param2, /* …, */ paramN)
setInterval(func, delay, arg1, arg2, /* …, */ argN)

都是有三个参数

  1. functionRef:(必填)当定时器经过delay后要执行的函数
  2. delay:(可选)定时器在执行指定的函数或代码之前应该等待的时间,单位是毫秒。如果省略该参数,则使用值 0,意味着“立即”执行,或者更准确地说,在下一个事件循环执行。(是宏任务)
  3. param1/arg1paramN/argN:(可选)会被传递给由 functionRef 指定的函数的附加参数。

延时比指定值更长的原因

造成setTimeoutsetInterval不准确的原因,MDN官方已经帮我们详细的整理好了,可以通过下面的链接查看详细内容

Window:setTimeout() 方法 - Web API | MDN

在这里,我将 MDN 中的原因再做了一个整理

  1. 嵌套超时

    HTML标准中规定,setTimeout如果被嵌套调用5次,从第五次开始,浏览器将强制执行4ms的最小超时。

  2. 非活动标签的超时(失活页面)

    为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在非活动标签中强制执行一个最小的超时延迟。如果一个页面正在使用 Web 音频 API AudioContext 播放声音,也可以不执行该延迟。不同的浏览器具体延迟并不相同。

  3. 追踪型脚本的限流

    浏览器对它识别为追踪型脚本的脚本实施额外的限流。当在前台运行时,限流的最小延迟仍然是 4 ms。然而,在后台标签中,限流的最小延迟是 10000 ms(即 10 s),在文档首次加载后 30 s开始生效。

  4. 超时延迟(JS单线程)

    如果页面(或操作系统/浏览器)正忙于其他任务(比如正在处理一个大的循环或复杂的操作),超时也可能比预期的晚。

    JavaScript 的执行是基于事件循环(Event Loop)的。setTimeout 并不立即执行,而是将回调函数放入事件队列中,直到当前执行栈清空。所以即使设定了精确的延迟,回调的执行时间也可能因为事件循环的队列处理和任务调度的顺序而有所不同

    也可以理解为设置setTimeout后的执行时间=执行栈清空的时间+duration

  5. 在加载页面时推迟超时

    当前标签页正在加载时,浏览器将推迟触发 setTimeout() 计时器。直到主线程被认为是空闲的(类似于 Window.requestIdleCallback(),或者直到加载事件触发完毕,才开始触发。

  6. WebExtension 背景页面和定时器

    WebExtensions 是一种用于开发浏览器扩展的跨浏览器系统。它提供了一种标准化的方式来创建可以在多个浏览器上运行的扩展。在 WebExtension中,setTimeout() 不会可靠工作。

  7. 最大延时值

    浏览器内部以 32 位带符号整数存储延时。

    // 浏览器中
    // 2 ** 32 - 5000  =>   -5000    =>   0     立即执行
    setTimeout(() => console.log("你好!"), 2 ** 32 - 5000);
    // 2 ** 32 + 5000  =>   5000    5秒后执行
    setTimeout(() => console.log("hi!"), 2 ** 32 + 5000);
    

    注:Node.js中任何大于 (2^31)-1 ms 都会立即执行,并且会报错

  8. 系统或硬件影响

    不同操作系统和硬件的性能差异也会影响 setTimeout 的精确度。如果系统负载较高,或者操作系统有时间调度策略,setTimeout 可能会稍微延迟执行。

代码输出题

来三道腾讯的事件循环的代码输出题结尾

第一题

console.log('1');
setTimeout(function () {
  console.log('2');
  process.nextTick(function () {
    console.log('3');
  })
  new Promise(function (resolve) {
    console.log('4');
    resolve();
  }).then(function () {
    console.log('5')
  })
})
process.nextTick(function () {
  console.log('6');
})
new Promise(function (resolve) {
  console.log('7');
  resolve();
}).then(function () {
  console.log('8')
})
setTimeout(function () {
  console.log('9');
  process.nextTick(function () {
    console.log('10');
  })
  new Promise(function (resolve) {
    console.log('11');
    resolve();
  }).then(function () {
    console.log('12')
  })
})

image.png

先执行宏任务script,打印1

第一个setTimeout放入宏任务队列

process.nextTick是微任务,放入微任务队列

Promise执行打印7,then放入微任务

第二个setTimeout放入宏任务队列

执行所有的微任务,打印6,8

执行第一个宏任务,打印2,4

执行微任务,打印3,5

执行第二个宏任务,打印9,11

执行微任务,打印10,12

注:本例中mjs与js的打印顺序不相同,在mjs下会先打印8,再打印6.

第二题

console.log(1);
const p = new Promise(r => setTimeout(r, 1000))
setTimeout(() => {
  console.log(2);
})
await p
console.log(3);

image.png

先打印1,

第一个setTimeout放入宏任务队列,

第二个setTimeout放入宏任务队列,

把 await p 之后的代码放入微任务队列

第二个setTimeout先执行掉,打印2

执行第一个setTimeout

执行微任务, 打印3

第三题

// 将时间改为0
console.log(1);
const p = new Promise(r => setTimeout(r, 0))
setTimeout(() => {
  console.log(2);
})
await p
console.log(3);

image.png

先打印1

第一个setTimeout放入宏任务队列,

第二个setTimeout放入宏任务队列,

第一个宏任务立即执行掉,打印微任务3

第二个宏任务,打印2