Vue3升级踩坑:Object.prototype扩展引发路由异常
2025-03-17 02:02:45
Vue3 踩坑:Object.prototype 扩展导致 Vue Router 异常
最近把一个老项目从 Vue2 升级到 Vue3,遇到一个奇葩问题。我在 Object.prototype
上定义了一个扩展方法,啥都没干,Vue Router 直接报错,一脸懵逼。
一、问题复现
为了方便大家理解,我把问题代码简化一下:
// ext.js
Object.prototype.mergeObjXXXX = function(obj2){
console.log(this);
console.log(obj2);
}
然后在项目里,即使你 不 调用这个 mergeObjXXXX
方法,甚至 不 导入 ext.js
文件, Vue Router (版本 4.5.0,Vue 3.5.13) 也会抛出异常:
main.js:38 TypeError: Cannot convert undefined or null to object
at Object.assign (<anonymous>)
at Object.mergeObjXXXX (ext.js:20:9)
at extractComponentsGuards (vue-router.js?v=ea680b7e:1465:32)
at vue-router.js?v=ea680b7e:2484:16
从错误信息来看,是 Vue Router 内部的 extractComponentsGuards
函数调用了 mergeObjXXXX
,并且传的参数有问题,导致 Object.assign
报错。Vue2 的时候,可没这事儿!
二、原因分析
问题出在哪儿? 咱们得扒一扒 Vue Router 的源码。
从错误堆栈往上追,extractComponentsGuards
函数的作用是提取路由组件中的导航守卫。这个函数内部会遍历组件的 beforeRouteEnter
、beforeRouteUpdate
、beforeRouteLeave
等钩子,并做一些处理。
问题就出在 Vue Router 内部对对象属性的遍历方式上。我怀疑,Vue Router 内部在处理组件选项时,使用了类似 for...in
循环或者其他会遍历原型链的方法。而我的 mergeObjXXXX
恰好挂在 Object.prototype
上,所以被“误伤”了。更具体地,传入的某个参数是 null
或 undefined
,因此Object.assign
会报错。
为啥 Vue2 没事?大概率是 Vue2 和 Vue3 的 Vue Router 在实现细节上有差异,对组件选项的处理方式不同。
三、解决方案
知道了问题的原因,就好办了。提供几种解决办法,你可以根据实际情况选择:
1. 修改扩展方法名 (不推荐)
这是最快,也是最不推荐的方法。 把 mergeObjXXXX
改成一个很特殊的名字, 例如加上项目独有前缀,或者随机数。
缺点很明显,治标不治本。万一哪天 Vue Router 升级了,或者你又加了别的扩展方法,可能还会出问题。
2. 使用 defineProperty
定义不可枚举属性 (推荐)
这是比较优雅的解决方案。 我们可以用 Object.defineProperty
来定义 mergeObjXXXX
方法,并把它设置为不可枚举(enumerable: false
)。
// ext.js
Object.defineProperty(Object.prototype, 'mergeObjXXXX', {
value: function(obj2) {
console.log(this);
console.log(obj2);
},
enumerable: false, // 关键是这个
writable: true,
configurable: true
});
这样,大多数遍历对象属性的方法(包括 for...in
、Object.keys
)都不会包含 mergeObjXXXX
,自然也就不会被 Vue Router “误伤”。
原理:
enumerable: false
:表示该属性不可枚举。writable: true
:表示该属性的值可以被修改。configurable: true
:表示该属性的特性可以被修改,且该属性可以被删除。
设置 writable
和 configurable
为 true
是为了保证扩展方法的灵活性,后续可以修改或删除。
3. 不要在 Object.prototype
上定义方法 (最佳实践)
虽然在 Object.prototype
上扩展很方便,但其实这是个不太好的做法。
污染全局对象可能导致各种意想不到的问题,尤其是在大型项目或多人协作时。
建议的做法:
-
定义普通函数: 如果
mergeObjXXXX
只是一个工具函数,完全可以定义成一个普通函数:// utils.js function mergeObjXXXX(obj1, obj2) { console.log(obj1); console.log(obj2); // ... } export { mergeObjXXXX }; // 在需要的地方导入使用 // import { mergeObjXXXX } from './utils.js';
-
使用 Class 扩展(如果适用)
如果你是要对某类对象做统一的操作,可以新建一个class来派生:
class MyObject extends Object{
mergeObjXXXX(obj2) {
console.log(this);
console.log(obj2);
}
}
//那么对于新类,就可以这样操作了:
const mergedObject = new MyObject();
mergedObject.mergeObjXXXX(anotherObject);
-
使用 Symbol 作为 key (进阶):
Symbol 是 ES6 引入的一种新的基本数据类型,它的特点是唯一性。
可以把它作为对象的 key,这样不会和其他属性名冲突,也不可枚举,完美避免各种冲突。// ext.js const mergeObjXXXKey = Symbol('mergeObjXXXX'); Object.prototype[mergeObjXXXKey] = function(obj2) { console.log(this); console.log(obj2); } // 使用示例: // let obj = {}; // obj[mergeObjXXXKey](anotherObj);
说明 用symbol作为键来调用相对复杂, 但确保了最佳安全性.
4. 修改 Vue Router 源码 (极不推荐)
这…不到万不得已,千万别这么干!
你可以去 Vue Router 的 GitHub 仓库提 issue,或者自己 fork 一份代码,找到 extractComponentsGuards
函数,修改它的遍历逻辑,避开原型链上的方法。
但这样做的风险很大,每次 Vue Router 更新,你都得重新合并代码,麻烦不说,还容易出错。
四. 安全提示
除了以上这些方法,日常写代码也要注意,给 Object.prototype
这种底层对象加方法,能不用就不用, 这和操作 window , document 同理, 不可预知的问题不少。 如果一定要用,也记得用 defineProperty
并设置 enumerable: false
。另外做好类型检测。确保代码健壮性.