阅读 12858

前端面试常见的知识点(四处搜刮)💻

前言

划重点

这个东东是四处收集来的,然后自己整理整理的,如果侵害的原作者的利益,如果造成了侵权等问题,请赶快联系俺!马上删除!!祝各位早日求职上岸!!冲鸭!!如果有错误,大噶评论区告诉俺一下!俺去改改!!加油!

小菜鸡的面试の初体验 juejin.im/post/5e5a64…

那俺们开始咯!!!冲鸭!!!

因为这些大部分也是从网上收集来的,如果大家发现有什么错误的地方,请在评论区指出来,俺去改正改正!

1. 模拟new的过程

实现步骤

  1. 创建一个新的对象obj
  2. 链接到原型(新对象的原型指向要继承的构造函数的原型),obj可以访问构造函数原型的属性
  3. 绑定this实现继承,obj可以访问构造函数的属性
  4. 如果构造函数返回的是对象则返回它,如果不是则返回obj
function Animals(name, color){
    this.name = name
} 

Animals.prototype.action = function () {
  console.log(this.name, 'walk')
复制代码

首先定义一个构造函数,以及他原型的方法
接着实现一个create方法来模拟new

function create(constructor, ...args){
    const obj = new Object()
    obj.__proto__ = constructor.prototype
    const res = constructor.apply(obj, args)
    return res instanceof Object ? res : obj
}
复制代码

具体使用则:

const dog = create(Animals, 'dog', 'red')

// const cat = new Animals('cat', 'yellow')
复制代码

通过改变一个对象的 [[Prototype]] 属性来改变和继承属性会对性能造成非常严重的影响,并且性能消耗的时间也不是简单的花费在 obj.proto = ... 语句上, 它还会影响到所有继承自该 [[Prototype]] 的对象,如果你关心性能,你就不应该修改一个对象的 [[Prototype]]。

所以我们可以通过别的方法来改变obj原型的指向,通过Object.create()方法来继承

function create(constructor, ...args) {
  const obj = Object.create(constructor.prototype)
  const res = constructor.apply(obj, args)
  return res instanceof Object ? res : obj
}
复制代码

2. 函数防抖和节流

首先模拟一下用户输入

<div>
  <input id="input"></input>
  <div id="text">0</div>
</div>
复制代码

然后防抖与节流

// 防抖
const debounce = function (fn, delay = 1000) {
  let time = null
  return function (...args) {
    let that = this
    time && clearTimeout(time)
    time = setTimeout(function () {
      fn.apply(that, args)
    }, delay)
  }
}

// 节流
// 时间戳版
function throttle(fn, delay = 500) {
  let last = 0
  return function (...args) {
    let now = new Date().getTime()
    if (now - last > delay) {
      last = now
      fn.apply(this, args)
    }
  }
}}

// 定时器版,初次调用会延迟
const throttle = function (fn, delay = 1000) {
  let time = null
  return function (...args) {
    let that = this
    if (!time) {
      time = setTimeout(function () {
        time = null
        fn.apply(that, args)
      }, delay)
    }
  }
}
复制代码

接下来调用即可

const input = document.getElementById('input')
input.oninput = debounce((e) => {
  document.getElementById('text').innerTexte.target.value
}, 1500)
复制代码

这样子就可以实现函数防抖和节流啦

其实这里还有另外一个简单一点的方法,就是setTimeout使用箭头函数,这样就可以直接使用this,就不用额外生成一个变量that

