Files
jquery-4-doc/jquery-closure-clojurescript-comparison.md
Adam Jeniski 115e37a750 Add jQuery vs Google Closure vs ClojureScript comparison
Comprehensive comparison document covering all jQuery 4.0.0 examples
with equivalent code in Google Closure Library and ClojureScript.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:20:37 -05:00

53 KiB

jQuery vs Google Closure Library vs ClojureScript Comparison

A comprehensive comparison of jQuery 4.0.0 with Google Closure Library and ClojureScript (with Google Closure), providing equivalent code for every example in the jQuery documentation.


Table of Contents

  1. Overview
  2. Installation
  3. Core Selection
  4. Utility Functions
  5. DOM Manipulation
  6. Event Handling
  7. Traversal
  8. Data Storage
  9. AJAX
  10. Deferred/Promises
  11. Callbacks
  12. Animation
  13. Security Features

Overview

Aspect jQuery 4.0.0 Google Closure Library ClojureScript + Closure
Status Active (Jan 2026) Sunset (Aug 2024) Active
Size ~30kb gzipped Modular (tree-shaken) Compiled & optimized
Philosophy Imperative, chaining Object-oriented, modular Functional, immutable
Browser compat Last 3 versions Cross-browser normalized Via Closure Compiler
Build system Optional Closure Compiler ClojureScript compiler

Key Differences

jQuery: Designed for simplicity and DOM manipulation with a fluent chaining API. Excellent for quick prototyping and small projects.

Google Closure Library: A comprehensive, modular library built for large-scale applications. Designed to work with the Closure Compiler for aggressive optimizations. Note: The library was sunset in August 2024.

ClojureScript: A Clojure dialect that compiles to JavaScript. Automatically includes Google Closure Library and uses the Closure Compiler. Offers functional programming paradigms with immutable data structures.


Installation

jQuery 4.0.0

<!-- CDN -->
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
// ES Module
import $ from 'jquery';

// CommonJS
const $ = require('jquery');

Google Closure Library

<!-- Include base.js -->
<script src="closure-library/closure/goog/base.js"></script>
<script>
  goog.require('goog.dom');
  goog.require('goog.events');
</script>
// Modern ES modules (if using Closure Compiler)
goog.module('myapp.main');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');

ClojureScript

;; deps.edn
{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}}}

;; In your namespace
(ns myapp.core
  (:require [goog.dom :as gdom]
            [goog.events :as gevents])
  (:import [goog.events EventType]))

Core Selection

jQuery: Select by ID

// jQuery
const $header = $('#header');

Google Closure

// Google Closure
const header = goog.dom.getElement('header');
// or shorthand
const header = goog.dom.$('header');

ClojureScript

