前端校招面试常见八股总结

213 阅读41分钟

一. HTML&CSS

1.HTML的语义化

HTML语义化的定义

HTML语义化指的是使用具有实际意义的名称的标签(见名知义)来构建网页,而不是只使用通用的

标签。是HTML5新特性


HTML语义化的优势

  • 可读性强: 使得代码结构清晰,开发者更直观地理解网页内容和各标签的用处。
  • 易于维护: 语义化的HTML有利于团队协作,让后期维护变得简单。
  • 易于阅读: 即使样式文件未加载,页面结构照样清晰,提升用户体验
  • 无障碍访问: 屏幕阅读器等辅助工具可以更好地解析网页,使网站对视障用户更友好。
  • SEO友好: 搜索引擎能更好地解析页面,确定关键字的权重,有利于爬虫读取有用信息,提升网页排名

**SEO :Search Engine Optimization 搜索引擎优化

常见的语义化标签

语义化标签用途
header定义网页或者页面某个部分的头部,如导航栏或logo部分
nav定义导航菜单,通常用于链接界面的不同部分
main>定义页面的主要内容,一般一个页面只有一个
section定义页面中的一块独立区域,一般用于分块内容
article表示一篇独立的文章或者博客等独立的内容块
aside侧边栏,通常放附加信息、广告或推荐内容
footer定义页面或者某个部分的底部,例如版权信息
figure图像标签,一般搭配
来对图像进行说明
mark标记重点文本,例如搜索结果的高亮显示
time表示日期或时间,有利于搜索引擎解析时间信息

拓展问题

Q:怎么看待虽然有着以上优势,但HTML5语义化标签并没有得到广泛应用,例如京东、淘宝等大厂的页面仍然使用div元素,用id标识用途?

A:语义化标签相较于通用标签的提升有限,不值得为了它重写网站。


2.盒子模型

HTML页面中所有的元素都可以看成一个盒子

盒子模型组成

内容(Content): 元素的实际内容,如文本、图片等

内边距(Padding): 内容与边框之间的间距,影响到元素的内部空间

边框(Border): 包围内容和内边距的边框,可以设定宽度、样式和颜色

外边距(Margin): 元素与其它元素之间的间距,影响元素的外部空间

盒子模型的分类

(1)标准盒子模型: 默认的盒子模型,width和height只包括内容区域大小,而不包括padding、border、margin。

box-sizing:content-box

(2)怪异(IE)盒子模型: width和height包含了padding和border,不会额外增加盒子尺寸

box-sizing:border-box

盒子模型的常见问题

margin折叠问题

(1)相邻兄弟元素的外边距折叠

当两个兄弟元素的垂直外边距相邻时,他们的外边距会折叠,取两个边距的最大值而不是相加。

(2)父子元素的外边距折叠

当子元素外边距与父元素的外边距相邻时,子元素的外边距可能会“穿透”父元素,与父元素的外边距折叠。

<div style="margin: 0; padding: 0; border: 1px solid black;">
    <p style="margin-top: 20px;">Hello</p>
</div>

上面代码中,p标签的margin-top会与div标签的外边距折叠,导致div标签共享了p标签的外边距,也移动了20px

如何避免外边距折叠

1.使用padding或者border

2.overflow:hidden

3.display:flex/inline-block

3.浮动

浮动定义

浮动(float)是CSS早期用于布局的方式,可以让元素脱离标准文档流向左或右浮动,使得文本或元素环绕它。最典型的应用是实现文字环绕图片。

浮动设置

float:none      /*默认值,元素不浮动 */
float:left      /*元素向左浮动     */
float:right     /*元素向右浮动     */

浮动的特点

  1. 元素会脱离文档流(但仍然占据空间)
  2. 不会影响父元素的高度(父元素可能会高度坍塌)
  3. 后续元素会环绕浮动元素
  4. 浮动元素仍然受margin、padding和width的影响

浮动常见问题

父元素高度坍塌

(1)使用overflow属性---最常用

为父元素设置 overflow:hiddenoverflow:auto 可以触发父元素块级格式化上下文(BFC)

(2) flow-root方法

display:flow-root

(3)使用高级布局方式

display=flex/grid

(4)手动给父容器指定高度或最小高度

以后开发中是否会使用float?

只在老项目中或者需要实现文本环绕的情况下使用,否则尽量使用高级布局方法flexbox和grid ## 4.样式优先级

