面试题

0 阅读25分钟

CommonJS 与 ESModule 的区别

commonjs
导入: require()
导出: module.exports/exports.*
加载时机: 同步加载
可以放入代码中任何位置,路径可以是动态表达
顶层的tihs指向这个模块本身
可以直接导入json
导入的模块是导出对象的一个副本(值拷贝),导入方可修改引用类型的属性的值

esm
导入:import
导出:export/export default
加载时机:编译时静态分析,支持异步加载
位置必须是模块顶层,路径必须是静态字符串
导入的是原始导出标识符的只读实时绑定,导入方不能修改(值引用)
顶层this指向undefined
不可以直接导入json
支持tree shaking,commonjs是运行时加载,所以编译时不知道用没用

不会冒泡的事件

focus blur load upload stop scroll

mouseEnter和mouseOver区别

mouseEnter:
 1.不触发事件冒泡
 2.鼠标从元素外部移动到元素内部时触发,鼠标在元素内部的子元素之间移动不会触发
 3.执行一次性操作,如信息提示
mouseOver:
 1.会触发事件冒泡
 2.鼠标从元素外部移动到元素内部时触发,鼠标在元素内部的子元素之间移动会触发
 3.拖拽,悬停

MessageChannel

MessageChannelJavaScript 中的一个 API,用于创建 双向通信通道 ,主要用于在不同执行上下文之间安全地传递消息
主线程与 Web Worker 之间
不同 iframe 之间
同一页面的不同组件之间
window.postMessage是单端口通信,需要指定目标窗口
MessageChannel是双端口通信,更适合需要双向通信的场景

迭代器

JavaScript中,迭代器(Iterator)是一个带有next()方法的对象。
通过在对象上实现next()方法,可以实现对集合中的每个元素进行迭代。
next()方法返回一个包含两个属性的对象:
value表示当前迭代的值,done表示迭代是否已经结束

const bears = ['ice', 'panda', 'grizzly']
function createArrIterator(arr) {
    let index = 0
    return {
        next(){
            if(index < arr.length) {
                return { done: false, value: arr[index++]}
            }
            return { done:true, value: undefined }
        }
    }
}
let iter = createArrIterator(bears)
console.log(iter.next()) // { done: false, value: 'ice' }
console.log(iter.next()) // { done: false, value: 'panda' }
console.log(iter.next()) // { done: false, value: 'grizzly' }
console.log(iter.next()) // { done: true, value: undefined }

可迭代对象: 符合可迭代对象协议,实现了Symbol.iterator为key的方法
原生可以迭代对象:StringArrayMapSetNodeListArguements
const bears = ['ice', 'panda', 'grizzly'] //数组的Symbol.iterator方法 
const iter = bears[Symbol.iterator]() 
console.log(iter.next()) 
console.log(iter.next()) 
console.log(iter.next()) 
console.log(iter.next())

可迭代对象的实现
let info = {
    bears: ['ice', 'panda', 'grizzly'],
    [Symbol.iterator]:function (){
        let index = 0
        let _iterator = {
            next:()=>{
                if(index < this.bears.length) {
                    return { done: false, value: this.bears[index++] }
                }
                return { done: true, value: undefined }
            }
        }
        return _iterator
    }
}
let iter = info[Symbol.iterator]()
console.log(iter.next())

生成器

生成器函数后面加*,通过yield来控制函数的执行
生成器函数返回一个生成器,生成器是一个特殊的迭代器
function* bar() {
    console.log('fn run start')
    yield 100
    console.log('fn run...') 
    yield 200 
    console.log('fn run end') 
    return 300
}
const gnerator = bar()
console.log(generator.next()) // fn run start {done:false, value: 100}
console.log(generator.next()) // fn run... {done:false, value: 100}
console.log(generator.next()) // fn run end {done:true, value: 300}

await-async原理
function execuGene(generatorFn) {
    const gene = generatorFn()
    function recurse(url){
        const result = gene.next(url)
        if(!result.done){
            result.value.then((res)=> {
                recurse(res)
            })
        }
    }
    recurse()
}

function* generator() {
    const result1 = yield request('aaa')
    const result2 = yield request(result1 + 'bbb')
    const result3 = yield request(result2 + 'ccc')
}

Object.defineProperty和Proxy

