前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入JavaScript原型链污染

深入JavaScript原型链污染

原创
作者头像
raye
修改2024-02-04 15:08:32
1581
修改2024-02-04 15:08:32

面向对象编程(OOP)的概念

面向对象编程包含哪些重要性质?封装、继承、多态

如何理解面向对象编程?

举例来说,用来表示一个单词或者短语的一串字符通常被称为字符串。

  • 数据:字符
  • 方法:可以对数据做什么,所有可以应用在这种数据上的行为(计算长度、添加数据、搜索,等等)都被设计成String类的方法。

所有字符串都是String类的一个实例

相比其他语言(如Java、python等传统OOP语言),JavaScript的机制和类完全不同。

JavaScript面向对象机制

JavaScript原来是没有class关键字的(es6新增的class其实也是语法糖),一般的,我们会通过类似构造函数 + new 的方法来新建对象(但其实并非构造函数)

代码语言:javascript
复制
function Foo(name) {
    this.name = name;
    this.myName = function(){
      return this.name 
    }
}
let foo1 = new Foo('Alice');
foo.show();

这种做法会有问题:

代码语言:javascript
复制
function Foo(name) {
    this.name = name;
    this.myName = function(){
      return this.name 
    }
}
let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');

console.log(foo1.myName === foo2.myName); // false 为什么?

两个对象的show方法是不同的,理论上这俩应该是相同的,毕竟都是继承自类,比如python中我们会有:

代码语言:python
复制
class Foo:
    def __init__(self, name):
        self.name = name

    def myName(self):
        return self.name

foo1 = Foo('Alice')
foo2 = Foo('Bob')

print(id(foo1.myName) == id(foo2.myName))  # 输出:False 

print(id(foo1.myName.__func__) == id(foo2.myName.__func__))  # 输出:True

为什么id得到的结果会不相等?foo1.myName(通过实例去访问方法时),Python 实际上会创建一个绑定方法(bound method)对象。这个对象将方法(在这个例子中是 Foo.myName)和特定的实例(在这个例子中是 foo1)绑定在一起。

实际上,在像 Python 这样的面向对象语言中,方法并不是每个实例的独立副本。当你创建一个对象(实例化一个类)时,实例并不会拷贝类的方法。相反,所有的实例都会共享同样的方法。

然后上文中我们的实现存在问题:

代码语言:javascript
复制
function Foo(name) {
  this.name = name;
  this.myName = function(){
    return this.name 
  }
}
let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');

console.log(foo1.myName === foo2.myName); // false 为什么?

也就是占了两片内存,这样每次新建一个对象,都会为show函数新开辟一段内存,浪费空间。

为了让两个对象上的 show 方法是同一个,我们可以利用函数的 prototype 属性(原型对象,后文会提到),这是每个函数都具有的属性。

代码语言:javascript
复制
function Foo(name) {
    this.name = name;
}
Foo.prototype.myName = function() {
    return this.name;
};

let foo1 = new Foo('Alice');
let foo2 = new Foo('Bob');

console.log(foo1.myName === foo2.myName); // true

这样每次新new出来的对象,它们的myName方法都是指向同一片内存。

但是foo1和foo2本身是没有myName方法的,它们都只有一个name属性。

不信我们将foo1打印出来看看(注意不同浏览器结果不一样)

Prototype 属性

JavaScript 中的 Prototype 是每个对象内部的一个隐藏属性,它是对另一个对象的引用,被称为这个对象的“原型”。这个原型对象本身也可能有它自己的原型,这样一层层向上直到一个对象的原型为 null,形成一条原型链。这种机制使得对象可以继承其他对象的属性和方法。

[[Prototype]] 属性在日常编程中通常不直接访问。在大多数现代浏览器中,可以使用 __proto__ 属性访问它,但这并不推荐,因为它不是所有环境都支持的标准属性。更常见的做法是使用 Object.getPrototypeOf(obj) 函数来获取一个对象的原型,或使用 Object.create(proto) 来创建一个新对象,同时设置其 [[Prototype]]

JavaScript 的许多内置方法,比如 toString(),valueOf() 等,都是定义在内置对象的 prototype 属性上的,例如 Object.prototype,Array.prototype。当我们创建一个新的对象或数组时,这些方法会通过原型链被新对象继承,因此我们可以调用 obj.toString() 或 arr.length 等。

当我们访问一个对象的属性时,JavaScript 首先会在该对象自身的属性中查找。如果没有找到,它会沿着原型链去查找,直到找到属性或者到达原型链的末端。

简单来说,[[Prototype]]其实就是一个特殊的属性,一般通过 __proto__访问,可以和其他对象的 [[Prototype]]进行关联,形成一条链

Object.create

Object.create 会创建一个对象并把这个对象的[[Prototype]]关联到指定的对象。

Object.create(null) 是一个常见用法,会创建一个拥有空(或者说null)[[Prototype]]链接的对象,适合保存数据。

代码语言:javascript
复制
        var anotherObject = {
            a:2
        };

        // 创建一个关联到anotherObject的对象
        var myObject = Object.create(anotherObject);

        myObject.a; // 2 为什么能找到?

我们可以理解为, myObject 的 [[Prototype]] 与 anotherObject 的 [[Prototype]] 关联起来了,形成了一条链

现在myObject对象的 Prototype 关联到了anotherObject。显然myObject.a并不存在,但是尽管如此,属性访问仍然成功地(在anotherObject中)找到了值2。但是,如果anotherObject中也找不到a并且 Prototype 链不为空的话,就会继续查找下去。

尽头在哪里?

所有普通的Prototype链最终都会指向内置的Object.prototype。由于所有的“普通”对象都“源于”(或者说把Prototype链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。

原型链中的屏蔽效应

理解如下代码:

代码语言:javascript
复制
  var anotherObject = {
      a:2
  };

  var myObject = Object.create(anotherObject);

  anotherObject.a; // 2
  myObject.a; // 2
  anotherObject.hasOwnProperty("a"); // true
  myObject.hasOwnProperty("a"); // false
  myObject.a++; // 关注这里!

  anotherObject.a; // 输出什么?
  myObject.a; // 输出什么?

  myObject.hasOwnProperty("a"); // true

思考结果

代码语言:javascript
复制
  anotherObject.a; // 2
  myObject.a; // 3

  myObject.hasOwnProperty("a"); // true

myObject.a++看起来应该查找并增加anotherObject.a属性,但是别忘了++操作相当于 myObject.a = myObject.a + 1

++操作首先会通过 [[Prototype]] 查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用将值3赋给myObject中新建的屏蔽属性a

原型对象

在 JavaScript 中,每一个函数都有一个 prototype 属性,这个属性是一个对象,称为“原型对象”。这个原型对象有一个特殊的用途:当一个函数被用作构造函数来创建新的对象时,这个新对象的内部 Prototype 属性就会被链接到构造函数的 prototype 对象。这就是说,新对象会继承原型对象的属性和方法。

原型对象和 Prototype 之间的主要联系:原型对象定义了一个模板,新对象通过其内部的 Prototype 属性继承了这个模板。

为何需要这种机制?

这是因为 JavaScript 是一种基于原型的语言,它使用原型链来实现继承。原型对象和 Prototype 的机制使得所有的对象实例可以共享相同的属性和方法,这样不仅可以节省内存,也使得在对象实例上的操作可以反映到所有的实例上。

代码语言:javascript
复制
  function Foo() {
      // ...
  }

  var a = new Foo();

  Object.getPrototypeOf(a) === Foo.prototype; // true

new 做了什么

JavaScript 的 new 关键字用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。new 关键字执行以下步骤:

  1. 创建一个新的空对象new 关键字首先会创建一个新的空对象。
  2. 设置原型:新创建的对象的 [[Prototype]] 属性会被链接到构造函数的 prototype 对象。新对象可以访问构造函数原型上的属性和方法。
  3. 构造函数执行:构造函数(你在 new 后面调用的函数)被执行。构造函数内部的 this 关键字会被指向新创建的对象。如果构造函数返回非空对象,则返回该对象,否则返回刚刚新创建的对象。
  4. 返回新对象:如果构造函数没有显式返回一个对象,那么 new 表达式将返回新创建的对象。

new 会依次执行下面的步骤

伪代码如下

代码语言:javascript
复制
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);

由此我们总结出

  1. 函数使用new关键字的时候,它就成为了构造函数,任何一个函数都可以是构造函数,任何一个函数都具有 prototype 属性
  2. 任何一个对象都有 [[Prototype]] 属性,称为对象的原型。
  3. 函数有一个 prototype 属性,称为原型对象
  4. 原型对象定义了模版,新对象通过内部的 [[Prototype]]属性继承了这个模版

constructor是什么

注意到,原型对象还有一个 constructor, 这个 constructor 指回原来的函数。

代码语言:javascript
复制
function Foo() {
    // ...
}

Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true

constructor 属性有什么用处呢?一般的,我们可以用 constructor 属性来判断一个实例对象是由哪个构造函数创建的。同时,如果我们想要复制一个对象,创建一个新的实例,我们也可以通过 constructor 属性来实现。

JavaScript原型继承

从上一节可以看到,所有类对象在实例化的时候将会拥有原型对象中的属性和方法,这个特性被用来实现JavaScript中的继承机制。

代码语言:javascript
复制
function Animal(name) {
    this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

console.log(cat1.color) // 'white'
console.log(cat2.color) // 'white'

color 的属性查找过程如下:

  1. 在对象 cat1 中寻找 color 属性
  2. 如果找不到,在 cat1.__proto__(也就是 Animal.prototype ) 中寻找 color
  3. 如果仍然找不到,继续在 cat1.__proto__.__proto__ 中寻找 color
  4. 这样一层层上溯,最终到达 Object.prototype,而 Object.prototype.__proto__null

原型对象的属性不是实例自身的属性,只要修改原型对象,变动就立刻体现在所有实例对象上

代码语言:javascript
复制
Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

函数对象

但是实际上原型链到这里还没有结束,因为我们忽略了一点,函数也是一个对象,它也有自己的 __proto__ 属性,但是这个属性指向哪里呢?

先看一下 JavaScript 的 function Function

代码语言:javascript
复制
let my_func = new Function("a", "b", "return a+b;");
my_func(1,2) //3

可以通过 Function 来创建一个函数对象,意味着所有的函数对象,都是通过 Function 生成的

即所有的 function 都是通过 Function 来生成的

所以最终的原型链图如下

总结:

  1. 所有的对象都有__proto__属性,该属性对应该对象的原型.
  2. 所有的函数(也只有函数才有)对象都有prototype属性,该属性的值会被赋值给该函数创建的对象的__proto__属性.
  3. 所有的原型对象都有 constructor 属性,该属性对应创建所有指向该原型的实例的构造函数.
  4. 函数对象和原型对象通过 prototypeconstructor 属性进行相互关联.

三种对象的关系:

  • 原型对象:这是通过函数的 prototype 属性定义的对象,它定义了所有通过特定构造函数创建的对象实例所共享的属性和方法。
  • **[[Prototype]]**:这是对象的一个内部属性,指向该对象的原型对象。当我们尝试访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript 会沿着 [[Prototype]] 链接去原型对象上查找。
  • **constructor**:这是原型对象的一个属性,指向与该原型关联的构造函数。实例对象通过 [[Prototype]] 链接可以访问到它,从而可以知道自己是由哪个构造函数创建的。

原型链污染

既然我们可以通过 foo.__proto__ 访问到 Foo.prototype ,同时,修改 Foo.prototype 就会影响到 foo.__proto__ 那么我们直接修改 foo.__proto__ 呢?

可以看到我们修改成功了,新生成的 foo2 对象也具有hacker 属性

更进一步,我们是否可以直接修改 Object.prototype

通过 foo1.__proto__.__proto__ 访问到 Object.prototype ,再给其添加上属性,这样原来的对象a也被加上了一个 hacker 属性。

原型链污染举例

哪些情况下我们可以设置 __proto__ 的值呢?找到能够控制数组(对象)的“键名”的操作即可:

  1. 对象merge
  2. 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

比如:

代码语言:javascript
复制
function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

试验一下

代码语言:javascript
复制
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

这里在赋值的过程中,可能存在 o1.__proto__.b = 2,但是并不成功

从图中可以看到,o2.__proto__ 已经被解析过一次了,导致不存在 o1.__proto__.b = 2 这一步

换一种方式,变成json格式,这样就不会被提前解析了。

代码语言:javascript
复制
function merge(target, source) {
    debugger;
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

从图中看到,此时正在向 Object.prototype 上添加属性。

我正在参与2024腾讯技术创作特训营第五期有奖征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 面向对象编程(OOP)的概念
  • JavaScript面向对象机制
  • Prototype 属性
    • Object.create
      • 原型链中的屏蔽效应
      • 原型对象
      • new 做了什么
      • constructor是什么
      • JavaScript原型继承
      • 函数对象
      • 原型链污染
      • 原型链污染举例
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档