读懂 webpack 混淆代码:(0, fn)() 模式与 RxJS 链解析

(0, fn)(...) 是什么

这是 webpack / Babel 生成代码中常见的间接调用写法:

(0, s.p)(args)

等价于:

const _fn = s.p;
_fn(args);        // 普通函数调用,this === undefined(严格模式)

为什么不直接 s.p(args)

s.p(args) 会将 this 绑定为 s;逗号运算符写法 (0, s.p) 会把方法”脱离”对象,变成普通函数调用,this 不再指向 s

这在以下场景有实际意义:

  • s.p 是从模块导入的函数(如 rxjs.from),不应绑 this
  • 打包工具为了兼容 ES Modules 的 strict mode 语义,确保 this === undefined
  • Tree-shaking 友好:逗号写法对某些打包器更易分析

常见形式:

(0, rxjs.of)(1, 2, 3)           // 等价 rxjs.of(1, 2, 3)
(0, lodash.map)(arr, fn)         // 等价 lodash.map(arr, fn)
(0, react.createElement)(...)    // 等价 react.createElement(...)
(0, o.Z)(list)                   // 混淆后的模块导出函数

读到这种代码,直接把 (0, expr) 去掉,剩下的 expr(...) 就是实际调用。

真实打包代码示例

下面是一段典型的混淆打包代码(来自实际页面逆向):

(0, s.p)(
    (t = window.$).when.apply(t,
        (0, o.Z)(window.beforeSubmitQueue.map(function(e) {
            return e();
        }))
    )
).switchMap(
    window.MarkLib.submitMarkAnswer.bind(window.MarkLib, !0, e)
).filter(function(t) {
    var n = t.fire_status, r = void 0 === n || n;
    var a = t.reject,   o = void 0 !== a && a;
    return !e && w(!1), r && !o;
}).map(JSON.stringify.bind(JSON)).do(function(t) {
    return y({
        type: "markpage/saveAnswer",
        payload: { isTemporary: 1, packetId: Number(p), ... }
    });
}).subscribe();

逐步还原

第一步:去掉 (0, expr) 包装

s.p(...)         // s.p 可能是 rxjs.from / rxjs.fromPromise
o.Z(list)        // o.Z 是某个导出函数,根据上下文判断

第二步:提取 window 上的对象引用

const { $, beforeSubmitQueue, MarkLib } = window;

第三步:还原 .apply.bind

// (t = window.$).when.apply(t, args) → $.when(...args)
// fn.bind(obj, true, e) → () => obj.fn(true, e)

第四步:改写旧版 RxJS API

旧版新版
.do(fn).tap(fn)
fromPromise(p)from(p)
.map(fn).map(fn) 不变

还原结果

const { $, beforeSubmitQueue, MarkLib } = window;

from($.when(...beforeSubmitQueue.map(fn => fn())))
    .pipe(
        switchMap(() => MarkLib.submitMarkAnswer(true, e)),

        filter(({ fire_status = true, reject = false }) => {
            if (!e) w(false);
            return fire_status && !reject;
        }),

        map(JSON.stringify),

        tap(answer => {
            y({
                type: "markpage/saveAnswer",
                payload: {
                    isTemporary: 1,
                    packetId: Number(p),
                    pageId: Number(a),
                    round: f,
                    stage: k,
                    answer: j(answer, c, u)
                },
                callback() { if (!e) w(false); }
            });
        })
    )
    .subscribe();

逻辑说明

还原后的业务逻辑只有一句话:

执行提交前钩子(beforeSubmitQueue)→ 提交答案(submitMarkAnswer)→ 过滤无效结果 → 序列化 → 保存到 Redux store

关键点:

  • $.when(...tasks):jQuery Deferred,等待所有 before-hooks 完成
  • switchMap:前一个 Observable 完成后切换到新的,丢弃之前未完成的
  • filterfire_status 不为 false 且 reject 不为 true 才继续
  • .subscribe():没有 subscribe,RxJS 链不会执行(冷 Observable)

其他常见混淆模式

// void 0 === undefined
void 0                // → undefined

// !! 转布尔
!!value               // → Boolean(value)

// 逗号运算符返回最后一个值
(a(), b(), c())       // 执行 a、b、c,返回 c 的值

// 短路赋值
x = void 0 === x ? defaultVal : x   // → x ?? defaultVal(现代写法)

读打包代码时,把这些模式认出来后,理解代码逻辑会快很多。