爬虫逆向基础,理解 JavaScript 模块化编程 webpack

关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶、JS/安卓逆向等技术干货!
简介
在分析一些站点的 JavaScript 代码时,比较简单的代码,函数通常都是一个一个的,例如:
function a() {console.log("a")}
function b() {console.log("a")}
function c() {console.log("a")}
但是稍微复杂一点的站点,通常会遇到类似如下的代码结构:
!function(i) {
function n(t) {
return i[t].call(a, b, c, d)
}
}([
function(t, e) {},
function(t, e, n) {},
function(t, e, r) {},
function(t, e, o) {}
]);
这种写法在 JavaScript 中很常见,对于熟悉 JavaScript 的人来说可能非常简单,但是爬虫工程师大多数都是用 Python 或者 Java 来写代码的,看到这种语法就有可能懵了,由于在剥离 JS 加密代码时会经常遇到,所以理解这种语法对于爬虫工程师来说是非常重要的。
这种写法貌似没有官方的名称,相当于进行了模块化编程,因此大多数人称其为 webpack,上面的示例看起来比较费劲,简单优化一下:
!function (allModule) {
function useModule(whichModule) {
allModule[whichModule].call(null, "hello world!");
}
useModule(0)
}([
function module0(param) {console.log("module0: " + param)},
function module1(param) {console.log("module1: " + param)},
function module2(param) {console.log("module2: " + param)},
]);
运行以上代码,会输出 module0: hello world!,相信通过浅显易懂的变量名和函数名,应该就可以看懂大致含义了,调用 useModule(0),从所有函数里选择第一个,将 hello world! 传递给 module0 并输出。
仔细观察以上代码,我们会发现主要用到了 !function(){}() 和 function.call() 语法,接下来就一一介绍一下。
函数声明与函数表达式
在 ECMAScript(JavaScript 的一个标准)中,有两个最常用的创建函数对象的方法,即使用函数声明或者函数表达式,ECMAScript 规范明确了一点,即函数声明必须始终带有一个标识符,也就是我们所说的函数名,而函数表达式则可以省略。
函数声明,会给函数指定一个名字,会在代码执行以前被加载到作用域中,所以调用函数在函数声明之前或之后都是可以的:
test("Hello World!")
function test(arg) {
console.log(arg)
}
函数表达式,创建一个匿名函数,然后将这个匿名函数赋给一个变量,在代码执行到函数表达式的时候才会有定义,所以调用函数在函数表达式之后才能正确运行,否则是会报错的:
var test = function (arg) {
console.log(arg)
}
test("Hello World!")
IIFE 立即调用函数表达式
IIFE 全称 Immediately-invoked Function Expressions,译为立即调用函数表达式,也称为自执行函数、立即执行函数、自执行匿名函数等,IIFE 是一种语法,这种模式本质上就是函数表达式(命名的或者匿名的)在创建后立即执行。当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。IIFE 主要用来隔离作用域,避免污染。
IIFE 基本语法
IIFE 的写法非常灵活,主要有以下几种格式:
1、匿名函数前面加上一元操作符,后面加上 ():
!function () {
console.log("I AM IIFE")
}();
-function () {
console.log("I AM IIFE")
}();
+function () {
console.log("I AM IIFE")
}();
~function () {
console.log("I AM IIFE")
}();
2、匿名函数后面加上 (),然后再用 () 将整个括起来:
(function () {
console.log("I AM IIFE")
}());
3、先用 () 将匿名函数括起来,再在后面加上 ():
(function () {
console.log("I AM IIFE")
})();
4、使用箭头函数表达式,先用 () 将箭头函数表达式括起来,再在后面加上 ():
(() => {
console.log("I AM IIFE")
})()
5、匿名函数前面加上 void 关键字,后面加上 (), void 指定要计算或运行一个表达式,但是不返回值:
void function () {
console.log("I AM IIFE")
}();
有的时候,我们还有可能见到立即执行函数前面后分号的情况,例如:
;(function () {
console.log("I AM IIFE")
}())
;!function () {
console.log("I AM IIFE")
}()
这是因为立即执行函数通常作为一个单独模块使用一般是没有问题的,但是还是建议在立即执行函数前面或者后面加上分号,这样可以有效地与前面或者后面的代码进行隔离,否则可能出现意想不到的错误。
IIFE 参数传递
将参数放在末尾的 () 里即可实现参数传递:
var text = "I AM IIFE";
(function (param) {
console.log(param)
})(text);
// I AM IIFE
var dict = {name: "Bob", age: "20"};
(function () {
console.log(dict.name);
})(dict);
// Bob
var list = [1, 2, 3, 4, 5];
(function () {
var sum = 0;
for (var i = 0; i < list.length; i++) {
sum += list[i];
}
console.log(sum);
})(list);
// 15
Function.prototype.call() / apply() / bind()
Function.prototype.call()、Function.prototype.apply()、Function.prototype.bind() 都是比较常用的方法。它们的作用一模一样,即改变函数中的 this 指向,它们的区别如下:
call()方法会立即执行这个函数,接受一个多个参数,参数之间用逗号隔开;apply()方法会立即执行这个函数,接受一个包含多个参数的数组;bind()方法不会立即执行这个函数,返回的是一个修改过后的函数,便于稍后调用,接受的参数和call()一样。
call()
call() 方法接受多个参数,第一个参数 thisArg 指定了函数体内 this 对象的指向,如果这个函数处于非严格模式下,指定为 null 或 undefined 时会自动替换为指向全局对象(浏览器中就是 window 对象),在严格模式下,函数体内的 this 还是为 null。从第二个参数开始往后,每个参数被依次传入函数,基本语法如下:
function.call(thisArg, arg1, arg2, ...)
示例:
function test(a, b, c) {
console.log(a + b + c)
}
test.call(null, 1, 2, 3) // 6
function test() {
console.log(this.firstName + " " + this.lastName)
}
var data = {firstName: "John", lastName: "Doe"}
test.call(data) // John Doe
apply()
apply() 方法接受两个参数,第一个参数 thisArg 与 call() 方法一致,第二个参数为一个带下标的集合,从 ECMAScript 第5版开始,这个集合可以为数组,也可以为类数组,apply() 方法把这个集合中的元素作为参数传递给被调用的函数,基本语法如下:
function.apply(thisArg, [arg1, arg2, ...])
示例:
function test(a, b, c) {
console.log(a + b + c)
}
test.apply(null, [1, 2, 3]) // 6
function test() {
console.log(this.firstName + " " + this.lastName)
}
var data = {firstName: "John", lastName: "Doe"}
test.apply(data) // John Doe
bind()
bind() 方法和 call() 接受的参数是相同的,只不过 bind() 返回的是一个函数,基本语法如下:
function.bind(thisArg, arg1, arg2, ...)
示例:
function test(a, b, c) {
console.log(a + b + c)
}
test.bind(null, 1, 2, 3)() // 6
function test() {
console.log(this.firstName + " " + this.lastName)
}
var data = {firstName: "John", lastName: "Doe"}
test.bind(data)() // John Doe
理解 webpack
有了以上知识后,我们再来理解一下模块化编程,也就是前面所说的 webpack 写法:
!function (allModule) {
function useModule(whichModule) {
allModule[whichModule].call(null, "hello world!");
}
useModule(0)
}([
function module0(param) {console.log("module0: " + param)},
function module1(param) {console.log("module1: " + param)},
function module2(param) {console.log("module2: " + param)},
]);
首先,这整个代码是一个 IIFE 立即调用函数表达式,传递的参数是一个数组,里面包含三个方法,分别是 module0、module1 和 module2,可以将其视为三个模块,那么 IIFE 接受的参数 allModule 就包含这三个模块,IIFE 里面还包含一个函数 useModule(),可以将其视为模块加载器,即要使用哪个模块,示例中 useModule(0) 即表示调用第一个模块,函数里面使用 call() 方法改变函数中的 this 指向并传递参数,调用相应的模块进行输出。
改写 webpack
对于我们爬虫逆向当中经常遇到的 webpack 模块化的写法,可以很容易对其进行改写,以下以一段加密代码为例:
CryptoJS = require("crypto-js")
!function (func) {
function acvs() {
var kk = func[1].call(null, 1e3);
var data = {
r: "I LOVE PYTHON",
e: kk,
i: "62bs819idl00oac2",
k: "0123456789abcdef"
}
return func[0].call(data);
}
console.log("加密文本:" + acvs())
function odsc(account) {
var cr = false;
var regExp = /(^\d{7,8}$)|(^0\d{10,12}$)/;
if (regExp.test(account)) {
cr = true;
}
return cr;
}
function mkle(account) {
var cr = false;
var regExp = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
if (regExp.test(account)) {
cr = true;
}
return cr;
}
}([
function () {
for (var n = "", t = 0; t < this.r.length; t++) {
var o = this.e ^ this.r.charCodeAt(t);
n += String.fromCharCode(o)
}
return encodeURIComponent(n)
},
function (x) {
return Math.ceil(x * Math.random())
},
function (e) {
var a = CryptoJS.MD5(this.k);
var c = CryptoJS.enc.Utf8.parse(a);
var d = CryptoJS.AES.encrypt(e, c, {
iv: this.i
});
return d + ""
},
function (e) {
var b = CryptoJS.MD5(this.k);
var d = CryptoJS.enc.Utf8.parse(b);
var a = CryptoJS.AES.decrypt(e, d, {
iv: this.i
}).toString(CryptoJS.enc.Utf8);
return a
}
]);
可以看到关键的加密入口函数是 acvs(),acvs() 里面又调用了 IIFE 参数列表里面的第一个和第二个函数,剩下的其他函数都是干扰项,而第一个函数中用到了 r 和 e 参数,将其直接传入即可,最终改写如下:
function a(r, e) {
for (var n = "", t = 0; t < r.length; t++) {
var o = e ^ r.charCodeAt(t);
n += String.fromCharCode(o)
}
return encodeURIComponent(n)
}
function b(x) {
return Math.ceil(x * Math.random())
}
function acvs() {
var kk = b(1e3);
var r = "I LOVE PYTHON";
return a(r, kk);
}
console.log("加密文本:" + acvs())
总结
看完本文后,你可能会觉得 webpack 也不过如此,看起来确实比较简单,但实际上我们在分析具体站点时往往不会像上述例子这么简单,本文旨在让大家简单理解一下模块化编程 webpack 的原理,后续 K 哥将会带领大家实战分析比较复杂的 webpack!敬请关注!

