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
$.isArrayand$.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 - 1property.
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
trueif 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
thisand 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>