前端面经

147 阅读43分钟

 作者什么时候能有一个毛孩子啊~~~


BFC(1次)

定义:

MDN区块格式化上下文(Block Formatting Cofntext,BFC)是 Web 页面的可视 CSS 渲染的一部分,是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。

解决布局问题: 浮动、外边距重叠

构成BFC方法:

  • HTML根元素
  • float: left | right(除none)
  • position: absolute | fixed
  • display: inline-block | flex | inline-flex | grid | inline-grid | table-cell |table-caption | flow-root(专门用来创建BFC属性)
  • overflow: 除了visible
  • contain: layout | content | paint(现代浏览器支持)

清除浮动

浮动会造成父元素高度塌陷(没有设置height)

方法:BFC、clear: both、伪元素(不影响布局复杂度)

像素单位(3次)

  • px:绝对单位
  • em:相对单位,相对父元素的字体大小
  • rem:相对单位,相对根元素(html)的字体大小
  • vw/vh:视口宽高的百分比

CSS选择器以及优先级(2次)

选择器格式优先级权重
id#id100
.class010
伪类    注意区分li : last-child010
属性a[class="refValue"]010
元素div001
伪元素    注意区分 div :: after001
相邻兄弟h1+p000
ul>li000
后代li a000
* 通配符*000

提示:内联样式的优先级是1000(比id还高) ,!important声明的样式的优先级最高,在样式中更改UI库的样式经常用到 :global, 设置某个节点下的全局样式;

伪类和伪元素的区别(3次,字节一面)

伪类

将特殊效果添加到特定选择器上,在已有元素上添加类别,不产生新元素 

a:hover {color: #FF00FF}
p:first-child {color: red}
p:nth-child(odd) {color: pink}

伪元素

在内容元素的前后插入额外元素/样式,插入的元素实际上并不在文档中生成。它们只在外部显示可见(渲染树存在,DOM树不存在),但不会在文档的源代码中找到它们,因此,称为“伪”元素。

p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}

行内元素、块级元素、行内块元素区别,

元素排列方式样式效果
行内 inline同行排列内容超出自动换行(盒子占两行)​编辑设置宽高无效,垂直margin、padding、border不占空间
块级 block独占一行,前后自动换行设置宽高有效,margin、padding、border均生效并影响布局
行内块 inline-block同行排列内容超出整个盒子换到下一行(注意区分inline)​编辑同block

置换元素

置换元素(Replaced Element) :是指其内容由外部资源决定,而非由 HTML 直接描述的元素。

常见置换元素:、、、、、

作者小问答

<div classname="block">块盒子</div>


//  书写样式时,div.block 和 div .block有区别吗?

让我想想...

看着很像,但是既然这么问了,应该有区别吧~

emmm

答案揭晓

  • div.block:div元素中类名为block的元素(“且”关系,筛选范围更小)
  • div .block(带空格):div元素下类名为block的后代(后代选择器,匹配范围更大)

H5C3新特性

H5

  • 语义化标签:header footer article section main nav

好处: 1)便于搜索引擎爬虫准确抓取页面,提高页面在搜索结果中的排名。2)增加代码易读性,便于代码维护和更新

  • 多媒体:video audio
  • canvas绘图(画布)
  • 本地存储:localStorage sessionStorage cookie

扩展: 三者区别

  • 地理位置的API
  • 拖放

CSS3 

  • 弹性布局: display: flex 重要
  • 网格布局: display: grid
  • 渐变、阴影、过渡、动画
  • 媒体查询:依据设备的屏幕尺寸、分辨率 使用不同的样式规则,实现响应式设计
  • 自定义字体:@font-face

如何画一个三角形(基础) 

 把一个元素的宽高设置为0,然后给不同方向的边框设置颜色和宽度,接着想要哪个方向的三角就留下颜色,其余改为透明

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .traingle {
      width: 0;
      height: 0;
      border: 10px solid transparent;
      border-top-color: pink;
    }
  </style>
</head>

<body>
  <!-- 画一个三角形 -->
  <div class="traingle"></div>
</body>
</html>

如何画一条0.5px的线

// 1
.border {
    width: 1000px;
    height: calc(1px/2);
}

// 2
.border {
    width: 100px;
    height: 1px;
    transform: scaleY(0.5);
    /* transform: scale(1, 0.5); */
    transform-origin: 0 0; /* 防止缩放后偏移 */
}

// 3 用边框+缩放
.border {
    width: 100px;
    border-top: 1px solid black;
    transform: scaleY(0.5);
    transform-origin: 0 0; /* 防止缩放后偏移 */
}

自适应布局和响应式布局(1次) 

自适应: 随着视口变化,元素进行放大缩小 (淘宝无限适配方案:可以动态调整根元素font-size的值 -> rem)

响应式: 通过媒体查询来设置特定屏幕尺寸的样式规则(750px 1080px 2000px)

<style>
    /* 小屏幕设备样式 */
   @media (max-width: 767px) {
            body {
                background-color: lightblue;
                font-size: 14px;
            }

            .container {
                padding: 10px;
            }
        }

        /* 大屏幕设备样式 */
        @media (min-width: 768px) {
            body {
                background-color: lightgreen;
                font-size: 16px;
            }

            .container {
                padding: 20px;
                max-width: 1200px;
                margin: 0 auto;
            }
        }
</style>

如何实现垂直居中对齐(5次,高频,唯品会一面,字节一面)

<!DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* 绝对定位 1.*/
    /* #parent {
      position: relative;
      width: 500px;
      height: 500px;
      background-color: aqua;
    }

    #center {
      position: absolute;
      width: 100px;
      height: 100px;
      line-height: 100px;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
      background-color: brown;
    } */

    /* 绝对定位 2.*/
    /* #parent {
      position: relative;
      width: 500px;
      height: 500px;
      background-color: aqua;
    }

    #center {
      position: absolute;
      width: 100px;
      height: 100px;
      line-height: 100px;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      text-align: center;
      background-color: brown;
    } */

    /* flex布局 */
    /* #parent {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 500px;
      height: 500px;
      background-color: aqua;
    }

    #center {
      width: 100px;
      height: 100px;
      line-height: 100px;
      text-align: center;
      background-color: brown;
    } */

    /* flex中用margin布局 */
    /* #parent {
      display: flex;
      width: 500px;
      height: 500px;
      background-color: aqua;
    } */

    /* #center {
      width: 100px;
      height: 100px;
      margin:auto;
      line-height: 100px;
      text-align: center;
      background-color: brown;
    } */

    /*grid布局*/
    /* #parent {
      display: grid;
      place-items: center;  //place-content: center;也可
      width: 500px;
      height: 500px;
      background-color: aqua;
    } 

    #center {
      width: 100px;
      height: 100px;
      background-color: brown;
    }*/
  </style>
</head>

<body>
  <!-- 实现三栏布局 -->
  <div id="parent">
    <div id="center">center</div>
  </div>
</body>

</html>

效果图如下:

​编辑

 延伸:说一下flex布局(6次,超高频,必会)

说一下flex怎么布局的,然后说一下常用属性的用法,让面试官觉得你会;

​编辑

如何实现三栏布局(笔试3次,面试3次,CSS基础)

 flex、绝对定位、浮动、双飞翼(1次)、圣杯(后两者布局还没理解到)

