Страницы

воскресенье, 6 октября 2013 г.

Клонируем объекты в 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));

Выполняем:

Получаем следующий объект:
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));

Выполняем:

То же самое. Но теперь можно понять куда пропал Сладкий Сахар и Сладкий Перец.
Добавим в код пару строчек для вывода в консоль свойств исходных объектов cache.cat и cache.tag:
console.log('cache.cat: ', JSON.stringify(cache.cat));
console.log('cache.tag: ', JSON.stringify(cache.tag));

Выполним код еще раз:

Получаем следующие объекты:
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));

Выполняем:

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));

Выполняем:

Все ОК. На первый взгляд не совсем - не видим свойства объекта, унаследованные от прототипа, но они есть, просто "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);
  });
});

Выполняем:

Теперь точно все ОК.
Еще один, "нативный" для 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));

Выполняем:

Окончательный вариант кода может выглядеть следующим образом:
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));

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