const debounce = function (fn, delay = 1000) {
  let time = null
  return function (...args) {
    time && clearTimeout(time)
    time = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}
复制代码

3. 输入url到展示的过程

  1. URL解析,如果有非法字符,就转义
  2. 判断请求资源缓存中有没有
  3. DNS解析
  4. TCP三次握手
  5. 发送请求,分析url,设置请求头
  6. 服务器返回请求的文件(html)
  7. 浏览器渲染
    • 解析html文件,生成dom树
    • 解析css文件,生成style树
    • 结合dom树和style树,生成渲染树(render tree)
    • layout布局渲染
    • GPU像素绘制页面

4. 函数的柯里化

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
复制代码

主要是要收集传进来的参数

function curry(){
    const argsList = [...arguments]
    
    const fn = function(){
        argsList.push(...arguments)
        return fn
    }
    
    fn.toString = function(){
        return argsList.reduce((a, b) => a + b)
    }
    
    return fn
}

console.log(curry(1, 2)(3)(4, 5, 6)) // 21
复制代码

这样就算完成了这个题了,可以自由传入参数来求和,接下来就将一个普通函数变成柯里化函数

function sum(a, b, c) {
  return a + b + c
}
function curry(fn) {
  const argsList = [...arguments].splice(1)
  return function () {
    const newArgsList = argsList.concat([...arguments])
    if (newArgsList.length < fn.length) {
      // 如果接收的参数还没有到达函数参数的个数继续收集参数
      return curry.apply(this, [fn, ...newArgsList])
    } else {
      return fn.apply(this, newArgsList)
    }
  }
}
const sumAll = curry(sum)
console.log(sumAll(1)(2)(3)) // 6
console.log(sumAll(1)(2, 3)) // 6
复制代码

5. 重绘与回流

1. 重绘

当元素样式发生改变,但不影响布局时,浏览器将使用重绘进行元素更新,由于此时只需要UI层面的绘制,因此损耗较小

2. 回流

当元素尺寸、结构或者触发某些属性的时候,浏览器会重新渲染页面,这就叫回流。此时,浏览器需要重新计算,重新进行页面布局,所以损耗较大

一般有以下几种操作:

  • 页面初次渲染
  • 浏览器窗口大小改变
  • 元素尺寸、位置、内容改变
  • 元素字体大小改变
  • 添加或删除可见的dom元素
  • 触发CSS伪类,如:hover
  • 查询某些属性或者调用某些方法
    • clientWidth, clientHeight, clientTop, clientLeft
    • offsetWidth, offsetHeight, offsetTop, offsetLeft
    • scrollWidth, scrollHeight, scrollTop, scrollLeft
    • getComputedStyle()
    • getBoundingClientRect()
    • scrollTo()

回流必定触发重绘,重绘不一定触发回流,重绘代价小,回流代价大

如何避免重绘和回流

CSS:

  • 避免使用table布局
  • 尽可能再dom树的末端修改class
  • 避免使用多层内联样式
  • 将动画效果应用到position: absolute || fixed
  • 避免使用css表达式(例如calc
  • CSS3硬件加速(GPU加速)

JavaScript:

  • 避免频繁操作样式,最好一次性修改style属性,或者将样式列表定义成class,并一次性更改class属性
  • 避免频繁操作dom,创建一个documentFragment,在他上面应用所有的dom操作,最后再把他添加到文档中
  • 也可以先为元素设置display: none,操作结束后再把它显示出来,因为再display为none的元素上进行dom操作不会引发重绘和回流
  • 避免频繁读取会引发重绘回流的属性,如果需要多次使用,就用一个变量缓存起来
  • 对具有复杂动画的元素使用绝对定位,使他脱离文档流,否则会引起父元素及后续元素频繁回流
  • 使用cssText来更改样式

6. 浏览器存储

  • cookie 通常用于存用户信息,登录状态等,可自行设置过期时间,体积上限为4K
  • localStorage 无限期存储,体积上限为4~5M
  • sessionStorage 浏览器窗口关闭则删除,体积上线为4~5M

7. 网络请求方式post和get

  • get 会被浏览器缓存,请求长度受限,会被历史保存记录,浏览器回退时候是无害的,一般不带请求体,发送一个TCP数据包
  • post 更安全,更多编码类型,可以发大数据,浏览器回退的时候会再次提交请求,一般带有请求体,发送两个TCP数据包

在网络差的时候,post发送两个TCP请求包,验证完整性更好

8. TCP三次握手

为什么要TCP握手呢

为了防止已失效的连接请求报文段突然又传回服务端,从而产生错误

  1. 客户端发送syn(同步序列编号)请求,进入syn_send状态,等待确认
  2. 服务端接受syn包并确认,发送syn + ack包,进入syn_recv状态
  3. 客户端接受syn + ack包,发送ack包,双方进入established状态

9. TCP四次挥手

  1. 客户端发送fin给服务端,用于关闭client到server的数据传输。客户端进入fin_wait状态
  2. 服务端接受fin后,发送一个ack包给客户端。服务端进入close_wait状态
  3. 服务端发送一个fin给客户端,用于关闭server到client的数据传输。服务端进入last_ack状态
  4. 客户端收到fin后,进入time_wait状态,接着发送一个ack给服务端,服务端进入closed状态

为什么建立是3次握手,而关闭是4次挥手呢?

因为建立连接的时候,客户端接受的是syn + ack包。而关闭的时候,服务端接受fin后,客户端仅仅是不再发送数据,但是还是可以接收数据的。服务端此时可以选择立刻关闭连接,或者再发送一些数据之后,再发送fin包来关闭连接。因此fin与ack包一般都会分开发送。

10. 内存泄漏

  • 意外的全局变量
  • 闭包
  • 未被清空的定时器
  • 未被销毁的事件监听
  • dom引用

11. 类继承的特点

class Dog {
  constructor(name) {
    this.name = name;
  }
}

class Labrador extends Dog {
  // 1 
  constructor(name, size) {
    this.size = size;
  }
  
  // 2 
  constructor(name, size) {
    this.name = name;
    this.size = size;
  }
  
  // 3
  constructor(name, size) {
    super(name);
    this.size = size;
  }
};
复制代码

像上面的代码,1和2都会报错ReferenceError引发一个引用错误,因为在子类中,在调用super之前,是无法访问this

12. 常用正则表达式

验证数字的正则表达式集

验证数字:^[0-9]*$

验证n位的数字:^\d{n}$

验证至少n位数字:^\d{n,}$

验证m-n位的数字:^\d{m,n}$

验证零和非零开头的数字:^(0|[1-9][0-9]*)$

验证有两位小数的正实数:^[0-9]+(.[0-9]{2})?$

验证有1-3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$

验证非零的正整数:^\+?[1-9][0-9]*$

验证非零的负整数:^\-[1-9][0-9]*$

验证非负整数(正整数 + 0) ^\d+$

验证非正整数(负整数 + 0) ^((-\d+)|(0+))$

验证长度为3的字符:^.{3}$

验证由26个英文字母组成的字符串:^[A-Za-z]+$

验证由26个大写英文字母组成的字符串:^[A-Z]+$

验证由26个小写英文字母组成的字符串:^[a-z]+$

验证由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$

验证由数字、26个英文字母或者下划线组成的字符串:^\w+$

验证用户密码:^[a-zA-Z]\w{5,17}$ 正确格式为:以字母开头,长度在6-18之间,只能包含字符、数字和下划线。

验证是否含有 ^%&',;=?$\" 等字符:[^%&',;=?$\x22]+

验证汉字:^[\u4e00-\u9fa5],{0,}$

验证Email地址:^\w+[-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$

验证InternetURL:^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$ ;^[a-zA-z]+://(w+(-w+)*)(.(w+(-w+)*))*(?S*)?$

验证电话号码:^(\d3,4\d3,4|\d{3,4}-)?\d{7,8}$:--正确格式为:XXXX-XXXXXXX,XXXX-XXXXXXXX,XXX-XXXXXXX,XXX-XXXXXXXX,XXXXXXX,XXXXXXXX。

验证身份证号(15位或18位数字):^\d{15}|\d{}18$

验证一年的12个月:^(0?[1-9]|1[0-2])$ 正确格式为:“01”-“09”和“1”“12”

验证一个月的31天:^((0?[1-9])|((1|2)[0-9])|30|31)$ 正确格式为:01、09和1、31。

整数:^-?\d+$

非负浮点数(正浮点数 + 0):^\d+(\.\d+)?$

正浮点数 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$

非正浮点数(负浮点数 + 0) ^((-\d+(\.\d+)?)|(0+(\.0+)?))$

负浮点数 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$

浮点数 ^(-?\d+)(\.\d+)?$

复制代码

13. 跨域

image

link,img,script标签可以跨域

跨域行为

  • 同源策略限制、安全性考虑(如cookies)
  • 协议、IP地址和端口不同都是跨域行为

常见跨域

  • jsonp
  • cors
  • websocket
  • postMessage + iframe
  • document.domain + iframe
  • window.name + iframe
  • nginx代理
  • iframe嵌套进行跨域

JSONP

const script = document.createElement('script')
script.type = 'text/javascript'

script.src = 'xxx.com/login?user=xxx&password=123&callback=onBack'
document.head.appendChild(script)

function onBack(res) {
    console.log(res)
}
复制代码

CORS跨域

CORS (Cross-Orgin Resources Share)跨域资源共享,允许浏览器向服务器发出XMLHttpRequest请求,从而克服跨域问题,他需要浏览器与服务器同时支持

  • 浏览器端会自动向请求头添加orgin字段,表明当前请求来源
  • 服务器需要设置响应头的Access-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Origin等字段,指定允许的方法、头部、来源等信息
  • 请求分为简单请求和非简单请求,非简单请求会先进行一次OPTIONS请求来判断当前是否允许跨域

简单请求

请求方法是以下三种之一:

  • HEAD
  • POST
  • GET

Http的请求头信息不超过以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type (只限三个值:application/x-www-form-urlencodedmultipart/form-datatext/plain

后端的响应头信息:

  • Access-Control-Allow-Orgin:此字段必须有,他的值要么是请求时orgin的值,要么是*表示接受任意域名的访问
  • Access-Control-Allow-Credentials:此字段可选,是一个布尔值,用来表示是否允许发送cookies
  • Access-Control-Expose-Headers:此字段可选,如果想XMLHttpRequest对象的getResponseHeader()方法获取更多请求头,就要在这个字段指定

非简单请求

非简单请求是指对服务器有特殊要求的请求,例如:

  • 请求方法为PUTDELETE
  • Content-Typeapplication/json

非简单请求会在正式通信之前,增加一次HTTP查询请求,称为预检请求,就是options

  • Access-Control-Request-Methor:此字段必须有,用来表示浏览器的CORS请求会用到哪些HTTP方法,例如PUT
  • Access-Control-Request-Headers:此字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的请求头信息

CORS 与 JSONP的对比

  • JSONP只可以用于GET请求,CORS支持所有类型的HTTP请求
  • JSONP的优势是支持老式浏览器,以及可以向不支持CORS的网站发送请求

14. 缓存

其实平时开发使用webpack进行构建的时候,后缀有hash值,其实这就是与缓存相关的东西

缓存可以分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器。协商缓存返回的状态码是304。两种缓存可以同时存在,强缓存的优先级大于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不执行协商缓存。

强缓存

Expires (HTTP 1.0)Expires的值为服务端返回的数据过期时间。当再次请求时间小于过期时间,则直接使用缓存数据。但由于服务端和客户端的时间可能有误差,可能会出现缓存命中的误差。另外,大多数使用Cache-Control代替

Pragma (HTTP 1.0):HTTP 1.0遗留下来的字段,当值为no-cache时强制验证缓存,Pragma禁用缓存,如果给Expires定义一个未过期的时间,那么Pragma的优先级更高。服务端响应添加Pragma: no-cache,浏览器表现的行为和刷新(F5)相似

Cache-Control (HTTP 1.1):有以下几种属性

  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • max-age=t:缓存内容将在t秒后失效
  • no-cache:需要使用协商缓存来验证缓存数据
  • no-store:所有内容都不会缓存

no-cache不是代表不缓存的意思,no-cache代表的是可以缓存,但是每次都要通过服务器验证缓存是否可用。no-store才是不缓存内容。

当响应头Cache-Control有指定max-age时,优先级会比expires高。

命中强缓存的形式:Firefox浏览器:灰色的200状态码,Chrome浏览器:200(from disk cache)或者200 OK(from memory cache)

协商缓存

协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时候,服务器会将缓存标识与数据一起响应给客户端,客户端将他们备份至缓存中。再次请求时候,客户端会将缓存中的标识发送给服务端,服务端根据此表示判断。若未失效,返回304状态码,浏览器拿到此状态码,就可以直接使用缓存数据了。

Last-Modified:服务器再响应请求时,会告诉浏览器资源的最后修改时间,因为可能最后一秒多次修改,或者是服务器与客户端时间不同,可能导致缓存未命中,所以之后推出了etag

if-Modified-Since:浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求发现有if-Modified-Since后,则与被请求资源的最后修改时间进行对比,如果一致则返回304响应,浏览器只需要从缓存中获取数据即可

  • 如果资源被修改了,服务端就传输一个整体数据,服务器返回200 OK
  • 如果没有被修改,服务器只需要传输响应头信息,服务器返回 304 Not Modified

if-Unmodified-Since:从某个时间点开始,文件是否没有被修改,使用的是相对时间,不需要关心客户端与服务端的时间偏差。

  • 如果没有被修改,则开始继续传送数据,服务器返回200 OK
  • 如果文件被修改了,则不传输,服务器返回412 Precondition failed(预处理错误)

这两个的区别是一个修改了才下载,一个是没修改才下载。

如果在服务器上,一个资源被修改了,但是他的实际内容根本没有发生变化,会因为Last-Modified时间匹配不上而返回了整个数据给客户端(即使客户端缓存里面有一个一模一样的资源),为了解决这个问题,我们就需要用到HTTP 1.1的Etag

Etag:服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)

If-Match:条件请求,携带上一次请求中资源的Etag,服务器根据这个字段判断文件是否有新的修改

If-None-Match:再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识,服务器接收到报文后发现If-None-Match则与被请求的资源的唯一表示进行对比

  • 不同,说明资源被修改过,则相应整个资源内容,返回状态码200
  • 相同,说明资源没有被修改,则响应header,浏览器直接从缓存中获取数据信息,服务器返回状态码304

Etag 的精度比 Last-modified 高,属于强验证,要求资源字节级别的一致,优先级高。如果服务器端有提供 ETag 的话,必须先对 ETag 进行 Conditional Request。

但是实际应用中由于Etag的计算是由算法得来的,而算法会占用服务器计算的资源,所有服务器端的资源都是宝贵的,所以就很少使用Etag

  • 浏览器输入URL地址,回车之后,浏览器发现缓存中已经有这个文件了,就不用继续请求,直接从缓存中获取数据信息
  • F5刷新,就是浏览器向服务器发送一个请求,携带If-Modified-Since来查看文件是否过期
  • Ctrl + F5强制刷新,就是先吧缓存中文件删除,然后再去服务器请求一个完整的资源,于是客户端就完成了强制更新的操作了

缓存场景

对于大部分的场景都可以使用强缓存配合协商缓存来解决,但是在一些特殊情况下,可能需要选择特殊的缓存策略

  • 对于某些不需要缓存的资源,可以使用Cache=Control: no-store来表示该资源不需要缓存
  • 对于频繁变动的资源,可以使用Cache-Control: no-cache并且配合Etag使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新
  • 对于代码文件来说,通常使用Cache-Control: max-age=31536000并且配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立即下载新的文件

15. 手写call、apply、bind

Function.prototype.myCall = function (obj = window) {
  obj.fn = this
  const args = [...arguments].splice(1)
  const result = obj.fn(...args)
  delete obj.fn
  return result
}

Function.prototype.myApply = function (obj = window) {
  obj.fn = this
  const args = arguments[1]
  let result = args ?
    obj.fn(...args) :
    obj.fn()
  delete obj.fn
  return result
}

Function.prototype.myBind = function (obj = window) {
  const that = this
  const args = [...arguments].splice(1)
  return function () {
    return that.apply(obj, [...args, ...arguments])
  }
}

// 然后使用即可
function log() {
  console.log(this)
  console.log(this.value)
  console.log(arguments)
}

const obj = {
  value: 123
}

log.myCall(obj, 1, 2, 3)
log.myApply(obj, [1, 2, 3])
log.myApply(obj)
log.myBind(obj)()
log.myBind(obj)(1, 2, 3)
log.myBind(obj, 4, 5, 6)(1, 2, 3)
复制代码

16. 伪类和伪元素的区别

伪类和伪元素是为了修饰不在文档树中的部分,比如一句话的第一个字母,列表中第一个元素。

伪类

伪类用于当已有元素处于某种状态时候,为其添加对应的样式,这个状态是根据用户行为变化而变化的。比如说hover。虽然他和普通的css类似,可以为已有的元素添加样式,但是他只有处于dom树无法描述的状态才能为元素添加样式,所以称为伪类

伪元素

伪元素用于创建一些原本不在文档树中的元素,并为其添加样式,比如说:before。虽然用户可以看到这些内容,但是其实他不在文档树中。

区别

伪类的操作对象是文档树中已存在的元素,而伪元素是创建一个文档树外的元素。因此,伪类与伪元素的区别就在于:有没有创建一个文档树之外的元素

css规范中使用两个双冒号::来表示伪元素,一个冒号:来表示伪类。例如:::before:hover

17. 绝对定位

  • 父元素是块级元素,则子元素设置为父元素内边距的边界
  • 父元素是行内元素,则子元素设置为父元素的内容边界

附上层叠上下文表

image

18. window.onload,doument.onload的区别

  • window.onload:指页面上所有的dom,样式表,脚本,图片,flash都已经加载完成了的时候触发
  • document.onload:指dom加载完成,不包括别的东西

19. 事件委托

事件委托就是把一个元素响应时间(click,keydown....)的函数委托到另一个元素。一般来说,会把一个或者一组元素的时间委托到父层或者更外层元素上。真正绑定事件的是外层元素,当事件响应到需要绑定的元素时候,会通过事件冒泡机制,从而触发他的外层元素的绑定事件上,然后再外层函数执行

  • 类似keydown,onclick这种鼠标事件,键盘事件,支持冒泡的事件类型才有事件委托
  • 类似接口事件:change,submit这种就没有事件代理

事件冒泡

image

事件模型分为三个阶段

  • 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件
  • 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上
  • 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层一层向外到最外层元素(根节点),事件代理就是利用事件冒泡的机制,把内层需要响应的事件绑定到外层

事件委托的优点

  1. 减少内存消耗 假设有一个列表,里面很多li,如果为他们每一个都绑定事件就很浪费内存,最好是把这个时间绑定到外层ul上统一处理
<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>
// ...... 代表中间还有未知数个 li
复制代码

所以事件委托可以大量减少内存的消耗,节约效率

  1. 动态绑定事件 假如我们需要做到动态增删列表内的元素,那么在每一次改变的时候又得重新进行事件绑定。如果用了事件委托就没有这烦恼了,因为事件绑定在父元素上,跟目标元素增删没有关系。

所以事件委托可以动态绑定时间,减少很多重复工作

window.onload = () => {
  const list = document.getElementById('list')
  list.addEventListener('click', function (event) {
    console.log(event.target)
  })
}

<ul class="list" id="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
复制代码

这样点击li标签的时间就会冒泡到ul上面,然后开始执行绑定的方法

注意

分情况分析:

  • 有拿到节点的,优先捕获,没有才往上冒泡寻找
  • 若是通过node.addEventListener('event',callback,bubble or capture); 谁先调用谁先执行

20. instanceof的的实现原理

while (x.__proto__) {
  if (x.__proto__ === y.prototype) {
    return true
  }
  x.__proto__ = x.__proto__.__proto__
}
if(x.__proto__ === null){
  return false
}
复制代码

判断x对象是否为y的一个实例,会在原型链上一直找,找到则返回true

21. 继承

  • 原型继承
Student.prototype = new Person('b')
Student.prototype.constructor = Student
复制代码

缺点:

  1. 子类型无法超类型传递参数
  2. 子类的方法必须写在new Person后面,不然会被覆盖
  • 类式继承
function Child(name, parentName) {
    Parent.call(this, parentName);
    this.name = name;
}
复制代码

缺点:

  1. 没有原型,每次创建都会执行Parent.call
  • 组合继承
function Child(name, parentName) {
    Parent.call(this, parentName);
    this.name = name;
}

Child.prototype = new Parent();      
Child.prototype.constructor = Child;
复制代码

缺点:

  1. 父类构造函数会被调用两次
  • 寄生组合
function Person(name) {
  this.name = name
}

Person.prototype.a = function () {
  console.log('person a')
}

Person.prototype.b = function () {
  console.log('person b')
}

function Student(name, superName) {
  Person.call(this, superName)
  this.name = name
}

Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student

Student.prototype.b = function () {
  console.log('student b')
}

const person = new Person('p')
const student = new Student('s')

console.log(person)
console.log(student)

person.a()
person.b()
student.a()
student.b()

console.log(student instanceof Person)
复制代码

22. for...in 和 for...of

  • for...in for...in会遍历可遍历对象的所有属性
const arr = [5, 4, 3, 2, 1]
arr.name = 'name'

for(let i in arr){
  console.log(i) // 0 1 2 3 4 name(字符串类型的索引)
}
复制代码

输出的是0 1 2 3 4 name,因为我们给arr加了一个name的属性,for...in也会把他遍历出来

所以for...in一般用于遍历对象,值是他的键值

  • for...of
const arr = [5, 4, 3, 2, 1]
arr.name = 'name'

for(let i in arr){
  console.log(i) // 0 1 2 3 4 name(字符串类型的索引)
}
复制代码

输出的是5 4 3 2 1,输入数组里面每个的值

总结

  • for...of使用遍历数组/数组对象/字符串/map/set等拥有迭代器对象,但是不能遍历对象,因为对象没有迭代器对象。他可以使用break,continue,return
  • for...in来遍历对象,或者使用Object.keys()
  • for...in遍历的是数组的索引(键名),for...of是数组的值

23. 数组扁平化

const test = [
  [1, 2, 2],
  [3, 4, 5, 5],
  [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
]

// 要求将以上数组扁平化排序且去重
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

// 递归版
function flat(arr) {
  let list = []
  arr.forEach(item => {
    if (Array.isArray(item)) {
      list.push(...flat(item))
      // list = list.concat(flat(item))
    } else {
      list.push(item)
    }
  })
  return list
}

const res1 = [...new Set(flat(test))].sort((a, b) => a - b)

// ES6版
const res2 = [...new Set(test.flat(Infinity))].sort((a, b) => a - b)

console.log(res1)
console.log(res2)
复制代码

24. 前端性能优化

csspod.com/frontend-pe…

编写时候

  • 写代码的时候注意内存泄露(定时器、事件监听、闭包、全局变量、dom引用等)

  • css部分不要用table布局、css表达式等会引起回流的样式、可以开启3d硬件加速(perspective、backface-visibility修复闪烁)

  • js部分不要频繁修改样式、操作dom、可以使用防抖节流等、脚本放在页面底部

  • webpack部分可以压缩js,css、分离公共资源例如ui组件库等、配置组件按需加载(import()实现)

  • 浏览器部分可以设置缓存,使用cdn

  • 图片可以压缩图片,使用webp格式、雪碧图等,图片多的时候可以用懒加载等方法

  • React部分可以使用shouldComponentUpdate、React.Memo、pureComponent、列表元素设置key,尽量在constructor里面绑定函数等

  • nginx可以配置负载均衡、多服务器可以用redis来保持会话

  • 页面内容

    • 减少HTTP请求数
    • 减少DNS查询
    • 避免重定向
    • 缓存Ajax请求
    • 延迟加载
    • 预先加载
    • 减少dom元素数量
    • 划分不同内容到不同域名
    • 尽量减少使用iframe
    • 避免404错误
  • 服务器

    • 使用CDN
    • 添加Expires或者Cache-Control响应头
    • 启用Gzip
    • 配置Etag
    • 尽早输出缓冲
    • Ajax请求使用get方法
    • 避免图片src为空
  • Cookie

    • 减少cookie大小
    • 静态资源使用无cookie域名
  • Css

    • 把样式表放在<head>
    • 不要使用CSS表达式
    • 使用<link代替@import
    • 不要使用filter
  • JavaScript

    • 把脚本放在页面底部
    • 使用外部JavaScript和Css
    • 压缩JavaScript和Css
    • 避免重复脚本
    • 减少dom操作
    • 使用高效的时间处理
  • 图片

    • 优化图片
    • 优化CSS Sprite
    • 不要再HTML中缩放图片
    • 使用体积小、可缓存的favicon.ico
  • 移动端

    • 保持单个文件小于25KB
    • 打包内容为分段(multipart)文档

25. js实现动画

将div平滑滑动,从快至慢,不能用css3的属性

<!DOCTYPE html>
<html>

<head>
  <script>
    window.onload = () => {
      var element = document.getElementById('t');

      function step(timestamp) {
        console.log(timestamp)
        var progress = timestamp;
        // element.style.left = (progress / 30) * (10 - progress / 1000) + 'px';
        element.style.left = Math.pow(progress / 5, .5) * 40 + 'px';
        if (progress < 5000) {
          window.requestAnimationFrame(step);
        } else {
          window.cancelAnimationFrame(step)
        }
      }

      window.requestAnimationFrame(step);
    }
  </script>
  <style>
    .container {
      position: relative;
      width: 1000px;
    }

    #t {
      position: absolute;
      width: 50px;
      height: 50px;
      background: red;
    }
  </style>
</head>

<body>
  <div class="container">
    <div id="t"></div>
  </div>
</body>

</html>
复制代码

26. 实现autocomplete属性

有一个input输入框,输入关键词的时候,模拟autocomplete效果补全

<!DOCTYPE html>
<html>

<head>
  <script>
    window.onload = () => {

      const input = document.getElementById('input')
      const words = ['珠海', '广州', '上海', '杭州', '成都']
      input.addEventListener("input", debounce(function (e) {
        const value = e.target.value
        const index = words.findIndex((item) => value && item.indexOf(value) !== -1)
        if (index !== -1) {
          e.target.value = words[index]
        }
      }, 500))

      function debounce(fn, wait = 500) {
        let timeout = null
        return function () {
          if (timeout) {
            clearTimeout(timeout)
          }
          timeout = setTimeout(() => {
            fn.apply(this, [...arguments])
          }, wait)
        }
      }
    }
  </script>
  <style>
  </style>
</head>

<body>
  <input id="input" />
</body>

</html>
复制代码

27. 代码实现深拷贝

const deepClone = (obj) => {
  let result = null
  if (typeof obj === 'object') {
    result = Array.isArray(obj) ? [] : {}
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (obj[key] && typeof obj[key] === 'object') {
          result[key] = deepClone(obj[key])
        } else {
          result[key] = obj[key]
        }
      }
    }
  } else {
    // 如果不是对象与数组,则直接赋值
    result = obj
  }
  return result
}
复制代码

