XSS的原型链污染1--原型链解释

发布于:2025-08-05 ⋅ 阅读:(16) ⋅ 点赞:(0)

因为js(<=ES6)中没有类的概念,所以衍生出了原型链的概念

面向对象的封装,多态和继承

JavaScript代码创建原型链时因为里面写的方法,每当创建一个实例对象的时候,都会调用其中的方法,都会创建一个新的内存空间,新建一个相同的方法,这很浪费系统的内存资源,但是创建的实例对象都是用的同一个方法,完全应该共享,所以这个问题的解决方法就衍生出了prototype原型对象

prototype 的属性的作用

JavaScript 继承机制的设计思想是:原型对象的所有属性和方法,都能被实例对象共享。若将属性和方法定义在原型上,所有实例对象可共享这些内容,既节省内存,又体现实例间的联系。

1. 为对象指定原型的规则

JavaScript 规定:每个函数都有一个 prototype 属性,指向一个对象

示例:

function f() {}  
typeof f.prototype // "object"

函数 f 默认自带 prototype 属性,其值为一个对象。

2. prototype 的作用差异:普通函数 vs 构造函数

  • 普通函数prototype 属性基本无用。
  • 构造函数:当通过 new 生成实例时,构造函数的 prototype 会自动成为实例对象的原型

3. 构造函数的 prototype 实践:属性共享

通过构造函数 Animal 演示 prototype 的共享特性:

// 定义构造函数
function Animal(name) {  
  this.name = name; // 实例自身属性(每个实例单独拥有)
}  

// 在原型上定义共享属性
Animal.prototype.color = 'white';  

// 创建实例
var cat1 = new Animal('大毛');  
var cat2 = new Animal('二毛');  

// 实例访问原型上的属性
cat1.color // 'white'  
cat2.color // 'white'  

核心逻辑
构造函数 Animal 的 prototype,是实例 cat1cat2 的原型对象。原型上的 color 属性被所有实例共享。

4. 原型属性的动态特性

原型对象的属性不属于实例自身,而是通过原型链关联。因此:
修改原型对象,所有实例会立即体现变动

示例(接上面代码):

Animal.prototype.color = 'black'; // 修改原型属性  
cat1.color // 'black'(实例自动同步变化)  
cat2.color // 'black'  

综上,prototype 的核心作用是 为构造函数的实例提供共享属性 / 方法的 “公共容器”,实现内存优化和继承关系,是 JavaScript 原型继承机制的基石。

可以理解为:他(prototype)是实例的父类

5 对比

object

res.name调用的是name属性,所以.prototype也是object属性

 

原型链

JavaScript 规定:所有对象都有自己的原型对象(prototype)

一、原型链的形成逻辑

  1. 双向特性
    • 任何对象都可以充当其他对象的原型;
    • 原型对象本身也是对象,因此也有自己的原型。
  2. 链式推导
    对象 → 原型 → 原型的原型 → … → 终点,形成 “原型链”(prototype chain)

二、原型链的示例与终点

1. 原型链的层级示例(以 cat1 为例): 
cat1.color --> Animal.prototype --> ...(中间原型)... --> Object.prototype --> null  
2. 所有对象的最终原型:Object.prototype
  • 所有对象的原型链最终都会上溯到 Object.prototypeObject 构造函数的 prototype 属性)。
  • 这就是 “所有对象都有 valueOf()toString() 方法” 的原因 —— 这些方法继承自 Object.prototype
3. 原型链的终点:null
  • Object.prototype 也有自己的原型,即 nullnull 没有属性、方法,也没有自身的原型)。
  • 验证代码:
    Object.getPrototypeOf(Object.prototype)  
    // 返回 null  
    

三、原型链的属性查找规则

当读取对象的某个属性时,JavaScript 引擎遵循以下逻辑:

  1. 优先查自身:先在对象自身查找属性;
  2. 向上遍历原型链:若自身没有,就到原型对象中查找;若仍没有,继续到原型的原型中查找…… 直到原型链顶端(Object.prototype);
  3. 最终结果
    • 若找到,返回属性值;
    • 若遍历到 null 仍未找到,返回 undefined
特殊情况:属性 “覆盖”(overriding)

对象自身原型定义了同名属性,则 优先读取对象自身的属性(自身属性覆盖原型链上的同名属性)。

四、原型链的性能影响

  • 属性查找成本:在原型链上越 “上层” 的属性(如 Object.prototype 上的属性),查找时遍历的层级越多,对性能影响越大;
  • 不存在属性的代价:若查找不存在的属性,引擎会遍历整个原型链(直到 null),这会带来额外性能损耗。

综上,原型链是 JavaScript 实现属性继承共享的核心机制,理解其查找规则和边界(终点 null),是掌握对象行为的关键。

function Father() {
  this.first_name = 'Donald'
  this.last_name = 'Trump'
}

function Son() {
  this.first_name = 'Melania'
}

Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

原型链关系解析

1. 构造函数与原型的初始关系
  • Father 是构造函数,其 prototype 属性指向 Father 的原型对象Father.prototype)。
  • Son 是构造函数,其 prototype 属性原本指向 Son 的原型对象Son.prototype,默认继承 Object.prototype)。
