Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Edilson Martins and Paulo Evaristo Rosa Dias
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Edilson Martins Paulo Evaristo Rosa Dias

Closure Variable-Access Changes With Function Expressions vs. Function Declarations In Lucee CFML 5.3.3.62

By
Published in Comments (2)

Yesterday, I came across an interesting Closure behavior in Lucee CFML. It seems that the way in which you define a Closure completely changes the variables that the Closure has access to (ie, the variables that it "closes over"). Specifically, function expressions close-over the expected variables whereas nested function declarations don't seem to close-over any variables in Lucee CFML 5.3.3.62.

If you come from the world of JavaScript, the concept of function expressions and function declarations is quite common. But, in the world of CFML and ColdFusion, where Closures are a relatively recent addition, these two modes of Function creation might not be obvious.

A function declaration is a stand-alone Function definition. A function expression is the defining of a Function as part of a larger expression. So, for example, this is function declaration:

  • function foo() {}

... which declares the Function foo and stands on its own. And, these are all function expressions:

  • var foo = function() {};
  • return( function() {} );
  • values.each( function() {} );
  • var callbacks = [ function() {}, function() {} ];
  • (function() {})();

... which are all defining Functions as part of a larger expression.

In JavaScript, there isn't really much of a difference between these two approaches. A function declaration always has a name is hoisted as part of the two-phase compilation in the JavaScript runtime; but, both approaches "close over" the same set of variables (lexical scoping is one of the most magical features of JavaScript).

ASIDE: "Hoisting" is the process by which the declaration of a value is moved-up to the top of an execution block such that it can be referenced before it is defined. Both "variable declarations" and "function declarations" are hoisted in JavaScript.

In ColdFusion, function declarations are also hoisted. And, historically, function declarations have never closed over variables - they are given access to various scopes at invocation time depending on the target of the invocation. This is why ColdFusion provides functions like invoke() and allows Function references to be copied from one scope to another.

However, if function declarations are nested, they are neither hoisted nor do they appear to "close over" any of the variables that you would expect to have access to within a "closure". To see this in action, I've declared a Function that defines three closures using three different techniques:

Each of these closures will be tested for access to the parent function's arguments scope and the parent function's local scope:

<cfscript>

	testScopeAccess( userID = 123 );

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	public void function testScopeAccess( required numeric userID ) {

		var localID = 456;

		var fatArrowExpressionCallback = () => {

			echo( "Fat-Arrow Expression Callback <br />" );
			echo( "----> UserID exists: <b>#isDefined( 'userID' )#</b> <br />" );
			echo( "----> localID exists: <b>#isDefined( 'localID' )#</b> <br />" );
			echo( "<br />" );

		};

		var functionExpressionCallback = function() {

			echo( "Function Expression Callback <br />" );
			echo( "----> UserID exists: <b>#isDefined( 'userID' )#</b> <br />" );
			echo( "----> localID exists: <b>#isDefined( 'localID' )#</b> <br />" );
			echo( "<br />" );

		};

		function functionDeclarationCallback() {

			echo( "Function Declaration Callback <br />" );
			echo( "----> UserID exists: <b>#isDefined( 'userID' )#</b> <br />" );
			echo( "----> localID exists: <b>#isDefined( 'localID' )#</b> <br />" );
			echo( "<br />" );

		}

		// --

		fatArrowExpressionCallback();
		functionExpressionCallback();
		functionDeclarationCallback();

	}

</cfscript>

As you can see, all three callbacks have the same logic: they check for the existence of a variable in both the arguments scope and the local scope of the parent context. And, when we run this ColdFusion code in Lucee CFML 5.3.3.62, we get the following output:

Scope access behavior demonstrated across closures defined via Function Expressions and Function declarations in Lucee CFML 5.3.3.62.

As you can see, both the Fat-Array function expression and the traditional function expression have access to the expected scopes: they have successfully "closed over" the variables. However, the function declaration, that is nested within the parent function declaration, is not acting as a closure and therefore does not "close over" the variables.

NOTE: This nested function declaration isn't really acting like a "function declaration" either as it is not being hoisted (which is not demonstrated in this test). So, in a way, it is neither a closure nor a declaration - it's kind of the worst of both worlds.

The main take-away here is that a Closure in Lucee CFML only acts as a Closure if it is defined as part of an expression. Function declarations do not act as closures, and do not exhibit closure behavior, even if they are defined within another function.

Epilogue On Function Declarations

In retrospect, this behavior is consistent with ColdFusion's historical treatment of Function Declarations; in so much as that Function Declarations have never closed-over variables. However, it could be argued that the intent of defining nested function declarations is to use them as closures. Which is exactly what this code was trying to do.

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

Reader Comments

15,848 Comments

@Andrew,

The plot thickens! I'll see if I can find a Lucee ticket about this then. Maybe someone is already tracking it.

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