ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

深入理解JavaScript对象:从创建、属性到高级技巧与性能优化

深入理解JavaScript对象:从创建、属性到高级技巧与性能优化 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(‘foobarbazqux‘); 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参数转换时非常高效可以替代很多循环拼接的代码。拥抱这些新特性能让你的代码更现代、更清晰。
返回列表