Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Concept Exercise: Windowing System (Classes) #1476

Merged
merged 27 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions concepts/classes/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"blurb": "JavaScript allows for object-oriented programming. Despite it having a \"class\" keyword, it is still a prototype-based language.",
"authors": ["junedev"],
"contributors": []
}
285 changes: 285 additions & 0 deletions concepts/classes/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
# About

JavaScript includes the capabilities for object-oriented programming ([OOP][wiki-oop]).
In OOP, you want to create objects (_instances_) from "templates" (_classes_) so that they include certain data and functionality.
The data properties are called _fields_ in the OOP context, the function properties are called _methods_.

JavaScript did not have classes at all before they were added to the language specification in 2015, but allowed for object-oriented programming using prototype-based inheritance.
And even though a `class` keyword is available nowadays, JavaScript is still a _prototype-based_ language.
junedev marked this conversation as resolved.
Show resolved Hide resolved

To understand what it means to be a prototype-based language and how JavaScript actually works, we will go back to the time when there were no classes.

## Prototype Syntax

### Constructor Function

In JavaScript, the template (class) is facilitated by a regular function.
When a function is supposed to be used as such a template, it is called a _constructor function_ and the convention is that the function name should start with a capital letter.
Instances (objects) are derived from the template using the `new` keyword when invoking the constructor function.

```javascript
function Car() {
// ...
}

const myCar = new Car();
const yourCar = new Car();
```

It is important to note that in JavaScript, the instances and the constructor function keep a relationship to each other even after the instances were created.
Every instance object includes a hidden, internal property referred to as `[[prototype]]` in the language specification.
It holds a reference to the value of the `prototype` key of the constructor function.
Yes, you read that correctly, a JavaScript function can have key/value pairs because it is also an object behind the scenes.

Since 2015, `[[prototype]]` can be accessed via `Object.getPrototypeOf()`.
Before that, it was accessible via the key `__proto__` in many environments.

Do not confuse the prototype of an object (`[[prototype]]`) with the `prototype` property of the constructor function.
junedev marked this conversation as resolved.
Show resolved Hide resolved

```exercism/note
To summarize:

- Constructors in JavaScript are regular functions.
- Constructing a new instance creates an object with a relation to its constructor called its _prototype_.
- Functions are objects (callable objects) and therefore they can have properties.
- The constructor's (function) `prototype` property will become the instance's _prototype_.
```

### Instance Fields

Often, you want all the derived objects (instances) to include some fields and pass some initial values for those when the object is constructed.
This can be facilitated via the [`this` keyword][mdn-this].
Inside the constructor function, `this` represents the new object that will be created via `new`.
`this` is automatically returned from the constructor function when it is called with `new`.

That means we can add fields to the new instance by adding them to `this` in the constructor function.

```javascript
function Car(color, weight) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it is helpful to show how this is constructed under the hood as an object:

function Car(color, weight) {
    let thisObj = Object.create(null);
    thisObj.color = color;
    thisObj.weight = weight;
    return thisObj;
}

this removes the need to explicitly set the object and return it. Might be overkill or not needed :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the student might not know about Object.create at this point, I felt this code is more confusing then helping. Imo the important part is that you don't have to explicitly return this and that is covered in the text above.

this is automatically returned from the constructor function ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great @junedev. This concept looks awesome and enjoyed reading the code when reviewing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(fun fact, returning a new object from the constructor function is in fact a way to get closures/private variables and private functions from a constructor, however, you need to set the prototype at Object.create if you want to keep inheritance to work)

this.color = color;
this.weight = weight;
this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
```

### Instance Methods
junedev marked this conversation as resolved.
Show resolved Hide resolved

Methods are added via the `prototype` property of the constructor function.
Inside a method, you can access the fields of the instance via `this`.
This works because of the following general rule.

> When a function is called as a method of an object, its `this` is set to the object the method is called on. [^1]

```javascript
function Car() {
this.engineRunning = false;
// ...
}

Car.prototype.startEngine = function () {
this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
// ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
```

### The Prototype Chain

`myCar` in the example above is a regular JavaScript object and if we would inspect it (e.g. in the browser console), we would not find a property `startEngine` with a function as value directly inside the `myCar` object.
So how does the code above even work then?

The secret here is called the _prototype chain_.
When you try to access any property (field or method) of an object, JavaScript first checks whether the respective key can be found directly in the object itself.
If not, it continues to look for the key in the object referenced by the `[[prototype]]` property of the original object.
As mentioned before, in our example `[[prototype]]` points to the `prototype` property of the constructor function.
That is where JavaScript would find the `startEngine` function because we added it there.
junedev marked this conversation as resolved.
Show resolved Hide resolved

```javascript
function Car() {
// ...
}

Car.prototype.startEngine = function () {
// ...
};
```

