web前端求职(deepseek版)面试题

一、HTML & CSS 基础

1.语义化HTML

问: HTML5新增的语义化标签有哪些?举例说明使用场景?

  • <header>

        定义文档或区块的页眉(通常包含标题、导航或者LOGO)

<header>
    <h1>网站标题</h1>
    <nav>主导航链接</nav>`在这里插入代码片`
</header>
  • <nav>

        定义导航栏链接集合(如左侧菜单、侧边导航栏)。

<nav>
    <ul>
        <li><a href="/">首页</a></li>
        <li><a href="/about">关于我们</a></li>
    </ul>
</nav>
  • <article>

        表示独立的内容区块(如:博客、新闻、评论)。

<article>
    <h2>文章标题</h2>
    <p>文章正文内容...</p>
</article>
  • <section>

        定义文档中的逻辑分区(如章节、主题分组)。

    注: 通常带有标题,但内容需关联

<section>
    <h2>产品介绍</h2>
    <p>详细描述产品特性...</p>
</section>
  • <aside>

        定义与主内容间接相关的内容(如侧边栏、广告、引用)。

<aside>
    <h3>相关推荐</h3>
    <ul>...</ul>
</aside>
  • <footer>

        定义文档或区块的页脚(如版权信息、联系方式)。

<footer>
    <p>© 2023 公司名称. 版权所有.</p>
</footer>
  • <main>

        标识页面的核心内容(一个页面仅一个)。

<main>
    <h1>主要标题</h1>
    <p>页面核心内容...</p>
</main>
  • <figure> & <figcaption>

        包裹媒体(如图片、图表)及其标题。

<figure>
    <img src="chart.png" alt="数据图表">
    <figcaption>图1: 年度销售数据</figcaption>
</figure>
  • <time>

        定义时间或日期(机器可读格式)。

<time datetime="2023-10-01">2023年10月1日</time>
  • <mark>

        高亮显示文本(如搜索关键词)。

<p>这是一个<mark>重要</mark>的提示。</p>

使用场景总结:

  • SEO优化:语义化标签帮助搜索引擎理解页面结构(如<article>明确内容主体)。

  • 可访问性:屏幕阅读器依赖标签识别内容类型(如<nav>提示导航区域)。

  • 代码可读性:替代无意义的<div>嵌套,便于团队协作维护。

注意:避免滥用<section>,仅在内容需要逻辑分组时使用;<article>应保证内容的独立性。

问: 如何优化页面SEO的HTML结构?

  •  语义化HTML5标签

1.使用正确的HTML5语义标签(header, nav, main, article, section, aside, footer)

2.避免过度使用div标签

  • 标题结构优化

1.保持标题层级清晰有序

2.每个页面只使用一个H1标签

3.避免跳过标题层级(如H1直接接H3)

  • 元标签优化
    <head>
      <title>简洁描述性标题(50-60字符)</title>
      <meta name="description" content="吸引人的页面描述(150-160字符)">
      <meta name="keywords" content="关键词1,关键词2,关键词3">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta charset="UTF-8">
      <link rel="canonical" href="https://example.com/page">
    </head>
  • 图片优化

1.始终使用alt属性

2.考虑使用懒加载(lazy loading)

3.使用适当的文件格式和压缩

  • 链接优化

1.使用描述性锚文本

2.避免"点击这里"等模糊文本

3.为重要链接添加title属性

  • 结构化数据标记

1.使用Schema.org标记

2.适用于文章、产品、活动等类型内容

  • 移动友好设计

1.使用响应式设计

2.确保视口设置正确

3.避免使用Flash和弹出窗口

  • 性能优化 

1.最小化HTML代码

2.延迟加载非关键资源

3.优化CSS和JavaScript交付

 注:SEO是一个持续的过程,需要定期审查和调整

2.CSS布局

问: 实现一个三栏布局(左右固定宽度,中间自适应),有哪些方案?

  • flex布局
    .container {
      display: flex;
      height: 100vh;
    }
    
    .left, .right {
      width: 200px; /* 固定宽度 */
      background: #ccc;
    }
    
    .center {
      flex: 1; /* 自适应 */
      background: #eee;
    }
  • Grid布局
    .container {
      display: grid;
      grid-template-columns: 200px auto 200px;
      height: 100vh;
    }
    
    .left, .right {
      background: #ccc;
    }
    
    .center {
      background: #eee;
    }
  •  float浮动
    .left {
      float: left;
      width: 200px;
      background: #ccc;
      height: 100vh;
    }
    
    .right {
      float: right;
      width: 200px;
      background: #ccc;
      height: 100vh;
    }
    
    .center {
      margin: 0 200px;
      background: #eee;
      height: 100vh;
    }
  •  绝对定位
    .container {
      position: relative;
      height: 100vh;
    }
    
    .left {
      position: absolute;
      left: 0;
      width: 200px;
      background: #ccc;
      height: 100%;
    }
    
    .right {
      position: absolute;
      right: 0;
      width: 200px;
      background: #ccc;
      height: 100%;
    }
    
    .center {
      margin: 0 200px;
      background: #eee;
      height: 100%;
    }
  • 表格布局 
    .container {
      display: table;
      width: 100%;
      height: 100vh;
    }
    
    .left, .center, .right {
      display: table-cell;
    }
    
    .left, .right {
      width: 200px;
      background: #ccc;
    }
    
    .center {
      background: #eee;
    }
  • calc()计算方案
    .left {
      float: left;
      width: 200px;
      background: #ccc;
      height: 100vh;
    }
    
    .right {
      float: right;
      width: 200px;
      background: #ccc;
      height: 100vh;
    }
    
    .center {
      width: calc(100% - 400px);
      float: left;
      background: #eee;
      height: 100vh;
    }

        现代前端开发中,Flexbox 和 Grid 是最推荐使用的方案,它们具有更好的灵活性和响应式特性,代码也更简洁。其中:

  • Flexbox 适合一维布局(行或列)

  • Grid 适合二维布局(行列同时控制)

