前言
这里依然算是一个番外篇,主要还是想领大家一起学习如何分析一段代码,养成良好的阅读习惯,为以后做铺垫
适合学有余力的人。
请注意这个代码是对方随手写的,不要拿着当正经函数用,可能存在多多少少的错误。
代码来自:cxxjackie
代码
function waitForProperty(obj, ...props) {
let _obj = obj;
let prop;
const realDP = Object.defineProperty;
const waitForValue = () => {
return new Promise(resolve => {
Object.defineProperty = function() {
if (arguments[0] === _obj && arguments[1] === prop) {
const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
Object.defineProperty = realDP;
resolve(value);
}
return realDP.apply(Object, arguments);
};
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: false,
get: () => undefined,
set: value => {
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
Object.defineProperty = realDP;
resolve(value);
}
});
});
};
return new Promise(async (resolve, reject) => {
while (props.length > 0) {
prop = props.shift();
if (prop in _obj) {
_obj = _obj[prop];
} else {
_obj = await waitForValue();
}
if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
return reject(`The property '${prop}' is not a function or object.`);
}
}
resolve(_obj);
});
}
希望大家跟着我的思路一步一步读。
function waitForProperty(obj, ...props)
首先传入了两个参数,一个obj,一个props,收集了除了第一个参数以外的所有参数。
调用方式await waitForProperty(unsafeWindow, 'a', 'b', 'c')
接下来我们继续看
let _obj = obj;
let prop;
这里创建了一个_obj作为obj的引用,为接下来做铺垫,prop根据名字和下文可以知道是单个属性的变量。
const realDP = Object.defineProperty;
保存defineProperty的引用。为接下来劫持做铺垫。
const waitForValue = xxxx
这里是一个函数,我们因为还没有调用到,所以先不看。继续往下走
return new Promise(async (resolve, reject) => {
while (props.length > 0) {
prop = props.shift();
if (prop in _obj) {
_obj = _obj[prop];
} else {
_obj = await waitForValue();
}
if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
return reject(`The property '${prop}' is not a function or object.`);
}
}
resolve(_obj);
});
这里函数最后返回了一个promise,这个promise内是一个async函数,是async函数是因为其内部有await阻塞promise。
通过while进行循环,判断props.length是否大于0,如果大于0则一直循环
prop接收props弹出的一个参数,每次shift函数调用都会弹出一个,并且props中少一个参数。
判断prop是否存在于_obj中,如果存在,则将_obj变为_obj[属性]。
这里是为了逐步往下递归,比如我之前传入了a,b,c和unsafewindow,是为了找到unsafewindow.a.b.c
逐步往下走,这里我们把unsafewindow.a变为了_obj,接下来继续判断b存不存在_obj上就可以了。
如果存在则进行了_obj的赋值。然后对其进行判断
if (props.length > 0 && typeof _obj !== 'function' && typeof _obj !== 'object') {
return reject(`The property '${prop}' is not a function or object.`);
}
判断参数是否大于0,并且_obj不为函数以及对象,则直接返回一个错误。
这里是因为function和object都是一个对象,function函数也可以存储属性。
这里只对这两种进行处理,以外的属性会出现报错的情况。
接下来我们继续分析。
之前我们分析了存在的情况,现在我们来分析不存在的情况。
const waitForValue = () => {
return new Promise(resolve => {
Object.defineProperty = function() {
if (arguments[0] === _obj && arguments[1] === prop) {
const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
Object.defineProperty = realDP;
resolve(value);
}
return realDP.apply(Object, arguments);
};
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: false,
get: () => undefined,
set: value => {
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
Object.defineProperty = realDP;
resolve(value);
}
});
});
};
waitForValue是一个箭头函数,返回一个promise,方便我们进行阻塞并等待。
Object.defineProperty = function() {
if (arguments[0] === _obj && arguments[1] === prop) {
const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
Object.defineProperty = realDP;
resolve(value);
}
return realDP.apply(Object, arguments);
};
这里是对defineProperty进行了一个劫持,因为我们接下来也用了defineProperty监控了属性的修改,但是这有一个致命的弊病,就是没法对抗对方直接使用define进行赋值,所以我们干脆也对其进行劫持,并且判断属性,如果属性符合,则结束这个函数,如果属性不符合,则正常调用define函数
if (arguments[0] === _obj && arguments[1] === prop) {
const value = 'value' in arguments[2] ? arguments[2].value : arguments[2].get.call(_obj);
Object.defineProperty = realDP;
resolve(value);
}
这里第一个参数判断对象是否相等,第二个参数判断属性,如果都相等证明正在赋值这个属性的操作。
然后判断value这个属性是否存在于第三个对象中,我们都知道define是需要传入三个对象的,并且存在两种,一种是{value:xxx}另外一中是{get:function,set:function}
这里通过判断value属性存在,如果存在则返回参数的value属性返回给const value,如果不存在则调用第二个参数内的get函数,并将其this指向到__obj上,来获取函数返回的value内容,然后恢复define劫持,并通过resolve结束这个函数。
接下来是
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: false,
get: () => undefined,
set: value => {
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
Object.defineProperty = realDP;
resolve(value);
}
});
刚才那种是为了防止调用define来进行属性的设置,如果直接设置属性如window.a=666,则不会触发define的劫持函数。为了监控这种直接设置属性,我们使用了define进行监控,由于刚才对define进行了劫持,所以这里我们用define的原函数进行设置。因为我们获取的是defineProperty的函数引用,之前我们调用的是Object.defineProperty,他的this是有.前边的对象来决定的,也就是Object,如果执行使用函数,则是defineProperty(),并没有点,这时候的this对象则是window,所以我们需要通过call传入Object来设置this。
接下来设置对象,属性,来进行监控。
设置了configurable和enumerable,前者代表可删除,后者代表可以被枚举到。
因为属性没有被初始化,所以一旦读取这个属性要返回undefined。
如果属性被set触发了,说明某个地方在设置这个属性的值,这时候我们再继续处理。
realDP.call(Object, _obj, prop, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
Object.defineProperty = realDP;
resolve(value);
这里依然与之前差不多,不同的是这里我们设置了很多属性
configurable设置了可删除,enumrable设置了可枚举,writable设置了可写,然后value将我们set的值设置到对应的属性上,接下来我们已经完成了操作并且得到了value,我们恢复define的劫持,并且返回这个值。
那么到这里我们已经拆解完了这个函数的所有内容,并且了解了大概的思想
相信你对如何学习他人代码以及劫持的思路都进一步加深了。
祝你写出越来越好的代码!
结语
撒花~