前端算法合集

4 阅读18分钟

前端算法

1.合并两个有序链表

  1. 递归方式
将两个升序链表合并为一个新的升序链表并返回, 新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入: 1->2->4, 1->3->4
输出: 1->1->2->3->4->4

const mergeTwoLists = function(l1, l2){
    if(l1 === null){
        return l2
    }
    
    if(l2 === null){
        return l1
    }
    
    if(l1.val < l2.val){
        l1.next = mergeTwoLists(l1.next, l2)
        return l1
    }else{
        l2.next = mergeTwoLists(l1, l2.next)
        return l2
    }
}

/**
M,N是两条链表l1, l2的长度
时间复杂度: O(M+N)
空间复杂度: O(M+N)
*/

2.通过迭代的方式实现

var mergeTwoLists = function (l1, l2){
    const prehead = new ListNode(-1)
    
    let prev= prehead
    
    while(l1 != null && l2!=null){
        if(l1.val <= l2.val){
            prev.next = l1;
            l1 = l1.next
        }else{
            prev.next =l2
            l2= l2.next
        }
        prev= prev.next
    }
    prev.next = l1 === null ? l2: l1
    return prehead.next
}

2. 括号生成

数组n代表生成括号的对数, 请你设计一个函数, 用于能够生成所有可能的并且
有效的括号组合
示例:
输入: n=3
输出: ['((()))', '(()())','(())()','()(())','()()()']

const generateParenthesis = function (n) {
    const res= [];
    function dfs(l, r, str){
        if(l == n && r == n){
            return res.push(str)
        }
        // 当l小于r的时候不满足条件, 剪枝
        if(l< r){
            retrun 
        }
        // l小于n 的时候可以插入左括号, 最多可以插入n个
        if(l<n){
            dfs(l+1,r, str+'(')
        }
        
        // r<l的时候, 可以插入右括号
        if(r<l){
            dfs(l , r+1, ')')
        }
    }
    dfs(0, 0, '')
    return res
}
时间复杂度: o(2^N)
空间复杂度: o(2^N)

合并K个排序链表

描述:
合并k个排序链表, 返回合并后的排序链表, 情分析和描述算法的复杂度
示例:
输入: [1->4->5, 1->3->4,2->6]
输出: 1->1->2->3->4->4->5->6

两数之和

题目: 给定一个数组nums和一个目标值target, 在该数组中找出和为目标值的两个数
输入: nums[8, 2, 6, 5, 4, 1, 3]; target: 7
输出: [2, 5]

// 时间复杂度o(n), 空间复杂度为o(n)

const twoNumsAdd = function (arr, target){
    if(Array.isArray(arr)){
    // 使用map将遍历过的数组存储起来, 空间换时间
        let map = new Map()
        for(let i=0; i< arr.length; i++){
            let val = target- arr[i]
            if(map.has(val)){
                return [val, arr[i]]
            }else{
                map.set(arr[i], i)
            }
        }
    
    }
}

三数之和

题目: 给定一个数组nums, 判断nums中是否存在三个元素a,b,c, 使得a+b+
c = target, 找出所有满足条件且不重复的三元组合

输入: nums:[5,2,1,1,3,4,6];target:8
输出: [[1,1,6], [1,2,5], [1,3,4]]
// 利用双指针的方式, 将三数之和转化为两数之和
const findTree = function (arr, target){
    // 先将数组从小到大排列
    arr.sort((a,b) => a-b)
    
    for(let i=0; i< arr.length; i++){
        // 跳过重复的arr[i]的值, 比如[2,1,1]跳过第二个1
        if(arr[i] && arr[i] === arr[i-1]) continue
        let left = i+1
        let right = arr.length-1
        
        // 双指针left, right
        while(left < right){
            let sum = arr[i]+ arr[left]+arr[right]
            if(sum> target){
                right--
            }else if(sum< target){
                left++
            }else{
                // 相等的时候, 先取出arr[left], 然后left++, 两步合成一步, arr[right--]同样的逻辑 
                result.push(arr[i], arr[left], arr[right])
                while(arr[left] === arr[left-1]){
                    left ++
                }
                
                while(arr[right] === arr[right+1]){
                    right--
                }
            }
        }
        
    }
    return result
}

