Lesson 36-In-Depth Study of the Lightweight Functional Programming Library Underscore.js

Overview of Underscore.js

Introduction to Underscore.js

Underscore.js is a lightweight JavaScript utility library that provides a wide range of functional programming helper methods for handling arrays, objects, functions, and other common data structures. Its core goal is to offer a concise, consistent API, enabling developers to write more elegant and maintainable code in a functional programming style.

Key Features:

  • Lightweight: Approximately 16KB when minified.
  • No dependencies: Works independently of other libraries.
  • High compatibility: Supports JavaScript environments from ES3 and above.
  • Functional programming style: Provides methods like map, reduce, filter, etc.
  • Utility toolkit: Includes a variety of practical utility functions.

Development History:

  • Created in 2009 by Jeremy Ashkenas (also the author of CoffeeScript and Backbone.js).
  • Gained prominence as a dependency of Backbone.js.
  • Evolved into a standalone utility library.
  • Updates slowed after 2017, but stable versions are still maintained.

Relationship Between Underscore.js and Lodash

Underscore.js and Lodash (another popular JavaScript utility library) are closely related:

FeatureUnderscore.jsLodash
CreatorJeremy AshkenasJohn-David Dalton
Origin20092012 (originally a fork of Underscore)
Design PhilosophyFunctional programming toolkitComprehensive utility library
PerformanceGoodTypically better (especially v4+)
Community SupportStable but less frequent updatesActive with frequent updates
FunctionalityCore functional methodsBroader range of utility functions
ModularitySupportedMore fine-grained modularity

Key Differences:

  1. Lodash has more performance optimizations.
  2. Lodash offers better support for modern JavaScript features.
  3. Lodash provides finer-grained modularization.
  4. Underscore focuses on core functional programming concepts.

Core Value of Underscore.js

  1. Functional Programming Paradigm: Offers map, reduce, filter, and other functional methods for declarative coding.
  2. Data Operation Abstraction: Simplifies operations on arrays, objects, and collections.
  3. Code Conciseness: Reduces boilerplate code, improving development efficiency.
  4. Consistency: Provides a unified API style, lowering the learning curve.
  5. Compatibility: Supports older JavaScript environments.

Detailed Explanation of Underscore.js Core Functions

Collection Processing Functions

Underscore.js provides a rich set of functions for handling collections, including arrays, objects, and array-like structures.

map/collect

_.map(list, iteratee, [context]): Executes the iteratee function on each element of the collection, returning an array of results.

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

// Equivalent to
const doubled2 = numbers.map(n => n * 2);

Differences:

  • Underscore’s map can handle objects and array-like structures.
  • Provides a consistent API across different collection types.

reduce/foldl/inject

_.reduce(list, iteratee, [memo], [context]): Reduces a collection to a single value.

const sum = _.reduce([1, 2, 3, 4], (sum, n) => sum + n, 0);
// sum: 10

// Equivalent to
const sum2 = [1, 2, 3, 4].reduce((sum, n) => sum + n, 0);

Features:

  • Supports setting an initial value (memo).
  • Can handle sparse arrays.

filter/select

_.filter(list, predicate, [context]): Returns an array of elements that satisfy the predicate condition.

const evens = _.filter([1, 2, 3, 4], n => n % 2 === 0);
// evens: [2, 4]

reject

_.reject(list, predicate, [context]): Returns an array of elements that do not satisfy the predicate condition.

const odds = _.reject([1, 2, 3, 4], n => n % 2 === 0);
// odds: [1, 3]

every/all

_.every(list, predicate, [context]): Checks if all elements satisfy the predicate condition.

const allPositive = _.every([1, 2, 3], n => n > 0);
// allPositive: true

some/any

_.some(list, predicate, [context]): Checks if at least one element satisfies the predicate condition.

const hasNegative = _.some([-1, 2, 3], n => n < 0);
// hasNegative: true

contains/include

_.contains(list, value, [fromIndex]): Checks if the list contains the specified value.

const hasTwo = _.contains([1, 2, 3], 2);
// hasTwo: true

Array Functions

Underscore.js provides numerous utility functions specifically for array operations.

first/head/take

