I'm baffled that this little issue has never cropped up for me until now, but I recently discovered a little annoyance in ServiceNow while iterating through an array. This issue had me going round in circles for hours, so hopefully by sharing my findings with our readers, I can spare some folks the frustration I felt.
First, I'll tell you a little story about how it might impact you, and then I'll tell you the explanation for this odd behavior. If you just want to skip to the explanation, scroll to the end.
I was building a fairly complex multi-table query that would return the sys_ids of a few thousand records in one table, based on matching criteria in several other tables.
So there I was, gleefully laying down code in my development instance. As I went along, I was pushing all of the relevant sys_ids into one of two arrays, based on some long encoded query containing my criteria. It might have looked something like this:
var gr1Array = []; var gr1 = new GlideRecord('table_1'); gr1.addEncodedQuery('SomeEncodedQuery'); gr1.query(); while (gr1.next()) { gr1Array.push(gr1.getValue('u_field_name')); //Get some value and push it to an array } var gr2Array = []; var gr2 = new GlideRecord('table_2'); gr2.addEncodedQuery('SomeOtherQuery'); gr2.query(); while (gr2.next()) { gr2Array.push(gr2.getValue('u_other_field')); //Get another value and push it to a separate array }
Just to give you a quick explanation, I'm creating 2 glide records (gr1 and gr2) and filtering them based on two separate encoded queries (SomeEncodedQuery and SomeOtherQuery). Then I'm using "while (gr1.next()) { }" to iterate through every record matching that query, and using ".push()" to add values to my arrays.
I eventually reached a point where I needed to compare the values in these two arrays. If there were any duplicates (values that exist in both arrays), I wanted to push them into yet a third array. That might look something like this:
var bothArray = []; var gr1Array = []; var gr1 = new GlideRecord('table_1'); gr1.addEncodedQuery('SomeEncodedQuery'); gr1.query(); while (gr1.next()) { gr1Array.push(gr1.getValue('u_field_name')); //Get some value and push it to an array } var gr2Array = []; var gr2 = new GlideRecord('table_2'); gr2.addEncodedQuery('SomeOtherQuery'); gr2.query(); while (gr2.next()) { gr2Array.push(gr2.getValue('u_other_field')); //Get another value and push it to a separate array } for (var i = 0; i < gr1Array.length; i++) { //Go over each element in gr1Array if (gr2Array.indexOf(gr1Array[i]) >= 0) { //Every element in gr1Array that exists in gr2Array... bothArray.push(gr1Array[i]); //Push to bothArray. } }
As you can see on line 16, I set up a 'for' loop to iterate through every element in gr1Array. Inside that for loop on line 17, I have a condition. If the index of the element we're currently checking in the for loop (gr1Array[i]) exists in gr2Array, then on line 18, we do bothArray.push() to add gr1Array[i] to bothArray.
The condition is this:
gr2Array.indexOf(gr1Array[i]) >= 0
This condition actually runs a function; a method of the Array class, called "indexOf". indexOf returns an integer value corresponding to the index in the array where the thing you're looking for exists. For example, the following code should log the integer 2 to the console (F12):
var arr = ['a', 'b', 'c', '123', 'xyz']; console.log(arr.indexOf('c'));
This is because the string 'c' is in the third position, and arrays (like just about everything else in programming) use a zero-based index - meaning that 'a' is in position 0, 'b' is in position 1, 'c' is in position 2, and so on.
However, if what you're looking for is not found in the array, .indexOf() will return -1. So the only time it'll return a number less than zero (a negative number) is when it fails to find the element at all. Therefore, if .indexOf() returns a number greater than or equal to zero (or in comparator terms: >= 0 ), then we know that the element we're looking for exists in the array.
Note: This example is meant to be friendly and familiar even to new developers, but Using .indexOf() to identify "collisions" within an array is something of a blunt tool. Not wildly inefficient, but also not the preferred method for getting a list of elements that exist in each of two arrays. For more info, see the bottom of this post.
Pretty cool, right?
Well, in ServiceNow, this doesn't work.
At least, it doesn't work in server-side scripts. If you run this code in a client-side script, then it's your browser's javascript engine that processes the code; not ServiceNow's. If you run the code in a server-side script such as a background script or business rule, it'll fail and return undefined every time.
Why is that?
Pro-Tip: Enabling security admin allows you to use "Scripts - Background", type or paste in your code, and execute. However, background scripts run server-side -- what if you want to run a client-side script (say, for testing or manipulating a form)?
As an Admin, visit any form and or list and press CTRL+SHIFT+J. You'll get a handy little client-side javascript executor. You can paste any client-side code in there, and click 'Run' to execute your code on that page. You'll have access to all of the client-side objects you're familiar with from Client Scripts and UI Actions, including g_form, g_user, and so on.
Turns out, the reason is that ServiceNow overrides the JavaScript default Array class, and the methods thereof, including indexOf(). Click here to see the wiki article on this.
Now that we know that, we can work around it and still get the results we're looking for by instantiating ServiceNow's ArrayUtil class, and using its' methods as directed in their documentation. Check it out below:
var arr = ['a', 'b', 'c', '123', 'xyz']; var arrUtil = new ArrayUtil(); console.log(arrUtil.indexOf(arr, 'c'));
The above code really will log the integer 2 to the console.
So, knowing this, we can now modify our original code from above:
var arrUtil = new ArrayUtil(); var bothArray = []; var gr1Array = []; var gr1 = new GlideRecord('table_1'); gr1.addEncodedQuery('SomeEncodedQuery'); gr1.query(); while (gr1.next()) { gr1Array.push(gr1.getValue('u_field_name')); //Get some value and push it to an array } var gr2Array = []; var gr2 = new GlideRecord('table_2'); gr2.addEncodedQuery('SomeOtherQuery'); gr2.query(); while (gr2.next()) { gr2Array.push(gr2.getValue('u_other_field')); //Get another value and push it to a separate array } for (var i = 0; i < gr1Array.length; i++) { //Go over each element in gr1Array if (arrUtil.indexOf(gr2Array, gr1Array[i]) >= 0) { //Every element in gr1Array that exists in gr2Array... bothArray.push(gr1Array[i]); //Push to bothArray. } }
Have you got ServiceNow questions of your own? Click here, or scroll to the top of this page and click "Ask a Question". You'll usually get an answer within 12 hours, and if your question or the answer are particularly interesting, we'll post it to the site!
This has been an example of usage meant to be familiar for new ServiceNow developers. However, as Christoph Lang points out in the comments below, there are far more efficient means by which to accomplish the goals outlined in my example use-case above. You might use Array.prototype.filter(), or even ArrayUtil.prototype.intersect(). Thanks for the Pro-Pro-Tip, Christoph!