function defineProperty(obj,key,val) {
    if(typeof val === 'object') {
        obvserver(val)
    }
    Object.defineProperty(obj,key,{
        get(){
            dep.depend()
            return val
        }
      
        set(newVal) {
            if(typeof newval === 'object' && newval !== null){
                obvserver(newval)
            }
            val = newVal
            dep.notify()
        }
    })
}

function observer(obj) {
    if(typeof obj !== 'object' || obj === null) {
        return
    }
    Object.keys(obj).forEach((key)=> {
        defineProperty(obj,key,obj[key])
    })
}
class Dep {
    constructor(){
        this.subs = [] // 存储watcher
    }
    depend(){
        if(Dep.target){
            this.subs.push(Dep.target)
        }
    }
    notify(){
        this.subs.forEach(watcher=> watcher.update())
    }
}
class Watcher {
    constructor(){
        this.cb = cb
        this.value = this.get() // 首次执行,触发依赖收集
    }
    get(){
        Dep.target = this
        const value = this.getter() // 触发getter 收集依赖
        Dep.target = null
        return value
    }
    update(){
        const newVlaue = this.get()
        if(newValue !== this.value){
            this.value = newValue
            this.cb(newValue)
        }
    }
}
1.初始化:observe遍历数据对象,劫持所有属性
2.依赖收集:组件初次渲染,触发数据的getter,将组件的Watcher添加到Dep3.数据更新:修改数据时,触发setter,Dep通知所有Wactcher执行更新
4.视图更新: watcher执行update,重新渲染组件

vue.js采用数据劫持,结合发布者-订阅者模式,通过Object.defineProperty()来劫持
各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图

缺陷
1.无法检测对象属性的添加或删除
2.无法监控到数组下标及数组长度的变化
3.性能问题,data中的数据较多且层级很深的时候,需要遍历data中的所有数据并设置成响应式,导致性能下降

const handler = {
  get(target, key) {
    console.log(`读取 ${key}`);
    const value = Reflect.get(target, key);
    
    // 关键:如果值是对象,递归包装成 Proxy
    if (value && typeof value === 'object') {
      return new Proxy(value, handler);  // 返回新的 Proxy
    }
    
    return value;
  },
  set(target, key, value) {
    console.log(`设置 ${key}:`, value);
    return Reflect.set(target, key, value);
  }
};

const proxy = new Proxy(obj, handler);

原型链

1.Prototype是函数的一个属性
2.是个对象
3.创建函数时,会默认添加Prototype这个属性的值

__proto__ 隐式原型
1.对象的属性
2.指向构造函数的prototype
3.obj.__proto__ === test.prototype

function Test(name){
    this.name = name
}
const obj = new Test('zs')
test.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
Test.prototype.constructor === Test
obj {
    __proto__: {
        test.prototype: {
            __proto__: Object.prototype: {
                __proto__: null
            }
        }
    }
}

instanceof:用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype

fn instanceof Object //true
fn.__proto__ === Function.prototype
fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype

arr instanceof Object // true
arr.__proto__ === Array.prototype
arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype

Object instanceof Object // true
Object.__proto__ === Function.prototype
Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype

Function instanceof Function // true
Function.__proto__ === Function.prototype

call,apply,bind

function fn(...args) {
    console.log(this,args)
}
let obj = {
   name:'zs'
}
fn.call(obj,1,2) // { name: 'zs' } [1,2]
fn.apply(obj,[1,2]) // { name: 'zs' } [1,2]
const binFn = fn.bind(obj)
binFn(1,2) // { name: 'zs' } [1,2]

三者都可以改变函数this的指向
第一个参数都是this要指向的对象,如果没有这个参数,默认指向window
第二个参数call是参数列表,apply是数组,bind可分为多次传入
bind是返回绑定this后的函数,apply,call立即执行

响应式设计原理

let price = 10, quantity = 2, total = 0
const dep = new Set()
const effect = () => { total = price * quantity }
const track = () => { dep.add(effec) }
const trigger = () => { dep.forEach(effect => effect()) }

track()
trigger() // 0
price = 20
trigger() // 40

