• Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
    Dedicated community for Japanese speakers
  • 한국 커뮤니티
    Dedicated community for Korean speakers
Exit
2

Splitting code into files, scopes, etc.

Participant ,
Jul 20, 2023 Jul 20, 2023

Copy link to clipboard

Copied

I keep experiencing weird, apparently-inconsistent behavior. It seems there's something fundamental I don't understand about scripting for InDesign using JavaScript.

 

A few questions, please:

 

1. When you #include one .jsx file in another, what does that mean, exactly? Is it equivalent to copying the content (the code) of one file and pasting it in another? If not, what then?

 

2. Is there a concept of namespace? If yes, how do you use them? If not, how do you avoid name clashing?

 

3. I want one script to construct an object and another script to use it. What are the possible ways to do so? What's the recommended way?

 

4. Sometimes when I declare a variable and assign it a value (e.g. "var x = 1;") outside any block (and in particular outside any function) and then use it in a function, even in the same file, it's undefined. Other times it's defined and has the assigned value. What decides if it's defined or not?

 

5. Considering a single file, does it matter, in any way, where in that file and in which order functions are declared and defined? What about more than one file?

 

6. Is there any mechanism of caching when it comes to running scripts? If so, is there a way to clear all caching?

 

7. If a script doesn't declare any target engine, is there any mechanism of persistence?

 

8. Is there some standard logging mechanism? What's the best way for a script to keep a log?

 

Thanks!

TOPICS
Scripting

Views

446

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Jul 21, 2023 Jul 21, 2023

Copy link to clipboard

Copied

Lots to unpack here. Not sure these are the answers you're looking for but will give it my best shot. 

 

1. Yes, typically the #include file contains one or more objects with functions and properties. You can then reference those methods and properties in the included file. 

 

2, 3 + 4. I'd suggest reading up on anonymous and anonymous self-executing functions in Javascript, as well as how JS handles scoping (global, function and block). There's no namespace, per se, but anonymous funcs allow you to avoid namespace conflicts by wrapping objects, variables, etc. within a function. 

 

5. Reading up on scoping should answer this question. Short answer, it depends. If you take a strictly functional approach to the code, including wrapping your main executable in, say, a main() function, then no, it doesn't matter. Within objects, ordering can depend on how the object is written and scoped. 

 

6. Can you explain a bit more what you mean? 

 

7. Generally, there is no persistence after a script finishes executing. You can read up on InDesign startup scripts and event handlers to always have a script running in the background (this does require a target engine), or you can write and read to files, and/or label properties (app.label, page.label, etc) to recall for later execution. 

 

8. You can use $.writeln(msg) in your script to log to console. There are also ways to create and write to a txt file using the File object. 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Jul 22, 2023 Jul 22, 2023

Copy link to clipboard

Copied

Hi @orib7317974,

 

To complete Brian's excellent summary:

 

I. On Lexical Scoping in ExtendScript

 

• A script runs within a specific engine that somehow defines a namespace, but that's an informal designation—memspace would probably be much more appropriate—because engines do not work together. Every script runs in its own engine, which by default is non-persistent and has the name "main", as reported by $.engineName. You can also set up a persistent engine using the directive #targetengine "myEngineName". Then your script can recover, from one execution to another, the lexical scope, assigned data and references as you left them. That's the most popular way to save/restore data or settings within an InDesign session. Persistent does not mean permanent. (We sometimes use the term session-persistent to clarify, by contrast with a few application-persistent entities that can still be scoped at a higher level: see the menu structure(s), or methods like app.insertLabel/extractLabel, $.setenv/getenv, etc.)

 

• Given the working engine of your script, assume that anything declared or done outside of an explicit function body is somehow processed from an implicit, global, unnamed function that encompasses the entire program. (The jsxbin encoding shows that things are handled exactly that way.) That hidden function determines the top-level lexical scope, which is often referred to as the [[global]] scope and can be (partially) dissected from the $.global object.

 

