面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 01:08

Mongoose 事务怎么用?乐观锁和悲观锁有什么区别?

Mongoose 事务依赖 MongoDB 副本集(单机不支持),通过 session 实现多文档原子操作。核心流程:startSession() 创建会话,session.startTransaction() 开启事务,操作时每条 CRUD 必须传 { session } 参数,最后 commitTransaction() 或 abortTransaction(),endSession() 释放资源。推荐用 withTransaction() 辅助方法自动提交/回滚。常见坑:忘记在操作中传 session 参数导致操作不在事务内、事务超时默认 60 秒需控制时长。并发控制方面,Mongoose 内置乐观锁:Schema 的 __v 字段在 save() 时自动递增,若文档已被修改则抛 VersionError;悲观锁需自行实现,通常用字段锁标记 + findOneAndUpdate 原子设置。追问withTransaction() 和手动管理事务有什么区别? withTransaction(fn) 自动处理 commit/abort,fn 返回时自动提交,fn 抛异常自动回滚,遇到 TransientTransactionError 还会自动重试。手动管理则需自己 try/catch 并调 commit/abort。哪些操作不支持事务? 创建集合、创建索引等 DDL 操作在 4.4 之前不能在事务中执行。事务内也不能操作 config、admin 等系统数据库的集合。乐观锁的 __v 在什么场景下会失效? 使用 updateOne/findOneAndUpdate 等不经过 save() 的操作时 __v 不会自动检查。需要手动在条件中加入版本号:{ _id, __v: currentVersion }。事务中读写同一个文档会死锁吗? 可能。两个事务同时修改同一文档会产生写冲突,后提交的事务会抛 WriteConflict 错误。解决:控制事务粒度、按固定顺序访问文档、加重试逻辑。写段代码const session = await mongoose.startSession();await session.withTransaction(async () => { await Account.updateOne( { _id: fromId, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session } ); await Account.updateOne( { _id: toId }, { $inc: { balance: amount } }, { session } );});session.endSession();
服务端阅读 05月29日 01:08

JSON 的语法规则有哪些?哪些是容易踩坑的?

JSON 语法看似简单,但有几条规则与 JavaScript 对象字面量不同,容易踩坑。核心规则:键名必须用双引号包裹("name" 不能写成 name);字符串也只能用双引号,单引号非法;最后一个键值对后不能有尾逗号;不支持注释;值只能是 string、number、boolean、null、object、array 六种类型,不支持 undefined、Date、Function、RegExp。数字不支持前导零(01 非法)和 NaN/Infinity。字符串中的特殊字符需转义,如 \n、\"、\\。大小写敏感:只有小写的 true、false、null 合法,TRUE 或 Null 都会解析失败。追问{"a": 1,} 在 JS 中合法但 JSON 非法,为什么 JSON 要禁止尾逗号?JSON.parse('{"a": undefined}') 会怎样?undefined 在对象和数组中的序列化行为有何不同?JSON 数字为什么不区分整数和浮点数?1e3 在 JSON 中合法吗?如何在 JSON 中表示二进制数据?Base64 编码有什么局限?JSON.parse('123') 合法吗?JSON.parse('"hello"') 呢?最外层必须是对象吗?写段代码// 合法 JSONconst a = JSON.parse('{"name":"Alice","age":25}');// 非法:单引号键名// JSON.parse("{'name':'Alice'}"); // SyntaxError// 非法:尾逗号// JSON.parse('{"name":"Alice",}'); // SyntaxError// 合法:最外层不是对象JSON.parse('123'); // 123JSON.parse('"hi"'); // "hi"
服务端阅读 05月29日 01:08

Mongoose 子文档和引用怎么选?

子文档是嵌套在父文档中的 Schema,分单个子文档和子文档数组两种形式。子文档与父文档同生同灭、原子更新,适合总是一起访问且数量有限的数据。当数据需要独立查询、数量可能很大时,改用 ObjectId 引用。追问子文档的验证是何时触发的?父文档保存时,子文档的验证会自动级联触发。如果子文档校验失败,整个保存操作回滚,无需手动调用子文档验证。子文档怎么访问父文档?通过子文档的 $parent 属性:comment.$parent 可拿到所属的 post 文档,便于在子文档方法中读取父级上下文。向子文档数组添加元素有哪些方式?push/unshift 追加到数组,Mongoose 会自动标记数组为 modified。也可用 $push + $each 原子操作批量添加,避免先查再改的两步操作。删除子文档用什么方法?pull 按条件删除:post.comments.pull({ author: 'Alice' });splice 按索引删除;也可用 $pull 原子操作。pull 更安全,不会因索引偏移误删。子文档数组的单文档大小限制是什么?MongoDB 单文档上限 16MB,子文档数组过大不仅逼近限制,还导致查询和更新性能下降。经验值:数组元素超过几百条就该考虑拆分为独立集合。写段代码const commentSchema = new Schema({ text: String, author: String});const postSchema = new Schema({ title: String, comments: [commentSchema]});// 添加 + 删除子文档const post = await Post.findById(id);post.comments.push({ text: 'nice', author: 'Tom' });post.comments.pull({ author: 'Tom' });await post.save();
服务端阅读 05月29日 01:08

Mongoose 中间件的 pre 和 post 钩子怎么用?

Mongoose 中间件分两类:文档中间件(作用于 save/validate/remove,this 指向文档实例)和查询中间件(作用于 find/findOne/updateOne/deleteOne,this 指向 Query 对象)。pre 钩子在操作前执行,可修改数据或通过 next(err) 中止操作;post 钩子在操作后执行,接收结果参数,不能中止但适合日志和通知。异步中间件用 async function 即可,不需要手动调 next(),抛异常自动中止。经典场景:pre save 哈希密码、post save 写审计日志、pre find 过滤软删除记录。注意 findOneAndUpdate 不触发 save 钩子,需要单独注册 findOneAndUpdate 的 pre/post。追问pre 钩子中 next() 和 async/await 能混用吗? 不建议。选一种即可:传统写法调 next(err),async 写法直接抛异常或正常返回。混用可能导致钩子执行两次或跳过后续中间件。查询中间件里怎么修改查询条件? 通过 this(Query 对象)调用 .where()、.skip() 等方法,例如 this.where({ deleted: false })。不要直接修改 this._conditions,应使用 Query API。post 钩子能拿到修改前的值吗? post save 中 this 是保存后的文档,doc 参数也是。如需旧值,在 pre save 中用 this.modifiedPaths() 记录变更字段,或用 this.$__original() 在 Mongoose 7+ 获取原始值。remove 中间件在 Mongoose 7+ 有什么变化? remove 钩子已废弃,改为 deleteOne。文档级的 this.deleteOne() 触发文档中间件,Model.deleteOne() 触发查询中间件,两者 this 含义不同。写段代码userSchema.pre('save', async function() { if (!this.isModified('password')) return; this.password = await bcrypt.hash(this.password, 10);});userSchema.post('save', function(doc) { AuditLog.create({ action: 'save', docId: doc._id });});
服务端阅读 05月29日 01:08

Mongoose 数据验证有哪些类型,自定义验证器怎么写?

Mongoose 验证分内置和自定义两类。内置验证器包括:required(必填)、min/max(数值范围)、minlength/maxlength(字符串长度)、enum(枚举值)、match(正则匹配)。自定义验证通过字段的 validate 选项实现,支持同步和异步两种方式。验证默认在 save() 时触发,也可显式调用 validate() 或 validateSync()。追问unique 是验证器吗?它和 required 有什么本质区别?unique 不是验证器,是索引声明器。它在数据库层创建唯一索引,Mongoose 验证阶段不检查唯一性,只有 MongoDB 写入时才报错。required 才是真正的应用层验证器,在保存前就拦截。所以 unique 的错误是 MongoDB E11000,不是 Mongoose ValidationError。异步验证器有什么使用场景和注意事项?典型场景:验证邮箱/用户名是否已存在(需查库)。注意异步验证器中 this 指向当前文档实例,用 this.constructor.findOne() 避免硬编码 Model 名;更新操作默认不触发验证,需设置 runValidators: true。update 操作的验证和 save 有什么区别?save 触发完整验证链(内置+自定义+pre validate钩子);updateOne/updateById 默认跳过验证,需手动传 { runValidators: true },且更新验证只能校验即将修改的字段,无法访问完整文档上下文。validateBeforeSave 什么时候该设为 false?仅在数据已通过其他可信渠道验证时使用(如内部迁移脚本、测试数据填充)。生产业务代码不建议跳过,否则脏数据会直接写入数据库。写段代码const userSchema = new Schema({ email: { type: String, required: true, validate: { validator: async function(v) { const doc = await this.constructor.findOne({ email: v }); return !doc || doc._id.equals(this._id); }, message: '邮箱已被注册' } }, role: { type: String, enum: ['admin', 'user'], default: 'user' }});
服务端阅读 05月29日 01:08

JSON 是什么?为什么它能取代 XML 成为数据交换的主流格式?

JSON(JavaScript Object Notation)是一种基于文本的轻量级数据交换格式,采用键值对和数组的结构来组织数据。它脱胎于 JavaScript 对象字面量语法,但已被所有主流编程语言支持,成为 Web API 响应的事实标准。相比 XML,JSON 去掉了冗余的闭合标签,数据体积更小、解析更快,且天然映射为编程语言的内置数据结构(对象和数组),无需额外的 DOM 解析层。JSON 的主要用途包括:客户端与服务端之间的数据传输、配置文件(如 package.json、tsconfig.json)、NoSQL 数据库存储(如 MongoDB),以及跨语言的数据序列化。追问JSON 和 JavaScript 对象字面量有什么区别?哪些合法的 JS 对象在 JSON 中是非法的?为什么 JSON 不支持注释?这在配置场景下会带来什么问题,社区有哪些变通方案?JSON 中的数字类型为什么不区分整数和浮点数?这会导致什么精度问题?如果要在跨语言场景中使用 JSON 传递日期,有哪些常见的约定做法?JSON5 和 JSON 的关系是什么?为什么不推荐在生产环境使用 JSON5?写段代码const data = { name: "Alice", scores: [95, 87, 92], active: true};const json = JSON.stringify(data);const parsed = JSON.parse(json);console.log(parsed.scores[0]); // 95
服务端阅读 05月29日 01:08

Mongoose Populate 如何实现文档关联查询?

Mongoose 通过 ObjectId 引用(ref)建立文档关联,再用 .populate() 将引用自动替换为完整文档。基本流程:Schema 中用 ref 声明引用,查询时链式调用 .populate('field') 即可获取关联数据,支持多字段、嵌套和条件填充。追问populate 的深层嵌套怎么写?使用对象参数嵌套 populate:.populate({ path: 'comments', populate: { path: 'user' } }),可无限层级嵌套,但超过 3 层说明数据模型需要重新设计。如何只填充关联文档的部分字段?在 populate 选项中加 select:.populate({ path: 'author', select: 'name email' }),减少数据传输量。Virtual Populate 和普通 populate 有什么区别?Virtual Populate 通过 schema.virtual() 定义,关联字段不存入数据库,查询时动态填充,适合反向关联(如通过 author 查其所有 books),不占用存储空间。populate 的 N+1 问题怎么解决?populate 本质是额外查询,列表场景下每条记录都会触发一次查询。替代方案:用 aggregation 的 $lookup 在数据库端完成关联,一条聚合语句搞定,性能更优。条件 populate 怎么用?.match 过滤关联文档:.populate({ path: 'author', match: { status: 'active' } }),不匹配时该字段返回 null,需在业务层处理。写段代码const bookSchema = new Schema({ title: String, author: { type: Schema.Types.ObjectId, ref: 'Author' }});// 基本 populate + 选择字段const book = await Book.findById(id) .populate({ path: 'author', select: 'name email' });// 嵌套 populateconst post = await Post.findById(id) .populate({ path: 'comments', populate: { path: 'user' } });
服务端阅读 05月29日 01:08

Mongoose 查询构建器的链式方法和性能优化有哪些?

Mongoose 查询构建器支持两种写法:对象语法 User.find({ age: { $gte: 18 } }) 和链式语法 User.find().where('age').gte(18)。链式方法包括 .where() 设置字段、.gt()/.lt()/.gte()/.lte() 范围筛选、.in() 匹配数组、.sort() 排序、.limit().skip() 分页、.select() 投影字段、.populate() 关联填充。性能优化关键三点:.lean() 跳过文档封装返回纯 JS 对象,查询速度提升约 30%;.select() 只取必要字段减少传输量;确保查询字段有索引。链式调用时方法顺序不影响查询结果,但可读性上建议先筛选再排序分页。追问链式查询和对象查询能混用吗? 可以,两者等价,Mongoose 内部会将链式调用合并为同一个查询条件对象。但混用时不建议对同一字段既用对象又用链式,容易产生冲突。lean() 返回的对象为什么不能调用 save()? lean() 跳过了 Mongoose 文档实例化,返回的是普通对象,没有 $__ 内部状态和实例方法。需要修改再保存时应去掉 lean()。populate() 的性能问题怎么解决? populate 本质是额外查询,N 个文档关联会产生 N+1 次查询。替代方案:用 $lookup 聚合在数据库端完成关联,或手动批量查询减少往返次数。如何调试慢查询? 开启 mongoose.set('debug', true) 打印实际查询语句,或对查询链调用 .explain('executionStats') 查看索引命中和扫描行数。写段代码const users = await User.find({ status: 'active' }) .where('age').gte(18).lte(60) .sort({ createdAt: -1 }) .select('name email age') .populate('department', 'name') .lean() .limit(20);
服务端阅读 05月29日 01:07

Mongoose Schema 怎么定义,有哪些常用配置项?

Schema 是 Mongoose 的文档结构蓝图,定义字段类型、验证规则、默认值和索引。它本身不操作数据库,需通过 mongoose.model() 编译为 Model 后使用。核心配置项包括:类型声明(String/Number/Date/ObjectId/Array)、验证器(required/min/max/enum/match)、修饰符(trim/lowercase/uppercase)、默认值(default)和索引(unique/index/sparse)。追问Schema 的 strict 选项有什么作用?strict 默认为 true,未在 Schema 中声明的字段会被自动忽略,不会写入数据库。设为 false 允许存储任意字段,但不推荐,会丧失结构约束的意义。虚拟字段怎么定义,和普通字段有什么区别?通过 schema.virtual('name').get() 定义,不存入数据库、不占存储、不能用于查询筛选。适合拼接展示字段(如 fullName),不适合业务排序或过滤。Schema 怎么添加实例方法和静态方法?实例方法用 schema.methods.xxx = function(),通过文档实例调用,this 指向文档;静态方法用 schema.statics.xxx = function(),通过 Model 类调用,this 指向 Model。注意不要用箭头函数,否则 this 绑定错误。嵌套文档和引用(ref)该选哪个?嵌套文档适合一对一或少量子文档,查询简单但文档体积大;引用(ref + ObjectId)适合多对多或大数据集,需 populate 联查但文档精简。频繁一起读取用嵌套,独立生命周期用引用。写段代码const postSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, author: { type: Schema.Types.ObjectId, ref: 'User', required: true }, tags: [{ type: String, enum: ['js', 'node', 'db'] }], views: { type: Number, default: 0, min: 0 }}, { timestamps: true });
服务端阅读 05月29日 01:07

Mongoose 是什么,为什么要用它而不是原生 MongoDB 驱动?

Mongoose 是 MongoDB 的 ODM(对象文档建模)库,在 Node.js 和 MongoDB 之间提供结构化抽象层。核心价值:通过 Schema 强制文档结构、内置数据验证、支持中间件钩子(pre/post)、自动类型转换和虚拟字段,让开发者以面向对象方式操作 MongoDB,显著降低数据不一致风险。追问Mongoose 的 Schema 和 Model 是什么关系?Schema 定义文档结构(字段类型、默认值、验证规则),Model 是 Schema 的编译产物,即操作数据库的构造函数。一个 Schema 可编译出多个 Model。pre 和 post 中间件分别适合做什么?pre 常用于密码加密(save 前 hash)、数据格式化、权限校验;post 常用于日志记录、缓存清理、触发异步通知。pre 可通过 next() 或 async/await 控制流程中断。Mongoose 的类型转换(casting)会带来什么隐患?查询时 Mongoose 自动将字符串 "123" 转为数字 123,可能掩盖数据错误。生产环境建议配合严格验证,或使用 strictQuery 选项控制行为。虚拟字段和真实字段有什么区别?虚拟字段不存入数据库,由 get/set 计算得出,无法用于查询和排序。适合组合展示(如 fullName = firstName + lastName),不适合业务筛选。写段代码const userSchema = new mongoose.Schema({ name: { type: String, required: true, trim: true }, email: { type: String, required: true, lowercase: true }, age: { type: Number, min: 0, default: 0 }});userSchema.virtual('info').get(() => `${this.name}, ${this.age}岁`);const User = mongoose.model('User', userSchema);