浮动和定位方案是传统方法,在需要兼容旧浏览器时可以考虑使用。

问: Flexbox和Grid的区别?如何选择? 

核心区别
特性FlexboxGrid
维度一维布局(行或列)二维布局(行和列同时控制)
控制方向一次只能控制一个方向可以同时控制行和列
项目排列基于内容流动排列基于网格线精确放置
隐式网格更依赖内容决定布局显式定义网格结构
对齐方式有更丰富的对齐属性对齐功能更强大且统一
适用场景组件内部布局、线性布局整体页面布局、复杂二维布局

选择 Flexbox 当:

  • 需要线性布局(沿一个轴排列)
  • 布局内容大小未知或动态变化
  • 需要内容优先的布局(内容决定布局)
  • 处理组件内部的排列
  • 需要等高列效果
  • 典型用例:
    • 导航栏
    • 卡片组件
    • 表单元素排列
    • 媒体对象(如图文混排)

选择 Grid 当:

  • 需要二维布局(同时控制行和列)
  • 需要精确控制项目位置
  • 布局结构固定或需要显式定义
  • 创建整体页面框架
  • 需要复杂的重叠布局
  • 典型用例:
    • 整个页面布局
    • 仪表盘
    • 图片画廊
    • 复杂的表单布局
    • 需要精确对齐的界面

实际开发中的建议

  1. 组合使用:它们不是互斥的,可以在同一个项目中组合使用。例如用Grid构建整体页面框架,用Flexbox排列内部组件。

  2. 渐进增强:对于简单布局先尝试Flexbox,当需求超出Flexbox能力时再使用Grid。

  3. 浏览器支持

    • Flexbox有稍好的旧浏览器支持

    • Grid在现代浏览器中支持良好(IE11有部分支持)

  4. 学习曲线:Flexbox通常更容易上手,Grid概念更多但更强大。

  5. 维护性:Grid布局通常更易于维护,特别是在需要调整布局结构时。

问:解释BFC(块级格式上下文)及其应用场景?  

什么是BFC?

BFC(Block Formatting Context,块级格式化上下文)是Web页面CSS渲染中的一种独立的渲染区域,它规定了内部的块级盒子如何布局,并且与外部环境隔离。

BFC的创建条件:

