/////////
function filter(array, predicate) {
const result = [];
for (let i = 0; i < array.length; ++i) {
if (predicate(array[i], i, array)) result.push(array[i]);
/////////////////////////////
}
return result;
}
const primes = [ 11, 2, 3, 13, 5, 7, 17, 19 ];
filter(primes, x => x < 10); // [ 2, 3, 5, 7 ]
///////////function smallerThan(limit) {
return x => x < limit;
} //////////////
filter(primes, smallerThan(10)); // [ 2, 3, 5, 7 ]
///////////////- Inner functions have access to variables of their outer function
- Even after the outer function (here
smallerThan) has returned! - JavaScript even allows modification of the outer variables:
function makeCounter() {
let next = 1;
return function () {
return next++;
};
}
const a = makeCounter();
const b = makeCounter();
// 1 2 3
[ a(), a(), b(), a(), b(), b() ] // [1, 2, 1, 3, 2, 3]
// 1 2 3Exercise:
- Complete the function
makeFibonacci:
function makeFibonacci() {
// TODO initialize state
return function () {
// TODO update state
// TODO return value
};
}
const f = makeFibonacci();
// 0 1 1 2 3 5 8 13 21 34 55 89
[ f(), f(), f(), f(), f(), f(), f(), f(), f(), f(), f(), f() ]- What is wrong with the following function?
function fibonacci() {
const result = [];
let a = 0n;
result.push(a); // 0n
let b = BigInt(1);
result.push(b); // 1n
while (true) {
result.push(a += b); // 1n 3n 8n 21n 55n ...
result.push(b += a); // 2n 5n 13n 34n 89n ...
}
return result;
}- The infinite
while(true)loop keeps consuming memory- This will eventually throw an out-of-memory error
- The unreachable
returnstatement is never executed- Generators “return” each element separately for immediate consumption:
/////// v
function* fibonacci() {
let a = 0n;
yield a; // 0n
let b = BigInt(1);
yield b; // 1n
while (true) {
yield a += b; // 1n 3n 8n 21n 55n ...
yield b += a; // 2n 5n 13n 34n 89n ...
}
}
const generator = fibonacci();
generator.next(); // { value: 0n, done: false }
generator.next(); // { value: 1n, done: false }
generator.next(); // { value: 1n, done: false }
generator.next(); // { value: 2n, done: false }
generator.next(); // { value: 3n, done: false }
generator.next(); // { value: 5n, done: false }
generator.next(); // { value: 8n, done: false }- Generator
function*s return generator objects - Generator objects are iterable:
for (const x of fibonacci()) {
if (x >= 1000) break;
log(x); // 0n 1n 1n 2n 3n 5n 8n 13n 21n 34n 55n 89n 144n 233n 377n 610n 987n
}- Iterating over generator objects roughly desugars to:
const generator = fibonacci();
let x, done;
while ({value: x, done} = generator.next(), !done) {
// destructuring // ^
} // comma operatorExercise:
- Fix the generator function
finiteCounterbelow
- It should yield all elements from
firsttolast- Validate the parameters
firstandlast
- Exceptions won't be thrown until the first
.nextcall- 🏆 Can you already throw at the
finiteCountercall?- Implement the generator function
findWordPositionsbelow
function* finiteCounter(first, last) {
// FIXME
yield first;
yield last;
}
const counter = finiteCounter(7, 9);
counter.next(); // {done: false, value: 7}
counter.next(); // {done: false, value: 8}
counter.next(); // {done: false, value: 9}
counter.next(); // {done: true, value: undefined}function* findWordPositions(text, word) {
// TODO
}
const t = `Wenn hinter Fliegen Fliegen fliegen,
fliegen Fliegen Fliegen nach.`;
findWordPositions(t, "Fliegen").toArray();
[12, 20, 45, 53]Even though ECMAScript includes syntax for class definitions,
ECMAScript objects are not fundamentally class-based
(such as those in C++, Smalltalk, or Java).
- Spot the difference:
const britisch = {
Erdnuss: "peanut",
Keks: "biscuit",
Kremeis: "ice cream",
Pommes: "potato chips",
Schokolade: "chocolate",
};
const amerikanisch = {
Erdnuss: "peanut",
Keks: "biscuit",
Kremeis: "ice cream",
Pommes: "french fries",
Schokolade: "chocolate",
};- Most words are the same
- Isn't this a waste of memory?
amerikanischcan inherit most words frombritisch:
const britisch = {
Erdnuss: "peanut",
Keks: "biscuit",
Kremeis: "ice cream",
Pommes: "potato chips",
Schokolade: "chocolate",
};
const amerikanisch = {
__proto__: britisch, // inheritance
Pommes: "french fries",
};- How does inheritance affect word access?
amerikanisch.Pommes // french fries
amerikanisch.Erdnuss // peanut
amerikanisch.__proto__.Erdnuss // peanut
amerikanisch.Keks = "cookie";
amerikanisch.Keks // cookie
britisch.Keks // biscuit
amerikanisch.Schokolade += "!!!";
amerikanisch.Schokolade // chocolate!!!
britisch.Schokolade // chocolate- 👁️
obj.key- starts at
objand climbs the__proto__chain - until
keyis found, or__proto__isnull
- starts at
- ✍️
obj.key = value- ignores
obj.__proto__completely
- ignores
⚠️ obj.key += value- just syntax sugar for ✍️
obj.key = obj.key + value👁️
- just syntax sugar for ✍️
- The default
__proto__isObject.prototype:
const britisch = {
__proto__: Object.prototype, // default
// ...
};
const amerikanisch = {
__proto__: britisch,
// ...
};Object.prototypecontains half a dozen functions:
Object.prototype = {
hasOwnProperty(key) {
// ...
},
isPrototypeOf(child) {
// ...
},
toString() {
// ...
},
// ...
__proto__: null, // orphan
};for inincludes inherited properties:
for (const deutsch in amerikanisch) {
if (amerikanisch.hasOwnProperty(deutsch)) {
log(`Die Amis haben ein eigenes Wort für ${deutsch}.`);
} else {
log(`Briten und Amis haben dasselbe Wort für ${deutsch}.`);
}
}Nordic.js 2014 • Douglas Crockford - The Better Parts:
classwas the most requested new feature in JavaScript.
All of the requests came from Java programmers who have to program in JavaScript and don't want to learn how to do that.
They wanted something that looks like Java so that they could be more comfortable.
class Account {
constructor(balance) {
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
}
getBalance() {
return this.balance;
}
}
const a = new Account(100);
a.deposit(23);
a.getBalance() // 123a instanceof Account // true... Is Account a class?
a.constructor === Account // true... Is Account a constructor?
typeof Account // "function"... It's a function!
Account(42) // TypeError: class constructors must be invoked with 'new'
new Account(42); // { balance: 42 } a // { balance: 123 }
a.__proto__ // { constructor, deposit, getBalance }
Account.prototype // { constructor, deposit, getBalance }new Account(123)does 2 things:- creates fresh object
{ __proto__: Account.prototype } - runs constructor function
- creates fresh object
- key points to remember:
new T()objects store fieldsT.prototypeobject stores methodsnew T().__proto__ === T.prototypenew T().constructor === T
| prototype | class |
|---|---|
function Account(balance) {
this.balance = balance;
}
// Account.prototype = { constructor: Account };
Account.prototype.deposit = function (amount) {
this.balance += amount;
};
Account.prototype.getBalance = function () {
return this.balance;
};
const a = new Account(100);
a.deposit(23);
a.getBalance() // 123 |
class Account {
constructor(balance) {
this.balance = balance;
}
deposit(amount) {
this.balance += amount;
}
getBalance() {
return this.balance;
}
}
const a = new Account(100);
a.deposit(23);
a.getBalance() // 123 |
classis mostly syntax sugar forprototype- In particular,
classis no more rigid thanprototype:
class Account {
// ...
}
const a = new Account(123);
// add field to object
a.audited = true;
// delete field from object
delete a.balance;
// deactivate method for object
a.deposit = undefined;
// delete method from class
delete Account.prototype.deposit;
// change object's class after creation
a.__proto__ = SavingsAccount.prototype;- 🕷️ With great power comes great responsibility
- 🕵️ Quite useful for testing (mock, spy)
| Function call syntax | this value |
|---|---|
f(x, y, z) |
undefined or global object |
obj.f(x, y, z) |
obj |
new F(x, y, z) |
{ __proto__: F.prototype } |
f.apply(obj, [x, y, z]) |
obj |
f.call(obj, x, y, z) |
obj |
f.bind(obj, x)(y, z) |
obj |
- Since ES2023, arrays have a
toSortedmethod:
const primes = [ 11, 2, 3, 13, 5, 7, 17, 19 ];
const sorted = primes.toSorted((a, b) => a - b);
primes // [ 11, 2, 3, 13, 5, 7, 17, 19 ]
sorted // [ 2, 3, 5, 7, 11, 13, 17, 19 ]- Not all JavaScript environments provide
toSortedyet - In that case, it can be monkey-patched into the prototype:
if (Array.prototype.toSorted === undefined) {
Array.prototype.toSorted = function (compare) {
const copy = [...this]; // spread operator
copy.sort(compare);
return copy;
};
}Exercise:
- Provide a
toReversedmethod on arrays- 🏆 Provide a
toReversedmethod on strings
// Implement your array toReversed method here...
const a = [ "peanuts", "and", "chocolate" ];
const b = a.toReversed(); // ...such that this line of code works, unmodified
a // [ "peanuts", "and", "chocolate" ]
b // [ "chocolate", "and", "peanuts" ]Exercise:
- Move your JavaScript code from
projects/01 password/index.htmlinto its ownindex.jsfile- before:
</body>
<script src="sha1.js"></script>
<script>
// ... your JavaScript code ...
// CUT and PASTE into index.js
</script>
</html>
- after:
</body>
<script src="sha1.js"></script>
<script src="index.js"></script>
</html>.htmlfiles traditionally “import” all required.jsfiles:
<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>.jsfiles traditionally cannot “import” other.jsfiles- Hence the dependencies of a
.jsfile are:- neither obvious,
- nor enforcable
- Implementation details can easily leak into/pollute the global scope
- What if
a.jsandb.jsboth define afunction f()?
- What if
- One
.jsfile per module - Explicit
exports andimports between.jsfiles - Simple but effective
// file trig.js
export const PI = 3.141592653589793;
const RADIANS_PER_DEGREE = PI / 180; // unexported
export function radians(degrees) {
return degrees * RADIANS_PER_DEGREE;
}
export function degrees(radians) {
return radians / RADIANS_PER_DEGREE;
}
export function distance(x, y) {
return Math.sqrt(square(x) + square(y));
}
// unexported
function square(x) {
return x * x;
}// some other file
import { PI, distance as distanceFromOrigin } from './trig.js';
const distance = 1.5;
console.log(PI);
console.log(distanceFromOrigin(3, 4));// some other file
import * as trig from './trig.js';
const distance = 1.5;
console.log(trig.PI);
console.log(trig.distance(3, 4));- Traditionally, all modules are bundled into a single
bundle.jsfile- by module bundlers like Webpack
- requires additional build step
- These days, browsers support modules directly, but:
⚠️ Modules must be served by a (local) web server- “double-click on
index.html” will not work - browse
http://localhost:8080instead
- “double-click on
- Any web server capable of serving files from the file system will do, for example:
| Node | Debian derivatives | |
|---|---|---|
| install (once, from anywhere) | npm install -g http-server |
sudo apt install webfs |
| serve (from project directory) | http-server |
webfsd -F -p 8080 |
Exercise:
- Convert
projects/01 passwordto modules:
- Which function(s) inside
sha1.jsare required by others? Addexport(s) andimport(s)- Which function(s) inside
index.jsare required by others? Addexport(s)- Replace both
<script src="...">with a single<script type="module" src="...">- Start
http-server- Browse
localhost:8080
- Try some passwords; it should no longer work
- Open the developer console (F12)
- You should see
ReferenceError: yourCallbackFunction is not definedmessages- The next section describes how to fix it
- Exported module functions are invisible to HTML tag attributes:
<button onclick="callback()">I have never met this callback in my life</button>
////////////////////
<script type="module" src="index.js"></script>- Import and register the callback inside a module script instead:
<button id="button">Of course I know him</button>
<script type="module">
import { callback } from "./index.js";
document.getElementById("button").onclick = callback;
</script> //////////////////- Before ES2022, fields were always public:
class Account {
constructor(initialBalance) {
this.balance = initialBalance;
}
deposit(amount) {
this.balance += amount;
}
getBalance() {
return this.balance;
}
}
const a = new Account(123);
a.balance = 1000000; // Who wants to be a millionaire?- Since ES2022, the
#prefix marks private fields:
class Account {
#balance; // mandatory declaration
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const a = new Account(123);
a.#balance = 1000000;
// Uncaught SyntaxError: Private field '#balance' ...
// Property '#balance' is not accessible outside class 'Account' ...- recent feature
- unique syntax
- Since ES2015, encapsulation can be achieved with
WeakMaps inside modules - one
WeakMapper property:
// file Account.js
const _Account_balance = new WeakMap(); // unexported, i.e. inaccessible outside the module
export class Account {
constructor(initialBalance) {
_Account_balance.set(this, initialBalance);
}
deposit(amount) {
_Account_balance.set(this, _Account_balance.get(this) + amount);
}
getBalance() {
return _Account_balance.get(this);
}
}// some other file
import { Account } from './Account.js';
const a = new Account(123);
_Account_balance.set(this, 1000000);
// Uncaught ReferenceError: _Account_balance is not defined- one
WeakMapper class:
// file Account.js
const _Account = new WeakMap(); // unexported, i.e. inaccessible outside the module
export class Account {
constructor(initialBalance) {
_Account.set(this, {
balance: initialBalance,
});
}
deposit(amount) {
_Account.get(this).balance += amount;
}
getBalance() {
return _Account.get(this).balance;
}
}- Why
WeakMapinstead ofMap?- A normal
Mapwould keep growing with everynew Account - But a
WeakMapcan shrink during garbage collection
- A normal
- TypeScript transpiles
#toWeakMapfor targets older than ES2022
- Encapsulation has always been achievable with closures:
function createAccount(balance) {
return {
deposit(amount) {
balance += amount;
},
getBalance() {
return balance;
},
};
}
const a = createAccount(123);
a.balance = 1000000; // unrelated property
a.getBalance() // 123
a.balance // 1000000- surprising absence of familiar OOP keywords:
- no
class - no
this - no
new
- no
balanceis not an object property!depositandgetBalanceclose overbalanceinstead
- Lisp programmers love this style
- Other programmers... usually don't
- In practice, programmers either
- just don't care about encapsulation that much, or
- use
privatein TypeScript
class Account {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
// ...
}
const a = new Account(123);
a.balance = 1000000;
// Property 'balance' is private and only accessible within class 'Account'- The property and constructor can be fused together:
class Account {
constructor(private balance: number) {
}
// ...
}
const a = new Account(123);
a.balance = 1000000;
// Property 'balance' is private and only accessible within class 'Account'- This approach to encapsulation is very popular
- Everybody knows
privatefrom some other language
- Everybody knows
- Note that
privateis only checked at compile-time- If you want to shoot yourself in the foot:
const a = new Account(123);
(a as any).balance = 1000000; // Well, if you insist...- When Lisp programmers write TypeScript:
function createAccount(balance: number) {
return {
deposit(amount: number): void {
balance += amount;
},
getBalance(): number {
return balance;
},
};
}
function f(account) {
///////
}- How can they make
ftype-safe? - What should the type of
accountbe? - Well, whatever
createAccountreturns:
type Account = ReturnType<typeof createAccount>;
function f(account: Account) {
////////////////
}