And the chain does not end there.
The `[[prototype]]` property of `Car.prototype` (`myCar.[[prototype]].[[prototype]]`) references `Object.prototype` (the `prototype` property of the `Object` constructor function).
It contains general methods that are available for all JavaScript objects, e.g. `toString()`.
The `[[prototype]]` of `Object` is usually `null` so the prototype chain ends there.
In conclusion, you can call `myCar.toString()` and that method will exist because JavaScript searches for that method throughout the whole prototype chain.
junedev marked this conversation as resolved.
Show resolved Hide resolved
You can find a detailed example in the [MDN article "Inheritance and the prototype chain"][mdn-prototype-chain-example].

```exercism/caution
Note that the prototype chain is only travelled when retrieving a value.
junedev marked this conversation as resolved.
Show resolved Hide resolved
Setting a property directly or deleting a property of an instance object only targets that specific instance.
This might not be what you would expect when you are used to a language with class-based inheritance.
```

### Dynamic Methods (Adding Methods to All Existing Instances)

JavaScript allows you to add methods to all existing instances even after they were created.

We learned that every instance keeps a reference to the `prototype` property of the constructor function.
That means if you add an entry to that `prototype` object, that new entry (e.g. a new method) is immediately available to all instances created from that constructor function.

```javascript
function Car() {
this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.
```

In theory, dynamic methods can even be used to extend the functionality of built-in objects like `Object` or `Array` by modifying their prototype.
This is called _monkey patching_.
Because this change affects the whole application, it should be avoided to prevent unintended side effects.
The only reasonable use case is to provide a [polyfill][wiki-polyfill] for a missing method in older environments.

## Class Syntax

Nowadays, JavaScript supports defining classes with a `class` keyword.
This was added to the language specification in 2015.
On the one hand, this provides syntactic sugar that makes classes easier to read and write.
The new syntax is more similar to how classes are written in languages like C++ or Java.
Developers switching over from those languages have an easier time to adapt.
On the other hand, class syntax paves the way for new language features that are not available in the prototype syntax.

### Class Declarations

With the new syntax, classes are defined with the `class` keyword, followed by the name of the class and the class body in curly brackets.
The body contains the definition of the constructor function, i.e. a special method with the name `constructor`.
This function works just like the constructor function in the prototype syntax.
The class body also contains all method definitions.
The syntax for the methods is similar to the shorthand notation we have seen for adding functions as values inside an object, see [Concept Objects][concept-objects].

```javascript
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}

startEngine() {
this.engineRunning = true;
}

addGas(litre) {
// ...
}
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
```

Similar to function declarations and function expressions, JavaScript also supports [class expressions][mdn-class-expression] in addition to the _class declaration_ shown above.

Keep in mind that behind the scenes, JavaScript is still a prototype-based language.
All the mechanisms we learned about in the "Prototype Syntax" section above still apply.

### Private Fields, Getters and Setters

By default, all instance fields are public in JavaScript.
They can be directly accessed and assigned to.

Adding actual private fields to the language specification is work in progress, see the [proposal document][proposal-private-fields] for details.

In the meantime, you can make use of the established convention that fields and methods that start with an underscore should be treated as private.
They should never be accessed directly from outside the class.

Private fields are sometimes accompanied by [getters][mdn-get] and [setters][mdn-set].
With the keywords `get` and `set` you can define functions that are executed when a property with the same name as the function is accessed or assigned to.

```javascript
class Car {
constructor() {
this._milage = 0;
}

get milage() {
return this._milage;
}

set milage(value) {
throw new Error(`Milage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}

const myCar = new Car();
myCar.milage;
// => 0
myCar.milage = 100;
// => Error: Milage cannot be manipulated, 100 is ignored.
```

### Class Fields and Class Methods

In OOP, you sometimes want to provide utility fields or methods that do not depend on the specific instance.
Instead, they are defined for the class itself.
This can be achieved with the `static` keyword.

```javascript
class Car {
static type = 'vehicle';

static isType(targetType) {
return targetType === 'vehicle';
}
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false
```

### Class-Based Inheritance

Besides the type of [inheritance][wiki-inheritance] along the prototype chain we saw earlier, you can also represent inheritance between classes in JavaScript.
This is covered in the [Concept Inheritance][concept-inheritance].

---

[^1]: `this` Examples - As an object method, MDN. <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method> (referenced December 03, 2021)

[wiki-oop]: https://en.wikipedia.org/wiki/Object-oriented_programming
[mdn-prototype-chain-example]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain#inheritance_with_the_prototype_chain
[concept-inheritance]: /tracks/javascript/concepts/inheritance
[mdn-class-expression]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Class_expressions
[wiki-inheritance]: https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)
[proposal-private-fields]: https://github.com/tc39/proposal-private-methods#private-methods-and-fields
[mdn-get]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get
[mdn-set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
[wiki-polyfill]: https://en.wikipedia.org/wiki/Polyfill_(programming)
[mdn-this]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
[concept-objects]: /tracks/javascript/concepts/objects
Loading