Lazy Evaluation, Thunks, and Generators in JavaScript Functional Programming
This article explores how closures, currying, pure functions, thunks, and ES6 generators enable lazy evaluation in JavaScript, illustrating the concepts with code examples and showing how they can reduce unnecessary computation, control execution order, and even create infinite sequences.
JavaScript, despite being an eager‑evaluation language, can adopt lazy‑evaluation techniques through functional‑programming concepts such as closures, currying, pure functions, and generators. The article begins by explaining that closures inherently delay computation because the inner function captures variables for later use.
Example of a closure that returns a new function:
function addA(A){
return function(B){
return B+A
}
}
let count = addA(7)
console.log(count(8)) // 15Currying is presented as a twin of closures, turning a multi‑argument function into a chain of single‑argument functions, e.g., add(1,2,3) becomes add(1)(2)(3)() . The following snippet demonstrates a curried addition that only performs the sum when the final call receives no arguments:
function addCurry(){
let arr = [...arguments]
let fn = function(){
if(arguments.length === 0){
return arr.reduce((a,b) => a + b) // compute when args are empty
} else {
arr.push(...arguments)
return fn
}
}
return fn
}Pure functions are discussed, and the idea of wrapping impure operations (IO, HTTP, DOM) in a “box” (a thunk) is introduced so that side effects are only realized when the thunk is explicitly evaluated, mirroring the concept of a monad.value() call.
The term “lazy evaluation” (or “惰性求值”) is linked to thunks. A thunk is defined as a parameter‑less closure that carries both the unevaluated expression and its environment, allowing delayed execution.
JavaScript’s native Promise is shown not to be lazy because it starts executing immediately upon creation. In contrast, ES6 generators provide true laziness by yielding control back to the caller until next() is invoked.
function* gen(x){
const y = yield x + 6;
return y;
}
const g = gen(1);
g.next() // { value: 7, done: false }
g.next() // { value: undefined, done: true }Generators can also model asynchronous flows. By yielding a promise or a value, the asynchronous operation only starts when the generator’s next() is called:
function* st1(){
setTimeout(() => {
console.log("done promise")
},1000)
yield("done promise")
}
let aThunk = st1()
console.log(aThunk) // Generator {}
// Execution begins only after aThunk.next()Practical patterns such as sequential API requests, infinite sequences, and alternating generators are demonstrated. For example, an infinite ID generator:
function* idMaker(){
let index = 0;
while(true) yield index++;
}
let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1And an alternating infinite sequence generator:
function* alternate(...seq){
while(true){
for(let item of seq){
yield item;
}
}
}
let alternator = alternate('one','two','three');
for(let i=0;i<6;i++){
console.log(`${alternator.next().value}`);
}The article concludes that generators, together with closures, currying, and composition, embody the same “delayed processing” philosophy, allowing JavaScript developers to write more efficient, controllable, and expressive code.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.