第三方库或者在组件在设计的时候,经常会通过一个立即执行函数,在立即函数里面去定义一个局部变量,然后返回一个可以获取这个局部变量的函数。
这种写法的好处是可以屏蔽外部使用者直接访问到内部的变量,防止外部使用者篡改内部变量引起整个程序发生错误。
var o = (function() {
?const obj = {
? ?name: 'csuxzy',
? ?age: 18,
}
?
?return {
? ?get(key) {
? ? ?return obj[key]
? },
? ?isOld() {
? ? ?return obj.age > 18;
? }
}
})()
?
console.log(o.get('name')); // csuxzy
console.log(o.isOld()); // false
例如这段代码模拟一个第三方库的行为。我们通过闭包定义了一个局部变量obj
,然后返回了这个obj
的一个访问函数get
和isOld
函数。正常情况下这段代码能正常运行,并且isOld
会一直返回false
。但是这样子真的安全吗?我们有没有办法在使用的时候找到这段代码的漏洞直接修改闭包变量obj
呢。
valueof
首先想到的是valueOf
。我们知道valueOf
是Object
原型上的一个方法,它的作用就是将 this
值转换成对象。对于对象就是直接返回这个对象this
。
const obj = {
?name: 'csuxzy',
?age: 18,
}
?
obj.valueOf().name = 'hello valueOf'
?
console.log(obj.name) // hello valueOf
例如这段代码,我们通过obj.valueOf().name
对obj
的name
重写之后调用obj.name
,也确实返回了重写之后。
那么针对上面闭包的场景。valueOf
是Object
原型上的一个方法,我们的obj
又是继承至Object
。根据属性查找的规则,如果当前对象没找到会一直去原型链上面去找。有了这个知识之后我们就可以尝试通过valueOf
的方式进行注入更改。
o.get('valueOf')()
我们传入valueOf
作为key
,在obj
上肯定没有这个属性,就会一直沿着原型链找,直到找到Object
原型上的方法,但是很遗憾,运行之后我们会报错:
TypeError: Cannot convert undefined or null to object
这是为啥呢。仔细分析我们可以看出来这和this
指向有关。valueOf
的简易实现如下:
Object.prototype.valueOf = function () {
?return Object(this);
};
其实就是返回this
。我们普通调用obj.valueOf
时this
肯定指向obj
。但是上面o.get('valueOf')()
这种写法可以改写为:
const obj = {
?name: 'csuxzy',
?age: 18,
}
const v = obj.valueOf
console.log(v())
这时候this
值并不会自动绑定到 obj
对象上,而是默认绑定到全局对象或者 undefined
(在严格模式下)。
所以valueOf
的方式行不通,但是最起码我们有这种思路就已经成功了一大半。
那么既然走this
的方式行不通,我们能不能通过属性的方式来访问返回本身呢,这样就不需要this
了呀。显然是没有这种属性的。但是上面原型链查找给了我们思路。我们可以在原型上实现这样一个getter
属性
var o = (function() {
?const obj = {
? ?name: 'csuxzy',
? ?age: 18,
}
?
?return {
? ?get(key) {
? ? ?return obj[key]
? },
? ?isOld() {
? ? ?return obj.age > 18;
? }
}
})()
?
Object.defineProperty(Object.prototype, 'getObj', {
?get() {
? ?return this
}
})
?
o.get('getObj').name = 'hello world';
o.get('getObj').age = 19;
console.log(o.get('name')); // hello world
console.log(o.isOld()); // true
通过在原型上增加getObj
属性,他有一个访问器属性getter
。调用getObj
就会返回obj
本身。那么这里为什么不会有this
指向的问题呢。其实我们可以思考下,o.get('getObj')
其实就是obj.getObj
。所以这个this
指向的就是obj
。
至此,我们就通过原型链这个漏洞实现了更改闭包里面的属性。所以与其说是闭包的漏洞,不如说是js
的漏洞。
如果我们确实有这种需求我们应该怎么防范呢。
var o = (function() {
?const obj = {
? ?name: 'csuxzy',
? ?age: 18,
}
?
?Object.setPrototypeOf(obj, null) // 关键代码
?
?return {
? ?get(key) {
? ? ?return obj[key]
? },
? ?isOld() {
? ? ?return obj.age > 18;
? }
}
})()
?
Object.defineProperty(Object.prototype, 'getObj', {
?get() {
? ?return this
}
})
既然你是通过原型链注入的,那我直接断了原型链不就行了。这样我们通过o.get('getObj')
拿到的就是undefined
了。但是这种方法太过于暴力,会导致原型链上的所有方法我们都访问不了啦。所以有第二种方式。
var o = (function() {
?const obj = {
? ?name: 'csuxzy',
? ?age: 18,
}
?
?return {
? ?get(key) {
? ? ?if (obj.hasOwnProperty(key)) // 关键代码
? ? ?return obj[key]
? },
? ?isOld() {
? ? ?return obj.age > 18;
? }
}
})()
?
Object.defineProperty(Object.prototype, 'getObj', {
?get() {
? ?return this
}
})
我们可以通过hasOwnProperty
属性来只读去obj
上的属性,对于其他属性我们就不管了,这样也不会去原型链上面找,更加的优雅。
可以看到,这一个小小的问题包含了非常多的知识点,在平时写代码或者实现第三方库的时候对于这些空子我们都应该尽量的避免,保证业务的健壮性。