13
\$\begingroup\$

I am trying to optimize an object filter function. Given an array of keys, I need to filter an object. I feel that creating a new object each time may degrade performance. Is there a way to mock the Array.prototype.filter function for objects?

function main() {
    var Types = {
        INTEGER:   { value: 'int'    },
        CHARACTER: { value: 'char'   },
        FLOAT:     { value: 'float'  },
        DOUBLE :   { value: 'double' },
        STRING:    { value: 'str'    },
        BOOLEAN:   { value: 'bool'   }
    };
    
    var Numbers = filterByKeys(Types, [ 'INTEGER', 'FLOAT', 'DOUBLE' ]);
    
    console.log(Numbers);
};

function listToSet(list) {
    var _set = {};
    if (Ext.isArray(list)) {
        for (var i = 0; i < list.length; i++) {
            _set[list[i]] = true;
        }
    }
    return _set;
};

function filterByKeys(obj, keep) {
    var result = {},
        unfiltered = keep === undefined,
        keys = listToSet(keep);
    if (Ext.isObject(obj)) {
        Ext.Array.each(Object.keys(obj), function(key) {
            if (unfiltered ||  (!unfiltered && keys[key])) {
                result[key] = obj[key];
            }
        });
    }
    return result;
};

Ext.onReady(function () {
    main();
});
.as-console-wrapper { top: 0; max-height: 100% !important; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/extjs/4.2.1/ext-all.js"></script>

Without ExtJS Dependency

function main() {
    var Types = {
        INTEGER:   { value: 'int'    },
        CHARACTER: { value: 'char'   },
        FLOAT:     { value: 'float'  },
        DOUBLE :   { value: 'double' },
        STRING:    { value: 'str'    },
        BOOLEAN:   { value: 'bool'   }
    };
    
    var Numbers = filterByKeys(Types, [ 'INTEGER', 'FLOAT', 'DOUBLE' ]);
    
    console.log(Numbers);
};

function listToSet(list) {
    var _set = {};
    if (isArr(list)) {
        for (var i = 0; i < list.length; i++) {
            _set[list[i]] = true;
        }
    }
    return _set;
};

function filterByKeys(obj, keep) {
    var result = {},
        unfiltered = keep === undefined,
        keys = listToSet(keep);
    if (isObj(obj)) {
        for (var key in obj) {
            if (unfiltered ||  (!unfiltered && keys[key])) {
                result[key] = obj[key];
            }
        };
    }
    return result;
};

var tObj='[object Object]',tArr='[object Array]',str=Object.prototype.toString;
var isArr='isArray'in Array?Array.isArray:function(v){return str.call(v)===tArr};
var isObj=str.call(null)===tObj?function(v){return v!==null&&v!==void 0&&str.call(v)===tObj&&v.ownerDocument===void 0}:function(v){return str.call(v)===tObj
};

$(document).ready(function () {
    main();
});
.as-console-wrapper { top: 0; max-height: 100% !important; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

\$\endgroup\$
  • \$\begingroup\$ If you already have lodash in your toolkit, you can use _.pick for this task. \$\endgroup\$ – John Rix Feb 5 '18 at 0:37
12
\$\begingroup\$
  1. Use var myKeys = Object.keys(myObject) to get the keys.
  2. Check if a myString exist in the array myKeys using native

    var matchingKey = myKeys.indexOf(myString) !== -1
    

    Processing an array? Use this:

    var matchingKeys = myKeys.filter(function(key){ return key.indexOf(myString) !== -1 });
    
  3. Get the value using myObject[matchingKey].

    Processing an array? Use this:

    var matchingValues = matchingKeys .map(function(key){ return myObject[key] });
    
  4. No need to check array-boundaries or sizes. It's 100% boundary-safe.

\$\endgroup\$
  • \$\begingroup\$ Step 2. Can be simplified var matchingKey = myKeys.includes(myString) and for array Processing var matchingKeys = myKeys.filter((key) => key.includes(myString)); \$\endgroup\$ – Tim Jun 28 '18 at 0:54
  • \$\begingroup\$ Hi @Jamal, in nowadays (2019!)... No modern EC6 filter for objects? \$\endgroup\$ – Peter Krauss Feb 11 at 18:52
  • \$\begingroup\$ @PeterKrauss: I'm the editor, not the poster. That account has since been deleted anyway. \$\endgroup\$ – Jamal Feb 12 at 4:02
5
\$\begingroup\$

I think that the more important questions you need to be asking yourself wrt your code are:

  1. What am I really trying to accomplish? (Answering this question will ultimately resolve your uncertainty related to whether you need to create a new object every time or, maybe, modify in place etc.)
  2. Am I using the right data-structure for the task? Look, you've built your data-structure from hash-tables, each with a single key! Not only that, the same key repeats in every such hash-table! What would you lose if you used just the value from that hash-table instead of the entire hash-table?
  3. This has become a mantra, but it is as true as ever: Make It Work, Make It Right, Make It Fast. Do not optimize unless the profiler tells you to. And most certainly not before you are absolutely confident that you made it right.

As an example of how your data-structure could've been made different such as to allow you to make filtering more straightforward:

var types = {
    'integer':   'int', 
    'character': 'char',
    'float':     'float',
    'double':    'double',
    'string':    'str', 
    'boolean':   'bool'
};

function filterTypes(accepted) {
    var result = {};
    for (var type in types)
        if (accepted.indexOf(type) > -1) 
            result[type] = types[type];
    return result;
}
// filterTypes(['integer', 'float', 'double']);
// { integer: 'int',
//   float: 'float',
//   double: 'double' }

And this is all there is to it. I would also question the purpose of these types in JavaScript, but I assumed they aren't to be implemented in JavaScript, just some kind of labels for some other programming language.

Note, additionally, that since the types hash-table is relatively small, there is no point of copying the keys to check against into another hash-table (to achieve faster lookup instead of indexOf). With this number of types indexOf will most likely outperform hash-table lookup, even more importantly. It offers a simpler solution, which works with acceptable speed.

\$\endgroup\$
  • \$\begingroup\$ But in nowadays (2019!)... No modern EC6 filter for objects? \$\endgroup\$ – Peter Krauss Feb 11 at 18:51
  • 1
    \$\begingroup\$ @PeterKrauss I think, you meant ES6, and... I don't have the standard handy, but, from looking at MDN: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… there isn't a method for filtering object fields, unless I'm blind. But, my guess is that JS standardization committee cannot still decide whether {} is just a hash-table, or is it more like a class instance in other languages. Every now and then the standard shifts from one perspective to another, but it's what prevents them from treating it just like a hash-table, and adding that kind of functionality \$\endgroup\$ – wvxvw Feb 12 at 11:20
3
\$\begingroup\$
  1. unfiltered || (!unfiltered && keys[key]) is redundant you can just do unfiltered || keys[key]

  2. You will run into false positives for any property set on Object.prototype such as "toString" or "valueOf" (for instance consider {valueOf: 1})

  3. keys[key] is not a sufficent as it will miss any falsey value (NaN, null, false, 0, "", etc). You should use either the in operation or hasOwnProperty

  4. This can be done considerably faster (and more accurately) by iterating the keep array instead of the properties on obj (lets you get around a Object.keys call)


This is how I would go about it

// Avoid issues with `{hasOwnProperty: 5}`
var hasOwnProperty = ({}).hasOwnProperty;
function filterByKeys(obj, keep) {
    if (!(Ext.isObject(obj) && keep)) {
        return Ext.clone(obj);
    }

    var result = {};
    for (var i = 0, len = keep.length; i < len; i++) {
        var key = keep[i];
        if (hasOwnProperty.call(obj, key)) {
            result[key] = obj[key];
        }
    }

    return result;
};
\$\endgroup\$

protected by Jamal Feb 5 '18 at 3:11

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.