1. 从“万物皆对象”说起:为什么JavaScript的Object是基石?
如果你写过JavaScript,哪怕只是几行代码,也一定和Object打过交道。它无处不在,却又常常被我们视为理所当然。从最简单的{name: ‘张三’},到复杂的DOM元素、浏览器API返回的结果,再到你通过new关键字创建的任何实例,本质上都是对象。很多人初学JavaScript时,会被告知“JavaScript是一门基于对象的语言”,或者更准确地说,它是一门“基于原型的面向对象语言”。这个“基于原型”的特性,让它的对象模型与Java、C++这类基于类的语言截然不同,也带来了独特的灵活性和一些令人困惑的“坑”。
今天,我们不谈高深的原型链和继承,就聚焦在这个最基础、最核心的Object上。很多开发者,包括一些有经验的,对对象的理解可能还停留在“键值对的集合”这个层面。这没错,但远远不够。理解对象,是理解JavaScript中函数、数组、模块化、乃至现代框架设计模式(如React Hooks的状态管理、Vue的响应式原理)的基石。一个对象,它不仅仅是一个数据容器,它更是一个拥有状态(属性)和行为(方法)的实体,是JavaScript组织代码和数据的根本单元。
为什么我要单独花时间来讲Object?因为在日常开发中,我见过太多由于对对象理解不透彻而引发的问题:比如试图修改一个“冻结”对象时的静默失败,比如在循环引用时进行深拷贝导致的内存溢出,再比如因为属性描述符配置不当,使得数据无法被Vue或React正确追踪。这些问题,追根溯源,都是对Object这个基础概念的掌握不够扎实。接下来,我会带你重新审视这个老朋友,从它的创建、属性、内部特性到一些高级但实用的技巧,目标是让你不仅能“用”对象,更能“懂”对象,写出更健壮、更高效的代码。
2. 对象创建面面观:从字面量到工厂模式
创建对象,是我们在JavaScript中最频繁的操作之一。你可能已经习惯了使用对象字面量,但JavaScript提供了多种创建方式,每种方式背后都有其适用的场景和细微的差别。
2.1 最直接的方式:对象字面量
这是最常用、最直观的创建方式。直接用大括号{}包裹一组键值对。
const person = { name: ‘李四’, age: 30, greet() { console.log(`你好,我是${this.name}`); } };这种方式简单明了,适合创建单个、结构明确的对象。但它有一个明显的局限性:当你需要创建多个具有相同结构的对象时,重复编写字面量会导致大量冗余代码。这时,我们就需要考虑其他模式。
2.2 构造函数模式:传统的“类”式创建
在ES6的class语法糖出现之前,这是模拟“类”实例化的主要方式。
function Person(name, age) { this.name = name; this.age = age; this.greet = function() { console.log(`你好,我是${this.name}`); }; } const person1 = new Person(‘王五’, 25); const person2 = new Person(‘赵六’, 28);这里有几个关键点需要注意:
new操作符的作用:它做了四件事:① 创建一个新的空对象;② 将这个新对象的[[Prototype]](即__proto__)内部属性指向Person.prototype;③ 将构造函数内部的this绑定到这个新对象;④ 执行构造函数内部的代码(为this添加属性);⑤ 如果构造函数没有显式返回一个对象,则自动返回这个新创建的对象。- 方法重复定义的问题:仔细观察,
greet方法是在构造函数内部定义的。这意味着每创建一个Person实例,都会在内存中创建一个新的greet函数。如果有1000个实例,就有1000个功能完全相同的函数,这显然是一种浪费。优化方法是将函数定义在构造函数的原型上(Person.prototype.greet),这样所有实例可以共享同一个函数。这引出了原型的概念,我们稍后会详细讨论。
2.3 工厂函数模式:封装创建细节
工厂模式不依赖于new操作符和this,它只是一个返回新对象的普通函数。
function createPerson(name, age) { const obj = {}; obj.name = name; obj.age = age; obj.greet = function() { console.log(`你好,我是${obj.name}`); }; return obj; } const person1 = createPerson(‘孙七’, 22);这种模式的优点在于避免了new关键字和复杂的this指向问题(在函数内部,this的指向很灵活,容易出错),创建逻辑被封装起来,更加清晰。但它同样无法解决对象类型识别和函数重复创建的问题(instanceof操作符无法识别工厂函数创建的对象来自哪个“工厂”)。
2.4 Object.create():基于原型的纯净创建
Object.create()方法创建一个新对象,并使用现有的对象作为新创建对象的原型。这是最贴近JavaScript原型继承本质的创建方式。
const personPrototype = { greet() { console.log(`你好,我是${this.name}`); } }; const person1 = Object.create(personPrototype); person1.name = ‘周八’; person1.age = 35; person1.greet(); // 输出:你好,我是周八 console.log(person1.__proto__ === personPrototype); // true console.log(personPrototype.isPrototypeOf(person1)); // true这种方式非常强大和灵活,它直接建立了对象与原型之间的链接,是理解原型链的绝佳示例。Vue 2.x的响应式系统在初始化数据时,就大量使用了Object.create()来建立原型链以隔离数据。
实操心得:在日常业务开发中,对象字面量和ES6 Class(本质是构造函数的语法糖)足以应对99%的场景。但当你需要实现更复杂的对象关联(如纯净的继承)、或者需要精确控制原型时,
Object.create()是你的不二之选。理解这几种方式,能帮助你在阅读框架源码或设计自己的工具库时,明白作者为何做出这样的选择。
3. 深入属性:数据属性与访问器属性
对象的核心是属性。在JavaScript中,属性远不止“键值对”那么简单。ECMAScript规范将属性分为了两种类型:数据属性和访问器属性。这是很多开发者容易忽略,却又至关重要的知识点。
3.1 数据属性:存储值的容器
我们平常定义的大多数属性都是数据属性。它包含一个数据值的位置,可以读取和写入。数据属性有4个描述其行为的特性(也称为“内部特性”或“属性描述符”):
[[Value]]:包含属性的实际值。默认是undefined。[[Writable]]:布尔值,表示属性的值是否可以被修改。默认为true。[[Enumerable]]:布尔值,表示属性是否可以通过for...in循环或Object.keys()枚举。默认为true。[[Configurable]]:布尔值,表示属性是否可以被删除、是否可以修改其特性(除了[[Value]]和将[[Writable]]从true改为false)。默认为true。
当我们用对象字面量或点符号赋值创建一个属性时,这些特性默认都是true。但我们可以使用Object.defineProperty()方法来精确地定义这些特性。
const obj = {}; Object.defineProperty(obj, ‘name‘, { value: ‘张三‘, writable: false, // 不可写 enumerable: true, configurable: false // 不可配置 }); console.log(obj.name); // ‘张三‘ obj.name = ‘李四‘; // 静默失败(严格模式下会报错) console.log(obj.name); // 仍然是‘张三‘ delete obj.name; // false,静默失败(严格模式下报错) console.log(obj.name); // ‘张三‘这个特性有什么用?一个经典的场景是定义常量属性,或者定义一些你希望被框架(如Vue)追踪,但又不希望被普通操作修改的核心属性。
3.2 访问器属性:不包含值,包含getter和setter
访问器属性没有[[Value]]和[[Writable]],取而代之的是[[Get]]和[[Set]]特性(两者都是函数,或者都是undefined)。它同样包含[[Enumerable]]和[[Configurable]]。
const person = { _age: 25, // 约定俗成,下划线开头表示“私有”属性(并非真正私有) get age() { console.log(‘读取age‘); return this._age; }, set age(newVal) { console.log(‘设置age‘); if (newVal > 0 && newVal < 150) { this._age = newVal; } else { console.error(‘年龄无效‘); } } }; console.log(person.age); // 输出‘读取age‘,然后输出 25 person.age = 30; // 输出‘设置age‘ console.log(person.age); // 输出‘读取age‘,然后输出 30 person.age = 200; // 输出‘设置age‘,然后输出‘年龄无效‘访问器属性的强大之处在于,它允许你在读取或设置属性值时执行自定义逻辑,比如数据验证、计算属性、依赖收集(这是Vue响应式系统的核心)和日志记录。在ES6中,我们也可以在类中使用get和set关键字来定义访问器属性。
3.3 查看与批量定义属性描述符
Object.getOwnPropertyDescriptor(obj, propertyName):获取某个对象自身属性(非原型链上的)的描述符。Object.getOwnPropertyDescriptors(obj):获取对象所有自身属性的描述符。这在创建对象的浅拷贝时非常有用,可以完整复制属性的特性。Object.defineProperties(obj, descriptors):批量定义多个属性。
const source = {}; Object.defineProperties(source, { name: { value: ‘源对象‘, writable: false }, id: { value: 1, enumerable: false } }); // 使用 getOwnPropertyDescriptors 进行“真正”的浅拷贝 const clone = Object.create( Object.getPrototypeOf(source), Object.getOwnPropertyDescriptors(source) ); console.log(Object.getOwnPropertyDescriptor(clone, ‘name‘).writable); // false,特性也被复制了注意事项:
[[Configurable]]被设置为false是一个单向操作。一旦设置为false,就无法再将其改回true。同时,该属性也无法被删除。这是一个不可逆的操作,使用时要格外小心。
4. 对象的遍历与检测:如何正确地“认识”一个对象
我们经常需要检查或遍历一个对象的属性。JavaScript提供了多种方式,但它们的行为有显著区别,用错了可能会导致bug或性能问题。
4.1 遍历方法大比拼
假设我们有一个对象和其原型:
const proto = { inheritedProp: ‘来自原型‘ }; const obj = Object.create(proto); obj.ownProp1 = ‘自有属性1‘; obj.ownProp2 = ‘自有属性2‘; // 设置一个不可枚举的自有属性 Object.defineProperty(obj, ‘hiddenProp‘, { value: ‘不可枚举属性‘, enumerable: false });for...in循环:遍历对象自身及原型链上所有可枚举的属性(不包括Symbol属性)。for (let key in obj) { console.log(key); // 输出:ownProp1, ownProp2, inheritedProp // 注意:hiddenProp 不会被枚举,因为它 enumerable: false }注意:
for...in的遍历顺序在ES6之后有规范,但对于普通对象,它不保证顺序(尽管现代引擎通常按添加顺序)。如果需要过滤掉原型链上的属性,必须使用obj.hasOwnProperty(key)进行判断。Object.keys(obj):返回一个数组,包含对象自身(不含原型链)所有可枚举属性的键名。console.log(Object.keys(obj)); // [‘ownProp1‘, ‘ownProp2‘]Object.values(obj)/Object.entries(obj)(ES2017):分别返回对象自身可枚举属性的值数组和键值对数组。console.log(Object.values(obj)); // [‘自有属性1‘, ‘自有属性2‘] console.log(Object.entries(obj)); // [[‘ownProp1‘, ‘自有属性1‘], [‘ownProp2‘, ‘自有属性2‘]]Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身所有属性(无论是否可枚举)的键名,不包括Symbol属性。console.log(Object.getOwnPropertyNames(obj)); // [‘ownProp1‘, ‘ownProp2‘, ‘hiddenProp‘]Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身所有Symbol属性的键名。const sym = Symbol(‘unique‘); obj[sym] = ‘一个Symbol属性‘; console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(unique)]Reflect.ownKeys(obj)(ES6):返回一个数组,包含对象自身所有属性的键名,包括字符串、Symbol,且无论是否可枚举。可以看作是Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()的合集。console.log(Reflect.ownKeys(obj)); // [‘ownProp1‘, ‘ownProp2‘, ‘hiddenProp‘, Symbol(unique)]
4.2 对象关系检测
如何判断一个对象与另一个对象的关系?
instanceof:检测构造函数的prototype属性是否出现在某个实例对象的原型链上。console.log(obj instanceof Object); // true console.log([] instanceof Array); // true console.log([] instanceof Object); // true,因为 Array.prototype 在 Object.prototype 的原型链上注意:
instanceof在多全局对象环境(如iframe)下可能不可靠,因为不同iframe有各自的内置构造函数。Object.prototype.isPrototypeOf():检查一个对象是否存在于另一个对象的原型链上。比instanceof更直接,因为它不依赖于构造函数。console.log(proto.isPrototypeOf(obj)); // true console.log(Object.prototype.isPrototypeOf(obj)); // truein操作符:检查属性是否在对象自身或原型链上。console.log(‘ownProp1‘ in obj); // true console.log(‘inheritedProp‘ in obj); // true console.log(‘toString‘ in obj); // true (来自 Object.prototype)Object.prototype.hasOwnProperty():检查属性是否为对象自身的属性(不查找原型链)。console.log(obj.hasOwnProperty(‘ownProp1‘)); // true console.log(obj.hasOwnProperty(‘inheritedProp‘)); // false一个重要的安全提示:如果一个对象是通过
Object.create(null)创建的纯净对象,它没有原型,因此也没有hasOwnProperty方法。直接调用会报错。安全的做法是使用Object.prototype.hasOwnProperty.call(obj, ‘prop‘)。
实操心得:在大多数需要遍历对象自身属性的场景下,
Object.keys()是你的首选,它清晰、高效,且返回一个数组,方便后续使用map、filter等方法。如果需要包含不可枚举属性,则使用Object.getOwnPropertyNames()。for...in一定要搭配hasOwnProperty使用,否则很容易引入原型链上的意外属性,造成污染。在编写工具函数或库时,使用Reflect.ownKeys()能获得最全面的自身属性列表。
5. 对象不变性:冻结、密封与不可扩展
在某些场景下,我们希望对象创建后就不能被修改,以保证数据的稳定性和安全性。JavaScript提供了三个逐级增强的方法来限制对象的可变性。
5.1 Object.preventExtensions():禁止扩展
调用此方法后,对象将不能再添加新的属性。但可以删除和修改现有的属性。
const obj = { name: ‘张三‘ }; Object.preventExtensions(obj); obj.age = 30; // 静默失败(严格模式下报 TypeError) console.log(obj.age); // undefined obj.name = ‘李四‘; // 成功 console.log(obj.name); // ‘李四‘ delete obj.name; // 成功 console.log(obj.name); // undefined使用Object.isExtensible(obj)可以检测对象是否可扩展。
5.2 Object.seal():密封
在preventExtensions的基础上,seal方法还会将所有现有属性的[[Configurable]]特性设置为false。这意味着不能添加新属性,也不能删除现有属性。但现有可写属性([[Writable]]为true)的值仍然可以修改。
const obj = { name: ‘张三‘ }; Object.seal(obj); obj.age = 30; // 失败,不能添加 delete obj.name; // 失败,不能删除 obj.name = ‘李四‘; // 成功,可以修改值 // 尝试重新配置属性描述符 Object.defineProperty(obj, ‘name‘, { configurable: true }); // TypeError使用Object.isSealed(obj)进行检测。
5.3 Object.freeze():冻结
这是最严格的限制。在seal的基础上,freeze方法还会将所有自身属性的[[Writable]]特性设置为false(如果属性是访问器属性,则其getter/setter不受影响,但也不能被重新定义)。被冻结的对象完全变成一个不可变的常量:不能增、删、改。
const obj = { name: ‘张三‘, info: { age: 25 } }; Object.freeze(obj); obj.name = ‘李四‘; // 静默失败 obj.newProp = ‘test‘; // 失败 delete obj.name; // 失败 // 注意:冻结是“浅冻结” obj.info.age = 30; // 成功!因为冻结只作用于对象本身,其内部的引用类型属性未被冻结 console.log(obj.info.age); // 30使用Object.isFrozen(obj)进行检测。
重要警告:freeze、seal、preventExtensions都是浅操作!它们只影响对象本身的直接属性。如果属性值是一个对象(数组、另一个对象等),这个嵌套对象内部仍然是可变的。要实现真正的“深冻结”,需要递归地处理所有属性。
function deepFreeze(obj) { const propNames = Object.getOwnPropertyNames(obj); for (const name of propNames) { const value = obj[name]; if (value && typeof value === ‘object‘) { deepFreeze(value); } } return Object.freeze(obj); } const obj = { info: { age: 25 } }; deepFreeze(obj); obj.info.age = 30; // 在严格模式下会报错或静默失败注意事项:这些不可变性操作在严格模式下会抛出错误,而在非严格模式下通常是静默失败。在生产环境中,为了代码的健壮性,建议始终使用严格模式(
‘use strict‘)。这些特性常用于定义配置对象、常量枚举或作为不可变状态(如Redux中的state),确保它们在应用生命周期内不会被意外修改。
6. 对象拷贝的深水区:浅拷贝与深拷贝
在JavaScript中,对象是通过引用传递的。简单的赋值const newObj = oldObj只会复制引用,而不是对象本身。因此,拷贝对象是一个常见且容易出错的需求。拷贝主要分为浅拷贝和深拷贝。
6.1 浅拷贝:只拷贝第一层
浅拷贝创建一个新对象,新对象拥有原始对象第一层属性的一个副本。如果属性是基本类型(string, number, boolean, null, undefined, symbol, bigint),则拷贝值;如果属性是引用类型(object, array, function等),则拷贝的是内存地址,新旧对象会共享这个引用。
常见的浅拷贝方法:
扩展运算符
...const source = { a: 1, b: { inner: 2 } }; const shallowCopy = { ...source }; shallowCopy.a = 99; // 不影响 source shallowCopy.b.inner = 100; // 会影响 source.b.inner! console.log(source.b.inner); // 100Object.assign()const shallowCopy = Object.assign({}, source);Object.assign会将源对象(source)的所有可枚举自身属性复制到目标对象(第一个参数)。它也是浅拷贝。数组的
slice()和concat()对于数组,slice()和concat()方法会返回一个新数组,也是浅拷贝。const arr = [1, 2, { name: ‘test‘ }]; const newArr = arr.slice(); newArr[2].name = ‘changed‘; console.log(arr[2].name); // ‘changed‘
6.2 深拷贝:完全独立的副本
深拷贝会递归地拷贝对象的所有层级,创建一个完全独立的新对象,新旧对象之间没有任何引用关联。
实现深拷贝并非易事,需要处理各种边界情况:循环引用、函数、Date、RegExp、Set、Map、Error等特殊对象,以及原型链。
简单但有限的方法:JSON.parse(JSON.stringify(obj))
const obj = { a: 1, b: { c: 2 }, d: new Date(), e: function() {}, f: undefined }; const deepCopy = JSON.parse(JSON.stringify(obj)); console.log(deepCopy); // 输出:{ a: 1, b: { c: 2 }, d: ‘2023-10-27T...‘ } // 注意:函数 e 和 undefined 属性 f 丢失了,Date 对象被转成了字符串。这种方法虽然简单,但缺陷明显:会忽略undefined、函数、Symbol;不能处理循环引用(会报错);会将Date对象转为字符串,将RegExp、Error等转为空对象。它只适用于纯数据对象(JSON-serializable)。
手动实现或使用可靠库对于生产环境,建议使用成熟的库,如Lodash的_.cloneDeep方法。如果不想引入库,可以自己实现一个(注意处理循环引用和特殊对象):
function deepClone(target, map = new WeakMap()) { // 处理基本类型和函数 if (target === null || typeof target !== ‘object‘) { return target; } // 处理循环引用 if (map.has(target)) { return map.get(target); } // 处理特殊对象类型 const constructor = target.constructor; if (/^(Date|RegExp|Map|Set)$/i.test(constructor.name)) { return new constructor(target); } // 处理数组和普通对象 const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); // 使用 Reflect.ownKeys 来获取所有自身键(包括Symbol) for (let key of Reflect.ownKeys(target)) { cloneTarget[key] = deepClone(target[key], map); } return cloneTarget; } // 测试 const original = { a: 1, b: [2, 3], c: { d: 4 }, sym: Symbol(‘test‘) }; original.circle = original; // 循环引用 const cloned = deepClone(original); console.log(cloned !== original); // true console.log(cloned.b !== original.b); // true console.log(cloned.circle === cloned); // true,循环引用被正确保留实操心得:在大多数业务场景中,如果你确定对象是纯数据且没有循环引用,
JSON方法是最快的。但在框架开发、状态管理或处理复杂配置时,必须使用真正的深拷贝。Lodash的_.cloneDeep经过了充分测试,是安全的选择。自己实现深拷贝是一个很好的练习,但要注意边界条件的覆盖,否则极易产生隐蔽的bug。记住一个原则:默认使用浅拷贝,仅在确有必要时使用深拷贝,因为深拷贝的性能开销要大得多。
7. 对象与性能、内存:那些看不见的坑
对象用起来方便,但如果不加注意,也可能成为性能瓶颈和内存泄漏的源头。
7.1 属性访问速度
JavaScript引擎(如V8)会对对象进行优化。对象属性的访问速度通常很快,但动态添加和删除属性会破坏引擎的“隐藏类”优化,导致性能下降。
// 好的做法:一次性初始化所有属性(或大部分属性) function GoodPoint(x, y) { this.x = x; this.y = y; } // 不好的做法:后续动态添加大量属性 function BadPoint(x, y) { this.x = x; this.y = y; } const p = new BadPoint(1, 2); p.z = 3; // 动态添加,可能触发隐藏类转换,影响优化 p.color = ‘red‘; p.label = ‘pointA‘;尽量保持对象的结构稳定。如果需要存储动态的键值对,考虑使用Map,它在键值对频繁增删的场景下性能通常优于普通对象。
7.2 内存泄漏
对象如果被意外地保留在内存中,无法被垃圾回收,就会导致内存泄漏。
常见场景一:意外的全局变量
function leak() { // 忘记写 var/let/const,导致 hugeArray 成了全局变量,永远不会被回收 hugeArray = new Array(1000000).fill(‘*‘); }常见场景二:闭包中持有不必要的引用
function outer() { const bigData = new Array(1000000).fill(‘*‘); return function inner() { // inner函数闭包持有了bigData的引用,即使outer执行完毕,bigData也无法释放 console.log(‘inner called‘); // 如果这里并不需要访问bigData,就造成了泄漏 }; } const holdRef = outer(); // bigData 被 inner 的闭包引用着常见场景三:DOM引用
const elements = {}; function registerElement(id, element) { elements[id] = element; // 在JS中保存了DOM元素的引用 } // 当这个DOM元素从页面被移除时,由于elements对象仍持有引用,该DOM节点无法被GC回收。排查技巧:使用Chrome DevTools的Memory面板,通过拍摄堆快照(Heap Snapshot)对比前后差异,可以找出内存泄漏的对象和保留它们的引用路径。
7.3 对象池模式
对于需要频繁创建和销毁的、构造成本较高的对象(如复杂的DOM元素、数据库连接、特定类的实例),可以使用对象池来复用对象,减少GC压力和提高性能。
class ObjectPool { constructor(createFn) { this.createFn = createFn; this.pool = []; } acquire() { return this.pool.length > 0 ? this.pool.pop() : this.createFn(); } release(obj) { // 重置对象状态到初始值 // ... reset logic ... this.pool.push(obj); } } // 使用 const pool = new ObjectPool(() => ({ data: null, inUse: false })); const obj1 = pool.acquire(); obj1.data = ‘任务1‘; // ... 使用 obj1 ... pool.release(obj1); // 放回池中,而不是丢弃 const obj2 = pool.acquire(); // 可能复用了 obj1注意事项:对象池增加了代码的复杂度,并且需要小心处理对象状态的清理,避免残留数据影响下一次使用。它通常用在性能敏感的底层框架或游戏开发中,对于一般的Web应用,过早优化是万恶之源,应先进行性能分析,确认对象创建/销毁是瓶颈后再考虑使用。
8. 现代JavaScript中的对象新特性
ES6及之后的版本为对象带来了许多非常实用的新语法和API,极大地提升了开发体验。
8.1 属性名简写与计算属性名
const name = ‘张三‘; const age = 30; // 属性名简写 const person = { name, age }; // 等同于 { name: name, age: age } // 方法简写 const obj = { sayHi() { // 等同于 sayHi: function() { ... } console.log(‘Hi‘); } }; // 计算属性名 const propKey = ‘foo‘ + ‘Bar‘; const dynamicObj = { [propKey]: ‘value‘, [‘prefix_‘ + ‘id‘]: 123 }; console.log(dynamicObj.fooBar); // ‘value‘ console.log(dynamicObj.prefix_id); // 123计算属性名使得我们可以动态地定义属性名,这在根据变量创建对象时非常有用。
8.2 Object的新静态方法
Object.is(value1, value2):比===更严格的相等比较。主要区别在于处理NaN和+0/-0。console.log(Object.is(NaN, NaN)); // true (而 NaN === NaN 是 false) console.log(Object.is(+0, -0)); // false (而 +0 === -0 是 true) console.log(Object.is(5, 5)); // trueObject.setPrototypeOf(obj, prototype):设置一个对象的原型。虽然可以做到,但性能极差,因为会破坏引擎优化。在可能的情况下,应优先使用Object.create()在创建时指定原型。// 不推荐 const obj = { a: 1 }; const proto = { b: 2 }; Object.setPrototypeOf(obj, proto); console.log(obj.b); // 2 // 推荐:创建时指定 const betterObj = Object.create(proto); betterObj.a = 1;**
Object.values()/Object.entries():如前所述,用于获取值数组和键值对数组,在处理数据时非常方便,特别是与for...of循环或数组解构结合时。const obj = { a: 1, b: 2, c: 3 }; for (const [key, value] of Object.entries(obj)) { console.log(`${key}: ${value}`); } // 输出:a: 1, b: 2, c: 3Object.fromEntries()(ES2019):与Object.entries()相反,将键值对列表转换为一个对象。这对于将Map结构或查询字符串转换为对象特别有用。const entries = [[‘name‘, ‘张三‘], [‘age‘, 30]]; const obj = Object.fromEntries(entries); console.log(obj); // { name: ‘张三‘, age: 30 } const params = new URLSearchParams(‘foo=bar&baz=qux‘); const paramsObj = Object.fromEntries(params); console.log(paramsObj); // { foo: ‘bar‘, baz: ‘qux‘ }
8.3 可选链?.和空值合并??
这两个操作符(ES2020)极大地简化了处理深层嵌套对象和默认值的代码。
可选链
?.:如果?.前面的部分是null或undefined,表达式会短路并返回undefined,而不会报错。const user = { profile: { address: { city: ‘北京‘ } } }; // 旧写法:冗长且容易出错 const oldCity = user && user.profile && user.profile.address && user.profile.address.city; // 新写法:简洁安全 const newCity = user?.profile?.address?.city; console.log(newCity); // ‘北京‘ const badUser = {}; console.log(badUser?.profile?.address?.city); // undefined (不会报错)空值合并
??:只有当左侧操作数为null或undefined时,才返回右侧操作数。与||不同,||会对所有假值(false,0,‘‘,NaN)生效。const count = 0; const defaultValue = 10; console.log(count || defaultValue); // 10 (因为 0 是假值) console.log(count ?? defaultValue); // 0 (因为 0 不是 null 或 undefined)
个人体会:
?.和??是我近年来最爱的语法糖之一,它们让代码变得异常简洁和健壮。但要注意,过度使用可选链可能会掩盖潜在的数据结构设计问题。如果某个深层属性经常需要可选链访问,也许意味着你的数据结构应该被扁平化或重新设计。同时,Object.fromEntries()在处理表单数据、URL参数转换时非常高效,可以替代很多循环拼接的代码。拥抱这些新特性,能让你的代码更现代、更清晰。