views:

254

answers:

4

I'm trying to add elements to an array that are lazy-evaluated. This means that the value for them will not be calculated or known until they are accessed. This is like a previous question I asked but for objects.

What I ended up doing for objects was

Object.prototype.lazy = function(var_name, value_function) {
  this.__defineGetter__(var_name, function() {
    var saved_value = value_function();
    this.__defineGetter__(var_name, function() {
      return saved_value;
    });
    return saved_value;
  });
}

lazy('exampleField', function() {
  // the code that returns the value I want
});

But I haven't figured out a way to do it for real Arrays. Arrays don't have setters like that. You could push a function to an array, but you'd have to call it as a function for it to return the object you really want. What I'm doing right now is I created an object that I treat as an array.

Object.prototype.lazy_push = function(value_function) {
  if(!this.length)
    this.length = 0;
  this.lazy(this.length++, value_function);
}

So what I want to know is, is there a way to do this while still doing it on an array and not a fake array?

UPDATE: The following function works only if the value_function returns a primitive data type.

Array.prototype.lazy_push = function(value_function) {
  var a = this,
  i = this.length;
  this.push({
    toString: function() {
      return a[i] = value_function();
    }
  });
}

If you try to push say an object that has properties in it, you won't be able to access the properties until you access the object directly. This doesn't happen with setters, that's why I want some kind of setting syntax for Javascript. For now I will be using the fake array, which is enough for what I'm doing.

A: 

There is not. Unfortunately this is a big deal.

orokusaki
A: 

Bleh. It's a major bummer you can't overload the indexer operator in JavaScript. Oh well. We'll just have to be creative and come up with a different solution. This is a good thing (and fun). :-)

LLer, the solution you settle with is a damn good one. Kudos. It's refreshing to come across people who truly understand JavaScript to that degree.

After reading this question I was struck with an epic idea and wrote up some code for the fun of it. My solution to the problem is very similar to yours and many others that have been done before. But, I feel I came up with something unique and very neat so I want to share it.

So I'm hosting a project of mine on CodePlex where I use a very jQuery-esque technique for defining properties (self-contained getter/setter functions) very similar to the one you're using. For the solution I came up with I simply just extrapolated from this pre-existing code of mine. Here's my approach toward the lazy loading of array indexes. Starting from the beginning...

Let's consider a property named "PageSize". Here's how the property would be used with my technique:

var MyClass = function() { }; // MyClass constructor.

var instance = new MyClass();
instance.PageSize(5);

alert(instance.PageSize());

Take notice that the property is a single function where providing a value as the first parameter invokes the setter and leaving out the parameter invokes the getter. The "PageSize" property would be defined as part of the MyClass class like so:

MyClass.prototype.PageSize = function(v) { return this.GetSetProperty("PageSize", v, myFunctionThatDoesLazyLoading); };

The property function is merely a wrapper around a call to the utitily GetSetProperty method which does the actual getting and setting. Here's a snippet of the GetSetProperty function:

Object.prototype.GetSetProperty = function(name, value, loadFunction) {
    if (!this.Properties)
    {
        this.Properties = {};
    }

if (value)
{
    this.Properties[name] = value;
}
else
{       
    if (!this.Properties[name] && loadFunction)
    {
        this.Properties[name] = loadFunction();
    }

    return this.Properties[name]; // This will return "undefined" if property doesn't exist or the loadFunction wasn't provided.
}
};

So that handles properties. But, to provide a means to access indexed values of a possible property of Array type I modify this code further like so:

Object.prototype.GetSetProperty = function(name, value, loadFunction, index) {
  if (!this.Properties)
  {
    this.Properties = {};
  }

  if (typeof index === "number" && this.Properties[name] && this.Properties[name].constructor == Array)
  {
    return this.GetSetArrayProperty(name, index, value, loadFunction);
  }
  else 
  {
    value = index;
  }

  if (value)
  {
    this.Properties[name] = value;
  }
  else
  {    
    if (!this.Properties[name] && loadFunction)
    {
      this.Properties[name] = loadFunction();
    }

    return this.Properties[name]; // This will return "undefined" if property doesn't exist or the loadFunction wasn't provided.
  }
};


Object.prototype.GetSetArrayProperty = function(name, index, value, loadFunction) {
  if (value)
  {
    this.Properties[name][index] = value;
  }
  else
  {
    if (!this.Properties[name][index] && loadFunction)
    {
      this.Properties[name][index] = loadFunction();
    }

    return this.Properties[name][index];
  }
};

The prototype declaration would need to be modified like so:

MyClass.prototype.PageSize = function(i, v) { return this.GetSetProperty("PageSize", v, myFunctionThatDoesLazyLoading, i); };

Everyone reading this can access a working set of the code here: http://jsbin.com/ajawe/edit