• In concrete terms, any identifier declared and/or assigned in the [[global] scope is to be referenced in the $.global object, but there are a few syntactical rules to assimilate well. (By the way, those rules work the same in lower lexical levels.) The most important one (which you already know for sure) is that every assignment or use of an undeclared identifier involves the [[global]] scope. Hence you need const… or var… to specifically apply a scope restriction to the current function body (including inner function(s) when a closure is involved). But another crucial rule is this: the line

    var x = 123;

represents two instructions that are completely separated in space. What the interpreter will see, in fact, is:

  1. var x; [at the top of the function body, declaring the identifier in the lexical scope and presetting its value to undefined]
  2. x = 123; [at the exact location where the line “var x = 123;” is written, which then performs the assignment]

 

• As a result, when you're writing code in the [[global]] scope, there is a very fine distinction between

   var x = 123;

and

   x = 123;

 

In both cases the variable will indeed exist [[globally]]. However, it is “known to exist” at any point of your code only in the first case, because the “var x;” declaration is implicitly read prior to any assignment, making $.global.x already set to undefined. So the following code will not throw an error (despite the post-assignment):

 

    alert( myVar ); // => undefined
    var myVar = 123;

 

while this code will fail:

 

    alert( myVar ); // => RUNTIME ERROR: "myVar is undefined"
    myVar = 123;

 

You could check as well that $.global.hasOwnProperty('x') is true in the first case, false in the 2nd case.

Of course these examples are stupid, because their brevity makes them appear stupid. But in a more complex program with nested functions and so, this may explain why something like (function testVar(){ alert(myVar) })() can produce an error whose origin no longer appears with the same obviousness.

 

• Along the same lines, a function declaration (as opposed to a function expression) is implicitly registered and processed at the beginning of the scope. To quote the ECMAScript standard, at the top level of a function, or script, function declarations are treated like var declarations rather than like lexical declarations”.That's the reason why this works,

 

    myHello(); // OK
    function myHello(){ alert("hello") }

 

while that does not:

 

    myHello(); // RUNTIME ERROR: myHello is not a function
    var myHello = function(){ alert("hello") };

 

The second case—throwing a runtime error—is interesting to analyze. As I explained, the identifier myHello is indeed declared upstream (in the scope), but still set to undefined when the statement myHello(); is encountered. And the fact that the (anonymous) function to be assigned (line 2) does not exist yet comes from the syntax “…=function(){…}” which forcibly introduces a function expression by now way enjoying the attributes of a function declaration.

 

→ These aspects are extensively documented in the ECMA-262 specification (3rd edition); ExtendScript—for once—respects those rules from start to finish.

 

II. About Anonymous Functions

 

• An anonymous function is, in all and for all, a Function instance whose name property has been automatically set to "anonymous" because of the functional expression which it emanates from. Although this characterizes function expressions over function declarations (that syntactically require a name), this is not connected in itself to auto-execution, closure, and similar phenomena.

 

• Moreover, naming a function is always possible—except from calling the Function contructor—and sometimes useful even with function expressions. Compare:

    ( function(){ alert(callee.name) } )(); // => 'anonymous'

and

    ( function myFunc(){ alert(callee.name) } )(); // => 'myFunc'

 

• The function name is a read-only property of the Function object and, when it's not "anonymous", it also becomes an identifier in the particular scope attached to that very function. Anyway, an anonymous function can still refers to itself using the callee reference.

 

• I think that the most common confusion in this field is the mixture that some developers make between the name of a function (which locally becomes an identifier) and the possible reference(s) to that object (via other identifiers in the outer scope). In

    const myRef = function myFunc(){…};

or

   $.myProp = function myFunc(){…};

myRef and $.myProp bear external references to a function whose internal name is “myFunc” (not “myRef” or “MyProp”.)

 

• One could simply note that in the case of a regular function declaration, the name property and the lexical identifier seem to play the same role, though they are in fact of quite a different nature. In the body of a named function, its name-as-identifier most certainly takes precedence over any externally-scoped identifier, but we simply cannot test this hypothesis in the case of a regularly declared function [see next comment]

 

    function myFunc(){ alert(myFunc.arity) } // Here ‘myFunc’ a (very likely) handled as a local identifier

    myFunc(); // Here ‘myFunc’ is necessarily an identifier of the outer scope, including [[global]]

 

Note. — A good reason to think that this assumption is correct, although unverifiable, is that in case of ambiguity local identifiers take precedence over external identifiers.

 

Best,

Marc

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Jul 22, 2023 Jul 22, 2023

Copy link to clipboard

Copied

(…) To correct myself, one can formally prove that the name-as-identifier of a function always takes precedence in the local scope:

 

    // Function declaration ; ‘myFunc’ is a [[global] identifier.
    function myFunc(){ alert( myFunc.name ) }

 

    var tmp = myFunc; // ‘tmp’ is another [[global]] id.
    myFunc = { name:"something else" }; // re-assign the [[global]] id. ‘myFunc’

 

    tmp(); // => "myFunc" 🙂

 

Best,

Marc

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Guide ,
Jul 22, 2023 Jul 22, 2023

Copy link to clipboard

Copied

ExtendScript is decades old. Keep that in mind when you read modern JavaScript literature. Some time later mainstream JavaScript came up with the idea to use function bodies to wrap up other function definitions in order to keep them out of the global namespace. Typical keyword to avoid is "closures". Modern JavaScript engines can handle this, ExtendScript will leak memory. You want to use "prototype" style programming- even if considered "old fashioned".

 

e.g., very simplified:

 

function MyBadClass()

{

var myVar = 123;

this.getMyVar = function () { return myVar; } 

this.setMyVar = function (a) { myVar = a; } 

this.method1 = function () {doSomething(); }

this.method2 = function () {doSomething(); }

}

 

var myObject = new MyBadClass();

 

function MyGoodClass() {}

MyGoodClass.prototype.myVar = 123;

MyGoodClass.prototype.method1 = function ()  {doSomething(); }

MyGoodClass.prototype.method2 = function ()  {doSomething(); }

 

var myObject = new MyGoodClass();

 

The first way will create an instance of all the methods per object instance. myVar securely stored away in a closure object (ExtendScript calls them Workspace), along Brian's 2,3+4 suggestion. Using prototypes, everybody can access your variable. But how many bad players do you have around?

Closures might be ok for a smallish program running in targetengine "main", to be discarded in entirety after run. When you have lengthy complicated execution with plenty objects that you consider long discarded, you'll be surprised at some point. Especially when your program is in a persistent target engine.

 

The utility method $.summary() will tell you the accumulated totals. It produces a string with the numbers of various objects that garbage collection did not handle.

 

Another issue to avoid : modern JavaScript has a way to scope variables by "let" rather than "var". Convenient until you find out "let" does not exist and just replace it with "var" at the top level execution of your script:

 

if( someCondition ) {

  var temp = 123;

}

 

As Marc explained, this will still introduce a variable at the global scope even if the condition is not met. Same for function accessThisOrThat() {} never to be used again. While such clutter is just annoying to debug unrelated code because it fills up the respective debugger view, it can also lead to subtle bugs because you might use that same temp locally and won't find the forgotten missing var in your own functions because it is too tedious to look at a messy global list.

The most relevant problem is speed though, every other entry in $.global makes your program slower.

 

You'd be in good company though, ExtendScript executes cross "suite" startup code from a folder shared by InDesign, Photoshop, Illustrator and so forth. That's how the globals for these programs come to existence so you can call photoshop.open(File…) . If you find more weird functions and variables in $.global of an empty new targetengine, and want to clean that up, track down those startup scripts.

 

While they share some common copy-paste ancestor, the matching one for InDesign is cleaner.

It only uses global names when they are meant that way, everything else takes place in functions stored at a different level.

 

So, how to reduce the number of global names? By usung objects as namespace. While there is no "namespace" feature in the language, you can do something like this:

 

var MyImageUtility = {};

MyImageUtility.FancyMetadataClass = function () {}

MyImageUtility.FancyMetadataClass.prototype.method1 = function () {}

MyImageUtility.FancyMetadataClass.prototype.method2 = function () {}

 

MyImageUtility.SuperDuperPixelClass = function () {}

 

Total: one global.

 

My use of anonymous functions keeps even the class names out of the globals. ExtendScript does a good job in picking up function names from the first such assignment, as you can see from ESTK stack panel. Unfortunately VSCode takes a different approach and will show them only as anonymous.

 

So this is a question of beauty vs. speed. If you can stand that a script takes 3 seconds instead of 2, then no optimizations needed. If this means you have to double the number of InDesign Server licenses, you'll take the other approach.

 

Writing logs: $.writeln() is the original statement meant for that. Just make sure that it does not happen when the debugger is not attached - it may cost unexpectedly much execution time when InDesign desparately attempts to connect to debugger to deliver that precious "starting execution".

Writing files - sure, it takes also time, and reduces the life of your SSD dependent on the amount of debug output. I'm using the MacOS system log via a plugin/ExternalObject, but that's part of a larger project.

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Participant ,
Jul 23, 2023 Jul 23, 2023

Copy link to clipboard

Copied

@brianp311 , @Marc Autret , @Dirk Becker ,

Thank you very much for providing such comprehensive answers; they exceeded my expectations by far. I really appreciate your efforts!

After reading through your responses, I've already gained a better understanding of certain aspects. I'll need more time to fully process everything. Time and experimenting with coding, that is.

Again, thank you so much!

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Jul 23, 2023 Jul 23, 2023

Copy link to clipboard

Copied

Wow, this is some collection of gems. @Dirk Becker your point of slowness added with each entry into the global space is something I will investigate. I have a set of IDS scripts where the code is humungous around 4k lines in one and 6K lines in other, which I am trying to reduce. However, I have noticed that when I remove some code which might not even be called in the code flow I am testing I see speed drops or ups and I couldn't explain this behaviour. I suppose I can attribute this to global space then. If you have any other pointers/tips to debug this scenario and find areas to debug in the code please share.

Regarding the scoping, decleration etc I would suggest @orib7317974 to read about hoisting in JS, that would be a good read in addition to the already brilliant explanation on this thread. One basic article on the topic with examples is linked below

https://www.programiz.com/javascript/hoisting

-Manan

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
New Here ,
Aug 02, 2023 Aug 02, 2023

Copy link to clipboard

Copied

LATEST
periodically i monitor the ups and downs of my workspace + Function counts in the (After Effects) Info panel, using these regexps:
 

 

writeLn (($.summary().match(/(\d+)\s*\(\s*workspace\s*\)/) || [])[1] + " workspaces");
writeLn (($.summary().match(/(\d+)\s*Function/) || [])[1] + " Functions");

 

Votes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines