1 (изменено: dab00, 2013-10-20 11:43:50)

Тема: Клонируем объекты в Node.js

Люблю я изобретать велосипеды. Не далее чем на прошлой неделе потратил немало времени на то, чтобы построить нужную структуру данных на Node.js. Убедился, что найти информацию на подобный предмет в сети непросто, поэтому спешу сохранить кое-какой экспириенс. Большинство изложенных примеров можно воспроизвести в консоли браузера (Chrome, Firefox, за прочие не ручаюсь).

На входе есть данные, которые выглядят примерно так:


var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}

На выходе нужно получить:


cache.tag_by_cat:  [
  {"id":1,"name":"Сладкий","path":"Сладкий/","tag":[
    {"id":1,"name":"Сахар","path":"Сладкий/Сахар/"},
    {"id":2,"name":"Перец","path":"Сладкий/Перец/"}]},
  {"id":2,"name":"Горький","path":"Горький/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]}
]

Если начну рассказывать зачем - затяну песню на неделю... просто нужно .

Открываем блокнот, я использую Notepad++, пишем код:


var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = cache.cat.map(function(cat) {
  cat.path = cat.name + '/';  
  cat.tag = cache.tag.filter(function(tag) {
    return cache.tag_to_cat.filter(function(tag_to_cat) {
      return tag_to_cat.cat_id == cat.id;
    }).map(function(tag_to_cat) {
      return tag_to_cat.tag_id
    }).indexOf(tag.id) !== -1;
  }).map(function(tag) {
    tag.path = cat.path + tag.name + '/'; 
    return tag;
  });
  return cat;
});
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Выполняем:
http://4.bp.blogspot.com/-b4pyPkqiV5k/UlEPxZTD5rI/AAAAAAAAB5U/gFl6cO-r3m8/s1600/node-clone-1.jpg

Получаем следующий объект:


cache.tag_by_cat:  [
  {"id":1,"name":"Сладкий","path":"Сладкий/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]},
  {"id":2,"name":"Горький","path":"Горький/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]}
]

Тысяча чертей, сударь! Где мой Сладкий Сахар и Сладкий Перец?

Переписываем код "по-стариковски" - без мапов и фильтров - по-моему так будет  понятнее что происходит:


var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = [];
for (var i=0; i<cache.cat.length; i++) {  
  cache.cat[i].path = cache.cat[i].name + '/';
  cache.cat[i].tag = [];
  for (var j=0; j<cache.tag_to_cat.length; j++) {
    if (cache.tag_to_cat[j].cat_id === cache.cat[i].id) {
      for (var k=0; k<cache.tag.length; k++) {
        if (cache.tag[k].id === cache.tag_to_cat[j].tag_id) {          
          cache.tag[k].path = cache.cat[i].path + cache.tag[k].name + '/';
          cache.cat[i].tag.push(cache.tag[k]);          
          break;
        }
      }
    }    
  }
  cache.tag_by_cat.push(cache.cat[i]);  
}
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Выполняем:
http://4.bp.blogspot.com/-03NNT7B4Bd4/UlEQr8sl7CI/AAAAAAAAB5c/1A5Gfx_Afos/s1600/node-clone-2.jpg

То же самое. Но теперь можно понять куда пропал Сладкий Сахар и Сладкий Перец.
Добавим в код пару строчек для вывода в консоль свойств исходных объектов cache.cat и cache.tag:


console.log('cache.cat: ', JSON.stringify(cache.cat));
console.log('cache.tag: ', JSON.stringify(cache.tag));

Выполним код еще раз:
http://3.bp.blogspot.com/-lwp-ICdSBNc/UlERLPWhXfI/AAAAAAAAB5o/RQPXmNV2E2Y/s1600/node-clone-3.jpg

Получаем следующие объекты:


cache.cat:  [
  {"id":1,"name":"Сладкий","path":"Сладкий/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]},
  {"id":2,"name":"Горький","path":"Горький/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]}
]
cache.tag:  [
  {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
  {"id":2,"name":"Перец","path":"Горький/Перец/"}
]

Становится понятно, что изменять свойства иходных объектов нам ни к чему, а нужно эти самые объекты как-то клонировать.
Первое, что приходит на ум - JSON.parse(JSON.stringify(obj)):


var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = [];
var cat, tag;
for (var i=0; i<cache.cat.length; i++) {
  cat = JSON.parse(JSON.stringify(cache.cat[i]));
  cat.path = cat.name + '/';
  cat.tag = [];
  for (var j=0; j<cache.tag_to_cat.length; j++) {
    if (cache.tag_to_cat[j].cat_id === cache.cat[i].id) {
      for (var k=0; k<cache.tag.length; k++) {
        if (cache.tag[k].id === cache.tag_to_cat[j].tag_id) {
          tag = JSON.parse(JSON.stringify(cache.tag[k]));
          tag.path = cat.path + tag.name + '/';
          cat.tag.push(tag);          
          break;
        }
      }
    }    
  }
  cache.tag_by_cat.push(cat);  
}
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Выполняем:
http://1.bp.blogspot.com/-RDuBwY9mVtU/UlERxHJSslI/AAAAAAAAB5w/mK0VX_pviLw/s1600/node-clone-4.jpg

Bingo! То, что нужно:


cache.tag_by_cat:  [
  {"id":1,"name":"Сладкий","path":"Сладкий/","tag":[
    {"id":1,"name":"Сахар","path":"Сладкий/Сахар/"},
    {"id":2,"name":"Перец","path":"Сладкий/Перец/"}]},
  {"id":2,"name":"Горький","path":"Горький/","tag":[
    {"id":1,"name":"Сахар","path":"Горький/Сахар/"},
    {"id":2,"name":"Перец","path":"Горький/Перец/"}]}
]

Но, согласитесь, как-то не по фэн-шую.
Еще один вариант - Object.create():


var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = [];
var cat, tag;
for (var i=0; i<cache.cat.length; i++) {
  cat = Object.create(cache.cat[i]);
  cat.path = cat.name + '/';
  cat.tag = [];
  for (var j=0; j<cache.tag_to_cat.length; j++) {
    if (cache.tag_to_cat[j].cat_id === cache.cat[i].id) {
      for (var k=0; k<cache.tag.length; k++) {
        if (cache.tag[k].id === cache.tag_to_cat[j].tag_id) {
          tag = Object.create(cache.tag[k]);
          tag.path = cat.path + tag.name + '/';
          cat.tag.push(tag);          
          break;
        }
      }
    }    
  }
  cache.tag_by_cat.push(cat);  
}
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Выполняем:
http://3.bp.blogspot.com/-96tU0lmtSc8/UlESZaZ57RI/AAAAAAAAB54/a72_v4tzGIE/s1600/node-clone-5.jpg

Все ОК. На первый взгляд не совсем - не видим свойства объекта, унаследованные от прототипа, но они есть, просто "by default properties ARE NOT writable, enumerable or configurable".
Для того, чтобы убедиться в их существовании добавим в код несколько строчек:


cache.tag_by_cat.forEach(function(cat) {
  console.log('cat.id: ' + cat.id + '\n' + 'cat.name: ' + cat.name + '\n' + 'cat.tag: ' + JSON.stringify(cat.tag));
  cat.tag.forEach(function(tag) {
    console.log('tag.id: ' + tag.id + '\n' + 'tag.name: ' + tag.name + '\n' + 'tag.path: ' + tag.path);
  });
});

Выполняем:
http://4.bp.blogspot.com/-BBQgdgprK4c/UlETB-91KiI/AAAAAAAAB6M/cUh1H9IDoE4/s1600/node-clone-6.jpg

Теперь точно все ОК.
Еще один, "нативный" для Node.js, вариант с использованием require('util')._extend:


var extend = require('util')._extend;
var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = [];
var cat, tag;
for (var i=0; i<cache.cat.length; i++) {
  cat = extend({path: cache.cat[i].name + '/'}, cache.cat[i])
  cat.tag = [];
  for (var j=0; j<cache.tag_to_cat.length; j++) {
    if (cache.tag_to_cat[j].cat_id === cache.cat[i].id) {
      for (var k=0; k<cache.tag.length; k++) {
        if (cache.tag[k].id === cache.tag_to_cat[j].tag_id) {
          tag = extend({path: cat.path + cache.tag[k].name + '/'}, cache.tag[k])
          cat.tag.push(tag);          
          break;
        }
      }
    }    
  }
  cache.tag_by_cat.push(cat);  
}
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Выполняем:
http://2.bp.blogspot.com/-PFAIZuguj4o/UlETlWi8QeI/AAAAAAAAB6U/poGoPGNZ_P8/s1600/node-clone-7.jpg

Окончательный вариант кода может выглядеть следующим образом:


var extend = require('util')._extend;
var cache = {
  cat: [{"id":1, "name":"Сладкий"}, {"id":2, "name":"Горький"}], 
  tag: [{"id":1, "name":"Сахар"}, {"id":2, "name":"Перец"}], 
  tag_to_cat: [
    {"tag_id":1, "cat_id":1}, {"tag_id":1, "cat_id":2}, // 'Сладкий Сахар', 'Горький Сахар'
    {"tag_id":2, "cat_id":1}, {"tag_id":2, "cat_id":2}, // 'Сладкий Перец', 'Горький Перец'
  ]
}
cache.tag_by_cat = cache.cat.map(function(cat) {  
  cat = extend({path: cat.name + '/'}, cat)
  cat.tag = cache.tag.filter(function(tag) {
    return cache.tag_to_cat.filter(function(tag_to_cat) {
      return tag_to_cat.cat_id == cat.id;
    }).map(function(tag_to_cat) {
      return tag_to_cat.tag_id
    }).indexOf(tag.id) !== -1;
  }).map(function(tag) {
    return extend({path: cat.path + tag.name + '/'}, tag);
  });  
  return cat;
});
console.log('cache.tag_by_cat: ', JSON.stringify(cache.tag_by_cat));

Вот как-то так. Есть вариант лучше?

2 (изменено: Rumata, 2013-10-06 18:29:09)

Re: Клонируем объекты в Node.js

JSON.parse(JSON.stringify(obj))

Я понимаю Ваши игру с двойной конвертацией, но предупреждаю - будьте осторожны: она не выполняет полного клорнирования и не работает с рекурсивными ссылками.

Если я правильно понял, Вам потребовалось из одной структуры данных получить другую. Каким образом node-js улучшил именно саму процедуру получения струтктур я не понял. Но Вы правильно заметили, что

Большинство изложенных примеров можно воспроизвести в консоли браузера

( 2 * b ) || ! ( 2 * b )

3 (изменено: dab00, 2013-10-06 19:53:20)

Re: Клонируем объекты в Node.js

Rumata, я ждал Ваш комментарий , может еще какие-нибудь варианты клонирования объектов в JavaScript (хорошие).
Мне "двойная конвертация" тоже не особенно понравилась, хотя в моем случае достаточно и этого.
Object.create() из ECMAScript 5 на мой взгляд кошернее, но если использовать в браузере, то не уверен на счет оперы, старых ие и т.п.

Каким образом node-js улучшил именно саму процедуру получения струтктур я не понял.

Не знаю на счет "улучшил", но использованный мною стандартный модуль util устроен вот так.
На счет данных - если посмотреть на них повнимательнее, можно сделать правильный вывод, что это сущности из базы данных, имеющие отношение многие-ко-многим.

4

Re: Клонируем объекты в Node.js

я ждал Ваш комментарий

Я пришел и ничего не сказал. (-:

сущности из базы данных, имеющие отношение многие-ко-многим.

Нет возможности сразу получить требуемую структуру из БД?

Object.create() из ECMAScript 5 на мой взгляд кошернее, но если использовать в браузере, то не уверен на счет оперы, старых ие и т.п.

Это решается просто - эмуляцией:


Object.create = Object.create || function(proto)
{
    var F = function() {};
    F.prototype = proto;
    return new F();
};

Полного соответствия стандарту в случае с Object.create Вы не добъетесь, но основную работу - создание нового объекта из заданного - функция делает. Но это совсем не клонирование.

Чтобы уж совсем сотрясанием воздуха не было мое сообщение.

Если Вам нужна функция клонирования - напишите свою - не используйте функции не по назначению. Object.create - это не клонирование объекта, а создание нового с указанием ему старого в качестве прототипа.

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

Мне кажется, надо не клонировать объекты, а копировать только требуемые свойства.

tag = JSON.parse(JSON.stringify(cache.tag[k]));

Этот код в примере выполняется многократно для каждого найденного тега - по количеству категорий. Весьма накладная операция.

( 2 * b ) || ! ( 2 * b )

5

Re: Клонируем объекты в Node.js

Нет возможности сразу получить требуемую структуру из БД?

Нет. Если в ближайшее время решение, по мотивам которого написан этот пост, не уйдет в продакшн - покажу - на мой взгляд "уникальный" движок на Node.js, весь такой "сео", в общем сам тащусь .
Ну если уйдет - найду время, напишу еще вкуснее и все равно покажу , сейчас мне приятно думать что это кому-нибудь будет интересно. И будет понятно почему "Нет возможности сразу получить требуемую структуру из БД".

Это решается просто - эмуляцией:

Согласен. В моем случае V8 с  ECMA-262, 5th edition дружит, поэтому можно без эмуляции, вы предложили  "браузерный" вариант, хозяйкам на заметку .
Хотелось как-то без обращения к прототипам, без велосипедов типа for (var prop in obj), ну вы поняли - заголовок поста - клонируем объект, хотя в моем случае нужно было создать объект с теми же свойствами - никаких методов, ссылок (тем более рекурсивных).
Короче если разработчик столкнется с подобной проблемой, прочитав этот пост наверняка сэкономит время.

Object.create - это не клонирование объекта, а создание нового с указанием ему старого в качестве прототипа.

Именно то, что в моем случае было нужно.

Мне кажется, надо не клонировать объекты, а копировать только требуемые свойства.

ДА, ДА, ДА ДА ДА .

Этот код в примере выполняется многократно для каждого найденного тега - по количеству категорий. Весьма накладная операция.

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