// 完整版
const targetMap = new WeakMap()
let activeEffect = null
const effect = (eff) => {
    activeEffect = eff
    activeEffect()
    activeEffect = null
}
const track = (target,key) =>{
    if(activeEffect) {
        let depsMap = targetMap.get(target)
        if(!depsMap) {
            targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key)
        if(!dep){
           depsMap.set(key, dep = new Set())
        }
        dep.add(activeEffect)
    }
}
const trigger = (target,key) => {
    let depsMap = targetMap.get(target)
    if(!depsMap) return
    let deps = depsMap.get(key)
    if(!deps) return
    deps.forEach((effect)=> effect())
}

const reactive = (target) => {
    const handler = {
        get(target,key,receiver) {
                const result = Reflect.get(target,key,receiver)
                track(target,key)
                if(typoof result === 'object' && result !== null){
                    return reactive(result)
                }
                return result
        }
        set(target,key,value,receiver){
            const oldValue = target[key]
            const result = Reflect.set(target,key,value,receiver)
            if(oldValue!==result){
                trigger(target,key)
            }
            return result
        }
    }
    return new Proxy(target,handler)
}
let product = reactive({price: 10, quantity: 0})
let total = 0, salePrice = 0
effect(()=> { total = product.price * product.quantity })
effect(()=> { salePrice = product.price * 2 })

Map WeakMap Set WeakSet

Map
1.任意类型的键,可以是对象,函数,原始类型
2.保持插入顺序:遍历顺序与插入顺序一致
3.可迭代,支持for of 循环
4.键值对数量可以通过size获取

WeakMap
1.弱引用键:不能阻止垃圾回收  避免内存泄漏 (当对象不再被引用时,WeakMap 中的对应条目会被自动清理)
2.不可迭代
3.无size属性
4.只能使用对象作为键

Set
1.集合中的值不重复 去重[...new Set(array)]
2.保持插入顺序
3.可迭代
4.值数量可以通过size获取

WeakSet
1.弱引用键:且不能阻止垃圾回收
2.不可迭代
3.无size属性
4.只能使用对象作为键

script位置

script放在head部分
优点:在渲染页面之前先下载head中的脚本文件,保证页面在加载过程中可以随时调用
适合需要在页面一加载就运行的脚本
缺点:阻塞渲染,渲染script标签的时候会暂停HTML的解析和渲染,直到脚本下载完毕,导致页面渲染加载变慢,页面白屏时间延长

script放在body部分
优点:非阻塞渲染,优先下载和渲染HTML内容,更好的用户体验
缺点:脚本延时执行,影响功能

defer:异步下载脚本,在页面解析完成后在执行脚本
async:异步加载脚本,脚本下载完立即执行,不考虑页面的解析进度,适合独立不依赖于其他脚本的脚本

进程和线程

进程:操作系统进行资源分配和调度的基本单位,是一个独立运行的程序实列
线程:是进程的执行单元,cpu调度的基本单位,一个进程可以包含多个线程
一个进程可以包含多个线程,所有线程共享进程的资源
线程是进程的子集,进程是线程的容器
进程是资源分配的单位,线程是执行的单位

闭包

函数可以访问其外部作用域的变量,即使该函数的外部作用域已经执行完成的情况下
核心:即使外包函数执行完毕,它的内部函数仍可以通过词法作用域来访问外部函数中的变量
function parent() { 
    let n = 1 
    function son() { // 词法作用域: 可以访问到n 
        console.log(n);
    } 
    return son // 函数是一等公民: 可以被返回 
} 
let son = parent() 
son()// 函数是一等公民: 返回的函数可以执行。

防抖
function debounce(fn,delay){
    let timer = null
    return function(...args){
        if(timer) {
                clearTimeout(timer)
        }
        timer = setTimeOut(()=> {
            fn.apply(this,args)
        },delay)
    }
}

立即执行
function debounce(fn,dealy){
    let timer = null
    return function(){
        const callnow = !timer
        if(timer){
            clearTimeout(timer)
        }
        timer = setTimeout(()=>{
            timer = null
        },dealy)
        if(callnow){
            fn.apply(this,args)
        }
    }
}

节流
function throttle(fn,delay){
    let lastTime = 0
    return function(...args){
        const now = Date.now()
        if(now - lastTime > delay){
            lastTime = now
            fn.apply(this,args)
        }
    }
}

从输入网页到页面显示的过程

1.DNS解析
将用户输入的域名转换为服务器的IP地址
浏览器先检查本地DNS缓存(浏览器缓存,操作系统缓存)
若未命中,向本地DNS服务器请求,先在本地域名服务器查找,没有去根域名服务器查找,
没有去顶级域名服务器查找,直到找到对应的ip地址