以下CSS属性可以触发BFC:

  • 根元素(<html>

  • float 不为 none

  • position 为 absolute 或 fixed

  • display 为 inline-blocktable-celltable-captionflexinline-flexgridinline-grid

  • overflow 不为 visible(常用 hidden 或 auto

  • contain 为 layoutcontent 或 strict

BFC的特性

  1. 内部盒子垂直排列:BFC内的块级盒子会垂直方向一个接一个放置

  2. Margin重叠解决:属于同一个BFC的两个相邻盒子的上下margin会重叠

  3. 独立布局环境:BFC内部元素不会影响外部元素

  4. 包含浮动元素:计算BFC高度时,浮动元素也参与计算

  5. 阻止元素被浮动覆盖:BFC区域不会与浮动元素重叠

应用场景:

  • 清除浮动(解决高度塌陷) 
    <div class="container">
      <div class="float-left"></div>
    </div>
    .float-left {
      float: left;
      width: 100px;
      height: 100px;
      background: red;
    }
    
    /* 未触发BFC时,container高度为0 */
    .container {
      overflow: hidden; /* 触发BFC,包含浮动元素 */
      background: #eee;
    }
  • 防止magin重叠
    <div class="box1">Box 1</div>
    <div class="box2">Box 2</div>
    .box1, .box2 {
      margin: 20px 0;
      background: #ccc;
    }
    
    /* 默认情况下两个盒子的上下margin会合并为20px */
    
    /* 解决方案:用BFC包裹 */
    .wrapper {
      overflow: hidden; /* 触发BFC */
    }
  • 实现自适应两栏布局 
    <div class="left">左侧浮动</div>
    <div class="right">右侧内容</div>
    .left {
      float: left;
      width: 200px;
      height: 100px;
      background: #ccc;
    }
    
    .right {
      overflow: hidden; /* 触发BFC,不与浮动元素重叠 */
      height: 100px;
      background: #eee;
    }
  • 阻止文本环绕浮动元素
<div class="float-box"></div>
<div class="text-content">...</div>
.float-box {
  float: left;
  width: 100px;
  height: 100px;
  background: #ccc;
}

.text-content {
  overflow: hidden; /* 触发BFC,文本不再环绕 */
}

BFC的注意事项

  1. 性能考量:某些BFC触发方式(如overflow: hidden)可能带来副作用(裁剪内容)

  2. 选择合适方式:优先使用副作用小的方式创建BFC(如display: flow-root

  3. 现代布局替代:Flexbox和Grid布局自身会创建类似BFC的环境,可以减少显式创建BFC的需求

 3.响应式设计

问:如何实现移动端1px边框问题?   

         在移动端开发中,由于高清屏幕(Retina等)的设备像素比(devicePixelRatio)较高,CSS中的1px实际上可能显示为2px或3px物理像素,导致边框看起来比设计稿更粗。

以下是几种主流解决方案:

  •  使用0.5px方案(简单但兼容性有限)
    .border {
      border: 0.5px solid #ccc;
    }
    

    优缺点

    • 优点:简单直接

    • 缺点:iOS8+支持,Android兼容性差

  •  伪元素 + transform缩放(推荐方案)
    .border {
      position: relative;
    }
    .border::after {
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      border: 1px solid #ccc;
      transform-origin: 0 0;
      transform: scale(0.5);
      box-sizing: border-box;
      pointer-events: none; /* 防止点击事件被拦截 */
    }

    优缺点

    • 优点:兼容性好,效果稳定

    • 缺点:代码稍复杂,需要占据伪元素

  •  viewport + rem方案(动态调整)
    <meta name="viewport" id="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no">
    <script>
      const viewport = document.getElementById("viewport");
      if (window.devicePixelRatio === 2) {
        viewport.setAttribute('content', 'width=device-width,initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no');
      } else if (window.devicePixelRatio === 3) {
        viewport.setAttribute('content', 'width=device-width,initial-scale=0.333333,maximum-scale=0.333333,minimum-scale=0.333333,user-scalable=no');
      }
    </script>

    优缺点

    • 优点:全局解决方案

    • 缺点:需要调整整个页面的缩放比例,可能影响其他元素

  •  使用border-image(适用于直线边框)
    .border {
      border-width: 1px;
      border-style: solid;
      border-image: linear-gradient(to bottom, #ccc, #ccc) 2 2 stretch;
    }
  •  使用box-shadow模拟(简单边框)
    .border {
      box-shadow: 0 0 0 0.5px #ccc;
    }

    优缺点

    • 优点:代码简单

    • 缺点:只能模拟单边边框,圆角支持不好

  •  SVG方案(复杂但精确)
    .border {
      background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><rect width='100%' height='100%' stroke='%23ccc' stroke-width='1' fill='none'/></svg>");
    }

最佳实践建议

  1. 通用方案:伪元素+transform方案(方案2)是当前最平衡的解决方案

  2. 框架项目:可以使用postcss插件如postcss-write-svgpostcss-px-to-viewport自动处理

  3. 简单项目:考虑使用0.5px方案,配合渐进增强

  4. 特殊需求:圆角边框推荐使用SVG方案

问:rem、vw/vh、媒体查询定的适用场景?    

  •  rem方案

        原理:基于根元素(html)的字体大小进行计算,1rem = 根字体大小

        适用场景:

  • 整体缩放布局:需要整个页面按比例缩放的场景

  • 固定比例设计:设计稿有固定比例要求时

  • 兼容性要求高的项目(支持到IE9)

  • 移动端H5页面开发(常配合flex布局)

  •  vw/vh 方案

        原理:基于视口尺寸的单位,1vw = 1%视口宽度1vh = 1%视口高度

        适用场景:

  • 视口比例布局:元素尺寸需要与视口大小直接关联

  • 全屏应用:需要填满整个屏幕的组件

  • 字体自适应:希望文字随视口变化

  • 现代浏览器项目 (IE9部分支持,IE10+完全支持)

  • 媒体查询(Media Queries)

        原理:根据设备特性(宽度、高度、方向等)应用不同的CSS规则

        适用场景:

  • 断点布局:在特定分辨率下改变布局

  • 设备适配:针对不同设备类型(手机/平板/PC)定制样式

  • 显示优化:根据屏幕特性(如打印样式、深色模式)调整

  • 渐进增强:为不同能力设备提供不同体验

现代项目推荐

  • 主单位:vw/vh + clamp() (如 font-size: clamp(1rem, 2vw, 1.5rem))

  • 辅助:媒体查询处理特殊断点

  • 备用:rem保证基本可访问性

二、JavaScript 核心

1.基础与原理

问:解释事件循环(Event Loop)机制,结合宏任务/微任务?

什么是事件循环?

        事件循环是 JavaScript 实现异步编程的核心机制,它使得 JavaScript 虽然是单线程语言,却能够处理非阻塞 I/O 操作。事件循环负责监控调用栈(Call Stack)、任务队列(Task Queue)和微任务队列(Microtask Queue),决定何时执行什么代码。

事件循环的基本流程:

  1. 执行同步代码:所有同步代码在主线程上顺序执行,形成调用栈

  2. 处理微任务:当调用栈清空后,事件循环会检查微任务队列并执行所有微任务

  3. 渲染页面(如果需要):浏览器可能会进行页面渲染

  4. 处理宏任务:从宏任务队列中取出一个任务执行

  5. 循环:重复上述过程

宏任务(MacroTask)与微任务(MicroTask):

宏任务

  • 包括:setTimeoutsetIntervalsetImmediate(Node.js), I/O 操作, UI 渲染, 事件回调等

  • 特点:每次事件循环只执行一个宏任务

微任务

  • 包括:Promise.then/catch/finallyprocess.nextTick(Node.js), MutationObserver

  • 特点:每次调用栈清空后,会执行所有微任务队列中的任务

代码执行顺序示例:

console.log('1. 同步代码 - 开始');

setTimeout(() => {
  console.log('6. 宏任务 - setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('4. 微任务 - Promise 1');
}).then(() => {
  console.log('5. 微任务 - Promise 2');
});

console.log('2. 同步代码 - 中间');

new Promise(resolve => {
  console.log('3. 同步代码 - Promise 构造函数');
  resolve();
}).then(() => {
  console.log('7. 微任务 - Promise 3');
});

console.log('8. 同步代码 - 结束');

输出顺序:

1. 同步代码 - 开始
2. 同步代码 - 中间
3. 同步代码 - Promise 构造函数
8. 同步代码 - 结束
4. 微任务 - Promise 1
5. 微任务 - Promise 2
7. 微任务 - Promise 3
6. 宏任务 - setTimeout

事件循环的详细步骤:

  1. 执行所有同步代码,直到调用栈清空

  2. 执行所有微任务(直到微任务队列为空)

  3. 如有需要,进行 UI 渲染

  4. 执行下一个宏任务

  5. 重复上述过程

Node.js 与浏览器的事件循环差异 ?

  • 浏览器:微任务在每个宏任务之后执行

  • Node.js:事件循环分为多个阶段,每个阶段有特定的任务类型,微任务在阶段切换之间执行

为什么需要理解事件循环?

  1. 避免阻塞主线程

  2. 优化代码执行顺序

  3. 解决异步编程中的竞态条件

  4. 理解为什么某些代码会以特定顺序执行

        掌握事件循环机制是成为高级 JavaScript 开发者的关键一步,它能帮助我们编写更高效、更可预测的异步代码。

问:闭包的实际应用场景及内存泄漏风险 

闭包的核心概念

        闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。简单说,当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,就形成了闭包。

 实际应用场景:

 1)数据封装与私有变量

function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount());  // 2

2)函数工厂

function createMultiplier(multiplier) {
  return function(x) {
    return x * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

3)事件处理与回调

function setupButtons() {
  const buttons = document.querySelectorAll('button');
  
  for (var i = 0; i < buttons.length; i++) {
    (function(index) {
      buttons[index].addEventListener('click', function() {
        console.log('Button ' + index + ' clicked');
      });
    })(i);
  }
}

4)模块模式

const myModule = (function() {
  const privateVar = 'I am private';
  
  function privateMethod() {
    console.log(privateVar);
  }
  
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // "I am private"

5)记忆化(Memoization)

function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

const factorial = memoize(function(n) {
  return n <= 1 ? 1 : n * factorial(n - 1);
});

console.log(factorial(5)); // 120 (计算并缓存)
console.log(factorial(5)); // 120 (从缓存读取)

常见的内存泄漏场景:

1)意外的全局变量引用

function leakMemory() {
  const bigArray = new Array(1000000).fill('*');
  
  // 内部函数保留了对外部变量的引用
  return function() {
    console.log('I still have access to bigArray');
  };
}

const leakedFn = leakMemory();
// 即使leakedFn不再需要,bigArray也无法被GC回收

2)DOM元素引用

function setup() {
  const element = document.getElementById('large-element');
  const data = new Array(1000000).fill('*');
  
  element.addEventListener('click', function() {
    console.log(data.length); // 闭包保留了data引用
  });
}

setup();
// 即使移除DOM元素,由于事件监听器的闭包保留了引用,元素和data都无法被回收

3)定时器未清除

