4

I am trying to delete an item from an object by passing a key to the method. For example I want to delete a1, and to do so I pass a.a1 to the method. It then should delete a1 from the object leaving the rest of the object alone.

This is the structure of the object:

this.record = {
  id: '',
  expiration: 0,
  data: {
    a: {
      a1: 'Cat'
    }
  }
}

I then call this method:

delete(key) {
  let path = key.split('.')
  let data = path.reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)
  if(data) delete data
}

Like this:

let inst = new MyClass()
inst.delete('a.a1')

This however gives me the following error:

delete data;
       ^^^^

SyntaxError: Delete of an unqualified identifier in strict mode.

I assume that data is a reference still at this point, or is it not?

Maybe reduce isn't the right method to use here. How can I delete the item from the object?

1
  • This may not be an efficient answer, but you could flatten the object, delete the key, and the unflatten it. There are several libraries out there that can do the heavy lifting for you. Here's one: github.com/hughsk/flat Commented Aug 21, 2018 at 18:50

5 Answers 5

3

Using your example, the value of data at the point where it is checked for truthiness is Cat, the value of the property you're trying to delete. At this point, data is just a regular variable that's referencing a string and it's no longer in the context of inst.

Here's a solution I managed to get to work using the one from your OP as the basis:

let path = key.split('.')
let owningObject = path.slice(0, path.length - 1)
    .reduce((obj, key) => typeof obj == 'object' ? obj[key] : null, this.record.data)

if (owningObject) delete owningObject[path[path.length - 1]]

The main difference between this and what you had is that reduce operates on a slice of the path segments, which does not include the final identifier: This ends up with owningObject being a reference to the a object. The reduce is really just navigating along the path up until the penultimate segment, which itself is used as the property name that gets deleted.

For an invalid path, it bails out either because of the if (owningObject) or because using delete on an unknown property is a no-op anyway.

Sign up to request clarification or add additional context in comments.

3 Comments

It took me an embarrassing amount of time to realize how owningObject was being calculated. How does this work on invalid paths would you say?
Just tested on a1.abc.efg, and it doesn't throw errors or seem to go rouge and delete things.
I also changed if(owningObject !== undefined) and instead of setting null I set undefined since it wouldn't delete if the value of owningObject is false or null
1

The solution I came up with which I am not super fond of but works, is looping over the items which will allow me to do long keys like this

  • a.a1
  • a.a1.a1-1
  • a.a1.a1-1.sub

The function then looks like this

let record = {
  data: {
    a: {
      a1: 'Cat',
      a2: {
        val: 'Dog'
      }
    }
  }
}

function remove(key) {
  let path = key.split('.')
  let obj = record.data
  for (let i = 0; i < path.length; i++) {
    if (i + 1 == path.length && obj && obj[path[i]]) delete obj[path[i]]
    else if(obj && obj[path[i]]) obj = obj[path[i]]
    else obj = null
  }
}

// Removes `a.a1`
remove('a.a1')
console.log(JSON.stringify(record))

// Removes `a.a2.val`
remove('a.a2.val')
console.log(JSON.stringify(record))

// Removes nothing since the path is invalid
remove('a.a2.val.asdf.fsdf')
console.log(JSON.stringify(record))

2 Comments

What do you dislike about the solution?
I just tried the reduce example I posted with the same test cases that you have here and it gives the same results. What made you switch from a reduce approach to a for-loop?
0

You can delete keys using [] references.

var foo = {
  a: 1,
  b: 2
};
var selector = "a";
delete foo[selector];
console.log(foo);

I'm not sure if this helps you but it might help someone googling to this question.

Comments

0

Here's another method which is very similar to the OP's own solution but uses Array.prototype.forEach to iterate over the path parts. I came to this result independently in my attempt to wrap this up as elegantly as possible.

function TestRecord(id, data) {
    let record = {
        id : id,
        data : data
    };

    function removeDataProperty(key) {
        let parent = record.data;
        let parts = key.split('.');
        let l = parts.length - 1;
        parts.forEach((p, i) => {
            if (i < l && parent[p]) parent = parent[p];
            else if (i == l && parent[p]) delete parent[p];
            else throw new Error('invalid key');
        });
    }

  return {
    record : record,
    remove : function(key) {
        try {
            removeDataProperty(key);
        } catch (e) {
            console.warn(`key ${key} not found`);
        }
    }
  }
}

let test = new TestRecord('TESTA', {
  a : { a1 : '1', a2 : '2' },
  b : { c : { d : '3' } }
});

test.remove('a'); // root level properties are supported
test.remove('b.c.d'); // deep nested properties are supported
test.remove('a.b.x'); // early exit loop and warn that property not found

console.log(test.record.data);

The usage of throw in this example is for the purpose of breaking out of the loop early if any part of the path is invalid since forEach does not support the break statement.

By the way, there is evidence that forEach is slower than a simple for loop but if the dataset is small enough or the readability vs efficiency tradeoff is acceptable for your use case then this may be a good alternative.

https://hackernoon.com/javascript-performance-test-for-vs-for-each-vs-map-reduce-filter-find-32c1113f19d7

Comments

-1

This may not be the most elegant solution but you could achieve the desired result very quickly and easily by using eval().

function TestRecord(id) {
  let record = {
    id : id,
    data : {
      a : {
        a1 : 'z',
        a2 : 'y'
      }
    }
  };

  return {
    record : record,
    remove : function (key) {
      if (!key.match(/^(?!.*\.$)(?:[a-z][a-z\d]*\.?)+$/i)) {
        console.warn('invalid path');
        return;      
      } else {
        let cmd = 'delete this.record.data.' + key;
        eval(cmd);      
      }
    }    
  };
}

let t = new TestRecord('TESTA');
t.remove('a.a1');
console.log(t.record.data);

I have included a regular expression from another answer that validates the user input against the namespace format to prevent abuse/misuse.

By the way, I also used the method name remove instead of delete since delete is a reserved keyword in javascript.

Also, before the anti-eval downvotes start pouring in. From: https://humanwhocodes.com/blog/2013/06/25/eval-isnt-evil-just-misunderstood/ :

...you shouldn’t be afraid to use it when you have a case where eval() makes sense. Try not using it first, but don’t let anyone scare you into thinking your code is more fragile or less secure when eval() is used appropriately.

I'm not promoting eval as the best way to manipulate objects (obviously a well defined object with a good interface would be the proper solution) but for the specific use-case of deleting a nested key from an object by passing a namespaced string as input, I don't think any amount of looping or parsing would be more efficient or succinct.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.