diff --git a/05 JavaScript.md b/05 JavaScript.md index 02081352..b6e863ad 100644 --- a/05 JavaScript.md +++ b/05 JavaScript.md @@ -17,9 +17,9 @@ Lets start with a simple example. ```javascript function largestNumber(a, b) { - if (a > b) return a - if (b > a) return b - return null + if (a > b) return a + if (b > a) return b + return null } const biggest = largestNumber(5, 8) @@ -31,45 +31,45 @@ const biggest = largestNumber(5, 8) - These are variables with local scope (they can't be accessed outside the function) - When the function is called, you need to pass two **values** which get assigned to the two parameters. - If you pass too many values the extra ones get _ignored_. - - If you don't pass enough values the remainder are assigned a value of `null`. `Null` is an assignment value (means a value of no value). + - If you don't pass enough values the remainder are assigned a value of [`undefined`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined). + >`undefined` is a [`primitive`](https://developer.mozilla.org/en-US/docs/Glossary/Primitive), which is data that is not an object and has no methods. 3. The function returns a value. - If the numbers are not the same it returns the largest. - - If they are the same it returns `null`. + - If they are the same, it returns [`null`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/null). #### 1.1.1 Test Your Understanding Start by running the `maths.js` script and map the output it generates against the `console.log` statements in the script. 1. Create a new function called `multiply()` that takes two parameters, `a` and `b` and returns the _product_ of the two. - - what happens if you call it with only a single parameter? -2. Write an _arrow function expression_ stored in a constant called `squareRoot` which calculates and returns the square root of the supplied number. You will need to use the `sqrt()` method which is part of the `Math` object. - -Open the `contact.js` script, implement the `validateEmail()` function and thoroughly test it, you should avoid using regular expressions at this stage: - -1. Check that the string is at least 5 character long -2. Check that there is a `@` character and that it is not at the start of the string (HINT: use the [indexOf](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf) String prototype method. -3. Check that there is a period (.) character after the `@` character but before the end of the string. + - what happens if you call it with only a single parameter? +2. Open the `contact.js` script, implement the `validateEmail()` function and thoroughly test it, you should avoid using regular expressions at this stage: + 1. Check that the string is at least 5 character long + 2. Check that there is a `@` character and that it is not at the start of the string (HINT: use the [`lastIndexOf`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf) String prototype method. + 3. Check that there are no spaces (`' '`) + 4. Check that there are no double dots (`'..'`) + 5. Check that there is a period (.) character after the `@` character but before the end of the string. #### 1.1.2 Function Parameters In the JavaScript language although we define a function with a set of specified _parameters_, when we call the function we don't need to pass these arguments: -We can choose to pass fewer arguments than are specified in the function parameters. Any parameters that don't have a matching argument are set to `undefined`. For example, the following code will print `undefined`. +We can choose to pass fewer arguments than are specified in the function parameters. Any parameters that don't have a matching argument are set to `undefined`. For example, in the following code num has a value of [`undefined`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined), so it can't be squared. ```javascript function sqr(num) { - return num * num + return num * num } sql() // returns NaN (not a number) ``` -This can cause issues in your code so to prevent this we provide [Default Parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters). If an arguement is missing when a function is called this specified a default value to use. For example consider this version of the function: +This can cause issues in your code so to prevent this we provide [Default Parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters). If an arguement is missing when a function is called this specifies a default value to use. For example consider this version of the function: ```javascript function sqr(num = null) { - return num * num + return num * num } -sqr() // returns 0 +sqr() // returns 0 ``` In JavaScript: @@ -81,48 +81,47 @@ It is also possible to pass in more arguements than there are parameters in the ```javascript function add(num1, num2) { - return num1 + num2 + return num1 + num2 } -add(4, 2, 1) // returns 6 +console.log(add(4, 2, 1)) // returns 6 ``` -As you can see, if there are too many arguments, the extra ones are ignored however JavaScript provides a mechanism to access all the arguments passed to a function regardless of whether they match the parameter list by using the _array-like_ `arguments` object, for example: +As you can see, if there are too many arguments, the extra ones are ignored, however, JavaScript provides a mechanism to access all the arguments passed to a function regardless of whether they match the parameter list by using the _array-like_ `arguments` object, for example: ```javascript function add() { - let total = 0 - for(arg of arguments) total += arg - return total + let total = 0 + for(const arg of arguments) total += arg + return total } -add(4, 2, 1) // returns 7 +console.log(add(4, 2, 1)) // returns 7 ``` Using _hidden_ or _magic_ variables that magically come into existence can make your code hard to understand so ECMA6 introduced [Rest Parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters), parameters that can hold any arguments that don't match the parameters in the function declaration. Take the following code: ```javascript function add(num1, num2, ...others) { - let total = num1 + num2 - for(arg of others) total += arg - return total + let total = num1 + num2 + for(const arg of others) total += arg + return total } -add(4, 2,1) // returns 7 +console.log(add(4, 2, 1)) // returns 7 ``` This demonstrates how the rest parameter _mops up_ any surplus arguments and could be written as: ```javascript function add(...numbers) { - let total = 0 - for(arg of numbers) total += arg - return total + let total = 0 + for(const arg of numbers) total += arg + return total } - -console.log(add(4, 2, 1)) // returns 7 +console.log(add(4, 2, 1)) // returns 7 ``` #### 1.1.3 Test Your Understanding -1. create a function called `divideThis()` that takes two arguments, a number to be divided, `dividend` and the number to divide by, `divisor`. The function should return the _quotient_. +1. In the `maths.js` file, create a function called `divideThis()` that takes two arguments, a number to be divided, `dividend` and the number to divide by, `divisor`. The function should return the _quotient_. 2. What happens if you don't pass a parameter for the `divisor` parameter? Can you fix this by supplying a suitable _default parameter_? 3. Call the `multiply()` function from the previous task omitting the second parameter. Can you modify the function so it uses a default parameter to multiply by 1 if the second parameter is missing. - What happens if you don't supply _any_ parameters? @@ -137,8 +136,8 @@ Functions are a data type in JavaScript (they are objects but more on that later ```javascript const remainder = function(dividend, divisor) { - const quotient = Math.floor(dividend / divisor) - return dividend - quotient + const quotient = Math.floor(dividend / divisor) + return dividend - quotient } ``` @@ -150,12 +149,12 @@ To execute the function you simply reference the variable and append `()`. const rem = remainder(8, 5) ``` -ECMA6 introduced a better way to handle function expressions, called an **arrow function expression**. This has a much shorter (and cleaner) syntax. Here is the same function expression written using this new syntax, make a careful note of the differences. +ECMA6 introduced a better way to handle function expressions, called an [**`arrow function expression`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). This has a much shorter (and cleaner) syntax. Here is the same function expression written using this new syntax, make a careful note of the differences. ```javascript const remainder2 = (dividend, divisor) => { - const quotient = Math.floor(dividend / divisor) - return dividend - quotient + const quotient = Math.floor(dividend / divisor) + return dividend - quotient } ``` @@ -174,25 +173,33 @@ const sqr = num => num * num ### 1.2.1 Test Your Understanding 1. Refactor the `remainder2` function expression to take advantage of the implicit return (you will need to reduce it to a single line of code). -2. Compare this to the original version: which is more _readable_? + - Compare this to the original version: which is more _readable_? +2. Write an _arrow function expression_ stored in a constant called `squareRoot` which calculates and returns the square root of the supplied number. You will need to use the `sqrt()` method which is part of the `Math` object. 3. Create a function expression that takes two string parameters and returns the longest string and assign this to a constant called `longest. check this works correctly. -4. Modify the function expression so that it can handle any number of string parameters (use a _rest parameter_). (hint: you will need to use a [`for...in`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) statement to loop through the strings. How does this differ from a [`for...of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) statement?) -5. Use a [ternary operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) instead of the `if` statement in the loop. -6. Finally use the [`reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) method to replace the `for...in` loop to reduce the function to a single line. + - Modify the function expression so that it can handle any number of string parameters (use a _rest parameter_). (hint: you will need to use a [`for...of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) statement to loop through the strings. How does this differ from a [`for...in`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in) statement?) + - Make sure there are no errors thrown and `null` is returned when the function is called without any arguments + - Use a [`ternary operator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator) instead of the `if` statement in the loop. + - Finally use the [`reduce()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) method to replace the `for...of` loop to reduce the function to a single line. ## 2 Callbacks -Since JavaScript supports _first class functions_, we can use a function in any place where we could use a literal, object or variable. Open the `currency.js` script and look at line 17. As you can see the `request` object has a key called `get` that stores a function (we have already covered this). This takes two parameters: +Since JavaScript supports [_first class functions_](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function), we can use a function in any place where we could use a literal, object or variable. Open the `currency.js` script and look at line 22. As you can see the `fs` object has a key called `readFile` that stores a function (we have already covered this). This takes two parameters: -1. A string representing the url to be accessed. -2. A function that will be called once the data has been retrived from the url. This was defined earlier in the script and takes 3 parameters. +1. A string representing the path of the file to be read. +2. A function that will be called once the file has been read. This was defined earlier in the script and takes 2 parameters. ```javascript -const printRates = (err, res, body) => { - const data = JSON.parse(body) - console.log(`for each EUR you will get ${data.rates[symbol]} ${symbol} today`) +const printRates = (err, data) => { + if (err) throw err + const parsedData = JSON.parse(data) // this converts the formatted string into a javascript object + for (const country of parsedData) { + if (country.code === symbol) { + console.log(`For each GBP you will get ${country.rate} ${symbol} today.`) + return + } + } } -request.get(url, printRates) +fs.readFile(filePath, printRates) ``` This is a common construct used in JavaScript/NodeJS. The second function parameter is known as a **callback**. @@ -206,9 +213,15 @@ Because callbacks are such a fundamental part of NodeJS you need to spend time t Although this code works, you will rarely see callbacks written in this manner. Creating a function literal is a bit clunky and we can clean up the code by simply passing an anonymous function. ```javascript -request.get( url, (err, res, body) => { - const data = JSON.parse(body) - console.log(`for each EUR you will get ${data.rates[symbol]} ${symbol} today`) +fs.readFile(filePath, (err, data) => { + if (err) throw err + const parsedData = JSON.parse(data) // this converts the formatted string into a javascript object + for (const country of parsedData) { + if (country.code === symbol) { + console.log(`For each GBP you will get ${country.rate} ${symbol} today.`) + return + } + } }) ``` @@ -219,10 +232,13 @@ Take a few moments to make sure you fully understand the syntax, you will be see You are now going to apply you knowledge of JavaScript callbacks by connecting to the [Open Weather API](https://openweathermap.org/api). Start by opening the `weather.js` file: 1. Read through the code to make sure you understand how it works. -2. Register for an API key and add this to the script where indicated. -3. Run the script and check the output, can you explain the first two lines of output, why are the data types as shown? -4. Can you make sense of the other data? -5. Use the [Open Weather API](https://openweathermap.org/api) to retrieve and display the hourly forcast. + - An API key was already registered and is stored in the `apiKey` variable + - Run the script and check the output, can you explain the first two lines of output, why are the data types as shown? + - Can you make sense of the other data? +2. Use the `Open Weather API` to retrieve and display the [hourly forecast](https://openweathermap.org/api/one-call-api). + - Look at the format of the API calls, and substitute the `{lat}`, `{lon}` and `{API key}` placeholders + >`https://api.openweathermap.org/data/2.5/onecall?lat={lat}&lon={lon}&appid={API key}` + - What's the easiest place to put the new request? Why? Why is this problematic? ## 2.2 Defining Functions with Callbacks @@ -238,7 +254,7 @@ Start by opening the `files.js` script and study it carefully: ## 3 Objects -Lets start by creating an manipulating objects using **object literals**. Open the `employee.js` file, read through it and see if you can work out what it does. Now run it to see if you were correct. +Lets start by creating and manipulating objects using **object literals**. Open the `employee.js` file, read through it and see if you can work out what it does. Now run it to see if you were correct. ### 3.1 Creating Object Literals @@ -246,12 +262,13 @@ The simplest way to create new objects is by creating an _object literal_ which ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen' + firstName: 'Colin', + 'last name': 'Stephen', + startYear: 2010 } ``` -As you can see from the simple example above, the data is stored in name-value pairs, referred to as **Properties**. This example is defining an object with **2** properties. +As you can see from the simple example above, the data is stored in name-value pairs, referred to as **Properties**. This example is defining an object with **3** properties. The _name_ part of each property is a JavaScript string which may be enclosed in single quotes. These quotes are optional if the _property name_ is a valid _JavaScript variable_ but they are required if this is not the case. @@ -290,9 +307,9 @@ Whilst it is possible (and useful) to log an entire object to the console, norma ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } console.log(employee) @@ -301,9 +318,9 @@ const lastName = employee['last name'] const grade = employee.grade ``` -Passing the object name to `console.log()` will print out the string representation of the object. To retrieve a specific property value there are two options. If the name is a _legal JS variable name_ the dot `.` notation can be used. This is used to log the `firstName` property in the example above. +Passing the object name to `console.log()` will print out the string representation of the object. To retrieve a specific property value there are two options. If the name is a _legal JS variable name_ the dot `.` notation can be used. This is used to extract the `firstName` property in the example above. -If the name is not a valid JavaScript variable name we need to turn it into a string by using quotes `''` and enclose it in square braces `[]`. This is used to log the `last name` property. +If the name is not a valid JavaScript variable name, we need to turn it into a string by using quotes `''` and enclose it in square brackets `[]`. This is used for the `last name` property. The `grade` variable will be `undefined` because `employee.grade` does not exist. If you want to avoid this and assign a default value if the property is missing you can use the **OR** operator `||`. @@ -317,8 +334,7 @@ This will retrieve the value of the grade property if defined and store it in th 1. Create a new object called `university` which should contain three properties, `year1`, `year2` and `year3`. Each of these properties should store an object whos keys are the module codes and values the titles of the modules. 2. Create a variable called `study01` containing the `year1` object. -3. Use the `for...in` statement to iterate over this `study01` object printing out all of the _module codes_. -4. Use the `for...of` statement to print out all of the _module names_. +3. Use the `for...in` statement to iterate over this `study01` object printing out all of the _module codes_ and the _module names_. ### 3.3 Context @@ -335,7 +351,7 @@ You should start by opening the `counties.js` file and studying it carefully. Th If you look at the console output you will notice that, when we access `this` from _inside_ an object, it is bound to the object from where it was called. -Because we are operating in _strict mode_, when we try to access `this` in the global context it returns `undefined`. +Because we are operating in [_strict mode_](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode), when we try to access `this` in the global context it returns `undefined`. ### 3.4 JSON Data @@ -358,9 +374,9 @@ In the same way that we can convert a JSON string into a JavaScript object we ca ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } const jsonstring = JSON.stringify(employee) console.log(jsonstring) @@ -373,9 +389,9 @@ In this example `jsonstring` is a **String**. If we print out this string we wil const jsonstring = JSON.stringify(employee, null, 2) /* { - "firstName": "Colin", - "last name": "Stephen", - "department": "Computing" + "firstName": "Colin", + "last name": "Stephen", + "department": "Computing" } *? ``` @@ -388,20 +404,20 @@ Lets apply our knowledge of callbacks to implement a simple quotes tool. 1. Create a json-formatted text file called `quotes.json` containing 10 quotes, you can find lots of these on websites such as [brainyquotes](https://www.brainyquote.com/topics/inspirational). Each quote should include the quote and the author. 2. Create a new script called `quotes.js` and use the [`fs.readfile()`](https://nodejs.org/api/fs.html#fs_fs_readfile_path_options_callback) function to read the contents of the file and display it in the terminal. -3. The contents of the file is a utf8 string, use `JSON.parse()` to convert this into a JavaScript object (array) and print this to the terminal instead. +3. The contents of the file is a `UTF-8` string, use `JSON.parse()` to convert this into a JavaScript object (array) and print this to the terminal instead. 4. Create a loop to iterate through the array, printing the contents of each index. -5. Modify the code so that it only prints the quotes string (not the entire object). +5. Modify the code so that it only prints the quote string (not the entire object). 6. Convert the `university` object from the previous exercise into a JSON string and save it to the filesystem as `university.json`. ### 3.5 ECMA6 Object Destructuring -There are situations where we want to retrieve multiple object properties and store then in different variables, for example: +There are situations where we want to retrieve multiple object properties and store them in different variables, for example: ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } const first = employee.firstName const last = employee['last name'] @@ -412,9 +428,9 @@ In ECMA6 it is possible to extract multiple pieces of data into separate variabl ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } const {firstName: first, 'last name': last, department: dept} = employee @@ -424,31 +440,33 @@ console.log(dept) // prints 'Computing' #### 3.5.1 Test Your Understanding -1. Take the `university` object you created in an earlier exercise and use a single line destructuring assignment to create three variables, `year1`, `year2` and `year3`. +1. Take the `university` object inside `employee.js` that you created in an earlier exercise and use a single line destructuring assignment to create three variables, `year1`, `year2` and `year3`. ### 3.6 Getters and Setters -Most object properties are simple values and you can simply assign a value. Sometimes however properties need to be calculated. One solution is to store a function as one of the properties however we would need to call a function to retrieve the value: +Most object properties are simple values and you can simply assign a value. Sometimes however properties need to be calculated. One solution is to store a function as one of the properties, however, we would need to call a function to retrieve the value: ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - getName: () => `${this.firstName} ${this['last name']}` + firstName: 'Colin', + 'last name': 'Stephen', + getName: function() { + return `${this.firstName} ${this["last name"]}` + } } const name = employee.getName() ``` -Whilst this works fine it looks a little clunky. Thankfully in the newer versions of JavaScript you can use a **getter** which makes the code far more intuitive. +Whilst this works fine, it looks a little clunky. Thankfully in the newer versions of JavaScript you can use a [**`getter`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) which makes the code far more intuitive. ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - get name() { - return `${this.firstName} ${this['last name']}` - } + firstName: 'Colin', + 'last name': 'Stephen', + get name() { + return `${this.firstName} ${this['last name']}` + } } const name = employee.name @@ -458,29 +476,29 @@ In the same manner, some properties when set may need to change other properties ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - setName: function(fullname) { - const words = fullname.toString().split(' ') - this.firstName = words[0] || '' - this['last name'] = words[1] || '' - } + firstName: 'Colin', + 'last name': 'Stephen', + setName: function(fullname) { + const words = fullname.toString().split(' ') + this.firstName = words[0] || '' + this['last name'] = words[1] || '' + } } employee.setName('Micky Mouse') ``` -By using a **setter**, it behaves just like any other property. +By using a [**`setter`**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set), it behaves just like any other property. ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - set name(fullname) { - const words = fullname.toString().split(' ') - this.firstName = words[0] || '' - this['last name'] = words[1] || '' - } + firstName: 'Colin', + 'last name': 'Stephen', + set name(fullname) { + const words = fullname.toString().split(' ') + this.firstName = words[0] || '' + this['last name'] = words[1] || '' + } } employee.name = 'Micky Mouse' @@ -488,8 +506,20 @@ employee.name = 'Micky Mouse' #### 3.6.1 Test Your Understanding -1. Print the person's details in an easy to understand sentence. -2. Add a getter to return the number of years the employee has been working for the company, you will need to round this down to a whole number. You should make use of one of the static methods of the built-in [Math](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math) object. +1. In `employee.js`, print the person's details in an easy to understand sentence. + - Notice how `JSON.stringify` also doesn't omit the getter from the string representation of the object, but displays its current output after the property name: + ```json + { + "firstName": "Micky", + "last name": "Mouse", + "startYear": 2010, + "gender": "male", + "date of birth": "1980-01-01", + "details": "firstName: Micky, lastName: Mouse, startYear: 2010, gender: male, DoB: 1980-01-01" + } + ``` +2. Add a getter to return the number of years the employee has been working for the company, you will need to create a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object containing the current time, and then its [`getFullYear()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear) method to get the year value. +3. Create a proper setter that updates the employee's `firstName` and `last name` properties the same way as the one provided does ### 3.7 Modifying Objects @@ -503,9 +533,9 @@ Once an object has been created, additional properties cane be added by setting ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } employee.grade = 4 @@ -519,9 +549,9 @@ Properties can be removed from an object literal using the `delete` operator. Th ```javascript const employee = { - firstName: 'Colin', - 'last name': 'Stephen', - 'department': 'Computing' + firstName: 'Colin', + 'last name': 'Stephen', + 'department': 'Computing' } delete employee.department @@ -530,23 +560,40 @@ delete employee.department ### 3.8 Undefined Values Undefined Objects - -If you try to retrieve a value from an object that is undefined, JS throws a TypeError exception: +- If you try to retrieve a non-existent object, JS throws a `ReferenceError` + - Same thing happens when you try to access "one of its properties" (quotes because the object doesn't exist) +- If you try to retrieve one of `undefined`'s properties, JS throws a `TypeError` +- If you retrieve an existing object's non-existent property, it returns `undefined` + - If you try to retrieve the `undefined` property's property, JS throws a `TypeError` as with any other `undefined` ```javascript -const nonExistentObject.postCode // throws "TypeError" -const addressObject = employee.address // returns undefined -const postCode = employee.address.postCode // throws "TypeError" +nonExistingObject // throws ReferenceError +nonExistentObject.whatever // throws ReferenceError +undefined.whatever // throws TypeError +const existingObject = {} +existingObject.nonExistentProperty // returns undefined +existingObject.nonExistentProperty.whatever // throws TypeError ``` -To see what a `typeError` looks like, try uncommenting the three lines at the end of the `employee.js` file. So how can we avoid this? +To see what a `TypeError` looks like, try uncommenting lines marked with `"TypeError:"` in the `employee.js` file. So how can we avoid this? -The **AND** operator `&&` can be used to guard against this problem. +The [**`Logical AND`** `(&&)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND) operator can be used to guard against this problem. +- As this operator doesn't evaluate the second value if the first one is [`falsy`](https://developer.mozilla.org/en-US/docs/Glossary/falsy), we can be sure it won't try to retrieve a property of `undefined`. ```javascript const postCode = employee.address && employee.address.postCode console.log(postCode) // returns undefined ``` +- `postCode` gets the value of `employee.address` if it's [`falsy`](https://developer.mozilla.org/en-US/docs/Glossary/falsy) (e.g. `undefined` (as it's in this case), `false`, `0`, or `''` (empty string)) +- but would get the value of `employee.address.postCode` if `employee.address` is [`truthy`](https://developer.mozilla.org/en-US/docs/Glossary/truthy) (e.g. `true`, `1`, `'a'` (non-empty string), or an existing object (even empty object)) + +General examples: +```javascript +'' && 'foo' // returns '' +false && 'foo' // returns false +{} && 'foo' // returns 'foo' +1 && 0 // returns 0 +``` #### 3.8.1 Test Your Understanding @@ -556,14 +603,13 @@ console.log(postCode) // returns undefined ### 3.9 Object Prototypes -All JavaScript object (such as String, Number, Array, etc.) inherit properties and methods from a **prototype**. This also applies to any new objects you create. Since JavaScript does not support _traditional_ classes, this becomes the way to add new functionality. Let's look at a simple example. +All JavaScript objects (such as `String`, `Number`, `Array`, etc.) inherit properties and methods from a [**`prototype`**](https://developer.mozilla.org/en-US/docs/Glossary/Prototype-based_programming). This also applies to any new objects you create. Since JavaScript does not support _traditional_ classes, this becomes the way to add new functionality. Let's look at a simple example. The `String` object does not have a way to convert a string into an array of characters so we will add this. After it is added we can see that _all strings_ have this new behaviour. ```javascript String.prototype.toArray = function() { - const strArr = this.split('') - return strArr + return this.split('') } const nameArray = 'John Doe'.toArray() @@ -582,14 +628,14 @@ There are a couple of important concepts here. ## 4 Object Constructors -As you have seen from the previous section, each object (String, Number, etc) has its own _prototype_, but what about the custom objects you created? It turns out that these also have a prototype, _Object_. Any functionality you add to this will get added to _all the objects in your application!_. To get round this problem NodeJS has the `new` keyword. When this is used we can isolate any changes to the targeted object. +As you have seen from the previous section, each object (String, Number, etc) has its own _prototype_, but what about the custom objects you created? It turns out that these also have a prototype, _Object_. Any functionality you add to this will get added to _all the objects in your application!_. To get around this problem NodeJS has the `new` keyword. When this is used, we can isolate any changes to the targeted object. ### 4.1 Object Constructor Functions -Until ECMA6, there wa a way to achieve this by using a **constructor function**. Whilst this is not now considered the optimal way to achieve our goal there are so many examples of this approach it is important you understand both the syntax and how it works. When we use this approach using the `new` keyword triggers four steps: +Until ECMA6, there was a way to achieve this by using a **constructor function**. While now this isn't considered the optimal way to achieve our goal, there are so many examples of this approach, that it's important you understand both the syntax and how it works. When we use this approach, using the `new` keyword triggers four steps: 1. We create an empty object. -2. We set the its prototype property to the constructor function's prototype function. +2. We set its prototype property to the constructor function's prototype function. 3. We bind its `this` object to the new object. 4. We then return the new object. @@ -621,7 +667,7 @@ Whilst this syntax is not using traditional classes, one object can _extend_ ano ```javascript function Student(name, startYear, course) { Person.call(this, name, startYear) - this.subject = course || 'not enrolled' + this.course = course || 'not enrolled' } const emily = new Student('emily', 2017, 'architecture') @@ -639,14 +685,14 @@ Whilst constructor functions are not particularly elegant they do provide a way ```javascript class Person { - const currentYear = 2019 - - constructor(name, startYear) { - this.name = name - this.startYear = startYear || currentYear - this.years = currentYear - this.startYear - return this - } + constructor(name, startYear) { + const currentYear = 2019 + + this.name = name + this.startYear = startYear || currentYear + this.years = currentYear - this.startYear + return this + } } ``` @@ -654,10 +700,10 @@ Since this is syntactic sugar for the constructor function we can extend this to ```javascript class Student extends Person { - constructor(name, startYear, course) { - super(name, startYear) - this.subject = course || 'not enrolled' - } + constructor(name, startYear, course) { + super(name, startYear) + this.subject = course || 'not enrolled' + } } ``` @@ -667,16 +713,16 @@ We can also make use of **getters** and **setters** to retrieve and modify objec ```javascript class Student extends Person { - constructor(name, startYear, course) { - super(name, startYear) - this.subject = course || 'not enrolled' - } - get course() { - return this.subject - } - set course(newCourse) { - this.subject = newCourse - } + constructor(name, startYear, course) { + super(name, startYear) + this.subject = course || 'not enrolled' + } + get course() { + return this.subject + } + set course(newCourse) { + this.subject = newCourse + } } ``` @@ -686,13 +732,13 @@ Currently each instance of a prototype function is completely self-contained. Wh ```javascript class ECMA6Student extends Person { - constructor(name, startYear, course) { + constructor(name, startYear, course) { super(name, startYear) this.subject = course || 'not enrolled' if(ECMA6Student.count === undefined) ECMA6Student.count = 0 ECMA6Student.count++ - } - static studentCount() { + } + static studentCount() { return ECMA6Student.count } } @@ -718,6 +764,7 @@ You should take time to understand the [pros and cons](https://2ality.com/2016/0 ### 4.6 Test Your Understanding +In a new file called `vehicles.js`: 1. Create a **constructor function** called `OldVehicle` that includes `make`, `model` and `price` information. Use this to create two vehicles of your choice. 2. Use this to create a second **constructor function** class called `OldPickup` that includes `payload` and `seats` fields and use this to create two pickup objects. 3. Now use the same information to create a class called `NewVehicle` and extend this to create a class called `NewPickup` and use this to create two or more pickup objects. diff --git a/exercises/05_javascript/currency.js b/exercises/05_javascript/currency.js index 0725b1f2..82551691 100644 --- a/exercises/05_javascript/currency.js +++ b/exercises/05_javascript/currency.js @@ -2,16 +2,21 @@ 'use strict' -const request = require('request') +const fs = require('fs') -const symbol = 'GBP' +const symbol = 'EUR' -const printRates = (err, res, body) => { - if (err) throw 'could not complete request' - const data = JSON.parse(body) // this converts the formatted string into a javascript object - console.log(`for each EUR you will get ${data.rates[symbol]} ${symbol} today`) +const printRates = (err, data) => { + if (err) throw err + const parsedData = JSON.parse(data) // this converts the formatted string into a javascript object + for (const country of parsedData) { + if (country.code === symbol) { + console.log(`For each GBP you will get ${country.rate} ${symbol} today.`) + return + } + } } -const url = 'currency.json' +const filePath = 'currency.json' -request.get( url, printRates) +fs.readFile(filePath, printRates) \ No newline at end of file diff --git a/exercises/05_javascript/employee.js b/exercises/05_javascript/employee.js index 507492a0..04133976 100644 --- a/exercises/05_javascript/employee.js +++ b/exercises/05_javascript/employee.js @@ -7,7 +7,9 @@ const employee = { firstName: 'Colin', 'last name': 'Stephen', startYear: 2010, - getName: () => `${this.firstName} ${this['last name']}`, + getName: function() { + return `${this.firstName} ${this["last name"]}` + }, setName: function(fullname) { console.log(fullname) const words = fullname.toString().split(' ') @@ -23,3 +25,7 @@ console.log(jsonString) employee.setName('Micky Mouse') console.log(JSON.stringify(employee, null, 2)) + +// TypeError: +// const postCode = employee.address.postCode +// console.log(postCode) \ No newline at end of file diff --git a/exercises/05_javascript/weather.js b/exercises/05_javascript/weather.js index f139cc00..e72b7bac 100755 --- a/exercises/05_javascript/weather.js +++ b/exercises/05_javascript/weather.js @@ -4,12 +4,13 @@ const request = require('request') -const url = 'https://api.openweathermap.org/data/2.5/weather?q=coventry,uk&appid=44c39f3fa462f86b3fc88f5678e5c5ff' +const apiKey = '44c39f3fa462f86b3fc88f5678e5c5ff' +const cityName = 'coventry,uk' -request(url, (err, response, body) => { +request(`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${apiKey}`, (err, response, body) => { if(err) console.log(err.message) console.log(`the body variable contains a ${typeof body}`) const data = JSON.parse(body) console.log(`the data variable contains an ${typeof data}`) console.log(data) -}) +}) \ No newline at end of file diff --git a/solutions/05_javascript/contact.js b/solutions/05_javascript/contact.js new file mode 100644 index 00000000..3d358404 --- /dev/null +++ b/solutions/05_javascript/contact.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +'use strict' + +/* This script demonstrates how JavaScript can be used to handle exceptions. */ + +const readline = require('readline-sync') + +/* any code that could throw an exception needs to be wrapped in a try block. If any line of code in the try block throws an exception the program immediately jumps to the catch block, passing the Error object thrown. Regardless of whether an error was thrown or not, the code in the finally block is run. */ +try { + const email = String(readline.question('enter email address: ')) + console.log('email is a '+typeof email+' and contains '+email) + validateEmail(email) + const score = Number(readline.question('assign score 1-10: ')) + console.log('score is a '+typeof score+' and contains '+score) + validateScore(score) + const comment = String(readline.question('enter your comment : ')) + validateComment(comment) + console.log(`Thank you ${email}. You gave us a rating of ${score}/10 with the comment "${comment}"`) +} catch(err) { + console.log(`${err.name} thrown`) + console.log(`The error message is: ${err.message}`) + console.log(err.stack) +} finally { + console.log('the script has finished') +} + +/** + * Checks to see if the supplied parameter is formatted as a valid email + * address. + * @param {string} email - The email address to be checked. + * @returns {bool} the validity of the email address string + * @throws {TypeError} if the parameter is not a valid email address. + */ +function validateEmail(email) { + console.log(email) + const atIndex = email.lastIndexOf('@') + const dotIndex = email.lastIndexOf('.') + if ( email.length < 5 + || atIndex < 1 + || email.indexOf(' ') !== -1 + || email.indexOf('..') !== -1 + || dotIndex < atIndex + || dotIndex === email.length - 1 ) { + throw TypeError + } + return true +} + +/** + * Checks to see if the supplied parameter is a valid integer in the range 1-10. + * @param {string} score - The user-specified score. + * @returns {bool} whether the parameter is a valid integer in range + * @throws {TypeError} if the parameter is not a valid integer. + * @throws {RangeError} if the parameter is not in the range 1-10. + */ +function validateScore(score) { + const minScore = 0 + const maxScore = 10 + if (Number.isNaN(score) || score % 1 !== minScore) { + throw new TypeError('parameter is not a valid integer') + } + if (score < 1 || score > maxScore) { + throw new RangeError('parameter should be in the range 1-10') + } + return true +} + +/** + * Checks to see if the supplied comment is 'valid'. + * @param {string} comment - The user-specified score. + * @returns {bool} whether the comment is 'valid' + * @throws {RangeError} if the comment is not long enough. + */ +function validateComment(comment) { + const minLen = 5 + if (comment.length < minLen) { + throw new RangeError('comment should be at least 5 characters long') + } + return true +} diff --git a/solutions/05_javascript/employee.js b/solutions/05_javascript/employee.js new file mode 100644 index 00000000..54429d58 --- /dev/null +++ b/solutions/05_javascript/employee.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +const employee = { + firstName: 'Colin', + 'last name': 'Stephen', + startYear: 2010, + gender: 'male', + 'date of birth': '1980-01-01', + getName: function() { + return `${this.firstName} ${this["last name"]}` + }, + setName: function(fullname) { + console.log(fullname) + const words = fullname.toString().split(' ') + console.log(words) + console.log(this) + this.firstName = words[0] || '' + this['last name'] = words[1] || '' + }, + get details() { + return `firstName: ${this.firstName}, lastName: ${this["last name"]}, startYear: ${this.startYear}, gender: ${this.gender}, DoB: ${this["date of birth"]}` + }, + get yearsWorked() { + return (new Date().getFullYear() - this.startYear) + }, + set name(fullname) { + const names = fullname.toString().split(' ') + this.firstName = names[0] + this["last name"] = names[1] + } +} + +const jsonString = JSON.stringify(employee, null, 2) +console.log(jsonString) + +employee.setName('Micky Mouse') +console.log(JSON.stringify(employee, null, 2)) + +// TypeError: +// const postCode = employee.address.postCode +// console.log(postCode) + +const university = { + year1: { + a101: 'something11', + b104: 'something12', + c134: 'something13' + }, + year2: { + d201: 'something21', + e204: 'something22', + f234: 'something23' + }, + year3: { + g301: 'something31', + h304: 'something32', + i334: 'something33' + } +} + +const study01 = university.year1 +for(const code in study01) console.log(`${code} ${study01[code]}`) + +const {year1: year1, year2: year2, year3: year3} = university + +delete employee.startYear +employee.startYear = '2010' + +String.prototype.toArray = function() { + return this.split('') +} +Array.prototype.toStr = function() { + return this.join('') +} +console.log('foobar'.toArray().toStr()) \ No newline at end of file diff --git a/solutions/05_javascript/maths.js b/solutions/05_javascript/maths.js new file mode 100644 index 00000000..e7e6e769 --- /dev/null +++ b/solutions/05_javascript/maths.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/* eslint no-magic-numbers: 0 */ + +'use strict' + +function largestNumber(a, b) { + if (a > b) return a + if (b > a) return b + return null +} + +const biggest = largestNumber(5, 8) +console.log(biggest) +// the code below achieves the same using the 'spread operator' +const nums = [5, 8] +const biggest2 = largestNumber(...nums) +console.log(biggest2) + +// example using the arguments object +function add() { + let total = 0 + console.log(arguments) + console.log(arguments['1']) + for(const arg of arguments) { + total += arg + } + return total +} + +const addNums = add(1, 2, 3, 4) +console.log(addNums) + + +// example using a rest parameter +function add2(...values) { + let total = 0 + console.log(values) + for (let i=0; i dividend - Math.floor(dividend / divisor) * divisor +console.log('remainder2: ' + remainder2(13, 4)) + +// function expression using arrow syntax and one parameter +const sqr = num => num * num +console.log(sqr(4)) + +function multiply(a = 1, b = 1) { + return a * b +} +console.log(multiply(3, 5)) + +function divideThis(dividend, divisor = 1) { + return dividend / divisor +} +console.log(divideThis(5, 2)) + +function average(...numbers) { + if (numbers.length === 0) return 0 + let sum = 0 + for (const number of numbers) sum += number + return sum / numbers.length +} +console.log(`average of [2, 4, 6]: ${average(2, 4, 6)}`) + +const squareRoot = a => Math.sqrt(a) +console.log(`squareRoot of 4096: ${squareRoot(4096)}`) + +const longest = (...strings) => strings.length === 0 ? null : strings.reduce((longestString, currentString) => currentString.length > longestString.length ? currentString : longestString) +// simple version without using reduce: +// const longest = (...strings) => { +// if (strings.length === 0) return null +// let longest = strings[0] +// for (const string of strings) if (string > longest) longest = string +// return longest +// } +console.log(`longest of ['a', 'aaa', 'aa']: ${longest('a', 'aaa', 'aa')}`) \ No newline at end of file diff --git a/solutions/05_javascript/quotes.js b/solutions/05_javascript/quotes.js new file mode 100644 index 00000000..45b2c6e4 --- /dev/null +++ b/solutions/05_javascript/quotes.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +'use strict' + +require('fs').readFile('quotes.json', (err, data) => { + if (err) console.log(err) + const quotesArray = JSON.parse(data) + console.log(quotesArray) + for (const quoteObject of quotesArray) { + console.log(quoteObject.quote) + } +}) \ No newline at end of file diff --git a/solutions/05_javascript/quotes.json b/solutions/05_javascript/quotes.json new file mode 100644 index 00000000..bd394721 --- /dev/null +++ b/solutions/05_javascript/quotes.json @@ -0,0 +1,42 @@ +[ + { + "quote":"We know what we are, but know not what we may be.", + "author":"William Shakespeare" + }, + { + "quote":"Perfection is not attainable, but if we chase perfection we can catch excellence.", + "author":"Vince Lombardi" + }, + { + "quote":"Do your little bit of good where you are; it's those little bits of good put together that overwhelm the world.", + "author":"Desmond Tutu" + }, + { + "quote":"My mission in life is not merely to survive, but to thrive; and to do so with some passion, some compassion, some humor, and some style.", + "author":"Maya Angelou" + }, + { + "quote":"We must let go of the life we have planned, so as to accept the one that is waiting for us.", + "author":"Joseph Campbell" + }, + { + "quote":"The measure of who we are is what we do with what we have.", + "author":"Vince Lombardi" + }, + { + "quote":"Don't judge each day by the harvest you reap but by the seeds that you plant.", + "author":"Robert Louis Stevenson" + }, + { + "quote":"I hated every minute of training, but I said, 'Don't quit. Suffer now and live the rest of your life as a champion.'", + "author":"Muhammad Ali" + }, + { + "quote":"Thousands of candles can be lighted from a single candle, and the life of the candle will not be shortened. Happiness never decreases by being shared.", + "author":"Buddha" + }, + { + "quote":"How wonderful it is that nobody need wait a single moment before starting to improve the world.", + "author":"Anne Frank" + } +] \ No newline at end of file diff --git a/solutions/05_javascript/university.json b/solutions/05_javascript/university.json new file mode 100644 index 00000000..50ce27a3 --- /dev/null +++ b/solutions/05_javascript/university.json @@ -0,0 +1,17 @@ +{ + "year1": { + "a101": "something11", + "b104": "something12", + "c134": "something13" + }, + "year2": { + "d201": "something21", + "e204": "something22", + "f234": "something23" + }, + "year3": { + "g301": "something31", + "h304": "something32", + "i334": "something33" + } +} \ No newline at end of file diff --git a/solutions/05_javascript/vehicles.js b/solutions/05_javascript/vehicles.js new file mode 100644 index 00000000..d0dfeaf1 --- /dev/null +++ b/solutions/05_javascript/vehicles.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +'use strict' + +function OldVehicle(make, model, price) { + this.make = make + this.model = model + this.price = price +} + +const OldVehicle1 = new OldVehicle('Jaguar', 'E-type', 5600) +console.log(OldVehicle1) +const OldVehicle2 = new OldVehicle('Volkswagen', 'Beetle', 3200) +console.log(OldVehicle2) + +function OldPickup(make, model, price, payload, seats) { + OldVehicle.call(this, make, model, price) + this.payload = payload + this.seats = seats +} + +const OldPickup1 = new OldPickup('GMC', 'Sonoma', 14000, 1500, 2) +console.log(OldPickup1) +const OldPickup2 = new OldPickup('Jeep', 'Comanche', 15900, 1800, 2) +console.log(OldPickup2) + +class NewVehicle { + constructor(make, model, price) { + this.make = make + this.model = model + this.price = price + } +} + +class NewPickup extends NewVehicle { + static totalValue = 0 + constructor(make, model, price, payload, seats) { + super(make, model, price) + this.payload = payload + this.seats = seats + NewPickup.totalValue += price + } +} + +const NewPickup1 = new NewPickup('Ford', 'F-150', 46900, 3000, 5) +console.log(NewPickup1) +const NewPickup2 = new NewPickup('Chevrolet', 'Silverado', 44900, 2500, 5) +console.log(NewPickup2) +console.log(NewPickup.totalValue) \ No newline at end of file diff --git a/solutions/05_javascript/weather.js b/solutions/05_javascript/weather.js new file mode 100644 index 00000000..d8f45dab --- /dev/null +++ b/solutions/05_javascript/weather.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +'use strict' + +const request = require('request') + +const apiKey = '44c39f3fa462f86b3fc88f5678e5c5ff' +const cityName = 'coventry,uk' + +request(`https://api.openweathermap.org/data/2.5/weather?q=${cityName}&appid=${apiKey}`, (err, response, body) => { + if(err) console.log(err.message) + console.log(`the body variable contains a ${typeof body}`) + const data = JSON.parse(body) + console.log(`the data variable contains an ${typeof data}`) + console.log(data) + + request(`https://api.openweathermap.org/data/2.5/onecall?lat=${data.coord.lat}&lon=${data.coord.lon}&appid=${apiKey}`, (err, response, body) => { + if(err) console.log(err.message) + const data = JSON.parse(body) + console.log(data) + }) +}) \ No newline at end of file