1. 简介

下面是一个常见的js函数:

1
2
3
function a() {console.log("a")}
function b() {console.log("a")}
function c() {console.log("a")}

而我们在网站中,常看到:

1
2
3
4
5
6
7
8
9
10
!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) {}
]);

上面的代码,我们对其进行简化:

1
2
3
4
5
6
7
8
9
10
!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)},
]);

运行上面代码,输出:moddul0:hello world!

上面主要用到了!function(){}()function.call()

2. 函数的声明和表达式

创建函数,有两种方法:

  • 函数声明
  • 函数表达式

2.1 函数声明

函数声明是通过使用 function 关键字后跟函数名称的方式来创建函数。函数声明会被提升到所在作用域的顶部,可以在声明之前调用。

1
2
3
4
5
6
test("Hello World!") // 函数声明之前调用

// 函数声明
function test(arg) {
console.log(arg)
}

2.2 函数表达式

函数表达式: 函数表达式是将函数赋值给变量或属性的方式来创建函数。函数表达式不会被提升,只能在赋值之后调用。

1
2
3
4
5
var test = function (arg) {
console.log(arg)
}

test("Hello World!")

函数表达式也可以使用箭头函数来定义:

1
2
3
var myFunction = () => {
// 函数体
};

函数表达式还可以作为立即执行函数(Immediately Invoked Function Expression,IIFE)使用:

1
2
3
(function() {
// 函数体
})();

需要注意的是,在使用函数表达式时,需要记住使用分号作为语句的结束符,以避免可能的错误。

2.3 立即执行函数(IIEF)

函数表达式创建以后立即执行,当函数变成立即执行表达式时,表达式中的变量不能从外部访问。IIFE主要用来隔离作用域,避免污染。

2.3.1 常见格式

说明: 我们将函数简写为SH,即

1
2
3
function () {
console.log("I AM IIFE")
}
  1. 匿名函数前面加上一元操作符,后面加上():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
!function () {
console.log("I AM IIFE")
}();

-function () {
console.log("I AM IIFE")
}();

+function () {
console.log("I AM IIFE")
}();

~function () {
console.log("I AM IIFE")
}();

简写:

1
!SH();
  1. 匿名函数后面加上(),然后再用()将整个括起来:
1
2
3
(function () {
console.log("I AM IIFE")
}());

简写:

1
(SH());
  1. 先用 () 将匿名函数括起来,再在后面加上 ()
1
2
3
(function () {
console.log("I AM IIFE")
})();

简写:

1
(SH)();
  1. 使用箭头函数表达式,先用()将箭头函数表达式括起来,再在后面加上():
1
2
3
(() => {
console.log("I AM IIFE")
})()

简写:

1
(S=>H)();
  1. 匿名函数前面加上 void 关键字,后面加上 ()void 指定要计算或运行一个表达式,但是不返回值:
1
2
3
void function () {
console.log("I AM IIFE")
}();

有时候,还能看到立即执行函数前后分号的情况,如:

1
2
3
4
5
6
7
;(function () {
console.log("I AM IIFE")
}())

;!function () {
console.log("I AM IIFE")
}()

有效于前面的代码隔离,否则可能会出现意外的情况。

2.3.2 参数传递

将参数放在末尾即可实现参数传递:

eg1:

1
2
3
4
5
6
7
var text = "I AM IIFE";

(function (param) {
console.log(param)
})(text);

// I AM IIFE

eg2:

1
2
3
4
5
6
7
var dict = {name: "Bob", age: "20"};

(function () {
console.log(dict.name);
})(dict);

// Bob

eg3:

1
2
3
4
5
6
7
8
9
10
11
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

2.4 call() / apply() / bind()

他们都是比较常用的方法,作用一模一样,即,改变函数中的this指向,区别:

  • call() 方法会立即执行这个函数,接受一个或多个参数,参数之间用逗号隔开;
  • apply()方法会立即执行这个函数,接受一个包含多个参数的数组;
  • bind()方法不会立即执行这个函数,返回一个修改后的函数,便于稍后调用,接受的参数和call一样。

2.4.1 call()

call() 方法接受多个参数,第一个参数 thisArg 指定了函数体内 this 对象的指向,如果这个函数处于非严格模式下,指定为 null 或 undefined 时会自动替换为指向全局对象(浏览器中就是 window 对象),在严格模式下,函数体内的 this 还是为 null。从第二个参数开始往后,每个参数被依次传入函数,基本语法如下:

1
function.call(thisArg, arg1, arg2, ...)

eg1:

1
2
3
4
5
function test(a, b, c) {
console.log(a + b + c)
}

test.call(null, 1, 2, 3) // 6

eg2:

1
2
3
4
5
6
function test() {
console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.call(data) // John Doe

2.4.2 apply()

apply() 方法接受两个参数,第一个参数 thisArg 与 call() 方法一致,第二个参数为一个带下标的集合,从 ECMAScript 第5版开始,这个集合可以为数组,也可以为类数组,apply() 方法把这个集合中的元素作为参数传递给被调用的函数,基本语法如下:

1
function.apply(thisArg, [arg1, arg2, ...])

eg1:

1
2
3
4
5
function test(a, b, c) {
console.log(a + b + c)
}

test.apply(null, [1, 2, 3]) // 6

eg2:

1
2
3
4
5
6
function test() {
console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.apply(data) // John Doe

2.4.3 bind()

bind() 方法和 call() 接受的参数是相同的,只不过 bind() 返回的是一个函数,基本语法如下:

1
function.bind(thisArg, arg1, arg2, ...)

eg1:

1
2
3
4
5
function test(a, b, c) {
console.log(a + b + c)
}

test.bind(null, 1, 2, 3)() // 6

eg2:

1
2
3
4
5
6
function test() {
console.log(this.firstName + " " + this.lastName)
}

var data = {firstName: "John", lastName: "Doe"}
test.bind(data)() // John Doe

2.5 重新认识webpack

我们重新看前面webpack中的函数的写法:

1
2
3
4
5
6
7
8
9
10
!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立即执行函数,传递一个数组,里面包含三个方法,视为三个模块,IIFE里面包含一个useModule()函数,将其视为模块加载器,即需要用哪个模块,即调用哪个,例中表示调用第一个模块,函数里面使用call()方法改变函数中的this指向并传递参数,调用相应的模块,并输出。

2.6 动手做

下面是一段加密参数的webpack模块化写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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
}
]);

有了前面的基础,直接将其中的函数传递进行,改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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())

参考

[1] 爬虫逆向基础,理解 JavaScript 模块化编程 webpack - 掘金 (juejin.cn)