版本号排序

题目: 输入一组版本号, 输出从大到小的排序
输入: ['2.1.0.1', '0.402.1', '10.2.1', '5.1.2', '1.0.4.5']
输出: ['10.2.1', '5.1.2', '2.1.0.1', '1.0.4.5', '0.402.1']

const versionSort = function (nums){
    return nums.sort((a, b) => {
    
        let i=0;
        const arr1 = a.split('.')
        const arr2 = b.split('.')
        
        while(true){
            const s1 = arr1[i]
            const s2= arr2[i]
            i++;
            
            // 若s1或者es2不存在, 说明相同的位置已比较完成, 接下来比较
            // arr1与arr2的长度, 长的版本号大
            
            if(s1 === undefined || s2 ===undefined){
                return arr2.length - arr1.length
            }
            
            if(s1===s2) continue
            
            return s2 -s1
        }
    })
    
}

找出第一个不重复的字符

题目: 输入一个字符串, 找到第一个不重复的字符的下标

输入: 'abcabcde'

输出: 6

// 时间复杂度o(n), 空间复杂度o(n)

const findIndex = function (str){
    if(!str) return -1;
    // 使用map存储每个字符出现的次数
    let map = new Map()
    
    let arr= str.split('')
    
    for(let-i=0; i< arr.length; i++){
        const key= arr[i]
        const mapVal = map[arr[i]]
        map[key] = mapVal? mapVal+11
    }
    
    // 在遍历一遍找到出现一次的下标
    for(let i =0; i< arr.length; i++){
        if(map.has(arr[i] === 1){
            return i
        }
    }
    return -1
}

字符串所有排列组合

题目: 输入一个字符串,打印出该字符串中,所有字符的排列组合

输入: 'abc'

输出: ['abc', 'acb', 'bca', 'bac', 'cab', 'cba']

function permute (str){
    if(str.length === 0) return ['']
    
    let result=[];
    let first = str[0]
    let subPermutes = permute(str.slice(1));
    for(let subPermute of subPermutes){
        for(let i=0; i<= subPermute.length; i++){
            result.push(subPermute.slice(0, 1)+first, subPermute.slice(i))
        }
        
    }
    return result
}

第二种方式实现:

/**
 * 交换数组中两个元素的位置
 * @param {Array} arr 数组
 * @param {number} i 第一个元素的索引
 * @param {number} j 第二个元素的索引
 */
function swap(arr, i, j) {
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

/**
 * 回溯函数,用于生成排列组合
 * @param {Array} arr 字符数组
 * @param {number} start 开始位置
 * @param {string[]} result 结果数组
 */
function backtrack(arr,start, result){
    if(start === arr.length-1){
        result.push(arr.join(''))
        return ;
    }
    
    for(let i= start; i< arr.length; i++){
        swap(arr, start, i)
        backtrack(arr, start+1, result)
        swap(arr, start, i)
    }
}

funciton permute(str){

    let result = [];
    let arr = str.split('')
    
    backtrack(arr, 0, result)
    
    return result
}

冒泡排序

时间复杂度为O(n^2) 稳定排序算法

const bubbleSort = function (arr){
    const length = arr.length;
    // 外层循环控制, 排序进行多少轮
    
    for(let i=0; i< length;i++){
        // 内层循环用于每一轮的数据比较
        // 注意 j的长度范围是length-i-1
        
        for(let j=0; j< length-i-1; i++){
            // 相邻元素, 大的放到后面
            if(arr[j] > arr[j+1]){
                // 交换位置
                [arr[j], arr[j+1]] = arr[arr[j+1], arr[j]]
            }
        }
    }
    return arr
}

选择排序

时间复杂度O(n^2) 不稳定

const selectSort = function (arr){

    // 定义index存储最小值得下标
    let index;
    // 外层循环控制, 排序进行多少轮
    for(let i=0; i< arr.length; i++){
        index= i;
        // 内层循环用于每一轮的数据比较
        // 注意j的起始范围是i+1
        for(let j=i+1;j<arr.length; j++){
        // 寻找最小值
            if(arr[j]< arr[index]){
                // 保存最小值得下标
                index = j
            }
        }
        if(index !== i) {
            [arr[i], arr[index]] = [arr[index], arr[j]]
        }
    }
    return arr
}

插入排序

时间复杂度为O(n^2), 稳定

const inserSort  = function (arr) {

    // 从第二个元素开始遍历序列
    for(let i=1; i< arr.length; i++){
        let j=i;
        // 记录要插入的目标元素
        let target = arr[j];
        // 从target所在位置向前遍历, 直至找到一个比目标元素小的元素, 
        // 目标元素插入到该元素之后的位置
        
        while(j> 0&& arr[j-1]> target){
            // 移动前一个元素的位置, 将其向后移动一个位置
            arr[j] = arr[j-1]
            j--
        }
        arr[j] = target
    }
    return arr
}

快速排序

时间复杂未O(n) 不稳定

const quickSort  = function (list){
    // 当list.length < =1 的时候, 推出递归
    if(list.lengtn <=1) return list
    
    // 找到中间节点
    let mid = Math.floor(list.length/2)
    // 以中间节点为基准, 比该节点大的参数让在right数组中, 比该节点小的放在left数组中
    let base = list.splice(mid, 1)[0]
    let left =[]
    let right =[]
    for(let i=0; i< list.length; i++){
        if(list[i]>base){
            right.push(list[i])
        }else{
            left.push(list[i])
        }
    }
    return quickSort(left).concat(base, quickSort(right))
}

实现方式2

//  交换数组中的两个元素的位置
function swap(arr, i, j) {
    let temp = arr[i]
    arr[i] = arr[j]
    arr[j]= temp
}

// 划分函数 , 将函数划分为两部分, 左边元素小于等于pivot, 右边大于等于pivot
function partition(arr, low, high){
    let pivot = arr[high]
    let i= (low -1)
    for(let j=low;j<high-1;j++){
        if(arr[j]<= pivot){
            i++;
            swap(arr, i, j)
        }
    }
    swap(arr, i+1, high)
    return i+1
}

// 快速排序函数
function quickSort(arr, low=0, high = arr.length-1){
    if(low< right){
        let pi = partition(arr, low, right)
        quickSort(arr, low, pi-1)
        quickSort(arr, pi+1, high)
    }
    return arr
}

归并排序

时间复杂度O(N)稳定

funciton MergeSort(array){
    
    function merge(left, right){
        let [l, r]=[0, 0]
        let result=[];
        // 从left和right区域中各取出第一个元素, 比较它们的大小
        while(l< left.length && r< right.length){
            // 将较小的元素添加到result中,然后从较小元素坐在的区域内取出
            // 下一个元素, 继续进行比较
            if(left[i]< right[t]){
                result.push(left[i])
                l++
            }else{
                result.push(right[r])
                r++
            }
        }
        // 如果left或者right有一个为空, 则直接将另一方的所有元素依次添加到result中
        result = result.concat(left.slice(1,left.length))
        result = result.concat(right.slice(r, right.length))
        return result
    }
    let len = array.length;
    // 当每个子序列中仅有1个元素的时候返回
    if(len <=1){
        return array;
    }
    
    // 将给定的列表分为两部分
    let num= Math.floor(len/2)
    let left  = MergeSort(array.slice(0, num))
    let right = MergeSort(array.slice(num, len))
    
    return merge(left, right)
}

列表转成树

题目: 输入一组列表如下。 转换成树形结构 输入:

[
  { id: 1, title: "child1", parentId: 0 },
  { id: 2, title: "child2", parentId: 0 },
  { id: 3, title: "child1_1", parentId: 1 },
  { id: 4, title: "child1_2", parentId: 1 },
  { id: 5, title: "child2_1", parentId: 2 }
]

输出:

tree=[
  {
    "id": 1,
    "title": "child1",
    "parentId": 0,
    "children": [
      {
        "id": 3,
        "title": "child1_1",
        "parentId": 1
      },
      {
        "id": 4,
        "title": "child1_2",
        "parentId": 1
      }
    ]
  },
  {
    "id": 2,
    "title": "child2",
    "parentId": 0,
    "children": [
      {
        "id": 5,
        "title": "child2_1",
        "parentId": 2
      }
    ]
  }
]
funciton listToTree(data){
    // 使用对象重新存储数据, 空间换时间
    let map = {}
    
    let treeData =[]
    
    // 遍历原始数据data, 存到map中, id为key, 值为数据
    for(let i=0; i< data.length; i++){
        map[data[i].id] = data[i]
    }
    
    // 遍历对象
    for(let i in map){
            if(map[i].parentId){
                if(!map[map[i.parentId]].children){
                    map[map[i].parentId].children =[]
                }
                map[map[i].parentId].children.push(map[i])
            }else{
                treeData.push(map[i])
            }
            
    }
    return treeData
}

深度优先遍历

题目: 对树进行遍历, 从第一个节点开始, 遍历其子节点, 直到它的所有子节点都被遍历完毕, 然后在遍历它的兄弟节点 输入:

tree=[
  {
    "id": 1,
    "title": "child1",
    "parentId": 0,
    "children": [
      {
        "id": 3,
        "title": "child1_1",
        "parentId": 1
      },
      {
        "id": 4,
        "title": "child1_2",
        "parentId": 1
      }
    ]
  },
  {
    "id": 2,
    "title": "child2",
    "parentId": 0,
    "children": [
      {
        "id": 5,
        "title": "child2_1",
        "parentId": 2
      }
    ]
  }
]

输出: [1,3,4,2,5]

// 递归版本
function deepTree(tree, arr=[]){
    if(!tree || !tree.length) return arr;
    tree.forEach(data => {
        arr.push(data.id)
        // 遍历子树
        data.children && deepTree(data.children, arr)
    })
    return arr
}

// 非递归版本
funciton deeepTree(tree){
    if(!tree || !tree.length) return 
    let arr = [];
    let stack =[]
    // 先将第一层节点放入栈中
    let len = tree.length
    for(let i=0; i< len; i++){
        stack.push(tree[j])
    }
    let node;
    while(stack.length){
        // 获取当前第一个节点
        node = stack.shift()
        arr.push(node.id)
        // 如果该节点有子节点, 继续添加进入栈中
        if(node,children && node.children.length){
            stack = node.children.concat(stack)
        }
    }
    
    return arr
}

广度优先遍历

题目: 以横向的维度对树进行遍历, 从第一个节点开始, 依次遍历其素有的兄弟节点, 再遍历第一个节点的子节点, 一层层的向下遍历 输入:

tree=[
  {
    "id": 1,
    "title": "child1",
    "parentId": 0,
    "children": [
      {
        "id": 3,
        "title": "child1_1",
        "parentId": 1
      },
      {
        "id": 4,
        "title": "child1_2",
        "parentId": 1
      }
    ]
  },
  {
    "id": 2,
    "title": "child2",
    "parentId": 0,
    "children": [
      {
        "id": 5,
        "title": "child2_1",
        "parentId": 2
      }
    ]
  }
]

输出:[1,2,3,4,5]

function rangeTree(tree){
    if(!tree || !tree.length) return ;
    let arr = [];
    let node, list = [...tree]
    // 取出当前节点
    while(node  = list.shift()){
        arr.push(node.id)
        node. children && list.push(...node.children)
    }
    return arr
}

买卖股票问题

题目: 给定一个整数数组, 其中第i个元素代表了第i天的股票价格; 非负整数fee代表了交易过票的手续费用, 求返回获的利润的最大值 输入: arr:[1, 12, 13, 9 , 15, 8, 6, 16] feee: 2 输出: 22

// 贪心算法求解

function buyStock(list, fee){
    // min为当前的最小值, 即买入
    let min = list[0]
    let sum = 0;
    for(let i =1; i< list.length; i++){
        // 从1开始, 依次判断
        if(list[i]<min){
        
            // 寻找数组的最小值
            min  = list[i]
        }else{
        // 计算如果当前卖出是否赚钱
            let temp = list[i]- min- fee
            if(temp> 0){
            // 赚钱, 存数据
            sum+= temp
            //重新计算min, 分两种情况, 如果后面继续涨, 则默认继续持有
            // 若后面跌, 则以后面的价格重新买入
            min=list[i]-fee
            }
        }
    }
    return sum
}

斐波那契数列

题目: 从第3项开始,当前项等于前两项之和: 1 1 2 3 5 8 13 21 ……,计算第n项的值

输入: 10

输出: 89

function fib(n){
    // 使用dp数组, 将之前计算的结果存起来, 防治栈溢出
    if(n === 0) return 0;
    if(n ===1) return 1
    let dp = [0,1];
    for(let i=2;i<=n ; i++){ 
        dp[i] = dp[i-1]+dp[i-2]
    }
    return dp[n]

}

实现方式2 ,利用三个变量形式, 动态规划

function fib(n){
    if(n===0) return 0
    if(n ===1) return 1
    lett rn2=0, rn_1 =1, rn;
    for(let i=2; i<=n ; i++){
        rn = rn_1+rn_2
        rn_2 = rn_1
        rn_1 = rn 
    }
    return rn
}

如何解决页面请求大规模并发问题

滑动窗口 算法, 专门来流量控制的

背景

数据采集平台, 低代码平台, 有序相对稳定发送到后端

方案

class RequestQueue{
    constructor(maxConcurrent){
        this.maxConcurrent = 0; // 最大请求并发
        this.currentConcurrent =0; // 当前并发请求数
        this.queue = [] // 请求队列
        
    }
    add(request){
        return new Promise((resolve, reject) => {
            this.queue.push({request, resolve, rejext})
            this.processQueue()
        })
    }
    processQueue(){
        if(this.Queue.length > 0 && this.currentConCurrent< this.maxConsurrent){
            const {request, resolve, reject} = this.queue.shift()
            
            this.currentConsurrent++;
            request()
            .then(resolve)
            .catch(reject)
            .finally(() => {
                this.currentXConcurrent--;
                this.processQueue()
            })
        }
    }
}

大文件上传怎么处理

场景: 上百兆适合使用

经常遇到的问题:

  • 网络断开, 之前传的没了
  • 传着传着,网络波动 结果啥都没有了
  • 关机了, 想接着传, 做不到

专业术语

  • 断点续传
  • 断开重连重传
  • 切片上传

方案:

  • 前端切片 chunk 1024M(1047576k) 按照500k切片, const size = 1048576/500

  • 将切片传递给后端,切的片要取名: hash, index

  • 后端组合切片

  • 前端切片: 主进程会卡顿, 利用web-worker多线程切片, 处理了完成交给主进程发送

  • 切完后, 将bolb 存储到IndexDB, 下次用来进来以后, 嗅探一下IndexDB里面是否存在未完成上传的切片, 有就尝试继续上传

  • websocket, 实时通知和请求序列的控制, 现在一般用的是wss

大文件上传的源码

<body>
    <input type='file' id='fileInput' >
    <button onclick='uploadFile()'>upload</button>
    <script>
        const CHUNK_SIZE= 5*1024*1024 // 每块大小为5MB
        
        function uploadFile(){
            const file = document.getElementById('fileInput')[0]
            if(!file){
                alert('请选择文件')
                return
            }
            const totalChunks = Math.ceil(file.size/ CHUNK_SIZE)
            let currenkChunk =0;
            function uploadChunk(){
                if(currentChunk >= totalChunk){
                    console.log('上传文件')
                    return 
                }
                const start = currentChunk * CHUNK_SIZE
                const end = Math.min(start + CHUNK_SIZE, file.size)
                const chunk = file.slice(start, end)
                
                const formData = new FormData()
                formData.append('file', chunk)
                formData.append('chunkNumer', currentChunk +1)
                formData.append('totalChunks', totalChunks)
                
                fetch('/upload', {
                    method:'POST',
                    body: formData
                })
                .then(response => {
                    if(response.ok){
                        currentChunk++
                        uploadChunk() // 递归调用上传 下一部分
                    }else{
                        console.log('')
                    }
                })
                .catch(error => {
                    console.log('上床异常')
                })
            }
            uploadChunk()
        }
    
    </script>
</body>

在前端怎么实现页面截图

交代背景

  • 像飞书文档 , 内容分在列表页想要查看
  • 内容导出为png格式
  • 设计类软件, 出图

方案

  • canvas
  • puppeteer 无头表格, 无头ui
  • html2canvas
  • 上传cdn

落地

全页面截图还是局部截图

  • 截图工具的时候, 需要考虑通用性, selector body header dom
  • 设计具体协议

h5移动后端如何做适配

背景

项目想支持PC,移动端

方案

  • 根据端来开发不同页面(成本较高)
  • 根据不同端加载不同的css样式(可取)
  • 根据响应式, 来运行不同的样式规则(常用
  • style预处理器来做

考虑的问题:

  1. 设置视窗, 通过元信息配置 meta
<meta name='viewport' content='width = device-width, inital-scale =1.0'>
  1. 掌握媒体查询
body{
    font-szie: 16px;
}

// 但是在某一些设备尺寸, 这个size是要更改的

@media(min-width: 780px) and(max-width: 1024px){
    body{
        font-size: 18px
    }
}
  1. 弹性布局 flex布局
  2. 图片的响应式
<picture>
    <source srcset='image-large.jpg' media='(min-width:800px)'>
    <source srcset='image-medium.jpg' media='(min-width: 400px)'>
    <img src='image-small.jpg' alt='response Image' >
</picture>
  1. rem rem单位的基础值由html的font-size决定
html{
    font-size: 16px;
    
}

.header{
    font-size: 1rem;
}

如何修改第三方的npm包

方案

  1. 稳定库, node_modules直接修改
  2. patch方案 'patch-package' 自动化 'pnpm i patch-package postinstall'
{
    'scripts':{
      'postinstall' :'patch-package' 
    }
}

创建补丁 npx pacth-package rspack 这个时候会在项目生成'patchs/rspack+1.0.0.patch'

  1. fork (github fork) 直接改源码, 源码修改之后, 构建,发布到npm私服

使用同一个链接, 如何实现pc打开的是web应用, 手机打开的是一个h5应用

方案

区分 pc mobile

识别端

  1. js识别userAgent

当QPS达到峰值的时候, 该如何处理

背景

当前端应用的QPS达到峰值的时候, 会对服务器和应用的性能造成很大的压力, 甚至可能导致系统崩溃, 为了解决这个问题, 我们需要采取一系列的措施优化和刮半年里高并发请求。

方案:

  1. 请求限流 以node.js为例, 限流

  2. 请求合并

  3. 短时间内的请求进行合并, 以此来降低服务端的压力

  4. 请求缓存

  5. 任务队列

如何实现网页加载进度条

web应用中如何对静态资源加载失败的场景做降级处理

场景

  1. 图片
  2. css文件
  3. javascript文件
  4. cdn
  5. 字体文件
  6. 服务端渲染

解决方案

  1. 占位图 来描述图片
  2. 重试机制(404, 无权限)
  3. 上报
<img src=''alt='' onerror='handleImgeError(this)'

function handleImageError(image){
    image.error = null // 防止死循环
    image.src ='placeholder.png' // 使用占位图
}

处理css文件

资源没有加载到

  1. 关键性样式, 通过内联
  2. 备用样式
  3. 上报
<head>
    <style>
    
        body{
            font-family:''
        }
    </style>
    <link rel='stylesheet' href='styles.css' onerror='handleCssError()'>
</head>
function handleCssError(){
    // 加载备用样式
    const fallbackCss = document.createElement('link')
    fallback.rel = 'stylesheet'
    fallbackCss.href='fallback-styles.css'
    document.head.appenChild(fallbackCss)
}

javascript 文件处理

网络异常, 导致资源没有加载 解决:

  1. 内联脚本
  2. 备用脚本处理
  3. 上报

cdn

  1. 本地备份, 如果cdn出错了, 就使用本地备份
  2. 动态切换, 切到另外一个有用的cdn服务
<head>
    <script src='https://cdn....' onerror='handleCdnError()'
</head>
function handleCssError(){
    // 加载本地备份
    const fallbackScript = document.createElement('script')
    fallbackScript.src ='beifen.js'
    document.head.appendChild(fallbackScript)
}

怎么设计一个全站请求耗时的统计工具

背景

通过这个统计工具, 可以更清晰看到整个站点的性能情况,首屏加载时间(FP/FCP)

  1. 监控请求耗时 http, 中间件,axios
  2. 前端监控, 监控整个请求相关并且记录耗时数据
  3. 后端监控:后端记录
  4. 数据汇总: 数据清洗加工, 数据可视化,可视化图表
(function (){
    const originaXhrOpen = XMLHttpRequest.prototype.open
    XMLHttpRequest.prototype.opne= function (...args){
        this._startTime = performance.now()
        this.addEventListener('load', function (){
            const duration = performance.now() - this._startTime
            console.log(`XHR${args[1]took${duration}}ms`)
            reportRequestDuration(args[1], duration, 'XHR')
        })
        originalXhrOpen.apply(this, args)
    }
    const originalFetch = window.fetch
    window.fetch = async function (...args){
        const startTime = performance.now()
        const response = await originalFetch.apply(this, args)
        const duration = performance.now() - startTime
        reportRequestDuration(args[0], duration, 'Fetch')
        return response
    }
    function reportRequestDuration(url, duration, type){
        fetch('', {
            method:'post',
            headers:{'Content-Type':'application/json'}
            body:JSON.strinify({url, duration, type })
        })
    }
})()

说说对函数编程思想的理解

基本概念

  1. 函数封装的方式解决问题, 纯函数, 没有副作用, 相同输入得到相同的输出
  2. 不可变
  3. 高阶函数, 函数柯里化
  4. 函数组合,类似于面向对象继承 总结:
  5. 可测试性吗, 更好写单元测试
  6. 可维护性
  7. 并发
  8. 简洁

请你说说对dns协议的理解

将域名映射到ip上

域名解析的整个过程

浏览器渲染原理

  1. 用户输入域名
  2. 检查自身DNS缓存
  3. 操作系统的DNS缓存
  4. 本地域名服务器
  5. 根据本地DNS服务器去查找跟DNS服务器, 顶级域名服务器, 权威DNS服务器
  6. 返回结果, 浏览器缓存并向IP发起请求

DNS 的记录类型

  1. A记录: 将域名映射到IPV4地址
  2. AAAA记录: 将域名映射到IPV6地址
  3. CNAME记录; 将一个域名映射到另一个域名
  4. MX记录: 指定邮件服务器
  5. TXT: 文本信息存储, 域名验证,SPF记录

DNS常见问题

  • DNS解析慢
  1. DNS预解析
  2. 使用CDN, CDN节点用户就近
  3. 减少外部资源请求,自己的域名+oss+cdn
  • DNS劫持
  1. HTTPS, 证书保证传输安全性
  2. DNSSES:DNS安全扩展

优化:

  1. DNS缓存
  2. nslookup
  3. dig
  4. 在线: dns.google.com dnschecker.org

美团电影院,怎么实现一个电影选座的功能

使用canvas来实现选座功能

实现思路:

  1. canvas基础处理
  2. 座位绘制
  3. 交互添加
  4. 座位状态的管理, 数据结构设计
  5. 性能优化与美化

具体实现: 初始化操作如下

<head>
    <style>
        canvas{
            border: 1px solid #000;
            display: block;
            margin: 0 auto;
        }
    </style>
    <body>
        <canvas id='cinemaCanvas' width='800' height='600'/>
        <script srcc='cinema.js'></script>
    </body>
</head>

绘制座位

cosnt canvas = document.getElementById('cinemaCanvas')
const ctx = canvas.getContext('2d')

const rows = 10
const cols = 15
const seatSize= 40
const seatSpacing = 10
const seats = []
for(let row =0; row< rows; rows++){
    const seatRow =[];
    for(let col=0; col< cols; col++){
        seatRow.push({
            x:col * (seatSize + seatSpacing), 
            y: row * (seatSize + seatSpacing), 
            status: 'available'
        })
    }
    seats.push(seatRow)
}

function drawSeats(){
    seats.forEach((row) => {
        row.forEach((seat) => {
           ctx.fillStyle = getSeatColor(seat.status)
           ctx.fillRect(seat.x, seat.y, seatSize, seatSize)
        })
    })
}

function getSeatColor(status){
    switch(status){
        case 'available':
            return 'green'
        case 'selected':
            return 'blue'
        case 'unavailable':
            return 'red'
        default:
            return 'grey'
    }
}
drawSeats()

说说图片性能优化的方案

方案对比

  1. loading='lazy'
<img src='iamge.jpg' alt='example image' loading='lazy' >
  1. intersection observer
<img data-src='image.jpg' alt='example image' class='lazyload'>
<script>
    document.addEventListener('DOMContenxtLoaded', function (){
        const lazyImages = document.querySelectorAll('img.lazyload')
        if('IntersectionObserve' in window){
            const observer = new IntersectionObserve(function(entries, observe){
            entries.forEach(entry => {
                if(entry.isIntersecting){
                    const img = entry.target
                    img.src = img.dataset.src
                    imhg.classList.remove('lazyload')
                    observer.unobserver(img)
                }
            })
            })
            lazyImages.forEach(img => {
                oberver.oberve(img)
            })
        }else{
            lazyImage.forEach(img => {
                img.src = img.dataset.src
            })
        }
    })
</script>
  1. 滚动事件来做 监听scroll事件 最原始的做法
<img data-src='image.jpg' alt='' class ='lazyload'>

<script>
document.addEventListener('DOMContentLoaded', function (){
    const lazyImages = document.querySelectAll('img.lazyload')
    
    const lazyLoad = () => {
        lazyImages.forEach(img => {
            if(img.getBoundingClientRect().top < window.innerHeight && img.getBoundingClientRect().bottom > 0 && getComputedStyle(img).display !== 'none'){
                img.src= img.dataset.src
                img.classList.remove('lazyload')
            }
        })
        if(lazyImage.length === 0){
            document.removeEventListener('scroll', lazyload)
            window.removeEventListener('resize', lazyload)
            window.removeEventListener('orientationchange', lazyload)
        }
        
    }
    document.addEventListener('scroll', lazyload)
    window.addEventListener('resize', lazyload)
    window.addEventListener('orientationchange', lazyload)
})
</script>

服务端渲染

服务端渲染的理解? 服务端渲染概述: 服务端渲染: 在服务器端将网页内容渲染为完整的HTML, 然后发送到客户端浏览器进行显示。 客户端渲染: 在浏览器端通过javsscript动态生成页面内容

ssr的优势与挑战 优势:

  1. 提高首屏渲染速度 用户无需等待javascript架子啊和执行即可查看到完成内容, 显著提升用户体验
  2. seo优化 搜索引擎可以直接抓取完整的HTML内容, 有助于提高网站在搜索结果中的排名
  3. 共享连接时显示预览 在社交媒体上分享链接的时候, 能够正确显示页面预览信息, 增加内容的传播性

挑战:

  1. 开发复杂度增加 需要处理服务器渲染逻辑, 设计Node.js等后端技术, 与前端开发结合更复杂
  2. 服务器压力增大 服务器需要承担渲染页面的工作, 可能导致性能瓶颈, 尤其是在高并发场景下
  3. 状态管理复杂 需要在服务器和客户端之间同步状态, 确保数据一致性, 避免因状态不同导致的错误