前端计算机基础

前端计算机基础

进程和线程的区别

简单记:进程是 “独立的容器”,线程是 “容器里干活的人”,多人共享容器资源,效率更高但也更容易互相影响。

进程:独立可运行的程序,比如微信,留言及,VSCODE
进程是操作系统资源分配的最小单位(资源包括内存、CPU 时间片、文件句柄等),每个进程都有自己独立的内存空间,进程之间互不干扰。
线程:是进程的执行单位,一个进程可以包含多个县城,比如微信进程中,有接收消息线程,渲染界面线程
线程是调度执行的最小单位 ,同一进程内的线程共享进程的内存和资源。

类比:进程像一家 “独立的公司”,有自己的办公场地(内存)、资金(系统资源);线程像公司里的 “员工”,共享公司的场地和资金,各自做不同的工作,协作完成公司整体任务。

维度进程线程
资源分配系统资源分配的最小单位资源调度 / 执行的最小单位
内存空间每个进程有独立的内存空间共享所属进程的内存空间
通信方式复杂(需 IPC:管道、套接字、共享内存等)简单(直接读写进程内共享变量)
创建 / 销毁开销大(需分配独立内存、资源)小(仅需栈空间,共享进程资源)
独立性高(进程崩溃不影响其他进程)低(线程崩溃可能导致整个进程崩溃)
切换开销大(需切换地址空间、资源上下文)小(仅切换执行上下文)
控制权由操作系统内核管理可由进程自己管理(用户级线程)或内核管理
1. 进程的使用场景
  • 独立运行的程序:比如你写的一个 UniApp 打包后的 APP、一个 Python 脚本单独运行,都是一个进程。
  • 需隔离资源的场景:比如服务器上的多个服务(数据库、后端接口、前端静态服务),用不同进程运行,避免一个服务崩溃影响其他服务。
2. 线程的使用场景
  • 同一任务的并行处理:比如你之前写的称重代码中,串口数据接收线程、定时读取重量线程,都属于同一个 APP 进程内的线程,共享串口资源和重量数据。
  • 耗时操作不阻塞主线程:比如 UniApp 中,网络请求、串口通信都要放在子线程,避免阻塞 UI 渲染线程(主线程),否则界面会卡顿。

进程间的通信

node中进程通讯分为两种:父子进程通信

​ 无亲缘关系进程通信

进程间通信(Inter-Process Communication,IPC)是指在操作系统中,不同进程之间交换信息和数据的过程,常见的进程通信方式包括:

  • 内置 IPC 通道 //父子进程
  • 管道(Pipe) //无亲缘关系进程通信
  • 网络套接字(Socket) //无亲缘关系进程通信
  • 文件 / 数据库(Redis 发布订阅) // 无亲缘关系进程通信
方式适用场景优点缺点
内置 IPC 通道父子进程(Node.js 间)高效、原生、使用简单仅父子进程、不跨机器
管道父子进程 / 本地进程轻量、系统原生仅本地、单向 / 半双工
TCP Socket任意进程(跨机器 / 跨语言)通用、跨平台、实时性好有网络开销、需处理连接
Redis 发布订阅跨进程 / 跨机器解耦、易扩展依赖 Redis 服务、有延迟
1.父子进程通信

Node.js 的 child_process 模块(尤其是 fork() 方法)内置了高效的 IPC 通道,这是实现父子进程通信最便捷的方式。

父进程 const {fork} = require("child_process"); //创建子进程 var fork1 = fork("./common/ChildIpc"); fork1.on("message", message => { console.log(`父进程收到子进程消息:`, message); }) fork1.send({ data: 123 }) setTimeout(()=>{ console.log("123") },5000) 
子进程 process.on("message", msg => { console.log("收到主进程的消息", msg) }) 

另外 cluster 模块, cluster 模块基于 child_process.fork() 实现,专门用于多核 CPU 利用,主进程和工作进程之间同样通过 send()/message 通信。

2.无亲缘关系进程通信
1. 管道(Pipe)

管道是操作系统提供的基础 IPC 机制,分为匿名管道(仅父子进程)和命名管道(任意进程)。Node.js 中 child_processspawn/exec 方法默认会创建管道,用于标准输入 / 输出(stdin/stdout/stderr)通信。

1.匿名管道
父进程 const {spawn} = require('child_process'); var childProcessWithoutNullStreams = spawn('node', ["./common/ChildIpc.js"]); childProcessWithoutNullStreams.stdin.write("父进程通过管道发送的数据"); childProcessWithoutNullStreams.stdin.end(); // 父进程监听子进程的 stdout 输出(子进程的回复) childProcessWithoutNullStreams.stdout.on('data', (data) => { console.log('父进程收到子进程回复:', data.toString()); }); setTimeout(() => { console.log("123") }, 5000) 
子进程 process.stdin.on("data", (data) => { console.log("收到主进程管道的消息", Buffer.from(data).toString()); process.stdout.write("反馈"); }) 
2.命名管道

命名管道本质是文件系统中的一个特殊文件(Linux/macOS 是 FIFO 文件,Windows 是 \\.\pipe\ 格式的管道名),不同进程可以通过读写这个 “文件” 来交换数据,它是双向通信的,且通信效率远高于网络套接字(无需走网络协议栈)。

  1. 命名管道服务端 (pipe-server.js)
