跳到主要内容

前端面试题

简介

收集前端面试题,以练代学,因此以基础知识为主,学习完能获得最基础最扎实的前端知识

网络与浏览器

什么是同源策略以及跨源资源共享(CORS)?

例如浏览器只允许百度页面获取百度服务器的响应数据,不允许获取搜狗服务器的响应数据,这就是同源策略。同源策略是浏览器只允许当前网页与同一源(即同一域/协议/端口)下的其他资源进行交互,是浏览器的一种安全机制,用于保护用户的安全和隐私。

需要注意的是,此时百度页面请求搜狗服务器,是能正常请求的,搜狗服务器也是能正常响应的,但是搜狗服务器的响应数据返回给浏览器后, 浏览器发现请求的是百度页面,不符合同源策略而把响应数据拦截了,因此主要是浏览器在拦截,而不是服务器拦截。

那怎么才能不拦截呢?例如搜狗服务器在得到百度的请求后,在服务器响应数据里添加附加信息(在响应头里加一个字段),表明服务器允许这次跨域, 浏览器接受到这个响应后发现允许跨域,从而将该响应数据返回给百度页面,这就是跨源资源共享(Cross-Origin Resource Sharing,简称CORS)。

例如百度页面的ajax请求在http header中添加上Origin: http://baidu.com字段后发送到搜狗服务器, 搜狗服务器接收后,看到请求头中的Origin字段,明白了这是来自百度页面的CORS跨域请求,在返回正常响应数据时,在响应头会添加例如Access-Control-Allow-Origin: *这样的字段, 当浏览器接收到该响应后,它会查看响应头中的Access-Control-Allow-Origin字段是否包含本域(baidu.com,星号表示全匹配),如果包含,则将该响应交给发出该ajax请求的js脚本,否则浏览器拒绝该响应。

讲一讲什么是URL编码?

