Сериализация методов объекта с использованием строк шаблона ES6 и eval

Понимание того, как манипуляции с AJAX и JSON работают в JavaScript, может иметь значение между написанием элегантного, легкого для понимания приложения и грубого взлома. Судя по моему опыту, лишь небольшое количество разработчиков, с которыми я разговаривал за последний год или около того, знают, как написать обычный XMLHttpRequest с нуля. Я согласен с тем, что важно выполнять работу своевременно и не изобретать велосипед, но использование чего-либо, не понимая его сути, — это то, с чем я не могу смириться для кого-то, кроме младшего разработчика.

Немного предыстории

Я работаю с JSON с 2009 года, и все, что я знал, это то, что вы можете сериализовать объекты JavaScript с помощью JSON.stringify, но сериализуются только свойства (значения), а методы остаются позади. Я начал изучать литературу по сериализации JSON в JavaScript, и единственная информация, которую я нашел на JSON.org и в сети разработчиков Mozilla, подтвердила то, что я уже сказал ранее, поэтому я начал думать о «запрещенных» частях JavaScript.

В моем случае я начал изучать JavaScript с чтения Хорошие части JavaScript, написанного Дугласом Крокфордом, и, если вы похожи на меня, всю остальную вашу карьеру сильно повлияла эта книга и подход Крокфорда к JavaScript и разработке программного обеспечения. Но за все приходится платить, и я должен был заплатить за то, что слишком сильно поверил в мантру Эвал — зло.

Что такое JSON?

Во-первых, давайте начнем с некоторых основ, а именно, давайте посмотрим, что такое JSON.

По данным JSON.org:

JSON (JavaScript Object Notation) — это облегченный формат обмена данными. […] JSON — это текстовый формат, который полностью не зависит от языка, но использует соглашения, знакомые программистам семейства языков C […]

По данным Сети разработчиков Mozilla

JSON — это синтаксис для сериализации объектов, массивов, чисел, строк, логических значений и null. Он основан на синтаксисе JavaScript, но отличается от него: часть JavaScript не является JSON, а часть JSON не является JavaScript.

Оба определения говорят нам об одном: JSON — это формат для распространения данных по «проводам», который может быть легко понят и сгенерирован как людьми, так и компьютерами.

Что такое сериализация?

Опять же, чтобы продолжить, нам сначала нужно установить, что такое сериализация, и, не прибегая для этого к внешним ресурсам, я дам вам свое собственное определение: «Сериализация — это процесс, посредством которого один формат данных транслируется посреднику. формат данных для целей распространения ».

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

См. также:  Импорт нескольких экспортированных классов с одинаковым именем - основы Angular

Теперь, когда теория в стороне, давайте перейдем к коду.

Сериализация объекта и функция замены

Основываясь на знаниях, которые у нас есть на данный момент, мы можем теперь продемонстрировать, как именно работает сериализация, поэтому давайте возьмем объект, пропустим его через JSON.stringify и посмотрим, что у нас получится.

'use strict'; 
let person = {  
  name: 'Susan',  
  age: 24,  
  sayHi: function() { 
    console.log('Susan says hi!');  
  }
}; 
const serialized =   JSON.stringify(person); 
console.log(serialized); // {"name":"Susan","age":24}  
typeof serialized === 'string' // true

Как видите, метода sayHi нигде не видно. Причина в том, что JSON.stringify сериализует только свойства объекта, оставляя позади его методы.
Теперь предположим, что мы хотим также сериализовать метод sayHi. Как бы мы это сделали? Во-первых, давайте взглянем на подпись JSON.stringify, описанную ниже.

JSON.stringify(value[, replacer[, space]]) 
// => value(required) — the object we want to serialize 
// => replacer(optional) — a function that will be called for each //    of the object’s properties, or an array of strings and numbers //    that serve as a whitelist for the property selection process
// => space(optional) — the number of spaces each key will receive //    as indent

Наш главный интерес — аргумент replacer, и мы собираемся использовать его функциональную форму. Чтобы этот трюк сработал и код метода sayHi был правильно преобразован в строку, мы можем вызвать его метод toString ().
Вызов toString () для функции возвращает строковое представление тела функции . Таким образом, если мы вызовемperson.sayHi.toString (), мы получим следующий результат:

"function () { console.log(‘Susan says hi!’); }"

Итак, теперь, когда мы знаем, как получить тело нашего метода, давайте создадим логику для сериализации всего объекта person, включая его метод.

'use strict'; 
let person = {  
  name: 'Susan',  
  age: 24,  
  sayHi: function() { 
    console.log('Susan says hi!');  
  }
}; 
let replacer = (key, value) => {  
  // if we get a function, give us the code for that function  
  if (typeof value === 'function') {    
    return value.toString();  
  }   
  return value;
} 
// get a stringified version of our object 
// and indent the keys at 2 spaces
const serialized = JSON.stringify(person, replacer, 2); 
console.log(serialized); 
// {"name":"Susan","age":24,"sayHi":"function () {\n\tconsole.log('Susan says hi!');\n  }"}

Десериализация и восстановление свойств

Как видите, теперь у нас есть наш метод, должным образом сериализованный и готовый к транспортировке «по сети». Что нам нужно сделать дальше, так это десериализовать наш объект и снова сделать метод sayHi исполняемым, и это можно сделать с помощью JSON.parse.

Подобно тому, что мы сделали с JSON.stringify, нам нужно будет взглянуть на подпись JSON.parse, чтобы получить более четкое представление о том, как мы будем выполнять процесс десериализации таким образом, чтобы мы могли преобразовать наш текущий sayHi строка в функцию, снова.

См. также:  Ошибка Flutter json.decode ‹! Doctype html› ошибка

Согласно Mozilla Developer Network, JSON.parse имеет следующую подпись:

