JavaScript原型链基础回顾
JavaScript中的每个对象都有一个隐式原型属性__proto__,指向其构造函数的原型对象,而原型对象本身也是对象,也有自己的隐式原型,这样层层向上直到Object.prototype,最终指向null,形成的链式结构就是原型链。当我们访问对象的某个属性时,会先在对象自身查找,找不到则沿着原型链向上查找,直到找到属性或者到达原型链末端。

比如我们创建一个简单的构造函数:
// 定义构造函数
function Person(name) {
this.name = name;
}
// 给原型添加方法
Person.prototype.sayName = function() {
console.log(this.name);
};
// 创建实例
const person1 = new Person('张三');
// 实例本身没有sayName方法,会沿着原型链找到Person.prototype上的方法
person1.sayName(); // 输出:张三
原型链面临的风险场景
原型污染攻击
原型污染是最常见的原型链风险,攻击者可以通过修改Object.prototype等原生原型,给所有对象添加恶意属性或者覆盖原有方法。比如在不安全的对象合并场景中,如果直接把用户输入的对象的属性合并到目标对象,且目标对象是普通对象,就可能触发原型污染:
// 不安全的对象合并函数
function unsafeMerge(target, source) {
for (const key in source) {
// 没有对key做校验,可能修改到__proto__属性
target[key] = source[key];
}
return target;
}
// 模拟攻击者输入
const maliciousInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const userObj = {};
unsafeMerge(userObj, maliciousInput);
// 所有普通对象都被污染了
console.log({}.isAdmin); // 输出:true
原生原型方法被覆盖
如果代码不小心覆盖了原生原型上的内置方法,比如Array.prototype.push、Object.prototype.toString,会导致所有依赖这些方法的代码出现异常,这种问题在大型项目中排查起来非常困难。
原型链保护与防御方法
冻结原生原型对象
可以使用Object.freeze方法冻结原生原型对象,防止其被修改。冻结后的对象不能添加新属性,不能删除已有属性,也不能修改已有属性的可写性、可配置性、可枚举性,也不能修改已有属性的值。
// 冻结Object的原型 Object.freeze(Object.prototype); // 尝试给Object.prototype添加属性,在严格模式下会报错,非严格模式下静默失败 Object.prototype.newProp = 'test'; console.log(Object.prototype.newProp); // 输出:undefined // 也可以冻结Array、Function等原生构造函数的原型 Object.freeze(Array.prototype); Object.freeze(Function.prototype);
需要注意的是,冻结操作要在项目初始化的最早阶段执行,确保所有代码运行前原生原型已经被保护。如果已经存在对原型的修改,冻结后再执行修改操作会失效。
安全处理对象合并操作
在进行对象合并、属性赋值等操作时,要避免修改到__proto__属性,同时可以使用Object.create(null)创建无原型的对象作为目标对象,这类对象没有继承Object.prototype,不会被原型污染影响。
// 安全的对象合并函数
function safeMerge(target, source) {
for (const key in source) {
// 跳过__proto__和constructor等可能导致原型修改的属性
if (key === '__proto__' || key === 'constructor') {
continue;
}
// 只合并自身可枚举属性
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
// 创建无原型对象作为目标
const safeTarget = Object.create(null);
const userInput = JSON.parse('{"__proto__": {"isAdmin": true}, "name": "李四"}');
safeMerge(safeTarget, userInput);
console.log(safeTarget.name); // 输出:李四
console.log(safeTarget.isAdmin); // 输出:undefined
// 普通对象也不会被污染
console.log({}.isAdmin); // 输出:undefined
避免直接修改原生原型
开发过程中尽量不要给原生原型添加自定义方法,如果确实需要给某个类型的实例添加公共方法,建议通过继承的方式实现,或者给实例单独添加方法,而不是直接修改构造函数的原型。如果必须修改,要做好兼容性校验,避免覆盖原有方法。
// 不推荐的做法:直接给Array原型添加方法
// Array.prototype.myMethod = function() {};
// 推荐的做法:创建自定义数组类继承Array
class MyArray extends Array {
myMethod() {
console.log('自定义方法');
}
}
const arr = new MyArray(1, 2, 3);
arr.myMethod(); // 输出:自定义方法
使用hasOwnProperty校验属性归属
在遍历对象属性时,尽量使用hasOwnProperty方法判断属性是否为对象自身的属性,避免访问到原型链上的属性,尤其是在处理不确定来源的对象时,这个习惯可以有效规避原型污染带来的问题。
const obj = {};
// 假设原型被污染,添加了polluted属性
Object.prototype.polluted = 'bad';
// 错误做法:直接遍历for...in会拿到原型上的属性
for (const key in obj) {
console.log(key); // 输出:polluted
}
// 正确做法:用hasOwnProperty过滤
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key); // 没有输出,因为obj自身没有属性
}
}
开发最佳实践建议
- 项目启动阶段优先执行原生原型冻结操作,从根源上避免原型被意外修改。
- 所有处理外部输入的对象操作,都要做属性名校验,禁止修改
__proto__、constructor等敏感属性。 - 代码审查时重点关注是否有修改原生原型的逻辑,非必要不通过原型扩展功能。
- 使用第三方库时,确认库没有修改原生原型的行为,避免引入原型污染风险。
- 在严格模式下开发,修改冻结对象或者给只读属性赋值时会直接抛出错误,便于早期发现问题。
常见问题解答
冻结原型后还能正常使用内置方法吗
可以,冻结原型只是禁止修改原型对象本身,不会影响到原型上已有方法的正常调用,内置方法的读写性、可配置性在冻结前已经是设定好的,冻结操作不会改变这些特性。
无原型对象有什么使用限制
无原型对象没有继承Object.prototype的方法,所以不能直接调用toString、hasOwnProperty等方法,如果需要使用这些方法,可以通过Object.prototype.toString.call(obj)的方式调用。
const nullProtoObj = Object.create(null); // 直接调用会报错 // nullProtoObj.toString(); // 正确调用方式 console.log(Object.prototype.toString.call(nullProtoObj)); // 输出:[object Object] console.log(Object.prototype.hasOwnProperty.call(nullProtoObj, 'name')); // 输出:false
JavaScript原型链原型链保护原型链防御修改时间:2026-06-19 20:12:37