跳转到内容

通向地狱的 ES1995

JavaScript 是一门极其灵活的语言,它基于原型链构建自身的对象系统。JS 的运行时像一个繁忙的自由市场。

正因为我们开发者永远无法得知用户会使用多么久远的浏览器,所以能够在运行时改造各种语言构件(变量,类,方法)是一件极其重要的事情。我们不但可以在运行中修改类生成的对象,我们还可以修改标准库中的对象。

增加标准库方法对于 JS 开发者来说稀松平常。之前我们会利用该方案加强功能,而现在我们利用这种方法为浏览器抹平差异(有些情况下无法抹平)。

ES1995 是一个不错的方法集成。

实例对比

我们尝试比对一下代码和 ES1995 来看一看。

Fancy FizzBuzz

对比

Number.range(1, 101)
.map(
Function.conditional([
// 15 === Number.leastCommonMultiple(3, 5)
[(n) => n.multipleOf(15), () => "FizzBuzz"],
[(n) => n.multipleOf(5), () => "Buzz"],
[(n) => n.multipleOf(3), () => "Fizz"],
[Function.true, Function.identity]
])
)
.join(", ")
.pipe(console.log);

生成唯一 key

对比

const count = Function.from({
state: 0,
[Symbol.callable]() {
this.state += 1;
return this.state;
}
});
count().pipe(console.log);
count().pipe(console.log);
count().pipe(console.log);

数字对象

对比

const n = -23.47;
const [s, i, f] = [n.sign(), n.integerPart(), n.fractionalPart()];
const m = s * (i + f);
console.assert(n === m);

数组方法

const suits = "♠♥♦♣".split("");
const ranks = [...Number.range(2, 11), ..."JQKA".split("")];
let deck = Array.cartesianProduct(suits, ranks).map((card) => card.join(""));
// Fisher-Yates + random cut
deck = deck.shuffle().rotate(Number.random(0, deck.length));
const players = ["Douglas Crockford", "Marc Andreessen", "John-David Dalton"];
let playersCards;
[playersCards, deck] = deck.splitAt(2 * players.length);
playersCards = Array.zip(...playersCards.chunk(players.length));
const hands = Object.fromEntries(players.zip(playersCards));
let flop, turn, river;
[flop, deck] = deck.drop(1).splitAt(3);
[turn, deck] = deck.drop(1).splitAt(1);
[river, deck] = deck.drop(1).splitAt(1);
const game = {
hands,
community: { flop, turn, river }
};
console.log(game);

合并排序

对比

const mergeSort = (L) =>
L.length <= 1
? L
: L.splitAt(L.length / 2)
.map(mergeSort)
.pipe((L) => merge(...L));
const merge = Function.conditional([
[(A, B) => A.empty() || B.empty(), (A, B) => A.concat(B)],
[([a], [b]) => a < b, ([a, ...A], B) => [a, ...merge(A, B)]],
[Function.true, (A, B) => merge(B, A)] // ba-dum-ts
]);
Number.range(10).shuffle().pipe(mergeSort).pipe(console.log);

字符串模糊匹配

const names = [
"Timothée",
"Beyoncé",
"Penélope",
"Renée",
"Clémence",
"Zoë",
"Chloë",
"Øyvind",
"Žofia",
"Michał",
"Clémentine"
];
const searchTerm = "cle";
names
.map((name) => name.removeDiacritics().toLowerCase())
// Sørensen–Dice coefficient: 0.0 – 1.0
.map((safeName) => searchTerm.similarityTo(safeName))
.zip(names)
.sorted(([a], [b]) => b - a)
.take(3)
.pipe(console.log);
// [0.4444444444444444, "Clémence"]
// [0.36363636363636365, "Clémentine"]
// [0, "Timothée"]

函数对象

const fetchArticle = (id) => {
// get the latest hot shit from Hacker News
};
const fetchArticleOnlyOnce = fetchArticle.memoize();
const onResizeWindow = () => {
// recalculate expensive layout
};
const smartOnResizeWindow = onResizeWindow.debounce(150);
const onClick = () => {
// http://clickclickclick.click
};
const rateLimitedOnClick = onClick.throttle(1000);
const add = (a, b) => a + b;
const add10 = add.partial(10);

源码分析