CSS选择器的优先级从高到低如下:

  1. !important
  2. 内联(标签中写style=” “)
  3. ID选择器(#id)
  4. 类选择器(.class)、伪类(:hover、:nth-child())、属性选择器([type="text"])
  5. 元素选择器(div、p、h1等)、伪元素(::before、::after)
  6. 继承的样式
  7. 浏览器默认的样式

5.CSS尺寸设置单位

1.绝对单位

(1)像素 px

(2)厘米(cm)毫米(mm)英寸(in) ---通常用于需要打印页面的场景

(3)点 (pt) 派卡(pc) 1pc=12pt

2.相对单位

(1)百分比(%) ---相较于父元素的尺寸

(2)em--相对于当前元素的字体大小,如果未指定则继承父元素的字体大小

rem--相对于根元素(html)的字体大小


6.块级格式化上下文BFC

BFC(Block Formatting Context,块级格式化上下文)是CSS布局中的一个概念,决定了元素如何定位及其子元素如何相互影响,在处理浮动、清除浮动、边距折叠等问题时尤为重要

BFC触发条件

满足以下条件之一会触发BFC

  • float不为none (即float为left或right)
  • overflow不为visible(即overflow为hidden或auto或scroll)
  • display为flex、inline-flex、grid、inline-grid、flow-root
  • position为absolute或fixed
  • contain:layout

BFC的特性

(1) BFC 内部的元素不会影响外部元素

(2) BFC 可以包含浮动元素(清除浮动问题)

(3) BFC 可以阻止外边距(margin)重叠

BFC高度计算原理

当BFC的高度是auto的情况下(不设置默认高度就是auto)

1.如果只有inline-level,BFC的高度是行高

2.如果有block-level,BFC的高度由最底层的块上边缘和最底层块盒子的下边缘之间的距离决定

3.如果有绝对定位元素,该元素将被忽略

4.如果有浮动元素,BFC会增加高度来包括这些浮动元素


7.未知宽高元素水平垂直居中方法

1.flexbox

.container {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
  height: 100vh; /* 让父容器充满视口高度 */
  background: lightgray;
}
​
.box {
  background: red;
  padding: 20px;
}
​

2.grid

.container {
  display: grid;
  place-items: center; /* 水平 + 垂直居中 */
  height: 100vh;
  background: lightgray;
}
​
.box {
  background: red;
  padding: 20px;
}
​

3.margin:auto (仅适用于固定宽高)

4.position:absolute + left right

position:absolute;
left:50%;
top:50%;
transform: translate(-50%,-50%);

8.三栏布局的实现方案

Flexbox实现

.container{
    display : flex;
    height:100vh
}
​
.left, .right{
    width: 200px;
    background: lightblue;
}
.middle{
    flex: 1; //占据剩余空间
    background: lightcoral;
}

GridBox实现

.container{
    display: grid;
    grid-template-columns: 200px auto 200px; //左右两边分别宽200px,中间占据剩余的空间
}
.left, .right{
    background: lightblue;
}
.middle{
    background:lightcoral;
}

二. JavaScript

9.Js中的数据类型

JavaScript中的数据类型可以分为基本数据类型引用数据类型

基本数据类型

  • 存储在栈中,访问速度快。
  • 不可变,值不能被修改,只能重新赋值。
  • 比较时按值比较(值相同即相等)

共有7种基本数据类型

数据类型示例描述
Numberlet a = 123包括整数和浮点数,NaN也是Number类型
BigIntlet b = 123n大整数,以n结尾
Stringlet s = "hello"字符串,用 " " 或 ' ' 或 `` 表示
Booleanlet isTrue = true;只有true和false
undefinedlet x;未定义的变量的默认值
nulllet y = null;空对象, typeof null === “object”
Symbollet sym = Symbol("id");创建唯一值,适用于对象属性

引用数据类型

引用数据类型储存在堆(Heap)中,变量保存的是引用地址,比较是引用比较(地址是否相同)

类型实例特点
Objectlet obj = {name : "Alice"};键值对结构,可存储多个数据
Arraylet arr = [1, 2, 3];有序列表,可以存储不同类型的数据
Functionlet fc = function(){}特殊对象,可以执行
Datelet date = new Date();日期对象
RegExplet regex = /abc/g;正则表达式
Map/Setnew Map()/new Set()Map存键值对,Set存唯一值

10.null和undefined的区别

nullundefined
含义表示空值或无对象表示变量未定义或未赋值
类型typeof null //objecttypeof undefined //undefined
是明确的值,代表没有对象变量声明了但未赋值时的默认值
使用场景手动赋值给变量或对象属性,表示“空”变量未赋值、函数无返回值、对象属性不存在等
BooleanBoolean(null) //falseBoolean(undefined) //false
NumberNumber(null) // 0Number(undefined) //NaN
相等性比较null == undefined //truenull === undefined //false

undefined适用场景

  1. 变量声明但未赋值

    let x;
    console.log(x);//undefined
    
  2. 访问对象不存在的属性

    let obj = {name:"Tom"};
    console.log(obj.age); //undefined
    
  3. 函数无返回值

    function foo(){}
    console.log(foo()); //undefined
    
  4. 数组中缺失的元素

    let arr = [1,,3];
    console.log(arr[1]); //undefined
    
  5. delete删除对象的属性

    let user = {name:"Bob"};
    delete user.name;
    console.log(user.name); //undefined
    

11.判断数据类型的方法

  1. typeof
  • 适用于基本数据类型的判断,null会被误判为 “object”

  • 对引用数据类型不适用:

      let arr=[1,2,3]
      let fc = function(){}
      let date = new Date()
      let regex = /abc/g;
      let m = new Map();
      let s = new Set();
    ​
      console.log(typeof arr);  //object
      console.log(typeof fc);   //function
      console.log(typeof date); //object
      console.log(typeof regex);//object
      console.log(typeof m);    //object
      console.log(typeof s);    //object
  1. instanceof

    适用于对象类型的判断:

    console.log([] instanceof Array); // true
    console.log({} instanceof Object); // true
    console.log(function(){} instanceof Function); // true
    

    无法用于检测原始类型:

    console.log(123 instanceof Number); // false
    console.log("hello" instanceof String); // false
    
    3. **Object.prototype.toString.call()** 
    

最可靠的方法,可以准确判断所有类型:

console.log(Object.prototype.toString.call(123));   // "[object Number]"
console.log(Object.prototype.toString.call("hi")); // "[object String]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call([]));   // "[object Array]"
console.log(Object.prototype.toString.call({}));   // "[object Object]"
console.log(Object.prototype.toString.call(Symbol())); // "[object Symbol]"

12.数组去重的方法

  1. Set Set是ES6引入的一种引用数据类型,不会包含重复的元素。适用于基本数据类型的去重

    const arr = [1,2,2,3,4,5,5]
    const newarr = [...new Set(arr)]
    console.log(newarr); //[1,2,3,4,5]
    
  2. filter+indexOf

    const arr = [1,2,2,3,4,4,5];
    const uniqueArr = arr.filter(
    (item, index)=> arr.indexOf(item) == index
    )
    
  3. reduce

    const arr = [1,2,2,3,4,4,5];
    const uniqueArr = arr.reduce(
        (acc,cur) =>{
        if(!acc.includes(cur)) acc.push(cur);
        return acc;
    },
        []);
    
  4. Map

    const arr = [1,2,2,3,4,4,5];
    const uniqueArr = [];
    const map = new Map();
    ​
    arr.forEach(item =>{
        if(!map.has(item)){
            map.set(item,true);
            uniqueArr.push(item);
        }
    })
    console.log(uniqueArr); // [1, 2, 3, 4, 5]
    
  5. 使用Object(兼容性好,适用旧浏览器)

对象的key是不能重复的,因此可以利用对象实现去重

const arr = [1,2,2,3,4,4,5];
const newArr = [];
const obj = {};
arr.forEach(item=>{
    if(!obj[item]){
        obj[item] = true;
        newArr.push(item);
    }
});
console.log(newArr); //[1,2,3,4,5]

6.数组对象去重(根据id去重)

可以用Map对对象数组去重

const users = [{id:1,name:"Alice"},
               {id:2,name:"Bob"},
               {id:1,name:"Alice"}];
const newUsers = [...new Map(users.map(user => [user.id,user])).values()];
console.log(newUsers);//[{id:1,name:"Alice"},{id:2,name:"Bob"}]

7.sort+for

const arr = [1, 1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [];
​
arr.sort((a, b) => a - b).forEach((item, index) => {
  if (index === 0 || item !== arr[index - 1]) uniqueArr.push(item);
});
​
console.log(uniqueArr); // [1, 2, 3, 4, 5]

总结

基本数据类型 用Set

对象数组用Map

兼容ES5及以下用filter+indexOf或Object

13.伪数组和数组的区别

伪数组

伪数组(类数组)指一个具有length属性且可以通过索引访问元素的对象,但它不具有数组的方法

const pseudoArray = {
  0: "a",
  1: "b",
  2: "c",
  length: 3
};
​
console.log(pseudoArray[0]); // "a"
console.log(pseudoArray.length); // 3
console.log(Array.isArray(pseudoArray)); // false
console.log(pseudoArray.push); // undefined(没有 push 方法)

伪数组与数组的对比

伪数组数组
数据存储以对象形式存储以数组形式存储
访问方式索引访问 obj[0]索引访问arr[0]
length有length属性有length属性
数组方法没有push、map、filter等数组方法有数组方法
是否是Array实例不是,Array.isArray() //false是,Array.isArray() //true

常见的伪数组

  1. arguments(es5及以下常见)

    arguments是函数的参数列表,不是数组,但可以通过索引访问

    function example() {
      console.log(arguments[0]); // "hello"
      console.log(arguments.length); // 2
      console.log(Array.isArray(arguments)); // false
    }
    ​
    example("hello", "world");
    
  2. NodeList NodeList是Document.querySelectorAll()返回的伪数组

    const divs = document.querySelectorAll("div");
    console.log(divs.length); // 3
    console.log(divs[0]); // <div>...</div>
    console.log(Array.isArray(divs)); // false
    
  3. HTMLCollection HTMLCollection由getElementsByTagName()返回

    const spans = document.getElementsByTagName("span");
    console.log(spans.length); // 2
    console.log(spans[0]); // <span>...</span>
    console.log(Array.isArray(spans)); // false
    

伪数组转成真实数组

  1. Array.from() (最推荐)

    const divs = document.querySelectorAll("div");
    const arr = Array.from(divs);
    console.log(arr.map(div => div.textContent)); // ✅ 现在可以使用 `map`
    
  2. Array.prototype.slice.call()

    function example() {
      const arr = Array.prototype.slice.call(arguments);
      console.log(arr.map(item => item.toUpperCase())); // ["A", "B", "C"]
    }
    ​
    example("a", "b", "c");
    
  3. 拓展运算符[...] (ES6)

    function example() {
      const arr = [...arguments]; // 使用扩展运算符
      console.log(arr.map(item => item + "!")); // ["hello!", "world!"]
    }
    ​
    example("hello", "world");
    

14.map和forEach的区别

mapforEach
返回值返回新数组无返回值(undefined)
适用场景需要返回新数组的情况(如数据转换)仅执行副作用操作
是否可以链式调用可以不可以
是否改变原数组不会改变原数组不会改变原数组
是否用于赋值适用于赋值不适用于赋值

map

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10] ✅ 返回新数组
console.log(numbers); // [1, 2, 3, 4, 5] ✅ 原数组不变

forEach

const numbers = [1,2,3,4,5];
numbers.forEach(num =>{console.log(num*2)});
console.log(numbers);//[1,2,3,4,5]原数组不变

总结

当需要返回新数组或需要进行数据的转换时使用map

当不需要返回值,只想遍历数组时用forEach

15.ES6中的箭头函数

箭头函数(Arrow Function)是 ES6 引入的一种简洁的函数写法,它可以减少代码冗余,并且不绑定 this,常用于回调函数、数组方法等场景。

基本用法

const func = (args)=>{codes}
​
function add(a,b){
    return a+b;
} //普通函数写法
const add = (a,b)=> a+b  //箭头函数

箭头函数格式

  1. 可以省略return 如果只有一条返回语句,可以省略return

  2. 单个参数可以省略()

    const square = (a)=>{return a*a} //箭头函数完整版
    const square = a => a*a          //箭头函数简写版
    
  3. 如果没有参数,必须写()

    const regreat = ()=> "hello" //没有参数,()不能省略
    
  4. 多行代码则必须使用{}和return

    const multiply =(a,b)=>{
        const res = a*b;
        return res
    }
    

箭头函数中的this问题

  • 普通函数中的this由调用者决定

  • 箭头函数的this由外层作用域决定

    const obj = {
        value:10;
        normalfunc : function(){
            console.log(this.value); //this指向obj对象
        }
        arrowfunc :() =>{
            console.log(this)        // this指向外层,一般是window,当开启严格模式时这个this是undefined
            console.log(this.value); //undefined
        }
    }
    

不能使用箭头函数的场景

  1. 不能作为构造函数

    const Person = (name) => {
        this.name = name;
    }//this指向的是外层,不是Person对象
    ​
    function Person(){
        this.name = name; //构造函数只能用普通函数
    }
    
  2. 不能使用arguments,要用rest参数

    const showArgs = ()=>{
        console.log(arguments)//报错 arguments未定义
    }
    const showArgs = (...args)=>{
        console.log(args) //正常运行 [1,2,3]
    }
    showArgs(1,2,3)
    

总结

使用场景是否推荐使用箭头函数?理由
数组方法(map、forEach、filter等)推荐代码简洁,不需要使用this
回调函数(如setTimeout)推荐继承对象的this,不需要手动bind
对象方法不推荐this指向window
构造函数不推荐this指向window

16.事件扩展符

...在JS中叫扩展运算符或剩余参数

  1. 剩余参数: 用于收集函数的多个参数,转换为数组

  2. 展开运算符: 用于拆解数组或对象

  3. 数组克隆

    • 数组展开

      const arr1=[1,2,3];
      const arr2=[...arr1,4,5]
      console.log(arr2) //[1,2,3,4,5]
      
    • 对象展开

      const obj1 = {name:"Alice",age:25};
      const obj2 = {...obj1,gender:"female"}
      console.log(obj2) //{name:"Alice",age:25,gender:"female"}
      
    • 剩余参数

      const sum = (...numbers) => numbers.reduce((acc,num)=> acc+num,0)
      console.log(sum(1,2,3,4)) //10
      
    • 数组克隆(可以将伪数组转成数组)

      let a=[1,2,3]
      let b=[...a]
      console.log(b)//[1,2,3]
      let c = [...a,...b]
      console.log(c)//[1,2,3,1,2,3]
      

17.闭包的理解

什么是闭包?

闭包(Closure) 是 JavaScript 中的一种重要机制,它指的是:

一个函数可以访问其外部作用域的变量,即使该外部作用域已经被销毁

闭包通常出现在函数内部嵌套函数的情况下,内部函数可以访问外部函数的变量,即使外部函数已经执行结束

function outer(){
    let count = 0;
    return function inner(){
        count++;
        console.log(count);
 };
}
const counter = outer(); //outer()执行后返回inner函数被counter接收
counter(); // 1
counter(); // 2
counter(); // 3

闭包的常见应用

  1. 私有变量 使用闭包可以创建私有变量,外部无法直接修改它

    function createCounter(){
        let count = 0;
        return {
            increment:()=>++count;,
            decrement:()=>--count;,
            getCount :()=>  count;
        };
    }
    ​
    const counter = createCounter();
    console.log(counter.increment()) //1
    console.log(counter.increment()) //2
    console.log(counter.decrement()) //1
    console.log(counter.getCount())  //1
    console.log(counter.count)       //undefined
    //count变量被保护,外部不能直接访问和修改
    
  2. 实现once函数(只执行一次的函数)

    function once(fn){
        let called = false
        return function(...args){
            if(!called){
                called = true
                return fn(...args)
            }
        }
    }
    ​
    const init = once(()=> console.log("初始化..."))
    init() //初始化...
    init() //不执行
    init() //不执行
    //once函数确保某个函数(init函数)只会执行一次,防止多次初始化
    
  3. 防抖(Debounce) 前端开发中,防抖可以控制用户频繁触发的事件

    function debounce(fn, delay){
        let timer
        return function(...args){
            clearTimeout(timer)
            timer = setTimeout(()=> fn(...args),delay)
        }
    }
    ​
    const onInput = debounce(() => console.log("搜索中...", 500))
    ​
    document.getElementById("search").addEventListener("input", onInput)
    
  4. 事件监听 闭包可以用于动态事件绑定

    function addEventListeners(){
        for(let i = 1; i <= 3; i++){
            document.getElementById(`btn${i}`).addEventListener("click",()=>{
                console.log(`Button ${i} clicked`)
            })
        }   
    }
    addEventListeners()
    

闭包的缺点

可能会导致内存泄露,如果闭包长期引用不需要的变量,会导致变量无法被回收,一直占用内存空间

手动释放变量:将其赋值为null即可

let memoryLeak = createBigObject()
memoryLeak = null //释放引用

18.JS中的变量提升

变量提升指的是变量(var)、函数声明(function)在JS代码被执行前,会提升到作用域的最前面,因此可以在声明之前使用他们。

变量提升的规则

  1. var声明的变量会被提升,但值不会提升,默认值为undefined
  2. function声明的函数会整体提升,包括函数体
  3. let和const声明的变量不会被提升

变量声明会被提升,但赋值不会提升

greet() //error greet声明了但是未赋值
var greet = function(){
console.log('Hello')
}
​
hello(); //Hello  函数体会被提升
function hello(){
    console.log("Hello!")
}

for循环中避免使用var

for(var i =0;i<3;i++){
    setTimeout(()=>console.log(i),1000); //异步执行,var声明的i变量是全局变量
}//结果 3 3 3

19.函数的this指向问题

普通函数的this规则

调用方式this指向
直接调用fn()全局对象(window,严格模式下是undefined)
对象方法obj.fn()调用该方法的对象
构造函数new fn()新创建的实例对象
call/apply/bind手动绑定的对象
事件监听 element.onclick=fn触发事件的元素

箭头函数的this规则

箭头函数没有自己的this,它继承外层作用域的this且无法改变

  1. 箭头函数的继承

    const obj = {
        name:"Alice"
        sayHello:function(){
            const arrowFn=()=>{
                console.log(this.name)
            }
            arrowFn()
            }
        }
    obj.sayHello() //Alice
    //arrowFn没有自己的this,继承sayHello函数的this
    //sayHello是对象obj的方法,this指向obj对象。
    
  2. 箭头函数的this不能改

    const obj = {name:"Bob"}
    const arrowFn = () =>{
        console.log(this.name)
    }
    arrowFn.call(obj)//arrowFn的this仍然指向外层作用域而不是obj
    
  3. 箭头函数在setTimeout里面的使用

    const obj = {
        name:"Daney",
        sayHello:function(){
            setTimeout(()=>{
                console.log(this.name)
            },1000)
        }
    }
    obj.sayHello() //Daney
    //箭头函数中的this继承sayHello函数的this,因此指向obj对象
    *********************************************************
    //如果将setTimeout函数换成普通函数
    const obj={
        name:"Daney",
        sayHello:function(){
            setTimeout(function(){
                console.log(this.name)
            },1000)
        }
    }
    obj.sayHello()//this指向Window 严格模式下this是undefined
    

    因此有时可以利用箭头函数继承外部this的特性,避免手动绑定this!

总结

  • 对象方法、构造函数等使用普通函数
  • 回调函数、定时器等需要继承外部this时使用箭头函数

20.call/apply/bind的作用与区别

都是用于手动改变this指向的方法

方法作用调用方式传参方式
call修改this并调用函数fn.call(this.Arg,arg1,arg2,...)依次传递
apply修改this并调用函数fn.apply(this.Arg,[arg1,arg2,...])数组传递
bind绑定this返回新函数fn.bind(this.Arg,arg1,arg2,...)依次传递
  • call示例

    function greet(greeting){
        console.log(`${greeting},${this.name}`)
    }
    ​
    const person = {name:"Alice"}
    greet.call(person,:"Hello")//Hello,Alice
    //greet函数的this绑定到person对象,并根据传入的“Hello”参数执行函数
    
  • apply示例

    const obj = {
        value: 10
    };
    ​
    function greet(message) {
        console.log(`${message}, my value is ${this.value}`);
    }
    ​
    greet.apply(obj, ['Hello']); // 输出: Hello, my value is 10
    //与call基本一样,只是函数的传参方式不同
    
  • bind示例

    const obj = {
        name:"Alice",
        sayHello:function(){
            setTimeout(function(){
                console.log(this.name)
            }.bind(this),1000)
        }
    }
    obj.sayHello()//Alice
    //使用bind手动将函数的this指向obj函数,且无法更改
    //使用箭头函数更方便
    

21.JS继承方法及其优缺点

原型链继承

function Parent(){
    this.name = "Parent"
}
Parent.prototype.getName = function(){
    console.log(this.name)
}
function Child(){}
Child.prototype = new Parent()//子类原型指向父类的实例
const child = new Child()
child.getName()//"Parent"

构造函数继承

function Parent(name){
    this.name = name
    this.colors = ["red", "blue", "green"]
}
function Child(name){
    Parent.call(this,name)
}
const child1 = new Child("Alice")
child1.colors.push("yellow")
console.log(child1.colors) //["red", "blue", "green","yellow"]
console.log(child1.name) //Aliceconst child2 = new Child("Bob")
console.log(child2.colors)//["red", "blue", "green"]
console.log(child1.name) //Bob

组合继承(原型链 + 构造函数)

function Parent(name){
    this.name = name
    this.color=["red", "blue", "green"]
}
Parent.prototype.getName = function(){
     console.log(this.name);
}
function Child(name,age){
    Parent.call(this,name)
    this.age=age
}
​
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Childconst child1 = new Child("Alice", 18)
child1.colors.push("yellow")
​
const child2 = new Child("Bob", 20)
console.log(child2.colors)//["red", "blue", "green"]

原型式继承

const parent = {
    name:"parent",
    colors:["red", "blue", "green"]
}
​
const child = Object.create(parent)
child.colors.push("yellow")
​
console.log(parent.colors)//["red", "blue", "green", "yellow"]

寄生式继承

function createChild(obj){
    const clone = Object.create(obj)
    clone.getName = function(){
        console.log("I am child")
    }
    return clone
}
​
const parent = {name : "Parent"}
const child = createChild(parent)
child.getName()

class继承(ES6新特性)

class Parent{
    constructor(name){
        this.name = name
        this.color = ["red", "blue", "green"]
    }
    getName(){
        console.log(this.name)
    }
}
​
class Child extends Parent{
    constructor(name,age){
        super(name)
        this.age=age
    }
}
const child = new Child("Alice",18)
child.getName()// Alice
方式继承原型方法引用属性性质是否可传参优点缺点
原型链继承可以共享可以继承父类的方法共享引用属性
构造函数继承不可以独立子类的属性独立不能继承方法
组合继承可以独立结合前两者优点调用了两次构造函数
原型式继承可以共享继承对象共享引用属性
寄生式继承可以共享增强对象不能复用方法
ES6 类继承可以独立简单有效本质基于prototype

总结

  • 一般情况下使用类继承 简单高效
  • 兼容老项目使用组合继承
  • 简单继承用原型式继承Object.create()(轻量级)

22.new关键字的执行过程

new用于创建一个实例对象,new执行时发生4个重要步骤

function Person(name,age){
    this.name = name
    this.age = age
}
const person = new Person("Alice",18)

以上代码执行时:

  1. 创建一个空对象,将其 proto 设置为构造函数的prototype,建立原型链连接

    const obj = {}
    obj._proto_ = Person.prototype
    
  2. 以新创建的对象为this,绑定执行Person构造函数

    const result = Person.call(obj, "Alice", 18)
    ​
    obj.name//Alice
    obj,age //18
    
  3. 检查构造函数返回值,如果构造函数返回对象,new会返回该对象

    function Parent(){
        return {name:"Bob"}
    }
    const p = new Parent()
    console.log(p.name) //p是上面构造函数return的对象{name:"Bob"}
    

    如果构造函数返回的是非对象(string、number、boolean、null、undefined)则new会忽略返回值,直接返回this(新创建的实例对象)

    function Parent(){
        return 11
    }
    const p = new Parent()
    console.log(p) //Parent{} p是新实例对象 
    
  4. 返回新对象

    return typeof result === "object" ? result :obj
    
  • 根据以上步骤,可手写自己的new方法
function myNew(Constructor,...args){
    const obj = Object.create(Constructor.prototype)
    const result = Constructor.apply(obj,args)
    return result instanceof Object ? result : obj
}
​
//测试
function Person(name){
    this.name = name
}
const p = myNew(Person, "Alice")
console.log(p.name) //"Alice"

23.defer和async的区别

defer和async都是用于控制

属性加载方式执行顺序适用场景
defer异步加载按HTML中的顺序执行依赖DOM的脚本
async异步加载下载完后立即执行独立脚本(广告,统计,追踪)

defer

<!DOCTYPE html>
<html lang="zh">
<head>
    <script src="script1.js" defer></script>
    <script src="script2.js" defer></script>
</head>
<body>
    <p>Hello World!</p>
</body>
</html><!-- 
1.异步下载 script1.js 和 script2.js,不会阻塞 HTML 解析
2.按照 HTML 代码的顺序执行,即 script1.js 先执行,然后是 script2.js。
3.会等到 DOM 解析完成后再执行,相当于 DOMContentLoaded 事件之前执行。
-->

async

<!DOCTYPE html>
<html lang="zh">
<head>
    <script src="script1.js" async></script>
    <script src="script2.js" async></script>
</head>
<body>
    <p>Hello World!</p>
</body>
</html><!-- 
1.异步下载 script1.js 和 script2.js,不会阻塞 HTML 解析。
2.下载完成后立即执行,不保证执行顺序,如果 script2.js 先下载完成,它会先执行!
3.执行时可能 HTML 还没完全解析完,所以如果脚本依赖 DOM 结构,可能会出问题。
-->
属性异步下载?阻塞HTML解析?执行顺序等DOM解析完成?
defer按HTML代码顺序
async先下载完的先执行

两者都可以使html和js同时加载,defer会等html加载完后再执行js,而async不会,js加载完后立即执行

24.promise及使用方法

promise是es6新增的一种异步编程解决方案,用于解决地狱回调问题

promise有三种状态

  • pending(进行中):异步操作尚未完成
  • fulfilled(已成功):异步操作成功,返回resolve值
  • rejected(已失败):异步操作失败,返回reject错误信息

promise使用方式:

  1. 创建promise

    const myPromise = new Promise((resolve,reject)=>{
        let success = true
        setTimeout(()=>{
            if(success){
                resolve("操作成功")
            }
            else {
                reject("操作失败")
            }
        },2000)
    })
    
  2. 处理Promise

    myPromise.then(result =>{
        console.log("成功",result)
    }).catch(error =>{
        console.log("失败",error)
    }).finally(()=>{
        console.log("成功或失败都会执行")
    })
    //.then(result={...}) 处理成功的结果
    //.catch(error={...}) 处理失败的结果
    //.finally(()=>{...}) 成功或失败都会执行
    

Promise的链式调用

new Promise((resolve) => {
    setTimeout(() => resolve(1),1000)
}).then(result=>{
    console.log(result) //1
    return result *2
}).then(result=>{
    console.log(result) //2
    return result *3
}).then(result=>{
    console.log(result) //6
})

Promise的API

Promise.all()

Promise.all([
    fetch("https://jsonplaceholder.typicode.com/posts/1"),
    fetch("https://jsonplaceholder.typicode.com/users/1")
])
    .then(responses => Promise.all(responses.map(res => res.json())))
    .then(data => console.log(data))
    .catch(error => console.error("某个请求失败", error));
    
//同时执行多个 Promise,所有任务成功才会 resolve,有 一个失败 就 reject。

Promise.race()

const p1 = new Promise(res => setTimeout(() => res("P1 完成"), 3000));
const p2 = new Promise(res => setTimeout(() => res("P2 完成"), 1000));
​
Promise.race([p1, p2]).then(console.log); // P2 完成
//谁先完成就返回谁

Promise.allSettled()

Promise.allSettled([
    Promise.resolve("成功"),
    Promise.reject("失败"),
    new Promise(res => setTimeout(() => res("延迟成功"), 2000))
])
    .then(results => console.log(results));
//等所有Promise结束,无论成功或失败

Promise.any()

Promise.any([
    Promise.reject("失败 1"),
    Promise.reject("失败 2"),
    Promise.resolve("成功!")
])
    .then(console.log) // 成功!
    .catch(console.error);
//只要有一个成功就返回该Promise的结果,如果全部失败则返回AggregateError
方法适用场景
Promise.all()所有成功才继续(全成功才行)
Promise.race()只要有一个完成就继续(比拼速度)
Promise.allSettled()获取所有结果(不会因失败终止)
Promise.any()只要有一个成功就继续(比拼成功)

Promise如何解决地狱回调?

地狱回调指的是回调函数嵌套过多,代码难以阅读和维护

// 回调地狱
function getData(callback) {
    setTimeout(() => {
        console.log("获取数据");
        callback();
    }, 1000);
}
​
getData(() => {
    getData(() => {
        getData(() => {
            console.log("回调地狱!");
        });
    });
});
​
//Promise链式调用
function getData() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log("获取数据");
            resolve();
        }, 1000);
    });
}
​
getData().then(getData).then(getData).then(() => console.log("结束"));
​

