Тема: Клонируем объекты в 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));
Вот как-то так. Есть вариант лучше?