JavaScript 是一门极其灵活的语言,它基于原型链构建自身的对象系统。JS 的运行时像一个繁忙的自由市场。
正因为我们开发者永远无法得知用户会使用多么久远的浏览器,所以能够在运行时改造各种语言构件(变量,类,方法)是一件极其重要的事情。我们不但可以在运行中修改类生成的对象,我们还可以修改标准库中的对象。
增加标准库方法对于 JS 开发者来说稀松平常。之前我们会利用该方案加强功能,而现在我们利用这种方法为浏览器抹平差异(有些情况下无法抹平)。
ES1995 是一个不错的方法集成。
我们尝试比对一下代码和 ES1995 来看一看。
对比
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);
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 cutdeck = 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 究竟是踏入地狱的一步还是?