25.JS实现异步的方法

  1. 回调函数

    function fetchData(callback) {
        setTimeout(() => {
            console.log("数据获取成功");
            callback("返回的数据");
        }, 2000);
    }
    ​
    fetchData((data) => {
        console.log("处理数据:", data);
    });
    
  2. Promise

    const fetchData = () => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                let success = true;
                if (success) {
                    resolve("数据获取成功");
                } else {
                    reject("获取失败");
                }
            }, 2000);
        });
    };
    ​
    // 使用 Promise
    fetchData()
        .then(data => console.log(data))
        .catch(error => console.error(error));
    ​
    
  3. async/await

    async function fetchData() {
        try {
            let data = await new Promise(resolve => setTimeout(() => resolve("数据获取成功"), 2000));
            console.log(data);
        } catch (error) {
            console.error("获取失败", error);
        }
    }
    ​
    fetchData();
    ​
    
  4. setTimeout和setInterval

    setTimeout(() => {
        console.log("2 秒后执行");
    }, 2000);
    ​
    let count = 0;
    let timer = setInterval(() => {
        console.log(`执行 ${++count} 次`);
        if (count === 5) clearInterval(timer);
    }, 1000);
    
  5. Generator

    function* asyncTask() {
        console.log("开始");
        yield new Promise(resolve => setTimeout(() => resolve(console.log("执行任务")), 2000));
        console.log("任务完成");
    }
    ​
    let task = asyncTask();
    task.next(); // 启动
    setTimeout(() => task.next(), 3000); // 继续执行

    26.cookie、sessionStorage、localStorage的区别