<!DOCTYPE html>
<html lang="zh">
 
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    /* flex布局 */
    /* #parent {
      width: 1000px;
      height: 100px;
      display: flex;
    }
 
    #left {
      width: 100px;
      text-align: center;
      background-color: palegoldenrod;
    }
 
    #right {
      width: 200px;
      text-align: center;
      background-color: blue;
    }
 
    #center {
      flex: 1;
      text-align: center;
      background-color: brown;
    } */
 
    /* 绝对定位 */
    #parent {
      position: relative;
      width: 1000px;
      height: 100px;
    }
    #left {
      position: absolute;
      height: 100px;
      width: 100px;
      text-align: center;
      background-color: palegoldenrod;
    }
    #right {
      position: absolute;
      width: 200px;
      height: 100px;
      top: 0;
      right: 0;
      text-align: center;
      background-color: blue;
    }
    #center {
      /* position: absolute; 
      left: 100px;
      right: 200px;*/
      height: 100%;
      margin-left: 100px;
      margin-right: 200px;
      text-align: center;
      background-color: brown;
    }
 
    /* 浮动:注意中间元素center要在末尾 */
    /* #parent {
      width: 1000px;
      height: 100px;
    }
    #left {
      float: left;
      height: 100px;
      width: 100px;
      text-align: center;
      background-color: palegoldenrod;
    }
    #right {
      float: right;
      width: 200px;
      height: 100px;
      text-align: center;
      background-color: blue;
    }
    #center {
      height: 100%;
      margin-left: 100px;
      margin-right: 200px;
      text-align: center;
      background-color: brown;
    } */
  </style>
</head>
<body>
    <!-- 实现三栏布局 -->
    <div id="parent">
      <div id="left">left</div>
      <div id="center">center</div>
      <div id="right">right</div>
    </div>
</body>
 
</html>

效果图都是这样,最好自己动手实操,加深印象~

​编辑

盒子模型(4次)

width和height属性的范围:

  • box-sizing: content-box,标准盒子模型总宽度:width(content) + padding + border + margin
  • box-sizing: border-box,IE(怪异)盒子模型总宽度: margin + width(content + padding + border)

实现隐藏元素的方法(1次)

  • opacity: 0 (占位)
  • visibility: hidden (占位)
  • display: none | hidden(脱离文档流)
  • HTML5 hidden属性 (同display: hidden)

面向对象(纪传体)vs面向过程(编年体)

面向过程(ex:C语言):负责完成某个具体任务的代码(可以理解为函数),核心:将要完成的事情拆分成一个个步骤,依次完成 

面向对象(Object Oriented Programming  ex:JAVA):以对象为核心,考虑各个对象有什么性质、能做什么事情;把事务先分解到对象身上,描述各个对象的作用,然后才是它们之间的交互

提取性质定义类创建对象(类是创建对象的模板,对象是类的实例),用对象绑定相关属性(放在类里面的变量)有利于让程序逻辑更加清晰,数据流动更加清晰

用对象绑定对象能实现的方法(放在类里面的函数)

结合方法和属性,能更优雅地处理逻辑

面向对象特性:

封装:写类的人将内部实现细节隐藏起来,使用类的人只通过外部接口(方法)访问和使用

继承:面向对象编程允许创建有层次的类 

多态:同样的接口,因为对象具体类不同而有不同的表现

new操作符做了什么

  1. 创建了一个空对象
  2. 将空对象的原型指向构造函数的原型
  3. 将空对象作为构造函数的上下文(改变this指向)
  4. 对构造函数有返回值的处理判断
function Fun(age, name){
    this.age = age;
    this.name = name;
}

function create(fn, ...args){
    // 1.创建了一个空对象
    let obj = {};
    // 2.将空对象的原型指向构造函数的原型
    Object.setPrototypeOf(obj, fn.prototype);
    // 3.将空对象作为构造函数的上下文(改变this指向)
    let res = fn.apply(obj, args)
    // 4.对构造函数有返回值的处理判断
    return res instanceof Object ? res : obj;
}


console.log(create(Fun, 18, '王五'));

原型和原型链(4次,重点!)

原型:每一个函数都有prototype显式属性,称为原型(原型对象),原型有属性和方法与实例对象共享,可以继承

原型链:对象都有隐式__proto__属性,指向它的原型对象,原型对象也是对象, 也有__proto__属性,指向原型对象的原型对象,这样一层一层形成的链式结构称原型链(最顶层为null),原型链是通过对象间的原型链接形成的属性查找路径

画图加深记忆(理解理解才能吸收!!)

​编辑

继承 

原型链继承:父类的实例赋值给子类的原型

 优:子类可以共享父类的方法

劣:不能给父类传参、父类的引用数据类型可能被共享

<script>
  function Person(name) {
    this.name = name
  }

  Person.prototype.say = function () {
    console.log(123);
  }

  function Son(age) {
    this.age = age
  }

  Son.prototype = new Person()
  console.log(new Son())
</script>

构造函数继承:在子类构造函数中调用父类构造函数

优:可以给父类传递属性

劣:无法共享父类方法

<script>
  function Person(name) {
    this.name = name
  }

  Person.prototype.say = function () {
    console.log(123);
  }

  function Son(age) {
    Person.call(this, age)
    this.age = age
  }

  console.log(new Son(18)) // {name:18, age:18}
</script>

组合式继承:结合原型链继承和构造函数继承的优点

劣:复杂度增加、增加了父类对象的创建

<script>
  function Person(name) {
    this.name = name
  }

  Person.prototype.say = function () {
    console.log(123);
  }

  function Son(age) {
    Person.call(this, age) // 1
    this.age = age
  }

  Son.prototype = new Person() // 2
  console.log(new Son(18)) // {name:18, age:18}
</script>

 class继承:本质基于原型链和构造函数继承

劣:低版本浏览器不支持

<script>
  // 父类
  class Person {
    constructor(name) {
      this.name = name
    }
    say() {
      console.log(123)
    }
  }

  // 子类
  class Son extends Person {
    constructor(name, age) {
      super(name)
      this.age = age
    }
    write() {
      console.log(456)
    }
  }
  const son = new Son('fu', 'zi')
  console.log(son) // {name: 'fu', age: 'zi'}
  son.say() // 123
  son.write() // 456
</script>

区分undefined和null(1次)

undefined

  • 定义变量未初始化,默认值为undefined
  • 函数没有返回值,默认返回undefined
  • 调用函数未传值,形参值为undefined
  • 访问对象/数组不存在的属性/元素

null--表示"空对象指针"

  • 不是默认值,一般需要主动赋值,表示"没有值"或"空",例:定义对象初始化为null
  • 对象的原型链顶端是null

ES6新特性(n次,必背必背!!!)

1、Symbol、BigInt数据类型

2、模版字符串:${}

3、新增let、const关键字

var、let、const区别:

  • var没有块级作用域(有函数作用域),let、const有(块级作用域 : { } )
  • var存在变量提升(变量只能在声明之后使用,否在会编译报错且打印为'undefined'),let(暂时性死区)、const没有 (tips:优先级:函数提升 > 变量提升)
  • var可以重复声明,let、const不可以
  • var、let不用设置初始值,const必须设置(初始化)
  • let创建的变量可以重新赋值,const不可以
  • 在全局作用域下var定义的变量挂载到window对象,let不会

4、箭头函数()=>{}

箭头函数和普通函数的区别:

  • 箭头函数更简洁
  • 箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this。箭头函数中this的指向在它在定义时已经确定了,不会改变。普通函数的this指向调用者(谁调用就指向谁)。
  • 箭头函数不能作为构造函数使用(如上因为不能绑定this)
  • call()、apply()、bind()等方法不能改变箭头函数中this的指向
  • 箭头函数没有prototype,当然就不存在原型;没有自己的arguments对象
  • 箭头函数不能用作Generator函数,不能使用yeild关键字(作者自己标红,还不懂呜呜呜呜)

5、扩展运算符

6、class类

7、解构赋值

如何在不添加第三个变量的情况下交换两个变量的值?

let a = 1;
let b = 2;
[a, b] = [b, a]

console.log(a, b)    // 2 1

8、新增Map、Set数据结构

Map、Set、json区别:

  • JSON 对象在 JavaScript 中以键值对的形式表示,但键必须是字符串,且整个 JSON 对象必须是一个字符串。
  • Map存储键值对的集合,键和值都能是任意类型,并且键具有唯一性,可迭代(用于 for...of 循环)。
  • Set存储唯一的值的集合,可迭代(用于 for...of 循环)。

Map和weakMap区别:待续...

Set和weakSet区别:待续...

扩展: 将Set转换为数组方法

  • 展开运算符:
const set= new Set([1, 2, 3, 4, 5]);
const arr = [...set];

  • Array.from() + Set / Set.prototype.keys() / Set.prototype.values()
const mySet = new Set([1,2,3,4,5]);

const myArr1 = Array.from(mySet);
const myArr2 = Array.from(mySet.keys());
const myArr3 = Array.from(mySet.values()); // keys 和 values相同
console.log(Array.isArray(myArr1, myArr2, myArr3));

  • for...of
  • Set.prototype.forEach()
const mySet = new Set([1, 2, 3, 4, 5])
const myArr1 = []
const myArr2 = []

// forEach
mySet.forEach((item) => {
  myArr1.push(item)
})

for...of
for (let item of mySet) {
  myArr2.push(item)
}

console.log(myArr1, myArr2)

9、默认参数

即给函数设置默认参数

function greet(name, message = "Hello") {
  console.log(`${message}, ${name}!`);
}

10、Promise:处理异步操作的对象

11、async/await

扩展:promise和async/await区别

async/await基于promise的语法糖(语法糖:简化,语法盐:复杂化)

12、模块导入导出:import export

13、??:空值合并运算符

  • 若左侧操作数 null 或者 undefined 时,返回侧操作数
  • 若左侧操作数不为 null 或 undefined,则返回侧操作数

14、?. :可选链操作符

const uer = {
    name: 'dili',
    address:{
        street: 'Main Street'
    }
}

console.log(user.address?.street)
// 若address属性存在,则继续访问其street属性并输出值
// 若address属性不存在,则返回undefined避免引用错误

 15、replaceAll()(String的api)

let str = 'hello world, hello agin'

console.log(str.replaceAll('h', 'H')) // Hello world, Hello agin

16、顶层await 

<script type="module">
  // 模拟一个异步操作
  function fetchData() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve('Data fetched successfully');
      }, 1000);
    });
  }

  // 顶层使用 await
  const data = await fetchData();
  console.log(data);
</script>

 JS的组成部分

ECMAScript、DOM(文档对象模型)、BOM(浏览器对象模型)

JavaScript中的原生请求方法(两种)

1. XMLHttpRequest

传统的原生方法,具有良好的兼容性

2.Fetch API

更强大、更灵活的原生请求方法,返回Promise对象,处理异步请求更便捷;可以使用Promise/fetch的API实现对老浏览器的支持(兼容)。

async和defer区别

作用:控制外部脚本的加载和执行顺序

  • async:异步加载脚本,脚本文件会并行下载,不会阻塞页面的解析,下载完就直接执行
  • defer:延迟加载脚本,脚本文件会并行下载,在文档完全解析之后,按顺序执行

 javascript执行过程

  预编译: 代码执行前,先进行编译

JS代码执行过程:

  • 通篇检查代码是否有语法错误
  • 预编译
  • 解释一行执行一行

 变量声明和函数声明

  •  var存在变量提升(声明提升,赋值不会提升)
  • 函数声明的提升是提升整个函数,包括里面的代码

注意: 如果给一个没有进行声明的变量赋值,那么这个变量默认为全局变量

函数执行期上下文

活跃对象AO: activation object

  1. 创建AO对象 AO = {}
  2. 找函数的形参和变量声明,并赋值为undefined
  3. 把实参赋值给形参
  4. 找函数声明,并赋值函数体
  5. 执行代码 在预编译阶段执行过的代码,在函数执行期不再执行

dom的事件模型

事件: 用户在浏览器中操作时自动触发的信号,JavaScript可以使用事件监听器来监听这些"事件",并执行回调函数作为响应

事件触发时,事件流的传播路径是从根节点自上而下地传播至目标节点(event.target),再从目标节点向上传播至根节点。

三个阶段:

事件捕获: 事件从最外层祖先节点(window对象:浏览器环境的全局对象)开始,自上而下传播,直至到达目标节点

目标阶段: 事件到达触发事件的目标元素,在这里事件会被处理

事件冒泡:事件委托利用的阶段,事件从目标元素开始,自下而上传播,直至到达window

扩展: 事件委托通过将事件监听器绑定到父元素上,利用事件冒泡机制处理子元素的事件;这样做可以减少事件监听器的数量,优化性能

遍历数组的方式有哪些?(1次) 

map forEach filter reduce some/every find/findIndex flatMap

操作数组常用的方法有哪些?哪些方法会改变原数组?

 MDN:面向开发者的 Web 技术 | MDN

不改变原数组,方法会返回一个新的数组或某个结果,不会修改原来数组的内容

slice、reduce、filter、map

改变原数组

 push、pop、uhshift(+)、shift(-)、splice、sort、reverse

扩展:sort方法(参数可选)就地对数组进行排序,返回的是对相同数组的引用,默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序

不改变原数组的排序方法用toSorted

tips: 数组方法均是浅拷贝

forEach和map区别

forEachmap
无返回值,仅读取数组元素返回原数组的映射结果,返回新数组
不可链式调用可链式调用

 深浅拷贝(3次)

  • 浅拷贝: 只复制对象/数组的一层属性/元素,如果对象/数组的属性/元素是基本数据类型复制其;但如果属性/元素是引用数据类型,则只会复制其引用,(即新对象/数组和原对象/数组的该属性/元素会指向同一个内存地址)

实现方式: Object.assign()、扩展运算符、Array.slice()

  • 深拷贝:递归地复制对象/数组的所有属性,包括嵌套的对象和数组,创建一个完全独立的新对象/数组,新对象/数组和原对象/数组在内存中占用不同的地址,修改新对象/数组不会影响原对象/数组,反之亦然

实现方式:

1、JSON的序列化+反序列化(JSON.parse(JSON.stringfy()))

缺点:

  • 不能识别BigInt类型
  • 不能拷贝undefined、Date、RegExp、Map、Set、symbol、函数类型的值
  • 不能处理循环引用(对象的属性直接或间接地引用了对象本身)

2、手搓递归

<script>
  function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
      return obj
    }

    let copy = Array.isArray(obj) ? [] : {}
    for (let key in obj) {
      copy[key] = deepCopy(obj[key])
    }

    return copy
  }

  let obj = { a: 1, b: { c: 2 } }
  let copy = deepCopy(obj)
  copy.b.c = 4
  console.log(obj, copy)
