JavaScript как воплощение зла

в 8:24, , рубрики: javascript, Блог компании RUVDS.com, разработка, Разработка веб-сайтов

JavaScript-разработчики часто жалуются на то, что их язык программирования несправедливо ругают за то, что он имеет слишком много чрезмерно усложнённых, запутанных возможностей. Многие борются с таким отношением к JS, рассказывая о том, почему критиковать этот язык за то, чем он является, неправильно. Автор материала, перевод которого мы сегодня публикуем, решил не защищать JS, обратившись, вместо этого, к тёмной стороне языка. Однако тут он не хочет говорить, например, о тех ловушках, которые JavaScript расставляет для неопытных программистов. Его интересует вопрос о том, что получится, если попытаться подтвердить плохую репутацию языка кодом, который мог бы написать тот, кто совершенно не заботится об окружающих.

JavaScript как воплощение зла - 1

В примерах к этому материалу будет использовано множество механизмов языка. Многое из того, что вы здесь увидите, кстати, работает и в других языках, поэтому, при должном усердии, можно обнаружить и их тёмные стороны. Но 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!

Буква в имени obj может выглядеть почти нормально, но это — не строчная латинская буква b. Это — так называемая полноширинная строчная латинская буква b. Символы это разные, поэтому любой, кто попытается ввести имя подобной переменной вручную, скорее всего, окажется сильно сбитым с толку.

Итоги

Несмотря на рассказ о разных гадостях, которые можно творить с помощью JavaScript, этот материал направлен на то, чтобы предостеречь программистов от использования приёмов, подобных описанным, и донести до них тот факт, что подобное может причинить реальный вред. Автор материала говорит, что всегда полезно знать о том, какие проблемы могут появляться в плохо написанном коде. Он полагает, что нечто подобное можно найти в реальных проектах, но надеется, что там это существует в менее деструктивной форме. Однако то, что программист, написавший подобный код, не стремился навредить другим, не делает легче работу с таким кодом и его отладку. При этом знание того, до чего могут довести целенаправленные попытки навредить, способно расширить кругозор программиста и помочь ему в поиске источника похожих ошибок. Никто не может быть полностью уверен в том, что в коде, с которым он работает, не затаилась ошибка. Возможно, кто-то, зная о своей склонности к чрезмерной подозрительности, попытается успокоить себя тем, что тревога о подобных ошибках — это лишь плод его воображения. Однако это не помешает таким ошибкам, возможно, внесённым в некий код намеренно, однажды себя проявить.

Уважаемые читатели! Сталкивались ли вы на практике с чем-то похожим на то, о чём шла речь в этой статье?

JavaScript как воплощение зла - 2

Автор: ru_vds

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js