jsFind. Выборка данных из массива объектов

    Задача: Язык javascript. Имеется большой массив объектов. Нужно выбрать из массива некоторые объекты, в зависимости от значений свойств этих объектов.

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

    В качестве вступления: основной код был написан больше двух лет назад. Сначала он использовался как временный и в последствии я его хотел заменить обработкой на backend'e. Время шло код обрастал, мутировал и начал «инфицировать» собой другие проекты. И на моё удивление он везде очень хорошо приживался, заменяя собой связку клиент-сервер в тех случаях где было не так много данных, например, каталоги товаров в которых кол-во наименований не более 1000шт.

    Для демонстрации подготовил вот такой простой массивчик(автолюбителям посвещается):

    var cars=[
    			{
    				brand: 'audi',		//фирма производитель
    				model: 'a4',		//модель
    				volume_engine: '1.8',	//объём двигателя
    				hp: '120',		//кол-во лошадиных сил
    				awd: 'нет',   		//полный привод [да/нет]
    				automat: 'да',		//автомат [да/нет]
    			},
    			{
    				brand: 'audi',		//фирма производитель
    				model: 'a4 allroad',	//модель
    				volume_engine: '2',	//объём двигателя
    				hp: '211',		//кол-во лошадиных сил
    				awd: 'да',   		//полный привод [да/нет]
    				automat: 'да',		//автомат [да/нет]
    			},
    			{
    				brand: 'audi',		//фирма производитель
    				model: 'a6',		//модель
    				volume_engine: '2',	//объём двигателя
    				hp: '180',		//кол-во лошадиных сил
    				awd: 'нет',   		//полный привод [да/нет]
    				automat: 'да',		//автомат [да/нет]
    			},
    			{
    				brand: 'bmw',		//фирма производитель
    				model: '3 Series',	//модель
    				volume_engine: '1.6',	//объём двигателя
    				hp: '135',		//кол-во лошадиных сил
    				awd: 'нет',   		//полный привод [да/нет]
    				automat: 'нет',		//автомат [да/нет]
    			},
    			{
    				brand: 'bmw',		//фирма производитель
    				model: '5 Series',	//модель
    				volume_engine: '3',	//объём двигателя
    				hp: '258',		//кол-во лошадиных сил
    				awd: 'нет',   		//полный привод [да/нет]
    				automat: 'да',		//автомат [да/нет]
    			},
    			{
    				brand: 'volkswagen',	//фирма производитель
    				model: 'passat',	//модель
    				volume_engine: '1.8',	//объём двигателя
    				hp: '152',		//кол-во лошадиных сил
    				awd: 'нет',   		//полный привод [да/нет]
    				automat: 'да',		//автомат [да/нет]
    			},
    ]
    


    Начнём сразу с примеров. Не обращайте внимание на «ещё...» — осталось от console.log(). Для начала выберем из массива все автомобили марки audi (попробовать):

    cars.find({ brand:"audi"})
    
    //результат:
    [
    	{ brand="audi",  model="a4",  volume_engine="1.8",  ещё...}, 
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...},
    	{ brand="audi",  model="a6",  volume_engine="2",  ещё...}
    ]
    


    Выберем все модели a4 марки audi (попробовать):

    cars.find({ brand: "audi", model: "%a4%" })
    
    //результат:
    [
    	{ brand="audi",  model="a4",  volume_engine="1.8",  ещё...},
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}
    ]
    


    Выберем все модели a4 марки audi с полным приводом (попробовать):

    cars.find({ brand: "audi", model: "%a4%", awd:"да" })
    
    //результат:
    [
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}
    ]
    


    — тоже самое, но выборка идёт по очереди (попробовать):

    cars.find({ brand: "audi" }).find({ model: "%a4%" }).find({ awd: "да" })
    
    //результат:
    [
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}
    ]
    


    А теперь найдем все авто с мощностью двигателя больше 200 лошадок (попробовать):

    cars.find({ hp: ">=200" })
    
    //результат:
    [
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}, 
    	{ brand="bmw",  model="5 Series", volume_engine="3",  ещё...}
    ]
    


    Выберем двух и трёхлитровые машины (попробовать):

    cars.find({ volume_engine: ["2","3"] })
    
    //результат:
    [
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}, 
    	{ brand="audi",  model="a6",  volume_engine="2",  ещё...}, 
    	{ brand="bmw",  model="5 Series",  volume_engine="3",  ещё...}
    ]
    


    Выберем двух и трёхлитровые машины фирмы audi (попробовать):

    cars.find({ brand:"audi", volume_engine:["2","3"] })
    
    //результат:
    [
    	{ brand="audi",  model="a4 allroad",  volume_engine="2",  ещё...}, 
    	{ brand="audi",  model="a6",  volume_engine="2",  ещё...}
    ]
    


    Выберем все машины у которых объём двигателя строго меньше 2ух литров и больше или равен 3ём (попробовать):

    cars.find({volume_engine:["<2",">=3"] })
    
    //результат:
    [
    	{ brand="audi",  model="a4",  volume_engine="1.8",  ещё...}, 
    	{ brand="bmw",  model="3 Series",  volume_engine="1.6",  ещё...}, 
    	{ brand="bmw",  model="5 Series",  volume_engine="3",  ещё...},
    	{ brand="volkswagen",  model="passat",  volume_engine="1.8",  ещё...}
    ]
    


    Думаю суть понятна. Метод find из исходного массива выбирает объекты, удовлетворяющие условиям, которые передаются методу, в качестве параметров. На выходе получается обычный массив объектов, и из него снова можно выбирать данные: cars.find({...}).find({...}).find({...}).find({...});

    GitHub
    jsfiddle

    p.s.#0 о методе find в MongoDB знаю, с него то всё и началось. Постараюсь в будущем сделать очень-очень похожим на него.
    p.s.#1 в сл.версии добавлю возможность расширять метод find своими способами фильтрации.
    p.s.#2 в следующей статье расскажу о jquery плагине на основе find.js, который берёт массив объектов и формирует из него фильтр со списком товаров и пагинацией, и разметкой bootstrap.
    p.s.#3 экранирование условий — пока ещё не придумал как это элегантно решить.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 35
    • 0
      А чем метод filter у массива не угодил?
      • –1
        Каждый раз для простых выборок писать callback функцию только код марать. Безусловно если нужно элементы отфильтровать каким-нибудь хитрым макаром, то filter это наш выбор.
        • 0
          Сравните:

          cars.filter(function(element, index, array) {
            return (element.brand == "audi" && (element.volume_engine == "2" || element.volume_engine == "3"));
          });
          

          и

          cars.find({ brand:"audi", volume_engine:["2","3"] })
          

          • +1
            Ну можно чуть покороче, конечно :)

            cars.filter(function(el) {
              return (el.brand == "audi" && (el.volume_engine == "2" || el.volume_engine == "3"));
            });
            


            Тут, конечно, CoffeeScript чуть более хорош:
            (c for c in cars when c.brand == "audi" and c.volume_engine in ["2", "3"])
            
            • +2
               cars.filter( (c) -> c.brand == "audi" and c.volume_engine in ["2", "3"] ) 

              если не ошибся в синтаксисе
              • 0
                Да, разумеется. Просто мне синтаксис генераторов нравится :)
              • +2
                И ещё короче:
                cars.filter(function(car) car.brand==="audi" && ["2", "3"].indexOf(car.volume_engine)>=0});
                


                А в проекте и ещё чуть короче:
                cars.filter(car => car.brand==="audi" && ["2", "3"].indexOf(car.volume_engine)>=0);
                


                Или так:
                [car for(car of cars) if(car.brand==="audi" && ["2", "3"].indexOf(car.volume_engine)>=0)]
                
                • +1
                  Да уж, скорее бы Harmony «в люди» вышел: многие вещи значительно упрощают жизнь.
            • +1
              Ну возможно, тогда ваш find — это просто «сахар», тем более что он, как я понимаю, является расширением для прототипа Array.

              А вообще, как идея — полноценно реализовать «язык запросов» MongoDB для обычных коллекций, да еще и с поддержкой аггрегирования было бы круто.
              • 0
                Ниже куча ссылок на подобные проекты.
                Плюс еще есть TinyData, которая полноценный запросник по слабоструктурированным данным :)
              • 0
                Как вариант (я использую)
                function Finder(arr){ this.data = arr; this.find = function(){ ...} }

                И уже кручу верчу массивами, без прототипирования базовых объектов.

                Как тот так
                var cars = new Finder(arrayCars); cars.find({ })
              • +1
                Ну если посмотреть в код то он там и используется.

                Не радует что идет расширение базового прототипа…
                • 0
                  Всегда есть дилемма расширять ли базовый прототип. С одной стороны в академических кругах это возбраняется, с другой стороны без расширения базового прототипа не получится сделать каскадную выбору данных:

                  cars.find({...}).find({...}).find({...}).find({...});
                  

                  Да и код такой:

                  cars.find({ brand: "audi" });
                  

                  читать приятнее, чем вот такой:

                  find(cars,{ brand: "audi" });
                  

                  • +2
                    То что вы свой метод дописываете это меньше из зол, это можно указать в readme и человек подключая вашу библиотеку будет знать что базовые классы расширяться методам и find и page. Но вот то что он не может знать, это то что добавляются еще и стандартные методы filter, indexOf, some. Вот такое неявство и есть зло. И вот представьте ситуацию, человек подключил вашу библиотеку, и чудным образом начал везде в коде юзать indexOf, some и прочее, потом отключил и у него херак, и в IE<9 все упало.
              • +1
                filter + функция, которая генерирует функцию.

                arr.filter( Array.findFn({brand: 'audi'}) )
                

                Можно из этого сделать обёртку find, но основной смысл — не парсить входные данные каждый раз, а иметь возможность во время инициализации один раз сделать

                var audiFilter = Array.findFn({brand: 'audi'});
                // а потом юзать во всех местах где надо выбрать audi 
                arr.filter( audiFilter ); 
                


                Кстати, это именно тот самый кейс где eval не является злом, а даёт ощутимое ускорение.

                Array.findFn({brand: 'audi'}) // должно генерировать вот такую простую функцию
                function(el){ return el['brand'] === 'audi'; }
                
                • 0
                  — ну как вариант. Только в таком случае уже теряется элегантность кода, что ли.
                  • +1
                    Вот для кейса поиска равенства:
                    Array.find = function( match ){
                        var i, pairs = [];
                        for( i in match )
                            match.hasOwnProperty( i ) &&
                                pairs.push( 'el[\'' + i + '\'] === \''+ match[i] +'\''); // тут надо учесть экранирование кавычек в кей и в валью
                                
                        return Function('el', 'return ' + pairs.join(' && ')  + ';');
                    };
                    Array.prototype.find = function( match ){
                        return this.filter( Array.find.call( this, match ) );
                    }
                    


                    После этого можно делать вот так:
                    cars.find({brand: 'audi'});
                    


                    А можно вот так:
                    var audiFilter = Array.find({ brand: 'audi' });
                    cars.filter( audiFilter );
                    


                    Чейнинг тоже работает. ( cars.find({ brand: «audi» }).find({ awd: «да» }) ) Получается покрытие обоих кейсов.
                • 0
                  Писать велосипеды это хорошо и полезно.
                  Но я все равно хочу обратить внимание, на замечательную библиотеку linq.js (как можно догадаться порт LINQ на JavaScript).
                  Позволяет делать с массивами данных все то же, что умеет оригинальный LINQ, т.е. выбрать данные любой вложенности по запросу любой сложности, отсортировать их по любому количеству полей и еще много всего.
                  Вы можете оценить возможности библиотеки посмотрев код из документации.
                  • 0
                    Есть ещё проект jLinq.
                    • 0
                      Интересная библиотека, но все же LINQ на мой взгляд гораздо функциональнее.
                  • +5
                    underscorejs.org/ умеет и выборки делать, и еще много всяких приятных плюшек
                    • 0
                      И sugarjs.com/ — кому-то такой вариант может показаться удобнее, особенно если был опыт работы с Prototype.
                    • +1
                      • +5
                        В стандарте ES6 есть метод Array.prototype.find и наиболее продвинутые его уже используют.
                        Плохо расширять прототип, у вас повторяется ситуация которая была с Prototype — стандартные методы реализованные по спецификации конфликтовали с методами библиотеки.

                        Тем более у вас же в библиотеки проверяется наличие этого метода и если я подключу es6-shim или когда браузеры реализуют Array#find ваша библиотека перестанет работать.

                        • 0
                          Правильно я понимаю, что .find (ES6) — это тот же .filter, только плюс механиз break при возврате null?
                          • +1
                            Что-то я не правильно прочитал описание метода, сейчас уже разобрался, что выше спросил глупость.
                        • 0
                          По моему — всего лишь простейшая обертка для filter.
                          Вот вещь! square.github.io/crossfilter/
                          • 0
                            Похоже ссылка неправильная.
                            • 0
                              Ссылка правильная, это пример работы кроссфильтр на массиве в 200к записей. Кроссфильтр это не просто обертка на массиве, строится индекс по каждому из заданых полей и благодаря этому обеспечивается такой быстрый поиск.
                              • 0
                                Прошу прощения, интернет подвел. Теперь все открывается как нужно.
                          • +1
                            Справедливости ради стоит отметить, что LoDash умеет where-предикат на большинстве функций, принимающих коллбек.

                            Например, представим себе функцию filter (или функцию find, или функцию any).
                            Каждая из них в прямолинейном варианте принимает коллбек, который должен возвращать истиностное значение, результат проверки.
                            Но есть также два специальных синтаксиса:
                            1. Если передать вместо коллбека строку, то будет создан специальный коллбек, который возвращает атрибут передаваемого ему объекта с таким именем (т.н. pluck-стиль, по имени функции, выбирающей атрибуты объектов).
                            2. Если передать вместо коллбека словарь, то будет производиться сверка его свойств со сверками объекта (т.н. where-стиль). Именно этот случай описан у топикстартера.
                            Также такой специальный коллбек можно создавать самому.

                            Преимущество данной либы в возможности задавать оператор (использовать не только равенство). Схожие возможности удобны в ORM. Из знакомых мне, ORM Django позволяет такие фишки.
                            • 0
                              Автору несомненно благодарность за поделку. Всячески поддерживаю ваше начинание. Но есть несколько пожеланий:

                              1) Полностью согласен с комментарием пользователя termi в отношении фразы плохо расширять прототип. Видел ваш код накануне и это стало поводом воздержаться от звезды.

                              2) Жаль что написано не на coffee. Я сам хотел написать подобную поделку just for fun или присоединиться к разработке чего-то подобного. Но писать на чистом JS уже лень

                              В целом считаю идею проекта хоть и не новой, но весьма занятной и достойной потраченного времени. Желаю успехов!

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

                            Самое читаемое