面试题小结(真实面试)
面试题
- 1.call与apply的区别
- 2.vue3的响应式原理
- 3.js的垃圾回收机制
- 4.说说原型链
- 5.什么是防抖和节流
- 6.说一下作用域链
- 7.在一个页面加载数据时(还没加载完成),切换到另一个页面,怎么暂停之前页面的数据加载。
- 浏览器自动中止机制
这些都是本人之前亲自遇到过的面试问题,本文是某次面试的记录
1.call与apply的区别
在 JavaScript 中,apply、bind 和 call 都是用来改变函数执行时的上下文,即改变函数运行时的 this 指向。它们的主要区别在于参数的传递方式和函数的执行时机。
call(this指向(原型对象),…arg)
function fn(...args) {console.log(this, args); } let obj = { myname: "张三" }; fn.call(obj, 1, 2); // this 指向 obj,参数以列表形式传入
apply(this指向(原型对象),[…arg])
function fn(...args) {console.log(this, args); } let obj = { myname: "张三" }; fn.apply(obj, [1, 2]); // this 指向 obj,参数以数组形式传入
bind与call类型,不同的是会返回一个改变this指向的新方法
function fn(...args) {console.log(this, args); } let obj = { myname: "张三" }; const bindFn = fn.bind(obj); // 返回一个新的函数,this 指向 obj bindFn(1, 2); // 执行新函数,this 指向 obj
2.vue3的响应式原理
Vue3的响应式系统是基于ES6的Proxy和Reflect实现的,相较于Vue2使用的Object.defineProperty,Vue3在性能和功能上都有显著提升
在Vue2中,响应式系统是通过Object.defineProperty来实现的。它通过拦截对象属性的读取和设置操作来实现响应式。
function reactive(obj, key, value) { Object.defineProperty(obj, key, { get() { console.log(`访问了${key}属性`); return value; }, set(val) { console.log(`将${key}由->${value}->设置成->${val}`); if (value !== val) { value = val; } } }); }const data = { name: '林三心', age: 22 }; Object.keys(data).forEach(key => reactive(data, key, data[key]));console.log(data.name); // 访问了name属性 // 林三心 data.name = 'sunshine_lin'; // 将name由->林三心->设置成->sunshine_lin console.log(data.name); // 访问了name属性 // sunshine_lin
Object.defineProperty有一个显著的缺陷:它无法监听对象新增或删除的属性
Vue3通过Proxy来实现响应式,解决了Vue2的缺陷。Proxy可以拦截并重新定义基本操作(例如属性查找、赋值、枚举、函数调用等)
function reactive(target) { const handler = {get(target, key, receiver) {console.log(`访问了${key}属性`);return Reflect.get(target, key, receiver);},set(target, key, value, receiver) {console.log(`将${key}由->${target[key]}->设置成->${value}`);return Reflect.set(target, key, value, receiver);} };return new Proxy(target, handler); }const data = { name: '林三心', age: 22 }; const proxyData = reactive(data);console.log(proxyData.name); // 访问了name属性 // 林三心 proxyData.name = 'sunshine_lin'; // 将name由->林三心->设置成->sunshine_lin console.log(proxyData.name); // 访问了name属性 // sunshine_lin
Proxy不仅可以监听对象的新增和删除属性,还可以直接监听数组的变化
Vue3的响应式系统还包括依赖收集和触发更新的机制。通过track函数收集依赖,trigger函数触发更新
const targetMap = new WeakMap();function track(target, key) { 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) { let depsMap = targetMap.get(target); if (depsMap) {const dep = depsMap.get(key);if (dep) {dep.forEach(effect => effect());} } }function reactive(target) { const handler = {get(target, key, receiver) {track(receiver, key);return Reflect.get(target, key, receiver);},set(target, key, value, receiver) {Reflect.set(target, key, value, receiver);trigger(receiver, key);} }; return new Proxy(target, handler); }
3.js的垃圾回收机制
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存
对于寻找到那些没有使用的变量的方法,通常情况下有两种实现方式:标记清除和引用计数
标记清除(主要)
这是JS最常用的垃圾回收方式。垃圾回收器会定期执行以下流程:
标记阶段:从根对象(全局对象、当前调用栈等)开始递归遍历,将所有可访问的对象标记为可达
清除阶段:回收所有未被标记的对象内存
以函数为例,在此函数内定义两个变量,如下
function demo(){const a = 'a'; // 标记可达const b = 1; // 标记可达 }demo(); // 函数调用后,执行上下文消失
函数执行时:a 和 b 被调用栈引用,在GC标记阶段会被标记为可达
函数结束后:执行上下文销毁,a 和 b 失去所有引用
下一次GC运行时:
- 标记阶段:从根对象无法访问 a 和 b → 不被标记
- 清除阶段:回收内存
闭包情况
function ex() {const a = 'a';return () => console.log(a); // 闭包引用a }const closure = ex(); // 保存返回的函数
函数执行结束后:变量
a
被返回的闭包函数引用,只要closure
变量可达(如全局引用),a
在GC
标记阶段就始终被标记为可达总结
当上下文消失时,其中不再被任何可达对象引用的变量,会在下一次垃圾回收的标记阶段未被标记,随后在清除阶段被回收
注意点:
- 标记是周期性行为:不是声明时立即标记,而是在GC的标记阶段统一处理
- 没有"取消标记":变量因失去引用在下一次GC时不被标记,而非主动取消
- 闭包的关键是引用链:必须保持闭包函数本身可达,内部变量才会保留
- 回收非即时:从变量失去引用到实际回收可能有延迟(取决于GC运行时机)
引用计数法
引用计数的含义是跟踪记录每个值被引用的次数。
例如,定义一个变量a值为1,值1的引用次数为1,后面有b=a引用次数+1,c=a引用次数+1,b=''引用次数-1,如下:const a = 1; // 值`1`的引用计数 = 1(由`a`引用) const b = a; // 值`1`的引用计数 = 2(`a`和`b`都引用) const c = a; // 值`1`的引用计数 = 3(`a`, `b`, `c`) b = ''; // `b`不再引用`1`,计数减1 → 变为2
当变量的最终引用次数为0时,则会被GC清除。
只有当a和c都不再引用1时,计数才会归零并被回收。这种方法遇到循环引用就会出问题
function createObjects() {const objA = { name: 'A' }; // 引用计数 = 1const objB = { name: 'B' }; // 引用计数 = 1objA.ref = objB; // objB计数 = 2objB.ref = objA; // objA计数 = 2 } createObjects(); // 函数执行结束
函数结束后:objA和objB的局部引用消失 → 计数各减1
但objA.ref和objB.ref互相引用 → 计数保持1
结果:两个对象永远不会被回收 → 内存泄漏
所以不会怎么使用这种方法。
4.说说原型链
js是基于原型生成对象的方式
所有对象都有它的原型对象
使用class创建一个Object类,Object本质是一个构造函数,可以使用 const obj = new Object(…)创建一个新的实例对象,而所有对象都有原型对象,所以obj._proto_ 会指向原型对象,构造函数的prototype也会指向同一个原型对象,他们基于同一个原型对象产生,如下图:
![]()
这就是最常见的原型链,整个的原型链还包括函数对象以及其他方式创建的对象
原型链的实际作用
之所以要挂在原型对象上面,是因为由构造函数实例化出来的每一个实例对象,属性值是不相同的,所以需要每个对象独立有一份。
但是对于方法而言,所有对象都是相同的,因此我们不需要每个对象拥有一份,直接挂在原型对象上面共用一份即可。
function Computer(name, price) { this.name = name; this.price = price; } Computer.prototype.showPrice = function () { console.log(`${this.name}的电脑价格为${this.price}`); };const huawei = new Computer("华为", 5000); const apple = new Computer("苹果", 8000);
5.什么是防抖和节流
防抖目的是防止事件过于频繁的触发,让函数如同电梯一样,在规定时间内有人按电梯就重新计时。常见场景于百度输入框,连续输入时设置一个定时时间,在此时间内继续输入就更新定时时间,直到超出定时时间调用搜索事件。
节流也是为了防止事件过于频繁的触发,不过思想不一样,它是让事件每隔一个固定时间触发一次。常见场景于窗口缩放时间,一般缩放窗口时会触发特别多的resize事件,节流就是定时每隔一段时间触发一次,以此优化性能。
6.说一下作用域链
作用域
作用域相当于地盘,有全局作用域,函数作用域,块级作用域,作用域可以很好的隔绝不同作用域的变量,外层作用域就访问不到内层作用域的内容。
作用域链
所有相邻的作用域会形成一条链路,就是作用域链,内层作用域可以沿着作用域链找到外层作用域的变量进行使用。
7.在一个页面加载数据时(还没加载完成),切换到另一个页面,怎么暂停之前页面的数据加载。
在单页应用(SPA)中切换页面时,暂停或取消之前页面的数据加载是优化性能和避免资源浪费的关键。以下是具体实现方案:
核心解决方案:使用
AbortController
现代浏览器提供了
AbortController
API,可优雅地取消网络请求。import { useEffect, useState } from 'react';function PageA() { const [data, setData] = useState(null);useEffect(() => { // 1. 创建控制器 const abortController = new AbortController();// 2. 发起带取消信号的请求 fetch('https://api.example.com/data', { signal: abortController.signal // 关键:绑定取消信号 }) .then(response => response.json()) .then(setData) .catch(err => { if (err.name === 'AbortError') { console.log('请求被取消'); // 正常取消不报错 } else { console.error('请求出错', err); } });// 3. 组件卸载时取消请求 return () => abortController.abort(); }, []);return <div>{data ? data : 'Loading...'}</div>; }// 页面切换时,React 自动触发清理函数取消请求
Vue的解决方法也是如此
<script setup> import { ref, onBeforeUnmount } from 'vue';const data = ref(null); let controller = null; // 存储 AbortControllerconst fetchData = async () => { // 如果已有请求进行中,先取消 if (controller) controller.abort();controller = new AbortController();try { const response = await fetch('https://api.example.com/data', { signal: controller.signal }); data.value = await response.json(); } catch (err) { if (err.name !== 'AbortError') { console.error('请求错误', err); } } };// 组件挂载时获取数据 fetchData();// 组件卸载前取消请求 onBeforeUnmount(() => { if (controller) controller.abort(); }); </script>
在非单页应用(传统多页应用)中,当用户切换页面时,浏览器会自动中止所有未完成的网络请求。这是浏览器内置行为,无需开发者手动处理。
浏览器自动中止机制
页面卸载时自动终止:
- 当用户点击链接/提交表单/刷新页面时
- 浏览器触发
beforeunload
→unload
事件- 所有进行中的网络请求会被自动取消
- 包括:XHR、Fetch、图片加载、CSS/JS文件下载等