Статьи

Как читать код с Марса: история разоблачения некоторого кода Javascript

Я тааак невежественен. Есть так много вещей, которые я не знаю. Например, когда я нашел  этот фрагмент кода Javascript :

for (var i=0; i<6; i++) {
    var row = document.querySelector("table").insertRow(-1);
    for (var j=0; j<6; j++) {
        var letter = String.fromCharCode("A".charCodeAt(0)+j-1);
        row.insertCell(-1).innerHTML = i&&j ? "<input id='"+ letter+i +"'/>" : i||letter;
    }
}

var DATA={}, INPUTS=[].slice.call(document.querySelectorAll("input"));
INPUTS.forEach(function(elm) {
    elm.onfocus = function(e) {
        e.target.value = localStorage[e.target.id] || "";
    };
    elm.onblur = function(e) {
        localStorage[e.target.id] = e.target.value;
        computeAll();
    };
    var getter = function() {
        var value = localStorage[elm.id] || "";
        if (value.charAt(0) == "=") {
            with (DATA) return eval(value.substring(1));
        } else { return isNaN(parseFloat(value)) ? value : parseFloat(value); }
    };
    Object.defineProperty(DATA, elm.id, {get:getter});
    Object.defineProperty(DATA, elm.id.toLowerCase(), {get:getter});
});
(window.computeAll = function() {
    INPUTS.forEach(function(elm) { try { elm.value = DATA[elm.id]; } catch(e) {} });
})();

Сначала  посмотрите, что делает код,  а затем вернитесь.

ОК, это электронная таблица, которая поддерживает формулу, подобную Excel. Но есть некоторые вещи, которые я не мог понять быстро при проверке этого кода. Я потратил на это 3 минуты и ничего не мог сказать о происходящем. Я знал, что это было что-то , принимая во внимание, что это было действительно коротко и достигло очень многих вещей

У меня был недостаток, так как я не эксперт по JavaScript и не видел некоторых синтаксисов и функций, используемых в этом фрагменте кода. Но я приготовился раскрыть то, что происходило под прикрытием. Для этого я проверил его пару с моим другом, и мы оба собрались атаковать его, не ища дополнительную информацию, основываясь только на интуиции, логике и экспериментах. Я оставляю вас с нашим обзором:

В конце этой статьи есть  дорожная карта,  показывающая наши мысли дидактически. Это может помочь вам лучше и быстрее понять, как мы прошли через это, если вам надоест.

ОК, откуда приходит магия? Я не вижу синтаксического анализатора формул, — сказал я, — подождите, есть функция eval , и над ней есть предложение if (value.charAt (0) == «=») , которое определяет, является ли значение формулой. Это единственное событие eval , так что должно происходить большинство из них.

Но что это с этим с оговоркой? Что это означает? — Я спросил.

Давайте сделаем эксперимент для чего-то, что я подозреваю, — сказал мой друг. И после некоторых настроек мы получили это:

var z;
with({
	a:1, 
	b:2, 
	c:3
}) z = eval("(a + b) * c");
alert(z);

9 !!!. Вот и все. По крайней мере, теперь мы знаем, как работает функция eval с предложением with перед ней . Это ново для нас.

Отлично. Теперь, что такое ДАННЫЕ ? Какое значение это может иметь, что работает с предложением with ? — спросил я. — В ней должны храниться значения каждой ячейки листа, верно? Но что, если формула ссылается на ячейку, которая также содержит формулу. Я не вижу здесь никакой рекурсии. Теперь мы на складе … Давайте исследуем больше на ДАННЫХ . Давайте покажем вхождение DATA и позволим нашему редактору выделить все остальные вхождения.

Он объявлен как пустой объект. Тогда это … ммм. Я не вижу, чтобы ему присваивали значение. Rare. И что означает эта строка кода? :

Object.defineProperty(DATA, elm.id, {get:getter});

Что ж, чтение выглядит так, как будто оно определяет свойство в объекте DATA , а также устанавливает обратный вызов функции getter для доступа к свойству — мы оба согласились.

Да, но что это нам дает? — спросил я. Ничего, кроме того, что я знаю, — ответил я сам. Давайте двигаться дальше .