Url编码通常也被称为百分号编码(Url Encoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——代表一个字节的十六进制形式。 例如a在US-ASCII码中对应的字节是0x61,那么Url编码之后得到的就是%61。Url编码默认使用的字符集是US-ASCII。

RFC3986文档规定,Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个符号以及所有保留字符。

保留字符:Url可以划分成若干个组件,协议、主机、路径等。有一些字符是用作分隔不同组件的。例如:冒号用于分隔协议和主机,/用于分隔主机和路径,?用于分隔路径和查询参数,等等。

对于Unicode字符,RFC文档建议使用utf-8对其进行编码得到相应的字节,然后对每个字节执行百分号编码。 如"中文"使用UTF-8字符集得到的字节为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到"%E4%B8%AD%E6%96%87"。

encodeURI() 函数可把字符串作为 URI 进行编码。 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码例如 - _ . ! ~ * ' ( ) 。 因为该方法的目的是对 URI 进行完整的编码使之成为一个合格的URI。

encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。 因此除了与encodeURI()相同的功能外,encodeURIComponent() 函数还必须转义用于分隔 URI 各个部分的标点符号。

强制缓存和协商缓存有什么区别?

浏览器缓存(Brower Caching)是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。

  • 强制缓存:根据Expires(response header里的过期时间)判断,浏览器再次加载资源时,如果在这个过期时间内,则命中强缓存,并不会向服务端发起请求,展示为200状态。
  • 协商缓存:客户端向服务端发送带有If-None-Match和If-Modified-Since的请求进行协商判断,如果资源没有变化继续使用本地缓存,记录为304状态;如果资源发生变化,服务端响应数据,记录为200状态。

强制缓存的优先级是要高于协商缓存,这两者其实都是http缓存,通过协议头的控制字段指示浏览器的操作,还有一种浏览器缓存,主要是存一些静态资源之类的。 建议要开启缓存,因为这会大幅度降低页面白屏,在nginex里设置add_header Cache-Control no-cache;即可。

  • no-cache表示不缓存过期资源,缓存会向服务器进行有效处理确认之后处理资源
  • 而no-store才是真正的不进行缓存 如何清除缓存,强制刷新呢?后端可以加no-store,在前端页面可以加<META HTTP-EQUIV="Cache-Control" CONTENT="no-cache, must-revalidate"> 标签。 或者在url上加随机字符串。或者在请求时加上一些掌管缓存的数据头。

get/post 请求的区别?

  • 在语义上,get请求用于获取数据,应该是幂等的,多次相同的GET请求不能对服务器的状态有影响,因此get请求可以被缓存以提高性能;post请求用于提交数据,不是幂等的,会对服务器状态产生影响,不能缓存。
  • get传参是通过地址栏URL传递,把请求的数据在URL后通过?连接,通过&进行参数分割,只支持ASCII字符,只能URL编码,长度最多在2KB到8KB左右; post将参数存放在HTTP的包体内,没有字符类型限制,有多种编码格式,长度最多在在2MB到10MB左右。

讲一讲https以及证书(SSL)

  • https是密文传输,解决了http明文传输易受到中间人攻击的问题,通过非对称密钥,数字证书等方式完成数据加密传输。
  • 非对称加密是指一对不同的密钥,用其中一个密钥加密的密文,只能被另一个密钥解开,公开的密钥称为公钥,不公开的称为私钥。能解决对称加密被中间人获取到密钥的问题。 但仍不能解决中间人在中间代理信息的问题,即中间人获取到公钥后,对两端用自己的公钥私钥代理信息。
  • 证书则能解决中间人代理信息的问题
    1. 首先服务端使用摘要算法(例如MD5)将证书明文(例如域名,服务端公钥)生成摘要,然后送给CA权威机构。
    2. CA机构将摘要用CA机构自己的私钥进行加密,得出来的叫签名,然后附在证书上。
    3. 证书被发送到客户端,客户端通过同样的摘要算法对证书明文计算摘要,然后用CA机构的公钥解开签名得到解密的摘要, 两者比对相同,则证明证书没有篡改,证书上服务端的公钥是该服务端生成的公钥,因此客户端拿到了服务端的公钥。
  • 浏览器向服务器发起Https请求的流程如下:
    1. 首先浏览器向服务器发起请求。
    2. 服务器将证书机构颁发给自己的证书传递给浏览器。
    3. 浏览器从本地安装的根证书中找到证书机构的公钥,用公钥来验签证书的正确性,确保是证书机构用私钥签名的合法证书,从而拿到了服务器公钥。
    4. 浏览器随机生成一个对称秘钥key,用证书中的服务器公钥加密这个key,再传输给服务器。
    5. 服务器用私钥解密后取出对称秘钥key,并用该key加密确认内容返回给客户端,告知可以开始通信。
    6. 浏览器与服务器开始采用该key进行加密通信。

http的状态码说几个?

1xx(临时响应)2xx(请求成功)3xx(重定向)4xx(请求错误)5xx(服务器错误)

  • 100:请求者应当继续提出请求。
  • 101:切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到 HTTP 的新版本协议。
  • 200:正确的请求返回正确的结果。
  • 201:表示资源被正确的创建。比如说,我们 POST 用户名、密码正确创建了一个用户就可以返回 201。
  • 202:请求是正确的,但是结果正在处理中。这时候客户端可以通过轮询等机制继续请求。
  • 300:请求成功,但结果有多种选择。相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择。
  • 301:请求成功,但是资源被永久转移。返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI 代替。
  • 304:请求的资源并没有被修改过。服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源。
  • 400:请求出现错误,比如请求头不对等。
  • 401:没有提供认证信息。请求的时候没有带上 Token 等。
  • 402:为以后需要所保留的状态码。保留,将来使用。
  • 403:请求的资源不允许访问。就是说没有权限。
  • 404:请求的内容不存在。
  • 500:服务器错误。
  • 501:请求还没有被实现。服务器不支持请求的功能,无法完成请求。

Cookie、localStorage、sessionStorage、IndexedDB的区别?

  • Cookie有过期时间,Cookie的信息会在http请求的时候携带到服务器。
  • localStorage是永久存储,最大限制一般为5-10MB,所有数据都将作为字符串存储。
  • sessionStorage是会话存储,浏览器关闭就会消失。
  • IndexedDB是前端数据库,能存储几百MB的数据,api比较复杂。

TCP与UDP是什么?

  • TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,使用三次握手协议建立连接、四次挥手断开连接。

    • 三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的:
      • 第一次握手(客户端发送 SYN 报文给服务器,服务器接收该报文):客户端什么都不能确认;服务器确认了对方发送正常,自己接收正常
      • 第二次握手(服务器响应 SYN 报文给客户端,客户端接收该报文):客户端确认了:自己发送、接收正常,对方发送、接收正常;服务器确认了:对方发送正常,自己接收正常
      • 第三次握手(客户端发送 ACK 报文给服务器):客户端确认了:自己发送、接收正常,对方发送、接收正常;服务器确认了:自己发送、接收正常,对方发送、接收正常

    服务器收到客户端第一次握手信息之后,此时双方还没有完全建立其连接,服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。同样,在关闭时, TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力,这叫半关闭特性。

    • 终止一个 TCP 连接要经过四次挥手。这是由于 TCP 的半关闭(half-close)特性造成的,因此需要同时关闭客户端和服务端,双方都应该知道对方已关闭,客户端或服务端均可主动发起挥手动作。例如客户端先发起的关闭请求:
      • 第一次挥手客户端发送给服务端一个信息,客户端关闭TCP链接,但并不知道服务端关闭,因此处于等待状态1。
      • 第二次挥手服务端发送给客户端一个信息,客户端接受到后知道服务端已经知道自己打算关闭了,于是客户端到服务端的连接释放,客户端处于等待状态2,此时的 TCP 处于半关闭状态,服务端仍可发送数据到客户端。
      • 第三次挥手服务端发送给客户端一个信息,表示服务端也想关闭连接,此时服务端处于一种等待状态,客户端接收到后处于等待状态3
      • 第四次挥手客户端发送给服务端一个信息,服务端收到后关闭连接,
  • UDP是一种无连接的,不可靠的,基于报文的传输层通信协议,能够多播广播。

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

主要分成两部分:渲染引擎(Layout Engine或Rendering Engine)和JS引擎。

  • 渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。
  • JS引擎:解析和执行javascript来实现网页的动态效果。

HTML

在HTML5中DOCTYPE的作用是什么?标准与兼容模式(混杂模式)各有什么区别?

DOCTYPE(文档类型声明)是一种在HTML文档中使用的标记,用于告诉浏览器使用哪个HTML版本解析文档。它的作用是确保浏览器正确地渲染和显示网页内容。

标准模式(严格模式)和兼容模式(混杂模式)是浏览器根据DOCTYPE声明来选择不同的渲染模式。它们有以下区别:

标准模式(严格模式):在标准模式下,浏览器会按照HTML和CSS规范的最新标准解析和渲染页面。 浏览器会严格地遵循规范,对错误的处理更加严格。能提供更一致的渲染结果。

兼容模式(混杂模式):在兼容模式下,浏览器会以一种更宽松的方式解析和渲染页面,以保持与旧版本浏览器的兼容性。 但也可能导致不同浏览器之间的渲染结果不一致。

通过使用正确的DOCTYPE声明,可以确保浏览器按照所选择的渲染模式来解析网页。一般推荐使用最新的HTML5 DOCTYPE声明(<!DOCTYPE html>),以便在标准模式下进行开发和测试,以获得更好的一致性和可靠性。

meta标签的作用?

<meta> 标签定义关于 HTML 文档的元数据。这些数据用于描述整个文档。

<meta> 标签始终位于 <head> 元素 内,通常用于指定字符集、页面描述、关键词、文档作者和视口设置,一个 HTML 文档中可以多个 meta 元素。

meta属性主要有:

  • charset,字符集,规定 HTML 文档的字符编码。
  • name,规定元数据的名称,相当于key。值可以为author,keywords,description,viewport,application-name,generator
  • content,文本,相当于value,规定与 http-equiv 或 name 属性关联的值。
  • http-equiv,为 content 属性的信息/值提供 HTTP 标头。可以为content-security-policy,content-type,default-style,refresh

用法例如:

为搜索引擎定义关键字:<meta name="keywords" content="HTML, CSS, JavaScript">使用 charset 属性声明该页面采用 UTF-8 字符编码:<meta charset="UTF-8">

哪些是行内(内联)元素,哪些是块级元素?

  • 行内元素不从新行开始,只占用必要宽度,不可以设置宽高,只能包含数据和其他行内元素。例如大部分文字有关的标签,如<a>、<span>等。
  • 块级元素从新行开始,占用全部宽度(除非设置了宽度),可以包含其他块级或内联元素。例如<div>、<p>、<table>、<h>等。
  • 还有一些行内块级元素,它可以自由设置元素宽度和高度,也可以在一行中放置多个行内块级元素。比如:<input>、<img>、<button>、<textarea>
  • 可以通过css属性display: inline|block改变元素的行内/块级属性。

了解重绘和重排吗?

  • 当Dom的变化影响到了元素的几何属性(宽和高等)——比如说修改了边框的宽度,或者是修改了高度,又或者给文章增加了内容导致元素的高度增加等,会引起浏览器进行重新计算元素的几何属性,同样,其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效。并重新构建渲染树,这个过程称为重排。 完成重排之后,浏览器会重新绘制受影响的元素,这个过程被称为重绘。
  • 当一个元素发生外观上的变化就会重绘,以下几点会发生重排:
    • 页面渲染器初始化
    • 元素尺寸,位置,内容发生改变
    • 浏览器窗口尺寸发生改变
  • 优化重排效率
    • 批量修改Dom,合并Dom的多次修改
    • 使用absolute或fixed样式脱离文档流修改
    • 使用GPU加速
    • 使用虚拟Dom

讲一讲HTML5有什么新特点?

HTML5是超文本标记语言(HTML)的第五版,在2014年10月正式获得“推荐”的地位,上一版HTML4.x是在2000年左右发布的。

  • 添加了许多语义化的标签。
  • 增强了表单能力。
  • 支持音/视频的控制。
  • 支持Canvas/SVG绘图。
  • 增加了web storage。
  • 增加web socket长联能力。

在地址栏里输入一个URL,到这个页面呈现出来,中间会发生什么?

浏览器检查当前url是否存在缓存和缓存是否过期,如果过期了则根据本地hosts或者DNS服务器进行域名解析得到ip,然后三次握手建立tcp连接, 返回页面报文,解析下载的HTML生成DOM树,解析下载的CSS生成CSS树,DOM树和CSS树合并为渲染树,然后根据渲染树绘制节点。

preventDefault和stopPropagation?

阻止了事件的默认行为,阻止了事件冒泡

script 标签中 defer 和 async 的区别?

  • <script> :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
  • <script async> :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
  • <script defer> :完全不会阻碍 HTML 的解析,当页面已完成加载后,才会执行脚本。

XSS 攻击/ CSRF 攻击是什么?

  • Cross Site Script,跨站脚本攻击。是指攻击者在网站上注入恶意script代码, 通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。 比如攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户, 都有可能在他们的浏览器中执行这段恶意的 JavaScript 代码。

CSRF就是利用用户的登录态发起恶意请求。

响应式布局,怎么做移动端适配

首先,需要设置viewport的meta标签,将width设置为device-width:

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  • width=device-width: 让当前 viewport 宽度等于设备的宽度
  • user-scalable=no: 禁止用户缩放
  • initial-scale=1.0: 设置页面的初始缩放值为不缩放
  • maximum-scale=1.0: 允许用户的最大缩放值为 1.0
  • minimum-scale=1.0: 允许用户的最小缩放值为 1.0

然后可以使用:

  • 媒体查询方案
  • 动态 rem 方案

元素的innerText outerText innerHTML的区别?

  • innerText和outerText取值都是对象起始和结束标签内部的文本内容。但是在设置的时候,outerText会把该标签也替换掉。
  • innerHTML取值是对象起始和结束标签内部的html,不包括对象本身的起始标签和结束标签。设置时是填充该标签内部。
  • outerHTML取值是也是html,但是包括对象本身的起始标签和结束标签。设置时是连该标签也替换掉。

如何获取标签上的自定义属性?

使用.getAttribute('xxx'),如果是标签上的自有属性,则直接可以ele.xxx就行。

HTML中childNodes和children有什么区别?

  • Node:在 DOM 树中,所有的节点都是 Node,包括 Element,也就是说 Node 包含了 HTML 元素标签、text、以及注释等等内容,它是所有 DOM 的基类。
  • Element:在 DOM 树中,Element 是所有 HTML 元素的基类,也就是说,Element 只包含 HTML 元素标签。

综上两点可以得出:Node 和 Element 两者是包含关系,Node 包含 Element。 从而衍生出了两个集合:NodeList 和 HTMLCollection。NodeList 是 Node 的集合,HTMLCollection 是 Element 的集合。 childNodes会返回所有节点,包括HTML、Text、注释等等内容(甚至包括换行)。children只返回元素节点。 虽然我们可以通过NodeType=1元素|2属性|3文本来辨别种类,但注释也会算进文本节点。

如何使用一个监听给ul下面多个li绑定监听事件

当逐个给每个li绑定监听事件,当li太多之后,需要绑定很多监听器,如果有1000个li,因此需要事件代理,又称事件委托。 简单的来讲就是利用JS中事件的冒泡属性,把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。

let ul = document.getElementById('ul')
ul.addEventListener('click', (e) => {
let t = e.target
switch(t.innerText) {
case '1':
break
case '2':
break
}
})

JavaScript

var,let,const,死区?

var,let,const声明用法是一样,都是定义变量,var的声明会被提升到全局作用域或函数作用域,但仍然在原地方赋值。let,const并不会声明提升,因此在声明之前的代码区域就是暂时性死区。

call,apply,bind的区别?

三者的功能都是用来改变函数中的 this 指向。

  • call() 方法是预定义的 JavaScript 方法。它可以用来调用所有者对象作为参数的方法。通过 call(),您能够使用属于另一个对象的方法。call传入的参数数量不固定,第一个参数代表函数内的this指向,从第二个参数开始往后,每个参数被依次传入函数。
  const person = {    fullName: function (country, city) {      return this.firstName + this.lastName + " " + country + " " + city    }  }  const newPerson = {    firstName: "fu",    lastName: "chaoyang",  }  person.fullName.call(newPerson, 'china', 'xian') // fuchaoyang china xian 
  • apply() 对比 call() 仅参数上的不同:接受两个参数,第一个参数指定了函数体内的this指向。第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数。
  const person = {    fullName: function (country, city) {      return this.firstName + this.lastName + " " + country + " " + city    }  }  const newPerson = {    firstName: "fu",    lastName: "chaoyang",  }  person.fullName.apply(newPerson, ['china', 'xian']) // fuchaoyang china xian
  • bind() 相对 call() 而言,仅绑定新对象,不立即执行。
  const person = {    fullName: function (country, city) {      return this.firstName + this.lastName + " " + country + " " + city    }  }  const newPerson = {    firstName: "fu",    lastName: "chaoyang",  }  person.fullName.bind(newPerson, 'china', 'xian') // 打印出fullName函数  person.fullName.bind(newPerson, 'china', 'xian')() // fuchaoyang china xian

说一下原型和原型链?

JavaScript 中,万物皆对象,对象分为普通对象和函数对象。 所有的函数都是函数对象(typeof f === 'function'),其他都是普通对象(typeof o === 'object')。

JS在没有类class前,创建一个对象都是通过 new 函数() 来实现的(也就是构造函数),例如:

var a = new Object() // 此时a是{}var a = {} // 等价于 var a = new Object()function b() {}var a = new b() // 此时a是{}

在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。 其中每个函数对象都有一个prototype 属性,这个属性就是所谓的原型对象。 可以把原型对象是函数内维护的一个对象模版,当 new 函数() ,就会把这个对象拷贝一份返回新对象,这样就完成了对象初始化,例如:

function b() {    b.prototype.c = 1    this.d = '2' // 在构造函数里,this指向b.prototype}var a = new b() // 此时a为{c: 1, d: '2'}

另外,JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做proto的内置预定义属性, 用于指向创建它的构造函数的原型对象,例如在上面👆的例子中,a.__proto__ === b.prototype

每个对象都有proto属性,但只有函数对象才有 prototype 属性。

每个对象都有constructor(构造函数)属性,这个属性指向 prototype 属性所在的函数, 例如在上面👆的例子中,a.constructor === b.prototype.constructor === b

注意Function.prototype是函数对象,但没有prototype属性,即Function.prototype.prototype === undefined,算是一个偏门知识点吧。

原型链:在多重继承中,一个对象可以是原型的拷贝,同时也是另一个对象的原型。 因此,当你尝试访问对象上的属性时,JavaScript引擎开始从对象自身中查找该属性, 如果没有,它会继续检查proto,一直到没有proto或者找到该属性。 如果找到最后,此属性不存在时,返回undefined。

// 多重继承function Animal() {}function Bird() {  Bird.prototype = new Animal()}function Crow() {  Crow.prototype = new Bird()}let crow = new Crow() // crow.proto = bird, bird.proto = animal, animal.proto = Object.prototype// 或者class Animal {}class Bird extends Animal {  constructor() { // 子类必须在 constructor 方法中调用super方法,否则新建实例时会报错        super()    }}class Crow extends Bird {  constructor() { // 子类必须在 constructor 方法中调用super方法,否则新建实例时会报错        super()    }}let crow = new Crow()

原型链的最上层是Object.prototype

讲一讲js的this指针?

  • 全局的this指针指向windows。但在严格模式下,全局作用域中的 this 的值是 undefined。
  • 在函数中,this 表示全局对象。但在严格模式下,this 是未定义的(undefined)。
  • 当一个方法被调用时,this被绑定到这个对象上。
  • 如果在一个函数当构造函数用,函数中的this会被绑定到这个新对象上。
  • 对象里的this指向window。例如:a = {b: this.k},此时this指向windows。
  • call/apply/bind,this指针会绑定指定的对象。
  • 事件的this指针指向元素本身。
  • 箭头函数中的this是指向箭头函数定义时的离箭头函数最近的一个函数的this。

闭包是什么?

闭包是指一个函数可以访问和使用其定义外部的变量,让这些变量的值始终保持在内存中,同时提供很好的封装和抽象。

手写防抖节流?

  • 防抖(debounce):每次触发定时器后,取消上一个定时器,然后重新触发定时器。防抖一般用于用户未知行为的优化,比如搜索框输入实时提示。

    let timer;function debounce(cb, time) {  clearTimeout(timer);  timer = setTimeout(() => {      cb();  }, time)} onInputChange(text) {  debounce(() => {    // 获取实时提示  }, 100)}
    // 复杂一点的function debounce(func, time) {  let timer;  return function() {    const [that, args] = [this, arguments];    clearTimeout(timer)    timer = setTimeout(() => {      func.apply(that, args);    }, time);  }} onInputChange = debounce((text) => {  // 获取实时提示}, 1000)
  • 节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发该函数。一般用于可预知的用户行为的优化,比如限制按钮点击的触发频率,防止重复的提交。

let previous = 0;function throttle(cb, time) {  const now = Date.now();  if (now - previous > time) {    cb();    previous = now;  }} onButtonClick() {  throttle(() => {    // 提交表单  }, 1000)}
// 复杂一点的function throttle(func, time) { let previous = 0; return function() { const [that, args] = [this, arguments]; const now = Date.now(); if (now - previous > time) { previous = now; func.apply(that, args); } }}

事件循环机制?

JS是一门单线程的语言,通过事件循环机制实现单线程异步的。JS所有同步任务都在主线程上执行,即执行栈。主线程之外还存在一个任务队列(也有人称之为消息队列),用于执行异步任务。事件循环的工作流程如下:

  • 首先,检查执行栈,看看是否有同步任务需要运行。
  • 如果执行栈为空,那么就查看任务队列。
  • 如果任务队列中有待处理的任务,那么就将它移出队列并放入调用堆栈,以便执行它的回调函数。

在JavaScript中,任务队列可以分为宏任务队列和微任务队列。在一个事件循环迭代中,首先会执行一个宏任务,然后执行所有的微任务。当所有的微任务完成后,再执行下一个宏任务。 宏任务包括如setTimeout,setInterval,setImmediate,I/O,UI rendering等,而微任务包括如Promise,MutationObserver等。

捕获与冒泡?

在 HTML 中,当事件被触发时,事件会经过三个阶段:捕获阶段、目标阶段和冒泡阶段。

  • 捕获阶段(Capture Phase):从 document 对象开始,逐级向下传递,直到事件源元素。在捕获阶段,事件处理函数会按照由外向内(由父元素到子元素)的顺序被依次执行。
  • 目标阶段(Target Phase):当事件传递到事件源元素时,就进入了目标阶段。在目标阶段,事件处理函数会被执行,此时可以通过 event.target 属性获取到具体触发事件的元素。
  • 冒泡阶段(Bubble Phase):从事件源元素开始,逐级向上传递,直到 document 对象。在冒泡阶段,事件处理函数会按照由内向外(由子元素到父元素)的顺序被依次执行。

在捕获阶段中,可以通过 addEventListener() 函数的第三个参数(选项)设置 capture 选项来监听事件的捕获阶段,否则监听将在冒泡阶段触发。

事件传递过程中,如果事件处理函数调用了 event.stopPropagation() 方法,当侦听的事件是捕获时,阻断的就是捕获过程,当侦听的事件是冒泡时,阻断的就是冒泡过程。

typeof,instantof是什么?

  • typeof是一个运算符,返回值是一个字符串,用来说明变量的数据类型,可以用此来判断number, string, object, boolean, function, undefined, symbol 这七种类型。对于对象、数组、null 返回的值是 object。
  • instanceof运算符用于指示一个变量是否属于某个对象的实例。返回值为布尔值。instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。

讲一讲es5/6/7/8/9新特点?

  • ECMAScript 5 也称为 ES5 和 ECMAScript 2009。
    • "use strict" 指令。例如,使用严格模式,不能使用未声明的变量
    • String.trim()/charAt() 删除字符串两端的空白字符
    • Array.isArray()/forEach()/map()/fliter()/indexOf()/reduce()
    • JSON.parse()/stringify()
    • Date.now()
    • Object.key()/defineProperty()/getPrototypeOf()
    • 属性 Getter 和 Setter
  • ECMAScript 6,也被称为ES6和ES2015,是JavaScript的第六个版本
    • let 和 const 以及块级作用域,在ES6之前,JavaScript只有全局作用域和函数作用域,没有块级作用域。
    • 模板字符串
    • String.startWith()/endWith()
    • 箭头函数
    • 函数参数的默认值,剩余参数
    • 解构赋值,展开运算符
    • 对象字面量的增强:属性和方法的简洁表示法,方括号表示法
    • Object.keys()/values()/entries()/assign()
    • 全局作用域中的 this 指向,在严格模式下,全局作用域中的 this 的值是 undefined。在非严格模式下,它会指向全局对象(浏览器中是 window 对象,Node.js 中是 global 对象)。
    • Array.includes()/find()/from()/fill()
    • set和map数据结构
    • 遍历器与for…of循环
  • ES2016(ES7)
    • 幂运算符** 但一般也没有什么需求会用到幂乘
    • Array.prototype.includes(),第二个参数是从第几个下标开始搜,默认0
  • ES2017 ES8
    • Object.values, Object.entries
    • 结尾允许逗号
    • async异步函数
  • ES2018 ES9
    • 异步循环,同步循环中调用能引起等待的异步函数,是不会达到预期目的,循环本身依旧保持同步,并在内部异步函数之前完成。因此新增异步循环for await(let i of array)
    • 异步的finally
    • 对象的展开运算符,这项特性在ES6中已经引入,但是仅限于数组。
  • ES2019 ES10
    • catch的参数e可以省略掉
    • Object.fromEntries()方便地将键值对列表(例如 Map、数组(符合键值对的)等)转换为一个对象。
    • Array.flat()能将高维数组降一维。
  • ES 2020 (ES11)
    • 可选链操作符
    • BigInt 它是JavaScript的第7个原始类型,可安全地进行大数整型计算。 只需要在数字后面加上 n 即可。但不能将 BigInt与Number算术计算。

for in, for of区别?

  • for in循环返回的值都是对象的键值名(数组即下标),遍历顺序有可能不是按照实际数组的内部顺序,使用for… in会遍历数组/对象所有的可枚举属性,包括继承属性,原型。所以不适合遍历数组,更适合遍历对象。
  • for… of 循环用来获取一对键值对中的值,相对于forEach而言可以与 break、continue和return 配合使用,可以随时退出循环。但for…of循环内部调用的是数据结构的Symbol.iterator方法。 因此不能遍历对象,因为没有迭代器对象,可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。

在ECMAScript规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符串属性根据创建时的顺序升序排列。」在这⾥我们把对象中的数字属性称为 「排序属性」,在V8中被称为 elements,字符串属性就被称为 「常规属性」, 在V8中被称为 properties。

遍历对象的几种方法?

  • for...in
  • Object.keys(),参数是一个对象,返回是该对象的key数组
  • Object.values(),参数是一个对象,返回是该对象的value数组
  • Object.entries(),返回值为Object.values()与Object.keys()的结合,返回一个二维数组,每个小数组都是[ [属性名,属性值],[属性名,属性值] ]
  • Object.getOwnPropertyNames(),其返回结果与Object.keys()相似,但会返回对象得所有属性,包括了不可枚举属性

JS 的array的函数中,有哪些是直接修改数组本体?

  • 改变原数组的方法: pop(),push(), shift(),unshift(),sort(),reverse(),splice()
  • 不改变原数组的方法: slice(),concat(),map(),forEach()

赋值/浅拷贝/深拷贝的区别?如何实现一个深拷贝?

  • 赋值是拷贝的对象指针,整个对象都是共用的。
  • 浅拷贝是拷贝一层,对象的内容仍是共用的,Object.assign(),拓展运算符都是浅拷贝。
  • 深拷贝是递归拷贝深层次,JSON.stringify()是深拷贝,但是会忽略undefined、symbol和函数。
// 一个简单的深拷贝function clone(target) {    if (typeof target === 'object') {        let cloneTarget = Array.isArray(target) ? [] : {}; // 考虑数组        for (const key in target) {            cloneTarget[key] = clone(target[key]);        }        return cloneTarget;    } else {        return target;    }};

知道JS的set和map的用法吗?

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set();[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));for (let i of s) {    console.log(i);}// 2 3 5 4// 去除数组的重复成员[...new Set(array)]// Array.from方法可以将 Set 结构转为数组。const array = Array.from(s);//去除字符串里面的重复字符。[...new Set('ababbc')].join('')// "abc"

另外,Set还有size()delete(v)has(v)clear()等方法。

Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。 如果你需要“键值对”的数据结构,Map 比 Object 更合适。

 const m = new Map(); const o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.has(o) // true m.delete(o) // true m.has(o) // false

NodeJS能用ES6模块吗?CommonJS 和 ES6模块的区别是什么?

JS能写前端web,也能写NodeJS。

  • Node.js 后端应用由模块组成,其模块系统采用 CommonJS 规范,它并不是 JavaScript 语言规范的正式组成部分。
  • 前端的模块系统则采用ES6模块规范,这是 JavaScript 语言规范的正式组成部分。

但是现在技术进步了,后端也能用ES6模块规范(NodeJS支持),前端也能用Common JS规范(Webpack支持)。

  • Node.js 默认用CommonJS规范,但也可以用ES6模块规范,但要求 ES6 模块采用.mjs后缀文件名。 也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。 Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。 如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。 一旦设置了以后,该项目里面的.js文件,就被解释用 ES6 模块。 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。
  • 在使用webpack等打包工具的前端项目中,默认用ES6规范,但也可以使用CommonJS,通过在项目的package.json文件中,指定type字段为commonjs,具体细节与NodeJS后端略有差异。

无论在前端还是在后端,import/export和require/module.exports也是可以在一个项目中同时使用,甚至相互混用,这样做需要一些配置,但是建议尽量不要混用。

一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易:

  • 如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。
  • 如果原始模块是 CommonJS 格式,那么可以加一个ES6模块包装层(import该CommonJS 模块,然后再export出去)。
// CommonJS模块的ES6模块包装层import cjsModule from '../index.js'; // index.js是CommonJS规范的export const foo = cjsModule.foo; 

ES6模块和Commonjs模块的相同点就是,二者对于同一模块多次加载都只会执行一次模块内代码,即首次加载执行,后面加载模块不执行其内部代码。

ES6模块 和 CommonJS的区别在于用法,加载时机和方式不同:

  • CommonJS 模块使用require()加载和module.exports输出,require()是代码运行阶段同步加载JS文件的(运行时加载),后面的代码必须等待这个命令执行完(只加载JS文件,不执行JS文件内容)才会执行。
  • ES6 模块使用import加载和export输出,为了不影响dom渲染异步加载JS文件,在JS代码静态解析阶段分析依赖关系,在代码运行前分析出export和import对应符号引用(同样只加载JS文件,不执行JS文件内容)。

JS代码在被JS引擎加载后,分为两个阶段:1、静态分析阶段(我们常说的编译阶段)2、运行阶段。静态分析阶段的主要工作是解析源码生成字节码。 ES6模块就是在静态分析阶段实现export和import的分析解析。

静态分析 & 静态作用域:如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。 它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量,这是固定的,这是非常有利于编译器做优化的。 因此export命令后面只能跟着声明式语句,而不能跟表达式(如变量名,字面量)。因为变量只有在声明时,才会产生一个变量引用的符号。 另外,ES6模块的export 中 不是一个对象简写形式,更不是一个对象,而是export 语法组成部分,用作收集符号用。 另外,CommonJS的module.exports不是模块内部的变量,而是外部传入模块的变量,所以一旦模块内部代码对于exports变量做了修改,其实就是对于外部该变量做了修改, 因此模块代码未执行完,模块的输出module.exports也是有值的,因为这是外部值。

  • ES6模块是在静态解析阶段分析import/export的输入输出的常/变量或函数,将其解析为一个“符号引用”(既不是一个对象,也不是一个变量,只是一种静态定义,一个简单的引用符号), 外部可以通过符号引用直接获取到对应模块中输出变量的实时数据。由于ES6输入的模块变量只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。
  • CommonJS则在运行阶段加载模块输出对象,由于只作用于运行时导致完全没办法在编译时做“静态优化”,挂载在该对象上数据都是拷贝数据(浅拷贝),和原模块中的输出变量没有关系了。 外部获取module.exports,其实获取的是缓存数据(值都是初始化后的初始值),而不是原模块内的实时数据。如果访问原模块内的实时数据,通过函数返回内部值仍然是可以的。

建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。

但人们往往说ES6模块是异步的,为什么呢?因为Common JS肯定是同步的,由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。 但ES6模块最初用于web,而传统的浏览器引入JS文件的方式就是使用script标签导入。 但是为了让script标签能够区分 模块JS文件 和 非模块JS文件,所以给script标签加入了 type = "module" 属性,来告诉浏览器引入的是ES6模块JS文件。 然后其他js文件内部就可以使用import xxx来引入该ES6模块JS文件里的内容了,就这样实现了模块化。 而设置了 type="module"的script标签,相当于带了 defer属性,即异步加载JS文件,不阻塞DOM构建,且会等DOM构建完成后,才执行JS模块代码(script标签默认是同步加载的)。

AMD/CMD 规范,以及ES6的模块?

AMD/CMD都是浏览器的 CMD 推崇依赖就近(用的时候再声明引用的依赖),AMD 推崇依赖前置(先声明引用的依赖)。

  • AMD规范,即异步模块加载机制。require/define,核心是预加载,先对依赖的全部文件进行加载,加载完了再进行处理。解决的是JS加载引起页面卡顿的问题。
  • CMD规范,是一个同步模块定义,require/define,按不同的先后依赖关系对 JavaScript 等文件的进行加载工作,确保各个JS文件的先后加载顺序,确保避免了以前因某些原因某个文件加载慢而导致其它加载快的文件需要依赖其某些功能而出现某函数或某变量找不到的问题。
  • CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。 模块加载的顺序,按照其在代码中出现的顺序。

JS的隐式转换?

在使用不同类型的值进行操作时,JavaScript会自动进行类型转换,这称为隐式转换。

  • 我们在对各种非Number类型运用数学运算符(- * /)时,会先将非Number类型转换为Number类型。

  • 我们在对各种非Number类型运用数学运算符(+)时: (以下 3 点,优先级从高到低)

    • 当一侧为String类型,会将另一侧转换为字符串类型并进行字符串拼接。
    • 当一侧为Number类型,另一侧为非字符串的其他原始类型,则将原始类型转换为Number类型。
    • 当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接。
  • 当我们使用逻辑语句(例如if while for)时,条件值将转为Boolean值,只有 null undefined '' NaN 0 false 这几个是 false,其他的情况都是 true,比如 , []。。

  • 当我们使用

    ==

    时:

    • NaN和其他任何类型比较永远返回false(包括和他自己)。
    • Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型。
    • String和Number比较,先将String转换为Number类型。
    • null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false。
    • 原始类型和引用类型做比较时,引用类型会依照ToPrimitive规则转换为原始类型。

null是原始类型,但为什么typeof null的结果是object?如何准确检测一个值是null类型而不是其他类型隐式转换而来?

造成这个结果的原因是null的内存地址是以000开头,而js会将000开头的内存地址视为object。 通过isNull()来判断一个值是不是null类型,但值得注意的是isNaN()会进行隐式转换。 typeof 无法精确的检测null、Object、Array。获取精确类型的话,可以自己写一个:

const getType = (value: any) => {  const str: string = Object.prototype.toString.call(value)  const typeStrArray = str.substring(1, str.length - 1).split(' ')  return typeStrArray[1].toLowerCase()}

会babel插件开发吗?

Babel编译是一个code to code的过程,整个流程分为三步:

  • 解析(parse):将源码解析成抽象语法树(AST)
  • 转换(transform):遍历AST,并使用babel api对AST节点进行增删改,babel 维护一个称作 Visitor 的对象,这个对象定义了用于 AST 中获取具体节点的方法,如果当前节点的类型匹配 visitor 中的类型,就会执行对应的方法, 遍历 AST 节点的时候会遍历两次(进入和退出),这样命中目标节点并进行逻辑处理。
  • 生成(generate):将转换后的 AST 转换成代码,同时可以创建Source Map映射。 编写一个文件,里面一个函数,返回值是一个指定类型的对象,visitor就是其中最主要的对象,其他都是一些配置。 在babel.config.json里的plugins,加上这个文件,就能进行转换了。

AST可视化平台:https://astexplorer.net/(opens in a new tab)

为什么在JS中0.1+0.2!=0.3?以及IEE 754标准

JavaScript使用Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准 通过64位来表示一个数字。

首先,计算机无法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。这样运算就分成了两个部分: 先按照IEEE 754转成相应的二进制,然后按照二进制运算。

回到0.1+0.2的例子上,首先转成二进制后,二进制数字是无限循环的,但是由于IEEE 754尾数位数限制, 需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失,两步的精度损失最后的结果转换成十进制之后就是0.30000000000000004。

只要是遵循遵循 IEEE 754 标准的语言都会有这个问题。

JS的垃圾回收机制?

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。

标记清除的缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题(不可达孤岛)。

垃圾回收是一个规模庞大的工作,尤其在代码量非常大的时候,频繁执行垃圾回收算法会明显拖累程序的执行。JavaScript算法在垃圾回收上做了很多优化,从而在保证回收工作正常执行的前提下,保证程序能够高效的执行。

  • 分代回收 JavaScript把局部/全局对象分开管理,对于快速创建、使用并丢弃的局部变量,垃圾回收器会频繁的扫描,保证这些变量在失去作用后迅速被清理。 而对于哪些长久把持内存的变量,降低检查它们的频率,从而节约一定的开销。
  • 增量收集 增量式的思想在性能优化上非常常见,同样可以用于垃圾回收。 在变量数目非常大时,一次性遍历所有变量并颁发优秀员工标记显然非常耗时,导致程序在执行过程中存在卡顿。 所以,引擎会把垃圾回收工作分成多个子任务,并在程序执行的过程中逐步执行每个小任务,这样就会造成一定的回收延迟,但通常不会造成明显的程序卡顿。
  • 空闲收集 CPU即使是在复杂的程序中也不是一直都有工作的,这主要是因为CPU工作的速度非常快,外围IO往往慢上几个数量级, 所以在CPU空闲的时候安排垃圾回收策略是一种非常有效的性能优化手段,而且基本不会对程序本身造成不良影响。 这种策略就类似于系统的空闲时间升级一样,用户根本察觉不到后台的执行。

如何实现new?

//Fun为构造函数, args表示传参function myNew(Fun, ...args) {    let obj = {};    obj.__proto__ = Fun.prototype;    let res = Fun.apply(obj, args); // 最关键的一步    return (res instanceof Object ? res : obj);} let obj = myNew(One, "XiaoMing", "18");console.log("newObj:", obj);

如何实现call?

// call实现,const args = [...arguments].slice(1)Function.prototype.myCall = function (context, ...args) {    const s = Symbol()    context[s] = this // 该this指向的是调用的函数,函数也是对象    const res = context[s](...args) // 此时该函数内的this指向的就是context了    delete context[s]    return res}

如何实现promise/async?

  • 通过构造函数生成一个promise对象,该构造函数有一个延时函数参数
  • 通过promise.then()或promise.catch()方法实现结果获取
  • then函数和catch函数可以链式调用
function MyPromise(func) {    this.status = 'pending';    this.res = '';    this.thenCbs = [];    this.catchCbs = [];    const resolve = (data) => {        this.status = 'fulfilled';        this.res = data;        this.thenCbs.forEach(cb => {            cb(this.res);        });    }    const reject = (data) => {        this.status = 'rejected';        this.res = data;        this.catchCbs.forEach(cb => {            cb(this.res);        });    }    this.then = function (cb) {        if (this.status == 'pending') {            this.thenCbs.push(cb);        }        if (this.status == 'fulfilled') {            var res = cb(this.res)        }        return this;    }    this.catch = function (cb) {        if (this.status == 'pending') {            this.catchCbs.push(cb)        }        if (this.status == 'rejected') {            var res = cb(this.res)        }        return this;    }    func(resolve, reject)}

如何实现async/await?

  • 理解async函数需要先理解Generator函数,因为async函数是Generator函数的语法糖。
function co(gen) {  return new Promise((resolve, reject) => {    const g = gen();    function next(param) {      const { done, value } = g.next(param);      if (!done) {        // 未完成 继续递归        Promise.resolve(value).then((res) => next(res));      } else {        // 完成直接重置 Promise 状态        resolve(value);      }    }    next();  });}function* readFile() {  const value = yield promise1();  const result = yield promise2(value);  return result;}co(readFile).then((res) => console.log(res));

CSS

页面导入样式时,使用link和@import有什么区别?

  • link是xhtml标签,除了加载css外,还可以定义RSS等其他事务,无兼容问题;@import属于CSS范畴,只能加载CSS,是在css2.1(2004年)提出来的,低版本的浏览器不支持
  • link引用CSS时候,页面载入时同时加载;@import需要在页面完全加载以后加载,而且@import被引用的CSS会等到引用它的CSS文件被加载完才加载
  • link支持使用javascript控制去改变样式,而@import不支持
  • link方式的样式的权重高于@import的权重

水平居中的方法,垂直居中的方法?

  • 内联元素居中
    • 通过text-align: center;实现水平居中,通过line-height = height实现垂直居中。
  • 块级元素居中
    • 通过flex的justify-contentalign-item实现水平/垂直居中。
    • 设置margin: auto实现水平/垂直居中。margin:0 atuo;代表的意思是“水平居中”。css margin属性设置对象外边距,如果值只有两个参数的话,第一个表示上下边距,第二个表示左右编辑;因为0 auto,表示上下边界为0,左右则根据宽度自适应相同值(即水平居中)。
    • 设置position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);实现水平/垂直居中。
    • 设置position: absolute; top: calc(50% - height/2px); left: (50% - width/2px);实现水平/垂直居中,但需要知道元素宽高

使元素消失的方法有哪些?

  1. display:none;把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素删除掉(但其实dom是存在的)。当display设置为none,任何对该元素直接的用户交互操作都不可能生效。
  2. visibility:hidden;该元素隐藏起来了,不会改变页面布局,仍占据原有空间,但不会触发该元素已经绑定的事件。
  3. opacity:0;该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定一些事件,也会触发其事件。
  4. position:absolute;top:-1000px;left:-1000px;这个方法是通过将left和top的值设的很大,让元素定位到浏览器外面。不占据空间,不能点击。
  5. transform: scale(0,0)通过缩放达到元素消失的视觉效果,元素仍占据空间。

文字换行有什么办法?

  • 使用

    word-wrap

    属性,可以用来控制文本的换行方式。该属性有以下几种取值:

    • normal: 只在合适的时候换行,不会在单词内部换行,如果一个单词长度超出了容器长度,单词将超出边界;
    • break-word: 比容器长的单词将在单词内部换行;
  • word-break属性关注单词内的换行方式,一般在设置

    word-wrap: break-word

    时使用,它有以下几种取值:

    • normal: 默认属性值,表示文本受限,不允许在单词中间截断。
    • break-all: 表示全部单词内换行,即便这个单词并没有长得超出了边界。处理中文则不会生效。
    • keep-all: 指CJK(中/日、韩)文本超出区域不断行,但会在标点出折行。
  • white-space是用来控制文本中空格和换行的属性。它有以下几种取值:

    • normal:默认属性值,表示文本中多个空格和换行都会被合并为一个空格;
    • nowrap:表示文本不会被自动换行;
    • pre:表示文本中多个空格和换行都会被保留;
    • pre-wrap:表示文本中多个空格和换行会被保留,但是会自动换行,不会出现水平滚动条。
    • pre-line:表示文本中多个空格会被合并成一个空格,但是换行会被保留,会自动换行,不会出现水平滚动条。

如果文字不换行,可以用text-overflow:ellipsis;显示省略符号来代表被修剪的文本。

盒模型是什么?

页面中的所有元素都可以看成一个盒子,由4个属性组成的(content(内容),padding(内边距),margin(外边距),border(边框)), 有标准盒模型和ie盒模型,在标准盒模型中,元素的宽度和高度等于内容区域的宽度和高度, 在ie盒模型中,元素的宽度和高度定义为内容区域的宽度和高度加上内边距和边框的宽度和高度。 用box-sizing属性来控制元素使用哪种盒模型。box-sizing属性有两个值:content-box和border-box。content-box是标准盒模型,border-box是IE盒模型。默认值是content-box。

box-sizing: content-box

box-sizing: border-box

讲一讲css3新特点?

2001年5月23日W3C完成了CSS3的工作草案(但至今没有定稿),主要包括盒子模型、列表模块、超链接方式、语言模块、背景和边框、文字特效、多栏布局等模块。

  • 盒模型
  • 新增文字属性
    • word-wrap: normal|break-word 使用浏览器的默认换行或者允许单词内换行
    • text-overflow: clip|ellipsis 设置当前行超过指定容器的边界时如何显示,修剪文本或者显示省略符号来代表被修剪的文本
  • 新增颜色透明度/渐变
  • 新增背景属性background-sizebackground-originbackground-breakbackground-clip
  • 新增边框属性border-radiusborder-imagebox-shadow
  • transform 转换
  • transition 过渡,多个属性之间用逗号进行分隔,必须规定过渡效果,持续时间两个内容,语法为:transition: CSS属性, 花费时间, 效果曲线(默认ease), 延迟时间(默认0)
  • animation 动画
  • 新增弹性布局,网格布局,多列布局
  • 媒体查询
  • 添加了伪类/伪元素/多重/属性选择器,例如:hover::beforeele1, ele2[attr=value]

css的渲染优先级?

  • 选择器都有一个权值,权值越大越优先
    • 内联样式表的权值最高 1000
    • ID 选择器的权值为 100
    • Class 类选择器的权值为 10
    • HTML 标签选择器的权值为 1
  • 当权值相等时,后出现的样式表设置要优于先出现的样式表设置
  • 网页编写者设置的CSS 样式会覆盖浏览器所设置的样式,后来指定的CSS 样式会覆盖继承的CSS 样式,后面指定的CSS样式会覆盖之前指定的CSS样式,后定义class会覆盖先定义的class
  • 在同一组属性设置中标有“!important”规则的优先级最大

clientHeight,scrollHeight,offsetHeight ,以及scrollTop, offsetTop,clientTop的区别?

BFC是什么?

BFC 即 Block Formatting Contexts (块级格式化上下文),具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。

BFC 特性及应用

  • 避免外边距重叠,但属于同一个BFC的两个相邻Box的外边距仍会发生重叠。但若两个相邻元素在不同的BFC中,就能避免外边距折叠。
  • 可以包含浮动的元素,其高度能被其内部的浮动元素撑开,因此可以清除浮动。

只要元素满足下面任一条件即可触发 BFC 特性:

  • html根标签
  • float: left|right
  • overflow: hidden|auto|scroll,就是不能为visible
  • display: table-cell|table-caption|inline-block|inline-flex|flex
  • position: absolute|fixed

了解float浮动布局吗?

float 属性早期用于图文混排, 实现文字环绕的效果。

元素一旦浮动后(即float: left),脱离文档流,朝着向左或向右方向移动,不占据原来的位置,但会影响其他块级元素内部文本。 当父元素一行内显示不下所有浮动元素时,最后显示不下会换行。被挤落下来的元素,会躲开被上一层元素占据的最大高度。

div1

内部文本被影响,这种特性常常被用于实现网页中的多列布局、图像与文本的并排显示等效果。div2(f)

div3(f)

span3.1

div4

div5

div6(f)

清除浮动的方法?

在文档流中,父元素的高度默认是由子元素撑开的。父元素如果不设置高度,当父元素内所有的子元素都设置了浮动,子元素脱离了文档流, 没有了子元素来撑起父元素的高度,父元素会认为内部没有内容而将高度height认为0,引起高度坍塌。高度坍塌会导致父元素下的所有元素上移,引起页面布局混乱。

浮动元素会脱离正常的文档流,可能导致父元素的高度塌陷,然后会影响下一个元素的文本(本段预期应在下一行展示),清除浮动就是这个问题。

  • 在浮动元素后面添加一个元素,并为其添加clear: both;样式。这个元素可以是实际的DOM元素,也可以是一个看不见的元素。缺点是它需要在你的HTML中添加额外的元素。
  • 使用::after或::before伪元素并为其添加clear: both;样式来清除浮动。这种方法的优点是不需要在HTML中添加额外的元素,但缺点是它可能会影响到其他样式(如背景和边框的渲染)。
  • 转成BFC。

外边距合并是什么意思?

外边距合并是CSS中一个特殊的概念。当两个外边距相邻时,它们会合并为较大的那一个。 无论是相邻元素的上下边距,还是父子元素的各自上边距,甚至是同一元素的上下边距(当元素高度为空,上下边距就会接触到一起)。

外边距合并是有意义的,主要体现在文本页面,一般来说,每一文本段都有相同的外边距,这意味着段落之间的空间是页面顶部的两倍。 如果发生外边距合并,页面顶/底部的空间将转为容器的margin,段落之间的上外边距和下外边距就合并在一起,这样各处的距离就一致了。

因此,合并发生在普通文档流的块级元素的垂直外边距上,并且只有两个外边距直接相邻时才会合并。阻止外边距合并的方法有:

  • 使用内边距或透明的border边框代替外边距
  • 使用flex布局
  • 使用绝对布局
  • 触发BCF(但两个相邻的块级盒子的垂直外边距会触发重叠)

width=device-width以及1rem、1em的含义?

设备像素(device pixels)是指与硬件设备直接相关的像素,是真实的屏幕设备中的像素点。比如说,一个电脑显示器的参数中,最佳分辨率是1920x1080,那么指的就是这个显示器在屏幕上用于显示的实际像素点,也就是设备像素。

另一个概念是css像素(css pixels)。css像素是指网页布局和样式定义所使用的像素,也就是说,css代码中的px,对应的就是css像素。 那么,css像素和设备像素有什么区别呢?简单地说,css像素比设备像素要更“虚拟”一些。下面来解释这一点。

css像素和设备像素之间是一种可变的转化关系。在100%缩放比例下,1个css像素等于1个设备像素。 在表示某一数目的css像素时,在放大状态下使用了更多的设备像素,而在缩小状态下使用了更少的设备像素。这就是css像素和设备像素的概念。

视口(viewport),指的是浏览器窗口中用来显示网页的区域。以浏览器来说,就是浏览器的窗口内容区域(除去标题栏,菜单栏,地址栏,状态栏等等浏览器的“周边”的东西)。 视口表现得像是之上的一个块元素,它限制并确定的宽度,但却不属于html结构,不能被设置样式。 而且,任何时候,视口的尺寸都会随着浏览器窗口的大小变化而变化。桌面电脑中的视口,就是这样的一个概念。

相比桌面电脑,在手机上浏览网页,最大的差异在于屏幕尺寸。电脑端的页面到手机上,很容易出现水土不服(一些流体布局的网页会在过窄的视口中变得一团乱), 为了让用户在手机上也获得最佳的网页浏览体验,应该让视口更宽,超越屏幕的宽度。所以,在手机浏览器中,视口被划分为了两个:可见视口(visual viewport)和布局视口(layout viewport)。

  • 可见视口是指当前在手机屏幕上显示的部分。当你做缩放的时候,可见视口的尺寸(css像素值)也会变化。
  • 和可见视口不同,布局视口则是整个页面的窗口,用于元素布局和尺寸计算(比如百分比的宽度值),而且比可见视口明显要更宽。无论你缩放,或者滑动页面,甚至翻转手机屏幕,布局视口始终不变。 布局视口的宽度是由手机浏览器定义的,随浏览器不同而不同。比如Safari是980px,Android Webkit是800px。这都远比屏幕宽度值要大。

手机中的布局视口是可以更改的。你一定在很多移动版网页中见到过下边这个标签元素。

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

这其中有一句width=device-width,它的意思是,把手机浏览器的布局视口的宽度,更改为当前设备的宽度,也就是等于可见视口。

  • em,是一个相对单位。相对于当前对象内文本的font-size,如果当前文本的字体尺寸没有设置,则相对于浏览器的默认字体尺寸。即1em=16px。
  • rem,是一个相对单位。是相对HTML根元素,通过rem既可以做到只修改根元素就可以成比例的调整所有字体。

无样式内容闪烁(FOUC)Flash of Unstyle Content 如何解决?

@import导入CSS文件会等到文档加载完后再加载CSS样式表。因此,在页面DOM加载完成到CSS导入完成之间会有一段时间页面上的内容是没有样式的。

解决方法:使用link标签加载CSS样式文件。因为link是顺序加载的,这样页面会等到CSS下载完之后再下载HTML文件,这样先布局好,就不会出现FOUC问题。

说说CSS渐变?

CSS3 Gradient 分为 linear-gradient(线性渐变)和 radial-gradient(径向渐变)。

参数:其共有三个参数,第一个参数表示线性渐变的方向,top 是从上到下、left 是从左到右,如果定义成 left top,那就是从左上角到右下角。第二个和第三个参数分别是起点颜色和终点颜色。你还可以在它们之间插入更多的参数,表示多种颜色的渐变。

react

useState塞入默认值是从props内拿到的时候,会发生什么?

state不会随着props变化,useState只执行一次,因此用useMemo比较好 为什么虚拟 dom 会提高性能 虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要 的 dom 操作,从而提高性能, 具体实现步骤如下:

  • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树, 插到文档当中;
  • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记 录两棵树差异;
  • 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

什么是受控组件和非受控组件?

  • 受控组件:只能通过 React 修改数据或状态的组件,就是受控组件;
  • 非受控组件:与受控组件相反,如 input, textarea, select, checkbox 等组件,本身控件自己就能控制数据和状态的变更,而且 React 是不知道这些变更的;
  • 那么如何将非受控组件改为受控组件呢?那就是把上面的这些纯 html 组件数据或状态的变更,交给 React 来操作

useState 的传参方式,有什么区别?

useState()的传参有两种方式:纯数据和回调函数。回调函数方式能在参数中获得最新state,多用于异步的情况。

为什么在本地开发时,组件会渲染两次?

在 React.StrictMode 模式下,如果用了 useState,usesMemo,useReducer 之类的 Hook,React 会故意渲染两次,为的就是将一些不容易发现的错误容易暴露出来,同时 React.StrictMode 在正式环境中不会重复渲染。 也就是在测试环境的严格模式下,才会渲染两次。

useEffect()的清除机制是什么?在什么时候执行?

useEffect(callback)的回调函数里,若有返回的函数,这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。

React 会在组件卸载的时候执行清除操作。若组件产生了更新,会先执行上一个的清除函数,然后再运行下一个 effect。

了解React的虚拟dom吗?

Virtual DOM 是以对象的方式来描述真实 dom 对象的,在做一些 update 的时候,可以在内存中进行数据比对, 减少对真实 dom 的操作减少浏览器重排重绘的次数,提高程序的性能,虚拟 dom 增加了一层内存运算,然后才操作真实 dom,将数据渲染到页面上。渲染上肯定会慢上一些。

什么是PureComponent?

React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent通过prop和state的浅对比来实现更新

Javascript 的 Pure Functions 是什么?不依赖函数范围以外的变量而独自内部生存的函数。 可以自己决定自己的结果,自力更生,如果输入值不变则一定会一直得出相同的结果。且它的输出结果一定要基于自己的输入参数。 由于每次如果输入值不变一定会结果相同,重复运行函数时,Chrome V8 Engine会返回cached的结果,而不是重新运行一遍函数,大大提高了整个项目的运行速度。 一个不返回任何结果的函数也不是一个pure function。

跳过类式组件不必要的重新渲染,当父组件重新渲染时,React 通常会重新渲染子组件。为了优化性能,你可以创建一个组件,在父组件重新渲染时不会重新渲染, 前提是新的 props 和 state 与旧的 props 和 state 相同。类式组件 可以通过继承 PureComponent 来选择此行为。

在函数组件中,通过memo来实现相同的效果。

react生命周期?

什么是合成事件(事件代理),与原生事件有什么区别?

React 中所有触发的事件,都是自己在其内部封装了一套事件机制(基于Virtual DOM实现了一个SyntheticEvent层(合成事件层))。目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。 React 合成事件采用的是事件冒泡机制,当在某具体元素上触发事件时,等冒泡到顶部被挂载事件的那个元素时,才会真正地执行事件。

在React底层,主要对合成事件做了两件事:

  • 事件委派: React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了 一个映射来保存所有组件内部事件监听和处理函数。
  • 自动绑定: React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。
  • react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

key 的作用是什么?

当出现大量相同的标签时,使用key可以帮助diff算法提升判断的速度,在页面重新渲染时更快消耗更少,因此你应当给数组中的每一个元素赋予一个确定的标识。

多次执行 setState(),会触发多次更新吗?

在 React18 中,无论是多个 useState()的 hook,还是操作(dispatch)多次的数据。只要他们在同一优先级,React 就会将他们合并到一起操作,最后再更新数据。 这是基于 React18 的批处理机制。React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setstate 事件合并);在 v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise 链、setTimeout 等异步代码以及原生事件处理函数;

Redux 中异步的请求怎么处理

用中间件,但是只不过就是从请求回调里调用dispatch变成dispatch的参数设置成请求函数,换个写法可能更易读,但代码量一点也没少

React.forwardRef是什么?它有什么作用?

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

  • 父组件调用子组件的方法
  • 把子组件的refs转发上去

如何实现组件的懒加载?

从 16.6.0 开始,React 提供了 lazy 和 Suspense 来实现懒加载。

import React, { lazy, Suspense } from 'react';const OtherComponent = lazy(() => import('./OtherComponent')); function MyComponent() {  return (    <Suspense fallback={<div>Loading...</div>}>      <OtherComponent />    </Suspense>  );}

属性fallback表示在加载组件前,渲染的内容。

高阶组件(HOC)?

高阶组件通过包裹(wrapped)被传入的 React 组件,经过一系列处理,最终返回一个相对增强(enhanced)的 React 组件,供其他组件调用。

React 的 diff 过程

React 只对比当前层级的节点,不跨层级进行比较; 根据不同的节点类型,如函数组件节点、类组件节点、普通 fiber 节点、数组节点等,进入不同的处理函数; 前后两个 fiber 节点进行对比,若 type 不一样,直接舍弃掉旧的 fiber 节点,创建新的 fiber 节点;若 key 不一样,则需要根据情况判断,若是单个元素,则直接舍弃掉,创建新的 fiber 节点;若是数字型的元素,则查找是否移动了位置,若没找到,则创建新的节点;若 key 和 type 都一样,则接着往下递归; 若是单个 fiber 节点,则直接返回;若是并列多个元素的 fiber 节点,这里会形成单向链表,然后返回头指针(该链表最前面的那个 fiber 节点); 通过上面的 diff 对比过程,我们也可以看到,当组件产生比较大的变更时,React 需要做更多的动作,来构建出新的 fiber 树,因此我们在开发过程中,若从性能优化的角度考虑,尤其要注意的是: 节点不要产生大量的越级操作:因为 React 是只进行同层节点的对比,若同一个位置的子节点产生了比较大的变动,则只会舍弃掉之前的 fiber 节点,从而执行创建新 fiber 节点的操作;React 并不会把之前的 fiber 节点移动到另一个位置;相应的,之前的 jsx 节点移动到另一个位置后,在进行前后对比后,同样会执行更多的创建操作; 不修改节点的 key 和 type 类型,如使用随机数做为列表的 key,或从 div 标签改成 p 标签等操作,在 diff 对比过程中,都会直接舍弃掉之前的 fiber 节点及所有的子节点(即使子节点没有变动),然后重新创建出新的 fiber 节点;

redux原理?

diff算法如何比较?

diff算法就是更高效地通过对比新旧Virtual DOM来找出真正的Dom变化之处,从而最高效地更新dom

  • DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较,只有删除、创建操作,没有移动操作
  • 如果是同一个类的组件,则会继续往下diff运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
  • 对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识,通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置

能移动则移动,不能移动的再新增删除

React和Vue的diff算法的异同?

共同点:

  • vue和diff算法,都是不进行跨层级比较,只做同级比较 不同点:
  • vue对比节点时,当节点元素类型相同,类名不同时,认为是不同的元素,删除重新创建,而react认为是同类型的节点,进行修改操作。
  • vue的列表比对,采用从两端到中间的比对方式,而react则采用从左到右依次比对的方式。 例如当一个集合,只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到第一个。 总体上,vue的对比方式更高效。 这样的操作来看,Vue的diff性能是高于react的

什么是React Portal?有哪些使用场景?

ReactDOM.createPortal(child, container)

Portals 提供了一种脱离 #app 的组件。特别是 position: absolute 与 position: fixed 的组件。比如模态框,通知,警告,goTop 等。

cloneElement 与 createElement 各是什么,有什么区别?

  • cloneElement,根据 Element 生成新的 Element,新添加的属性会并入原有的属性 一般配合 React.children.map使用,如用于动态地给子组件添加更多 props 信息、样式
  • createElement,根据 Type 生成新的 Element, 第一个参数是 type 简单来说就是各种标签名字,第二个参数是传入的属性,第三个参数以及之后的参数就是作为组件的子组件。

React16的fiber架构?

默认情况下,JavaScript 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系,如果 JavaScript 运算持续占用主线程,页面就没法得到及时的更新, 阻塞了浏览器的 UI 渲染,Fiber 架构将更新任务分解成小的、可中断的单元,分批完成,也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染,及时地响应用户的交互。

React18 有哪些新变化?

react18以于2022年3月29日发布正式版本。

  • Concurrent Mode(以下简称 CM)翻译叫并发渲染机制:在以前,React 在状态变更后,会开始准备虚拟 DOM,然后渲染真实 DOM,整个流程是串行的。一旦开始触发更新,只能等流程完全结束,期间是无法中断的。 在 CM 模式下,React 在执行过程中,每执行一个 Fiber,都会看看有没有更高优先级的更新,如果有,则当前低优先级的的更新会被暂停,待高优先级任务执行完之后,再继续执行或重新执行。
  • startTransition:主动降低优先级。本质上是用于一些不是很急迫的更新上,用来进行并发控制,在v18之前,所有的更新任务都被视为急迫的任务,而CM模式能将渲染中断,可以让高优先级的任务先更新渲染。 比如「搜索引擎的关键词联想」,用户在输入框中的输入希望是实时的,而联想词汇则不是很在意。我们可以用 startTransition 来降低联想词汇更新的优先级。
  • 入口优化:现在是要先通过createRoot()创建一个 root 节点,然后该 root 节点来调用render()方法;后面再想重新渲染整个应用的时候,通过root节点进行render,就不用重新渲染这个根结点了。
  • 全部自动批处理优化:批处理是 React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(例如将多次 setstate 事件合并); 在v17的批处理只会在事件处理函数中实现,在 v18 中所有更新都将自动批处理,包括 promise 链、setTimeout 等异步代码。
  • useId:主要用于 SSR 服务端渲染的场景,方便在服务端渲染和客户端渲染时,产生唯一的 id;

React的并发问题是如何处理的?

React 中的并发,是JS的逻辑代码会跟 UI 渲染竞争主线程。若一个很耗时的任务占据了线程,那么后续的执行内容都会被阻塞。 为了避免这种情况,React 就利用 fiber 结构和时间切片的机制,将一个大任务分解成多个小任务,然后按照任务的优先级和线程的占用情况,对任务进行调度。

  • 对于每个更新,为其分配一个优先级 lane,用于区分其紧急程度。
  • 通过 Fiber 结构将不紧急的更新拆分成多段更新,并通过宏任务的方式将其合理分配到浏览器的帧当中。这样就能使得紧急任务能够插入进来。
  • 高优先级的更新会打断低优先级的更新,等高优先级更新完成后,再开始低优先级更新。

React.Children.map 和 js 的 map 有什么区别?

JavaScript 中的 map 不会对为 null 或者 undefined 的数据进行处理,而 React.Children.map 中的 map 可以处理 React.Children 为 null 或者 undefined 的情况。

redux与vuex的区别?

  • redux使用的是不可变数据,每次都是用新的state替换旧的state,通过diff算法比较差异的;而Vuex是可变的,通过getter/setter直接修改。
  • 另外就是在api上有不同,vuex定义了state,getter,mutation,action;redux定义了state,reducer,action。

说一下Redux功能化编程的概念?

在redux中使用了功能化编程的概念。在参数中可以传递函数。使用了数据流控制, 递归调用, 函数和数组等等。帮助函数, 如reduce和map filter被大量使用。 允许函数的串联。状态只读。代码执行顺序的优先级没有必要考虑。

vue

vue是什么?有什么特点?

vue是一个创建单页应用的Web开源框架,基于数据驱动MVVM(Model-View-ViewModel),实时响应数据更新,并动态渲染DOM。

什么是SPA(单页面应用)?

它通过动态重写当前页面来与用户交互,避免了页面之间切换打断用户体验。

vue的生命周期?

Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后。

v-ifv-show的区别?

  • v-show隐藏则是为该元素添加display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除,因此有较高的切换消耗
  • v-if是真正的条件渲染,一开始为false就不会渲染,但v-show一定会渲染,因此v-show有较高的初始渲染消耗
  • v-if切换会触发生命周期的一些事件;v-show只是简单的基于css切换

在vue2中,为什么data属性是一个函数而不是一个对象?

在 Vue 2 中,组件的 data 选项必须是一个函数,而不是一个对象。这个函数返回一个新的数据对象,这样每个实例可以维护一份被返回对象的独立的拷贝。如果 data 直接是一个对象,那么由于 JavaScript 中对象是引用类型,所有的组件实例将共享同一个数据对象,当在一个组件实例中改变数据,其他所有的组件实例的数据也会被改变。

vue中,父子/兄弟/非关系组件之间消息的传递方式有哪些?

  • 父子关系组件,可以通过props传递参数,回调函数实现消息传递。
  • 可以通过vue自带的emit实现消息传递。
  • 可以通过第三方消息库实现消息传递。
  • 可以通过第三方数据管理库例如vuex。pina实现消息传递。

你知道vue中key的原理吗?说说你对它的理解

  • 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
  • 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed。

比如插入情况,就能只有一次插入操作,将效率发挥到极致。

如何插入原始HTML?

文本插值使用的是双大括号语法,双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:

<span v-html="rawHtml"></span>

注意: 在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。

如何响应式地绑定一个属性?

v-bind 指令:

<div v-bind:myId="dynamicId"></div>

如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。布尔型 Attribute,当其为其他假值时 attribute 将被忽略(但空字符串不会)。

因为 v-bind 非常常用,我们提供了特定的简写语法:

<div :myId="dynamicId"></div>3.4+有同名写法:<div :myId></div>

动态绑定多个值:

const objectOfAttrs = {  id: 'container',  class: 'wrapper'}<div v-bind="objectOfAttrs"></div>

如何绑定一个click事件?

指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind 和 v-html。 某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind 指令来响应式地更新一个 HTMLv-bind:href="url"

<a v-on:click="doSomething"> ... </a><!-- 简写 --><a @click="doSomething"> ... </a>

计算属性如何使用?

const publishedBooksMessage = computed(() => {  return author.books.length > 0 ? 'Yes' : 'No'})

Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books, 所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。

Vue常用的修饰符有哪些有什么应用场景?

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()。

  • 表单修饰符 lazy,trim,number
  • 事件修饰符 stop,prevent,self,
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

如何进行Class/Style绑定?

<div :class="{ active: isActive }"></div>上面的语法表示 active 是否存在取决于数据属性 isActive 的真假值。:class 指令也可以和一般的 class attribute 共存。<div :class="[activeClass, errorClass]"></div>绑定数组

对于只有一个根元素的组件,当你使用了 class attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。 如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。

<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div><div :style="[baseStyles, overridingStyles]"></div>

当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。

如何列表渲染?

<li v-for="(item, index|key, [index]) in items" :key="item.id">  {{ parentMessage }} - {{ index }} - {{ item.message }}</li>你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:<div v-for="item of items" :key="item.id"></div>注意此处 n 的初值是从 1 开始而非 0。<span v-for="n in 10">{{ n }}</span>

v-model指令是做什么用的?

用于表单数据的双向绑定,是:value和@input的结合体:

<input v-model="text">v-model 会忽略任何表单元素上初始的 value、checked 或 selected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。但antd好像是v-model:value<input v-model.trim="msg" />

对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME 输入还在拼字阶段时触发更新。 如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model。

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据

侦听器watch怎么用?

// 可以直接侦听一个 refwatch(question, async (newQuestion, oldQuestion) => {  ...})注意,你不能直接侦听响应式对象的属性值watch(() => obj.count, (count) => {}, { immediate: true })直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行

模版引用如何解决?

const input = ref(null)<input ref="input" />

如何父子组件传参?

const props = defineProps(['foo'])console.log(props.foo)

Vue 插件是什么?与组件有什么区别?

插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身,例如添加全局方法或者属性,添加 Vue 实例方法。

组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue。

vue2和vue3的双向绑定原理?

  • Object.definePropertyvue2中双向数据绑定的原理,用于定义对象属性,允许我们精确地控制属性的行为,包括读取、写入和删除等操作。
  • Proxy是vue3中双向数据绑定的原理,是ES6中一种用于创建代理对象的特殊对象,它允许我们拦截并自定义目标对象的操作,例如属性访问、赋值、函数调用等。

NextTick是什么?

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,将获取更新后的 DOM。

数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中, 如果我们一直修改相同数据,异步操作队列还会进行去重, 等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新,

说说你对slot的理解?slot使用场景有哪些?

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口。

说说你对keep-alive的理解是什么?

keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM, keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

vue的mixin是什么?

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods、created、computed等等, 我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来。

Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂。在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立, 这时,可以通过Vue的mixin功能将相同或者相似的代码提出来。

小程序

小程序的原理?

小程序的渲染时基于双线程模型的,在这个模型中小程序的逻辑层与渲染层分开在不同的线层运行。这样的好处是JS逻辑不会阻塞UI渲染, 但所有数据传递都是线程间的异步通信,有延迟、开销大。

小程序的初始化?

小程序初始化时,视图线程和服务线程同时初始化,服务线程初始化完毕后触发onLoad()onShow(),等待视图线程初始化完毕后传递给视图线程最初的状态数据供视图线程首次渲染状态。 渲染完毕后触发onReady(),完成所有的初始化。

小程序的加载和销毁?

小程序冷启动时如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。 小程序切后台5秒后挂起,5分钟后销毁,系统资源紧张会提前销毁。

设计模式

MVC和MVVM的区别?

  • MVC是包括view视图层、controller控制层、model数据层。各部分之间的通信都是单向的。 View传送指令到ControllerController完成业务逻辑后,要求Model改变状态,Model将新的数据发送到View,用户得到反馈。 比如说,用户在输入框里输入一个字符,触发controller,controller修改model数据,最后model数据传递给view层进行渲染。
  • MVVM是包括view视图层、viewModel视图模型层、model数据层。各部分之间的通信都是双向的。 View和Model并没有直接联系,而是通过ViewModel传递,Model和ViewModel之间的交互是双向的。 比如说,用户在输入框里输入一个字符,同时就会修改model数据,同理,如果model数据发生改变,view也会做出相应的改变,这就是双向绑定。

讲一讲创建型模式?

共五种:

  • 工厂方法模式:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建。 但将全部创建逻辑集中到了一个工厂类中,它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了,违反开闭原则(对扩展开放,对修改关闭); 产品类不断增多时候,对系统的维护和扩展也非常不利。
  • 抽象工厂模式:可能工厂也会越来越多,我们用一个抽象工厂来生成其他工厂,然后用其他工厂生产产品。 工厂类随着目标类数量一起爆炸增长,管理多个工厂是抽象工厂要做的事情。
  • 单例模式:单例模式可以分为两种:预加载(还没有使用该单例对象,该单例对象就已经被加载到内存了。会造成内存的浪费。但是比较快)和懒加载(用到该单例对象的时候再创建)。
  • 建造者模式(生成器模式):当一个产品太过复杂的时候,就需要一个建造者以构造该产品的各个部件并组装起来。优点是细节分离,降低代码耦合度,扩展性强。
  • 原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。用于创建重复的对象,同时又能保证性能。

讲一讲结构型模式?

共七种:

  • 适配器模式:作为两个不兼容的接口之间的桥梁。例如一个数据传入一个函数但数据结构不对,可以造一个函数适配,就是适配器。主要就是解决接口不兼容问题,同时提高复用性,和扩展能力。 但也会增加一些代码复杂度。
  • 装饰器模式:将有新功能的新对象包裹在原对象上,从而拓展原对象的新功能。从而在为对象添加新的行为的同时遵循开闭原则,也提高了灵活性和可维护性。缺点是增加了一定的复杂性。
  • 代理模式:用新对象包裹在原对象上,实现对原对象输入输出的监听与修改。可以起到保护目标对象的作用。但也会造成数据传递变慢,增加了系统复杂度。
  • 外观模式:门面模式,为多个子系统对外提供一个共同的接口,从而降低请求复杂度,提高可维护性,降低耦合度;但外观对象强耦合所有子系统,外观对象出错就会引起阻塞全局的单点故障。
  • 桥接模式:将一个维度的代码抽出来作为一个独立的工具类,而不参与到子类的继承与派生中,防止指数式增长的子类派生。这样的工具类属性引用就成为了不同维度之间的桥梁。 虽然说桥接模式的定义是将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。但这里的抽象与编程语言中的抽象类无关。 它们并不是一回事。 “抽象”更类似于调用api,一个封装的接口,一个抽出来的独立的工具类。这样提高了扩展性和灵活性。 但是识别独立变化的维度困难。正确识别系统中两个独立变化的维度并不总是容易的。
  • 组合模式:如果一个对象有若干个相互独立的部分组成,那么就将这几个部分的代码相互隔离开,组成“整体-部分“结构,解耦,简化了代码逻辑,提高可维护性。 问题在于这几个部分往往会藕断丝连,硬用组合模式会引起不必要的信息传输成本。比较适合的比如说文件系统,菜单等等。
  • 享元模式:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。 但为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。

讲一讲行为模式?

行为型模式,共十一种:

  • 策略模式:用于管理不同策略或算法,要将不同策略或算法放到不同类中,避免耦合,在运行时根据需要选择不同的算法。
  • 模板方法模式:用在一个功能的完成需要经过一系列步骤,这些步骤是固定的,但是中间某些步骤具体行为是待定的,在不同的场景中行为不同。 此时就可以考虑在这里插入一个模版函数作为占位符,不同的场景对应不同的子类实现。
  • 观察者模式:事件订阅,订阅者向发布者注册接口,发布者想发布东西,就挨个接口调用一遍,通知所有订阅者,松耦合,扩展性强,但性能可能有问题,更新顺序不确定。
  • 迭代器模式:提供一个统一的迭代接口,从第一个迭代到最后一个,从而不用管底层是什么数据结构以及遍历细节。符合单一职责,开闭原则,但是如果是太简单的遍历,直接for循环可能会更方便。
  • 责任链模式:将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理返回, 或将其传递给链上的下个处理者。适合多个权限检查步骤。或者说dom的冒泡也算。 符合单一职责,开闭原则,但部分请求由于提前返回导致接下来的处理者无法处理。
  • 命令模式:将GUI与业务逻辑分离,GUI捕获用户行为,并将该命令传递给业务逻辑层,GUI层没有业务逻辑处理。因此关键是所有命令最好统一接口。 符合单一职责,开闭原则,但由于需要实现命令发送与接收,代码可能会更复杂,也有传输效率问题。
  • 备忘录模式:也叫快照模式,例如文本编辑器的撤销于重做,我们不应该从外部复制一份编辑器的状态,而应该让编辑器自己提供复制函数让外部来调用。自行生成的快照放在备忘录对象中。 符合解耦,单一职责,开闭原则,但是备忘录会消耗大量内存。
  • 状态模式:在一个对象的内部状态变化时改变其行为, 例如React或vue都是这样的。
  • 访问者模式:将算法与其所作用的对象分离开来,让这些对象成为纯数据对象,操作可以独立变化。当访问者用不同算法访问这些纯数据对象,将产生不同的结果。 问题是每次修改数据对象,所有的访问方法都需要重新修改,另外算法被抽出放到访问者那里,可能没有权限了。
  • 中介者模式:不同GUI组件之间可能会有互动,甚至相互影响,多个GUI组件之间的交互应该都统一让父GUI作为中介处理,这就是中介者模式。 符合单一职责和开闭原则,但父GUI会承载很多不属于它的方法。
  • 解释器模式:一般就是解析编程语言用的,例如解析sql,正则,自定义语法的语句,然后将其解析为一个对象。 一般不要自制,因为解析器往往需要编译原理,引入第三方包调用就行了

算法

经典面试题

最大连续子序和

动态规划问题,r(n) = r(n-1)+a(n) > a(n) ? r(n-1)+a(n) : a(n)

// r[n] = r[n-1] > 0? a[n]+r[n-1] : a[n]var maxSubArray = function(nums) {    let res = nums[0];    nums.reduce((sum, num) => {        sum = num + Math.max(sum, 0);        res = Math.max(res, sum);        return sum;    })    return res;};

跳台阶:一个为n的台阶,小明可以一次走一步、两步或者五步,问一共几种走法?

分析:典型的动态规划问题,那么就是要找到递归公式是什么。

r[n]是第几个台阶,它有多少种走法,与r[n-1]的关系是,首先肯定+1,因为一次可以走一步;其次两步的时候就是r[n-2],五步的时候就是r[n-5],因此公式为:

r[n] = r[n-1] + r[n-2] + r[n-5]

任务调度器II

给你一个下标从 0 开始的正整数数组 tasks ,表示需要 按顺序 完成的任务,其中 tasks[i] 表示第 i 件任务的 类型 。

同时给你一个正整数 space ,表示一个任务完成 后 ,另一个 相同 类型任务完成前需要间隔的 最少 天数。

在所有任务完成前的每一天,你都必须进行以下两种操作中的一种:

  • 完成 tasks 中的下一个任务
  • 休息一天

请你返回完成所有任务所需的 最少 天数。

示例 1:输入:tasks = [1,2,1,2,3,1], space = 3输出:9解释:9 天完成所有任务的一种方法是:第 1 天:完成任务 0 。第 2 天:完成任务 1 。第 3 天:休息。第 4 天:休息。第 5 天:完成任务 2 。第 6 天:完成任务 3 。第 7 天:休息。第 8 天:完成任务 4 。第 9 天:完成任务 5 。可以证明无法少于 9 天完成所有任务。示例 2:输入:tasks = [5,8,8,5], space = 2输出:6解释:6 天完成所有任务的一种方法是:第 1 天:完成任务 0 。第 2 天:完成任务 1 。第 3 天:休息。第 4 天:休息。第 5 天:完成任务 2 。第 6 天:完成任务 3 。可以证明无法少于 6 天完成所有任务。

每次更新 同类型任务需要的最小天数

完成任务的时候 判断 是比上次加1天 还是 受同类型任务影响的 更多天数

var taskSchedulerII = function(tasks, space) {    
let ans = 0, hash = {}
for(let t of tasks){
ans = Math.max(ans + 1, hash[t] ?? 0)
hash[t] = ans + space + 1
}
return ans
};

如何得到一个数组中第k大的数?

  • o(n): 采用普通排序,返回第k个元素。既然有o(n)的做法,实际上就不会出这道题了
  • o(logn): 采用快速排序,比较哨兵的index与第k个位置的关系,选第0个当哨兵,也从第0个开始,左边是小于等于哨兵的,右边是大于哨兵的,这就很清楚了,接下来再递归即可。
// @param k 从1开始
// @return 第k大的数
function maxK(arr, k) {
if (!arr || !arr.length)
return
if (arr.length == 1)
return arr[0]
let lArr = []
let rArr = []
let mid = arr[0]
arr.forEach(a => {
if (a <= mid) {
lArr.push(a)
} else {
rArr.push(a)
}
})
const i = arr.length - k // 要找的第k大的值的index
if (lArr.length > i) {
return maxK(lArr, k - 1 - rArr.length)
} else if (lArr.length == i) {
return mid
} else {
return maxK(rArr, k)
}
}

将数组循环右移k个位置

首先将前k个元素逆置,然后将后n-k个元素逆置,最后整体逆置一次

原地旋转图像

右上-左下对称swap,然后水平翻转180度完成操作,最少也要O(n)的时间复杂度。

如何在一个数组中获取总和是固定值的长度最小的连续子数组?

滑动窗口经典题,用滑动窗口法,不够就移动左指针,够了就移动右指针,直到不够为止。

function minEqualSumLen(arr, sum) {    
let i = 0
let j = 0
let minLen = arr.length
while(true) {
const t_sum = getSum(arr, i, j)
if (t_sum == sum) {
if (minLen > j-i)
minLen = j-i
}
if (t_sum > sum) {
if (i < arr.length - 1) i += 1
} else {
if (j < arr.length - 1) {
j += 1
} else {
i += 1
}
}
if (i >= arr.length) {
break
}
}
return minLen + 1
}

盛水最多的容器:找最大面积,假设数组的元素值为高度,找到最大的矩形面积

滑动窗口经典题,向中间移动数字较少的哪那一个,为什么呢?如果移动较大的,保留小板,那么必然整体就会变小,这就是求盛水最小的容器了。

必会的基本算法

快速排序

大根堆(优先队列)

单调栈

是一种特殊的数据结构,它维持栈内元素的单调性,即栈内元素要么单调递增,要么单调递减。这种数据结构在处理某些算法问题时非常高效。单调栈的主要操作包括元素的入栈和出栈,但在入栈时需要保证栈内元素的单调性。

排序

冒泡排序(Bubble Sort)

  • 在一轮中,依次比较所有相邻元素,如果第一个比第二个大,则交换它们,最后能把最大的数放到最后面
  • 在第二轮,继续依次比较,这样能把第二大的数推到倒数第二位
  • 执行n轮,完成排序。这个算法的名字由来是因为大的数会经由交换慢慢“浮”到数列的顶端。
function sort(arr) {    for(let i = 0; i < arr.length - 1; i++) {        for(let j = 0; j < arr.length - 1 - i; j++) {             if(arr[j] > arr[j+1]) {                 const temp = arr[j];                arr[j] = arr[j+1];                arr[j+1] = temp;            }        }    }}

选择排序(Selection Sort)

  • 找到数组中的最大值,将其放置在最后一位
  • 接着找到剩余数组中的最大值,将其放置到倒数第二位
  • 执行n轮,就可以完成排序。与冒泡排序的区别是选择排序不是相邻比较,而是全局比较
function sort(arr) {    for(let i = 0; i < arr.length - 1; i++) {        let pos = 0;        let max = arr[0];        for(let j = 0; j < arr.length - 1 - i; j++) {            if(max < arr[j]) {                pos = j;                max = arr[j];            }        }        const temp = arr[arr.length - 1 - i]        arr[arr.length - 1 - i] = max        arr[pos] = temp    }}

插入排序(Insertion Sort)

  • 从第二个数开始往前比,在前面排好序的数据中找到自己应该插入的地方插入
  • 以此类推进行到最后一个数
function sort(arr) {    for(let i = 1; i<arr.length; ++i) {         const temp = arr[i];        let j = i;        while(j > 0) {             if(arr[j-1] > temp) {                 arr[j] = arr[j-1];            } else {                break;            }            j -= 1;        }        arr[j] = temp;    }}

归并排序(Merge Sort)

  • 分:把数组劈成两半,再递归地对数组进行“分”操作,直到分成一个个单独的数
  • 合:把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组

快速排序

  • 分区:从数组中任意选择一个基准,所有比基准小的元素放到基准前面,比基准大的元素放到基准的后面
  • 递归:递归地对基准前后的子数组进行分区
function sort(arr) {     const rec = (arr) => {        if(arr.length === 1 || arr.length === 0) { return arr; }       const left = [];       const right = [];       const mid = arr[0];       for(let i = 1; i < arr.length; ++i) {            if(arr[i] < mid) {                left.push(arr[i]);           } else {                right.push(arr[i]);           }       }         return [...rec(left), mid, ...rec(right)];    };    return rec(arr);}

堆排序

实际上堆是一个一个插入进去的,反应到数组上,就是一次遍历。但是每次只能得到最大值而已。

外部排序

用归并法,最佳归并树,败者树

数组部分

寻找数组峰值(一定有峰值,元素不重复,任意一个就行,这个值比两边相邻的值都大就称为峰值)(首尾元素不相同)

  • 一般需要O(n),挨个找一遍即可。
  • 但最快可以log(n),因为有条件的限制,用二分法,首先两端节点一定是上升或下降趋势的,假如是下降趋势的(首元素大于尾元素),先找中间节点,如果首节点与中节点是上升趋势的(中间节点大于首节点),那么表明有峰值在右边,如果是下降趋势的,那么表明有峰值在左边。(因为随便一个峰值就行),这样时间复杂度就是O(logn),好厉害。

找到乱序数组中缺失的最小数(从1开始)(缺失的第一个正数)

从1开始,1234....直到有一个地方断开了,直接到100之类的(不重复),必须O(n)的时间复杂度和常数级别的空间。如果题目给定的数组是不可修改的,那么就不存在满足时空复杂度要求的算法;但如果我们可以修改给定的数组,那么就可以采用原地数组哈希方法。这个方法自然是需要INT_MAX的数组大小(因为数就这么大,你可以扩展原空间或者复制到新空间里面去),每个i位置上的数是i+1,然后从0开始swap,找到第一个位置与值不相符的地方就行了。 如果能使用常数级别的空间,那么就设一个INT_MAX的数组就好了。 但是有负数怎么办?这就不行了。有重叠的怎么办? 用额外空间的话也只是O(n)而已。

荷兰国旗问题

荷兰国旗是由红白蓝3种颜色的条纹拼接而成,假设这样的条纹有多条,且各种颜色的数量不一,并且随机组成了一个新的图形,把这些条纹按照颜色排好,红色的在上半部分,白色的在中间部分,蓝色的在下半部分,我们把这类问题称作荷兰国旗问题。

抽象一下,就是数组中只有三种类型的数,对其进行排序。

一般而言,首先要考虑o(logn),原空间。

设三个指针,边缘指针是下一项,它之前的都是最左边或最右边的数了,当中间指针指向两边的数,那么就跟两边指针对换,如果是中间的数,就没有任何关系,时间复杂度O(n),但是超过三种颜色就不行了,可能是说,在一个数组里区分好几种队列,每一次移动都要移动相当量的数。如果不限的话,跟普通排序没什么区别了,用普通排序更好。

合并区间

输入: [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: 区间 [1,3][2,6] 重叠, 将它们合并为 [1,6]

排序,如果后面的区间开始小于前面区间结束的话就合并删除好了。基本区间的问题总是先排序。

插入区间

必定先排好序,将区间插入后,合并区间就好了。当合并好后,可以直接将上下的统统加入即可。判断效率也不低。 第二种方法是依次比对扩充区间,最后排序去重,还是排序。

数组中重复元素

或者两个数的差值小于一定范围,用桶排序(哈希实现)

滑动窗口中的最大值

这个就是用大根堆实现的最好方式了,大根堆是优先队列的一种,这里问题的关键是如何判断堆里元素还是否在窗口内,关键就是连index也一起存上,就酱

链表

没有特殊说明,是有头的,有头结点的链表统一了算法的实现,无头节点减少了节点个数,头节点的存在就是为了方便操作,减少特殊情况。 链表排序,用最普通的选择排序最棒了。

k个一组反转链表

只想说没有什么特殊解法,正常做就好了。

集合

取差集,交集,并集(必须有序)

当然两个链表$A,B$是有序的,每个节点是一个区间,例如取$A-B$吧,每过一个i,保证j稍大于它,这样就会判断出等于,判断出等于后就减去就好了。本身两个集合自己自然是没有重复的,也是取稍大一个的,证明B之前的都已经被检验过。并集也是一样。 必然有序,然后一个额外的res

栈和队列

优先队列

先进,优先级大的先出,往往用二叉堆实现(二叉树堆是一棵完全二叉树)(最后一层的叶子节点全左排列)(二叉堆可以很容易用数组来表示),二叉堆的插入就是插入到最后然后挨个往前即可,删除时就是最顶部的,然后下面两个子节点挑一个小的,然后依次。。。都是logn,例如用于进程调度系统,

柱状图中最大面积的矩形

第一次学单调栈,如果能获取以每个柱为最高限度的矩形,求最大就可以了,另外单调栈在运算的过程中,是能够一遍获取到所有的柱子的最远距离的,左边遍历一次,右边遍历一次,宽度区间就出来了。 单调栈如果栈里的数大于该数,就全pop掉,这样里面的数据都是稍微小于该节点的,自然不要,这样就出来了。实际上,由于每次循环都是一根柱子,因此两次遍历可以放在一个循环中写,更进一步,在进行出栈操作时可以确定它的右边界!当被弹出栈时,说明这个节点高度大于等于要进来的节点高度,且中间没有更低的了,但是这里是小于等于,如果后面还有等于的,就添加不进去,实际上,如果还有等于的,那么相等的最后一个就肯定是正确的了,没有太大关系。。

规范化路径

给出一串路径(例如/home/ubuntu/../hello),让其变成绝对路径,这的确是用栈,它说的单调栈应该是遇见.或者..就当场运算掉。

逆波兰表达式求值

栈的老问题了,看到数就一直压,看到操作符就弹出来两个。

中序表达式转后序表达式

只维护一个符号栈,如果栈里的符号优先级比较小就弹出来,如果是左括号直接放入,优先级重新计数。

单调栈

字符串

KMP算法

原串与子串比较,就这样比(分别在原串的第i位和子串的第j位),一旦发现不相符了,j不是直接归零,而是变成一个较小的数,它能保证前面的仍然比对正确,一旦j到达终点,就比对出来一个串。 那么这个数组怎么构建呢?是子串自己跟自己的比对,没法只能硬记:

void Getnext(int next[],String s){   int i=0,j=-1; // i是主串位置序号,j是从串位置序号   next[0]=-1;   while(i<s.length-1)   {      if(j==-1 || s[i]==s[j]) // 对,这两个算一个状态      {         i++; j++;         next[i] = j; // 在这里就会补上0      }      else j = next[j];   }}

这没有任何意义,因为这是经过情况压缩的。这里讲一下next[i]=j,如果两个字符相等,那么它们的下一个字符进行设置,为什么,因为我们保证的是之前的字符串相同,当换掉之后,能保证之前的序列相同,同时,此时的仍要比较,形成继续,就是这样。

马拉车算法

它的优点就是在于用了对称点的回文信息 首先插入分隔符,没问题吧 有一个最右回文串,它的中心是C,它的右边界是R。 遍历的指针i,被放在C与R之间, 每次遍历,是确定以指针i为中心的最长回文串, 这里分情况了,通过跟C的映射,得到基础回文串长度,如果长度不超过最右边界,就填这个好了。 如果刚好的话,也只能是,毕竟限制是左边的。 如果更长的话,就延展R,看是否跟左边的相符。这时候就要更换最右字符串为本i。 为什么最右字符串呢?普通字符串怎么样呢?首先说你要确保i在一个回文串的右臂上对吧,如果是普通字符串,就不能保证是在右臂上了。 注意,添加#号的字符串的回文串都是奇数长度(单中心),每个位置保存的长度是单臂长度(不包括中心),也是实际回文串长度,注意#号的值也是有意义的,它代表双中心的长度。 获取开头下标也有简单技巧,用 P 的下标 i 减去 P [ i ],再除以 2 ,就是原字符串的开头下标了。

最长不重复子串算法(是元素的不重复)

采用滑动窗口,每进来一个字符都要整体检查,或者从后向前检查,如果没有就加上,如果有就滑动窗口,$o(n)$复杂度。

最小覆盖子串(单词是否在这个字符串里)

经典的滑动窗口算法,指针r一直向右滑动,直到刚好成立,然后收缩l指针,直到不能成立,记录下最小字符串。关键在于如何确定里面有这个字符串,自然是哈希表,哈希表比对减1,26个元素数组即可。

将字符串拆分成回文子字符串的最小数量(切分)

动态规划,每个元素是当前字符串回文串最小数量,再来一个字符,就遍历前面的某一个元素,该元素之后是否为一个回文串,如果是就是这个元素的值+1,这样遍历下来取最小值就是了。时间复杂度双层循环了。

将字符串拆分成单词

这个蛮重要的,虽然英语多以空格分割,但是汉语没有啊,自然是动态规划, 在所有已过的字母中,前面已经形成了单词的不用管,剩余不能组成单词的字母,再向前看一个,加上该字母是否形成单词。 如果加空格返回字符串呢?所有的情况就都要保留着了,分为两个字符串数组,第一个是已经形成句子的,第二个是剩余字母,到最后挑剩余字母为0的就是了(剪枝就是减去超过最大单词长度的)。

格雷编码

格雷码是一个二进制数系,其中两个相邻数的二进制位只有一位不同. 3 位二进制数的格雷码序列为 000,001,011,010,110,111,101,100,(可以看成一个环,第一个格雷码 000 与最后一个格雷码 001 也只有一位不同). 交替构造:从全 0 格雷码开始构造 k 位格雷码: 翻转最低位得到下一个格雷码,如 000 变成 001. 把最右边的 1 左边的位翻转得到下一个格雷码,如 001 变成 011. 交替按照上述步骤进行$2^k-1$次,即可得到所有的 k 位格雷码. 镜像构造:k 位格雷码可以根据 k-1 位格雷码以上下镜射后加上新位的方式快速的得到,如下图:

递归与回溯

解数独

就是回溯(回溯其实跟dp一样,可以保存状态的),从左到右,从上到下的排列,如果没有,就返回上一个,进行改变,每一个位置都维护着一个占用数组,通过剪枝,效率还是蛮高的,就这样。

括号匹配

进行深搜或广搜,只需要一个int标志位,左括号+1,右括号-1,这个标志位不能为负,到最后整体为0,就是如此

欧拉回路

不重复地经过每一条边,最后回到原点。判断它的做法是一是判断是不是孤立点(度为0),二是判断度是不是偶数,就是这么简单。度是进去有几条线或者出去有几条线。

汉密尔顿回路

不重复地走过所有的节点(不必通过图中的每一条边),最后回到原点。没有充分必要条件,只能暴力解。

有向图中的环

这叫拓扑排序,经典的拓扑排序题。 根据依赖关系,构建入度数组(是二维数组),表项都是入度。 选取入度为 0 的数据,根据邻接表,减小依赖它的数据的入度(就是消消消)。 找出入度变为 0 的数据,重复第 2 步。 直至所有数据的入度为 0,得到排序,如果还有数据的入度不为 0,说明图中存在环。 实现邻接表的方法绝对有100种以上。邻接表就是用来存储图的。这里就适合一个二维数组,每次只消仅有这个值的,然后别的行也同时消去,直到最后全部消去,就算成功,否则会出现明明还有数据,但是没有那种单一的情况,那就是有环了。 如果没有环,返回顺序的话,就每次消去的时候记上就可以了。

普利姆算法

用于构建最小生成树的,从一个点开始,不断求其连向外的最短路径,直到生成这个最小生成树。

克鲁斯卡尔算法

用的是线段,先pop出一个最小的线段,看它是否在两个连通分量之间,不是则抛弃。

迪杰斯特拉算法求最短路径

就是绕圈取最小值的方法,它的原理是比较这个点直接到原点的距离和其他已知点最短路径到它的距离, 找到最短路径就填入了。它只能求任一点到某一点的距离。贝尔曼-福特算法(Bellman–Ford algorithm )用于计算出起点到各个节点的最短距离,支持存在负权重的情况,它的原理是对图进行最多V-1次松弛操作,得到所有可能的最短路径。其优于狄克斯特拉算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(VE)。所谓的松弛,以边ab为例,若dist(a)代表起点s到达a点所需要花费的总数,dist(b)代表起点s到达b点所需要花费的总数,weight(ab)代表边ab的权重, 若存在:

(dist(a) +weight(ab)) < dist(b)

则说明存在到b的更短的路径,s->…->a->b,更新b点的总花费为(dist(a) +weight(ab)),父节点为a 这也表明了的解斯特拉算法不支持负数。

弗洛伊德算法求最小路径

每个顶点都有可能使得另外两个顶点之间的路程变短。 基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。举一个例子:开始时,第一个点连接其他的点时,用第二个点来判断,即判断(1,3)和(1,2)+(2,3),然后判断完成一遍,紧接着用3来判断一遍,,,就是这样。这个可以求任一点到任一点的距离。

二叉树

完全二叉树,都有左右孩子(但最后一层没有满没关系,其余每层节点数都达到最大值),完美二叉树,所有孩子在同一层。

二叉树的迭代遍历

用栈了。如果有左节点,就一直压栈,如果没有了,看右节点,压栈后继续左节点压栈,这样就实现了遍历。

二叉搜索树的迭代器

可以将二叉搜索树展开到数组中输出,但是会占用空间,如果是受控递归,只需要一个层大的栈就可以了,如果有,一直到最后的左子节点,没有了,输出本值,然后转移一个右节点,继续,如果右节点也没有,输出,这样。has_Next若栈中还有元素,则返回 true,反之返回 false。所以这是一个 O(1)的操作。都是O(1)操作。

二叉树层序交叉输出

你明白什么意思啊,这个解法就是两个队列,就可以实现分层效果了。一个队列就正向输出,另一个队列就是反向输出。

二叉树原地成链表

关键在于一个全局前驱节点,例如都放在左孩子成为一个链表,这样每处理一个节点,那么这个节点就会存入前驱节点,等回溯的时候,欸,就很自然的连上了。

二叉树序列化和反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

搜索二叉树节点的最低公共父节点

判断数字,当分叉时就是公共最近父节点

给二叉树节点添加一个next指针(一层一层指)

这个指针横着指,一层一层的指,可以用双队列处理,不难,但是很重要。但关键最后是完美二叉树才行,当然,看怎么用了。

生成所有由 1 ... n 为节点所组成的 二叉搜索树

如果仅仅是得出数量的话,这是一个动态规划题,如果输出树的话,就是一个回溯题,遍历完各种树然后输出,这个题要层序输出,而且空位置要填null,好像不是问题,填入就好了,那么如何从这种数组恢复呢?维持一个队列然后一层一层装就可以了。

恢复二叉树

二叉树中的两个节点被对调了,现在恢复它,因为中序遍历是递增的,只要找到两个递减位置就可以了,如果只有一个递减的,那么就是相邻的。中序节点的返回就是再遍历一次就可以了。直接隐式遍历就可以了,遍历的同时记录出错节点,就是这样。

构造二叉树

根据数组构造二叉树,当然必须是排序二叉树(也叫搜索二叉树)了,不然谁知道遍历到哪里了(当然还不能有重复元素),而且还必须至少有两个序遍历,前序就用递归,就一直递归下去就可以了,中序遍历的话,中序遍历是没法做的,只能根据前序遍历来。必须同时给出前序遍历和中序遍历,根据前序遍历找到父节点,根据中序遍历找到左子树和右子树,递归之。

二叉树的最大路径和

就是一串路径的最大和,不一定从根节点开始,方法是回溯法,从上而下做,很像动态规划,该节点的最大路径和就是自己加上子节点的最大路径和,就是这样,二叉树天然有动态规划的优点。

霍夫曼编码

霍夫曼编码是根据出现概率(权重)来编写的二叉树,概率低的代码也长,就是挑取两个权重 小的组成新节点,新节点的值为两个节点的和,左边是0右边是1或者左边是1右边是0都可以。 带权路径长度WSL=叶子节点*其所在层数(算上叶子节点,不算根节点,或者说,到根节点的路径长度)

最小(大)堆

最小(大)堆的特点就是能够始终保证第一个最小(大),构造堆并不是给一个数组排序,而是依次插入,或者说,不是从后向前排序,是从前向后遍历,然后往前排序。 然后使用,是自上而下的方法,把根节点去除后,把最后一个节点放到首部,然后从上而下进行比较交换。

完全二叉树节点数的快速计算

可以比O(N)还快实现,一个非常完美的二叉树有n层吧,1,2,4:1,3,7=>2^n-1,就是如此。

BST第k小的元素

用遍历+大根堆,将元素全部取出然后放入容量为k的大根堆,就是如此了

二叉平衡搜索树AVL

  1. 它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1。
  2. 它的左子树和右子树都是一颗平衡二叉树(左子节点小于该节点,右子节点大于该节点)。
  3. 插入删除过程中需要几种旋转完成操作。### 红黑树

10亿数据进行不到30次比较就能查找到目标

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

性质1:每个节点要么是黑色,要么是红色。 性质2:根节点是黑色。 性质3:每个叶子节点(NIL)是黑色。 性质4:每个红色结点的两个子结点一定都是黑色。 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。 八种条件,太难。

B树

B树由于良好的查找性与局部性,对磁盘更友好,往往被用于数据库。

败者树

败者树相当有意思,它可以实现n路归并的算法,每个节点存放的是比较失败的那一项,从而在下次比较的时候好比较,最后的根节点的相反方就是胜利者,当然,每次都是胜利者往上传,胜者树也是胜利者往上传。

TRI树,字典树

就是这样一棵树,自动补齐有大用

树状数组

例如计算右侧小于当前元素的个数,注意是所有元素都要算

线段树

哈希表

搜索

二分搜索

关键在于理解思想,就不写代码了。二分搜索的一个注意点就是不要数字溢出,因为往往会比较大。

找出数组中第n大的数

通过快速排序,快速排序能够把哨兵左右排好(每个阶段都要做完,不会损失时间复杂度的),然后根据第几个到相应区域继续排序,直到排完,就是这样。 但是前三个就不必这么做,完全可以先找出第一个,然后找出第二个这样解决。

贪心

跳到最后位置

从正向出发,在该数笼罩的范围内,找到跳的最远的那个,就是那个。时间复杂度是O(n)

文本左右对齐

贪心,尽可能往一行塞尽可能多的单词,塞不了再匀。或者说,这就是最普通的做法吧?

11. 动态规划

找到二维数组中的最大矩形

注意是矩形,因此长方形也可以,因此每个节点都保存有当前最大的宽高,每一个点取最小值,是为最长的宽高,最后遍历一遍获得最大矩形。

单词1转换成单词2所使用的最少操作数

编辑距离算法被数据科学家广泛应用,是用作机器翻译和语音识别评价标准的基本算法。最直观的方法是暴力检查所有可能的编辑方法,取最短的一个。所有可能的编辑方法达到指数级。动态规划算法用 D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。

买卖股票最佳时机

实际上是找后面减前面的最大值,这里的关键是维护了一个最小价格,也就是说在动态规划数组外还要维护一个变量,这个是之前没想过的。 当然这只是一次,如果多次的话,用动态规划版的状态机,实际上是个多维数组,但每维都有特别的含义,例如dp[3][0]表示第三天未持有,这样两层循环,填到最后就是最终结果了。关键在于这个状态机思维。 如果是两次呢?

排序二叉树总数

这道题放到二叉树不对的,只是用了二叉树的形式而已。这道题是两种函数叠加形成的。比较困难。

组合与排列

组合(子集)

一个递归,一个回溯,组合的效率是比较坑爹的,一个一个存入也要O(n^2)的时间, 回溯算法就是前序遍历树,树的结构为

固定大小的组合

回溯法可以剪枝,当大于count的时候就不要递归下去了。只有回溯法可以使用了。仅仅的区别只是最开始的res.push有判断和return而已。

排列

排列也是用回溯,这个回溯是从0开始选择,判断新来的数是否contain了,就是这样。之所以不会乱序是因为for循环的缘故。按大小进行排列的话,可以递归实现,每位每次一个数,接着下一层,如此填入res中。字典序算法的话,就是下面的算法,你可以从123456789最小开始,这样填入。生成 N! 个全排列需要时间 $O(N×N!)$(生成下一个排列是$O(N)$).

获取字典序的下一个排列

我们希望下一个数比当前数大,因此只需要将后面的大数与前面的小数交换,就能得到一个更大的数。 我们还希望下一个数增加的幅度尽可能的小,这样才满足紧邻的要求。在尽可能靠右的低位进行交换,将一个 尽可能小的大数与前面的小数交换。将大数换到前面后,还需要将大数后面的所有数重置为升序,升序排列才是最小的排列。

获取指定位置排列

普通递归全排列即可,递归时剪枝,深度优先,我们在递归前就可以通过阶乘预先知道子节点数量,时间复杂度超级小。(能否在O1时间计算出?)

获取指定位置排列(允许重复)

用最小堆或动态规划,最小堆比不上动态规划不说了

和为某个特定数的所有组合

给定一个不重复数组和一个目标数,找到数组里的,emmm,几个数相加是目标数,返回这几个数的数组,是二维数组。 输入:candidates = [2,3,6,7], target = 7, 所求解集为:[ [7], [2,2,3] ] 解法是递归+回溯,击败99.8%,这就是最好的,回溯就是树,树你要想清树的结构,然后就好办了,根据树的结构写递归代码。

数学

把分数变成小数

关键就是在于循环小数的判断,如果余数是循环的话,商也是循环的。

只出现一次的数字

异或:如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。 任何数和 0做异或运算,结果仍然是原来的数 任何数和其自身做异或运算,结果是 0 异或运算满足交换律和结合律

求众数(默认有数字超过多半数)

这个并没有什么直观的做法(除了哈希表),最好的用投票法O(n),什么是投票法(Boyer-Moore 算法)呢? 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值(比如第一个),count 为 0;

求中位数

可以使用两个优先队列,保证这两个队列一致(前提是排好序,但是排好序直接求中间就可以了。。。)

颠倒二进制位

不用循环的话,用分治法,虽然它们的时间复杂度都是O(n),就算颠倒普通十进制数字,也只是循环做而已。

数组按位与

这个问题的关键是当数特别大时有更好的算法,我们观察按位与运算的性质。只要有一个零值的位,那么这一系列位的按位与运算结果都将为零。它的规律是最大的那一位是1,后面都是0,因为这是连续的,进一步说,是第一个数字和最后一个数字的公共前缀,我们通过右移,将两个数字压缩为它们的公共前缀。在迭代过程中,我们计算执行的右移操作数。将得到的公共前缀左移相同的操作数得到结果。

返回二进制数中1的总数

  1. 不断把数字最后一个 1反转,并把答案加一。当数字变成 0 的时候偶,我们就知道它没有 11 的位了,此时返回答案。 这里关键的想法是对于任意数字 n ,将 n 和 n−1 做与运算,会把最后一个 1的位变成 0(不是最后一位!证明稍显困难) 。

欧里几德算法,辗转相除法,给定两个正整数m和n,求它们的最大公因子,即能够同时整除m和n的最大正整数

m%n=r,然后n%r=k,直到k为0,那么r就是答案。 更相减损术是任意给定两个正整数;以较大的数减较小的数,接着把所得的差与较小的数比较是否相等,并接着以大数减小数。继续这个操作,直到所得的减数和差相等为止。则就是最大公约数。 扩展的欧里几德算法,am+bn=c

求一年是否是闰年

1.普通年(不能被100整除)能被4整除为闰年。(如2004年就是闰年,1900年不是闰年) 2.世纪年(能被100整除)能被400整除的是闰年。(如2000年是闰年,1900年不是闰年)

四平方和定理

四平方和定理证明了任意一个正整数都可以被表示为至多四个正整数的平方和。