Functional Utilities page

Create some of jQuery’s functional utility methods.

Overview

We will learn about:

  • Extending objects by building $.extend
  • Type checking by building $.isArray and $.isArrayLike
  • Iterating objects and arrays by building $.each
  • Binding functions to a particular context by building $.proxy

Video

Slides

Setup

Run the following example in CodePen:

<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css" />
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
  (function () {
    $ = function (selector) {};

    $.extend = function (target, object) {};

    // Static methods
    $.extend($, {
      isArray: function (obj) {},
      isArrayLike: function (obj) {},
      each: function (collection, cb) {},
      makeArray: function (arr) {},
      proxy: function (fn, context) {},
    });

    $.extend($.prototype, {
      // These will be added later.
    });
  })();
</script>

Each exercise builds on the previous exercise. There is a completed solution at the end of this page.

Exercise: $.extend( target, source ) -> target

The problem

jQuery.extend merges the contents of a source object onto the target object.

Click to see test code

QUnit.test("$.extend", function () {
  var target = { first: "Justin" },
    object = { last: "Meyer" };

  var result = $.extend(target, object);

  equal(result, target, "target and result are equal");
  deepEqual(
    result,
    { first: "Justin", last: "Meyer" },
    "properties added correctly"
  );
});

PRO TIP: Use Object.assign in a modern app.

What you need to know

  • Loop through an object’s enumerable properties with a for-in loop:

    var obj = { foo: "bar", zed: "ted" };
    for (var prop in obj) {
      console.log(prop); // Logs "foo" then "zed"
    }
    
  • Read a property with a string using the [] member operator.

    var obj = { foo: "bar" };
    var prop = "foo";
    console.log(obj[prop]); // Logs "bar"
    
  • Assign a property with a string using the [] member operator:

    var obj = {};
    var prop = "foo";
    obj[prop] = "bar";
    console.log(obj.prop); // Logs "bar"
    
  • Use Object.prototype.hasOwnProperty to detect if an object has the specified property as it’s own property:

    var obj = { foo: "bar" };
    console.log(obj.hasOwnProperty("foo")); // Logs true
    

The solution

Click to see the solution

$.extend = function (target, object) {
  for (var prop in object) {
    if (object.hasOwnProperty(prop)) {
      target[prop] = object[prop];
    }
  }
  return target;
};

Exercise: $.isArray( obj ) -> boolean

The problem

jQuery.isArray determines whether the argument is an array.

Click to see test code

QUnit.test("$.isArray", function () {
  equal($.isArray([]), true, "An array is an array");
  equal($.isArray(arguments), false, "Arguments are not an array");

  var iframe = document.createElement("iframe");
  document.body.appendChild(iframe);

  var IframeArray = iframe.contentWindow.Array;

  equal(
    $.isArray(new IframeArray()),
    true,
    "Arrays from other iframes are Arrays"
  );

  document.body.removeChild(iframe);
});

PRO TIP: Use Array.isArray in a modern app.

What you need to know

  • Object.prototype.toString called on built-in types returns the object class name:

    console.log(Object.prototype.toString.call(new Date()));
    // Logs "[object Date]"
    

The solution

Click to see the solution

    isArray: function(obj) {
      return Object.prototype.toString.call(obj) === "[object Array]";
    },

Exercise: $.isArrayLike(obj) -> boolean

The problem

jQuery uses an internal isArrayLike method to detect if an object looks like an array. A value is array-like if:

  • it is an object.
  • the object has a number length property:
    • the length property is 0, or
    • the length is greater than 0 and there is a length - 1 property.

Click to see test code

QUnit.test("$.isArrayLike", function () {
  equal($.isArrayLike([]), true, "An array is array like");
  equal($.isArrayLike(arguments), true, "Arguments is array like");
  equal($.isArrayLike({ length: 0 }), true, "length: 0 is array like");
  equal(
    $.isArrayLike({ length: 5, 4: undefined }),
    true,
    "length > 0 and has property is array like"
  );

  equal($.isArrayLike(null), false, "Null is not array like");
  equal($.isArrayLike({}), false, "Plain object is not array like");
  equal($.isArrayLike({ length: -1 }), false, "length: -1 is not array like");
});

What you need to know

  • The in operator returns true if the specified property is in the specified object or its prototype chain.
    var obj = { foo: "bar" };
    console.log("foo" in obj); // Logs true
    

The solution

Click to see the solution

    isArrayLike: function(obj) {
      return obj &&
        typeof obj === "object" &&
        (   obj.length === 0 ||
            typeof obj.length === "number" &&
            obj.length > 0 &&
            obj.length - 1 in obj );
    },

Exercise: $.each( obj , cb(index, value) ) -> obj

The problem

jQuery.each loops through objects and arrays, calling the cb callback for each value.

import "https://unpkg.com/jquery@3/dist/jquery.js";

var collection = ["a", "b"];
$.each(collection, function (index, item) {
  console.log(item + " is at index " + index);
  // logs "a is at 0"
  //      "b is at 1"
});

collection = { foo: "bar", zed: "ted" };
res = $.each(collection, function (prop, value) {
  console.log("prop: " + prop + ", value: " + value);
  // logs "prop: foo, value: bar"
  //      "prop: zed, value: ted"
});

Click to see test code