2. 关键操作:修改 Son.prototype

javascript

Son.prototype = new Father()

这行代码将 Son 的原型对象 替换为 Father 的实例(即执行 new Father() 创建的对象)。此时:

  • Son.prototype 不再是默认的空对象,而是一个包含 first_name: 'Donald' 和 last_name: 'Trump' 的对象(因为 new Father() 会执行 Father 的构造函数,给实例添加这两个属性)。
  • Son 的实例(如 son)的 __proto__ 会指向这个新的 Son.prototype(即 Father 的实例)。
3. son 实例的原型链结构

当创建 let son = new Son() 时,son 的原型链如下:

plaintext

son(实例)  
↓(__proto__指向)  
Son.prototype(即 Father 的实例,包含 first_name: 'Donald'、last_name: 'Trump')  
↓(__proto__指向)  
Father.prototype(Father 构造函数的原型对象,默认继承 Object.prototype)  
↓(__proto__指向)  
Object.prototype  
↓(__proto__指向)  
null(原型链终点)  
4. 属性查找过程(解释 console.log 的输出)

执行 console.log(Name: ${son.first_name} ${son.last_name}) 时,JS 引擎按原型链查找规则解析属性:

  • son.first_name
    • 先查 son 自身 → 发现 this.first_name = 'Melania'(自身属性),直接使用。
  • son.last_name
    • 先查 son 自身 → 没有该属性;
    • 沿着原型链向上找,查 Son.prototype(即 Father 的实例)→ 发现 this.last_name = 'Trump'(继承自 Father 的构造函数),于是使用。
5. 原型链的核心作用:实现属性继承
  • last_name 是通过 原型链继承 获得的(Son 自身没定义,从 Son.prototype 即 Father 的实例继承)。
  • first_name 是 自身属性覆盖 了原型链上的同名属性(Son 自身定义了,优先使用自身值)。

总结

这段代码通过 “修改构造函数的 prototype 为另一个构造函数的实例”,手动实现了 原型链继承

  • Son 的实例能访问 Father 定义的属性(如 last_name),体现了 原型链的属性继承能力
  • 自身属性可覆盖原型链上的同名属性(如 first_name),体现了 原型链的查找优先级

这种写法是 ES5 之前实现继承的经典方式,核心依赖 原型链的层级查找机制

constructor 属性

一、核心定义

每个函数的 prototype 对象,都自带一个 constructor 属性,默认指向该 prototype 所属的构造函数

function P() {}  
// prototype 的 constructor 指向构造函数 P  
P.prototype.constructor === P; // true  

二、constructor 的继承特性

constructor 定义在 prototype 对象 上,因此会被 所有实例对象通过原型链继承

代码验证:
function P() {}  
var p = new P();  

// 1. 实例 p 的 constructor 继承自原型链  
p.constructor === P; // true(因为 p.__proto__ === P.prototype,继承了 constructor)  

// 2. 对比原型上的 constructor  
p.constructor === P.prototype.constructor; // true  

// 3. 检查 p 自身是否有 constructor 属性  
p.hasOwnProperty('constructor'); // false(说明 constructor 是继承来的,非自身属性)  

三、底层逻辑:原型链的继承关系

实例 p 自身没有 constructor 属性,它的 constructor 是 从原型链(P.prototype)上 “借” 来的(通过 __proto__ 访问原型对象的 constructor)。

四、constructor 的核心作用

判断实例对象的 “构造函数来源” —— 明确某个实例是由哪个构造函数创建的。

例如:

function Dog() {}  
function Cat() {}  

var animal = new Dog();  
console.log(animal.constructor === Dog); // true(确认 animal 由 Dog 构造)  

constructor 是原型链继承中 “身份溯源” 的关键纽带,让实例和构造函数的关联更清晰。但需注意:若手动修改原型对象(如 P.prototype = {}),可能破坏 constructor 的指向,需手动修复(P.prototype.constructor = P)。

constructor 属性的拓展用法与注意事项

一、验证实例的构造函数归属
function F() {};  
var f = new F();  

f.constructor === F; // true  
f.constructor === RegExp; // false  

解释constructor 明确实例 f 的构造函数是 F,而非其他函数(如内置的 RegExp)。

二、通过 constructor 从实例新建同类实例
1. 基础场景:从现有实例创建新实例
function Constr() {}  
var x = new Constr();  

// 利用 x 的 constructor 间接调用构造函数,创建新实例 y  
var y = new x.constructor();  
y instanceof Constr; // true  
2. 实例方法中复用构造函数(更实用)

javascript

Constr.prototype.createCopy = function () {  
  // 通过 this.constructor 调用自身构造函数,新建实例  
  return new this.constructor();  
};  

解释createCopy 方法借助 constructor,让实例能动态创建 “同类型” 新实例(即使构造函数后续被修改,也能自动适配)。

三、修改原型时的 constructor 维护

constructor 是 原型对象与构造函数的关联纽带。若直接替换原型对象,会破坏 constructor 指向,需 手动修复

function OldConstr() {}  

// 错误操作:直接替换原型,导致 constructor 丢失  
OldConstr.prototype = {  
  // ...新方法...  
};  

var instance = new OldConstr();  
instance.constructor === OldConstr; // false(此时指向 Object)  


// 正确做法:替换原型时,显式恢复 constructor  
OldConstr.prototype = {  
  constructor: OldConstr, // 手动关联构造函数  
  // ...新方法...  
};  

instance.constructor === OldConstr; // true(修复后正常)  

核心总结

  1. 身份标识constructor 帮助实例 “溯源” 自己的构造函数;
  2. 动态创建:允许从实例反向调用构造函数,灵活生成新实例;
  3. 原型维护:修改原型时,务必修复 constructor,避免关联关系断裂

原型对象替换时的 constructor 维护问题

一、核心背景

constructor 是 原型对象与构造函数的关联纽带。若直接替换整个原型对象(而非增量扩展),必须手动修复 constructor,否则关联会断裂。

二、代码演示:原型替换导致 constructor 异常
function Person(name) {  
  this.name = name;  
}  

// 初始状态:prototype.constructor 正确指向 Person  
console.log(Person.prototype.constructor === Person); // true  


// 错误操作:直接替换原型对象(覆盖原有 prototype)  
Person.prototype = {  
  method: function () {}  
};  


// 问题暴露:新原型的 constructor 不再指向 Person  
console.log(Person.prototype.constructor === Person); // false  
console.log(Person.prototype.constructor === Object); // true(默认继承自 Object.prototype)  
三、现象分析
  • 当直接赋值 Person.prototype = {} 时,新原型是全新的普通对象,其 constructor 会默认继承 Object.prototype.constructor(即 Object)。
  • 这导致后续通过 实例.constructor 判断构造函数时,结果错误(实例会 “误以为” 自己由 Object 创建)。
四、修复方法:替换原型时,显式重置 constructor
Person.prototype = {  
  constructor: Person, // 手动关联回原构造函数  
  method: function () {}  
};  

// 验证:恢复正确关联  
console.log(Person.prototype.constructor === Person); // true  

关键总结

原型操作方式 constructor 是否需要修复 原因
扩展原型(如 Person.prototype.method = ... 不需要 原型对象未被替换,constructor 仍保留原值。
替换原型(如 Person.prototype = {} 需要(手动设置 constructor 新原型是全新对象,默认继承 Object 的 constructor,需手动修正关联。

若忽略修复,实例的 constructor 会错误指向 Object(或其他无关构造函数),导致身份判断、动态创建实例等逻辑失效。

Object.prototype.proto

_proto_:指向的值就是父类,_proto_属性指向当前对象的原型对象,及就是构造函数的prototype属性 

1. 基本定义

实例对象的 __proto__ 属性(名称前后各有两个下划线),用于 读取或设置该对象的原型(即它所继承的原型对象),该属性可读写

2. 代码示例:通过 __proto__ 修改原型
var obj = {};     // 创建空对象 obj  
var p = {};      // 创建空对象 p  
obj.__proto__ = p; // 将 p 设为 obj 的原型  

// 验证原型设置:Object.getPrototypeOf 是标准方法,返回对象的原型  
Object.getPrototypeOf(obj) === p; // 输出 true  
3. 标准规范与使用建议
  • 环境限制__proto__ 是 浏览器环境的非标准特性(语言标准未强制要求所有 JavaScript 环境支持,如 Node.js 早期版本行为不同)。
  • 设计意图:双下划线 __ 表明它是 内部属性,初衷是供引擎内部使用,而非直接暴露给开发者。
  • 替代方案:生产环境中,优先使用标准 API 操作原型:
    • 读取原型Object.getPrototypeOf(obj)(替代 obj.__proto__ 读操作);
    • 设置原型Object.setPrototypeOf(obj, newProto)(替代 obj.__proto__ 写操作)。
4. 用 __proto__ 直观表示原型链

通过 __proto__ 手动构建原型链,实现方法共享(示例)

// 1. 定义“原型对象”(存放共享方法)  
var proto = {  
  print: function () {  
    console.log(this.name); // 访问实例的 name 属性  
  }  
};  

// 2. 定义两个实例对象  
var A = { name: '张三' };  
var B = { name: '李四' };  

// 3. 让 A、B 继承 proto 的方法(通过 __proto__ 关联原型)  
A.__proto__ = proto;  
B.__proto__ = proto;  

// 4. 调用共享方法  
A.print(); // 输出:张三(A 自身有 name,调用 proto 的 print)  
B.print(); // 输出:李四(B 自身有 name,调用 proto 的 print)  

核心总结

  • __proto__ 是实例对象访问原型的 “便捷入口”,但因非标准、内部属性的特性,不推荐直接依赖它开发
  • 若需兼容不同环境,优先使用 Object.getPrototypeOf() 和 Object.setPrototypeOf()
  • __proto__ 可辅助理解原型链的层级关系,但实际开发中更注重 “原型继承” 的逻辑,而非直接操作该属性

网站公告

今日签到

点亮在社区的每一天
去签到