多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

浏览器复制对象属性及值:控制台、剪贴板与序列化完整指南

浏览器复制对象属性及值:控制台、剪贴板与序列化完整指南 如果你在浏览器里要“copy复制浏览器中的对象属性及值”我猜你大概率遇到过这样的场景打开F12控制台打印了一个对象结构层次清清楚楚可你想把它原样带走时却发现右键菜单里那个“Copy”只能复制当前展开的节点控制台里直接拖选又容易漏掉嵌套层或者你在做后台管理系统用户点了“复制订单数据”结果粘贴出来是一串[object Object]。这个需求看起来只是“复制”两个字实际涉及JavaScript对象引用、属性枚举、剪贴板API、序列化差异等一系列问题绝不是简单按一下CtrlC就能搞定的。这篇文章我会从浏览器环境里的真实操作出发把“复制对象属性及值”这件事拆细先讲清楚对象在浏览器里是怎么被“复制”的再给你几套能直接用的方案最后把我在Chrome、Edge、Firefox、Safari上踩过的坑整理成速查表。适合前端开发、测试、运维、数据分析师也适合那些经常要倒腾接口返回数据的同学。1. 这个需求到底藏在哪里先想清楚你要复制什么1.1 浏览器里复制对象最常撞见的三个场景先说第一类场景调试时复制。打开DevTools在Network面板里看到一个接口返回了JSON或者在控制台里输出了某个全局变量你想把它完整复制出来丢给后端、写进文档、或者贴到Postman里比对。这种场景下对象的来源是“浏览器运行时已经存在的JS对象”而我们能拿到手的往往是控制台给我们的字符串展示。第二类场景是业务功能里的“复制到剪贴板”。管理后台里要复制一条订单信息、复制一段配置JSON、复制某个表格行的属性集合。这类场景前端要自己写复制逻辑也就是把JS对象序列化之后写入剪贴板。问题在于很多同学直接用了JSON.stringify(obj)遇到undefined、函数、Symbol或者循环引用就会出各种诡异结果。第三类场景是自动化脚本或浏览器扩展。扩展脚本需要在页面上读取对象、处理属性、再写入剪贴板或者通过chrome.scripting注入代码来复制页面数据。这类场景兼顾了前面两类的难点而且还要考虑扩展的权限模型。这三类场景背后其实都是同一个核心动作把“浏览器内存里的对象”变成“一段可以跨环境使用的文本”。搞清楚这一点你就知道为什么这篇文章不会只讲navigator.clipboard.writeText一个API了。1.2 引用类型和值类型为什么“复制”在JS里不是CtrlC很多新手在这里翻车是因为没区分“复制变量”和“复制对象的内容”。在JavaScript里基本类型字符串、数字、布尔值是值传递你复制一个字符串就是把值本身重新存了一份但对象是引用类型变量里存的其实是对象在堆内存里的地址。const a { name: order-1001, total: 299 }; const b a; // 复制的是引用不是对象内容 b.total 0; console.log(a.total); // 0a也被改了这个例子说明如果目标只是“把对象的属性及值提取出来”不能直接赋值必须走“读取属性 - 组织文本 - 写入新载体”的路径。而读取属性又涉及一个问题一个对象能拿到的属性不只是它自己的还有原型链上的继承属性同时也有不可枚举属性、Symbol属性以及访问器属性getter。如果你只在意“属性及值”通常指的是“自有可枚举属性”但有时候你连不可枚举的都想带出去这就得用不同的API。另一个让新手懵的点是浏览器控制台打印对象时打印的是“对象的实时引用”的快照或展开状态而复制时你要的是“此刻对象的静态文本”。这两者之间的转换本质是一次序列化操作。序列化的规则不同复制出来的结果就不一样。1.3 动手之前先分类你要复制的对象是哪种我的经验是动手写复制逻辑之前先给对象分类不同类别用不同方案扁平对象属性都是基本类型比如{ a: 1, b: x }。这种最简单用JSON.stringify基本不会出问题。嵌套对象属性还有对象、数组比如接口返回的树形结构。这类要留意序列化深度以及循环引用。带特殊类型的对象包含 Date、Map、Set、RegExp、BigInt、函数、Symbol 等。JSON.stringify会把这些自动“降级”比如函数直接丢掉Symbol变成undefinedDate变成字符串Map变成空对象。超大对象属性多、嵌套深、数组大。复制时容易卡顿甚至因为JSON.stringify递归太深而抛错。要保留格式的对象复制出来之后要给人读、要格式化对齐或者要能直接作为代码字面量使用。分类清楚之后你才能选择对应方案是直接copy()还是写一个通用深拷贝序列化器还是用第三方库。下一节我会把涉及的底层原理讲清楚这样你后面看代码时就不会觉得那些分支是多余的。2. 核心细节拆解属性如何被“拿”出来又如何被“放”进剪贴板2.1 枚举对象属性的四种姿势差别比你想的大“复制属性及值”第一步就是把属性枚举出来。JavaScript里枚举属性至少有四种常用方式它们拿到的属性集合不一样用错了就会丢数据。for...in遍历对象自身的和原型链上的所有可枚举属性。最常见但容易带上你根本不想要的继承属性比如继承自Object.prototype的某些东西。实际用的时候还要加hasOwnProperty判断。Object.keys(obj)只返回对象自身的可枚举属性不含继承属性不含Symbol不含不可枚举属性。日常处理业务对象这个最稳。Object.getOwnPropertyNames(obj)返回对象自身的所有属性包括不可枚举的属性比如数组的length、自定义对象的某些隐藏属性。但不含Symbol。Reflect.ownKeys(obj)返回对象自身的所有属性键包括不可枚举属性和Symbol。这是最“全”的枚举方式。举个例子const symbolKey Symbol(sku); const obj { name: iPhone }; Object.defineProperty(obj, hidden, { value: 1, enumerable: false }); obj[symbolKey] YP001; console.log(Object.keys(obj)); // [name] console.log(Object.getOwnPropertyNames(obj)); // [name, hidden] console.log(Reflect.ownKeys(obj)); // [name, hidden, Symbol(sku)]如果你开发的是一个“通用复制函数”我建议默认走Reflect.ownKeys拿到最全的属性集合再由调用方决定要不要过滤。如果你只是复制接口返回的JSON对象那Object.keys就够了。另一个容易忽略的点是数组用Object.keys处理数组拿到的下标字符串是“不全”的length这种不可枚举属性不会出现所以数组通常直接用Array.prototype.slice或展开运算符处理不走对象那一套。枚举属性之后你还得处理属性值本身。取值时要注意属性可能是 getterobj[key]会触发函数调用并可能抛错属性值也可能是另一个对象递归进去又是一层枚举。写通用复制函数的时候这些边界都要兜住。2.2 浅拷贝、深拷贝、结构化克隆边界在哪里很多人把“复制对象属性及值”理解成“复制对象”于是直接找浅拷贝或深拷贝方案。这两个概念要分清浅拷贝只拷贝第一层嵌套对象仍然共享引用深拷贝递归拷贝所有层级。// 浅拷贝 const copy1 { ...original }; const copy2 Object.assign({}, original); // 深拷贝常见实现 const copy3 JSON.parse(JSON.stringify(original));在复制到剪贴板这个场景里我们其实做的是“序列化”也就是深拷贝的一种“导出形式”。JSON.stringify是最常见的手段但它的边界很明确function类型的属性会被丢弃undefined单独作为属性值时该属性会被丢弃在数组里则变成nullSymbol键和值都会被丢掉Date会变成 ISO 字符串Map、Set会变成空对象BigInt会直接抛TypeError: Do not know how to serialize a BigInt循环引用会抛Converting circular structure to JSON。所以如果你要复制的是“普通接口JSON对象”JSON.stringify是最快的如果你要复制的是“复杂内存对象”就得写递归方案或者用structuredClone。structuredClone是浏览器原生提供的深拷贝API支持循环引用、Date、Map、Set、RegExp、Blob、ArrayBuffer等类型但它拷贝出来的还是“对象”不是文本所以不能直接写入剪贴板得先JSON.stringify(structuredClone(obj))或者structuredClone(obj)后再拼接格式化文本。要注意的是structuredClone不能拷贝函数和Symbol遇到这些依然会抛错而且它在老版本浏览器里不可用。写复制的递归函数时我的做法是先判断类型分几种分支处理。基本类型直接返回数组则遍历每个元素递归普通对象用Reflect.ownKeys枚举并递归处理每个值Date和RegExp这类有专用构造器的类型直接构造新实例Map和Set用循环插入遇到循环引用就用WeakMap记录已经处理过的对象否则会无限递归。2.3 剪贴板那层“玻璃纸”writeText、execCommand 与权限对象属性及值最终要写进剪贴板浏览器里常见的写法有三种navigator.clipboard.writeText、document.execCommand(copy)、还有通过ClipboardEvent来自定义复制内容。先说navigator.clipboard.writeText这是现代推荐方式返回Promise用法简单await navigator.clipboard.writeText(text);但它有两个硬性前提一个是页面必须在安全上下文HTTPS或localhost里才能用普通HTTP页面拿不到navigator.clipboard另一个是它要求页面处于用户手势的上下文中并且浏览器可能会弹出权限提示比如Firefox和Safari会询问“是否允许粘贴”之类的权限。很多公司内网环境是HTTP就会出现“本地好好的部署到内网就不行了”的怪现场。再说回document.execCommand(copy)虽然官方已经标记废弃但兼容性反而很广。它的思路是先把文本放进一个隐藏的textarea或input选中它然后执行document.execCommand(copy)。function execCopy(text) { const textarea document.createElement(textarea); textarea.value text; textarea.style.position fixed; textarea.style.opacity 0; document.body.appendChild(textarea); textarea.select(); try { document.execCommand(copy); } finally { textarea.remove(); } }这种方案在旧的Safari上最实用但要注意它同样通常需要用户手势触发而且在部分浏览器里如果页面没有获得焦点复制会静默失败。最后一种ClipboardEvent的方式主要是给浏览器扩展或富文本编辑器用的比如在copy事件里拦截默认行为用event.clipboardData.setData(text/plain, text)自定义复制内容。这个方式最灵活可以在用户按下CtrlC时把对象格式化后的文本放入剪贴板比较适合扩展脚本。写到这里可以看到真正的难点其实不是“复制”这个动作而是“获取属性”和“序列化”。剪贴板API只是最后一公里很多人前面的数据整理没做好后面用再高级的API也是白搭。3. 实操四种可复制的“复制对象”方案3.1 控制台三秒复制法copy() 与 Store as global variable如果你只是在调试时想把某个接口返回或内存对象复制出来根本不需要写代码浏览器控制台就自带复制能力。Chrome、Edge、Firefox的DevTools控制台都提供了copy()全局函数它会把传入的参数序列化成字符串然后写入剪贴板。最常见的组合是copy(JSON.stringify(responseData, null, 2))这样复制出来的是格式化后的JSON文本粘到编辑器里立即可读。如果你直接copy(responseData)控制台会按它自己的序列化规则处理通常效果也OK但有些浏览器对深层嵌套对象的支持不如JSON.stringify稳定。另一个技巧是在控制台里打印对象后右键打印结果选择“Store as global variable”控制台会生成temp1这样的临时变量然后你再copy(JSON.stringify(temp1))这样就能绕开那些你复制不过来的深层节点。copy()虽然方便但它的陷阱在于它使用的序列化规则在某些浏览器里不完全等同于JSON.stringify遇到undefined、函数、Symbol时表现会不一致。我的建议是调试复制一律显式JSON.stringify少依赖浏览器默认的copy逻辑。另外在Safari的开发者控制台里copy()方法不是所有版本都有保险做法是右键选择“Copy Value”或自己贴一段代码执行。3.2 通用复制函数 safeCopy兼容循环引用、函数、Symbol、BigInt如果你需要在业务页面里复制“复杂对象”我给一个我一直在用的通用序列化函数把它和剪贴板写入封装在一起。设计目标有三点第一复制出来的文本是合法的JSON或代码可读结构第二兼容循环引用第三遇到函数、Symbol、BigInt、Date、Map、Set时不会抛错而是用合理方式降级。function safeStringify(obj) { const seen new WeakSet(); return JSON.stringify(obj, (key, value) { if (typeof value object value ! null) { if (seen.has(value)) { return [Circular]; } seen.add(value); } if (typeof value bigint) { return value.toString() n; } if (value instanceof Map) { return { $type: Map, value: Array.from(value.entries()) }; } if (value instanceof Set) { return { $type: Set, value: Array.from(value) }; } if (typeof value function) { return [Function: ${value.name || anonymous}]; } if (typeof value symbol) { return value.toString(); } if (value instanceof RegExp) { return value.toString(); } return value; }, 2); } async function safeCopy(obj) { const text safeStringify(obj); if (navigator.clipboard window.isSecureContext) { await navigator.clipboard.writeText(text); } else { execCopy(text); } }用起来就是一行await safeCopy({ a: 1, b: [2, 3], fn: () {}, big: 10n });这个函数里的WeakSet就是用来解决循环引用的。JSON.stringify的replacer参数会在递归过程中逐层收到每个键值对我在这里做两件事一是用WeakSet记录已经出现过的对象引用第二次遇到就替换成[Circular]二是把函数、Symbol、BigInt、Map、Set这些特殊类型转成可读的标记字符串。这样复制出来的东西不会让JSON.stringify直接抛错粘出来之后你也能一眼看出哪个属性是函数、哪个属性是循环引用。不过要提醒一句safeStringify输出的并不是严格意义上的JSON因为它包含了[Function: xxx]、[Circular]这样的非标准片段。如果你的下游程序要求粘贴的内容必须能被JSON.parse解析那这个方案就不合适你需要在调用前先过滤掉这些属性或者返回一个“干净版”。这也是为什么我做了safeCopy和纯JSON复制两条路径的原因。3.3 按需提取只复制指定属性过滤空值很多时候你并不需要整个对象只需要其中几个字段。比如接口返回了20个字段你只想复制id、title、updatedAt或者你在做数据导出想把空字符串、null、undefined全部过滤掉。这时候写一个小函数按需提取比先序列化整个对象再手动删字段高效得多。function pickAndCopy(obj, keys, options {}) { const { dropEmpty true, pretty true } options; const result {}; keys.forEach((key) { if (!(key in obj)) return; const value obj[key]; if (dropEmpty) { if (value undefined || value null) return; if (typeof value string value.trim() ) return; if (Array.isArray(value) value.length 0) return; } result[key] value; }); return pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result); } // 使用 const apiResult { id: 101, title: 测试, desc: null, list: [], createdAt: 2025-01-01 }; copy(pickAndCopy(apiResult, [id, title, list])); // 输出: {id:101,title:测试}这个函数的实用场景很多运营后台里复制一行的核心字段、用户详情里复制个人敏感信息的白名单、或者你在调试时想快速只提取某个嵌套节点。dropEmpty参数注意控制因为有时候你恰恰需要保留null比如要给后端提交完整的字段模板那就把dropEmpty设为false。3.4 大对象、频繁复制时的性能注意点我遇到过一次生产事故某后台的“复制订单信息”按钮用户一按页面直接卡死好几秒原因就是订单对象里有几十个字段其中某个字段是一个巨大的数组数组里每个元素还有自己的嵌套对象再叠加React的渲染和剪贴板写入直接把主线程拖垮了。如果你要复制的对象很大注意三点。第一点序列化不要在主线程里容忍过深递归。如果对象层级特别深JSON.stringify本身就会比较慢更别提前面那种WeakSet 递归的组合。对大数据量场景优先选择“只提取展示层需要的字段”也就是先pick再序列化哪怕提取后仍然有5万个字段也比重写10万个字段快很多。第二点不要频繁触发复制。如果你是给列表每一行都加了“复制”按钮用户连续点击时会反复做序列化和剪贴板写入。建议做一次性序列化缓存比如把行数据格式化后的文本缓存到组件state或者Map里点击时直接读缓存。第三点超过一定大小的文本剪贴板可能写入失败。navigator.clipboard.writeText在文本特别长时某些浏览器会报NotAllowedError或DataError。我的兜底策略是先走navigator.clipboard.writeText捕获异常后回退execCopy如果execCopy也失败就弹出一个textarea让用户手动CtrlC至少不丢数据。4. 常见问题与排查技巧实录4.1 问题速查表我整理了一张表基本覆盖了浏览器复制对象时的高频问题。先看这个再往下看具体展开。现象直接原因解决办法复制出来是[object Object]直接把对象传给了writeText它默认调用了toString()必须先用JSON.stringify得到字符串navigator.clipboard是undefined页面不是安全上下文HTTP改用execCommand(copy)或部署到HTTPSJSON.stringify抛循环引用错误对象存在父子互相引用用WeakSet记录已处理对象替换为[Circular]函数、Symbol、undefined 被丢掉JSON.stringify的默认行为自己写replacer函数降级处理粘贴到Excel里全部挤在一格复制的是JSON字符串不是表格数据生成CSV格式或用\t分隔再复制复制大对象页面卡死主线程做了大量序列化先提取字段再做缓存必要时放到setTimeout里执行点击按钮后复制没有反应剪贴板API需要用户手势、页面焦点在click事件处理函数里同步调用复制逻辑Safari里复制失败Safari对剪贴板API支持不统一全部回退execCommand必要时弹textarea4.2 三个最容易踩的坑第一个坑是“把对象直接传给剪贴板API”。这是新手最容易犯的原因很好理解看到writeText就觉得传什么都能写进去。实际上剪贴板API只接受字符串对象传进去会被隐式转成[object Object]。这个问题在调试复制时也常见控制台右键「Copy」复制的是浏览器渲染后的展示值一旦它包含循环引用或者复杂类型复制出来根本没法用。第二个坑是“循环引用没有提前处理”。业务数据里出现循环引用并不罕见比如前端通过Object.defineProperty挂了一个带引用的辅助字段或者把DOM节点、React实例存进了state里。一旦你JSON.stringify直接报错。这时候需要到DevTools里先查是谁导致的循环。我的排查技巧是在控制台执行JSON.stringify(obj, (k, v) { console.log(k); return v; })看它卡在哪个键上通过观察输出的键名基本能定位到造成循环引用的属性。第三个坑是“用户手势丢失”。现代浏览器的剪贴板写入都很看重“由用户交互触发”。如果你在一个按钮的click回调里先发了一个网络请求然后在网络请求回来之后再调用navigator.clipboard.writeText用户手势上下文往往已经失效复制会静默失败。解决办法是请求前先调一次navigator.clipboard.writeText()预热权限或者把复制逻辑留在click回调里同步执行想要复制的内容提前准备好。另外如果页面在iframe里需要确认iframe有allowclipboard-write权限否则也会被上层页面策略挡住。5. 工具选型与再进一步从“复制对象”到“导出数据”5.1 原生方案 vs 第三方库聊完手写实现再聊聊什么时候该用第三方库。浏览器原生的navigator.clipboard只解决了“写入剪贴板”这一步前面提到的序列化、深拷贝、循环引用它一概不管。所以你看到很多人项目里还是会引入这些工具库。clipboard.js专注于复制动作解决浏览器兼容性和用户手势问题但它操作的是已经成型的文本不帮你做对象序列化。lodash.cloneDeep解决深拷贝问题处理大部分基础类型和数组对象但遇到Map、Set、循环引用依然不如structuredClone全面。json-stringify-safe专门解决JSON.stringify循环引用报错的问题适合“只要JSON能用的结果”的场景。fast-copy性能较好的深拷贝库适合大数据量场景。我的建议是如果只是页面里复制一段JSON字符串完全没必要引入库写好safeCopy就够了如果项目里大量用到复杂对象的深拷贝再考虑lodash或structuredClone。第三方库能省事但也会增加依赖体积这个取舍要自己拿捏。5.2 浏览器兼容性一览表下面这张表是我实测过的兼容情况不代表所有版本但基本稳定。能力Chrome/EdgeFirefoxSafari备注navigator.clipboard.writeText支持HTTPS下稳定支持权限询问较严格部分版本支持需用户手势非HTTPS下不可用document.execCommand(copy)支持已废弃但可用支持支持兼容性最好的回退方案copy()控制台函数支持支持部分版本不支持调试用勿依赖structuredCloneChrome 98Firefox 94Safari 15.4比JSON更能处理循环引用如果你要兼容到老旧浏览器最稳妥的做法是优先clipboard失败回退execCommand再失败弹文本框。我在实际项目里基本是把这个回退逻辑封装成一个小函数全项目共用省得每个页面各写一套。5.3 扩展思路复制CSV、JSON下载、多行批量复制最后聊几个进阶玩法。复制对象属性及值不一定非得复制成JSON文本很多场景下你更需要的是“复制成其他格式”。比如运营想要把对象数组粘到Excel里最合适的是生成CSV或者TSV格式用制表符\t分隔点复制之后直接粘贴Excel会自动按列拆分。function toTSV(arrayOfObjects, keys) { const header keys.join(\t); const rows arrayOfObjects.map((obj) keys.map((key) { const val obj[key]; if (val null || val undefined) return ; if (typeof val object) return JSON.stringify(val); return String(val); }).join(\t) ); return [header, ...rows].join(\n); } copy(toTSV(userList, [name, age, address]));再比如你复制的是一个“配置模板”那可以直接帮用户生成一个.json文件下载比复制到剪贴板更利于保存和分享function downloadJson(obj, filename) { const blob new Blob([JSON.stringify(obj, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; a.click(); URL.revokeObjectURL(url); }还有一种是批量复制比如用户在表格里勾选了100行点一个“复制选中数据”。这时候不要对每一行单独调用剪贴板API而是先把100行数据整体拼成一个TSV或JSON数组文本一次性写入剪贴板。既能减少多次写入失败的几率也能让用户拿到一个完整的数据快照。实现逻辑就是把前面的toTSV函数应用到选中行数组然后调用safeCopy。我个人在实际操作中的体会是复制对象这个功能写起来不难难的是把“边界情况”想清楚。我踩过几次坑之后给自己定了一条规矩凡是写复制函数必须先列出这个对象里可能出现哪些特殊类型再动手写序列化逻辑不要想当然认为接口返回的都只是普通JSON。最后再分享一个小技巧复制前先在控制台执行console.table(obj)看一眼二维结构能帮你快速判断哪些属性是嵌套对象哪些是空值这样复制出来的东西更符合你的预期。记住浏览器里的“复制”永远不是复制对象本身而是复制对象的“可读投影”你用什么规则投影最终粘贴出来的就是什么。
返回列表