function startProcess() {
  const data = new Array(1000000).fill('*');
  
  setInterval(function() {
    console.log(data.length); // 闭包保留了data引用
  }, 1000);
}

startProcess();
// 即使不再需要data,定时器的回调函数仍然持有引用

如何避免内存泄漏:

1)及时解除引用

function cleanUp() {
  const data = new Array(1000000).fill('*');
  const element = document.getElementById('my-element');
  
  function handleClick() {
    console.log(data.length);
  }
  
  element.addEventListener('click', handleClick);
  
  // 需要时移除事件监听
  return function() {
    element.removeEventListener('click', handleClick);
  };
}

const dispose = cleanUp();
// 当不再需要时调用dispose()

2)使用WeakMap/WeakSet

const weakMap = new WeakMap();

function processElement(element) {
  const data = new Array(1000000).fill('*');
  weakMap.set(element, data);
  
  element.addEventListener('click', function() {
    console.log(weakMap.get(element).length);
  });
}

const element = document.getElementById('my-element');
processElement(element);
// 当element被移除DOM时,WeakMap中的引用会自动释放

3)避免不必要的闭包

// 不好的做法 - 创建不必要的闭包
function processData(data) {
  return function() {
    console.log(data);
  };
}

// 更好的做法 - 直接传递参数
function createHandler(data) {
  return function() {
    console.log(data);
  };
}

调试内存泄漏:

  1. 使用Chrome DevTools的Memory面板

  2. 拍摄堆快照比较内存变化

  3. 使用Performance Monitor监控内存使用

  4. Node.js中可以使用--inspect标志和Chrome DevTools调试

总结:

闭包是JavaScript强大功能之一,合理使用可以实现:

  • 数据封装和私有变量

  • 函数工厂和柯里化

  • 模块化开发

  • 高效的事件处理

但同时需要注意:

  • 避免意外保留对大对象的引用

  • 及时清理事件监听器和定时器

  • 考虑使用WeakMap/WeakSet管理DOM关联数据

  • 定期检查应用的内存使用情况

