JavaScript, unlike most object-oriented programming languages, lets you work with objects without first having to define classes. You have already seen how to produce objects:
let harry = { name: 'Harry Smith', salary: 90000 }
According to the classic definition, an object has identity, state, and behavior. The object that you just saw certainly has identity—it is different from any other object. And the state is provided by the properties. Let's add behavior in the form of a “method”—that is, a function-valued property:
harry = { name: 'Harry Smith', salary: 90000, raiseSalary: function(percent) { this.salary *= 1 + percent / 100 } }
Now we can raise the employee's salary with the familiar dot notation:
harry.raiseSalary(10)
Note that raiseSalary
is a function defined in the harry
object. That function looks like an ordinary function, except for one twist: in the body, we refer to this.salary
. When the function is called, this
refers to the object to the left of the dot operator.
The this
reference only works in functions declared with function
, not with arrow functions. See XREF for more details.
There is a shortcut syntax for defining function-valued properties in object literals. Simply omit the colon and the function
keyword:
harry = { name: 'Harry Smith', salary: 90000, raiseSalary(percent) { this.salary *= 1 + percent / 100 } }
This looks similar to a method definition in Java or C++, but it is just “syntactic sugar” for a function-valued property.
Suppose you have many employee objects similar to the one of the preceding section. Then you need to make a raiseSalary
property for each of them. You can write a factory function to automate that task:
function createEmployee(name, salary) { return { name, salary, raiseSalary: function(percent) { this.salary *= 1 + percent / 100 } } }
Still, each employee object has its own raiseSalary
property, even though the property value is the same function for all employees. It would be better if all employees could share one function.
That is where prototypes come in. A prototype collects properties that are common to multiple objects. We can set up a prototype for employee methods:
const employeePrototype = { raiseSalary: function(percent) { this.salary *= 1 + percent / 100 } }
When creating an employee object, we set its prototype. The easiest way to do this is with the Object.create
method. It creates a new object with a given prototype, to which we can then add properties:
function createEmployee(name, salary) { const result = Object.create(employeePrototype) result.name = name result.salary = salary return result }
In the preceding section, you saw how to write a factory function that creates new object instances with a shared prototype. There is special form for writing and invoking such factories, using the new
operator. When a factory function is invoked with new
, then:
.prototype
this
is bound to the newly created objectnew
operator yields the created objectBy convention, such factory functions are named after what would be the class in a class-based language. In our example, we call the factory function Employee
, as follows:
function Employee(name, salary) { this.name = name this.salary = salary }
Recall that any function is an object, so it can have properties. Each function has a prototype
property, used when the function is called with new
. You add methods to it like this:
Employee.prototype.raiseSalary = function(percent) { this.salary *= 1 + percent / 100 }
Now a client can call:
const harry = new Employee('Harry Smith', 100000)
The Employee
function creates a new object. The this
parameter points to that newly created object. The body of the Employee
function serves as a constructor, setting the object properties, by using the this
parameter. Finally, the prototype is automatically set to Employee.prototype
, courtesy of the new
operator.
The upshot of all this magic is that the new
operator looks just like a constructor call in Java or C++. However, Employee
isn't a class. It's just a function.
Then again, what is a class? A class is a set of objects with the same behavior. All objects that are obtained by calling new Employee(...)
have the same behavior. In JavaScript, constructor functions are the equivalent of classes in class-based programming languages.
Nowadays, JavaScript has a class syntax that bundles up a constructor function and prototype methods in a familiar form. Here is the class syntax for the example of the preceding section:
class Employee { constructor(name, salary) { this.name = name this.salary = salary } raiseSalary(percent) { this.salary *= 1 + percent / 100 } }
This syntax does exactly the same as that of the preceding section. There still is no actual class. The constructor
keyword defines the body of the Employee
constructor function, so that you can construct an object by calling:
const harry = new Employee('Harry Smith', 100000)
The raiseSalary
method is added to Employee.prototype
.
By all means, use the class
syntax. Just realize that it is nothing like a class declaration in a class-based language. You get a constructor function whose prototype has methods.
A class can have at most one constructor
. After all, class
is just syntactic sugar for defining a constructor function.
If you declare a class without a constructor
, it automatically gets a constructor with an empty body.
class
declaration, you do not use commas to separate the method definitions.You can dynamically set an object property in the constructor or any method by assigning to this.
propertyName. These properties work the same way as instance fields in a class-based language.
class BankAccount { constructor() { this.balance = 0 } deposit(amount) { this.balance += amount } ... }
An alternative notation is, as of early 2019, in “proposal stage 3”, which means that it is likely to be adopted in a future version of JavaScript.
You list the names and initial values of the fields in the class declaration, like this:
class BankAccount { balance = 0 deposit(amount) { this.balance += amount } ... }
A field is private (that is, inaccessible outside the methods of the class) when its name starts with #
:
class BankAccount { #balance = 0 deposit(amount) { this.#balance += amount } ... }
A separate stage 3 proposal is to make methods private if their name starts with a #
.
In a class
declaration, you can define a method as static
. Such a method does not operate on any object. It is a plain function that is a property of the class. Here is an example:
class BankAccount { ... static percentOf(amount, rate) { return amount * rate / 100 } ... addInterest(rate) { this.balance += BankAccount.percentOf(this.balance, rate) } }
To call a static method, whether inside or outside the class, add the class name, as in the example above.
Of course, you can achieve the same effect by adding the function to the constructor:
BankAccount.percentOf = (amount, rate) => amount * rate / 100
A getter is a method with no parameters that is declared with the keyword get
:
class Person { constructor(last, first) { this.last = last; this.first = first } get fullName() { return `${this.last}, ${this.first}` } }
You call the getter without parentheses, as if you accessed a property:
const harry = new Person('Smith', 'Harry') const harrysName = harry.fullName // 'Smith, Harry'
The harry
object does not have a fullName
property, but the getter method is invoked. You can think of a getter as a dynamically computed property.
You can also provide a setter, a method with one parameter:
class Person { ... set fullName(value) { const parts = value.split(/,\s*/) this.last = value[0] this.first = value[1] } }
The setter is invoked when assigning to fullName
:
harry.fullName = 'Smith, Harold'
When you provide getters and setters, users of your class have the illusion of using properties, but you control the property values and any attempts to modify them.
this
ReferenceYou have already seen how the this
reference is set in constructors and methods:
new ClassName
, a new object is created, and this
refers to the newly created object in the constructor function.object.method(arguments)
, this
refers to object in the body of the method.That sounds simple enough, but there are two points of confusion.
new
, or a method without object.
, then this
is set to undefined
(in strict mode) or the global object (in non-strict mode). It is best not to invoke constructors or methods in this way.function
, this
is undefined
(in strict mode) or the global object (in non-strict mode). A remedy is to use arrow functions instead.Let us look at both of these points in more detail.
It is possible to define constructor functions so that they also do something useful when they are called without new
. For example:
const price = Number("19.95") // Parses the string and returns a primitive number const zeroObject = new Number(0)
In this case, the constructor is actually not useful—you don't want to construct number objects but use primitive numbers instead.
You could write constructor functions like that, taking different actions depending on the value of this
. However, you risk confusing your fellow programmers.
It takes work to invoke a method without an object. The method first needs to be in a separate variable:
const action = BankAccount.prototype.deposit
action(1000) // Error—this
doesn't point to any bank account
When the function is called, it fails when accessing this.balance
.
Note that this does not work either:
const harrysAccount = new BankAccount()
const action = harrysAccount.deposit
action(1000) // Error—this
doesn't point to any bank account
The expression harrysAccount.deposit
is the exact same function as BankAccount.prototype.deposit
If you want an acrtion that deposits money in a specific account, just provide that:
const action = amount => harrysAccount.deposit(amount)
If you want an action that deposits money in an arbitrary account, you can do that too:
const action = (account, amount) =< account.deposit(amount)
You can also use the Function.bind
method to yield a function that has this
bound to a specific value. For example,
const action = BankAccount.prototoype.deposit.bind(harrysAccount)
Now we turn to the other source of this
confusion: Using this
in a callback function:
class BankAccount { ... spreadTheWealth(accounts) { accounts.forEach(function(account) { account.deposit(this.balance / accounts.length) // Error—this
not correctly bound inside nestedfunction
}) } }
For historical reasons, this
is set to undefined
or the global object inside nested functions declared with the function
keyword. The best remedy is to use an arrow function instead:
class BankAccount {
...
spreadTheWealth(accounts) {
accounts.forEach(account => {
account.deposit(this.balance / accounts.length) // Error—this
correctly bound
})
}
}
Another approach is to initialize another variable with this
:
spreadTheWealth(accounts) { const that = this accounts.forEach(function(account) { account.deposit(that.balance / accounts.length) }) }
A key concept in object-oriented programming is inheritance. A class specifies behavior for its instances. You can form a subclass of a given class (called the superclass) whose instances behave differently in some respect, while inheriting other behavior.
A standard teaching example is an inheritance hierarchy with a superclass Employee
and a subclass Manager
. While employees are expected to complete their assigned tasks in return for receiving their salary, managers get bonuses on top of their base salary if they actually achieve what they are supposed to do.
In JavaScript, as in Java, you use the extends
keyword to express this relationship among the Employee
and Manager
classes:
class Employee { constructor(name, salary) { ... } getSalary() { ... } raiseSalary(percent) { ... } } class Manager extends Employee { constructor(name, salary, bonus) { super(name, salary) this.bonus = bonus } getSalary() { return super.getSalary() + this.bonus } }
When super
is used in the constructor, it invokes the superclass constructor. In methods, super
calls a superclass method. In this example, the getSalary
method is overridden to add the bonus to the salary returned from the getSalary
method of the superclass.
Behind the scenes, a prototype chain is established. The prototype
of the Manager
constructor is set to the Employee
constructor. In that way, any method that is not defined in the subclass is looked up in the superclass.
Prior to the extends
syntax, JavaScript programmers had to establish such a prototype chain themselves—see .
You can override getter and setter methods in a subclass. Suppose the superclass has a salary
getter that yields a private field value:
class Employee { ... get salary() { return this.#salary } }
Then you can override the getter in the subclass:
class Manager extends Employee { ... get salary() { return super.salary + this.bonus } }