或者使用自带方法

JSON.parse(JSON.stringify(obj))
复制代码

28. 乱序算法

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

简单版

arr.sort(() => Math.random() - 0.5)
复制代码

洗牌算法

const shuffle = (arr) => {
  let len = arr.length
  while (len > 1) {
    let index = Math.floor(Math.random() * len--);
    [arr[index], arr[len]] = [arr[len], arr[index]]
  }
  return arr
}
复制代码

29. 数组去重

const arr = [1, 1, 1, 2, 3, 4, 5, 5, 3, 2, 1, 1] 简单版

[...new Set(arr)]
复制代码

循环版

const removeDrop = function () {
  const result = []
  const map = {}
  arr.forEach((item) => {
    if (!map[item]) {
      map[item] = true
      result.push(item)
    }
  })
  return result
}
复制代码

30. React中的key作用

概述

key是用来帮助react识别哪些内容被更改、添加或者删除。key值需要一个稳定值,因为如果key发生了变化,react则会触发UI的重渲染。

  • 唯一性: key值必须唯一,如果出现了相同,会抛出警告,并且只会渲染第一个重复key值的元素。因为react会认为后续拥有相同key值的都是同一个组件。

  • 不可读性: 虽然在组件定义了key,但是子组件中是无法获取key值的

改变组件的key值的话,可以销毁之前的组件,再创建新的组件

注意

如果涉及到数组的动态变更,例如数组新增元素,删除,重新排列,这时候如果index作为key值,会导致显示错误的数据

image

{this.state.data.map((v,idx)=><Item key={idx} v={v} />)}
// 开始时:['a','b','c']=>
<ul>
    <li key="0">a <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">c <input type="text"/></li>
</ul>

// 数组重排 -> ['c','b','a'] =>
<ul>
    <li key="0">c <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">a <input type="text"/></li>
</ul>
复制代码

key相同,组件有变化,则只会更新变化的属性,如果key不同,则会销毁之前的组件,整个组件重新渲染

使用index作为key的场景

第一页
<ul>
    <li key="0">张三</li>
    <li key="1">李四</li>
    <li key="2">王五</li>
</ul>
第二页
<ul>
    <li key="0">张三三</li>
    <li key="1">李四四</li>
    <li key="2">王五五</li>
</ul>

复制代码

翻页后,子组件值发生变化,但key不变,只会发生更新,而不会重新渲染

反模式

现在有一个例子,我们不添加key的情况下,react会自动以索引的形式作为key值

let arr = ['first', 'second'];

// list1 和 list2 是等价的
const list1 = arr.map(item => <p>{item}</p>);
const list2 = arr.map((item, index) => <p key={index}>{item}</p>);
复制代码

接着我们向数组末尾加一个元素,react经过diff算法之后,key值0和1的元素没有发生改变,所以UI上的操作只是仅仅在末尾添加多一个元素

但是如果在开头添加一个元素

<!-- before -->
<p key="0">first</p>
<p key="1">second</p>

<!-- after -->
<p key="0">zero</p>
<p key="1">first</p>
<p key="2">second</p>
复制代码

所以元素的key值都发生了改变,这样每个元素都会重新渲染一次,性能就大大受到影响了

总结

简而言之,改变 key 值来重渲染组件是一种——相较于复杂componentWillReceiveProps生命周期——十分低成本的方式。

31. React系列

组件的render函数在何时被调用

每一次state的更改都会使得render函数被调用,但页面的dom不一定会发生修改

组件的生命周期

  1. 初始化阶段(Mounting)
  2. 更新阶段(Updating)
  3. 析构阶段(Unmounting)

初始化阶段:

  • constructor(): 用于绑定事件,初始化state
  • componentWillMount(): 组件将要挂载,在render之前调用,每一个组件render之前就调用,一般在这操作state。可以在服务端调用
  • render(): 用作渲染dom
  • componentDidMount(): 在render之后,而且是所有子组件都render之后才调用,通常用于异步请求数据,因为在这里组件都初始化完成了

更新阶段:

  • componentWillReceiveProps(nextProps): 在这里可以拿到即将改变的状态,可以在这里通过setState方法设置state
  • shouldComponentUpdate(nextProps, nextState): 他的返回值决定了接下来的声明周期是否会被调用,默认返回true
  • componentWillUpdate(): 不能在这里改变state,否则会陷入死循环
  • componentDidUpdate(): 和componentDidMount()类似,在这里执行Dom操作以及发起网络请求

析构阶段

  • componentWillUnmount(): 主要执行清除工作,比如取消网络请求,清除事件监听

Virtual Dom

难点在于如何判断旧的对象和新的对象之间的差异 react的办法是只对比同层的节点,而不是跨层对比
步骤分为两步:

  • 首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步会将每一个节点添加索引,便于最后渲染差异
  • 一旦节点有子元素,就去判断子元素是否有不同

Virtual Dom的算法实现主要是三步

  1. 通过js来模拟创建dom对象
  2. 把虚拟dom转换成真实dom插入页面中
  3. 发生变化时候,比较两颗树的差异,生成差异对象
  4. 根据差异对象渲染差异到真实dom

setState同步?异步?

  1. setState只在合成事件(JSX中的onClick、onChange等)和钩子函数中是异步的,在原生事件和setTimeout中都是同步的
  2. setState的“异步”并不是说内部是由异步代码实现的,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,当然也可以用setState第二个参数来获得
  3. setState的批量更新优化是建立在“异步”(合成事件,钩子函数)上的。在原生时间和setTimeout中不会批量更新,在“异步”中如果对一个值多次进行setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新
class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}
复制代码

因为在钩子函数里面setState是异步的,所以前两次无法获得val的值,所以都输出0,然后到setTimeout的时候,setState已经更新了,而且因为是批量更新所以只执行最后一次,所以到setTimeout的时候,val是1。因为setTimeout中是同步的,所以第一次是2,第二次是3

32. React常见面试题

调用setState之后发生了什么

在代码调用setState函数之后,React会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation),经过调和过程,React会以相对高效的方式根据新的状态构建React元素树,并且重新渲染整个UI界面。React得到元素树之后,会计算出新树与老树的节点差异,然后根据差异对界面进行最小化的重渲染。在差异算法中,React能够相对精确的知道那些地方发生了改变,这就保证了按需更新,而不是全部重新渲染

React中Element和Component的区别

React Element是描述屏幕上可见内容的数据结构,是对于UI的对象表述。而React Component这是可以接受参数输入并且返回某个React Element的函数或者类

什么情况下你会优先选择使用Class Component而不是Functional Component

当组件需要内部状态以及生命周期函数的时候就选择Class Component,否则就是用Functional Component

React中refs的作用是什么

Refs是React提供的可以安全访问dom元素或者某个组件实例的句柄,可以为元素添加ref属性然后再回调函数中接受该元素在dom树中的句柄

React中keys的作用

keys是React用于追踪列表中哪些元素被修改、添加或者删除的辅助标识,需要保证key的唯一性。React diff算法中,会借助key值来判断元素是新建的还是移动而来的,从而减少不必要的元素重渲染。此外React还需要借助Key值来判断元素与状态的关联关系

受控组件与非受控组件的区别

  • 受控组件指那些将表单数据给React统一管理的组件,需要使用setState来更新表单的值
this.state = {
    value: ''
}

...

handleChange = (e) => {
    this.setState({
        value: e.target.value
    })
}

...

<input onChange={() => this.handleChange} />
复制代码
  • 非受控组件就是由dom来存放表单数据,我们可以使用refs来操控dom元素
handleChange = () => {
    console.log(this.input.value)
}

...

<input refs={(input) => this.input = input}
    onChange={() => this.handleChange} />
复制代码

尽管非受控组件比较容易实现,但是我们有时候会需要对数据进行处理,使用受控组件会更加容易

在声明周期中哪一步应该发起Ajax请求

应该放在componentDidMount()

  • Fiber调和算法会影响到compnentWillMount的触发次数,如果放在里面,可能会发起多次Ajax请求
  • 如果放在其他生命周期中,假设我们获取了Ajax请求的结果,并且添加到组件的状态中,未挂载的组件就会报错。而在componentDidMount中就不会有这些问题了

shouldComponentUpdate的作用是什么,为何他如此重要

shouldComponentUpdate允许我们手动的判断组件是否更新,就可以避免不必要的更新渲染

React中的事件处理逻辑

为了解决跨浏览器兼容性的问题,React会将浏览器原生事件封装为合成事件传入设置的事件处理器中。合成事件与原生时间采用相同的接口,不过他们屏蔽了底层浏览器的细节差异,保证了行为的一致性。React没有将事件依附在子元素上,而是将所有事件发送到顶层 document 进行处理

JSX上面的事件全部绑定在 document 上,不仅减少了内存消耗,而且组件销毁的时候可以统一移除事件

如果我们不想事件冒泡的话,应该是调用event.preventDefault,而不是传统的event.stopPropagation

createElement和cloneElement的区别是什么

createElement函数是JSX编译之后使用的创建Element的函数,而cloneElement这是复制某个元素并传入新的Props

setState函数的第二个参数的作用是什么

是一个回调函数,这个函数会在setState函数调用完成并且组件开始重渲染的时候被调用,我们可以通过这个函数来判断更新渲染是否完成

为什么虚拟dom会提高性能

虚拟dom相当于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的dom操作,从而提高性能 具体步骤如下:

  1. 使用js来模拟创建一个dom
  2. 将这个虚拟dom构建真实dom,插入到页面中
  3. 状态变更时,比较两颗对象树的差异,生成差异对象
  4. 根据差异对象进行更新

diff算法

  • 把树形结构按照层次分解,只比较同级元素
  • 为列表结构的每一个元素都添加唯一的key属性,方便比较
  • 如果是同一类型的组件,则按照tree diff一样遍历
  • 如果不是同一类,则该组件判断为dirty component,从而替换整个组件下的所有子节点,就不用花时间来比较差异了
  • 合并操作,调用setState的时候会将组件标记为dirty,到一个事件循环结束,React检查所有标记dirty的component重新绘制。这样dom只会被更新一次,节约性能
  • 选择性子树渲染,利用shouldComponentUpdate来提高diff性能

diff算法的三种优化策略

  1. 树的优化策略
  • 层级控制,diff树同层只比较同一层次的节点
  • 如果某个节点不存在,则这个节点直接删除
  • 跨层级比较会进行相应的创建或者删除
  1. 组件的优化策略
  • 是否为同一类型,有则使用tree diff
  • 不是同一类型,则判断为dirty component。从而替换掉整个组件下的子节点
  • 对于同一类型的组件,可以使用shouldComponentUpdate来手动终止diff算法,加快渲染速度
  1. 元素的优化策略
  • 插入,全新的节点需要执行插入操作
  • 移动,节点本身存在,需要进行移动
  • 删除,结构相同内容不同,无需直接复用,需要删除重建
  • 基于唯一标记key来进行计算,快速定位

react性能优化

  • 利用shouldComponentUpdate来避免不必要的dom操作
  • 使用key来帮助React识别列表子组件的最小变化
  • React.memo
  • SSR
  • render里面不要使用箭头函数或者bind,应该再constructor中bind

react的坑点

  • componentWillUnmounted
  • this.setState不同步
  • pureComponent如果prop是多层对象,则不更新

简述Flux思想

Flux最大的特点就是,数据单向流动

  1. 用户访问View
  2. View发出用户的Action
  3. Dispatcher收到Action,要求Store进行相应的更新
  4. Store更新后,发出一个“change”事件
  5. View收到“change”事件后,更新页面
  6. 使用防抖需要保持事件引用event.persist()

展示组件(Presentational component)和容器组件(Container component)之间有何不同

  • 展示组件关心组件看起来是什么,专门通过props来接收数据和回调,一般不会有自己的状态
  • 容器组件关心组件如何运作,容器组件会为展示组件或者其他容器组件提供数据和行为,一般拥有自己的状态,因为他是其他组件的数据源

组件的状态(state)和属性(props)有何区别

  • state是一种数据结构,用于组件挂载时所需的数据默认值。一般由用户事件行为改变state
  • props是组件的配置,由父组件传给子组件,props不可改变。

什么是高阶组件

高阶组件是一个以组件为参数并返回一个新组建的函数。HOC运行重用代码、逻辑和引导抽象。最常见是Redux的connect函数。HOC最好的方式是分享组件之间的行为。如果发现一个地方大量代码用来做一件事情,可以考虑重构为HOC

为什么建议setState的参数是一个callback而不是对象

因为state和props的更新可能是异步的,不能依赖他们去计算下一个state

除了在构造函数绑定this,还有其他办法吗

在回调中可以使用箭头函数,但是问题就是每次渲染都会创建一个新的回调

构造函数中为什么要调用super

因为super()被调用之前,子类是不可以使用this的

React的三种构建组件的方式

React.createClass、ES6 class、无状态函数

useEffect和useLayoutEffect的区别

调用时间不同

  • useEffect是在组件渲染完之后执行
  • useLayoutEffect是在组件渲染前就执行

useLayoutEffect比useEffect先执行

实现 useState 和 useEffect

React是使用类似单链表的形式来代替数组,通过next按顺序串联所有hook

useEffect特点:

  1. 两个参数:一个回调函数,一个依赖数组
  2. 如果依赖数组不存在,则callback每次render都触发
  3. 如果依赖数组存在,只有他发生改变的时候才执行callback

image

let arr = [] // 一个数组,存储每一个hook
let index = 0 // 下标,存取每个hook对应的位置

function useState(init){
    arr[index] = arr[index] || init // 如果存在则用之前的值
    const cur = index // 设置一个变量存储当前下标
    
    function setState(newState){
        arr[cur] = newState
        render() // 渲染,页面触发更新
    }
    
    return [arr[index++], setState]
}

function useEffect(callback, deps){
    const hasNoDeps = !deps // 如果没有依赖,则直接每次更新
    const lastDeps = arr[index] // 获取上一次的依赖数组
    const hasChangeDeps = lastDeps
      ? !deps.every((e, i) => e === lastDeps[i]) // 如果每一项都相同
      : true
    if(hasNoDeps || hasChangeDeps){
        callback()
        arr[index] = deps
    }
    index++
}
复制代码

React新特性

  1. Lazy 和 Supense
import React, {Component, lazy, Suspense} from 'react'

const About = lazy(() => import(/*webpackChunkName: "about" */'./About.jsx'))

class App extends Component {
  render() {
    return (
      <div>
        // 我们需要用suspense来进行过渡,显示一个loading
        <Suspense fallback={<div>Loading...</div>}>
          <About></About>
        </Suspense>
        
        // 这样子是不行的,因为还没有加载完成不知道显示什么
        <!--<About></About>-->
      </div>
    );
  }
}

export default App;
复制代码
  1. 错误边界(Error boundaries)
class App extends Component {
  state = {
    hasError: false,
  }
  static getDerivedStateFromError(e) {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) {
      return <div>error</div>
    }
    return (
      <div>
        <Suspense fallback={<div>Loading...</div>}>
          <About></About>
        </Suspense>
      </div>
    );
  }
}
复制代码
  1. React.memo React.memo适用于函数组件,不适用于class组件

pureComponent类似,但是pureComponent提供的shouldComponentUpdate只是对比传入的props本身有没有变化,如果内部的值变了,他也不会更新。例如

class Foo extends PureComponent {
  render () {
    console.log('Foo render');
    return <div>{this.props.person.age}</div>;
  }
}

person: {
  count: 0,
  age: 1
}

// person.age++, 本来应该触发更新,但是不会因为props的第一级没有发生变化
复制代码

这时候我们可以用memo来优化一下

const Foo = memo(function Foo (props) {
  console.log('Foo render');
  return <div>{props.person.age}</div>;
})
复制代码

memo的第一个参数是函数组件,第二个是与shouldComponentUpdate类似是一个函数

(nextProps, nextState)=>{
    
}
复制代码
  1. hook的优势
  • 函数组件没有this的问题
  • 自定义hook方便复用状态逻辑
  • 副作用关注点分离
  • 写起来更加简单、易懂
  1. hook的缺点
  • hook的运行次数可能会比你预期的还要多
  • 有点难以维护useEffect的依赖项

React 状态提升

常见于子组件的数据存在父组件中

// 父组件
class AllInput extends React.Component {
    constructor(props) {
        super(props)
        this.state = { content: '' }
        this.handleContentChange = this.handleContentChange.bind(this)
    }
    handleContentChange(newContent) {
        this.setState({ content: newContent })
    }
    
    render() {
        return (
            <div>
                <Input content={ this.state.content } onContentChange={ this.handleContentChange }/>
                <br /><br />
                <Input content={ this.state.content } onContentChange={ this.handleContentChange }/>
            </div>
        )
    }
}


// 子组件
class Input extends React.Component {
    constructor(props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
    }

    handleChange(e) { 
        setState(修改数据的方法)
        this.props.onContentChange(e.target.value)
    }

    render() {
        return (
            <input type='text' value={ this.props.content } onChange={ this.handleChange } />
        )
    }
}
复制代码

高阶函数

高阶函数就是接受一个或者多个函数然后返回一个函数的东西

主要功能有两个

属性代理

属性代理有三个常用的作用

  • 操作props
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            const newProps = {
                name: '大板栗',
                age: 18,
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };
}
复制代码

这样子,就可以将自定义的props传给子组件了

  • 抽离state
// 普通组件Login
import React, { Component } from 'react';
import formCreate from './form-create';

@formCreate
export default class Login extends Component {
  render() {
    return (
      <div>
        <div>
          <label id="username">
            账户
          </label>
          <input name="username" {...this.props.getField('username')}/>
        </div>
        <div>
          <label id="password">
            密码
          </label>
          <input name="password" {...this.props.getField('password')}/>
        </div>
        <div onClick={this.props.handleSubmit}>提交</div>
        <div>other content</div>
      </div>
    )
  }
}

//HOC
import React, { Component } from 'react';

const formCreate = WrappedComponent => class extends Component {

  constructor() {
    super();
    this.state = {
      fields: {},
    }
  }
  onChange = key => e => {
    const { fields } = this.state;
    fields[key] = e.target.value;
    this.setState({
      fields,
    })
  }
  handleSubmit = () => {
    console.log(this.state.fields);
  }
  getField = fieldName => {
    return {
      onChange: this.onChange(fieldName),
    }
  }
  render() {
    const props = {
      ...this.props,
      handleSubmit: this.handleSubmit,
      getField: this.getField,
    }
    return (<WrappedComponent
      {...props}
    />);
  }
};
export default formCreate;
复制代码

例如这样,就可以将输入框的state抽离出来

  • 操作refs
import React, { Component } from 'react';

const refHoc = WrappedComponent => class extends Component {

  componentDidMount() {
    console.log(this.instanceComponent, 'instanceComponent');
  }

  render() {
    return (<WrappedComponent
      {...this.props}
      ref={instanceComponent => this.instanceComponent = instanceComponent}
    />);
  }
};

export default refHoc;
复制代码

这样就可以获取组件的示例了
不可以再无状态组件(函数类型组件)上使用refs,因为无状态组件没有实例

反向继承

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}
复制代码

反向继承就是一个函数接收一个组件作为参数传入,然后返回一个继承该组件的类,在该类的render里面调用super.render()

  • 可以操作state,但是会难以维护
  • 渲染劫持

渲染劫持我们可以控制组件的render,有条件的展示组件 例如

  1. 条件渲染
function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}
复制代码

这样就可以按条件来控制组件渲染了

  1. 修改render输出
  //hijack-hoc
  import React from 'react';

  const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent {
    render() {
      const { style = {} } = config;
      const elementsTree = super.render();
      console.log(elementsTree, 'elementsTree');
      if (config.type === 'add-style') {
        return <div style={{...style}}>
          {elementsTree}
        </div>;
      }
      return elementsTree;
    }
  };

  export default hijackRenderHoc;
  
  //usual
  @hijackRenderHoc({type: 'add-style', style: { color: 'red'}})
  class Usual extends Component {
    ...
  }
复制代码

HOC存在的问题

  • 静态方法丢失
  • refs属性不能透传
  • 反向继承不能保证子组件完整被解析

HOC约定

  • props保持一致
  • 不能再函数式(无状态)组件中使用refs,因为他没有
  • 不要改变子组件
  • 可以添加包装名便于调试
  • render里不要使用HOC

应用场景

  1. 权限控制
  2. 页面复用

React组件通信

  • 父组件传子组件props
  • 子组件调用父组件的回调函数
  • 使用context
  • 使用EventEmitter

为什么选择React而不是Vue

  • React是函数式思想,状态逻辑什么的都是通过参数传入
  • 通过js来操作一切,css什么的也可以使用js的方式
  • 社区十分强大,很多功能由社区一起努力做出
  • UI库很棒
  • JSX让组件变得可读性更好,组件之间的结构看的比较清楚
  • 支持服务端渲染,更好的SEO和渲染性能

React 与 MVVM 的关系

  • M(odal): 对应组件中生命周期啊,state这些数据
  • v(iew): 对应虚拟dom生成的真是dom,还有样式等
  • V(iew)-M(odal): 对应组件中的JSX会通过diff算法将虚拟dom转换为真实dom

React的单向绑定与双向绑定的区别

一般只有UI表单控件需要双向绑定。

  • 单向绑定值UI表单变化的时候需要我们手动更新状态,也就是设置监听事件onChange
  • 双向绑定则自动更新

在表单交互很多的时候,单向绑定数据更容易追踪管理和维护,但是需要写较多的代码

33. CSRF 和 XSS

区别

  • CSRF需要登录之后操作,XSS不需要
  • CSRF是请求页面api来实现非法操作,XSS是向当前页面植入js脚本来修改页面内容

CSRF

跨站点伪造请求
用户在一个网站登录之后,产生一个cookie,此时若打开了一个新的网址,此网址返回了一些恶意的请求,就属于CSRF攻击

  • 预防
  1. 验证http reference
  2. 请求地址中添加token验证
  3. 请求头中添加token验证

XSS

跨站脚本攻击,一般是通过web页面插入恶意js代码来攻击。 主要分为三类:

  1. 反射性: xss代码在请求url中攻击
  2. 存储型: 将攻击脚本存入服务端,从而传播
  3. dom型: 通过dom修改页面内容
  • 预防
  1. 输入过滤: 例如过滤<script>
  2. 输出转义: 例如将<,>,/等字符利用转义符号转换一下
  3. 使用httponly: 让js脚本无法访问cookie
  4. 尽量使用post方法,使用get的时候限制一下长度

总结

  • 加入token是为了防止CSRF的而不是XSS
  • CSRF的攻击是因为浏览器自动带上cookie,而不会自带token

34. localStorage 和 sessionStorage 和 cookie

localStorage

生命周期是永久,只能存字符串类型

  • 跨域 使用iframe与postMessage实现

sessionStorage

生命周期为,当前窗口或标签页未被关闭,一旦关闭则存储的数据全部清空

cookie

cookie生命周期在过期时间之前都有效,同浏览器下,所有同源窗口之间共享

cookie的属性

  • name: 必须,定义cookie的名称
  • value: 必须,指定cookie的值
  • expires: 指定过期时间,采用UTC或GMT格式,如果不设置cookie只会在当前会话(session)有效,浏览器窗口关闭cookie就会删除
  • domain: 指定cookie所在域名,默认为设定cookie时候的域名,所指定的域名必须是当前发送cookie的一部分,只有访问的域名匹配domain属性,cookie才会发送
  • path: 指定路径,必须是绝对路径,默认为请求该cookie的网页路径,只有path属性匹配向服务器发送的路径,cookie才会发送,只要path属性匹配发送路径的一部分就可以了,例如path=/blog,发送的路径是/blogroll也可以。path属性生效的前提是domain属性匹配
  • secure: 指定cookie只能在加密协议https下发送到服务器,如果通信是https协议,自动打开
  • max-age: 用来指定cookie有效期,max-age优先级高于expires
  • httponly: httponly属性用于设置该cookie不能被JavaScript访问,主要是为了防止xss工具盗取cookie

总结

  • 不同浏览器无法共享localStorage和sessionStorage
  • 相同浏览器不同页面之间可以共享相同的localStorage(页面属于相同域名和端口)
  • 相同浏览器不同标签页中无法共享sessionStorage的信息

35. BFC 及其作用

BFC就是格式化上下文,相当于一个容器,里面的元素和外部相互不影响,创建的方式有:

  1. html根元素
  2. float浮动
  3. 绝对定位元素position: fixed || absolute
  4. overflow不为visiable
  5. display为table、inline-block、flex

BFC主要的作用是

  1. 清除浮动
  2. 防止同一个BFC中相邻元素间的外边距重合
  3. BFC元素和浮动元素不会重叠
  4. BFC在计算高度时会把浮动元素也计算进去

36. 实现 (5).add(3).minus(2) 功能

Number.prototype.add = function(n) {
  return this + n;
};
Number.prototype.minus = function(n) {
  return this - n;
};
复制代码

37. 连续赋值

例1:

var a = 3;
var b = a = 5;

=

var a = 3
var a = 5
var b = a
复制代码

所以最后a和b都是5

例2:

var a = { n: 1 }
var b = a

a.x = a = { n: 2 }
复制代码

一开始a和b都引用对象{n:1},然后到赋值语句
如果赋值语句中出现.则他会比=先一步执行,也就是说,我们先给a.x开辟一个空间
也就是说现在的a和b都是变成了

{
  n: 1,
  x: {
      n: 2
  }
}
复制代码

接下来到a的赋值了,a现在就变成了

{
    n: 2
}
复制代码

38. display: none, visibility: hidden 和 opacity: 0

  • display: none: 不占空间,不能点击,一般用于隐藏元素,会引发回流,性能开销大,非继承属性,子孙节点改变也无法显示(父元素为none)
  • visibility: hidden: 占据空间,不能点击,显示不会导致页面结构变化,不会撑开,属于重绘操作,是继承属性,子孙节点可以通过修改显示出来
  • opacity: 0: 占据空间,可以点击,一般与transition一起用

39. for 和 forEach 的性能

for 比 forEact 快 因为

  • for没有额外的函数调用栈,和上下文

40. react-router的<Link><a> 有何区别

是禁用了标签的默认事件,他只会更新匹配的页面的内容,而不会刷新整个页面。他使用的是`history`来进行跳转,会改变url,但是不会刷新页面

41. 执行上下文

  • 全局执行上下文:默认的上下文,任何不在函数内部的代码都在全局上下文里面。他会执行两件事情: 创建一个全局的window对象,并且设置this为这个全局对象。一个程序只有一个全局对象
  • 函数执行上下文:每当一个函数被调用,就会为该函数创建一个新的上下文,每个函数都有自己的上下文,不过是在函数被调用的时候创建的。函数上下文可以有任意多个,每当一个新的执行上下文被创建,他会按照定义的顺序执行一系列的步骤
  • Eval函数执行上下文:执行在eval函数内部的代码有他自己的执行上下文

42. 执行栈

执行栈就是一个调用栈,是一个后进先出数据结构的栈,用来存储代码运行时创建的执行上下文

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');
复制代码

image

43. 创建执行上下文

分为两个阶段

  • 创建阶段
  • 执行阶段

创建阶段

代码执行前会创建执行上下文,会发生三件事

  1. this绑定
  2. 创建词法环境组件
  3. 创建变量环境组件

this绑定

全局执行上下文中,this指向全局对象

函数执行上下文中,this取决于函数是如何被调用的。如果他被一个引用对象调动,那么this会设置成那个对象。否则是全局对象

let foo = {
  baz: function() {
  console.log(this);
  }
}

foo.baz();   // 'this' 引用 'foo', 因为 'baz' 被
             // 对象 'foo' 调用

let bar = foo.baz;

bar();       // 'this' 指向全局 window 对象,因为
             // 没有指定引用对象
复制代码

词法环境

是一个用代码的词法嵌套结构定义标识符和具体变量和函数关联的东西。用来存储函数声明和let, const声明的变量绑定

变量环境

同样是一个词法环境,用来存储var变量绑定

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);
复制代码

只有在调用函数multipy的时候才会创建函数执行上下文,let和const一开始不会关联任何值,但是var已经会设置为undefined了,这就是常说的变量声明提升

44. 事件循环

任务分为两类

  • 同步任务
  • 异步任务

image

宏任务

  • 主体代码
  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame

微任务

  • process.nextTick
  • mutation.observer
  • Promise.then cath finally

45. link 与 @import

  • link属于html标签,@import属于css提供的
  • 页面加载的时候link就加载,而@import是等页面加载完成之后再加载css
  • link不存在兼容性问题
  • link权重更大

46. 外边距重叠

  • 两个相邻的外边距都是正数时,折叠效果是两者之间较大的
  • 两个相邻的外边距都是负数时,折叠效果是绝对值最大的
  • 两个外边距一正一负时候,效果是相加起来

47. 渐进增强 和 优雅改进

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

区别:渐进增强是向上兼容,优雅改进是向下兼容

48. 立即执行函数

作用

  • 不破坏全局的命名空间
  • 如需使用,要将变量传入
(function(x){
  console.log(x);
}(12345))
复制代码

49. HTTP2.0 与 HTTP1.1

HTTP2.0

  1. HTTP2.0基本单位为二进制,以往是采用文本形式,健壮性不是很好,现在采用二进制格式,更方便更健壮
  2. HTTP2.0的多路复用,把多个请求当做多个流,请求响应数据分成多个帧,不同流中的帧交错发送,解决了TCP连接数量多,TCP连接慢,所以对于同一个域名只用创建一个连接就可以了
  3. HTTP2.0压缩消息头,避免了重复请求头的传输,又减少了传输的大小
  4. HTTP2.0服务端推送,浏览器发送请求后,服务端会主动发送与这个请求相关的资源,之后浏览器就不用再次发送后续的请求了。
  5. HTTP2.0可以设置请求优先级,可以按照优先级来解决阻塞的问题

HTTP1.1

  • 缓存处理新增etag、if-none-match之类的缓存头来控制缓存
  • 长连接,可以在TCP连接上发送多个请求和响应

50. css动画与js动画的区别

  • css性能好
  • css代码逻辑相对简单
  • js动画控制好
  • js动画兼容性好
  • js可以实现动画多
  • js可以添加事件

51. offset、client和scroll的属性

  • client:padding + content - 滚动条
  • offset:padding + content + boder
  • scroll:padding + 实际的content

52. 手撕代码题

  1. 输入根节点,打印标签路径,以及路径上data-v最大值
const root = document.getElementById("root");

function walk(node, path, max) {
  if (!node) {
    return;
  }
  path = [...path, node.tagName];
  max = Math.max(max, node.dataset.v);
  console.log(path, max);
  for (let child of node.children) {
    walk(child, path, max);
  }
}
walk(root, [], -1)

...

<div id="root" data-v="3">
  <p data-v="1">p1</p>
  <span data-v="2">
    span1
    <span data-v="4">span2</span>
    <span data-v="5">
      <p>1</p>
    </span>
  </span>
  <p data-v="99">p2</p>
</div>
复制代码
  1. 实现compose函数
var arr = [func1, func2, func3];
function func1(ctx, next) {
  ctx.index++
  next();
}
function func2(ctx, next) {
  setTimeout(function () {
    ctx.index++
    next();
  });
}
function func3(ctx, next) {
  console.log(ctx.index);
}

compose(arr)({ index: 0 });

function compose(arr) {
  return function (ctx) {
    if (arr.length !== 0) {
      let func = arr.shift()
      func(ctx, next)
    }
    function next() {
      if (arr.length !== 0) {
        let func = arr.shift()
        func(ctx, next)
      }
    }
  }
}
复制代码
  1. 放大镜

碎碎酱:codepen.io/yinxin630/p…

53. 行内元素的margin 和 padding

  • 水平方向:水平方向上,都有效
  • 垂直方向:垂直方向上,都无效,但是padding-bottom, padding-top会显示出效果,但是高度不会撑开,不会对周围元素有影响

54. 行内元素和块级元素

  • 行内、块级元素可以相互转换
  • 行内元素会在一条水平线上排列,块级则会在新的一行
  • 行内元素不可以设置宽高,但是可以设置行高(line-height)
  • 行内元素垂直方向上设置margin和padding无效,padding垂直方向有显示但是不会撑开,不会影响别的元素
  • 块级可以包含行内元素和块级元素,行内元素只可以包含行内元素和文本

55. margin、padding和translate百分比是按照什么计算的

  • margin:按照父元素的宽来计算的
  • padding:按照父元素的宽来计算的
  • translate:是按照本身的宽高计算的

56. display: inline-block元素之间有间隙

原因 inline-block对外是inline,对内是block,会将连续的空白符解析为一个空格

解决办法

  • 删除空格
  • 父元素设置font-size: 0
  • 父元素设置letter-spacing: -4px
  • 子元素设置vertical-align: bottom去除垂直间隙

57. HTTPS原理

HTTP是明文传输,传输的每一个环节都可能会被第三方窃取或者篡改。具体来说就是HTTP数据经过TCP层,然后经过WIFI路由器、运营商和目标服务器,都可能被中间人拿到数据并进行篡改,这就是常说的中间人攻击

HTTPS是一个加强版的HTTP。采用对称加密非对称加密结合的方式来保护浏览器和服务端之间的通信安全。 对称加密算法加密数据 + 非对称加密算法交换密钥 + 数字证书验证身份 = 安全

HTTPS由两部分组成:HTTP + SSL/TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据是加密后的数据

加密过程

首先浏览器会给服务器发送一个client_random和一个加密的方法列表

服务器接受后给浏览器返回另一个随机数server_random和加密方法

现在两者拥有三种相同的凭证: client_random, server_random加密方法

接着用这个加密方法将两个随机数混合起来生成密钥,这个密钥就是浏览器和服务器通信的暗号

对称加密和非对称加密

  • 对称加密是最简单的方式,指的是加密和解密用的是同样的秘钥,优点是保证了消息的保密性,但缺点是密钥可能会泄露

  • 非对称加密是说,如果有A、B两把密钥,用A加密过的数据包只能用B解密,反之用B加密的数据包只能由A解密,客户端在发消息的时候,先用公钥加密,服务器再用私钥解密。但是因为公钥不是保密的,可能会被窃取然后对消息进行篡改

数字证书和数字签名

为了解决非对称加密中公匙来源的不安全性。我们可以使用数字证书和数字签名来解决。

首先本地生成一对密钥,通过公钥去CA申请数字证书

  • 数字签名:CA拿到信息后会通过单向的hash算法把信息加密,然后生成一个摘要,然后CA会用自己的私钥对摘要进行加密,最后得出的结果就是数字签名了

    image

  • 数字证书:最后CA会将申请的信息还有数字签名整合在一起,就生成了数字证书了,其实也就是一个公钥

    image

  • 数字证书如何起作用

  1. 客户端用CA的公钥解密数字证书,如果数字证书来源合法,解密成功后,客户端就获得了信息摘要了。然后客户端会按照和CA一样的hash算法将申请信息生成一份摘要。
  2. 然后和解密出来的那份摘要进行对比,如果内容完整,则说明没有被篡改。客户端就可以从证书中拿到服务器的公钥和服务器进行安全的非对称加密通信了。服务器要想获得客户端的公钥也可以通过这个方式

握手过程

  1. 客户端申请https通信发送一个随机数,还有加密方法
  2. 服务器响应然后向客户端发送数字证书(公钥)还有随机数给客户端
  3. 客户端TLS验证证书,拿到公钥,如果没有问题就生成一个随机值,然后用公钥加密传给服务器
  4. 服务器收到后用私钥解密,拿到了对称密钥,之后可以通过这个密钥进行信息传输了,然后建立SSL连接通道
  5. 共享密钥交换成功,客户端和服务端开始加密通信
  6. 断开连接
    image
客户端: 你好,我要发起一个 HTTPS 请求,请给我公钥
服务器: 好的,这是我的证书,里面有加密后的公钥
客户端: 解密成功以后告诉服务器: 这是我的 (用公钥加密后的) 对称秘钥。
服务器: 好的,我知道你的秘钥了,后续就用它传输。
复制代码

使用

https加解密耗时比较长,也很消耗资源,如果不是安全性要求非常高可以不用

58. setImmediate() 和 process.nextTick()

  • setTimeout为在指定时间之后就执行
  • setImmediate设计是当前轮询阶段完成后就立即执行 如果放在同一个I/O循环内使用,setImmediate总是被优先调用,他的优势是如果在I/O周期内被调度的话,他会在所有定时器之前执行。他执行的方式是当前任务队列的尾部
  • process.nextTick触发的是在当前执行栈的尾部,下一次事件循环之前,总是在异步之前。

为什么要使用process.nextTick(),因为可以允许用户处理错误,或者是在事件循环继续之前重试请求

59. 浏览器和node的事件循环

浏览器任务分为宏任务微任务

  • 宏任务:script中的代码、setTimeout、setInterval、I/O、UI render
  • 微任务:promise、Object.observe、MutationObserver

node分为以下几种:

  • microTask:微任务
  • nextTick:process.nextTick
  • timers: 各类定时器
  • I/O callback:是否有已完成的I/O操作的回调函数,来自上一轮的轮训的残留,执行几乎所有的回调,但是不包括close,定时器还有setImmediate
  • idle,prepare:仅在内部使用
  • poll:等待新的I/O事件,会因timers和超时事件等结束时间,一般阻塞在这里
  • check:执行setImmediate的回调
  • close callback:关闭所有的closing handles,一些onclose事件

Node环境下

循环之前,会执行以下操作:

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()

开始循环 循环中进行的操作:

  • 清空timers队列,清空nextTick队列,清空microTask队列
  • 清空I/O队列,清空nextTick队列,清空microTask队列
  • 清空check队列,清空nextTick队列,清空microTask队列
  • 清空close队列,清空nextTick队列,清空microTask队列

image

区别

  • node环境下的setTimeout定时器会依次一起执行,浏览器是一个一个分开的
  • 浏览器环境下微任务的执行是每个宏任务执行之后,而node中微任务会在各个阶段之间执行,一个阶段结束立刻执行mircroTask

总结

浏览器环境下:

while(true){
    宏任务队列.shift()
    微任务队列全部任务()
}
复制代码

Node环境下:

while(true){
    loop.forEach((阶段)=>{
        阶段全部任务()
        nextTick全部任务()
        microTask全部任务()
    })
}
复制代码

60. React Fiber

原先React采用的是stack reconciler调度算法。页面可能会出现卡顿,因为setState之后React会立即开始调和过程,会遍历找不同,所有virtual dom都遍历完之后,才会渲染。在调和的过程时候,主线程被js占用了,所以交互、布局都会停止,就会卡顿

所以现在采用了Fiber reconciler来优化

  • 任务拆分
  • 更新暂停,终止和复用
  • 设置优先级
  • render可以返回多个元素

Scheduler

调度是fiber调和的一个过程

如果UI要有不错的表现,那就得考虑多点东西

  • 并不是所有state都需要立刻更新,例如不在可视范围内的
  • 不是所有更新优先级都是一样的,例如用户输入的优先级比请求资源展示要高
  • 高优先级的应该打断低优先级的更新

所以调和的过程是每次只做一个小小的任务,完成之后查看还有没有优先级高的任务需要处理

Fiber数据结构

image

  • return: 父节点
  • child:第一个子节点
  • sibling:兄弟节点
fiber {
  	stateNode: {},
    child: {},
    return: {},
    sibling: {},
}
复制代码

任务拆分

Fiber拆分的单位是按照虚拟dom节点来拆,因为fiber tree是根据虚拟dom来构造出来的

任务暂停

Fiber利用浏览器的apirequestIdleCallback来让浏览器空闲的时候执行某种任务

function fiber(剩余时间) {
 if (剩余时间 > 任务所需时间) {
 做任务;
 } else {
 requestIdleCallback(fiber);
 }
}
复制代码

使用requestAnimationFrame来渲染动画

优先级

{ 
 Synchronous: 1, // 同步任务,优先级最高
 Task: 2, // 当前调度正执行的任务
 Animation 3, // 动画
 High: 4, // 高优先级
 Low: 5, // 低优先级
 Offscreen: 6, // 当前屏幕外的更新,优先级最低
}
复制代码

61. webpack流程

  1. 初始化:从配置文件读取与合并参数,然后实例化插件new Plugin()
  2. 开始编译:通过上一步获取的参数,初始化一个Complier对象加载插件,执行Compiler.run开始编译
  3. 确定入口:根据配置中entry找出所有入口文件
  4. 编译模块:从entry出发,调用配置的loader,对模块进行转换,同时找出模块依赖的模块,一直递归,一直到找到所有依赖的模块
  5. 完成模块编译:这一步已经使用loader对所有模块进行了转换,得到了转换后的新内容以及依赖关系
  6. 输出资源:根据入口与模块之间的依赖关系,组装成chunk代码块,生成文件输出列表
  7. 输出成功:根据配置中的输出路径还有文件名,把文件写入系统,完成构建

62. webpack的热更新

主要依赖webpack, express, websocket

  • 使用express启动本地服务,当浏览器访问的时候做出相应
  • 服务端和客户端使用websocket实现长连接
  • webpack监听源文件的变化
    • 每次编译完成之后会生成hash值,已改动模块的json文件,已改动模块代码的js文件
    • 编译完成后通过socket向客户端推送当前编译的hash值
  • 客户端的websocket监听到有文件改动推送过来的hash值,会和上一次进行对比
    • 一致就走缓存
    • 不一致则通过ajax和jsonp获取最新的资源
  • 使用内存文件系统去替换有修改的内容实现局部更新

63. CommonJs 与 ES6模块化区别

  • CommonJs支持动态导入,ES6不支持
  • CommonJs是同步导入,ES6是异步导入
  • CommonJs导入的时候是值拷贝,如果导出变了他也不会变,但是ES6是引用的,如果导出变了,导入的部分也会变了

用法

  • 导出:CommonJs是module.exports,ES6是export / export default
  • 导入:CommonJs是const x = require('xx'),ES6是import xx from 'xxx'

64. 箭头函数和普通函数

普通函数

  • 一般this指向全局对象window
  • 作为对象方法调用的时候,this指向对象
  • 作为构造函数时候,this指代new的对象
  • 可以通过call、apply来改变this

箭头函数

  • this总是指向词法作用域
  • 无法通过call、apply改变this

65. node读取文件转换为buffer

const fs = require('fs')
const path = require('path')
const mimeType = require('mime-types') // 文件类型

const filePath = path.resolve('./01.png')
const data = fs.readFileSync(filePath)
const buffer = new Buffer(data).toString('base64')
const base64 = `data:${mimeType.lookup(filePath)};base64,${buffer}`
console.log(base64)

复制代码

66. sort函数

  • 数量小于10的数组使用插入排序
  • 数量大于10的数组使用快排

67. 闭包 和 自执行函数

闭包

优点

  • 可以将局部变量一直存在内存中
  • 可以避免使用全局变量

缺点:

  • 占用内存变多,可能导致内存泄漏

自执行函数

  • 避免作用域命名污染
  • 提升性能,减少对作用域的查找
  • 避免全局命名冲突
  • 保存闭包状态

区别

  • 立即执行函数声明完立刻执行,一般只用于一次
  • 闭包主要是为了让外部函数可以访问内部变量,减少了全局变量的使用

68. 0.1 + 0.2 !== 0.3

js采用IEEE 754的双精度标准来进行计算,如果他碰到无法整除的小数的时候,就会取一个近似值,如果足够接近就觉得是那个值了

69. React服务端渲染

服务端渲染就是React通过Node.js转换成HTML再返回给浏览器,这个过程被称为“同构”,因为应用程序的大部分代码都可以在服务器和客户端上运行

优点

与传统的SPA单页应用程序比,

  • 更好的SEO,因为搜索引擎等爬虫工具可以直接查看完全渲染的页面
  • 更好的用户体验,如果网络缓慢,或者运行缓慢的设备,服务器渲染就很好

弊端

  • 由于浏览器跟服务端有区别,document,window等可能获取不了,会报错
  • 服务器会占用更多的内存

70. 前端路由的原理

  • hash模式,后面带了个#,无论hash值怎么变,服务端都只会收到#号前的url,通过监听hashchange事件,根据hash来切换页面
  • history模式,主要使用history.pushState和history.replaceState来改变URL,改变url只会更新浏览器的历史记录,不会引起页面更新,通过监听popstate来改变页面内容,同时禁止a标签的默认事件。

hash模式不需要后端配置,兼容性好,history模式需要后端配置才能使用

www.cnblogs.com/jcxfighting…

71. Mobx原理

image
特点:

  • 开发难度低
  • 代码量少
  • 渲染性能好

工作原理

  1. 定义状态让他变成可观察
  2. 创建衍生来响应状态变化
  3. 使用action来改变状态

设计原则

Mobx支持单向数据流,动作改变状态,状态更新改变视图

  • Mobx通过不缓存数据,在需要的时候重新计算来保证所有衍生在一致的状态
  • 没有参与反应的衍生,就会被简单的垃圾回收

Mobx是同步运行的,有2个好处

  • 不会获得旧的衍生
  • 追踪堆栈会更简单

主要利用一个autoRun,这个函数可以让被我们用到的属性改变时候触发回调,而没被使用的属性发生改变则不会发生回调。

72. Object.create()实现原理

其实就是新建一个对象,然后覆盖对象的原型为传入的原型对象(类似继承)

function(constructor){
    const F = function(){}
    F.prototype = constructor
    const res = new F()
    return res
}
复制代码

73. 事件穿透

设置css3属性pointer-events: none

74. 常见http状态码

  • 200:成功,正常处理并返回
  • 204:处理成功,但是返回的主体信息没有内容
  • 301:永久重定向,请求的资源分配给另一个url
  • 302:临时重定向,希望本次访问可以通过另一个url来获取资源
  • 303:应该使用get来访问另一个url
  • 304:表示协商缓存可用
  • 400:请求中有语法错误
  • 401:未经许可,要验证
  • 403:拒绝访问,权限不够
  • 404:访问资源不存在
  • 500:请求异常,也可能是前端bug
  • 503:服务器停机维护,无法处理请求

75. js、css阻塞

js的async加载还有defer加载都不阻塞页面渲染还有资源加载

  • defer会按顺序加载,async乱序
  • defer是页面都解析完了在运行,立即下载延迟执行
  • async下载完成后立刻执行
  • 同时指定的话,async优先级高

内嵌和外部js

  • 内嵌js会阻塞所有资源的加载
  • 外部js会阻塞后面的内容呈现以及资源的下载

css

  • css不会阻塞dom解析,会阻塞渲染,因为要dom树跟style树结合生成渲染树才会渲染
  • css会阻塞后续js语句执行

如果css后面跟着内嵌js,则会出现阻塞情况,因为浏览器会维持css跟js顺序,样式表必须在嵌入的js之前加载完。

加载顺序

  • js在head中会立即执行,阻塞后续资源下载与执行。因为js可能会修改dom,不阻塞的话,dom操作顺序不可控。
  • js的执行依赖css。只有前面的css全部下载完,才会执行js

注意

  • css应放在head中,这样可以尽快响应,渲染页面,因为他不会阻塞资源下载
  • js应该放在body底部,让dom尽快渲染出来,避免dom被阻塞
  • css会阻塞js执行,他不会阻塞js下载,但是js会等到css加载玩之后才会执行

JS 代码在执行前,浏览器必须保证在 JS 之前的所有 CSS 样式都解析完成,不然不就乱套了,前面的 CSS 样式可能会覆盖 JS 文件中定义的元素样式,这是 CSS 阻塞后续 JS 执行的根本原因。

76. 隐式转换

  • 数字运算符
  • 点号操作符
  • if语句内
  • = =

blog.csdn.net/itcast_cn/a…

77. React首屏优化

  1. 使用浏览器缓存
  2. webpack的JS压缩,html压缩
  3. 提取公共资源
  4. 将webpack开发环境修改为生产环境
  5. 使用雪碧图
  6. 使用cdn

78. Object.assign

如果对象只有一层,则是深拷贝,如果是多层则是浅拷贝

const obj1 = {
  a: 1,
  b: {
    c: 2
  }
}

const obj2 = Object.assign({}, obj1)
console.log(obj1 == obj2)
console.log(obj1, obj2)
obj1.b = 2
console.log(obj1 == obj2)
console.log(obj1, obj2)
复制代码

79. innerHtml 和 outerHtml