_.first(array, [n]): Returns the first element or the first n elements of an array.

const first = _.first([1, 2, 3]);
// first: 1

const firstTwo = _.first([1, 2, 3], 2);
// firstTwo: [1, 2]

last

_.last(array, [n]): Returns the last element or the last n elements of an array.

const last = _.last([1, 2, 3]);
// last: 3

const lastTwo = _.last([1, 2, 3], 2);
// lastTwo: [2, 3]

rest/initial

_.rest(array, [index]): Returns all elements after the specified index (defaults to 1).

const rest = _.rest([1, 2, 3, 4]);
// rest: [2, 3, 4]

const restFrom2 = _.rest([1, 2, 3, 4], 2);
// restFrom2: [3, 4]

_.initial(array, [n]): Returns all elements except the last n elements.

const initial = _.initial([1, 2, 3, 4]);
// initial: [1, 2, 3]

const initialLast2 = _.initial([1, 2, 3, 4], 2);
// initialLast2: [1, 2]

flatten

_.flatten(array, [shallow]): Flattens a nested array.

const flat = _.flatten([1, [2], [3, [[4]]]]);
// flat: [1, 2, 3, 4, [4]] (fully flattened)

const shallowFlat = _.flatten([1, [2], [3, [[4]]]], true);
// shallowFlat: [1, 2, 3, [4]] (flattened one level)

without

_.without(array, *values): Returns a new array with specified values removed.

const withoutTwo = _.without([1, 2, 3, 2], 2);
// withoutTwo: [1, 3]

union

_.union(*arrays): Returns the union of multiple arrays (deduplicated).

const union = _.union([1, 2, 3], [2, 3, 4]);
// union: [1, 2, 3, 4]

intersection

_.intersection(*arrays): Returns the intersection of multiple arrays.

const intersection = _.intersection([1, 2, 3], [2, 3, 4]);
// intersection: [2, 3]

difference

_.difference(array, *others): Returns elements in array that are not in other arrays.

const diff = _.difference([1, 2, 3, 4], [2, 3]);
// diff: [1, 4]

uniq/unique

_.uniq(array, [isSorted], [iteratee]): Returns a deduplicated array.

const unique = _.uniq([1, 2, 1, 3, 2]);
// unique: [1, 2, 3]

// Using iteratee
const uniqueByAbs = _.uniq([-1, 2, -1, 3, 2], false, Math.abs);
// uniqueByAbs: [-1, 2, 3]

Object Functions

Underscore.js provides many utility functions for operating on JavaScript objects.

keys

_.keys(object): Returns an array of the object’s enumerable property names.

const keys = _.keys({a: 1, b: 2, c: 3});
// keys: ['a', 'b', 'c']

values

_.values(object): Returns an array of the object’s enumerable property values.

const values = _.values({a: 1, b: 2, c: 3});
// values: [1, 2, 3]

pairs

_.pairs(object): Converts an object into an array of [key, value] pairs.

const pairs = _.pairs({a: 1, b: 2, c: 3});
// pairs: [['a', 1], ['b', 2], ['c', 3]]

invert

_.invert(object): Inverts the object’s key-value pairs.

const inverted = _.invert({a: 1, b: 2, c: 3});
// inverted: {1: 'a', 2: 'b', 3: 'c'}

functions/methods

_.functions(object): Returns an array of the object’s method names.

const obj = {
  a: 1,
  b: function() {},
  c: function() {}
};
const funcs = _.functions(obj);
// funcs: ['b', 'c']

extend

_.extend(destination, *sources): Copies properties from one or more source objects to a target object.

const obj = _.extend({a: 1}, {b: 2}, {c: 3});
// obj: {a: 1, b: 2, c: 3}

defaults

_.defaults(object, *defaults): Copies properties from source objects to the target object, but only for properties that don’t exist in the target.

const obj = _.defaults({a: 1}, {a: 2, b: 2}, {b: 3, c: 3});
// obj: {a: 1, b: 2, c: 3}

clone

_.clone(object): Performs a shallow clone of an object.

const obj = {a: 1, b: {c: 2}};
const cloned = _.clone(obj);
// cloned: {a: 1, b: {c: 2}} (b is a reference)

