П'ятничний JS: як надихнутися Smalltalk'ом і потрапити в пекло

Коли я читав книгу «Патерни розробки ігор», написану чудовою людиною по імені Bob Nystrom (я не пишу його ім'я по-російськи, оскільки не маю ні найменшого поняття, як це вимовляється), в одній із глав мені на очі потрапила невелика ода мови Smalltalk як праотцу всіх сучасних об'єктно-орієнтованих мов, набагато випередив свій час. Оскільки я за життя відчуваю непоборну приязнь до всяких вінтажним мов, природно, я поліз про нього гуглити. І зрозуміло, замість того, щоб винести з цього досвіду щось корисне, я навчився поганому.



Особливість мови Smalltalk, за яку зачепився мій погляд — це відсутність спеціально підготовлених керуючих конструкцій. Замість них control flow реалізується з допомогою відправки повідомлень об'єктам. Наприклад, якщо відправити об'єкту типу Boolean повідомлення ifTrue з блоком коду в якості додаткового аргументу, цей код буде виконаний тоді і тільки тоді, коли значення булевского об'єкта буде істинним.

result := a > b
ifTrue:[ 'greater' ]
ifFalse:[ 'less or equal' ]

Словосполучення «булевский об'єкт» звучить трохи дивно, якщо не знати, що в Smalltalk немає простих значень: кожне значення є об'єктом. «Постійте-ка! — вигукнув я, — щось мені це нагадує!» І все заверте…

ДисклеймерЯкщо ви зробите щось схоже в продакшн-коді, ви потрапите в пекло. І там ніхто не стане з вами дружити. Навіть Гітлер. У Гітлера, принаймні, була якась мета.
В JavaScript не всяке значення є об'єктом. Однак там є ще більш кумедна річ: автоматичне приведення типів. Кожен раз, коли ми намагаємося використовувати просте значення як об'єкт (скажімо, отримати доступ до його властивості), воно «обертається» у відповідну об'єктну обгортку. Саме завдяки цьому ми можемо написати що-небудь на зразок true.toString(). Значення true не має методу toString, його має об'єкт new Boolean(true). Якщо задуматися, це іронічно: навіть коли ми намагаємося зробити явне приведення типів, ми неявно (вибачте за тавтологію) використовуємо неявне.

До цієї об'єктної обгортці, точніше, до її прототипу, ми можемо «причепити» свої власні методи. Це не дуже хороша ідея: якщо всі будуть так робити, рано чи пізно виникне колізія. Яка-небудь маленька бібліотека для роботи з буфером обміну перевизначити метод String.prototype.foo, який до цього визначив який-небудь віджет для валідації користувальницького введення. Можу вас запевнити, віджету це не сподобається. Але оскільки сьогодні п'ятниця, і ми не збираємося (не збираємося?) використовувати в коді, з яким потім будуть працювати невинні люди, можна дозволити собі трохи Темних Мистецтв.

Почнемо з якогось аналога смолтоковского ifTrue.

Boolean.prototype.ifThenElse = function(trueCallback, falseCallback){
return this.valueOf() ? trueCallback() : falseCallback();
}

Після цього, якщо ми не боїмося засмучувати маму, ми можемо робити речі типу:

(2 * 2 == 5).ifThenElse(
//сподіваюся, у 2017 році стрілочні функції вже нікого не бентежать
() => alert("Freedom is Slavery"),
() => alert("O brave new world!")
)

Є кілька нюансів. По-перше, інтерпретатор буде лаятися на нас помилками, якщо ми передамо в якості аргументу не функцію, а щось інше (наприклад, нічого). По-друге, в JS існує традиція (що прийшла ще з C, де не було булевского типу) використовувати в конструкції if не тільки логічні значення, а взагалі будь попало. У нормальному випадку автоматичне приведення типів зробить всю брудну роботу, перетворивши «falsy» значення false, а решта у true, але в нашому випадку цього не станеться:

(2 * 2).ifThenElse(
() => alert("Freedom is Slavery"),
() => alert("O brave new world!")
) // розповість нам повчальну історію про те, що undefined is not a function

Значить, треба лізти вище. Замість того, щоб додавати метод прототип Boolean, додамо його прототип Object. Звучить як відмінний план, чи не так?

function call(arg){
return typeof arg == "function" ? arg() : arg;
}
Object.prototype.ifThenElse = function(trueCallback, falseCallback){
if(this.valueOf()){
return call(trueCallback);
}else{
return call(falseCallback);
}
}

Майже добре. Тепер наш метод є у чисел, рядків, об'єктів… але не у undefined і не у null. На щастя чи на жаль, у них об'єктна обгортка відсутня. На жаль, це дуже поширені помилкові значення. Втім, цю проблему легко вирішити:

const nil = {
valueOf: () => false
}
//завжди використовуйте nil замість null
//вже занадто товсто, так?

Давайте визначимо ще пару корисних методів.

Number.prototype.for = function(callback){
for(let i = 0; i < this.valueOf(); i++){
callback(i);
}
}

function countdown(n){
console.log(10 - n);
}

10..for(countdown); //дві точки потрібні, оскільки десятку з однією точкою js сприймає як літерал числа з плаваючою точкою

Звичайно, це лише бліда тінь цього циклу for, який (якщо не боїшся зіпсувати карму і в наступному житті народитися опосумом) можна писати взагалі з порожнім тілом, винісши всю логіку в умова і фінальне вираз. З іншого боку, саме найпростіший сценарій використання циклу for зустрічається найчастіше.

Object.prototype.forIn = function(callback){
for(let key in this){
callback(key, this[key], this);
}
}

Object.prototype.forOwn = function(callback){
for(let key in this){
if(this.hasOwnProperty(key)){
callback(key, this[key], this);
}
}
}

var obj = {foo: "bar"};
obj.forIn(key => console.log(key)); // "forIn", "forOf", "foo" 
//а також "ifThenElse", якщо ви виконували попередній код в тому ж контексті
obj.forOwn(key => console.log(key)); // "foo"

Це вже навіть схоже на щось корисне. Не дайте цій схожості себе обдурити.

Function.prototype.while = function(callback){
while(this()){
callback();
}
}

var power = 5;
var result = 2;

(() => --power).while(
() => result *= 2
)

Якщо вам потрібно звести двійку у п'яту ступінь, але нічого розумнішого, ніж код вище, ви не придумали — краще ворвитесь в найближчий магазин, візьміть в заручники касирку та всіх покупців і вимагайте, щоб хто-небудь вважав це за вас. Теж ідея так собі, але з двох зол треба вибирати менше.

І нарешті:

String.prototype.switch = function(callbackObject){
var f = callbackObject[this.valueOf()];
return typeof f == "function" ? f() : f;
};

("1" + 2).switch({
"12": () => console.log("Це JS"),
"3": () => console.log("Це не JS")
})

У порівнянні зі стандартним JS'івським switch у цього методу є кілька недоліків. По-перше, він працює тільки для рядків. При бажанні можна розширити його на довільні значення, використовуючи замість об'єкта Map, але моє бажання робити аморальні речі на сьогодні вичерпано, і я надаю це допитливому читачеві. По-друге, немає default. По-третє, відсутня можливість робити такі штуки:

switch(value){
case 1:
case 2:
console.log("Це одиниця і двійка");
break;
case 3:
console.log("Точно трійка");
case 4:
console.log("Трійка або четвірка");
}

Втім, багато хто скаже, що це швидше гідність.

Що ж, сподіваюся, вам було так само весело, як і мені. А мені пора повертатися до роботи — ну, до цієї роботи. Де так не пишуть. Ну, ви мене зрозуміли.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.