Why do we code JXA scripts using closures?

@ComplexPoint Rob -

It seems to be kind of a de facto standard (and if I’m not mistaken, your standard) to code JXA scripts using this pattern:

(function() {
    'use strict';

    // do something

    // return something
})();

Why do we do this?

I have a specific reason for asking. Using the above enclosed module pattern, it’s not really possible to create unit tests, because the code inside the enclosure isn’t accessible to outside testing mechanisms.

For example I’d like to do something like this thoroughly stripped-down example:

(function() {
    'use strict';

    var FileUtils = (function() {
        return {
            fileExists: function(path) {
                var result = this.getFileOrFolderExists(path);
                return result.exists && result.isFile;
            },

            getFileOrFolderExists: function(path) {
                var isDirectory = Ref();
                var exists = $.NSFileManager.defaultManager
                    .fileExistsAtPathIsDirectory(path, isDirectory);
                return {
                    exists: exists,
                    isFile: isDirectory[0] !== 1
                };
            }
        };
    })();

    function execute(FileUtils) {
        return FileUtils.fileExists("test.txt");
    }

    try {
        return execute(FileUtils);
    } catch (e) {
        return "Error: " + e.message;
    }
})();

The method I’d like to test is “execute”. I’d pass in a mock “FileUtils” object, which will would act appropriately for whatever functionality I’d like to test.

But of course I can’t get at the “execute” method, because of closure rules - the fact the the entire anonymous function is enclosed in parens.

Thoughts? Ideas? Thanks.

This appears to work, and returns the appropriate values:

'use strict';

var FileUtils = (function() {
    return {
        fileExists: function(path) {
            var result = this.getFileOrFolderExists(path);
            return result.exists && result.isFile;
        },

        getFileOrFolderExists: function(path) {
            var isDirectory = Ref();
            var exists = $.NSFileManager.defaultManager
                .fileExistsAtPathIsDirectory(path, isDirectory);
            return {
                exists: exists,
                isFile: isDirectory[0] !== 1
            };
        }
    };
})();

function execute(FileUtils) {
    return FileUtils.fileExists("test.txt");
}

try {
    execute(FileUtils);
} catch (e) {
    "Error: " + e.message;
}

You can shed the outer module wrapping for the purpose of unit tests, subject to a couple of checks and cautions.

For production, the general issue is JavaScript has an unprotected and often heavily over-populated and unexpectedly persistent global namespace, so roping off a clear (local) space simply avoids name clashes.

The overpopulated name space is often less of an issue in the Script Editor / osascript JS context than in a browser context, but:

  1. Wrapping in a module still makes the Safari Debugger much more usable, simply because the local (module) variables then appear in their own list, where they are easily found.
  2. Name bindings in the global context can persist between script runs (within a single Script Editor session, for example), so outside a module you may get bitten if you assume that each script run has a fresh namespace context.

For unit testing, simply:

  • drop the module enclosure
  • check that there are no name clashes in the global context
  • check that your tests are not affected by the persistence of name-binding between runs in the global context
1 Like

Those are very compelling reasons. I knew there was method to your madness.

So if persistence is possible, can you give me an example to test with? My concern is that when running unit tests, I might develop the same issue, so I’d like to check that out.

Thanks.

The first thing that comes to mind is that if you evaluate a script (in Script Editor) consisting of the single word:

this

you will see the objects already in the namespace.

Any settable properties of any of those objects are candidates for persistence between sessions.

Cool. Thanks. I can work with that.

More simply, if you don’t recompile, all names in the global namespace are liable to persist by default.

Try repeatedly running this script several times successively (without recompilation) for example.

(Just clicking repeatedly on the run button in Script Editor)

var a = a && (a * 2) || 2

a

whereas

(function() {
    'use strict';

    var a = a && (a * 2) || 2

	return a;

})();

always returns the same result

Do you think that would be true if run from KM via “Execute a JXA” action? How about from Atom, which issues “osacompile”?

Always better to experiment than to speculate, so I’ll say ‘not sure’ :slight_smile:

( Give us a report )

1 Like

I’m trying to decide right now whether this particular process is worth testing or not.

Sometimes, I think automated tests are more trouble than they’re worth, and often can’t or don’t test for the kinds of actual errors that occur in the real world.

OTOH, there’s nothing like the feeling of seeing “56 Tests Passed”. :slight_smile:

1 Like

As a footnote, for a more graphic insight into the persistence of the global namespace, you can repeatedly click Run (in Script Editor) on a script which returns this as its final value:

Isn't this the same behavior as AppleScript properties, which will persist across executions unless the script is compiled?

These JavaScript variables in the global namespace seem to all persist in my limited testing. So it means we could do stuff like this:

'use strict';

if (!ptyScript) {

  //--- SET DEFAULT SCRIPT PROPERTIES ---

  var ptyScript = {
    Name: 'My Script',
    LastRun: new Date(),
    NumRun: 1
    }
}
else {
  ptyScript.LastRun = new Date();
  ptyScript.NumRun = ptyScript.NumRun + 1
}  

ptyScript

The updated values of AppleScript properties are actively saved into the .scpt file

( The JavaScript global state is not, so it persists across runs in one session, but doesn’t persist across sessions )

Good point.

Just tested in KM, and every run of the Macro is apparently a new session, since it returns the same default values for ptyScript each time.

So, there is no utility in JavaScript persistent variables.
But doesn't this mean that, if using in KM, there is no issue as well?

That is true, but it's not that hard to get variable data just by a mouse-over:

In KM:

  • you would still hit the persistence issue with Execute JavaScript in Safari | Chrome actions
  • you would still benefit from a module enclosure while developing actions - particularly in any use of the Safari Debugger, and also generally for name-clash safety

Run this several times.kmmacros (19.8 KB)

1 Like