diff --git a/README.md b/README.md index 924a02f7..fddd9d65 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,30 @@ This repository was created with the intention of helping developers master their concepts in JavaScript. It is not a requirement, but a guide for future studies. It is based on an article written by Stephen Curtis and you can read it [here](https://medium.com/@stephenthecurt/33-fundamentals-every-javascript-developer-should-know-13dd720a90d1). +## Examples (runnable) + +This repository now includes a small set of concise, runnable examples that illustrate a few of the most important concepts. You can find them in the `examples/` folder. Each file prints a short output that demonstrates the concept. + +- `examples/call-stack.js` — simple nested calls to show the call stack order. +- `examples/primitives-vs-references.js` — shows how primitives are copied by value and objects by reference. +- `examples/closures.js` — demonstrates how closures retain lexical scope. +- `examples/map-filter-reduce.js` — quick usage of `map`, `filter` and `reduce`. +- `examples/event-loop.js` — microtask vs macrotask ordering (Promises vs setTimeout). +- `examples/promises-async-await.js` — basic Promise and `async/await` example. +- `examples/this-call-bind.js` — `this` behavior and `call` / `bind` usage. + +Try one quickly from the project root: + +``` +node examples/event-loop.js +``` + +Or run the default examples script: + +``` +npm run examples +``` + ## Community Feel free to submit a PR by adding a link to your own recaps or reviews. If you want to translate the repo into your native language, please feel free to do so. diff --git a/examples/call-stack.js b/examples/call-stack.js new file mode 100644 index 00000000..e04971f8 --- /dev/null +++ b/examples/call-stack.js @@ -0,0 +1,25 @@ +// Example: Call Stack (LIFO order) +function a() { + console.log('a start'); + b(); + console.log('a end'); +} + +function b() { + console.log('b start'); + c(); + console.log('b end'); +} + +function c() { + console.log('c'); +} + +a(); + +// Expected output: +// a start +// b start +// c +// b end +// a end diff --git a/examples/call-stack.test.js b/examples/call-stack.test.js new file mode 100644 index 00000000..ee3df3e9 --- /dev/null +++ b/examples/call-stack.test.js @@ -0,0 +1,15 @@ +const { expectOutput } = require('./test-utils'); + +function testCallStack() { + return expectOutput(() => { + require('./call-stack'); + }, [ + 'a start', + 'b start', + 'c', + 'b end', + 'a end' + ]); +} + +module.exports = testCallStack; \ No newline at end of file diff --git a/examples/closures.js b/examples/closures.js new file mode 100644 index 00000000..1b05e91d --- /dev/null +++ b/examples/closures.js @@ -0,0 +1,12 @@ +// Example: Closures — a function that remembers its lexical scope +function makeCounter() { + let count = 0; + return function () { + count += 1; + return count; + }; +} + +const counter = makeCounter(); +console.log('closure:', counter()); // 1 +console.log('closure:', counter()); // 2 diff --git a/examples/closures.test.js b/examples/closures.test.js new file mode 100644 index 00000000..29c1b46c --- /dev/null +++ b/examples/closures.test.js @@ -0,0 +1,12 @@ +const { expectOutput } = require('./test-utils'); + +function testClosures() { + return expectOutput(() => { + require('./closures'); + }, [ + 'closure: 1', + 'closure: 2' + ]); +} + +module.exports = testClosures; \ No newline at end of file diff --git a/examples/event-loop.js b/examples/event-loop.js new file mode 100644 index 00000000..e84e06da --- /dev/null +++ b/examples/event-loop.js @@ -0,0 +1,14 @@ +// Example: Event Loop ordering (macrotasks vs microtasks) +console.log('script start'); + +setTimeout(() => console.log('timeout callback (macrotask)'), 0); + +Promise.resolve().then(() => console.log('promise callback (microtask)')); + +console.log('script end'); + +// Expected order: +// script start +// script end +// promise callback (microtask) +// timeout callback (macrotask) diff --git a/examples/event-loop.test.js b/examples/event-loop.test.js new file mode 100644 index 00000000..0cefbe69 --- /dev/null +++ b/examples/event-loop.test.js @@ -0,0 +1,14 @@ +const { expectOutputAsync } = require('./test-utils'); + +function testEventLoop() { + return expectOutputAsync(() => { + require('./event-loop'); + }, [ + 'script start', + 'script end', + 'promise callback (microtask)', + 'timeout callback (macrotask)' + ], 100); +} + +module.exports = testEventLoop; \ No newline at end of file diff --git a/examples/map-filter-reduce.js b/examples/map-filter-reduce.js new file mode 100644 index 00000000..965840e3 --- /dev/null +++ b/examples/map-filter-reduce.js @@ -0,0 +1,10 @@ +// Example: map, filter and reduce +const arr = [1, 2, 3, 4, 5]; + +const squares = arr.map(n => n * n); +const evens = arr.filter(n => n % 2 === 0); +const sum = arr.reduce((acc, n) => acc + n, 0); + +console.log('map -> squares:', squares); // [1,4,9,16,25] +console.log('filter -> evens:', evens); // [2,4] +console.log('reduce -> sum:', sum); // 15 diff --git a/examples/map-filter-reduce.test.js b/examples/map-filter-reduce.test.js new file mode 100644 index 00000000..948e6b6f --- /dev/null +++ b/examples/map-filter-reduce.test.js @@ -0,0 +1,13 @@ +const { expectOutput } = require('./test-utils'); + +function testMapFilterReduce() { + return expectOutput(() => { + require('./map-filter-reduce'); + }, [ + 'map -> squares: [1,4,9,16,25]', + 'filter -> evens: [2,4]', + 'reduce -> sum: 15' + ]); +} + +module.exports = testMapFilterReduce; \ No newline at end of file diff --git a/examples/primitives-vs-references.js b/examples/primitives-vs-references.js new file mode 100644 index 00000000..ece06fec --- /dev/null +++ b/examples/primitives-vs-references.js @@ -0,0 +1,12 @@ +// Example: Primitive vs Reference types +// Primitives are copied by value +let a = 1; +let b = a; +b = 2; +console.log('primitives:', { a, b }); // a stays 1, b is 2 + +// Objects are copied by reference +const obj1 = { x: 1 }; +const obj2 = obj1; +obj2.x = 2; +console.log('references:', { obj1, obj2 }); // both reflect the change diff --git a/examples/primitives-vs-references.test.js b/examples/primitives-vs-references.test.js new file mode 100644 index 00000000..839787ce --- /dev/null +++ b/examples/primitives-vs-references.test.js @@ -0,0 +1,12 @@ +const { expectOutput } = require('./test-utils'); + +function testPrimitivesVsReferences() { + return expectOutput(() => { + require('./primitives-vs-references'); + }, [ + 'primitives: {"a":1,"b":2}', + 'references: {"obj1":{"x":2},"obj2":{"x":2}}' + ]); +} + +module.exports = testPrimitivesVsReferences; \ No newline at end of file diff --git a/examples/promises-async-await.js b/examples/promises-async-await.js new file mode 100644 index 00000000..5f72d1ad --- /dev/null +++ b/examples/promises-async-await.js @@ -0,0 +1,12 @@ +// Example: Promises and async/await +function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function run() { + console.log('before await'); + await wait(50); + console.log('after await'); +} + +run(); diff --git a/examples/promises-async-await.test.js b/examples/promises-async-await.test.js new file mode 100644 index 00000000..2daa1491 --- /dev/null +++ b/examples/promises-async-await.test.js @@ -0,0 +1,12 @@ +const { expectOutputAsync } = require('./test-utils'); + +function testPromisesAsyncAwait() { + return expectOutputAsync(() => { + require('./promises-async-await'); + }, [ + 'before await', + 'after await' + ], 100); +} + +module.exports = testPromisesAsyncAwait; \ No newline at end of file diff --git a/examples/run-tests.js b/examples/run-tests.js new file mode 100644 index 00000000..a3becaff --- /dev/null +++ b/examples/run-tests.js @@ -0,0 +1,31 @@ +// Test runner +const { expectOutput } = require('./test-utils'); + +// Run all example tests +async function runTests() { + const tests = [ + require('./call-stack.test.js'), + require('./primitives-vs-references.test.js'), + require('./closures.test.js'), + require('./map-filter-reduce.test.js'), + require('./event-loop.test.js'), + require('./this-call-bind.test.js'), + require('./promises-async-await.test.js'), + ]; + + console.log('\nRunning tests...\n'); + + let passed = 0; + let failed = 0; + + for (const test of tests) { + const result = await test(); + if (result) passed++; + else failed++; + } + + console.log(`\nResults: ${passed} passed, ${failed} failed\n`); + process.exit(failed > 0 ? 1 : 0); +} + +runTests().catch(console.error); \ No newline at end of file diff --git a/examples/test-utils.js b/examples/test-utils.js new file mode 100644 index 00000000..e0b54742 --- /dev/null +++ b/examples/test-utils.js @@ -0,0 +1,81 @@ +// Simple test harness +function expectOutput(fn, expectedLines) { + const originalLog = console.log; + const actualOutput = []; + + console.log = (...args) => { + // Handle object stringification consistently + const formatted = args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + ).join(' '); + actualOutput.push(formatted); + }; + + try { + fn(); + console.log = originalLog; + + const passed = expectedLines.every((expected, i) => { + if (!actualOutput[i]) return false; + // Normalize JSON strings and arrays for comparison + const normalizedExpected = expected.replace(/[\s{}[\]]/g, ''); + const normalizedActual = actualOutput[i].replace(/[\s{}[\]]/g, ''); + return normalizedActual.includes(normalizedExpected); + }); + + if (passed) { + console.log('\x1b[32m✓\x1b[0m', fn.name || 'Test passed'); + return true; + } else { + console.log('\x1b[31m✗\x1b[0m', fn.name || 'Test failed'); + console.log('Expected:', expectedLines); + console.log('Got:', actualOutput); + return false; + } + } catch (err) { + console.log = originalLog; + console.error('\x1b[31m✗\x1b[0m Test error:', err); + return false; + } +} + +// For async tests +function expectOutputAsync(fn, expectedLines, timeout = 1000) { + return new Promise((resolve) => { + const originalLog = console.log; + const actualOutput = []; + + console.log = (...args) => { + const formatted = args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + ).join(' '); + actualOutput.push(formatted); + }; + + fn(); + + // Wait for all async operations to complete + setTimeout(() => { + console.log = originalLog; + + const passed = expectedLines.every((expected, i) => { + if (!actualOutput[i]) return false; + const normalizedExpected = expected.replace(/[\s{}[\]]/g, ''); + const normalizedActual = actualOutput[i].replace(/[\s{}[\]]/g, ''); + return normalizedActual.includes(normalizedExpected); + }); + + if (passed) { + console.log('\x1b[32m✓\x1b[0m', fn.name || 'Test passed'); + resolve(true); + } else { + console.log('\x1b[31m✗\x1b[0m', fn.name || 'Test failed'); + console.log('Expected:', expectedLines); + console.log('Got:', actualOutput); + resolve(false); + } + }, timeout); + }); +} + +module.exports = { expectOutput, expectOutputAsync }; \ No newline at end of file diff --git a/examples/this-call-bind.js b/examples/this-call-bind.js new file mode 100644 index 00000000..a15472ff --- /dev/null +++ b/examples/this-call-bind.js @@ -0,0 +1,13 @@ +// Example: this, call, apply and bind +const person = { + name: 'Alice', + greet() { + return `Hello ${this.name}`; + } +}; + +const greet = person.greet; +console.log('direct method:', person.greet()); // Hello Alice +console.log('extracted function (this lost):', greet()); // undefined or global +console.log('call ->', greet.call({ name: 'Bob' })); // Hello Bob +console.log('bind ->', greet.bind({ name: 'Carol' })()); // Hello Carol diff --git a/examples/this-call-bind.test.js b/examples/this-call-bind.test.js new file mode 100644 index 00000000..b22304d2 --- /dev/null +++ b/examples/this-call-bind.test.js @@ -0,0 +1,14 @@ +const { expectOutput } = require('./test-utils'); + +function testThisCallBind() { + return expectOutput(() => { + require('./this-call-bind'); + }, [ + 'direct method: Hello Alice', + 'extracted function (this lost): Hello undefined', + 'call -> Hello Bob', + 'bind -> Hello Carol' + ]); +} + +module.exports = testThisCallBind; \ No newline at end of file diff --git a/index.js b/index.js index 3d3b929b..21f1c40c 100644 --- a/index.js +++ b/index.js @@ -5,3 +5,7 @@ Any kind of contribution is welcome. Feel free to contribute. */ + +// NOTE: For short runnable examples that illustrate many of the concepts listed +// in this repo, check the `examples/` folder. Each example is a small Node script +// you can run with `node examples/.js` or `npm run examples` (default). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0c5c6d6d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "33-js-concepts", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "33-js-concepts", + "version": "1.0.0", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index 588446b6..0740b72f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ }, "homepage": "https://github.com/leonardomso/33#readme", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "examples": "node examples/event-loop.js" }, "repository": { "type": "git",