Search code examples
javascriptgetter-setter

Possible to loop over properties to dynamically define getters/setters?


Assuming the following:

var first = {
    bob: null,
    jim: null,
    sue: null
},
second = {};

I'd like to be able to loop over the properties of first and define getters/setters on second for each; something like the following:

for (var name in first) {
    Object.defineProperty(second, ('' + name), {
        get: function() {
            return first[name];
        },
        set: function(value) {
            first[name] = value;
        }
    });
}

The problem is the getters/setters seem to only affect the last property iterated over, in this case sue.

However, the following seems to work just fine:

var second = {
    get bob() {
        return first['bob'];
    },
    set bob(value) {
        first['bob'] = value;
    },
    get jim() {
        return first['jim'];
    },
    set jim(value) {
        first['jim'] = value;
    },
    get sue() {
        return first['sue'];
    },
    set sue(value) {
        first['sue'] = value;
    }
};

I know this has to be something obvious I'm missing, but I couldn't seem to find another question on here that accurately addresses how to accomplish this.

Thanks in advance!


Solution

  • As mentioned in the comments it is a problem of scope.

    Your variable is, indeed, "hoisted", but that's not really the problem. This is mainly because the variable name is part of the global scope. In JavaScript, scopes are mainly functions. Control structures as loops, conditions... does not declare new scopes.

    If you do that :

    var first = {
      bob: 'text',
      jim: 42,
      sue: true
    },
    second = {};
    
    for (var name in first) {
      Object.defineProperty(second, name, {
        value: first[name],
        enumerable: true
      });
    }
    
    console.log(second.bob); // "test"
    console.log(second.jim); // 42
    console.log(second.sue); // true

    It will works because first[name] is evaluates at each iteration of the loop.

    Now if you do that :

    var first = {
      bob: 'text',
      jim: 42,
      sue: true
    },
    second = {};
    
    for (var name in first) {
      Object.defineProperty(second, name, {
        get: function() {
          return first[name];
        },
        set: function(value) {
          first[name] = value;
        },
        enumerable: true
      });
    }
    
    console.log(second.bob); // true
    console.log(second.jim); // true
    console.log(second.sue); // true

    It won't works because first[name] is evaluates only when you call the getter/setter function. If after the loop you do : second.bob the getter function will be call and it won't find name in his own scope. It will then search in the parent scope, which is here the global scope, where name is defined. The loop being finished, name is equal to the last loop iteration. It is the same with the setter if you do second.bob = 'new value';. The setter function scope doesn't have a name variable and will look in the global scope where name is the last loop iteration.

    The solution is to define the control structure loop for as a scope for the variable name with the let or const keywords. You could also declare your definedProperty process in a function outside of the loop and only call this function inside the loop by passing the parameter name to her. In this case the value will be copy and 'bound' to the new function scope.

    // With 'let'
    for (let name in first) {
      Object.defineProperty(second, name, {
        // ...
      });
    }
    
    // With 'const'
    for (const name in first) {
      Object.defineProperty(second, name, {
        // ...
      });
    }
    
    // With function
    var defineProp = function (name) {
      Object.defineProperty(second, name, {
        // ...
      });
    };
    
    for (var name in first) {
      defineProp(name);
    }