尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript对象完全指南:从引用类型到原型链、深拷贝与调试

JavaScript对象完全指南:从引用类型到原型链、深拷贝与调试 前阵子整理 HoRain 云的技术分享内容正好把 JavaScript 对象这条主线从头到尾梳理了一遍。聊到对象这个词很多刚入门的朋友第一反应是“我好像知道”但真到用的时候又总是出问题属性取不到、this 指向对不上、深拷贝把函数丢了、对象数组去重去不干净……这些场景我这些年基本全踩过。这篇文章就按我自己的学习路径写从对象是什么、为什么叫引用类型讲到创建方式、读写遍历、深浅拷贝、解构合并、原型链、class 继承再到 JSON 序列化与调试排错尽量做到层层递进。写前端十多年我最大的体会是JavaScript 里的函数、数组、正则、DOM 节点、事件对象本质上都是对象。把对象吃透了就等于拿到了搞懂 JavaScript 的一把钥匙。这篇文章适合刚入门的读者通读也适合有一定经验的同学查漏补缺。1. 对象到底是什么从抽屉模型到引用类型1.1 对象就是一排带标签的抽屉先把键值对看明白对象其实一点都不神秘。你可以把它想象成一组贴了标签的抽屉柜每个抽屉外面贴一个名字抽屉里放一个东西。抽屉上的名字叫“键”抽屉里的东西叫“值”。一个对象就是一堆“键值对”的集合只是 JavaScript 里把这些抽屉统一收在一个花括号里。const user { name: HoRain, age: 18, isAdmin: true, profile: { city: Shanghai }, greet: function () { console.log(hello); } };user 对象里同时有字符串、数字、布尔值、嵌套对象和函数值可以是任意类型。对象的主要作用是把一组有业务关联的数据打包在一起这样无论是声明变量、传递参数还是接收接口数据都能保持结构清晰。这里有两个小细节值得记一下。第一键名最终会被转换成字符串Symbol 除外哪怕你写{ 1: a }真正的键也是字符串1第二键名如果是合法的标识符可以省略引号但像user-name这种带横线的键就必须加引号后面访问时也得用方括号。1.2 值传递和引用传递两把钥匙开同一个保险箱这是对象和普通类型最本质的区别也是新手最容易掉进去的坑之一。字符串、数字、布尔这些原始类型赋值时拷贝的是值本身对象一旦被赋值给变量变量里保存的其实是指向内存中某个位置的“引用”。你可以理解为两把钥匙都能打开同一个保险箱不管用哪把钥匙往里面放东西另一把钥匙再打开时里面的东西都会变。let a { n: 1 }; let b a; b.n 2; console.log(a.n); // 2 let x 1; let y x; y 2; console.log(x); // 1值类型像把一段话抄到两张纸上改第二张纸完全不影响第一张。引用类型则像多把钥匙开同一个保险箱任意一把钥匙改变了箱内物品其他钥匙再打开时看到的也是新状态。这个特性在函数传参时尤其明显。很多人写代码时遇到“我在函数里改了参数外面的变量怎么也跟着变了”的疑问本质就是这个原因函数拿到的是同一个对象的引用修改属性自然会影响原来的对象。但如果你在函数里给参数重新赋了一个新对象那只是把局部变量指向了另一个地方外部对象不会因此改变。function change(obj) { obj.name changed; } function reassign(obj) { obj { name: new object }; } const user { name: origin }; change(user); console.log(user.name); // changed reassign(user); console.log(user.name); // 还是 changed因为重新赋值没有影响原对象把这个区别记牢后面看浅拷贝、深拷贝、对象比较、React 状态更新这些概念时都会顺很多。1.3 为什么框架和业务都离不开对象对象不是教科书里的抽象概念它是前端开发里的基本单位。Vue 的 data 是对象React 的 state、props 是对象组件配置是对象axios 请求参数是对象后端返回的接口数据更是一层层嵌套对象。你在页面上看到的几乎所有动态数据本质上都跑不出“对象”这个容器。很多框架的源码也建立在对象操作之上。比如 Vue 2 的响应式核心是遍历对象的每个属性做拦截Vue 3 换用了 Proxy其实就是在对象外层包了一个拦截器。所谓“注入配置对象中的部分内容会被提取到框架实例中”说的也正是框架读取、合并用户传入的配置对象。不懂对象这些内容只能靠死记懂了对象你再看框架文档很多“魔法”都能看穿是普通的属性读取和赋值。2. 对象的创建方式五种写法与最终选型建议2.1 对象字面量日常开发九成场景都够用创建一个对象最直接的方式就是字面量const config { url: /api/user, timeout: 5000, retry: 2 };可能有人会问为什么不用new Object()其实new Object()很少出现在实际项目中原因有两个。一是字面量写法更简洁代码可读性更高二是 V8 引擎在解析相同形状的对象时能做更好的优化最常见、最直观的写法往往也是性能最好的写法。我自己的习惯是单纯想装一组数据、写一份配置、传一个参数一律用字面量不用任何附加写法。实际上Object构造函数还存在一些极少用到的包装用法比如Object(1)会返回一个 Number 包装对象。这类写法容易让代码变得难以理解日常开发中基本可以无视。把字面量用熟就够应付绝大多数场景了。2.2 工厂函数与构造函数批量生产对象的经典方案如果业务里需要批量创建多个结构相同的对象直接复制粘贴字面量不是不行但维护成本很高。这时候可以考虑工厂函数或构造函数。工厂函数就是一个普通的函数内部创建对象并返回function createUser(name, age) { return { name, age }; } const u1 createUser(Tom, 20); const u2 createUser(Jerry, 21);这种写法简单直观但它有一个明显短处u1 instanceof 什么无从判断因为返回的都是字面量对象原型都是Object.prototype。构造函数配合new关键字就能解决这个问题function User(name, age) { this.name name; this.age age; } const u new User(Tom, 20); console.log(u instanceof User); // truenew的执行过程可以拆成四步创建空对象把空对象绑定到 this执行构造函数内部代码给 this 添加属性把 this 作为结果返回。需要注意的是如果构造函数里显式返回了一个对象那么最终结果会是那个对象而不是 this如果返回的是普通类型则会被忽略仍然返回 this。这个知识点在面试里经常出现理解它的关键不是背步骤而是明白构造函数和普通函数唯一的区别就是被new调用时JavaScript 引擎自动帮你搭好了“对象初始化”的流程。2.3 Object.create亲手指定对象的原型Object.create(proto)会创建一个以 proto 作为原型的新对象相当于给新对象指定了“父亲”。const animal { speak() { return ...; } }; const dog Object.create(animal); dog.speak function () { return wang wang; }; console.log(dog.speak()); // wang wang这个 API 最常见的两个用途。一个是实现简单的原型式继承把公共方法放到一个基础对象上然后通过Object.create派生多个子对象另一个是创建“纯净字典”const dict Object.create(null); dict.version 1.0;普通对象会继承Object.prototype所以dict.toString、dict.hasOwnProperty这一类方法天然存在在某些场景下递归遍历或做哈希存储时可能会干扰判断。用Object.create(null)创建出来的对象没有任何原型链它是真正的“一张白纸”适合当 Map 功能的替代品。反向理解这个 API 也有用它让你意识到对象之间的继承关系不是写死在哪一行代码里的而是可以通过指定原型来主动控制的。2.4 class 语法糖ES6 之后更清晰的建模方式ES6 之后官方推出了 class 语法本质上仍是构造函数的语法糖但结构清晰得多class User { constructor(name, age) { this.name name; this.age age; } greet() { return hi, ${this.name}; } } const u new User(HoRain, 18); console.log(u.greet());使用 class 时有几个细节需要注意。class 不存在变量提升必须声明后再使用不能像普通函数那样“先调用后声明”class 只能通过new调用直接调用会直接报错class 内部默认是严格模式写起来更安全定义在 class 里的方法会挂到 prototype 上由所有实例共享而不是每个实例复制一份。如果类里的某个状态不希望外部随便改ES2022 之后可以用#声明私有字段比如#password。私有字段在类内部可以访问类外部只能通过方法间接操作这算是 JavaScript 对象模型向“真正封装”迈出的一大步。2.5 创建方式怎么选一张表说清创建方式写法适用场景注意事项对象字面量{}日常数据、配置、临时对象最推荐简洁高效工厂函数function create() { return {} }批量生成简单对象原型链不清晰构造函数function X() { this.a 1 }需要instanceof判断类型必须配合newObject.createObject.create(proto)自定义原型、纯净字典参数可以是 nullclassclass X { }面向对象建模、复杂业务模型无提升必须 new选型逻辑其实并不复杂。只装数据用字面量需要批量创建优先用 class要做原型级复用考虑 Object.create。没有必要为了用 class 而用 classJavaScript 的对象模型本来就灵活把对象当“数据容器”用也是一种非常健康的思路。3. 对象的读写、遍历与属性控制常用操作与隐藏细节3.1 点语法还是方括号小细节里有大学问访问对象属性有两种方式点语法obj.name和方括号obj[name]。绝大多数时候点语法更直观但方括号有很多不可替代的场景。const user { name: HoRain, user-name: horain }; const key name; console.log(user[key]); // HoRain console.log(user[user-name]); // 必须用方括号带横线的键不合法方括号之所以重要是因为它的方括号里可以放字符串表达式也可以放变量。比如后端返回user_profile这种字段前端写user[user_profile]没问题如果键名会动态变化比如用户选择排序字段就不可能写死点语法必须通过变量去取。另一个常见坑是方括号里如果直接写变量不要加引号加了引号就变成了取字符串字面量键。比如obj[key]和obj[key]是完全不同的两个意思。还有一点值得留意访问一个不存在的属性不会报错只会返回undefined。初学者往往会把“属性不存在”和“程序报错”混在一起实际上这恰恰是 JavaScript 相对宽容的地方但也因此在多层访问时容易踩坑比如user.profile.city在profile不存在时就会抛 TypeError。3.2 属性增删改查与属性描述符对象不只是一张表对象的属性可以随时增删改天然就是一个动态数据结构const user { name: HoRain }; user.age 18; // 增 user.name Horain; // 改 delete user.age; // 删 console.log(name in user); // trues删除操作看起来简单但有个冷知识delete只能删除对象自身的可配置属性如果属性通过Object.defineProperty设置了configurable: falsedelete会失败。继承来的属性也不能通过delete删除真正要删只能去改原型对象。说到Object.defineProperty它其实是给对象属性加“精确控制”的核心 API。每个属性内部都有一组描述符包括 writable 是否可写、enumerable 是否可枚举、configurable 是否可配置还有 get/set 访问器const user { _age: 18, get age() { return this._age; }, set age(v) { if (v 0) this._age v; } }; user.age 20; console.log(user.age); // 20这套机制应用最广的地方就是框架。Vue 2 的响应式本质就是对 data 里的每个属性调用Object.defineProperty拦截 getter 和 setter从而在属性被读取或赋值时触发依赖更新。到了 Vue 3换成了更强大的 Proxy但核心思想依然是“对对象属性做拦截”没有变过。如果你希望对象整体不可修改可以用Object.freeze冻结对象冻结后不能增删改属性Object.seal稍微宽松一点禁止增删属性但允许修改已有值。这两招在排查“谁偷偷改了我的配置”这类问题时特别好用。3.3 遍历对象的几种姿势哪些方法有坑遍历对象最常用的方式有四种const user { name: HoRain, age: 18 }; // 1. for...in会遍历原型链上的可枚举属性 for (const key in user) { console.log(key, user[key]); } // 2. Object.keys forEach Object.keys(user).forEach((key) { console.log(key, user[key]); }); // 3. Object.values Object.values(user).forEach((value) { console.log(value); }); // 4. Object.entries for...of for (const [key, value] of Object.entries(user)) { console.log(key, value); }这里最大的坑是for...in。它不止会遍历对象自身的属性还会把原型链上可枚举的属性一起遍历出来。实际开发中我们通常只关心自身属性所以用for...in时最好配合hasOwnProperty过滤或者直接改用Object.keys、Object.entries后者天然只返回自身属性。还有一个容易被忽略的细节对象的遍历顺序并不是纯按插入顺序来的。整数类型的键会先按数字升序排列然后才是字符串键按插入顺序排列。比如const obj { b: 1, 2: 2, a: 3 }; console.log(Object.keys(obj)); // [2, b, a]这个规则在渲染列表、拼接字符串时偶尔会整出“看似随机”的 bug知道它存在很多诡异现象就有了答案。4. 对象高频操作实战深浅拷贝、解构、判空与去重4.1 浅拷贝与深拷贝别再说 JSON.parse(JSON.stringify()) 万能对象的引用特性决定了拷贝这件事不简单。所谓浅拷贝就是只复制最外层属性内层对象仍然是同一个引用深拷贝则需要把整棵对象树完整复制一份互不影响。浅拷贝的常见写法是展开运算符和 Object.assignconst user { profile: { city: Shanghai } }; const copy { ...user }; copy.profile.city Beijing; console.log(user.profile.city); // Beijing内层对象还是同一个引用很多初学者以为浅拷贝就是完全拷贝结果改了副本的内层数据原对象也跟着变。这个问题在做表单初始化、列表编辑时很容易踩。深拷贝最常见的偷懒写法是const deepCopy JSON.parse(JSON.stringify(user));这个写法不是不能用但你要清楚它的三个大坑。第一函数、undefined、Symbol 会被直接丢弃第二Date 会被转成字符串再解析回来也不是 Date 对象了第三对象存在循环引用时会直接抛错比如a.self a这种结构JSON.stringify直接罢工。如果目标是现代浏览器或 Node 18 以上的环境首选原生structuredClone它支持循环引用、Date、Map、Set 等类型的深拷贝简单可靠const copy structuredClone(user);如果想手动实现一个能处理循环引用、数组、Date、正则等常见类型的递归版深拷贝是这样的function deepClone(value, cache new WeakMap()) { if (value null || typeof value ! object) return value; if (cache.has(value)) return cache.get(value); if (value instanceof Date) return new Date(value); if (value instanceof RegExp) return new RegExp(value.source, value.flags); if (Array.isArray(value)) { const result []; cache.set(value, result); value.forEach((item, i) { result[i] deepClone(item, cache); }); return result; } const result {}; cache.set(value, result); Object.keys(value).forEach((key) { result[key] deepClone(value[key], cache); }); return result; }WeakMap 在这里的作用是记住已经拷贝过的对象。遇到循环引用时直接返回之前拷贝好的那份而不是无限递归下去。用 WeakMap 而不是普通 Map还顺带避免了缓存对象无法被垃圾回收的问题写起来一点不亏。其实这个“深拷贝”的概念在别的语言里也一样绕。Java 里的深度拷贝、C# 里对对象做全量复制本质上都是在处理“引用关系怎么复刻”的问题。理解了 JavaScript 对象的引用模型再接触其他语言的对象拷贝很多思路都能平移。4.2 解构与剩余参数提取对象数据的高效玩法ES6 的对象解构是我日常用频率最高的语法之一写起来非常舒服const user { name: HoRain, age: 18, city: Shanghai }; const { name, age } user;重命名和默认值也很常用const { name: userName, email defaultexample.com } user;嵌套对象可以继续往下拆const { profile: { city } } user; console.log(city);配合剩余参数可以一次性把“要用的”和“剩下的”分开const { name, ...rest } user; console.log(rest); // { age: 18, city: Shanghai }函数参数直接用对象解构接收一个对象参数时非常常见function printUser({ name, age }) { console.log(name, age); } printUser({ name: HoRain, age: 18 });这个写法的收益在于调用方传参时能看到键名语义更明确函数内部也不再用user.name这种写法反复访问代码更短。热搜词里“提取数组对象一部分”其实也是这个套路比如从接口返回列表里只抽几个字段const list [ { id: 1, name: a, secret: x }, { id: 2, name: b, secret: y } ]; const safeList list.map(({ id, name }) ({ id, name }));用解构顺手把敏感字段过滤掉既清晰又安全。要特别注意解构不存在的属性得到的是 undefined但解构 null 或 undefined 本身会直接抛错。所以如果要解构的对象可能为空最好先做兜底判断比如user?.profile或给整个解构包一层默认值。4.3 判空、合并与对象数组去重日常需求一次搞定“判断对象是否为空”是高频需求。最推荐的做法是检查属性个数function isEmptyObject(obj) { if (obj null) return true; return Object.keys(obj).length 0; }有的人喜欢用JSON.stringify(obj) {}判断也不是不行但有隐患如果对象里只有函数、undefined 这类 JSON 序列化会忽略的属性JSON.stringify也会返回{}结果就误判了。所以能数属性个数就尽量数属性个数。合并对象也属于日常高频操作注意后面的对象会覆盖前面的同名键const merged Object.assign({}, a, b); const merged2 { ...a, ...b };建议永远把第一个参数留成空对象避免修改原对象。展开运算符写起来更短我个人的偏好是直接用它。对象数组去重也是出镜率极高的场景。很多人的第一反应是new Set(arr)但对象是按引用比较的字面量属性相同也算不同对象所以这样根本去不掉。const arr [ { id: 1, name: a }, { id: 2, name: b }, { id: 1, name: a } ]; // 基于 id 去重 const seen new Set(); const unique arr.filter((obj) { if (seen.has(obj.id)) return false; seen.add(obj.id); return true; }); // 或者用 Map 一行搞定 const unique2 [...new Map(arr.map((obj) [obj.id, obj])).values()];这两种方式的本质都是“按某个键去重”而不是“整个对象完全相等”。实际业务里到底是按 id 去重还是按多个字段组合去重你一定要先想清楚否则结果很容易不符合预期。5. 原型链、this 与继承最难啃的骨头一次讲透5.1 原型链对象之间到底怎么继承每个对象都有一个隐藏的内部引用指向另一个对象那个对象就是它的原型。访问对象属性时如果自身没有就会沿着原型链一路向上找直到找到或者到达null为止。看一个最传统的构造函数的例子function User(name) { this.name name; } User.prototype.greet function () { return hi this.name; }; const user new User(HoRain); console.log(user.greet()); // hi HoRain console.log(user.__proto__ User.prototype); // true console.log(User.prototype.constructor User); // trueuser本身没有greet属性但它可以调用就是因为User.prototype上有这个方法。原型链把“实例”和“构造它的函数”连接了起来也让所有实例能共享同一份方法而不是每个实例复制一份。这里要区分三个容易混的概念prototype是构造函数上的属性用来挂载所有实例共享的方法__proto__是每个对象上的隐藏属性指向它的原型Object.getPrototypeOf是推荐的读取原型方法。日常开发中我们很少直接操作原型链但理解它是读懂 class、继承、instanceof、甚至是各种 polyfill 的前提。5.2 this 指向记住四条规则从此不再靠猜this 是 JavaScript 里劝退新手的一大难点但真正的规律其实不复杂关键看函数是怎么被调用的。简单归纳就是四条。默认绑定普通函数直接调用时严格模式下 this 是 undefined非严格模式下是全局对象。所以如果你在一个普通函数里写了this.xxx严格模式下往往会直接报错。隐式绑定作为对象方法调用时this 指向调用它的对象。比如user.greet()里的 this 就是 user。但如果你把方法拆出来单独调用const user { name: HoRain, greet() { console.log(this.name); } }; const fn user.greet; fn(); // this 指向丢了name 是 undefined这个坑特别容易出现在把方法当回调传给 setTimeout、事件监听器、或者 React 组件里。解决办法也简单用箭头函数或者用bind显式绑定。显式绑定call、apply、bind可以手动指定 this。bind会把函数和 this 绑死返回一个新函数适合在需要回调的场景稳定传入上下文。new 绑定构造函数被new调用时this 指向新创建的对象这正好接上前面讲 new 执行流程时的内容。箭头函数比较特殊它没有自己的 this它会捕获外层作用域的 this。这个特性让箭头函数在回调中几乎“零心智负担”但反过来如果你在对象方法里定义箭头函数它并不会指向对象而是继承外层作用域的 this。这也是一个经典面试题。5.3 class 继承与 super面向对象的现代写法ES6 class 的继承写法很接近传统面向对象语言但底层仍然是原型链那一套class User { constructor(name) { this.name name; } greet() { return hi ${this.name}; } } class Admin extends User { constructor(name, permissions) { super(name); this.permissions permissions; } manage() { return ${this.name} can manage; } } const admin new Admin(HoRain, [read, write]); console.log(admin.greet()); // hi HoRain console.log(admin instanceof User); // true使用 class 继承时子类的 constructor 里必须先调用super()之后才能使用 this否则会报 ReferenceError。原因在于子类的 this 是由父类 constructor 初始化出来的不先调用super()this 就没有被正确创建。这个细节很多人只在报错时被动记住理解了背后的原理就不会忘。子类可以重写父类方法需要调用父类原方法时用super.method()。instanceof判断也更直观admin instanceof Admin和admin instanceof User都是 true因为原型链上挂了不止一层。到了这一步你再回头看Object.create、构造函数、原型链和 class会发现它们其实是一张网class 是最容易上手的表达方式原型链是底层的实现机制两者并不矛盾。6. JSON、DOM 与对象真实业务场景怎么配合6.1 JSON 序列化与反序列化前后端数据交换的必修课前端最常见的对象使用场景就是 JSON。接口返回的数据本质是一个 JSON 字符串前端用JSON.parse把它变成对象向服务端提交数据时再用JSON.stringify把对象变回字符串。const user { name: HoRain, age: 18 }; const jsonText JSON.stringify(user); const parsed JSON.parse(jsonText);JSON.stringify和JSON.parse都有第二个参数很多人没用过但其实很有用。stringify的第二参数可以是数组表示只序列化这些键比如过滤敏感字段const user { name: HoRain, age: 18, password: 123456 }; console.log(JSON.stringify(user, [name, age]));也可以是函数逐个键值做处理。parse的第二个参数是 reviver可以在解析过程中把某个字段转成需要的类型比如把日期字符串还原成 Date 对象。JSON 本身有几个苛刻的约束。键必须用双引号字符串也必须用双引号不能有尾逗号。后台上游手写的配置如果在解析时报SyntaxError: Unexpected token十有八九就是这些格式问题。另外JSON 和对象字面量长得像但不完全一样别混着写。还要记住一个点对象里的函数、undefined、Symbol 在 JSON 序列化时会被忽略BigInt 会直接抛错。所以把复杂业务对象直接塞进 localStorage 或者传给后端之前一定要想清楚哪些数据会丢。反过来说如果对象里定义了toJSON方法JSON.stringify会优先调用它来自定义序列化结果这在输出复杂格式时很管用。6.2 浏览器里的对象实践DOM 节点、事件对象与内建对象对象不止活在纯数据里。你在浏览器里操作的一切几乎都是对象。用document.querySelector选中一个元素拿到的就是一个 DOM 节点对象它能被改样式、加事件、测属性事件监听时的event对象里装着触发源、按键状态、坐标位置等一堆信息。日常用 JS 操作视频元素就是一个典型例子const video document.querySelector(video); // 直接设置 CSS 旋转现在的浏览器可以直接写在 style 上 video.style.rotate -90deg; // 监听播放结束 video.addEventListener(ended, () { console.log(video ended); }); // 模拟播放结束事件自动触发后续逻辑 video.dispatchEvent(new Event(ended));手动派发事件的用处很大。写自动化测试或做视频播放统计时你不想真的等视频播完只要派发一个ended事件就能验证后面的上报逻辑是否正常。DOM 节点是对象事件是对象RegExp 也是对象甚至连函数也是对象只是它额外拥有可调用能力和 prototype 属性。这种“万物皆对象”的统一性让 JavaScript 的处理模型变得非常简洁只要你掌握了对象的基本规则无论是写业务逻辑、操作页面还是做跨端通信底层思想都一样。比如在 App 内嵌的 WebView 里做 JavaScript 和原生能力互调消息体通常也会被设计成一个对象前端拿到后再解析成自己需要的结构本质还是对象在“搬运”数据。7. 高频报错与调试技巧踩坑实录速查7.1 高频报错速查表遇到报错先对照做对象开发时最常遇到的报错我整理成了一张速查表遇到问题直接对照着排查比硬读堆栈要快得多。报错信息出现原因排查思路TypeError: Cannot read properties of undefined (reading xx)在 undefined 上访问属性检查上游数据是否为空用可选链或兜底值TypeError: xxx is not a function把非函数值当函数调用确认变量引用的确实是函数检查解构和 thisSyntaxError: Unexpected token x语法错误JSON 不合法常见检查引号、括号、尾逗号TypeError: Converting circular structure to JSON对象循环引用后被 stringify排查谁引用了谁用 structuredClone 或自定义处理ReferenceError: xxx is not defined变量未声明或拼错检查作用域和名字拼写TypeError: Invalid array length数组长度设置成负数或超大检查计算出来的 length 是否异常还有一个看起来像报错但并不是报错的问题链接地址里写javascript:void(0)在部分现代浏览器里可能没有效果因为它本质是一个“执行后返回 undefined”的伪协议表达式。现在的主流做法是别再往 href 里塞这种东西了换成 button 元素或者在外层容器上用event.preventDefault()拦截默认跳转行为和可访问性都更好。看到 “Cannot read properties of undefined”你可以类比其他语言里常见的“未将对象引用设置到对象的实例”这类提示通通都是空引用问题。解决思路也一致先找到那个对象在哪一步变成了 undefined再决定是加判断还是给默认值。7.2 对象调试技巧比 console.log 更高效的办法遇到复杂对象不要只会console.log(obj)我推荐几个更高效的方法。console.table打印数组对象特别直观表格形式展示每一列字段一眼就能看出数据结构和异常值console.table([{ id: 1, name: a }, { id: 2, name: b }]);多个变量一起打印时用对象字面量包一层开发者工具里会直接显示变量名console.log({ user, config, result });这样就不用在控制台里逐个展开逻辑关系一眼看全。排查“谁修改了我的对象”这种问题可以在关键位置临时Object.freeze(obj)如果后面有代码尝试修改属性在严格模式下会直接抛错运行到那一行就能定位到元凶。这个方法我实测很多次比瞎猜代码路径有效得多。判断类型时不要只靠typeof它会把 array 判成 object集合类也区分不了。更可靠的姿势是Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call(/re/); // [object RegExp]排查函数里 this 丢失的时候可以观察报错信息里是不是出现了 undefined 的属性访问。如果确实需要固定 this直接给方法包一层箭头函数或者用bind绑定改起来很直接。最后说一个我自己的检查习惯。每次读不懂一段 JavaScript 代码我会先问三个问题这里的对象是谁它从哪里来它的方法是作为什么形式被调用的把这三件事想清楚对象访问、this 指向、数据来源基本就不容易再出岔子。JavaScript 对象说到底并不复杂真正复杂的是人带着各种调用习惯去操作它。希望这篇长文能把对象这条线彻底打通让你后面写代码、看源码、排查问题时都能少踩几个坑。
返回列表