外观
TypeScript 与 Node.js
1. 如何管理和优化 tsconfig?
答案:
tsconfig 应按项目运行环境和构建工具明确配置:用 compilerOptions 设置 target、module、moduleResolution、lib、rootDir、outDir、baseUrl/paths;开启 strict(尤其 strictNullChecks),配置 noEmit 或声明文件生成策略。用 include、exclude 控制范围,避免把构建产物和依赖纳入编译;多环境可用基础配置配合 extends。同时让编辑器、tsc、打包器使用同一份配置,并定期用 tsc --noEmit 校验。
2. Boolean 和 boolean 有什么区别?
答案:
boolean 是 TypeScript 的原始布尔类型,只允许 true 和 false,应作为变量、参数和返回值的类型。Boolean 是 JavaScript 的包装对象构造函数及对象类型,值可能是 new Boolean(false),它本身是真值,容易造成判断错误。通常写 boolean,只有调用 Boolean() 做显式转换时才使用构造函数;不要用 Boolean 作为业务字段类型。
3. 你了解 ts 工具类型 Exclude 与 Omit 的使用吗?及它们两个的区别?
答案:
Exclude<T, U> 从联合类型 T 中去掉可赋值给 U 的成员,例如 Exclude<'a' | 'b', 'a'> 得到 'b';它是条件类型,主要处理联合成员。Omit<T, K> 则从对象类型 T 中删除键 K,本质上近似 Pick<T, Exclude<keyof T, K>>,例如 Omit<User, 'id'>。前者操作联合成员,后者操作对象属性,使用场景不同。
4. 说说 Typscript 的 extends 关键字会在哪些场景下使用?
答案:
extends 有几类用途:类继承类、接口继承接口或类以复用并扩展成员;泛型约束如 <T extends { id: string }> 要求类型满足结构;条件类型中 T extends U ? X : Y 做类型分支;还可用于条件类型的分发。它表达的是继承、约束或可赋值关系,接口和类的 extends 不能与类型别名条件表达式混为一谈。
5. process.nextTick, setTimeout 以及 setImmediate 三者的执行顺序?
答案:
在 Node.js 中,每轮事件循环开始时会优先清空 process.nextTick 队列,再处理 Promise 微任务;因此它们通常先于计时器和 I/O 回调执行。setTimeout(fn, 0) 进入 timers 阶段,setImmediate(fn) 进入 check 阶段,二者谁先取决于注册位置和当前所处阶段:在 I/O 回调中通常 setImmediate 先,在主模块中顺序不稳定。不要递归堆积 nextTick,否则会饿死 I/O。
6. 说说 express middleware(中间件) 的工作原理
答案:
Express 中间件本质是按注册顺序组成的函数链,签名通常为 (req, res, next);函数可读取或修改请求、响应,调用 next() 交给后续处理,调用 next(err) 转入错误中间件,或直接结束响应。路由匹配后继续沿链执行,错误处理中间件必须是四参数 (err, req, res, next),且通常放在业务中间件之后。未调用 next 也未响应会使请求悬挂。
7. 怎么进行 Node 服务的内存优化?
答案:
先用 process.memoryUsage()、堆快照和 GC/监控确认是泄漏还是正常峰值,再处理根因:限制缓存大小并设置淘汰策略,及时移除事件监听器、定时器和连接,避免全量读取大文件而改用 stream,分页处理大数据,复用连接池并释放资源。检查闭包、全局变量和请求上下文是否长期持有对象;生产环境可合理设置 --max-old-space-size,但它只是缓解,不是泄漏修复。
8. 怎么在 koa 中,进行中间件的异常处理?
答案:
在 Koa 最外层注册错误处理中间件,用 try...catch 包住 await next(),捕获下游异常后统一记录日志、设置状态码和安全的响应体:app.use(async (ctx, next) => { try { await next() } catch (err) { ctx.status = err.status || 500; ctx.body = { message: ctx.status === 500 ? 'Internal Server Error' : err.message }; ctx.app.emit('error', err, ctx) } })。它必须先注册,生产环境不要把堆栈和敏感信息返回客户端。
9. 在没有async/await 的时候, koa是怎么实现的洋葱模型?
答案:
早期 Koa 使用 Generator 中间件和 co:中间件写成 function* (next) { yield next; ... },next 是下一个 Generator 的迭代器。组合器递归执行每层的 iterator.next(value),遇到 yield 暂停,等下游 Promise 完成后再恢复,因此形成先进后出。示意:function* a(next){ console.log(1); yield next; console.log(4) },执行顺序是进入外层、进入内层、内层结束、外层结束。Koa 2 已改用 Promise/async 函数。
10. 纯 TS 项目工程中, 如何使用 alias path?
答案:
TypeScript 的 paths 只影响编译器类型解析,不会自动改变 Node.js 的运行时解析。可在 tsconfig.json 配置 baseUrl 与 paths,例如 @/*: ['src/*'],开发时配合 ts-node/tsx 的路径支持,打包时让 bundler 配置同名 alias;纯 Node 运行编译产物时必须让运行时也认识该别名,或在构建阶段改写为相对路径。别把 paths 当成部署后的模块解析方案。
11. 单线程的 nodejs 是如何充分利用计算机 CPU 资源的呢?
答案:
Node.js 主线程负责 JavaScript 执行,但 I/O 由操作系统和 libuv 线程池处理;要利用多核 CPU,CPU 密集任务可用 worker_threads,在独立线程中运行并通过消息传递通信;独立服务也可用 cluster 或多个进程由负载均衡分发请求。实际选型取决于任务:线程适合共享部分内存、低开销计算,进程隔离更强;同时要避免阻塞主线程,并做好进程重启、优雅退出和会话/状态共享。
12. 枚举和常量枚举的区别
答案:
普通 enum 通常会生成运行时代码:数字枚举还会生成反向映射,可能增加包体并引入运行时对象。const enum 默认在编译时内联成员值,不生成枚举对象,体积更小、访问更快,但依赖编译器处理;使用 isolatedModules、Babel 或发布给其他项目时可能无法安全内联。若需要运行时遍历、反向映射或跨编译边界使用,普通 enum 或 as const 对象更稳妥。
13. 说说 const 和 readonly 的区别
答案:
const 是运行时变量声明,限制变量不能重新赋值,但对象或数组内部仍可修改;它的作用域是词法作用域。readonly 是 TypeScript 类型层面的只读约束,可用于属性、数组和元组,防止通过该类型引用修改,但编译后不存在,运行时也不提供深冻结。const 不能替代 readonly,常见写法是 const user: { readonly id: string };需要运行时不可变时应使用 Object.freeze 或复制。
14. 说说你对 npm 包管理的了解
答案:
npm 用 package.json 描述项目元数据、脚本和依赖;dependencies 是运行时依赖,devDependencies 是开发/构建依赖,peerDependencies 用于要求宿主提供兼容版本。package-lock.json 锁定完整依赖树,保证可复现安装,CI 应使用 npm ci。版本范围遵循 semver,发布包要注意入口、类型声明、exports 和依赖边界;安装脚本、依赖供应链及锁文件变更都应审查。
15. 说说对 node 子进程的了解
答案:
Node 子进程主要有三类:spawn 直接启动命令并以流处理输入输出,适合持续或大输出任务;exec 通过 shell 执行并缓存完整输出,使用方便但要限制 maxBuffer,且要防命令注入;fork 专门启动另一个 Node 模块,额外提供 IPC。它们都应监听 error、exit/close,处理退出码并在结束时释放资源;不可信参数不要拼接进 shell。
16. PM2 部署 nodejs 有哪些优势?
答案:
PM2 提供进程守护、崩溃自动重启、日志管理、环境变量配置和零停机 reload;集群模式可启动多个 Node 实例利用多核,并提供基本监控、启动脚本和开机自启。它不能替代反向代理、容器编排和应用自身的健康检查:无状态服务更适合 cluster,状态应放在外部存储;部署时还要处理优雅退出、信号转发、日志轮转和实例间会话一致性。
17. never 是什么类型,详细讲一下
答案:
never 表示不可能出现的值,是所有类型的底部类型。函数始终抛异常或不会结束时可返回 never;穷尽判断联合类型后,剩余分支可收窄为 never,借此在新增成员时让编译器报错;条件类型中也常用它过滤联合成员。它不同于 void:void 表示函数没有可用返回值,而 never 表示函数不会正常返回。
18. unknown 是什么类型?
答案:
unknown 是类型安全的顶层类型,任何值都可以赋给它,但在未进行类型收窄前不能直接读取属性、调用或当作具体类型使用,只能比较、判断或先断言。相比 any,unknown 会保留编译期检查,适合 JSON.parse、异常对象和外部输入。实践中应通过 typeof、instanceof、自定义类型保护或 schema 校验后再使用。
19. 联合类型是什么?
答案:
联合类型 A | B 表示一个值可以属于 A 或 B,但只能访问所有成员共有的属性和方法;使用 typeof、in、instanceof 或判别字段进行收窄后,才能访问具体成员。它描述的是“或”,不是把两种结构合并;合并应使用交叉类型 A & B。联合适合表达状态和多种输入,最好设计明确的 discriminant 以便穷尽检查。
20. extends 条件类型怎么定义?
答案:
条件类型用 T extends U ? X : Y 表示:若 T 可赋值给 U,结果为 X,否则为 Y。例如 type MessageOf<T> = T extends { message: unknown } ? T['message'] : never。当检查对象类型参数是裸类型参数时,联合会触发分布式计算,即逐个成员判断;用 [T] extends [U] 可关闭分发。它常与 infer、映射类型组合实现类型级别的提取和转换。
21. infer 关键字是什么?
答案:
infer 只能出现在条件类型的 extends 约束中,用来声明一个待推断的类型变量。例如 type Return<T> = T extends (...args: any[]) => infer R ? R : never 会提取函数返回值。它不是运行时关键字,也不是任意位置的泛型声明;推断结果取决于结构匹配。标准工具类型 ReturnType、Parameters、InstanceType 都大量使用这种模式。
22. in 运算符作用是什么?
答案:
in 在运行时用于判断对象或其原型链上是否存在某个属性,例如 'id' in value;在 TypeScript 中,若左侧是字面量键,它还可作为联合对象的类型收窄条件。它判断的是属性存在,不判断值是否为真,属性值为 undefined 仍可能返回 true。对可能为 null 的值先做非空检查;判断自有属性可用 Object.hasOwn。
23. Node.js 是单线程的,那该如何充分利用多核 CPU 呢?
答案:
Node 的 JavaScript 执行通常在单个事件循环线程上,但可以通过 cluster/child_process 启动多个进程,让多个实例分别占用不同 CPU 核心;也可用 worker_threads 在线程中执行 CPU 密集型代码。HTTP 请求应尽量无状态并由共享负载均衡分发,session、缓存和队列放到外部系统。I/O 密集任务通常不需要盲目增加进程,关键是避免同步 API 和长时间计算阻塞事件循环。
24. 说说 TypeScript 中,有哪些内置的类型方法和工具类型
答案:
常见内置工具类型包括 Partial、Required、Readonly、Pick、Omit、Record、Exclude、Extract、NonNullable、ReturnType、Parameters、InstanceType 和 Awaited。它们基于映射类型、条件类型、keyof 与 infer 在编译期变换类型,不会生成运行时代码。选择时要明确目标:改属性可选性用 Partial,挑键用 Pick/Omit,从联合中过滤用 Exclude/Extract。
25. 在开发过程中, 使用过哪些 TS 的特性或者能力?
答案:
实际项目中常用接口/类型别名描述数据契约,联合类型与判别字段表达状态,泛型复用 API,类型收窄和自定义 type guard 处理外部输入;用 keyof、typeof、infer、条件类型和映射类型构造派生类型,用 utility types 减少重复。配合 strict、可选链、空值合并和编辑器检查提升安全性;但类型只在编译期存在,接口、网络数据仍要运行时校验。
26. express 里面的"中间件"和"插件"是同一个东西吗?
答案:
不完全是。Express 中间件是符合 (req, res, next) 约定、参与请求处理链的函数,负责预处理、路由、响应或错误传递;“插件”是更宽泛的扩展称呼,可以注册多个路由、中间件、配置和生命周期逻辑。一个 Express 插件往往内部使用中间件,但不是所有插件都等同于单个中间件。判断关键在于它是否直接接收请求上下文并通过 next 参与链路。
27. 为什么Node在使用es module时必须加上文件扩展名?
答案:
Node 的 ESM 解析遵循更接近浏览器的 URL/包导入规则,不能像 CommonJS 那样无条件尝试补 .js、index.js 等候选路径,因此相对导入通常必须写完整扩展名,如 import './util.js'。文件按 ESM 还是 CommonJS 解析由 .mjs、.cjs 以及最近的 package.json type 决定;TypeScript 源码可写 .js 以对应编译后的文件。裸模块名仍由包导出规则解析。
28. typescript 中的 is 关键字有什么用?
答案:
is 用于声明类型谓词,让 TypeScript 根据布尔结果收窄类型:function isDog(x: Animal): x is Dog { return x instanceof Dog }。调用 isDog(value) 为真时,value 在该分支被视为 Dog;这只是编译期信息,函数内部的判断必须真实可靠,否则会通过类型检查却在运行时出错。它也可用于联合类型、对象属性检查和数组 filter 的类型推断。
29. Koa 中,如果一个中间件没有调用 await next(),后续的中间件还会执行吗?
答案:
通常不会。Koa 中间件只有调用 await next() 才会把控制权交给下一个中间件;不调用就会在当前层结束,后续链路不会执行,但当前中间件在调用前的逻辑仍已执行。它可以是有意的短路,例如鉴权失败直接设置 ctx.status = 403;若需要下游处理响应并在返回后继续执行,就必须 await next()。
30. body-parser 这个中间件是做什么用的?
答案:
body-parser 解析 HTTP 请求体,并把结果放到 Express 的 req.body,常见有 JSON 和 application/x-www-form-urlencoded 两种解析器:app.use(express.json({ limit: '1mb' }))、app.use(express.urlencoded({ extended: false }))。新版 Express 已内置这些能力,通常不必单独安装 body-parser。它不会自动验证业务字段,也不是文件上传解析器;仍需限制大小、检查 Content-Type 并做 schema 校验。
31. koa 框架中,该怎么处理中间件的异常?
答案:
把错误处理中间件放在所有路由之后,并在最外层 try/catch 包住 await next():app.use(async (ctx, next) => { try { await next() } catch (err) { ctx.status = err.status || 500; ctx.body = { message: ctx.status === 500 ? 'Internal Server Error' : err.message }; ctx.app.emit('error', err, ctx) } })。这样能捕获下游同步和异步异常;应统一记录日志、区分可预期业务错误与 500,生产环境不要泄露堆栈。
32. 在没有async await 的时候,koa是怎么实现的洋葱模型?
答案:
Koa 1 时代使用 Generator、yield 和 co 实现洋葱模型。组合器递归调用每个中间件的 Generator:中间件执行到 yield next 暂停,先运行下游;下游完成后恢复当前 Generator,继续执行后半段。例如外层打印 enter,yield next,内层完成后外层再打印 exit,顺序就是外层进入、内层进入、内层退出、外层退出。Koa 2 改用 Promise 和 async/await,原理相同。
33. 说说你对 koa 洋葱模型的理解
答案:
Koa 中间件按注册顺序组成一条调用链。外层中间件先执行 next() 之前的代码,await next() 进入内层;内层完成后,再返回执行外层 next() 之后的代码,因此请求阶段由外到内、响应阶段由内到外,形似洋葱。错误处理中间件应放在前面,用 try/catch 包住 next(),才能捕获后续异常。
34. pm2守护进程的原理是什么?
答案:
PM2 通常由后台 daemon 管理应用:启动时创建并记录子进程、PID、环境和日志;daemon 监听子进程的 exit、异常和资源状态,进程意外退出时按策略重启,也可用 cluster 模式拉起多个 worker。它还负责日志、优雅重载和开机自启。守护并不等于修复业务错误,需设置重启次数、内存阈值和 graceful shutdown,避免故障循环。
35. Node.js 如何调试?
答案:
开发时可用 Node Inspector:node --inspect-brk app.js 在首行暂停,Chrome DevTools 的 chrome://inspect 或 VS Code 附加后即可断点、单步、查看调用栈和变量。普通问题可配合结构化日志与错误堆栈;性能问题使用 DevTools CPU/Heap Profile、--trace-gc 等定位。生产环境要限制 inspector 暴露地址,并结合 APM、指标和日志排查。
36. 请实现下面的 sum 方法
ts
/**
* @file 假设加法是一个异步过程,如何计算多个数组之和?
*/
function sleep(ms: number) {
return new Promise(r => {
setTimeout(() => {
r(undefined)
}, ms);
})
}
async function asyncAdd(a: number, b: number) {
await sleep(1000);
return a + b;
}
function sum(arr: number[]): Promise<number> {
// 补全这里代码,涉及 arr 中两数求和只能使用 asyncAdd,禁止使用加号
throw new Error('功能待实现');
}
console.time('a')
sum([1, 2, 3, 4, 5, 6, 7, 8])
.then(v => {
console.log(v) // 36
console.timeEnd('a') // a: <耗时>
})
export default {}答案:
ts
async function sum(arr: number[]): Promise<number> {
if (arr.length === 0) return 0;
let total = arr[0];
for (let i = 1; i < arr.length; i++) {
total = await asyncAdd(total, arr[i]);
}
return total;
}
``` 输出为 `36`。这里每次合并必须等待上一次结果,所以 8 个数约需 7 秒;时间复杂度 O(n),额外空间 O(1)。空数组返回 0,单元素直接返回。
## 37. 请实现下面的 myAll 方法
```ts
/**
* @file 实现 PromiseAll 方法
*/
import { sleep } from "./8.sleep";
async function myAll<T extends unknown[] | []>(values: T): Promise<{ [P in keyof T]: Awaited<T[P]> }> {
// 补全此处代码,使用 Promise.all 以外的语法完成
throw new Error('功能待实现');
}
// 一秒钟后返回结果 value
async function request(value: string) {
await sleep(1000);
return value;
}
async function main() {
console.log('start');
const res = await myAll([
request('a'),
request('b'),
request('c'),
])
console.log(res); // 预期输出 start 一秒后输出 ['a', 'b', 'c']
}
main()
export default {}答案:
ts
async function myAll<T extends readonly unknown[]>(values: T): Promise<{ [P in keyof T]: Awaited<T[P]> }> {
const result: unknown[] = [];
for (const value of values) result.push(await value);
return result as { [P in keyof T]: Awaited<T[P]> };
}
``` 输入的三个请求会在调用前同时开始,因此虽按顺序 await,约一秒后输出 `['a', 'b', 'c']`;结果顺序与输入一致,任一 Promise 拒绝就拒绝。时间为等待最慢任务的时间,遍历开销 O(n),结果空间 O(n)。
## 38. 请实现下面的 sleep 方法
```ts
/**
* @file 返回一个 Promise,并在 ms 毫秒后 Promise 变为完成状态
*/
export function sleep(ms: number): Promise<undefined> {
// 补全此处代码
throw new Error('功能待实现');
}
async function main(){
console.log('a')
await sleep(1000);
console.log('b');
await sleep(1000);
console.log('c');
}
main()
export default {}答案:
ts
export function sleep(ms: number): Promise<undefined> {
return new Promise(resolve => {
setTimeout(() => resolve(undefined), Math.max(0, ms));
});
}
``` `main` 会立即输出 `a`,约一秒后输出 `b`,再约一秒后输出 `c`。定时器只能保证不早于指定时间,不能保证精确触发;负数按 0 处理,Promise 只完成一次。
## 39. 请实现下面的 product 方法
/**
* @file 计算数组笛卡尔积
*/
// 示例
product([1, 2], [3, 4]) // [[1, 3], [1, 4], [2, 3], [2, 4]]
function product(xList: number[], yList: number[]): [number, number][] {
// 补全内部实现
throw new Error('功能待实现');
}
export default {}
**答案:**
```ts
function product(xList: number[], yList: number[]): [number, number][] {
const result: [number, number][] = [];
for (const x of xList) {
for (const y of yList) result.push([x, y]);
}
return result;
}
``` 对 `[1,2]` 和 `[3,4]` 输出 `[[1,3],[1,4],[2,3],[2,4]]`。笛卡尔积保留重复项;任一数组为空时结果为空。设长度为 m、n,时间复杂度 O(mn),输出空间 O(mn)。
## 40. 请实现下面的 treePath 方法
```ts
/**
* @file 二叉树所有路径
*/
type Tree = {
value: number;
left?: Tree;
right?: Tree;
}
const tree: Tree = {
value: 1,
left: {
value: 2,
right: { value: 5 }
},
right: { value: 3 }
};
function treePath(root: Tree): string[] {
// 补全此处代码
throw new Error('功能待实现');
}
console.log(treePath(tree)) // [ '1->2->5', '1->3' ]
export default {}答案:
ts
function treePath(root: Tree): string[] {
const result: string[] = [];
const dfs = (node: Tree, path: string[]) => {
const next = [...path, String(node.value)];
if (!node.left && !node.right) { result.push(next.join('->')); return; }
if (node.left) dfs(node.left, next);
if (node.right) dfs(node.right, next);
};
dfs(root, []);
return result;
}
``` 示例输出 `['1->2->5', '1->3']`。空树需将参数类型改为可选并返回 `[]`;时间 O(N),递归栈和路径空间 O(H)。
## 41. 请实现下面的 myMap 方法
```ts
/**
* @file 实现数组 map 方法
*/
function myMap<T, R>(arr: T[], callbackFn: (v: T) => R): R[] {
// 补全此处代码,可以使用除数组 map 以外的其他任何函数
throw new Error('功能待实现');
}
// 测试
console.log(myMap([1, 2, 3], v => v * 2)) // [2, 4, 6]
export default {};答案:
ts
function myMap<T, R>(arr: T[], callbackFn: (v: T) => R): R[] {
const result: R[] = new Array(arr.length);
for (let i = 0; i < arr.length; i++) result[i] = callbackFn(arr[i]);
return result;
}
``` `myMap([1,2,3], v => v * 2)` 输出 `[2,4,6]`。实现不修改原数组,按索引顺序调用回调;时间复杂度 O(n),结果空间 O(n)。题目签名未包含 index、array 和 thisArg,因此只传当前元素。
## 42. 请实现下面的 mergeArray 方法
```ts
/**
* @file 合并两个有序数组
*/
function merge(arr: number[], arr2: number[]): number[] {
// 补全此处代码
throw new Error('功能待实现');
}
// 参数数组从小到大排列
console.log(merge([1, 2, 3], [2, 5, 6])) // [ 1, 2, 2, 3, 5, 6 ]
export default {}答案:
ts
function merge(arr: number[], arr2: number[]): number[] {
const result: number[] = [];
let i = 0, j = 0;
while (i < arr.length && j < arr2.length) {
if (arr[i] <= arr2[j]) result.push(arr[i++]);
else result.push(arr2[j++]);
}
return result.concat(arr.slice(i), arr2.slice(j));
}
``` 示例输出 `[1,2,2,3,5,6]`。双指针保持稳定的相等元素顺序;时间复杂度 O(m+n),额外输出空间 O(m+n),不改动输入数组。
## 43. 实现下面的 firstSingleChar 方法
```ts
/**
* @file 找出字符串中第一个只出现一次的字符
*/
function firstSingleChar(str: string) {
// 补全此处代码
throw new Error('功能待实现');
}
// a 和 b 都出现了两次,只有 c 出现了一次,返回 c
console.log(firstSingleChar('abcba')) // c
// b c d 都出现了一次,返回第一个
console.log(firstSingleChar('aabcdee')) // b
// a 和 b 都出现了多次,没有只出现一次的元素,返回 undefined
console.log(firstSingleChar('aaaabbbb')) // undefined
export default {}答案:
ts
function firstSingleChar(str: string): string | undefined {
const count = new Map<string, number>();
for (const ch of str) count.set(ch, (count.get(ch) ?? 0) + 1);
for (const ch of str) if (count.get(ch) === 1) return ch;
return undefined;
}
``` `abcba` 返回 `c`,`aabcdee` 返回 `b`,全重复时返回 `undefined`。按 Unicode 码点遍历,若要按用户可见字符处理需改用 `Array.from` 或 Intl.Segmenter;时间 O(n),空间 O(k)。
## 44. 实现下面的 reverseWord 方法
```ts
/**
* @file 反转句子
*
* 同时满足以下条件:1、去除首尾空格,2、单词间隔中多个空格变成一个;
* 注意console示例运行结果
*/
function reverseWord(str: string) {
// 补全此处代码
throw new Error('功能待实现');
}
console.log(reverseWord('the sky is blue')); // blue is sky the
// 去除首尾空格
console.log(reverseWord(" hello world ")); // world hello
// 单词间隔中多个空格变成一个
console.log(reverseWord("a good example")); // example good a
export default {}答案:
ts
function reverseWord(str: string): string {
return str.trim().split(/\s+/).reverse().join(' ');
}
``` 因而依次输出 `blue is sky the`、`world hello`、`example good a`。`trim()` 去除首尾空白,正则把连续空白(空格、制表符、换行等)视为一个分隔符;空字符串或全空白字符串应特别处理,否则 `''.split(/\s+/)` 会得到 `['']`,可返回空串。
## 45. 请补充 objToArray 函数
```typescript
/**
* @file objToArray
*
* 将对象按照要求转为数组
* 注意console示例运行结果
*/
type Obj = Record<string, string>;
interface FormatItem {
key: string;
op: string;
value: string;
}
function objToArray(obj: Record<string, Obj>): FormatItem[] {
// 补全此处代码
throw new Error("功能待实现");
}
console.log(
objToArray({
key1: {
op1: "value1",
},
key2: {
op2: "value2",
},
})
);
// result示例
// [
// {key: 'key1', op: 'op1', value: 'value1'},
// {key: 'key2', op: 'op2', value: 'value2'}
// ]
export default {};答案:
ts
function objToArray(obj: Record<string, Obj>): FormatItem[] {
return Object.entries(obj).map(([key, detail]) => {
const [op, value] = Object.entries(detail)[0] ?? ['', ''];
return { key, op, value };
});
}
``` 对示例得到 `[{key:'key1',op:'op1',value:'value1'},{key:'key2',op:'op2',value:'value2'}]`。对象键按 JavaScript 的枚举规则处理;每个内层对象应有一个键,空对象这里返回空字符串,生产代码也可选择抛出校验错误。
## 46. 如何定义一个数组,它的元素可能是字符串类型,也可能是数值类型?
**答案:**
可用联合类型声明:`let value: string | number;`,或写成数组类型 `const list: Array<string | number> = ['a', 1];`。联合类型只表示值可能属于其中一种,读取时只能使用两者共有成员;调用字符串或数字专属方法前要用 `typeof value === 'string'` 等类型收窄。若数组元素必须固定位置,则使用元组 `[string, number]`。
## 47. 使用TS实现一个判断传入参数是否是数组类型的方法
**答案:**
```ts
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
``` `Array.isArray` 能正确识别跨 iframe 或不同 Realm 创建的数组,避免 `value instanceof Array` 的局限。返回类型谓词 `value is unknown[]` 会让 TypeScript 在条件分支中完成收窄;它也能处理 `null`、对象、类数组和 TypedArray,后几者均返回 `false`。
## 48. koa和express有哪些不同?
**答案:**
Express 的中间件模型更接近线性回调链,调用 `next()` 向后传递,错误通过四参数中间件处理;Koa 基于 Promise 和 `async/await`,`await next()` 形成洋葱模型,异常可用 try/catch 统一捕获。Koa 核心更小、默认能力少,Express 生态和中间件数量更丰富。两者都运行在 Node HTTP 之上,选择取决于团队生态和流程控制偏好。
## 49. 两个 Node.js 进程如何通信?
**答案:**
常见方式有:父子进程用 stdin/stdout、IPC 或 `child_process.send()`;同机独立进程可用 Unix Domain Socket、命名管道、共享文件或 Redis;跨机器通常用 TCP、HTTP/gRPC、消息队列。IPC 传对象时会序列化,需考虑背压、超时、重试、幂等和进程退出。高吞吐场景应避免频繁大对象拷贝,并明确消息协议和版本。
## 50. 如何将 unknown 类型指定为一个更具体的类型?
**答案:**
`unknown` 不能直接赋给更具体类型,也不能直接访问属性,必须先做类型收窄。可用 `typeof`、`Array.isArray`、`instanceof` 或自定义类型谓词:`function isUser(x: unknown): x is User { return typeof x === 'object' && x !== null && 'id' in x; }`,通过判断后才可按 `User` 使用。类型断言 `value as User` 只改变编译器视角,不会执行运行时校验,应谨慎使用。
## 51. ts中any和unknown有什么区别?
**答案:**
`any` 基本关闭类型检查:可任意读属性、调用、赋值,错误容易扩散;`unknown` 是安全的顶类型,任何值都能赋给它,但使用前必须通过判断或断言收窄。两者都可接收任意输入,区别在于 `unknown` 强制边界校验。处理接口响应、JSON 或异常值时优先用 `unknown`,确认类型后再转换;只有确实无法建模时才使用 `any`。
## 52. 说说对 TypeScript 中命名空间与模块的理解?区别?
**答案:**
命名空间是 TypeScript 的内部组织机制,用 `namespace` 把类型和值放进同一作用域,并通过 `export` 暴露成员,适合遗留脚本或全局声明。模块以文件为边界,使用 `import/export`,具有独立作用域,依赖关系可静态分析,能被现代构建工具拆分和摇树。两者都能避免命名冲突,但新项目通常优先 ES Module;不要把 namespace 与模块混用造成额外层级。
## 53. 说说你对 typescript 的理解?与 javascript 的区别?
**答案:**
TypeScript 是 JavaScript 的静态类型超集,增加类型注解、接口、泛型、联合类型、访问修饰符等能力,最终编译为可运行的 JavaScript。JS 多在运行时发现类型错误,TS 可在编译期发现更多问题,并改善 IDE 补全、重构和可读性;但类型大多会在编译后擦除,运行时仍需对用户输入、网络数据做校验,且 TS 不改变 JS 的执行语义。
## 54. 使用ts实现一个判断入参是否是数组类型的方法?
**答案:**
```ts
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
``` 调用 `isArray(input)` 成功后,TypeScript 会把 `input` 收窄为数组。不要依赖 `typeof value === 'array'`,因为 JavaScript 中数组的 `typeof` 是 `object`;也不建议用 `instanceof Array` 判断跨窗口数据。需要更精确元素类型时,再结合 `every` 做运行时校验,例如判断是否全为字符串。
## 55. Node性能如何进行监控以及优化?
**答案:**
先监控再优化:用 `process.memoryUsage`、CPU/延迟、事件循环延迟、吞吐、错误率和 GC 指标建立基线,结合日志、APM、Inspector 的 CPU/Heap Profile 定位。优化方向包括减少同步阻塞和大 JSON,使用流与背压,控制并发,缓存热点,避免泄漏,合理连接池和集群扩展。上线前压测,优化后对比 p95/p99;不要只看平均耗时,也要设置告警和资源上限。
## 56. 如果让你来设计一个分页功能, 你会怎么设计? 前后端如何交互?
**答案:**
接口可采用 `GET /users?page=2&pageSize=20`,返回 `items`、`page`、`pageSize`、`total` 或 `hasNext`,前端据此渲染页码、禁用边界按钮并在查询条件变化时重置页码。后端校验并限制 pageSize,稳定排序后执行分页;大数据量优先游标分页,如 `cursor` + `limit`,避免深页 offset 扫描。响应还应明确空结果、非法参数和筛选条件,前后端统一字段语义。
## 57. 如何实现文件上传?说说你的思路
**答案:**
小文件可用 `multipart/form-data` 接收,服务端校验大小、扩展名和真实 MIME,使用随机文件名写入非公开目录,再落库记录 URL,成功后返回资源标识。大文件应采用分片上传:初始化得到 uploadId,各片带序号和哈希,可并发上传、断点续传,服务端校验后合并并原子改名。生产环境还要鉴权、限流、防路径穿越、病毒扫描,并在失败时清理临时片段。
## 58. 如何实现jwt鉴权机制?说说你的思路
**答案:**
登录成功后服务端生成 JWT,载荷放用户 id、过期时间和必要权限,用密钥签名后返回;客户端后续在 `Authorization: Bearer <token>` 中携带,服务端验证签名、算法、issuer、audience 和 exp,再加载权限执行授权。JWT 适合无状态校验,但不能直接撤回,不能放密码等敏感信息。应短期 access token 配合安全存储的 refresh token,登出、改密或封禁时做撤销/版本校验,并全程 HTTPS。
## 59. 说说对中间件概念的理解,如何封装 node 中间件?
**答案:**
中间件是位于请求与最终处理器之间的可组合函数,用来统一处理鉴权、日志、解析、限流和错误。Node 中常见签名是 `(req, res, next)`,异步逻辑完成后调用 `next()`,错误调用 `next(err)`;Koa 则常用 `async (ctx, next) => { await next() }`。封装时应明确执行顺序、是否结束响应、异常传播和返回值,避免既响应又继续 next,且不要把跨请求状态放在模块全局。
## 60. 说说 Node 文件查找的优先级以及 Require 方法的文件查找策略?
**答案:**
`require()` 先按核心模块解析,再按路径模块解析;相对或绝对路径会从目标位置查找文件,通常依次尝试精确文件、补 `.js/.json/.node`,然后查找目录的 `package.json` 的 `main`,再找 `index` 文件。非路径模块则从当前模块目录逐级向上查找 `node_modules`,直到根目录。解析结果会被 `require.cache` 缓存,删除缓存才会重新执行;循环依赖可能拿到尚未完成初始化的部分导出。
## 61. 说说对Nodejs中的事件循环机制理解?
**答案:**
Node 先执行当前同步代码,再处理微任务,通常先清空 `process.nextTick` 队列,再执行 Promise 微任务,随后进入事件循环阶段:timers、pending callbacks、poll、check、close callbacks。`setImmediate` 位于 check 阶段,`setTimeout(0)` 属于 timers,二者在主模块中顺序不固定;在 I/O 回调中通常先到 check 阶段,`setImmediate` 更早。事件循环只负责调度,CPU 密集同步代码仍会阻塞全部请求。
## 62. 说说Node中的EventEmitter? 如何实现一个EventEmitter?
**答案:**
EventEmitter 是 Node 事件驱动的观察者实现:监听器通过 `on/addListener` 注册,`emit` 同步、按注册顺序调用,`once` 触发一次后移除,`off/removeListener` 取消监听。自定义实现可用 `Map<string, Set<Function>>` 保存事件,emit 时复制监听器集合再调用,避免回调中删除导致遍历异常;需约定 `error` 事件,否则无监听器时可能抛出。还应关注监听器泄漏和 `setMaxListeners` 警告。
## 63. 说说对 Node 中的 Stream 的理解?应用场景?
**答案:**
Stream 是按块处理有序数据的抽象,避免一次性把完整内容装入内存,并通过背压协调生产和消费。Node 主要有 Readable、Writable、Duplex 和 Transform;`readable.pipe(writable)` 会转发数据并处理暂停/恢复,现代代码可用 `stream.pipeline` 统一传播错误和关闭资源。常见场景是大文件读写、HTTP 上传下载、压缩、加密和日志处理;应监听 error,并正确处理 backpressure。
## 64. 说说对 Node 中的 Buffer 的理解?应用场景?
**答案:**
Buffer 是 Node 中表示原始字节的固定长度内存区域,常用于文件、网络和加密数据。`Buffer.from('你好')` 按 UTF-8 编码生成字节,`toString('utf8')` 再解码;`Buffer.alloc(size)` 会初始化为零,比 `allocUnsafe` 更安全。它常作为 Stream 的数据块参与 I/O、压缩和加解密。注意 `length` 是字节数不是字符数,截断多字节字符可能乱码,大 Buffer 也会增加内存压力。
## 65. 说说对 Node 中的 fs模块的理解? 有哪些常用方法
**答案:**
`fs` 是 Node.js 提供文件系统能力的核心模块,方法通常分异步和同步两套。常用 API 有 `readFile`/`readFileSync` 读取、`writeFile`/`writeFileSync` 覆盖写入、`appendFile` 追加、`copyFile` 拷贝、`mkdir` 创建目录,以及 `stat`、`readdir`、`rename`、`unlink`。服务端优先异步或 Promise API,避免同步 I/O 阻塞事件循环;大文件应使用 `createReadStream`/`createWriteStream`,并正确处理错误、编码和路径。
## 66. 说说对 Node 中的 process 的理解?有哪些常用方法?
**答案:**
`process` 是描述并控制当前 Node.js 进程的全局对象。常用属性和方法包括:`process.argv` 获取命令行参数,`process.env` 读取环境变量,`cwd()` 获取工作目录,`pid`/`ppid` 获取进程及父进程 ID,`platform` 判断平台,`uptime()` 获取运行时长,`exit()` 退出进程,`on()` 监听 `exit`、`uncaughtException` 等事件,`stdout`/`stderr`/`stdin` 访问标准流,`nextTick()` 将回调放入当前轮询前的微任务队列。生产代码应避免滥用 `uncaughtException` 继续运行。
## 67. Node. js 有哪些全局对象?
**答案:**
Node.js 的真正全局对象是 `globalThis`(Node 中也可用 `global`)。常见全局能力包括 `process`、`console`、`Buffer`、`setTimeout`/`clearTimeout`、`setInterval`/`clearInterval`、`setImmediate`,以及 `queueMicrotask`。CommonJS 模块还提供模块级变量:`__dirname`、`__filename`、`exports`、`module`、`require`,它们并非真正挂在全局对象上,而是由模块包装函数注入;ESM 中应使用 `import.meta.url` 等方式替代前两个变量。
## 68. 说说你对Node.js 的理解?优缺点?应用场景?
**答案:**
Node.js 是基于 V8 的跨平台 JavaScript 运行时,采用事件驱动、非阻塞 I/O 和事件循环模型;JavaScript 主线程负责调度,I/O 等工作可交给操作系统或线程池。优点是开发栈统一、处理大量 I/O 并发和长连接效率高、生态丰富;缺点是 CPU 密集计算会阻塞事件循环,异步错误和内存问题也需治理。适合 Web/API、网关、BFF、实时通信、命令行和构建工具;CPU 密集任务应使用 `worker_threads`、多进程或专用服务。
## 69. tsconfig.json文件有什么用?
**答案:**
`tsconfig.json` 是 TypeScript 项目的编译配置文件,告诉 `tsc` 如何解析、检查和输出代码。常见配置有 `target`、`module`、`moduleResolution`、`lib`、`rootDir`、`outDir`、`include`/`exclude`、`baseUrl`/`paths`,以及严格性选项 `strict`、`noImplicitAny`、`strictNullChecks`。它还能被 `extends` 继承。配置只影响编译期和类型检查,不会自动改变运行时语义;使用构建工具时还需确认其是否读取该文件。
## 70. TypeScript中的 Declare 关键字有什么用?
**答案:**
`declare` 用来向 TypeScript 描述“运行时已经存在、但当前源码没有实现”的类型或模块,主要产生类型信息,不生成 JavaScript。它常见于 `.d.ts` 声明文件,例如 `declare const version: string`、`declare function parse(s: string): Data`,也可用于扩展全局或声明模块。声明必须与真实运行时 API 一致,否则只能通过编译、运行时却可能报错;有实现的函数或变量不能在同一处重复用 `declare`。
## 71. TypeScript支持的访问修饰符有哪些?
**答案:**
TypeScript 支持 `public`、`protected`、`private` 三种访问修饰符,默认是 `public`。`public` 可在任意位置访问;`protected` 只能在类自身及其子类中访问;`private` 只能在声明它的类内部访问。它们主要是编译期约束,普通 TS `private` 编译后仍可能通过 JavaScript 方式触达;若需要运行时真正私有,应使用 ECMAScript 的 `#field`。此外还有 `readonly`,表示属性只能在初始化或构造阶段赋值。
## 72. TypeScript中有哪些声明变量的方式?
**答案:**
声明变量通常使用 `let`、`const` 和兼容旧代码的 `var`,并可在变量名后写类型标注,例如 `let count: number = 0`。优先用 `const` 表示引用不变的绑定,需要重新赋值时用 `let`;避免 `var` 的函数作用域和变量提升带来的问题。TypeScript 会根据初始值做类型推断,也支持联合类型、字面量类型和显式注解,例如 `const mode: 'dev' | 'prod' = 'dev'`。类型标注只在编译期存在。
## 73. TypeScript 的主要特点是什么?
**答案:**
TypeScript 是 JavaScript 的超集,核心特点是静态类型检查、类型推断、结构化类型系统、泛型、联合与交叉类型、接口、枚举、装饰器支持以及现代 ECMAScript 语法。代码先经编译器检查并转换为目标 JavaScript,因此类型通常不会在运行时保留,也不能替代运行时校验。它能改善大型项目的重构、补全和协作体验,同时兼容 JavaScript 生态;配置 `strict` 可获得更可靠的检查。
## 74. Typescript中never 和 void 的区别?
**答案:**
`void` 表示函数调用结果不应被使用,典型类型是返回后正常结束但没有有意义返回值的函数,如 `function log(): void {}`;它并不严格等同于“永远没有返回值”,实现仍可能返回值但调用方不能依赖。`never` 表示不可能产生完成值的类型,函数通常会一直抛异常、死循环,或在穷尽分支后用于类型收窄。`void` 可正常返回,`never` 连正常返回都不会发生。
## 75. Typescript中什么是类类型接口?
**答案:**
类类型接口用于约束一个类必须具备哪些实例成员,类通过 `implements` 实现它。例如 `interface Shape { area(): number }`,`class Circle implements Shape { area() { return 1 } }`。接口只检查实例侧;若要约束类的静态成员,需要单独定义构造函数接口,并用 `typeof Circle` 或构造签名接收它。`implements` 不会改变类的继承关系,也不会生成运行时代码,只在编译期验证结构是否满足契约。
## 76. TypeScript 和 JavaScript 的区别是什么?
**答案:**
JavaScript 是动态类型、运行时解释或编译执行的语言;TypeScript 是其超集,增加了静态类型、类型推断、接口、泛型等能力,并在编译后生成 JavaScript。TS 能在开发阶段发现更多错误,提升 IDE 提示、重构和大型团队协作效率,但需要编译配置和类型维护,类型信息通常不会进入运行时。TS 最终仍受 JavaScript 运行时规则约束,第三方数据、网络响应等外部输入必须额外做运行时校验。
## 77. TypeScript中的方法重写是什么?
**答案:**
方法重写是子类提供与父类同名、兼容签名的实现,以改变或扩展继承来的行为。TypeScript 中使用 `extends` 继承,在子类方法中可通过 `super.method()` 调用父类实现;若要明确检查父类成员,可使用 `override`,并开启 `noImplicitOverride`。重写必须满足父类方法的可赋值类型,不能随意缩窄参数或改变返回契约;它与方法重载不同,重载是在同一个类中为调用者提供多个签名。
## 78. 什么是TypeScript映射文件?
**答案:**
TypeScript 映射文件通常指 source map,即 `.js.map` 文件。编译 TS 时,编译器在生成 JavaScript 的同时记录生成代码与源文件、行列号之间的映射,调试器据此把断点、堆栈和报错位置还原到 `.ts`。配置 `sourceMap: true` 可生成它,`inlineSourceMap` 可内嵌映射;生产环境是否发布需权衡调试便利与源码暴露风险。它不参与程序运行,也不会修复源码与构建产物不一致的问题。
## 79. TypeScript中的类型有哪些?
**答案:**
TypeScript 类型可分为原始类型 `string`、`number`、`boolean`、`bigint`、`symbol`,以及 `null`、`undefined`;复合和特殊类型包括数组、元组、对象、函数、字面量、联合、交叉、枚举、接口、类、泛型、`any`、`unknown`、`never`、`void`。`unknown` 比 `any` 安全,使用前必须收窄;`any` 会关闭大部分检查;`never` 表示不可能产生值。开启 `strictNullChecks` 后,`null` 和 `undefined` 不再默认属于所有类型。
## 80. 如何检查TypeScript中的null和undefined ?
**答案:**
在类型层面,推荐开启 `strictNullChecks`,此时 `null` 和 `undefined` 是独立类型,访问前需通过 `if (value == null)`、`value !== undefined`、可选链或类型守卫进行收窄。`value == null` 能同时判断两者,`value === null` 或 `=== undefined` 则只判断一种。非空断言 `value!` 只是告诉编译器“我确定不为空”,运行时不会检查,若判断错误仍会异常,因此不应把它当作校验。外部数据还需运行时验证。
## 81. 什么是TypeScript Declare关键字?
**答案:**
`declare` 关键字用于声明外部实体的类型而不提供实现,常见于第三方库的 `.d.ts` 文件、全局变量、函数、类和模块声明,例如 `declare const API_URL: string`。编译器会据此进行类型检查,但不会生成初始化代码或加载模块;真正的值必须由脚本、运行环境或其他模块提供。若声明内容与实际 API 不一致,TypeScript 可能通过编译,运行时仍会出现 `undefined` 或调用错误。
## 82. 如何在TypeScript中实现继承?
**答案:**
TypeScript 中用 `extends` 实现类继承:子类继承父类的可见实例成员,并可通过 `super()` 调用父类构造函数、通过 `super.method()` 复用父类方法;若父类有构造函数,子类构造函数必须先调用 `super()` 才能使用 `this`。子类可以重写方法,且可用 `override` 强制校验。接口继承使用 `interface Child extends Parent`,类也可 `implements` 一个或多个接口,但接口只提供类型契约,不提供实现。
## 83. 什么是Typescript的方法重载?
**答案:**
方法重载是为同一个实现函数声明多个调用签名,让调用者获得不同参数组合对应的类型提示和返回类型。例如先写 `parse(x: string): A; parse(x: number): B;`,最后写一个兼容所有情况的实现签名。实现签名对外不可见,必须覆盖所有重载签名,并在函数体内自行判断参数。JavaScript 运行时没有真正的重载,最终只有一个函数;重载数量应适度,简单场景可用联合类型表达。
## 84. 解释一下TypeScript中的枚举。
**答案:**
枚举用一组有名字的常量表示有限状态,支持数字枚举、字符串枚举和异构枚举;数字枚举默认从 0 递增,也可手动指定值,字符串枚举需逐项赋值。普通枚举会生成运行时代码,数字枚举还通常支持反向映射;`const enum` 可在编译时内联,但与某些隔离模块、构建工具兼容性较差。若只需类型约束,可用字符串字面量联合类型,通常产物更小、与 JSON 更自然。
## 85. Typescript中 interface 和 type 的差别是什么?
**答案:**
`interface` 和 `type` 都能描述对象结构,也都支持泛型、联合场景中的组合。`interface` 更适合可扩展的对象契约,支持 `extends` 和同名声明合并;`type` 是类型别名,除对象外还能表示联合、交叉、元组、基本类型、条件类型和映射类型,但不能同名合并。二者很多场景可互换:接口继承接口,类型常用交叉类型组合。团队应按可扩展性和类型表达能力选择,而不是把某一种绝对化。
## 86. Typescript中泛型是什么?
**答案:**
泛型允许把类型作为参数传入函数、接口或类,从而复用逻辑并保留调用处的精确类型,而不是退化为 `any`。例如 `function first<T>(xs: T[]): T | undefined { return xs[0] }`,传入 `string[]` 时返回类型就是 `string | undefined`。泛型可有多个参数,也能用 `T extends Constraint` 约束能力,或用 `keyof` 保证键属于对象:`function get<T, K extends keyof T>(o: T, k: K): T[K]`。
## 87. 说一说TypeScript中的类及其特性。
**答案:**
TypeScript 的类由属性、构造函数和方法组成,并支持 `public`、`protected`、`private`、`readonly`、静态成员、继承、抽象类和多态。类同时具有实例侧和静态侧类型:`new User()` 得到实例,`typeof User` 表示构造函数及静态成员。类可实现接口以满足契约,也可用抽象类复用部分实现并要求子类完成抽象方法。编译后类会转换为 JavaScript 构造函数或原生 class,修饰符主要是编译期约束。
## 88. TypeScript中的变量以及如何声明?
**答案:**
变量声明通常使用 `const`、`let` 或旧式 `var`,并可写类型标注:`let age: number = 18`。`const` 和 `let` 具有块级作用域,分别表示绑定不可重新赋值和可重新赋值;`var` 是函数作用域,存在提升和重复声明风险。TypeScript 支持显式注解,也会根据初始值推断类型;对象属性可用接口或类型别名描述。变量名遵循 JavaScript 标识符规则,不能以数字开头,类型标注只在编译期生效。
## 89. TypeScript 中的类是什么?你如何定义它们?
**答案:**
类是对一组具有共同状态和行为的对象的抽象,使用 `class` 定义,实例通过 `new` 创建。定义时可声明属性、构造函数和方法,例如 `class Student { constructor(public id: number, public name: string) {} }`,参数属性会自动生成并初始化成员。类支持继承、封装和多态,也可实现接口或声明为抽象类。需要注意,TypeScript 类型检查发生在编译期,运行时仍是 JavaScript 对象。
## 90. TypeScript 中的 getter/setter 是什么?你如何使用它们?
**答案:**
`getter` 和 `setter` 是访问器属性,用 `get`/`set` 定义,调用时像普通属性而不是函数。getter 适合计算或读取值,setter 可在赋值时做校验、转换或维护不变量,常与私有字段配合实现封装:`class User { private _name = ''; get name() { return this._name } set name(v: string) { if (!v) throw Error('empty'); this._name = v } }`。访问器必须注意递归命名、异常处理和同步语义;只定义 getter 的属性是只读的。
## 91. Typescript中什么是装饰器,它们可以应用于什么?
**答案:**
装饰器是以 `@表达式` 形式应用于类或类成员的元编程机制,可在定义阶段读取或包装目标,从而实现注册、校验、依赖注入等功能。传统 TypeScript 装饰器需在配置中启用 `experimentalDecorators`,其语义与新的 ECMAScript decorators 不完全相同;可应用于类、方法、访问器、属性和参数,但不同版本的参数签名不同。装饰器不应依赖未明确支持的执行顺序,且属性装饰器通常不能直接改写实例字段初始化。
## 92. 解释如何使用 TypeScript mixin。
**答案:**
Mixin 是把多个独立能力组合到一个类中的模式,适合表达“可同时具备的能力”,而不是单一的父子继承关系。常见写法是定义构造函数类型和混入函数,用 `Object.assign` 复制原型方法,再让目标类继承或组合这些能力:`type Ctor<T = {}> = new (...a: any[]) => T; function Timestamped<TBase extends Ctor>(Base: TBase) { return class extends Base { createdAt = Date.now() } }`。混入时要处理构造函数、成员冲突、初始化顺序和类型声明,避免隐式覆盖。
## 93. TypeScript 中的类型断言是什么?
**答案:**
类型断言是告诉编译器把某个表达式按指定类型看待,语法为 `value as Type`,在 JSX 中也可写 `<Type>value`。它只影响静态检查,不会转换数据、执行校验或改变运行时值,因此不是 Java、C# 意义上的强制类型转换。断言必须在类型上具有合理的可重叠性;确需绕过检查可先断言为 `unknown`,但应尽量用类型守卫、判别联合或运行时校验替代,避免掩盖真实错误。
## 94. TypeScript 中的模块是什么?
**答案:**
TypeScript 模块是具有顶层 `import` 或 `export` 的文件,模块成员默认只在本文件作用域内可见,通过 `export` 暴露、`import` 引入。它可使用默认导出、命名导出、类型专用导入,以及动态 `import()`。编译器根据 `module` 和 `moduleResolution` 配置解析模块;Node.js 中还要区分 CommonJS 与 ESM 的加载规则、扩展名和 `package.json` 的 `type`。没有导入导出的文件会被当作脚本,容易污染全局,可加 `export {}` 使其成为模块。
## 95. TypeScript 的内置数据类型有哪些?
**答案:**
常见内置类型包括原始类型 `string`、`number`、`boolean`、`bigint`、`symbol`、`null`、`undefined`,以及 `object`、数组、元组、函数、枚举、接口和类。类型工具还包括 `any`、`unknown`、`void`、`never`,以及联合类型 `A | B`、交叉类型 `A & B`、字面量类型和泛型。应优先使用精确类型;`unknown` 需要收窄后才能操作,`any` 会削弱检查,`void` 描述无可用返回值,`never` 描述不可能完成。
## 96. 为什么推荐使用 TypeScript ?
**答案:**
推荐 TypeScript 的核心原因是把许多错误提前到编译期,并为大型代码库提供更好的补全、重构、接口协作和文档能力。它是 JavaScript 的超集,能逐步引入类型,支持泛型、联合类型和类型推断,并可编译到不同 ECMAScript 目标,复用成熟 JS 生态。代价是编译流程、类型维护和第三方声明质量;它不能替代测试或运行时校验,尤其不能保证网络输入、数据库数据等外部值符合静态类型。