2.建立TCP连接
通过三次握手建立客户端与服务端直接的连接

3.发送HTTP请求
向服务器发送请求,获取页面资源
服务器接收http请求,解析请求参数路径等,执行业务逻辑(查询数据库,调用api等)
生成html,css,js等响应内容

4.服务器处理请求,返回HTTP响应

5.浏览器接收响应解析并渲染
  解析HTML:构建DOM树
  解析CSS: 构建CSSOM树
  合并并渲染树
  布局绘制
6.执行js代码,处理页面交互逻辑,动态修改DOM

首屏加载速度慢怎么解决

1.减少入口文件的体积
  路由懒加载:把不同路由对应的组件分割成不同的代码块,待路由请求时会单独打包路由,使得入口文件变小,加载速度增加
2.使用tree shaking,移除未使用的代码 vite和webpack生产默认开启
3.第三方组件库按需引入
4.图片资源的压缩  
    使用svg图标,雪碧图
    图片懒加载 <img loading="lazy" src="..." alt="">
    字体,图片放到oss上
5.合理配置打包
   minChunks:3 假设某个文件是一个常用的库,多个路由中使用了,这就造成了
   重复下,表示把使用3次以上的包抽离出来,放进公共依赖,避免重复加载组件
   splitChunk:提取公共代码
6.开启gzip压缩,同时也需要服务端配置支持gzip静态文件的返回
  compression-webpack-plugin
7.生产环境禁用source-map
8.使用SSR服务端渲染
9.代码压缩 使用terser压缩js post-css压缩css

http websocket sse

HTTP:超文本传输协议,是web应用的基础,基于请求响应模式,无状态
WebSocket:全双工通信协议,通过一次握手建立长连接,支持双向实时通信
SSE:基于http长连接,支持服务器向客户端单向推动

连接方式:
http:每次请求都需要建立新的连接,无状态,服务器不保存客户端的状态,每次请求需要重新认证
websocket:一次握手后保持连接,双向通信,有状态,服务端可识别客户端,维持会话状态
sse:客户端发起一次连接后保持,服务端向客户端单向推送

数据格式与传输
HTTP: 支持多种格式JSON,XML,表单数据等
websocket: 支持二进制(图片,视频)和文本数据,数据以帧(frame)形式传输,开销小
sse:仅支持文本数据,数据以事件流(eventStream)形式传输

场景:
http:普通网页请求,适合交互短,非实时场景
websocket:聊天,游戏,协调编辑,需要客户端和服务端频繁交互的场景
sse:实时监控,股票,新闻等

vite和webpack的区别

Vite:
开发环境:基于浏览器原生的esmodule支持,省略费时的编译环节,只需要加载当前模块即可
生产环境:使用Rollup进行打包,利用tree shaking和代码分割能力
核心思想:开发时避免打包,生产优化打包
热更新:只更新修改的模块,不影响其他模块,响应时间快
默认配置简洁:配置了常用的功能,开箱即用,内置了ts,css预处理器,无需额外配
使用esbulid处理项目依赖,底层是go编写,比node.js编写的编译器快
Webpack:
开发和生产均基于打包模式,将所有模块打包成一个或多个文件
核心思想:通过loader处理各种类型的文件,通过plugin扩展功能
热更新:修改文件后,需要重新打包相关模块,响应时间长
拥有庞大的loader和plugin生态,几乎支持所有前端场景

CDN

内容分发网络,由分布在不同区域的边缘节点服务器群组成的分布式网络
CDN加速的本质是缓存加速,将服务器上存储的静态内容缓存在CDN节点上,当访问这些静态资源的时候,
无需访问服务器原站,就近访问CDN节点即可获取相同的内容,从而达到加速的效果减轻服务器的压力

虚拟dom

虚拟dom是一种用js对象来描述真实DOM的结构,属性和子节点
1.减少操作真实DOM的次数,每次修改真实DOM都会触发重排和重绘,造成性能问题
2.跨平台能力,虚拟dom是和平台无关的js对象,可以渲染到不同的平台

