2025最新面试题来啦!
前言:
讲讲苦逼前端仔为啥又要面试的背景吧哈哈哈,就当自己的碎碎念。呆了两年的公司,被迫转型当项目经理,并不是我们理解的那种可以带项目开发的那种,是整不完的各种材料,跪舔甲方,以及干了活还不被领导认可产出的苦逼牛马。所以说还是该换换环境吧,最近大环境不咋滴,投了一周的简历也才一两个面试。整理一下自己碰到的面试题吧,希望能对大家也有所帮助。对了,现在的我也是中级前端牛马了毕竟干了两三年不能再面初级岗了,大家一起加油!有内推的哥们欢迎私信!!!
面试题:
1. 说说做过哪些项目、项目之中碰到的难点、以及如何解决的?
-
- 基建项目:Vue2到Vue3迁移
想要重构vue2项目,那就要从里到外都要升级重构一遍。
选择合适的构建工具:Vue3 推荐使用 Vite 作为构建工具,其热更新速度更快,开发效率更高。
安装 Vite:使用 npm 或 yarn 安装 Vite。
创建 Vue3 项目:使用 Vite 创建一个新的 Vue3 项目。
迁移代码:将 Vue2 项目的 src 目录下的内容复制粘贴到新创建的 Vue3 项目中。
修改配置文件:根据项目需求修改 Vite 配置文件。
测试和优化:测试项目功能,并对代码进行优化。
渐进式迁移
- 使用@vue/composition-api逐步替换Options API
- 构建双版本并行运行环境
创建两个版本环境逐步将依赖配置添加,建立好相应路由然后直接迁移代码,最后根据对两个环境的页面进行功能测试,修改相应vite配置进行优化。
首先讲讲为什么会有vue2项目重构到vue3的背景
对于网页端项目,访问速度越快、用户操作越流畅就越牛逼,代码的简洁程度也能影响一个网站的seo优化。
1. 速度更快:
Vue3 通过重写虚拟 DOM 实现和编译模板优化,实现了性能上的显著提升。以下是一些具体的数据:
**组件初始化**:Vue3 的组件初始化性能比 Vue2 提高了 50%。
**更新性能**:Vue3 的更新性能比 Vue2 提高了 1.3-2 倍。
**SSR**:Vue3 的服务器端渲染速度比 Vue2 提高了 2-3 倍。
2. 体积更小:
Vue3 通过 Webpack 的 tree-shaking 功能,将无用模块剪辑,仅打包需要的代码,从而减小了包体积。以下是 Vue3 包体积的对比数据:
**Vue2**:约 20-30 KB(压缩后)
**Vue3**:约 10-15 KB(压缩后)
3. 更易维护
Vue3 引入了 Composition API,简化了组件逻辑的组织,提高了代码的可读性和可维护性。以下是一些具体的变化:
**Composition API**:允许开发者将组件逻辑分解为更小的函数,方便复用和维护。
**Options API**:Vue3 支持与 Options API 一起使用,方便开发者逐步迁移。
4. 更好的 TypeScript 支持
Vue3 基于 TypeScript 编写,提供了自动的类型定义提示和编译器优化,使得开发者能够更方便地使用 TypeScript 进行开发。
2.平时项目之中有遇到过跨域问题吗,什么场景下遇到的呢,又是如何解决的呢。
回答:
一、跨域问题产生原因
-
同源策略限制
浏览器安全机制要求协议、域名、端口三要素完全一致才能直接交互,否则会触发跨域拦截。 -
典型场景
- 前后端分离开发时本地调试(localhost:8080 → api.domain.com)
- 调用第三方API服务(your-site.com → weather-api.com)
- 不同协议或子域访问(http → https 或 app.domain.com → api.domain.com)
1. CORS(推荐方案)
javascript
// 服务端设置响应头
Access-Control-Allow-Origin: * // 允许所有域
Access-Control-Allow-Methods: GET,POST // 允许的请求方法
Access-Control-Allow-Headers: Content-Type // 允许的请求头
*特点:需要后端配合,支持所有请求类型,安全可控
2. JSONP(传统方案)
javascript
// 前端
<script>
function callback(data) {
console.log('Received:', data)
}
</script>
<script src="https://api.com/data?callback=callback"></script>
// 服务端返回
callback({ "result": "data" })
*特点:仅支持GET请求,存在XSS风险
3. 代理服务器
javascript
// Vue开发环境配置(vue.config.js)
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://target-domain.com',
changeOrigin: true // 修改请求头host
}
}
}
}
*特点:开发环境常用,生产环境需配合Nginx
4. Nginx反向代理
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
*特点:生产环境推荐方案,性能更好
5. 浏览器临时方案(仅开发用)
# Chrome启动参数(Windows)
chrome.exe --disable-web-security --user-data-dir=C:\temp
*特点:完全禁用安全策略,仅限本地调试
3.前端网页的渲染机制。
一、关键渲染流程
-
HTML解析与DOM构建
- 字节数据 → 字符串 → Token → DOM节点 → DOM树
- 遇到
<script>
标签会暂停解析,等待JS加载执行完成 - 预解析线程提前下载CSS/JS资源
-
CSS解析与CSSOM构建
- 解析CSS规则生成样式规则树
- 与DOM树并行构建但会阻塞渲染
-
渲染树合成
- 合并DOM树与CSSOM树,过滤不可见节点(如
display:none
) - 形成包含样式和布局信息的渲染树(Render Tree)
- 合并DOM树与CSSOM树,过滤不可见节点(如
-
布局阶段(回流)
- 计算元素精确位置和尺寸(盒模型计算)
- 首次布局称为"布局",后续布局调整称为"回流"
-
绘制与合成
- 分层绘制(Layer)→ 光栅化(Raster)→ 合成(Composite)
- GPU加速的动画会启用独立合成层
二、关键优化机制
-
阻塞特性
- CSSOM构建会阻塞渲染(建议将CSS放在头部)
- JS执行会阻塞DOM解析(建议使用async/defer)
-
重绘与回流
- 回流必定触发重绘(几何属性变化触发回流)
- 避免强制同步布局(如频繁读取offsetHeight)
-
现代浏览器优化
- 增量式布局(Incremental Layout)
- 合成线程独立于主线程(CSS动画建议使用transform/opacity)
4.后端反了10万条数据,存在一个数组之中,数组中有a,b,c三个字母如何最快找出他们的下标。
方法一:单次遍历法(最优解)
const targets = new Set(['a', 'b', 'c']);
const result = [];
for(let i=0; i<arr.length; i++) {
//使用ES6的Set数据结构存储目标字符,利用其O(1)时间复杂度的`has()`方法实现快速存在性检测
if(targets.has(arr[i])){
//当检测到当前元素属于目标集合时,立即将当前索引i存入结果数组。
result.push(i);
}
}
5.vue2和vue3的区别。
1.首先谈谈生命周期。
vue3的生命周期相较于vue2并没有太大区别,vue2的生命周期前加上'on' .有一点需要特别注意一下,Vue3 在组合式API
(Composition API)中使用生命周期钩子时需要先引入
,而 Vue2 在选项API
(Options API)中可以直接调用生命周期钩子。
Tips: setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义。
2、组合式api Vue2是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
Vue3组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。所有逻辑在setup函数中,使用 ref、watch 等函数组织代码。
3、setup函数 setup函数是组合式API的入口函数,默认导出配置选项,setup函数声明,返回模板需要数据与函数。
setup 函数是 Vue3 特有的选项,作为组合式API的起点 从组件生命周期看,它在 beforeCreate 之前执行 函数中 this 不是组件实例,是 undefined 如果数据或者函数在模板中使用,需要在 setup 返回 今后在vue3的项目中几乎用不到 this , 所有的东西通过函数获取。
4.多根节点
// Vue2只能存在一个根节点,需要用一个<div>来包裹着
`<template>
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</template>
// Vue3支持多个根节点
<template>
<header></header>
<main></main>
<footer></footer>
`</template>``
5、 响应式原理 Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。
Object.defineProperty 基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。 Proxy提供了一种更强大和灵活的拦截器机制,允许开发者定义代理对象并拦截对该对象的各种操作(如属性读取、属性设置、函数调用等)。 以下分别是Vue2和Vue3的响应式原理的简单示例。
// 定义一个对象
const data = {
name: 'Vue2',
count: 0
};
// 使用Object.defineProperty为每个属性添加getter和setter
Object.defineProperty(data, 'name', {
get() {
console.log('name getter called');
return this._name;
},
set(newValue) {
console.log('name setter called with value:', newValue);
this._name = newValue;
// 这里可以触发视图更新等操作
},
configurable: true,
enumerable: true
});
// 初始化_name属性
data._name = data.name;
// 访问和修改属性
console.log(data.name); // 输出: name getter called,Vue2
data.name = 'New Name'; // 输出: name setter called with value: New Name
vue3的示例
import { reactive } from 'vue';
// 创建一个响应式对象
const state = reactive({
name: 'Vue3',
count: 0
});
// 访问和修改属性
console.log(state.name); // 访问属性,触发getter
state.name = 'New Name'; // 修改属性,触发setter
// 在Vue3中,你不需要显式地定义getter和setter,因为Proxy会为你处理这些
// 但是,你可以通过调试或查看Vue3的内部实现来了解它是如何工作的
// 打印出来的是一个proxy对象
6.你平时用过ES6语法吗说说有哪些。
1.let与const 好处:
let用来声明变量,类似于var,但是所声明的变量,只在let命令所在的代码块内有效
特点:
1)存在块级作用域
2)不存在变量提升(考虑暂时性死区)
3)不允许重复声明(包括普通变量和函数参数)
const用于声名一个只读的常量,常量的值不支持改变
2.箭头函数 箭头函数和普通匿名函数有哪些不同?
- 函数体内的this对象,指向所在的对象。
- 不可以当作构造函数
- 不可以使用arguments对象,该对象在函数体内不存在,如果要用,可以使用rest参数代替
- 不可以使用yield命令
ES6 允许使用“箭头”(=>
)定义函数。
场景:用于替换匿名函数
基本用法
//匿名函数
div.onclick=function(){
console.log("你好")
}
//箭头函数
div.onclick=()=>{
console.log("你好")
}
3. 模板字符串 语法
let a=`我的名字是:${name}`
字符串和变量拼接
let s3 =" a " + s1 + " b " + s2;
let s4 = ` a ${s1} b ${s2}`;
字符串换行
var box =`<div>
<p>
<span>123</span>
</p>
<p>${a1}</p>
</div>`;
4. 解构赋值
解构赋值允许从数组或对象中提取数据,并将其赋值给变量。
解构赋值可以方便地交换变量的值,同时还支持默认值
对象结构赋值
var obj ={ name:"abc",age:18 };
//用解构赋值的方式获取name、age
let { name } = obj; //创建了一个变量name,值=obj.name
console.log(name); //"abc"
let { age } =obj;
console.log(age); //18
函数参数结构赋值
function f1(obj){
console.log(obj.age);
console.log(obj.height)
}
//等价于
function f1({ age,height }){
console.log(age);
console.log(height)
}
f1({age:5,height:180})
5. 展开运算 展开运算符...可以将可迭代对象宅开为多个元素
// 案例1
let a={age:19,hobby:'抽烟'}
let user={name:'green',age:21,...a}
console.log(user) //{ name: 'lqz', age: 19, hobby: '抽烟' }
// 案例2
let l=[1,2,3]
let l1=[44,55,66,...l]
console.log(l1) // [ 44, 55, 66, 1, 2, 3 ]
// 案例3
# 案例3
function demo01(a,...b){
console.log(a)
console.log(b)
}
demo01(1,2,34,4)
let l=[44,5,66,7]
demo01(...l)
6.Promise构造函数
Promise自身有我们常用的all、race、resoleve、reject等方法,原型中还有catch、then等方法,所以我们创建Promise实例时,将then会作为callback使用,避免回调地狱的出现。
Promise.all的提供了并行的操作能力,并且是在所有的一步操作执行完成后才执行回调。 all接收一个数组参数,它会把所有异步操作的结果放进一个数组中传给then。
// all的使用
function async1 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('async1');
resolve('async1完成')
}, 1000);
})
}
function async2 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('async2');
resolve('async2完成')
}, 2000);
})
}
function async3 () {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('async3');
resolve('async3完成')
}, 3000);
})
}
Promise.all([async1(), async2(), async3()]).then((result) => {
console.log(result);
// do something...
}).catch((err) => {
console.log(err);
});
// result:
async1
async2
async3
[ 'async1完成', 'async2完成', 'async3完成' ]
一、Promise.resolve()
和 Promise.reject()
-
Promise.resolve()
创建一个 立即成功 的 Promise 对象,常用于将非 Promise 值转为 Promise[5][6][9]:javascript // 返回一个已成功的 Promise,值为 'Hello' Promise.resolve('Hello').then(res => console.log(res)); // 输出 'Hello' [5][9]
-
Promise.reject()
创建一个 立即失败 的 Promise 对象,常用于抛出错误[5][6]:javascript // 返回一个已失败的 Promise,错误为 'Error' Promise.reject('Error').catch(err => console.log(err)); // 输出 'Error' [5][6]
二、Promise.all()
作用:接收一个 Promise 数组,当所有 Promise 都成功时返回结果数组;若有一个失败,则立即失败[1][4][9]。
javascript
const p1 = Promise.resolve('Task1');
const p2 = new Promise(resolve => setTimeout(resolve, 1000, 'Task2'));
Promise.all([p1, p2])
.then(results => console.log(results)) // 1秒后输出 ['Task1', 'Task2']
.catch(err => console.log(err)); // 任一失败时触发 [4][9]
三、Promise.race()
作用:接收一个 Promise 数组,返回 第一个完成 的 Promise 的结果(无论成功或失败)[1][4][9]:
javascript
const p1 = new Promise(resolve => setTimeout(resolve, 500, 'Fast'));
const p2 = new Promise(resolve => setTimeout(resolve, 1000, 'Slow'));
Promise.race([p1, p2])
.then(result => console.log(result)) // 0.5秒后输出 'Fast'
.catch(err => console.log(err)); // 第一个失败的 Promise 触发 [4][9]
四、构造函数中的 resolve
和 reject
在创建 Promise 实例时,通过 resolve
和 reject
控制状态[3][6][9]:
javascript
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('操作成功'); // 触发 then 回调 [5][6]
} else {
reject('操作失败'); // 触发 catch 回调 [5][6]
}
});
promise
.then(res => console.log(res))
.catch(err => console.log(err));
总结
方法 | 作用 | 示例场景 | 来源 |
---|---|---|---|
Promise.resolve | 创建立即成功的 Promise | 同步返回已知结果 | |
Promise.reject | 创建立即失败的 Promise | 快速抛出错误 | |
Promise.all | 并行执行多个 Promise,全部成功时返回 | 多接口数据聚合 | |
Promise.race | 取第一个完成的 Promise 结果 | 请求超时控制、竞速场景 |
promise的三种状态: pending、fulfilled、rejected(未决定,履行,拒绝),同一时间只能存在一种状态,且状态一旦改变就不能再变。promise是一个构造函数,promise对象代表一项有两种可能结果(成功或失败)的任务,它还持有多个回调,出现不同结果时分别发出相应回调。
7.解决异步
我们都知道js是单线程执行代码,导致js的很多操作都是异步执行(ajax)的,以下是解决异步的几种方式:
1.回调函数(定时器)。
2.事件监听。
3.发布/订阅。
4.Promise对象。(将执行代码和处理结果分开)
5.Generator。
6.ES7的async/await
8.聊聊你平时对数组的操作用法,尽可能全一点。
一、改变原数组的方法
-
push()
向数组末尾添加元素,返回新长度 [1][3][5]javascript let arr = [1,2]; arr.push(3); // arr变为[1,2,3]
-
pop()
删除数组最后一个元素,返回被删除的值 [1][3][5]javascript let last = arr.pop(); // last=3,arr变为[1,2]
-
shift()
删除数组第一个元素,返回被删除的值 [1][3][5]javascript let first = arr.shift(); // first=1,arr变为[2]
-
unshift()
向数组开头添加元素,返回新长度 [1][3][5]javascript arr.unshift(0); // arr变为[0,2]
-
splice()
添加/删除元素,返回被删除元素的数组 [1][3][4]javascript arr.splice(1,1, 'new'); // 从索引1删除1个元素并插入'new'
-
reverse()
反转数组顺序 [1][3][4]javascript arr.reverse(); // [2,0] → [0,2]
-
sort()
排序(默认按Unicode),可传比较函数 [1][4][7]javascript [3,1].sort((a,b) => a-b); // [1,3]
-
copyWithin()
复制数组部分内容到其他位置 [1]javascript [1,2,3,4].copyWithin(0,2); // [3,4,3,4]
-
fill()
填充数组为指定值 [1][2]javascript new Array(3).fill(0); // [0,0,0]
二、返回元素信息或索引的方法
-
indexOf()
返回元素首次出现的索引,无则-1 [1][3][7]javascript ['a','b'].indexOf('b'); // 1
-
lastIndexOf()
返回元素最后一次出现的索引 [1][7][8]javascript [2,5,2].lastIndexOf(2); // 2
-
find()
返回第一个符合条件的元素 [1][7]javascript [1,2,3].find(x => x>1); // 2
-
findIndex()
返回第一个符合条件的元素索引 [1][7]javascript [1,2,3].findIndex(x => x>1); // 1
-
includes()
判断是否包含某值,返回布尔值 [1][3][7]javascript [1,2].includes(2); // true
三、生成新数组或值的方法
-
concat()
合并数组,返回新数组 [1][3][4]javascript [1,2].concat([3]); // [1,2,3]
-
slice()
截取数组片段,返回新数组 [1][3][4]javascript [1,2,3].slice(1,3); // [2,3]
-
map()
遍历处理元素,返回新数组 [1][4][7]javascript [1,2].map(x => x*2); // [2,4]
-
filter()
过滤元素,返回符合条件的新数组 [1][7][8]javascript [1,2,3].filter(x => x>1); // [2,3]
-
flat()
数组扁平化(默认展开1层) [1][2]javascript [1,[2]].flat(); // [1,2]
-
flatMap()
先映射再扁平化 [1]javascript [1,2].flatMap(x => [x, x*2]); // [1,2,2,4]
-
join()
将数组转为字符串,默认逗号分隔 [1][3][7]javascript [1,2].join('-'); // "1-2"
-
reduce()
/reduceRight()
累计计算,返回最终值 [1][7][8]javascript [1,2].reduce((sum, x) => sum + x, 0); // 3
四、遍历与判断方法
-
forEach()
遍历数组,无返回值 [1][3][7]javascript [1,2].forEach(x => console.log(x));
-
every()
所有元素满足条件则返回true
[1][7][8]javascript [2,4].every(x => x%2===0); // true
-
some()
任一元素满足条件则返回true
[1][7][8]javascript [1,2].some(x => x>1); // true
五、其他实用方法
-
Array.from()
将类数组转为数组 [1][6]javascript Array.from('abc'); // ['a','b','c']
-
Array.of()
创建包含任意类型元素的数组 [1][6]javascript Array.of(1, 'a'); // [1, 'a']
-
Array.isArray()
判断是否为数组 [3][6]javascript Array.isArray([1]); // true
-
keys()
/values()
返回数组索引或值的迭代器javascript [...['a','b'].keys()]; // [0,1]
六、ES6+新增方法
findLast()
/findLastIndex()
从后向前查找元素或索引(ES2023)toReversed()
/toSorted()
返回反转/排序后的新数组(ES2023)
9.讲一下foreach、map的区别
forEach()方法返回undefined ,而map()返回一个包含已转换元素的新数组。
const numbers = [1, 2, 3, 4, 5];
// 使用 forEach()
const squareUsingForEach = [];
numbers.forEach(x => squareUsingForEach.push(x*x));
// 使用 map()
const squareUsingMap = numbers.map(x => x*x);
console.log(squareUsingForEach); // [1, 4, 9, 16, 25]
console.log(squareUsingMap); // [1, 4, 9, 16, 25]
由于forEach()返回undefined,所以我们需要传递一个空数组来创建一个新的转换后的数组。map()方法不存在这样的问题,它直接返回新的转换后的数组。在这种情况下,建议使用map()方法。
10.async await
由于 JavaScript 是单线程执行模型,因此必须支持异步编程才能提高运行效率。异步编程的语法目标是让异步过程写起来像同步过程。
为了解决 Promise 的问题,async、await 在 ES7 中被提了出来,是目前为止最好的解决方案
const fs = require('fs')
async function readFile() {
try {
var f1 = await readFileWithPromise('/etc/passwd')
console.log(f1.toString())
var f2 = await readFileWithPromise('/etc/profile')
console.log(f2.toString())
} catch (err) {
console.log(err)
}
}
async、await 函数写起来跟同步函数一样,条件是需要接收 Promise 或原始类型的值。异步编程的最终目标是转换成人类最容易理解的形式。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数
11.vuex用过吗,讲讲其中的一些方法和作用
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。简单来说就是:应用遇到多个组件共享状态时,使用vuex。
1.怎么使用?
vue框架中状态管理。在main.js引入store,注入。
新建了一个目录store.js,…… export 。
vuex的流程
页面通过mapAction异步提交事件到action。action通过commit把对应参数同步提交到mutation,mutation会修改state中对应的值。最后通过getter把对应值跑出去,在页面的计算属性中,通过,mapGetter来动态获取state中的值
二.vuex有哪几种属性?
vuex的store有几个属性值?分别讲讲它们的作用是什么? 有五种,分别是State , Getter , Mutation , Action , Module (就是mapAction)
state => 基本数据(数据源存放地) getters => 从基本数据派生出来的数据 mutations => 提交更改数据的方法,同步! actions => 像一个装饰器,包裹mutations,使之可以异步。 modules => 模块化Vuex
-
state:vuex的基本数据,用来存储变量
-
geeter:从基本数据(state)派生的数据,相当于state的计算属性
-
mutation:提交更新数据的方法,必须是同步的(如果需要异步使用action)。每个mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数,提交载荷作为第二个参数。
-
action:和mutation的功能大致相同,不同之处在于 ==》1. Action 提交的是 mutation,而不是直接变更状态。 2. Action 可以包含任意异步操作。
-
modules:模块化vuex,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。
三、vuex的state、getter、mutation、action、module特性分别是什么?
state:存放公共数据的地方 getter:获取根据业务场景处理返回的数据 mutations:唯一修改state的方法,修改过程是同步的 action:异步处理,通过分发操作触发mutation module:将store模块分割,减少代码臃肿
四、vuex怎样赋值?vuex存储数据的方法有哪些?
使用下面这两种方法存储数据:
dispatch:异步操作,写法: this.$store.dispatch('actions方法名',值)
commit:同步操作,写法:this.$store.commit('mutations方法名',值)
12.vue中的watch和computed区别
1、computed是计算属性;watch是监听,监听data中的数据变化。
2、computed支持缓存,当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值; watch不支持缓存,当对应属性发生变化的时候,响应执行。
3、computed不支持异步,有异步操作时无法监听数据变化;watch支持异步操作。
4、computed第一次加载时就监听;watch默认第一次加载时不监听。
immediate 组件创建时刻执行与否
immediate: true,第一次加载时监听(默认为false)
deep 深度监听 不推荐使用(非常的消耗性能)
监听的属性是对象的话 不开启deep 对象子属性变化不会触发watch
开启了deep 对象内部所有子属性变化 都会触发watch
5、computed中的函数必须调用return;watch不是。
6、使用场景: computed:一个属性受到多个属性影响,如:购物车商品结算。 watch:一个数据影响多条数据,如:搜索数据。 数据变化响应,执行异步操作,或高性能消耗的操作,watch为最佳选择。
13.v-show和v-if的区别以及适用场景
v-if为动态添加/移除 DOM 元素,v-show为通过 CSS display
属性控制显隐。
适用场景
-
使用
v-if
的场景- 条件很少变化(如权限控制、首次加载判断)
- 需要减少初始渲染开销(如首屏隐藏大组件)
- 需要销毁组件状态(如切换后需重置表单)
-
使用
v-show
的场景- 频繁切换显示状态(如选项卡、折叠面板)
- 需要保持组件状态(如表单输入内容保留)
14.qiankun项目搭建,以及关键配置,分别讲讲vue2和vue3的。
一、主应用配置(Vue3与Vue2对比)
1. Vue3主应用
javascript
// main.js
import { createApp } from 'vue'
import { registerMicroApps, start } from 'qiankun'
const app = createApp(App)
// 注册微应用(支持Vue2/Vue3子应用)
registerMicroApps([
{
name: 'vue2-sub',
entry: '//localhost:8080',
container: '#sub-container',
activeRule: '/vue2-app', // 激活路由规则
props: { token: '主应用传递的数据' } // 通信数据[1][2][7]
},
{
name: 'vue3-sub',
entry: '//localhost:8888',
activeRule: '/vue3-app'
}
])
start() // 启动qiankun[1][2][7]
2. Vue2主应用
javascript
// main.js
import Vue from 'vue'
import { registerMicroApps, start } from 'qiankun'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
// 注册逻辑与Vue3一致[5][9]
registerMicroApps([...])
start()
二、子应用配置(Vue3与Vue2差异)
1. Vue3子应用
javascript
// main.js
import { createApp } from 'vue'
let instance = null
function render(props = {}) {
instance = createApp(App).use(router).mount('#app')
}
// 暴露qiankun生命周期
export async function bootstrap() {}
export async function mount(props) { render(props) }
export async function unmount() { instance?.unmount() }
2. Vue2子应用
javascript
// main.js
let router = null
let instance = null
function render(props = {}) {
instance = new Vue({ router, render: h => h(App) }).$mount('#app')
}
// 暴露生命周期(与Vue3格式相同)
export async function bootstrap() { /*...*/ }
export async function mount(props) { render(props) }
export async function unmount() { instance.$destroy() }
三、关键配置项
-
打包配置(公共)
javascript // vue.config.js(Vue2/Vue3通用) module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*' } // 允许跨域 }, configureWebpack: { output: { library: `${name}-[name]`, // 微应用名称 libraryTarget: 'umd' // 模块格式 } } }
-
路由配置差异
- Vue3子应用:使用
createWebHistory
时需设置base: activeRule
路径 - Vue2子应用:建议使用
hash
路由避免路径冲突
- Vue3子应用:使用
四、注意事项
-
通信机制
- 主应用通过
props
传递数据,子应用通过window.__POWERED_BY_QIANKUN__
判断运行环境 - Vue3推荐使用
provide/inject
,Vue2可用Vue.observable
- 主应用通过
-
样式隔离
- 添加
scoped
或CSS Modules避免样式污染
- 添加
-
部署差异
- 生产环境需将子应用入口
entry
改为线上地址(如CDN)
- 生产环境需将子应用入口
总结
- 相同点:注册逻辑、生命周期暴露方式一致
- 差异点:Vue3使用
createApp
+组合式API,Vue2使用new Vue()
+选项式API - 高频考点:微应用注册配置、通信机制、路由隔离
15.以下是Vue2与Vue3组件传参方式对比及核心差异总结
一、相同点
- 父传子
均通过props
接收数据,父组件使用v-bind
绑定属性
<Child :message="parentMsg" />
<script>
export default {
props: ['message']
}
</script>
2. 子传父
```
//Vue3:需显式使用`provide/inject`
<button @click="$emit(‘update’, data)">提交</button>
<Child @update=“handleUpdate” />
```
3. Vue3 的 provide 和 inject
provide和inject的基本用法 让我们通过一个简单的例子来了解如何在Vue 3中使用provide和inject进行依赖注入。
父组件 - 使用provide 首先,我们创建一个父组件ParentComponent。在这个组件中,我们使用provide方法来提供数据:
什么是依赖注入? 依赖注入(Dependency Injection, DI)是一种设计模式,它允许一个类或组件从外部获得它依赖的对象或资源,而不是在内部自己创建这些对象。这种模式可以提高代码的可测试性和可扩展性,使代码结构更加清晰。
provide和inject方法就是Vue 3实现这种依赖注入的工具。父组件通过provide提供数据,后代组件通过inject获取数据。这种模式特别适用于需要跨组件传递状态或配置的情况。
<template>
<div>
<h1>Parent Component</h1>
<child-component></child-component>
</div>
</template>
<script>
export default {
name: 'ParentComponent',
setup() {
const message = 'Hello from Parent Component';
// 使用provide提供数据
provide('message', message);
return {};
},
};
</script>
在这个例子中,我们在setup函数中调用了provide方法,并提供了一个键值对,键是message,值是我们要传递的数据Hello from Parent Component。
子组件 - 使用inject
接下来,我们创建一个子组件ChildComponent
。在这个组件中,我们使用inject
方法来获取父组件提供的数据:
<template>
<div>
<h2>Child Component</h2>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
setup() {
// 使用inject获取父组件提供的数据
const message = inject('message');
return {
message,
};
},
};
</script>
我们可以通过provide
和inject
共享一个对象,而不是单个值。、
如果我们想提供响应式数据,可以使用ref
或reactive
<template>
<div>
<h1>Parent Component</h1>
<child-component></child-component>
</div>
</template>
<script>
import { ref, provide } from 'vue';
export default {
name: 'ParentComponent',
setup() {
const count = ref(0);
provide('count', count);
return {};
},
};
</script>
16.Vue3中ref与reactive的核心区别及使用场景总结
一、核心差异对比
特性 | ref | reactive |
---|---|---|
适用数据类型 | 基本类型(String/Number等) 或需要.value访问的引用类型[1][2][3][6] | 对象/数组等引用类型[1][3][5][6] |
访问方式 | JS中需通过.value 访问 模板中自动解包[2][6][8] | 直接访问属性(无需.value)[1][3][6] |
深度响应式 | 若存储对象,底层转为reactive实现深度响应[5][8] | 原生深度响应式[1][3][5] |
解构响应性 | 保持响应性 | 需配合toRefs 保持响应性[8] |
二、使用场景推荐
-
ref 适用场景
-
基本类型数据(计数器、开关状态等)
js const count = ref(0)
-
需要显式追踪引用的DOM元素或组件实例
<input ref="myInput">
-
需要灵活处理值类型变化的场景
-
-
reactive 适用场景
-
复杂对象结构(表单对象、配置项等)
js const form = reactive({ name: '', age: 18 })
-
需要深度嵌套响应的数据结构
js const tree = reactive({ nodes: [{ children: [...] }] }) // [1][5]
-
需要直接操作属性的场景(避免.value繁琐操作
-
三、最佳实践建议
-
优先使用
ref
处理基本类型,reactive
处理对象/数组 -
组合式API中推荐配合
toRefs
解构reactive对象js const state = reactive({ x: 1, y: 2 }) const { x, y } = toRefs(state) // [8]
-
避免混用导致代码可读性下降(如:用ref包装对象)
17.讲讲Vue3中的proxy对象
一、Proxy的核心概念
-
定义
Proxy是ES6引入的元编程特性,用于创建对象的代理,通过拦截器(traps)实现对对象基本操作(属性访问、赋值、枚举等)的自定义行为[1][2][5][6][9]。 -
基本语法
javascript const proxy = new Proxy(target, handler)
target
:被代理的目标对象(支持对象/数组/函数等)[6][7][9]handler
:包含拦截方法的对象(如get/set/deleteProperty等)[4][6][7]
二、在Vue3中的应用
-
响应式系统核心
Vue3使用Proxy替代Vue2的Object.defineProperty,通过reactive()
函数将普通对象转换为响应式对象[1][3][5][8][9]。 -
核心拦截器
javascript const handler = { get(target, key) { // 拦截属性读取 track(target, key) // 依赖收集[8][9] return Reflect.get(...arguments) }, set(target, key, value) { // 拦截属性修改 trigger(target, key) // 触发更新[8][9] return Reflect.set(...arguments) } }
-
优势对比
特性 Proxy (Vue3) Object.defineProperty (Vue2) 数组响应性 支持索引/长度变化[4][8] 需重写数组方法 动态属性 自动追踪新增属性[5][8] 需$set强制添加 嵌套对象 按需深度代理[3][8] 初始化递归代理 性能 内存占用减少30%[8] 全量递归转换
三、典型使用示例
javascript
// 创建响应式对象(Vue3底层实现原理简化版)
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`读取 ${key}`);
return Reflect.get(target, key);
},
set(target, key, value) {
console.log(`设置 ${key} 为 ${value}`);
return Reflect.set(target, key, value);
}
});
}
const state = reactive({ count: 0 });
state.count++; // 输出:读取 count → 设置 count 为 1
四、注意事项
- 原始对象保护
Proxy代理会修改原始对象,建议仅对副本进行代理[6][7] - 浏览器兼容性
需注意IE11等旧浏览器不支持Proxy(Vue3默认不兼容IE)[8] - 性能优化
Vue3通过WeakMap缓存代理对象避免重复代理[8][9]
18.浅拷贝和深拷贝的区别,讲一下你平时是怎么使用深拷贝的
一、核心区别
-
浅拷贝
- 定义:仅复制对象的顶层属性,若属性是引用类型(如数组、对象),则复制的是内存地址而非实际数据,新旧对象共享同一块内存[1][4][6]
- 修改影响:修改拷贝对象的引用类型数据时,原对象也会同步变化[1][4][5]
javascript // 示例:JS中浅拷贝 const obj = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, obj); shallowCopy.b.c = 99; // 原对象obj.b.c也会变为99 [4][6]
-
深拷贝
- 定义:递归复制对象的所有层级,包括引用类型的数据,生成完全独立的新对象[3][6][8]
- 修改影响:新旧对象完全隔离,修改互不影响[3][4][8]
javascript // 示例:JS中深拷贝 const deepCopy = JSON.parse(JSON.stringify(obj)); deepCopy.b.c = 100; // 原对象obj.b.c仍为99 [6][8]
二、项目中的深拷贝应用
-
典型场景
- 状态管理:在Redux/Vuex中修改状态时,需深拷贝旧状态生成新对象,避免直接修改原状态[6][8]
- 数据隔离:处理表单数据、配置对象时,深拷贝可防止原始数据被意外污染[6][8]
- 缓存数据:保存数据快照用于撤销/重做功能时,需深拷贝保证历史记录独立性[8][9]
-
实现方式
- JSON方法:
JSON.parse(JSON.stringify(obj))
(不支持函数/循环引用)[6][8] - 递归复制:手动实现递归遍历对象属性进行复制[6][8]
- 工具库:使用
lodash.cloneDeep
或immer
等库处理复杂对象[6][8]
javascript // 使用lodash实现深拷贝 import { cloneDeep } from 'lodash'; const safeCopy = cloneDeep(originalData);
- JSON方法:
使用该方法存在的问题为:
函数丢失
- 对象中的方法会被完全忽略,拷贝后属性消失
const obj = { date: new Date(), regex: /abc/ };
const copy = JSON.parse(JSON.stringify(obj)); // copy.date变为字符串,copy.regex变为{} [9]
简单场景:仍可使用该方法(仅处理基础类型和普通对象/数组时)
复杂场景:使用lodash.cloneDeep
或实现递归拷贝(处理函数/循环引用/特殊对象)
// 使用lodash实现深拷贝 import { cloneDeep } from 'lodash';
const safeCopy = cloneDeep(originalData)
三、面试回答技巧
回答模板:
浅拷贝只复制顶层数据,引用类型共享内存,修改会影响原对象;深拷贝递归复制所有层级,新旧对象完全独立。
项目中在需要数据隔离的场景(如状态管理、表单处理)使用深拷贝,常用JSON方法或工具库实现,但需注意JSON方法的局限性。
手写代码:
let arr = [5, 4, 9, 8]
let obj1 = {
name: 'xxx',
sex: '男',
like: ['红色', '蓝色'],
book: {
title: 'js程序',
price: '88'
}
}
function deepClone(obj) {
//查看要拷贝的对象是数组还是对象,如果数组创建空数组,是对象创建空对象
let newObj = obj instanceof Array ? [] : {}
for (let k in obj) {
//K属性名 obj[k]值
//判断当前每个元素是否是对象或者数组
//如果还是对象,继续递归拷贝
//是值,直接添加到新创建的对象或者数组里
if (typeof obj[k] === 'object') {
//递归拷贝完 放入到新对象的属性
newObj[k] = deepClone(obj[k])
} else {
//否则是值, 直接添加到新建的 newObj中
newObj[k] = obj[k]
}
}
//返回新对象
return newObj
}
var obj2 = deepClone(obj1)
obj2.like.push('紫色')
obj2.book.price = 999
console.log(obj1);
console.log(obj2);
第二种方法简单一点:
// JSON.stringify 简单粗暴
var origin_data = { a: 1, b: 2, c: [1, 2, 3] }
var copy_data = JSON.parse(JSON.stringify(origin_data))
origin_data.a = 3;
origin_data.b = 4;
origin_data.c[0] = '呵呵哒';
console.log(origin_data)
console.log(copy_data)
19.TS相关面试题
一、基础核心概念
-
基本类型
- 7种原始类型:
number
/string
/boolean
/null
/undefined
/symbol
/bigint
- 扩展类型:
array
/tuple
/enum
/any
/void
/never
/object
let age: number = 25; // 数字类型[4][8][9] let list: number[] = [1,2,3]; // 数组类型[8]
- 7种原始类型:
-
接口(Interface) vs 类型别名(Type)
特性 接口 类型别名 扩展方式 通过继承(extends) 通过交叉类型(&) 适用场景 描述对象结构/类契约[1][8] 任意类型别名[2][3][8] 合并声明 支持同名合并 不允许重复声明 interface User { name: string } // 接口定义[1] type ID = string | number; // 类型别名[2]
二、高频面试题解析
-
TS与JS的核心区别
- 静态类型检查 vs 动态类型[5][8]
- 支持接口/泛型/装饰器等OOP特性[5][8]
- 编译时错误提示,提升代码健壮性[2][3]
-
类型断言(Type Assertion)
两种语法实现类型强制声明:let str = <string>someValue; // 尖括号语法[4] let len = (someValue as string).length; // as语法[4]
-
泛型应用场景
实现组件复用同时保持类型安全:function identity<T>(arg: T): T { return arg; } // 泛型函数[1][8] interface GenericArray<T> { // 泛型接口 data: T[]; }
-
联合类型 vs 交叉类型
- 联合类型:
string | number
(多选一)[1][8] - 交叉类型:
Person & Serializable
(合并属性)[1][7]
- 联合类型:
三、高级特性
-
工具类型
type PartialUser = Partial<User>; // 所有属性可选[7] type ReadonlyUser = Readonly<User>; // 只读属性[7] type UserNames = Pick<User, 'name'>; // 属性筛选[7]
-
装饰器(Decorators)
通过@
语法实现元编程:@sealed // 类装饰器 class Person { @log // 方法装饰器 greet() { /*...*/ } }
四、实战技巧
-
错误处理
- 使用
strict
编译选项开启严格模式[2][3] - 通过
@ts-ignore
临时忽略类型错误(慎用)
- 使用
-
项目迁移策略
- 逐步添加
tsconfig.json
配置文件 - 将
.js
文件重命名为.ts
并修复类型错误 - 使用
declare
声明第三方库类型[2][3]
- 逐步添加
20.讲一下事件循环和如何阻止事件冒泡
- JavaScript 事件循环 JavaScript 是单线程执行的,这意味着在任何给定的时间点,只能执行一个任务。然而,在现代 web 应用程序中,我们经常需要处理异步操作,如网络请求、定时器等。为了管理这些异步操作,JavaScript 引入了事件循环(Event Loop)的概念。
1.1 基本概念 事件循环负责协调 JavaScript 引擎与 Web API 之间的通信。它遵循以下简单的流程:
执行栈(Call Stack):这是一个数据结构,用来存储当前正在执行的函数。每当调用一个函数时,它会被压入栈顶;当函数执行完毕后,它会从栈顶弹出。 消息队列(Message Queue):这是一个保存待处理任务的数据结构。当异步任务完成时,它们会被放入消息队列。 事件循环:检查执行栈是否为空。如果为空且消息队列中有任务,则将队列中的第一个任务推送到执行栈中执行。
考虑一个简单的例子,演示事件循环的工作原理:
Javascript
深色版本
1console.log('Start');
2
3setTimeout(() => {
4 console.log('Timeout');
5}, 0);
6
7console.log('End');
输出结果将是:
深色版本
1Start
2End
3Timeout
这里,setTimeout 函数并不会立即执行,而是被放入消息队列等待执行栈空闲时再执行。
一、事件冒泡机制详解
定义:当DOM元素触发事件时(如点击),事件会从触发元素逐级向上传播至根节点,依次触发父元素的同类事件处理函数。例如点击子元素时,父元素、祖父元素等会依次触发点击事件[1][2][4][5]。
传播过程:
- 捕获阶段(可选):从根节点向下传递至目标元素(需通过
addEventListener
的capture:true
开启)[3] - 目标阶段:触发目标元素的事件处理函数
- 冒泡阶段:事件从目标元素向上逐层传递至根节点(默认阶段)[1][4]
二、阻止事件冒泡的5种方法
-
event.stopPropagation()
-
作用:阻止事件继续向上冒泡,不影响当前元素的其他事件监听器[1][3][5]
-
示例:
javascript button.addEventListener('click', (e) => { e.stopPropagation(); // 阻止父元素接收点击事件[1][3] });
-
-
event.stopImmediatePropagation()
- 增强版:不仅阻止冒泡,还会阻止当前元素的其他同类型事件监听器执行[1][3]
-
return false
- 特性:在jQuery中同时阻止冒泡和默认行为;原生JS中需配合
event.preventDefault()
[2][5][6][8] - 注意:仅适用于通过
onclick
属性或jQuery绑定的事件[5][7]
- 特性:在jQuery中同时阻止冒泡和默认行为;原生JS中需配合
-
事件代理优化
-
策略:在父元素监听事件,通过
event.target
判断来源,避免子元素冒泡影响[4] -
示例:
javascript document.getElementById('parent').addEventListener('click', (e) => { if (e.target.tagName === 'BUTTON') { // 仅处理按钮点击,避免其他子元素冒泡干扰[4] } });
-
-
兼容IE的写法
-
方案:统一处理
event.cancelBubble
属性(IE8及以下)[5][8][9] -
封装函数:
javascript function stopBubble(e) { e = e || window.event; if (e.stopPropagation) e.stopPropagation(); else e.cancelBubble = true; // IE兼容[5][8] }
-
三、阻止冒泡与默认行为的区别
方法 | 阻止冒泡 | 阻止默认行为 | 适用场景 |
---|---|---|---|
event.stopPropagation() | ✔️ | ❌ | 仅需阻止事件传播时[1][6] |
event.preventDefault() | ❌ | ✔️ | 阻止表单提交/链接跳转[6] |
return false | ✔️ | ✔️ | jQuery场景下的快捷操作[6][8] |
四、应用建议
- 优先使用
stopPropagation
:明确需要阻断事件传播时使用,不影响默认行为[1][3] - 慎用
return false
:在原生JS中可能无法完全阻止冒泡,且语义不明确[5][7] - 复杂场景用事件代理:动态元素或批量处理时更高效[4]
通过合理使用上述方法,可精准控制事件传播流程[1][3][5]。
21.函数闭包这个概念知道嘛,哪些时候会碰到闭包这个概念。
一、闭包的核心定义
闭包是能够访问其他函数作用域中变量的函数,即使这些函数已经执行完毕[1][4][5][7]。其本质是函数与其词法作用域的结合体,通过作用域链保留对外部变量的引用[3][6][9]。
二、闭包的形成原理
- 作用域链保留:内部函数被外部函数返回或传递时,会携带外部函数的作用域链[3][6]
- 变量生命周期延长:即使外部函数执行完毕,其变量仍被内部函数引用,不会被垃圾回收[2][5][8]
- 词法作用域特性:函数定义时确定作用域链,而非运行时[3][6]
示例:
javascript
function outer() {
let count = 0;
return function inner() {
return ++count; // 形成闭包,持续访问outer的count变量[1][4]
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
三、5种常见闭包应用场景(面试高频考点)
-
函数返回函数
- 场景:工厂模式、计数器实现[1][4][8]
- 特点:内部函数长期持有外部变量
-
事件处理与异步回调
- 场景:
setTimeout
/addEventListener
中访问外部变量[8]
javascript function initButton() { const btn = document.getElementById('btn'); btn.addEventListener('click', function() { console.log('点击次数统计'); // 闭包保留btn引用[8] }); }
- 场景:
-
模块化与私有变量
- 场景:实现数据封装,防止全局污染[5][8]
javascript const module = (function() { let privateVar = 0; return { get: () => privateVar, add: () => privateVar++ }; })();
-
循环中的闭包陷阱
- 经典面试题:循环中使用
var
导致变量共享问题[4][6]
javascript for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出3次3,需用闭包解决 }, 100); } // 闭包修正 for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); // 输出0,1,2[4] })(i); }
- 经典面试题:循环中使用
-
函数柯里化
- 场景:参数复用、延迟计算[3][6]
javascript function curry(fn) { return function(a) { return function(b) { return fn(a, b); // 闭包保留a的值 }; }; }
四、闭包的关键理解(面试回答要点)
- 不是特殊语法:所有JS函数都是闭包(根据词法作用域)[1][6][9]
- 内存管理注意点:滥用会导致内存泄漏,需及时解除引用[2][8]
- 设计价值:实现模块化、数据隐私、高阶函数等核心编程模式[5][8]
五、快速记忆口诀
“内访外,外已死,作用链,变量活”
(内部函数访问外部变量 → 外部函数已执行完毕 → 作用域链保留 → 变量持续存活)
21.讲一下构造函数的原型和对象的原型是一样的嘛,也说一下原型链。
一、构造函数原型与对象原型的关系
结论:构造函数的原型(prototype
)与实例对象的原型(__proto__
)指向同一个原型对象,但两者是不同维度的概念[1][4][5][7]。
对比维度 | 构造函数的prototype | 实例对象的__proto__ |
---|---|---|
归属对象 | 构造函数独有属性 | 所有对象共有属性 |
作用 | 存储共享方法/属性 | 实现原型链继承的桥梁 |
关系验证 | 实例.__proto__ === 构造函数.prototype [1][4] | 自动指向创建该实例的构造函数的原型对象 |
示例:
javascript
function Person() {}
const p = new Person();
// 验证关系
console.log(p.__proto__ === Person.prototype); // true [1][4]
二、原型链的核心概念
定义:通过对象的__proto__
属性形成的链式结构,用于实现继承和属性查找机制。
核心规则:
- 查找路径:访问对象属性时,若自身不存在,则通过
__proto__
逐级向上查找原型链 - 终点验证:
Object.prototype.__proto__ === null
,原型链终止于null
- 动态继承:原型对象修改会影响所有已创建的实例
原型链示意图:
javascript
实例对象(p)
↓ __proto__
Person.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
三、原型链的工作机制(面试高频考点)
-
属性屏蔽原则
- 若实例与原型链上层有同名属性,优先使用实例自身属性[2][5][7]
javascript Person.prototype.name = "原型"; const p = new Person(); p.name = "实例"; console.log(p.name); // "实例"(优先访问自身属性)
-
构造函数关系验证
Person.prototype.constructor === Person
(原型对象通过constructor指向构造函数)[2][7]p.constructor === Person
(通过原型链继承得到)[7]
-
跨级原型链访问
javascript console.log(p.toString()); // 来自Object.prototype [5][8]
四、关键结论(面试回答要点)
- 原型一致性:构造函数的
prototype
与实例的__proto__
指向同一对象[1][4][7] - 原型链本质:基于
__proto__
形成的继承链,实现属性和方法的共享[2][5][8] - 设计价值:解决构造函数模式的内存浪费问题,支持继承和多态[6][7][9]
五、快速记忆口诀
“构造函数造实例,prototype存共享,对象proto连成链,层层查找终到null” [2][5][7]
22.js中有哪些方法会改变原数组而哪些不会改变。
一、改变原数组的核心方法
-
增删类操作
push()
:末尾添加元素(返回新长度)[1][2][3][5][6][7]pop()
:删除末尾元素(返回被删元素)[1][2][3][5][6][7]shift()
:删除首元素(返回被删元素)[1][2][3][5][6][7]unshift()
:开头添加元素(返回新长度)[1][2][3][5][6][7]splice()
:增删改元素(返回被删元素数组)[1][2][3][4][5][9]
-
顺序调整类
sort()
:元素排序(返回排序后数组)[1][2][3][4][5][7]reverse()
:反转数组顺序(返回反转后数组)[1][2][3][4][5][7]
-
填充替换类
fill()
:填充指定值[1][2][3][5][7]copyWithin()
:内部元素复制替换[1]
二、典型场景示例
javascript
// splice示例(改变原数组)
const arr = [1,2,3];
arr.splice(1, 1); // 删除索引1的元素
console.log(arr); // [1,3] [来源1][9]
// sort示例(原地排序)
const nums = [3,1,2];
nums.sort();
console.log(nums); // [1,2,3] [来源4][7]
三、易混淆方法对比
改变原数组的方法 | 不改变原数组的方法 |
---|---|
push/pop | concat |
splice | slice |
fill | map |
reverse | filter |
sort | reduce |
23.qiankun框架如何集成子应用如何配置。
一、主应用配置(3大核心步骤)
-
安装依赖
yarn add qiankun # 或 npm install qiankun -S [6]
-
注册子应用(关键参数)
javascript registerMicroApps([ { name: 'sub-app', // 【唯一标识】必须与子应用package.json的name一致 [7] entry: '//子应用地址', // 【入口地址】开发用localhost,生产用域名 [5][6] container: '#container',// 【挂载节点】主应用HTML中必须存在该DOM元素 [9] activeRule: '/sub-path' // 【路由规则】匹配子应用激活路径 [2][5] } ]); start(); // 启动qiankun [5]
-
容器配置
html <div id="container"></div>
二、子应用配置(3个改造重点)
-
导出生命周期钩子
javascript // 必须导出bootstrap/mount/unmount三个函数 [5][7] export async function mount(props) { // 挂载逻辑:将子应用渲染到props.container指定的DOM节点 [9] ReactDOM.render(<App/>, props.container.querySelector('#root')); }
-
动态设置publicPath
javascript // 入口文件顶部添加 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; [7] }
-
Webpack跨域配置
javascript // vue.config.js / webpack.config.js devServer: { headers: { 'Access-Control-Allow-Origin': '*' } // 允许主应用跨域访问 [6][7] }
24.对于鉴权,子模块和父模块之间是如何通信的,父模块的登录的token如何保存并与子应用共享
一、通信机制实现(2种核心方式)
-
全局状态管理
主应用通过initGlobalState
创建共享状态池,子应用通过onGlobalStateChange
监听变化[5][9]javascript // 主应用设置token import { initGlobalState } from 'qiankun'; const actions = initGlobalState({ auth: { token: 'xxx' } }); // 子应用监听变化 export function mount(props) { props.onGlobalStateChange((state) => { console.log('收到token:', state.auth.token); }, true); }
-
Props透传机制
注册子应用时通过props直接注入token[5][9]javascript // 主应用配置 registerMicroApps([{ name: 'subapp', entry: '//sub.domain.com', props: { token: localStorage.getItem('token') // 直接传递存储的token } }]);
二、Token存储与共享方案
-
安全存储方式
- 主应用登录后采用
HttpOnly Cookie
存储(防XSS攻击)[9] - 敏感系统可结合AES加密后存
sessionStorage
(会话级隔离)[9]
- 主应用登录后采用
-
跨应用同步策略
javascript // 主应用登录成功后 document.cookie = `token=xxx; Path=/; HttpOnly; Secure`; // 子应用获取方式 const token = document.cookie.split('; ').find(row => row.startsWith('token='))?.split('=')[1];
三、架构设计建议
- 中小型项目:优先使用
props透传
+Cookie存储
- 大型系统:采用
全局状态管理
+服务端鉴权
- 安全要求高:必须使用
HttpOnly Cookie
,配合网关统一鉴权
25.多个tab页面如何共享一个websoket对象(多个tab页面做登录操作就会互相挤掉)。
解决方案(基于Web技术标准):
-
主从模式
指定一个主Tab维护WebSocket连接,其他Tab通过BroadcastChannel
或localStorage
事件与主Tab通信[9] -
共享存储
使用localStorage
同步状态,所有Tab监听storage事件:javascript // 主Tab const ws = new WebSocket(url); localStorage.setItem('wsMaster', 'active'); // 其他Tab window.addEventListener('storage', (e) => { if(e.key === 'wsMessage') { // 处理来自主Tab的消息 } });
-
服务端优化
支持同一用户多设备同时在线(需服务端改造)
典型场景:金融交易系统/实时协作工具常用方案[9]
26.react和vue对象改变了,但是数据没有渲染(可能有哪些原因造成的)。
在使用Vue.js时,数据有时不会渲染有几个关键原因:
1、数据未被Vue实例监测到
**在Vue实例创建之后添加新的属性**:Vue无法检测到在实例创建之后添加的新属性,因为这些属性不是响应式的。
let vm = new Vue({
data: {
a: 1
}
});
vm.b = 2; // 这不会触发视图更新
**使用`Object.assign`或直接修改对象属性**:这会直接替换对象,Vue无法检测到这种变化。
vm.someObject = Object.assign({}, vm.someObject, { newProperty: 'new value' }); // 不会触发视图更新
**解决方案**:
使用`Vue.set`方法添加新属性:
Vue.set(vm.someObject, 'newProperty', 'new value');
this.$set(this.someObject, 'newProperty', 'new value');
2、数据未绑定到模板中 数据必须在模板中进行绑定,否则Vue不会知道需要更新视图。这种情况下,即使数据发生变化,也不会影响视图。
<template>
<div></div> <!-- 未使用数据 -->
<!-- 标签中的数据未定义未定义或拼写错误也不会渲染 -->
</template>
3、数据更新未触发视图更新 Vue.js依赖于数据响应式系统,但有时数据更新可能不会触发视图更新,尤其是在处理数组和对象时。
vm.items[indexOfItem] = newValue; // 不会触发视图更新
直接修改数组索引:直接修改数组的某个索引不会触发视图更新。
- 使用
Array.prototype.push
、pop
等方法:这些方法会触发视图更新,但其他数组方法如splice
、sort
等可能不会。
使用响应式方法,如Vue.set
或this.$set
:
4、数据依赖问题
computed: {
correctComputed() {
return this.someData + this.otherData; // 声明所有依赖项已声明
}
}
5、渲染函数或生命周期钩子错误
自定义渲染函数或生命周期钩子中的错误可能导致数据未正确渲染。例如,beforeUpdate
或updated
钩子中的逻辑错误。
6、异步数据未处理好。这些问题可能导致 Vue 无法正确渲染数据。下面将详细解释这些原因并提供解决方案。
在处理异步数据时,如果数据尚未加载完成就试图渲染,可能导致视图未能正确更新。例如,在created
钩子中发起异步请求,但未处理返回数据。
created() {
axios.get('/api/data').then(response => {
this.$set(this, 'someData', response.data); // 确保视图更新
});
}
27.TS里面有哪些基础类型
TypeScript 基础类型详解(截至2025年最新实践)
结合高频面试考点和实际开发场景,TS 基础类型可分为以下两类:
一、JavaScript 原有基础类型
-
boolean
布尔值,仅接受true/false
:let isDone: boolean = false; // [1][2][6]
-
number
支持十进制、二进制、十六进制等:let hex: number = 0xf00d; // [1][2]
-
string
支持模板字符串:let name: string = `Gene`; let sentence: string = `Hello, ${name}`; // [2][6]
-
Array
两种声明方式:let list1: number[] = [1,2,3]; let list2: Array<number> = [1,2,3]; // [2][6]
-
null
/undefined
所有类型的子类型(需注意严格模式):let u: undefined = undefined; // [3][6]
二、TypeScript 新增核心类型
-
Tuple
(元组)
固定长度和类型的数组:let x: [string, number] = ["hello", 10]; x[0].substring(1); // 正确 x[1].toFixed(2); // 正确 x[2] = "world"; // 错误!
-
enum
(枚举)
默认从0开始自增,支持反向映射:enum Direction { Up, Down=2, Left } console.log(Direction.Up); // 0 console.log(Direction[2]); // "Down"
-
any
绕过类型检查(慎用):let notSure: any = 4; notSure = "maybe a string";
-
unknown
类型安全的any
(需类型断言):let value: unknown = "hello"; let str: string = value as string;
-
void
函数无返回值:function warn(): void { console.log("Warning!"); }
-
never
表示永不存在的值(常用于错误处理):function error(message: string): never { throw new Error(message); }
三、高频面试扩展
- 联合类型:
string | number
- 字面量类型:
type Status = "success" | "fail"
- 类型断言:
<string>value
或value as string
- 类型别名:
type Point = { x: number; y: number }
实际应用场景:
- 枚举替代魔法字符串(如状态码)
- 元组处理CSV文件等固定结构数据
unknown
替代any
提升类型安全性
28.v-for和v-if为什么不能一起用呢为什么会报错一起用
v-for与v-if不能同时使用的原因及解决方案(截至2025年Vue最佳实践)
一、核心原因
-
优先级冲突
Vue2中v-for优先级高于v-if,导致每次渲染时:- 先执行完整列表循环 → 再对每个元素执行条件判断 → 即使大部分元素无需渲染也会被遍历
-
性能损耗
当列表数据量大时,无用的循环计算会导致严重性能浪费(如1000条数据中仅需渲染10条) -
语法规范限制
Vue官方规范明确禁止该用法,启用ESLint等工具时会直接报错:The 'undefined' variable inside 'v-for'...(错误示例见[2][3])
二、解决方案
-
外层包裹
<template>
将条件判断提升到循环外部:html <template v-if="isShowList"> <div v-for="item in items" :key="item.id"></div> </template>
(通过隔离作用域避免性能损耗 [5][7])
-
计算属性过滤
预处理需要渲染的数据:javascript computed: { filteredItems() { return this.items.filter(item => item.isActive) } }
html <div v-for="item in filteredItems" :key="item.id"></div>
(推荐方案,符合响应式特性 [1][6][9])
-
Vue3优化方案
Vue3中v-if优先级高于v-for,但仍建议保持分离写法:html <div v-if="shouldRender"> <div v-for="item in items"></div> </div>
(版本差异需注意 [4][7])
三、高频面试扩展
- Diff算法影响:无效的DOM节点创建/销毁会增加虚拟DOM比对复杂度 [3][8]
- 代码可维护性:分离逻辑使代码更易理解(条件判断与数据展示解耦 [9])
- SSR优化:服务端渲染时无效循环会增加首屏渲染时间 [8]
典型错误示例:
html
<ul>
<li>{{ user.name }}</li>
</ul>
(正确改造方案见[6][7])
2025年最新建议:使用Composition API的computed
+ <template>
组合实现条件循环,兼顾性能与可维护性。
29.vue中的代理和反代理(讲一下其中的原理)
一、Vue开发环境代理(正向代理)
核心作用:解决前端开发中的跨域问题,将API请求代理到目标服务器[1][2]
实现原理:
-
通过
vue.config.js
配置开发服务器代理规则:javascript // [1]示例配置 devServer: { proxy: { '/api': { target: 'http://api.yourservice.com', // 目标服务器地址 changeOrigin: true, // 伪造Host头(绕过服务端校验) pathRewrite: {'^/api': ''} // 路径重写规则 } } }
-
浏览器请求
/api/user
→ Vue开发服务器拦截 → 转发到http://api.yourservice.com/user
[1][2] -
本质是正向代理:代理客户端请求,服务端无法感知真实客户端(开发者的浏览器)[3][6][9]
二、反向代理原理
与Vue开发代理的区别:
正向代理 | 反向代理 | |
---|---|---|
代理对象 | 客户端 | 服务端 |
典型场景 | 开发环境跨域、VPN | 生产环境负载均衡、Nginx |
可见性 | 服务端看不到真实客户端 | 客户端看不到真实服务端 |
反向代理核心功能:
- 负载均衡:将请求分发到多个服务器(如Nginx轮询策略)[3][6][9]
- 安全防护:隐藏真实服务器IP,防止直接攻击[3][9]
- 静态资源缓存:提升高频访问内容的响应速度[3][6]
- SSL终端:统一处理HTTPS加密/解密[9]
三、高频面试扩展
-
为什么需要路径重写(pathRewrite)
用于去除代理标识(如示例中的/api
),使目标服务器收到规范路径[1][2] -
changeOrigin参数作用
修改请求头中的Host
字段为目标服务器域名,避免服务端拒绝跨域请求[1][2] -
生产环境如何实现反向代理
使用Nginx配置:location /api { proxy_pass http://backend-server; proxy_set_header Host $host; }
(通过反向代理实现服务集群管理[6][9])
-
与Vue响应式系统的关联
代理配置仅影响网络请求,与Vue的Object.defineProperty
/Proxy
响应式原理无关[7]
2025最新实践建议:开发环境使用Vue CLI代理解决跨域,生产环境通过Nginx反向代理实现服务治理,两者配合保障全链路安全与性能
30.webpack和vite的区别(vite为什么快一些呢)
一、核心区别对比
维度 | Webpack | Vite |
---|---|---|
启动机制 | 先打包所有模块再启动服务器[1][2][4] | 直接启动服务器,按需编译[1][2][7] |
构建语言 | 基于Node.js(毫秒级)[2][4][7] | 预构建用Esbuild(Go语言,纳秒级)[1][2][4] |
热更新 | 需重新构建相关依赖链[3][6][7] | 仅更新修改文件,无需全量编译[1][3][6] |
生产构建 | 默认打包工具 | 使用Rollup(可切换Webpack)[4][7][9] |
生态成熟度 | 插件丰富,生态完善[3][4] | 生态逐步完善,但部分场景仍需Webpack[3][9] |
二、Vite更快的原因
-
ESM原生支持
直接利用浏览器ES Module特性,无需打包即可运行源码,启动速度提升10倍以上[1][2][4][7]。javascript // 浏览器直接请求ES模块 import { component } from '/src/App.vue?t=1623345' // Vite动态编译
-
按需编译(On-demand Compilation)
仅编译当前请求的模块,避免全量打包:- Webpack:1000个模块需全量分析 → 启动慢[1][2][6]
- Vite:首屏仅编译10个模块 → 毫秒级响应[2][4][7]
-
Esbuild预构建
Go语言编写的Esbuild比Node.js快10-100倍,完成依赖预构建仅需秒级[1][2][4][7]。 -
内存热更新优化
模块编译结果直接存于内存,避免磁盘IO开销,HMR更新速度提升50%+[6][7]。 -
动态依赖解析
通过浏览器发起模块请求,天然支持代码分割,对比Webpack的静态分析更高效[6][8]。
三、适用场景建议
-
Vite首选场景:
- 新项目开发,尤其是Vue/React SPA
- 需要快速启动和热更新的高频迭代项目[3][4][7]
-
Webpack仍适用场景:
- 遗留项目维护
- 需要复杂自定义构建配置(如微前端)[3][9]
2025趋势:Vite已逐步成为新项目开发标准工具,但Webpack凭借成熟生态仍在大型企业级项目中广泛使用,Rspack等Rust工具正在崛起[9]。
31.js中的异步操作是怎么样的(举个例子)。
一、什么是异步操作?
就像餐厅点餐:
- 同步:排队点餐 → 等厨师做完 → 再点下一单(卡住后续操作)[4][8]
- 异步:点餐后拿号码牌 → 继续服务其他客人 → 餐好了叫号通知(不阻塞后续代码)[2][4]
核心特点:代码执行不阻塞,耗时操作完成后通过回调通知[2][4][8]
二、常见异步场景(附代码示例)
-
定时器(
setTimeout
/setInterval
)javascript console.log("开始点餐"); // 同步 setTimeout(() => { console.log("您的汉堡做好了🍔"); // 异步回调 }, 2000); console.log("继续打扫卫生"); // 不会等待2秒 // 输出顺序:开始 → 打扫 → 汉堡
-
网络请求(AJAX/Fetch)
javascript fetch('https://api.example.com/data') // 异步请求 .then(response => response.json()) // Promise链式调用[1][6] .then(data => console.log("收到数据:", data)) .catch(err => console.error("出错:", err));
-
事件监听(DOM事件)
javascript button.addEventListener('click', () => { // 异步响应点击 console.log("按钮被点击了!"); });
三、异步解决方案演进
-
回调函数(Callback Hell)
javascript getData(data => { // 第1层回调 process(data, result => { // 第2层回调 save(result, () => { // 第3层回调 → 嵌套地狱[1][7] console.log("完成!"); }); }); });
-
Promise(链式调用)
javascript getData() .then(process) // 清晰的任务链[1][6] .then(save) .then(() => console.log("完成!")) .catch(handleError);
-
async/await(同步写法)
javascript async function orderFood() { try { const data = await getData(); // 等待异步完成[3][8] const result = await process(data); await save(result); console.log("完成!"); } catch (err) { handleError(err); } }
四、为什么需要异步?
- 单线程限制:JavaScript主线程像单车道,异步让耗时操作(如IO)靠边停车,避免堵住后续车辆[3][8]
- 性能优化:浏览器UI渲染、接口请求等操作并行处理,提升用户体验[2][4]
32.讲一下箭头函数吧(有什么特点然后什么时候用到的比较多),其中的this指向又是什么样子的呢
一、核心特点
-
简洁语法
- 省略
function
关键字,单参数可省括号,单行代码可省return
[1][2]
javascript // 传统函数 → 箭头函数简化 const sum = function(a, b) { return a + b; } const sum = (a, b) => a + b; // [1][2]
- 省略
-
无独立
this
- 继承外层
this
:箭头函数的this
由定义时的外层作用域决定,而非调用时[1][2][8]
javascript const obj = { name: 'Vue', getName: () => console.log(this.name) // this指向window而非obj[8] }
- 继承外层
-
其他限制
- 无
arguments
对象(可用...rest
替代)[1] - 不能作为构造函数(无法用
new
调用)[8] - 无
prototype
属性[8]
- 无
二、典型使用场景
-
回调函数(避免
this
丢失)javascript // 传统函数this指向问题 button.addEventListener('click', function() { console.log(this); // 指向button元素 }); // 箭头函数保持外层this(如Vue组件中常用) button.addEventListener('click', () => { console.log(this); // 指向外层组件实例[1][8] });
-
简化短函数
javascript // 数组方法回调 const nums = [1, 2, 3].map(x => x * 2); // [2][8]
三、this
指向对比
场景 | 传统函数 | 箭头函数 |
---|---|---|
对象方法 | this 指向调用对象 | this 指向外层(如window)[8] |
事件回调 | this 指向触发元素 | this 继承外层定义时的值[1] |
setTimeout 内部 | this 指向window | this 与外部一致[8] |
黄金规则:箭头函数的
this
定义时确定,普通函数的this
调用时确定[
33.网页中的性能是比较差的,如果现在有一个超长数据列表,需要在页面里将其渲染出来,成千上万条数据渲染时页面肯定会卡顿,如果卡顿的话如何优化呢(vue这边利用一个虚拟dom渲染,这种时候是不是也可以用到呢)。
一、卡顿原因分析
- DOM节点爆炸:成千上万条数据直接渲染 → DOM操作耗时(重排/重绘)[2]
- 内存占用过高:每个列表项的响应式数据追踪消耗资源[8]
- 主线程阻塞:同步渲染大量数据导致JS执行时间过长[8]
二、Vue专属优化方案
-
虚拟滚动(核心方案)
- 原理:仅渲染可视区域内的元素(如只保留20条),滚动时动态替换内容[8]
- 实现:使用
vue-virtual-scroller
等库
<RecycleScroller :items="list" :item-size="50" key-field="id"> <template #default="{ item }"> <div>{{ item.content }}</div> </template> </RecycleScroller>
-
减少响应式数据
- 冻结数据:对静态数据使用
Object.freeze
关闭响应式追踪[8]
javascript this.list = Object.freeze(rawData); // 冻结后Vue不再添加getter/setter
- 冻结数据:对静态数据使用
-
组件级优化
- 拆分子组件:避免父组件频繁重新渲染
- 使用
v-memo
:Vue 3特性缓存静态节点[8]
<div v-for="item in list" :key="item.id" v-memo="[item.id]"> {{ item.content }} </div>
三、通用优化手段
-
分页/懒加载
- 监听滚动事件动态加载数据(适合非一次性展示场景)[2]
-
时间分片
- 使用
requestAnimationFrame
分批渲染(避免阻塞主线程)[8]
javascript const renderChunk = (data) => { requestAnimationFrame(() => { renderNext100Items(data); }); };
- 使用
-
Web Worker处理数据
- 将数据排序/过滤等耗时操作放到Worker线程[8]
四、虚拟DOM的作用与局限
- 优势:通过diff算法减少不必要的DOM操作[8]
- 局限:虚拟DOM的diff计算本身也有开销,超长列表仍需结合虚拟滚动等方案
优化效果对比
方案 | DOM节点数 | 内存占用 | 适用场景 |
---|---|---|---|
直接渲染 | 10000+ | 高 | 极小数据量 |
虚拟滚动 | 20~50 | 低 | 超长列表 |
分页加载 | 每页100 | 中 | 可分块加载场景 |
34.讲一下vue这边的组件和插件的区别吧,平时用到的插件有哪些呢
一、核心区别对比
维度 | 组件 | 插件 | 来源 |
---|---|---|---|
功能定位 | 构建UI的独立单元(如按钮、表单) | 扩展Vue全局功能(路由、状态管理) | [1][4] |
注册方式 | 局部注册(components)或全局注册 | 必须通过Vue.use() 全局安装 | [2][6] |
作用范围 | 局部作用(仅在注册范围内生效) | 全局生效(影响整个Vue应用) | [1][3] |
典型内容 | 包含模板、逻辑、样式三要素 | 可含全局方法/指令/混入/实例方法等 | [3][7] |
二、常用Vue插件及作用
-
Vue Router
- 作用:实现SPA路由管理,提供
<router-view>
等组件[6][8] - 安装:
Vue.use(VueRouter)
- 作用:实现SPA路由管理,提供
-
Vuex
- 作用:集中式状态管理,提供
store
全局访问[6][8] - 核心:
state/mutations/actions/getters
- 作用:集中式状态管理,提供
-
Element UI / Vuetify
- 作用:提供全局UI组件库(如表格、弹窗)[3][6]
- 特性:通过插件一次性注册所有组件
-
Vue-i18n javascript Vue.use(VueI18n); // 注册后可在任意组件使用翻译功能
三、快速记忆口诀
“组件造零件,插件改引擎”
- 组件:UI拼图(局部作用)
- 插件:功能外挂(全局增强)
35.vue的组件之间的通信
1.组件通信常用方式有以下8种:
props
on(vue3废弃)
parent
listeners(vue3废弃)
ref
$root
eventbus
vuex
1.父子
props/parent/ref/$attrs
2.兄弟组件
root/eventbus/vuex
3.跨层级关系
eventbus/vuex/provide+inject