Here's a complete listing of the code with tests:

Object.prototype.GetSetProperty = function(name, value, loadFunction, index) {
  if (!this.Properties)
  {
    this.Properties = {};
  }

  if (typeof index === "number" && this.Properties[name] && this.Properties[name].constructor == Array)
  {
    return this.GetSetArrayProperty(name, index, value, loadFunction);
  }
  else 
  {
    value = index;
  }

  if (value)
  {
    this.Properties[name] = value;
  }
  else
  {    
    if (!this.Properties[name] && loadFunction)
    {
      this.Properties[name] = loadFunction();
    }

    return this.Properties[name]; // This will return "undefined" if property doesn't exist or the loadFunction wasn't provided.
  }
};


Object.prototype.GetSetArrayProperty = function(name, index, value, loadFunction) {
  if (value)
  {
    this.Properties[name][index] = value;
  }
  else
  {
    if (!this.Properties[name][index] && loadFunction)
    {
      this.Properties[name][index] = loadFunction();
    }

    return this.Properties[name][index];
  }
};


// Here's a nifty function that declares the properties for you.
Function.prototype.CreateProperty = function(propertyName, loadFunction) {
  eval("this.prototype['" + propertyName.toString() + "'] = function(i, v) { return this.GetSetProperty('" + propertyName.toString() + "', v, " + eval(loadFunction) + ", i); };");
};




var myFunctionThatDoesLazyLoading = function() {
  return "Ahoy!";
};


var MyClass = function() { }; // MyClass constructor.
MyClass.prototype.PageSize = function(i, v) { return this.GetSetProperty("PageSize", v, myFunctionThatDoesLazyLoading, i); };

var instance = new MyClass();
alert(instance.PageSize()); // PageSize is lazy loaded.

instance.PageSize(5); // PageSize is re-assigned.
alert(instance.PageSize()); // Returns the new value.

instance.PageSize([1, 2, 3]); // PageSize is re-assigned to have an Array value.
alert(instance.PageSize(2)); // Returns the value at index 2 of the Array value.

instance.PageSize(2, "foo"); // Re-assigns the value at index 2.
alert(instance.PageSize(2)); // Returns the new value at index 2.

MyClass.CreateProperty("NewProp", function() { return ["a", "b", "c"]; }); // Demo of the CreateProperty function.
alert(instance.NewProp());
alert(instance.NewProp(1));
peanutbutter_lou
+1  A: 

Ok, unless I misunderstood your question, I believe I have found a simple solution which does not require a so-called "lazy_push" function.

Following the method from my previous answer, you create a MyArray Class:

function MyArray(){
     this.object = [];
 }

MyArray.prototype.push = function(what){
     this.object.push(what);
}

Now the important part is the getter function, we will create a getIdx() function to grab the value out of the array. The function then uses the 'typeof' operator to determine if the returned value is a function. If it is, return the value returned from the function, if not return the original value.

Code makes more sense:

MyArray.prototype.getIdx = function(which){
     if(typeof this.object[which] == 'function'){
         alert("a function");
         //NOTICE THE '()' AT THE END OF THE NEXT LINE!!!
         return this.object[which]();
     }
     return this.object[which];
 }

Hopefully if I didn't completely answer your question you can figure it out from here.

<--------- My original post ------------->

Not really an answer, but a few important points.

  1. There are no real arrays in Javascript, an Array is just an extended Object (as is everything in JS)

  2. You should ideally never add prototype functions to the native objects in JS, you might accidentally overwrite an existing property, or create confusing errors down the line. For instance, adding to the prototype of Object is going to add to every single Object in JS (which is everything), you need to make absolutely sure that you want every type in JS to have that property. This is just dangerous because if you accidentally overwrite the actual Array() or Object() functions, you will break javascript in the browser, period, a refresh of the page won't fix it.

  3. Rather than adding to the prototype of the Object you are modifying, create a new Object that extends it. For instance if you want to extend the Array class:

    //create the constructor, 
    //with an object variable that holds the object you wish to extend
    function MyArray(){
         this.object = [];
    }
    
    
    //create functions which reference the native functions of the object
    MyArray.prototype.push = function(what){
         this.object.push(what);
    }
    
    
    //Etc... Etc....  Etc.....
    

Its not necessarily fun writing all the accessor methods for native Object functions, but it keeps the Javascript engine safe.

G. Shearer
A: 

I don't particularly like this answer, but could you store your "variable" as an expression string, and then eval() it when you need it? Not ideal, but it's compact...

var x = 10, arr = [];
arr.push("x + 10");
alert(eval(arr[0]));
x = 20;
alert(eval(arr[0]));

I've tested it, and it works, even if it's not exactly what you're looking for.

Ryan Kinal