key在diff算法中的作用
key给每个节点唯一标识,diff算法通过key来判断节点是否新增,删除还是移动,而不是简单的按位置对比
找到key相同的节点,复用已有的dom,只更新变化的内容,避免不必要的创建和销毁,提升性能
可以帮助vue更精确的判断哪些部分需要更新,提高组件的复用性和渲染效率

为什么不能使用index作为key
当列表中的组件或元素重新排序,添加或删除时,其对应的index也会发生变化,这会导致key不稳定,可能引发一些错误的更新和重新渲染

<li v-for="(item, index) in list" :key="index">
  {{ item.name }}
</li>
列表: [A, B, C]
key:  [0, 1, 2]

删除 A 后: [B, C]
key:       [0, 1]  ← B 的 key 从 1 变成 0,C 的 key 从 2 变成 1

结果: 框架认为所有节点都变了,全部重新渲染!虽然没有创建新节点,但每个节点的内容都被更新了

<div v-for="(item, index) in list" :key="index">
  <input v-model="item.text" />
  <button @click="deleteItem(index)">删除</button>
</div>
删除前:
index=0: key=0, 数据={id:1, text:'第一条'}, 输入框内容="第一条"
index=1: key=1, 数据={id:2, text:'第二条'}, 输入框内容="第二条"
index=2: key=2, 数据={id:3, text:'第三条'}, 输入框内容="第三条"
删除后:
index=0: key=0, 数据={id:2, text:'第二条'}  ← 复用了原来的 key=0
index=1: key=1, 数据={id:3, text:'第三条'}  ← 复用了原来的 key=1
- 输入框显示的还是"第一条"(因为 DOM 复用了)
- 但数据已经是"第二条"
- 数据和视图不一致!

使用随机字符串作为key
不稳定性:虽然可以保证key的唯一性,但重新渲染时,生成的随机字符串也会发生变化,使得key变得不稳定,每次更新时,vue都会认为组件和元素不同,导致不必要的重新渲染和更新

diff算法
数据改变->触发setter->Dep.notify->通知订阅者->patch(oldNode,newNode)->是否同类标签
不是同类标签 直接替换
是同类标签 patchVnode
oldVnode和newVnode是否相等  相等直接return
不相等 分情况
oldVnode和newVnode都有文本节点 --> 用新的文本替换旧的文本
oldVnode没有子节的,newVnode的有子节点 --> 增加新的子节点
oldVnode有子节的,newVnode的没有子节点 --> 删除旧的子节点
oldVnode和newVnode都有子节点 --> updateChildren
收尾指针法  头头 尾尾 头尾 尾头

vue2和3的区别

1.响应式系统的升级
vue2使用Object.definePoroperty劫持对象的属性,只能监听属性的读写,无法直接监听新增和删除或数组索引的变化,需要通过Vue.setVue.delete或重写数组方法来解决
vue3使用proxy来代理整个对象,原生支持监听新增,删除属性,数组索引的变化,以及map,set等复杂的数据结构

2.API风格演进
vue2使用Options API,按data,methods,computed等选项组织代码,逻辑分散在不同的选项中,复用逻辑依赖mixins
vue3使用Composition API,按照功能逻辑组织代码,通过setup函数和组合式函数实现更灵活的逻辑复用,避免了mixins问题

3.新特性
    多根节点,Vue3支持多根节点,无需外层包裹div
    Teleport:可将dom元素传递到任意位置,适用于弹窗,模态框等
    Suspense:优雅的处理异步组件和数据加载的状态

4.生命周期

继承的方式

1.原型链继承 修改子类prototype指向父类的实例
  缺点:通过实例改变对象的某个key值,所有的实例相应的值都发生变化
function Parent(){
    this.name = 'parent'
    this.colors = ['red', 'blue'];
}
Parent.prototype.sayHello = function() {
  console.log('Hello from Parent');
};
function Child = {}
Child.prototype = new Parent()
Child.prototype.constructor = Child; 

const child = new Child();
console.log(child.name); // 'Parent'
child.sayHello(); // 'Hello from Parent'

child1.colors.push(111);
child2.colors.push(222);

console.log(child1.colors); // ['red', 'blue', 111, 222]
console.log(child2.colors); // ['red', 'blue', 111, 222]
当执行 Child.prototype = new Parent() 时, Child.prototype 指向了一个 父类实例 (我们称之为 parentInstance )
这个 parentInstance 有自己的 colors 属性( ['red', 'blue'] )
当创建 child1 和 child2 时,它们的原型链都指向同一个 parentInstance

