恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
JS对象本质、高频操作与实战避坑:从键值对到深拷贝
首页
资讯中心
/
JS对象本质、高频操作与实战避坑:从键值对到深拷贝
JS对象本质、高频操作与实战避坑:从键值对到深拷贝
发布时间:2026/10/9 17:24:09
我记得自己刚开始写 JS 的时候“对象”这个概念让我绕了很久。学 HTML 和 CSS 时一个标签、一个类名就能控制样式到了 JS一个变量里居然能塞函数、数组还能点出一串方法。后来真正用起来才明白JS 对象的难点不在于语法多复杂而在于它几乎到处都是数组、函数、事件底层都和对象脱不开关系。这篇内容就是一份基于实际使用经验写成的“JS对象简述”不讲大而全的文档只把对象的核心概念、常见操作、真实场景和踩过的坑一条条掰开。适合已经写过一些简单脚本、但面对“对象的创建”“函数是不是对象”“判断对象为空”“对象数组去重”这类问题还有点含糊的人。1. 先搞明白对象是什么键值对、引用和函数的身份很多人学对象时第一个困惑是对象到底是个什么东西我现在的理解是对象就是一组“键值对”的集合但它又不像一个普通的容器那么省心因为你会和“引用”这个概念撞上。1.1 对象的本质是键值对集合看一个最普通不过的例子const user { name: 张三, age: 25, skills: [JavaScript, CSS], login: false };这里的name、age、skills、login是属性名冒号后面是属性值。属性名在 JS 里会被强制转换成字符串或者使用 Symbol属性值可以是任意类型包括数组、函数甚至又是一个对象。所以你也可以把对象理解成一张“带标签的收纳箱”标签是属性名箱子里装的是属性值。有个细节很容易忽略如果属性名看起来像数字它会被转成字符串。const obj {}; obj[1] a; obj[true] b; console.log(obj[1]); // a console.log(obj[true]); // b很多人写动态取值时踩坑往往就是没意识到这个转换规则。1.2 引用 vs 基本类型最容易卡住新人的分水岭对象的第一个大坑是在“值传递”和“引用传递”上。let a 1; let b a; b 2; console.log(a); // 1基本类型互不影响 const objA { count: 1 }; const objB objA; objB.count 2; console.log(objA.count); // 2两个变量指向同一个对象基本类型存的是“值”你把a赋值给b等于是把数字 1 又复制了一份。对象不一样objB拿到的是objA的地址引用两者指向同一个内存里的对象。所以修改其中一个另一个也会跟着变。这个特性还引出了另一个常见误解以为const声明的对象不能改。实际上const只是保证变量名不能被重新赋值并不保证对象内部的属性不能改。真正要防止修改得用后面提到的浅拷贝、深拷贝或者不可变更新的思路。1.3 函数是对象吗答案比你想的要有意思搜索引擎里“js中函数是对象吗”是高频问题。答案是函数是一种特殊对象通常叫“可调用对象”。function greet() { console.log(hello); } greet.author 小鹿; greet.getAuthor function () { return this.author; }; console.log(greet.author); // 小鹿 console.log(greet.getAuthor()); // 小鹿函数可以挂属性可以被传来传去也可以作为值存进变量这是对象的典型特征。但typeof greet返回的是function不是object因为有特殊标识让 JS 知道它是可调用的。数组也一样typeof []返回object但需要靠Array.isArray才能区分。既然函数是对象它天然也有原型链也可以通过Function.prototype一路连接到Object.prototype。所以理解 JS 面向对象时别把“函数”和“对象”对立起来它们更像“对象家族里专门负责执行的那一类”。1.4 三种常见创建方式字面量、new Object、Object.create抓“对象的创建”这个关键词时我会把三种方式放在一起理解const obj1 {}; // 字面量最常用 const obj2 new Object(); // 作用与字面量类似但实际写代码时没必要 const obj3 Object.create(null); // 创建一个没有原型链的对象 const obj4 Object.create({ x: 1 }); // 指定一个对象作为原型创建方式原型情况适用场景对象字面量{}继承自Object.prototype日常 90% 的普通对象场景new Object()同样继承自Object.prototype极少需要可用字面量替代Object.create(null)没有原型链安全字典、不想被原型属性干扰Object.create(proto)自定义原型对象原型式继承、数据隔离你不需要每次都背这些方式但至少要清楚普通对象默认带着Object.prototype上的一堆方法比如toString、hasOwnProperty。如果哪天你发现某个对象的toString不见了多半是因为它用了Object.create(null)。注意Object.create(null)创建的对象没有hasOwnProperty、toString这些方法它不是普通对象别拿它做需要这些方法的操作。2. 属性增删改查几个容易踩坑的细节对象的语法看着简单但真正写业务代码时“读不到属性”“判断空对象失败”“类型判断不准”这类问题特别多。这一章聊几个最常见的坑。2.1 点号与方括号动态属性名的正确打开方式访问属性可以用点号也可以用方括号但两者并不完全等价。const obj { user-name: xiaoyu, age: 18 }; console.log(obj.age); // 18 console.log(obj[user-name]); // xiaoyu属性名里带有-或者你想用变量的值作为属性名就必须用方括号。const key age; console.log(obj[key]); // 18 const field status; const item { [field]: pending, [${field}At]: Date.now() };方括号里的表达式会被计算后再作为属性名所以它能做很多动态操作。另一个隐含坑是用obj[null]、obj[undefined]时它们会被转成null、undefined这样的字符串属性不小心就会存出奇怪的数据。2.2 delete 到底删掉了什么删除对象属性的操作符是delete。const obj { a: 1, b: 2 }; delete obj.a; console.log(a in obj); // false需要记住三点delete只删除对象自身的属性原型链上的属性删不掉删除不存在的属性也会返回true所以返回值不能用来判断是否存在如果是不可配置的属性在非严格模式下返回false严格模式下可能直接抛错。还需要注意delete通常不能删除用let、const、var声明的变量它主要面向对象属性。很多人一开始以为delete能“删除某个变量”但实际上变量不是对象的可配置属性删了没效果。2.3 判断对象为空别拿obj {}去比“判断对象为空”是另一个搜索高频词也是初学者最容易产生错觉的地方。const obj {}; console.log(obj {}); // false两个{}是两个不同的引用永远不相等。正确的判断对象是否有“自有可枚举属性”最常见的方式是看Object.keysfunction isEmptyObject(value) { if (value null || value undefined) return false; return Object.keys(value).length 0; } console.log(isEmptyObject({})); // true但有两点要注意Object.keys只统计可枚举的自有字符串属性不统计 Symbol 属性如果对象里定义了不可枚举属性Object.keys也会返回空但它并不算空对象。如果只是写业务校验用Object.keys(value).length 0基本够了。想更严谨可以再加上Object.getOwnPropertySymbols的判断不过多数场景用不到。2.4 类型判断typeof 和 instanceof 都有边界判断一个值是不是对象时typeof很容易误导你console.log(typeof null); // object console.log(typeof []); // object console.log(typeof function(){});// functiontypeof null是历史遗留问题数组也被归到object函数是特殊对象。所以实际判断时建议组合使用数据类型推荐判断方式数组Array.isArray(value)普通对象排除null、数组后再配合Object.prototype.toString函数typeof value function日期对象Object.prototype.toString.call(value) [object Date]常用的一个“普通对象”判断范式是function isPlainObject(value) { if (value null || typeof value ! object) return false; if (Array.isArray(value)) return false; return Object.getPrototypeOf(value) Object.prototype || Object.getPrototypeOf(value) null; }不需要每次手写但要理解typeof做粗筛Array.isArray排掉数组再看原型链确认是不是普通对象这套顺序才是稳的。3. 方法、this 和 event对象在真实代码里最常见的三副面孔对象不止用来存数据它还会变成“方法集合”和“事件载体”。前端里你几乎每天都要和this、event打交道这部分值得单独说。3.1 对象方法的本质与 this 绑定对象里的方法本质上就是“值类型为函数”的属性。const user { name: 小鹿, getName() { return this.name; } }; console.log(user.getName()); // 小鹿这里的关键是this。当你以user.getName()的形式调用时this指向user。但一旦把方法单独取出来this就可能丢const fn user.getName; fn(); // 严格模式下 this 是 undefined访问 name 会报错所以面对“方法 this 丢失”的问题时解决办法是保留绑定关系const fn user.getName.bind(user);或者用箭头函数包裹但这个要分场景因为箭头函数有自己的this规则不是所有时候都合适。3.2 浏览器里的 event 对象另一种“对象形态”“js 事件中的 event”也是常见搜索词。事件处理函数会自动接收到一个事件对象里面保存了这次事件的所有信息。document.querySelector(.btn).addEventListener(click, function (event) { console.log(event.type); // click console.log(event.target); // 实际被点击的元素 console.log(event.currentTarget); // 绑定监听器的元素 console.log(event.clientX, event.clientY); // 鼠标坐标 });event不是一个普通的数据对象它根据事件类型不同可能是MouseEvent、KeyboardEvent、InputEvent等。但共同点是你可以像操作普通对象一样读取它的属性甚至可以阻止默认行为event.preventDefault(); // 阻止默认行为 event.stopPropagation(); // 阻止冒泡新手经常搞混target和currentTarget。target是你真正点到的那个 DOM 元素currentTarget是当前监听事件的那个元素。在小程序或复杂列表里这个区别直接影响你取数据的准确性。3.3 箭头函数为什么不适合直接做对象方法箭头函数不是为了替代对象方法设计的。它的特点是没有自己的this会继承定义位置的this。拿它当对象方法很容易出现奇怪的结果。const counter { count: 0, tick: () { this.count; } }; counter.tick(); console.log(counter.count); // 还是 0因为箭头函数定义时this是它所在的上下文而不是counter。如果你想让对象方法里的回调保持对象的this反而适合用箭头函数const counter { count: 0, start() { setInterval(() { this.count; }, 1000); } };这里setInterval的回调如果写成普通函数this会指向全局或 undefined写成箭头函数就能继承start方法的this。所以是不是用箭头函数取决于作用在哪一层。3.4 解构赋值从对象里安全取值对象解构是日常开发里很舒服的语法但也需要注意边界。const options { title: 标题, width: 100 }; const { title, width 50 } options; console.log(title); // 标题 console.log(width); // 100不会被默认值覆盖默认值只在属性值为undefined时生效如果属性值是null默认值不生效const { a 1 } { a: null }; console.log(a); // null解构null或undefined会直接抛错const data null; const { name } data; // TypeError安全做法是在外层先给默认值const { user {} } data; const { name } user;对象解构不是花哨语法它能帮你少写很多obj.xxx obj.xxx.yyy的判断但前提是你要知道它默认值触发的条件。4. 真实场景对象数组去重、三级联动和拷贝对象讲概念容易空落到场景里才有感觉。我挑三个很典型的场景展开说都是项目里反反复复会用到的。4.1 对象数组去重别再用 JSON.stringify 当唯一键数组里放对象去重通常指的是“按照某个字段去重”比如每个对象都有id。const list [ { id: 1, name: 小米 }, { id: 2, name: 小明 }, { id: 1, name: 小米 } ]; const seen new Set(); const unique list.filter((item) { if (seen.has(item.id)) return false; seen.add(item.id); return true; });如果想保留“最后出现的重复项”可以用Mapconst map new Map(); list.forEach((item) map.set(item.id, item)); const unique [...map.values()];有人会把整个对象JSON.stringify后放进Set这个方法慎用。因为{ name: x, age: 1 }和{ age: 1, name: x }的属性顺序不一样序列化结果也不同但业务上它们可能完全一样另外遇到循环引用的对象JSON.stringify会直接抛错。4.2 用映射对象实现三级联动比一长串 if/else 优雅多了“js三级联动”这种需求最容易把代码写成一堆if (province 浙江) { city [...] }。数据少还好数据一多就非常难维护。更适合的做法是把数据关系放进对象里形成映射const region { zhejiang: { label: 浙江, cities: [ { code: hangzhou, label: 杭州, districts: [西湖区, 滨江区, 余杭区] }, { code: ningbo, label: 宁波, districts: [海曙区, 鄞州区, 江北区] } ] }, beijing: { label: 北京, cities: [ { code: chaoyang, label: 朝阳区, districts: [望京, 亚运村, 三里屯] } ] } };选省的时候用 province code 去取城市列表选城市的时候再取区县列表const cities region?.[provinceCode]?.cities ?? []; const districts cities.find(item item.code cityCode)?.districts ?? [];这种写法的好处是数据和逻辑分离。以后加一个省、加一个城市不需要改逻辑只改对象结构即可。用?.和??兜底即使拿不到数据也不会直接爆掉。4.3 深浅拷贝从赋值到 structuredClone 的坑前面说过直接const b obj只是复制了引用。想要一份“独立数据”至少要浅拷贝const original { name: x, nested: { count: 1 } }; const shallow { ...original }; shallow.name y; // 不影响 original shallow.nested.count 2; // 会影响 original浅拷贝只复制了第一层。nested仍然指向同一个对象。真正要完全独立需要用深拷贝。现代浏览器里可以直接用structuredCloneconst deep structuredClone(original); deep.nested.count 2; console.log(original.nested.count); // 1没有structuredClone的环境最常用的替代方案是JSON.parse(JSON.stringify(obj))但它的限制很明显函数会被丢掉undefined属性会被丢掉Symbol键会被忽略Date会变成字符串Map、Set会变成空对象循环引用会抛错。所以别把它当成万能深拷贝。普通 JSON 数据用它还省事复杂数据就老老实实用structuredClone或者自己写一个递归拷贝函数。4.4 前端状态更新时为什么要返回新对象现在前端框架基本都依赖“引用变化”来触发更新。如果你直接修改对象内部属性框架可能不会认为状态发生了变化。const state { items: [], filter: all }; // 不推荐直接改数组 state.items.push(第一条); // 推荐返回一个全新的 state const nextState { ...state, items: [...state.items, 第一条] };这个思路在原生 JS 里可能觉得多此一举但一旦接触 React、Vue 的状态管理就会发现“不可变更新”是对象操作的必修课。核心原则是不修改原来的对象而是基于它创建一个新对象。5. 复盘一下对象相关的高频坑以及我养成的几个习惯下面这些内容不是标准语法里的“必考点”但它们是实际开发中反复出现的坑。我踩过之后慢慢形成了一些习惯。5.1 别依赖 Object.keys 的返回顺序对象属性的枚举顺序有规则但不是所有属性都按“插入顺序”来。数字型的键会从小到大排字符串型的键才按插入顺序。const obj { b: 1, 2: 2, a: 3, 1: 4 }; console.log(Object.keys(obj)); // [1, 2, b, a]如果你真的在意顺序应该用Map而不是普通对象。Map的插入顺序是明确可靠的也更适合做缓存、计数的场景。5.2 别扩展 Object.prototype也别在 for...in 里忘掉 hasOwnProperty有人喜欢给Object.prototype挂一个“全局方法”看起来方便但隐患很大。Object.prototype.extra polluted; const obj { a: 1 }; for (const key in obj) { console.log(key); // a, extra }扩展Object.prototype会让所有对象都继承到新属性for...in还会把它遍历出来。更严重的是如果恶意代码往原型链上挂数据可能会造成原型污染攻击。所以我的习惯是不随便扩展Object.prototype如果一定要用for...in配合Object.hasOwn或hasOwnProperty判断做纯数据字典时优先Map或Object.create(null)。5.3 判断属性归属用 Object.hasOwn比“点出方法”更稳对象自带hasOwnProperty但这个方法也有翻车的时候。如果对象恰好也定义了一个叫hasOwnProperty的属性或者对象是用Object.create(null)创建的点击调用就不奏效了。const obj { hasOwnProperty: 1, value: 2 }; console.log(obj.hasOwnProperty(value)); // TypeError console.log(Object.hasOwn(obj, value)); // trueES2022 提供了Object.hasOwn直接以静态方法调用不依赖对象的原型链更安全。老环境没有这个方法时可以用Object.prototype.hasOwnProperty.call(obj, value)在写对象相关工具函数时我默认都用这种写法避免各种意外。5.4 把“对象操作”练成肌肉记忆理论说再多不如动手练。如果让我给初学者推荐几个对象练习我会选这三类第一把对象数组按某个字段分组const list [ { type: 前端, name: A }, { type: 后端, name: B }, { type: 前端, name: C } ]; const groups list.reduce((acc, item) { acc[item.type] ?? []; acc[item.type].push(item); return acc; }, {}); console.log(groups); // { 前端: [...], 后端: [...] }第二写一个安全的深层取值函数避免像user.info.address.city这种链式取值报错function getValue(obj, propPath, fallback) { const keys propPath.split(.); let current obj; for (const key of keys) { if (current null) return fallback; current current[key]; } return current undefined ? fallback : current; } const data { user: { address: { city: 杭州 } } }; console.log(getValue(data, user.address.city, 未知)); // 杭州 console.log(getValue(data, user.address.road, 未知)); // 未知第三把第 4.2 节的三级联动自己手写一遍不要复制代码。数据结构和渲染逻辑都由你决定跑通后你基本就掌握了“用对象组织关系”的思考方式。写对象相关代码时我最后还有一个习惯复杂对象先画结构图再写代码。哪怕只是在脑子里画也会比直接堆代码少出很多逻辑漏洞。你能说清楚这个对象有哪些字段、哪些字段是嵌套的、哪些字段允许为空就说明你真正理解了当前的数据。