JSON.parse(text[, reviver]) 
// => text(required) - the string we wish to de-serialize 
//    and convert back to a standard JavaScript object(JSON)
// => reviver(optional) - function used to pre-process keys and
//    values in order to render a specific object structure

Прежде чем продолжить работу с решением, давайте сначала посмотрим, что такое строки шаблона на самом базовом уровне. Согласно MDN, шаблонные литералы (обычно называемые шаблонными строками) представляют собой строковые литералы, допускающие встроенные выражения. Чтобы упростить это объяснение, давайте взглянем на фрагмент кода, использующий строки шаблона ES6, и сравним его с тем, как мы использовали для имитации этого в ES5.

//ES6 template strings
let someComputedValue = 5 + 3;
let theTemplate = `This is the result of the computation: 
                   ${someComputedValue}`; 
console.log(theTemplate); 
// "This is the result of the computation: 8" 
// ES5 version
var someComputedValue = 5 + 3;
var theTemplate = 'This is the result of the computation: ' + 
                   someComputedValue; 
console.log(theTemplate);
// "This is the result of the computation: 8"

Вероятно, вы можете увидеть несколько хороших вариантов использования шаблонных литералов, одним из которых являются шаблоны DOM. Если вам нужна дополнительная информация по теме, эта страница в сети разработчиков Mozilla суммирует ее довольно хорошо.

Учитывая, что у нас уже есть наша сериализованная версия объекта person вместе с его методом sayHi, давайте теперь попробуем преобразовать тело метода обратно в исполняемый код.
В этом случае нам придется прибегнуть к к «злому» eval, и прежде чем использовать что-то, что многие люди называют злом, давайте посмотрим, что он делает, чтобы мы осознавали риски, на которые мы идем.

Вкратце, eval принимает строки и рассматривает их как исполняемый код. Один из самых больших рисков, которым вы подвергаете себя и свое приложение, — это инъекция кода. Если вы в конечном итоге сериализуете введенные пользователем данные, обязательно тщательно проверьте их ввод и, как правило, не запускайте код, вставленный вашим пользователем, если только вы не создаете следующий jsbin.com или следующий codepen.io

Зная, что у нас есть о строках eval и шаблонах, мы можем теперь построить нашу функцию оживления следующим образом:

let reviver = (key, value) => {  
  if (typeof key === 'string' && key.indexOf('function ') === 0) {
    let functionTemplate = `(${value})`;    
    return eval(functionTemplate);  
  }   
  return value;
};

Код довольно понятен, но для большей ясности мы создаем новый шаблон для всех элементов в строке, которую мы анализируем, которые содержат ключевое слово ‘function’, за которым следует пробел, в самом начале, а затем мы оцениваем получившееся выражение и возвращаем его в JSON.parse, чтобы добавить к окончательному объекту.
Окончательная версия кода будет выглядеть так, как показано ниже:

'use strict'; 
// serialize.js 
let person = {  
  name: 'Susan',  
  age: 24,  
  sayHi: function() {    
    console.log('Susan says hi!');  
  }
}; 
let replacer = (key, value) => {  
  // if we get a function give us the code for that function  
  if (typeof value === 'function') {
    return value.toString();  
  }   
  return value;
} 
// get a stringified version of our object
// and indent the keys at 2 spaces
const serialized = JSON.stringify(person, replacer, 2);
 
console.log(serialized); 
// {"name":"Susan","age":24,"sayHi":"function () {\n\tconsole.log('Susan says hi!');\n  }"}  
// de_serialize.js
let reviver = (key, value) => {  
  if (typeof value === 'string' 
      && value.indexOf('function ') === 0) {    
    let functionTemplate = `(${value})`;    
    return eval(functionTemplate);  
  }  
  return value;
} 
const parsedObject = JSON.parse(serialized, reviver); 
parsedObject.sayHi(); // Susan says hi!

Причина, по которой мы заключаем функцию в круглые скобки, заключается в том, что нам нужно заставить vm оценивать функцию в контексте выражения, а наша функция должна быть выражением функции, поскольку у нее отсутствует ее имя. Мы работаем с eval только со значением ключа sayHi, который является безымянной функцией. Если вам нужна дополнительная информация об этом ограничении eval, взгляните на этот ответ на StackOverflow.
Как вы можете видеть, у вновь полученного объекта есть метод sayHi, доступный и готовый к использованию, и выполнение метода печатает то же сообщение, что и person.sayHi.

См. также:  Как объединить таблицу с древовидной структурой в один вложенный объект JSON?

Еще одна проблема, которую я не упомянул о eval, заключается в том, что он довольно медленный, потому что он должен вызывать виртуальную машину для оценки кода, в отличие от встроенного конструктора функций, который сильно оптимизирован в новых версиях движков JavaScript. Если производительность для вас критична, есть возможность использовать конструктор Function вместо eval в функции yourreviver, как показано в приведенном ниже фрагменте:

let reviver = (key, value) => {  
  if (typeof value === 'string' 
      && value.indexOf('function ') === 0) {    
    let functionTemplate = `(${value}).call(this)`;    
    return new Function(functionTemplate);  
  }  
  return value;
};

Несмотря на то, что сказано, что конструктор функций быстрее, чем eval, будьте осторожны, так как в этом случае вам нужно создать в памяти 2 функции — внешний IIFE, который автоматически запускается и возвращает нужный нам метод.
Вам также необходимо использовать Function.prototype.call, чтобы сохранить это относительно текущего объекта, а не окна / глобального объекта.

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

Статья также доступна в моем личном блоге.

Ссылка

Понравилась статья? Нашли это полезным? Следуйте за мной в Medium / Twitter.

Понравилась статья? Поделиться с друзьями:
IT Шеф
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: