啊?学JavaScript?我吗?纯学习笔记,极度混乱,如果想找参考,建议赶紧跑。推荐:这篇文章。
prototype和__proto__
我觉得js的对象很像细胞结构,prototype就是细胞膜,套在内部的核心(对象)上。
function Foo() { this.bar = 1}这个很像是一个模板,换个细胞核也能用。或者说更像是抽象层,反正就是套在了对象的外面,提供了一个稳定的接口。
Foo.prototype.show = function show() { console.log(this.bar)}
let foo = new Foo()foo.show() // 1
console.log(foo.__proto__ === Foo.prototype) // true这个是对象的原型链,foo.__proto__和Foo.prototype像是夹在foo和Foo之间的一块飞地。
foo.__proto__指向了Foo.prototypefoo.__proto__.__proto__指向了Object.prototypeObject.prototype.__proto__指向null。
function Father() { // 这个属性实际上是在Father的实例上,而不是在Father.prototype上。 this.first_name = 'Donald' this.last_name = 'Trump'}
function Son() { this.first_name = 'Melania'}
Son.prototype = new Father() // 就好比把细胞膜套在了Son的细胞核上,Son的实例就能访问Father的属性了。
let son = new Son()console.log(`Name: ${son.first_name} ${son.last_name}`) // Name: Melania Trumpconsole.log(son.first_name === Son.prototype.first_name) // falseconsole.log(son.first_name === son.__proto__.__proto__.first_name) // false, son.__proto__.__proto__实际上就是Father的实例console.log(son.first_name === (new Father()).first_name) // false,这个和上面那个实际上是等价的console.log(son.__proto__.__proto__ === (new Father()).__proto__) // true,Father的实例的原型就是Father.prototypeconsole.log(son.__proto__ === (new Father())) // false,son.__proto__是Father的实例,而不是Father.prototype// 卧槽我有点绕晕了,睡一觉再说。Son类继承了Father类的last_name属性,最后输出的是Name: Melania Trump。
总结一下,对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在son.__proto__中寻找last_name
- 如果仍然找不到,则继续在son.proto.__proto__中寻找last_name
- 依次寻找,直到找到null结束。比如,Object.prototype的__proto__就是null
继承的方式一共有四种,分别是:
- 原型链继承
- 构造函数继承
- 组合继承
- 寄生继承
之后再补吧,反正24h前我应该是看懂了。
pollution
// 污染!
// foo是一个简单的JavaScript对象let foo = {bar: 1} // 等价于 new Object({bar: 1})
// foo.bar 此时为1console.log(foo.bar) // 1
// 修改foo的原型(即Object)foo.__proto__.bar = 2// foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2
// 由于查找顺序的原因,foo.bar仍然是1console.log(foo.bar) // 1
// 此时再用Object创建一个空的zoo对象let zoo = {}
// 查看zoo.barconsole.log(zoo.bar); // 2污染的原因在于,zoo也是Object类的实例,所以zoo.bar会去Object类中寻找bar属性,而这个属性已经被foo污染了。
Object.prototype是细胞膜,而zoo是细胞核,套上了这层膜,自然也带上了Object.prototype.bar = 2;
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
再来看这个merge函数。这个函数的作用是把source对象的属性合并到target对象中,如果target对象中已经有了source对象中的属性,则会递归合并。
但是,如果source对象中有__proto__属性(字面量),则会污染target对象的原型链。
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] } }}
let o1 = {} // 这是一个空对象// let o2 = {a: 1, "__proto__": {b: 2}} // 这个就不行,__proto__没有被当做键名// let o2 = { a: 1, ['__proto__']: { b: 2 } } // 这个就可以,不过不如JSON来的方便let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')// 这个就可以,JSON.parse会把__proto__当做键名,这里的__proto__是o2自己的属性,而不是Object.prototype.__proto__
console.log('==before merge==')console.log('Object.prototype', Object.prototype) // [Object: null prototype] {}console.log('o2', o2) // { a: 1, ['__proto__']: { b: 2 } }console.log('o2.__proto__', o2.__proto__) // { b: 2 }console.log('o2.a', o2.a) // 1console.log('o2.b', o2.b) // undefined,o2对象本身没有b属性,b属性在o2的原型链上console.log('o2.__proto__.b', o2.__proto__.b) // 2,o2.__proto__是一个对象,它有一个属性b,值为2
merge(o1, o2) // 把o2的属性合并到o1中,o1.a = 1,o1.__proto__.b = 2console.log('==after merge==')
console.log('Object.prototype', Object.prototype) // [Object: null prototype] { b: 2 }console.log('o1', o1) // 打印的是{ a: 1 },实际上是{ a: 1, __proto__: { b: 2 } },o1.__proto__已经被污染了,o1.__proto__.b = 2console.log('o1.__proto__', o1.__proto__) // [Object: null prototype] { b: 2 }console.log('o1.a', o1.a, 'o1.b', o1.b) // 1 2
let o3 = {} // 等价于 new Object({})console.log('o3.b', o3.b) // 2,o3是Object类的实例,所以o3.b会去Object类中寻找b属性,而这个属性已经被o1污染了。console.log('o3.b', (new Object()).b) // 2,Object类的实例也会被污染也就是说,merge不仅把o2自己的属性合并到了o1中,还把o2的原型链上的属性也合并到了o1中,导致o1的原型链被污染了。
而o1的原型链是Object.prototype,所以所有Object类的实例都会受到影响。
这里我想展开我理解时遇到的问题,也就是o2.__proto__到底是什么。举个例子:
console.log('==o2.__proto__==')let o2 = JSON.parse('{"a":1,"__proto__":{"b":2}}')
console.log(Object.hasOwn(o2, "__proto__")) // true,o2对象本身有一个属性__proto__,值为{b: 2},这个属性是o2自己的属性,而不是Object.prototype.__proto__console.log(o2.__proto__) // { b: 2 },o2.__proto__是一个对象,它有一个属性b,值为2console.log(o2.__proto__.b) // 2,o2.__proto__是一个对象,它有一个属性b,值为2console.log(Object.getPrototypeOf(o2)) // 是空的,原型链上没有b属性console.log(Object.prototype) // 空的// 所以我更倾向理解为,JSON.parse允许了禁止的命名规则。// 目前,Object还没有被污染,接着来试一下正常创建__proto__属性的对象:
console.log('==o3.__proto__==')let o3 = {}o3.__proto__ = {b: 2} // 这个是正常的创建__proto__属性的对象,o3.__proto__是一个对象,它有一个属性b,值为2console.log(Object.hasOwn(o3, "__proto__")) // false,因为这是原型链上的东西,不是o3自己的属性console.log(o3.__proto__) // { b: 2 },o3.__proto__是一个对象,它有一个属性b,值为2console.log(o3.__proto__.b) // 2,按照查找顺序,o3自己没有__proto__属性,于是去Object.prototype上找,找到了Object.prototype.__proto__,它是一个对象,它有一个属性b,值为2。卧槽真的好像倒模啊。console.log(Object.getPrototypeOf(o3)) // { b: 2 },这是真的在原型链上console.log(Object.prototype) // 还是空的,因为 o3.__proto__ = {b: 2} 只对o3生效,这个类似于Overlay
// 开始手动污染Object.prototype.b = 2console.log('==o4.__proto__==')let o4 = {}console.log(Object.hasOwn(o4, "__proto__")) // false,因为这是原型链上的东西,不是o4自己的属性console.log(o4.__proto__) // [Object: null prototype] { b: 2 }console.log(o4.__proto__.b) // 2console.log(Object.getPrototypeOf(o4)) // [Object: null prototype] { b: 2 },这是真的在原型链上console.log(Object.prototype) // [Object: null prototype] { b: 2 }我这里真正好奇的是
console.log(Object.getPrototypeOf(o3)) // { b: 2 },这个实际上是一个匿名对象console.log(Object.getPrototypeOf(o4)) // [Object: null prototype] { b: 2 },这个实际上是Object.prototype这两个的差异。
因为 o3.__proto__ = {b: 2} 实际上等价于 Object.setPrototypeOf(o3, {b:2}),修改的只是 o3 的 [[Prototype]],可以用 CoW 写时复制(创建新对象)去理解他,创建了一个挂载b属性的匿名对象。
所以我认为原型链污染的本质在于让o2这样自身的__proto__属性去污染Object.prototype,达到等效 Object.prototype.b = 2 的效果
有一说一GPT画的这图还是不错的:
o2 | | +---------------+ | | a:1 | | "__proto__": { | b:2 } +---------------+ | | ▼ Object.prototype | ▼ null
o3 | | [[Prototype]] | ▼ +---------+ | b:2 | +---------+ | | [[Prototype]] | ▼ Object.prototype | ▼ null
o4 | | [[Prototype]] | ▼ +----------------+ | Object.prototype| | | | b:2 | +----------------+ | | null// JavaScript 太美妙了。constructor
好像该学一下constructor了。
这篇文章真的非常详细,而且入门门槛不高。
Class,起源于function,实际上就是一个函数,class的实例就是一个对象,而这个对象的原型就是class.prototype。class只不过是语法糖。
构造函数,实际上也是函数,只不过当var inst = new MyClass()的时候,会执行这个函数,并且把this指向新创建的对象(inst)。
function MyClass() { this.myProperty = 'Hello'; // 这个属性是MyClass的实例的属性,而不是MyClass.prototype的属性 // 也就是说,myProperty是每个实例独有的,而不是所有实例共享的; // 并且只有当实例被创建的时候,才会执行这个构造函数,才会给实例添加myProperty属性。
}MyClass.prototype.myVar = 114514;
MyClass.prototype.myFunction = function() { console.log('Hello, world!');};
var inst = new MyClass();// 当执行构造函数的时候,可以理解为inst就是那个this;也正因此,MyClass.myProperty是undefined,因为根本没执行过!inst.myFunction(); // 输出 "Hello, world!"inst.__proto__.myFunction(); // 输出 "Hello, world!"console.log( inst.myFunction() === inst.__proto__.myFunction()) // true,因为这是MyClass原型链上的东西,是共享的原始的class思想就是运行在运行时修改prototype上的属性,这可以是function,进而当new MyClass()的时候,实例就会继承这个prototype上的属性(function)。我们继续:
console.log(inst.myProperty); // 输出 "Hello"console.log(inst.constructor === MyClass); // 输出 true// constructor属性指向创建该对象的函数,默认情况下,所有对象的prototype的constructor属性都指向它们的构造函数。console.log(inst.constructor.myProperty); // 输出 undefined,因为myProperty是实例的属性,而不是构造函数的属性console.log(inst.constructor.prototype.myVar); // 输出 114514文章里是这样解释的:
“__proto__” is a magic property that returns the “prototype” of the class of the object. While this property is not standard in the JavaScript language it’s fully supported in the NodeJS environment. What’s good to note about this property is that it’s implemented as a getter/setter property which invokes getPrototypeOf/setPrototypeOf on read/write. So assigning a new value to the property “__proto__” doesn’t shadow the inherited valuedefined on the prototype. The only way to shadow it involves using “Object.defineProperty”
嗯,我觉得这个讲解很到位,特别是最后一句;完美解释了我在原型链污染里关于o2、o3、o4的差异。
所以o3.__proto__ = {b: 1},实际上是修改了o3自己的原型链,而不是Object.prototype;
而如果Object.prototype.b = 1后,我再new一个Object,就会自己带上来自Object的prototype属性b
好像已经超出constructor的范畴了,没关系。不不有关系,再不睡觉要猝死了。晚安玛卡巴卡,zzz…
// constructor.constructor在JavaScript中,某个对象的.constructor 属性是这个对象的构造函数,而.constructor.constructor就是这个对象的构造函数的构造函数,而任何函数的构造函数都是Function。
// 立刻弹窗eval('alert(1)')
// f调用后才会弹窗var f = Function('alert(1)')f()
// xx.constructor.constructor()也一样var f = {}.constructor.constructor('alert(1)')f()Function和eval类似,只不过它其中的代码不是立刻执行,而是需要再次调用才能执行。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









