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, 表示程序正常退出。
    }
});

显示结果:

b9ce797e-f45e-454b-afc8-f0e3243bf920

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 })

这个回调函数可以拆成三个部分来理解:参数、函数体、返回值。

  1. 参数:([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: '赵六', ... }

  1. 箭头函数体与返回值:({ 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' });
});

六、常见问题和注意事项

  1. 为什么用 chunk.toString()?
// chunk 是 Buffer 对象
console.log(chunk);  // <Buffer 7b 22 6e 61 6d 65 22 3a...>
chunk.toString();    // 转换为字符串 '{"name":"张三","age":30}'
  1. 数据量过大怎么办?
// 设置最大接收限制,防止内存溢出
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();
});
  1. 处理非 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),值可以是任意类型。

  1. 增(添加属性)
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) - 布尔值

五、注意事项

  1. 数组的 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);
posted @ 2026-04-07 14:24  冀未然  阅读(7)  评论(0)    收藏  举报