QUnit.test("$.each", function () {
  expect(9);
  var collection = ["a", "b"];
  var res = $.each(collection, function (index, value) {
    if (index === 0) equal(value, "a");
    else if (index === 1) equal(value, "b");
    else ok(false, "called back with a bad index");
  });
  equal(collection, res);

  collection = { foo: "bar", zed: "ted" };
  res = $.each(collection, function (prop, value) {
    if (prop === "foo") equal(value, "bar");
    else if (prop === "zed") equal(value, "ted");
    else ok(false, "called back with a bad index");
  });
  equal(collection, res);

  collection = { 0: "a", 1: "b", length: 2 };
  res = $.each(collection, function (index, value) {
    if (index === 0) equal(value, "a");
    else if (index === 1) equal(value, "b");
    else ok(false, "called back with a bad index");
  });
  equal(collection, res);
});

Use forEach on arrays in apps.

What you need to know

  • Use a for statement to loop through something array-like:
    var items = ["a", "b", "c"];
    for (var i = 0; i < items.length; i++) {
      console.log(items[i]); // logs "a", "b", "c"
    }
    

The solution

Click to see the solution

    each: function(collection, cb) {
      if ($.isArrayLike(collection)) {
        for (var i = 0; i < collection.length; i++) {
          if (cb.call(this, i, collection[i]) === false) {
            break;
          }
        }
      } else {
        for (var prop in collection) {
          if (collection.hasOwnProperty(prop)) {
            if (cb.call(this, prop, collection[prop]) === false) {
              break;
            }
          }
        }
      }
      return collection;
    },

Exercise: $.makeArray

The problem

jQuery.makeArray converts an array-like object into a true JavaScript array. For example, it can make arrays of the following:

$.makeArray(document.body.childNodes);

$.makeArray(document.getElementsByTagName("*"));

$.makeArray(arguments);

$.makeArray($("li"));

Click to see test code

QUnit.test("$.makeArray", function () {
  var childNodes = document.body.childNodes;

  ok(!$.isArray(childNodes), "node lists are not arrays");

  var childArray = $.makeArray(childNodes);

  ok($.isArray(childArray), "made an array");

  equal(childArray.length, childNodes.length, "lengths are the same");

  for (var i = 0; i < childArray.length; i++) {
    equal(childArray[i], childNodes[i], "array index " + i + " is equal.");
  }
});

What you need to know

You already know everything you need to know. You can do it!

In modern apps, use Array.from instead of jQuery.makeArray.

The solution

Click to see the solution

    makeArray: function(arr) {
      if ($.isArray(arr)) {
        return arr;
      }
      var array = [];
      $.each(arr, function(i, item) {
        array[i] = item;
      });
      return array;
    },

Exercise: $.proxy

The problem

jQuery.proxy takes a function and returns a new one that will always have a particular context.

The following logs "undefined says woof" instead of "fido says woof":

var dog = {
  nickname: "fido",
  speak: function () {
    console.log(this.nickname + " says woof");
  },
};

setTimeout(dog.speak, 500);

$.proxy fixes this:

import "https://unpkg.com/jquery@3/dist/jquery.js";

var dog = {
  nickname: "fido",
  speak: function () {
    console.log(this.nickname + " says woof");
  },
};

setTimeout($.proxy(dog.speak, dog), 500);

$.proxy can pass arguments too:

import "https://unpkg.com/jquery@3/dist/jquery.js";

var dog = {
  nickname: "fido",
  speak: function (word) {
    console.log(this.nickname + " says " + word);
  },
};

var dogSpeak = $.proxy(dog.speak, dog);
dogSpeak("ruff"); // Logs 'fido says ruff'

Click to see the test code

QUnit.test("$.proxy", function () {
  var dog = {
    name: "fido",
    speak: function (words) {
      return this.name + " says " + words;
    },
  };

  var speakProxy = $.proxy(dog.speak, dog);

  equal(speakProxy("woof!"), "fido says woof!");
});

PRO TIP: Use Function.prototype.bind or arrow functions instead of $.proxy.

What you need to know

  • Use Function.prototype.apply to call a function with a specified this and arguments:

    var cat = { name: "sparky" };
    
    var dog = {
      name: "fido",
      speak() {
        console.log(this.name + "says woof");
      },
    };
    
    dog.speak.apply(cat, []); // Logs "sparky says woof"
    

The solution

Click to see the solution

    proxy: function(fn, context) {
      return function() {
        return fn.apply(context, arguments);
      };
    },

Completed Solution

Click to see completed solution

<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css" />
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
  (function () {
    $ = function (selector) {};

    $.extend = function (target, object) {
      for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
          target[prop] = object[prop];
        }
      }
      return target;
    };

    // Static methods
    $.extend($, {
      isArray: function (obj) {
        return Object.prototype.toString.call(obj) === "[object Array]";
      },
      isArrayLike: function (obj) {
        return (
          obj &&
          typeof obj === "object" &&
          (obj.length === 0 ||
            (typeof obj.length === "number" &&
              obj.length > 0 &&
              obj.length - 1 in obj))
        );
      },
      each: function (collection, cb) {
        if ($.isArrayLike(collection)) {
          for (var i = 0; i < collection.length; i++) {
            if (cb.call(this, i, collection[i]) === false) {
              break;
            }
          }
        } else {
          for (var prop in collection) {
            if (collection.hasOwnProperty(prop)) {
              if (cb.call(this, prop, collection[prop]) === false) {
                break;
              }
            }
          }
        }
        return collection;
      },
      makeArray: function (arr) {
        if ($.isArray(arr)) {
          return arr;
        }
        var array = [];
        $.each(arr, function (i, item) {
          array[i] = item;
        });
        return array;
      },
      proxy: function (fn, context) {
        return function () {
          return fn.apply(context, arguments);
        };
      },
    });

    $.extend($.prototype, {
      // These will be added later.
    });
  })();
</script>