正确理解和使用闭包,可以让你写出既强大又高效的JavaScript代码。

问:this的指向问题:call/apply/bind的区别  

this 的基本指向规则:

在 JavaScript 中,this 的指向取决于函数的调用方式:

  1. 全局上下文:在全局作用域中,this 指向全局对象(浏览器中是 window,Node.js 中是 global

  2. 函数调用:普通函数调用时,this 指向全局对象(严格模式下为 undefined

  3. 方法调用:作为对象方法调用时,this 指向调用该方法的对象

  4. 构造函数:使用 new 调用时,this 指向新创建的对象

  5. 箭头函数:箭头函数没有自己的 this,它继承自外层函数

 改变 this 指向的方法:

1).call()方法

call() 方法立即调用函数,并显式设置 this 值和参数列表。

语法func.call(thisArg, arg1, arg2, ...)

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // 输出: "Hello, Alice!"

2).apply()方法

apply() 方法与 call() 类似,但参数以数组形式传递。

语法func.apply(thisArg, [argsArray])

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Bob' };
const args = ['Hi', '!!!'];

greet.apply(person, args); // 输出: "Hi, Bob!!!"

 3).bind()方法

bind() 方法创建一个新函数,当调用时,其 this 值设置为提供的值。

语法func.bind(thisArg[, arg1[, arg2[, ...]]])

function greet(greeting) {
  console.log(greeting + ', ' + this.name);
}

const person = { name: 'Charlie' };
const greetPerson = greet.bind(person);

greetPerson('Hey'); // 输出: "Hey, Charlie"

实际应用场景 

1)借用方法

// 借用数组的slice方法处理类数组对象
function toArray() {
  return Array.prototype.slice.call(arguments);
}

const arr = toArray(1, 2, 3); // [1, 2, 3]

2)函数柯里化(使用 bind)

function multiply(a, b) {
  return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5)); // 10

3)事件处理中的 this(使用 bind)

class Button {
  constructor() {
    this.text = 'Click me';
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    console.log(this.text);
  }
}

const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);

4)找出数组中的最大值(使用 apply)

const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// ES6中可以更简单地使用: Math.max(...numbers)

注意事项:

  1. 严格模式:在严格模式下,如果 thisArg 为 null 或 undefinedthis 将保持为 null 或 undefined,而不会转换为全局对象

  2. 箭头函数:箭头函数的 this 不能被 call/apply/bind 改变,它始终指向定义时的上下文

  3. 性能考虑:在性能敏感代码中,call 通常比 apply 稍快,因为不需要处理数组参数

总结

  • call 和 apply 都是立即调用函数,区别在于参数传递方式

  • bind 返回一个新函数,适合需要延迟执行或作为回调的场景

  • 三者都可以显式设置 this 值,是 JavaScript 中灵活控制执行上下文的重要工具

  • 根据具体场景选择合适的方法,可以提高代码的可读性和性能

 2.ES6+

问:Promise、async/await的异常处理方式   

 Promise 的异常处理:

1)使用 .catch() 的方法

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => {
    console.error('Fetch error:', error);
    // 可以在这里进行错误恢复或用户通知
  });

2).then() 的第二个参数

fetch('https://api.example.com/data')
  .then(
    response => response.json(),
    error => console.error('Initial fetch failed:', error)
  )
  .then(data => console.log(data));

3)区别与注意事项

  • .catch() 会捕获前面所有 .then() 中的错误

  • 第二个参数错误处理只捕获当前 .then() 之前的错误

  • 推荐统一使用 .catch() 以获得更清晰的错误处理流程

 async/await 的异常处理

1) try/catch 块

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error in fetchData:', error);
    // 错误处理逻辑
  }
}

fetchData();

2) 在调用处处理错误

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return await response.json();
}

// 在调用async函数的地方处理错误
(async () => {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error('Failed to fetch data:', error);
  }
})();

3) 结合 Promise.catch()

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return await response.json();
}

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

高级异常处理技巧

1) 全局未捕获的 Promise 异常

// 浏览器环境
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled rejection:', event.reason);
  event.preventDefault(); // 阻止默认错误输出
});

// Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

2) 错误边界模式(React 中的概念)

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

// 使用
<ErrorBoundary>
  <MyAsyncComponent />
</ErrorBoundary>

3) 自定义错误类

class ApiError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
  }
}

async function fetchUser(userId) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new ApiError('User not found', response.status);
  }
  return await response.json();
}

try {
  const user = await fetchUser(123);
} catch (error) {
  if (error instanceof ApiError) {
    console.error(`API Error ${error.statusCode}: ${error.message}`);
  } else {
    console.error('Unexpected error:', error);
  }
}

4) 并行请求的错误处理

async function fetchMultipleUrls(urls) {
  try {
    // 使用Promise.allSettled获取所有结果(成功和失败)
    const results = await Promise.allSettled(
      urls.map(url => fetch(url).then(r => r.json()))
    );
    
    const successful = results
      .filter(r => r.status === 'fulfilled')
      .map(r => r.value);
    
    const errors = results
      .filter(r => r.status === 'rejected')
      .map(r => r.reason);
    
    if (errors.length > 0) {
      console.warn(`${errors.length} requests failed`);
    }
    
    return successful;
  } catch (error) {
    console.error('Unexpected error in fetchMultipleUrls:', error);
    throw error;
  }
}

