JavaScript 对象实战:属性访问、浅拷贝与安全字典
本文目录
普通对象既是数据记录,也是原型链上的对象。读写属性前要分清“自己的属性”和“继承来的属性”,尤其是字段来自 API、URL、导入文件或用户输入时。对象语法简洁,不代表它天然适合任意键的安全字典。
读写属性与判断存在
点语法适合已知标识符键;动态键用方括号。对象解构可设默认值,但默认值仅在属性值为 undefined 时启用,不会把 null 替换掉。可选链能避免对 null/undefined 继续读取时报错,却不负责验证数据形状。
const user = { profile: { displayName: 'Haoge' }, quota: 0 };
const name = user.profile?.displayName ?? '访客';
const quota = user.quota ?? 10;
const record = Object.create(null);
record['request-id'] = 'r-42';
const payload = JSON.parse('{"role":"reader","active":true}');
const isRoleProvided = Object.hasOwn(payload, 'role');
Object.hasOwn(value, key) 检查自有属性,适用于对象本身重写了 hasOwnProperty 的情况,也不会误把原型链属性当输入字段。不要用 key in value 代替:in 会检查继承链。
属性反射 API 解决的是不同问题:Object.keys/values/entries 面向可枚举自有字符串键;Object.getOwnPropertyNames 还包含不可枚举字符串键;Object.getOwnPropertySymbols 返回自有 symbol 键;Object.getOwnPropertyDescriptors 可读取全部自有属性描述符。Object.defineProperty / defineProperties 可声明 getter、setter、writable 和 enumerable 等属性行为,普通业务数据不要为“炫技”改成访问器。
Object.create(proto) 显式创建指定原型的对象。Object.getPrototypeOf 适合检查原型;动态调用 Object.setPrototypeOf 往往会损害优化,也容易掩盖数据建模问题,应尽量在创建时确定原型。Object.is 提供与 === 略有差异的值比较(例如识别 NaN,区分正负零)。Object.preventExtensions、Object.seal、Object.freeze 的限制逐级增强,但都不是深冻结;对应的 isExtensible、isSealed、isFrozen 用于检查当前状态。
枚举、合并和浅拷贝
Object.keys 返回可枚举的自有字符串键,Object.values 返回相应值,Object.entries 返回键值对;它们不包括 symbol 键。键顺序受规范规则影响:整数索引键优先按数值顺序,其他字符串按创建顺序,symbols 另行排列。若顺序是业务语义,使用数组表达更清楚。
对象展开和 Object.assign 都不是深拷贝。嵌套对象会共享引用;Object.assign(target, source) 还会修改 target,并通过普通属性读写触发 getter/setter。用展开语法创建新顶层对象可避免修改 source,但要逐层复制真正需要独立的嵌套结构。
structuredClone 能处理很多结构化可克隆值并保留循环引用,但它不支持任意函数、DOM 节点和所有宿主对象;它也不是“复制任意类实例并保留原型”的万能函数。克隆之前先明确数据边界。Object.freeze 默认也只是浅冻结,不能当成安全隔离或授权机制。
动态键与原型污染
记录固定字段时用对象;需要由外部输入驱动的键值集合时优先用 Map,它不会把键名解析为对象原型属性。需要输出普通对象时,对键做允许列表校验,并只复制确实需要的字段;不要直接把任意 JSON 合并到配置对象或状态对象。
原型污染常出现在递归合并器、路径赋值器或把不可信键写入普通对象的代码中。对动态路径拒绝 __proto__、constructor、prototype 等敏感段并不总是足够;优先采用成熟、维护中的解析/更新库,并测试深层对象和数组输入。不要依赖旧的 __proto__ 访问器。Object.create(null) 可以建立无原型字典,但它缺少常用对象方法;多数业务场景使用 Map 更直接。
const editableFields = new Set(['displayName', 'timezone']);
function pickEditableFields(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('Expected a record');
}
return Object.fromEntries(
Object.entries(input).filter(([key]) => editableFields.has(key)),
);
}
客户端筛字段只能改善交互和减少误传,服务端仍须重新校验字段、类型、权限和业务约束。对象结构不是授权边界。
valueOf、toString 多数来自原型,通常不需要作为“对象静态工具”单独调用;对象也没有通用的 length 或 name 属性,这些字段属于特定类型或函数。函数的 caller、arguments.callee 属于不应依赖的遗留反射方式,在严格模式中还会受到限制;用具名函数、显式参数和模块结构表达调用关系。
交付前自查
- 字段是自有属性还是原型链继承属性?缺失、
undefined、null是否要区别处理? - 合并是否会修改输入,浅拷贝后是否仍共享嵌套引用?
- 动态键是否改用 Map、允许列表或安全的无原型字典?
- 输入是否有 schema 校验,服务端是否按当前用户权限重新判定?
- 是否把冻结、客户端过滤或前端校验误当成安全措施?
参考:MDN Object、MDN Map、MDN structuredClone、OWASP Prototype Pollution Prevention。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。