Когда я читал книгу «Паттерны разработки игр», написанную замечательным человеком по имени Bob Nystrom (я не пишу его имя по-русски, поскольку не имею ни малейшего понятия, как это произносится), в одной из глав мне на глаза попалась небольшая ода языку Smalltalk как праотцу всех современных объектно-ориентированных языков, намного опередившему своё время. Поскольку я по жизни испытываю необоримую приязнь ко всяким винтажным языкам, естественно, я полез про него гуглить. И разумеется, вместо того, чтобы вынести из этого опыта что-то полезное, я научился плохому.

Особенность языка Smalltalk, за которую зацепился мой взгляд — это отсутствие специально обученных управляющих конструкций. Вместо них control flow реализуется с помощью отправки сообщений объектам. Например, если отправить объекту типа Boolean сообщение ifTrue с блоком кода в качестве дополнительного аргумента, этот код будет исполнен тогда и только тогда, когда значение булевского объекта будет истинным.
Словосочетание «булевский объект» звучит несколько странно, если не знать, что в Smalltalk нет простых значений: каждое значение является объектом. «Постойте-ка! — воскликнул я, — что-то мне это напоминает!» И всё заверте…
В JavaScript не всякое значение является объектом. Однако там есть ещё более забавная вещь: автоматическое приведение типов. Каждый раз, когда мы пытаемся использовать простое значение как объект (скажем, получить доступ к его свойству), оно «оборачивается» в соответствующую объектную обёртку. Именно благодаря этому мы можем написать что-нибудь вроде true.toString(). Значение true не имеет метода toString, его имеет объект new Boolean(true). Если задуматься, это иронично: даже когда мы пытаемся сделать явное приведение типов, мы неявно (простите за тавтологию) используем неявное.
К этой объектной обёртке, точнее, к её прототипу, мы можем «прицепить» свои собственные методы. Это не очень хорошая идея: если все станут так делать, рано или поздно возникнет коллизия. Какая-нибудь маленькая библиотека для работы с буфером обмена переопределит метод String.prototype.foo, который до этого определил какой-нибудь виджет для валидации пользовательского ввода. Могу вас уверить, виджету это не понравится. Но поскольку сегодня пятница, и мы не собираемся (не собираемся ведь?) использовать это в коде, с которым потом будут работать невинные люди, можно позволить себе немного Тёмных Искусств.
Начнём с некоего аналога смолтоковского ifTrue.
После этого, если мы не боимся огорчать маму, мы можем делать вещи типа:
Есть несколько нюансов. Во-первых, интерпретатор будет ругаться на нас ошибками, если мы передадим в качестве аргумента не функцию, а что-то другое (например, ничего). Во-вторых, в JS существует традиция (пришедшая ещё из C, где не было булевского типа) использовать в конструкции if не только логические значения, а вообще какие попало. В нормальном случае автоматическое приведение типов сделает всю грязную работу, превратив «falsy» значения в false, а остальные в true, но в нашем случае этого не произойдёт:
Значит, нужно лезть выше. Вместо того, чтобы добавлять метод в прототип Boolean, добавим его в прототип Object. Звучит как отличный план, не так ли?
Почти хорошо. Теперь наш метод есть у чисел, строк, объектов… но не у undefined и не у null. К счастью или к сожалению, у них объектная обёртка отсутствует. К сожалению, это очень распространённые ложные значения. Впрочем, эту проблему легко решить:
Давайте определим ещё пару полезных методов.
Конечно, это лишь бледная тень настоящего цикла for, который (если не боишься испортить карму и в следующей жизни родиться опоссумом) можно писать вообще с пустым телом, вынеся всю логику в условие и финальное выражение. С другой стороны, именно самый простой сценарий использования цикла for встречается чаще всего.
Это уже даже похоже на нечто полезное. Не дайте этой похожести себя обмануть.
Если вам нужно возвести двойку в пятую степень, но ничего умнее, чем код выше, вы не придумали — лучше ворвитесь в ближайший магазин с предметом, похожим на пистолет, возьмите в заложники кассиршу и всех покупателей и требуйте, чтобы кто-нибудь посчитал это за вас. Тоже идея так себе, но из двух зол нужно выбирать меньшее.
И наконец:
По сравнению со стандартным JS'овским switch у этого метода есть несколько недостатков. Во-первых, он работает только для строк. При желании можно расширить его на произвольные значения, используя вместо объекта Map, но моё желание делать аморальные вещи на сегодня иссякло, и я предоставляю это любознательному читателю. Во-вторых, нет default. В-третьих, отсутствует возможность делать такие штуки:
Впрочем, многие скажут, что это скорее достоинство.
Что ж, надеюсь, вам было так же весело, как и мне. А мне пора возвращаться к работе — ну, к настоящей работе. Где так не пишут. Ну, вы меня поняли.

Особенность языка 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("Тройка или четвёрка"); }
Впрочем, многие скажут, что это скорее достоинство.
Что ж, надеюсь, вам было так же весело, как и мне. А мне пора возвращаться к работе — ну, к настоящей работе. Где так не пишут. Ну, вы меня поняли.
