Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Mehul Saiya
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Mehul Saiya ( @mehulsaiya )

Exploring The Triple Equals (===) Operator In Lucee CFML 5.3.4.77

By on
Tags:

CAUTION: I am incorrect about what is happening with regard to Simple Values in my write-up -- the Lucee Documentation is, apparently, also wrong. See Brad Wood's comments below and his link to a demo showing that the triple equals operator (===) always does memory reference comparisons (I only thought this was true for Complex values). Thanks Brad!!


A few weeks ago, I created a function that proxies the dump() function, making it safe for complex objects that may include circular-references. In that post, I made use of a Lucee-only feature, the triple equals (===) operator. Since this operator isn't a part of the common history of ColdFusion, I thought I should take a minute and just look at how it works in Lucee CFML 5.3.4.77.

The triple equals operator is documented as being relevant to simple, castable values:

Returns true if operands are equal in value, and are of the same type, e.g. 1 === "1" is false, but 1 === 1 is true.

However, the triple equals operator also works with complex values (such as Struct, Arrays, Queries, and Java objects). Only, the rules are slightly different.

Triple Equals Operator and Simple Values

When it comes to simple values like Strings, Numbers, Booleans, and Null, the triple equals operator works like it does in languages like JavaScript: it tests to make sure that the given references are the same value and type. This has nothing to do with memory locations, since simple values are always copied by value, not by reference.

So, if we run this:

<cfscript>

	// Two different memory locations, but same "value".
	a = "test";
	b = "test";

	dump( a === b );

</cfscript>

... where a and b are different variable references but have the same value and type, we get the following output:

True

The same works for numeric values:

<cfscript>

	// Two different memory locations, but same "value".
	a = 4;
	b = 4;

	dump( a === b );

</cfscript>

... which gives us:

True

It's not until we get the same value and different data types that we see some interesting behavior. Here, we're going to compare a numeric value to a string value:

<cfscript>

	// Two different memory locations and DIFFERENT VALUES.
	a = 4;
	b = "4";

	// The double-equals will cast simple values "as needed" for comparison.
	dump( a == b );

	// The triple-equals WILL NOT CAST SIMPLE VALUES for comparison.
	dump( a === b );

</cfscript>

This time, we get the following output:

True
False

When using the double equals (==) operator, the 4 and the "4" are equal, despite being different data types - Lucee automatically casts them for comparison. However, we can see that when using the triple equals (===) operator, these two values are not identical because they are different data types.

Triple Equals Operator and Complex Values

When it comes to complex values like Structs, Arrays, Queries, and Java Objects, we've never been able to use the double equals (==) operator. Attempting to do so would result in a casting error like:

Can't cast Complex Object Type Array to String - Use Built-In-Function "serialize(Array):String" to create a String from Array.

However, we can use the triple equals operator. And, doing so checks to see if the operands reference the same location in memory; or, in other words, checks to see if the two variables point to the same "object".

Unlike with simple values, the triple equals operator doesn't care about the "value" of a complex object:

<cfscript>

	// Two different memory locations, but same "value" (loosely speaking).
	a = [];
	b = [];

	dump( a === b );

	c = {};
	d = {};

	dump( c === d );

</cfscript>

Here, we are creating set of variables that are different objects, but that represent the same "value". However, when we run this, we can see that they are not identical:

False
False

Where it gets exciting is when we have multiple variables that reference the same location in memory!

<cfscript>

	// Two different variables, but same MEMORY LOCATION (via pointer).
	a = [];
	b = a;

	dump( a === b );

	c = {};
	d = c;

	dump( c === d );

</cfscript>

This time, the triple equals operator gives us:

True
True

... because the two different variables (being compared) are referencing the same object in memory!

This even works for Java objects:

<cfscript>

	// Two different memory locations, but same "value" (loosely speaking).
	a = createObject( "java", "java.util.regex.Pattern" ).compile( "\bhello\b" );
	b = createObject( "java", "java.util.regex.Pattern" ).compile( "\bhello\b" );

	dump( a === b );

	// Assign to be reference to same memory location.
	c = a;
	d = a;

	dump( c === d );

