HTML篇

语义化标签

讲讲xxxx:总结:「是什么、怎么做、解决了什么问题、优点是、缺点是、怎么解决缺点」

  • 是什么:语义化就是根据内容的结构化,选择合适的标签,简单说就是用正确的标签做正确的事情。
  • 怎么做:实现方法是遇到标题就用 h1 到 h6,遇到段落用 p,遇到文章用 article,主要内容用 main,边栏用 aside,导航用 nav……(就是找到中文对应的英文)
  • 解决了什么问题:明确了 HTML 的书写规范
  • 优点是:

一、对机器友好:适合搜索引擎检索;
二、对开发者友好:让页面的内容结构化,增加代码可读性,利于团队维护。

  • 缺点是:没有。
  • 怎么解决缺点:无需解决。

image.png

HTML常用行级标签和块级标签 (区别)

  1. 个行内元素只占据它对应标签的边框所包含的空间。
  2. 块级元素占据其父元素(容器)的整个水平空间,垂直空间等于其内容高度,因此创建了一个“块”。

  3. 格式上,默认情况下,行内元素不会以新行开始,而块级元素会新起一行。

  4. 内容上,默认情况下,行内元素只能包含文本和其他行内元素。而块级元素可以包含行内元素和其他块级元素。
  5. 行内元素与块级元素属性的不同,主要是盒模型属性上:行内元素设置 width 无效,height 无效(可以设置 line-height),设置 margin 和 padding 的上下不会对其他元素产生影响。
  6. 设置居中时:行内元素需要在其外层块级元素上设置text-align:center;块级元素需要将本身宽度设置为父容器宽度即可

image.png

HTML5新标签

  • 文章相关:header main footer nav section article figure mark
  • 多媒体相关:video audio svg canvas
  • 表单相关:type=email type=tel

Canvas 和 SVG 的区别

  1. Canvas 主要是用笔刷来绘制 2D 图形的。
  2. SVG 主要是用标签来绘制不规则矢量图的。
  3. 相同点:都是主要用来画 2D 图形的。
  4. 不同点:Canvas 画的是位图,SVG 画的是矢量图。
  5. 不同点:SVG 节点过多时渲染慢,Canvas 性能更好一点,但写起来更复杂。
  6. 不同点:SVG 支持分层和事件,Canvas 不支持,但是可以用库实现。

    script 标签中 defer 和 async 的区别

    script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
    async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。
    defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。
    image.png

    src和href的区别

    src和href都是用来引用外部的资源,它们的区别如下:
  • src: 表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置。src会将其指向的资源下载并应⽤到⽂档内,如请求js脚本。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执⾏完毕,所以⼀般js脚本会放在页面底部。
  • href: 表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系。当浏览器识别到它他指向的⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理。 常用在a、link等标签上。

CSS篇

BFC 是什么

是什么:块级格式化上下文
指的是一个独立的渲染区域,可以理解为一个隔离的独立容器,容器里面的子元素不会影响外面元素
BFC就是符合一些特性的HTML标签

怎么做:背诵 BFC 触发条件,虽然 MDN 的这篇文章 列举了所有触发条件,但本押题告诉你只用背这几个就行了

  • body根元素
  • 浮动元素( float 除了 none 以外的值)
  • 绝对定位元素(position 为 absolute 或 fixed)
  • overflow 值不为 visible 的块元素
  • 弹性元素(display为 flex 或 inline-flex元素的直接子元素 或 inline-block、table-cell、table-caption)

解决了什么问题:

  • 清除浮动(为什么不用 .clearfix 呢?)
  • 防止 margin 合并

BFC特性:

  • 内部的Box会在垂直方向上一个接一个的放置
  • 垂直方向上的距离由margin 决定
  • BFC的区域不会与float的元素区域重叠

如何实现垂直居中

CSS 选择器优先级

  • 选择器越具体,其优先级越高
  • 相同优先级,出现在后面的,覆盖前面的
  • 属性后面加 !important 的优先级最高,但是要少用

可以把样式的应用方式分为几个等级,按照等级来计算权重
1、!important,加在样式属性值后,权重值为 10000
2、内联样式,如:style=””,权重值为1000
3、ID选择器,如:#content,权重值为100
4、类,伪类和属性选择器,如: content、:hover 权重值为10
5、标签选择器和伪元素选择器,如:div、p、::before 权重值为1
6、通用选择器(*)、子选择器(>)、相邻选择器(+)、同胞选择器(~)、权重值为0

如何清除浮动

方法一,给父元素加上 .clearfix

  1. .clearfix:after{
  2. content: '';
  3. display: block; /*或者 table*/
  4. clear: both;
  5. }
  6. .clearfix{
  7. zoom: 1; /* IE 兼容*/
  8. }

方法二,给父元素加上 overflow:hidden。

两种盒模型的区别

content-box 内容盒: 宽度只是:content的宽度;
border-box 边框盒: 宽度包括:content+ padding + border的宽度

简单理解:
content-box操控的 只是 content 的宽度 ,整体会比意料的大
border-box 操控包含了padding 和 border ,整体和意料的一样

JS篇

JS数据类型

8种
数字、字符串、布尔、null、undefined、bigint、symbol、object(可用中文可用英文)

问:为什么要新增bigint
答:因为JS中的数字number默认是双精度浮点数,这导致了精度有限的问题。在 js 中,Number 基本类型可以精确表示的最大整数是2^53;如1会变成1.0,并且以64位保存,虽然保存的数字很大但精度有限,而bigint可以表示任意大的整数
使用:在无法使用number表示的大数时,可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()

undefined 和 null 区别

  • Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法:

    • 变量被声明了,但没有赋值时,就等于undefined。
    • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
    • 对象没有赋值的属性,该属性的值为undefined。
    • 函数没有返回值时,默认返回undefined。
  • Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法

    • 作为函数的参数,表示该函数的参数不是对象。
    • 作为对象原型链的终点。

原型链

定义?

→大概念题思路:大概念化为小概念(分割),抽象化为具体(举例)

示例:
原型链涉及到的概念挺多的,我来举例说明一下:假设我们有一个普通对象x={},这个x会有一个隐藏属性,叫做???,这个属性会指向Object.prototype。即

原型公式

对象.proto === 其构造函数.prototype
「x 的原型」等价于「x.proto 」/「其构造函数.prototype 」

this

万能解this:
在函数调用的时候,改为使用call形式调用函数「转换代码」

  1. func(p1, p2) 等价于
  2. func.call(undefined, p1, p2)
  3. obj.child.method(p1, p2) 等价于
  4. obj.child.method.call(obj.child, p1, p2)

记忆:

  1. fn()
  2. this => undefined-->window/global
  3. obj.fn()
  4. this => obj
  5. fn.call(xx)
  6. this => xx
  7. fn.apply(xx)
  8. this => xx
  9. fn.bind(xx)
  10. this => xx
  11. new Fn()
  12. this => 新的对象
  13. fn = ()=> {}
  14. this => 外面的 this

new

  • new X() 自动做的事情
    • 自动创建空对象
    • 自动为空对象关联原型,原型地址指定为 X.prototype

(即将X.prototype保存的地址复制到空对象proto里)

  • 自动将空对象作为 this 关键字运行构造函数
  • 如果该函数没有手动返回对象,或者return的不是一个对象,则自动 return this对象

简化记忆:

  • 创建临时对象
  • 绑定原型
  • 指定this = 临时对象
  • 执行构造函数
  • 返回临时对象

image.png
原型是保存共有属性;对象是保存自身属性的,Object.protptype =…

JS的立即执行函数是什么

概念题/论述题:是什么、怎么做、解决了什么问题/场景/优点、优点是、缺点是、怎么解决缺点

是什么:声明一个匿名函数,然后立即执行它
怎么写:

  1. (function(){alert('我是匿名函数')}()) //用括号把整个表达式包起来
  2. (function(){alert('我是匿名函数')}) () //用括号把函数包起来
  3. !function(){alert('我是匿名函数')}() //求反,我们不在意值多少,只想通过语法检查
  4. +function(){alert('我是匿名函数')}()
  5. -
  6. ~
  7. void
  8. new
  9. var
  10. ...