2.构造函数继承  Parent.call 将父级构造函数的this指向当前构造函数的this
  只能继承父级实列上的属性和方法,不能继承父级原型对象prototype上的属性和方法
// 父类
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.sayHello = function() {
  console.log('Hello from Parent');
};

function Child(name.age){
    Parent.call(this,name)
    this.age = age
}
// 使用
const child = new Child('Child', 10);
console.log(child.name); // 'Child'
console.log(child.age); // 10
console.log(child.colors); // ['red', 'blue']
// child.sayHello(); // 报错:sayHello is not a function

3.组合继承 重写prototype + Parent.call(this)
继继承了父类实列上的属性,又继承了父类原型上的方法
缺点:Parent会调用两次
function Parent(name){
    this.name = name
}
function Child(name,age){
    Parent.call(this)
    this.age = age
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const child = new Child('Child', 10);

4.原型式继承 Object.create(Parent)
原型指向同一个对象,通过某个实例改变的原型上的 `key` 会导致所有实例读取到的值都是被修改后的 `key` 值
const parent = {
    name:'Parent'colors: ['red', 'blue']
}
const child1 = Object.create(parent);
const child2 = Object.create(parent);
child1.colors.push(111)
child2.colors.push(222)
console.log(child1.colors) // ['red', 'blue', 111, 222]

5.寄生式继承
跟原型式继承一样一样的,就是多加了一些额外的属性和方法。缺点也一样。
function createChild(parent) {
  // 1. 原型式继承
  const child = Object.create(parent);
  
  // 2. 增强对象
  child.sayAge = function() {
    console.log('Age: 10');
  };
  
  return child;
}

// 父对象
const parent = {
  name: 'Parent',
  sayHello() {
    console.log('Hello from Parent');
  }
};

// 使用
const child = createChild(parent);

6.寄生组合式继承 Object.create(Parent)+Parent.call(this)
重写构造函数的原型为父级构造函数的实列对象,同时在构造函数内部将父级构造函数指向当前构造函数
Parent只会调用一次
function Parent(){
    this.name = 'parent'
}
function Child(){
    Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child;

7.extends继承
class Parent {
    constructor(money){
        this.money = money
    }
    getMoney(){
        return this.money
    }
}
class Child extends Parent {
    constructor(money,name){
        super(money)
        this.name = name
    }
}
const child = new Child(1000,'zs')
child.getMoney()

Promise

1.如果resolve传入一个普通的值或对象,这个值作为then的回调参数
let p1 = new Promise((resolve, reject) => { 
    resolve('情况一:普通值或对象') 
}) 
p1.then((res) => { console.log(res) }) //情况一:普通值或对象

2.如果resolve中传入的是另外一个promise,那么这个新的promise会决定原promise的状态,并且新promise的resolve或reject的参数也会传给原来的promise

const p = new Promise((resolve,reject)=> {
    setTimeout(()=>{
        resolve(111)
    },1000)
})
const p1 = new Promise((resolve,reject)=> {
    resolve(p)
})
p1.then((res)=> {
    console.log(res) //1s后打印111
})

3.resolve传入的是一个对象,改对象有then方法,那么会执行then方法,并且根据then方法的结果来决定Promise的状态
const p = { 
    name: 'javascript', 
    then: function (resolve) { 
        resolve("情况三") 
    } 
} 
const p1 = new Promise((resolve, reject) => { resolve(p) }) p1.then((res) => { 
    console.log("res:" + res)  // 情况三
}).catch((err) => { 
    console.log("err:" + err) 
})

then方法本身是有返回值的,他的返回值是一个promise,所以可以对其进行链式调用
1.返回一个普通值,这个值作为resolve的参数,在后面的then方法里回调函数获取到的参数是a
const p1 = new Promise((resolve,reject)=>{
    resolve('成功的回调')
})
p1.then((res)=> {
    return 'aaa'
}).then((res1)=>{
    console.log(res1) //aaa
    return 'bbb'
}).then((res2)=>{
    console.log(res2) // bbb
})

2.返回一个Promise,如果返回一个PromiseA,那么then返回的PromiseB的状态会由PromiseA的状态决定,
并且将PromiseA的状态的回调函数的参数作为PromiseB的状态回调函数的参数
const p = new Promise((resolve, reject) => { 
    setTimeout(() => { 
        resolve('ccccc') 
    }, 1000) 
}) 
const p1 = new Promise((resolve, reject) => { 
    resolve("成功的回调") 
}) 
p1.then((res) => { 
    return p 
}).then((res1) => { 
    console.log("res1:" + res1)  //cccc
    return 'ddddd' 
}).then((res2) => { 
    console.log("res2:" + res2)  // dddd
})

3.返回一个thenable对象,如果then方法里面的回调函数返回了一个带有then方法的对象,那么then方法返回的PromiseA的状态由then方法里的结构决定
const promise = new Promise((resolve,reject)=> {
    resolve('aa')
})
promise.then(res=>{
    return{
        then:function(resolve,reject){
            resolve('thenable')
        }
    }
}).then(res=>{
    console.log("thenable test:", res) // thenable test: thenable
})

get和post区别

参数位置: url中   http请求体中
数据类型: 字符串类型   application/json (JSON 格式) pplication/x-www-form-urlencoded (表单格式)- multipart/form-data (文件上传) text/plain (纯文本)
大小限制: URL长度限制(2-8kb)  大小无固定限制
安全性  参数暴露在url中,不安全    参数在请求体中,相对安全

状态码

1xx:接收请求正在处理
100:客户继续发送请求,这是临时响应
101:服务器根据客户端的请求切换协议,主要用websocket或http2的升级
200:请求被正确处理并返回了结果
201:请求成功并且服务器创建了新的资源
202:服务器已经接收请求,但尚未处理
203:服务器已成功请求处理,但为非授权信息
204:服务器成功处理请求,但没有返回任何内容
205:服务器成功处理请求,但没有返回任何内容,要求请求方重置内容
206:服务器成功处理了部分请求
301:永久重定向,表示请求资源已分配新的url
302:临时重定向:表示请求的资源临时分配了新的url
303:表示请求的资源还存在另一个url,应该用get方法定向获取请求的资源
304:表示未修改,客户的缓存资源是最新的,要客户端使用缓存
305:请求者只能使用代理访问资源
400:错误请求,服务器不理解请求的语法
401:未授权,请求要求身份验证
403:服务器拒绝请求
404:服务器找不到请求的网页
405:禁用请求中指定的方法
406:服务器不接受请求
408:服务器请求时发生超时
500:服务器内部错误
501:服务器不支持完成请求的功能,无法完成请求
502:服务器作为网关或代理,从上游服务器收到无效响应,网关错误
503:服务器目前无法使用,过载或维护中
504:网关超时
505:htpp版本不受支持

BFC

块级格式化上下文,它是一个独立的渲染区域,内部的元素布局不受外部的影响,同时也不影响外部的元素
1.内部的盒子会在垂直方向一个个放置
2.同一个BFC相邻的box上下会发生重叠
3.BFC会计算浮动元素的高度,解决父元素高度坍塌的问题

触发
根元素 <html>
浮动元素 float:left/right
绝对定位/固定定位:position:absolute/fixed
行内元素:display:inline-block
表格单元:display:table-cell
表格标题:display:table-caption
flex/grid:display:flex/grid
overflow:auto/hidden/scroll

浏览器缓存机制

缓存可以减少用户等待时间,提升用户体验
减少网络带宽的消耗
减少服务器的压力
用户请求->离线缓存->内存缓存->硬盘缓存->强缓存->协商缓存->发起网络请求->更新缓存
强缓存:直接使用缓存,不请求服务器
Epires:缓存过期时间 绝对时间
Cache-Control:
    max-age:缓存最大时间 
    public:响应可以被任何对象缓存
    private:响应只能被客户端缓存
    no-cache:跳过强缓存,进入协商缓存
    no-store:不存储缓存
协商缓存:向服务器验证缓存是否有效
    Last-Modified:资源最后修改时间  向服务器验证缓存是否有效,请求验证返回304200
    Etag:资源的唯一标识

函数柯里化

将一个接收多个参数的函数转换为一系列接收单个参数的函数的过程,如将f(a,b,c)转换为f(a)(b)(c)

function curry(fn){
    const arity = fn.length //原函数的参数个数
    return curried(...args){
        if(args.length>fn.length){
            fn.call(this,...args)
        }else {
            return function(...moreArgs){
             return curried.apply(this,[...args,...moreArgs])
            }
        }
    }
}

事件循环

宏任务:UI交互事件,I/O,setTimeoutsetInterval,setImmediate,requestAnimationFrame
微任务:nextTick,MutationObserverPromise

nextTick
本质是将回调函数放入异步任务队列中,等同步代码执行完毕,dom更新后再执行nextTick中的回调函数

webpack打包运行原理

1.读取webpack的配置参数
2.启动webpack,创建compiler对象并开始解析项目
3.从入口文件开始解析,并找到其导入的依赖模块,开始递归遍历解析,形成依赖关系树
4.对不同文件类型的依赖模块文件使用对应的loader进行编译,转换成最终的js文件
5.整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,webpack的插件可以监听这些关键的事件节点,执行插件任务而达到干预输出结构的目的

优化webpack的打包速度
1.缩小文件搜索范围
 resolve: {
    modules: [path.resolve(__dirname, 'node_modules')], // 指定 node_modules 位置
    extensions: ['.js', '.jsx', '.json'], // 减少后缀尝试
    alias: {
      '@': path.resolve(__dirname, 'src'), // 别名,减少路径解析
      'components': path.resolve(__dirname, 'src/components')
    },
    mainFields: ['jsnext:main', 'browser', 'main', 'module'] // 优先使用 ES Module 版本
 },
 
2.配置loader时缩小范围
module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'), // 只处理 src 目录
        exclude: /node_modules/, // 排除 node_modules
        use: 'babel-loader'
      }
    ]
}