cookiesessionStoragelocalStorage 都用于在浏览器端存储数据

特性cookiesessionStoragelocalStorage
存储大小4KB5MB-10MB5MB-10MB
生命周期默认当前会话,可手动指定过期时间仅在当前会话永久,只能手动删除
作用范围所有同源界面当前界面(标签页)所有同源页面
随Http请求发送?是(影响性能)
适用场景身份验证、跨页面数据存储临时数据存储(表单填充)长期存储用户偏好数据

27.如何实现可过期的localStorage

localStorage 默认不会过期,数据永久存储,但我们可以手动实现过期机制,即在存储数据时附加时间戳,并在读取时检查是否过期

// ✅ 设置 localStorage 数据并指定过期时间(单位:毫秒)
function setLocalStorageWithExpire(key, value, expireTime) {
    const data = {
        value: value, 
        expires: Date.now() + expireTime // 计算过期时间
    };
    localStorage.setItem(key, JSON.stringify(data));
}
​
// ✅ 读取 localStorage 数据(判断是否过期)
function getLocalStorageWithExpire(key) {
    const item = localStorage.getItem(key);
    
    if (!item) return null; // 数据不存在
​
    const data = JSON.parse(item);
    
    if (Date.now() > data.expires) {
        localStorage.removeItem(key); // 过期删除
        return null; 
    }
    
    return data.value; // 返回数据
}
​
// ✅ 示例:存储数据并设置 10 秒后过期
setLocalStorageWithExpire("user", "John", 10000);
​
// ✅ 读取数据(10 秒后数据会被自动清除)
console.log(getLocalStorageWithExpire("user"));
​

