diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..06e1b39 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "ltex.language": "en-GB" +} \ No newline at end of file diff --git a/src/js/classes-and-instances.md b/src/js/classes-and-instances.md index 895a2aa..69ae9df 100644 --- a/src/js/classes-and-instances.md +++ b/src/js/classes-and-instances.md @@ -1,5 +1,7 @@ # Classes and instances + + In programming, a **class** is like a blueprint for creating objects. These objects are known as **instances** of the class. Using a class for object creation means objects are created the same way every time, which helps us @@ -7,9 +9,7 @@ adhere to DRY (don't repeat yourself) principles. ## Defining a class -Let's imagine we're writing software to manage smart home devices. We're going -to be managing lots of smart lights, so we should make a class which will serve -as a blueprint for every smart light in the home. +We create a class using the `class` keyword. ```js class SmartLight { @@ -17,8 +17,7 @@ class SmartLight { } ``` -As you can see, we use the `class` keyword followed by the desired name of the -class. In javascript, we capitalise the first letter of class names. +In javascript, it is conventional capitalise the first letter of class names. ## Creating instances @@ -47,33 +46,13 @@ SmartLight {} ::: The output shows us that we have two instances of `SmartLight`. But they don't -have any properties yet. They are empty objects. Let's change that! +have any properties yet. They are empty objects. ## Constructor function A **constructor** is a special function inside a class that sets up new objects. It's where we initialise properties. -```js -class SmartLight { - constructor(color, brightness) { - this.color = color - this.brightness = brightness - } -} -``` - -Inside the constructor, we can't refer to the instances as `kitchenLight` or -`bedroomLight` because they're still under construction! That's what the `this` -key word is for: it refers to the current instance under production inside the -class. Think of `this` as an unfinished smart light on the production line - -we're adding things to it as we go. - -## Providing properties - -We can pass values to the constructor when we create new instances. This is what -it looks like: - ::: code-group ```js @@ -98,44 +77,12 @@ SmartLight { color: 'cool blue', brightness: 50 } ::: -### Instances are objects - -An instance of a class is just a javascript object, which means all of the -normal stuff you can do with objects still works. - -::: code-group - -```js -class SmartLight { - constructor(color, brightness) { - this.color = color - this.brightness = brightness - } -} - -const kitchenLight = new SmartLight('warm white', 75) - -// access properties with dot notation -console.log(kitchenLight.color) - -// update properties -kitchenLight.brightness = 20 -console.log(kitchenLight.brightness) -``` - -```console [output] -warm white -20 -``` - -::: +Think of `this` as an unfinished smart light on the production line - we're +adding things to it as we go. ## Additional properties -There is no reason why all properties need to be passed to the constructor as -arguments. The constructor can add extra properties. This is useful for adding -default values that aren't intended to be provided by the creator of the -instance. +The constructor can add properties that are not passed in as arguments. ::: code-group diff --git a/src/js/getters-and-setters.md b/src/js/getters-and-setters.md index c07cf41..432599b 100644 --- a/src/js/getters-and-setters.md +++ b/src/js/getters-and-setters.md @@ -12,25 +12,27 @@ Let's revisit our smart light class: ```js class SmartLight { - #color #brightness + constructor(color, brightness) { - this.#color = color + this.color = color this.#brightness = brightness - this.isOn = false } getBrightness() { - return `${this.#brightness}%` + if (this.isOn) { + return this.#brightness + } else { + return 0 + } } - setBrightness(level) { - if (level < 0 || level > 100) { - console.log('Brightness must be between 0 and 100') - } else { - this.#brightness = level - console.log(`Brightness set to ${this.#brightness}.`) + setBrightness(newBrightness) { + if (newBrightness < 0 || newBrightness > 100) { + throw new Error('Brightness must be between 0 and 100') } + + this.#brightness = newBrightness } } ``` @@ -41,27 +43,29 @@ Because the methods are concerned with getting and setting a property, they could be refactored to make use of javascript's special `get` and `set` keywords like this: -```js{10,14} +```js{9,17} class SmartLight { - #color #brightness + constructor(color, brightness) { - this.#color = color + this.color = color this.#brightness = brightness - this.isOn = false } - // [!code focus:13] + get brightness() { - return `${this.#brightness}%` + if (this.isOn) { + return this.#brightness + } else { + return 0 + } } - set brightness(level) { - if (level < 0 || level > 100) { - console.log('Brightness must be between 0 and 100') - } else { - this.#brightness = level - console.log(`Brightness set to ${this.#brightness}.`) + set brightness(newBrightness) { + if (newBrightness < 0 || newBrightness > 100) { + throw new Error('Brightness must be between 0 and 100') } + + this.#brightness = newBrightness } } ``` @@ -78,29 +82,19 @@ but crucially: This is what it looks like: -::: code-group - ```js const bathroomLight = new SmartLight('amber', 50) -// this will fail, it calls "set .brightness(104)" +// this will throw an error, it calls "set brightness(104)" bathroomLight.brightness = 104 -// but this is ok, it calls "set .brightness(50)" +// but this is ok, it calls "set brightness(50)" bathroomLight.brightness = 50 // this calls "get brightness()" -console.log(bathroomLight.brightness) +bathroomLight.brightness // 50 ``` -```console [output] -Brightness must be between 0 and 100 -Brightness set to 50. -50% -``` - -::: - It is not necessary to use this special syntax, it comes down to style and -preference. So long as your class is well documented and other developers can +preference. So long as your class is well documented, and other developers can use it, that is what's important. diff --git a/src/js/inheritance.md b/src/js/inheritance.md index bb4ba55..0c1dacc 100644 --- a/src/js/inheritance.md +++ b/src/js/inheritance.md @@ -1,94 +1,63 @@ # Inheritance -Inheritance enables you to create a new class that is based on an existing -class. The new class inherits the properties and methods of the existing class. -This existing class is often referred to as the parent class, superclass, or -base class, while the new class is known as the child class, subclass, or -derived class. - -## Understanding Inheritance - -The primary benefit of inheritance is code reuse. You can define common -functionality in a parent class and then extend this class to create child -classes that inherit this functionality, adding or overriding properties and -methods as needed. - -## Example Scenario - -Let's continue with our smart home theme. Imagine we have a general -`SmartDevice` class that defines properties and methods common to all smart -devices, like connectivity status or device name. We can then create more -specific device classes (like `SmartLight` or `SmartCamera`) that inherit from -`SmartDevice` and add their unique features. + ## Creating a parent class -First, we define our parent class, `SmartDevice`, with some basic properties and -methods. +If we want to create a set of classes that share some common functionality, we +should create a parent class. This parent class will contain the shared +functionality, and the child classes will inherit from it. ```js class SmartDevice { - constructor(name) { - this.name = name - this.isConnected = false + constructor() { + this.isOn = false } - connect() { - this.isConnected = true - console.log(`${this.name} is now connected.`) + togglePower() { + this.isOn = !this.isOn } } ``` -We don't intend to use this class to directly create objects, but we'll use it -as a starting point to build our more specific classes. - ## Inheriting from a parent class -Next, we create a child class that inherits from `SmartDevice`. We use the -`extends` keyword for inheritance in JavaScript. +To inherit from a parent class, we use the `extends` keyword, and then call the +`super()` method in the child class's constructor. -An important step is calling the special `super()` function inside the child's -`constructor()`. The `super()` function calls the parent's constructor, which is -necessary to set up shared properties like `name` and `isConnected`. +::: info -::: code-group +The `super()` method calls the parent class's constructor, passing in any +arguments that are needed for the parent class's constructor. -```js -class SmartLight extends SmartDevice { - constructor(name, color) { - super(name) // Calls parent's constructor with the name parameter - this.color = color - } +::: + +::: code-group - changeColor(newColor) { - this.color = newColor - console.log(`${this.name}'s color changed to ${this.color}.`) +```js {1,3} +class SmartCamera extends SmartDevice { + constructor(location) { + super() + this.location = location + this.batteryLife = 100 } } -const studyLight = new SmartLight('Epic Lamp', 'yellow') -console.log(studyLight) - -// use the inherited .connect() method -studyLight.connect() - -// use the child-specific .changeColor() method -studyLight.changeColor('ochre') +const poolCam = new SmartCamera('Pool House') +poolCam.togglePower() +console.log(poolCam) ``` ```console [output] -SmartLight { name: 'Epic Lamp', isConnected: false, color: 'yellow' } -Epic Lamp is now connected. -Epic Lamp's color changed to ochre. +SmartCamera { + isOn: true, + batteryLife: 100, + location: 'Pool House' +} ``` ::: -Because we have used `extends` and `super()`, we now have access to all -`SmartDevice` functionality on our `SmartLight`. Neat! - -The next step would be to create a `SmartCamera` class which also inherits from -`SmartDevice`. Each child class doesn't need to implement things like `name` and -`connect()`, meaning we do not repeat and have to maintain all the repeated -code. +Notice that the `SmartCamera` class has access to the `togglePower()` method and +the `isOn` property, even though we didn't define them in the `SmartCamera` +class. This is because `SmartCamera` inherits from `SmartDevice`. diff --git a/src/js/instance-methods.md b/src/js/instance-methods.md index 8dc9f23..9f0f990 100644 --- a/src/js/instance-methods.md +++ b/src/js/instance-methods.md @@ -1,52 +1,40 @@ # Instance methods + + Instance methods are functions defined inside a class that operate on instances of the class. They can access and modify the instance's properties, take -parameters, return values, and even call other methods. Let's continue with our -smart home theme and enhance our `SmartLight` class. +parameters, return values, and even call other methods ## Defining a method Let's start by adding a simple method `.togglePower()` to toggle our smart light on and off. -::: code-group - -```js +```js{8-10} class SmartLight { constructor(color, brightness) { this.color = color this.brightness = brightness this.isOn = false } - // [!code focus:5] + togglePower() { this.isOn = !this.isOn - console.log(`Light is now ${this.isOn ? 'on' : 'off'}.`) } } -// [!code focus:6] -const kitchenLight = new SmartLight('warm white', 75) -// we access methods using dot notation: -kitchenLight.togglePower() -kitchenLight.togglePower() -``` +const kitchenLight = new SmartLight('warm white', 75) -```console [output] -Light is now on. -Light is now off. +kitchenLight.togglePower() // on +kitchenLight.togglePower() // off ``` -::: - ## Methods with parameters Methods can take parameters to allow more dynamic operations. -::: code-group - -```js +```js{12-14} class SmartLight { constructor(color, brightness) { this.color = color @@ -56,35 +44,22 @@ class SmartLight { togglePower() { this.isOn = !this.isOn - console.log(`Light is now ${this.isOn ? 'on' : 'off'}.`) } - // [!code focus:5] + changeColor(newColor) { this.color = newColor - console.log(`Light color changed to ${this.color}.`) } } -// [!code focus:3] const kitchenLight = new SmartLight('warm white', 75) kitchenLight.changeColor('lava red') ``` -```console [output] -Light color changed to lava red. -``` - -::: - ## Returning values -Methods can return values as well, which allows instances to do useful -calculations. For example, we could add a `.currentBrightness()` which returns -`0` if the light is off, otherwise it returns the brightness. - -::: code-group +Methods can return values as well. -```js +```js{16-22} class SmartLight { constructor(color, brightness) { this.color = color @@ -94,14 +69,12 @@ class SmartLight { togglePower() { this.isOn = !this.isOn - console.log(`Light is now ${this.isOn ? 'on' : 'off'}.`) } changeColor(newColor) { this.color = newColor - console.log(`Light color changed to ${this.color}.`) } - // [!code focus:8] + currentBrightness() { if (this.isOn) { return this.brightness @@ -110,25 +83,14 @@ class SmartLight { } } } -// [!code focus:9] -const kitchenLight = new SmartLight('warm white', 75) -// check the brightness when off -console.log(kitchenLight.currentBrightness()) +const kitchenLight = new SmartLight('warm white', 75) +console.log(kitchenLight.currentBrightness()) // 0 -// now turn it on and call again kitchen.togglePower() -console.log(kitchenLight.currentBrightness()) +console.log(kitchenLight.currentBrightness()) // 75 ``` -```console [output] -0 -Light is now on. -75 -``` - -::: - ## Calling other methods The last thing we need to know about methods is that they can call each other @@ -136,7 +98,7 @@ using the `this` key word. ::: code-group -```js +```js{26-33} class SmartLight { constructor(color, brightness) { this.color = color @@ -161,9 +123,8 @@ class SmartLight { return 0 } } - // [!code focus:10] + factoryReset() { - console.log('Reverting to factory settings...') this.changeColor('white') this.brightness = 100 @@ -172,7 +133,7 @@ class SmartLight { } } } -// [!code focus:8] + // make a light and turn it on const bedroomLight = new SmartLight('cool blue', 50) bedroomLight.togglePower() @@ -183,11 +144,6 @@ console.log(bedroomLight) ``` ```console [output] -Light is now on. - -Reverting to factory settings... -Light color changed to white. -Light is now off. SmartLight { color: 'white', brightness: 100, isOn: false } ``` diff --git a/src/js/private-properties.md b/src/js/private-properties.md index afe9277..e2fe9d1 100644 --- a/src/js/private-properties.md +++ b/src/js/private-properties.md @@ -1,40 +1,23 @@ # Private properties -In object-oriented programming, encapsulation is a core principle that involves -bundling the data (properties) and methods that operate on the data into a -single unit, or class, and restricting access to some of the object's -components. Private properties are a way to enforce encapsulation in JavaScript. -They are properties accessible only within the class that declares them, making -them invisible to any code outside that class. + -## Understanding Private Properties +## Defining private properties Private properties in JavaScript are defined by prefixing the property name with a hash (`#`). This syntax ensures that the property cannot be accessed or modified directly from outside the class. -### Why Use Private Properties? - -- **Encapsulation**: They help in keeping certain details of an object hidden - from the outside world. -- **Control**: They provide control over how a property is accessed or modified - through public methods. -- **Security**: They make it harder to access data. - -## Defining private properties - -Let's refactor our `SmartLight` class to benefit from private properties. - ::: code-group -```js +```js{2,6} class SmartLight { - #color - #brightness + #brightness // declare private property + constructor(color, brightness) { - this.#color = color + this.color = color this.#brightness = brightness - this.isOn = false // Public property for simplicity + this.isOn = false } } @@ -48,30 +31,22 @@ SmartLight { isOn: false } ::: -Notice that the output only displays the `isOn` property - `color` and -`brightness` are no longer accessible. +Notice that the `brightness` property is not logged to the console. + +## Private properties cannot be accessed directly If we try and modify these properties directly, we will get an error: ::: code-group ```js -bathroomLight.#color = 'ice blue' +bathroomLight.#brightness = 90 ``` ```console [output] -bathroomLight.#color = 'ice blue' - ^ - +bathroomLight.#brightness = 90 + ^ SyntaxError: Private field '#color' must be declared in an enclosing class - at internalCompileFunction (node:internal/vm:73:18) - at wrapSafe (node:internal/modules/cjs/loader:1153:20) - at Module._compile (node:internal/modules/cjs/loader:1197:27) - at Module._extensions..js (node:internal/modules/cjs/loader:1287:10) - at Module.load (node:internal/modules/cjs/loader:1091:32) - at Module._load (node:internal/modules/cjs/loader:938:12) - at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:83:12) - at node:internal/main/run_main_module:23:47 ``` ::: @@ -81,95 +56,29 @@ SyntaxError: Private field '#color' must be declared in an enclosing class Since private properties cannot be accessed directly from outside the class, we need to provide public _methods_ to work with them. -::: code-group - -```js +```js{9-15,17-23} class SmartLight { - #color #brightness - constructor(color, brightness) { - this.#color = color - this.#brightness = brightness - this.isOn = false // Public property for simplicity - } - // [!code focus:8] - getColor() { - return this.#color - } - - setColor(newColor) { - this.#color = newColor - console.log(`Color set to ${this.#color}.`) - } -} -// [!code focus:4] -const bathroomLight = new SmartLight('amber', 50) -console.log(bathroomLight.getColor()) -bathroomLight.setColor('neon green') -``` -```console [output] -amber -Color set to neon green. -``` - -::: - -## What is the point? - -By now, you are probably wondering why bother with all this? It's a fair -question - we can read and write properties with our public methods just the -same as if we accessed them directly. - -In the next example, though, we will see that private properties with public -methods allow us to control **how** people interact with the object. - -::: code-group - -```js -class SmartLight { - #color - #brightness constructor(color, brightness) { - this.#color = color + this.color = color this.#brightness = brightness - this.isOn = false // Public property for simplicity } - // [!code focus:13] + getBrightness() { - return `${this.#brightness}%` + if (this.isOn) { + return this.#brightness + } else { + return 0 + } } - setBrightness(level) { - if (level < 0 || level > 100) { - console.log('Brightness must be between 0 and 100.') - } else { - this.#brightness = level - console.log(`Brightness set to ${this.#brightness}.`) + setBrightness(newBrightness) { + if (newBrightness < 0 || newBrightness > 100) { + throw new Error('Brightness must be between 0 and 100') } + + this.#brightness = newBrightness } } -// [!code focus:5] -const bathroomLight = new SmartLight('amber', 50) -bathroomLight.setBrightness(104) -bathroomLight.setBrightness(50) -console.log(bathroomLight.getBrightness()) -``` - -```console [output] -Brightness must be between 0 and 100. -Brightness set to 50. -50% ``` - -::: - -The `.getBrightness()` method formats the return value with a `%` symbol. -Although the brightness is stored privately as an integer, which is more -convenient for internal class operations like adjusting the brightness, it is -displayed publicly as a formatted string, which is better for presentation. - -The `.setBrightness()` method prevents the user from setting the brightness -level to a value outside `0` to `100`. We allow the brightness to be modified, -but we've taken control over _how_ it can be modified as the user cannot -directly overwrite the property with an invalid number. diff --git a/src/js/static-properties.md b/src/js/static-properties.md index 329e926..151c422 100644 --- a/src/js/static-properties.md +++ b/src/js/static-properties.md @@ -1,62 +1,31 @@ # Static properties -Static properties and methods in JavaScript provide a way for you to add -properties and functions to classes themselves, rather than to instances of -those classes. This feature is incredibly useful for utility functions, -constants, or any data and methods that should be shared across all instances of -a class. - -## Understanding static properties - -Static properties are part of the class definition itself. They are accessed -using the class name, _not_ an instance of the class. This makes them ideal for -utility functions or data that is common to all instances of a class. - -Why Use Static Properties and Methods? - -- **Utility methods**: Perfect for utility or helper functions that don't - require access to instance data. -- **Constants**: Useful for defining constants related to the class. -- **Singleton patterns**: Can assist in implementing singleton patterns by - ensuring only one instance of the class can be created. + ## Identifying static properties Let's say we're adding some smart cameras to our smart home system. -::: code-group - ```js +import SmartDevice from './SmartDevice.js' + class SmartCamera { constructor(location) { this.location = location - this.batteryLife = 100 // charged by default + this.batteryLife = 100 } } const gardenCam = new SmartCamera('Garden') -console.log(gardenCam) ``` -```console [output] -SmartCamera { - location: 'Garden', - batteryLife: 100 -} -``` - -::: - -Now let us suppose we need to manage assigning ip addresses to all the cameras. -Should we put this functionality on camera instances? _No_. One camera doesn't -have access to the properties of another camera, so it wouldn't know which -addresses are taken and which aren't. Because this is "meta" information about -the whole class of cameras, it belongs as a static property. +If we want to add a property to our camera class which is shared by all cameras, +we should use a static property. ## Defining static properties Let's add a static property to our camera class which holds information about -the ip range cameras are allowed to use. +the IP range cameras are allowed to use. ::: code-group