HTML和css
盒模型:
了解标准盒模型与怪异盒模型(IE)之间的区别 由border、content、padding、margin几部分组成
- 标准盒模型: box-sizing : content-box width:content
- IE盒模型:box-sizing : border-box; width : content+padding+border 由此可以看出IE盒模型更加直观,IE的元素总的宽度只需要计算(width+margin),而标准的则需要计算全部相加
Flex/Grid布局
-
display:flex(一维布局):通过给父盒子设定属性使得子盒子沿x轴或y轴排列,常用于水平垂直居中
-
flex-direction:row(默认x轴)|row-reverse(从右到左)|column(y轴纵向排列)|column-reverse(从后往前排)
-
justify-content: flex-start | flex-end | center(居中紧挨着填充) | space-between(平均分布在该行) | space-around(平均分布在该行上,两边留有一半的间隔空间)
-
align-items: flex-start | flex-end | center | baseline(与基线对齐) | stretch(拉满一列)
-
flex-wrap:wrap(溢出换行)|nowrap(溢出不换行)|wrap-reverse
flex: 1 是什么意思?“它是 flex-grow: 1、flex-shrink: 1 和 flex-basis: 0% 的简写。意思是这个项目的初始尺寸为0,它会根据剩余空间按1的比例放大,也会在空间不足时按1的比例收缩。”
在使用 Flexbox(弹性布局)时,flex-direction 属性决定了主轴的方向,而 align-items 和 justify-content 属性分别用于在交叉轴和主轴上对齐项目。
display:grid(二维布局):网格布局
详细可以看别人的文章Grid 布局:从入门到精通,一篇就够了—— 探索最强大的 CSS 布局系统_grid布局-CSDN博客
grid-template-columns: repeat(auto-fill, 100px):自动填充,列宽100px |
grid-template-rows:100px 1fr 3fr: 100px,后面两个盒子按1:3的比例排列
grid-row-gap属性设置行与行的间隔(行间距),grid-column-gap属性设置列与列的间隔(列间距)
grid-column: start-line / end-line; (开始的列,结束的列)
grid-row: start-line / end-line;
grid-area: row-start / column-start / row-end / column-end
响应式设计&移动端适配
-
响应式设计的核心就是通过流体网络(使用%、fr、vw等代替px)、弹性媒体以及媒体查询(media())来实现网页适配不同大小的屏幕。
-
移动端适配主要是让网页能够在手机与平板等移动端也能正常显示。首先得在HTML中设置viewport元标签,然后使用vw、vh、rem等
BFC
(块级格式化上下文),简单说就是页面上的一个独立渲染的区域,这个区域内的布局与区域外的布局互相之间不会有影响。
可以触发BFC的方法:行内块元素、浮动元素、表格标题、表格单元格、绝对定位元素、overflow: hidden | scroll | auto、弹性元素、网格元素、display:flow-root(无副作用)
BFC的三大运用场景:
1.清除内部浮动,防止父元素高度坍塌:当父元素的子元素浮动时,父元素的高度会坍塌为0,BFC计算高度时,浮动元素也参与计算
2.解决外边距合并:相邻两个块级元素会合并外边距,而不是相加,BFC的相邻两个块级元素外边距相加
3.实现自适应布局:浮动元素会覆盖旁边的标准流元素,BFC区域不会与浮动元素重叠,实现两栏布局。
选择器优先级
!Important>内联样式>id选择器>类选择器>伪类选择器>属性选择器(input[type="text"])>标签选择器>通配符选择器。
css伪类和css伪元素:css伪类是描述某一元素某一个特定状态(:hover、:active、:first-child、:visited)下的样式,作用于整一个元素;
css伪元素是样式化某一部分或插入额外内容(::first-line、::first-letter、::befor、::after)
javaScript
es6+新特性
- let和const:let声明块级作用域的变量,可重新赋值;const声明常量,不可重新赋值,且两者都不允许重新声明;而var可以重新声明。
let/const 和 var 的核心差异就 3 点: var 无块域,可改可重; let 有块域,可改不重; const 有块域,不改不重;
- 箭头函数:没有自己的this和arguments,继承外层函数的上下文。
const person = {
name: 'pupu',
sex: "男",
hobby: ['sing', 'play'],
sayHi: function () { //普通函数
console.log(`Hi ${this.name}`) //作为对象的方法被调用,this 指向调用该方法的对象。输出Hi pupu
this.hobby.forEach(function () {
console.log(this.name); //错误,this作为forEach普通函数自己内部调用,指向全局作用域
})
this.hobby.forEach(()=>{
console.log(this.name);//箭头函数的this指向外层的上下文,输出 pupu
})
},
sayHello: (()=>{ //箭头函数
console.log('Hi' + this.name) // 报错,箭头函数作为对象方法时,不会绑定到该对象,指向全局作用域
this.hobby.forEach(function () {
console.log(this.name); //报错
})
}),
}
// 调用方法
person.sayHi();
箭头函数与普通函数的区别: 1.绑定 this:箭头函数 “跟着外层走”,普通函数 “谁调用算谁的”;2.不能当构造函数:箭头函数 “不能 new”,普通函数 “可以 new”;3.普通函数有 arguments,箭头函数没有,且箭头函数写法更简洁。(
arguments:普通函数的 “隐藏参数列表”(类数组),能拿所有实参,但可读性差;箭头函数没有arguments,用...args(剩余参数)替代,拿到的是真正的数组,用法更简洁。)
- 模板字符串:支持多行字符串、直接字符串直接插值
const name = 'Sarah';
const age = 30;
// 旧方式
const greeting = 'Hello, my name is ' + name + ' and I am ' + age + ' years old.';
// 新方式
const greeting = `Hello, my name is ${name} and I am ${age} years old.`;
// 多行字符串
const message = `
This is a
multi-line
string.
`;
-解构
原型与原型链
- 作用域 : 函数和变量可被访问的范围;有函数作用域、块级作用域和函数作用域(意思看字面即可理解)。
- 作用域链 : ‘变量查找的作用域路径链’,就是在当前作用域找不到,会逐级往外查找而形成的查找链。
- new操作:
1.自动生成一个空对象{ },作为要返回的实例。
2.绑定原型:空对象的原型(proto)指向构造函数Person的prototype,让new实例可以访问到构造函数的原型上。
3.绑定this并执行构造函数:构造函数中的this指向新对象 。
4.返回实例对象 - 原型对象:存储可共享的属性和方法的载体。显示原型(prototype)和隐式原型(proto)。
原型是描述继承关系的机制。
prototype : 函数的属性,是一个用于实现子类继承父类共享的属性和方法的对象。
proto : 是实例对象的属性,指向其构造函数的prototype,形成原型链。 - 原型链:属性的查找路径,属性和方法实现继承的主要机制。主要通过_proto_向外查找。
再想详细理解的话,[可以看看Rockky的文章](图文并茂🌈聊聊原型与原型链图文并茂,循序渐进,以图解的方式描述JS核心知识原型与原型链,最终给出一张汇总图助力你掌握原 - 掘金)
ES5继承,繁琐
function Animals(name,age){
this.name=name,
this.age=age
} //基类构造函数
Animals.prototype.eat = function(){
return `${this.name} is eating.`
} //基类的原型方法
function Dog(name,age,breed){
Animals.call(this,name,age) //第一次继承基类的属性
this.breed = breed
} //子类的构造函数
//object创建空对象{},且Animals是Dog的原型,Dog.prototype 的原型指向 Animals.prototype
Dog.prototype = Object.create(Animals.prototype) //第二次继承基类的原型方法,建立原型链
Dog.prototype.constructor = Dog
const dog1 = new Dog('Buddy', 3, 'Golden Retriever')
//形成原型链:`dog1` → `Dog.prototype` → `Animals.prototype` → `Object.prototype` → `null`
ES6继承(类似于C++的继承方式,通过class和extends),super( )调用基类方法
class Parent {
constructor(name) { this.name = name; }
sayName() { console.log(this.name); }
}
class Child extends Parent {
constructor(name, age) {
super(name); // 相当于 Parent.call(this, name)
this.age = age;
}
}
- 应用场景 原型与原型链主要是有一个基础的构造函数,可以继承这个构造函数的属性和方法下添加新的属性和方法。具体的应用场景有:
- 当开发一个项目的时候,有一个基础的组件模块,包含点击、拖拽等功能;但在具体的登录模块、支付模块、文章模块等可以继承基础组件模块的功能下还能添加自己特定的功能。
- 在开发类似JQuery、vue组件或库的时候,可以通过修改原型,为所有实例都添加新的插件方法,实现一种混入Mixin模式,现在vue3常用更现代化的组合式函数(白盒)的方法,Mixin(黑盒)旨用在维护旧版的vue2项目中。
在举例代码之前先了解一下Mixin模式,通常JS的继承只能有一个父类,不能有多个父类。但是在开发项目的时候引入Mixin模式,它可以让你的一个类获取多个父类的功能。它可以分为两种。一种是混入原型中(所有实例方法都添加新的功能),另一种是混入对象中(特定的对象添加)。通过Object.assign()混入。
混入原型中
// 1. 定义一些 Mixin 对象(功能模块)
const CanSwim = {
swim() {
console.log(`${this.name} is swimming!`);
}
};
const CanFly = {
fly() {
console.log(`${this.name} is flying!`);
}
};
const CanSpeak = {
speak(sound) {
console.log(`${this.name} says: ${sound}`);
}
};
// 2. 一个基础的构造函数
function Duck(name) {
this.name = name;
}
// 3. 使用 Object.assign 将多个 Mixin 的功能“混入”到 Duck 的原型中
// 这就相当于让所有的鸭子实例都拥有了游泳、飞行、说话的能力
Object.assign(Duck.prototype, CanSwim, CanFly, CanSpeak);
// 4. 创建实例并使用所有混入的功能
const donald = new Duck('Donald');
donald.swim(); // Donald is swimming!
donald.fly(); // Donald is flying!
donald.speak('Quack quack!'); // Donald says: Quack quack!
混入单个实例(拓展特定对象)
// 一个简单的汽车对象
const myCar = {
brand: 'Toyota',
drive() {
console.log(`The ${this.brand} is driving.`);
}
};
// 一个导航 Mixin
const GPSNavigator = {
navigateTo(destination) {
console.log(`Navigating to ${destination}...`);
}
};
// 一个音响 Mixin
const PremiumSoundSystem = {
playMusic(song) {
console.log(`Now playing: ${song}`);
}
};
// 将功能混入到 myCar 这个特定对象中
Object.assign(myCar, GPSNavigator, PremiumSoundSystem);
// 现在 myCar 拥有了所有功能
myCar.drive(); // The Toyota is driving.
myCar.navigateTo('Home'); // Navigating to Home...
myCar.playMusic('My Song'); // Now playing: My Song
3.在需要为现有方法添加统一横切关注点时,比如为所有API请求方法添加日志记录,我们可以通过重写原型上的方法来实现AOP。至于AOP面向切面编程的实现方法与概念,在这里就不多讲了,自己可以去了解一下通过原型或者proxy如何实现AOP。
异步编程
首先,要知道JS是单线程的,如果遇到上传视频、定时器等耗时操作时,它会一直等待这些操作完成再执行下面的操作。异步编程就是让单线程的JS能处理耗时操作而不阻塞主线程的进行。异步编程的发展史分为四个阶段。 了解发展史之前,先了解底层的事件循环(Loop Event)。
时间循环涉及到三个栈:调用栈、微任务、宏任务。首先,代码有同步任务和异步任务,同步任务直接在调用栈中执行,异步任务被分为微任务队列(promise.then().catch().finally())和宏任务队列中(setTimeout、setInterval, setImmediate)。 执行顺序:
- 首先在调用栈执行完所有同步任务
- 异步任务分为微任务和宏任务队列中,微任务先执行完
- 执行所有宏任务(开始循环)
- 同(同步) ->微(所有微任务)->宏(一个宏任务) ->(循环开始)->微(所有微任务)->宏(一个宏任务)-> ... 以下代码辅助理解
console.log('1')
setTimeout(()=>console.log('2'),0)
setTimeout(()=>console.log('3'),1)
const p =Promise.resolve()
for(let i=0;i<3;i++){
p.then(()=>{
setTimeout(()=>{
console.log('4');
setTimeout(()=>console.log('5'),0)
p.then(()=>console.log('6'))
},0)
console.log('7');
})
}
console.log('8');
// 最终执行顺序为:1 8 7 7 7 2 3 4 6 4 6 4 6 5 5 5
- 阶段1:回调函数 回调函数就是一个函数被当作参数传给另一个函数做参数,在另一个函数内部调用。这个大家都了解,不多说了。
- 阶段2:Promise 知道三种状态:pending(进行中)、fulfilled(已完成)、rejected(已失败)
const myPromise = new Promise((resolve,reject)=>{
setTimeout(()=>{
const success = Math.random > 10
if(success){
resolve('操作成功') //状态变为fulfilled
}
else{
reject(new Error('操作失败')) //状态变为rejected
}
},1000)
})
myPromise.then((result)=>{
console.log(result); //成功时
}).catch((error)=>{
console.error(error); //失败时
}).finally(()=>{
console.log('123'); //无论成功与否
})
promise还解决了回调地狱难以阅读和维护的问题。
// 表层逻辑:清晰的链式调用
getUser(id) // 返回 Promise
.then(user => getPosts(user.id)) // 返回新的 Promise
.then(posts => getComments(posts[0].id)) // 返回新的 Promise
.then(comments => getReplies(comments[0].id)) // 返回新的 Promise
.then(replies => console.log(replies)) // 最终目标
.catch(error => console.error(error)); // 一个 catch 捕获所有错误
常用静态方法都是返回新的promise对象:
- Promise.all([p1,p2,…]) //等待所有promise都成功,返回的结果值resolve(value)是一个数组,状态变为fulfilled。如果遇到一个失败,返回reject原因,状态变rejected
- Promise.allsettled([p1,p2,...]) ///等待所有promise都完成(无论成功与否),返回所有结果
- Promise.race([p1,p2,...]) //第一个改变状态的 Promise 决定结果。
- Promise.any([p1,p2,...]) //等待第一个成功,如果全部失败则返回一个聚合错误。
// 模拟异步请求API,使用promise.all
function f1(userid){
return new Promise((resolve)=>{
setTimeout(()=>resolve({id:userid,name:'A'}),100)
})
}
function f2(userid){
return new Promise((resolve)=>{
setTimeout(()=>resolve({orderID:1,name:'B'}),300)
})
}
function f3(userid){
return new Promise((resolve)=>{
setTimeout(()=>resolve({id:"000",name:'C'}),200)
})
}
//
async function pro(userid){
try {
const [userData, orders, permissions] = await Promise.all([
f1(userid),
f2(userid),
f3(userid)
])
// 所有数据都获取成功后才执行下一步
console.log('用户数据:', userData);
console.log('订单列表:', orders);
console.log('权限:', permissions);
} catch (error) {
// 任何一个请求失败都会进入这里
console.error('加载用户数据失败:', error);
}
}
pro(111)
-
阶段3:Promise+Generator
Generator在异步操作发起时暂停函数执行(yield),在Promise有结果的时候会恢复执行(next)。传统的Promise(是异步风格的异步代码)在异步操作执行时,会去处理其他计算,Generator(同步风格的异步代码,提高了代码的可读性和可维护性。)就是要等待异步操作完成,才执行下一步。??这里有点问题不是很理解 下面代码帮助理解Generator的机制
// 模拟异步函数
function getUser(id) {
return new Promise(resolve => setTimeout(() => resolve({ id, name: 'User' + id }), 1000));
}
function getPosts(userId) {
return new Promise(resolve => setTimeout(() => resolve(['Post1', 'Post2']), 1000));
}
// 定义一个 Generator 函数来写主逻辑
function* mainGenerator(id) { //function* 类似async
try {
// yield 一个 Promise,并“等待”它的结果
const user = yield getUser(id); //yield 类似await
const posts = yield getPosts(user.id);
return { user, posts }; // 最终返回结果
} catch (error) {
console.error('Generator Error:', error);
}
}
// 2. 自动执行器 Runner //类似async的引擎内置的执行器
function runGenerator(generatorFunc, ...args) {
// 创建迭代器
const iterator = generatorFunc(...args);
// 递归函数,用于处理每一步
function handleNext(yieldedValue) {
const next = iterator.next(yieldedValue); // 将上一次的结果传入,推动迭代器下一步
if (next.done) {
// 如果生成器执行完毕,返回最终结果
return Promise.resolve(next.value);
}
// 如果未完成,yieldedValue 应该是一个 Promise
return Promise.resolve(next.value)
.then(result => handleNext(result)) // 等待 Promise 完成,将结果传回生成器,继续下一步
.catch(error => iterator.throw(error)); // 如果 Promise 失败,将错误抛回生成器(可用 try-catch 捕获)
}
return handleNext(); // 启动执行
}
// 3. 使用执行器运行 Generator
runGenerator(mainGenerator, 123)
.then(finalResult => {
console.log('Final Result:', finalResult);
});
为什么说 Generator 函数需要配合一个“执行器(Runner)”才能用于异步流程控制? Generator其实就是一个暂停与恢复执行的机制,但是他不知道什么时候需要暂停与恢复,执行器就是驱动整个异步流程的自动化。整个流程就是
- 执行器会递归的调用iterator.next()来驱动Generator的执行;
- Generator执行到yield暂停Generator的执行,跳到异步函数的getUser中执行;
- 当遇到yield出一个Promise时,执行器会等待Promise执行完成得到结果,然后通过next(value)将结果传回给Generator;
- Generator从暂停处恢复,yiled getUser(id) 出来的值就是传入的值;
- 执行下一步
- 阶段4:async+await 是Promise+Generator的官方语法糖。async声明异步函数(类似function*)返回Promise对象、await暂停函数执行(类似yield)。
并发执行多个await
// 快:并行执行
const userPromise = fetchUser(); // 立即启动
const postsPromise = fetchPosts(); // 立即启动
const [user, posts] = await Promise.all([userPromise, postsPromise]); // 并行等待
// 串行循环:一个接一个执行
for (const url of urls) {
const data = await fetch(url); // 等待上一个完成
}
// 并行循环:同时发起所有请求
const promises = urls.map(url => fetch(url));
const results = await Promise.all(promises);
//实际项目中运用的较多的:
//串行执行:
onMounted(async()=>{
// 串行执行异步函数,必须先获取到id才能获取视频资源
await getRecommendData().then(res=>{
recommendDatas.value=res
console.log("recommendDatas.value",recommendDatas.value);
})
await getVideoSource().then(res=>{
videoSource.value = res
})
})
//改为并行执行
onMounted(async()=>{
// 并行执行两个异步操作
const [recommendRes, videoRes] = await Promise.all([
getRecommendData(),
getVideoSource()
])
recommendDatas.value = recommendRes
videoSource.value = videoRes
})
闭包
闭包到底是什么?我理解的就是内部函数使用外部函数的变量。 下面代码帮助理解。
function f1(){
//以下四行组成闭包
> let a=1
> function f2(){
> console.log(a)
> }
return f2//返回内部函数
}
var result = f1() //执行外部函数,返回内部函数
result() //执行内部函数,输出1
闭包常应用于setTimeout、Ajax请求、事件处理中。
function sayHelloAfterDelay(name, delay) {
setTimeout(function() { // 这个回调函数形成了一个闭包,记住了`name`
console.log(`Hello, ${name}!`);
}, delay);
}
sayHelloAfterDelay('Alice', 1000); // 1秒后输出: "Hello, Alice!"
// 即使sayHelloAfterDelay早已执行完毕,回调函数依然能访问到当时的`name`
闭包与内存泄漏
闭包的核心是内层函数引用外层函数的变量/作用域,导致外层函数执行完成后,其作用域不会被GC回收--如果引用大量用不上的数据/DOM元素,且闭包本身长期存在,就会一直占用内存,形成泄露。下面说说如何避免闭包内存泄漏:
- 不引用冗余数据/大对象,只拿必须变量
- 使用完手动把引用设为null,让GC回收
- 循环里写闭包用 let,或者套个立即执行函数,别让所有闭包都用着同一个变量
this指向
独立函数调用
当函数被单独调用时,不带有上下文,这时的this指向全局windows或者undefined
function fun(){
console.log(this)
}
fun() //输出window{...}或undefined
隐式绑定(方法调用)
当调用对象的方法时,this会指向被调用的那个方法的对象
const obj = {
name : "up",
sayHi : function(){
console.log(`Hi,${this.name}`)
}
}
obj.sayHi()
const sayHHH = obj.sayHi()
sayHHH() //隐式丢失:将对象的方法赋值给变量的时候,再调用这个变量时,this就会指向window或undefined
显示绑定(call,apply,bind)
call(thisArg,args) //thisArg指的是调用函数时this指向的对象,args是参数列表
apply(thisArg,args1,args2,...)
bind(thisArg,args) //返回新的函数,thisArg指的是调用函数时this指向的对象,且是永远的
function introduce(lang, ide) {
console.log(`I code in ${lang} using ${ide}. My name is ${this.name}`);
}
const developer = { name: 'Bob' };
// 使用 call
introduce.call(developer, 'JavaScript', 'VS Code');
// 输出:I code in JavaScript using VS Code. My name is Bob
// 使用 apply
introduce.apply(developer, ['Python', 'PyCharm']);
// 输出:I code in Python using PyCharm. My name is Bob
//使用bind
const developer = { name: 'Bob' };
const boundIntroduce = introduce.bind(developer, 'Java'); // 可以预先绑定部分参数
boundIntroduce('IntelliJ'); // 输出:I code in Java using IntelliJ. My name is Bob
// 解决之前的隐式丢失问题
const boundGreet = person.greet.bind(person);
setTimeout(boundGreet, 1000); // 一秒钟后正确输出:Hello, I'm Alice
函数调用指向全局,方法调用指向对象,箭头函数继承父作用域
new操作
在构造函数内使用this,this指向的是new实例的对象。
function Car(brand) {
this.brand = brand; // this 指向新创建的实例
this.drive = function() {
console.log(`Driving a ${this.brand}`);
};
}
const myCar = new Car('Toyota');
console.log(myCar.brand); // 输出:Toyota
myCar.drive(); // 输出:Driving a Toyota
箭头函数的this
通用方法
数组方法
字符串方法
对象方法
日期方法
JS 常用方法简短总结(改原数据 + 核心格式)
类型 方法名 改变原数据? 核心格式(简短) 全局通用 parseInt (值,进制) 否 转整数 全局通用 parseFloat (值) 否 转小数 全局通用 JSON.parse (串) 否 串→JS 数据 全局通用 JSON.stringify (数据) 否 JS 数据→串 数组增删 push (元素...) 是 末尾加元素 数组增删 pop() 是 末尾删 1 个 数组增删 unshift (元素...) 是 开头加元素 数组增删 shift() 是 开头删 1 个 数组增删 splice (起始,删数,加...) 是 指定位置增删改 数组查询 indexOf (元素) 否 找元素第一个索引 数组查询 includes (元素) 否 判断是否包含 数组查询 find (函数) 否 找第一个满足条件的元素 数组转换 map (函数) 否 批量加工元素→新数组 数组转换 filter (函数) 否 筛选满足条件的元素→新数组 数组转换 slice (起始,结束) 否 截取片段→新数组 数组转换 concat (数组...) 否 合并数组→新数组 数组转换 join (分隔符) 否 数组→字符串 数组转换 reverse() 是 数组反转 数组转换 sort (函数) 是 数组排序(数字传 a-b 升序) 数组汇总 reduce (函数,初始值) 否 累加 / 汇总成一个值 字符串查找 indexOf (子串) 否 找子串第一个索引 字符串查找 includes (子串) 否 判断是否包含子串 字符串查找 startsWith (子串) 否 判断是否以子串开头 字符串查找 endsWith (子串) 否 判断是否以子串结尾 字符串截取 slice (起始,结束) 否 截取片段→新串 字符串截取 split (分隔符) 否 字符串→数组 字符串修改 replace (旧,新) 否 替换第一个匹配子串→新串 字符串修改 replaceAll (旧,新) 否 替换所有匹配子串→新串 字符串修改 trim() 否 去前后空格→新串 字符串修改 toUpperCase() 否 转大写→新串 字符串修改 toLowerCase() 否 转小写→新串 对象操作 Object.keys (对象) 否 取所有键→数组 对象操作 Object.values (对象) 否 取所有值→数组 对象操作 Object.entries (对象) 否 取键值对→二维数组 对象操作 Object.assign (目标,源...) 是(目标对象) 合并对象属性 日期操作 new Date() - 创建日期对象 日期操作 getFullYear() 否 取年份(4 位) 日期操作 getMonth() 否 取月份(0-11,需 + 1) 日期操作 getDate() 否 取日期(1-31) 日期操作 toLocaleString() 否 转本地易读时间串
BOM方法
BOM 核心方法简短总结
- 窗口可视大小:
document.documentElement.clientWidth/Height(常用,不含滚动条)、window.innerWidth/Height(含滚动条);- 滚动距离:垂直
scrollTop/scrollY、水平scrollLeft/scrollX;- 控制滚动:
window.scrollTo({top/left, behavior: 'smooth'})(固定位置)、元素.scrollIntoView({smooth})(指定元素);- 内容总高 / 宽:
元素.scrollHeight/scrollWidth(含滚动隐藏内容);- 窗口 resize 监听:
window.addEventListener('resize', 回调)(加防抖避免卡顿)。
浏览器网络&存储
HTTP&HTTPS
HTTP:超文本传输协议,基于明文传输,端口80,安全性低; HTTPS: HTTP+SSL/TSL,基于加密传输(对称加密与非对称加密),端口443,提供身份验证和数据完整性校验,安全性高。
浏览器缓存机制
浏览器缓存是为了减少网络请求、提升加载(静态的HTML、CSS等资源)速度,分为强缓存和协商缓存。
强缓存直接从本地缓存读取资源,不向服务器发送请求。(了解cache-Control:no-cache和no-store)协商缓存是当强缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器决定是否使用缓存。
no-cache是不允许浏览器直接使用本地缓存,浏览器将资源存入缓存,下次请求携带验证信息询问服务器(协商缓存),根据服务器返回的状态判断资源是否更新从而决定是否需要更新缓存资源,既更不更新缓存取决于服务器返回的信息。
no-store是完全禁止缓存,涉及登录凭证、支付等敏感信息。
客户端存储机制
Cookie
cookie的工作流程: 用户登录,浏览器发送登录请求到服务器,服务器验证凭证正确后在返回响应头中通过 Set-Cookie 设置 Cookie并返回给浏览器。 浏览器 收到响应后,将这些 Cookie 保存在本地。后续页面访问网站其他页面,浏览器自动在请求头上携带Cookie,服务器确认请求头的Cookie,返回数据。这个"携带Cookie → 服务器验证"的过程在每一次请求中都会重复,直到用户退出登录或者Cookie过期。
主要用途:
LocalStorage
纯前端操作,没什么好讲的
// 存储数据(以 LocalStorage 为例,SessionStorage 只需替换对象名)
localStorage.setItem('key', 'value'); // 存储一个字符串
// 或
localStorage.key = 'value';
// 或
localStorage['key'] = 'value';
// 读取数据
let data = localStorage.getItem('key');
// 或
let data = localStorage.key;
// 删除单个数据
localStorage.removeItem('key');
// 清空所有本域名下的数据
localStorage.clear();
// 获取第 n 个键的名称(用于遍历)
const keyName = localStorage.key(0);
// 存储对象或数组(需要转换为 JSON 字符串)
const userSettings = { theme: 'dark', fontSize: 14 };
localStorage.setItem('settings', JSON.stringify(userSettings));
// 读取对象或数组
const settings = JSON.parse(localStorage.getItem('settings'));
工作流程:用户操作,客户端脚本执行将设置存入LocalStorage,浏览器收到指令将键值对持久化存储在本地,用户再次访问该网站时,网站的前端js会在初始化时读取 LocalStorage。内存较大,约5兆。
SessionStorage
仅在当前浏览器标签页有效。关闭标签页(即使是同一个网站,不同标签打开的sessionStorage也不同),数据即被销毁。也是纯前端操作。
核心结论:
- Cookie 的核心价值在于 “自动的客户端-服务器通信” ,用于维持状态。
- Web Storage 的核心价值在于 “纯客户端的大容量存储” ,用于提升客户端体验。
- LocalStorage 与 SessionStorage 的唯一关键区别在于 生命周期和作用域,根据你需要数据“永久存在”还是“仅限本次浏览”来选择。
vue
VUE生命周期
vue生命周期是指组件从被创建到挂载到DOM,再到数据更新,最终被销毁的整个过程。
组件通信
[(【前端--Vue】组件之间的多种通信方式,一文彻底搞懂组件通信!-阿里云开发者社区)]
状态管理
状态管理就是在一个集中的地方存储和管理应用的状态,并且确保整个状态是可预测的、可追踪的。
[彻底搞懂 Vue 的状态管理(含 Vuex 与 Pinia 实战对比)-CSDN博客](url)
Vue 响应式原理与双向绑定
响应式原理
vue2用object.property劫持对象的get/set,给每个属性装“监听器”,数据访问时收集依赖,修改时触发更新;但是只能监听已声明的属性,新增/删除属性,数组索引变化要手动用this.$set。
vue3用proxy直接劫持整个对象/数组,所有操作都能拦截,还支持懒监听,性能更好。
总而言之,Proxy 监听整个对象,场景覆盖全、性能优;Object.defineProperty 只监听单个属性,有局限。
双向绑定
vue2和vue3底层都遵循 数据->视图靠响应式劫持,视图->数据靠事件监听 的核心逻辑。简单说,v-model = 响应式数据绑定(数据→视图) + input 事件监听(视图→数据)。
构建工具:Webpack 与 Vite的区别
理解几个概念,Bundle(打包)、Bundleless(不打包)、开发环境(本地写代码时用)、生产环境(项目上线后用户用)
先大概讲一下webpack吧,webpack本质上是“模块打包工具”,但是它只能解析js模块以及json文件。而loader-based就是webpack处理非js/json资源的核心机制,它把这些资源转换成webpack可以识别的js模块,再纳入打包流程。
vite(前端构建工具)开发时 “浏览器直接加载 ESM 源码(基于原生ES Module) + 预构建第三方依赖”,跳过打包步骤,所以快到飞起。下面讲一下他们的主要区别:
开发环境原理:webpack是先打包后启动服务。修改一个文件后,它需要重新编译该文件及其所有依赖的模块。vite是直接启动服务,按需编译。浏览器需要什么,才编译什么。
生产环境处理:webpack是自带打包引擎,原生支持全模块系统(就是什么模块和场景都支持),vite不自己打包,它依赖Rollup做生产构建,只专注现代浏览器项目,不兼容旧浏览器。准确点说,vite是开发环境Bundleless+生产环境Bund le.
生态和定位:Webpack:大而全,插件生态极其丰富,配置灵活但复杂;vite:开箱即用,追求极致的开发体验。
双端兼容
- 什么是服务端渲染?它和客户端渲染的主要区别是什么? 答:服务端渲染(SSR)是服务端先编译组件、获取数据,生成完整 HTML 字符串返回给浏览器,浏览器直接渲染;客户端渲染(CSR)是浏览器先加载 JS,再在客户端编译组件、拉取数据、生成 DOM。核心区别:SSR 首屏快、SEO 友好,CSR 交互体验好、服务端压力小。(即什么端渲染,在哪里编译组件,获取数据,生成DOM)
项目布局
性能优化
重排&重绘
重排 当元素的几何属性(位置、尺寸、结构)变化时,影响文档流或其他元素布局时,浏览器需要重新计算布局。减少重排:按照“批量处理(文档碎片DocumentFragment批量存储节点再统一增加或删除)→脱离文档流(absolute/fixed 定位)→避免强制同步→其他优化(用 transform、opacity 实现动画)”的原理。
const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++)
{
const el = document.createElement('li');
el.style.width = '100px';
el.style.height = '100px';
el.style.margin = '10px';
fragment.appendChild(el); // 这里不会触发重排和重绘
}
document.body.appendChild(fragment); // 一次性触发一次重排和重绘
项目中遇到过哪些重排重绘导致的性能问题?怎么排查和解决的?
- 应答核心:结合实际场景(如长列表、动画卡顿),说清 “排查工具→问题原因→解决方案”。
- 参考回答:之前做移动端长列表下拉加载时,滚动卡顿。用 Chrome 的 Performance 面板排查,发现每次加载新数据时,循环创建节点并 appendChild,触发了多次重排。解决方式是:1. 用 DocumentFragment 批量创建节点;2. 给列表项设置 fixed 定位脱离文档流;3. 开启虚拟列表,只渲染可视区域内的节点,减少 DOM 数量,从根源减少重排。
重绘 当元素的外观样式改变,但不影响布局时,浏览器只需要重新绘制。
Tree-shaking
vite项目是默认开启tree-shaking,不需要额外配置的,只需要做到两点即可,第一点是使用ESM规范的第三方库,第二是避免动态导入(require)。
webpack 项目则需要额外配置webpack.config.js文件的 module.exports.mode='production' 以及 package.json 文件的sideEffect=false[.](什么是 Tree Shaking?如何在前端项目中启用它?_treeshaking-CSDN博客)
资源加载优化
图片格式选择
基础用法 现代浏览器推荐使用webp\avif,但是也要向下兼容png、gif等图片。例如:
<picture>
<!-- 现代浏览器加载 WebP -->
<source srcset="example.webp" type="image/webp">
<!-- 不支持 WebP 的浏览器降级加载 JPG/PNG -->
<img src="example.jpg" alt="示例图片" loading="lazy">
<!-- 可选加懒加载 -->
</picture>
关键前提:确保服务器正确返回 WebP 的 MIME 类型
图片懒加载
v-loading='lazy',可以实现图片进入视口边缘时开始加载,但是可能会让用户看到短暂空白。配合intersection-observer 自定义加载时机。
<script setup>
// 自定义懒加载指令:兼容原生不支持的浏览器,且可自定义加载时机
const vLazy = {
mounted(el) {
// 优先使用原生懒加载(如果浏览器支持)
if ('loading' in HTMLImageElement.prototype) {
el.loading = 'lazy';
} else {
// 不支持原生时,用 IntersectionObserver 模拟
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// 加载图片
img.src = img.dataset.src;
img.srcset = img.dataset.srcset;
observer.unobserve(img);
}
});
},
{ rootMargin: '200px' } // 提前 200px 开始加载
);
observer.observe(el);
}
}
};
</script>
<template>
<picture>
<source :data-srcset="item.webpUrl" type="image/webp">
<img
:data-src="item.jpgUrl" <!-- 用 data-* 存储真实地址 -->
:alt="item.title"
v-lazy <!-- 使用自定义指令 -->
width="400"
height="300"
/>
</picture>
</template>
下面了解一下自定义指令:首先在directives.module.js 文件上添加自定义指令的逻辑,再在directives.index.js文件上添加批量注册指令的函数。以v-copy示例:
import {ZyMessage} from "../../libs/util.toast";
const copyTextDirective = {
mounted(el, binding) {
// 添加点击事件监听器
el.addEventListener('click', () => {
// 动态创建 textarea 标签
const textarea = document.createElement('textarea');
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = binding.value
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// 尝试执行复制操作
try {
const successful = document.execCommand('copy')
const message = successful ? '复制成功' : '复制失败'
successful ? ZyMessage.success(message) : ZyMessage.error(message)
} catch (error) {
ZyMessage.error('复制操作失败')
console.log('复制操作失败:', error)
}
// 移除临时创建的textarea元素
document.body.removeChild(textarea)
})
}
}
// 导出自定义指令
export default copyTextDirective
import copy from './modules/copy'
import permission from './modules/permission'
import debounce from './modules/debounce'
// 自定义指令
const directives = {
copy,
permission,
debounce
};
// 这种写法可以批量注册指令
// 2. 导出一个 Vue 插件(含 install 方法)
// Vue 规定:如果一个对象有 install 方法,它就是一个 “Vue 插件”
export default {
// 调用app.use()时,vue会自动调用install
install(app) {
// 遍历 directives 对象的所有 key(即指令名:copy、permission、debounce)
Object.keys(directives).forEach((key) => {
// 注册指令:app.directive(指令名, 指令对象)
app.directive(key, directives[key]);
// app.directive("copy", copy)
});
},
};
最后在main.js触发全局自定义指令:app.use()//使用全局.../app.component()//注册全局组件
图片自动转换
vite和webpack可通过插件自动将jpg/png转换为webp。
vite推荐插件:vite-plugin-image-optimizer(自动压缩 + 转 WebP)。
配置vite.config.js文件:组件正常使用img即自动将img标签降级为picture.
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { imageOptimizer } from 'vite-plugin-image-optimizer';
import VuePicture from 'vite-plugin-vue-picture';
export default defineConfig({
plugins: [
vue(), // Vue 基础插件
// 1. 自动将 JPG/PNG 转 WebP(打包时生成 .webp 文件)
imageOptimizer({
// 禁用其他图片格式的压缩(只关注 WebP 转换)
optipng: { enabled: false },
jpegtran: { enabled: false },
mozjpeg: { enabled: false },
// WebP 转换配置
webp: {
enabled: true,
quality: 80, // 压缩质量(0-100,越高越清晰但体积越大)
lossless: false, // false=有损压缩(默认,体积更小),true=无损压缩
},
// 可选:同时生成 AVIF 格式(比 WebP 更小,兼容性稍差)
avif: { enabled: false },
}),
// 2. 自动将 Vue 模板中的 <img> 替换为 <picture> 标签
VuePicture({
// 配置需要转换的图片格式(默认已包含 jpg/jpeg/png)
extensions: ['jpg', 'jpeg', 'png'],
// 可选:添加懒加载(默认 false,按需开启)
lazy: true,
// 可选:指定 WebP 的 MIME 类型(默认 image/webp,无需修改)
webpType: 'image/webp',
}),
],
// 配置图片路径(确保 Vite 能正确识别图片资源)
resolve: {
alias: {
'@': '/src', // 别名,方便引用 src 下的图片
},
},
});
webpack推荐插件:image-webpack-loader + html-loader(需手动配置降级逻辑)。
减少HTTP请求数
- 非关键的CSS和JS合并成一个文件
- 小于 10KB 的图片 / 图标用 url-loader 转 Base64,嵌入到 CSS/HTML 中,无需额外请求。
- 使用精灵图
优化加载速率
- 使用CDN(让资源从就近节点传输)
- 预加载(preload),提前加载关键资源(优化加载优先级)
- 预连接(preconnect),提前建立跨域连接,减少连接建立耗时。(节省 TCP 握手 / SSL 协商时间)
- 延迟加载,非关键资源加载用异步加载。
渲染性能优化
避免重排(Reflow)和重绘(Repaint)
- 批量操作 DOM:用 DocumentFragment 批量插入子元素,避免频繁修改 DOM;
- 避免布局抖动:先读取所有布局属性(如
offsetWidth),再批量修改(不连续读 + 写);- 动画优化:用 CSS transforms/opacity 实现动画(仅触发合成阶段,不重排重绘);
- 脱离文档流:频繁动画的元素用
position: absolute/fixed(减少对其他元素的影响);- 避免 table 布局(频繁重排),用 Flex/Grid 替代。
- 优化方法 3:加 contain: layout paint; 隔离元素渲染
优化渲染构建
- 嵌套Dom结构<=6层
- 隐藏不可见元素(建议用display:none)
- tree-shaking css
运行时性能优化
- 虚拟列表:当数据量很大(1w+,如聊天记录、订单列表、日志表格),使用vue-virtual-scroller