import {
cond,
constant,
filter,
includes,
intersection,
zip,
range,
inRange,
partition,
shuffle,
clone,
cloneDeep,
ceil,
round,
floor,
clamp,
random,
groupBy,
partial,
deburr,
identity,
noop,
memoize,
once,
compact,
throttle,
debounce,
chunk,
drop,
head,
tail,
isFunction
} from "lodash-es";
import dedent from "dedent";
import mdlog from "mdlog";
import colorScheme from "mdlog/color/solarized-dark.json";
import { compareTwoStrings } from "string-similarity";
const log = mdlog(colorScheme);
// console.log(mdlog.convert("aloha **bold** mu", colorScheme));
const Documentation = Symbol.for("documentation");
function pipe(func) {
return func(this);
}
pipe[Documentation] = `
# Object.prototype.pipe
Usage:
"hello world".pipe(s => s.toUpperCase())
`;
/*
Object
*/
const ObjectPrototype = {
pipe
};
const ObjectObject = {
clone,
cloneDeep
};
/*
Array
*/
const ArrayPrototype = {
at(n) {
if (Array.isArray(n)) {
return n.map((i) => this.at(i));
}
n = Math.trunc(n) || 0;
if (n < 0) n += this.length;
if (n < 0 || n >= this.length) {
return undefined;
}
return this[n];
},
chunk(size) {
return chunk(this, size);
},
compact() {
return compact(this);
},
distinct() {
return [...new Set(this)];
},
drop(n) {
return drop(this, n);
},
duplicates() {
return filter(this, (val, i, iteratee) => includes(iteratee, val, i + 1));
},
empty() {
return this.length === 0;
},
except(toRemove) {
return this.filter((el) => !toRemove.includes(el));
},
groupBy(iteratee) {
return groupBy(this, iteratee);
},
head() {
return head(this);
},
intersect(other) {
return intersection(this, other);
},
partition(predicate) {
return partition(this, predicate);
},
reversed() {
return [...this].reverse();
},
rotate(n) {
return this.slice(n, this.length).concat(this.slice(0, n));
},
shuffle() {
return shuffle(this);
},
sorted(comparator) {
return this.slice(0).sort(comparator);
},
splitAt(n) {
const i = Math.trunc(n) || 0;
return [this.slice(0, i), this.slice(i)];
},
tail() {
return tail(this);
},
take(count) {
return this.slice(0, count);
},
tap(func) {
this.forEach(func);
return this;
},
zip(...arrays) {
return zip(this, ...arrays);
}
};
const ArrayObject = {
cartesianProduct(...a) {
return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));
},
zip
};
/*
String
*/
const StringPrototype = {
removeDiacritics() {
return deburr(this);
},
dedent() {
return dedent(this);
},
similarityTo(string) {
return compareTwoStrings(this, string);
}
};
const StringObject = {};
/*
Number
*/
const NumberPrototype = {
absoluteValue() {
return Math.abs(this);
},
ceil(precision) {
return ceil(this, precision);
},
clamp(lower, upper) {
return clamp(this, lower, upper);
},
multipleOf(k) {
return Number.isInteger(this / k);
},
floor(precision) {
return floor(this, precision);
},
fractionalPart() {
return parseFloat("0." + (this + "").split(".")[1]);
},
integerPart() {
return Math.abs(Math.trunc(this));
},
inRange(start, end) {
return inRange(this, start, end);
},
round(precision) {
return round(this, precision);
},
sign() {
return Math.sign(this);
}
};
const NumberObject = {
greatestCommonDivisor: (a, b) => {
a = Math.abs(a);
b = Math.abs(b);
while (a !== b) {
if (a > b) {
a -= b;
} else {
b -= a;
}
}
return a;
},
leastCommonMultiple: (a, b) => {
return Math.abs(a * b) / Number.greatestCommonDivisor(a, b);
},
random,
range
};
/*
Function
*/
const originalFunctionToString = Function.prototype.toString;
const FunctionPrototype = {
debounce(wait, options) {
return debounce(this, wait, options);
},
memoize(resolver) {
return memoize(this, resolver);
},
once() {
return once(this);
},
partial(...partials) {
return partial(this, ...partials);
},
throttle(wait, options) {
throttle(this, wait, options);
},
toString() {
if (this[Documentation]) {
log(dedent(this[Documentation]));
return originalFunctionToString.call(this);
} else {
return originalFunctionToString.call(this);
}
}
};
const createLambda = (expression) => {
const regexp = new RegExp("[$]+", "g");
let maxLength = 0;
let match;
// eslint-disable-next-line
while ((match = regexp.exec(expression)) != null) {
let paramNumber = match[0].length;
if (paramNumber > maxLength) {
maxLength = paramNumber;
}
}
const argArray = [];
for (let i = 1; i <= maxLength; i++) {
let dollar = "";
for (let j = 0; j < i; j++) {
dollar += "$";
}
argArray.push(dollar);
}
const args = Array.prototype.join.call(argArray, ",");
// eslint-disable-next-line
return new Function(args, "return " + expression);
};
class CallableObject extends Function {
constructor(props) {
super();
return Object.assign(props[Symbol.for("callable")].bind(props), props);
}
}
const FunctionObject = {
constant,
conditional: cond,
false: () => false,
fixedPoint: (f) => {
const g = (h) => (x) => f(h(h))(x);
return g(g);
},
from: (arg, ...rest) => {
if (typeof arg === "string") {
return createLambda(arg);
}
if (Array.isArray(arg)) {
return arg.zip(rest).flat().compact().join("").pipe(createLambda);
}
if (arg[Symbol.for("callable")]) {
return new CallableObject(arg);
}
},
identity,
isFunction,
noop,
true: () => true
};
/*
Symbol
*/
const SymbolObject = {
callable: Symbol.for("callable"),
documentation: Symbol.for("documentation")
};
const RegexObject = {
email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
IPv4: /^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}$/
//url
};
/*
Effects
*/
const enrichMap = [
[Object.prototype, ObjectPrototype],
[Object, ObjectObject],
[Array.prototype, ArrayPrototype],
[Array, ArrayObject],
[String.prototype, StringPrototype],
[String, StringObject],
[Number.prototype, NumberPrototype],
[Number, NumberObject],
[Function.prototype, FunctionPrototype],
[Object.getPrototypeOf(() => {}), FunctionPrototype],
[Function, FunctionObject],
[Symbol, SymbolObject],
[RegExp, RegexObject]
];
const toPropertyDescriptorMap = (propertyObject) =>
Object.fromEntries(
Object.entries(propertyObject).map(([key, value]) => [key, { value }])
);
enrichMap.forEach(([o, po]) =>
Object.defineProperties(o, toPropertyDescriptorMap(po))
);

当然,为什么我们不在为了简单的实现方式再次修改 prototype 了呢?

原因当然也是非常简单,因为我们无法预见将来的 js 会不会增加该方法,而 JS 经过那么多年的发展,也逐渐稳定起来。

之前的 container 方法也是因为这种原因而无法使用,反而变为了 includes。

但是对于修改 prototype 究竟是踏入地狱的一步还是?