image

  • inner只是标签内,outer是包括标签

80. rem 和 em

  • rem是根据根元素的字体大小定的
  • em是根据自身元素的字体大小定的,如果自身没有,那可以从父元素继承字体大小

81. 垃圾回收机制 和 内存泄漏

垃圾回收

  • 标记清除

最常用的垃圾回收方式,运行的时候他会给变量标记,如果环境中的变量已经无法访问到这些变量,就清除

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}
复制代码

例如函数的局部变量和参数,外部访问不了,则就会被清除

  • 引用计数

判断资源被引用的次数

var arr = [1, 2, 3, 4];
arr = [2, 4, 5]
复制代码

例如一开始[1,2,3,4]的引用次数为1,不会被清除,但是下面吧arr的引用换了,[1,2,3,4]的引用数变成0,将被清除

内存泄漏

  1. 意外的全局变量
function foo(arg) {
    bar = "this is a hidden global variable";
}
复制代码

这样子bar会声明在全局变量里面,不会被释放

  1. 计时器 如果使用setInterval而没有关闭,则他会一直存在不会被释放

  2. 闭包 闭包会维持局部变量,就无法释放了

  3. 没有清理dom引用 dom元素的引用如果不清除,他就会一直存在

优化

  1. 数组优化
var arr = [1,2,3]
arr.length = 0 // 这样子优化可以不用新生成一个空数组
复制代码
  1. 对象复用
var t = {}

while(){
    t.a = 1
}

t = null
复制代码

尽量复用对象不要每次都生成,然后吼用完设置为null,垃圾回收

  1. 循环使用函数表达式
function t(){
    
}

while(){
    t()
    // var t = function(){} 这样子就不用循环创建很多函数了
}
复制代码

82. 手写Promise 和 Promise.all

Promise

juejin.im/post/5b2f02…

以下为简略版

function myPromise(executor) {
  this.state = 'pending';
  this.value = undefined;
  this.reason = undefined;
  this.onResolvedCallbacks = [];
  this.onRejectedCallbacks = [];
  let resolve = value => {
    if (this.state === 'pending') {
      this.state = 'fulfilled';
      this.value = value;
      this.onResolvedCallbacks.forEach(fn => fn());
    }
  };
  let reject = reason => {
    if (this.state === 'pending') {
      this.state = 'rejected';
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn());
    }
  };
  try {
    executor(resolve, reject);
  } catch (err) {
    reject(err);
  }
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
  // 声明返回的promise2
  let promise2 = new Promise((resolve, reject) => {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value)
    };
    if (this.state === 'rejected') {
      onRejected(this.reason)
    };
    if (this.state === 'pending') {
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  });
  // 返回promise,完成链式
  return promise2;
}

new myPromise((resolve, reject) => {
  console.log('1')
  resolve(1)
  console.log('2')
}).then((res) => {
  console.log(res)
  console.log('4')
  return 2
})
复制代码

Promise.all

const myPromiseAll = function (promiseList){
    const result = []
    return new Promise((resolve, reject) => {
        let index = 0
        next()
        function next(){
            promiseList[index].then((res) => {
                result.push(res)
                index++
                if(index === promiseList.length){
                    resolve(result)
                } else {
                    next()
                }
            })
        }
    })
    
}
复制代码

83. post的方法

  • application/x-www-form-urlencoded (url传参数)
  • multipart/form-data (上传文件)
  • application/json (传json)
  • text/xml

84. http options预检请求

正式跨域之前,会发起option请求,预检一下。检测发送的请求是否安全,同时发送请求地址,请求方法,服务器判断来源域名是否再许可名单内,请求方法支不支持,支持则允许请求

复杂请求才会发送预检,以下为复杂请求

  • put/delete/patch/post的其中一种
  • 发送json格式(content-type: application/json)
  • 请求中有自定义头部

为什么要进行预检? 复杂请求可能会对服务器产生副作用,数据修改等。所以要检测一下请求来源在不在许可名单上

85. oop三大特点

  • 封装
  • 继承
  • 多态

86. 手写限制并发数

function limitPromise(taskList, limit) {
  return new Promise((resolve, reject) => {
    let index = 0
    let active = 0
    let finish = 0
    let result = []

    function next() {
      if (finish >= taskList.length) {
        resolve(result)
        return
      }

      while (active < limit && index < taskList.length) {
        active++
        let cur = index
        taskList[index].then((res) => {
          active--
          finish++
          result[cur] = res
          next()
        })
        index++
      }
    }
    next()
  })
}
复制代码

87. webpack的tree shaking

tree shaking是用于清除无用代码的方式,webpack3/4开始之后就自动集成

88. getComputedStyle 和 style

  • getComputedStyle只读,style可读可写
  • style只能获取内联样式

window.getComputedStyle(dom, null).getPropertyValue('display')

89. 怪异盒模型

  • 标准盒模型:大小 = content + margin + padding + border

  • 怪异盒模型:大小 = width(content + padding + border) + margin

  • content-box: 默认,标准盒模型

  • border-box:border和padding算入width中

    .container {
      display: flex;
      width: 200px;
      height: 50px;
    }

    .item1 {
      width: 50%;
      background: red;
      border: 10px solid black;
    }

    .item2 {
      width: 50%;
      background: yellow;
    }

  <div class="container">
    <div class="item1">1</div>
    <div class="item2">2</div>
  </div>
复制代码

效果如图

image

假设现在两个div一行放置,都是50%宽,假如我们再期中一个加上边框,这样另一个就会被挤压

这时候我们可以将box-sizing设置为border-box

.item1 {
  width: 50%;
  background: red;
  border: 10px solid black;
  box-sizing: border-box;
}
复制代码

image

挤压的问题就没有啦

90. Object.defineProperty

Object.defineProperty(obj, prop, descriptor)

MDN描述:
obj要在其上定义属性的对象。
prop要定义或修改的属性的名称。
descriptor将被定义或修改的属性描述符。
复制代码
  • value:设定值
  • writable:是否可被重写
  • enumberable:是否可枚举(for..in, Object.keys)
  • set/get
  • configrable:是否可重定义

91. HTTP如何复用连接

设置http的header:Connection: keep-alive,告诉服务器,待会我还要请求,就可以避免再次三次握手了

92. HTTP和TCP的关系

  • http是应用层,tcp是传输层
  • http是基于tcp的基础上实现的
  • tcp只是单纯的进行连接,http是会进行收发数据

93. node的优势

  • 完全是js语法
  • 高并发能力很好
  • 非阻塞I/O
  • 开发周期短
  • 单进程、单线程

94. HTTP报文结构

  • 请求报文:请求行,请求头,空行,请求数据
  • 响应报文,状态行,消息头,空行,响应正文

请求头

  • 请求方法:get/post
  • host:请求主机
  • connection:连接状态
  • cache-control:缓存
  • accept:能发送的类型
  • user-agent:代理
  • reference:请求资源的url
  • accept-encoding:支持的编码方式
  • cookie:cookie

响应头

  • date:响应时间
  • last-modify:最后修改时间
  • etag:资源标识
  • connection:连接状态
  • content-type:资源类型

95. async / await

async

async总是返回promise

  • 没有显示return,则返回promise.resolve(undefined)
  • 返回不是promise的话,则返回promise.resolve(data)
  • 返回promise就会得到promise

await

  • await只可以用在async里面
  • 如果await后是非promise,则获取里面resolve的值

与generator的区别

async就是generator的语法糖

async对比generator的改进

  1. 内置执行器,不需要next来执行
  2. 更好的语义化
  3. 返回promise,可以继续操作

96. 是否每一次请求都会三次握手

如果没有缓存的情况下,请求头设置connection:keep-alive则可以不重新握手

97. TCP的keepAlive和HTTP的keep-alive

  • TCP的keepAlive是侧重于保持客户端和服务端的连接,会不定时发送心跳包验证有没有断开连接,如果没有这个机制的话,一方断开,另一方不知道,就会对服务器资源产生较大的影响

  • HTTP的keep-alive可以让服务器客户端保持这个连接,不需要重新tcp进行连接了

98. TCP两次握手可不可以呢

假如第一次发出连接请求,如果服务器端没有确认,客户端再进行一次请求,然后他们开始连接,在这之后,如果第一次只是延迟了,然后现在发送给客户端,如果没有三次握手的话,就直接开启连接,然而客户端并没有数据要传输,则服务端会一直等待客户端发数据

99. TCP三次握手中可以传输数据吗

第一次第二次不可以,因为很容易被人攻击,而第三次握手已经进入establish状态了,已经确认双方的收发能力,可以传输

100. 2MSL等待状态

MSL是报文最大生存时间,客户端进入timewait的状态之后,需要等待2msl的时间,这样可以让最后发送的ack不会丢失,保证连接正常关闭,time_wait就是为了防止最后ack重发可能丢失的情况

101. websocket

http连接具有被动型,只能主动去向服务器请求看有没有新消息,一直轮询。

websocket是一个持久化的协议,连接了就不会自动断开。而且传递信息的时候只需要传递一点点头信息即可。一次握手,持久连接

  1. 先进行TCP三次握手
  2. TCP连接建立成功后,浏览器通过http协议发送websocket支持的版本号,地址等信息给服务端
  3. 服务端接受后,如果协议版本匹配,数据包格式正确,则接受连接(Upgrade: websocket,Connection:Upgrade)
  4. 浏览器收到回复后,触发onopen事件,就可以通过send发送数据了

102. hasOwnProperty

这个方法只会遍历自身属性,不会从原型链上遍历,可以用来判断属性是否在对象上或者是原型链继承来的

const obj = new Object()
obj.num = 1

Object.prototype.fn = function(){
    console.log(2)
}

obj.hasOwnProperty(num) // true
obj.hasOwnProperty(fn)  // false
复制代码

103. DNS域名解析

域名解析过程

  1. 检查浏览器缓存中有没有该域名的ip地址
  2. 检查本机缓存中有没有该域名的ip地址
  3. 向本地域名解析服务系统(LDNS)发起域名解析请求
  4. 向根域名解析服务器发起域名解析请求
  5. 根域名服务器返回gTLD域名解析服务器地址
  6. 向gTLD服务器发起解析请求
  7. gTLD服务器返回Name Server服务器
  8. Name Server服务器返回IP地址给本地服务器
  9. 本地域名服务器缓存解析结果
  10. 返回解析结果给用户

域名解析方式

  • A记录:A代表address,用来指定域名对应的ip地址
  • MX记录:可以将某个域名下的邮件服务器指向自己的Mail Server
  • CNAME记录:别名解析,可以将指定域名解析到其他域名上
  • NS记录:为某个域名指定特定的DNS服务器去解析
  • TXT记录:为某个主机名或域名设置指定的说明

104. JWT原理

使用JWT服务器就不需要保存session数据了,更容易扩展

数据结构

由三部分组成,中间以.分隔

  • header
  • payload
  • signature

header

{
    "alg": "HS256",
    "typ": "JWT"
}
复制代码

alg代表算法,typ代表类型,最后用base64编码转换一下变成字符串

payload

除了官方给的字段,我们还可以自定义字段,例如:

{
    "name": "bb",
    "age" : 21
}
复制代码

signature

这是一个签名,用于防止前面的数据被篡改

我们需要自定义一个 密钥 ,不能给用户知道,然后生成签名

使用方式及特点

使用方式

  • 客户端收到后可以存入cookie或localStorage
  • 每次请求时候可以在自定义请求头加上Authorization

特点

  • 默认不加密,可以自己通过密钥再加密一下
  • 不能保存session状态,在有效期前一直有效
  • 尽量使用https防止泄露

105. 什么是DOCTYPE及其作用

DOCTYPE是用来声明文档类型和DTD规范的。主要用于验证文件合法性。如果要提高浏览器的兼容性,需要设置一下<!DOCTYPE>

106. koa原理

koa是基于express来开发出来的,但是他更简洁,自由度更高,十分轻量。功能都通过插件来实现,这种拔插式的架构设计模式,很符合unix哲学

koa2不在使用generator,而是采用了async/await

与 express 的区别

express:

  • 整合了更多功能
  • 但是回调函数十分不便

koa:

  • 使用async/await更加方便
  • 需要自己配置插件中间件等

结构

application.js

这个是入口文件,继承了events可以执行事件监听还有触发事件。

  • listen是通过对http.createServer的封装,传入一个回调函数,包含了中间件、上下文还有res等
  • use是收集中间件,将多个中间件放进缓存队列中,用koa-compose组合

context.js

这个就是koa的应用上下文ctx了,可以通过他来访问请求头啊设置响应头等等

request/response.js

对原生的req,res等做一下封装,可以在这里取到headers设置headers/body等

洋葱模型

app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
});

app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
});

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});
复制代码

输出 1 2 3 4 5 6

意思就是说,先从上到下执行完await next()之前的内容,然后再从下到上执行await next()之后的内容

koa通过use函数,吧中间件push到队列中,洋葱模型可以让所有中间件依次执行,每执行完一次,就通过next()传递ctx参数

next()就相当于一个接力棒,将参数等传递给下一个中间件。只需要执行一下next就可以将之后的中间件都执行了!

不同于express框架,修改响应需要等请求结束之后,但用koa就可以将修改响应的代码放到next后面就可以了

