一、对象创建机制:不只是 {} 的魔法
1.1 普通对象 vs 异质对象
核心差异:[[Class]] 内部槽的不同实现
// 普通对象
const obj = { a: 1 };
console.log(obj.toString()); // [object Object]
// 异质对象(以数组为例)
const arr = [1, 2];
console.log(arr.toString()); // "1,2"(重写了toString方法)
console.log(Object.prototype.toString.call(arr)); // [object Array]
// 函数对象(异质对象)
function foo() {}
console.log(foo.toString()); // function foo() {}
原理剖析:
- 普通对象:继承标准 Object 原型方法
- 异质对象:内部实现特殊 [[Class]] 标记(如 Array、Function)
- 现代 JS 中通过 Symbol.toStringTag 自定义类型标签
1.2 属性键的存储秘密
V8 引擎优化策略:
const obj = {};
obj[1] = 'num'; // 存入 elements 存储区(连续内存)
obj['1'] = 'string'; // 覆盖前值(数字键被标准化为字符串)
obj[Symbol()] = 'symbol';// 存入 properties 存储区(离散存储)
obj.a = 'direct'; // 内联缓存(快速访问)
// 验证存储方式
console.log(obj); // {1: 'string', a: 'direct', Symbol(): 'symbol'}
性能启示:
- 数字键优先使用连续存储
- 相同键的不同类型会覆盖
- Symbol 键独立存储
1.3 对象字面量优化
V8 快速路径(Fast Path)机制:
// 快速路径(优化后)
const optimized = {
a: 1,
b: 2
};
// 慢速路径(动态添加)
const unoptimized = {};
unoptimized.a = 1;
unoptimized[Math.random() > 0.5 ? 'b' : 'c'] = 2; // 无法预测形状
// 性能对比
console.time('快速路径');
for(let i=0; i<1e6; i++) optimized.a++;
console.timeEnd('快速路径'); // ~15ms
console.time('慢速路径');
for(let i=0; i<1e6; i++) unoptimized.a++;
console.timeEnd('慢速路径'); // ~120ms
关键优化点:
- 字面量初始化可预测对象形状
- 隐藏类(Hidden Class)共享机制
- 动态属性导致隐藏类切换开销
二、属性描述符:不只是 Object.defineProperty
2.1 [[Get]]/[[Set]] 全流程解析
const obj = {
_value: 0,
get count() {
console.log('触发 [[Get]]');
return this._value;
},
set count(val) {
console.log('触发 [[Set]]');
if(val > 10) throw new Error('超过最大值');
this._value = val;
}
};
// 等价于:
Object.defineProperty(obj, 'count', {
get() { /*...*/ },
set(val) { /*...*/ },
enumerable: true,
configurable: true
});
// 操作验证
obj.count = 5; // 触发 [[Set]]
console.log(obj.count); // 触发 [[Get]]
内部流程:
- 检查对象自身属性
- 遍历原型链
- 调用可能的 getter/setter
- 默认 [[Get]] 返回值,[[Set]] 创建属性
2.2 冻结层级差异
const obj = {
prop: '可修改',
nested: { a: 1 }
};
// Object.seal
Object.seal(obj);
obj.prop = '新值'; // 允许
obj.newProp = '新增'; // 静默失败(严格模式报错)
delete obj.prop; // 失败
// Object.freeze
Object.freeze(obj);
obj.prop = '再修改'; // 静默失败
obj.nested.a = 2; // 成功!浅冻结
// 深度冻结实现
function deepFreeze(o) {
Object.freeze(o);
Object.getOwnPropertyNames(o).forEach(prop => {
if(typeof o[prop] = 'object' && o[prop] ! null)
deepFreeze(o[prop]);
});
}
冻结维度对比:
| 方法 | 修改属性值 | 添加属性 | 删除属性 | 配置属性 |
|---|---|---|---|---|
| Object.seal | ✅ | ❌ | ❌ | ❌ |
| Object.freeze | ❌ | ❌ | ❌ | ❌ |
2.3 枚举顺序规范
ECMA-262 规定顺序:
const obj = {
2: '数字2',
'10': '字符串10',
b: '字母b',
1: '数字1',
a: '字母a'
};
console.log(Object.keys(obj));
// 正确输出: ['1', '2', '10', 'b', 'a']
排序规则:
- 数字键升序排列(按数值大小)
- 字符串键按创建顺序
- Symbol 键按创建顺序(ES6+)
注意陷阱:
const obj = {
'+1': '特殊数字',
'1': '纯数字'
};
console.log(Object.keys(obj)); // ['1', '+1']
// 因为 '+1' 不被识别为数字键
三、原型链:隐藏在继承背后的性能杀手
3.1 proto vs setPrototypeOf
性能对比测试:
// 测试用例
const obj = {};
const newProto = { x: 1 };
// __proto__ 方式
console.time('__proto__');
obj.__proto__ = newProto;
console.timeEnd('__proto__'); // ~0.02ms
// setPrototypeOf 方式
console.time('setPrototypeOf');
Object.setPrototypeOf(obj, newProto);
console.timeEnd('setPrototypeOf'); // ~0.25ms
// 但真正的性能差异体现在后续操作:
function testAccess(obj) {
console.time('属性访问');
for(let i=0; i<1e6; i++) obj.x++;
console.timeEnd('属性访问');
}
testAccess(obj); // 首次访问: ~150ms
testAccess(obj); // 后续访问: ~5ms (隐藏类优化失效)
结论:
__proto__是早期浏览器实现的非标准方法Object.setPrototypeOf是ES6标准方法- 修改原型会破坏隐藏类优化
- 生产环境应避免动态修改原型
3.2 原型链缓存机制
V8 隐藏类优化:
function Person(name) {
this.name = name;
}
const john = new Person('John');
// 首次访问
console.log(john.name); // 触发原型链查找
// 后续访问(缓存生效)
console.log(john.name); // 直接读取缓存偏移量
// 破坏隐藏类
john.age = 30; // 创建新的隐藏类
console.log(john.name); // 重新查找
优化建议:
- 在构造函数中初始化所有属性
- 保持属性添加顺序一致
- 避免在实例化后添加新属性
3.3 instanceof 的边界陷阱
异常案例集锦:
// 案例1:基本类型值
console.log('str' instanceof String); // false
console.log(new String('str') instanceof String); // true
// 案例2:跨窗口对象
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
console.log([] instanceof iframeArray); // false
console.log(Array.isArray([])); // true(更安全的检测方式)
// 案例3:修改原型链
function Foo() {}
const obj = {};
Object.setPrototypeOf(obj, Foo.prototype);
console.log(obj instanceof Foo); // true(即使没有构造函数)
// 案例4:Symbol.hasInstance 自定义
class MyClass {
static [Symbol.hasInstance](instance) {
return 'magic' in instance;
}
}
const obj2 = { magic: true };
console.log(obj2 instanceof MyClass); // true
instanceof 实现原理:
function myInstanceof(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while(proto) {
if(proto === constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
四、最佳实践总结
-
对象创建:
- 优先使用字面量初始化
- 避免动态添加不同"形状"的属性
- 异质对象选择正确的构造函数
-
属性操作:
- 敏感对象使用 freeze/seal
- 注意数字键的排序特性
- 使用 Proxy 替代直接 getter/setter
-
原型链:
- 避免修改已创建对象的原型
- 使用
Object.create(null)创建纯净字典 - 优先使用
Object.getPrototypeOf代替__proto__
-
类型判断:
- 使用
Symbol.toStringTag自定义类型标签 - 数组检测使用
Array.isArray() - 考虑
typeof与instanceof的局限性
- 使用
掌握这些底层原理,将助你写出更高性能、更健壮的 JavaScript 代码!关注本人公众号(鱼樱AI实验室)更多干货持续日更输出适用零基础小白也适用0-5年内cv选手!!!