This is part three of the series javascript simplified. This entry takes what we learned from the previous two posts to enable us to create our own version of LINQ in javascript.
Those of you reading this that come from a C# background will be familiar with a language enhancment added to .NET in 2007 by Anders Hejlsberg and Peter Golde, called Language Integrated Query (LINQ). I find the way that LINQ was integrated into the language quite interesting, especially when you compare it to what has been possible with javascript.
In order for LINQ to work, it needed to allow for a callback to occur. Since C# is a typed language, a new construct called a delegate needed to be added to the language. Simply put, a delegate defines a type that represents the blueprint for the code it will point to. Defining the types is burdomsome. Just like naming functions was annoying in the previous blog entry. To make the developers life better, javascript introduced fat-arrows, whereas, C# introduced lambda expressions.
customers.Where(c => c.Name == "Bilbo"); //C# - lambda expression
customers.filter(c => c.Name == "Bilbo"); //javascript - fat-arrow syntax (filter introduced in ES5)
I find it interesting how similar the syntax ended up being.
The other piece to the puzzle behind the magic of LINQ is the ability to extend objects of a particular type by simply importing the LINQ namespace. The ability to do this was added to the C# language through Extension Methods. Just by importing a namespace, C# will add methods to specific types, without the need to change the original underlying code for those objects. With LINQ, the methods are added to the IEnumerable objects.
using System.Linq; //this statement will extend the IEnumberable objects in the code below to include new LINQ methods
myList.Where(x => x.Id == 2); //myList gets all the extension methods from LINQ
As we learned previously, javascript offers a similar capability in the defining of prototypes. In that article we used our own custom Person object, but there is nothing stopping us from extending a built-in javascript object.
Lets add a method the the Array object.
Array.prototype.myMethod = function() {
console.log(this.length);
}
var a = [1,2,3];
a.myMethod(); //outputs 3
The final touch would be to defined the prototype methods in their own js file. This would allow us to "extend" the array object by referencing the script file, just like importing the System.Linq namespace.
CAUTION: One thing that we need to be careful of is not defining methods that would be utilized by the language in an upcoming version of javascript. Therefore, this technique of adding prototypes to built-in types should be thought through carefully. For my examples here I am using ProperCasing in the method names (starts with a capital letter). This will minimize the chances of a conflict as most javascript uses camelCasing.
With this information in hand, lets set out to create our own version of LINQ for javascript.
For all the examples we will need some sample data.
Lets create an array of objects to work with.
var heros = [
{ id: 1, firstName: 'Samwise', lastName: 'Gamgee'},
{ id: 2, firstName: 'Bilbo', lastName: 'Baggins'},
{ id: 3, firstName: 'Frodo', lastName: 'Baggins'},
];
In LINQ, we would use a .Where method to find only the entries in the array that match our criteria. So, we need our new method to be able to
Lets write this to only look for heros with the last name of Baggins.
Array.prototype.WhereLastName = function(name) {
var ret = []; //create array to return
for (var i = 0; i < this.length; i++) { //loop through each item
if (this[i].lastName == name) { //check the lastName property against passed in value
ret.push(this[i]); //if matches, add it to our return array
}
}
return ret; //return matches to caller
}
console.log(heros.WhereLastName('Baggins')); //outputs [ { id: 2, firstName: 'Bilbo', lastName: 'Baggins' }, { id: 3, firstName: 'Frodo', lastName: 'Baggins' } ]
While this works, it is rather limiting. This is because it makes the assumption that the array will be a specific object that has a lastName.
What we really desire is to "delegate" our work of determining a match off to some other piece of code.
We already learned that C# uses delegates (typed anonymous function) for this. We also learned that javascript has the ability to pass anonymous functions as well using callbacks.
So lets modify our method to pass in a callback. Internally it will delegate the responsibility of determining a match to that callback.
Array.prototype.Where = function(callback) {
var ret = []; //create array to return
for (var i = 0; i < this.length; i++) { //loop through each item
if (callback(this[i])) { //call the callback and let it tell us if there was a match or not
ret.push(this[i]); //if matches, add it to our return array
}
}
return ret; //return matches to caller
}
console.log(heros.Where(function(item) {
return item.lastName == 'Baggins';
})); //outputs [ { id: 2, firstName: 'Bilbo', lastName: 'Baggins' }, { id: 3, firstName: 'Frodo', lastName: 'Baggins' } ]
We are almost at our ideal code. Lets substitute out our function for a fat arrow.
console.log(heros.Where((item) => {
return item.lastName == 'Baggins';
})); //outputs [ { id: 2, firstName: 'Bilbo', lastName: 'Baggins' }, { id: 3, firstName: 'Frodo', lastName: 'Baggins' } ]
Finally, there is another shorthand allowed. When our method has a single parameter, we don't need to specify the surrounding parenthesis on the input. Additionally, when there is a single line that returns the result, we don't need to specify the return keyword nor the surrounding curley brackets.
console.log(heros.Where(item => item.lastName == 'Baggins' )); //outputs [ { id: 2, firstName: 'Bilbo', lastName: 'Baggins' }, { id: 3, firstName: 'Frodo', lastName: 'Baggins' } ]
NOTE: ES5 introduced a filter method to the array type. You should use the filter method if your application is targeting ES5 or later.
Now that understand these key concepts, writing the other commonly used LINQ extensions should be a breeze.
Array.prototype.Select = function(callback)
{
var ret = [];
for (var i = 0; i < this.length; i++)
ret.push(callback(this[i]));
return ret;
}
console.log(heros.Select(h => h.firstName)); //outputs [ 'Samwise', 'Bilbo', 'Frodo' ]
NOTE: ES5 introduced a map method to the array type. You should use the map method if your application is targeting ES5 or later.
Array.prototype.OrderBy = function(callback)
{
return this.sort((a, b) =>
{
var x = callback(a);
var y = callback(b);
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});
}
console.log(heros.OrderBy(h => h.lastName)); //outputs [ 'Baggins', 'Baggins', 'Gamgee' ]
Array.prototype.FirstOrDefault = function()
{
return this.length > 0 ? this[0] : null;
}
console.log(heros.FirstOrDefault()); //outputs { id: 1, firstName: 'Sam', lastName: 'Gamgee' }
A cool side effect here is that since most methods return an array we can combine our methods, just like in LINQ.
console.log(
heros
.Where(h => h.lastName == 'Baggins'})
.Select(h => h.firstName)
.FirstOrDefault()
); //outputs Bilbo
This article demonstrates how we can use the building blocks from earlier articles to create interesting new functionality. Check back later for more articles in this series 😉.