解决了什么问题:在ES6之前,通过它来创建局部变量(由于匿名所以全局访问不到,ES6后新增了块级作用域block+let{ let a }注意不能用var,var的话还是全局变量
优点:兼容性好
缺点:丑

补充:逗号表达式
将两个及其以上的式子联接起来,从左往右逐个计算表达式,整个表达式的值为最后一个表达式的值
(3+5,6+8)的值是14;(a=35,a4)的值是60

JS的闭包是什么

概念题/论述题

是什么:
闭包是JS的一种语法特性(几乎所有语言都有,但有些语言没有)
闭包 = 函数 + 自由变量(不是全局变量和局部变量的变量,局部是相对的,全局是绝对的)
(与自由变量相对应的是全局变量,闭包访问必须确保没有全局变量的参与,否则分不清是闭包访问还是全局访问)

怎么做:

  1. {
  2. let a
  3. function fn(){
  4. console.log(a)
  5. }
  6. }
  7. //把上面代码除了最外面大括号放在一个 非全局环境里,就形成闭包

上面代码什么也没用,我们需要外部间接访问到局部作用域,需要return

  1. const add2 = function() {
  2. var count
  3. return function add() { //访问了外部变量的函数
  4. count +=1
  5. }
  6. }()
  7. //此时,add2() 相当于 add()

解决了什么问题/场景/优点:

  1. 避免污染全局环境
  2. 提供对局部变量的间接访问
  3. 维持变量,使其不被垃圾回收

(什么情况会被回收:不用它就会被回收)
优点:好用
缺点:闭包使用不当可能造成内存泄漏
(闭包造成内存泄漏是以讹传讹,只有使用不当才可能造成内存泄漏,曾经旧版本 IE 的bug导致的问题,自由变量绑定了同级自由变量,造成没有用的变量没有被回收,而对于一个正常浏览器来说,无用变量会在一段时间后自动被垃圾回收期给回收掉)

君子不立于危墙之下,我们应该尽量少用闭包,因为有些浏览器对闭包的支持不够友好

怎么解决缺点:慎用、少用、不用(但基本不可能不用闭包)

如何实现类

方法一:使用原型
ES5之前没有类的概念
缺点:在ts中不支持

方法二:使用Class
优点:ts支持
缺点: ES6 明确规定,Class 内部只有静态方法,没有静态属性。目前只有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。(静态属性即 类的静态属性

解决缺点:

  1. 添加构造函数.propotype.静态属性 = 静态属性值 / 添加实例对象.proto.静态属性 = 静态属性值
  2. 写成原型上的getter方法
    1. class A {
    2. constructor() {
    3. ...初始化实例的操作
    4. }
    5. get father() {
    6. return 'me'
    7. }
    8. }

    如何实现继承

    继承是类和类之间的关系

方法一:基于原型链
思路:写两个类,并将两个类联结起来,并且私有属性要继承,共有属性propotype也要继承

  1. function Animal(legs) {
  2. this.legs = legs
  3. }
  4. Animal.propotype.kind = animal
  5. function Dog(name) {
  6. Animal.call(this,4) //通过调用父类,将两个类联结起来,实现私有属性继承
  7. this.name = name
  8. }
  9. Dog.propotoype.kind = '狗狗'
  10. Dog.propotype.__proto__ = Animal.propotype //实现propotype继承
  11. //注意是 ‘拓展propotype而不是改propotype’

方法二:基于Class

  1. Class Animal {
  2. constructor(legs) {
  3. this.legs = legs
  4. }
  5. run(){}
  6. }
  7. Class Dog extend Animal { //默认实现propotype上静态方法的继承
  8. constructor(name) {
  9. super(4) //实现私有属性的继承;注意必须先super再写自身属性如下句
  10. this.name = name
  11. }
  12. }

手写通用节流化、防抖化函数

节流throttle、防抖debounce
手写节流:
应用场景:点击按钮,点击后一段时间内不能再点

  1. //节流---技能冷却
  2. const d = () => {
  3. console.log('闪现')
  4. }
  5. let 冷却中=false
  6. let timer = null
  7. function sx() {
  8. if(冷却中) {return}
  9. d()
  10. 冷却中 = true
  11. timer = setTimeout(() => {冷却中 = fasle},120*1000)
  12. }
  13. sx()
  14. //如何一个函数实现 任何技能的'冷却中'?
  15. //即一个函数实现 生成函数(闪现函数)的功能
  16. //封装通用节流
  17. const d = () => {
  18. console.log('任何技能') //首先提前声明任何技能
  19. }
  20. const throttle = (f,time) { //通过计时器和判断机制实现 节流
  21. let switched = true
  22. let args = arguments
  23. return (...args) => {
  24. if(switched){
  25. f.call(this,...args)
  26. switched = false
  27. setTimeout(() => {
  28. switched = true
  29. },time)
  30. }
  31. }
  32. const d2 = throttle(d,120*1000)

补充:写函数小技巧:先确定输入和输出(入参、返回值等),再进行填空

手写防抖:
应用场景:持续一段时间内频繁变化的事情先不要处理,等一段时间后再处理,如频繁拖拽窗口大小、input持续输入

  1. //防抖 -----回城被打断
  2. const f = () => {
  3. console.log('回城成功') //声明回城函数
  4. }
  5. let timer = null
  6. function hc() {
  7. if(timer){
  8. clearTimeout() //还没回城成功,打断回城
  9. }
  10. timer = setTimeout(() = { //回城成功,重新回城
  11. f()
  12. timer = null
  13. },3000)
  14. }
  15. //封装通用防抖
  16. const f = () => {
  17. console.log('回城成功')
  18. }
  19. const debounce = (f,delay) => {
  20. let timer = null
  21. let args = arguments
  22. return (...args) => {
  23. if(timer) {
  24. clearTimeout(timer)
  25. }
  26. timer = setTimeout(() => {
  27. f.call(this,...args)
  28. },delay)
  29. }
  30. const d3 = debounce(f,3000)

异步编程

手写Ajax

  1. var request = new XMLHttpRequest()
  2. request.open('GET','url',true);
  3. request.send()
  4. request.onreadystatechange = function() {
  5. if(request.readyState === 4 && request.status ===200) {
  6. console.log('request.responseText')
  7. }
  8. }

try…catch 和 promise 的.catch() 区别

首先两者都是捕获错误信息的方法,并且能够对错误信息进行处理

但try catch 是同步的,所以它不能捕获promise里reject的错误信息
而Promise的catch方法是异步的
image.png
如何捕获promise中的错误?使用try catch

如果需要使用try catch捕获promise 的错误信息,则需要在try中返回promise对象,如:
补充:Promise.reject()方法返回一个 带有拒绝原因的 Promise对象

  1. async function fn1() {
  2. let n = Promise.reject(出错原因)
  3. try {
  4. await n //关键点,等待其返回promise对象
  5. console.log(n)
  6. } catch(err) {
  7. console.error('err',err)
  8. }
  9. }
  10. fn1()

Promise中的then第二个参数和catch有什么区别?

首页我们先要区分几个概念:
第一,reject是用来抛出异常的,catch是用来处理异常的;
第二:reject是Promise的方法(Promise.reject),而then和catch是Promise实例的方法(Promise.prototype.then 和 Promise.prototype.catch)

区别:

  • 如果在then的第一个函数里抛出了异常,后面的catch能捕获到,而then的第二个函数捕获不到。
  • catch可以捕获Promise中的异常,无论成功还是失败,即可以省略失败的回调,写catch

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获
catch只是一个语法糖而己 还是通过then 来处理的

捕获到错误信息和未捕获到的区别:
捕获到:
image.png
未捕获到:
image.png

async / await 使用

复习promise使用

  1. function 摇骰子 {
  2. return new promise(function(resolve,reject){
  3. setTimeout(() => {
  4. let n = parseInt(Math.random() * 6 + 1, 10)
  5. },3000)
  6. resovle(n)
  7. })
  8. }
  9. //promise调用的时候仍然是回调的思想
  10. 摇骰子().then((n) => {console.log('骰子的点数是' + n )},() => {console.log('失败的预案函数')})

promise 三种状态

image.png

理解Promise责任链调用

  1. const p = Promise.reject(1)
  2. .then(() => {console.log('成功1')},() => {console.log('失败1')})
  3. .then(() => {console.log('成功2')},() => {console.log('失败2')})
  4. //调用顺序是:'失败1';'成功2'
  5. //因为第一层then的失败处理结果已经完成(被log出来了),
  6. //故认为第一层then的责任在一定程度上已‘成功完成’,所以第二层then执行成功的预案
  7. // 如果在第一层then后再次 return Promise.reject(xxx)
  8. //则表示第一层then责任没有处理完成,第二层then执行失败的预案

async / await 使用

  1. //--------------改造promise调用
  2. function 摇骰子() {
  3. return new Promise((resolve,reject) =>{
  4. setTimeout(() => {
  5. let n = parseInt(Math.random() * 6 + 1, 10)
  6. resovle(n)
  7. },3000)
  8. }
  9. )
  10. }
  11. //promise调用的时候采用形似同步的写法:
  12. async function test() {
  13. let n = await 摇骰子() //n 仍然是三秒后才拿到摇骰子的值
  14. console.log(n)
  15. }
  16. //注意 test函数仍然是异步的,
  17. //需要将await包裹在async函数中以便浏览器引擎识别,async可以当做只是一个标记,为了标记异步函数
  18. test()
  19. //遇到问题:摇骰子不可能失败,那如果出现错误,如何进行错误处理?
  20. //----------------捕获错误用try catch
  21. function 猜大小(猜测值) {
  22. return new Promise((resolve,reject) => {
  23. setTimeout(() => {
  24. let n = parseInt(Math.radom() * 6 +1 , 10)
  25. if(n >3 ) {
  26. if(猜测值 ==='大'){
  27. resolve(n)
  28. }else{
  29. reject(n)
  30. }
  31. }else{
  32. if(猜测值 === '小') {
  33. resolve(n)
  34. }else{
  35. reject(n)
  36. }
  37. }
  38. }
  39. ,3000)
  40. })
  41. }
  42. async function test() {
  43. try { //捕获错误try{ 成功的预案 } catch{ 失败的预案 }
  44. let n = await 猜大小('大')
  45. console.log('赢了')
  46. }
  47. catch { //对错误进行的处理(错误的预案)
  48. console.log('输了')
  49. }
  50. }
  51. test()
  52. //遇到问题:await只能接一个promise
  53. //await中需要触发多个异步操作/promise的话,结合Promise.all/Promise.race使用
  54. async function test() {
  55. try {
  56. let n = await Promise.all([猜大小('大'),猜大小('小')]) //结合起来用即可
  57. console.log('赢了')
  58. }catch {
  59. console.log('输了')
  60. }
  61. }
  62. //总结:async await可以理解为Promose的语法糖,在深入使用的时候仍然要借鉴Promise

Promise.all()和Promise.race()

  1. //-------promise.all的使用
  2. //同时处理多个promise,传入一个数组,当多个promise都成功则进行成功的预案,当多个pormise只要有一个错误均进行错误的处理
  3. Promise.all([猜大小('大'),猜大小('大')]).then((x) => {console.log('成功的预案')},(y) => {console.log('失败的预案')})
  4. //-------promise.race的使用
  5. //同时处理多个promise,传入一个数组,当多个pormise只要有一个成功均进行成功的预案
  6. Promise.race([猜大小('大'),猜大小('大')]).then((x) => {console.log('成功的预案')},(y) => {console.log('失败的预案')})

async/await优势

相比于 Promise,它能更好地处理then链
使得异步代码看起来像同步代码
可以同时处理同步和异步错误

Set、Map、WeakSet、WeakMap

补充: 数组、Set、Map属于常见的哈希表,数组可以说是最简单的哈希表 一般哈希表都是用来快速判断一个元素是否出现集合里

Set、Map

Map:类似Object;键值对的集合,键和值可以接受任意类型的数据,相同键不同值则新的值会覆盖旧的值,键的插入是有顺序的
Set:类似Array; 没有键只有值,值是唯一的,可接受的参数为 所有具有iterable接口的数据

WeakSet、WeakMap

WeakMap与Map区别:
① 与Map 结构类似,也是用于生成键值对的集合;但只接收对象(除null外)作为键名
②WeakMap的key所引用的对象是弱引用;只要对象的其他引用被删除,垃圾回收机制就会释放该对象占用的内存,从而避免内存泄漏
③由于WeakMap的成员随时可能被垃圾回收机制回收,成员的数量不稳定,所以没有size属性、keys()/values()/entries()这样的迭代方法,也没有clear方法

WeakSet 与 Set 区别:
同理

简而言之本质区别:
Map 和 Set 对于keys是一种强持有的方式,当keys 为object对象时,即使obj对象为null即引用丢失,但存储在Map和Set中的数据仍然持有,即在obj引用丢失的情况下, 垃圾回收机制也无法将他们完全回收
WeakMap 和 WeakSet 对于keys 是 弱持有,即obj对象为null的情况下,弱持有的value值 会被垃圾回收机制所删除

应用场景:

当你想确保没有内存泄漏的时候,可以使用弱集合
因为每当执行垃圾回收的时候,丢失的引用值总是被清除,也从集合中清除

使用正则实现trim()

  1. String.proptype.trim = function() {
  2. return this.replace(/^\s+|\s+$/g,'') //使用原型
  3. }
  4. //或者
  5. function trim(string) {
  6. return stirng.replace(/^\s+|\s+$/g,'') //使用普通函数
  7. }

获取url参数并转为js对象

关键api:
image.png

  1. function getParams() {
  2. let params ={}
  3. let paramsString = window.location.search //得到url参数片段
  4. let paramsArr = paramsString.substring(1).split('&') //去掉? 并以&分隔,得到一个数组[name=npc,age=18]
  5. paramsArr.forEach( item => { //遍历数组
  6. let resultItem = item.split('=') // 将数组中的每一项以 = 分隔,得到多个数组,每个数组都为key和value两项组成[name,npc],[age,18]
  7. params[resultItem[0]] = resultItem[1]
  8. })
  9. return params
  10. }
  1. let URL = "http://www.baidu.com?name=Jack&age=25&sex=men&wife=Lucy"
  2. function getUrlParams2(url) {
  3. let urlStr = url.split('?')[1]
  4. const urlSearchParams = new URLSearchParams(urlStr)
  5. const result = Object.fromEntries(urlSearchParams.entries())
  6. return result
  7. }
  8. console.log(getUrlParams2(URL))
  1. function getParams(url) {
  2. const res = {}
  3. if (url.includes('?')) {
  4. const str = url.split('?')[1]
  5. const arr = str.split('&')
  6. arr.forEach(item => {
  7. const key = item.split('=')[0]
  8. const val = item.split('=')[1]
  9. res[key] = decodeURIComponent(val) // 解码
  10. })
  11. }
  12. return res
  13. }
  14. // 测试
  15. const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16')
  16. console.log(user) // { user: '阿飞', age: '16' }
  1. let URL = "http://www.baidu.com?name=张三&age=25&sex=男&wife=小红"
  2. function getUrlParams(url) {
  3. // 通过 ? 分割获取后面的参数字符串
  4. let urlStr = url.split('?')[1]
  5. // 创建空对象存储参数
  6. let obj = {};
  7. // 再通过 & 将每一个参数单独分割出来
  8. let paramsArr = urlStr.split('&')
  9. for(let i = 0,len = paramsArr.length;i < len;i++){
  10. // 再通过 = 将每一个参数分割为 key:value 的形式
  11. let arr = paramsArr[i].split('=')
  12. obj[arr[0]] = arr[1];
  13. }
  14. return obj
  15. }
  16. console.log(getUrlParams(URL))
  1. <script src="https://cdn.bootcdn.net/ajax/libs/qs/6.10.3/qs.min.js"></script>
  2. <script>
  3. let URL = "http://www.baidu.com?product='iPhone 13 Pro'&price=¥9999.00"
  4. function getUrlParams4(url){
  5. // 引入 qs 库时会默认挂在到全局 window 的 Qs 属性上
  6. // console.log(window)
  7. let urlStr = url.split('?')[1]
  8. //将 url 参数形式转为参数对象
  9. let result = Qs.parse(urlStr)
  10. // 拼接额外参数
  11. let otherParams = {
  12. num:50,
  13. size:6.1
  14. }
  15. //还能实现将参数对象转为 url 参数形式
  16. let str = Qs.stringify(otherParams)
  17. let newUrl = url + str
  18. return {result,newUrl}
  19. }
  20. console.log(getUrlParams4(URL))
  21. </script>

面试训练题集 - 图10

数组拍平

  1. function flat1(arr) {
  2. let arr1 = []
  3. arr.forEach((val) => {
  4. if (val instanceof Array) {
  5. arr1 = arr1.concat(flat1(val))
  6. } else {
  7. arr1.push(val)
  8. }
  9. })
  10. return arr1
  11. }

浅拷贝和深拷贝

浅拷贝和深拷贝区别

  • 浅拷贝:修改新变量的值会影响原有的变量的值。默认情况下引用类型(object)都是浅拷贝。
  • 深拷贝:修改新变量的值不会影响原有变量的值。默认情况下基本数据类型(number,string,null,undefined,boolean)都是深拷贝。

    如何实现浅拷贝

    默认赋值
    Object.assign() 和 …展开运算符
    函数库lodash的.clone方法
    特殊:解构赋值(只拷贝一层,第二层开始又属于浅拷贝) ```javascript var
    = require(‘lodash’); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.clone(obj1); console.log(obj1.b.f === obj2.b.f);// true
  1. <a name="q5PoA"></a>
  2. ### 如何实现深拷贝
  3. <a name="c3t0Q"></a>
  4. #### 使用JSON
  5. ```javascript
  6. const b = JSON.parse(JSON.stringify(a))
  7. //缺点:
  8. 不支持Date、正则、undefined、函数等数据
  9. 不支持引用(如环状结构,如对象里还有对其他对象的引用)

函数库lodash的_.cloneDeep方法

  1. var _ = require('lodash');
  2. var obj1 = {
  3. a: 1,
  4. b: { f: { g: 1 } },
  5. c: [1, 2, 3]
  6. };
  7. var obj2 = _.cloneDeep(obj1);
  8. console.log(obj1.b.f === obj2.b.f);// false

使用递归

  • 递归
  • 判断类型
  • 检查环(避免循环递归爆栈,使用 hash)
  • 不拷贝原型上的属性 ```javascript //内部源码 cosnt deepClone = (a,cache) => { if(!cache) {

    1. cache = new Map() //缓存不能全局,需要临时创建并递归传递

    } if(a instanceof Object) { //引用类型;不考虑跨 iframe 判断八种JS数据类型

    1. if(cache.get(a)) {return cache.get(a)}

    let result

    if(a instanceof Function) { //函数

    1. if(a.prototype) { //普通函数(有prototype)
    2. result = function() {return a.apply(this,arguments)}
    3. } else { //箭头函数
    4. result = (...args) => {return a.call(undefined,...args)}
    5. }

    }else if(a instanceof Array) { //数组

    1. result = []

    }else if(a instanceof Date) { //日期

    1. result = new Date(a - 0)

    }else if(a instanceof RegExp) { //正则

    1. result = new RegExp(a.source,a.flags)

    }else {

    1. result = {} //普通对象

    }

    cache.set(a,result)

    for(let key in a) {

    1. if(a.hasOwnProperty(key)) { //不能拷贝原型上的属性
    2. result[key] = deepClone(a[key],cache) //递归 ,且传入缓存
    3. }

    } return result

    }else {

    1. return a //基本数据类型

    } }

//使用 const a ={ number:1, bool:false, str:’hi’, empty1:undefined, empty2:null, array:[{name:’npc’,age:18},{name:’player’,age:19}], date:new Date(2000,0,1,20,30,0), regex:/.(j|t)sx/i, obj:{name:’npc2’,age:17}, f1:(a,b) => {a+b}, f2:function(a,b) => {return a+b} }

a.self = a //检查环,自己引用自己,递归循环没有出口会爆栈

const b = deepClone(a)

b.self = b //true

  1. **分析** 检查环:<br />当出现:`a.self = a `自己引用自己,递归循环没有出口无法退出会爆栈<br />解决:额外开辟一个存储空间来记录缓存,存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/12625512/1646498619095-6fa83ed3-acb3-4f42-b640-5a03275104e8.png#clientId=ua7f8fc58-39a3-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=291&id=u2ce70086&margin=%5Bobject%20Object%5D&name=image.png&originHeight=364&originWidth=513&originalType=binary&ratio=1&rotation=0&showTitle=false&size=68639&status=done&style=none&taskId=u5cd9a117-3284-4699-a3e9-e443377d59d&title=&width=410.4)<br />使用Map数据结构,声明一个缓存:cosnt cache = new Map() ,每次进入递归的时候检查一下缓存有无递归过该节点,如果缓存中能获取到对应的值则说明已经拷贝过,直接return对应值即可:`if(cache.get(a)) {return cache.get(a)}`,缓存中找不到对应值的话,在拷贝完成后set一下<br />问:为什么使用Map记录缓存?<br />答:因为Map的key可以是对象,object的key只能是string
  2. 问:缓存为什么无法放全局?<br />答:如果放全局,缓存无法清空,每次深拷贝的话,缓存中都会残留之前的对象,下次需要拷贝其他对象的话,可能存在冲突<br />问:那么将缓存放入递归函数内呢?<br />答:不可以,因为每次调用递归,都会生成新的缓存,这样无法遍历到嵌套对象的缓存<br />问:那缓存放哪里最合适?<br />答:缓存应该放入递归函数的参数中,每次进入递归的时候判断一下有无缓存,没有缓存则初始化缓存,然后在内部递归调用的时候传入缓存
  3. 注意:深拷贝无法考虑周到,无法考虑浏览器iframeDOM<br />-------<br />可参考loadash__.clone方法<br />实际工作场景基本不用,最好的方法是干掉出现深拷贝的代码需求
  4. <a name="xvf7M"></a>
  5. ## 实现数组去重
  6. ```javascript
  7. //计算排序变形的方法:缺点是只支持字符串
  8. function unique(arr) {
  9. const arr1 = []
  10. for(let i = 0;i<arr.length;i++){
  11. if(arr1.indexOf(arr[i]) === -1){
  12. arr1.push(arr[i])
  13. }
  14. }
  15. return arr1
  16. }
  17. //使用Set
  18. function unique(arr) {
  19. return [...new Set(arr)]
  20. }
  21. //使用Map
  22. function unique(arr) {
  23. const arr1 = []
  24. const map = new Map()
  25. for(let i = 0;i<arr.length;i++) {
  26. if(!map.has(arr[i])) { //只是借用了map的has判断机制,本质和方法一类似
  27. map.set(arr[i])
  28. arr1.push(arr[i])
  29. }
  30. }
  31. return arr1
  32. }

手写EventBus

  1. //内部源码
  2. const eventBus = {
  3. map:{
  4. // click:[f1,f2],
  5. //input:[f1,f2,f3...], --------- //使用hash表数据结构表示事件及其对应回调函数们,
  6. ... //每个事件都是一个任务队列
  7. }, //而任务队列用数组结构表示
  8. on:(name,fn) => {
  9. if(!eventBus.map[name]){return []}
  10. eventBus.map[name].push(fn)
  11. },
  12. off:(name,fn) => {
  13. if(!eventBus.map[name]){return}
  14. const index = eventBus.map[name].indexOf(fn)
  15. eventBus.map[name].splice(index,1)
  16. },
  17. emit:(name,data) => {
  18. if(!eventBus.map[name]){return}
  19. eventBus.map[name].map((fn) => {
  20. fn.call(undefined,data)
  21. return undefined //可思考有无必要返回值而定
  22. })
  23. }
  24. }
  25. //举例调用
  26. eventBus.on('click',f1)
  27. eventBus.on('click',f2)
  28. eventBus.off('click',f1)
  29. setTimeout(()=> {eventBus.emit('click','npc')},3000)
  1. //使用class手写发布订阅
  2. class EventHub {
  3. map = {}
  4. on(name, fn) {
  5. this.map[name] = this.map[name] || []
  6. this.map[name].push(fn)
  7. }
  8. emit(name, data) {
  9. const fnList = this.map[name] || []
  10. fnList.forEach(fn => fn.call(undefined, data))
  11. }
  12. off(name, fn) {
  13. const fnList = this.map[name] || []
  14. const index = fnList.indexOf(fn)
  15. if(index < 0) return
  16. fnList.splice(index, 1)
  17. }
  18. }
  19. // 使用
  20. const e = new EventHub()
  21. e.on('click', (name)=>{
  22. console.log('hi '+ name)
  23. })
  24. e.on('click', (name)=>{
  25. console.log('hello '+ name)
  26. })
  27. setTimeout(()=>{
  28. e.emit('click', 'frank')
  29. },3000)

手写一层Promise

  1. //内部源码
  2. class Promise2 {
  3. #status = 'pending'
  4. constructor(fn) {
  5. this.queue = []
  6. const resolve = (data) => {
  7. this.#status = 'fulfilled'
  8. //以下重难点开始理解:为什么需要一个个弹出f,因为promise调用的责任链关系,
  9. //比如resolve后返回reject
  10. const f1f2 = this.queue.shift()
  11. if(!f1f2 || !f1f2[0]) return
  12. const x = f1f2[0].call(undefined,data)
  13. if(x instanceof Promise2) { //如果resolve的预案执行后还返回promise对象的话:
  14. x.then((data) => {
  15. resolve(data) //调用下一个f1,开始使用递归
  16. },(reason) => {
  17. reject(reason) //调用下一个f2
  18. })
  19. }else {
  20. resolve(x) //调用下一个f1
  21. }
  22. }
  23. const reject = (reason) => {
  24. this.#status = 'rejected'
  25. const f1f2 = this.queue.shift()
  26. if(!f1f2 || !f1f2[1]) return
  27. const x = f1f2[1].call(undefined,reason)
  28. if(x instanceof Promise2) {
  29. x.then((data) => {
  30. resolve(data)
  31. },(reason) => {
  32. reject(reason)
  33. })
  34. }else {
  35. resolve(x)
  36. }
  37. }
  38. fn.call(undefined,resolve,reject)
  39. }
  40. then(f1,f2) {
  41. this.queue.push([f1,f2])
  42. }
  43. }
  44. //使用
  45. const p = new Promise2(function(resolve,reject) {
  46. setTimeout(function() {
  47. reject('出错')
  48. },3000)
  49. })
  50. p.then((data) => {console.log(data)},(reason) => {console.error(reason)})

手写Promise.all

  1. //内部源码
  2. Promise.all2 = (promiseList) => {
  3. return new Promise(
  4. (resolve,reject) => {
  5. const result = []
  6. const length = promiseList.lenghth
  7. let count = 0
  8. promiseList.map((promise,index) => { //遍历调用promiseList的每个promise后 再进入then
  9. promise.then((data) => { //调用promiseList 后调用then的预案
  10. result[index] = data
  11. count +=1
  12. if(count === length-1) {
  13. resolve(result) // result放满了三个并发promise的话 才调用resolve
  14. }
  15. },(reason) => {
  16. reject(reason) //只要有一个promise触发了reject的话 就调用reject
  17. })
  18. })
  19. })
  20. }
  21. //使用
  22. const promiseList = [Promise.resolve(1),Promise.reject(2),Promise.resolve(3)]
  23. Promise.all2(promiseList)
  24. .then((data) => {console.log(data)},(reason) => {console.error(reason)})

关于Promise 的手写建议搭配 画流程图 理解

垃圾回收机制

判断是否为垃圾(是否有可能‘被使用’)

那些没有被引用的+只是互相引用形成一个环的
举例:

  • 所有全局变量均不是垃圾
  • window对象不是垃圾(除非退出页面,如Object、Array、Promise这些对象均会长久在内存中保持着)
  • 局部变量在函数退出后就变成垃圾了
    • 所有的变量都有生命周期
    • 局部变量在函数执行的时候才会真正创建,而后再次执行函数的话,实际上会重新创建局部变量(与之前销毁的局部变量不是完全同一个)

单引用

双引用
注意:
两个互相引用的对象a和b,如果a不引用b,尽管b还引用a,但b仍然会被回收
因为判断是否为垃圾的要点是‘被使用’

环引用
如何做到回收垃圾c?
只需要断开 所有 指向/使用/引用 c 的的引用
image.png
无法到达的岛屿
几个对象相互引用,但外部没有对其任意对象的引用,这些对象也可能是不可达的,并被从内存中删除。
没有了外部对其的引用,所以它变成了一座“孤岛”,并且将被从内存中删除。
image.png

如何捡垃圾(垃圾回收算法)

标记扫除算法

  • 从全局作用域开始,把所有它遇到的变量都标记一下,然后它所有标记的变量如果引用了其他变量,继续标记其他变量,一直标记到找不到新的对象,标记就结束
  • 然后把所有没有被标记到的对象全都清除掉

    引用计数算法

  • 记录每个对象被引用的次数,每次对象被赋值引用的时候+1,删除引用的时候-1

  • 通过更新计数器来确定引用次数,如果计数器值为0则直接回收内存

补充:前端有个特殊性:
JS进程和DOM进程
页面上的元素尽管没有被引用,但不确定DOM没有使用它,就算JS内存中没有使用它,也不该说它被垃圾回收了

Node

Node中Eventloop

Event Loop即事件循环,是指 浏览器 或 Node 的一种解决 javaScript单线程 运行时不会阻塞的一种机制

Node.js 将各种函数(也叫任务或回调)分成至少 6 类,按先后顺序调用,因此将时 间分为六个阶段(每个阶段都有任务和微任务):

  1. timers 阶段(setTimeout)
  2. I/O callbacks 该阶段不用管
  3. idle, prepare 该阶段不用管
  4. poll 轮询阶段,停留时间最长,可以随时离开。一般从此开始
    a. 主要用来处理 I/O 事件,该阶段中 Node 会不停询问操作系统有没有文件数
    据、网络数据等
    b. 如果 Node 发现有 timer 快到时间了或者有 setImmediate 任务,就会主动离
    开 poll 阶段
  5. check 阶段,主要处理 setImmediate 任务
  6. close callback 该阶段不用管

每个阶段都有对应自己的nextTick(微任务),只是不同Node版本的执行时机不同
Node.js 会不停的从 1 到 6 循环处理各种事件,这个过程叫做事件循环(Event Loop)。

Process.nextTick()

执行时机?
在 Node.js 11 之前,会在每个阶段的末尾集中执行(俗称队尾执行)。
在 Node.js 11 之后,会在每个阶段的任务间隙执行(俗称插队执行)。 即在当前阶段执行完后立马执行该任务
浏览器跟 Node.js 11 之后的情况类似,由于浏览器中没有nextTick,故可以用 window.queueMicrotask 模拟 nextTick。
nextTick不插队自己人,只插队事件循环中不同的任务间隙

Promise

new Promise(fn) 的fn:是立刻执行的同步的,相当直接写 fn()
Promise.resolve(1).then(fn) 的 fn :直接参考nextTick
这要看 Promise 源码是如何实现的,一般都是用 process.nextTick(fn) 实现的,
所以直 接参考 nextTick。
resolve()会把Promise标记为完成状态,并能让后续then回调中具体操作resolve出去的异步数据,仅此而已

async/await

这是 Promise 的语法糖,所以直接转为 Promise 写法即可。
遇到await,将await删除,增加then回调在async()后面,将await后面的内容放在then中的回调里

函数执行顺序题目

粗暴技巧:先找同步的—->有nextTick则找nextTick—->微任务—->宏任务

  1. setTimeout(()=>{
  2. console.log('setTimeout')
  3. },0)
  4. setImmediate(()=>{
  5. console.log('setImmediate')
  6. })
  7. //在Node.js运行会输出:输出顺序不一定,事件循环是C++在做,关键看JS引擎和C++引擎哪个先启动
  8. //若是C++引擎(EventLoop)先启动,则先setTimout,若是Js引擎先启动,则先setImmdiate
  9. //EventLoop先启动,则Timer阶段已过停留在Poll阶段轮询,直到JS引擎一启动,发现check阶段有setImmdiate直接触发setImmdiate
  10. //Js引擎先启动,直到EventLoop启动则直接触发Timer阶段的setTimeout,因为EventLoop从Timer阶段开始
  11. //注意:JS引擎一启动则直接自动将各个任务分配到Eventloop的各个阶段,没有顺序之分
  12. //如何让setImmdiate必定先输出?
  13. //全局用一个setTimeout包裹
  14. setTimeout(()=>{
  15. setTimeout(()=>{
  16. console.log('setTimeout')
  17. },0)
  18. setImmediate(()=>{
  19. console.log('setImmediate')
  20. })
  21. },1000)
  22. //在浏览器中运行:
  23. //报错,因为浏览器中没有setImmdiate,可以使用queueMicrotask模拟,则必定setImmdiate先输出
  1. setImmediate(()=>{
  2. console.log('setImmediate1')
  3. setTimeout(()=>{
  4. console.log('setTimeout2')
  5. },0)
  6. })
  7. setTimeout(()=>{
  8. console.log('setTimeout2')
  9. setImmediate(()=>{
  10. console.log('setImmediate2')
  11. })
  12. })
  13. //setImmediate1 setTimeout2 setTimeout2 setImmediate2
  1. async function async1(){
  2. console.log('1')
  3. async2().then(()=>{
  4. console.log('2')
  5. })
  6. }
  7. async function async2(){
  8. console.log('3')
  9. }
  10. console.log('4')
  11. setTimeout(function(){
  12. console.log('5')
  13. },0)
  14. async1()
  15. new Promise(function(resolve){
  16. console.log('6')
  17. resolve()
  18. }).then(function(){
  19. console.log('7')
  20. })
  21. console.log('8')
  22. //4 1 3 6 8 2 7 5

浏览器中的任务和微任务

宏任务和微任务是存在于Node.js中的,浏览器中不存在宏任务(Macrotask)和微任务,
浏览器中的是任务(task)和微任务(Microtask)

浏览器事件循环只有一个阶段,前面是任务,后面是微任务
1. 使用 script 标签、setTimeout 可以创建任务。
2. 使用 Promise.then、window.queueMicrotask、MutationObserver、Proxy 可以 创建微任务。

执行顺序:
和Node.js类似,微任务会在任务间隙执行(俗称插队执行)
注意:
微任务不能插微任务的队,微任务只能插任务的队
一般来说,考Promise不考Node.js中的,因为Node.js中的Promise可能是setImmdiate或者是nextTick构造的
或者通过观察有无setImmediate和nextTick,没有的话当做浏览器做题即可

函数执行顺序题目

  1. Promise.resolve()
  2. .then(()=>{
  3. console.log(0)
  4. return Promise.resolve('4x')
  5. })
  6. .then((res)=>{console.log(res)})
  7. Promise.resolve().then(()=>{console.log(1);})
  8. .then(()=>{console.log(2);},()=>{console.log(2.1)})
  9. .then(()=>{console.log(3)})
  10. .then(()=>{console.log(5)})
  11. .then(()=>{console.log(6)})

express和koa区别

  1. 中间件模型不同:express 的中间件模型为线型,而 koa 的为U型(洋葱模型)。
  2. 对异步的处理不同:express 通过回调函数处理异步,而 koa 通过generator 和
    async/await 使用同步的写法来处理异步,后者更易维护,但彼时 Node.js 对
    async 的兼容性和优化并不够好,所以没有流行起来。
  3. 功能不同:express 包含路由、渲染等特性,而 koa 只有 http 模块。
    总得来说,express 功能多一点,写法烂一点,兼容性好一点,所以当时更流行。虽
    然现在 Node.js 已经对 await 支持得很好了,但是 koa 已经错过了风口。

算法篇

排序算法

唯一的特例:MAth 只是普通对象,不是函数也不是构造函数,虽然看起来像构造函数

什么是递归:(需要 代入法/调用栈 理解)
函数不停调用自己,每次调用的参数略有不同,但格式基本相同
先递进,后回归
先递进到当满足某个简单条件时,实现一个简单的调用
然后陆续回归
最终算出结果

  1. //递归示例:
  2. min([2,3,6,1])
  3. min(2,min(3,6,1))
  4. min(2,min(3,min(6,1)))
  5. min(2,min(3,1))
  6. min(2,1)
  7. 1
  8. //也可以用调用栈来理解
  9. //每次进入下一行即压栈,直到实现简单调用后,再陆续弹栈

两个数字找最小:
用什么表示两个数字?数组
let minOf2 = ([a,b]) => a < b ? a : b
以此类推:三个数字找最小、四个数字找最小。。。
image.png
那么,任意长度数组求最小值?补充:递归求
image.png
发现问题:以上代码是个死循环,因为min会一直调用min
解决问题:添加判断机制,判断数组长度为2时简单调用然后回归
image.png

那么,将正整数数组从小到大排序?结合求最小值
同理,先求2个数排序、3个数排序、4个数排序、然后使用递归

排序思路:

递归/循环,且所有递归均可以改写为循环

以下示例 为递归写法

选择排序从0到1

总体思路:每次选择最小的放前面,然后后面部分重复 选择最小的放前面,选到最后没得选了就是排完了

2个数字排序:
let sort2 = ([a,b]) => a < b ? [a,b] : [b,a]
image.png
3个数字排序:
image.png
其中 minIndex 和min 方法如下:
image.png
4个数字排序:
image.png
任意长度数字排序:
image.png
遇到问题:死循环
解决问题:添加判断机制
image.png
以上排序其实就是‘选择排序’的思路
每次排序都找到最小的提到前面,然后对剩余部分进行排序,在剩余部分排序中,依然把其中最小的提到前面,如此递进,即每次找到最小的数放前面,然后对后面的数做同样的事情

补充:传值/传址 区别,举例如下:

  1. //传值(基本数据类型)
  2. let swap = (a,b) => {
  3. let temp = a
  4. a = b
  5. b = temp
  6. }
  7. numbers = [100,200]
  8. swap(numbers[0],numbers[1]) // [100,200]
  9. //若想要期待的结果,可以在函数中return函数体操作的值
  10. let swap = (a,b) => {
  11. let temp = a
  12. a = b
  13. b = temp
  14. return [a,b] //函数返回多个值可以用数组表示
  15. }
  16. numbers = [100,200]
  17. swap(numbers[0],numbers[1]) // [200,100]
  18. //传址(引用数据类型)
  19. let swap = (numbers,i,j) => {
  20. let temp = numbers[i]
  21. numbers[i] = numbers[j]
  22. numbers[j] = temp
  23. }
  24. numbers = [100,200,300]
  25. swap(numbers,1,2) //[100,300,200]

以上选择排序示例改写为循环写法
思路不变:每次找到最小的数放前面,然后对后面的数做同样的事情,然后++

  1. //选择排序的循环写法
  2. let sort = (numbers) => {
  3. for(let i = 0 ; i<numbers.length -1 ; i++){
  4. let index = minIndex(numbers.slice(i)) + i
  5. if(index !=== i ) {
  6. swap(numbers,index,i) //交换index 和 i 对应元素的位置
  7. }
  8. }
  9. return numbers
  10. }
  11. //minIndex 和 swap方法的封装
  12. let swap = (numbers,i,j) => {
  13. let temp = numbers[i]
  14. numbers[i] = numbers[j]
  15. numbers[j] = temp
  16. }
  17. let minIndex = (numbers) => {
  18. let index = 0
  19. for(let i = 1;i<numbers.length;i++){
  20. if(number[i] < numbers[index]) {
  21. index = i
  22. }
  23. }
  24. return index
  25. }

总结:

  • 所有递归都能改成循环
  • 如果数组有10个元素,则需要选择8次,因为第1个不用选,用第1个对比第2个即可,第10个不用选,用第9个对比第10个即可
  • 循环的时候有很多细节干扰,需要动手枚举找到规律,尤其是边界条件很难确定
  • 学会打log
  • 时间复杂度大概是n^2

快速排序

总体思路:以某个元素为基准,小的放前面,大的放后面,那个基准就排好了,然后前后两个部分继续重复操作,直到一个数组中只剩下一个元素

使用递归思路
指谁就固定谁
数组有10个元素,则需要指定10次

  1. let quickSort = () => {
  2. if(arr.lenght <= 1 ) {return arr}
  3. let pivotIndex = Math.foor(arr.length /2)
  4. let piovt = arr.splice(pivotIndex,1)[0]
  5. let left = []
  6. let right = []
  7. for(let i = 0;i<arr.length;i++) {
  8. if(arr[i] < pivot) {left.push(arr[i])}
  9. else{right.push(arr[i])
  10. }
  11. }
  12. return quickSort(left).concat([pivot],quickSort(right))
  13. }

归并排序

总体思路:将多个元素组成的数组分裂至一个个独占一个元素的数组们,然后我需要做的只是两两判断数组的头个元素大小,将小的放入一个新的数组中,然后重复判断剩余部分,直到需要判断的部分为空

①先让数组完全分裂直至一个数组只包含一个元素
②然后让每个数组彼此两两对比大小,按小大顺序分别塞入新的数组中

  1. //先分后治
  2. let mergeSort = (arr) => {
  3. if(arr.length === 1) {return arr} //精髓部分,假装排好序,其实全部数组均分裂为每个元素独占一个数组的情况即可
  4. let left = arr.splice(0,Math.floor(arr.length / 2))
  5. let right = arr.splice(Math.floor(arr.length / 2))
  6. return merge(mergeSort(left),mergeSort(right)) //进入递归
  7. }
  8. //封装merge方法
  9. let merge = (a,b) => {
  10. if(a.length === 0) {return b}
  11. if(b.length === 0) {return a}
  12. return a[0] < b[0] ? // 精髓部分,重点是重复对比数组头一个的大小
  13. [a[0]].concat(merge(a.slice(1),b)) //进入递归
  14. :
  15. [b[0]].concat(merge(a,b.slice(1))) //进入递归
  16. }

计数排序

总体思路:遍历数组,将其元素和对应个数放入新建的哈希表中,同时获取其最大值max和最小值min,然后遍历哈希表,从最小值开始,若该值存在于哈希表中则将其放入新建的结果数组中,值得注意的是,若该值存在多个,则需要写双重遍历,而双重遍历也涵盖了值只有一个的情况,只需要让双重遍历的计数器从min-1开始

时间复杂度最低
使用了额外的哈希表
只遍历数组一遍,不过还要遍历一次哈希表(用空间换时间)

应用:
统计一段字符串中字符出现的次数

  1. let countSort = (arr) => {
  2. let hashTable = {}
  3. let max = 0
  4. let result = []
  5. for(let i =0;i<arr.length; i++) { //遍历数组,创建哈希表以计数
  6. if(!arr[i] in hashTable) {
  7. hashTable[arr[i]] = 1
  8. }else {
  9. hashTable[arr[i]] += 1
  10. }
  11. if(arr[i] > max ) { //确定最大值
  12. max = arr[i]
  13. }
  14. }
  15. for(let j =0;j<=max; j++) { //遍历哈希表
  16. if(j in hashTable) {
  17. for(let k = 0; k<hashTable[j]; k++) { //难点:情况:有两个相同的元素;思考k为什么取0
  18. result.push(j)
  19. }
  20. }
  21. }
  22. return result
  23. }

时间复杂度

选择排序O(n^2)
快速排序O(n log2 n)
归并排序O(n log2 n)
计数排序O(n+ max)

HTTP篇

HTTP1 和 HTTP2 的区别

image.png

HTTP 和 HTTPS 的区别

HTTPS = HTTP + SSL / TLS(安全层)

  • http 使用80端口;https 使用443端口
  • https 的证书一般需要购买,http不需要购买
  • http 连接是无状态的;https 协议是由ssl+http协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全

image.png

HTTP2.0的多路复用和HTTP1.X中的长连接复用有什么区别?

  • HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个请求都要建立一个连接
  • HTTP/1.1 Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,因为传输格式是文本的,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞
  • HTTP/2多个请求可同时在一个连接上并行执行(由于支持二进制的格式,可以无序)某个请求任务耗时严重,不会影响到其它连接的正常执行

    跨域问题

    同源策略

    window.origin 或者 location.origin 可以得到当前源
    源 = 协议 + 域名 + 端口号
    定义:
    不同源的页面之间,不能相互访问数据(本地存储的各种数据都不能跨域)

注意:
https://baidu.comhttps://www.baidu.com 不同源;需要完全一致才算同源
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了

如果 JS 运行在 源A 里,那么就只能获取源A的数据,不能获取源B的数据,即不予许跨域
这是浏览器自带的功能

举例:
假设a.com/index.html 引用了 cdn.com/1.js
那么就说‘1.js 运行在源a.com里
注意这和cdn.com没有关系,只是1.js从它那下载,真正看的是js运行在哪 则源在哪
所以1.js 就只能获取a.com 的数据
不能获取 qq.com或者其他源的数据

目的:为了保护用户隐私
假设没有同源策略,那 a.com 和 b.com 就能发送同样的请求给服务器然后获取重要数据信息,因为发送的请求标头中 只有 Referer 不同,而Referer 标记的是当前源,即可以做 Referer 检查

理解:为什么可以跨域使用CSS、JS和图片等?
引用/访问JS,但不能读取JS,同源策略限制的是数据访问;
我们引用CSS、JS和图片的时候,其实并不知道其内容,我们只是在引用

一般cdn会默认所有的跨域请求

CORS跨域资源共享

特点:
CORS使用简单,支持post方式,但是存在兼容问题,复杂请求需要先做一个预检请求才能再发真实的请求,发了两次会有性能上的损耗

简单请求和复杂请求

①对于简单请求:
GET、某些POST、HEAD(post一个json的Content-Type是复杂请求)
满足下面两个条件,就属于简单请求👇
条件1:使用下列方法之一:
GET
HEAD
POST
条件2:Content-Type 的值仅限于下列三者之一👇
text/plain
multipart/form-data
application/x-www-form-urlencoded

对应方法:
在有数据的那头源里设置:在其后台设置响应头:
response.setHeader('Access-Control-Allow-Origin':'https//foo.example')
允许任何网站跨域请求:
response.setHeader('Access-Control-Allow-Origin':request.header['referer'])一般不写*

②对于复杂请求:
除了简单请求,其他均为复杂请求,复杂请求会在正式发起请求之前进行一次预检请求,该预检请求的方式为options 方法,可以检测服务器支持哪些 HTTP 方法,”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响
如PATCH请求

请求响应交互补充:

预检请求头request header的关键字段:

Request Header 作用
Access-Control-Request-Method 告诉服务器实际请求所使用的 HTTP 方法
Access-Control-Request-Headers 告诉服务器实际请求所携带的自定义首部字段,本次实际请求首部字段中content-type为自定义

预检响应头response header的关键字段:

response header 作用
Access-Control-Allow-Methods 返回了服务端允许的请求,包含GET/HEAD/PUT/PATCH/POST/DELETE
Access-Control-Allow-Credentials 允许跨域携带cookie(跨域请求要携带cookie必须设置为true)
Access-Control-Allow-Origin 允许跨域请求的域名,这个可以在服务端配置一些信任的域名白名单
Access-Control-Request-Headers 客户端请求所携带的自定义首部字段content-type

后端设置对应方法:

  1. 响应OPTIONS预检请求,在响应中添加如下响应头:

    1. //响应OPTIONS请求:
    2. response.setHeader('Access-Control-Allow-Origin':'https//foo.example')
    3. response.setHeader('Access-Control-Allow-Method':'POST,GET,OPTIONS')//表明服务器允许客户端使用 POST 和 GET 方法发起请求
    4. response.setHeader('Access-Control-Allow-Headers':...预检请求header中自定义字段如'Content-Type')
  2. 响应实际请求,在响应中添加Access-Control-Allow-Origin

如果需要附带身份信息,JS中需要在AJAX中设置:
xhr.withCredentials = true

JSONP

本质:
让JS包含数据
通过沟通协商,确定函数名,使用跨域的回调
image.png
定义:
创建一个script标签,请求一个JS文件,这个JS文件会执行回调
回调里面就有我们想要的数据

问:这个回调的函数名怎么确定?
答:回调的名字是由随机数随机生成的,我们以callback的参数传给后台,后台会把这个函数返回给我们并执行

优点:

  • 兼容IE
  • 改动较小,只需要后端改动js文件内容

缺点:

  • 由于它是script标签,所以它读不到像Ajax那么精确的状态,没有更好的错误处理(不知道状态码、响应头是什么,只知道成功还是失败,即只能监听 onload 和 onerrer )
  • script 只能发 GET 请求,不支持POST
  • 没有认证,全世界所有人都可以访问,解决缺点是 加上token进行用户认证或者进行Referer检查
  • 主要是老的浏览器支持

问:这样的话所有网站均可跨域访问我们的JS
答:那如何做到指定网站才可以访问? 做 Referer 检查

HTTP 是什么?HTTP 是基于 TCP/IP 的关于数据如何在万维网中如何通信的协议。

Nginx代理 / Node.js 代理

使用nginx或者node做中间服务器

HTTP状态码

  • 2XX 成功

200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
206 Partial Content,进行范围请求

  • 3XX 重定向

301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源
304 not modified,//未修改 自动上次请求后,请求的网页未修改过。服务器返回此响应,不会返回网页的内容
307 temporary redirect,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
301 和 302 使用场景:

  • 301:

比较常用的场景是使用域名跳转。
比如,我们访问 http://www.baidu.com 会跳转到 https://www.baidu.com,发送请求之后,就会返回301状态码,然后返回一个location,提示新的地址,浏览器就会拿着这个新的地址去访问。

  • 302:

比如,未登陆的用户访问用户中心重定向到登录页面。
访问404页面会重新定向到首页。
301 和 302区别:

  • 302重定向只是暂时的重定向,搜索引擎会抓取新的内容而保留旧的地址,因为服务器返回302,所以,搜索搜索引擎认为新的网址是暂时的。
  • 而301重定向是永久的重定向,搜索引擎在抓取新的内容的同时也将旧的网址替换为了重定向之后的网址。

  • 4XX 客户端错误

400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源,可能是请求的路径有问题

  • 5XX 服务器错误

500 internal sever error,表示服务器端在执行请求时发生了错误
501 Not Implemented,表示服务器不支持当前请求所需要的某个功能
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

HTTP缓存

  • http缓存只能缓存get请求响应的资源,对于其他类型的响应则无能为力,所以后续说的请求缓存都是指GET请求。
  • HTTP 缓存的相关设置参数都是在头信息中携带的;
  • 强制缓存如果生效,不需要再和服务器发生交互,而对比缓存不管是否生效,都需要与服务端发生交互。
  • http缓存都是从第二次请求开始的。第一次请求资源时,服务器返回资源,并在respone header头中回传资源的缓存参数;第二次请求时,浏览器判断这些请求参数,命中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否命中协商缓存,命中则返回304,否则服务器会返回新的资源。
  • 缓存相关的规则信息就包含在header中。boby中的内容是HTTP请求真正要传输的部分

面试训练题集 - 图25
常见http缓存有哪几种?

  • Etag
  • Expires
  • Cache-Control

以上缓存两两区别是什么?

强缓存

image.png
以上图按行看
最快的web性能优化?
不做web请求

Expire(http1.0):
Expires 响应头表示到期时间点。即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据,作为HTTP 1.0的作品,所以它基本可以忽略
问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。 所以HTTP 1.1 的版本,使用Cache-Control替代

Cache-Control(http1.1):
Cache-Control 响应头表示时间段。常见的取值有no-cache、no-store、max-age、private、public、,默认为private。见下表:

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

no-cache 和 no-store 应用场景区别:

字段值 作用
no-cache 防止从缓存中返回过期的资源,所以使用之前,需要和源服务器发起请求比对过期时间
no-store 这个指令才是真正的不进行缓存,暗示请求报文中可能含有机密信息,不可缓存

如何更新缓存?
在后台把路径url改了即可,浏览器检测到url改变则会自动重新请求资源
(在webpack打包的时候可以配置每次更新打包均生产新的hash文件名,如此一来文件路径就更改了,浏览器接收到不同路径的文件就会自动重新请求资源)

在不改变url 的时候把强缓存删掉?
用ajax发送 no-cache 的请求,而且必须从浏览器做这件事情,因为缓存是在浏览器上的

在Cache-Control 中,这些值可以自由组合,多个值如果冲突时,也是有优先级的,而no-store优先级最高。如下图:
image.png

区别

Cache-Control 与 Expires 强缓存的区别:(安全性角度)
两者都是存储过期时间,但Expire存储的是到期时间点,客户端可以随意更改本地时间从而影响资源到期时间,而Cache-Control存储的是时间段max-age,相对来说Cache-Control的安全性更高

协商缓存

也称对比缓存,客户端会先从浏览器的缓存数据库拿到一个缓存的标识,然后向服务端验证标识是否失效,如果没有失效服务端会返回304,这样客户端可以直接去缓存数据库拿出数据,如果失效,服务端会返回新的数据并且返回新的缓存标识;
响应头中一定有etaglast-modified属性。 其http的状态码返回传说中的:Status Code: 304

last-modified(http1.0):

  1. 服务器第一次响应请求时,告诉浏览器资源的最后修改时间,并存储到浏览器端。
  2. 再次请求时,请求头中携带If-Modified-Since字段,将上次请求服务器资源的最后修改时间传到服务器与被请求资源的最后修改时间进行比对。
  3. 若资源的最后修改时间大于If-Modified-Since的值,说明资源又被改动过,那么开始传输响应一个整体,服务器返回:200 OK
  4. 若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,那么只需传输响应header,服务器返回:304 Not Modified,告知浏览器继续使用所保存的cache

etag(http1.1):

  1. 服务器第一次响应请求时,告诉浏览器资源唯一标志MD5值,并存储到浏览器端。
  2. 同理,再次请求时,请求头中携带If-None-Match字段。与被请求资源的唯一标识进行比对
  3. 若不同,说明资源又被改动过,则响应整片资源内容,返回状态码200;
  4. 若不同,说明资源没有被改动过,则响应HTTP 304,告知浏览器继续使用所保存的cache
    区别
    last-modified 和 etag 的区别:(安全性角度)
    last-modified 带的特征值为一个时间点,精度只到秒,精度太粗,可能一秒内发生十次请求,则无法确定最后资源的修改时间点,而etag特征值为哈希,每次生成哈希值都不同,从而确保安全性

image.png

HTTP缓存优先级

强制缓存优先级 高于 协商缓存。 也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。

  • 强制缓存: cache-control (http1.1) > expires(http1.0)、
  • 对比缓存: etag(传送If-None-Match) > last-modified (传送If-Modified-Since)

    HTTP缓存优点

  1. 减少了冗余的数据传递,节省宽带流量
  2. 减少了服务器的负担,大大提高了网站性能
  3. 加快了客户端加载网页的速度 这也正是HTTP缓存属于客户端缓存的原因。

不同刷新的请求执行过程

浏览器地址栏中写入URL,回车:
浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)
强制缓存有效,协商缓存有效
F5:
F5就是告诉浏览器,别偷懒,好歹去服务器看看这个文件是否有过期了。于是浏览器就战战兢兢的发送一个请求带上If-Modify-since。
强制缓存失效,协商缓存有效
Ctrl+F5:
告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端就完成了强行更新的操作.
强制缓存失效,协商缓存失效

GET和POST的区别

get和post最大的区别是语义上的区别,以下讲解实现上具体的区别
image.png
image.png
image.png

语义上:
GET 是从服务器获取数据,POST 是向服务器提交数据

参数传递的方式上:
GET 的参数一般是通过 ? 跟在 URL 后面的,多个参数通过 & 连接,比如:www.example.comserach=bianchengsanmei&content=123。
POST 的参数一般是包含在 request body 中的

其实,这个区别不是绝对的,GET 也可以通过 params 携带参数,而 POST 的URL 后面也可以携带参数,只是我们通常不建议这么做而已。

安全性上:
因为参数传递方式的不同,所以 GET 和 POST 的安全性不同:
GET 比 POST 更不安全,因为参数直接暴露在URL上,所以 GET 不能用来传递敏感信息。
GET 请求参数会被完整保留在浏览器历史记录里,而 POST 中的参数不会被保留。
GET 用于获取信息,是无副作用的,是幂等的, 而POST 用于修改服务器上的数据,有副作用,非幂等。

从传输的角度来说,他们都是不安全的,因为 HTTP 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文,要想安全传输,就只有加密,也就是 HTTPS。 幂等的概念是 这个操作重复很多遍,并不改变结果

参数长度限制上:
get传送的数据量较小,不能大于2KB。
post传送的数据量较大,一般被默认为不受限制,但其实可以设置为4M 10M 20M

在这里我们要明确一点:HTTP 协议没有 Body 和 URL 的长度限制,对 URL 限制的大多是浏览器和服务器的原因。 服务器是因为处理长 URL 要消耗比较多的资源,为了性能和安全(防止恶意构造长 URL 来攻击)考虑,会给 URL 长度加限制。

参数数据类型上:
GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。

缓存机制上:
GET 请求会被浏览器主动缓存,而 POST 不会,除非手动设置。
GET 请求参数会被完整保留在浏览器历史记录里,而 POST 中的参数不会被保留。
GET 产生的 URL 地址可以被收藏为书签,而 POST 不可以。
GET 在浏览器回退时是无害的,而 POST 会再次提交请求。

时间消耗上:
GET 和 POST 请求时间的不同主要是因为:
GET 产生一个 TCP 数据包/报文;(因为GET请求参数只在url中,没有request body)
POST 产生两个 TCP 数据包/报文。

对于 GET 方式的请求,浏览器会把 header 和 data 一并发送出去,服务器响应 200(返回数据);而对于 POST,浏览器先发送 Header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据),详细分析一下:

POST 请求的过程:
浏览器请求 TCP 连接(第一次握手)
服务器答应进行 TCP 连接(第二次握手)
浏览器确认,并发送 POST 请求头(第三次握手,这个报文比较小,所以 HTTP 会在此时进行第一次数据发送)
服务器返回100 Continue响应
浏览器发送数据
服务器返回 200 OK响应

GET 请求的过程:
浏览器请求 TCP 连接(第一次握手)
服务器答应进行 TCP 连接(第二次握手)
浏览器确认,并发送 GET 请求头和数据(第三次握手,这个报文比较小,所以 HTTP 会在此时进行第一次数据发送)
服务器返回 200 OK响应

HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上request body,给POST带上url参数,技术上是完全行的通的。

Cookie、LocalStorage、SessionStorage、Session区别

image.png
Cookie V.S. LocalStorage
主要区别是 Cookie 会被发送到服务器,而 LocalStorage 不会
Cookie 一般最大 4k,LocalStorage 可以用 5Mb 甚至 10Mb(各浏览器不同)
LocalStorage V.S. SessionStorage
LocalStorage 一般不会自动过期(除非用户手动清除),而 SessionStorage 在回话结束时过期(如关闭浏览器)
Cookie V.S. Session
Cookie 存在浏览器的文件里,Session 存在服务器的文件里
Session 是基于 Cookie 实现的,具体做法就是把 SessionID 存在 Cookie 里

localStroage是html5才出的,早期用的是cookie,前端一般不碰cookie

Cookie 和 Session

Cookie:服务器发给浏览器的一段字符串,浏览器每次访问服务器的时候需要带上这段字符串
Session:会话;表示浏览器与服务器一段时间内的会话
区别:
Cookie一般是存在于浏览器上;Session 一般是存在于服务器上
Session 是基于Cookie 实现的,具体做法是把 Session ID 放到Cookie 里

应用场景:
cookie:
(1)判断用户是否登陆过网站,以便下次登录时能够实现自动登录(或者记住密码)。如果我们删除cookie,则每次登录必须从新填写登录的相关信息。
(2)保存上次登录的时间等信息。
(3)保存上次查看的页面
(4)浏览计数
session:
Session用于保存每个用户的专用信息,变量的值保存在服务器端,通过SessionID来区分不同的客户。
(1)网上商城中的购物车
(2)保存用户登录信息
(3)将某些数据放入session中,供同一用户的不同页面使用
(4)防止用户非法登录

生命周期:
cookie:可设置失效时间,没有设置的话,默认是关闭浏览器后失效
localStorage:除非被手动清除,否则将会永久保存。
sessionStorage: 仅在当前网页会话下有效,关闭页面或浏览器后就会被清除。

存放数据大小:
cookie:4KB左右
localStorage和sessionStorage:可以保存5MB的信息。

http请求:
cookie:每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题
localStorage和sessionStorage:仅在客户端(即浏览器)中保存,不参与和服务器的通信

HTTP1.x和HTTP2区别

  • 多路复用:

即连接共享,即每一个request 都是用作连接共享机制的。一个request 对应一个id ,这样一个连接上可以有多个request ,每个连接的request 可以随机的混杂在一起, 接收方可以根据request 的id 将request 再归属到各自不同的服务端请求里面。

  • 服务端推送
  • HTTP2强制开始https协议

    TCP 三次握手四次挥手

    TCP :传输内容协议,可以传输任何内容,是个传输层,一般传输HTTP内容,即应用层是HTTP,包括请求和响应
    TCP连接发生在 找到服务器IP地址后
    三次握手:
    image.png
    四次挥手:
    image.png
    image.png
    为什么2和3不合并?
    因为2的时候服务器还可以有接收和发送的能力

    TCP和UDP的区别

问:为什么需要三次握手,两次不行吗?
答:其实这是由 TCP 的自身特点可靠传输决定的。
客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。

浏览器输入url到页面加载完毕发生了什么

  1. DNS 解析:将域名解析成 IP 地址
  2. TCP 连接:TCP 三次握手
  3. 发送 HTTP (S)请求
  4. 服务器处理请求并返回 HTTP 报文
  5. 浏览器解析渲染页面(由GUI渲染线程的主要工作)
    1. 解析html文件,构建DOM树
    2. 解析css文件,构建CSSOM树
    3. 合并DOM和CSSOM变成渲染树
    4. 布局和绘制(重绘和回流)
  6. 断开连接:TCP 四次挥手

1.DNS解析:
输入url后,浏览器会进行DNS查找,根据域名查到对应的IP地址,根据IP地址找到提供网站内容的服务器(可以理解为DNS是个数据库)

2.TCP连接
现在大部分浏览器基于https协议,所有会增加 TLS握手,建立加密隧道,保证数据传输不被监听和篡改

3.发送http(s)请求
以获取服务器响应,对应网站而言,响应内容就是html文件
【一开始浏览器得到的是显示 字节 内容的html文件,然后把字节转化为字符,然后把字符转化为Token字符标签,然后把Token转化为节点对象,对象是可以通过编程操作的,因为对象会有自己的属性方法,相当于把Token盘活了,最后把这些对象都连接在一起形成 文档对象模型,也就是DOM,DOM其实就是浏览器自己的语言,每个节点对象父子相连形成树型结构,也便于对节点进行操作

注意:html、css、js文件生命周期:请求、下载、解析、执行

5.浏览器解析渲染页面
DOM树 是html文档在浏览器中的对象表示,可以使用JavaScript来操作它,同时也可以操作CSSOM树
浏览器在解析html的时候是顺序执行的,并且只有一个主线程负责解析

  • 构建DOM树:

会影响主线程html解析的有:
script标签:浏览器会暂停执行html(构建DOM),而去加载JS文件和执行JS文件
CSSOM的构建:也会阻塞渲染过程,需要等待css树一次性构建完成然后和Dom树合并成渲染树
DOM的构建
不会影响主线程html解析的有:
图片、设置了async或者defer的script标签 会异步加载
另外,浏览器有个与扫描线程:它会扫描html代码,提前把css文件、字体以及js文件异步下载下来,不会影响主线程

  • 构建CSSOM树:

浏览器在构建DOM树的时候遇到link标签,然后向服务器发送http请求,得到css文件,后面的流程同处理html文件相似,最后节点会形成 CSS对象模型

  • DOM和CSSOM的区别:

DOM可以部分解析,而CSSOM不能部分解析

  • 合并DOM和CSSOM:

浏览器会从DOM根节点开始,合并CSSOM样式到DOM中的每个节点,捕获可见内容,形成一颗渲染树,某些html标签和css样式不会被挂在渲染树上,比如meta标签和link标签的内容、display为none的样式

  • 布局和绘制:

生成渲染树后,获取渲染树的结构、节点位置和大小,布局是依据盒子模型来进行的,也就是每个元素都用一个盒子来表示,然后这些盒子在页面上进行排列和嵌套
布局完成后,把渲染树以像素的形式通过GPU绘制,合成图层,将内容显示在屏幕上

补充:GPU绘制和CPU绘制
GPU是图形处理器,开启gpu渲染加速后可以提升图形加载速度,降低cpu处理器的负担,使系统运行更加流畅,但是也更加耗电。
CPU,即中央处理器

示例如下:
image.png

CSS 和 JS 会阻塞浏览器渲染吗

  • head中的link标签的引入的css:

异步加载,异步解析,但是阻塞render tree的生成,所以会阻塞首屏渲染

  • head中的script标签引入的js:

同步加载,同步解析,完全阻塞html解析和首屏渲染

性能优化之回流重绘

回流
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

  • 页面首次渲染
  • 浏览器窗口大小发生改变
  • 元素尺寸或位置发生改变
  • 元素内容变化(文字数量或图片大小等等)
  • 元素字体大小变化
  • 添加或者删除可见的DOM元素
  • 激活CSS伪类(例如::hover)
  • 查询某些属性或调用某些方法

一些常用且会导致回流的属性和方法:

  • clientWidth、clientHeight、clientTop、clientLeft
  • offsetWidth、offsetHeight、offsetTop、offsetLeft
  • scrollWidth、scrollHeight、scrollTop、scrollLeft
  • scrollIntoView()、scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

重绘
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

解决方案:
CSS:

  • 避免使用table布局。
  • 避免设置多层内联样式。
  • 将动画效果应用到position属性为absolute或fixed的元素上,因为它们脱离了文档流
  • 使用transform代替left、top位置变化
  • 避免使用CSS表达式(例如:calc())

JS:

  • 避免频繁操作样式和DOM,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

XSS 和 CSRF 攻击及防御

XSS

案例:

恶意用户 H 提交评论「」,然后用户 B 来访问网站,这段脚本在 B 的浏览器直接执行,恶意用户 H 的脚本就可以任意操作 B 的 cookie,而 B 对此毫无察觉。有了 cookie,恶意用户 H 就可以伪造 B 的登录信息,随意访问 B 的隐私了。而 B 始终被蒙在鼓里。

攻击原理:
跨站脚本攻击(Cross Site Scripting)
恶意攻击者往 Web 页面里插入恶意 Script 代码,当用户浏览该页之时,嵌入其中 Web 里面的 Script 代码会被执行,从而达到恶意攻击用户的目的

防御手段:

  • 设置HttpOnly,在 cookie 中设置 HttpOnly 属性后, js 脚本将无法读取到 cookie 信息。严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。
  • 输入检查,检查用户输入的数据中是否包含<,>等特殊字符,如果存在,则对特殊字符进行过滤或编码

    CSRF

    攻击原理:
    跨站请求伪造(Cross-site request forgery)
    伪造请求,冒充用户在站内的正常操作。我们知道,绝大多数网站是通过 cookie 等方式辨识用户身份,再予以授权的。所以要伪造用户的正常操作,最好的方法是通过 XSS 或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。

防御手段:

  • 验证码
  • Referer 检查,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的”源”
  • token 验证,在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是CSRF 攻击而拒绝该请求

区别

  • 原理不同,CSRF是利用网站A本身的漏洞,去请求网站A的api;XSS是向目标网站注入JS代码,然后执行JS里的代码。
  • CSRF需要用户先登录目标网站获取cookie,而XSS不需要登录
  • CSRF的目标是用户,XSS的目标是服务器
  • XSS是利用合法用户获取其信息,而CSRF是伪造成合法用户发起请求

DOM篇

DOM事件模型

  • 先经历从上到下的 捕获阶段,再经历从下到上的 冒泡阶段
  • 元素没有绑定监听器addEventListener,则默认传播路径为 冒泡
  • 元素绑定监听器addEventListener(‘click’,fn,true/false) 可触发捕获阶段;第三个参数可以true 为捕获,false/不传为冒泡
  • 可以使用 event.stopPropagation() 阻止传播(不可阻止捕获,可以阻止冒泡)
  • 传播路径:默认传播路径为冒泡

image.png
注意:window不存在与DOM中,它是全局对象

手写DOM事件委托

获取触发事件的元素的 引用:event.target
注意区分event.target 和 event.currentTarget
event.target 用户点击哪个就是哪个(经常变)
event.currentTarget 是 addEventListener监听哪个就是哪个(不变)

vue在for循环的时候默认绑定了事件委托

  1. //错误版本(一般只回答该版本即可)
  2. ul.addEventListener('click',function(e) {
  3. if(e.target.tagName.toLowerCase() === 'li) {
  4. fn()
  5. }
  6. }) //bug在于用户点击的是li里面的span的话就无法触发fn
  7. //高级版本
  8. //思路:点击span后,递归遍历span 的祖先元素,看其中有无ul里面的li
  9. function delegate(element,eventType,selector,fn) {
  10. element.addEventListener(eventType,e => {
  11. let el = e.target
  12. while(!el.matches(selector)) {
  13. if(element === el) {
  14. el = null
  15. break
  16. }
  17. el.el.parentNode
  18. }
  19. el && fn.call(el,e,el)
  20. })
  21. return element
  22. }

获取触发事件的元素的值

根据元素的类型使用:
input类型:event.target.value
其他类型:event.target.innerHTML

手写拖拽div

思路:
首先监听鼠标的mousedown事件
当鼠标按下的时候,记录对应位置
鼠标移动多少,div就移动多少

  1. //html文件
  2. <div id="xxx"></div>
  3. //JS文件
  4. var dragging = false
  5. var position = null
  6. xxx.addEventListener('mousedown',function(e) {
  7. dragging = true
  8. position = [e.clientX,e.clientY]
  9. })
  10. document.addEventListener('mousemove',function(e) {
  11. if(dragging === false) {return}
  12. const x = e.clientX
  13. const y = e.clientY
  14. const deltaX = x - position[0]
  15. const deltaY = y - position[1]
  16. const left = parseInt(xxx.style.left || 0)
  17. const top = parseInt(xxx.style.top || 0)
  18. xxx.style.left = left + deltaX + 'px'
  19. xxx.style.top = top + deltaY + 'px'
  20. position = [x,y]
  21. })
  22. document.addEventListener('mouseup',function(e) {
  23. dragging = false
  24. })

要点:

  • 记录移动差距,将其添加到节点的样式上
  • 监听范围的变化,mousedown监听的是div,mousemove和mouseup监听的是document,如果都监听div,小幅度移动可以,但大幅度移动会脱节,因为你移动范围超出了div,但又只监听div
  • 移动的效能优化,将直接添加样式left和right改为transform的translateX和translateY,因为transform不会重绘

    浏览器原理

    浏览器主要构成:

    image.png

  • 用户界面

  • 浏览器引擎
  • 渲染引擎
  • 数据存储层
  • UI BackEnd
  • JavaScript 解析器 (脚本引擎)
  • 网络层
用户界面 (User Interface) 包括地址栏、后退/前进按钮、书签目录等,也就是你所看到的除了用来显示你所请求页面的主窗口之外的其他部分
浏览器引擎 (Browser Engine) 用来查询及操作渲染引擎的接口
渲染引擎 (Rendering Engine) 用来显示请求的内容,例如,如果请求内容为html,它负责解析html及css,并将解析后的结果显示出来
网络 (Networking) 用来完成网络调用,例如http请求,它具有平台无关的接口,可以在不同平台上工作
JS解释器 (JS Interpreter) 用来解释执行JS代码
UI后端 (UI Backend) 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口

数据存储 (DB Persistence)
属于持久层,浏览器需要在硬盘中保存类似cookie的各种数据,HTML5定义了web database技术,这是一种轻量级完整的客户端存储技术

多进程的浏览器

浏览器是多进程的,有一个主控进程,以及每一个tab页面都会新开一个进程(某些情况下多个tab会合并进程)
进程可能包括主控进程,插件进程,GPU,tab页(浏览器内核)等等

  • Browser进程:浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU进程:最多一个,用于3D绘制
  • 浏览器渲染进程(内核):默认每个Tab页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白tab会合并成一个进程)

多线程的浏览器内核

每一个tab页面可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:

  • GUI渲染线程
  • JS引擎线程
  • 事件触发线程
  • 定时器线程
  • 网络请求线程

其中JS引擎线程和GUI渲染线程是互斥的。

GUI渲染线程主要工作内容

  • 解析html文档生成DOM
  • css代码转换为cssom (css object model)
  • 结合DOM和CSSOM生成渲染树
  • 生成布局(layout)
  • 将布局绘制(paint)在屏幕上

举例理解:

例如百度打开一个知乎网页,知乎网页这个页面上运行渲染引擎和JS引擎,知乎已经是一个进程了,而这两个引擎在知乎页面内运行,成为线程。
每一个页面都会开启一个GUI渲染线程和JS线程。