最佳实践:

  1. 不要忽略错误:即使只是记录错误,也比完全忽略好

  2. 错误分类处理:根据错误类型采取不同的恢复策略

  3. 提供有意义的错误信息:帮助调试和用户理解问题

  4. 考虑错误恢复:如果可能,提供备用数据或重试机制

  5. 全局错误处理:设置全局捕获作为最后防线

  6. 日志记录:将错误信息记录到服务器以便分析

总结

  • Promise:使用 .catch() 或 .then() 的第二个参数处理错误

  • async/await:使用 try/catch 块捕获错误

  • 高级技巧:全局错误捕获、自定义错误类、并行请求处理

  • 最佳实践:始终处理错误,提供有意义的反馈,考虑恢复策略

        正确的异常处理可以使你的异步代码更健壮、更易于维护,并提供更好的用户体验。

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

1) 语法区别

普通函数:

function sum(a, b) {
  return a + b;
}

const sum = function(a, b) {
  return a + b;
};

箭头函数

const sum = (a, b) => {
  return a + b;
};

// 简写形式(当函数体只有一条返回语句时)
const sum = (a, b) => a + b;

2)  this绑定(最重要区别

普通函数:

  • 有自己的 this 上下文
  • this 值取决于调用方式(动态绑定)
  • 可以使用 callapplybind 改变 this
const obj = {
  value: 10,
  getValue: function() {
    return this.value; // this 指向 obj
  }
};

箭头函数:

  • 没有自己的 this,继承自外层作用域(词法绑定)
  • this 在定义时确定,不会改变
  • 不能使用 callapplybind 改变 this
const obj = {
  value: 10,
  getValue: () => {
    return this.value; // this 指向外层作用域(通常是 window)
  }
};

3) 构造函数与 new 关键字

普通函数

  • 可以用作构造函数,使用 new 调用
  • 会创建新的实例对象
    function Person(name) {
      this.name = name;
    }
    const p = new Person('Alice');

箭头函数

  • 不能用作构造函数
  • 使用 new 调用会抛出错误
    const Person = (name) => {
      this.name = name; // 错误
    };
    // TypeError: Person is not a constructor

4) arguments 对象

普通函数

  • 有自己的 arguments 对象
  • 包含所有传入的参数
    function logArgs() {
      console.log(arguments);
    }
    logArgs(1, 2, 3); // Arguments(3) [1, 2, 3]

箭头函数

  • 没有自己的 arguments 对象
  • 可以访问外层函数的 arguments(如果有)
    const logArgs = () => {
      console.log(arguments); // 错误:arguments is not defined
    };
    
    // 替代方案:使用剩余参数
    const logArgs = (...args) => {
      console.log(args);
    };

5) prototype 属性

普通函数

  • 有 prototype 属性
  • 用于实现基于原型的继承
    function Person() {}
    console.log(Person.prototype); // 存在

箭头函数

  • 没有 prototype 属性
  • 因为它们不能用作构造函数
    const Person = () => {};
    console.log(Person.prototype); // undefined

6) yield 关键字

普通函数

  • 可以用作生成器函数(使用 function* 语法)
  • 可以使用 yield 关键字

箭头函数

  • 不能用作生成器函数
  • 不能使用 yield 关键字

7) 方法定义

对象方法

  • 普通函数更适合作为对象方法(需要访问 this
  • 箭头函数会导致 this 指向错误
    const obj = {
      value: 10,
      // 正确方式
      getValue: function() {
        return this.value;
      },
      // 错误方式(this 不会指向 obj)
      badGetValue: () => {
        return this.value;
      }
    };

 8) 事件处理函数

DOM 事件处理

  • 普通函数可以正确绑定事件目标
  • 箭头函数会导致 this 指向错误
    button.addEventListener('click', function() {
      console.log(this); // 指向 button 元素
    });
    
    button.addEventListener('click', () => {
      console.log(this); // 指向外层作用域(通常是 window)
    });

9) 简写语法

箭头函数的简洁性

  • 单参数时可以省略括号
  • 单表达式时可以省略大括号和 return
    const double = x => x * 2;
    const greet = name => `Hello, ${name}!`;

何时用哪种该函数 ?

使用箭头函数的场景:

  1. 需要词法 this 绑定时(如回调函数)

  2. 需要简洁的匿名函数时

  3. 函数不需要自己的 thisarguments 或作为构造函数时

  4. 函数式编程(如 mapfilterreduce

使用普通函数的场景:

  1. 需要作为构造函数时

  2. 需要作为对象方法时

  3. 需要访问 arguments 对象时

  4. 需要动态 this 绑定时

  5. 需要生成器函数时

问:实现一个简单的发布-订阅模式 

        发布-订阅模式(Pub/Sub)是一种消息通信模式,用于实现对象间的松耦合通信。下面是几种实现方式: 

1)  基础实现

class EventEmitter {
  constructor() {
    this.events = {}; // 存储事件及回调
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
    return this; // 支持链式调用
  }

  // 发布事件
  emit(eventName, ...args) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      callbacks.forEach(cb => cb.apply(this, args));
    }
    return this; // 支持链式调用
  }

  // 取消订阅
  off(eventName, callback) {
    const callbacks = this.events[eventName];
    if (callbacks) {
      if (callback) {
        // 移除特定回调
        this.events[eventName] = callbacks.filter(cb => cb !== callback);
      } else {
        // 移除所有回调
        delete this.events[eventName];
      }
    }
    return this; // 支持链式调用
  }

  // 一次性订阅
  once(eventName, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
    return this;
  }
}

 使用示例:

