Along with reading AngularJS, which I started in an earlier post, I will also start reading the source of several other world-class JavaScript projects. In this post, we will start reading jQuery. jQuery is easily one of the most successful, long-lived, and most influential JavaScript libraries of all time. Even if I have sometimes argued that we don’t need jQuery anymore, don’t take that to mean I don’t respect what the jQuery authors have done. It is a magnificent piece of literature, and as I learn more of it, I have started using it again in my own projects in various ways—for flourish and for unit testing.
I forked the jQuery source repo and I created a new branch xero-6-2019 that will remain static. It’s a historical copy at one point in time made so that readers of my blog can refer to it even as the real jQuery source code continues to change and evolve.
Today I will start reading the file src/core.js. You can view my static copy of this file here: https://github.com/xerocross/jquery/blob/xero-6-2019/src/core.js . You may also find it helpful to clone the entire repo locally so you can view the code in your own favorite code editor. That’s what I did, and I’m viewing the code in VS Code. It has nice highlighting and such.
As always, the source code of a large project like this is organized into many different files. We will get a better understanding of the overall architecture as we read, but for now the file src/core.js looks like a good place to begin.
We can see right away that jQuery favors the AMD form for defining modules. They use the define
function. See this Stackoverflow QA here: https://stackoverflow.com/questions/16950560/what-is-define-function-in-javascript for more on that. The define function takes a list of dependencies to be resolved and then it inserts them into the function in the next argument.
jQuery core defines a list of dependencies as follows.
define( [
"./var/arr",
"./var/getProto",
"./var/slice",
"./var/concat",
"./var/push",
"./var/indexOf",
"./var/class2type",
"./var/toString",
"./var/hasOwn",
"./var/fnToString",
"./var/ObjectFunctionString",
"./var/trim",
"./var/support",
"./var/isWindow",
"./core/DOMEval",
"./core/toType"
], function( arr, getProto, slice, concat, push, indexOf,
class2type, toString, hasOwn, fnToString, ObjectFunctionString,
trim, support, isWindow, DOMEval, toType ) {
At this point I’m willing to guess that each of those files defines a small utility function or a small collection of utility functions. The important thing here is that none of the names sounds like a large module with a lot of functionality. It really does look like core is the bedrock. Speaking on that hypothesis, right below we see a jQuery
function confusingly defined in terms of itself.
jQuery = function( selector, context ) {
// The jQuery object is actually just the init constructor //'enhanced'
// Need init if jQuery is called (just allow error to be //thrown if not included)
return new jQuery.fn.init( selector, context );
};
I suppose that definition won’t throw any error in JavaScript unless jQuery gets called without jQuery.fn.init
defined. By the time somebody calls jQuery()
, that jQuery.fn.init
has to be defined. Along those lines, right after that we see where they begin building the jQuery prototype.
jQuery.fn = jQuery.prototype = {
....
This defines fn
as an alias for the prototype. In this case it’s clear that the programmers are thinking of jQuery as a constructor function (among other things). The prototype
property is a property of constructor functions. If we define jQuery.prototype
using our own custom methods and fields, then any object we create by executing new jQuery(...)
will have jQuery.prototype
as its prototype. That means all of our custom methods and fields will be inherited automatically by that new objects.
On this note, by the way, it’s nice to see idiomatic JavaScript well written. There are no “classes” here. This is native JavaScript constructor function and prototypal inheritance. As of this writing, JavaScript’s implementation of classes is still incomplete and offers no real alternative to writing things the JavaScript way.
Starting on line 39, you can see that the authors have defined jQuery.prototype as a custom object using object-literal notation. For example, they defined a custom toArray
function like this.
toArray: function() {
return slice.call( this );
},
Let’s examine that briefly. There’s a well known trick where you define slice = [].slice
. Basically you just pluck it from the Array class. Then you can apply it to array-like objects using the call
method. The call
method allows you to pass the context of a function in as the argument of call. That is: slice.call(obj)
executes the slice method as if that functions context was obj
.
What about the use of this
in that function above? The this
keyword is perennially confusing to students of JavaScript. Half the time people have no idea what it points to. I wrote this little JavaScript experiment to clarify matters.
let obj = {
myname : "apple",
getMyName : function() {
return this.myname;
}
}
function TestObject () {
}
TestObject.prototype = obj;
let obj1 = new TestObject();
let obj2 = new TestObject();
obj2.myname = "pear";
console.log(obj.getMyName()); // logs "apple"
console.log(obj1.getMyName()); // logs "apple"
console.log(obj2.getMyName()); // logs "pear"
This experiment helps us understand what this
means in this scenario. The object obj
is its own first-class object. If you use the keyword this
inside one of its methods, then this
refers to the context, which is obj
. That is, when you execute a method by directly referencing the object as in obj.getMyName()
then the context of the method is obj
and any reference to this
in that method points to obj
because it is the context.
Next we make a new constructor function TestObject and set its prototype to be our obj
object. Then when we create a new object by running let obj1 = new TestObject()
the result is that obj
is the prototype of obj1
. Same with obj2
. Both of these objects inherit fields and methods from obj
. When they are first created, obj1.myname = obj2.myname = "apple"
. But we can override this value. That’s what we did when we wrote obj2.myname = "pear"
. In either case, when we call obj1.getMyName()
or obj2.getMyName()
the keyword this
in the method definition points to the context. For obj1
, this
is obj1
. For obj2
, this
is obj2
.
Going back to the toArray
function, remember it ends with return slice.call( this);
In this case, this
refers to whatever object its called on. Our familiarity with jQuery tells us that they are not arrays but they are array-like objects. Recall that in jQuery the familiar dollar sign notation $
is just an alias for the jQuery
function. Using jQuery with a selector like $("li")
returns an array-like wrapper around special objects wrapping the DOM <li> elements. With jQuery, you can refer to the elements using array notation like $(“li”)[1]. When you do this, the value you get is a raw DOM object.
When you use the Array.slice
method with a normal context, like [4,3,2].slice(1), then it returns a new array with data cloned from the original (it’s a shallow clone). If you don’t pass any argument to the slice
method, then it returns a complete clone of the array. But what happens in this case, when we are using this disembodied slice
method and applying it to an array-like object that does not have the slice
method defined on it natively?
The Mozilla docs tell us, “For object references (and not the actual object), slice
copies object references into the new array. Both the original and new array refer to the same object. If a referenced object changes, the changes are visible to both the new and original arrays.”
I wrote a little experiment to clarify this matter.
let obj = {
"0" : "apple",
"1" : "pear",
"2" : "bear",
length : 3,
other : 5
}
console.log(obj) // {0: "apple", 1: "pear", 2: "bear", length: 3,
// other: 5}
console.log(Object.prototype.toString.call(obj)); // [object
// Object]
let clone = Array.prototype.slice.call(obj);
console.log(Object.prototype.toString.call(clone)); //[object
// Array]
console.log(clone); // (3) ["apple", "pear", "bear"]
Here I first created an array-like object. It has type Object, but it also has definitions for indexes 0, 1, 3, and length is defined as 3. Calling Array.prototype. slice.call(obj)
executes without any problem, and the result is a new object, clone
. The difference between them is that clone is a true array. We can check the type by calling Object.prototype.toString.call(clone)
. For unknown reasons this is more reliable than calling typeof
. Also note that the value of “other” does not get copied over. Even a real array can have extra, non-array-like properties. But it looks like slice
does not copy them. (There are other ways to do that.)
From this we can reason that the jQuery.toArray
method returns a genuine JavaScript array from an object that was only array-like. Also it does not mutate the original object.
To be continued in Part 2.
One thought on “Reading jQuery (1)”