Reading jQuery (2)

This is a continuation of Reading jQuery (1), wherein we started reading the jQuery source code.

jQuery offers a get method as in $("li").get(0). The object $("li") is an array-like object that wraps a collection of <li> DOM objects. The get method is defined in this short block starting on src/core.js at line 55.

get: function( num ) {

   // Return all the elements in a clean array
   if ( num == null ) {
	return slice.call( this );
   }

   // Return just the one element from the set
   return num < 0 ? this[ num + this.length ] : this[ num ];  
},

This says that if you call get with no arguments then you get a native JavaScript array created fresh with the numbered elements from the jQuery object. If you call it with a number greater than or equal to 0, like $("li").get(i), then it returns the element with key "i": $("li") [i];

Recall that in JavaScript an array is just a specialized kind of object. The keys of an object in JavaScript are always strings (exclude the possibility of symbols for a later conversation). If you define something like myobj[4] = "apple", then behind the scenes JavaScript coerces 4 into the string type "4". Since 4 is not a legal identifier in JavaScript, if you try to call myobj.4 it throws a SyntaxError. But you can call myobj[4]. The bracket syntax evaluates the expression and tries to coerce it into a string. In this case, that makes the string “4” which is a key on the object.

From my experience using jQuery, I know that if you get something like $("li")[0] then the result is a raw DOM object (or undefined), but the file src/core.js does not contain the init function that actually builds the jQuery object from its arguments. That is: from this file alone, we can’t tell what to expect when we call $("li")[0]. The init function is defined in a file at src/core/init.js. It’s a very dense function. Let’s put some other pieces together before diving headlong into reading that.

Inside the core.js file again, we see that there is an alternative to the get method. There is a method called eq. You can read the docs about this method here: https://api.jquery.com/eq/. The docs say this method “[reduces] the set of matched elements to the one at the specified index.” The difference seems to be that the result of calling eq is still a jQuery object whereas calling get returns a raw DOM object. Let’s see if we can trace that through in the source.

Starting on line 103 in core.js we see the definition of eq. It’s short:

eq: function( i ) {
  var len = this.length,
    j = +i + ( i < 0 ? len : 0 );
  return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : 
    [] );
}

Let’s see what pushStack does. The definition starts on line 66.

// Take an array of elements and push it onto the stack
// (returning the new matched element set)
pushStack: function( elems ) {
  // Build a new jQuery matched element set
  var ret = jQuery.merge( this.constructor(), elems );

  // Add the old object onto the stack (as a reference)
  ret.prevObject = this;

  // Return the newly-formed element set
  return ret;
},

In this file, on line 44, they have explicitly defined constructor: jQuery. So this.constructor() is the same as jQuery(), an empty jQuery object.

The merge function begins on line 288. It’s simple.

merge: function( first, second ) {
   var len = +second.length,
      j = 0,
      i = first.length;
   for ( ; j < len; j++ ) {
      first[ i++ ] = second[ j ];
   }
   first.length = i;
   return first;
},

Apparently, the merge function just reads all of the numbered elements from second and pushes them onto the end of first. Note how it treats both arguments as array-like objects, not arrays. It does not use the array method push for appending something at the end. It explicitly defines the new elements by number, and at the end it has to explicitly update the value of “length”. Since the elements jQueryObj[i] are raw DOM objects, it looks like you can add new elements to a jQuery object by just pushing them onto the end of it like this merge function does. I would have guessed it was more complicated than that.

Let’s retrace our steps back to pushStack. Now we can understand it better. Calling jQuery.merge( this.constructor(), elems ) just creates a new empty jQuery object and pushes elems onto it. It looks like elems is expected to be an array of the same type as a typical $(...)[0] object—that is, an array of raw DOM objects. This creates a brand new jQuery object, but we also attach a reference to the current jQuery object that is the context. So using the pushStack method creates a linked list of jQuery objects. Presumably that is what they mean when they refer to a “stack” in the name.

Now let’s look again at the eq method. It returns the result of calling this.pushStack. So what we get from eq is definitely a jQuery object. This new object also contains a reference to the previous one, a fact not mentioned in the docs. We know, however, that jQuery is big into chaining. If you write something like $("li").eq(2) then you might apply some method to eq(2) and then use some other method to climb backward to get the original $("li") again. That doesn’t sound like great programming to me (on the part of this hypothetical jQuery user), but it is better than executing $("li") again from scratch.

According to the docs at https://api.jquery.com/end/#end , the end method does exactly what I was just describing. One could do something like $("li").eq(2) .css("background-color", "red") .end().eq(3).css("border-style","solid"). That looks gross, and I would not write that, but it is a possible use of the chaining “stack” made possible by the linked list that pushStack creates.

The end method is defined at line 109 as follows.

end: function() {
   return this.prevObject || this.constructor();
}

This makes sense. When chaining selectors like $("li").eq(2) the current jQuery object has a reference to the original $("li") object stored in the field prevObject. So if prevObject is defined, then that is what you get. Otherwise you get an empty jQuery object. Thus $(document).end() evaluates to an empty jQuery object. One consequence of this is that executing jQuery methods on an empty object does not throw an error. It just does nothing. That may or may not be desirable behavior, but that is what happens if you execute something like $(document).end().css("background-color","red"). It just does nothing with no complaint. There won’t be any error indicating that the line you wrote had no effect.

To move any further, I think we really need to study the jQuery init function now. Recall that init is defined in its own file at src/core/init.js.

Inside the core.js file, the jQuery constructor is defined simply like this.

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 );
};

The arguments given to the constructor just get passed directly into init. So let’s switch over to src/core/init.js. You can view the file online here: https://github.com/xerocross/jquery/blob/xero-6-2019/src/core/init.js . It’s quite a meaty function, so breaking it down may not be easy. Let’s dive in. You probably want to keep core.js open in another tab.

