- Published on
面试 Javascript
- Authors
- Name
- Shelton Ma
TypeScript
1. TypeScript与JavaScript的区别是什么?为什么使用TypeScript而不是JavaScript?
TypeScript是JavaScript的超集,主要增加了静态类型检查和更强的类型系统。TypeScript在编译时会检查代码中的类型错误,避免运行时错误。它还支持高级类型功能,如接口、泛型、类型别名等,能提高代码的可维护性和可读性。使用TypeScript可以提前捕获潜在的错误,提升开发效率。
2. 解释一下TypeScript中的接口(Interface)和类型别名(Type Alias)的区别
- 接口(Interface) 用于定义对象的结构,支持扩展(extends)和实现(implements)。它更注重定义“形状”,通常用于面向对象编程。
- 类型别名(Type Alias) 可以为任何类型(包括基本类型、联合类型、交叉类型等)创建别名,不仅限于对象。它更灵活,但不支持扩展。
type Name = string; type NameOrResolver = Name | NameResolver;
- interface 主要用于定义对象类型,支持声明合并;type 可以定义更广泛的类型,包括基本类型、联合类型等。
3. 什么是泛型(Generics)?如何使用它来提高代码的可重用性和类型安全性?
泛型是指在定义函数、类或接口时不预先指定具体的类型,而是在使用时动态地指定类型。泛型提高了代码的可重用性和类型安全性,避免了重复代码。比如,function identity<T>(arg: T): T { return arg; }
,这个函数可以接受任何类型的参数,并返回相同类型的结果。
4. 如何使用TypeScript的声明合并(Declaration Merging)?举一个实际应用的例子
声明合并是指多个相同名字的接口或类型会合并为一个。比如,interface Person { name: string }
和 interface Person { age: number }
会合并成 interface Person { name: string, age: number }
。这种特性在扩展第三方库时非常有用。
4. 你如何处理TypeScript中的类型推断?有何限制?
TypeScript会根据变量的赋值来推断类型。例如,let x = 10;,TypeScript会推断出x是number类型。类型推断在静态检查中提供了便利,但如果类型不明确或者赋值是动态的,推断可能不准确,此时需要显式声明类型。
5. TypeScript中的“never”和“void”有什么区别?
- never 表示函数永远不会正常返回,它通常用于抛出异常或死循环的函数。
- void 表示没有返回值,通常用于那些没有返回值的函数(如
function logMessage(): void { console.log('Hello'); }
)。
6. 你如何处理TypeScript中的类型推断?有何限制?
TypeScript会根据变量的赋值来推断类型。例如,let x = 10;,TypeScript会推断出x是number类型。类型推断在静态检查中提供了便利,但如果类型不明确或者赋值是动态的,推断可能不准确,此时需要显式声明类型。
7. 解释下js中的异步方法实现, 并举例说明promise的优势/回调地狱
在JavaScript中,异步操作是指不阻塞主线程(UI线程)执行的任务,例如网络请求、定时器、文件读取等。为了处理这些异步任务,JavaScript提供了几种机制,如回调函数(Callback)、Promise和async/await
。
回调函数是最早的一种异步处理方式。回调函数在异步操作完成后执行。例如,处理文件读取时,可以传入一个回调函数,读取完成后回调函数被执行。
function fetchData(callback) {
setTimeout(() => {
console.log('Data fetched');
callback('Data');
}, 1000);
}
fetchData((data) => {
console.log('Received:', data);
});
问题: 回调地狱(Callback Hell),即嵌套多个回调函数,会导致代码层级过深,难以维护和理解,尤其是在多重异步操作的场景下。
Promise是ES6引入的一种更为现代化的异步编程方式。它是一个代表未来可能完成或失败的操作的对象,提供了链式调用的能力。Promise有三种状态:
- Pending(等待中):异步操作尚未完成。
- Resolved(已完成):异步操作成功完成。
- Rejected(已拒绝):异步操作失败。
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 模拟成功与失败
if (success) {
resolve('Data fetched');
} else {
reject('Error fetching data');
}
}, 1000);
});
}
fetchData()
.then((data) => {
console.log('Received:', data);
})
.catch((error) => {
console.log('Error:', error);
});
Promise的优势:
链式调用: Promise提供了.then()方法,可以将异步操作串联在一起,避免回调嵌套。例如,可以对多个异步操作进行链式调用,使得代码更清晰易懂。
fetchData() .then((data) => { console.log('First step:', data); return fetchData(); // 返回一个新的 Promise }) .then((data) => { console.log('Second step:', data); }) .catch((error) => { console.log('Error:', error); });
错误处理: 使用Promise时,错误处理更加集中和统一。你可以通过catch()捕获链式调用中的任何错误,而不需要为每个异步操作编写错误回调。
防止回调地狱: 比较明显的优势是能够避免回调地狱。回调地狱会导致嵌套层级过深,影响代码的可读性和可维护性,而Promise通过链式调用使代码结构更加清晰。
并行异步操作: Promise.all()可以让你并行执行多个异步操作,并在所有操作完成时进行处理。这样可以提高代码的执行效率。
const fetchData1 = () => new Promise((resolve) => setTimeout(() => resolve('Data 1'), 1000)); const fetchData2 = () => new Promise((resolve) => setTimeout(() => resolve('Data 2'), 1500)); Promise.all([fetchData1(), fetchData2()]) .then(([data1, data2]) => { console.log('Received:', data1, data2); }) .catch((error) => { console.log('Error:', error); });
async/await(基于Promise) async/await是ES2017引入的语法糖,它基于Promise,但提供了更加简洁的异步代码写法。async标记函数为异步函数,await只能在async函数内部使用,它会等待Promise的结果返回。
async function fetchData() {
const result = await new Promise((resolve, reject) => {
setTimeout(() => resolve('Data fetched'), 1000);
});
console.log(result);
}
fetchData();
8. 如何在Node.js中处理错误?
对于同步代码,错误通常通过try...catch语句来捕获。
try { let result = someFunction(); // 调用可能抛出错误的函数 } catch (err) { console.error('Error occurred:', err); // 捕获并处理错误 }
回调函数中的错误处理, 在Node.js的回调风格中,错误通常作为第一个参数传递给回调函数。这是一种约定俗成的做法,也称为”错误优先”回调。
const fs = require('fs'); fs.readFile('nonexistentFile.txt', 'utf8', (err, data) => { if (err) { console.error('File read failed:', err); return; } console.log(data); });
Promise中的错误处理, 错误可以通过.catch()方法或try...catch语句来捕获。
未捕获的Promise拒绝(Unhandled Rejection), 从Node.js 15开始,未捕获的Promise拒绝会导致进程退出。为了避免进程退出,通常应该显式处理每个Promise的错误。
const fs = require('fs').promises; fs.readFile('nonexistentFile.txt', 'utf8') .then(data => console.log(data)) .catch(err => console.error('Error occurred:', err)); // 如果没有.catch(),你可以监听unhandledRejection事件来处理未捕获的Promise拒绝: process.on('unhandledRejection', (err, promise) => { console.error('Unhandled promise rejection:', err); // 可以根据需要决定是否退出进程 });
全局错误处理
process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); // 进行必要的清理工作,并退出进程(推荐退出进程) process.exit(1); }); throw new Error('This will be caught'); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); // 可以根据需要决定是否退出进程 });
9. 解释流(Streams)的概念,以及如何在Node.js中使用它们
**流(Streams)**是处理数据的一种抽象方式。Node.js中有四种流类型:
Readable(可读流):用于从源(如文件、网络等)读取数据。
const fs = require('fs'); const readStream = fs.createReadStream('file.txt'); readStream.on('data', (chunk) => { console.log(chunk.toString()); });
Writable(可写流):用于向目标(如文件、网络等)写入数据。
const fs = require('fs'); const writeStream = fs.createWriteStream('output.txt'); writeStream.write('Hello, Node.js!'); writeStream.end(); // 关闭流,确保写入完成
Duplex(双工流):可读可写流。
const fs = require('fs'); const readStream = fs.createReadStream('example.txt'); const writeStream = fs.createWriteStream('copy.txt'); readStream.pipe(writeStream); writeStream.on('finish', () => { console.log('文件复制完成'); });
Transform(转换流):可读可写流,数据经过处理后转换。
const { Transform } = require('stream'); const upperCaseTransform = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()); callback(); } }); const readStream = fs.createReadStream('example.txt'); const writeStream = fs.createWriteStream('output.txt'); readStream.pipe(upperCaseTransform).pipe(writeStream);
为什么使用流?
- 内存效率:流按需读取和写入数据,而不是将数据一次性加载到内存中。
- 性能:通过流,数据可以在流动过程中进行处理,而不需要等待所有数据加载完。
- 适合大文件或实时数据:流非常适合处理大文件、实时数据或网络请求等场景。
10. Node.js的EventEmitter是如何工作的?
EventEmitter是Node.js中的一个类,用于处理事件和监听器。你可以创建自己的事件,并通过on方法监听,emit方法触发事件。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 监听事件
emitter.on('event', (message) => {
console.log(message);
});
// 触发事件
emitter.emit('event', 'Hello, world!');
11. Node.js的process对象包含哪些有用的属性和方法?
process对象是Node.js提供的一个全局对象,用于与当前Node.js进程交互。常见属性和方法包括:
- process.env:获取环境变量。
- process.argv:获取命令行参数。
- process.exit():退出Node.js进程。
- process.cwd():获取当前工作目录。
- process.pid:获取进程ID。
12. 你如何优化Node.js应用程序的性能
- 异步编程: 使用异步I/O和非阻塞操作来提高应用的响应能力。
- 负载均衡: 使用cluster模块来创建多个进程,利用多核CPU提升性能。
- 内存管理: 使用内存分析工具(如node --inspect)来识别和修复内存泄漏。
- 缓存: 使用Redis等缓存机制减少数据库查询的压力。
- 压缩: 使用Gzip或Brotli压缩响应数据,减少网络传输时间。
13. Node.js中如何实现身份验证和授权?
常用的身份验证方法包括:
- JWT(JSON Web Token):使用JWT来实现无状态的身份验证,前端通过JWT令牌发送请求,后端验证JWT的有效性。
- Session:通过会话存储用户登录状态,用户每次请求时携带session ID。
- OAuth2:通过第三方认证服务(如Google、Facebook)来授权用户。
const jwt = require('jsonwebtoken');
// 登录,生成JWT
app.post('/login', (req, res) => {
const token = jwt.sign({ userId: 123 }, 'your-secret-key');
res.json({ token });
});
// 认证中间件
const authenticate = (req, res, next) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(403).json({ message: 'No token provided' });
}
jwt.verify(token, 'your-secret-key', (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Invalid token' });
}
req.user = decoded;
next();
});
};
14. 闭包(Closure)
闭包(Closure)是JavaScript中的一个非常重要的概念,它描述了函数如何访问其外部作用域中的变量,即使这个函数在其外部作用域之外执行时,依然能够“记住”并访问这些变量。
- 弊端:闭包可能导致一些问题,尤其是在异步编程中,容易引发 状态泄漏 或 数据共享 问题。特别是 var 声明的变量会导致异步回调共享同一个引用。
- 解决方案:
- 使用 let 或 const 替代 var,避免共享同一作用域。
- 使用 IIFE(立即调用函数表达式)来创建独立的作用域,确保闭包中的变量不会冲突。
15. Node.js 事件循环是如何工作的?
事件循环是Node.js的核心机制,它通过异步非阻塞I/O实现高效的并发操作。事件循环有多个阶段,每个阶段会处理不同的任务,例如执行计时器回调、I/O回调等。
16. Node.js 中的 process.nextTick() 和 setImmediate() 有什么区别?
process.nextTick() 会将回调推入当前执行栈的顶部,优先执行;setImmediate() 会在当前事件循环结束时执行回调,优先级低于 process.nextTick()。
17. 如何使用 Node.js 的 cluster 模块实现多核 CPU 的利用?
cluster 模块允许你创建多个进程来充分利用多核 CPU,每个进程都可以监听同一个端口,增强应用的并发处理能力。
注意事项:
- 共享状态:工作进程之间是相互独立的,它们无法共享内存状态。如果需要共享数据,可以考虑使用 worker 进程间的 IPC(进程间通信),或使用外部的存储(如 Redis)。
- 进程重启:当一个工作进程崩溃时,主进程会通过 cluster.on('exit') 监听事件来重启它。
18. Node.js 中如何实现文件操作的异步和同步方式?
使用 fs 模块,异步操作通过 fs.readFile() 和 fs.writeFile() 实现,同步操作通过 fs.readFileSync() 和 fs.writeFileSync() 实现。
19 如何在 TypeScript 中进行类型断言?
类型断言是通过 as 或尖括号语法来告诉编译器某个值的类型
20. TypeScript 中如何使用声明合并?
TypeScript 允许多个相同名称的接口或模块进行声明合并,通常用于扩展类型或模块功能。
// 假设有一个外部库 `thirdPartyLib` 和其声明
declare module 'thirdPartyLib' {
interface User {
name: string;
}
}
// 在你的项目中扩展这个接口
declare module 'thirdPartyLib' {
interface User {
age: number;
}
}
// 使用合并后的接口
import { User } from 'thirdPartyLib';
const user: User = {
name: 'Alice',
age: 25
};
21. TypeScript 中如何使用命名空间?
命名空间用于将一组相关功能组合在一起,避免命名冲突。例如:
namespace MyNamespace {
export function greet() {
console.log("Hello");
}
}
22. TypeScript 中如何定义类和接口的关系?
interface Person {
name: string;
age: number;
}
class Employee implements Person {
constructor(public name: string, public age: number) {}
}
23. 如何在 Node.js 中实现定时任务?
setTimeout
// 在 2000 毫秒后执行一次任务 setTimeout(() => { console.log('定时任务执行了!'); }, 2000);
setInterval
// 每隔 2000 毫秒执行一次任务 const intervalId = setInterval(() => { console.log('定时任务执行了!'); }, 2000); // 停止定时任务 setTimeout(() => { clearInterval(intervalId); console.log('定时任务已停止!'); }, 10000); // 10秒后停止定时任务
node-cron
24. 描述 RESTful API 和 GraphQL 的区别
数据获取方式 • RESTful API: • REST 是基于资源的,每个资源有一个唯一的 URL 地址,客户端通过 HTTP 请求访问这些资源。 • 每个 HTTP 请求代表一个操作(如 GET、POST、PUT、DELETE 等),每种操作对应于对某个资源的不同操作。 • 数据是由服务器返回的,客户端通常无法控制返回的字段。每次请求都可能获取完整的资源,导致可能的数据过多或过少。 示例: • GET /users/1 返回用户的完整信息。 • 如果你只需要用户的名字和邮箱,你仍然需要获取完整的用户对象。 • GraphQL: • GraphQL 是一种查询语言,客户端可以精确地指定需要哪些字段,而不需要返回所有数据。 • 客户端通过构建查询请求,选择自己需要的资源字段,并可以一次请求多个资源。 • 服务器响应是按需的,仅返回客户端请求的字段,避免了数据过多或过少的问题。
端点设计
- ESTful API 的设计简单,易于理解和实现。
- GraphQL 使用单一的端点来处理所有请求。客户端通过构建查询来定义需要哪些资源或字段。
请求与响应的灵活性
25. 如何处理和优化 Node.js 的错误处理和日志记录?
错误处理
- 使用 try-catch 处理同步错误
- 异步处理
.catch
- async try
- 未捕获异常, 使用
process.on('uncaughtException', (error) => {
日志记录
console
winston
日志结构化与持久化
const express = require('express'); const morgan = require('morgan'); const fs = require('fs'); const path = require('path'); const app = express(); // 创建一个写入流,日志将写入到 'access.log' 文件 const logStream = fs.createWriteStream(path.join(__dirname, 'access.log'), { flags: 'a' }); // 使用 morgan 中间件并将日志输出到文件 app.use(morgan('combined', { stream: logStream })); // 示例路由 app.get('/', (req, res) => { res.send('Hello, world!'); }); // 启动服务器 app.listen(3000, () => { console.log('Server running on <http://localhost:3000>'); });
26. node.js如何传BigInt给前端
JSON 序列化问题:
JSON 的 stringify() 方法不支持直接序列化 BigInt 类型。当尝试将 BigInt 序列化为 JSON 时,JSON.stringify() 会抛出错误。
为了解决这个问题,你可以将 BigInt 转换为字符串,然后在前端将其转换回 BigInt。
const bigIntValue = BigInt("1234567890123456789012345678901234567890"); res.json({ value: bigIntValue.toString() }); // 自定义 BigInt 序列化 BigInt.prototype.toJSON = function () { return this.toString(); // 将 BigInt 转换为字符串 }; const bigIntValue = BigInt("1234567890123456789012345678901234567890"); res.json({ value: bigIntValue }); // 使用自定义的 toJSON
使用 BigInt 在前端:
前端 JavaScript 也支持 BigInt(从 ECMAScript 2020 开始),但需要确保传递的 BigInt 数据能够正确地被解析和处理。
<script> fetch('<http://localhost:3000/>') .then(response => response.json()) .then(data => { // 将接收到的字符串转换回 BigInt const bigIntValue = BigInt(data.value); console.log(bigIntValue); // 在控制台输出 BigInt }) .catch(error => console.error('Error:', error)); </script>
React
1. React的生命周期函数都有哪些?你如何在函数组件中使用它们?
React的生命周期函数分为三个阶段:挂载、更新和卸载。在类组件中有如 componentDidMount, componentDidUpdate, componentWillUnmount 等方法。在函数组件中,可以通过 useEffect 来模拟这些生命周期方法。例如,useEffect(() => , [])。
2. React中的“虚拟DOM”是什么?它是如何提高性能的?
虚拟DOM是React对DOM的抽象表示。每次组件更新时,React会先在虚拟DOM中进行比较(称为diffing算法),然后只对比发生变化的部分进行更新,而不是重新渲染整个DOM,这样就显著提高了性能。
3. 什么是React中的“hooks”?请解释一下useState、useEffect、useContext、useReducer的使用场景
- useState: 用于在函数组件中管理状态。const [count, setCount] = useState(0);
- useEffect: 用于副作用操作,如数据获取、订阅等。它相当于类组件的 componentDidMount 和 componentDidUpdate。
- useContext: 用于访问上下文值,它让多个组件能够共享状态或功能。
- useReducer: 当状态逻辑比较复杂时,使用 useReducer 比 useState 更合适,它通过“reducer”函数来管理状态。
4. 什么是“高阶组件”(HOC)?举一个例子说明如何使用它
高阶组件(HOC)是一个接受组件作为输入并返回一个新的组件的函数。它通常用于组件的逻辑复用。例如,withLoading HOC可以包装一个组件,使它在加载数据时显示loading状态。
5. React中的“key”属性有什么作用?为什么它对列表渲染性能很重要?
key用于帮助React识别哪些元素改变、添加或删除,从而高效地更新DOM。如果没有key,React会根据索引重新渲染整个列表,这会影响性能。key的值应该是唯一且稳定的。
6. 当有复杂数据请求, 排序和用户操作时, 使用react如何组织组件?
分层组件结构
将复杂的组件分解成多个子组件,每个组件负责不同的功能。通常的分层组件结构包括:
- Container (容器组件):负责获取数据,处理排序、过滤、分页等逻辑,并传递数据和回调函数给展示组件。
- Presentational (展示组件):负责渲染UI,不关心数据如何获取或处理。接收数据和回调作为 props。
- State Management (状态管理):对于全局或共享状态的管理,可以使用 React Context 或第三方库如 Redux、Recoil 等。
数据请求和排序逻辑
- 使用 useEffect 来进行数据请求。
- 使用 useState 来管理数据和排序状态。
- 用户操作(如点击排序按钮)时,更新 useState 状态,从而触发重新渲染。
- 如果数据量非常大,考虑使用分页或懒加载。
控制排序和过滤 排序和过滤通常是用户操作中的常见需求。我们可以通过以下方式来管理:
- 排序:可以通过改变排序状态(升序或降序),并重新排序数据。
- 过滤:根据用户的输入过滤数据,通常与搜索框结合。
总结
- 组件拆分: 将容器组件和展示组件分开,确保逻辑和UI分离。
- 状态管理: 用 useState 和 useEffect 管理数据请求和用户交互。
- 性能优化: 考虑数据量较大时的分页和懒加载机制,避免一次性加载过多数据导致性能问题。
7. 乐观操作
乐观操作(Optimistic UI) 是一种用户界面设计模式,允许用户在等待服务器响应的过程中看到操作的结果。简而言之,乐观操作会“提前假设”操作成功,从而更新UI,提升用户体验,避免因等待响应而产生的延迟感。
实现乐观操作的思路
- 更新 UI: 在发起请求之前,立即在前端应用中更新界面,让用户看到操作已生效。
- 服务器请求: 向服务器发送请求,进行实际的操作(例如创建、更新、删除等)。
- 恢复操作: 如果请求成功,保留前端的更新。如果请求失败,恢复到操作前的状态,或者显示失败信息。
Express
1. Express中的中间件是什么?如何使用它来处理请求和响应?
中间件是处理请求和响应的函数,可以在请求到达路由处理之前或之后执行。它可以用于日志记录、身份验证、请求体解析等。示例:app.use(express.json());
来解析JSON请求体。
2. 如何在Express中处理错误?请举例说明如何使用错误处理中间件
错误处理中间件需要放在路由和其他中间件之后,通过next(err)将错误传递给它。示例:
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send('Something went wrong!');
});
3. 如何在Express中设置路由,支持GET和POST请求?
可以使用 app.get() 和 app.post() 来处理不同类型的请求。例如:
app.get('/home', (req, res) => {
res.send('Home Page');
});
app.post('/submit', (req, res) => {
res.send('Form Submitted');
});
4. Express中的请求体(Request Body)如何处理,如何解析JSON和表单数据?
使用express.json()来解析JSON数据,express.urlencoded()来解析表单数据
5. 如何在Express中进行认证和授权?你如何使用JWT实现身份验证?
JWT(JSON Web Token)可以在Express中用于认证。通过中间件验证请求头中的token,来授权用户访问受保护的资源。
6. 如何优化Express应用的性能?
可以使用缓存、压缩响应、异步处理、负载均衡等方式来优化Express应用的性能。