// { a: 1, b: { c: 2 } }  { a: 1, b: { c: 4 } }
</script>

3、使用第三方库(lodash中的cloneDeep)

将类数组对象转换为数组

Array.from()(es6+推荐)

展开运算符:[...arguments](es6+推荐)

Array.prototype.slice.call(类数组对象)(es5传统方法)

Array.prototype.concat.call([], 类数组对象)(邪修)

Promise(有空还是去看看源码吧,看看作者的逻辑就懂Promise了,嗨嗨嗨!)

Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,改善了回调地狱

如何捕获Promise中的错误信息(1次)

try/catch,Promise.then(,reject), Promise.catch(),Promise.finally()(无论成功失败都会执行)

Promise实例的三个状态(1次)

  • Pending(进行中)
  • Fulfilled(已完成)
  • Rejected(已拒绝)

两个过程(pending起手):状态凝固

  • pending -> fulfilled : Resolved(已完成)
  • pending -> rejected:Rejected(已拒绝)

注意:一旦从进行状态变成为其他状态就永远不能更改状态了。

tips: 在回调中执行resolve(),函数中resolve后面的代码依然会继续执行

处理成功、失败 

.then() 处理成功的情况 .then(onResolve,onRejected) 接受两个函数,第一个处理成功,第二个处理失败

.catch() 处理失败的情况         本质:.then(null,onRejected)语法糖

***Promise.all()和.race()(1次 美团)

.all() 接受一个数组,返回一个新的promise,只有所有的promise都成功才会成功,如果有一个失败,那么立刻返回失败。

成功: 将所有成功的结果收集到一个数组中,并将这个数组作为最终的 resolve 值

失败: 将失败的 Promise 的 reject 值作为最终的 reject 值

注意: Promise.all 本身会立即终止并抛出错误(被最近的 .catch() 或 try/catch 捕获),但不会阻止整个程序的执行。后续代码(位于 Promise.all 调用之后)会继续运行,但依赖 Promise.all 结果的代码将无法执行。)

场景题: 我要上传多张图片,但是我要等所有图片上传成功后,再去调提交,应该怎么做?(待更新...)

.race() 接受一个数组,返回一个新的promise,只要有一个promsie完成(成功/失败),就立刻决定整个promsie的结果

***原理

  1. 初始化promise:创建promise对象,构造函数会立刻调用一个函数,该函数有两个参数resolve(函数,把promise状态从pending -> fullfilled),reject((函数,把promise状态从pending -> rejected))
  2.  执行异步操作:在执行函数的时候,它一般是个异步任务(发送请求),异步任务的结果决定了promise的状态(任务成功则调用resolve函数,失败则调用reject函数,改变promise状态),一旦执行状态不可变

 如何实现Promis功能(要看Promis源码)(1次)

this/call/apply/bind区别(6次,背背背!!!)

这三个方法都显式指定调用函数的 this 指向

  • call,第一个参数this绑定的对象,其余参数需要依次列举出来,立即执行
  • apply,第一个参数this绑定的对象,第二个传参数数组,立即执行
  • bind,第一个参数this绑定的对象,仅定义(创建新函数)未执行
// bind

<script>
  function say(arg, msg) {
    return `${arg}, ${this.name}, ${msg}`
  }
  const obj = { name: 'dili' }
  const newSay = say.bind(obj, 'Hello')
  console.log(newSay('I'm liuyifei'))
</script>

this指向(1次)

全局this --> window       

普通函数中this  --> 谁调用指向谁(默认window)       

对象中this --> 函数作为对象的方法被调用,this指向调用该方法的对象

构造函数this --> 新创建的实例对象

箭头函数this --> 外层普通函数/外层作用域的this

定时器中this -> window(定义在window对象下)

if语句条件判断归纳(1次)

值/类型if判断结果
falsefalse
0, -0, NaNfalse
"", ''false
nullfalse
undefinedfalse
其他所有值true

注意: 负值, ****{}, []都判定为true, 想判断空对象/数组可用length属性 

浏览器事件循环(必要)

JS是单线程的(why?设计为多线程如果同时添加删除同一DOM节点会出问题),为了防止阻塞,将代码分为同步and异步

常见宏任务:script(代码块),setTimeout/setInterval,setImmediate定时器、事件、ajax

常见微任务:Promise.then()/catch(),ASync/Await,Object.observe,process.nextTick(node)

 执行顺序:执行栈中的同步代码 -> 微任务队列(直到没有微任务) -> 宏任务队列

​编辑

 说完概念面试官一般会让你做一道关于时间循环的题(穿插seetTimeout、Promise),我的建议是多看多练,就会对事件循环理解更深刻。

先自己做一下哦~

setTimeout(() => {
  console.log('setTimeout');

  Promise.resolve().then(() => {
    console.log('setPro1');
  }).then(() => {
    console.log('setPro2');
  })
}, 0);

setTimeout(() => {
  console.log('set');
}, 0)

Promise.resolve().then(() => {
  console.log('then1');
}).then(() => {
  console.log('then2');
}).then(() => {
  console.log('then3');
})

 公布答案啦~

​编辑

 下面出一个纯Promise的,很绕很绕,直接晕厥

Promise.resolve().then(() => {
      console.log(0);
      return Promise.resolve(4);
}).then(res => {
      console.log(res);
})
Promise.resolve().then(() => {
      console.log(1);
}).then(() => {
      console.log(2);
}).then(() => {
      console.log(3);
}).then(() => {
      console.log(5);
})

浏览器内核

定义: 通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,又称作渲染引擎

一个浏览器通常由以下常驻线程组成:

GUI渲染线程,主要负责页面的渲染,解析HTML、CSS(两者并行),构建DOM树、CSS规则树,合并成渲染树,布局和绘制...

javascript引擎线程,主要负责处理javascript脚本,执行代码

定时触发器线程,负责执行异步定时器一类的函数的线程,如:setTimeout、setInterval

事件触发线程,主要负责将准备好的事件(定时器结束、异步请求成功触发的回调,点击事件)交给JS引擎执行

异步http请求线程,负责执行异步请求一类的函数的线程,如Promise、fetch、ajax...

tips: GUI渲染线程与javascript引擎互斥(性能问题)

Node的事件循环

主要关注的三个阶段:timers poll check(考点)

​编辑

timer阶段: 会执行setTimeout和setInterval回调,并且是由poll控制的(注意:在node.js中定时器指定的事件也不是准确时间,只能是尽快执行)

poll阶段: 至关重要的阶段,系统会做两件事:回到timer阶段执行回调;执行I/O回调

如果该阶段没有设定了timers的话,会发生以下两件事情:

如果poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

如果poll为空,会发生两件事:

如果有setImmediate回调需要执行,poll阶段会停止并进入到check阶段执行回调

如果没有setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调(这里会设置超时时间防止一直等待)

​编辑

check阶段:setImmediated()的回调会被加入check队列中,从事件循环的阶段图可以知道,check阶段的执行顺序在poll阶段之后。(注意:当二者在异步I/O callback内部调用时,总是先执行setImmediate,再执行setTimeout)

问: 简述一下Node.js中的事件循环和浏览器环境的事件循环有何不同?

Node.js事件循环的6个阶段,该循环的执行顺序为:

外部输入数据 - 轮询(poll) - 检查(check) - 关闭时间回调(close callback) - 定时器检测(timers) - I/O事件回调(I/O callbacks) - 闲置(idle、prepare) - 轮询(poll)

浏览器和Node.js环境下,微任务队列的执行时机不同:

  • 在Node.js中,每个任务队列的每个任务执行完毕之后,就会清空这个微任务队列
  • 在浏览器环境下就两个队列,分别是宏任务和微任务队列。微任务的任务队列是每个宏任务执行完之后执行(微任务->渲染->下一次宏任务,微任务会阻塞渲染)

 DOM树、CSSOM树和渲染树在结构不一致的场景

DOMCSSOM渲染树
使用伪元素(::before/::after...)×√(若可见)
display: none;×(被过滤)

tips: visibility:hidden;在三个树中均存在,只是在渲染树中占位不可见,控制台中的Elements属于DOM树

栈、队列、堆区别

队列
先进后出先进先出
内存块小、自动分配/释放内存块小、自动分配/释放内存块大、需手动分配/释放
线性结构线性结构树形结构

闭包(3次)

MDN:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。

内部函数可以访问外部函数作用域且被外部函数返回就形成闭包,闭包可以使变量私有(不会造成变量污染)(优点)

经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout(function timer() {
        console.log(j)
    }, j * 1000)
  })(i)
}