</cfscript>

In the first test, we're attempting to compare "value"; then, in the second test, we're attempting to compare object references. Which gives us:

False
True

To be honest, I don't have a lot of uses-cases for the triple equals operator in ColdFusion. But clearly, I have at least one use-case; and, it's great to know that it's a tool in the Lucee CFML toolbox when I need it.

Want to use code from this post? Check out the license.

Reader Comments

45 Comments

I actually hate how Lucee implemented === because they followed how Java works, not how JS works. In Lucee, the === operator is only checking to see if both operands are the same object on heap, not if they are the same type. I put this ticket years ago because I think most people misunderstand it

https://luceeserver.atlassian.net/browse/LDEV-1282

Your link to the Lucee docs I think might actually be wrong and your first example where you set two strings may only be the same due to a compiler optimization that re-uses the same string. Unless, of course, the behavior was changed at some point, but my ticket was never addressed. I'll double check internally and see if Micha can comment on whether those docs are even correct.

45 Comments

An update, I don't think your examples showing the number and string type comparisons are actually working the way you think. Like I said above, I believe they only return true in your tests because the Lucee compiler is smart enough to re-use the same object in memory behind the scenes. Look at this example that "tricks" lucee into using different variables in java to store the same number and string

https://trycf.com/gist/2effd5850860f9f0ec56fb4a99780475/lucee5?theme=monokai

The === returns false in my version for these since they are not the same exact variable in memory. You can see I'm outputting the system identity hashcode to show that your examples are actually re-using the same variables, but if you have to strings with the same internal value declared as different variables, === no longer does what you think it does!

15,665 Comments

@Brad,

Oh, that's crazy!! So, the Lucee Documentation is wrong then as well; cause, they definitely make it sound like it's checking based on Type. But, from your demo, that is clearly not true!

I will add a CAUTION to the top of my post. Thanks for pointing this out! Bananas!

15,665 Comments

@Brad,

Thanks again -- I've added a cautionary note to the top of the post. So, it seems that === is only good for complex object comparisons (when my intent is test if they are the same object). I'm not sure why that would ever be relevant for simple values. Hmm.

45 Comments

Yep, it's pretty frustrating that it was implemented in a confusing manner. I had a long discussion with Micha in Slack prior to entering that ticket 3 years ago and he disagreed with me at the time that there was anything wrong with Lucee's implementation. I'm like you, I literally see no use for it as-is. It's a dead feature, and worse a confusing one that has no doubt caused bugs for people. As a loosely typed language, CFML needs to follow JS's lead on this.

426 Comments

I thought that:

===

Checked type & value? Both have to be equal to pass the test. In Lucee, does it only check type?

And, can someone explain why Lucee's implementation is broken. I cannot see some of Brad's test on my mobile phone. TryCF is a little broken on the mobile.

426 Comments

Is Brad saying that for:

===

To evaluate to true for complex objects, you would need to do something like this:

foo = {
  a: 1,
  b: 2
};

bar = {
  a: 1,
  b: 2
};

if ( foo === bar ) {
  // true
}

Rather than:

foo = {
  a: 1,
  b: 2
};

bar = foo;

if ( foo === bar ) {
  // true
}

15,665 Comments

@Charles,

So, it turns out that the === operator, at least the way it is currently implemented, is only checking for identity. Meaning, it only check to see if the two operands are literally the same reference in memory. So, it doesn't really have anything to do with "type" or "value", just references.

To recreate part of Brad's demo, consider this:

<cfscript>

	a = "foofoo".left( 3 );
	b = "foofoo".right( 3 );

	dump( a == b ); // TRUE
	dump( a === b ); // FALSE

</cfscript>

Here's we're getting a and b to be the same "value"; but, different references in memory (since Lucee can't optimize the difference away). And, when we try to compare the two values via === it is false.

Really, the only reason I would use === at this point is to compare complex object references.

426 Comments

Thanks for this explanation. Actually, this might come in useful from time to time, to check the reference origin of a complex object, although I am sure this isn't how it is meant to work!

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel