Week 4: Objects, Classes, this
Objects
An object is a collection of related data and/or functionality (which usually consists of several variables and functions — which are called properties and methods when they are inside objects.) The data we store in an object is not ordered — we can only access it by calling its associated key. You can create an object with key-value pairs using the following syntax:
var person = {
name: ['Bob', 'Smith'],
age: 32,
gender: 'male',
interests: ['music', 'skiing'],
bio: function() {
return `${this.name[0]} ${this.name[1]} is ${this.age} years old. He likes ${this.interests[0]} and ${this.interests[1]}.`;
},
greeting: function() {
return `Hi! I'm ${this.name[0]}.`;
}
};
You can access object properties using dot notation. Below are some examples of how we can access properties on our person
object:
person.name[0] // 'Bob'
person.age // 32
person.interests[1] // 'skiing'
person.bio() // 'Bob Smith is 32 years years old. He likes music and skiing.'
person.greeting() // 'Hi! I'm Bob.'
You can also access object properties using bracket notation. Here’s how we would access the same properties as above, this time using bracket notation:
person['name'][0] // 'Bob'
person['age'] // 32
person['interests'][1] // 'skiing'
person['bio']() // 'Bob Smith is 32 years years old. He likes music and skiing.'
person['greeting']() // 'Hi! I'm Bob.'
Dot notation has the advantage of concision. On the other hand, bracket notation is useful when you want to construct a property name dynamically:
var introduction = function(inFirstPerson) {
var prop = inFirstPerson ? 'greeting' : 'bio';
return person[prop]();
}
console.log(introduction(true)); // 'Hi! I'm Bob.'
You can update object property values as well as add new ones:
person.age = 29;
person['eyes'] = 'hazel';
person.farewell = function() { return 'Bye everybody!'; }
Accessing these properties then gives us:
person.age // 29
person.eyes // 'hazel'
person.farewell() // 'Bye everybody!'
JavaScript objects are like map data structures in Java or C++, with the restriction that the keys are strings. When accessing object properties, the property name is automatically converted to a string. As JavaScript arrays are objects, this applies to arrays as well. Thus, we observe the following behavior:
var a = [37, 47];
a['1'] = 53;
console.log(a[1]); // 53
Classes
A recent specification of JavaScript introduced a class
syntax that looks and feels like classes in Java or C++. I only say “looks and feels” because JavaScript obeys a different object model from those languages. In fact, the JavaScript class
syntax is merely syntactic sugar over its true object model. For our purposes in this course, however, we may assume that JavaScript classes behave in the same way as the classes with which we’re already familiar.
Classes act as object templates. Let’s look at a concrete example.
class Animal {
constructor(name) {
this.name = name;
this.belly = [];
}
eat(food) {
this.belly.push(food);
}
getName() {
return this.name;
}
}
// Dog is a subclass of Animal
class Dog extends Animal {
constructor(name, breed) {
// call the parent class constructor
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
JavaScript class
definitions look similar to how we define objects but with two main differences. First, every class definition requires a constructor
method, which is called every time a new instance of the class is created. Second, there are no commas between methods in a class definition.
Once we’ve defined a class, we can create an object instance of it using the new
operator.
var a = new Animal('Abu');
var d = new Dog('Buddy', 'Golden Retriever');
These objects have access to the properties and methods defined in their associated class definitions.
a.eat('banana');
console.log(a.name); // 'Abu'
console.log(a.belly); // ['banana']
d.eat('basketball');
console.log(d.name); // 'Buddy'
console.log(d.belly); // ['basketball']
d.bark(); // 'Woof!'
this
Many JavaScript programmers get confused by what gets bound to the this
keyword. Part of this is because this
behaves differently in JavaScript than it does in other languages like Java or C++.
Let’s consider how this
works in Java.
class Vehicle {
int maxSpeed;
public Vehicle(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
public int getMaxSpeed() {
return this.maxSpeed;
}
}
class Car extends Vehicle {
String make;
public Car(int maxSpeed, String make) {
super(maxSpeed);
this.make = make;
}
public String getMake() {
return this.make;
}
}
In Java, we can always assume that this
will be bound to an instance of the containing class or a subclass thereof. This is because Java methods are always associated with a calling object. For example, consider how we can invoke the method getMaxSpeed
defined in the Vehicle
class. We can call v.getMaxSpeed()
, where v
is an instance of Vehicle
or Car
, but we cannot invoke getMaxSpeed()
(from outside of the class definition) without an object calling the method.
Things get more complicated in JavaScript, where functions are first-class objects. Unlike Java methods, which are always associated with some class definition, JavaScript functions can be passed around freely like numbers and booleans can. For example, returning to our Animal
/Dog
example from above, we can do the following:
var d = new Dog('Snowball');
var bark = d.bark;
bark(); // 'Woof!'
We run into trouble, however, when we try to do the same thing with a method that has this
in its definition:
var a = new Animal('Joe');
var getName = a.getName;
getName(); // Uncaught TypeError: Cannot read property 'name' of undefined
The error message here is saying that, when running the getName
function, this
is bound to undefined
rather than a
as we might expect.
To resolve this, we can explicitly bind the value of this
to the desired object:
var a = new Animal('Joe');
var getName = a.getName.bind(a);
getName(); // 'Joe'
How to determine what is bound to this
Now that we’ve seen how this
can behave differently in JavaScript than we might expect, let’s see how we can predict its behavior.
What is bound to this
in a function body is determined by the function call-site: the location in the code where the function is called (not where it’s declared). Once we have identified the function call-site, there are four rules we can use to determine what is bound to this
.
- If the function is called when instantiating a class using the
new
operator (i.e., the function is theconstructor
method of the class), thenthis
is bound to the newly instantiated object.class Foo { constructor() { this.bar = 'baz'; } } var foo = new Foo(); // <-- at this call-site, JavaScript runs the constructor function and binds `this` to the new object
- If the function is an explicitly bound function, then
this
is bound to the specified object.class Foo { constructor(bar) { this.bar = bar; } getBar() { return this.bar; } } var foo = new Foo('baz'); var explicitlyBoundFunction = foo.getBar.bind({ bar: 'not baz'}); console.log(explicitlyBoundFunction()); // 'not baz'
- If the function is called as the method of an object, then
this
is bound to that object.var foo = { bar: 'baz', getBar() { return this.bar; } } console.log(foo.getBar()); // 'baz'
- Otherwise,
this
isundefined
.
this
in the context of arrow functions
The four rules above apply to this
in a regular function definition. JavaScript also comes with another class of functions called arrow functions.
var square = (x) => x * x;
console.log(square(4)); // 16
Arrow functions have their own rule for binding this
. Whereas regular functions bind this
according to their call-sites, arrow functions bind this
according to their declarations. Specifically, they bind this
to whatever is bound to this
at the site of their declaration.
var foo = {
bar: 'baz',
getRegularFunction() {
return function() {
return this.baz;
}
},
getArrowFunction() {
return () => this.baz;
}
};
var regularFunction = foo.getRegularFunction();
var arrowFunction = foo.getArrowFunction();
console.log(regularFunction()); // undefined because none of the first three
// rules for regular functions apply at this call-site
console.log(arrowFunction()); // 'baz' because foo.getArrowFunction() binds
// `this` to `foo` in the method body of getArrowFunction
// and the returned arrow function binds `this` to
// whatever is `this` in its surrounding context
Arrow functions are often useful for fixing this
in the definition of callback functions.
var foo = {
bar: 'baz',
setUpEventHandler() {
$('button').click(() => { console.log(this.bar); })
}
}
foo.setUpEventHandler();
In the above program, the last line binds this
in the body of setUpEventHandler
to foo
. By then passing an arrow function into the click
method, we ensure that the value logged upon clicking a button is 'baz'
. If we had passed a regular function into the click
method instead, it is not clear what value would be logged because it would depend on where exactly jQuery invokes the click handler in its internal implementation.