首先使用了立即执行函数(5次)将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变(j取到的值都是对应循环中的i值),当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

弊端以及注意点:

  • 避免内存泄漏:确保在不需要闭包时,手动释放捕获的变量。
  • 优化性能:避免在嵌套很深的函数中使用闭包,减少作用域链的长度。
  • 保持代码清晰:尽量避免复杂的闭包逻辑,确保代码易于理解和维护。

注意:内存泄漏可能会延伸垃圾回收机制(V8)

websocket和SSE区别

SSEwebsocket
协议基于HTTP基于TCP
通信单工,只能服务端单向发送消息全双工,可以同时发送和接受消息
资源消耗单向,服务端只需要推送数据,资源消耗更少全双工通信,服务器需要监听来自客户端的消息,增大资源消耗
适用场景天气、新闻消息推送聊天室、实时游戏

高阶函数

满足下列条件之一:

  • 接收一个或多个函数作为参数
  • 返回一个函数

常见的应用场景

  • 回调函数:map、filter、reduce
  • 事件处理:监听事件
  • 闭包:防抖、节流
  • 柯里化

高阶组件(Higher-Order-Components)

本质: 一个接受组件并返回新组件的函数,用于封装通用的逻辑和行为,以便在多个组件中重复使用

解决的核心问题:

  • 横切关注点的复用:将通用逻辑抽离为装饰器,避免重复代码
  • 渲染劫持:通过控制render方法修改输出(ex:条件渲染、样式注入)
  • Props动态扩展:向子组件注入新的props或覆盖现有props

浏览器的跨域问题

跨域问题其实就是浏览器的同源策略造成的。同源策略是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议域名、端口号必须一致。

如何解决跨域问题

 跨域问题涉及后端,建议找视频看看,文字内容比较抽象实践一下印象更深;

(1)CORS(跨域资源共享),CORS需要浏览器和服务器同时支持,整个CORS过程都是浏览器完成的,无需用户参与。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。

浏览器将CORS分为简单请求非简单请求

简单请求(不会触发预检请求OPTIONS):请求方法:HEAD/GET/POST,请求头信息默认不改变

​编辑

浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Origin(协议+域名+端口)字段, 如果Orign指定的域名在许可范围之内,服务器返回的响应头字段中至少有Access-Control-Allow-Origin(和Origin字段存储的数据相等)

非简单请求: 简单请求的要求之外的情况,非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。预检请求使用的请求方法是OPTIONS, 他的头信息中的关键字段有OriginAccess-Control-Request-Method,Access-Control-Request-Headers, 服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。

(2)nginx反向代理接口跨域;

(3)JSONP(已过时):利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET(只能是GET)请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

XSS攻击(3次)

定义:跨站脚本注入攻击,攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。

本质: 网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。

攻击类型:

  • 存储型:攻击者将恶意脚本存储在服务端数据库中,当有请求发送过来,跟随响应数据一起返回,浏览器解析并执行,完成攻击
  • DOM型:通过修改页面DOM节点形成xss
  • 反射型:攻击者诱导用户访问带有恶意代码的URL,服务端处理请求并返回带有恶意代码的数据,浏览器把这段数据当脚本解析并执行,完成攻击

 如何预防?(2次)

1.对用户输入进行严格验证

明确规定用户输入的格式、长度和允许的字符范围。(脚本通常需要很长的字段)

2.过滤危险字符

对于用户输入的内容,过滤掉可能会被用于 XSS 攻击的危险字符,如 <>& 等

3.设置HTTP头Content-Security-Policy(CSP)

Content-Security-Policy: default-src'self'; script-src'self' example.com

4.对敏感的 Cookie 设置 HttpOnly 标志,这样javascript脚本就无法访问(服务端)

setcookie('session_id', '123456', time() + 3600, '/', '', false, true);

CSRF(Cross-site request )攻击

定义:跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求(和被攻击网站同源)。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。

本质: 利用 cookie 会在同源请求中携带发送给服务器的特点(服务器根据域名判断),以此来实现用户的冒充。

常见攻击类型:

  • GET型:ex: 在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
  • POST型:ex: 构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
  • 链接型:ex: 在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

预防:

CSRF token验证:服务端返回一个随机token,客户端再次发送请求时需携带

设置cookie属性:可以设置samesite属性,有两种模式,严格模式下任何情况下不允许第三方使用;宽松模式下仅允许被实现页面跳转的get请求使用

中间人攻击(Man-in-the-middle attack, MITM)

指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。

攻击过程: 客户端发送请求 -> 中间人截获 -> 服务端接收请求

服务端响应发送公钥 -> 中间人截获,伪造公钥 -> 客户端接受假公钥

客户端发送假公钥加密hash值 -> 中间截获,用私钥获取真hash值,伪造hash值 -> 服务端私钥解假hash值(获取假密钥)

HTTPS加密

前言 

对称加密:用一个密钥进行加密/解密

非对称加密:产生一对密钥:公钥-私钥,公钥加密,私钥解密(私钥加密,公钥解密)

对称+非对称:服务端生成一对密钥(公钥/私钥key1),将公钥key1发送给客户端-->客户端生成key2用公钥key1进行加密发送给服务端-->服务端用私钥key1解密得到key2-->二者用key2进行加密通信(可能会有中间人攻击,截获公钥key1,生成假公钥发送给客户端)

正文:

https的加密流程(ssl):服务端生成一对密钥(公钥/私钥key1),将公钥key1提交给CA生成数字证书(电子版)-->服务端发送数字证书给客户端校验-->客户端校验完成后生成key2用公钥key1加密发送给服务端-->服务端用私钥key1解密得到key2-->二者用key2进行加密通信

注意: 数字证书不能被篡改(除非机构的私钥泄露),CA会对主体信息(域名,公钥,有效期...)计算出哈希值,通过CA私钥加密生成数字签名,客户端用公钥key1解密得到的信息可以和计算证书信息得到的哈希值比较,判断证书是否被篡改

常见的状态码(待更新...)

当输入一个网址按下Enter键会发生什么(美团一面,2次)

解析URL

        |

