js
数据类型
String,Number,BigInt,Object,undefined,null,Boolean,Symbol 基本类型存在栈中,引用类型实例本身存在推中,引用的数据如果是基本类型,就在栈中。
箭头函数和普通函数的核心区别?(至少说出 3 点)
「考察要点」:this 指向(箭头函数继承外层作用域 this,普通函数 this 动态绑定)、不能作为构造函数、没有 arguments 对象、没有 prototype 属性、不能使用 yield。
解构赋值的常见使用场景?举例说明对象解构和数组解构的差异。
「考察要点」:解构的语法、默认值、剩余运算符、对象解构按属性名,数组解构按索引。
原型与原型链
函数或类可以继承父类,子类可以从prototype继承父类属性与函数,这里的prototype指向父类的原型,原型链是指子类优先使用自己的函数定义及属性定义,如果自身没有,就会向父类去寻找,如果仍没有,会像父类的父类去找,直至找到或者为null
闭包
闭包是指一个函数可以访问其外部作用域的变量,即使外部函数已经返回。闭包可以用来创建私有变量和函数。
闭包的缺点:
- 可能导致内存泄漏
- 闭包会持有外部变量的引用,导致变量无法被垃圾回收
- 解决方案:手动将变量置为 null 或谨慎管理作用域 滥用闭包可能影响性能
- 每次调用都会创建新的作用域,影响垃圾回收机制
- 适度使用,避免不必要的闭包
async/await
async/await是基于Promise的语法糖,用于处理异步操作。async函数返回一个Promise对象,await用于等待Promise的结果。
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}Event Loop
Event Loop的核心组成 组成:
- 调用栈:js的同步函数执行时会压入调用栈中执行。
- web api:协调异步函数,将其交给web api处理。
- 任务队列: 其分为宏任务队列和微任务队列,存放回调函数。
- 事件循环:同步任务 清空执行栈 -> 微任务队列(一次清空) ->页面渲染(最先插入宏任务)-> 宏任务队列(每次一个)->进入系统idle(空闲) 等待下一次循环,其实也就是不断检查调用栈,当调用栈为空,就将任务队列的任务取出压入调用栈中执行。
数据类型检测的方式有哪些
typeof
数组、对象、null都会被判断为object,其他判断都正确。
instanceof
只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
Object.prototype.toString.call()
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么? 这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了
this
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
数组有哪些原生方法?
数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法 数组归并方法 reduce() 和 reduceRight() 方法
浏览器的垃圾回收机制
(1)垃圾回收的概念
垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
(2)垃圾回收的方式
浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。
1)标记清除
- 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
2)引用计数
- 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
- 这种方法会引起循环引用的问题:例如:
obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
function fun() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}这种情况下,就要手动释放变量占用的内存:
obj1.a = null
obj2.a = null(3)减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对
object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 - 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
哪些情况会导致内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
浏览器存储数据方式有哪些
浏览器存储是指浏览器在本地计算机上保存数据的方式,以便在用户访问网站时能够更快地加载内容,提供更好的用户体验。浏览器存储主要有以下几种方式:
Cookie:Cookie是一种小型的文本文件,存储在客户端的浏览器中。它主要用于存储用户的身份认证、会话状态等信息。Cookie的大小通常受到浏览器和服务器的限制,一般不超过4KB。每次向同一个域名下发送请求时,都会携带相同的Cookie,这样服务器就能识别客户端的身份信息。然而,由于Cookie的大小限制,它只能用来存储少量的信息。 Web Storage:Web Storage是HTML5引入的一种新的存储方式,主要包括Local Storage和Session Storage两种类型。
Local Storage:这种存储方式类似于电脑或手机上的下载功能,可以永久保存数据。除非用户主动删除,否则数据会一直存在。 Session Storage:与Local Storage不同,Session Storage只在当前会话下有效。当页面关闭时,存储的数据就会被删除。这种方式常用于存储一些临时性的信息,如网页微博之类的密码保存。
IndexedDB:IndexedDB是一种浏览器本地数据库,可以在客户端浏览器中存储大量的结构化数据。它支持事务操作、索引查询等功能,存储空间相对较大,通常限制为几百MB到几GB。与Web Storage相比,IndexedDB的存储方式更为复杂,但提供了更强大的数据操作能力。
请详细解释 ES6 中的 Symbol 类型,它的核心特性是什么?至少说出 3 个实际应用场景,并说明为什么这些场景要用 Symbol 而不是其他类型。
参考答案
核心特性
唯一性:Symbol() 每次调用返回的值都是独一无二的,即使传入相同描述符;Symbol.for(key) 会全局注册,相同 key 会返回同一个 Symbol。
不可枚举性:Symbol 作为对象属性名时,不会被 for...in、Object.keys()、JSON.stringify() 遍历到,可用于定义对象的 “私有属性”。
不可转换性:不能与其他类型的值进行运算,只能通过 String(symbol) 或 symbol.toString() 转为字符串。
应用场景
定义对象私有属性:避免属性名冲突(比如第三方库的对象扩展)。
const privateKey = Symbol('private');
const obj = {
[privateKey]: '私有数据',
publicKey: '公有数据'
};
Object.keys(obj); // ["publicKey"],无法获取 privateKey定义常量枚举:避免枚举值重复(比如状态码、事件类型)。
const STATUS = {
PENDING: Symbol('pending'),
SUCCESS: Symbol('success'),
FAIL: Symbol('fail')
};
// 不会出现字符串枚举的“值重复”问题扩展内置对象方法:比如给数组添加自定义方法,避免覆盖原有方法。
const uniqueMethod = Symbol('unique');
Array.prototype[uniqueMethod] = function() {
return [...new Set(this)];
};// 不会污染 Array.prototype 的原有属性 核心考点: Symbol 的唯一性、不可枚举性,以及解决 “命名冲突” 的核心价值。 评分标准:答出特性 + 3 个场景 + 原因 → 满分;缺场景 / 原因 → 酌情扣分。
css
CSS盒模型
css盒子模型 又称为框模型(Box Model),包含了元素内容(content)、内边距(padding)、边框(border)、外边距(margin)几个要素
react
React 的虚拟 DOM(VDOM)是什么?它解决了什么问题?虚拟 DOM 的 diff 算法核心规则?
「考察要点」:虚拟 DOM 定义(JS 对象描述 DOM)、优势(减少真实 DOM 操作)、diff 规则(同层比较、key 作用、列表 diff 策略)。
React 的 Fiber 架构是什么?它解决了传统 React 渲染的什么问题?请详细解释 React 的 Fiber 架构,它的核心设计目标是什么?Fiber 节点的结构包含哪些关键属性?Fiber 是如何实现 “可中断渲染” 的?
「考察要点」:Fiber 核心(将渲染任务拆分为小单元,可中断 / 恢复)、解决的问题(长任务阻塞主线程导致的卡顿)、时间切片(Time Slicing)概念。
参考答案
(1) 核心设计目标
解决 React 15 及以前 Stack Reconciler 的缺陷: 旧架构的渲染是同步且不可中断的,一旦开始渲染,会占用主线程直到完成。如果组件树很深,长任务会阻塞浏览器的布局、绘制、用户交互(如点击、滚动),导致页面卡顿。
Fiber 架构的目标是:将渲染任务拆分为多个小单元,支持任务的中断、暂停、恢复和优先级排序,优先处理高优先级任务(如用户输入、动画),提升页面响应性。
(2) Fiber 节点的关键属性
- Fiber 是一个链表结构的对象,每个 Fiber 节点对应一个组件,关键属性包括:
- type:组件类型(如函数组件 / 类组件的构造函数、原生标签字符串)。
- key:组件的唯一标识,用于 diff 算法。
- stateNode:组件的实例(类组件实例 / 原生 DOM 节点)。
- child:指向第一个子 Fiber 节点(构建 Fiber 树的子节点)。
- sibling:指向下一个兄弟 Fiber 节点(构建 Fiber 树的兄弟节点)。
- return:指向父 Fiber 节点(用于完成渲染后回溯)。
- pendingProps/memoizedProps:待处理的 props / 上一次渲染的 props(用于判断是否需要更新)。
- pendingWorkPriority:任务优先级(如用户交互是高优先级,数据请求是低优先级)。
- effectTag:标记当前 Fiber 节点的更新类型(如 Placement 插入、Update 更新、Deletion 删除)。
- nextEffect:指向下一个有更新的 Fiber 节点(用于构建副作用链表)。
(3) 可中断渲染的实现原理
Fiber 架构将渲染过程分为两个阶段,且两个阶段都支持中断:
Reconciliation 阶段(协调阶段)
任务:遍历 Fiber 树,对比新旧节点,计算出需要更新的内容(如插入 / 删除 / 更新组件),并为 Fiber 节点打上 effectTag。
特点:可中断、可暂停、可重启。React 会根据任务优先级和浏览器的空闲时间,分批执行小单元任务。
实现:借助浏览器的 requestIdleCallback(React 内部实现了更精准的 scheduler),在浏览器空闲时执行任务;如果有高优先级任务(如用户点击),则暂停当前任务,先执行高优先级任务,待空闲后恢复执行。
Commit 阶段(提交阶段)
任务:根据 Reconciliation 阶段生成的副作用链表,将更新应用到真实 DOM 上。
特点:不可中断(因为涉及真实 DOM 操作,中断会导致页面不一致),但该阶段的任务量很小。
核心考点: Fiber 架构的设计动机、链表结构、双阶段渲染模型、优先级调度。
评分标准: 答出目标 + 节点属性 + 可中断原理 → 满分;仅知道 “可中断” 但说不清实现细节 → 中等分。
useEffect 的依赖项数组作用?空数组依赖和不写依赖的区别?如何避免依赖项导致的 bug(比如捕获旧值)?
「考察要点」:依赖项触发副作用执行、空数组 = 仅挂载 / 卸载执行、不写依赖 = 每次渲染执行、解决方案(useCallback/useMemo 缓存、函数式更新、useRef 保存最新值)。
React 的状态提升和 Context API 分别适用于什么场景?Context API 有什么性能问题?如何优化?
「考察要点」: 状态提升:兄弟组件共享状态、层级较浅的组件共享; Context API:跨层级(深层)组件共享状态; 性能问题:Context 变化时,所有消费 Context 的组件都会重渲染; 优化:拆分 Context、配合 useMemo/memo 缓存组件、使用 useContext 精准消费。
问:React 组件重渲染的触发条件?如何避免不必要的重渲染?(至少说出 3 种方案)
「考察要点」: 触发条件:state 变化、props 变化、父组件重渲染、context 变化; 优化方案:React.memo、useCallback、useMemo、拆分组件、懒加载、虚拟列表。
问:React 中 key 的作用?为什么不能用索引作为 key?
「考察要点」:key 是列表元素的唯一标识,diff 算法通过 key 识别元素;索引作为 key 会导致列表更新时(比如删除 / 排序),React 误判元素身份,引发错误渲染或性能问题。
React 18 的并发渲染(Concurrent Mode)是什么?有什么新特性(比如 useTransition、useDeferredValue)?
并发渲染不是一个具体的 API,而是 React 18 的底层渲染机制,核心是:React 可以同时处理多个版本的 UI 状态,根据优先级决定哪个版本的 UI 先呈现给用户。
旧架构是 “单车道”:一次只能处理一个更新,必须等上一个更新完成才能处理下一个。
并发渲染是 “多车道”:可以同时处理多个更新,高优先级更新(如用户输入)可以插队,低优先级更新(如大数据列表渲染)可以被暂停或放弃。
Fiber 是并发渲染的基础:Fiber 架构实现了任务的拆分和优先级调度,为并发渲染提供了底层能力;
并发渲染是 Fiber 架构的升级:React 18 基于 Fiber 架构,新增了调度优先级的 API(如 useTransition),让开发者可以手动标记更新的优先级。
useTransition 区分紧急更新和非紧急更新。比如:输入框输入(紧急) + 输入后筛选长列表(非紧急) 将非紧急更新标记为 transition,优先级低于紧急更新。React 会先处理紧急更新(保证输入框响应),再在浏览器空闲时处理非紧急更新。如果用户在非紧急更新过程中又触发了紧急更新,React 会放弃当前的非紧急更新,避免卡顿。
useDeferredValue | 为非紧急数据创建一个延迟更新的副本。比如:输入框输入(紧急) + 实时预览(非紧急) | 接收一个值,返回该值的“延迟副本”。当原值变化时,副本不会立即更新,而是等紧急更新完成后再更新。本质是 useTransition 的“数据版本”实现。
Redux 的中间件(比如 redux-thunk、redux-saga)作用?分别适用于什么场景?
RN
RN 中 ScrollView 和 FlatList 的区别?什么场景下必须用 FlatList?
「考察要点」: ScrollView:一次性渲染所有子组件,内存占用高,适合短列表; FlatList:按需渲染(可视区域内)、支持下拉刷新 / 上拉加载、内存优化,适合长列表(比如商品列表、消息列表)。
RN 中如何实现页面跳转?StackNavigator 中如何传递参数?如何接收参数?
「考察要点」:React Navigation 用法、navigation.navigate('Page', { id: 1 }) 传参、route.params.id 接收参数。
RN 中如何实现原生模块(Native Module)?比如调用 iOS/Android 原生方法。
vue
vue2 vue3双向绑定
vue2采用的是Object.defineProperty,vue3采用的是Proxy
Vue SSR 原理,优缺点
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端。
SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求。
vue 中的 spa 应用如何优化首屏加载速度?
优化首屏加载可以从这几个方面开始:
请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。 缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验 gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。 http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。 懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件 预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化 合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积 使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化 提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程 封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化 图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验 使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求 压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片
如何减少打包后的代码体积
代码分割(Code Splitting):将应用程序的代码划分为多个代码块,按需加载 Tree Shaking:配置Webpack的Tree Shaking机制,去除未使用的代码 压缩代码:使用工具如UglifyJS或Terser来压缩JavaScript代码 使用生产模式:在Webpack中使用生产模式,通过设置mode: 'production'来启用优化 使用压缩工具:使用现代的压缩工具,如Brotli和Gzip,来对静态资源进行压缩 利用CDN加速:将项目中引用的静态资源路径修改为CDN上的路径,减少图片、字体等静态资源等打包
打包优化
压缩代码 Tree Shaking/Scope Hoisting 使用 cdn 加载第三方模块 多线程打包 happypack splitChunks 抽离公共文件 sourceMap 优化
接口请求一般放在哪个生命周期中?为什么要这样做?
接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间 SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性 created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题
手写代码
- 手写一个深拷贝函数
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const clone = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}function deepCopy(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 如果是日期或正则对象则直接返回一个新对象
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 如果hash中有这个对象,则直接返回hash中存储的对象引用
if (hash.has(obj)) {
return hash.get(obj);
}
let newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepCopy(obj[key], hash);
}
}
return newObj;
}
const original = { a: 1, b: { c: 2 } };
const copied = deepCopy(original);
console.log(copied); // { a: 1, b: { c: 2 } }
console.log(original === copied); // false
console.log(original.b === copied.b); // false- 手写一个防抖函数
function debounce(func, delay) {
let timer;
return function(...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}- 手写一个节流函数
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
func.apply(this, args);
}
};
}- 手写一个Promise
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(callback => callback());
}
};
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(callback => callback());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
if (this.status === 'fulfilled') {
resolve(onFulfilled(this.value));
} else if (this.status === 'rejected') {
reject(onRejected(this.reason));
} else {
this.onResolvedCallbacks.push(() => {
resolve(onFulfilled(this.value));
});
this.onRejectedCallbacks.push(() => {
reject(onRejected(this.reason));
});
}
});
}
}- 手写一个instanceof
function myInstanceof(left, right) {
if (typeof left !== 'object' || left === null) {
return false;
}
let proto = Object.getPrototypeOf(left);
while (true) {
if (proto === null) {
return false;
}
if (proto === right.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}- 手写一个new
function myNew(constructor, ...args) {
if (typeof constructor !== 'function') {
throw new Error('myNew: first argument must be a function');
}
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return typeof result === 'object' ? result : obj;
}- 手写一个call
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('myCall: caller must be a function');
}
context = context || globalThis;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};- 手写一个apply
Function.prototype.myApply = function(context, args) {
if (typeof this !== 'function') {
throw new TypeError('myApply: caller must be a function');
}
context = context || globalThis;
const fn = Symbol('fn');
context[fn] = this;
const result = context[fn](...args);
delete context[fn];
return result;
};- 手写一个bind
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('myBind: caller must be a function');
}
const fn = this;
return function(...newArgs) {
return fn.apply(context, [...args, ...newArgs]);
};
};- 手写一个数组去重
function unique(arr) {
return Array.from(new Set(arr));
}- 手写一个数组扁平化
function flatten(arr) {
return arr.reduce((acc, val) => {
if (Array.isArray(val)) {
return acc.concat(flatten(val));
}
acc.push(val);
return acc;
}, []);
}- 手写一个数组排序
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(item => item < pivot);
const right = arr.filter(item => item > pivot);
return [...quickSort(left), pivot, ...quickSort(right)];
}- 手写链表
链表是线性表的一种,通过指针(引用)连接节点,无需连续内存空间; 单向链表的每个节点包含 value(值)和 next(指向下一个节点的指针); 核心操作:初始化、新增节点、删除节点、查找节点、遍历链表、反转链表(反转是高频考点)。
// 定义链表节点类
class ListNode {
constructor(value) {
this.value = value; // 节点值
this.next = null; // 指向下一个节点的指针,初始为 null
}
}
// 定义单向链表类
class LinkedList {
constructor() {
this.head = null; // 头节点,初始为 null
this.length = 0; // 链表长度,方便快速获取节点数量
}
/**
* 1. 新增节点(尾部追加)
* @param {*} value 要添加的节点值
*/
append(value) {
const newNode = new ListNode(value);
// 边界条件:链表为空时,新节点作为头节点
if (this.length === 0) {
this.head = newNode;
} else {
// 找到最后一个节点
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.length++;
}
/**
* 2. 指定位置插入节点
* @param {number} position 插入位置(从 0 开始)
* @param {*} value 节点值
* @returns {boolean} 插入是否成功
*/
insert(position, value) {
// 边界校验:位置越界
if (position < 0 || position > this.length) {
console.error('插入位置越界');
return false;
}
const newNode = new ListNode(value);
// 情况1:插入到头部(position = 0)
if (position === 0) {
newNode.next = this.head; // 新节点的 next 指向原头节点
this.head = newNode; // 头节点更新为新节点
} else {
// 情况2:插入到中间/尾部
let current = this.head;
let previous = null; // 记录前一个节点
let index = 0;
// 找到插入位置的前一个节点
while (index < position) {
previous = current;
current = current.next;
index++;
}
newNode.next = current; // 新节点的 next 指向当前节点
previous.next = newNode; // 前一个节点的 next 指向新节点
}
this.length++;
return true;
}
/**
* 3. 根据位置删除节点
* @param {number} position 要删除的节点位置
* @returns {*} 被删除的节点值(删除失败返回 null)
*/
removeAt(position) {
// 边界校验:位置越界或链表为空
if (position < 0 || position >= this.length || this.length === 0) {
console.error('删除位置越界或链表为空');
return null;
}
let current = this.head;
let previous = null;
let index = 0;
// 情况1:删除头节点
if (position === 0) {
this.head = current.next; // 头节点更新为下一个节点
} else {
// 情况2:删除中间/尾部节点
while (index < position) {
previous = current;
current = current.next;
index++;
}
// 前一个节点的 next 跳过当前节点,指向当前节点的下一个节点
previous.next = current.next;
}
this.length--;
return current.value; // 返回被删除的节点值
}
/**
* 4. 根据值查找节点位置
* @param {*} value 要查找的值
* @returns {number} 节点位置(未找到返回 -1)
*/
indexOf(value) {
let current = this.head;
let index = 0;
while (current) {
if (current.value === value) {
return index; // 找到值,返回位置
}
current = current.next;
index++;
}
return -1; // 未找到
}
/**
* 5. 遍历链表,返回所有节点值的数组
* @returns {Array} 链表值数组
*/
traverse() {
const result = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
/**
* 6. 反转链表(核心高频考点)
* 思路:双指针法,遍历链表时反转节点的 next 指向
*/
reverse() {
// 边界条件:空链表或只有一个节点,直接返回
if (!this.head || !this.head.next) {
return;
}
let prev = null; // 前一个节点,初始为 null
let current = this.head; // 当前节点,初始为头节点
while (current) {
const nextTemp = current.next; // 临时保存下一个节点(防止链表断裂)
current.next = prev; // 反转当前节点的 next 指向
prev = current; // 前一个节点后移
current = nextTemp; // 当前节点后移
}
this.head = prev; // 反转后,原尾节点变为头节点
}
/**
* 辅助方法:清空链表
*/
clear() {
this.head = null;
this.length = 0;
}
/**
* 辅助方法:判断链表是否为空
* @returns {boolean}
*/
isEmpty() {
return this.length === 0;
}
/**
* 辅助方法:获取链表长度
* @returns {number}
*/
getLength() {
return this.length;
}
}
// 测试用例(面试时可让候选人手写测试逻辑)
const linkedList = new LinkedList();
// 追加节点
linkedList.append(10);
linkedList.append(20);
linkedList.append(30);
console.log('初始链表:', linkedList.traverse()); // [10, 20, 30]
// 插入节点
linkedList.insert(1, 15);
console.log('插入后:', linkedList.traverse()); // [10, 15, 20, 30]
// 删除节点
linkedList.removeAt(2);
console.log('删除后:', linkedList.traverse()); // [10, 15, 30]
// 查找节点位置
console.log('15 的位置:', linkedList.indexOf(15)); // 1
// 反转链表
linkedList.reverse();
console.log('反转后:', linkedList.traverse()); // [30, 15, 10]
// 清空链表
linkedList.clear();
console.log('清空后是否为空:', linkedList.isEmpty()); // true