;; ClojureScript with goog.dom
(require '[goog.dom :as gdom])
(def header (gdom/getElement "header"))

;; Or using native JS interop
(def header (.getElementById js/document "header"))

jQuery: Select by Class

// jQuery
const $buttons = $('.btn');

Google Closure

// Google Closure - single element
const button = goog.dom.getElementByClass('btn');

// Multiple elements
const buttons = goog.dom.getElementsByClass('btn');

ClojureScript

;; ClojureScript
(def button (gdom/getElementByClass "btn"))
(def buttons (gdom/getElementsByClass "btn"))

;; With dommy library
(require '[dommy.core :as dommy :refer-macros [sel sel1]])
(def button (sel1 :.btn))
(def buttons (sel :.btn))

jQuery: Select by Tag

// jQuery
const $paragraphs = $('p');

Google Closure

// Google Closure
const paragraphs = goog.dom.getElementsByTagName('p');

// With class filter
const items = goog.dom.getElementsByTagNameAndClass('div', 'item');

ClojureScript

;; ClojureScript
(def paragraphs (gdom/getElementsByTagName "p"))

;; With dommy
(def paragraphs (sel :p))

jQuery: Select with Context

// jQuery
const $items = $('.item', '#container');

Google Closure

// Google Closure
const container = goog.dom.getElement('container');
const items = goog.dom.getElementsByClass('item', container);

// Using query selector
const items = goog.dom.query('.item', container);

ClojureScript

;; ClojureScript
(def container (gdom/getElement "container"))
(def items (gdom/getElementsByClass "item" container))

;; With dommy
(def items (sel container :.item))

jQuery: Create Elements

// jQuery
const $newDiv = $('<div class="new">Content</div>');

Google Closure

// Google Closure
const newDiv = goog.dom.createDom('div', {'class': 'new'}, 'Content');

// Shorthand
const newDiv = goog.dom.$dom('div', {'class': 'new'}, 'Content');

// Complex nesting
const complex = goog.dom.createDom('div', {'class': 'wrapper'},
  goog.dom.createDom('h1', null, 'Title'),
  goog.dom.createDom('p', null, 'Paragraph')
);

ClojureScript

;; ClojureScript with goog.dom
(def new-div (gdom/createDom "div" #js {:class "new"} "Content"))

;; With hiccup-style libraries (hipo, hiccups)
(require '[hipo.core :as hipo])
(def new-div (hipo/create [:div.new "Content"]))

;; Complex with hiccup
(def complex (hipo/create
  [:div.wrapper
    [:h1 "Title"]
    [:p "Paragraph"]]))

Utility Functions

jQuery: $.extend() - Object Merging

// jQuery - Shallow merge
const defaults = { color: 'blue', size: 'medium' };
const options = { color: 'red' };
const settings = $.extend({}, defaults, options);
// Result: { color: 'red', size: 'medium' }

// jQuery - Deep merge
const obj1 = { a: { b: 1, c: 2 } };
const obj2 = { a: { b: 3 } };
const result = $.extend(true, {}, obj1, obj2);
// Result: { a: { b: 3, c: 2 } }

Google Closure

// Google Closure - goog.object
goog.require('goog.object');

// Shallow merge (modifies target)
const settings = {};
goog.object.extend(settings, defaults, options);

// Clone then merge
const settings = goog.object.clone(defaults);
goog.object.extend(settings, options);

// Note: No built-in deep merge - use goog.object.unsafeClone for deep copy

ClojureScript

;; ClojureScript - native merge (immutable, shallow)
(def defaults {:color "blue" :size "medium"})
(def options {:color "red"})
(def settings (merge defaults options))
;; => {:color "red" :size "medium"}

;; Deep merge
(def obj1 {:a {:b 1 :c 2}})
(def obj2 {:a {:b 3}})
(def result (merge-with merge obj1 obj2))
;; => {:a {:b 3 :c 2}}

;; Or use a deep-merge library
(require '[clojure.walk :refer [postwalk]])

jQuery: $.each() - Iteration

// jQuery - Array iteration
$.each(['a', 'b', 'c'], function(index, value) {
    console.log(`${index}: ${value}`);
});

// jQuery - Object iteration
$.each({ name: 'John', age: 30 }, function(key, value) {
    console.log(`${key}: ${value}`);
});

// jQuery - Return false to break
$.each([1, 2, 3, 4, 5], function(i, num) {
    if (num > 3) return false;
    console.log(num);
});
// Output: 1, 2, 3

Google Closure

// Google Closure - goog.array and goog.object
goog.require('goog.array');
goog.require('goog.object');

// Array iteration
goog.array.forEach(['a', 'b', 'c'], function(value, index) {
    console.log(index + ': ' + value);
});

// Object iteration
goog.object.forEach({ name: 'John', age: 30 }, function(value, key) {
    console.log(key + ': ' + value);
});

// Break with goog.array.some (return true to stop)
goog.array.some([1, 2, 3, 4, 5], function(num) {
    if (num > 3) return true;
    console.log(num);
    return false;
});

ClojureScript

;; ClojureScript - Array iteration
(doseq [[index value] (map-indexed vector ["a" "b" "c"])]
  (println (str index ": " value)))

;; Simpler
(run! println ["a" "b" "c"])

;; Object/map iteration
(doseq [[k v] {:name "John" :age 30}]
  (println (str (name k) ": " v)))

;; Early termination with reduced
(reduce (fn [_ num]
          (if (> num 3)
            (reduced nil)
            (println num)))
        nil [1 2 3 4 5])
;; Output: 1, 2, 3

jQuery: $.map() - Transform Array

// jQuery
const numbers = [1, 2, 3, 4];
const doubled = $.map(numbers, n => n * 2);
// Result: [2, 4, 6, 8]

// Return multiple values
const expanded = $.map([1, 2], n => [n, n * 2]);
// Result: [1, 2, 2, 4]

// Return null to remove
const filtered = $.map([1, 2, 3, 4], n => n > 2 ? n : null);
// Result: [3, 4]

Google Closure

// Google Closure
goog.require('goog.array');

// Map
const doubled = goog.array.map([1, 2, 3, 4], function(n) {
    return n * 2;
});

// Flat map (expand)
const expanded = goog.array.flatten(
    goog.array.map([1, 2], function(n) {
        return [n, n * 2];
    })
);

// Filter then map (no null removal in map)
const filtered = goog.array.filter([1, 2, 3, 4], function(n) {
    return n > 2;
});

ClojureScript

;; ClojureScript - map
(def numbers [1 2 3 4])
(def doubled (map #(* % 2) numbers))
;; => (2 4 6 8)

;; mapcat for expansion (flat map)
(def expanded (mapcat (fn [n] [n (* n 2)]) [1 2]))
;; => (1 2 2 4)

;; filter + map or keep
(def filtered (filter #(> % 2) [1 2 3 4]))
;; => (3 4)

;; Or use keep (removes nils)
(def filtered (keep #(when (> % 2) %) [1 2 3 4]))

jQuery: $.grep() - Filter Array

// jQuery
const numbers = [1, 2, 3, 4, 5, 6];

// Keep elements where callback returns true
const evens = $.grep(numbers, n => n % 2 === 0);
// Result: [2, 4, 6]

// With invert=true, keep elements where callback returns false
const odds = $.grep(numbers, n => n % 2 === 0, true);
// Result: [1, 3, 5]

Google Closure

// Google Closure
goog.require('goog.array');

const numbers = [1, 2, 3, 4, 5, 6];

// Filter
const evens = goog.array.filter(numbers, function(n) {
    return n % 2 === 0;
});

// Inverted (negate the condition)
const odds = goog.array.filter(numbers, function(n) {
    return n % 2 !== 0;
});

ClojureScript

;; ClojureScript
(def numbers [1 2 3 4 5 6])

;; Filter
(def evens (filter even? numbers))
;; => (2 4 6)

;; Inverted with remove or complement
(def odds (remove even? numbers))
;; => (1 3 5)

;; Or
(def odds (filter odd? numbers))
(def odds (filter (complement even?) numbers))

jQuery: $.merge() - Merge Arrays

// jQuery
const arr1 = [1, 2];
const arr2 = [3, 4];
$.merge(arr1, arr2);
// arr1 is now [1, 2, 3, 4]

Google Closure

// Google Closure
goog.require('goog.array');

const arr1 = [1, 2];
const arr2 = [3, 4];

// Extend modifies first array
goog.array.extend(arr1, arr2);
// arr1 is now [1, 2, 3, 4]

// Or create new array
const merged = goog.array.concat(arr1, arr2);

ClojureScript

;; ClojureScript - concat (immutable, creates new sequence)
(def arr1 [1 2])
(def arr2 [3 4])
(def merged (concat arr1 arr2))
;; => (1 2 3 4)

;; As vector
(def merged (into arr1 arr2))
;; => [1 2 3 4]

jQuery: $.inArray() - Find Index

// jQuery
const arr = ['a', 'b', 'c', 'd'];

$.inArray('b', arr);     // Returns: 1
$.inArray('x', arr);     // Returns: -1
$.inArray('b', arr, 2);  // Returns: -1 (starts search at index 2)

Google Closure

// Google Closure
goog.require('goog.array');

const arr = ['a', 'b', 'c', 'd'];

goog.array.indexOf(arr, 'b');        // Returns: 1
goog.array.indexOf(arr, 'x');        // Returns: -1
goog.array.indexOf(arr, 'b', 2);     // Returns: -1

ClojureScript

;; ClojureScript
(def arr ["a" "b" "c" "d"])

;; Using .indexOf on vector
(.indexOf arr "b")  ;; => 1
(.indexOf arr "x")  ;; => -1

;; Or idiomatic ClojureScript
(defn index-of [coll item]
  (first (keep-indexed #(when (= %2 item) %1) coll)))

(index-of arr "b")  ;; => 1
(index-of arr "x")  ;; => nil

jQuery: .isPlainObject() / .isEmptyObject()

// jQuery
$.isPlainObject({});              // true
$.isPlainObject({ a: 1 });        // true
$.isPlainObject([]);              // false
$.isPlainObject(new Date());      // false

$.isEmptyObject({});              // true
$.isEmptyObject({ a: 1 });        // false

Google Closure

// Google Closure
goog.require('goog.object');

goog.isObject({});              // true (but also true for arrays!)

// Check plain object more carefully
function isPlainObject(obj) {
    return goog.isObject(obj) &&
           !goog.isArray(obj) &&
           Object.getPrototypeOf(obj) === Object.prototype;
}

// Empty object check
goog.object.isEmpty({});          // true
goog.object.isEmpty({ a: 1 });    // false

ClojureScript

;; ClojureScript
(map? {})              ;; true
(map? {:a 1})          ;; true
(map? [])              ;; false

;; Empty check
(empty? {})            ;; true
(empty? {:a 1})        ;; false

;; For JS objects specifically
(object? #js {})       ;; true
(goog.object/isEmpty #js {})  ;; true

Removed jQuery Functions (Use Native)

// jQuery 3.x (deprecated) -> Native JavaScript

// $.isArray() -> Array.isArray()
if (Array.isArray(myVar)) { /* ... */ }

// $.isFunction() -> typeof
if (typeof callback === 'function') { /* ... */ }

// $.trim() -> String.prototype.trim()
var trimmed = userInput.trim();

// $.parseJSON() -> JSON.parse()
var data = JSON.parse(jsonString);

// $.now() -> Date.now()
var timestamp = Date.now();

Google Closure Equivalents

// Google Closure
goog.isArray(myVar);
goog.isFunction(callback);
goog.string.trim(userInput);
// JSON.parse is native, or use goog.json.parse for older browsers
var timestamp = goog.now();

ClojureScript Equivalents

;; ClojureScript
(array? my-var)
(fn? callback)
(clojure.string/trim user-input)
(js/JSON.parse json-string)
(.now js/Date)

DOM Manipulation

jQuery: DOM Ready

// jQuery - Shorthand (recommended)
$(function() {
    console.log('DOM ready');
});

// jQuery - Explicit ready
$(document).ready(function() {
    console.log('DOM ready');
});

// jQuery - Arrow function
$(() => {
    console.log('DOM ready');
});

Google Closure

// Google Closure
goog.require('goog.dom');
goog.require('goog.events');

// Using DOMContentLoaded
goog.events.listen(document, 'DOMContentLoaded', function() {
    console.log('DOM ready');
});

// Or check if already ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
    console.log('DOM already ready');
} else {
    goog.events.listen(document, 'DOMContentLoaded', function() {
        console.log('DOM ready');
    });
}

ClojureScript

;; ClojureScript
(require '[goog.events :as gevents])

;; Using goog.events
(gevents/listen js/document "DOMContentLoaded"
  (fn [_] (println "DOM ready")))

;; Or with native JS
(.addEventListener js/document "DOMContentLoaded"
  (fn [_] (println "DOM ready")))

;; Check if already loaded
(if (or (= "complete" (.-readyState js/document))
        (= "interactive" (.-readyState js/document)))
  (println "Already ready")
  (gevents/listen js/document "DOMContentLoaded"
    (fn [_] (println "DOM ready"))))

jQuery: Append/Remove Elements

// jQuery - Append
$('#container').append('<p>New paragraph</p>');

// jQuery - Prepend
$('#container').prepend('<p>First paragraph</p>');

// jQuery - Remove
$('#element').remove();

// jQuery - Empty (remove children)
$('#container').empty();

Google Closure

// Google Closure
goog.require('goog.dom');

const container = goog.dom.getElement('container');

// Append
const newP = goog.dom.createDom('p', null, 'New paragraph');
goog.dom.appendChild(container, newP);

// Prepend
const firstP = goog.dom.createDom('p', null, 'First paragraph');
goog.dom.insertChildAt(container, firstP, 0);

// Remove
const element = goog.dom.getElement('element');
goog.dom.removeNode(element);

// Empty (remove children)
goog.dom.removeChildren(container);

ClojureScript

;; ClojureScript with goog.dom
(require '[goog.dom :as gdom])

(def container (gdom/getElement "container"))

;; Append
(let [new-p (gdom/createDom "p" nil "New paragraph")]
  (gdom/appendChild container new-p))

;; Prepend
(let [first-p (gdom/createDom "p" nil "First paragraph")]
  (gdom/insertChildAt container first-p 0))

;; Remove
(gdom/removeNode (gdom/getElement "element"))

;; Empty
(gdom/removeChildren container)

;; With dommy library
(require '[dommy.core :as dommy :refer-macros [sel1]])
(dommy/append! (sel1 :#container) some-element)
(dommy/remove! (sel1 :#element))

jQuery: Class Manipulation

// jQuery
$('#element').addClass('active');
$('#element').removeClass('inactive');
$('#element').toggleClass('visible');
$('#element').hasClass('active');  // returns boolean

Google Closure

// Google Closure
goog.require('goog.dom.classlist');

const element = goog.dom.getElement('element');

goog.dom.classlist.add(element, 'active');
goog.dom.classlist.remove(element, 'inactive');
goog.dom.classlist.toggle(element, 'visible');
goog.dom.classlist.contains(element, 'active');  // returns boolean

// Multiple classes
goog.dom.classlist.addAll(element, ['class1', 'class2']);
goog.dom.classlist.removeAll(element, ['class1', 'class2']);

// Swap classes
goog.dom.classlist.swap(element, 'old-class', 'new-class');

ClojureScript

;; ClojureScript with goog.dom.classlist
(require '[goog.dom.classlist :as classlist])

(def element (gdom/getElement "element"))

(classlist/add element "active")
(classlist/remove element "inactive")
(classlist/toggle element "visible")
(classlist/contains element "active")  ;; returns boolean

;; With dommy
(require '[dommy.core :as dommy :refer-macros [sel1]])
(dommy/add-class! (sel1 :#element) :active)
(dommy/remove-class! (sel1 :#element) :inactive)
(dommy/toggle-class! (sel1 :#element) :visible)
(dommy/has-class? (sel1 :#element) :active)

jQuery: CSS/Style Manipulation

// jQuery - Get style
const color = $('#element').css('color');

// jQuery - Set single style
$('#element').css('color', 'red');

// jQuery - Set multiple styles
$('#element').css({
    color: 'red',
    fontSize: '16px',
    backgroundColor: '#fff'
});

Google Closure

// Google Closure
goog.require('goog.style');

const element = goog.dom.getElement('element');

// Get style
const color = goog.style.getStyle(element, 'color');

// Set single style
goog.style.setStyle(element, 'color', 'red');

// Set multiple styles
goog.style.setStyle(element, {
    'color': 'red',
    'font-size': '16px',
    'background-color': '#fff'
});

// Other style utilities
goog.style.setOpacity(element, 0.5);
goog.style.setSize(element, 100, 200);  // width, height
goog.style.setPosition(element, 10, 20);  // x, y
goog.style.showElement(element, true);  // show/hide

ClojureScript

;; ClojureScript with goog.style
(require '[goog.style :as gstyle])

(def element (gdom/getElement "element"))

;; Get style
(def color (gstyle/getStyle element "color"))

;; Set single style
(gstyle/setStyle element "color" "red")

;; Set multiple styles
(gstyle/setStyle element #js {:color "red"
                               :font-size "16px"
                               :background-color "#fff"})

;; With dommy
(dommy/set-style! (sel1 :#element) :color "red")
(dommy/set-style! (sel1 :#element) :font-size "16px")

jQuery: Text and HTML Content

// jQuery - Get/Set text
const text = $('#element').text();
$('#element').text('New text content');

// jQuery - Get/Set HTML
const html = $('#element').html();
$('#element').html('<strong>Bold text</strong>');

Google Closure

// Google Closure
goog.require('goog.dom');

const element = goog.dom.getElement('element');

// Get/Set text
const text = goog.dom.getTextContent(element);
goog.dom.setTextContent(element, 'New text content');

// Get/Set HTML (be careful with XSS)
const html = element.innerHTML;
element.innerHTML = '<strong>Bold text</strong>';

// Safe HTML setting (with Trusted Types support)
goog.require('goog.dom.safe');
goog.dom.safe.setInnerHtml(element, trustedHtml);

ClojureScript

;; ClojureScript with goog.dom
(def element (gdom/getElement "element"))

;; Get/Set text
(def text (gdom/getTextContent element))
(gdom/setTextContent element "New text content")

;; Get/Set HTML
(def html (.-innerHTML element))
(set! (.-innerHTML element) "<strong>Bold text</strong>")

;; With dommy
(dommy/text (sel1 :#element))
(dommy/set-text! (sel1 :#element) "New text")
(dommy/html (sel1 :#element))
(dommy/set-html! (sel1 :#element) "<strong>Bold</strong>")

jQuery: Attributes

// jQuery - Get attribute
const href = $('a').attr('href');

// jQuery - Set attribute
$('a').attr('href', 'https://example.com');

// jQuery - Set multiple attributes
$('#img').attr({
    src: 'image.jpg',
    alt: 'Description'
});

// jQuery - Remove attribute
$('#input').removeAttr('disabled');

Google Closure

// Google Closure
const element = goog.dom.getElement('link');

// Get attribute
const href = element.getAttribute('href');

// Set attribute
element.setAttribute('href', 'https://example.com');

// Set multiple with goog.dom
goog.dom.setProperties(element, {
    'href': 'https://example.com',
    'target': '_blank'
});

// Remove attribute
element.removeAttribute('disabled');

ClojureScript

;; ClojureScript
(def element (gdom/getElement "link"))

;; Get attribute
(def href (.getAttribute element "href"))

;; Set attribute
(.setAttribute element "href" "https://example.com")

;; Set multiple
(gdom/setProperties element #js {:href "https://example.com"
                                  :target "_blank"})

;; Remove
(.removeAttribute element "disabled")

;; With dommy
(dommy/attr (sel1 :a) :href)
(dommy/set-attr! (sel1 :a) :href "https://example.com")
(dommy/remove-attr! (sel1 :#input) :disabled)

Event Handling

jQuery: Basic Event Binding

// jQuery - on() method
$('#button').on('click', function(e) {
    console.log('Button clicked');
});

// jQuery - Shorthand
$('#button').click(function() {
    console.log('Button clicked');
});

// jQuery - Multiple events
$('#input').on('focus blur', function(e) {
    console.log('Focus state changed');
});

Google Closure

// Google Closure
goog.require('goog.events');
goog.require('goog.events.EventType');

const button = goog.dom.getElement('button');

// Basic listener
goog.events.listen(button, goog.events.EventType.CLICK, function(e) {
    console.log('Button clicked');
});

// Multiple events
const input = goog.dom.getElement('input');
goog.events.listen(input, [goog.events.EventType.FOCUS, goog.events.EventType.BLUR],
    function(e) {
        console.log('Focus state changed');
    }
);

ClojureScript

;; ClojureScript with goog.events
(require '[goog.events :as gevents])
(import '[goog.events EventType])

(def button (gdom/getElement "button"))

;; Basic listener
(gevents/listen button EventType.CLICK
  (fn [e] (println "Button clicked")))

;; Multiple events
(doseq [event-type [EventType.FOCUS EventType.BLUR]]
  (gevents/listen input event-type
    (fn [e] (println "Focus state changed"))))

;; With dommy
(dommy/listen! (sel1 :#button) :click
  (fn [e] (println "Button clicked")))

jQuery: Event Delegation

// jQuery - Event delegation
$('#container').on('click', '.item', function(e) {
    console.log('Item clicked:', $(this).text());
});

Google Closure

// Google Closure - Manual delegation
goog.require('goog.events');
goog.require('goog.dom');

const container = goog.dom.getElement('container');

goog.events.listen(container, goog.events.EventType.CLICK, function(e) {
    // Check if target matches selector
    const target = e.target;
    if (goog.dom.classlist.contains(target, 'item')) {
        console.log('Item clicked:', goog.dom.getTextContent(target));
    }

    // Or find closest matching ancestor
    const item = goog.dom.getAncestorByClass(target, 'item');
    if (item) {
        console.log('Item clicked:', goog.dom.getTextContent(item));
    }
});

ClojureScript

;; ClojureScript - Manual delegation
(gevents/listen (gdom/getElement "container") EventType.CLICK
  (fn [e]
    (let [target (.-target e)]
      (when (classlist/contains target "item")
        (println "Item clicked:" (gdom/getTextContent target))))))

;; With dommy - built-in delegation
(dommy/listen! [(sel1 :#container) :.item] :click
  (fn [e]
    (println "Item clicked:" (dommy/text (.-selectedTarget e)))))

jQuery: One-time Events

// jQuery - one() for single execution
$('#button').one('click', function() {
    console.log('This only fires once');
});

Google Closure

// Google Closure
goog.events.listenOnce(button, goog.events.EventType.CLICK, function(e) {
    console.log('This only fires once');
});

ClojureScript

;; ClojureScript
(gevents/listenOnce button EventType.CLICK
  (fn [e] (println "This only fires once")))

;; With dommy
(dommy/listen-once! (sel1 :#button) :click
  (fn [e] (println "This only fires once")))

jQuery: Removing Event Handlers

// jQuery - Named handler for removal
function clickHandler(e) {
    console.log('Clicked');
}

$('#button').on('click', clickHandler);
$('#button').off('click', clickHandler);

// Remove all click handlers
$('#button').off('click');

// Remove all handlers
$('#button').off();

Google Closure

// Google Closure
function clickHandler(e) {
    console.log('Clicked');
}

// Listen returns a key for removal
const key = goog.events.listen(button, goog.events.EventType.CLICK, clickHandler);

// Remove specific handler
goog.events.unlistenByKey(key);

// Or remove by parameters
goog.events.unlisten(button, goog.events.EventType.CLICK, clickHandler);

// Remove all handlers from element
goog.events.removeAll(button);

// Remove all handlers of specific type
goog.events.removeAll(button, goog.events.EventType.CLICK);

ClojureScript

;; ClojureScript
(defn click-handler [e]
  (println "Clicked"))

;; Store the key
(def key (gevents/listen button EventType.CLICK click-handler))

;; Remove by key
(gevents/unlistenByKey key)

;; Remove by parameters
(gevents/unlisten button EventType.CLICK click-handler)

;; Remove all
(gevents/removeAll button)

;; With dommy
(dommy/listen! (sel1 :#button) :click click-handler)
(dommy/unlisten! (sel1 :#button) :click click-handler)

jQuery: Focus Event Order (W3C in 4.0)

// jQuery 4.0 order (W3C compliant):
// blur → focusout → focus → focusin

const $input = $('#myInput');

$input.on('blur', () => console.log('1. blur'));
$input.on('focusout', () => console.log('2. focusout'));
$input.on('focus', () => console.log('3. focus'));
$input.on('focusin', () => console.log('4. focusin'));

Google Closure

// Google Closure - Uses native event order
const input = goog.dom.getElement('myInput');

goog.events.listen(input, goog.events.EventType.BLUR, function() {
    console.log('blur');
});

goog.events.listen(input, goog.events.EventType.FOCUS, function() {
    console.log('focus');
});

// focusin/focusout via FocusHandler for cross-browser support
goog.require('goog.events.FocusHandler');
const focusHandler = new goog.events.FocusHandler(input);
goog.events.listen(focusHandler, goog.events.FocusHandler.EventType.FOCUSIN, function() {
    console.log('focusin');
});

ClojureScript

;; ClojureScript
(gevents/listen input EventType.BLUR #(println "blur"))
(gevents/listen input EventType.FOCUS #(println "focus"))

;; For focusin/focusout
(import '[goog.events FocusHandler])
(def focus-handler (FocusHandler. input))
(gevents/listen focus-handler (.-FOCUSIN FocusHandler.EventType) #(println "focusin"))

Traversal

jQuery: .find()

// jQuery - Find all paragraphs inside #content
$('#content').find('p').addClass('styled');

// Chained traversal
$('#nav')
    .find('li')
    .find('a')
    .addClass('nav-link');

Google Closure

// Google Closure
const content = goog.dom.getElement('content');
const paragraphs = goog.dom.getElementsByTagName('p', content);
goog.array.forEach(paragraphs, function(p) {
    goog.dom.classlist.add(p, 'styled');
});

// Or with query
const links = goog.dom.query('#nav li a');
goog.array.forEach(links, function(a) {
    goog.dom.classlist.add(a, 'nav-link');
});

ClojureScript

;; ClojureScript
(let [content (gdom/getElement "content")
      paragraphs (gdom/getElementsByTagName "p" content)]
  (doseq [p (array-seq paragraphs)]
    (classlist/add p "styled")))

;; With dommy
(doseq [p (sel (sel1 :#content) :p)]
  (dommy/add-class! p :styled))

jQuery: .filter()

// jQuery - Filter by selector
$('li').filter('.active').css('font-weight', 'bold');

// Filter by function
$('input').filter(function() {
    return $(this).val().length > 0;
}).addClass('has-value');

Google Closure

// Google Closure
goog.require('goog.array');

// Filter by class
const items = goog.dom.getElementsByTagName('li');
const activeItems = goog.array.filter(items, function(li) {
    return goog.dom.classlist.contains(li, 'active');
});
goog.array.forEach(activeItems, function(li) {
    goog.style.setStyle(li, 'font-weight', 'bold');
});

// Filter by function
const inputs = goog.dom.getElementsByTagName('input');
const filledInputs = goog.array.filter(inputs, function(input) {
    return input.value.length > 0;
});

ClojureScript

;; ClojureScript
(->> (array-seq (gdom/getElementsByTagName "li"))
     (filter #(classlist/contains % "active"))
     (run! #(gstyle/setStyle % "font-weight" "bold")))

;; Filter inputs with values
(->> (array-seq (gdom/getElementsByTagName "input"))
     (filter #(> (count (.-value %)) 0))
     (run! #(classlist/add % "has-value")))

;; With dommy
(->> (sel :li)
     (filter #(dommy/has-class? % :active))
     (run! #(dommy/set-style! % :font-weight "bold")))

jQuery: .closest()

// jQuery - Find parent form
$('input').on('blur', function() {
    const $form = $(this).closest('form');
    validateForm($form);
});

Google Closure

// Google Closure
goog.require('goog.dom');

goog.events.listen(input, 'blur', function(e) {
    const form = goog.dom.getAncestorByTagNameAndClass(e.target, 'form');
    validateForm(form);
});

// By class
const container = goog.dom.getAncestorByClass(element, 'container');

ClojureScript

;; ClojureScript
(gevents/listen input "blur"
  (fn [e]
    (let [form (gdom/getAncestorByTagNameAndClass (.-target e) "form")]
      (validate-form form))))

;; By class
(def container (gdom/getAncestorByClass element "container"))

jQuery: .has()

// jQuery - Only select divs that contain a span
$('div').has('span').addClass('has-span');

Google Closure

// Google Closure
const divs = goog.dom.getElementsByTagName('div');
goog.array.forEach(divs, function(div) {
    if (goog.dom.getElementsByTagName('span', div).length > 0) {
        goog.dom.classlist.add(div, 'has-span');
    }
});

ClojureScript

;; ClojureScript
(->> (array-seq (gdom/getElementsByTagName "div"))
     (filter #(pos? (.-length (gdom/getElementsByTagName "span" %))))
     (run! #(classlist/add % "has-span")))

jQuery: .index()

// jQuery
$('li.active').index();  // Position among siblings

const $items = $('li');
$items.index($('.active'));  // Position in collection

Google Closure

// Google Closure
goog.require('goog.array');

// Among siblings
const active = goog.dom.getElementByClass('active');
const siblings = goog.dom.getChildren(active.parentNode);
const index = goog.array.indexOf(siblings, active);

// In collection
const items = goog.dom.getElementsByTagName('li');
const activeItem = goog.dom.getElementByClass('active');
const indexInCollection = goog.array.indexOf(items, activeItem);

ClojureScript

;; ClojureScript
(let [active (gdom/getElementByClass "active")
      siblings (gdom/getChildren (.-parentNode active))]
  (.indexOf (array-seq siblings) active))

jQuery: .add()

// jQuery - Combine selections
$('p')
    .add('span')
    .add('div.highlight')
    .addClass('styled');

Google Closure

// Google Closure - Combine into array
const combined = goog.array.concat(
    goog.array.toArray(goog.dom.getElementsByTagName('p')),
    goog.array.toArray(goog.dom.getElementsByTagName('span')),
    goog.array.toArray(goog.dom.getElementsByClass('highlight'))
);

goog.array.forEach(combined, function(el) {
    goog.dom.classlist.add(el, 'styled');
});

ClojureScript

;; ClojureScript
(let [combined (concat
                (array-seq (gdom/getElementsByTagName "p"))
                (array-seq (gdom/getElementsByTagName "span"))
                (array-seq (gdom/getElementsByClass "highlight")))]
  (run! #(classlist/add % "styled") combined))

Data Storage

jQuery: .data()

// jQuery - Set data
$('#element').data('userId', 123);
$('#element').data('config', { theme: 'dark', lang: 'en' });

// Get data
const userId = $('#element').data('userId');  // 123
const config = $('#element').data('config');

// Get all data
const allData = $('#element').data();

// HTML5 data attributes are auto-loaded
// <div id="user" data-user-id="456" data-role="admin">
$('#user').data('userId');  // 456
$('#user').data('role');    // 'admin'

// Remove data
$('#element').removeData('temp');

Google Closure

// Google Closure - Using dataset API (HTML5)
const element = goog.dom.getElement('element');

// Set data (via dataset)
element.dataset.userId = '123';
element.dataset.config = JSON.stringify({ theme: 'dark', lang: 'en' });

// Get data
const userId = element.dataset.userId;
const config = JSON.parse(element.dataset.config);

// Or use a WeakMap for complex data
const dataStore = new WeakMap();

function setData(element, key, value) {
    let data = dataStore.get(element) || {};
    data[key] = value;
    dataStore.set(element, data);
}

function getData(element, key) {
    const data = dataStore.get(element) || {};
    return key ? data[key] : data;
}

ClojureScript

;; ClojureScript - Using dataset
(def element (gdom/getElement "element"))

;; Set via dataset
(set! (.. element -dataset -userId) "123")

;; Get
(.. element -dataset -userId)

;; For complex data, use atoms or state management
(def data-store (atom {}))

(defn set-data! [element key value]
  (swap! data-store assoc-in [element key] value))

(defn get-data [element key]
  (get-in @data-store [element key]))

;; Or use a library like reagent for reactive state

AJAX

jQuery: $.ajax() and Shortcuts

// jQuery - Full AJAX
$.ajax({
    url: '/api/data',
    method: 'GET',
    dataType: 'json'
})
.done(function(data) {
    console.log('Success:', data);
})
.fail(function(jqXHR, textStatus, error) {
    console.error('Error:', error);
})
.always(function() {
    console.log('Complete');
});

// jQuery - Shorthand GET
$.get('/api/data', function(data) {
    console.log(data);
});

// jQuery - Shorthand POST
$.post('/api/data', { name: 'John' }, function(response) {
    console.log(response);
});

// jQuery - Get JSON
$.getJSON('/api/data.json', function(data) {
    console.log(data);
});

// jQuery - Get Script
$.getScript('https://example.com/script.js')
    .done(function() {
        console.log('Script loaded');
    });

Google Closure

// Google Closure
goog.require('goog.net.XhrIo');
goog.require('goog.events');

// GET request
goog.net.XhrIo.send('/api/data', function(e) {
    const xhr = e.target;
    if (xhr.isSuccess()) {
        const data = xhr.getResponseJson();
        console.log('Success:', data);
    } else {
        console.error('Error:', xhr.getStatus());
    }
    console.log('Complete');
});

// POST request
goog.net.XhrIo.send('/api/data', function(e) {
    const data = e.target.getResponseJson();
    console.log(data);
}, 'POST', 'name=John', {'Content-Type': 'application/x-www-form-urlencoded'});

// With instance for more control
const xhr = new goog.net.XhrIo();
goog.events.listen(xhr, goog.net.EventType.COMPLETE, function(e) {
    if (xhr.isSuccess()) {
        console.log(xhr.getResponseJson());
    }
});
goog.events.listen(xhr, goog.net.EventType.ERROR, function(e) {
    console.error('Request failed');
});
xhr.send('/api/data', 'GET');

// JSON with headers
const headers = {'Content-Type': 'application/json'};
goog.net.XhrIo.send('/api/data', callback, 'POST',
    JSON.stringify({name: 'John'}), headers);

ClojureScript

;; ClojureScript with goog.net.XhrIo
(require '[goog.net.XhrIo :as xhr])

;; GET request
(xhr/send "/api/data"
  (fn [e]
    (let [target (.-target e)]
      (if (.isSuccess target)
        (println "Success:" (.getResponseJson target))
        (println "Error:" (.getStatus target))))))

;; POST request
(xhr/send "/api/data"
  (fn [e] (println (.getResponseJson (.-target e))))
  "POST"
  (js/JSON.stringify #js {:name "John"})
  #js {"Content-Type" "application/json"})

;; With cljs-ajax library (recommended)
(require '[ajax.core :refer [GET POST]])

(GET "/api/data"
  {:handler (fn [response] (println "Success:" response))
   :error-handler (fn [error] (println "Error:" error))})

(POST "/api/data"
  {:params {:name "John"}
   :format :json
   :response-format :json
   :handler (fn [response] (println response))})

;; With cljs-http and core.async
(require '[cljs-http.client :as http]
         '[cljs.core.async :refer [<!]])
(require-macros '[cljs.core.async.macros :refer [go]])

(go
  (let [response (<! (http/get "/api/data"))]
    (println (:body response))))

jQuery: $.when() - Parallel Requests

// jQuery
function fetchUser() {
    return $.ajax('/api/user');
}

function fetchPosts() {
    return $.ajax('/api/posts');
}

$.when(fetchUser(), fetchPosts())
    .done(function(userData, postsData) {
        console.log('User:', userData[0]);
        console.log('Posts:', postsData[0]);
    })
    .fail(function() {
        console.error('One or more requests failed');
    });

Google Closure

// Google Closure - Using goog.Promise.all (or native Promise.all)
goog.require('goog.Promise');
goog.require('goog.net.XhrIo');

function fetchUser() {
    return new goog.Promise(function(resolve, reject) {
        goog.net.XhrIo.send('/api/user', function(e) {
            if (e.target.isSuccess()) {
                resolve(e.target.getResponseJson());
            } else {
                reject(e.target.getStatus());
            }
        });
    });
}

function fetchPosts() {
    return new goog.Promise(function(resolve, reject) {
        goog.net.XhrIo.send('/api/posts', function(e) {
            if (e.target.isSuccess()) {
                resolve(e.target.getResponseJson());
            } else {
                reject(e.target.getStatus());
            }
        });
    });
}

goog.Promise.all([fetchUser(), fetchPosts()])
    .then(function(results) {
        console.log('User:', results[0]);
        console.log('Posts:', results[1]);
    })
    .thenCatch(function(error) {
        console.error('Failed:', error);
    });

ClojureScript

;; ClojureScript with core.async
(require '[cljs-http.client :as http]
         '[cljs.core.async :refer [<! go]])

(go
  (let [user-chan (http/get "/api/user")
        posts-chan (http/get "/api/posts")
        user (<! user-chan)
        posts (<! posts-chan)]
    (println "User:" (:body user))
    (println "Posts:" (:body posts))))

;; Or with promesa library
(require '[promesa.core :as p])

(-> (p/all [(fetch-user) (fetch-posts)])
    (p/then (fn [[user posts]]
              (println "User:" user)
              (println "Posts:" posts)))
    (p/catch (fn [error]
               (println "Failed:" error))))

Deferred/Promises

jQuery: $.Deferred()

// jQuery
function asyncOperation() {
    const deferred = $.Deferred();

    setTimeout(function() {
        const success = Math.random() > 0.5;

        if (success) {
            deferred.resolve('Operation succeeded');
        } else {
            deferred.reject('Operation failed');
        }
    }, 1000);

    return deferred.promise();
}

asyncOperation()
    .done(function(result) {
        console.log(result);
    })
    .fail(function(error) {
        console.error(error);
    })
    .always(function() {
        console.log('Complete');
    });

Google Closure

// Google Closure - goog.Promise (or native Promise)
goog.require('goog.Promise');

function asyncOperation() {
    return new goog.Promise(function(resolve, reject) {
        setTimeout(function() {
            const success = Math.random() > 0.5;

            if (success) {
                resolve('Operation succeeded');
            } else {
                reject('Operation failed');
            }
        }, 1000);
    });
}

asyncOperation()
    .then(function(result) {
        console.log(result);
    })
    .thenCatch(function(error) {
        console.error(error);
    })
    .thenAlways(function() {
        console.log('Complete');
    });

// Or with native Promise
function asyncOperation() {
    return new Promise(function(resolve, reject) {
        // ... same as above
    });
}

ClojureScript

;; ClojureScript with core.async channels
(require '[cljs.core.async :refer [<! >! chan go timeout]])

(defn async-operation []
  (let [c (chan)]
    (go
      (<! (timeout 1000))
      (if (> (rand) 0.5)
        (>! c {:status :success :result "Operation succeeded"})
        (>! c {:status :error :error "Operation failed"})))
    c))

(go
  (let [{:keys [status result error]} (<! (async-operation))]
    (case status
      :success (println result)
      :error (println "Error:" error))
    (println "Complete")))

;; With promesa library (Promise-based)
(require '[promesa.core :as p])

(defn async-operation []
  (p/create
    (fn [resolve reject]
      (js/setTimeout
        (fn []
          (if (> (rand) 0.5)
            (resolve "Operation succeeded")
            (reject "Operation failed")))
        1000))))

(-> (async-operation)
    (p/then #(println %))
    (p/catch #(println "Error:" %))
    (p/finally #(println "Complete")))

Callbacks

jQuery: $.Callbacks()

// jQuery - Basic usage
const callbacks = $.Callbacks();

function fn1(value) {
    console.log('fn1:', value);
}

function fn2(value) {
    console.log('fn2:', value);
}

callbacks.add(fn1);
callbacks.add(fn2);
callbacks.fire('hello');
// Output:
// fn1: hello
// fn2: hello

// With flags
const onceCallbacks = $.Callbacks('once');
onceCallbacks.add(fn1);
onceCallbacks.fire('first');   // fn1: first
onceCallbacks.fire('second');  // (nothing - already fired)

// Memory flag
const memoryCallbacks = $.Callbacks('memory');
memoryCallbacks.fire('done');
memoryCallbacks.add(fn1);  // Immediately called with 'done'

Google Closure

// Google Closure - Custom implementation
goog.provide('app.Callbacks');

app.Callbacks = function(flags) {
    this.list_ = [];
    this.fired_ = false;
    this.lastArgs_ = null;
    this.once_ = flags && flags.indexOf('once') !== -1;
    this.memory_ = flags && flags.indexOf('memory') !== -1;
};

app.Callbacks.prototype.add = function(fn) {
    this.list_.push(fn);
    if (this.memory_ && this.fired_) {
        fn.apply(null, this.lastArgs_);
    }
};

app.Callbacks.prototype.fire = function() {
    if (this.once_ && this.fired_) return;

    this.fired_ = true;
    this.lastArgs_ = arguments;

    for (var i = 0; i < this.list_.length; i++) {
        this.list_[i].apply(null, arguments);
    }
};

// Usage
var callbacks = new app.Callbacks();
callbacks.add(fn1);
callbacks.fire('hello');

ClojureScript

;; ClojureScript - Using atoms
(defn create-callbacks [& {:keys [once memory]}]
  (let [callbacks (atom [])
        fired (atom false)
        last-args (atom nil)]
    {:add (fn [f]
            (swap! callbacks conj f)
            (when (and memory @fired)
              (apply f @last-args)))
     :fire (fn [& args]
             (when-not (and once @fired)
               (reset! fired true)
               (reset! last-args args)
               (doseq [f @callbacks]
                 (apply f args))))}))

;; Usage
(def cbs (create-callbacks))
((:add cbs) (fn [v] (println "fn1:" v)))
((:add cbs) (fn [v] (println "fn2:" v)))
((:fire cbs) "hello")

;; With memory
(def mem-cbs (create-callbacks :memory true))
((:fire mem-cbs) "done")
((:add mem-cbs) (fn [v] (println "Late callback:" v)))
;; Immediately prints: Late callback: done

Animation

jQuery: fadeIn/fadeOut

// jQuery
$('.dynamic-content').fadeIn();
$('.dynamic-content').fadeOut();
$('.dynamic-content').fadeToggle();

// With duration
$('#element').fadeIn(400);
$('#element').fadeOut('slow');

// With callback
$('#element').fadeIn(400, function() {
    console.log('Fade complete');
});

Google Closure

// Google Closure
goog.require('goog.fx.dom');
goog.require('goog.events');

const element = goog.dom.getElement('element');

// Fade in
const fadeIn = new goog.fx.dom.FadeIn(element, 400);
fadeIn.play();

// Fade out
const fadeOut = new goog.fx.dom.FadeOut(element, 400);
fadeOut.play();

// With callback
goog.events.listen(fadeIn, goog.fx.Transition.EventType.END, function() {
    console.log('Fade complete');
});

// Fade to specific opacity
const fade = new goog.fx.dom.Fade(element, 0, 0.5, 400);
fade.play();

ClojureScript

;; ClojureScript with goog.fx.dom
(require '[goog.fx.dom :as fx])
(import '[goog.fx Transition])

(def element (gdom/getElement "element"))

;; Fade in
(let [anim (fx/FadeIn. element 400)]
  (.play anim))

;; Fade out
(let [anim (fx/FadeOut. element 400)]
  (.play anim))

;; With callback
(let [anim (fx/FadeIn. element 400)]
  (gevents/listen anim (.-END Transition.EventType)
    (fn [_] (println "Fade complete")))
  (.play anim))

jQuery: slide animations

// jQuery
$('#element').slideDown();
$('#element').slideUp();
$('#element').slideToggle();

// With duration and callback
$('#element').slideDown(400, function() {
    console.log('Slide complete');
});

Google Closure

// Google Closure
goog.require('goog.fx.dom');

const element = goog.dom.getElement('element');
const size = goog.style.getSize(element);

// Slide down (from 0 height)
const slideDown = new goog.fx.dom.Resize(
    element,
    [size.width, 0],
    [size.width, size.height],
    400
);
slideDown.play();

// Slide up (to 0 height)
const slideUp = new goog.fx.dom.Resize(
    element,
    [size.width, size.height],
    [size.width, 0],
    400
);
slideUp.play();

ClojureScript

;; ClojureScript
(require '[goog.fx.dom :as fx]
         '[goog.style :as gstyle])

(def element (gdom/getElement "element"))
(def size (gstyle/getSize element))

;; Slide down
(let [anim (fx/Resize. element
             #js [(.-width size) 0]
             #js [(.-width size) (.-height size)]
             400)]
  (.play anim))

jQuery: Custom Animations

// jQuery - animate()
$('#element').animate({
    opacity: 0.5,
    left: '+=50px',
    height: 'toggle'
}, 1000, 'swing', function() {
    console.log('Animation complete');
});

Google Closure

// Google Closure - Combining animations
goog.require('goog.fx');
goog.require('goog.fx.dom');
goog.require('goog.fx.AnimationParallelQueue');

const element = goog.dom.getElement('element');

// Create parallel animations
const queue = new goog.fx.AnimationParallelQueue();

// Fade to 0.5
queue.add(new goog.fx.dom.Fade(element, 1, 0.5, 1000));

// Move right
const pos = goog.style.getPosition(element);
queue.add(new goog.fx.dom.Slide(
    element,
    [pos.x, pos.y],
    [pos.x + 50, pos.y],
    1000
));

goog.events.listen(queue, goog.fx.Transition.EventType.END, function() {
    console.log('Animation complete');
});

queue.play();

ClojureScript

;; ClojureScript
(require '[goog.fx :as gfx]
         '[goog.fx.dom :as fx])
(import '[goog.fx AnimationParallelQueue Transition])

(def element (gdom/getElement "element"))
(def pos (gstyle/getPosition element))

(let [queue (AnimationParallelQueue.)]
  (.add queue (fx/Fade. element 1 0.5 1000))
  (.add queue (fx/Slide. element
                #js [(.-x pos) (.-y pos)]
                #js [(+ 50 (.-x pos)) (.-y pos)]
                1000))
  (gevents/listen queue (.-END Transition.EventType)
    (fn [_] (println "Animation complete")))
  (.play queue))

Security Features

jQuery: Prototype Pollution Prevention

// jQuery 4.0 blocks these attempts
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'));
$.extend(true, {}, { constructor: { prototype: { polluted: true } } });

// Object.prototype remains unpolluted
console.log({}.polluted);  // undefined

Google Closure

// Google Closure - goog.object doesn't have deep merge by default
// Use Object.freeze for protection
const safeDefaults = Object.freeze({
    setting1: 'value1'
});

// Manual safe merge
function safeMerge(target, source) {
    for (const key in source) {
        if (source.hasOwnProperty(key) &&
            key !== '__proto__' &&
            key !== 'constructor') {
            target[key] = source[key];
        }
    }
    return target;
}

ClojureScript

;; ClojureScript - Immutable data structures prevent this by design
;; Clojure maps don't have prototype chain issues

(def defaults {:setting1 "value1"})
(def user-input (js->clj (js/JSON.parse "{\"__proto__\": {\"polluted\": true}}")))

;; merge just treats __proto__ as a regular key
(def merged (merge defaults user-input))
;; => {:setting1 "value1", "__proto__" {"polluted" true}}

;; The JS Object.prototype is unaffected

jQuery: XSS Mitigation

// jQuery - Escape selectors
const userInput = 'div"><script>alert("xss")</script>';
const escaped = $.escapeSelector(userInput);
$(escaped);  // Safe

Google Closure

// Google Closure - String escaping
goog.require('goog.string');
goog.require('goog.html.SafeHtml');

// HTML escaping
const escaped = goog.string.htmlEscape(userInput);

// Safe HTML creation
const safeHtml = goog.html.SafeHtml.htmlEscape(userInput);

ClojureScript

;; ClojureScript
(require '[goog.string :as gstring])

;; HTML escaping
(def escaped (gstring/htmlEscape user-input))

;; With hiccup libraries, escaping is automatic
;; (hipo/create [:div user-input]) - auto-escaped

jQuery: Trusted Types Support

// jQuery 4.0 accepts TrustedHTML objects
const policy = trustedTypes.createPolicy('myPolicy', {
    createHTML: (string) => DOMPurify.sanitize(string)
});

const trustedContent = policy.createHTML('<p>Safe content</p>');
$('#container').html(trustedContent);

Google Closure

// Google Closure - Built-in safe DOM methods
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');

// Create safe HTML
const safeHtml = goog.html.SafeHtml.create('p', {}, 'Safe content');

// Set inner HTML safely
goog.dom.safe.setInnerHtml(container, safeHtml);

ClojureScript

;; ClojureScript
(require '[goog.dom.safe :as safe]
         '[goog.html.SafeHtml :as SafeHtml])

(def safe-html (SafeHtml/create "p" nil "Safe content"))
(safe/setInnerHtml container safe-html)

Summary: Choosing Between Libraries

Use Case Recommendation
Quick prototyping jQuery - simplest API
Large-scale app ClojureScript - functional, optimized
Legacy codebase jQuery + migrate plugin
Functional programming ClojureScript
Maximum optimization ClojureScript (Closure Compiler)
React/modern framework Consider native APIs or framework tools

Migration Path

If migrating from jQuery:

  1. To Google Closure: Replace jQuery calls with goog.dom, goog.events, goog.style
  2. To ClojureScript: Use goog.dom namespace or wrapper libraries like dommy

Key Takeaways

  • jQuery provides the most concise syntax for DOM manipulation
  • Google Closure offers modular, optimizable code (but is sunset)
  • ClojureScript combines functional programming with Closure Library benefits
  • Modern browsers support many jQuery features natively (querySelector, fetch, classList)

Resources

jQuery

Google Closure Library

ClojureScript


Comparison document for jQuery 4.0.0 (January 2026) vs Google Closure Library vs ClojureScript