判断缓存

        |

DNS解析

        |

获取MAC地址

        |

TCP三次握手

        |

HTTPS握手(TLS/SSL)

        |

发送请求,返回数据

        |

页面渲染

        |

TCP四次挥手

 重排一定会触发重绘,重绘不一定触发重排

布局没有发生变化,跳过layout+layer阶段,直接进行paint(绘制)阶段

​编辑

触发回流(重排):

  • 首次渲染页面
  • 添加/删除元素
  • 改变元素大小/位置/内容、字体大小
  • 调整浏览器窗口大小
  • 查询某些属性或调用某些方法(clientWidth/clientLeft/clientHeight..., getcomputerdStyle(),getBoundingClientReact())

如何避免回流:避免频繁操作样式、DOM(脱离文档流,隐藏修改再显示)、transform/opacity/filters/will-change不会引起回流重绘

触发重绘:没有出现几何图形的变化的CSS样式

浏览器缓存机制(百度一面,浏览器原理必需知道,2次)

我将缓存机制分为四种类型

浏览器首次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header(响应头),以供下次加载时对比使用;

  • cache-control: max-age; 请求数据时,响应头中的cache-control字段,表示有效时间内的重复请求浏览器无需再次访问服务器,直接使用缓存资源;
  • expires: 指定过期时间,同样是服务端的响应头字段,在有效的时间点之前,浏览器无需再次访问服务端,直接使用缓存资源;
  • Etag(服务端)/If-None-Match(客户端): 首次请求资源时,服务端将缓存结果签名并设置在响应头的Etag字段中发送给客户端,客户端缓存Etag和结果数据,并在下一次请求时将Etag设置在请求头的If-None-Match字段中,服务端接收请求会比较Etag和If-None-Match是否一致, 若一致,返回304告诉客户端资源没有发生变化,客户端接收304状态码直接访问之前缓存的结果数据;
  • Last-Modified(服务端)/If-Modified-Since(客户端): 首次请求资源时,服务端将缓存Last-Modified(结果数据最新的更改时间)并设置在响应头的Last-Modified字段中发送给客户端,客户端缓存Last-Modified和结果数据,并在下一次请求时将Last-Modified设置在请求头的If-Modified-Since字段中,服务端接收请求比较Last-Modified和If-Modified-Since,如果Last-Modified < If-Modified-Since(请求资源的时间在结果数据变化之后,资源是最新的),说明结果数据并没有发生变化,返回304告诉客户端资源没有发生变化,客户端接收304状态码直接访问之前缓存的结果数据;

 下一次加载时,强缓存优先级更高,先开始强缓存,判断cahe-control中的max-age(时间点)是否过期(若没有max-age,则判断expirse(时间段)是否过期);

若没过期,命中强缓存,直接使用本地资源;

若过期,开始协商缓存,在请求头中携带If-None-Match/If-Modified-Since向服务端发送请求,服务端判断If-None-Match中的etag值(若没有etag,比较If-Modified-Since是否在Last-Modified之后)是否改变;

如果没有改变(在后面),命中协商缓存,返回304;

如果改变(在之前),返回200+新的资源文件+新etag(Last-Modified)

​编辑

总的来说,就是设置资源过期时间和判断结果数据是否发生变化两种方式,理解记忆哦~ 

 OSI七层模型(字节一面)

应用层为应用程序提供服务(http发生在这一层)
表示层数据格式转化、数据加密
会话层建立、管理和维护会话
传输层建立、管理和维护端到端的连接(TCP、UDP在这一层,规定了数据包的传输方式)
网络层IP选址以及路由选择(规定了数据包的传输路线)
数据链路层提供介质访问和链路管理(传输路线)
物理层物理层(通过物理介质传输比特流)

标红要考滴,加深印象不吃亏不上当~     那么我问你::http发生在哪一层?TCP发生在哪一层?

REACT的钩子useEffect和useLayoutEffect区别

  • useEffect:浏览器重新绘制屏幕之触发,尽量将每个Effect作为一个独立的过程编写,并且每次只考虑一个单独的setup/cleanup(可以有多个)
  • useLayoutEffect 浏览器重新绘制屏幕之触发,内部的代码和所有计划的状态更新阻塞了浏览器重新绘制屏幕。如果过度使用,这会使你的应用程序变慢。

REACT中useCallback和useMemo区别(3次)

当尝试优化子组件时,它们都很有用。他们会 记住(或者说,缓存)正在传递的东西:

主要区别是使用场景的不同:

  • useMemo缓存调用的结果
  • useCallback缓存缓存函数本身

useMemo返回一个函数等价于使用useCallback,useCallback的出现是为了避免使用useMemo编写额外嵌套函数

REACT组件通信

通过props:父子、兄弟

使用useContext HOOK跨层级通信

redux/zustand状态管理

事件总线(eventBus):通过发布-订阅者模式实现任意组件通信

useRef获取子组件实例(慎用!!!避免频繁操作DOM)

受控组件和非受控组件(1次)

受控组件

定义: 由react控制并管理其内部状态的组件。它的状态通常由props传递给子组件,通过事件处理程序更新。受控组件提供了更精确的控制和验证,但更新会触发渲染(存在性能问题)

应用场景: 即时验证(密码强度),表单值依赖其他状态,遵循react单项数据流

非受控组件

定义: 由组件本身管理其内部状态的组件。它的状态通常通过ref从DOM中获取,不依赖于react处理状态的更新

应用场景: 处理大型表单,第三方集成,文件上传

useEffect和useLayoutEffect区别

useEffect允许你 将组件与外部系统同步。useEffect在浏览器完成对 DOM 的绘制后执行,不会阻塞渲染进程

useLayoutEffectuseEffect 的一个版本,在浏览器重新绘制屏幕之前触发。useLayoutEffect 在浏览器完成对 DOM 的布局(layout)和绘制(paint)之前执行。这意味着它可以同步访问和修改 DOM,但会阻塞浏览器的渲染过程。性能影响较大,使用不当会造成页面卡顿和性能问题

REACT事件机制

 REACT的diff算法(2次,待更新...)

核心思想:

REACT的Fiber 

问题: REACTv15在渲染时会递归对比虚拟DOM,找到需要更新的节点然后同步更新他们,一气呵成。整个过程REACT会占用浏览器资源,,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿

解决:

为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
  • 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。

核心思想: Fiber也称协程或纤程,它和线程不一样,本身不具有并发或并行的能力(需要线程配合),它是一种控制流程让出机制。让出CPU的执行权,让CPU先执行优先级高的任务(如与用户交互)。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

V8的垃圾回收机制——GC算法分代式垃圾回收机制

V8将内存(堆)分为新生代老生代

新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法标记压缩算法

对象会出现在老生代空间中的情况:

  • 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。

在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

V8数组模式-触发机制(性能优化)

快速模式

概念: 对应C语言的数组,速度快,紧凑

触发条件: 索引[0, length-1]且无空洞 || 预分配数组长度 < 100000(10w),无论有无空洞

字典模式

概念: 对应C语言的哈希表,速度慢,松散

触发条件: 预分配的数组长度 >= 100000(10w)且有空洞

优化策略

  1. 从0开始连续地初始化数组,避免进入字典模式
  2. 避免预分配>=10w的数组
  3. 删除元素避免使用delete,让数组保持紧凑
  4. 不要访问未初始化/已删除的数组元素

防抖和节流

