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>