const emitter = new EventEmitter();

// 订阅
const callback1 = data => console.log('Callback 1:', data);
emitter.on('event', callback1);

// 一次性订阅
emitter.once('event', data => console.log('One-time:', data));

// 发布
emitter.emit('event', { message: 'Hello' }); 
// 输出: 
// Callback 1: {message: "Hello"}
// One-time: {message: "Hello"}

// 再次发布
emitter.emit('event', { message: 'World' });
// 输出: 
// Callback 1: {message: "World"}

// 取消订阅
emitter.off('event', callback1);

2) 支持命名空间的实现

class AdvancedEventEmitter {
  constructor() {
    this.events = {};
    this.maxListeners = 10; // 默认最大监听器数量
  }

  // 添加监听器
  on(eventName, listener) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    
    // 检查监听器数量限制
    if (this.events[eventName].length >= this.maxListeners) {
      console.warn(`Possible memory leak detected. ${this.events[eventName].length} listeners added for event "${eventName}".`);
    }
    
    this.events[eventName].push(listener);
    return this;
  }

  // 触发事件
  emit(eventName, ...args) {
    const listeners = this.events[eventName];
    if (listeners) {
      listeners.forEach(listener => {
        try {
          listener.apply(this, args);
        } catch (err) {
          console.error(`Error in listener for event "${eventName}":`, err);
        }
      });
    }
    return this;
  }

  // 移除监听器
  off(eventName, listenerToRemove) {
    if (!this.events[eventName]) return this;
    
    this.events[eventName] = this.events[eventName].filter(
      listener => listener !== listenerToRemove
    );
    
    return this;
  }

  // 一次性监听器
  once(eventName, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(eventName, onceWrapper);
    };
    return this.on(eventName, onceWrapper);
  }

  // 设置最大监听器数量
  setMaxListeners(n) {
    this.maxListeners = n;
    return this;
  }

  // 获取所有监听器
  listeners(eventName) {
    return this.events[eventName] || [];
  }

  // 移除所有监听器
  removeAllListeners(eventName) {
    if (eventName) {
      delete this.events[eventName];
    } else {
      this.events = {};
    }
    return this;
  }
}

3)更简洁的函数实现方式

function createPubSub() {
  const subscribers = {};

  function subscribe(eventName, callback) {
    if (!subscribers[eventName]) {
      subscribers[eventName] = new Set();
    }
    
    const callbacks = subscribers[eventName];
    callbacks.add(callback);
    
    return () => {
      callbacks.delete(callback);
      if (callbacks.size === 0) {
        delete subscribers[eventName];
      }
    };
  }

  function publish(eventName, data) {
    if (subscribers[eventName]) {
      subscribers[eventName].forEach(callback => {
        callback(data);
      });
    }
  }

  return {
    subscribe,
    publish,
  };
}

// 使用示例
const { subscribe, publish } = createPubSub();

// 订阅
const unsubscribe = subscribe('message', data => {
  console.log('Received:', data);
});

// 发布
publish('message', 'Hello World!');

// 取消订阅
unsubscribe();

4)支持异步事件的实现

class AsyncEventEmitter {
  constructor() {
    this.events = {};
  }

  async on(eventName, listener) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(listener);
  }

  async emit(eventName, ...args) {
    const listeners = this.events[eventName];
    if (listeners) {
      for (const listener of listeners) {
        try {
          await listener(...args);
        } catch (err) {
          console.error(`Error in async listener for "${eventName}":`, err);
        }
      }
    }
  }

  async off(eventName, listenerToRemove) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        listener => listener !== listenerToRemove
      );
    }
  }
}

// 使用示例
const asyncEmitter = new AsyncEventEmitter();

asyncEmitter.on('data', async data => {
  await new Promise(resolve => setTimeout(resolve, 100));
  console.log('Processed data:', data);
});

asyncEmitter.emit('data', { id: 1, value: 'test' });

选择建议:

  1. 基础需求:使用第一种基础实现即可满足大多数场景

  2. 生产环境:建议使用第二种更健壮的实现

  3. 函数式偏好:第三种实现更简洁

  4. 异步事件:第四种实现支持异步监听器

发布-订阅模式的优点:

  • 解耦发布者和订阅者
  • 支持一对多的消息通信
  • 动态添加和移除订阅关系

实际应用场景:

  • 组件间通信
  • 插件系统
  • 状态管理
  • 事件驱动的架构

3.手写代码 

问:防抖(Debounce)与节流(Throttle)的实现?

1) 防抖实现代码:

        防抖的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

/**
 * 防抖函数
 * @param {Function} fn 需要防抖的函数
 * @param {number} delay 延迟时间(毫秒)
 * @param {boolean} immediate 是否立即执行
 * @return {Function} 返回防抖后的函数
 */
function debounce(fn, delay = 300, immediate = false) {
  let timer = null;
  
  return function(...args) {
    const context = this;
    
    // 清除已有的定时器
    if (timer) {
      clearTimeout(timer);
    }
    
    // 立即执行模式
    if (immediate && !timer) {
      fn.apply(context, args);
    }
    
    // 设置新的定时器
    timer = setTimeout(() => {
      timer = null;
      if (!immediate) {
        fn.apply(context, args);
      }
    }, delay);
  };
}