先介绍具体含义和可能使用的场景(一定一定一定不要游戏回城技能起手,典型的B站大学选手)

  • 防抖: 确保在指定时间间隔内,仅最后一次触发的事件才会执行函数       

        应用场景:输入框输入、窗口大小调整

function debounce(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, wait);
  };
}

// 使用示例
const inputHandler = debounce(() => {
  console.log('Input event triggered');
}, 300);

document.getElementById('myInput').addEventListener('input', inputHandler);

  • 节流: 防止高频触发,在一段时间内仅触发一次

        应用场景:滚动、鼠标移动事件

function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// 使用示例
window.addEventListener('resize', throttle(() => {
  console.log('Window resized');
}, 1000));

扩展: 为什么要绑定传入函数上下文?

当防抖节流函数执行时,执行环境可能发生变化,给函数绑定this可以保证this指向的是预期对象

复杂表单的性能优化的方法

  • 分页加载
  • 虚拟滚动
  • 懒加载

 Webpack热更新原理(4次,唯品会一面,字节一面,待更新...)

TS篇

interface和type区别(3次 重点!)

interface用于定义类的实现对象类型的结构,type用于创建类型别名,多用于元组,联合类型

  • type 比 interface 更灵活,能表示一些 interface 无法表示的类型,如联合类型(|)、交叉类型(&)。
  • 重复定义interface 具有可扩展性,可以重复定义并合并,而 type 重复定义会报错(不被允许)
  • interface用extends实现继承性, type需要用&交叉类型来组合多种类型

如何实现子接口仅继承部分父接口且具有额外属性

使用typescript的工具类型Pick筛选出子接口想要继承的变量

Pick属于 TypeScript 内置的工具类型,其用途是从某个类型里选取特定的属性,进而构建出一个新类型

interface Parent {
  P1: string;
  P2: number;
  P3: boolean;
}

type PartialParent = Pick<Parent, "P1" | "P2">;

// 添加额外属性1.
interface Child extends PartialParent {
  C1: string;
}

let children: Child = {
  P1: "hello",
  P2: 123,
  C1: "world",
};

console.log(children);

// 添加额外属性2.添加一个字符串索引签名 可自定义添加0-n个属性
interface Child {
  [propName: string]: any;
}

let children1: Child = {
  P1: "hello",
  P2: 123,
  C1: "world",
  extra: "extra",
  name:"name"
}

console.log(children1);

接口引入文件形式

import VS import type

  • import  type: 仅导入类型信息,编译后会被移除,纯类型引入
  • import: 同时导入类型和值(ex: 类, 函数)

Record<K, T>

TypeScript 内置的工具类型,用于构造一个键类型为 K、值类型为 T 的对象类型。

type UserRole = 'admin' | 'user' | 'guest';
type RolePermissions = Record<UserRole, boolean>;
 
const permissions: RolePermissions = {
  admin: true,
  user: false,
  guest: false,
};

手写代码篇

数组去重(5次)

// 数组去重的方法
 const equal = function (list) {
      // 1.利用Set特性
      // let _list = [...new Set(list)];

      
      // 2.数组indexOf、includes方法
      // for (let i = 0; i < list.length; i++) {
      //   // if (_list.indexOf(list[i]) < 0) {

      //   if (!_list.includes(list[i])) {
      //     _list.push(list[i]);
      //   }
      // }

      // 3.数组filter方法和indexOf组合技
      // let _list = list.filter((item, index, array) => {
      //   return array.indexOf(item) === index;
      // })

      // 4.数组reduce和indexOf/includes组合技
      let _list = list.reduce((pre, cur) => {
        if (!pre.includes(cur)) {
          pre.push(cur);
        }
        return pre;
      }, [])


      return _list;
    }

console.log(equal([1, 2, 3, 4, 5, 23, 1, 2, 3, 4, 5]));


//4. 利用Map映射+循环 Map.has()的时间复杂度是O(1)!!!!!!
//总体时间复杂度为O(n)
function uniqueArray(arr: number[]) {
  const map = new Map<number, boolean>();
  const uniArr: number[] = [];
  for (let index in arr) {
    if (!map.has(arr[index])) {
      map.set(arr[index], true);
      uniArr.push(arr[index]);
    }
  }

  return uniArr;
}

console.log(uniqueArray([1, 1, 1, 1, 2, 2, 2, 3, 4, 5, 5, 6, 6, 7, 8, 8]));

对象扁平化

// 处理对象,使得 { a: {b: { c: 1, d:2 } } } 变成 { abc: 1, abd:2 }
function flatObj(obj: any, prefix: string = "", result: object = {}) {
  for (let key in obj) {
    if (typeof obj[key] === "object" && obj[key] !== null) {
      flatObj(obj[key], prefix + key, result);
    } else {
      result[prefix + key] = obj[key];
    }
  }

  return result;
}
const obj = { a: { b: { c: 1, d: 2 }, e: 3 } };    // { abc: 1, abd: 2, ae: 3 }
console.log(flatObj(obj));

判断对象是否相等

// 判断对象是否相等

function objEqual (obj1, obj2) {
  if (obj1 === obj2) return true;
  if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) return false;
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) return false;
  for (let i of keys1) {
    if (!obj2.hasOwnProperty(i) || !objEqual(obj1[i], obj2[i])) return false;
  }

  return true;
}

let o1 = { name: 'Tom', age: 18, ability: { basketball: true, run: true, swimming: true }, sex: 'woman' };
let o2 = { age: 18, name: 'Tom', ability: { swimming: true, basketball: true, run: true }, sex: 'woman' };

console.log(objEqual(o1, o2));

用setTimeout来实现setInterval

// 用setTimeout 实现 setInterval效果
function mySetInterval(fn, intervalTime) {
  let timeId;

  function interval(){
    fn();
    timeId = setTimeout(interval, intervalTime)
  }

  timeId = setTimeout(interval, 0)

  return {
    clear:function () { 
      clearTimeout(timeId);
     }
  }
}

const interval1 = mySetInterval(()=>{
  console.log(new Date())
}, 1000)

setTimeout(interval1.clear, 5000) // 5s之后清空定时器mySetInterval

手写Promise(必须记住的干货)

不要死记硬背捏(也是告诫我寄己),要跟着作者的逻辑走,消化前辈的思路