const fs = require('fs'); const net = require('net'); const os = require('os'); // 定义跨平台的命名管道路径 const PIPE_PATH = os.platform() === 'win32' ? '\\\\.\\pipe\\my-node-pipe' // Windows 管道格式 : '/tmp/my-node-pipe'; // Linux/macOS FIFO 文件路径 // Linux/macOS 下需要先创建 FIFO 文件(Windows 无需此步骤) function createFifoIfNeeded() { if (os.platform() !== 'win32') { try { // mkfifo 命令创建 FIFO 文件,模式 0o666 表示读写权限 fs.mkfifoSync(PIPE_PATH, 0o666); console.log(`创建 FIFO 文件: ${PIPE_PATH}`); } catch (err) { if (err.code === 'EEXIST') { console.log(`FIFO 文件已存在: ${PIPE_PATH}`); } else { throw err; } } } } // 启动命名管道服务端 function startPipeServer() { createFifoIfNeeded(); // Windows 用 net.createServer 监听管道,Linux/macOS 用 fs.createReadStream/fs.createWriteStream if (os.platform() === 'win32') { // Windows 命名管道基于 net 模块实现 const server = net.createServer((connection) => { console.log('客户端已连接(Windows 管道)'); // 接收客户端数据 connection.on('data', (data) => { console.log(`收到客户端消息: ${data.toString().trim()}`); // 回复客户端 connection.write(`服务端已收到: ${data.toString().trim()}\n`); }); // 监听连接关闭 connection.on('end', () => { console.log('客户端断开连接'); }); }); // 监听命名管道 server.listen(PIPE_PATH, () => { console.log(`Windows 命名管道服务端启动: ${PIPE_PATH}`); }); } else { // Linux/macOS 读取 FIFO 管道数据 const readStream = fs.createReadStream(PIPE_PATH); readStream.setEncoding('utf8'); console.log(`Linux/macOS FIFO 服务端启动: ${PIPE_PATH}`); // 接收客户端数据 readStream.on('data', (data) => { const msg = data.toString().trim(); console.log(`收到客户端消息: ${msg}`); // 回复客户端(写入同一个管道) const writeStream = fs.createWriteStream(PIPE_PATH, { flags: 'a' }); writeStream.write(`服务端已收到: ${msg}\n`); writeStream.end(); }); // 监听管道关闭 readStream.on('close', () => { console.log('FIFO 管道关闭'); }); } } // 启动服务端 startPipeServer(); // 优雅退出,清理资源 process.on('SIGINT', () => { if (os.platform() !== 'win32' && fs.existsSync(PIPE_PATH)) { fs.unlinkSync(PIPE_PATH); // 删除 Linux/macOS 下的 FIFO 文件 console.log(`已删除 FIFO 文件: ${PIPE_PATH}`); } process.exit(0); }); 
  1. 命名管道客户端 (pipe-client.js)
const fs = require('fs'); const net = require('net'); const os = require('os'); // 和服务端一致的管道路径 const PIPE_PATH = os.platform() === 'win32' ? '\\\\.\\pipe\\my-node-pipe' : '/tmp/my-node-pipe'; // 向命名管道发送消息 function sendMessage(msg) { if (os.platform() === 'win32') { // Windows 客户端连接管道 const client = net.connect(PIPE_PATH, () => { console.log('已连接到 Windows 命名管道服务端'); // 发送消息 client.write(msg + '\n'); }); // 接收服务端回复 client.on('data', (data) => { console.log(`服务端回复: ${data.toString().trim()}`); client.end(); // 关闭连接 }); // 处理错误 client.on('error', (err) => { console.error('连接失败:', err.message); }); } else { // Linux/macOS 客户端写入 FIFO 管道 if (!fs.existsSync(PIPE_PATH)) { console.error(`FIFO 文件不存在: ${PIPE_PATH}`); return; } // 写入消息 const writeStream = fs.createWriteStream(PIPE_PATH, { flags: 'a' }); writeStream.write(msg + '\n'); writeStream.end(() => { console.log('客户端消息已发送'); // 读取服务端回复 const readStream = fs.createReadStream(PIPE_PATH); readStream.setEncoding('utf8'); readStream.on('data', (data) => { console.log(`服务端回复: ${data.toString().trim()}`); readStream.destroy(); // 关闭读取流 }); }); } } // 执行客户端:node pipe-client.js "Hello Named Pipe" const message = process.argv[2] || '默认消息:Hello Node.js 命名管道'; sendMessage(message); 
系统管道路径格式实现方式
Windows\\.\pipe\<管道名>基于 net 模块(类似 TCP)
Linux/macOS/tmp/<管道名>(FIFO 文件)基于 fs 模块读写 FIFO 文件
2.socket
// 进程 A(服务端) const net = require('net'); const server = net.createServer((socket) => { console.log('有进程连接成功'); // 接收客户端数据 socket.on('data', (data) => { console.log('服务端收到:', data.toString()); // 回复客户端 socket.write('服务端已收到消息:' + data.toString()); }); }); server.listen(3000, '127.0.0.1', () => { console.log('TCP 服务端启动,监听 3000 端口'); }); // 进程 B(客户端) const net = require('net'); const client = net.connect({ port: 3000, host: '127.0.0.1' }, () => { console.log('客户端连接成功'); // 发送数据给服务端 client.write('进程 B 发送的消息'); }); // 接收服务端回复 client.on('data', (data) => { console.log('客户端收到:', data.toString()); client.end(); }); 
2.共享存储 (redis发布订阅)
// 进程 1(发布者) const { createClient } = require('redis'); const publisher = createClient(); // 默认连接本地 Redis publisher.connect(); setInterval(async () => { const msg = `当前时间:${new Date().toLocaleString()}`; await publisher.publish('process-channel', msg); console.log('发布者发送:', msg); }, 1000); // 进程 2(订阅者) const { createClient } = require('redis'); const subscriber = createClient(); subscriber.connect(); subscriber.subscribe('process-channel', (msg) => { console.log('订阅者收到:', msg); }); 

