Methods and this
18 Aug 2015Methods and this
Here is a simple instance object representing a rectangle:
var rect = {
width:2,
height:1
}
Here is an ordinary function which can compute a rectangle's area. We might call it "freelance" since it belongs to no particular rectangle:
function area(rect) { //needs target object as argument
return rect.width * rect.height;
}
var answer = area(rect);
We can attach that function as a method of the target rectangle:
var rect1 = {
width:1,
height:2,
area: function() { //no target arg needed
return rect1.width * rect1.height;
}
};
answer = rect1.area();
But a better alternative uses the keyword this
:
var rect2 = {
width:2,
height:3,
area: function() {
return this.width * this.height;
}
};
answer = rect2.area();
Why is using 'this' better?
Exercise
Make an object for an animal of your choice, and give it a method to talk by displaying a noise
property. Use this
to refer to that noise from within the talk
method.
Lexical vs. Dynamic Scoping
All variables we've ever seen are 'lexically' scoped-- the declaration they refer to (the closure for that variable) can be determined in advance by looking at the structure of the code (how function definitions are nested).
For example: what is the scope of rect1
in
var rect1 = {
width:1,
height:2,
area: function() { //no target arg needed
return rect1.width * rect1.height;
}
};
The word this
behaves somewhat like a variable, except:
You can never set it (e.g.
this=that
)It always refers to an object, never a primitive
It is 'dynamically' scoped; its referent cannot be determined by looking at the code surrounding it. Its value is not determined by closure, like a local variable, but instead depends on how its function is called, like a parameter. It is essentially an invisible parameter.
For example:
var rect2 = {
width:2,
height:3,
area: function() {
return this.width * this.height;
}
};
answer = rect2.area();
this
refers to rect2, but not because the function is 'embedded' in the description of rect2. Instead, it's only because the function is called as a method of rect2.
Sharing methods:
var square1 = {
width:1,
height:1,
area: rect2.area // share rect2's method
}
answer = square1.area();
Single-use borrowing with call
This object has no method of its own:
var square2 = {
width:2,
height:2
}
We could "borrow" another object's method by momentarily linking to it:
square2.area = rect2.area;
answer = square2.area();
delete square2.area;
But an easier way is to use the 'call' method of the borrowed method:
answer = rect2.area.call(square2);
Sharing predefined methods
Freelance method:
function area() {
return this.width * this.height;
}
Permanently shared:
var rhombus1 = {width:1, height:1, area:area};
var rhombus2 = {width:2, height:2, area:area};
answer = rhombus1.area();
answer = rhombus2.area();
Borrowed on demand:
var rhombus3 = {width:3, height:2};
var rhombus4 = {width:4, height:2};
answer = area.call(rhombus3);
answer = area.call(rhombus4);
Exercise
Write a method talk
which can be used by any animal object. When called via that animal, it should display a noise
property of that animal.
Use your method to make several different animals talk, each with a unique noise.
Instance Objects: Arrays!
Testing Arrays
Write some code to verify that Arrays behave as advertised. Specifically, write three different functions, each testing one method of Arrays:
testPush(array)
should verify thatarray.push(val)
adds val to the end of array and returns its new length;testPop(array)
should verify thatarray.pop()
removes and returns the last element of array;testJoin(array)
should verify thatarray.join(delim)
concatenates all elements of array into a single string, with string delim inserted between each element.
Each function should do several tests: adding, removing, or joining values under various conditions to ensure that array produces the correct outcome. Each outcome may require multiple assertions to verify. For each function, make sure one test is for how an empty array behaves. Any assertion which fails should log a message to the console, but your test functions don't need return values.
More detailed instructions are in the template file.
Simulating Arrays
Now that you have a testing suite, implement your own version of Arrays!
Create a pseudo-array, an ordinary object which is not an actual Array but behaves
(somewhat) like one. You may use a global variable array to store
your pseudo-array.
It will have a property length, which is initially zero but needs to be adjusted as elements are added or removed.
The elements of array will be stored as properties named by their index numbers.
So for example, an array representing [5,9]
would have three properties named "length", "0", and "1" whose values are 2, 5, and 9.
For this exercise, you don't need to delete any array elements beyond its length if the length shrinks; just ignore them. Setting array.length to 0 is enough to reset it to "empty".
In addition to property length and the element properties, give array three more properties pop, push, and join which are functions (i.e. methods) behaving exactly like (and returning the same values as) the corresponding methods of real Arrays. When your pop and push methods modify the array, length should change accordingly.
You may use the enclosed template file to get started.
Hint: Within each method, use the keyword this
to refer to your array object.
Testing the Simulation
Test your pseudo-array implementation using your tests from above. Your pseudo-array should be able to pass the same tests of push, pop, and join as a real Array.
Toolkit Objects
Toolkit Object Example
Here's an example of a dictionary object, whose keys are not known in advance:
var unitsPerDollar = {
Dollar: 1,
Euro: 0.90,
Pound: 0.64,
Peso: 16.42,
Yen: 124.41,
Yuan: 6.40
}
In contrast, here's a simple example of a Toolkit object, a currency converter. Its keys are fixed and can be mentioned in the code:
var exchange = {
rate: 1.10, //dollars per euro
toDollars: function(euros) {
return euros * this.rate;
},
toEuros: function(dollars) {
return dollars / this.rate;
},
convert: function(string) {
if (string[0]==='$')
return 'E'+this.toEuros(string.slice(1));
if (string[0]==='E')
return '$'+this.toDollars(string.slice(1));
return this.toDollars(string);
}
};
exchange.convert('$20.00');
Toolkit Exercise
Modify the exchange toolkit to have one data property, a dictionary object listing multiple exchange rates, and two methods:
convertTo(amount,toUnit)
: convertamount
of dollars into the equivalent intoCurrency
;convertFrom(amount,fromUnit)
: convert 'amount' of foreign currency infromUnit
s to the equivalent in dollars.
It might be used as follows:
exchange.convertTo(20,"Yen");
exchange.convertFrom(5,"Euro");
Playing Cards, Episode 2: Toolkit!
Revisit your playing card functions from last Wednesday. Repackage them in a Toolkit pattern, as methods of a single master object. You may hold that object in a global variable named anything you like (it's cardTools in the template below), but its name should not appear in the definitions of your methods; instead, refer to that object as this
. You'll need to change the form of your method definitions and the way they call other methods, but their logic and most of their code will remain the same as last week.
You may adopt the enclosed template file. Make sure your code still passes all the assertions there!