Now we want a more versatile Bag constructor that can accept more parameters and creates methods that return the items added.
Here's one that accepts two arguments.
function Bag(item1, item2) {
this.getitem1 = function() {
return item1;
};
this.getitem2 = function() {
return item2;
};
}
var bag = new Bag("Apple", "Banana");
And here's one that accepts three.
function Bag(item1, item2, item3) {
this.getitem1 = function() {
return item1;
};
this.getitem2 = function() {
return item2;
};
this.getitem3 = function() {
return item3;
};
}
var bag = new Bag("Apple", "Banana", "Cucumber");
Notice that the objects don't create any properties to store the parameters. The 'getitem' methods maintain a reference to the original parameters via a closure. So, the only way to access each item is by using the getter methods. In this way, the original parameters become private variables.
The constructors above accept any type of arguments. The button examples use strings but the values could be numbers, objects, arrays, etc.
It would be nicer if the Bag could accept any number of arguments without having to create different versions of the constructor. So, how can we add some flexibility?
One solution is to use the arguments list that is created whenever a function is invoked. arguments is an array-like object that holds all values passed into a function as parameters.
function Bag() {
this.getitem1 = function() {
return arguments[0];
};
this.getitem2 = function() {
return arguments[1];
};
}
var bag = new Bag("Apple", "Banana");
But that doesn't work! The buttons report 'undefined'.
The problem here is that arguments is used within inner functions where it will hold the arguments for those functions, rather than the arguments for the constructor function. As those inner functions aren't passed any arguments, arguments[0] and arguments[1] return 'undefined'.
To maintain a reference to the original arguments we need to assign them to their own variable. That variable will then be held in a closure by the getter methods.
function Bag() {
var args = arguments;
this.getitem1 = function() {
return args[0];
};
this.getitem2 = function() {
return args[1];
};
}
var bag = new Bag("Apple", "Banana");
We no longer specify how many arguments to expect. However, the getter methods are still hard coded into the constructor. We need a dynamic approach that only creates the required getter methods. Once again we can use the arguments list to help us out.
function Bag() {
var args = arguments;
for (var i = 0; i < args.length; i++) {
this["getitem" + i] = function() {
return args[i];
};
}
}
var bag = new Bag("Apple", "Banana");
That's much neater and is designed to work with any number of arguments. That fact that it doesn't work does take the shine off it a little. Both buttons return 'undefined'.
This is a classic inner function in a loop scenario. Do you know why it breaks?
Because args[i] is in an inner function which is set as a method on whatever this points to and so could be called at a later time, a closure is formed which maintains a reference to i. When the method is called, the value of i at the time of the call is used. But by then the loop has completed and the constructor finished. The value of i will be args.length which will always be one bigger than the last index in the args array.
Let's step through the loop when there are two arguments passed in.
i = 0:
this["getitem" + 0] = function() {
return args[i]; // notice: i not 0!
};
i = 1:
this["getitem" + 1] = function() {
return args[i]; // notice i not 1!
};
We need to force the function to use the actual loop value for each iteration, rather than maintaining a reference to the loop variable.
arguments and this both have special meaning which changes when within inner functions. To avoid being caught out by such changes we can assign them to variables of our choosing, which don't have special names, and use those variables in inner functions instead. We assign arguments to args and this to that.
To solve the inner function in a loop scenario outlined above, in other words to have args[i] point to the correct argument when called, we wrap the inner function in a self-executing anonymous function. This creates a local scope in which i is now the parameter of the new function. The closure now includes the parameter rather than the original loop variable.
function Bag() {
var args = arguments;
var that = this;
for (var i = 0; i < arguments.length; i++) {
(function(i) {
that["getitem" + i] = function() {
return args[i];
};
})(i);
}
}
var bag = new Bag("Apple", "Banana");
Rather than assigning arguments and this to explicit variables, we can assign them to parameters of the new inner function. This tidies up the code a little.
function Bag() {
for (var i = 0; i < arguments.length; i++) {
(function(that, i, args) {
that["getitem" + i] = function() {
return args[i];
};
})(this, i, arguments);
}
}
var bag = new Bag("Apple", "Banana");
var bag = new Bag("Apple", "Banana", "Cucumber");