Lecture plan JavaScript 20-10-26 kl 13
JavaScript for Python and Java programmers
Cristian Bogdan
const, let, for(;;), if()… But do we need for and if??
const arr= [1,2,3,5];
// sum up the odd elements plus one:
// Procedural: �let sum= 0;�for(let i=0; i<arr.length; i++){� if(arr[i] %2===1) {� sum+= arr[i] +1;� }�}
// Functional:�// keeping only the odd ones�arr.filter(function(x){return x%2===1;})
// or:�arr.filter(x=>x%2)
// adding 1�arr.map(x=>x+1)
// sum up�arr.reduce((acc, x)=> acc+x, 0)
// filter, map, reduce do NOT change the array
// they return a new array (immutable)
// The new array also has the filter, map, reduce methods
// So the functional form is
arr.filter(TODO).map(TODO).reduce(TODO)
Variables and scopes
let a = 42; // global scope, explicit declaration
function f1(param1) { // parameters visible in local scope� let b = 12; // local scope, explicit declaration� neverUse = 27; // global scope, implicit declaration, NEVER use!!!� const d="the d string"; // local scope, constant declaration� let a= 10; // local a shadowing the global a
function f2(param2){� let e= new Date(); // only visible in f2, not f1� let f= a+b+param1+param2; // f2 has access to all f1 variables and parameters � let a= 12; // shadowing a yet again
}
}
From past years
function f(){
for(i=0; i< someMax;i++) { call to g() }
}
// same technique used in g() and all other functions for all loops
What’s the problem?
What if you call a monetized API from these loops?
Therefore you will call the lab API through a proxy that counts how many calls per second each lab group makes
Which data types are new to you?
let myInt = 7;
let myBool = true;
let myString = "ABC";
let myQuoteMixString= 'AB"C"D';
let myRegex= /ab+c/ // regular expression
let myTemplateString= `first line
second line`;
let myTemplateFormatString= `first ${myInt} line
second line`; // ^^^^ will expand ${myInt} to 7
let anObject={ a:3, b:true }; // see Objects later
let myArray = [1, 2, false, [4, 2], "x"]; // see Arrays later
const aFunction= function(param){ console.log(param);} // functions are just like any other variable
const arrowFunction= (param) => console.log(param) ; // arrow function
Operators
Assignment and Comparison operators are the same as in C, Java, Python
=, +=, -= …
Logical operators are like in Java and C so they differ from Python
|| or && and ! not
C/Java Conditional operator �condition?ifTrue:ifFalse Python >=2.5: ifTrue if condition else ifFalse
Assignment is an expression, returns a value (like in C, Java, unlike in Python) but do not use that!
(a=1)+2 → 3
let a=0;
if(a=1)console.log(“was true”)�????
Automatic conversion
Happens often in Javascript so the code is briefer… �From basic types to Objects (autoboxing)
(1).toString() the integer 1 is converted automatically to a Number object, which has a toString() method. See also �(1).constructor �(/ab+c/).constructor
Conversion between basic types: string + number converts the number to string automatically. number+ boolean also works (boolean converted to 0 or 1)
Comparison operators try to convert first!
1=="1" → true Why in the world do they have this feature?�1==="1" → false. Try also 1!=="1" 1!=="0"
Objects and methods
Very easy to create:
let rect={height:10, width:20} ;
Adding a property (aka field in C, Java)
rect.color="blue";
Methods are just function properties:
rect.hello=function(){ return "I am a rectangle";} ;
rect.hello= () => "I am a rectangle" ;
How to add a surface() method? �Hint: use this�Hint: do not use arrow functions
You can skip the end-of-statement ; (semicolon) if it comes at the end of the line. ��Especially useful at the Console
You may see it skipped in lecture materials or slides when it’s meant to experiment at the console
this in arrow functions
It may be attractive to use
rect.surface=() => this.height* this.width
But it will not work!
In traditional function(), this has a very complicated definition. In arrow functions, the JS designers simplified: they do not have an own this, but inherit it from the enclosing traditional function, if any.
In short: in Classes and Objects, use traditional, not arrow, functions as methods. Within methods, you can use arrow functions, they will inherit the method scope and therefore the “right” this (the object).
Read more at MDN. Look for “No separate this” in the page
Another resource.
“arrow functions don't bind their own scope, but inherit it from the parent scope”
const obj={ // form used a lot by Vue.js� someProp: 13,� method(param){ � const f= x=> x+this.someProp;� return f(param); � },� lastProp: 42�};
class MyClass{� constructor(){ this.someProp=13; }� method(param){ � const f= x=> x+this.someProp;� return f(param); � }�};
for(;;) for( in ) for ( of )
for (;;) is like in the traditional C/Java. You can declare the variable on the spot using let
for(let i=0; i< someMax; i++) {... }
for(... in …) iterates through properties of an object
for(const x in rect) {console.log(x); } // will print a line for each of: height, width, surface
for(...of …) iterates through elements of an Iterable object (string, array, ...)
// as in C or Java, you can skip the curly braces {} if there’s just one statement�for(const x of "some string") console.log(x); // will print a line for each character in the string
Which one do we recommend? None!
Use array.map(func) or array.reduce(func, initialAccumulatorValue). �Sometimes array.forEach(func) will do
Objects keys, properties
let o={} // equivalent with:�o= new Object() // especially python ppl: always use new!
A key is a property name. obj[“key”] is the same as obj.key but much more general:
rect.height → 10 �rect["height"] → 10
rect["height"]=12 �let field="height"�rect[field] → 12 // Compare with java Core Reflection...
for(const f in rect) console.log(rect[f]) � // will print the values of height and width, and the surface function
delete rect.surface → true // delete was successful�delete rect[0] → true // delete is successful even for non-existent keys...
for(const x in rect) console.log(x) // for ... in can be rewritten using for ... of and Object.keys�Object.keys(rect) → ["height", "width", "surface"]
for(const x of Object.keys(rect)) console.log(x) // print same as for … in above!
Keys in object initializers can be variable
let k="height":�let rect={[k]:10, width:20}
Arrays as objects with integer keys
let arr=[7,2,9] // arrays are objects with integer keys and a special prototype. Same as new Array(7,2,9)�arr.length → 3 �arr.something="bla" // can be mixed with ordinary keys �arr.length → 3 �arr[0] → 7�arr[2] → 9 �arr["2"] → 9 // 2 above was actually converted automatically to "2"!!! See Object.keys() call below�arr["something"] → "bla" // the integer and non-integer keys are accessed the same way!!!�arr["length"] → 3 // length is always the max integer key + 1. something is a non-integer key�Object.keys(arr) → ["0", "1", "2", "something"] , length is hidden… Object.keys returns an array of keys for any object�arr[7]=11 // array grows automatically�arr.length → 8 // max integer key is 7; 7 +1 = 8�arr[5] → undefined �Object.keys(arr) → ["0", "1", "2", “7”, "something"] �arr → [7, 2, 9, empty × 4, 11, something: "bla"]��arr[6]={someProp:5} // arrays can contain objects�arr[5]=[1,3,5] // arrays can contain arrays, a consequence of the above�
Higher-order functions. Functional programming
Array.prototype.filter, map, sort, etc. return an array so they can be chained
[3,1,2].sort() → [1,2,3]�let plusOne= x=>x+1 // same as function plusOne(x){ return x+1; }�[0,7,5].map(plusOne) → [1,8,6]�[2,0,7].map(plusOne).sort() → [1,3,8]�[2,0,7].map(plusOne).sort().map(plusOne) → [2,4,9]
let isEven= x=>x%2==0 �[2,0,7].filter(isEven) → [2,0]�[2,0,7].filter(isEven).map(plusOne) → [3,1]�[2,0,7].filter(isEven).map(plusOne).filter(isEven) → [] of course :))�[2,0,7].filter(isEven).map(plusOne).sort() → [1,3]
reduce passes an accumulator together with each array element to a given function, to calculate the new accumulator. The result is the final accumulator.
let addUp= (acc, x) => acc+x�[2,0,7].reduce(addUp, 0) → 9 // calculates sum of all array elements�[2,0,7].reduce(addUp, "") → "207" // same function, different initial accumulator
Any for() can be replaced with functional code
Array functions can easily replace any for loop, which are a known source of bugs
for(;;), for( of ) just use map(), reduce() or forEach()
for( in )
for(const x in rect) {console.log(x); } // This is procedural… functional version:�Object.keys(rect).forEach(x=>console.log(x)) // or shorter:�Object.keys(rect).forEach(console.log)
When to use map()?
When to use reduce()? When you want to sum array elements, concatenate elements (arrays, strings), collect object properties, etc
When to use forEach() ? When you are not interested in the result of any iteration
More on functional programming
Functional programming usually leads to safer code. This compensates for JavaScript’s lack of type safety, private members, etc.
Even if a function is passed to them, higher-order functions, are different from Alice/Bob subscription/notification
Callbacks and asynchronous execution
// Alice: my code, subscribing to: when 7 seconds passed Bob: the browser, subscription method: setTimeout
setTimeout(()=> console.log("tick"), 7000) // when will this finish? in 7 seconds or immediately?
Subscriptions finish immediately (be it setTimeout, addEventListener, etc)! The callback executes at a point in time independent from Alice’s code (at the time of Bob’s choosing). That is, execution is asynchronous
let tick= ()=> console.log("tick")�setTimeout(tick, 2000)
// Frequent mistake: Alice should never call its callback
setTimeout(tick(), 2000)
Shorthand: from variables to object properties
let height=5; �let width=10;�let rectangle={ height, width } // shorthand object initialiser equivalent with:
let rectangle={height:height, width:width} // and with�rectangle={"height":height, "width":width}
let height="alpha"; // copied by value not by reference�let rectangle={ height, width };�height="beta";�rectangle → {height: "alpha", width: 10}
Spread syntax ...
Combining arrays, elements�[...arr1, sep, ...arr2] makes a new array with the elements of arr1, followed by a “separator” element (sep), followed by the elements of arr2. Very easy to concatenate arrays or to insert elements at beginning or end
Making a true Array from an iterable�[...node.children] �HTML collections like node.children, selectElement.options are not of the class Array, so they don’t have map(), forEach() etc. If you want to do functional programming with them, you need to convert them to array first, using the spread.
Variable number of parameters�function(param1, param2, ...restOfParams) �We know for sure that we need two parameters but from parameter 3 on, we leave it flexible. restOfParams is available to the programmer as an array, can have zero or more elements. E.g. console.log takes any number of parameters
Passing each array element as a parameter to a function with variable parameter number�func(p1, p2, ...arr) Example:�console.log([1,2,3]) sees one parameter, prints the array BUT�console.log(0, ...[1,2,3]) sees 4 parameters, prints them separated by spaces
Instead of�arr.push(x); // statement, changes the array�obj[k]=v; // statement, changes the object
Much better for functional programming:�[...arr, x] // expression, creates new array
Spread syntax works also with objects lately
{...obj, ...obj2, key:value}
{... obj, [key1]:val, ...obj2} // variable key name
From object properties to variables: destructuring
let {height, width} = rect;
Destructuring is especially useful in function parameters. Used by React functional components, and any hyperscript/JSX functional component
function someFunction({height, width}) { console.log(height, width); } // equivalent with:� function someFunction(obj) { console.log(obj.height, obj.width); }
// or the thick arrow versions:�const someFunction= ({height, width}) => console.log(height, width); // equivalent with:� const someFunction= obj => console.log(obj.height, obj.width);
// Test all with �someFunction({height:10, width:20})
Destructuring works also with arrays. Used by React hooks like e.g. useState()�const [a, b]= someArray;
If you come from Java
The syntax is the same (comes from C: if(){}else{}, for(;;){}, ==, !=, +=, -=), with a few additions
Functions are represented as strings in JavaScript, they are interpreted when they are called (with optimization). You can always retrieve the source code of a function, just call func.toString()
If you come from Python
you have to learn the C/Java syntax if(){}else{}, for(;;){}, ==, !=, +=, -=
In principle JavaScript is designed to accept as much as possible without complaining. It is a scripting language!
JavaScript closures: event listener
We use closures naturally in JS but let’s examine them closer. �Consider a DOM event listener on each element of an array. �h("div", {}, dishes.map(dish=>� h("div", {}� , h("span", {}, dish.title)� , h("button", {onClick: e=> model.delete(dish) }, "delete")� )� ) // end of map()�);
In a traditional programming language, the value of the dish local variable/parameter will disappear from memory (stack space) at the end of the function dish=> h("div", …). However, in JavaScript such local variables are kept around for as long as they are needed!
dish will be needed at every button click! Will be kept until the click listener is garbage collected (usually together with the button). �
Closures are typically used in connection to Bob-Alice notifications like event listeners. Typically the callback uses a local variable from its enclosing function.�
Declaring function: dish=>h(...)�Variable: dish�Callback: e=>delete(dish)�Alice: hyperscript (renderer)�Bob: button�Subscription: onClick
JavaScript closures: cancelling the previous render
Consider a “render search result” function invoked at each keystroke. The render performs a query fetch() and renders the result. However if a new key was typed, the query string changed so there’s no point to render!�class SearchView{� renderSearchResults(query){� if(this.cancelRender)� this.cancelRender();� let cancelled= false;� fetch(ENDPOINT+query).then(r=>r.json())� .then(data=> !cancelled && h("div", {}, data));� this.cancelRender= ()=> { cancelled=true; } ;� }�}�At the end, the function creates another function which it stores as this.cancelRender. The function will set to true the local variable ‘cancelled’
Declaring function: renderSearchResults()�Variable: cancelled�Callback: data=> !cancelled && h() �Alice: hyperscript (renderer)�Bob: Promise�Subscription: then()
renderSearchResults(query1= "p")�this.cancelRender: undefined�cancelled1: false�this.cancelRender: ()=>{cancelled1=true}
renderSearchResults(query2= "pi")�this.cancelRender() cancelled1 becomes true
promise1.then(data=>!true && ) //no render!
cancelled2: false�this.cancelRender: ()=>{cancelled2=true}�
A more advanced version: abort the fetch(). Closure variable is the fetch controller�const controller = new AbortController();�fetch(url, {signal:controller.signal}).then(r=>r.json())� .then(d=>h("div",{}, d)).catch(err=>/*ignore on abort */); �this.cancelRender=()=>controller.abort() ; // catch() -> no render