First observe that there is a high-level switch defining different behaviors depending on the type of selector, the first argument. The simple cases are when selector is falsy, or it’s a DOM element, or its a function. If selector is a reference to a DOM object, then the init function creates the new jQuery object by doing this.

...
} else if ( selector.nodeType ) {
  this[ 0 ] = selector;
  this.length = 1;
  return this;
...
}

This supports my conjecture above that you can add DOM elements to a jQuery object by just attaching them explicitly to number keys like “4” and then updating the value of “length”. I would warn the reader, however, that most likely this is what we call an “implementation detail”, not part of the API. The difference is that APIs remain the same so as to give users a stable interface to work with, but the implementation details underneath might change at any time without notice.

That said, the jQuery docs do specify that you can address the underlying raw DOM objects using jQueryObj[index]. Simply put, they publish that you can read the contents this way, but I’ve never seen anything saying that you are allowed to write DOM objects into a jQuery object this way.

Note the return statement in that bit of code above. It means we never get to the return statement at the bottom of the function.

If the selector argument has type “function”, then this is interpreted as a shortcut for the behavior $(document).ready(function(){}) which people use to delay execution of a function until all of the DOM objects have been loaded (but not necessarily the contents of image files and other such media.

The most complex case is when the selector argument is a string—an actual selector string like “.my-class” or “[data-testid=special-div]”. That case is handled by a rather daunting block of code starting at init.js on line 34. For one thing, it uses the regular expression /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/. That notation is a JavaScript regular expression literal. Regular expressions are possibly the most densely inscrutable programming language known to man. The comments say this is intended to match HTML tags or ids, and that looks true enough

At this point I am confused because I don’t see any handling at all for CSS selectors like “[data-testid=special-div]”. That kind of string does not match the regular expression above. But I see that if selector is a string and it does not match the regular expression, then it gets handed off to ( context || root ).find( selector ). Is that where we find handling for CSS selectors?

After some digging, we find jQuery.find defined in the file src/selector-sizzle.js. Here we see that it is just an alias for Sizzle defined by jQuery.find = Sizzle. I’m actually not familiar with Sizzle, so let’s take a brief look at that project. The Sizzle project’s code is at https://github.com/jquery/sizzle . It looks like Sizzle is just part of jQuery that the jQuery authors decided to factor out into a standalone library that anyone can use. Its API looks very simple. You give it a selector and it returns an array of DOM objects.

Here I should clear up something in the jQuery source that is easy to overlook or misunderstand. Consider the following find function that is added onto the jQuery.prototype object. The code is in src/traversing/findFilter.js starting on line 53.

find: function( selector ) {
    var i, ret,
        len = this.length,
        self = this;
    if ( typeof selector !== "string" ) {
        return this.pushStack( jQuery( selector ).filter( function() 
        {
            for ( i = 0; i < len; i++ ) {
                if ( jQuery.contains( self[ i ], this ) ) {
                    return true;
                }
            }
        }) );
    }

    ret = this.pushStack( [] );
    for ( i = 0; i < len; i++ ) {
        jQuery.find( selector, self[ i ], ret );
    }
    return len > 1 ? jQuery.uniqueSort( ret ) : ret;
},

Toward the bottom we see a reference to jQuery.find. This function is not self-referential. Rather, there is a function find defined directly on the jQuery constructor function. Remember that in JavaScript a function is an object, and you can attach keys and values to a function just like any other object. The find method in the code above gets attached to the jQuery.prototype object. So if you execute something like $("li").find(...) it calls the method above that is on the jQuery prototype—not the find method defined directly on the jQuery constructor. It does not directly call the Sizzle function.

(Aside: One cool thing about this is that $.find is an alias for directly accessing Sizzle if you have jQuery loaded because $ is an alias for jQuery and jQuery.find is an alias for Sizzle.)

Let’s follow this through and see what happens if selector is a CSS selector. The first check in this method is whether it is not a string, so we can skip that whole block of code inside the if statement. The real meat is in these lines:

ret = this.pushStack( [] );
for ( i = 0; i < len; i++ ) {
    jQuery.find( selector, self[ i ], ret );
}

The definition of ret makes it a new empty jQuery object with a back-reference to the previous jQuery object, which is this. Here jQuery.find is an alias for Sizzle. According to Sizzle’s docs, the signature of this function is Sizzle(String selector[, DOMNode context[, Array results]] ). The third argument results is “an array or an array-like object, to which Sizzle will append results.” Therefore, in this for loop we search for all elements matching the selector in each context—which is each of the DOM objects in the current jQuery object—and everything we find gets appended to ret. In the end, this function applies some kind of sorting algorithm to ret and then returns it. Therefore, ret is a jQuery object and it contains all the elements matching the selector within the context of the previous jQuery object. The results are all aggregated into one jQuery object even though they come from different contexts.

Just to reiterate for clarity, Sizzle returns an array of raw DOM objects, and these are the objects that get pushed into the jQuery object. This is where we finally know that the numbered elements in a jQuery object like $("li") are raw DOM objects. That is what we expect from the docs, but now we actually know that that’s what they are: $("li")[0] isn’t a DOM object wrapped in some other object. It’s a direct reference to a raw DOM object.

In this post we’ve learned a little more about how jQuery objects are constructed under the hood. Maybe we even learned a little more about JavaScript’s constructor functions and prototypal inheritance. We definitely learned a useful difference between the get method and the eq method. And now we know how to use ugly chains of jQuery using the end method, and we see that that works by using a linked list underneath. I’d say this is enough for a day. More to come later.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s