plain old functions
jQuery is nothing more than JavaScript itself. You could easily write greenify
and boldify
as plain functions. This is a good approach because each simple function is easy to write, test, and reuse in any part of your program that needs this specific behaviour -
function greenify (elem) {
elem.style.color = "green"
}
function boldify (elem) {
elem.style.fontWeight = "bold"
}
greenify(document.querySelector("#foo"))
boldify(document.querySelector("#bar"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 1 |
 |
classes and caveats
jQuery uses an object-oriented style which allows for "chaining" of methods. This interface is desirable because it enables us to write programs as long chained expressions, much like we can write sentences. Most people would accomplish this using a class
, which is syntactic sugar for defining a function and adding "methods" to the function's prototype -
class MyTools {
constructor (elem) {
this.elem = elem
}
greenify() {
this.elem.style.color = "green"
return this
}
boldify() {
this.elem.style.fontWeight = "bold"
return this
}
}
function mytools (elem) {
return new MyTools(elem)
}
mytools(document.querySelector("#foo")).greenify()
mytools(document.querySelector("#bar")).boldify()
mytools(document.querySelector("#qux")).boldify().greenify()
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 2 |
 |
But what if we had a lot of methods like greenify
and boldify
in our mytools
class? You can see how the class could become quite large, and repeating return this
for every method is a bit of burden. And what if someone wanted to use just part of your tool library in the future? Using a monolithic class makes it impossible to use dead code elimination, meaning the entire module must be included even if one function is needed.
higher-order functions
Can we build an intuitive interface without such drawbacks? One approach to address these issues is to use higher-order functions. Such a function can take a function as input and/or return a new function as output. You may have used built-in higher-order functions like Array.prototype.map
, Array.prototype.filter
, Array.prototype.reduce
, or others, but we can easily make our own, too -
const effect = f =>
x => { f(x); return x }
function identity (value) {
return { value, map: f => identity(f(value)) }
}
const boldify =
effect(elem => elem.style.fontWeight = "bold")
const greenify =
effect(elem => elem.style.color = "green")
// prefix style; ordinary function calls
greenify(boldify(document.querySelector("#foo")))
// "chaining" style; by use of identity
identity(document.querySelector("#qux"))
.map(boldify)
.map(greenify)
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
html render: example 3 |
 |
modules
Even the "slim" version of jQuery 3.6.0 is over 72 KB, and there's no way to import just a piece of it. This is a consequence of how jQuery choose to implement everything through a single object-oriented interface, the jQuery
function, aliased as $
. However the future of JavaScript lies in its design for modules. They are reusable, flexible, and can be loaded on-the-fly, resulting in highly-optimized and streamlined scripts that are instantly ready to respond to the user.
We've written a few functions above. Let's see how we might start to organize the modules for our own program -
// func.js
function identity (value) {
return { value, map: f => identity(f(value)) }
}
const effect = f =>
x => { f(x); return x }
export { effect, identity }
// css.js
import { effect } from "./func.js"
const boldify =
effect(elem => elem.style.fontWeight = "bold")
const greenify =
effect(elem => elem.style.color = "green")
export { boldify, greenify }
When we use a module, we import
only the parts we need. This gives the compiler (transpiler, "packer", "bundler", etc) an opportunity to remove dead code and dramatically cut down the size of a completed production-ready program -
// main
import { greenify } from "./css.js"
greenify(document.querySelector("#some-elem"))
Even *
imports can be optimized as a dependency resolver can determine only particular functions from the css
-named import are used -
import { identity } from "./func.js"
import * as css from "./css.js"
identity(document.querySelector("#some-elem"))
.map(css.boldify)
.map(css.greenify)
growing your modules
So you want to expand your module to support some other colours, like bluify
, purplify
, and more. You might try something like this -
// css.js
// DON'T DO THIS!
const bluify =
effect(elem => elem.style.color = "green")
const greenify =
effect(elem => elem.style.color = "green")
const purplify =
effect(elem => elem.style.color = "purplify")
// ...
export { bluify, greenify, purplify, ... }
Instead, notice how only the ...
portion of elem.style.color = ...
changes in each function. To avoid repeating ourself over and over, we make functions -
// css.js
import { effect } from "./func.js"
// any color, not just green!
const color = value =>
effect(elem => elem.style.color = value)
// any font-weight, not just bold!
const fontWeight = value =>
effect(elem => elem.style.fontWeight = value)
// other CSS properties...
const fontSize = value =>
effect(elem => elem.style.fontSize = value)
// ...
export { color, fontWeight, fontSize, ... }
Notice how color
, fontWeight
and fontSize
each have a value
parameter. We reduced code duplication and made our functions more useful -
// main.js
import { identity } from "./func.js"
import * as css from "./css.js"
identity(document.querySelector("#bar"))
.map(css.color("limegreen"))
.map(css.fontSize("24pt"))
identity(document.querySelector("#qux"))
.map(css.color("dodgerblue"))
.map(css.fontWeight("900"))
html render: example 4 |
 |
Expand snippet 4 below to verify this result in your own browser -
// func.js
const effect = f =>
x => { f(x); return x }
function identity (value) {
return { value, map: f => identity(f(value)) }
}
// css.js
const fontWeight = value =>
effect(elem => elem.style.fontWeight = value)
const fontSize = value =>
effect(elem => elem.style.fontSize = value)
const color = value =>
effect(elem => elem.style.color = value)
// main.js
identity(document.querySelector("#bar"))
.map(color("limegreen"))
.map(fontSize("24pt"))
identity(document.querySelector("#qux"))
.map(color("dodgerblue"))
.map(fontWeight("900"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
jQuery Lite
Maybe you like jQuery because it accepts a CSS selector and doesn't require you to use identity
or document.querySelector
. jQuery made a choice and so can you. It's your program and you're free to invent any convenience you desire -
// myquery.js
import { identity } from "./func.js"
function $(selector) {
const loop = t => plugin => loop(t.map(plugin))
return loop(identity(document.querySelector(selector)))
}
export { $ }
We can start to see our very own $
evolving! -
// main.js
import { $ } from "./myquery.js"
import { color, fontSize, fontWeight } from "./css.js"
// example expressions
$("#bar")(color("gold"))(fontSize("8pt"))
$("#qux")(fontSize("14pt"))(color("hotpink"))(fontWeight(900))
html render: example 5 |
 |
Expand snippet 5 below to verify the results in your browser -
// func.js
const effect = f =>
x => { f(x); return x }
function identity (value) {
return { value, map: f => identity(f(value)) }
}
// css.js
const fontWeight = value =>
effect(elem => elem.style.fontWeight = value)
const fontSize = value =>
effect(elem => elem.style.fontSize = value)
const color = value =>
effect(elem => elem.style.color = value)
// myquery.js
function $(selector) {
const loop = t => plugin => loop(t.map(plugin))
return loop(identity(document.querySelector(selector)))
}
// main.js
$("#bar")(color("gold"))(fontSize("8pt"))
$("#qux")(fontSize("14pt"))(color("hotpink"))(fontWeight(900))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
Maybe you would like to operate on multiple elements, similar to jQuery? We can do that by using querySelectorAll
and apply plugin
to all elements in an iterable using Array.from
. Note we also can bypass the need for identity
as loop
already closes over its context
-
// myquery.js (updated)
function $(selector) {
const loop = context => plugin => loop(Array.from(context, plugin))
return loop(document.querySelectorAll(selector))
}
// main.js
// target all <p> elements
$("p")(color("orchid"))
// target #foo element
$("#foo")(fontSize("18pt"))(fontWeight("bold"))
// target #bar and #qux elements
$("#bar,#qux")(fontSize("12pt"))
html render: example 6 |
 |
Expand snippet 6 below to verify the results in your browser -
// func.js
const effect = f =>
x => { f(x); return x }
function identity (value) {
return { value, map: f => identity(f(value)) }
}
// css.js
const fontWeight = value =>
effect(elem => elem.style.fontWeight = value)
const fontSize = value =>
effect(elem => elem.style.fontSize = value)
const color = value =>
effect(elem => elem.style.color = value)
// myquery.js
function $(selector) {
const loop = t => plugin => loop(t.map(v => Array.from(v, plugin)))
return loop(identity(document.querySelectorAll(selector)))
}
// main.js
$("p")(color("orchid"))
$("#foo")(fontSize("18pt"))(fontWeight("bold"))
$("#bar,#qux")(fontSize("12pt"))
<p id="foo">hello world</p>
<p id="bar">hello world</p>
<p id="qux">hello world</p>
eagle eyes
Did you notice how color
, fontWeight
, and fontSize
looked almost exactly the same? We can continue to use abstraction to remove additional duplication. By capturing the smallest identifiable essence of a behaviour, we gain the most flexibility, allowing us to easily build sophisticated behaviours from simple ones -
// css.js
import { effect } from "./func.js"
const style = property => value =>
effect(elem => elem.style[property] = value)
const color = style("color")
const fontWeight = style("fontWeight")
const fontSize = style("fontSize")
// ...
export { color, fontWeight, fontSize, style, ... }