JavaScript-разработчики часто жалуются на то, что их язык программирования несправедливо ругают за то, что он имеет слишком много чрезмерно усложнённых, запутанных возможностей. Многие борются с таким отношением к JS, рассказывая о том, почему критиковать этот язык за то, чем он является, неправильно. Автор материала, перевод которого мы сегодня публикуем, решил не защищать JS, обратившись, вместо этого, к тёмной стороне языка. Однако тут он не хочет говорить, например, о тех ловушках, которые JavaScript расставляет для неопытных программистов. Его интересует вопрос о том, что получится, если попытаться подтвердить плохую репутацию языка кодом, который мог бы написать тот, кто совершенно не заботится об окружающих.
В примерах к этому материалу будет использовано множество механизмов языка. Многое из того, что вы здесь увидите, кстати, работает и в других языках, поэтому, при должном усердии, можно обнаружить и их тёмные стороны. Но JavaScript, определённо, обладает настоящим даром ко всякого рода издевательствам, и с ним в этой области очень непросто тягаться другим языкам. Если вы пишете код, с которым нужно будет работать другим людям, JS даёт вам неисчерпаемое количество возможностей для того, чтобы этих людей раздражать, путать, всячески изводить и обманывать. Собственно говоря, тут мы рассмотрим лишь небольшую часть подобных приёмов.
JavaScript поддерживает геттеры — функции, которые позволяют работать с тем, что они возвращают, как с обычным свойством. При нормальном использовании это выглядит так:
Если же воспользоваться геттерами, замышляя недоброе, то, например, можно создавать саморазрушающиеся объекты:
Здесь, при каждом обращении к
Геттеры — это здорово, но они существуют уже многие годы, о них знает множество разработчиков. Теперь же, благодаря прокси, в нашем распоряжении оказывается куда более мощный инструмент для развлечений с объектами. Прокси — это возможность ES6, которая позволяет создавать обёртки вокруг объектов. С их помощью можно управлять тем, что происходит, когда пользователь пытается читать или записывать свойства проксируемых объектов. Это позволяет, например, создать объект, который, в одной трети попыток доступа к некоему ключу такого объекта, будет возвращать значение по случайно выбранному ключу.
Нашу подлость, к сожалению, частично раскрывают инструменты разработчика, идентифицирующие
До сих пор мы говорили о том, как объекты могут модифицировать сами себя. Но мы, кроме того, можем создавать невинно выглядящие функции, которые инфицируют объекты, передаваемые им, меняя их поведение. Например, предположим, у нас есть простая функция
Такую функцию легко переписать так, чтобы она инфицировала передаваемые ей объекты, слегка изменяя их. Например, можно сделать так, чтобы свойство, к которому она помогла получить доступ, больше не выводилось бы при попытке перебрать ключи объекта:
Это — образец весьма тонкого вмешательства в поведение объекта. Перечисление ключей объекта — не самая заметная операция, так как она и не является очень редкой, но и используется не слишком часто. Так как ошибки, к которым может привести подобная модификация объектов, нельзя привязать к их коду, они могут существовать в некоем проекте довольно долго.
Выше мы обсуждали разные возможности JS, в том числе — и довольно свежие. Однако иногда нет ничего лучше, чем старые, проверенные временем технологии. Одной из особенностей JS, из-за которой на него обрушивается больше всего критики, является возможность модификации встроенных прототипов. Эта возможность использовалась в ранние годы JS для расширения встроенных объектов, например — массивов. Вот как расширить стандартные возможности массивов, скажем, добавив к прототипу объекта
Как оказывается, если сделать что-то подобное в реально используемой библиотеке, это может нарушить работу с базовыми механизмами языка во всём приложении, использующем эту библиотеку. Поэтому включение дополнительных полезных методов в прототипы стандартных объектов можно считать весьма удачным ходом для терпеливых разработчиков, которые стремятся делать другим гадости. Однако если речь идёт о нетерпеливых социопатах, им можно предложить кое-что быстродействующее, но не менее интересное. У модификации прототипов есть одно весьма полезное свойство, которое заключается в том, что модификация воздействует на весь код, который выполняется в некоем окружении, даже на тот, который загружается из модулей или находится в замыканиях. В результате, если оформить следующий код в виде стороннего скрипта (скажем, это может быть скрипт рекламной сети или аналитической службы), то весь сайт, использующий этот скрипт, окажется подверженным небольшим ошибкам.
Тут мы переопределили стандартный метод
Тут мы запустили его три раза. То, что получилось при его первом использовании, немного отличается от следующих двух результатов его вызова. Это мелкое изменение, оно далеко не всегда вызовет какой-нибудь сбой. А самое приятное здесь то, что понять причину редко возникающих ошибок, вызываемых подобным методом, невозможно без чтения его исходного кода, который и является причиной этих ошибок. Наша функция не бросается в глаза при работе с инструментами разработчика, она не выдаёт ошибок при работе в строгом режиме. В общем, с помощью чего-то подобного вполне можно свести кого-нибудь с ума.
Именование сущностей, как известно, является одной из двух сложнейших задач компьютерной науки. Поэтому плохие имена придумывают не только те, кто сознательно стремится навредить другим. Конечно, в это может быть трудно поверить бывалым линуксоидам. В их распоряжении были годы на то, чтобы связать страшнейшего IT-нарушителя в сфере именования (Microsoft) с глубочайшими формами зла. Но неудачные имена не приносят прямого вреда программам. Не будем говорить о мелочёвке вроде имён, вводящих в заблуждение и о потерявших актуальность комментариях. Например, о таких:
Для того чтобы в это вникнуть и понять, что тут что-то не так и с комментарием, и с именем переменной, тому, кто читает код, в котором встречается подобное, придётся малость притормозить и подумать. Но это — ерунда. Давайте лучше поговорим о по-настоящему интересных штуках. Вы знали, что большинство Unicode-символов можно использовать для именования переменных в JavaScript? Если вы, в вопросе назначения имён переменных, настроены на позитив, то вам понравится идея использования имён в виде значков (Хабр вырезал emoji, хотя в оригинале тут после
Хотя, мы ведь тут говорим о настоящих гадостях, поэтому обратимся лучше к символам, которые похожи на те, какими обычно пользуются для именования переменных, но ими не являются. Например, сделаем вот так:
Буква
Несмотря на рассказ о разных гадостях, которые можно творить с помощью JavaScript, этот материал направлен на то, чтобы предостеречь программистов от использования приёмов, подобных описанным, и донести до них тот факт, что подобное может причинить реальный вред. Автор материала говорит, что всегда полезно знать о том, какие проблемы могут появляться в плохо написанном коде. Он полагает, что нечто подобное можно найти в реальных проектах, но надеется, что там это существует в менее деструктивной форме. Однако то, что программист, написавший подобный код, не стремился навредить другим, не делает легче работу с таким кодом и его отладку. При этом знание того, до чего могут довести целенаправленные попытки навредить, способно расширить кругозор программиста и помочь ему в поиске источника похожих ошибок. Никто не может быть полностью уверен в том, что в коде, с которым он работает, не затаилась ошибка. Возможно, кто-то, зная о своей склонности к чрезмерной подозрительности, попытается успокоить себя тем, что тревога о подобных ошибках — это лишь плод его воображения. Однако это не помешает таким ошибкам, возможно, внесённым в некий код намеренно, однажды себя проявить.
Уважаемые читатели! Сталкивались ли вы на практике с чем-то похожим на то, о чём шла речь в этой статье?
В примерах к этому материалу будет использовано множество механизмов языка. Многое из того, что вы здесь увидите, кстати, работает и в других языках, поэтому, при должном усердии, можно обнаружить и их тёмные стороны. Но JavaScript, определённо, обладает настоящим даром ко всякого рода издевательствам, и с ним в этой области очень непросто тягаться другим языкам. Если вы пишете код, с которым нужно будет работать другим людям, JS даёт вам неисчерпаемое количество возможностей для того, чтобы этих людей раздражать, путать, всячески изводить и обманывать. Собственно говоря, тут мы рассмотрим лишь небольшую часть подобных приёмов.
Геттеры-модификаторы
JavaScript поддерживает геттеры — функции, которые позволяют работать с тем, что они возвращают, как с обычным свойством. При нормальном использовании это выглядит так:
let greeter = {
name: 'Bob',
get hello() { return `Hello ${this.name}`}
}
console.log(greeter.hello) // Hello Bob
greeter.name = 'World';
console.log(greeter.hello) // Hello World
Если же воспользоваться геттерами, замышляя недоброе, то, например, можно создавать саморазрушающиеся объекты:
let obj = {
foo: 1,
bar: 2,
baz: 3,
get evil() {
let keys = Object.keys(this);
if(keys) {
delete this[keys[0]]
}
return 'Nothing to see here';
}
}
Здесь, при каждом обращении к
obj.evil
, будет удаляться одно из других свойств объекта. При этом код, работающий с obj.evil
, не будет знать о том, что прямо у него под носом происходит нечто весьма странное. Однако, это — только начало разговора о вредных побочных эффектах, которых можно добиваться с помощью механизмов JavaScript.Неожиданные прокси
Геттеры — это здорово, но они существуют уже многие годы, о них знает множество разработчиков. Теперь же, благодаря прокси, в нашем распоряжении оказывается куда более мощный инструмент для развлечений с объектами. Прокси — это возможность ES6, которая позволяет создавать обёртки вокруг объектов. С их помощью можно управлять тем, что происходит, когда пользователь пытается читать или записывать свойства проксируемых объектов. Это позволяет, например, создать объект, который, в одной трети попыток доступа к некоему ключу такого объекта, будет возвращать значение по случайно выбранному ключу.
let obj = {a: 1, b: 2, c: 3};
let handler = {
get: function(obj, prop) {
if (Math.random() > 0.33) {
return obj[prop];
} else {
let keys = Object.keys(obj);
let key = keys[Math.floor(Math.random()*keys.length)]
return obj[key];
}
}
};
let evilObj = new Proxy(obj, handler);
// вот что может получиться при работе с подобным объектом
console.log(evilObj.a); // 1
console.log(evilObj.b); // 1
console.log(evilObj.c); // 3
console.log(evilObj.a); // 2
console.log(evilObj.b); // 2
console.log(evilObj.c); // 3
Нашу подлость, к сожалению, частично раскрывают инструменты разработчика, идентифицирующие
evilObj
как объект типа Proxy
. Однако, вышеописанная конструкция, до того, как будет раскрыта её низменная сущность, способна доставить тем, кто будет с ней работать, немало приятных минут.Заразные функции
До сих пор мы говорили о том, как объекты могут модифицировать сами себя. Но мы, кроме того, можем создавать невинно выглядящие функции, которые инфицируют объекты, передаваемые им, меняя их поведение. Например, предположим, у нас есть простая функция
get()
, которая позволяет выполнять безопасный поиск свойства в передаваемом ей объекте с учётом того, что такого объекта может и не существовать:let get = (obj, property, default) => {
if(!obj) {
return default;
}
return obj[property];
}
Такую функцию легко переписать так, чтобы она инфицировала передаваемые ей объекты, слегка изменяя их. Например, можно сделать так, чтобы свойство, к которому она помогла получить доступ, больше не выводилось бы при попытке перебрать ключи объекта:
let get = (obj, property, defaultValue) => {
if(!obj || !property in obj) {
return defaultValue;
}
let value = obj[property];
delete obj[property];
Object.defineProperty(obj, property, {
value,
enumerable: false
})
return obj[property];
}
let x = {a: 1, b:2 };
console.log(Object.keys(x)); // ['a', 'b']
console.log(get(x, 'a'));
console.log(Object.keys(x)); // ['b']
Это — образец весьма тонкого вмешательства в поведение объекта. Перечисление ключей объекта — не самая заметная операция, так как она и не является очень редкой, но и используется не слишком часто. Так как ошибки, к которым может привести подобная модификация объектов, нельзя привязать к их коду, они могут существовать в некоем проекте довольно долго.
Прототипный беспорядок
Выше мы обсуждали разные возможности JS, в том числе — и довольно свежие. Однако иногда нет ничего лучше, чем старые, проверенные временем технологии. Одной из особенностей JS, из-за которой на него обрушивается больше всего критики, является возможность модификации встроенных прототипов. Эта возможность использовалась в ранние годы JS для расширения встроенных объектов, например — массивов. Вот как расширить стандартные возможности массивов, скажем, добавив к прототипу объекта
Array
метод contains
:Array.prototype.contains = function(item) {
return this.indexOf(item) !== -1;
}
Как оказывается, если сделать что-то подобное в реально используемой библиотеке, это может нарушить работу с базовыми механизмами языка во всём приложении, использующем эту библиотеку. Поэтому включение дополнительных полезных методов в прототипы стандартных объектов можно считать весьма удачным ходом для терпеливых разработчиков, которые стремятся делать другим гадости. Однако если речь идёт о нетерпеливых социопатах, им можно предложить кое-что быстродействующее, но не менее интересное. У модификации прототипов есть одно весьма полезное свойство, которое заключается в том, что модификация воздействует на весь код, который выполняется в некоем окружении, даже на тот, который загружается из модулей или находится в замыканиях. В результате, если оформить следующий код в виде стороннего скрипта (скажем, это может быть скрипт рекламной сети или аналитической службы), то весь сайт, использующий этот скрипт, окажется подверженным небольшим ошибкам.
Array.prototype.map = function(fn) {
let arr = this;
let arr2 = arr.reduce((acc, val, idx) => {
if (Math.random() > 0.95) {
idx = idx + 1
}
let index = acc.length - 1 === idx ? (idx - 1 ) : idx
acc[index] = fn(val, index, arr);
return acc;
},[]);
return arr2;
}
Тут мы переопределили стандартный метод
Array.prototype.map
так, что он, в целом, работает нормально, но в 5% случаев меняет местами два элемента массива. Вот что можно получить после нескольких вызовов подобного метода:let arr = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15];
let square = x => x * x;
console.log(arr.map(square));
// [1,4,9,16,25,36,49,64,100,81,121,144,169,196,225
console.log(arr.map(square));
// [1,4,9,16,25,36,49,64,81,100,121,144,169,196,225]
console.log(arr.map(square));
// [1,4,9,16,25,36,49,64,81,100,121,144,169,196,225]
Тут мы запустили его три раза. То, что получилось при его первом использовании, немного отличается от следующих двух результатов его вызова. Это мелкое изменение, оно далеко не всегда вызовет какой-нибудь сбой. А самое приятное здесь то, что понять причину редко возникающих ошибок, вызываемых подобным методом, невозможно без чтения его исходного кода, который и является причиной этих ошибок. Наша функция не бросается в глаза при работе с инструментами разработчика, она не выдаёт ошибок при работе в строгом режиме. В общем, с помощью чего-то подобного вполне можно свести кого-нибудь с ума.
Непростые имена
Именование сущностей, как известно, является одной из двух сложнейших задач компьютерной науки. Поэтому плохие имена придумывают не только те, кто сознательно стремится навредить другим. Конечно, в это может быть трудно поверить бывалым линуксоидам. В их распоряжении были годы на то, чтобы связать страшнейшего IT-нарушителя в сфере именования (Microsoft) с глубочайшими формами зла. Но неудачные имена не приносят прямого вреда программам. Не будем говорить о мелочёвке вроде имён, вводящих в заблуждение и о потерявших актуальность комментариях. Например, о таких:
// Инициализация даты
let arrayOfNumbers = { userid: 1, name: 'Darth Vader'};
Для того чтобы в это вникнуть и понять, что тут что-то не так и с комментарием, и с именем переменной, тому, кто читает код, в котором встречается подобное, придётся малость притормозить и подумать. Но это — ерунда. Давайте лучше поговорим о по-настоящему интересных штуках. Вы знали, что большинство Unicode-символов можно использовать для именования переменных в JavaScript? Если вы, в вопросе назначения имён переменных, настроены на позитив, то вам понравится идея использования имён в виде значков (Хабр вырезал emoji, хотя в оригинале тут после
let
была emoji какахи):let = { postid: 123, postName: 'Evil JavaScript'}
Хотя, мы ведь тут говорим о настоящих гадостях, поэтому обратимся лучше к символам, которые похожи на те, какими обычно пользуются для именования переменных, но ими не являются. Например, сделаем вот так:
let obj = {};
console.log(obj); // Error!
Буква
b
в имени obj
может выглядеть почти нормально, но это — не строчная латинская буква b. Это — так называемая полноширинная строчная латинская буква b. Символы это разные, поэтому любой, кто попытается ввести имя подобной переменной вручную, скорее всего, окажется сильно сбитым с толку.Итоги
Несмотря на рассказ о разных гадостях, которые можно творить с помощью JavaScript, этот материал направлен на то, чтобы предостеречь программистов от использования приёмов, подобных описанным, и донести до них тот факт, что подобное может причинить реальный вред. Автор материала говорит, что всегда полезно знать о том, какие проблемы могут появляться в плохо написанном коде. Он полагает, что нечто подобное можно найти в реальных проектах, но надеется, что там это существует в менее деструктивной форме. Однако то, что программист, написавший подобный код, не стремился навредить другим, не делает легче работу с таким кодом и его отладку. При этом знание того, до чего могут довести целенаправленные попытки навредить, способно расширить кругозор программиста и помочь ему в поиске источника похожих ошибок. Никто не может быть полностью уверен в том, что в коде, с которым он работает, не затаилась ошибка. Возможно, кто-то, зная о своей склонности к чрезмерной подозрительности, попытается успокоить себя тем, что тревога о подобных ошибках — это лишь плод его воображения. Однако это не помешает таким ошибкам, возможно, внесённым в некий код намеренно, однажды себя проявить.
Уважаемые читатели! Сталкивались ли вы на практике с чем-то похожим на то, о чём шла речь в этой статье?