上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

[油猴脚本开发指南]监控对象初始化属性分析

[复制链接]
  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5087

    回帖

    5960

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5960

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2021-10-3 00:44:30 | 显示全部楼层 | 阅读模式

    前言

    这里依然算是一个番外篇,主要还是想领大家一起学习如何分析一段代码,养成良好的阅读习惯,为以后做铺垫

    适合学有余力的人。

    请注意这个代码是对方随手写的,不要拿着当正经函数用,可能存在多多少少的错误。

    代码来自: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的劫持,并且返回这个值。

    那么到这里我们已经拆解完了这个函数的所有内容,并且了解了大概的思想

    相信你对如何学习他人代码以及劫持的思路都进一步加深了。

    祝你写出越来越好的代码!

    结语

    撒花~

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    857

    回帖

    1356

    积分

    荣誉开发者

    积分
    1356

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2021-10-3 10:04:07 | 显示全部楼层
    这个其实是我随手写的,最好不要直接拿来用,还有Object.defineProperties的情况没有考虑。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2023-2-28 23:59
  • 签到天数: 191 天

    [LV.7]常住居民III

    620

    主题

    5087

    回帖

    5960

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    5960

    荣誉开发者管理员油中2周年生态建设者喜迎中秋

    发表于 2021-10-3 11:03:52 | 显示全部楼层
    cxxjackie 发表于 2021-10-3 10:04
    这个其实是我随手写的,最好不要直接拿来用,还有Object.defineProperties的情况没有考虑。 ...

    标注了哥哥
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表