Когда наступает момент, когда начинается расчет формулы? Ах, вот оно: событие onblur ячейки (элемент ввода: elm ). Он хранит значение ячейки внутри localStorage (что для нас тоже было чем-то новым, но чем-то, что нас не развлекало), а затем вызывает функцию computeAll … и функция computeAll должна вычислять значения каждой ячейки для того, что я вижу, не так ли? Но это не вызывает ничего дальше !!! Что тут происходит?.

Ага, каждой ячейке присваивается предположительно предварительно вычисленное значение в DATA [elm.id] . Может ли быть так, что функция getter вызывается, когда мы получаем доступ к индексу в DATA , прямо сейчас, как мы и предполагали ранее? , Должно быть, так сказал мой друг.

Да, и функция getter возвращает оценку выражения, ранее сохраненного в localStorage , с учетом индекса ячейки -I ответил. Если это формула, он использует DATA в качестве источника данных для оценки выражения. Но опять же , DATA не присваивается никакого значения … или , возможно , он используется только в качестве оправдания или заполнитель для вызова геттер функции, которая является один , что делает фактические расчеты с использованием LocalStorage ?.

DATA является проксирование LocalStorage ? DATA является проксирование LocalStorage !!! — утверждал мой друг, почти крича.

Конечно!!! Я кричал. И рекурсия происходит, как только Javascript пытается получить доступ к индексам в DATA, когда он выполняет предложение with (DATA) , которое перебирает необходимые  свойства DATA . Он начинает вызывать геттер функцию внутри геттерной функции, иначе рекурсии .

Должно быть, так сказал мой друг.

ХОРОШО, ХОРОШО, ХОРОШО. Подождите. Давайте сделаем следующее:

var target = {a:1, b:2, c:3, d:"(a + b) * c"};
var proxy = {};

["a", "b", "c", "d"].forEach(function(idx) {		
	var getter = function() {
		with (proxy) return eval(target[idx]);
	};
	Object.defineProperty(proxy, idx, {get:getter});
});

alert(proxy["d"]);

9 !!!. Пум, вот и все. Мы поняли. ДАННЫЕ ПРОКСИРУЮТ МЕСТНОЕ ХРАНЕНИЕ . В этом последнем эксперименте мы видим это ясно … Ничего себе, хорошо сделано .

Теперь мы получили всю картину и были готовы воссоздать события на месте преступления. Я имею в виду выполнение программы:


Когда ячейка теряет фокус, выражение ее значения (формула или число) сохраняется в
localStorage , а каждое значение ячейки пересчитывается в
функцию
computeAll . Функция
computeAll просто присваивает каждой ячейке значение, которое предположительно хранится в
DATA [cellIndex]; только то, что этого значения еще нет. Выражение
Object.defineProperty (DATA, elm.id, {получает: поглотитель}) удостоверяется , что каждый раз, когда я пытаюсь получить доступ к индексу в
DATA , то
геттер функция срабатывает. Поэтому, когда мы говорим
DATA [cellIndex], оно срабатывает. Функция
getter возвращает значение в соответствии с выражением, хранящимся в
localStorage для текущего индекса ячейки. Если выражение является формулой, оно выполняет выражение
с (DATA) return eval (value.substring (1)) . Вот где происходит рекурсия, потому что
с (DATA) return eval (value.substring (1)) будет выполнять итерацию по
свойствам
DATA , что, как указано, будет вызывать
функцию
getter при каждом доступе: происходит рекурсия; условие завершения рекурсии — это предложение
else {return isNaN (parseFloat (value))? значение: parseFloat (значение); } . Там у вас есть это. Сочетание проксировании
LocalStorage и перехватывают
DATAДоступ к свойствам является причиной смерти … Я имею в виду, ключ для расчетов формулы.

И мы остались с этим. Нам не нужно было искать дальше. Мы не отлаживали и не проверяли выполнение кода с примерами значений или не проводили исчерпывающий анализ возможных путей выполнения. Мы только посмотрели на код и сделали некоторые предположения. Правильны ли наши выводы или нет, мы не уверены; но это то, что мы можем проверить сейчас, выполнив поиск в Интернете. Что бы мы ни нашли, это была хорошая тренировка.

Дорожная карта

Примечание. До даты написания этой статьи мы ничего не исследовали, поэтому мы не будем склонны исправлять свои мысли. В любом случае, реальный анализ кода имеет определенную ценность.