PCS JS Immersion

Methods and this

Methods 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:

  1. You can never set it (e.g. this=that)

  2. It always refers to an object, never a primitive

  3. 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.

Morning scratchpad


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 that array.push(val) adds val to the end of array and returns its new length;

  • testPop(array) should verify that array.pop() removes and returns the last element of array;

  • testJoin(array) should verify that array.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): convert amount of dollars into the equivalent in toCurrency;

  • convertFrom(amount,fromUnit): convert 'amount' of foreign currency in fromUnits 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!