这个法子是 jQuery 之父发明的,最开始只支持匹配函数参数数量,稍作修改后现在支持同时匹配函数参数类型。

↑ 这句话具有极强的筛选作用,这样知道我在说啥的就会关掉这个页面而不会浪费人生中宝贵的 10 分钟

本文涉及知识点:

  • 多层闭包
  • applythis
  • ES6 Symbol

0x00 Implementation

0x00.1 主体代码

跑通本段代码需要你的运行时(Runtime)支持 ES6。

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"use strict";

/**
* @description fallback function
* @throws ReferenceError
* @returns {never}
*/
function invalidArgumentsHandler() {
throw new ReferenceError("There's no function that matches the length and types of arguments you provided.");
}
invalidArgumentsHandler.__count = 0;

/**
* @param {*} obj
* @returns {String} the type of `obj`, in lower case
*/
function getType(obj) {
if(obj === null)
return "null";
if(obj === undefined)
return "undefined";
if(obj.constructor && obj.constructor.name)
return obj.constructor.name.toLowerCase();
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}

/**
* @description define `attr` method of `obj` as a flex-length function.
* @param {Object} obj The object to operate on.
* @param {String} attr The method name of `obj` for `func`
* @param {Function} func
* @returns {Object} the modified obj
*/
function defineFlexMethod(obj, attr, func, argtype) {
if(getType(argtype) === "undefined") {
argtype = new Array(func.length);
for(let i = 0; i < func.length; i++) {
argtype[i] = "any";
}
}
if(typeof func !== "function") {
throw new TypeError("The third argument should be a function!");
}
if(getType(argtype) !== "array") {
throw new TypeError("The fourth argument should be an array, if specified!");
}
if(argtype.length !== func.length) {
throw new RangeError("Array's length should be equal to the provided function's args count!");
}
let prevMethod = obj[attr];
if(!["function", "undefined"].includes(getType(prevMethod))) {
throw new TypeError(`Non-function-type attribute ${attr} already exists!`);
}
if(prevMethod === undefined) {
// Set the default fallback function
prevMethod = invalidArgumentsHandler;
} else {
if(!("__count" in prevMethod)) {
prevMethod.__count = 0;
} else {
if(getType(prevMethod.__count) !== "number") {
throw new TypeError(`attribute ${attr} already has __count attribute, and it is not a number!`);
}
}
}
obj[attr] = function () {
/* This function is generated by defineFlexMethod() */
if(arguments.length === func.length) {
for(let i = 0; i < arguments.length; i++) {
if(argtype[i] !== "any" && getType(arguments[i]) !== argtype[i]) {
return prevMethod.apply(obj, arguments);
}
}
return func.apply(obj, arguments);
} else {
return prevMethod.apply(obj, arguments);
}
};
obj[attr].__count = prevMethod.__count + 1;
return obj;
}

0x00.2 Example Usage

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
const db = {
__data: ["John Doe", "Chekra Ben", "Oh Man"]
};
function insert(person) {
db.__data.push(person);
}
function find0() { // has no arguments
return this.__data;
}
function find1(prefix) { // has 1 argument
return this.__data.filter(name => name.startsWith(prefix));
}
function find2(prefix, suffix) { // has 2 arguments
return this.__data.filter(name =>
name.startsWith(prefix) && name.endsWith(suffix)
);
}
defineFlexMethod(db, "find", find0);
defineFlexMethod(db, "find", find1);
defineFlexMethod(db, "find", find2);

db.find(); // -> [ 'John Doe', 'Chekra Ben', 'Oh Man' ]
db.find("J"); // -> [ 'John Doe' ]
db.find("O", "n"); // -> [ 'Oh Man' ]

insert("New Person");

db.find("N"); // -> ['New Person']

0x01 Explanation

0x01.1 invalidArgumentsHandler

1
2
3
4
5
6
7
8
9
/**
* @description fallback function
* @throws ReferenceError
* @returns {never}
*/
function invalidArgumentsHandler() {
throw new ReferenceError("There's no function that matches the length and types of arguments you provided.");
}
invalidArgumentsHandler.__count = 0;

invalidArgumentsHandler 是当传入的函数参数类型和值与已定义的无法匹配时将会调用的函数。它将抛出一个 ReferenceError

随后,将 invalidArgumentsHandler__count 属性赋值为 0。该值表示 invalidArgumentsHandler 是第 0 层重载。

0x01.2 getType

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @param {*} obj
* @returns {String} the type of `obj`, in lower case
*/
function getType(obj) {
if(obj === null)
return "null";
if(obj === undefined)
return "undefined";
if(obj.constructor && obj.constructor.name)
return obj.constructor.name.toLowerCase();
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}

