Node.js 对mssql server 数据表的 通用增删改查
Node.js 对mssql server 数据表的 通用增删改查
一、数据库 环境变量 .env 文件
DB_SERVER=服务器IP地址
DB_PORT=1433
DB_DATABASE=数据库名称
DB_USER=登录用户名
DB_PASSWORD=登录口令
DB_CONNECTION_TIMEOUT=180000
DB_POOL_MAX=20
DB_POOL_MIN=5
const SALT_ROUNDS = 10; // 加盐轮数,越高越安全
const HASH_ALGORITHM = 'sha256'; // 摘要算法(必须是字符串,不能undefined)
const KEY_LENGTH = 64; // 哈希长度,匹配数据库 VarBinary(64)
const ENCODING = 'hex'; // 编码格式
const SALT_SIZE: 16, // 盐值长度(固定16字节,新旧逻辑一致)
const NEW_HASH_SIZE: 32, // 新数据哈希长度
const OLD_HASH_SIZE: 16, // 旧数据哈希长度(兼容存量数据)
const ITERATIONS: 10000, // 迭代次数(新旧逻辑一致)
const ALGORITHM: 'sha512', // 哈希算法(固定)
二、数据库连接
//config/db.js
const dotenv = require('dotenv');
dotenv.config();
const sql = require('mssql');
const dbConfig = {
user: process.env.DB_USER || '用户名称',
password: process.env.DB_PASSWORD || '口令',
server: process.env.DB_SERVER || '服务器IP地址',
database: process.env.DB_DATABASE || '数据库名称',
port: parseInt(process.env.DB_PORT) || 1433,
pool: { max: 10, min: 0, idleTimeoutMillis: 3600000 },
options: { encrypt: false, trustServerCertificate: true }
};
let pool;
async function initPool() {
try {
pool = await sql.connect(dbConfig);
console.log('数据库连接成功');
return pool;
} catch (err) {
console.error('数据库连接失败', err);
process.exit(1);
}
}
function getPool() {
return pool;
}
module.exports = { initPool, getPool};
三、带验证的登录
// models/BaseLogin.js
const jwt = require('jsonwebtoken');
const db = require('../config/db');
class BaseLogin {
constructor(UserId = 1) {
this.UserId = UserId;
this.pool = null;
}
// 确保数据库连接已初始化
async ensureConnection() {
if (!this.pool) {
try {
// 确保连接池已初始化
if (!db.getPool || !db.getPool()) {
await db.initPool();
}
this.pool = db.getPool();
} catch (error) {
console.error('数据库连接初始化失败:', error);
throw new Error('数据库连接失败: ' + error.message);
}
}
return this.pool;
}
// 登录主方法
async login() {
try {
const pool = await this.ensureConnection();
// 查询用户信息 - 修正参数传递方式
const result = await pool.request()
.input('UserId', this.UserId) // 使用 .input() 方法传递参数
.query(`
SELECT UserId, Username, Realname, PasswordHash, IsActive, UnitId, UserRole
FROM Users
WHERE UserId = @UserId
`);
const user = result.recordset[0];
if (!user) {
throw new Error('用户不存在');
}
if (!user.IsActive) {
throw new Error('用户已被禁用');
}
// 更新登录信息(如果存储过程存在)
try {
await pool.request()
.input('UserId', this.UserId)
.execute('sp_UserLogin');
} catch (err) {
console.warn('更新登录信息失败(存储过程可能不存在):', err.message);
// 不抛出错误,继续执行
}
// 生成 token
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const token = jwt.sign({
userId: user.UserId,
unitId: user.UnitId,
userRole: user.UserRole,
realname: user.Realname,
username: user.Username
}, JWT_SECRET, { expiresIn: '8h' });
// 返回用户信息和token
return {
success: true,
user: {
userId: user.UserId,
username: user.Username,
realname: user.Realname,
unitId: user.UnitId,
userRole: user.UserRole
},
token: token,
headers: this.getHeaders(token)
};
} catch (error) {
console.error('登录失败:', error);
throw error;
}
}
// 获取请求头的方法
getHeaders(token) {
const headers = {
'Content-Type': 'application/json'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
// 验证token的方法
static verifyToken(token) {
try {
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const decoded = jwt.verify(token, JWT_SECRET);
return { valid: true, data: decoded };
} catch (error) {
return { valid: false, error: error.message };
}
}
// 刷新token的方法
async refreshToken() {
try {
const pool = db.getPool();
// 重新查询用户信息
const result = await pool.request()
.input('UserId', this.UserId)
.query(`
SELECT UserId, Username, Realname, UnitId, UserRole
FROM Users
WHERE UserId = @UserId AND IsActive = 1
`);
const user = result.recordset[0];
if (!user) {
throw new Error('用户不存在或已禁用');
}
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const newToken = jwt.sign({
userId: user.UserId,
unitId: user.UnitId,
userRole: user.UserRole,
realname: user.Realname,
username: user.Username
}, JWT_SECRET, { expiresIn: '8h' });
return {
success: true,
token: newToken,
headers: this.getHeaders(newToken)
};
} catch (error) {
console.error('刷新token失败:', error);
throw error;
}
}
}
module.exports = BaseLogin;
四、通用增删改查函数
// models/BaseModel.js
// 1. 导入依赖模块
//const sql = require('mssql'); // 使用 mssql 连接 SQL Server
class BaseModel {
constructor(getHeaders, pool=null, tableName, primaryKey = 'Id') {
this.getHeaders = getHeaders;
this.tableName = tableName;
this.primaryKey = primaryKey;
this.pool = pool
}
/**
* 创建记录
* @param {Object} data - 要插入的数据对象
* @returns {Promise<Object>} 插入的记录
*/
async create(data) {
try {
const keys = Object.keys(data);
const values = Object.values(data);
// 构建参数化查询
const columns = keys.join(', ');
const paramPlaceholders = keys.map((_, index) => `@p${index}`).join(', ');
const request = new this.pool.Request();
values.forEach((value, index) => {
request.input(`p${index}`, value);
});
const query = `
INSERT INTO ${this.tableName} (${columns})
OUTPUT INSERTED.*
VALUES (${paramPlaceholders})
`;
const result = await request.query(query);
return result.recordset[0];
} catch (error) {
throw new Error(`创建记录失败: ${error.message}`);
}
}
/**
* 批量创建记录
* @param {Array} dataArray - 要插入的数据对象数组
* @returns {Promise<Array>} 插入的记录数组
*/
async batchCreate(dataArray) {
try {
const results = [];
for (const data of dataArray) {
const result = await this.create(data);
results.push(result);
}
return results;
} catch (error) {
throw new Error(`批量创建失败: ${error.message}`);
}
}
/**
* 根据ID查询记录
* @param {number|string} id - 主键值
* @param {Array} fields - 要查询的字段,默认查询所有
* @returns {Promise<Object|null>} 记录对象或null
*/
async findById(id, fields = ['*']) {
try {
const selectFields = fields.join(', ');
const query = `
SELECT ${selectFields}
FROM ${this.tableName}
WHERE ${this.primaryKey} = @id
`;
const result = await this.pool.request()
.input('id', id)
.query(query);
return result.recordset[0] || null;
} catch (error) {
throw new Error(`查询失败: ${error.message}`);
}
}
/**
* 查询单条记录
* @param {Object} conditions - 查询条件
* @param {Array} fields - 要查询的字段
* @returns {Promise<Object|null>} 记录对象或null
*/
async findOne(conditions = {}, fields = ['*']) {
try {
const { whereClause, parameters } = this.buildWhereClause(conditions);
const selectFields = fields.join(', ');
const query = `
SELECT ${selectFields}
FROM ${this.tableName}
${whereClause}
`;
const request = this.pool.request();
parameters.forEach(param => {
request.input(param.name, param.value);
});
const result = await request.query(query);
return result.recordset[0] || null;
} catch (error) {
throw new Error(`查询失败: ${error.message}`);
}
}
/**
* 查询多条记录
* @param {Object} conditions - 查询条件
* @param {Object} options - 选项(分页、排序等)
* @param {Array} fields - 要查询的字段
* @returns {Promise<Array>} 记录数组
*/
async find(conditions = {}, options = {}, fields = ['*']) {
try {
const { whereClause, parameters } = this.buildWhereClause(conditions);
const selectFields = fields.join(', ');
let query = `
SELECT ${selectFields}
FROM ${this.tableName}
${whereClause}
`;
//console.log('query =' + query);
// 添加排序
if (options.orderBy) {
query += ` ORDER BY ${options.orderBy} ${options.orderDirection || 'ASC'}`;
}
// 添加分页
if (options.limit) {
query += ` OFFSET ${options.offset || 0} ROWS FETCH NEXT ${options.limit} ROWS ONLY`;
}
const request = this.pool.request();
parameters.forEach(param => {
request.input(param.name, param.value);
});
const result = await request.query(query);
return result.recordset;
} catch (error) {
throw new Error(`查询失败: ${error.message}`);
}
}
/**
* 查询所有记录
* @param {Object} options - 选项(分页、排序等)
* @param {Array} fields - 要查询的字段
* @returns {Promise<Array>} 记录数组
*/
async findAll(options = {}, fields = ['*']) {
return this.find({}, options, fields);
}
/**
* 更新记录
* @param {number|string} id - 主键值
* @param {Object} data - 要更新的数据
* @returns {Promise<Object|null>} 更新后的记录
*/
async update(id, data) {
try {
const keys = Object.keys(data);
const setClause = keys.map((key, index) => `${key} = @p${index}`).join(', ');
const request = this.pool.request();
request.input('id', id);
keys.forEach((key, index) => {
request.input(`p${index}`, data[key]);
});
const query = `
UPDATE ${this.tableName}
SET ${setClause}
OUTPUT INSERTED.*
WHERE ${this.primaryKey} = @id
`;
const result = await request.query(query);
return result.recordset[0] || null;
} catch (error) {
throw new Error(`更新失败: ${error.message}`);
}
}
/**
* 条件更新
* @param {Object} conditions - 更新条件
* @param {Object} data - 要更新的数据
* @returns {Promise<Array>} 更新的记录数组
*/
async updateWhere(conditions, data) {
try {
const { whereClause, parameters: whereParams } = this.buildWhereClause(conditions);
const setKeys = Object.keys(data);
const setClause = setKeys.map((key, index) => `${key} = @s${index}`).join(', ');
const request = this.pool.request();
// 添加set参数
setKeys.forEach((key, index) => {
request.input(`s${index}`, data[key]);
});
// 添加where参数
whereParams.forEach(param => {
request.input(param.name, param.value);
});
const query = `
UPDATE ${this.tableName}
SET ${setClause}
OUTPUT INSERTED.*
${whereClause}
`;
const result = await request.query(query);
return result.recordset;
} catch (error) {
throw new Error(`更新失败: ${error.message}`);
}
}
/**
* 删除记录
* @param {number|string} id - 主键值
* @returns {Promise<boolean>} 是否删除成功
*/
async delete(id) {
try {
const query = `
DELETE FROM ${this.tableName}
OUTPUT DELETED.${this.primaryKey}
WHERE ${this.primaryKey} = @id
`;
const result = await this.pool.request()
.input('id', id)
.query(query);
return result.recordset.length > 0;
} catch (error) {
throw new Error(`删除失败: ${error.message}`);
}
}
/**
* 条件删除
* @param {Object} conditions - 删除条件
* @returns {Promise<number>} 删除的记录数
*/
async deleteWhere(conditions) {
try {
const { whereClause, parameters } = this.buildWhereClause(conditions);
const query = `
DELETE FROM ${this.tableName}
OUTPUT DELETED.${this.primaryKey}
${whereClause}
`;
const request = this.pool.request();
parameters.forEach(param => {
request.input(param.name, param.value);
});
const result = await request.query(query);
return result.recordset.length;
} catch (error) {
throw new Error(`删除失败: ${error.message}`);
}
}
/**
* 统计记录数
* @param {Object} conditions - 统计条件
* @returns {Promise<number>} 记录数
*/
async count(conditions = {}) {
try {
const { whereClause, parameters } = this.buildWhereClause(conditions);
const query = `
SELECT COUNT(*) as total
FROM ${this.tableName}
${whereClause}
`;
const request = this.pool.request();
parameters.forEach(param => {
request.input(param.name, param.value);
});
const result = await request.query(query);
return result.recordset[0].total;
} catch (error) {
throw new Error(`统计失败: ${error.message}`);
}
}
/**
* 检查记录是否存在
* @param {Object} conditions - 检查条件
* @returns {Promise<boolean>} 是否存在
*/
async exists(conditions) {
const count = await this.count(conditions);
return count > 0;
}
/**
* 构建WHERE子句
* @param {Object} conditions - 条件对象
* @returns {Object} 包含whereClause和parameters的对象
* @private
*/
buildWhereClause(conditions) {
if (Object.keys(conditions).length === 0) {
return { whereClause: '', parameters: [] };
}
const parameters = [];
const whereConditions = [];
Object.entries(conditions).forEach(([key, value], index) => {
const paramName = `w${index}`;
if (value === null) {
whereConditions.push(`${key} IS NULL`);
} else if (Array.isArray(value)) {
// IN 查询
const inParams = value.map((_, idx) => `@${paramName}_${idx}`).join(', ');
whereConditions.push(`${key} IN (${inParams})`);
value.forEach((v, idx) => {
parameters.push({ name: `${paramName}_${idx}`, value: v });
});
} else if (typeof value === 'object' && value.operator) {
// 操作符查询,如 { operator: '>', value: 100 }
const operator = value.operator;
whereConditions.push(`${key} ${operator} @${paramName}`);
parameters.push({ name: paramName, value: value.value });
} else if (typeof value === 'string' && value.includes('%')) {
// LIKE 查询
whereConditions.push(`${key} LIKE @${paramName}`);
parameters.push({ name: paramName, value: value });
} else {
// 等值查询
whereConditions.push(`${key} = @${paramName}`);
parameters.push({ name: paramName, value: value });
}
});
const whereClause = `WHERE ${whereConditions.join(' AND ')}`;
return { whereClause, parameters };
}
/**
* 执行原生SQL查询
* @param {string} query - SQL查询语句
* @param {Object} params - 参数对象
* @returns {Promise<Array>} 查询结果
*/
async rawQuery(query, params = {}) {
try {
const request = this.pool.request();
Object.entries(params).forEach(([key, value]) => {
request.input(key, value);
});
const result = await request.query(query);
return result.recordset;
} catch (error) {
throw new Error(`SQL执行失败: ${error.message}`);
}
}
/**
* 事务执行
* @param {Function} callback - 事务回调函数
* @returns {Promise<any>} 回调函数返回值
*/
async transaction(callback) {
const transaction = new this.pool.Transaction();
try {
await transaction.begin();
const result = await callback(transaction);
await transaction.commit();
return result;
} catch (error) {
await transaction.rollback();
throw error;
}
}
}
module.exports = BaseModel;
五、使用示例
// 使用示例
const BaseModel = require('./BaseModel');
// 为 AuditReport 表创建模型实例
const auditReportModel = new BaseModel('AuditReport', 'ReportId');
// 创建记录
async function createAuditReport() {
const newReport = await auditReportModel.create({
UnitId: 1,
ProjectName: '2024年度审计项目',
SerialNumber: 'AUD-2024-001',
ReportNumber: 'RPT-2024-001',
IssueAmount: 100000.00,
Qualitative: '违规使用资金',
CreatedBy: 'admin'
});
console.log('创建成功:', newReport);
}
// 查询记录
async function queryAuditReports() {
// 根据ID查询
const report = await auditReportModel.findById(1);
console.log('查询结果:', report);
// 条件查询
const reports = await auditReportModel.find(
{
UnitId: 1,
IssueAmount: { operator: '>', value: 50000 }
},
{
orderBy: 'CreatedDate',
orderDirection: 'DESC',
limit: 10,
offset: 0
},
['ReportId', 'ProjectName', 'IssueAmount', 'CreatedDate']
);
console.log('条件查询结果:', reports);
}
// 更新记录
async function updateAuditReport() {
const updated = await auditReportModel.update(1, {
RectificationStatus: '已整改',
RectificationAmount: 100000.00,
ModifiedBy: 'system',
ModifiedDate: new Date()
});
console.log('更新成功:', updated);
}
// 删除记录
async function deleteAuditReport() {
const deleted = await auditReportModel.delete(1);
console.log('删除成功:', deleted);
}
// 复杂查询示例
async function complexQuery() {
// 分页查询
const page1 = await auditReportModel.findAll({
orderBy: 'CreatedDate',
orderDirection: 'DESC',
limit: 20,
offset: 0
});
// 统计数量
const total = await auditReportModel.count({
AuditStatus: 1,
IssueAmount: { operator: '>', value: 0 }
});
// 检查是否存在
const exists = await auditReportModel.exists({
SerialNumber: 'AUD-2024-001'
});
// 批量更新
const updated = await auditReportModel.updateWhere(
{ AuditStatus: 0 },
{ AuditStatus: 1, AuditRemark: '自动审核通过' }
);
}
// 事务示例
async function transactionExample() {
await auditReportModel.transaction(async (transaction) => {
// 在事务中执行操作
const report1 = await auditReportModel.create({
UnitId: 1,
ProjectName: '项目1',
// ... 其他字段
});
const report2 = await auditReportModel.create({
UnitId: 1,
ProjectName: '项目2',
// ... 其他字段
});
// 如果任何操作失败,事务会自动回滚
return { report1, report2 };
});
}
六、又一使用示例
// mytest.js
const BaseLogin = require('./BaseLogin');
const BaseModel = require('./BaseModel');
//const dayjs = require('./utils/dayjs.min.js');
async function initializeAndQuery() {
let loginResult = null;
try {
// 1. 登录
console.log('正在登录...');
const login = new BaseLogin();
loginResult = await login.login();
if (!loginResult || !loginResult.success) {
throw new Error('登录失败:' + (loginResult?.error || '未知错误'));
}
console.log(`✅ 登录成功!用户:${loginResult.user.realname} (${loginResult.user.username})`);
let pool = login.pool;
let header = login.getHeaders();
// 2. 查询审计报告
console.log('正在查询审计报告...') + JSON.stringify(header);
const auditModel = new BaseModel(getHeaders=header, pool,'AuditReport', 'ReportId');
const report = await auditModel.findAll({
orderBy: 'CreatedDate',
orderDirection: 'DESC',
limit: 20,
offset: 0
});
console.log('完整数据长度:'+ report.length)
if (report) {
console.log('✅ 查询成功!');
console.log('📊 审计报告详情:');
report.forEach(r=>{
console.log(` - 报告ID: ${r.ReportId}`);
console.log(` - 项目名称: ${r.ProjectName}`);
console.log(` - 系列编号: ${r.SerialNumber}`);
console.log(` - 所涉金额: ¥${r.IssueAmount?.toLocaleString() || 0}`);
console.log(` - 整改状态: ${r.RectificationStatus || '未设置'}`);
console.log(` - 创建时间: ${r.CreatedDate.toISOString().split('T')[0]}`);
})
// 如果需要完整数据
//console.log('完整数据:', JSON.stringify(report, null, 2));
} else {
console.log('⚠️ 未找到ID为1的审计报告记录!');
}
return { success: true, report };
} catch (error) {
console.error('❌ 程序执行失败:', error.message);
// 根据错误类型给出具体提示
if (error.message.includes('登录')) {
console.error('提示:请检查用户ID是否正确或用户是否被禁用');
} else if (error.message.includes('查询')) {
console.error('提示:请检查数据库连接或表是否存在');
}
return { success: false, error: error.message };
}
}
// 执行主函数
initializeAndQuery().then(result => {
if (!result.success) {
process.exit(1);
//process.exit(1)作用是:立即强制终止当前 Node.js 进程,
//并向操作系统返回一个状态码 1,表示程序因错误而退出;状态码 0, 表示程序正常退出。
}
});
显示结果:

JSON.stringify(report, null, 2)
在 JavaScript 中的作用是:将 JavaScript 对象 report 转换为格式化的 JSON 字符串,方便阅读或输出。
具体参数解释:
report: 要转换的 JavaScript 对象(或数组)。
null(第二个参数):替换器。这里为 null 表示不筛选或修改属性,所有属性都会被序列化。
2 (第三个参数):空格数。表示在生成的 JSON 字符串中,每一层缩进使用 2 个空格,使输出具有易读的层级结构。
作用效果:
普通 JSON.stringify(report) 输出紧凑、无换行的字符串。
加上 **null, 2 **后输出格式化、带缩进和换行的字符串,常用于:
调试时打印对象结构
保存日志或配置文件
在控制台或文件中展示可读的 JSON 数据
示例:
const report = { name: "test", data: [1, 2, 3] };
console.log(JSON.stringify(report, null, 2));
输出:
{
"name": "test",
"data": [
1,
2,
3
]
}
而 JSON.stringify(report) 会输出 {"name":"test","data":[1,2,3]}。
JSON.parse() 的作用: 将 JSON 格式的字符串转换为 JavaScript 对象。
JSON.parse(text, reviver?)
- text:一个有效的 JSON 格式字符串
- reviver(可选):转换器函数,可以在返回之前对每个键值对进行修改
场景 1:从服务器接收数据
// 从 API 接收到的 JSON 字符串
const jsonString = '{"name":"张三","age":30,"city":"北京"}';
// 解析为 JavaScript 对象
const user = JSON.parse(jsonString);
console.log(user.name); // 输出: 张三
console.log(user.age); // 输出: 30
console.log(typeof user); // 输出: object
场景 2:从 localStorage 读取数据
// 存储时:对象 -> 字符串
const userSettings = { theme: 'dark', fontSize: 14 };
localStorage.setItem('settings', JSON.stringify(userSettings));
// 读取时:字符串 -> 对象
const storedSettings = localStorage.getItem('settings');
const settings = JSON.parse(storedSettings);
console.log(settings.theme); // 输出: dark
场景 3:处理复杂数据结构
// 包含数组和嵌套对象的 JSON
const jsonStr = `{
"users": [
{"id": 1, "name": "张三"},
{"id": 2, "name": "李四"}
],
"total": 2
}`;
const data = JSON.parse(jsonStr);
console.log(data.users[0].name); // 输出: 张三
console.log(data.total); // 输出: 2
使用 reviver 函数(高级用法)
reviver 函数可以在返回前对每个键值对进行转换处理。
示例 1:转换日期字符串
const jsonStr = '{"name":"张三","birthday":"1990-01-15T00:00:00.000Z"}';
const user = JSON.parse(jsonStr, (key, value) => {
// 将 birthday 字段从字符串转换为 Date 对象
if (key === 'birthday') {
return new Date(value);
}
return value;
});
console.log(user.birthday.getFullYear()); // 输出: 1990
console.log(typeof user.birthday); // 输出: object (Date对象)
示例 2:数值转换和验证
const jsonStr = '{"price":"199.99","quantity":"5"}';
const product = JSON.parse(jsonStr, (key, value) => {
if (key === 'price') {
return parseFloat(value); // 字符串转数字
}
if (key === 'quantity') {
return parseInt(value, 10);
}
return value;
});
console.log(product.price + product.quantity); // 输出: 204.99 (数值相加)
console.log(typeof product.price); // 输出: number
常见错误处理
❌ 错误 1:解析无效的 JSON
// 错误示例:使用单引号(JSON **必须使用双引号**)
const invalidJSON = "{'name':'张三'}";
try {
JSON.parse(invalidJSON);
} catch (error) {
console.error('解析失败:', error.message);
// 输出: Unexpected token ' in JSON at position 1
}
// 正确写法
const validJSON = '{"name":"张三"}';
const obj = JSON.parse(validJSON); // 正常工作
❌ 错误 2:包含 JavaScript 注释或函数
// JSON 不支持注释
const withComment = '{"name":"张三" // 这是注释}';
// JSON.parse(withComment); // 会报错
// JSON 不支持函数
const withFunction = '{"func": function() { return 1; }}';
// JSON.parse(withFunction); // 会报错
✅ 正确做法:使用 try-catch 包裹
function safeJSONParse(str) {
try {
return { data: JSON.parse(str), error: null };
} catch (error) {
console.error('JSON 解析错误:', error.message);
return { data: null, error: error };
}
}
// 使用示例
const result = safeJSONParse('{"name":"张三"}');
if (result.error) {
// 处理错误
} else {
console.log(result.data.name); // 张三
}
JSON.parse() vs JSON.stringify()
| 方法 | 方向 | 输入 | 输出 |
|---|---|---|---|
| JSON.parse() | 解码 | JSON 字符串 | JavaScript 对象 |
| JSON.stringify() | 编码 | JavaScript 对象 | JSON 字符串 |
const obj = { name: '张三', age: 30 };
// 对象 -> 字符串
const jsonStr = JSON.stringify(obj);
console.log(jsonStr); // '{"name":"张三","age":30}'
console.log(typeof jsonStr); // string
// 字符串 -> 对象
const newObj = JSON.parse(jsonStr);
console.log(newObj.name); // 张三
console.log(typeof newObj); // object
实际应用示例
示例 1:处理 API 响应
// Node.js 后端接收请求
app.post('/api/user', (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
// 将接收到的 JSON 字符串解析为对象
const userData = JSON.parse(body);
console.log(userData.name, userData.age);
res.json({ status: 'success' });
} catch (error) {
res.status(400).json({ error: 'Invalid JSON' });
}
});
});
示例 2:从数据库读取 JSON 字段
// SQL Server 中有 JSON 类型的字段
const result = await pool.request()
.query(`
SELECT UserId, Preferences
FROM Users
WHERE UserId = 1
`);
const user = result.recordset[0];
// Preferences 字段存储的是 JSON 字符串,如 '{"theme":"dark","language":"zh"}'
const preferences = JSON.parse(user.Preferences);
console.log(preferences.theme); // dark
console.log(preferences.language); // zh
示例 3:配置文件解析
const fs = require('fs');
// 读取 config.json 文件
const configFile = fs.readFileSync('./config.json', 'utf8');
const config = JSON.parse(configFile);
console.log(config.db.host);
console.log(config.jwt.secret);
注意事项总结
| 要点 | 说明 |
|---|---|
| 键名必须用双引号 | {"name":"value"} ✅ |
| 字符串值必须用双引号 | {"key":"value"} ✅ |
| 不支持注释 | 不能在 JSON 中使用 // 或 /* */ |
| 不支持函数和 undefined | 这些值会被忽略或报错 |
| 尾随逗号不允许 | [1,2,3,] ❌ |
| 总是使用 try-catch | 因为输入数据可能无效 |
快速记忆
- JSON.parse() = 解析 = 字符串 → 对象 = 读
- JSON.stringify() = 序列化 = 对象 → 字符串 = 写
// 记忆口诀:parse 处理字符串,stringify 处理对象
const str = '{"name":"张三"}';
const obj = JSON.parse(str); // 字符串变对象
const back = JSON.stringify(obj); // 对象变字符串
七、result.recordset
result.recordset 是 mssql 这个 npm 包(用于连接 SQL Server 的 Node.js 库)中,查询返回结果对象里的一个标准属性。
result.recordset 是一个数组,存放着 SQL 查询返回的数据行。
详细说明
1. result 对象的结构
当你使用 mssql 包执行 SELECT 查询时,返回的 result 对象通常包含以下属性:
{
recordsets: [ [行1, 行2, ...] ], // 二维数组,用于多结果集场景
recordset: [行1, 行2, ...], // 一维数组,默认第一个结果集(最常用)
rowsAffected: [行数], // 受影响的行数(对于 SELECT 通常为 0)
output: {} // 存储过程的输出参数
}
2. recordset 的具体内容
recordset 是一个对象数组,每个对象代表数据库中的一行数据,对象的键是列名,值是对应的数据。
示例:
假设 Users 表中有以下数据:
UserId Username Realname
1 john_doe 张三
2 jane_doe 李四
那么执行你的查询后:
result.recordset = [
{ UserId: 1, Username: 'john_doe', Realname: '张三', PasswordHash: '...', IsActive: true, UnitId: 10, UserRole: 'admin' },
{ UserId: 2, Username: 'jane_doe', Realname: '李四', PasswordHash: '...', IsActive: true, UnitId: 20, UserRole: 'user' }
]
// 所以:
const user = result.recordset[0]; // 得到第一行:UserId=1 的用户
console.log(user.Username); // 输出:john_doe
console.log(user.Realname); // 输出:张三
recordsets vs recordset 的区别
这两个属性容易混淆,区别如下:
属性 类型 用途
recordset 数组 第一个结果集的行数组(90% 的场景用这个)
recordsets 二维数组 所有结果集的数组,用于存储过程返回多个 SELECT 结果
多结果集示例:
// 假设存储过程返回两个结果集
const result = await pool.request()
.execute('GetUsersAndOrders');
// result.recordset → 第一个结果集(用户列表)
// result.recordsets[0] → 同样也是第一个结果集
// result.recordsets[1] → 第二个结果集(订单列表)
八、node中的JWT
JWT(JSON Web Token)在 Node.js 中是一个非常流行的身份认证和信息交换方案。可以把它理解为一个安全的、加密的、自带信息的电子通行证。
1、通俗类比:游乐园的“腕带”
想象你去一个大型游乐园:
购买门票(登录):你在入口处买票,工作人员验证你的身份后,给你戴上一个防水腕带。
腕带里写的信息:这个腕带里写着你的用户ID、会员等级(VIP/普通)、腕带有效期(今天有效)等信息,并且用特殊工艺加密,任何人都无法伪造或篡改。
自由游玩(访问资源):你要玩过山车时,工作人员只需扫描你的腕带,就知道你是谁、是否有权限、票是否过期,然后直接让你进去。不需要每次都去入口处重新验证。
这个腕带,就是 JWT。服务器(游乐园)生成它,客户端(你)保存它,每次请求时带上它,服务器就能快速识别你的身份。
2、JWT 的物理结构
一个 JWT 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywidXNlcm5hbWUiOiJraW1naXUiLCJpYXQiOjE2NDQ4MDAwMDAsImV4cCI6MTY0NDg4NjQwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
它由三部分组成,用英文句点 . 分隔:
(1). Header(头部) - 元数据
{
"alg": "HS256", // 签名算法(HMAC SHA256)
"typ": "JWT" // 令牌类型
}
告诉服务器这个令牌是 JWT,以及用什么算法验证真伪。
(2). Payload(负载) - 核心数据
{
"userId": 123, // 用户ID(自定义数据)
"username": "kimgui", // 用户名
"role": "admin", // 角色
"iat": 1644800000, // 签发时间(issued at)
"exp": 1644886400 // 过期时间(expiration)
}
存放你想要传递的信息(不能放密码等敏感信息,因为这部分只是 Base64 编码,并未加密)。
(3). Signature(签名) - 防伪标识
签名 = 加密算法( base64UrlEncode(Header) + "." + base64UrlEncode(Payload), 密钥 )
确保令牌没有被篡改。如果有人修改了 Header 或 Payload,签名验证就会失败。
3、在 Node.js 中的典型工作流程
(1). 安装 JWT 库
npm install jsonwebtoken
(2). 登录时生成 JWT
const jwt = require('jsonwebtoken');
// 用户登录成功
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// 验证用户名密码(从数据库查询)
const user = await db.findUser(username, password);
if (!user) {
return res.status(401).json({ error: '用户名或密码错误' });
}
// 生成 JWT
const token = jwt.sign(
{
userId: user.id, // 自定义数据
username: user.name,
role: user.role
},
'your-secret-key', // 密钥(存在环境变量中)
{ expiresIn: '24h' } // 有效期 24 小时
);
// 返回给客户端
res.json({
message: '登录成功',
token: token
});
});
(3). 客户端保存并发送 JWT
// 前端登录成功后
localStorage.setItem('token', response.token);
// 后续请求在 HTTP Header 中带上
fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
(4). 服务器验证 JWT(中间件)
// 验证 JWT 的中间件
function authenticateToken(req, res, next) {
// 从请求头获取 token
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '未提供认证令牌' });
}
// 验证 token
jwt.verify(token, 'your-secret-key', (err, decoded) => {
if (err) {
return res.status(403).json({ error: '令牌无效或已过期' });
}
// 将解析出的用户信息挂载到 request 上
req.user = decoded;
next(); // 继续处理请求
});
}
// 受保护的路由
app.get('/api/profile', authenticateToken, (req, res) => {
// 这里的 req.user 就是 JWT 中解码出来的数据
res.json({
message: `欢迎回来,${req.user.username}!`,
userId: req.user.userId
});
});
4、JWT 的核心优势
特性 说明 对比传统 Session
无状态 服务器不需要存储 Session,JWT 本身包含了所有信息 Session 需要在服务器内存或 Redis 中存储
易扩展 适合分布式系统,任何服务器都能验证 JWT Session 需要共享存储或粘性会话
跨域友好 通过 HTTP Header 传递,不受跨域限制 Cookie 有跨域限制
移动端友好 可以存储在 localStorage / 原生存储中 Session Cookie 在移动端处理较麻烦
自包含 包含用户信息,减少数据库查询 每次请求可能需要查数据库获取用户信息
5、常见问题和注意事项
(1). JWT 的安全性
// ❌ 错误:在 Payload 中存放敏感信息
jwt.sign({ password: '123456', creditCard: '1234-5678' }, secret);
// ✅ 正确:只存放非敏感的标识信息
jwt.sign({ userId: 123, role: 'user' }, secret);
// ✅ 更好:使用 HTTPS + 设置较短有效期
jwt.sign({ userId: 123 }, secret, { expiresIn: '15m' });
(2). 如何注销/失效 JWT?
JWT 一旦签发,在有效期内无法主动失效。解决方案:
短有效期 + Refresh Token:Access Token 15分钟过期,Refresh Token 7天过期
黑名单:维护一个 Redis 黑名单,存储需要提前失效的 Token
版本号:在用户表中增加 tokenVersion 字段,JWT 中包含版本号
// 黑名单方案
const blacklist = new Set();
// 登出时加入黑名单
app.post('/logout', authenticateToken, (req, res) => {
const token = req.headers['authorization'].split(' ')[1];
blacklist.add(token);
res.json({ message: '已登出' });
});
// 验证中间件中检查黑名单
function authenticateToken(req, res, next) {
const token = getTokenFromHeader(req);
if (blacklist.has(token)) {
return res.status(401).json({ error: '令牌已失效' });
}
// ... 继续验证
}
(3). Refresh Token 机制(推荐)
// 生成 Access Token(短期)和 Refresh Token(长期)
const accessToken = jwt.sign({ userId: 123 }, secret, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: 123 }, refreshSecret, { expiresIn: '7d' });
// 客户端保存 refreshToken,当 accessToken 过期时用它换取新的
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
try {
const decoded = jwt.verify(refreshToken, refreshSecret);
const newAccessToken = jwt.sign({ userId: decoded.userId }, secret, { expiresIn: '15m' });
res.json({ accessToken: newAccessToken });
} catch (err) {
res.status(403).json({ error: 'Refresh token 无效' });
}
});
九、Object.keys() 和 Object.values()
在 Node.js(以及所有 JavaScript 环境)中,Object.keys() 和 Object.values() 是两个用于遍历对象属性的静态方法。它们的作用分别是:获取对象的键名(属性名)数组和获取对象的值数组。
1、Object.keys() - 获取所有键名
基本语法
Object.keys(obj)
作用 返回一个数组,包含对象自身的(不含继承的)所有可枚举属性的键名。
示例
const user = {
id: 1,
username: '张三',
role: 'admin',
isActive: true
};
const keys = Object.keys(user);
console.log(keys);
// 输出: ['id', 'username', 'role', 'isActive']
// 遍历键名
keys.forEach(key => {
console.log(`${key}: ${user[key]}`);
});
// 输出:
// id: 1
// username: 张三
// role: admin
// isActive: true
2、Object.values() - 获取所有值
基本语法
Object.values(obj)
作用 返回一个数组,包含对象自身的(不含继承的)所有可枚举属性的值。
示例
const user = {
id: 1,
username: '张三',
role: 'admin',
isActive: true
};
const values = Object.values(user);
console.log(values);
// 输出: [1, '张三', 'admin', true]
// 直接遍历值
Object.values(user).forEach(value => {
console.log(value);
});
// 输出: 1, 张三, admin, true
3、实际应用场景(结合你的 ERP 项目)
场景 1:动态表单验证
// 验证用户提交的数据是否完整
const formData = {
username: '李四',
email: 'lisi@example.com',
phone: '' // 空值
};
// 检查是否有空值
const hasEmpty = Object.values(formData).some(value => value === '');
console.log(hasEmpty); // true
// 找出所有为空的字段
const emptyFields = Object.keys(formData).filter(key => formData[key] === '');
console.log(emptyFields); // ['phone']
场景 2:数据库查询结果处理
// 从 SQL Server 查询到的用户数据
const dbResult = {
recordset: [
{ UserId: 1, Username: 'admin', Realname: '管理员', IsActive: true },
{ UserId: 2, Username: 'user1', Realname: '普通用户', IsActive: false }
]
};
// 获取所有用户名
const usernames = dbResult.recordset.map(row => row.Username);
console.log(usernames); // ['admin', 'user1']
// 动态生成表头
if (dbResult.recordset.length > 0) {
const columns = Object.keys(dbResult.recordset[0]);
console.log('数据列:', columns);
// 输出: 数据列: ['UserId', 'Username', 'Realname', 'IsActive']
}
场景 3:数据统计与转换
// 销售数据统计
const salesData = {
'产品A': 15000,
'产品B': 23000,
'产品C': 8700,
'产品D': 19200
};
// 计算总销售额
const total = Object.values(salesData).reduce((sum, value) => sum + value, 0);
console.log(`总销售额: ${total}`); // 总销售额: 65900
// 找出销售额超过10000的产品
const topProducts = Object.keys(salesData).filter(product => salesData[product] > 10000);
console.log('畅销产品:', topProducts); // 畅销产品: ['产品A', '产品B', '产品D']
场景 4:对象复制与转换
// 深拷贝对象(浅层)
const original = { a: 1, b: 2, c: 3 };
const copy = { ...original };
// 或使用 Object.assign
const copy2 = Object.assign({}, original);
// 对象转数组(键值对数组)
const user = { id: 5, name: '王五', age: 28 };
const entries = Object.entries(user); // 顺便提一下 Object.entries()
console.log(entries);
// 输出: [['id', 5], ['name', '王五'], ['age', 28]]
// 过滤对象属性
const rawData = { id: 1, password: '123456', email: 'test@example.com', token: 'xyz' };
const safeData = Object.keys(rawData)
.filter(key => !['password', 'token'].includes(key))
.reduce((obj, key) => {
obj[key] = rawData[key];
return obj;
}, {});
console.log(safeData);
// 输出: { id: 1, email: 'test@example.com' }
4、三个方法的对比
方法 返回内容 示例输入 示例输出
Object.keys(obj) 键名数组 {a:1, b:2} ['a', 'b']
Object.values(obj) 值数组 {a:1, b:2} [1, 2]
Object.entries(obj) 键值对数组 {a:1, b:2} [['a',1], ['b',2]]
const obj = { name: '张三', age: 25, city: '北京' };
console.log(Object.keys(obj)); // ['name', 'age', 'city']
console.log(Object.values(obj)); // ['张三', 25, '北京']
console.log(Object.entries(obj)); // [['name', '张三'], ['age', 25], ['city', '北京']]
5、重要特性与注意事项
(1). 只包含对象自身的属性(不包含继承的)
const parent = { inherited: '来自原型' };
const child = Object.create(parent);
child.own = '自身属性';
console.log(Object.keys(child)); // ['own'] ← 不包括 inherited
console.log(Object.values(child)); // ['自身属性']
(2). 只包含可枚举属性
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: '不可枚举',
enumerable: false // 设置为不可枚举
});
obj.visible = '可枚举';
console.log(Object.keys(obj)); // ['visible']
console.log(Object.values(obj)); // ['可枚举']
(3). 处理类数组对象
// 类数组对象
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
console.log(Object.keys(arrayLike)); // ['0', '1', '2', 'length']
console.log(Object.values(arrayLike)); // ['a', 'b', 'c', 3]
// 真正转换为数组
const realArray = Array.from(Object.values(arrayLike).filter(v => typeof v === 'string'));
console.log(realArray); // ['a', 'b', 'c']
(4). 处理空值
console.log(Object.keys(null)); // TypeError: Cannot convert null to object
console.log(Object.keys(undefined)); // TypeError
// 安全使用
const safe = obj => (obj && Object.keys(obj)) || [];
console.log(safe(null)); // []
console.log(safe(undefined)); // []
3、在 Node.js 后端开发中的典型用法
(1). 日志记录
function logRequest(req) {
const headers = req.headers;
console.log('请求头:', Object.keys(headers).join(', '));
console.log('关键信息:', {
method: req.method,
url: req.url,
ip: req.ip
});
}
(2). API 响应格式化
// 过滤敏感字段
function sanitizeUser(user) {
const sensitive = ['password', 'token', 'salt'];
return Object.keys(user)
.filter(key => !sensitive.includes(key))
.reduce((clean, key) => {
clean[key] = user[key];
return clean;
}, {});
}
const dbUser = { id: 1, username: 'admin', password: '123456', email: 'admin@test.com' };
console.log(sanitizeUser(dbUser));
// { id: 1, username: 'admin', email: 'admin@test.com' }
(3). 配置验证
const requiredConfig = ['DB_HOST', 'DB_USER', 'DB_PASSWORD', 'JWT_SECRET'];
const envConfig = process.env;
const missing = requiredConfig.filter(key => !Object.keys(envConfig).includes(key));
if (missing.length > 0) {
console.error(`缺少必要配置: ${missing.join(', ')}`);
process.exit(1);
}
(4). 批量更新数据库字段
// 动态构建 SQL UPDATE 语句
function buildUpdateQuery(tableName, data, whereClause) {
const fields = Object.keys(data);
const setClause = fields.map(field => `${field} = @${field}`).join(', ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
return { query, params: data };
}
const updateData = { Username: 'newName', Email: 'new@email.com' };
const { query, params } = buildUpdateQuery('Users', updateData, 'UserId = @UserId');
console.log(query);
// UPDATE Users SET Username = @Username, Email = @Email WHERE UserId = @UserId
3、性能与最佳实践
性能对比
// ❌ 低效:在循环中重复调用
for (let i = 0; i < 1000; i++) {
const keys = Object.keys(largeObject); // 每次都重新获取
// ...
}
// ✅ 高效:缓存结果
const keys = Object.keys(largeObject);
for (let i = 0; i < 1000; i++) {
// 使用缓存的 keys
}
链式调用
// 组合使用 map/filter/reduce
const result = Object.entries(users)
.filter(([id, user]) => user.isActive)
.map(([id, user]) => ({ id, name: user.name }))
.reduce((acc, user) => {
acc[user.id] = user.name;
return acc;
}, {});
举例:
const users = {
'user_001': {
name: '张三',
age: 30,
isActive: true,
email: 'zhangsan@example.com'
},
'user_002': {
name: '李四',
age: 25,
isActive: false,
email: 'lisi@example.com'
},
'user_003': {
name: '王五',
age: 28,
isActive: true,
email: 'wangwu@example.com'
},
'user_004': {
name: '赵六',
age: 35,
isActive: true,
email: 'zhaoliu@example.com'
},
'user_005': {
name: '周杰',
age: 22,
isActive: false,
email: 'zhoujie@example.com'
}
};
代码逐步拆解执行
这段代码使用了链式调用,我们将一步步拆解,以便清晰地看到每个中间步骤产生的数据。
步骤 1: Object.entries(users)
将 users 对象转换成一个由 [键, 值] 对组成的数组。
// 结果
[
['user_001', { name: '张三', age: 30, isActive: true, email: 'zhangsan@example.com' }],
['user_002', { name: '李四', age: 25, isActive: false, email: 'lisi@example.com' }],
['user_003', { name: '王五', age: 28, isActive: true, email: 'wangwu@example.com' }],
['user_004', { name: '赵六', age: 35, isActive: true, email: 'zhaoliu@example.com' }],
['user_005', { name: '周杰', age: 22, isActive: false, email: 'zhoujie@example.com' }]
]
步骤 2: .filter(([id, user]) => user.isActive)
过滤出 isActive 为 true 的用户。 ([id, user]) 这种写法是数组的解构赋值,直接把 Object.entries 产生的每一项(一个 [key, value] 数组)拆解成 id 和 user 变量。
// 结果 (只保留了 user_001, user_003, user_004)
[
['user_001', { name: '张三', age: 30, isActive: true, email: 'zhangsan@example.com' }],
['user_003', { name: '王五', age: 28, isActive: true, email: 'wangwu@example.com' }],
['user_004', { name: '赵六', age: 35, isActive: true, email: 'zhaoliu@example.com' }]
]
步骤 3: .map(([id, user]) => ({ id, name: user.name }))
将上一步过滤出的每个用户,映射(转换)成一个新的、更简单的对象,这个对象只包含我们需要的 id 和 name 属性。注意 ({ id, name: user.name }) 外面的括号是必需的,这是箭头函数返回一个对象字面量的语法。
// 结果 (产生了一个新的对象数组)
[
{ id: 'user_001', name: '张三' },
{ id: 'user_003', name: '王五' },
{ id: 'user_004', name: '赵六' }
]
一、map 函数的核心作用
map 是数组的一个方法,它的作用是:遍历原数组的每一个元素,对每个元素执行你提供的函数,然后用该函数的返回值组成一个新数组。
输入:一个数组。
处理:对每个元素执行“转换”逻辑。
输出:一个长度相同的新数组。
二、你代码中 map 的输入是什么?
在 map 执行之前,代码已经执行了 .filter,所以 map 拿到的输入数组 filteredEntries 如下:
const filteredEntries = [
['user_001', { name: '张三', age: 30, isActive: true }],
['user_003', { name: '王五', age: 28, isActive: true }],
['user_004', { name: '赵六', age: 35, isActive: true }]
];
可以看到,map 将处理一个数组,其中的每一个元素本身也是一个数组,结构为 [userId, userObject]。
三、详细拆解 map 的回调函数
现在,让我们深入 map 的回调函数:([id, user]) => ({ id, name: user.name })
这个回调函数可以拆成三个部分来理解:参数、函数体、返回值。
- 参数:([id, user]) —— 数组解构
这是理解这段代码的关键。它等价于:
// 完整的参数写法(不解构)
(entry) => {
const id = entry[0];
const user = entry[1];
// ...
}
// 使用数组解构的写法(就是你代码中的)
([id, user]) => {
// 现在可以直接使用 id 和 user 变量
}
map 每次遍历时,会将 filteredEntries 中的一个元素(比如 ['user_001', { name: '张三' }])传递给回调函数。
使用 ([id, user]) 这种写法,可以一步到位地将这个包含两个元素的数组“拆开”,分别赋值给 id 和 user 变量。
第一次遍历:id = 'user_001',user = { name: '张三', age: 30, isActive: true }
第二次遍历:id = 'user_003',user = { name: '王五', ... }
第三次遍历:id = 'user_004',user = { name: '赵六', ... }
- 箭头函数体与返回值:({ id, name: user.name })
这是另一个关键点。它等价于:
// 完整写法
(id, user) => {
// 创建一个新对象
const newObject = {
id: id, // 属性名 id,其值来自参数 id
name: user.name // 属性名 name,其值来自 user 对象的 name 属性
};
return newObject; // 返回这个新对象
}
// 简写(隐式返回)
(id, user) => ({ id, name: user.name })
这里有两个 ES6 的重要语法:
对象属性简写:当属性名和变量名相同时,可以只写一个。{ id } 是 { id: id } 的简写。
隐式返回:箭头函数 => 后面如果不写 {},则表示直接返回该表达式的值。为了返回一个对象字面量 {},需要用 () 把它包起来,否则 JS 引擎会混淆 {} 是函数体还是对象。
四、整个 map 的执行过程图示
为了让你看得更清楚,我们把整个过程画出来:
步骤 map 接收的当前元素 (一个数组) 解构后得到 执行函数后返回的新对象
1 ['user_001', { name: '张三', age: 30, isActive: true }] id = 'user_001'
user = { name: '张三', ... } { id: 'user_001', name: '张三' }
2 ['user_003', { name: '王五', age: 28, isActive: true }] id = 'user_003'
user = { name: '王五', ... } { id: 'user_003', name: '王五' }
3 ['user_004', { name: '赵六', age: 35, isActive: true }] id = 'user_004'
user = { name: '赵六', ... } { id: 'user_004', name: '赵六' }
最终,map 函数返回一个包含这三个新对象的新数组:
[
{ id: 'user_001', name: '张三' },
{ id: 'user_003', name: '王五' },
{ id: 'user_004', name: '赵六' }
]
五、map 的特点总结
不改变原数组:map 会返回一个新数组,而 users、filteredEntries 这些原始数据都不会被修改,这符合函数式编程的“不可变性”原则。
需要显式返回:回调函数必须有返回值,这个返回值会作为新数组的元素。
如果没有 return(或者箭头函数没有隐式返回),新数组的对应位置就会是 undefined。
长度相同:新数组的长度永远等于原数组的长度。如果你想减少或增加数量,应该使用 filter 或 flatMap。
可链式调用:正因为 map 返回一个新数组,你可以直接在后面继续调用 .filter()、.reduce() 或其他数组方法,形成我们之前看到的 filter().map().reduce() 链式调用。
六、更多 map 的实际应用示例
为了更好地理解 map,我们再来看几个你在 Node.js 后端开发中可能遇到的场景。
示例 1:从数据库查询结果中提取特定字段
// 模拟从 SQL Server 查询到的用户列表
const dbUsers = [
{ UserId: 1, Username: 'admin', PasswordHash: 'hash1', Email: 'admin@xx.com', IsActive: true },
{ UserId: 2, Username: 'zhang', PasswordHash: 'hash2', Email: 'zhang@xx.com', IsActive: true },
{ UserId: 3, Username: 'li', PasswordHash: 'hash3', Email: 'li@xx.com', IsActive: false },
];
// 只需要返回用户的 Id 和用户名给前端,并且重命名字段
const safeUsers = dbUsers.map(user => ({
value: user.UserId,
label: user.Username,
status: user.IsActive
}));
console.log(safeUsers);
// 输出:
// [
// { value: 1, label: 'admin', status: true },
// { value: 2, label: 'zhang', status: true },
// { value: 3, label: 'li', status: false }
// ]
示例 2:格式化数据以供 BI 看板使用
const salesData = [
{ product: '产品A', amount: 15000, date: '2024-01-15' },
{ product: '产品B', amount: 23000, date: '2024-01-15' },
{ product: '产品A', amount: 12000, date: '2024-01-16' },
];
// 为图表组件准备数据,将金额转换为万元单位,并添加一个格式化的字符串
const chartData = salesData.map(item => ({
name: item.product,
value: (item.amount / 10000).toFixed(2),
display: `${item.product}: ¥${item.amount} (${item.date})`
}));
console.log(chartData);
// 输出:
// [
// { name: '产品A', value: '1.50', display: '产品A: ¥15000 (2024-01-15)' },
// { name: '产品B', value: '2.30', display: '产品B: ¥23000 (2024-01-15)' },
// { name: '产品A', value: '1.20', display: '产品A: ¥12000 (2024-01-16)' }
// ]
示例 3:在 JWT 验证后转换用户信息
// 假设这是从 JWT 解码后得到的用户信息数组(可能来自多系统)
const jwtPayloads = [
{ sub: 101, name: '张三', roles: ['finance', 'audit'] },
{ sub: 102, name: '李四', roles: ['sales'] },
];
// 转换为内部系统使用的用户对象格式
const internalUsers = jwtPayloads.map(payload => ({
id: payload.sub,
displayName: payload.name,
permissions: payload.roles.includes('finance') ? ['read', 'write'] : ['read']
}));
console.log(internalUsers);
// 输出:
// [
// { id: 101, displayName: '张三', permissions: ['read', 'write'] },
// { id: 102, displayName: '李四', permissions: ['read'] }
// ]
总结
概念 解释
map 的作用 遍历数组,将每个元素“转换”成一个新值,并收集到新数组中返回。
你代码中的用法 将 [userId, userObject] 形式的数组,转换成了 { id: userId, name: userObject.name } 形式的对象。
([id, user]) 这是数组解构在参数中的运用,直接“拆开”了 map 传给回调函数的数组参数。
({ id, name: user.name }) 这是一个返回新对象的箭头函数,利用了对象属性简写和隐式返回的语法。
理解 map 的核心理念——“转换”,并熟练运用其回调函数的各种简写技巧,是编写简洁、高效、可读性强的 JavaScript 代码的关键一步。
步骤 4: .reduce((acc, user) => { acc[user.id] = user.name; return acc; }, {})
将上一步产生的对象数组,归并(累加)成一个单一的对象。
acc 是累加器,初始值为 {} (一个空对象)。
user 是数组中的当前对象。
每次迭代,我们都在累加器 acc 上添加一个新的属性,属性名是 user.id,属性值是 user.name。最后返回这个累加器。
迭代过程:
初始: acc = {}
处理第一个用户 { id: 'user_001', name: '张三' }: acc['user_001'] = '张三' -> acc 变成 { user_001: '张三' }
处理第二个用户 { id: 'user_003', name: '王五' }: acc['user_003'] = '王五' -> acc 变成 { user_001: '张三', user_003: '王五' }
处理第三个用户 { id: 'user_004', name: '赵六' }: acc['user_004'] = '赵六' -> acc 变成 { user_001: '张三', user_003: '王五', user_004: '赵六' }
最终结果
// 最终 result 变量的值
{
user_001: '张三',
user_003: '王五',
user_004: '赵六'
}
总结: 这段代码的核心功能是:将一个以ID为键、用户详细信息为值的用户对象,转换为一个仅包含活跃用户、并以“用户ID:用户名”这种简单键值对形式存储的新对象。
这种数据转换在实际开发中非常常见,比如你需要从一个复杂的用户数据源中,提取一个用于下拉选择框的简单映射表时,这段代码就能派上用场。
总结
方法 一句话总结 典型场景
Object.keys() 获取对象的"钥匙"(属性名) 遍历对象、获取字段列表、验证属性是否存在
Object.values() 获取对象的"内容"(属性值) 数据统计、数组转换、批量操作值
Object.entries() 同时获取"钥匙和内容" 同时需要键和值、对象转 Map
这三个方法是现代 JavaScript 处理对象数据的核心工具,配合数组方法(map、filter、reduce)可以写出非常简洁优雅的数据处理代码,特别适合你正在做的 ERP 数据分析项目。
十、req.on 是 Node.js 原生 HTTP 服务器中用于监听请求数据流的事件监听器。它们处理 POST 请求中异步到达的数据块。
app.post('/api/user', (req, res) => {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
// 将接收到的 JSON 字符串解析为对象
const userData = JSON.parse(body);
console.log(userData.name, userData.age);
res.json({ status: 'success' });
} catch (error) {
res.status(400).json({ error: 'Invalid JSON' });
}
});
});
一、为什么需要这两个事件?
HTTP 请求(尤其是 POST 请求)发送的数据不是一次性完整到达服务器的,而是分批次(数据块/chunk)传输的。req 对象是一个可读流(Readable Stream),需要监听其事件来逐步接收数据。
二、详细解释
1. req.on('data', chunk => { ... })
作用:监听 data 事件,每次接收到一个数据块时触发。
参数:
'data':事件名称,当有数据块到达时触发
chunk:当前接收到的数据块(默认是 Buffer 对象,二进制数据)
代码逻辑:
req.on('data', chunk => {
body += chunk.toString(); // 将 Buffer 转换为字符串并拼接到 body
});
执行过程:
客户端发送 POST 请求,携带 JSON 数据
数据可能分多个包发送(例如:第一个包 {"name":"张三","age":30})
每次收到一个包,就触发一次 data 事件
将每个包转换成字符串并拼接到 body 变量中
2. req.on('end', () => { ... })
作用:监听 end 事件,当所有数据块接收完毕时触发。
代码逻辑:
req.on('end', () => {
try {
// 此时 body 已经包含完整的请求数据
const userData = JSON.parse(body);
console.log(userData.name, userData.age);
res.json({ status: 'success' });
} catch (error) {
res.status(400).json({ error: 'Invalid JSON' });
}
});
执行过程:
所有 data 事件执行完毕后,触发 end 事件
此时 body 变量已包含完整的请求数据(如 {"name":"张三","age":30})
尝试将完整字符串解析为 JSON
成功则返回响应,失败则返回 400 错误
三、完整执行流程图解
客户端发送 POST 请求: {"name":"张三","age":30}
│
▼
到达服务器(分包)
│
┌─────────┴─────────┐
│ │
第1包: {"name":"张三", "age":30}
│ │
▼ ▼
data事件触发 data事件触发
body = '{"name":"张' body = '{"name":"张三","age":30}'
│ │
└─────────┬─────────┘
│
所有包接收完毕
│
▼
end事件触发
│
▼
JSON.parse(body) 解析完整字符串
│
▼
res.json({ status: 'success' })
四、实际数据演示
假设客户端发送:
POST /api/user HTTP/1.1
Content-Type: application/json
Content-Length: 27
{"name":"张三","age":30}
分步执行过程:
步骤 1:收到第一个数据块
// 假设第一个 chunk 包含: {"name":"张
chunk.toString() // 返回 '{"name":"张'
body += '{"name":"张' // body = '{"name":"张'
步骤 2:收到第二个数据块
// 第二个 chunk 包含: 三","age":30}
chunk.toString() // 返回 '三","age":30}'
body += '三","age":30}' // body = '{"name":"张三","age":30}'
步骤 3:所有数据接收完毕,触发 end 事件
JSON.parse(body) // 解析成功,得到 { name: '张三', age: 30 }
console.log('张三', 30) // 输出: 张三 30
res.json({ status: 'success' }) // 返回响应
五、为什么不直接使用 req.body?
在原生 Node.js HTTP 模块中,req 对象没有 body 属性,必须手动监听事件来收集数据。
对比现代框架:
Express 4.x 之前的版本(或原生写法)
// 需要手动处理数据流
app.post('/api/user', (req, res) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const userData = JSON.parse(body);
// 处理业务逻辑
});
});
Express 4.x+ 使用内置中间件(推荐)
// 使用 express.json() 中间件自动处理
app.use(express.json()); // 全局使用
app.post('/api/user', (req, res) => {
// req.body 已经自动解析好了
const userData = req.body;
console.log(userData.name, userData.age);
res.json({ status: 'success' });
});
六、常见问题和注意事项
- 为什么用 chunk.toString()?
// chunk 是 Buffer 对象
console.log(chunk); // <Buffer 7b 22 6e 61 6d 65 22 3a...>
chunk.toString(); // 转换为字符串 '{"name":"张三","age":30}'
- 数据量过大怎么办?
// 设置最大接收限制,防止内存溢出
let body = '';
let bodyLength = 0;
const MAX_SIZE = 1024 * 1024; // 1MB 限制
req.on('data', chunk => {
bodyLength += chunk.length;
if (bodyLength > MAX_SIZE) {
// 请求体过大,拒绝处理
res.status(413).json({ error: 'Request too large' });
req.destroy(); // 中止请求
return;
}
body += chunk.toString();
});
- 处理非 JSON 格式
req.on('end', () => {
try {
// 尝试解析 JSON
const userData = JSON.parse(body);
// 处理 JSON 数据
} catch (error) {
// 如果不是 JSON,作为普通文本处理
console.log('Received text:', body);
res.json({ status: 'text received' });
}
});
七、事件监听 vs 中间件对比
特性 手动监听 data/end Express 中间件
代码量 多(需手动拼接、解析) 少(一行 express.json())
错误处理 需手动 try-catch 框架自动处理
性能 相同 相同(底层实现类似)
适用场景 学习原理、定制化需求 生产环境、常规开发
八、总结
事件 触发时机 作用
data 每收到一个数据块 逐步接收并拼接请求体
end 所有数据接收完毕 处理完整的请求数据
核心要点:
req 是一个可读流,数据是分块到达的
data 事件负责收集碎片
end 事件表示收集完成,可以开始处理
生产环境建议使用 Express 的 express.json() 中间件,避免重复造轮子
十一、Express 4.x 的请求对象(req)提供的属性和方法。可以分为获取参数与数据、请求信息以及进行内容协商的几个大类。
🧩 获取请求参数与数据
这是最常用的功能,主要用来获取客户端传递的各种参数。
属性/方法 说明 代码示例
req.params 获取路由参数:从URL路径中定义的动态参数。 路由定义为 /users/:userId
访问 /users/123
req.params.userId 的值是 "123"
req.query 获取查询字符串参数:从URL问号 ? 后面的参数。 访问 /search?keyword=express&page=1
req.query.keyword 的值是 "express",req.query.page 的值是 "1"
req.body 获取请求体参数:从POST、PUT等请求的Body中获取。 需要配合 express.json() 或 express.urlencoded() 等中间件使用。
req.cookies 获取普通Cookie:解析客户端发送的Cookie。 需要先使用 cookie-parser 中间件。
req.signedCookies 获取签名Cookie:用于获取经过签名验证的Cookie,可以防止客户端篡改。 需要先使用 cookie-parser 中间件并传入密钥。
ℹ️ 获取请求相关信息
这些属性帮助你了解请求的来源、协议和路径等上下文信息。
属性/方法 说明 代码示例
req.headers 获取请求头:一个包含所有HTTP请求头的对象。 req.headers['content-type'] 可获取 Content-Type 请求头的值。
req.ip 获取客户端IP地址:获取发送请求的客户端IP地址。 req.ip 的值可能是 "127.0.0.1"。
req.method 获取HTTP方法:获取请求的HTTP方法(如GET、POST、PUT等)。 req.method 的值是 "GET"。
req.path 获取请求路径:获取请求URL中的路径部分(不包含查询字符串)。 访问 /users/123?page=1,req.path 的值是 "/users/123"。
req.hostname 获取主机名:获取请求头 Host 字段中的主机名。 req.hostname 的值是 "example.com"。
req.protocol 获取协议类型:获取请求的协议(http 或 https)。 req.protocol 的值是 "http"。
req.secure 判断是否为HTTPS:布尔值,true 表示是一个TLS加密连接。 req.secure 的值是 true。
req.xhr 判断是否为AJAX请求:布尔值,true 表示请求头中包含 X-Requested-With: XMLHttpRequest。 req.xhr 的值是 true。
🤝 内容协商方法
这些方法用于判断客户端期望接收的响应内容格式。
方法 说明 代码示例
req.accepts(types) 检查客户端接受的响应类型:根据请求头 Accept,返回客户端期望的最佳内容类型。req.accepts('json') 会检查客户端是否接受 application/json。
req.get(field) 获取指定请求头字段的值:与 req.headers[field] 类似,但更方便。 req.get('Content-Type') 会返回 "application/json"。
req.is(type) 检查请求体的Content-Type类型:用于判断请求的 Content-Type 是否匹配给定的MIME类型。 req.is('json') 用于检查请求体是否是 application/json 格式。
💡 实战示例:解析不同类型的参数
一个典型的接口需要同时处理来自不同位置的参数,下面的代码清晰地展示了如何在一个路由中分别获取 params、query 和 body:
const express = require('express');
const app = express();
// 必须使用中间件,req.body 才会被解析
app.use(express.json()); // 解析 application/json 格式的请求体
// 定义一个包含路由参数 :userId 的接口
app.put('/users/:userId', (req, res) => {
// 1. 获取 URL 路径中的路由参数
const userId = req.params.userId; // 例如:/users/123 -> '123'
// 2. 获取 URL 问号后的查询字符串参数
const isAdmin = req.query.isAdmin; // 例如:?isAdmin=true -> 'true'
// 3. 获取请求体中的 JSON 数据
const { name, email } = req.body; // 例如:{"name": "John", "email": "john@example.com"}
// 处理业务逻辑...
console.log(`正在更新用户 ${userId},管理员权限:${isAdmin},新数据:${name}, ${email}`);
// 返回响应
res.json({ message: `用户 ${userId} 更新成功` });
});
app.listen(3000, () => console.log('Server started on port 3000'));
使用 cURL 测试这个接口:
curl -X PUT \
'http://localhost:3000/users/123?isAdmin=true' \
-H 'Content-Type: application/json' \
-d '{"name": "John Doe", "email": "john@example.com"}'
在这个例子中,userId、isAdmin、name 和 email 这三个值分别来自不同的地方,通过 req 对象的不同属性被准确获取。
十二、values.map((_, i) => @p${i}).join(',') 中的 _ 是一个占位符,用于表示“这个参数我不需要使用”
1. map 函数的参数机制
在 JavaScript 中,map 方法的回调函数通常接受三个参数:
array.map((当前元素的值, 当前元素的索引, 原数组本身) => {
// ...
})
第一个参数 (value):当前遍历到的元素的值。
第二个参数 (index):当前遍历到的元素的下标(从 0 开始)。
第三个参数 (array):原数组本身(很少用到)。
2. 你的代码中发生了什么?
你的代码是 values.map((_, i) => ...)。
values 是一个数组,例如 ['张三', 'zhang@example.com']。
map 方法会遍历这个数组。
当 map 第一次遍历时:
第一个参数(元素的值)是 '张三'
第二个参数(索引)是 0
但你的代码写作 (_, i):
_ (第一个参数) 接收了 '张三',但因为你用 _ 命名,表明你知道这里有一个值,但你在函数体内不打算用它。
i (第二个参数) 接收了 0。
当 map 第二次遍历时:
_ 接收了 'zhang@example.com',仍然被忽略。
i 接收了 1。
3. 为什么需要 _?
因为 map 总是会传递第一个参数(值),如果你只写 (i),其实是错误的:
// ❌ 错误示例
values.map((i) => `@p${i}`)
// 你以为 i 是 0,1,2... 但实际上 i 接收的是 '张三', '李四'...
// 结果会变成 ['@p张三', '@p李四'],这不是你想要的。
为了跳过第一个参数,只拿到第二个参数(索引),你必须定义一个变量来占据第一个参数的位置。使用 _ 是一个广泛接受的编程惯例,意思是:“这个变量是占位符,我不会用它。”
4. 完整执行示例
假设你的代码是插入用户数据:
const values = ['张三', 'zhang@example.com', 25];
const paramPlaceholders = values.map((_, i) => `@p${i}`).join(',');
console.log(paramPlaceholders); // 输出: @p0, @p1, @p2
执行过程拆解:
循环次数 当前元素值 (第一个参数) 索引 i (第二个参数) 返回结果
1 '张三' (被 _ 接收并忽略) 0 @p0
2 'zhang@example.com' (被 _ 接收并忽略) 1 @p1
3 25 (被 _ 接收并忽略) 2 @p2
最终,join(',') 将这三个结果用逗号连接成字符串 '@p0, @p1, @p2'。
5. 总结
_:一个合法的变量名,用作占位符,代表“这个位置有参数,但我用不到”。
i:代表 index(索引),正是你需要的数字(0, 1, 2...)。
目的:将数组 ['张三', 'email', 25] 转换为 SQL 参数占位符字符串 '@p0, @p1, @p2',方便后续安全地传入数据库查询。
JavaScript 对象和数组的增删改查操作示例
一、对象的增删改查
对象是一组键值对的集合,键通常是字符串(或 Symbol),值可以是任意类型。
- 增(添加属性)
const user = { name: '张三', age: 25 };
// 方法1:点语法
user.email = 'zhangsan@example.com';
// 方法2:方括号语法(适用于键名是变量或包含特殊字符)
const key = 'phone';
user[key] = '13800138000';
user['home-address'] = '北京市朝阳区'; // 键名包含特殊字符必须用方括号
// 方法3:Object.assign() 合并对象
Object.assign(user, { city: '北京', country: '中国' });
// 方法4:扩展运算符(创建新对象)
const newUser = { ...user, role: 'admin', status: 'active' };
console.log(user);
// { name: '张三', age: 25, email: 'zhangsan@example.com', phone: '13800138000', home-address: '...', city: '北京', country: '中国' }
2. 删(删除属性)
const user = { name: '张三', age: 25, email: 'test@example.com', password: '123456' };
// 方法1:delete 操作符
delete user.password;
console.log(user); // { name: '张三', age: 25, email: 'test@example.com' }
// 方法2:解构赋值(创建新对象,不改变原对象)
const { email, ...userWithoutEmail } = user;
console.log(userWithoutEmail); // { name: '张三', age: 25 }
console.log(user); // 原对象不变 { name: '张三', age: 25, email: '...' }
// 注意:delete 会修改原对象,解构不会修改原对象
3. 改(修改属性值)
const user = { name: '张三', age: 25, email: 'old@example.com' };
// 方法1:点语法
user.name = '李四';
// 方法2:方括号语法
user['age'] = 26;
// 方法3:Object.assign() 修改
Object.assign(user, { email: 'new@example.com', phone: '13800138000' });
// 方法4:扩展运算符(创建新对象)
const updatedUser = { ...user, name: '王五' };
console.log(updatedUser); // { name: '王五', age: 26, email: 'new@example.com', phone: '...' }
console.log(user); // 原对象不变
console.log(user); // { name: '李四', age: 26, email: 'new@example.com', phone: '13800138000' }
4. 查(访问属性)
const user = { name: '张三', age: 25, email: 'test@example.com' };
// 方法1:点语法
console.log(user.name); // '张三'
// 方法2:方括号语法(支持变量)
const key = 'age';
console.log(user[key]); // 25
console.log(user['email']); // 'test@example.com'
// 方法3:可选链操作符 ?. (防止访问 undefined 的属性)
const address = user.address?.city;
console.log(address); // undefined(不会报错)
// 方法4:in 操作符(检查属性是否存在)
console.log('name' in user); // true
console.log('address' in user); // false
// 方法5:hasOwnProperty(检查自身属性,不包含继承的)
console.log(user.hasOwnProperty('name')); // true
// 方法6:Object.keys/values/entries 遍历查询
Object.keys(user).forEach(key => {
console.log(`${key}: ${user[key]}`);
});
// 输出:
// name: 张三
// age: 25
// email: test@example.com
二、数组的增删改查
数组是有序集合,元素通过索引(从 0 开始)访问。
1. 增(添加元素)
const arr = [1, 2, 3];
// 方法1:push() - 在末尾添加(修改原数组)
arr.push(4);
console.log(arr); // [1, 2, 3, 4]
// 方法2:unshift() - 在开头添加(修改原数组)
arr.unshift(0);
console.log(arr); // [0, 1, 2, 3, 4]
// 方法3:splice() - 在指定位置添加(修改原数组)
// 语法:splice(起始索引, 删除数量, 要插入的元素...)
arr.splice(2, 0, 99, 100); // 在索引2的位置插入 99 和 100
console.log(arr); // [0, 1, 99, 100, 2, 3, 4]
// 方法4:扩展运算符(创建新数组)
const newArr = [...arr, 5, 6];
console.log(newArr); // [0, 1, 99, 100, 2, 3, 4, 5, 6]
console.log(arr); // 原数组不变
// 方法5:concat()(创建新数组)
const anotherArr = arr.concat([7, 8]);
console.log(anotherArr); // [0, 1, 99, 100, 2, 3, 4, 7, 8]
2. 删(删除元素)
const arr = [10, 20, 30, 40, 50, 60];
// 方法1:pop() - 删除末尾元素(返回被删除的元素)
const last = arr.pop();
console.log(last); // 60
console.log(arr); // [10, 20, 30, 40, 50]
// 方法2:shift() - 删除开头元素(返回被删除的元素)
const first = arr.shift();
console.log(first); // 10
console.log(arr); // [20, 30, 40, 50]
// 方法3:splice() - 删除任意位置的元素(返回被删除元素的数组)
// 语法:splice(起始索引, 删除数量)
const deleted = arr.splice(1, 2); // 从索引1开始删除2个元素
console.log(deleted); // [30, 40]
console.log(arr); // [20, 50]
// 方法4:filter() - 过滤删除(创建新数组,不修改原数组)
const arr2 = [1, 2, 3, 4, 5];
const filtered = arr2.filter(item => item !== 3); // 删除值为3的元素
console.log(filtered); // [1, 2, 4, 5]
console.log(arr2); // [1, 2, 3, 4, 5] 原数组不变
// 方法5:delete 操作符(不推荐,会留下 empty 占位)
const arr3 = ['a', 'b', 'c'];
delete arr3[1];
console.log(arr3); // ['a', empty, 'c']
console.log(arr3.length); // 3(长度不变,有空洞)
3. 改(修改元素)
const arr = [10, 20, 30, 40, 50];
// 方法1:通过索引直接赋值(修改原数组)
arr[2] = 99;
console.log(arr); // [10, 20, 99, 40, 50]
// 方法2:splice() - 替换元素(修改原数组)
arr.splice(1, 2, 88, 77); // 从索引1开始,删除2个元素,再插入88和77
console.log(arr); // [10, 88, 77, 40, 50]
// 方法3:map() - 映射创建新数组(不修改原数组)
const doubled = arr.map(item => item * 2);
console.log(doubled); // [20, 176, 154, 80, 100]
console.log(arr); // [10, 88, 77, 40, 50] 原数组不变
// 方法4:fill() - 填充(修改原数组)
const arr2 = [1, 2, 3, 4, 5];
arr2.fill(0, 1, 3); // 从索引1到索引3(不包括3)填充0
console.log(arr2); // [1, 0, 0, 4, 5]
// 方法5:with() - ES2023新增,创建新数组
const arr3 = [1, 2, 3, 4, 5];
const newArr3 = arr3.with(2, 99); // 将索引2改为99
console.log(newArr3); // [1, 2, 99, 4, 5]
console.log(arr3); // [1, 2, 3, 4, 5] 原数组不变
4. 查(访问/查找元素)
const arr = [10, 20, 30, 40, 50, 30];
// 方法1:通过索引访问
console.log(arr[0]); // 10
console.log(arr[arr.length - 1]); // 30(最后一个元素)
// 方法2:indexOf() - 查找元素的索引(从左到右,返回第一个匹配的索引)
console.log(arr.indexOf(30)); // 2
console.log(arr.indexOf(100)); // -1(不存在)
// 方法3:lastIndexOf() - 从右到左查找
console.log(arr.lastIndexOf(30)); // 5
// 方法4:includes() - 判断是否存在
console.log(arr.includes(40)); // true
console.log(arr.includes(100)); // false
// 方法5:find() - 根据条件查找第一个满足条件的元素
const found = arr.find(item => item > 25);
console.log(found); // 30(第一个大于25的元素)
// 方法6:findIndex() - 根据条件查找索引
const index = arr.findIndex(item => item > 25);
console.log(index); // 2
// 方法7:filter() - 查找所有满足条件的元素
const allBig = arr.filter(item => item > 25);
console.log(allBig); // [30, 40, 50, 30]
// 方法8:some() - 判断是否有元素满足条件
console.log(arr.some(item => item > 45)); // true
// 方法9:every() - 判断是否所有元素都满足条件
console.log(arr.every(item => item > 0)); // true
// 方法10:at() - ES2022新增,支持负数索引
console.log(arr.at(0)); // 10
console.log(arr.at(-1)); // 30(最后一个元素)
console.log(arr.at(-2)); // 50(倒数第二个)
三、综合示例:实际应用场景
场景1:用户管理系统
// 模拟用户数据
let users = [
{ id: 1, name: '张三', age: 25, active: true },
{ id: 2, name: '李四', age: 30, active: false },
{ id: 3, name: '王五', age: 28, active: true }
];
// 增:添加新用户
const newUser = { id: 4, name: '赵六', age: 35, active: true };
users.push(newUser);
// 删:删除 id=2 的用户
users = users.filter(user => user.id !== 2);
// 改:修改 id=3 的用户年龄
users = users.map(user =>
user.id === 3 ? { ...user, age: 29 } : user
);
// 查:查找活跃用户
const activeUsers = users.filter(user => user.active);
console.log(activeUsers);
// [
// { id: 1, name: '张三', age: 25, active: true },
// { id: 3, name: '王五', age: 29, active: true },
// { id: 4, name: '赵六', age: 35, active: true }
// ]
场景2:购物车功能
// 购物车(数组存储商品对象)
let cart = [];
// 增:添加商品
function addToCart(productId, name, price, quantity = 1) {
const existingItem = cart.find(item => item.productId === productId);
if (existingItem) {
// 改:如果已存在,修改数量
existingItem.quantity += quantity;
} else {
// 增:添加新商品
cart.push({ productId, name, price, quantity });
}
}
// 删:移除商品
function removeFromCart(productId) {
cart = cart.filter(item => item.productId !== productId);
}
// 改:修改商品数量
function updateQuantity(productId, newQuantity) {
const item = cart.find(item => item.productId === productId);
if (item) {
item.quantity = newQuantity;
}
}
// 查:计算总价
function getTotalPrice() {
return cart.reduce((total, item) => total + (item.price * item.quantity), 0);
}
// 使用示例
addToCart(1, '手机', 2999, 1);
addToCart(2, '耳机', 199, 2);
addToCart(1, '手机', 2999, 1); // 手机数量变为2
console.log(cart);
// [
// { productId: 1, name: '手机', price: 2999, quantity: 2 },
// { productId: 2, name: '耳机', price: 199, quantity: 2 }
// ]
console.log('总价:', getTotalPrice()); // 总价: 2999*2 + 199*2 = 6396
updateQuantity(2, 1);
console.log('修改后总价:', getTotalPrice()); // 修改后总价: 2999*2 + 199*1 = 6197
removeFromCart(1);
console.log(cart); // [{ productId: 2, name: '耳机', price: 199, quantity: 1 }]
四、方法汇总表
对象方法汇总
操作 方法 是否修改原对象 返回值
增 obj.newKey = value 是 新值
增 Object.assign(obj, {key: value}) 是 修改后的对象
增 {...obj, newKey: value} 否 新对象
删 delete obj.key 是 布尔值
删 {...omit(obj, 'key')} (需工具库) 否 新对象
改 obj.key = newValue 是 新值
改 {...obj, key: newValue} 否 新对象
查 obj.key 或 obj['key'] - 值或 undefined
查 'key' in obj - 布尔值
数组方法汇总
操作 方法 是否修改原数组 返回值
增(末尾) push(item) 是 新长度
增(开头) unshift(item) 是 新长度
增(中间) splice(index, 0, item) 是 空数组
增(末尾,不修改) [...arr, item] 否 新数组
删(末尾) pop() 是 被删除的元素
删(开头) shift() 是 被删除的元素
删(中间) splice(index, count) 是 被删除元素的数组
删(条件) filter(item => condition) 否 新数组
改(索引) arr[index] = newValue 是 新值
改(映射) map(item => newItem) 否 新数组
查(索引) indexOf(item) - 索引或 -1
查(条件) find(item => condition) - 元素或 undefined
查(所有) filter(item => condition) - 新数组
查(判断存在) includes(item) - 布尔值
五、注意事项
- 数组的 delete 操作符不推荐
// ❌ 不推荐:会留下空洞
const arr = [1, 2, 3];
delete arr[1];
console.log(arr); // [1, empty, 3]
console.log(arr.length); // 3(空洞仍然占位)
// ✅ 推荐使用 splice
arr.splice(1, 1);
console.log(arr); // [1, 3]
2. 对象属性的遍历顺序
const obj = { '3': 'c', '1': 'a', '2': 'b', name: '张三' };
console.log(Object.keys(obj));
// 数字键会按数字升序排列,字符串键按添加顺序:['1', '2', '3', 'name']
3. 数组方法链式调用
const users = [
{ name: '张三', age: 25, active: true },
{ name: '李四', age: 30, active: false },
{ name: '王五', age: 28, active: true }
];
// 链式调用:筛选 -> 映射 -> 聚合
const result = users
.filter(user => user.active) // 只保留活跃用户
.map(user => user.name) // 提取名字
.join(', '); // 连接成字符串
console.log(result); // "张三, 王五"
4. 不可变操作(函数式编程)
// 推荐:不修改原数据,返回新数据
const addToCart = (cart, newItem) => [...cart, newItem];
const removeFromCart = (cart, itemId) => cart.filter(item => item.id !== itemId);
const updateItem = (cart, itemId, newQuantity) =>
cart.map(item => item.id === itemId ? { ...item, quantity: newQuantity } : item);
// 使用
let cart = [{ id: 1, name: '手机', quantity: 1 }];
cart = addToCart(cart, { id: 2, name: '耳机', quantity: 2 });
cart = updateItem(cart, 1, 3);
cart = removeFromCart(cart, 2);
浙公网安备 33010602011771号