线程间的通信

和进程间通信不同,Node.js 的工作线程虽然有独立的 V8 实例和内存,但支持两种通信模式:

  1. 消息传递(默认):通过复制数据的方式通信(序列化 / 反序列化),安全但有少量性能开销;
  2. 共享内存:通过 SharedArrayBuffer 直接共享内存,无数据复制开销,但需要手动处理并发安全。
1. 基础方式:主线程 ↔ 工作线程(消息传递)
主线程 const {Worker} = require("worker_threads") var worker = new Worker("./common/ChildThread.js", { workerData: {num: 1e9} // 传递给工作线程的数据 }); // 监听工作线程的消息 worker.on('message', (result) => { console.log('工作线程计算结果:', result); //关闭子线程 worker.terminate() }); // 监听工作线程错误 worker.on('error', (err) => { console.error('工作线程出错:', err); }); // 监听工作线程退出 worker.on('exit', (code) => { if (code !== 0) { console.error(`工作线程退出,码值: ${code}`); } }); worker.postMessage({data: {num: 1e9}}); // 主线程继续执行其他逻辑 console.log('主线程不阻塞,继续执行'); 
子线程 parentPort.on('message', message => { console.log("收到主线程的消息", message); parentPort.postMessage("xxxxxxxxxxxxxx"); }) 
2. 进阶方式:工作线程 ↔ 工作线程(直接通信)
const {Worker} = require("worker_threads") const thread = require("worker_threads") var worker = new Worker("./common/ChildThread.js",); var worker1 = new Worker("./common/ChildThread1.js",); let {port1, port2} = new thread.MessageChannel(); // 监听工作线程的消息 worker1.on('message', (result) => { console.log('工作线程计算结果:', result); //关闭子线程 worker.terminate() worker1.terminate() }); 
const {parentPort} = require("worker_threads") let worker2; parentPort.on("message", message => { worker2 = message.port; worker2.on('message', (data) => { console.log(`worker2 收到 worker1 的消息:${data}`); parentPort.postMessage("dfdf") }); }) let worker1; parentPort.on('message', message => { worker1 = message.port; worker1.on('message', (data) => { console.log(`worker2 收到 worker1 的消息:${data}`); worker1.postMessage('hello worker1,我是 worker2!'); }) // 主动向 worker2 发消息 worker1.postMessage('hello worker2,我是 worker1'); }) 
3. 高性能方式:共享内存(SharedArrayBuffer)

通过 SharedArrayBuffer 实现线程间内存共享,数据无需复制,适合大数据量传输(如实时计算、数据共享),但需要注意并发安全。

// main.js const { Worker } = require('worker_threads'); // 创建共享内存(单位:字节,这里分配 4 字节存整数) const sharedBuffer = new SharedArrayBuffer(4); // 用 Uint32Array 包装共享内存,方便操作 const sharedArray = new Uint32Array(sharedBuffer); // 初始化值为 0 sharedArray[0] = 0; // 创建两个工作线程,共享同一块内存 const worker1 = new Worker('./worker-shared.js', { workerData: { sharedBuffer, id: 1 } }); const worker2 = new Worker('./worker-shared.js', { workerData: { sharedBuffer, id: 2 } }); // 监听线程退出,打印最终结果 setTimeout(() => { console.log(`共享内存最终值:${sharedArray[0]}`); worker1.terminate(); worker2.terminate(); }, 1000); 
// worker-shared.js const { workerData } = require('worker_threads'); const { sharedBuffer, id } = workerData; // 包装共享内存 const sharedArray = new Uint32Array(sharedBuffer); // 两个线程同时累加 1000 次(模拟并发) for (let i = 0; i < 1000; i++) { // 使用 Atomics 保证原子操作,避免竞态问题 Atomics.add(sharedArray, 0, 1); } console.log(`线程 ${id} 累加完成`); 

单核CPU如何实现并发

单核CPU 主要是通过时间片轮转和上下文切换来实现并发

时间片轮转

  • CPU将时间划分为很小的时间片,通常是几十毫秒
  • 每个进程、线程分配到一个时间片
  • CPU轮流执行每个进程、线程的时间片
  • 当一个时间片用完,CPU就会切换到下一个进程、线程

上下文切换

  • 在切换进程、线程时,CPU需要保存当前进程的状态(上下文),包括
    • 程序计数器的值
    • 寄存器的值
    • 内存映射信息
  • 加载下一个要执行的进程、线程的上下文
在任意时刻,CPU只能执行一个任务,由于切换速度非常快,给用户的感觉就像是在同时运行多个程序,所以这种机制被称为“伪并发”,若线程过多也不好,频繁的上下文切换会带来一定的性能开销,所以过多的线程反而会带来性能下降的问题

CPU 调度算法有哪些?

  • 先来先服务(First Come First Serve)
    • 最简单的调度算法
    • 进程按到达的顺序排队,先到达的先执行
    • 缺点:可能导致长时间的等待,特别是当一个长进程在队列前面时
  • 短作业优先(Shortest Job First)
    • 优先执行预计运行时间最短的进程
    • 可以是非抢占式或是抢占式(shortest Remaining Time First,SRTF)
    • 可能导致“饥饿”现象,即长作业可能永远得不到执行
  • 优先级调度(Priority Scheduling)
    • 每个进程分配一个优先级,优先级高的进程先执行
    • 也可能导致“饥饿”现象,通常使用老化(aging)技术来解决
  • 轮转调度(Round Robin)
    • 每个进程分配一个固定的时间片,时间片用完后,进程被放到队列的末尾
    • 适用于时间共享系统
    • 时间片的大小对系统性能有很大影响
  • 多级队列调度(Multilevel Queue Scheduling)
    • 将进程分成多个队列,每个队列有不同的优先级
    • 不同队列可以使用不同的调度算法
  • 多级反馈队列调度(Multilevel Feedback Queue)
    • 允许进程在不同的队列之间移动
    • 根据进程的行为动态调整其优先级

linux 如何查找你的进程占用的那个端口

# 查看所有端口占用情况 netstat -tunlp # 查看特定端口,比如 8080 netstat -tunlp | grep 8080 

返回pid

# TCP 端口 fuser 8080/tcp # UDP 端口 fuser 8080/udp 

结束占用

 kill 805 

什么是编译型语言和解释型语言,他们有什么区别?

可以把编程语言的执行过程想象成 “看书”:

  • 编译型语言:就像先请翻译把整本外文书翻译成中文书,你之后直接看翻译好的中文书就行。程序运行前,会通过编译器将源代码一次性翻译成机器能直接执行的二进制指令(可执行文件),后续运行时不再需要源代码。
  • 解释型语言:就像请翻译逐句给你解释外文书,你看一句,翻译解释一句。程序运行时,由解释器逐行读取源代码、逐行解释执行,不会生成独立的二进制可执行文件。
维度编译型语言解释型语言
执行流程先编译(一次性翻译)→ 后运行边解释(逐行翻译)→ 边运行
运行效率运行速度快(直接执行机器码)运行速度慢(每次都要解释)
开发调试效率编译报错后需修改重新编译,调试稍慢改代码后可立即运行,调试更灵活
跨平台性编译后的可执行文件和平台强绑定(如 Windows 编译的 exe 不能在 Linux 运行)只要有对应解释器,代码可跨平台运行
代表语言C、C++、Go、Rust、javaPython、JavaScript、PHP、Ruby
优点代码在运行前已经被编译为机器码,运行速度快由于不需要编译成机器码,开发和调试过程通常更快,更灵活
缺点需要编译步骤,开发和调试较慢运行速度通常比编译型语言慢,因为每次执行都需要进行翻译

编译型语言和解释型语言的区别

  • 执行速度:编译型语言通常比解释型语言快,因为他们直接运行机器码
  • 开发灵活性:解释型语言通常更灵活,适合快速开发和迭代
  • 错误检测:编译型语言在编译阶就可以捕获更多的语法和类型错误,而解释型语言通常在运行时才发现错误

JIT(Just-In-Time)编译

为了结合编译型和解释型语言的优点,JIT 随之诞生,可以理解为“即时编译”

  • 执行方式:JIT 编译在程序运行时将部分代码编译成机器码,而不是逐行解释,这种编译方式在代码即将被执行时进行,因此得名“即时编译”
  • 应用场景:现代 JavaScript 引擎(如 V8 引擎)通常使用 JIT 编译来提高性能,包括 Java 虚拟机(JVM)也会使用 JIT 编译来提高性能

栈与堆

在 JavaScript 中,所有变量和数据都会被存储在内存中,而栈和堆是两种不同的内存分配区域,分工明确:

  • 栈(Stack):类似 “叠盘子”,遵循「先进后出(LIFO)」原则,存储占用空间固定、大小已知的数据,访问速度极快。
  • 堆(Heap):类似 “开放式仓库”,存储占用空间不固定、大小动态变化的数据,访问速度相对慢,但灵活性高。
1. 栈(Stack)—— 存储基础类型 + 引用类型的指针

JavaScript 的基本数据类型会直接存在栈中:

  • 基本类型:NumberStringBooleanUndefinedNullSymbolBigInt
  • 特点:占用空间小且固定,赋值时会复制整个值(值传递)。

此外,函数调用的执行上下文(比如函数的参数、局部变量、执行状态)也会存在栈中,函数执行完后会被立即弹出栈,释放内存。

// 基本类型存储在栈中 let a = 10; let b = a; // 复制栈中的值,a和b是两个独立的变量 b = 20; console.log(a); // 输出 10(a不受b修改影响) console.log(b); // 输出 20 
2. 堆(Heap)—— 存储引用类型

JavaScript 的引用类型会存储在堆中,栈中仅保存指向堆内存的「指针(地址)」:

  • 引用类型:Object(包括数组、对象、函数、正则等)
  • 特点:占用空间大且动态变化,赋值时仅复制指针(引用传递),多个变量可能指向堆中同一个数据。
// 引用类型存储在堆中,栈中存地址 let obj1 = { name: "张三" }; let obj2 = obj1; // 复制栈中的地址,obj1和obj2指向堆中同一个对象 obj2.name = "李四"; console.log(obj1.name); // 输出 李四(修改obj2会影响obj1) console.log(obj2.name); // 输出 李四 // 验证地址指向:修改obj2的引用不会影响obj1 obj2 = { name: "王五" }; // 栈中obj2的地址指向新的堆内存 console.log(obj1.name); // 输出 李四(obj1仍指向原地址) console.log(obj2.name); // 输出 王五 
特性栈(Stack)堆(Heap)
存储内容基本类型、引用类型的指针引用类型(对象、数组等)
空间大小固定、小动态、大
访问速度快(直接访问)慢(通过指针间接访问)
内存分配自动分配(函数执行 / 变量声明)手动 / 引擎分配(需垃圾回收)
回收机制函数执行完自动释放需 JS 垃圾回收器(GC)清理
  • 栈内存:函数执行上下文出栈时,对应的变量会被立即释放,无需垃圾回收。
  • 堆内存:当一个对象不再被任何变量(栈中的指针)引用时,垃圾回收器会在合适时机清理这块内存,避免内存泄漏。

简述 JS 垃圾回收的过程。用什么算法?

首先,我们可以用一个通俗的比喻来理解:JS 引擎就像一个管家,内存是家里的储物空间。当你创建变量、对象、函数时,管家会分配空间存放;垃圾回收 就是管家定期清理那些你再也用不到的 “杂物”(不再被引用的内存),释放空间避免堆积。

核心原则:找出不再使用的变量 / 对象,释放其占用的内存

二、JS 垃圾回收的主要算法

JS 引擎(如 V8)主要使用两种核心算法,且会根据数据大小 / 类型选择不同策略:

1. 引用计数法(早期算法,有缺陷)
  • 原理:跟踪每个值被引用的次数。
    • 当一个值被创建并赋值给变量时,引用数 = 1;
    • 若该值被其他变量引用,引用数 +1;
    • 若引用它的变量被覆盖 / 销毁,引用数 -1;
    • 当引用数变为 0 时,说明该值无法被访问,会被回收。
  • 缺陷:无法解决 循环引用 问题(比如两个对象互相引用,即使都不再被外部使用,引用数也永远不为 0)。
function fn() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.b = obj1; // obj2 引用 obj1 } fn(); // 函数执行完后,obj1/obj2 本应被回收,但循环引用导致引用数不为 0 
2. 标记清除法(现代 JS 引擎主流算法)

这是目前 Chrome/V8、Node.js 等环境的核心算法,解决了循环引用问题。

  • 原理(分两步):
    1. 标记阶段:从根对象(全局对象 window/global、执行栈中的变量等)出发,遍历所有可访问的对象,给这些 “可达” 的对象打上标记;
    2. 清除阶段:清理所有未被标记的对象(即 “不可达” 的对象),释放其内存。
  • 优势:即使对象循环引用,只要它们无法从根对象访问到,就会被标记为垃圾并清除。
function fn() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; obj2.b = obj1; } fn(); // 函数执行完后,obj1/obj2 从根对象(全局)无法访问,会被标记为垃圾并清除 
3. 分代回收(V8 优化策略)

V8 把内存分为两类,针对不同特点优化回收效率:

  • 新生代(Young Generation):存放短期存活的对象(如临时变量),空间小(几 MB),回收频率高,采用 Scavenge 算法(将内存分为 From/To 两个区域,复制存活对象到 To 区,清空 From 区,然后交换两区角色);
  • 老生代(Old Generation):存放长期存活的对象(如全局变量),空间大,回收频率低,主要用 标记 - 清除 + 标记 - 整理 算法(标记清除后,会整理内存碎片,避免内存不连续)。

闭包为什么不会被销毁

JavaScript 有自动垃圾回收机制,它的核心判断逻辑是:如果一个对象 / 变量没有任何可达的引用(即 “无人使用”),就会被标记为可回收,最终销毁并释放内存

闭包不会被销毁,本质是打破了这个规则 —— 闭包让内部函数保留了对外部函数作用域的引用,导致外部函数的作用域始终有 “可达引用”,因此不会被回收。

function outer() { // 外部函数的变量 let count = 0; // 内部函数(闭包):引用了外部的count function inner() { count++; console.log(count); } // 返回内部函数,让外部能访问到inner return inner; } // 执行outer,得到inner函数,并赋值给变量fn const fn = outer(); // 调用fn,依然能访问到count fn(); // 输出 1 fn(); // 输出 2 

不是所有闭包都不会销毁:如果闭包没有被外部引用(比如内部函数只在外部函数内部调用,没有返回 / 赋值给外部变量),那么外部函数执行完后,闭包和对应的作用域会被正常回收。

闭包不销毁≠内存泄漏:合理使用闭包(比如用完后手动切断引用)不会造成内存泄漏;只有滥用闭包(比如长期保留不必要的闭包引用)才会导致内存占用过高。

什么是内存泄漏?如何排查?JS 内存泄漏的常见原因?

内存泄露是指在程序运行过程中,程序未能释放不再使用的内存空间,导致内存资源被浪费。

排查内存泄露

  1. 使用内存分析工具
    • 浏览器开发者工具:Chrome 的 DevTools 提供了内存分析工具 Memory,可以监控内存使用情况
    • 也可以结合 setInterval 使用 console.memory 查看内存使用的快照
  2. 代码审查
    • 检查代码中是否有未释放的事件监听器,定时器,全局变量,确保不再需要某对象时,及时解除引用
  3. 性能监控
    • 监控应用程序的内存使用情况,观察是否有持续增长的趋势
    • 使用日志记录内存使用情况,帮助识别内存泄露的模式

JS 内存泄露的常见原因

  1. 意外的全局变量
    • 忘记使用 var,let,const 声明变量时,变量会被挂载到全局对象上
  2. 闭包
    • 闭包中引用了不再需要的外部变量,导致这些变量无法被垃圾回收
  3. 未清理的 DOM 引用
    • 删除 DOM 元素时,未能清理相关的事件监听器或引用
  4. 定时器和回调
    • 未能清理不再需要的 setInterval 或 setTimeout 回调

冯·诺依曼架构是什么?

冯·诺依曼架构确定了现代计算机结构中的五大部件:

  1. 输入设备: 键盘,鼠标,摄像头等
  2. 输出设备: 显示器,打印机,扬声器等
  3. 存储器: 计算机的记忆装置,主要存放数据和程序。分为内部存储器(内存/主存储器)和外部存储器(硬盘,光盘,U盘等)
    • 内存:也称之为主存储器,又分为随机存储器(RAM)和只读存储器(ROM)
      • RAM 存放的是计算机在通电运行的过程中即时的数据,计算机的内存容量就是指的 RAM 的容量。RAM 可读,可写,断电会数据丢失
      • ROM 存放的是每次计算机开机都需要处理的,固定不变的程序和数据,比如 BIOS 程序。ROM 可读,不可写,断电不会丢失
    • 外存:外存是硬盘,是计算机的辅助存储器,可以长期保存数据,断电不会丢失。当计算机需要从外存读取数据时,需要将数据从外存读取到内存中,然后才能下一步处理。根据介质不同,外存可分为软盘,硬盘,光盘。硬盘最为常见,硬盘又分为机械硬盘(HD)和固态硬盘(SSD)。
  4. 运算器: 算术逻辑单元(ALU),负责执行算术和逻辑运算
  5. 控制器: 控制整个计算机系统的工作流程,包括指令的执行顺序,数据传输等,运算器和控制器通常集成在一起,就是我们熟知的 CPU

计算机内部为何使用二进制?

  • 硬件实现简单:二进制只需要两个状态,通常用电压的高低来表示(如高电压表示 1,低电压表示 0)。这种简单的状态切换使得硬件电路设计更为简单和可靠
  • 抗干扰能力强:在电路中,二进制的两个状态(0 和 1)可以通过明显的电压差来区分,这使得系统对噪声和干扰的容忍度更高,数据传输更稳定
  • 逻辑运算方便:计算机的基本运算是逻辑运算,二进制系统与布尔代数非常契合,能够进行与,或,非等逻辑运算,简化了计算机的设计和操作
  • 存储和处理效率高:二进制数据在计算机中可以直接存储和处理,避免了其他进制系统转换带来的复杂性和效率损失
  • 历史和标准化:从计算机发展的早期,二进制就被广泛采用,形成了标准化的设计和技术积累,进一步推动了其普及和应用

什么是虚拟内存,为何要使用虚拟内存?

虚拟内存是操作系统搞出来的一个 “假内存” 机制:

  • 让每个程序都以为自己独占一大片连续的内存地址(虚拟地址)
  • 实际物理内存(内存条)很小,系统会把这些 “虚拟地址”映射到真实的物理内存或硬盘上
  • 程序只管使用虚拟地址,不用关心数据到底在内存还是硬盘

简单说:

虚拟内存 = 程序看到的地址空间

物理内存 = 真正的内存条

为什么要用虚拟内存?(核心 4 点)
  1. 让程序不用管物理内存多大程序写死用虚拟地址,不用考虑内存条大小,兼容性极强。
  2. 让内存 “看起来更大”物理内存不够时,系统把暂时不用的数据放到硬盘(页面文件 /swap),相当于用硬盘当 “临时内存”,能同时跑更多程序。
  3. 让每个程序互相隔离、更安全每个进程有独立虚拟地址空间,一个程序崩溃 / 出错,不会搞崩整个系统或其他程序

解决 “内存碎片” 问题物理内存可能零零碎碎,但虚拟内存永远是连续的,程序申请大块内存时更方便。

一句话总结

虚拟内存就是操作系统给程序画的 “大饼”:让程序以为内存很大、连续、独占,实际由系统偷偷管理真实物理内存和硬盘,既省硬件,又安全稳定。

什么是 Unicode 编码?它和常见的 UTF-8 有什么关系?

  • Unicode 俗称万国码,它为每个字符提供了一个唯一的数字标识,这个数字标识被称为码点。Unicode 的出现就是为了解决 ASCII 编码的局限性,ASCII 编码只能表示 128 个字符,全球各个国家的字符远不止 128 个,所以 Unicode 应运而生。
  • Unicode 的定义了一个字符集和一系列编码方案(UTF-8,UTF-16,UTF-32),UTF-8 是 Unicode 最常用的编码方案,它是一种变长编码,根据不同的字符,使用不同的字节数来表示。对于 ASCII 字符,使用 1 个字节表示,与 ASCII 编码兼容
  • GBK 编码是一种用于中文字符的编码标准,扩展了 GB2312 编码,支持简体和繁体中文字符。GBK 编码使用 2 个字节表示一个中文字符,适合中文环境,但不支持 Unicode

简述计算机网络的 OSI 模型

1. 应用层(Application Layer)

为应用程序提供网络服务接口。

协议:HTTP、HTTPS、FTP、SMTP、DNS

2. 表示层(Presentation Layer)

负责数据格式转换、加密、解密、压缩、解压缩。

3. 会话层(Session Layer)

建立、管理、终止应用之间的会话连接。

4. 传输层(Transport Layer)

负责端到端的数据传输、流量控制、差错重传。

协议:TCP、UDP

数据单元:段(Segment)

5. 网络层(Network Layer)

负责寻址、路由选择,将数据从源送到目标。

协议:IP、ICMP、ARP

数据单元:包(Packet)

将物理层收到的信号组成帧,进行差错检测。

设备:交换机、网卡

数据单元:帧(Frame)

7. 物理层(Physical Layer)

传输比特流,定义电气、物理接口标准。

设备:集线器、中继器、网线、光纤

数据单元:比特(Bit)

一个域名对应一个 ip 吗

1. 一个域名 → 多个 IP

很常见,目的:负载均衡、高可用、就近访问

  • 比如:baidu.com 会解析到一堆 IP
  • 你访问时,DNS 会返回其中一个
  • 多用于大型网站、CDN、云服务
2. 一个 IP → 多个域名

更常见,叫虚拟主机 / 共享主机

  • 一台服务器一个 IP,但上面跑几十个网站
  • 浏览器请求时会带上 Host 头,服务器知道你要访问哪个站
简单记
  • 域名 → IP:可以一对多(负载均衡)
  • IP → 域名:可以多对一(共享服务器)

只有极少数场景才是严格一对一

  • 独立服务器、单独备案、独享 IP 的站点等。

UDP 和 TCP和HTTP 协议的区别?有什么应用场景

1. 三者本质关系
  • TCP/UDP传输层协议,负责数据怎么传
  • HTTP应用层协议,基于 TCP,负责数据是什么、要干嘛

一句话:

HTTP 跑在 TCP 之上,TCP 和 UDP 是平级的传输协议。

2. TCP vs UDP 核心区别
TCP(传输控制协议)
  • 可靠:有确认、重传、有序、不丢包
  • 面向连接:先三次握手建立连接
  • 有拥塞控制
  • 速度慢、开销大
UDP(用户数据报协议)
  • 不可靠:发出去就不管,可能丢包、乱序
  • 无连接:直接发,不用握手
  • 速度极快、开销小
3. HTTP 是什么
  • 应用层协议,基于 TCP
  • 定义了:请求方法(GET/POST)、头、状态码、报文格式
  • 一问一答模式:客户端请求 → 服务器响应
4. 最直观对比表
特点TCPUDPHTTP
层级传输层传输层应用层
可靠性可靠不可靠可靠(基于 TCP)
连接面向连接无连接短连接 / 长连接
速度极快较慢
有序性有序无序有序
底层依赖IPIPTCP
特性TCPUDP
连接必须先建立连接(三次握手)无需连接,直接发数据
可靠性可靠,不丢不乱序不可靠,可能丢包、乱序
速度慢,开销大快,开销极小
流量 / 拥塞控制
数据形式字节流数据报(一包一包)
重传机制
5. 应用场景(最实用部分)
TCP 场景(要求不丢、不错、不乱
  • 网页浏览(HTTP/HTTPS)
  • 文件传输(FTP、HTTP 下载)
  • 邮件(SMTP、POP3)
  • 登录、支付、接口请求
UDP 场景(要求快、实时,丢一点没关系)
  • 直播、实时音视频(抖音、微信通话)
  • 游戏(王者荣耀、吃鸡)
  • DNS 查询
  • IoT 设备上报、实时监控
HTTP 场景(业务接口、网页、API
  • 网站、H5、小程序请求
  • RESTful API、后端接口交互
  • 文件上传 / 下载
  • 登录、支付、查询等必须可靠的业务

Read more

一文掌握 Git 分支:本地管理 + 远程协作 + 最佳实践

前言:为什么分支如此重要? 在现代软件开发中,分支(Branch) 是 Git 最强大的特性之一。想象一下: * 🚀 你可以在不影响主代码的情况下开发新功能 * 🐛 你可以独立修复紧急 Bug * 🧪 你可以安全地尝试实验性想法 * 👥 团队成员可以并行工作而不互相干扰 这一切都归功于 git branch 命令。本文将带你从零开始,全面掌握 Git 分支管理的核心技能。 一、分支的本质:理解 Git 分支模型 在深入命令之前,先理解分支的本质: ┌─────────────────────────────────────────────────┐ │ Git 分支 = 指向提交的轻量级指针 │ │ │ │ main ──→ ● ──→ ● ──→ ● (最新提交) │ │ ↘ │ │ feature ──→ ● ──→ ● (独立开发线) │ └─────────────────────────────────────────────────┘ 关键概念: * 分支只是一个指向特定提交的指针 * 创建分支几乎零成本(只创建指针,不复制文件)

By Ne0inhk
PandaWiki:更轻量的开源知识库,问答效果到底如何?(本地部署教程+效果实测)

PandaWiki:更轻量的开源知识库,问答效果到底如何?(本地部署教程+效果实测)

开源 RAG 项目我之前主要围绕 RAGFlow 写了不少落地案例。RAGFlow 定位是大而全的企业级 RAG 引擎,所以社区里也一直有人吐槽:资源吃得多、处理慢。但这事儿某种程度上就是端到端全包(解析、切分、向量化、检索、权限、工作流、评测)的代价,工程体量上去了,默认就不可能太轻。 如果你想找一款更轻量的开源方案,主要用来处理产品文档、技术文档、FAQ、博客等内容,那可以看看今天要介绍的 PandaWiki。一句话总结:PandaWiki 更像开源版的知识库产品,而不是一个给工程师从零拼装的 RAG 引擎。 这个项目实际我也是近期才注意到,GitHub 目前 8.6K Star,看趋势图下半年热度是一路走高。我花了几天集中测了下,确实有一些可圈可点的地方,这篇就抓大放小,来和各位说道说道。 这篇试图说清楚: PandaWiki 的手把手本地部署过程、

By Ne0inhk

3大开源修复模型横评:云端镜像快速部署,1天完成全面测试

3大开源修复模型横评:云端镜像快速部署,1天完成全面测试 你是不是也遇到过这样的情况:团队要选一个AI图像修复工具,大家各自在本地跑GFPGAN、CodeFormer、GPEN,结果有人用笔记本CPU跑,有人用高端显卡,测试速度、画质效果完全没法比?最后开会讨论时,谁的电脑配置高,谁的结果就“看起来更好”,根本没法做出公正决策。 这正是很多技术主管在搭建AI工具链时最头疼的问题——缺乏统一、可复现的测试环境。不同设备、不同依赖版本、不同参数设置,导致评估结果偏差巨大,选型变成“看运气”。 别急,今天我就来帮你解决这个痛点。我们不靠本地部署“拼电脑”,而是直接上云端标准化镜像环境,一键部署三大主流开源人脸修复模型:GFPGAN、CodeFormer 和 GPEN,在相同GPU资源下完成公平对比测试,1天内搞定从部署到出报告的全流程。 ZEEKLOG星图平台提供了预置好这三大模型的AI镜像,无需手动安装复杂依赖,不用折腾CUDA、PyTorch版本兼容问题,点击即用,还能对外暴露API服务,方便团队成员远程调用测试。整个过程就像租了一台“AI修复工作站”,谁都能用,结果可比对。

By Ne0inhk
手把手教你在GitHub上运行开源项目(新手必看版)

手把手教你在GitHub上运行开源项目(新手必看版)

📦 说在前面 GitHub这个程序员宝藏平台(我愿称之为代码界的金矿),每天都有成千上万的开源项目更新。但是很多新手朋友看到那些酷炫项目时,经常会遇到三大灵魂拷问:这项目怎么跑起来?需要装什么软件?报错了怎么办?今天咱们就用最接地气的方式,手把手教你从0到1运行GitHub项目! 🔧 准备工具包(装机三件套) 1. 代码编辑器(必装) 推荐直接上VS Code这个万金油,装好记得在扩展商店安装这两个插件: * GitLens(代码时光机,能看到每行代码的修改记录) * Code Runner(一键运行脚本的神器) (超级重要)👉 如果项目里有.vscode文件夹,一定要用VS Code打开,里面可能有预置的调试配置! 2. Git客户端(下载代码必备) Windows用户直接装Git for Windows,安装时记得勾选这个选项: Use Git and optional Unix tools from the Command Prompt (这样就能在CMD里用Linux命令了,真香!

By Ne0inhk