前期处理 -> 交给并等待其他中间件处理 -> 后期处理

107. babel基本原理

什么是babel

babel是一个js的编译器,用来将代码转换成兼容浏览器运行的代码

  • 语法转换
  • 通过polyfill的方式添加缺失的特性
  • js源码转换

babel基本原理

核心就是AST抽象语法树。

  1. parsing(解析):首先将源码转成抽象语法树,词法分析,语法分析
  2. transforming(转换):将抽象语法树处理生成新的语法树
  3. generating(生成):将新的语法树生成js代码

image

108. npm版本号

版本号

版本号分为X.Y.Z三位

  • 大变动,向下不兼容,需要更新X
  • 增加功能,向下兼容,更新Y
  • 修复bug,更新Z

具体符号

  • ~: 大约匹配,固定Y,Z随意变动
  • ^: 兼容某个版本,固定X, Y, Z变动
  • x: 任意版本,例如1.x.2,就可以是1.4.2, 1.8.2

109. git rebase 和 merge

  • rebase:会取消分支的每一个提交,然后更新到最新分支,找到两个分支第一个相同的提交,然后提取之后所有记录,添加到目标分支的后面,
  • merge: 将两个分支,合并提交为一个分支,并且提交两个commit。新建一个commit对象,吧两个分支以前的记录都指向新commit对象,会保留之前所有的commit

110. React-Router 原理

主要依赖的是history库。

顶层router监听history,history发生变化的时候,router调用setState把location向下传递。设置到RouterContext。router根据RouterContext决定是否显示。

Link标签禁用a标签的默认事件,调用history.push方法来改变url。

111. document.readyState

  • loading:dom还在加载
  • interactive:dom加载完了,但是图片,css等还在加载
  • complete:全部加载完了

112. symbol

symbol函数会返回symbol类型的值,他不支持new。

  • 解决属性名冲突
  • 私有属性

113. Samsite cookie

cookie的属性SameSite有三个值

  • strict:完全禁止第三方cookie,跨站点时,任何情况都不会发送cookie。
  • lax:大多数情况禁止第三方cookie,但是导航到目标网址的get请求除外(例如a标签,预加载请求,get表单)
  • none:关闭samesite属性,但必须设置secure才能生效

114. async 和 promise的区别

  • 更加简洁
  • 可以使用try/catch来进行错误处理
  • 可以很方便的使用中间值传递给下一个promise,不需要像promise那样一直then
  • 容易追踪错误,报错会具体到哪一行

115. import 和 require如何在浏览器实现

import

需要用babel进行转换,转换成commonJS规范

export

例如export default会被转换成exports.default

// babel编译前
export default {}

// babel编译后
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = {};
/*
* exports
* {default: {} , __esModule: true}
*/
复制代码

import

使用_interopRequireDefault来处理一遍,为了兼容和区分commonjs和es6

// babel编译前
import Test, {msg, fn} from 'test'

// babel编译后
'use strict';

var _test = require('test');

var _test2 = _interopRequireDefault(_test);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
复制代码

webpack中会为importexport添加一个__esModule: true的标识,来标记这是es6的东东

116. target 和 currentTarget的区别

  • target:返回触发事件的源对象
  • currentTarget:返回事件绑定的对象
  document.getElementById('test').addEventListener('click', function (e) {
    console.log(e.target)
    console.log(e.currentTarget)
  })

  <div id="test">
    <button>1</button>
  </div>
复制代码

117. 为什么javascript是单线程的

  • 防止dom渲染冲突的问题
  • html5中的web worker可以实现多线程

118. DNS预解析

  • html源码下载完后,会解析页面包含的链接的标签,提前查询对应的域名
  • 对访问过的页面,会记录一个域名列表,再次打开下载html的时候会同时解析DNS
预解析某域名
<link rel="dns-prefetch" href="//img.alicdn.com">

强制开启HTTPS下的DNS预解析
<meta http-equiv="x-dns-prefetch-control" content="on">
复制代码

119. 什么是bundle、chunk、module

  • bundle是指webpack打包出来的文件
  • chunk是指webpack进行模块依赖分析的时候,代码分隔出来的代码块
  • module是开发中的单个模块

120. 什么是loader、plugin

  • loader是webpack可以加载解析非js文件的能力
  • plugin可以扩展webpack得功能,更加灵活。

121. 组件懒加载原理

  • 例如react-loadable主要利用import()来返回一个promise的性质,进行loading的异步操作。
  • webpack通过require.ensure()来实现按需加载

require.ensure

require.ensure(dependencies,callback,errorCallback,chunkName)他会返回一个promise,先是判断dependencies是否被加载过,加载过则获取缓存值。没有的话就生成一个promise,然后缓存起来。接着生成一个script标签,填充好信息放到html文件上,就完成了按需加载了。

require.ensure可以自定义文件名

122. HTML5离线缓存

利用manifest属性,当浏览器发现头部有manifest属性,就会请求manifest文件,如果是第一次,则根据文件内容下载响应的资源。如果已经离线缓存过了,则直接使用离线的资源加载。

123. 点击劫持,如何防范

攻击者通过iframe嵌套的方式,将iframe设为透明,诱导用户点击。

可以通过http头设置X-FRAME-OPTIONS来防御iframe嵌套的点击劫持攻击

124. 观察者模式 与 发布订阅模式

观察者模式

  function Subject() {
    this.state = 0
    this.observers = []
  }
  Subject.prototype.attach = function (observer) {
    this.observers.push(observer)
  }
  Subject.prototype.getState = function () {
    return this.state
  }
  Subject.prototype.setState = function (state) {
    this.state = state
    this.noticefyAllObservers()
  }
  Subject.prototype.noticefyAllObservers = function () {
    this.observers.forEach(observer => {
      observer.update()
    })
  }

  function Observer(name, subject) {
    this.name = name
    this.subject = subject
    this.subject.attach(this)
  }
  Observer.prototype.update = function () {
    console.log(this.name, this.subject.getState())
  }

  const subject = new Subject()
  const o1 = new Observer('o1', subject)
  const o2 = new Observer('o2', subject)
  const o3 = new Observer('o3', subject)

  subject.setState(1)
  subject.setState(2)
  subject.setState(3)
复制代码

发布订阅模式

  function Event() {
    this.events = {}
  }
  Event.prototype.on = function (event, fn) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(fn)
  }
  Event.prototype.emit = function (event) {
    const args = [...arguments].splice(1)
    this.events[event].forEach(fn => {
      fn.apply(this, args)
    })
  }
  Event.prototype.off = function (event, fn) {
    const currentEvent = this.events[event]
    this.events[event] = currentEvent.filter(item => item !== fn)
  }

  const publisher = new Event()
  const fn1 = function (e) {
    console.log('a1')
    console.log(e)
  }
  const fn2 = function (e) {
    console.log('a2')
    console.log(e)
  }
  publisher.on('a', fn1)
  publisher.on('a', fn2)
  publisher.emit('a', 'aowu')
  publisher.off('a', fn2)
  publisher.emit('a', 'aowu')
复制代码

区别

  • 观察者模式,观察者是知道subject的,发布订阅模式中,发布者跟订阅者不知道对方的存在,只有通过消息代理进行通信
  • 观察者模式大多是同步的,发布订阅大多是异步的
  • 发布订阅模式大多是松散耦合的,观察者反之

128. nginx负载均衡

www.cnblogs.com/alterem/p/1… www.jianshu.com/p/4c250c1cd…

129. redis

www.imweb.io/topic/58441…

130. Set,Map,WeakSet,WeakMap

  • Set:可以遍历,元素唯一
  • WeakSet:元素都是对象,元素都是弱引用,可以被垃圾回收,不容易造成内存泄露,不能遍历
  • Map:键值对,可以遍历
  • WeakMap:键名为对象,键名是弱引用,不能遍历

131. token 与 cookie的区别

token

  • 加密解密时间较长,占用内存大
  • 不仅仅支持浏览器,还支持移动端等
  • 可以防止crsf攻击
  • token是无状态的,很好扩展

cookie

  • 自动发送,方便
  • app端无法使用cookie
  • 不在https中使用容易遭受crsf攻击

132. null 与 undefined

  • null:对象类型,是对象原型链的终点,已定义,为空值
  • undefined:表示没有赋值,undefined类型,未被定义

133. react中使用persist

react的合成事件调用之后全都会被重用,所有属性都无效了,如果要异步访问的话,就要用event.persist(),这样就会从事件池里面删除,允许保留事件的引用

134. position: relative会影响子元素offsetLeft等属性

135. 小程序和H5有何区别

  1. 小程序渲染方式与H5不同,小程序一般通过Native原生渲染的,但是小程序也支持web渲染,可以初始化一个webview组件,在webview中加载H5页面

小程序下,native方式性能比web方式好

  1. 小程序特有的双线程设计。H5下,通常我们全部打包在一个文件里面。小程序编译之后有两个,分别是view层,和业务逻辑。运行的时候会有两条线程分别处理两个文件,一个是主渲染线程,负责渲染view层的内容。另一个是service worker线程,他负责执行业务逻辑。
  • 运行环境不同,h5大多用于web-view或者浏览器,小程序是使用一个基于浏览器重构的内置解析器,大大地优化了性能、缓存等
  • 性能不同,小程序依赖微信客户端实现,对解析进行了很多优化,首次打开会缓存很多资源,因此比H5要流畅很多,底层优化技术使得小程序更接近于原生的APP
  • 小程序开发成本低,不需要担心莫名其妙的bug出现,可以调用官方的api
  • 微信用户更多,相比h5,微信流量更多
  • 小程序无法使用dom
  • 小程序能使用很多手机的权限

136. 字符串slice、substr、substring

  • slice:正常的截取,如果有负数则倒着数
  • substr:第一个参数为开始下标,第二个参数代表截取多少个字符
  • substring:传入两个参数,较小的一个为起始下标

137. 基本包装类型

基本类型不是对象,但是他们又有一些方法,为什么呢?

因为后台会对基本类型进行包装,例如字符串、整数、布尔值会首先利用构造函数创建实例,使用完之后就销毁

let s1 = 'hello'
let s2 = s1.substring(2)
// ↓ 后台包装
let s1 = new String('hello') // 包装
let s2 = s1.substring(2) // 可以调用方法
s1 = null // 销毁
复制代码

138. toString 与 valueOf

  • null 与 undefined 没有这俩方法
  • toString:值类型返回字符串形式,对象类型返回[ibject Object]形式
  • valueOf:无论是值还是引用类型,基本都是原样返回,是Date的时候,返回时间戳
  • 进行字符串转换时候,优先toString,进行数值计算优先valueOf
  • toString支持传参,可以进行进制转换

139. es6的class实现私有变量

  1. 闭包
class A {
    constructor (x) {
        let _x = x
        this.showX = function () {
            return _x
        }
    }
}

let a = new A(1)
// 无法访问
a._x        // undefined
// 可以访问
a.showX()   // 1
复制代码
  1. 利用symbol
class A {
  constructor(x) {
    // 定义symbol
    const _x = Symbol('x')

    // 利用symbol声明私有变量
    this[_x] = x
  }

  showX() {
    return this[_x]
  }
}

let a = new A(1)

// 1. 第一种方式
a[_x]    // 报错 Uncaught ReferenceError: _x is not defined

// 2. 第二种方式
// 自行定义一个相同的Symbol
const x = Symbol('x')
a[x]        // 无法访问,undefined

// 3. 第三种方式,可以访问(正解)
a.showX()   //1
复制代码

140. TypeScript的type与interface

interface

  • 同名interface自动聚合,也可以跟同名的class自动聚合
  • 只能表示object, function, class类型

type

  • 不仅仅表示object, function, class类型
  • 不能重名
  • 支持复杂的类型操作

141. JavaScript的V8引擎

JavaScript引擎

JavaScript是一个解释型语言,跟编译型语言不同,js是一边执行一边解析的。为了提高性能引入了java虚拟机和C++编译器的一些东西

执行流程

源代码 -> 抽象语法树 -> 字节码 -> JIT -> 本地代码

V8引擎

相比其他的javascript引擎(转换成字节码或解释执行),V8引擎将代码直接编译成原生机器码,使用了内联缓存提高性能。

工作流程:源代码先被解析成抽象语法树然后用JIT编译器直接从抽象语法树生成代码,提高代码执行速度(不用转换成字节吗了)

142. 堆与栈的区别

  • 堆:主要存new等分配的内存块,编译器不会自动释放他们,引用类型的放在堆里面
  • 栈: 主要存放函数参数,局部变量等,编译器自动分配和释放,基本数据类型放在栈里面

143. unicode utf8 utf16

  • unicode是为了解决世界上编码不统一的问题,包含了所有符号、文字的编码集,每个字符由2个字节表示,英文字符只需要1个字节,所以保存起来就会浪费空间
  • utf8每次8个位传输,是变长的编码方式,使用1~4个字节表示一个符号,utf8中文字符一般用到3个字节
  • utf16每次16个位传输

区别

unicode:世界上所有符号的编码集 utf8、utf16:是unicode的字符集,不同的编码实现

144. class 和 function的区别

  • class封装的更加合理,方便维护和使用
  • class没有像function那样状态提升
  • class不用写原型方法那些方便一点
  • class要使用super才能使用this
关注下面的标签,发现更多相似文章
评论