/*使用:*/
// 普通防抖
const debouncedFn = debounce(() => {
  console.log('窗口大小改变了');
}, 500);

window.addEventListener('resize', debouncedFn);

// 立即执行防抖
const debouncedSearch = debounce(function(query) {
  console.log('搜索:', query);
}, 1000, true);

searchInput.addEventListener('input', (e) => {
  debouncedSearch(e.target.value);
});

2) 节流实现代码:

        节流的核心思想是:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。 

/**
 * 节流函数
 * @param {Function} fn 需要节流的函数
 * @param {number} delay 节流时间间隔(毫秒)
 * @param {boolean} trailing 是否在节流结束后执行最后一次调用
 * @return {Function} 返回节流后的函数
 */
function throttle(fn, delay = 300, trailing = true) {
  let lastTime = 0;
  let timer = null;
  
  return function(...args) {
    const context = this;
    const now = Date.now();
    
    // 清除trailing的定时器
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    
    // 如果距离上次执行时间超过delay,立即执行
    if (now - lastTime >= delay) {
      fn.apply(context, args);
      lastTime = now;
    } 
    // trailing模式,设置定时器在delay后执行
    else if (trailing) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        lastTime = Date.now();
        timer = null;
      }, delay - (now - lastTime));
    }
  };
}


/*  使用示例 */
// 基本节流
const throttledScroll = throttle(() => {
  console.log('滚动事件处理');
}, 200);

window.addEventListener('scroll', throttledScroll);

// 带trailing的节流
const throttledResize = throttle(() => {
  console.log('窗口大小改变处理');
}, 500, true);

window.addEventListener('resize', throttledResize);

实际应用建议

  1. 搜索建议:使用防抖,等待用户停止输入后再发送请求
  2. 无限滚动:使用节流,定期检查滚动位置
  3. 窗口调整:根据需求选择 - 如果只需要最终状态用防抖,如果需要实时调整用节流
  4. 游戏开发:玩家移动通常使用节流保证流畅体验

问:深拷贝函数(考虑循环引用)? 

        以下是一个健壮的深拷贝函数实现,可以处理各种数据类型(包括 Date、RegExp、Map、Set 等)和循环引用情况: 

/**
 * 深拷贝函数
 * @param {*} target 要拷贝的目标值
 * @param {WeakMap} [map] 用于存储已拷贝对象的WeakMap(处理循环引用)
 * @return {*} 拷贝后的值
 */
function deepClone(target, map = new WeakMap()) {
  // 处理基本数据类型和null/undefined
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 处理循环引用
  if (map.has(target)) {
    return map.get(target);
  }

  // 处理Date对象
  if (target instanceof Date) {
    return new Date(target);
  }

  // 处理RegExp对象
  if (target instanceof RegExp) {
    return new RegExp(target);
  }

  // 处理Map对象
  if (target instanceof Map) {
    const cloneMap = new Map();
    map.set(target, cloneMap);
    target.forEach((value, key) => {
      cloneMap.set(deepClone(key, map), deepClone(value, map));
    });
    return cloneMap;
  }

  // 处理Set对象
  if (target instanceof Set) {
    const cloneSet = new Set();
    map.set(target, cloneSet);
    target.forEach(value => {
      cloneSet.add(deepClone(value, map));
    });
    return cloneSet;
  }

  // 处理数组和普通对象
  const cloneTarget = Array.isArray(target) ? [] : {};
  map.set(target, cloneTarget);

  // 使用Reflect.ownKeys获取所有自有属性(包括Symbol)
  Reflect.ownKeys(target).forEach(key => {
    cloneTarget[key] = deepClone(target[key], map);
  });

  return cloneTarget;
}

使用示例:

// 测试循环引用
const obj = {
  a: 1,
  b: {
    c: 2,
    d: [3, 4]
  }
};
obj.circularRef = obj; // 循环引用

// 测试特殊对象
const testObj = {
  date: new Date(),
  regex: /abc/gi,
  map: new Map([['key1', 'value1'], ['key2', obj]]),
  set: new Set([1, 2, 3, obj]),
  [Symbol('unique')]: 'symbolValue'
};

const clonedObj = deepClone(testObj);

console.log(clonedObj);
console.log(clonedObj !== testObj); // true
console.log(clonedObj.map !== testObj.map); // true
console.log(clonedObj.circularRef === clonedObj); // true (循环引用保持)

实现说明

  1. 基本数据类型:直接返回,不需要拷贝

  2. 循环引用处理:使用 WeakMap 存储已拷贝对象,遇到相同引用时直接返回

  3. 特殊对象处理

    • Date 对象:创建新的 Date 实例

    • RegExp 对象:创建新的 RegExp 实例

    • Map/Set:递归拷贝其内容

  4. 数组和对象

    • 区分数组和普通对象

    • 使用 Reflect.ownKeys 获取所有自有属性(包括 Symbol)

    • 递归拷贝每个属性

  5. 性能考虑

    • 使用 WeakMap 而不是 Map,避免内存泄漏

    • 只在必要时创建新对象

注意事项

  1. 该实现不处理函数(函数通常不需要深拷贝)

  2. 不处理 DOM 节点(DOM 节点通常也不应该被深拷贝)

  3. 不处理 Buffer、ArrayBuffer 等二进制数据

  4. 对于非常复杂的对象结构,可能需要特殊处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大樊子

有钱捧个人场,没钱捧个钱场😜

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值