3.使用thread-loader开启多线程并行处理
4.使用babel-loader和cache-loader缓存
{
  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    options: {
      cacheDirectory: true, // 开启缓存
      cacheCompression: false // 不压缩缓存,提升速度
    }
  }
}
{
  test: /\.js$/,
  use: [
    'cache-loader', // 缓存 loader 执行结果
    'babel-loader'
  ]
}
5.区分开发生产环境
开发:关闭代码压缩 source-map:cheap-module-eval-source-map 开启热更新
生产:开启代码优化 splitChunks devtools:source-map
6.使用externals+cdn,减少第三方库的引入
7.按需加载 treeshaki  ng

图片懒加载

最简单的实现方式是给 `img` 标签加上 `loading="lazy"`
把图片的地址放入data-src属性里,然后监听图片是否进入可是区域内,把data-src赋值给src

如何实现首页内容的分模块异步加载

代码分割:使用import或defineAsyncComponent将非关键模块拆分成独立的chunk
异步加载:首屏加载关键模块,非关键模块通过滚动触发或延迟加载
使用IntersectionObserver监听滚动,进入可视区域加载
加载状态管理:使用suspense展示加载状态

双token

双token是一种用于身份认证和授权的安全机制,通过访问令牌(Access-token)和刷新令牌(Refresh-token)组成,目的是在保证安全性的同时提升用户体验,避免频繁登录
流程:用户输入密码登录,服务器验证后返回access-token和refersh-token,客户端使用访问
令牌请求受保护的api,token过期,返回401错误,客户端使用刷新令牌向服务端请求访问令牌,
服务端验证token后更新access-token

HTTP/1和HTTP/2的区别

1.多路复用:
http1同一连接上的请求必须是串行发送,导致队列阻塞
http2将一个连接分为多个数据流,每一个数据流对应一个请求/响应,可并行发送,无队头阻塞

2.头部压缩:
http1的头部以文本形式传输,包含大量重复字段,每次请求都会重复发送
http2使用算法压缩头部,减少传输量

3.数据传输单位:
http1的传输单位是报文(request/response)
http2的传输单位是帧(frame),将文件拆分成多个帧更灵活

4.服务器推送
http1无法主动推送
http2服务器可主动推送相关资源(css,js)减少客户端请求次数,提升首屏加载速度

TCP和UDP的区别

tcp需要三次握手建立连接,四次挥手释放连接,确保双方通信的状态
udp无需连接,直接发送数据,发送方和接收方无状态连接
tpc传输的是字节流,数据无边界
udp传输的数据报,每个数据报独立,有明确的边界
tcp适合数据可靠性要求高的场景,如大文件传输,保证数据的完整性
udp适合实时性要求高的场景,视频通话,语音通话等(低延时,丢包不重传,开销小)