/**
 * 定义全局的状态常量,避免魔法字符串
 * pending: 初始状态,既不是成功,也不是失败
 * fulfilled: 成功状态,操作成功完成
 * rejected: 失败状态,操作失败
 */
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  private _state
  private _result
  private handlers: any[] = []
  constructor(executor: Function) {
    this._state = PENDING
    this._result = null

    const resolve = (value: any) => {
      this.changeState(FULFILLED, value)
    }

    const reject = (reason: any) => {
      this.changeState(REJECTED, reason)
    }

    // 捕获错误,如果executor执行过程中抛出错误,直接reject
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  private changeState = (state: string, result: any) => {
    /** 只有状态为pending时才能改变状态(状态不可逆) */
    if (this._state !== PENDING) return
    this._state = state
    this._result = result
    this.run() // 状态转变后,执行then方法中存储的回调函数
  }

  then(onFulfilled: any, onRejected?: any) {
    /** 因为可链式调用,所以then方法需要返回一个新的Promise实例 */
    return new MyPromise((resolve: Function, reject: Function) => {
      // 因为状态不会立刻转变
      // 需要把onFulfilled和onRejected存储起来,等状态转变后再执行
      // 同一实例可以多次调用then方法,所以handlers是一个数组
      // 可以封装一个run方法,在then/changeState中调用run方法
      this.handlers.push({
        onFulfilled,
        onRejected,
        resolve,
        reject,
      })
      this.run()
    })
  }

  catch(onRejected: any) {
    //直接复用then,简简单单
    return this.then(undefined, onRejected)
  }

  /** 执行handlers中存储的回调函数 */
  run() {
    // then调用的时候可能是pending状态
    if (this._state === PENDING) return
    /** 状态一旦改变,需要执行同一实例所有then的回调函数 */
    while (!!this.handlers.length) {
      const { onFulfilled, onRejected, resolve, reject } = this.handlers.shift()
      if (this._state === FULFILLED) {
        this.runOne(onFulfilled, resolve, reject)
      } else {
        this.runOne(onRejected, resolve, reject)
      }
    }
  }

  runOne(callBack: Function, resolve: Function, reject: Function) {
    // 放入微队列中
    this.runMicroTask(() => {
      if (typeof callBack !== 'function') {
        const settledState = this._state === FULFILLED ? resolve : reject
        // 不是函数,直接把结果/原因传递给下一个then(穿透)
        settledState(this._result)
      } else {
        try {
          const data = callBack(this._result)
          // 需要判断返回值是否是一个Promise
          if (this.isPromiseLike(data)) {
            // 如果是Promise,需要等待这个Promise完成后再决定下一个then的状态
            data.then(resolve, reject)
          } else {
            resolve(data)
          }
        } catch (error) {
          reject(error)
        }
      }
    })
  }

  isPromiseLike(value: any) {
    // 满足PromiseA+规范的对象/函数必须具有then方法(函数)
    if (!!value && (typeof value === 'object' || typeof value === 'function')) {
      return typeof value.then === 'function'
    }
  }

  runMicroTask(fn: any) {
    // node环境
    if (typeof process === 'object' && typeof process.nextTick === 'function') {
      process.nextTick(fn)
    } else if (typeof MutationObserver === 'function') {
      // 浏览器环境,MutationObserver是一个监听DOM变化的API,可以用来模拟微任务
      const observer = new MutationObserver(fn)
      const textNode = document.createTextNode('1')
      observer.observe(textNode, { characterData: true })
      textNode.data = '2'
    } else {
      // 脱离环境,setTimeout也是一个宏任务,但在没有更好的选择时可以用来模拟微任务
      setTimeout(fn, 0)
    }
  }
}

const p = new MyPromise((resolve, reject) => {
  // setTimeout(() => {
  resolve('等了2秒')
  // }, 2000)
})

setTimeout(() => {
  console.log('打印了')
}, 1000)

p.then(132, (err: any) => {
  console.log('MyPromise error', err)
  return '错误被捕获了'
}).then(
  (res) => {
    console.log('resolve2', res)
  },
  (err: any) => {
    console.log('MyPromise error2', err)
  },
)

p.then(
  (res: any) => {
    console.log('MyPromise', res)
  },
  (err: any) => {
    console.log('MyPromise error', err)
  },
)

​编辑

算法篇

二叉树

递归三部曲

  1. 确定递归函数的参数和返回值
  2. 确定终止条件
  3. 确定单层递归的逻辑

巧记前-中-后序遍历

tips:左节点始终比右节点先遍历,根节点随顺序变换位置

  • - 左 - 右
  • :左 - - 右
  • :左 - 右 -

性能优化

这里记录开发过程中优化性能的手段

  • 在组件中定义变量/函数,使用useMemo/useCallback包裹
  • 自身调用的递归函数谨慎使用,推荐while(数据量太大容易堆栈溢出)
  • 避免大量的嵌套循环,用映射代替Array.incules 获取数据(O(1)优于O(n))
  • Array.push在大量数据场景中,用concat代替push(push参数有限制,大数据量push可能会卡死)(性能:concat > push > 展开运算符)
  • Array.unshift 大数据量下性能不好,推荐使用push+reverse或for循环倒序
  • forEach优于map

场景题

登录无感刷新方案

1.单点登录

概念: 一种身份认证机制,允许用户通过一次登录访问多个相互信任的关联系统或应用,无需重复登录验证(ex:闲鱼和淘宝都是阿里巴巴旗下产品)

模式:

SESSION + COOKIE:

(同一浏览器)统一通过认证中心进行登录校验,管理一个session表格,表格记录有效登录状态的用户信息(唯一标识(sid)+用户信息),第一次登录时,认证中心返回cookie(sid),浏览器保存,登录其他子系统时只需发送cookie到认证中心即可完成登录

​编辑

TOKEN

单TOKEN:(同一浏览器)通过认证中心登录校验拿到token,使用其他子系统时,可以在系统内完成token验证(加密解密算法,系统与认证中心进行约定),无需再向认证中心验证

劣势:失去对用户的控制

​编辑

双TOKEN:提高对用户的控制,(同一浏览器)通过认证中心登录校验拿到两个TOKEN(TOKEN:所有子系统系统能够识别,REFRESHTOKEN:仅认证中心识别)

​编辑

2.无感刷新(双token)

首次登录校验成功得到短token&长token,短token时效很短,当短token失效时,自动发起刷新(短)token请求(携带有效长token),即可获得有效短token;

无需用户手动刷新,增加用户体验,保证安全性

核心代码:

// request.js

import axios from 'axios'
import { getToken, setToken, setRefreshToken } from './token' //存在localStorage中
import { refreshToken, isRefreshRequest } from './refreshToken'
const request = axios.create({
  baseURL: 'https://api.example.login.com',
  headers: {
    Authorization: `Bearer ${getToken}`
  }
})

request.interceptors.response.use(
  async (response) => {
    if (response.headers.authorization) {
      const token = response.headers.authorization.split(' ')[1]
      setToken(token)
      request.defaults.headers['Authorization'] = `Bearer ${token}`
    }
    if (response.headers.refreshtoken) {
      const refreshToken = response.headers.refreshtoken.split(' ')[1]
      setRefreshToken(refreshToken)
    }
    //短token过期&不是刷新token的请求
    if (response.data.code === 401 && !isRefreshRequest(response.config)) {
      // 刷新token 
      const isSuccess = await refreshToken()
      if (isSuccess) {
        // 重新请求
        console.log('重新请求')
        response.config.headers['Authorization'] = `Bearer ${getToken()}`
        const resp = await request.request(response.config)
        return resp
      }else{
        //无权限
        console.log('无权限,跳转至登录页')
        return response.data
      }
    }
    return response.data
  }, (error) => {
    // 对响应错误做些什么
    return Promise.reject(error)
  }
)

export default request

// refreshToken.js

import request from './request'
import { getRefreshToken } from './token'

// 防止多次刷新请求
let promise = null

export async function refreshToken () {
  console.log('刷新token')
  if (promise)  return promise
  promise = new Promise(async  (resolve, reject) => {
    const res = await request.get('/refresh_token', {
      headers: {
        Authorization: `Bearer ${getRefreshToken()}`
      },
      __isRefreshToken: true,
    })
    resolve(res.code === 200)
  }).finally(() => {
    // 每次刷新结束都销毁promise
    promise = null
  })

  return promise
}

export function isRefreshRequest (config) {
  return config.__isRefreshToken
}