面经原文内容
📍面试公司:MetaAPP
🕐面试时间:近期,用户上传于 2026-03-03
💻面试岗位:前端
💬面试状态:很有压力,面试官 1 请假叫 2 来面,但问题很有启发
❓面试问题:
项目相关
- 自我介绍
- 实习拷打 - 看你组件封装那么多,讲一个最牛逼的吧
- 怎么确保你封装组件之后的 api 兼容性?
CSS/动画 4. css 如何做响应式设计移动端 PC 段 iPad 端? 5. rem 的原理是什么 6. 用 css 实现一个抛物线 7. 复杂运动怎么办?
JS 进阶 8. let a=5 经过一系列操作之后,if(a.b=100 && a=5) 是 true,这一系列操作是什么?(面试官举例 number 强转对象然后原型链类似) 9. promise.all 跟 allseted 区别
Vue 原理 10. vue 组件都是一个实例吗? 11. a 组件定时器跳转到 b,a 的会销毁吗? 12. vue3 响应式 13. 副作用函数知道吗?数据改变原理是什么?在源码的数据格式是什么? 14. 为什么使用 weakmap 15. nexttick 是微任务吗? 16. 低版本浏览器还能用吗?
反问
- 业务:游戏业务,都是 h5 跟官网
- 规模:10 个人
📝 MetaAPP 前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 公司定位 | MetaAPP - 游戏业务(H5/官网) |
| 面试风格 | 项目深潜型 + 原理追问型 + 边界拓展型 |
| 难度评级 | ⭐⭐⭐(三星,从项目到原理到边界问题) |
| 考察重心 | 组件封装能力、响应式布局、动画实现、JS 语言特性、Vue 源码原理 |
| 特殊之处 | 问题很有启发性,尤其是 number 强转对象的题目考察了对 JS 语言本质的理解 |
🔍 逐题深度解析
二、组件封装与 API 兼容性
问题:讲一个最牛逼的组件 + 怎么确保 API 兼容性?
发散性问题,需要平时多做积累,这里我们只复盘一下 API 兼容性设计的内容
// 1. 组件封装的核心原则
// 1.1 组件设计思路
// 以弹窗组件为例
<Modal v-model="visible"
title="提示"
width="500"
:before-close="handleClose"
:on-ok="handleOk"
:on-cancel="handleCancel"
>
<div>内容</div>
</Modal>
// 1.2 API 兼容性设计
// 方案 1:属性透传
// 让用户可以传递任意原生属性
<Button type="primary" :loading="loading" @click="handleClick" custom-prop="value">
按钮
</Button>
// 组件内部
<button v-bind="$attrs" class="buttonClass">
<slot />
</button>
// 方案 2:默认值 + 扩展 props
props: {
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
type: {
type: String,
default: 'default'
}
}
// 方案 3:版本兼容
// 同时支持新旧 API
props: {
// 新 API
modelValue: {},
// 兼容旧 API
value: {}
},
emits: ['update:modelValue', 'input']
三、响应式设计
问题:css 如何做响应式设计移动端 PC 段 iPad 端?
/* 1. 媒体查询 */
/* 移动端 */
@media (max-width: 767px) {
.container {
width: 100%;
padding: 10px;
}
}
/* iPad 端 */
@media (min-width: 768px) and (max-width: 1024px) {
.container {
width: 750px;
margin: 0 auto;
}
}
/* PC 端 */
@media (min-width: 1025px) {
.container {
width: 1200px;
margin: 0 auto;
}
}
/* 2. 弹性布局 */
.container {
display: flex;
flex-wrap: wrap;
}
.item {
flex: 1 1 300px; /* 基础 300px,可伸缩 */
}
/* 3. 栅格系统 */
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 20px;
}
.col-4 {
grid-column: span 4;
}
/* 4. 视口单位 */
.hero {
height: 100vh; /* 视口高度 */
width: 100vw; /* 视口宽度 */
font-size: clamp(16px, 4vw, 24px); /* 动态字体 */
}
/* 5. 图片响应式 */
img {
max-width: 100%;
height: auto;
}
picture {
display: block;
}
source {
media: "(max-width: 767px)" /* 根据屏幕宽度加载不同图片 */
}
/* 6. 容器查询(新特性) */
@container (max-width: 500px) {
.card {
flex-direction: column;
}
}
四、rem 原理
问题:rem 的原理是什么
// 1. rem 定义
// rem = root em,相对于根元素 (html) 的字体大小
// 2. 原理
// 设置根元素字体大小
html { font-size: 16px; } // 默认
// 1rem = 16px
.box {
width: 10rem; /* 160px */
height: 5rem; /* 80px */
}
// 3. 动态 rem 适配
// 根据屏幕宽度动态设置根元素字体大小
(function flexible() {
function setRem() {
const width = document.documentElement.clientWidth; // 设计稿宽度 750px,分成 7.5 份,1rem = 100px
const rem = width / 7.5;
document.documentElement.style.fontSize = rem + 'px';
}
setRem();
window.addEventListener('resize', setRem);
})();
// 4. postcss-pxtorem 插件
// 在 webpack 中配置,自动将 px 转换为 rem
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue: 75, // 设计稿宽度/10
propList: ['*']
}
}
};
// 5. rem vs vw/vh
// rem:基于根元素,需要 JS 动态设置
// vw:基于视口宽度,纯 CSS,不需要 JS
五、CSS 实现抛物线
问题:用 css 实现一个抛物线
/* 1. 使用 transform + 动画 */
.ball {
width: 50px;
height: 50px;
background: red;
border-radius: 50%;
animation: parabola 2s ease-out forwards;
}
@keyframes parabola {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(300px, 200px);
}
}
/* 2. 更精确的抛物线(使用贝塞尔曲线) */
@keyframes parabola-bezier {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(300px, 200px);
}
}
.ball-bezier {
animation: parabola-bezier 2s cubic-bezier(0.2, 0.8, 0.4, 1) forwards;
}
/* 3. 使用两个动画组合(水平匀速 + 竖直加速) */
.ball-combo {
animation: horizontal 2s linear forwards, vertical 2s ease-in forwards;
}
@keyframes horizontal {
to {
transform: translateX(300px);
}
}
@keyframes vertical {
to {
transform: translateY(200px);
}
}
/* 4. 使用 JS 动态计算(更精确) */
function parabola(element, start, end, duration) {
const startTime = performance.now();
function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 抛物线公式:y = ax² + bx
const x = start.x + (end.x - start.x) * progress;
const y = start.y + (end.y - start.y) * Math.pow(progress, 2);
element.style.transform = `translate(${x}px, ${y}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
六、复杂运动处理
问题:复杂运动怎么办?
// 1. Web Animation API
const element = document.querySelector('.ball');
const animation = element.animate([
{ transform: 'translate(0, 0)' },
{ transform: 'translate(300px, 200px)' }
], {
duration: 2000,
easing: 'cubic-bezier(0.2, 0.8, 0.4, 1)',
iterations: 1
});
animation.onfinish = () => {
console.log('动画完成');
};
// 2. GSAP(GreenSock)
gsap.to('.ball', {
x: 300,
y: 200,
duration: 2,
ease: 'power2.out',
onUpdate: () => { /* 每一帧回调 */ },
onComplete: () => {
console.log('完成');
}
});
// 3. 使用 Canvas(适合复杂物理模拟)
class Ball {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 5;
this.vy = -10;
this.gravity = 0.5;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += this.gravity; // 碰撞检测
if (this.y > canvas.height - 50) {
this.y = canvas.height - 50;
this.vy *= -0.7; // 能量损失
}
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, 25, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
}
}
// 4. 使用 Framer Motion(React)
<motion.div
animate={{ x: 300, y: 200, rotate: 360, scale: [1, 2, 1] }}
transition={{ duration: 2, ease: "easeInOut", times: [0, 0.5, 1], loop: Infinity }}
/>
七、JS 语言特性(第 8 题)
问题:let a=5,如何使 if(a.b=100 && a=5) 为 true?
// 这道题考察的是 JS 的包装对象和原型链
// 1. 背景知识
// JS 中,基本类型(number、string、boolean)有对应的包装对象
// 当访问基本类型的属性时,JS 会临时创建一个包装对象
let a = 5;
console.log(a.toString()); // 临时创建 Number 对象
// 2. 解题思路
// 我们需要让 a.b = 100 这个赋值操作成功,同时不改变 a 本身的值
// 3. 解决方案:修改 Number 原型
// 给 Number 原型添加 setter
Object.defineProperty(Number.prototype, 'b', {
set(value) {
console.log('设置 b 为', value);
// 这里可以什么都不做,或者返回什么
},
get() {
return 100; // 让 a.b 返回 100
}
});
let a = 5;
// 现在执行 if(a.b = 100 && a === 5){ console.log('true') }
// 执行过程
// 1. a.b = 100:访问 a.b,触发 getter 返回 100,赋值操作成功
// 2. 100 && a === 5:100 为真,继续判断 a === 5
// 3. a === 5 为真
// 4. 整个条件为真
// 4. 更完整的解法
Number.prototype.b = 100; // 直接设置属性
let a = 5;
console.log(a.b); // 100(临时包装对象访问到原型上的 b)
// 现在条件成立
if (a.b = 100 && a === 5) {
console.log('条件成立');
}
// 5. 原理总结
// - 基本类型访问属性时,会创建临时包装对象
// - 包装对象从原型链上继承属性
// - 通过修改原型,可以控制基本类型的属性访问行为
八、Promise.all vs allSettled
问题:promise.all 跟 allseted 区别
// 1. Promise.all
// - 全部成功才成功
// - 一个失败就失败
const promises = [
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
];
try {
const [user, posts, comments] = await Promise.all(promises); // 全部成功才能走到这里
} catch (error) {
// 任何一个失败就会进入 catch
}
// 2. Promise.allSettled
// - 等待所有完成
// - 返回每个 promise 的状态和结果
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求${index}成功:`, result.value);
} else {
console.log(`请求${index}失败:`, result.reason);
}
});
// 3. 返回结果对比
// Promise.all: [userData, postsData, commentsData]
// Promise.allSettled: [{status:'fulfilled',value: userData}, {status:'rejected',reason: error}, {status:'fulfilled',value: commentsData}]
// 4. 适用场景
// Promise.all:依赖所有请求结果的场景
// 如:需要用户信息和帖子信息才能渲染页面
// Promise.allSettled:不关心个别失败,需要所有结果的场景
// 如:批量上传文件,想看看哪些成功了哪些失败了
九、Vue 组件实例
问题:vue 组件都是一个实例吗?
// 1. 组件与实例的关系
// 每个组件在使用时都会创建一个实例
// 2. 不同情况
// 2.1 单次使用
// <MyComponent /> -> 创建一个实例
// 2.2 多次使用
// <div v-for="item in 3" :key="item"><MyComponent /></div> -> 创建 3 个独立实例
// 2.3 动态组件
// <component :is="currentComponent"/> -> 每次切换创建新实例或复用(取决于 keep-alive)
// 3. 实例标识
export default {
mounted() {
console.log(this._uid); // 每个实例有唯一 id
console.log(this === this.$root); // 判断是否是根实例
}
};
// 4. 单例组件特殊情况
// 通过 Vue.extend 创建的可复用构造函数
const ComponentClass = Vue.extend({
data() {
return { count: 0 };
}
});
// 每次 new 都会创建新实例
const instance1 = new ComponentClass();
const instance2 = new ComponentClass();
十、组件销毁与定时器
问题:a 组件定时器跳转到 b,a 的会销毁吗?
// 1. 路由切换时的组件生命周期
// 组件 A
export default {
data() {
return {
timer: null
};
},
mounted() {
this.timer = setInterval(() => {
console.log('A 组件定时器执行');
}, 1000);
},
// 路由离开时会调用 beforeDestroy
beforeDestroy() {
// 需要手动清理定时器
if (this.timer) {
clearInterval(this.timer);
console.log('定时器已清理');
}
},
destroyed() {
console.log('A 组件已销毁');
}
};
// 2. 如果不清理会发生什么?
// 组件 A 虽然销毁了,但定时器还在执行
// 造成内存泄漏,定时器回调中的 this 会报错
// 3. 最佳实践
export default {
mounted() {
// 方案 1:在 beforeDestroy 中清理
this.timer = setInterval(this.tick, 1000);
},
beforeDestroy() {
clearInterval(this.timer);
},
// 方案 2:使用$once 监听 hook 事件
mounted() {
const timer = setInterval(this.tick, 1000);
this.$once('hook:beforeDestroy', () => {
clearInterval(timer);
});
},
// 方案 3:使用 keep-alive 的特殊处理
deactivated() {
// 组件被缓存时暂停定时器
clearInterval(this.timer);
},
activated() {
// 组件被激活时重启
this.timer = setInterval(this.tick, 1000);
}
};
十一、Vue3 响应式
问题:vue3 响应式 + 副作用函数 + 数据格式 + WeakMap
// 1. 副作用函数
// 副作用函数是指会对外部环境产生影响的函数
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn(); // 执行时会触发依赖收集
activeEffect = null;
}
// 2. 响应式原理
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
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);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn());
}
// 3. 为什么使用 WeakMap?
// - WeakMap 的 key 是弱引用,不影响垃圾回收
// - 当 target 对象不再被使用时,可以被正常回收
// - 防止内存泄漏
// 4. 源码中的数据结构
// targetMap: WeakMap<target, Map<key, Set<effect>>>
// {
// target1: Map{'name': Set[effect1, effect2], 'age': Set[effect3]},
// target2: Map{'title': Set[effect4]}
// }
// 5. Vue3 响应式完整示例
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
return value;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
十二、nextTick
问题:nexttick 是微任务吗?
// 1. nextTick 的作用
// 在下次 DOM 更新循环结束后执行回调
// 2. 实现原理
// Vue 的 nextTick 会根据环境选择不同的实现
// 优先使用微任务,降级使用宏任务
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
copies.forEach(cb => cb());
}
let timerFunc;
// 优先级:Promise > MutationObserver > setImmediate > setTimeout
if (typeof Promise !== 'undefined') {
// 微任务
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (typeof MutationObserver !== 'undefined') {
// 微任务
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, { characterData: true });
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== 'undefined') {
// 宏任务
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 宏任务
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb, ctx) {
callbacks.push(() => {
if (cb) cb.call(ctx);
});
if (!pending) {
pending = true;
timerFunc();
}
}
// 3. 使用示例
this.message = 'updated';
this.$nextTick(() => {
console.log('DOM 已更新');
});
十三、低版本浏览器兼容
问题:低版本浏览器还能用吗?
// 1. Vue3 的浏览器兼容性
// Vue3 不支持 IE11
// 支持:Chrome、Firefox、Edge、Safari 等现代浏览器
// 2. 解决方案
// 2.1 使用 Vue2
// Vue2 支持 IE9+
// 2.2 引入 polyfill
// 在 index.html 中引入
// <!--[if IE]><script src="https://polyfill.io/v3/polyfill.min.js"></script><![endif]-->
// 2.3 配置 babel
module.exports = {
presets: [
['@babel/preset-env', {
targets: { ie: '11' },
useBuiltIns: 'usage',
corejs: 3
}]
]
};
// 2.4 动态降级
if (isIEBrowser()) {
// 加载 Vue2 版本
import('./vue2-app.js');
} else {
// 加载 Vue3 版本
import('./vue3-app.js');
}
// 3. 需要支持的 ES6 特性
// - Promise: 需要 polyfill
// - Proxy: 无法 polyfill(Vue3 核心)
// - Map/Set: 可 polyfill
// - Symbol: 可 polyfill
🧠 面试复盘
面试特点总结
| 类型 | 问题 | 考察点 |
|---|---|---|
| 项目深挖 | 组件封装、API 兼容性 | 工程实践能力 |
| CSS 基础 | 响应式、rem、抛物线 | 布局和动画能力 |
| JS 语言 | 包装对象、原型链 | 语言本质理解 |
| Promise | all/allSettled | 异步编程能力 |
| Vue 原理 | 响应式、nextTick、WeakMap | 源码理解深度 |
业务方向
- 游戏业务,H5 和官网开发
- 团队规模 10 人
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 组件封装 | 属性透传、默认值、版本兼容 |
| 响应式设计 | 媒体查询、弹性布局、栅格、视口单位 |
| rem 原理 | 根元素字体、动态适配、px 转 rem |
| 抛物线 | 组合动画、贝塞尔曲线、JS 计算 |
| 包装对象 | 基本类型属性访问、原型链修改 |
| Promise | all(全成功)、allSettled(全结果) |
| Vue 实例 | 每个使用创建一个实例、_uid 标识 |
| 组件销毁 | beforeDestroy 清理定时器、hook 事件 |
| 响应式原理 | WeakMap、Map、Set 存储依赖 |
| nextTick | 微任务优先、降级策略 |
| 兼容性 | Vue3 不支持 IE、polyfill、动态降级 |