此函数用于获得传入的参数的类型。注意,其表现和 typeof 运算符并不相同。getType 支持对用户自定义的类进行基于类名的类型判断。

由于 nullundefined 这俩货既不能是 in 操作符的右值,又不能用 null.attribute 来取属性,所以先单独把它俩搞定了再用 general 的方法对付其它的。

obj.constructor.name.toLowerCase() 可以获得 obj 的类名。如果 obj 没有 constructor 属性,那么就使用通用的 toString 方法来获得类名。

(进阶内容)注意,尽管用户可能使用 ES6 中引入的 Symbol 来影响 Object.prototype.toString.call(obj) 的结果:

1
2
3
4
const example = {
[Symbol.toStringTag]: "test"
};
example.toString(); // [object test]

但是 obj.constructor.name 仍然会是原本的 "Object"

然而,用户仍然可以通过特殊的方式来影响 getType

1
2
3
4
const example2 = Object.create(null);
example2[Symbol.toStringTag] = "test";
"constructor" in example2; // false
({}).toString.call(example2); // [object test]

不过这种 Corner Case 如果出现在用户的代码里,那么用户必然是有意为之的。(否则谁会同时使用 Object.create(null)[Symbol.toStringTag] 呢?)

0x01.3 defineFlexMethod

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
/**
* @description define `attr` method of `obj` as a flex-length function.
* @param {Object} obj The object to operate on.
* @param {String} attr The method name of `obj` for `func`
* @param {Function} func
* @returns {Object} the modified obj
*/
function defineFlexMethod(obj, attr, func, argtype) {
if(getType(argtype) === "undefined") {
argtype = new Array(func.length);
for(let i = 0; i < func.length; i++) {
argtype[i] = "any";
}
}
if(typeof func !== "function") {
throw new TypeError("The third argument should be a function!");
}
if(getType(argtype) !== "array") {
throw new TypeError("The fourth argument should be an array, if specified!!");
}
if(argtype.length !== func.length) {
throw new RangeError("The array length should equals to the expected arguments length of the given function!");
}
let prevMethod = obj[attr];
if(!["function", "undefined"].includes(getType(prevMethod))) {
throw new TypeError(`Non-function-type attribute ${attr} already exists!`);
}
if(prevMethod === undefined) {
// Set the default fallback function
prevMethod = invalidArgumentsHandler;
} else {
if(!("__count" in prevMethod)) {
prevMethod.__count = 0;
} else {
if(getType(prevMethod.__count) !== "number") {
throw new TypeError(`attribute ${attr} already has __count attribute, and it is not a number!`);
}
}
}
obj[attr] = function () {
/* This function is generated by defineFlexMethod() */
if(arguments.length === func.length) {
for(let i = 0; i < arguments.length; i++) {
if(argtype[i] !== "any" && getType(arguments[i]) !== argtype[i]) {
return prevMethod.apply(obj, arguments);
}
}
return func.apply(obj, arguments);
} else {
return prevMethod.apply(obj, arguments);
}
};
obj[attr].__count = prevMethod.__count + 1;
return obj;
}

传入参数检测

  • 如果未指明 argType 参数,则将 argType 初始化为长度为 func.length 且每项均为 "any" 的数组
  • 第三个参数 func 必须是函数
  • 如果传入了 argType,则必须是个数组,其长度必须等于 func.length

旧值检测

  • 如果 obj 对象已经有了 attr 属性,则其必须是函数(也即“方法”)。
  • 如果 obj 对象已经有了 attr 方法,且该方法有 __count 属性,则此属性必须是数字。
  • 如果 obj 对象已经有了 attr 方法,但该方法没有 __count 属性,则初始化此属性为 0。
  • 如果 obj 对象没有 attr 属性,则初始化其为 invalidArgumentsHandler

包装新方法

将新函数包装一层,然后赋值给 obj[attr]。包装后的函数将形成一个闭包,并持有对 funcprevMethod 的引用(以避免它们被回收、且保有在适当时调用此二者的能力)。

令包装函数的 __count 为旧函数的 __count + 1

使用新方法

  • 调用该方法时,判断得到的参数的类型和数量。
    • 如果和用户最后一次定义时提供的 func 的参数类型和数量匹配,则指定 obj 为运行时 this 并使用得到的参数调用 func(注意 apply 的使用)。
    • 否则,调用 prevMethodprevMethod 即为上一轮包装的新方法,它也将进行同样的判断。
    • 以此类推,直到有某一轮定义的 func 的参数类型和数量与给出的参数类型和数量匹配为止。
      • 如果所有的 func 都无法与给出的参数类型和数量匹配,则会最终调用 invalidArgumentsHandler,抛出异常。

来源:https://blog.jiejiss.com/