tap

_.tap(object, interceptor): Calls the interceptor function with the object and returns the object, often used for debugging.

const result = _.tap({a: 1}, obj => {
  console.log(obj); // {a: 1}
});
// result: {a: 1}

Function Utilities

Underscore.js provides many tools for manipulating and enhancing functions.

bind

_.bind(function, object, *arguments): Binds a function to an object, optionally pre-setting some arguments.

const obj = {
  a: 1,
  add: function(b, c) { return this.a + b + c; }
};
const boundAdd = _.bind(obj.add, obj, 2);
const result = boundAdd(3); // 1 + 2 + 3 = 6

partial

_.partial(function, *partialArgs): Pre-sets some arguments of a function, creating a new function.

const add = (a, b, c) => a + b + c;
const addPartial = _.partial(add, 1, 2);
const result = addPartial(3); // 1 + 2 + 3 = 6

memoize

_.memoize(function, [hashFunction]): Caches function results to avoid redundant computations.

const fibonacci = _.memoize(n => {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

fibonacci(10); // Computes and caches result
fibonacci(10); // Reads directly from cache

delay

_.delay(function, wait, *arguments): Delays the execution of a function.

_.delay(() => console.log('Delayed'), 1000); // Executes after 1 second

throttle

_.throttle(function, wait, [options]): Creates a throttled function that executes at most once every wait milliseconds.

const throttledScroll = _.throttle(() => {
  console.log('Scroll event');
}, 200);

window.addEventListener('scroll', throttledScroll);

debounce

_.debounce(function, wait, [immediate]): Creates a debounced function that executes after wait milliseconds of no further triggers.

const debouncedResize = _.debounce(() => {
  console.log('Window resized');
}, 300);

window.addEventListener('resize', debouncedResize);

once

_.once(function): Creates a function that executes only once.

const init = _.once(() => {
  console.log('Initialized');
});
init(); // 'Initialized'
init(); // No output

after

_.after(count, function): Creates a function that executes only after being called count times.

const saveAfter3 = _.after(3, () => {
  console.log('Saved!');
});

saveAfter3(); // No output
saveAfter3(); // No output
saveAfter3(); // 'Saved!'

wrap

_.wrap(function, wrapper): Wraps a function with a wrapper function.

const greet = name => `Hello, ${name}!`;
const wrappedGreet = _.wrap(greet, (func, name) => {
  return `Before: ${func(name)} After`;
});

wrappedGreet('John'); // 'Before: Hello, John! After'

Utility Functions

Underscore.js also provides a variety of general-purpose utility functions.

identity

_.identity(value): Returns the input value as-is.

const value = _.identity(42);
// value: 42

constant

_.constant(value): Returns a function that always returns the specified value.

const alwaysFour = _.constant(4);
alwaysFour(); // 4

noop

_.noop(): A no-operation function.

_.noop(); // No operation

times

_.times(n, iteratee, [context]): Calls the iteratee function n times.

const results = _.times(3, i => i * 2);
// results: [0, 2, 4]

random

_.random(min, max): Returns a random integer between min and max.

const rand = _.random(1, 10);
// rand: Random integer between 1 and 10

mixin

_.mixin(object): Adds methods from object to the Underscore namespace.

_.mixin({
  capitalize: function(string) {
    return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
  }
});

_.capitalize('hello'); // 'Hello'

uniqueId

_.uniqueId([prefix]): Generates a unique ID.

const id1 = _.uniqueId(); // '1'
const id2 = _.uniqueId(); // '2'
const id3 = _.uniqueId('item_'); // 'item_3'

escape

_.escape(string): Escapes HTML special characters.

const escaped = _.escape('<script>alert("xss")</script>');
// escaped: '<script>alert("xss")</script>'

result

_.result(object, property, [defaultValue]): Retrieves a property value from an object, executing it if it’s a function.

const obj = {
  a: 1,
  b: function() { return 2; }
};
const val1 = _.result(obj, 'a'); // 1
const val2 = _.result(obj, 'b'); // 2
const val3 = _.result(obj, 'c', 3); // 3 (default value)

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here

Share your love