爬虫逆向基础,理解 JavaScript 模块化编程 webpack的更多相关文章
- Javascript模块化编程详解
在这篇文章中,我将会回顾一下js模块化编程的基础,并且将会讲到一些真的非常值得一提的进阶话题,包括一个我认为是我自创的模式. 模块化编程是一种非常常见Javascript编程模式.它一般来说可以使得代 ...
- 深入理解Javascript面向对象编程
深入理解Javascript面向对象编程 阅读目录 一:理解构造函数原型(prototype)机制 二:理解原型域链的概念 三:理解原型继承机制 四:理解使用类继承(继承的更好的方案) 五:建议使用封 ...
- Javascript模块化编程之难处
接着上一篇“Javascript模块化编程之Why”说起,Javascript担子重了之后程序也就复杂了.在大把语言都模块化编程的形势下,Javascript也不可能袖手旁观啊,毕竟这是一条经过实践检 ...
- 应用require.js进行javascript模块化编程小试一例
长久以来都渴望应用javascript的模块化编程.今日紧迫更甚,岁月蹉跎,已经不能再等了. 拜读阮一峰的有关文章已经好几遍,文章写得真好,简洁流畅,头头是道,自觉有点明白了.但经验告诉我们,一定要亲 ...
- Javascript模块化编程(三):require.js的用法
Javascript模块化编程(三):require.js的用法 原文地址:http://www.ruanyifeng.com/blog/2012/11/require_js.html 作者: 阮一峰 ...
- Javascript模块化编程(二):AMD规范
Javascript模块化编程(二):AMD规范 作者: 阮一峰 原文地址:http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_d ...
- Javascript模块化编程(一):模块的写法
Javascript模块化编程(一):模块的写法 作者: 阮一峰 原文链接:http://www.ruanyifeng.com/blog/2012/10/javascript_module.html ...
- Javascript模块化编程(二):AMD规范(转)
这个系列的第一部分介绍了Javascript模块的基本写法,今天介绍如何规范地使用模块. (接上文) 七.模块的规范 先想一想,为什么模块很重要? 因为有了模块,我们就可以更方便地使用别人的代码,想要 ...
- Javascript模块化编程(一):模块的写法(转)
随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂. 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者 ...
随机推荐
- 爱思助手备份 iPhone 时没有设置密码,恢复备份时需要密码的问题
i4.cn 备份时 iPhone 上登陆的 Apple ID 曾经设置过备份密码,这个密码就是恢复备份时需要输入的密码!
- vue引用 element-ui 的 el-aside -> el-menu -> el-menu-item 路由侧边栏跳转链接,接上动态路由
npm 下载 npm i element-ui -S Vue main.js //引入element-ui的包 import ElementUI from 'element-ui'; //引入el ...
- shell中的引号
单引号: 所见即所得 原封不动输出 双引号: 与单引号类似 特殊符号进行解析 ( $ $() `` ! ) 无引号: 与双引号类似 支持通配符( {} * ) 反引号: 优先执行 优先执行里面的命令, ...
- 2021秋 noip 模拟赛
9.9 T3 第负二题 \(f_i\) 的数学意义:中心在第 \(i\) 行的全 \(1\) 组成的最大正方形(对角线水平/竖直),对角线长 \(2f_i-1\). 显然 \(f_i\) 具有单调性( ...
- 安全系列之:跨域资源共享CORS
目录 简介 CORS举例 CORS protocol HTTP request headers HTTP response headers 基本CORS Preflighted requests 带认 ...
- 硕盟USB3.0 转RJ45千兆网卡 TYPE A USB3.0 TO RJ45
硕盟SM-A44是一款USB3.0转RJ45千兆网口转换器.这是一种高性能和低开销的解决方案.转换USB端口到10 / 100/ 1000M以太网端口可以让您的笔记本,台式机电脑能够通过USB接口连接 ...
- 【第二篇】- Maven 环境配置之Spring Cloud直播商城 b2b2c电子商务技术总结
Maven 环境配置 Maven 是一个基于 Java 的工具,所以要做的第一件事情就是安装 JDK. 如果你还未安装 JDK,可以参考我们的 Java 开发环境配置. 系统要求 项目 要求 JDK ...
- 重学VUE——vue 常用指令有哪些?
一.什么是指令? 在 vue 中,指令以 v- 开头,是一种特殊的自定义行间属性.指令属性的预期值是一个表达式,指令的职责就是:表达式的值改变时,相应地将某些行为应用到DOM上.只有v-for是一个类 ...
- php去除html标签
function cutstr_html($string){ $string = strip_tags($string); $string = preg_replace(["\t" ...
- 由浅入深了解cookie
什么是 Cookie "cookie 是存储于访问者的计算机中的变量.每当同一台计算机通过浏览器请求某个页面时,就会发送这个 cookie.你可以使用 JavaScript 来创建和取回 c ...