28.Token的存放位置

Token放在cookie中

优点:

  • 自动携带Token,cookie默认会在同源请求中自动携带
  • 可设置HttpOnly禁止 JavaScript 读取防止XSS(跨站脚本)攻击
  • 可设置Secure只允许HTTPS运输,避免HTTP传输中被攻击

缺点:

  • 会被CSRF(跨站请求伪造) 攻击,由于 cookie 在同源请求时会自动携带,可能被恶意网站利用 伪造请求
  • 大小受限,cookie存储空间有限(4KB)
方式特点安全适用场景
localStorage易于操作,不随请求自动发送易受 XSS 攻击适用于 SPA 单页面应用
sessionStorage仅当前会话有效,页面关闭即丢失易受 XSS 攻击适用于 短生命周期的 Token
cookie可与请求自动发送,可设 HttpOnly防 XSS(HttpOnly ,但 易受 CSRF 攻击适用于 后端主导的身份认证

29.axios的拦截器原理及应用

axios拦截器是一种拦截HTTP请求和响应的机制,可以在请求发送前或者响应返回前对数据进行处理,包括:

  • 自动添加token
  • 请求/响应数据格式化
  • 错误统一处理
  • 全局loading状态管理

工作原理

axios内部使用拦截器链来管理请求和响应。本质是利用Promise的链式调用处理请求和响应。

拦截器的执行顺序:

  1. 请求拦截器: 先注册的拦截器先执行,主要用于修改请求头(添加token)或数据预处理
  2. 请求发送
  3. 响应拦截器: 先注册的拦截器后执行,主要用于格式化响应数据、统一错误处理

请求拦截器1 → 请求拦截器2 → 发送请求 → 响应拦截器2 → 响应拦截器1

axios拦截器的应用

(1)自动携带token

在localStorage/cookie中获取token,并自动加到请求头

instance.interceptors.request.use(config => {
    const token = localStorage.getItem("token");
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

(2)全局错误处理

instance.interceptors.response.use(
    response => response.data,
    error => {
        if (error.response.status === 401) {
            console.log("未授权,跳转到登录页");
            window.location.href = "/login";
        }
        return Promise.reject(error);
    }
);

(3)请求Loading处理

instance.interceptors.request.use(config => {
    // 显示 loading
    showLoading();
    return config;
});
​
instance.interceptors.response.use(response => {
    // 隐藏 loading
    hideLoading();
    return response;
});

(4)自动刷新token

instance.interceptors.response.use(
    response => response,
    async error => {
        if (error.response.status === 401) {
            const refreshToken = localStorage.getItem("refreshToken");
            if (refreshToken) {
                const newToken = await refreshAccessToken(refreshToken);
                localStorage.setItem("token", newToken);
                error.config.headers.Authorization = `Bearer ${newToken}`;
                return instance(error.config); // 重新发送请求
            }
        }
        return Promise.reject(error);
    }
);

30.创建ajax的过程

ajax是一种异步数据交互技术,可以在不刷新页面的情况下与服务器通信

ajax创建过程:

  1. 创建XMLHttpRequest对象
  2. 配置请求信息(请求方式、URL、是否异步)
  3. 监听onreadystatechange事件,获取服务器响应
  4. 发送请求
  5. 处理服务器返回结果
//1.创建XMLHttpRequest请求
const xhr = new XMLHttpRequest()
//2.配置请求
xhr.open("GET","https://jsonplaceholder.typicode.com/posts/1",true)
//3.监听请求状态变化
xhr.onreadystatechange = function(){
    if(xhr.readyState === 4){
        if(xhr.status === 200){
            console.log("数据返回成功:",JSON.parse(xhr.responseText))
        }else{
            console.error("请求失败",xhr.status)
        }
    }
}
​
//4.发送请求
xhr.send()

XMLHttpRequest详解

(1)配置请求--open(method,url,async)

参数说明
method请求方法(GET,POST,PUT,DELETE)
url请求的服务器地址
async是否异步?(true = 异步,false=同步)

(2)监听状态--readyState

readyState说明
0UNSENT(未初始化)
1OPENED(已调用open(),但未发送请求)
2HEADERS_RECEIVED(已收到响应头)
3LOADING(正在下载响应体)
4DONE(请求完成,可获取数据)

(3)获取服务器响应--status

status说明
200请求成功
404资源未找到
500服务器错误

(4)发送请求--send(data)

xhr.send(); // GET 请求,不需要传递数据
xhr.send(JSON.stringify({ name: "Alice" })); // POST 请求,发送 JSON 数据

GET和POST的区别

GET用于获取数据,把参数直接拼接在URL上

POST用于提交数据,把数据放在请求体中

总结

AJAX使用XMLHttpRequest进行异步请求,现在更推荐使用fetchaxios

31.fetch请求方式

fetch是基于Promise的API,用于执行HTTP请求

基本用法

fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then(response =>response.json())
    .then(data = > console.log("请求成功",data))
    .catch(error => console.error("请求失败", error))
  • 默认是GET请求
  • fetch返回的是Promise
  • response.json()也是Promise,需要通过.then()处理

fetch请求方式

fetch(url,options) options是请求配置对象

  • GET请求

    fetch("https://jsonplaceholder.typicode.com/posts/1",{
        method:"GET",
        headers:{
        "Content-Type" : "application/json"
    }
    })
    .then(response => response.json())
    .then(data => console.log("GET请求成功", data))
    .catch(error => console.error("GET请求失败",error))
    
  • PUT请求

    fetch("https://jsonplaceholder.typicode.com/posts/1",{
        method:"PUT",
        headers:{
            "Content-Type" : "application/json"
        },
        body: JSON.stringify({title:"Updated Title", body: "Updated Content"})
    })
    .then(response => response.json())
    .then(data => console.log("PUT请求成功:", data))
    .catch(error => console.error("PUT请求失败:", error))
    
  • PATCH请求

    fetch("https://jsonplaceholder.typicode.com/posts/1", {
        method: "PATCH",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({ title: "New Title" }) // 只更新 title,不修改 body
    })
    .then(response => response.json())
    .then(data => console.log("✅ PATCH 请求成功:", data))
    .catch(error => console.error("❌ PATCH 请求失败:", error));
    
  • DELETE请求

    fetch("https://jsonplaceholder.typicode.com/posts/1", {
        method: "DELETE"
    })
    .then(response => console.log("✅ DELETE 请求成功"))
    .catch(error => console.error("❌ DELETE 请求失败:", error));
    

fetch处理错误

fetch只在网络错误时catch,如果是http错误(404、500),不会触发catch,需要手动处理:

fetch("https://jsonplaceholder.typicode.com/posts/1000")
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP 错误!状态码: ${response.status}`);
        }
        return response.json();
    })
    .then(data => console.log("✅ 数据:", data))
    .catch(error => console.error("❌ 请求失败:", error));

fetch超时处理

fetch没有内置超时,可以用Promise.race()来实现

function fetchWithTimeout(url, timeout = 5000) {
    return Promise.race([
        fetch(url),
        new Promise((_, reject) => setTimeout(() => reject(new Error("请求超时")), timeout))
    ]);
}
​
fetchWithTimeout("https://jsonplaceholder.typicode.com/posts/1", 3000)
    .then(response => response.json())
    .then(data => console.log("✅ 请求成功:", data))
    .catch(error => console.error("❌ 请求失败:", error));

fetch取消请求

fetch支持AbortController中断请求

适用场景:

  • 搜索框请求防抖
  • 组件卸载时取消请求
  • 网络请求超时控制
const controller = new AbortController();
const signal = controller.signal;
​
fetch("https://jsonplaceholder.typicode.com/posts/1", { signal })
    .then(response => response.json())
    .then(data => console.log("✅ 请求成功:", data))
    .catch(error => console.error("❌ 请求被取消:", error));
​
// 2 秒后取消请求
setTimeout(() => controller.abort(), 2000);

总结

请求方式适用场景是否需要body
GET获取数据
POST提交新数据
PUT更新整个资源
PATCH更新部分资源
DELETE删除资源

三.浏览器

32.保持前后端实时通信的方式

轮询

客户端定期发送请求到服务器,服务器返回最新数据

setInterval(()=>{
    fetch("/api/getDate")
    .then(response => response.json())
    .then(data => console.log("获取到数据:",data))
    .catch(error => console.log("请求失败",error))
},5000)//每5秒请求一次

优点: 实现简单

缺点: 请求过多,浪费资源,延迟高,数据更新不及时

长轮询

客户端请求服务器,服务器不立即响应,而是等有新数据产生时才返回

客户端收到数据后立即发送新请求,实现伪实时通信

function longPolling(){
    fetch("/api/getData")
        .then(response => response.json())
        .then(data =>{
            console.log("获取到新数据:", data)
            longPolling()//立即发送下一个请求
    })
        .catch(error => {
            console.log("请求失败",error)
            setTimeout(longPolling, 3000)
    })
}
longPolling()

优点: 比普通轮询高效,减少无用请求。兼容性好

缺点: 仍然存在请求浪费,资源利用率不如WebSocket

WebSocket

全双工通信,服务器可以主动推送数据给客户端

连接建立后,数据在TCP连接上传输,减少了HTTP请求开销

const ws = new WebSocket("ws://localhost:8080");ws.onopen = () => {
    console.log("WebSocket 连接成功");
    ws.send("Hello Server");
};ws.onmessage = (event) => {
    console.log("收到服务器消息:", event.data);
};ws.onerror = (error) => {
    console.error("WebSocket 错误:", error);
};ws.onclose = () => {
    console.log("WebSocket 连接关闭");
};

优点:

  • 真正的实时通信,服务器可以主动推送数据
  • 比轮询更高效,减少了HTTP请求的开销
  • 适用于聊天、股票、实时通知等场景

缺点:

  • 不支持HTTP/1.X 服务器
  • 可能会被防火墙阻止

其它技术

SSE、WebRTC等,了解即可

方案双向适用场景优点缺点
轮询简单场景简单资源浪费
长轮询数据不频繁变化简单资源浪费
WebSocket聊天、游戏实时通信
SSE服务器推送消息自动重连只能单向
WebRTC音视频、P2P低延迟复杂

33.浏览器输入URL发生了什么

在浏览器地址栏输入URL按下回车后,浏览器会经历从DNS解析页面渲染,最终把页面呈现。整个流程分为以下阶段:

DNS解析(域名转换成IP地址)

  1. 浏览器检查缓存
  2. 操作系统检查缓存(查看本地DNS缓存)
  3. 检查本地hosts文件(有无手动指定的IP)
  4. 询问本地DNS服务器(ISP提供)
  5. 递归查询: 根DNS服务器(返回顶级域名.com的服务器) 顶级域名服务器(返回google.com的权威DNS服务器) 权威DNS服务器(返回www.google.com的IP地址)

结果以上步骤,浏览器拿到服务器的IP地址

建立TCP连接(三次握手)

浏览器与目标服务器通过三次握手建立TCP连接

  1. 客户端 -> 服务器:发送SYN(同步)请求
  2. 服务器 -> 客户端:返回SYN + ACK(确认)
  3. 客户端 -> 服务器:发送ACK(确认)

发送HTTP请求

建立连接后,浏览器发送HTTP(或HTTPS)请求

服务器处理请求并返回响应

服务器收到请求后:

  1. 检查缓存
  2. 解析URL并找到对应资源
  3. 检查权限
  4. 执行动态请求
  5. 生成HTML页面
  6. 返回HTTP响应

浏览器解析HTML

浏览器解析HTML页面,分为以下阶段:

  1. 构建DOM树:解析HTML标签,形成DOM
  2. 构建CSSOM树:下载CSS并解析为CSSOM
  3. 执行JavaScript:遇到
  4. 合并DOM和CSSOM:生成渲染树(Render Tree)
  5. 布局(Layout):计算每个元素的大小和位置
  6. 绘制:显示到界面

34.浏览器如何渲染页面

  1. 解析HTML,构建DOM树
  2. 解析CSS,构建CSSOM
  3. 生成渲染树(Render Tree)
  4. 计算布局(Layout)
  5. 绘制
  6. 合成

35.重绘、重排问题

重绘(Repaint)重排(Reflow) 都是浏览器渲染机制中的重要概念,它们影响页面的性能,合理优化可以提高渲染效率,让页面更流畅。

重绘(Repaint)

当元素的样式(颜色、背景、阴影)发生变化,但不影响元素的布局时,浏览器触发重绘

重排(Reflow)

当元素的几何属性(位置、大小)发生变化时,浏览器会重新计算布局,触发重排,也叫回流

重排比重绘更消耗性能。重排一定触发重绘,重绘不一定触发重排

优化

避免不必要的重绘和重排可以优化性能

  1. 避免逐条修改样式,使用classList或style.cssText

    //错误写法,会触发多次重绘和重排
    element.style.width = "100px";
    element.style.height = "200px";
    element.style.backgroundColor = "red";
    ​
    //正确写法
    element.style.cssText = "width: 100px; height: 200px; background-color: red;";
    //或者
    element.classList.add("new-style");
  2. 批量操作DOM,使用DocumentFragment

    //错误写法
    for (let i = 0; i < 100; i++) {
        let div = document.createElement("div");
        div.innerText = "Item " + i;
        document.body.appendChild(div); // 每次插入都会触发重排
    }
    //使用DocumentFragment一次性插入
    let fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
        let div = document.createElement("div");
        div.innerText = "Item " + i;
        fragment.appendChild(div);
    }
    document.body.appendChild(fragment); // 只触发一次重排
  3. 使用transform代替top/left 移动元素

    //错误写法,会触发重排+重绘
    element.style.position = "absolute";
    element.style.left = "100px"; // 触发重排
    ​
    //正确写法,transform只触发重绘,避免了重排
    element.style.transform = "translateX(100px)"
    

36.浏览器的垃圾回收机制

JavaScript 是一门 自动垃圾回收(GC) 语言,浏览器会定期回收不再使用的内存,以防止内存泄漏。但 GC 并不是实时运行的,而是 在特定时间点触发,以优化性能和减少内存占用

垃圾回收算法

  1. 标记清除算法(Mark-and-Sweep) 从根对象出发遍历所有能访问到的对象,并打上“可达”标记 没有被标记的对象是不可达的,它们会被回收 优点: 能处理循环引用问题 缺点: 每次回收会暂停JS线程,可能导致性能问题
  2. 引用计数算法(Reference Counting) 每个对象都存储一个引用计数(ref count),表示被多少个变量引用 当引用数为0时,对象就会被回收

V8引擎的优化机制

Google Chrome 的 V8 引擎 对垃圾回收做了优化

  1. 分代回收机制 新生代对象(Young Generation):存活时间短(如临时变量) 采用Scavenge算法 老生代对象(Old Generation):存活时间长(如全局对象) 采用标记清除算法
  2. 增量标记(Incremental Marking) 为了避免 GC 暂停页面运行,V8 将标记过程拆分成多步,减少性能影响。

37.事件循环Eventloop,宏任务和微任务

JavaScript 是单线程的,它通过 事件循环(Event Loop) 来实现异步任务的执行,使得浏览器可以同时处理多个任务,而不会阻塞 UI 渲染。

事件循环流程

  1. 执行同步代码(属于主线程上的任务,直接执行)。
  2. 遇到异步代码(如 setTimeoutPromise ,将其交给 Web API 处理,并继续执行同步代码。
  3. 同步代码执行完毕后,Event Loop 开始检查 任务队列 先执行微任务队列(Microtasks) (如 Promise.then()MutationObserver)。 再执行宏任务队列(Macrotasks) (如 setTimeoutsetIntervalsetImmediate、事件监听)。
  4. 重复以上步骤,形成循环,确保 JavaScript 程序持续运行。

38.跨域的解决方案

什么是跨域

跨域(Cross-Origin Request)指的是在浏览器中,当一个网页的 JavaScript 代码尝试请求不同源(域名、协议或端口不同)的资源时,受到 浏览器的同源策略(Same-Origin Policy, SOP) 限制,导致请求被阻止

同源策略:* 同源策略* 是浏览器的一种安全机制,它规定:1.只有当协议、域名和端口都相同 时,网页中的 JavaScript 才能访问服务器上的资源。2.不同源之间的请求(跨域)默认会被浏览器拦截,除非服务器允许跨域访问。

解决方案

CORS(跨域资源共享)

CORS(Cross-Origin Resource Sharing) 是最主流的跨域解决方案,它允许服务器在响应头中添加特殊的 HTTP 头信息,从而允许跨域访问。前端无需修改,只需后端支持

JSONP(仅支持GET请求)

JSONP(JSON with Padding) 通过 <script> 标签不受同源策略限制的特点来实现跨域请求。适用于 只需要 GET 请求的情况

代理服务器(Nginx/Node.js 代理)

如果前端和后端在同一台服务器,但端口不同,可以使用代理服务器,让前端请求代理服务器,而代理服务器再请求真正的后端。

前端WebPack代理

如果你的前端使用 Webpack(如 Vue/React),可以配置 devServer.proxy 进行跨域代理:

总结
方案适用场景优点缺点
CORS服务器可配置推荐使用,安全需要后端支持
JSONP仅限 GET 请求兼容老浏览器安全性低
Nginx 代理服务器有 Nginx对前端透明需要额外部署
Webpack 代理前端开发时简单易用仅适用于本地开发
window.postMessageiframe 跨域安全、简单仅适用于前端页面通信

四.React

39.React的生命周期

旧生命周期

react生命周期(旧)

新生命周期

react生命周期(新)

40.ReactRouter路由

安装ReactRouter

使用ReactRouter需要安装react-router-dom

npm i react-router-dom

创建路由

import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
​
const Home = () => <h1>首页</h1>;
const About = () => <h1>关于我们</h1>;
const NotFound = () => <h1>404 - 页面未找到</h1>;
​
const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
};
​
export default App;
​

路由导航

Link组件
组件用来代替a标签,页面跳转时不会触发刷新
import { Link } from "react-router-dom";
​
const Navbar = () => {
  return (
    <nav>
      <Link to="/">首页</Link> | <Link to="/about">关于我们</Link>
    </nav>
  );
};
NavLink组件

NavLink与Link功能类似,但可以根据当前URL高亮选中的链接

import { NavLink } from "react-router-dom";
​
const Navbar = () => {
  return (
    <nav>
      <NavLink to="/" style={({ isActive }) => ({ color: isActive ? "red" : "black" })}>
        首页
      </NavLink> |
      <NavLink to="/about">关于我们</NavLink>
    </nav>
  );
};

路由传参

URL传参
import { useParams } from "react-router-dom";
​
const Product = () => {
  const { id } = useParams();
  return <h1>当前产品 ID:{id}</h1>;
};
​
const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/product/:id" element={<Product />} />
      </Routes>
    </Router>
  );
};
//访问 /product/123 时,页面会显示 当前产品 ID:123
Query传参
import { useSearchParams } from "react-router-dom";
​
const SearchPage = () => {
  const [searchParams] = useSearchParams();
  const keyword = searchParams.get("keyword");
​
  return <h1>搜索关键字:{keyword}</h1>;
};
//如果访问 /search?keyword=React,页面会显示 搜索关键字:React

路由重定向

使用Navigate组件重定向

import { Navigate } from "react-router-dom";
​
const ProtectedPage = ({ isAuthenticated }) => {
  return isAuthenticated ? <h1>受保护页面</h1> : <Navigate to="/" />;
};
//如果 isAuthenticated 为 false,会自动跳转到首页。

路由懒加载

使用React.lazy()+Suspense实现懒加载

import React, { Suspense, lazy } from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
​
const Home = lazy(() => import("./Home"));
const About = lazy(() => import("./About"));
​
const App = () => {
  return (
    <Router>
      <Suspense fallback={<div>加载中...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Suspense>
    </Router>
  );
};
​
export default App;

嵌套路由

const Dashboard = () => {
  return (
    <div>
      <h1>仪表盘</h1>
      <Outlet />
    </div>
  );
};
​
const Profile = () => <h2>用户资料</h2>;
​
const App = () => {
  return (
    <Router>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />}>
          <Route path="profile" element={<Profile />} />
        </Route>
      </Routes>
    </Router>
  );
};
//<Outlet /> 代表子路由渲染位置。
//访问 /dashboard/profile 时,会渲染 Profile 组件。

41.React组件传值方式

方法适用场景适用关系适用规模
props父组件向子组件传值父-->子小型
回调函数子组件向父组件传值子-->父小型
状态提升兄弟组件通信兄-->兄(通过父)小型
Context API深层组件传值任意中型
Redux全局状态管理任意大型
Event Emitter组件间事件通知任意小型
ref访问子组件方法父-->子小型

42.React中setState()的异步性

setState()代码本身是同步的,但是它一般表现出异步。setState()的异步与否与它是否被React进行了批量更新有关。

React 会收集多个 setState 调用,在 下一次渲染前合并更新(批量处理)

一般认为,在setTimeout或者原生DOM事件中是同步的,其它所有地方都是异步。

React18更新了优化批处理机制,在任何地方调用setState都会批处理,因此在任何地方都表现为异步

43.React事件绑定原理

React中event事件不是原生事件,而是对原生event进行了封装的新类SyntheticBaseEvent,模拟出DOM事件的所有功能。

React17之前所有事件绑定在document上,React17之后,所有事件绑定在root根组件上

React不是将事件绑定在真实DOM上,而是在root处监听所有支持的事件,当事件发生并冒泡到root处时,React将事件内容封装并交给真正的处理函数运行。这种方式减少了内存消耗,还能在组件挂载和销毁时统一处理订阅和移除事件。

冒泡到root上的事件不是原生浏览器事件,而是React自己实现的合成事件(SyntheticBaseEvent),合成事件的好处:1.兼容性好 2.统一挂载在document上,减少内存的消耗,方便在组件挂载/卸载时统一订阅和移除事件 3.方便事件统一管理

如果不想要事件冒泡的话调用event.stopPropagation是无效的,应该调用event.preventDefault

44.React中的Hooks

Hooks是React16.8为了使函数组件也能使用state和生命周期而提出的一组API

useState

usestate用于管理组件状态,能够在函数组件中声明状态变量

function Counter(){
    const [count, setCount] = useState(0)
    //返回值: 包含2个元素的数组, 第1个为内部当前状态值, 第2个为更新状态值的函数
    //setXxx()2种写法:
    //setXxx(newValue): 参数为非函数值, 直接指定新的状态值, 内部用其覆盖原来的状态值
    //setXxx(value => newValue): 参数为函数, 接收原本的状态值, 返回新的状态值, 内部用其覆盖原来的状态值
    return (
        <button onClick={()=>setCount(count+1)}>
            点击次数:{count}
        </button>
    )
}

useState不会合并状态,要手动管理对象合并

const [state, setState] = useState({ name: "Tom", age: 25 });
setState({ age: 26 }); // ❌ name 会丢失!
setState((prev) => ({ ...prev, age: 26 })); // ✅ 解决

useEffect

用于执行副作用例如状态获取(fetch)、订阅事件、手动操作DOM

useEffect(() => { 
          // 在此可以执行任何带副作用操作  相当于componentDidMount()的功能
          return () => { // 在组件卸载前执行
            // 在此做一些收尾工作, 比如清除定时器/取消订阅等 相当于componentWillUnmount()的功能
          }
        }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
                         // 相当于componentDidUpdate()的功能

可以把 useEffect Hook 看做如下三个函数的组合 componentDidMount() componentDidUpdate() componentWillUnmount()

Hook作用
useState声明状态变量,使得函数式组件也可以使用state属性
useEffect处理副作用,相当于componentDidMount+componentWillUnmount+componentDidUpdate
useContext共享全局状态(相当于Context API的功能)
useRef获取DOM元素、保存可变值(不触发组件更新)
useReducer复杂状态管理(类似 Redux)
useMemo计算缓存,优化性能
useCallback记忆化函数,防止不必要的组件重新渲染
useImperativeHandle暴露自定义的 ref 方法
useLayoutEffect在 DOM 变更后同步触发(比 useEffect 早)
useDebugValue自定义 Hook 的调试信息

45.React中Hooks的优缺点

优点

  • 函数组件无需this,避免了类组件复杂的this绑定问题
  • 状态管理更加集中,而不用依赖生命周期函数
  • 可读性强,代码逻辑更清晰

缺点

  • 状态异步更新,需要注意

    //useState是异步更新的,连续更新时要使用回调式参数
    const[count, setCount] = useState(0)
    ​
    setCount(count+1)
    setCount(count+1)
    //React会批量处理state的更新,因此state的更新是异步的,相当于调用了两次setCount(0+1) 所以count值为1setCount(precount => precount+1)
    setCount(precount => precount+1)
    //正确写法。 因此在使用useState钩子时,一定要注意异步问题,当更新的状态依赖于之前的状态时,尽量使用回调函数参数
    
  • hooks的useEffect只包括了componentDidMount、componentDidUpdate还有componentWillUnmount这三个生命周期,对于getSnapshotBeforeUpdate和componentDidCatch等其他的生命周期没有支持

五.前端性能

46.前端性能优化手段

资源压缩与合并

CSS、JS、HTML文件压缩,减少文件大小。

WebPack压缩JS代码

使用WebP代替PNG/JPG,SVG代替小图标,减少HTTP请求

懒加载

只有当客户需要时才加载资源,提高首屏速度

React.lazy

const ComponentA = React.lazy(()=>import('./ComponentA'))

预加载

  • preload(优先加载):用于当前页面必须的资源(字体,关键CSS)

    <link rel="preload" href="styles.css" as="style">
    
  • prefetch(预取):适用于用户可能访问的资源

    <link rel="prefetch" href="next-page.js">
    

减少重排和重绘

📌 重排(Reflow):改变布局,会影响整个页面的渲染。 📌 重绘(Repaint):只影响视觉,不影响布局,代价较小。

优化技巧:

  • 避免频繁操作 DOM,使用 DocumentFragmentrequestAnimationFrame

  • 批量修改样式

    // ❌ 低效,触发多次重排
    element.style.width = "100px";
    element.style.height = "100px";
    ​
    // ✅ 高效,一次性修改
    element.style.cssText = "width: 100px; height: 100px;";
    
  • 避免使用top/left移动元素

    /* ❌ 触发重排 */
    position: absolute;
    top: 100px;
    ​
    /* ✅ 触发 GPU 加速 */
    transform: translate3d(0, 100px, 0);
    

虚拟DOM

列表渲染时要设置唯一key,使React中的Diff算法能够正常工作。避免不必要的DOM重新渲染

{list.map(item => <div key={item.id}>{item.name}</div>)}

存储优化

利用缓存,使用localStorage、sessionStorage存储静态数据,减少网络请求

避免阻塞渲染

script 标签会导致HTML的解析阻塞而去解析JS代码,因此尽量将JS代码放在body的结尾。

或者使用async/defer来异步加载JS

<script src="script.js" async></script>  <!-- 不保证执行顺序 -->
<script src="script.js" defer></script>  <!-- 按顺序执行 -->
优化方式优化方法
加载优化代码压缩、懒加载、预加载
渲染优化避免重排重绘、虚拟DOM
存储优化localStorage缓存
代码优化异步加载JS
网络优化HTTP/2、HTTP/3、CDN

47.性能优化指标

页面加载指标

指标说明
TTFB(Time To First Byte)首字节时间,浏览器收到服务器响应的第一个字节的时间
FCP(First Contentful Paint)首次内容绘制,用户首次看到页面上的内容
LCP(Largest Contentful Paint)最大内容绘制,页面上最大可见元素加载完成的时间
FP(First Paint)首次绘制,浏览器首次渲染页面像素的时间
DOM ReadyDOM解析完成,页面HTML解析完成,还未加载CSS/JS
Load(onLoad)页面完全加载,所有资源都加载完成

交互体验指标

指标说明
TTI(Time to Interactive)可交互时间,页面加载完成,JS执行完毕,用户可以顺畅交互
FID(First Input Delay)首次输入延迟,用户点击按钮或输入的响应时间
TBT(Total Blocking Time)总阻塞时间,JS阻塞主线程时间,影响用户交互流畅度
CLS(Cumulative Layout Shift)累计布局偏移,测量页面元素的视觉稳定性,防止跳动

48.XSS攻击

XSS跨站脚本攻击,是一种前端攻击手段,攻击者在网页中注入恶意JavaScript代码,从而对访问该网页的用户执行恶意操作,如窃取Cookie、会话劫持、伪造请求等。

XSS本质:攻击者利用网站存在的漏洞,让用户的浏览器执行恶意的JavaScript代码

类型触发方式危害解决方案
反射型XSS恶意代码存在URL参数窃取用户数据输入转义、HTTP头CSP
存储型XSS恶意代码存入数据库持续感染访问者过滤输入、转义输出
DOM型XSSJS解析,location.hash执行操作用户的浏览器避免innerHTML,用textContent

49.CSRF攻击

CSRF跨站请求伪造是一种前端安全攻击 ,攻击者诱导受害者在已登录状态下执行恶意请求,导致用户在不知情的情况下执行攻击者指定的操作。

CSRF 的本质是:攻击者利用用户的身份认证信息(如 Cookie)发起恶意请求,执行用户本不想执行的操作。

CSRF典型攻击流程:

  1. 用户登录受害网站(如银行网站 bank.com),浏览器保存了登录状态(Session 或 Cookie)。

  2. 用户未退出受害网站,继续浏览其他网站。

  3. 攻击者在恶意网站或钓鱼邮件中嵌入恶意请求

    <img src="https://bank.com/transfer?amount=1000&to=attacker_account">
    
  4. 用户访问恶意页面,浏览器自动发送请求到 bank.com: 由于用户已登录,浏览器会自动带上 bank.comCookie,导致请求被银行服务器接受并执行。

  5. 攻击成功,用户的钱被转走。

防御CSRF请求:

  1. 使用CSRF Token 服务器随机生成一串字符串,每次请求时附带,避免攻击者伪造请求

  2. Same Site Cookie

    限制Cookie仅在同源请求中发送

  3. Referer头校验 服务器检查请求的Referer头

  4. Origin头检验

    比Referer头检验更可靠

50.Diff算法

Diff算法是一种比较两棵树(例如虚拟DOM)之间差异并进行高效更新的算法,React中Diff算法的核心作用是:

  1. 计算新旧虚拟DOM之间的差异
  2. 最小化真实DOM的更新次数
  3. 高效计算出需要进行更新的节点,执行DOM操作

React中Diff算法策略:

  • 同级比较:只比较相同层级的节点
  • 不同类型节点直接删除重建
  • 使用key进行高效列表对比