JavaScript removing with() expressions


  • 🚽 Regular

    I decided it might be fun to take an old Firefox addon, and maintain and modernize it for Pale Moon. I want to be able to turn on strict mode, but the original author liked to use with(x) to create namespaces, and with() isn't allowed in strict mode.

    I am unsure how to translate that into a different way of doing it, and so far the things I've read haven't really helped.

    Here is the object where the the global namespace is created. I'm afraid I don't know enough to know if there is anything bad about it, or how to change it.

    // Global object for NoSquint.  'NoSquint' is the only name added to the global
    // namespace by this addon.
    var NoSquint = {
        id: 'NoSquint',
        namespaces: [],
        _initialized: false,
        dialogs: {},            // dialogs namespace
    
        ns: function(fn) {
            var scope = {
                extend: function(o) {
                    for (var key in o)
                        this[key] = o[key];
                    }
            };
            scope = fn.apply(scope) || scope;
            NoSquint.namespaces.push(scope);
            return scope;
        },
    
        /* This function is the load handler.  It calls init() on all namespaces
         * previously registered with ns(), which happens for most .js files that
         * are loaded via the overlay.
         *
         * Consequently, init() for each namespace should be kept light so as not
         * to adversely affect load times.
         *
         * Currently initialization takes about 5-10ms with ff4 on my fairly peppy
         * Thinkpad (i7 M 620 2.67GHz), which isn't horrible, but there's room for
         * improvement.
         */
        init: function() {
            if (NoSquint._initialized)
                return;
            NoSquint._initialized = true;
    
            //var t0 = new Date().getTime();
            for (let i = 0; i < NoSquint.namespaces.length; i++) {
                //var t1 = new Date().getTime();
                var scope = NoSquint.namespaces[i];
                if (scope.init !== undefined)
                    scope.init();
                //dump(scope.id + " init took " + (new Date().getTime() - t1) + "\n");
            }
            //dump("Total init took: " + (new Date().getTime() - t0) + "\n");
        },
    
        destroy: function() {
            // Invoke destroy functions in all registered namespaces
            for (let i = 0; i < NoSquint.namespaces.length; i++) {
                var scope = NoSquint.namespaces[i];
                if (scope.destroy !== undefined)
                    scope.destroy();
            }
        }
    };
    
    window.addEventListener("load", NoSquint.init, false);
    window.addEventListener("unload", NoSquint.destroy, false);
    

    Other files mostly follow the pattern of:

    NoSquint.interfaces = NoSquint.ns(function() { with (NoSquint) {
        // Implementation here
    }
    

    Does anyone have any tips I could follow?



  • Assuming that I've understood your question, the starting point could just be to get rid of the with statements and to write out the references to anything from NoSquint in full, i.e.

    NoSquint.interfaces = NoSquint.ns(function() { with (NoSquint) {
    
        // doing something with NoSquint, e.g.
        dialogs.foo = "bar";
        
    }
    });
    

    becomes

    NoSquint.interfaces = NoSquint.ns(function() {
    
        // doing something with NoSquint, e.g.
        NoSquint.dialogs.foo = "bar";
        
    });
    

    That won't significantly improve the code quality (which from the fragment you've provided looks suspiciously cargo-cultish) but could at least get the thing running in strict mode, after which you could begin more drastic refactoring.

    with doesn't do anything particularly clever, it just adds an extra scope for variable look-up. It doesn't create a closure or grant any additional access to an object that wouldn't otherwise be accessible, it's really just a way of saving some typing (adding some nasty ambiguity along the way).



  • When you refer to a name in Javascript, it searches the scope chain for that name. All that with() does is add an object at the top of the scope chain, so that its properties can be accessed without repeating the object name.

    E.g.

    with (Math) {
      console.log(cos(PI));
    }
    

    is exactly the same as:

    console.log(Math.cos(Math.PI));
    

    The with(Math) makes it so that any name you use will automatically reference Math, if such a property exists in it. (Otherwise, the Javascript engine will continue searching the scope chain to find it.)

    Note that the with object is the first place that Javascript will search, so this:

    var PI = 3;
    with (Math)
      console.log(PI);
    

    will log the value of Math.PI (because it exists), not the value assigned to the variable PI in the line directly above. This, on the other hand:

    var PIE = 3;
    with (Math)
      console.log(PIE)
    

    logs the value assigned to the variable PIE, 3. The Math object does not have a property named PIE, so the scope chain is searched further and the variable PIE is found.

    So you can see the advantage and disadvantage of with: on the one hand, your code becomes more succinct, because you don't have to continue repeating the same object name over and over; but on the other hand, it causes the Javascript engine to have to do more work because it has to check in an extra place to find any name that you use, and if misused, it can easily lead to ambiguity that makes the code difficult to read and debug.

    As far as removing the with() expressions, if the code is not overly complex then it is fairly straightforward: simply find every use of a property of the with object, and add the object's name before it explicitly. As long as you know what properties the object has, this is not too difficult.


  • 🚽 Regular

    @anotherusername @japonicus Now that makes a lot more sense. Why couldn't anyone else on the internet have explained it like that...

    I think part of my problem is I come from Java/Kotlin, so it slightly baffles me how JS knows where things are without some sort of import statement. Heh.



  • with can get really ugly sometimes, which is why it's considered bad form. For example, suppose you're lazy and you wrote this:

    with (Math) with (console) {
      // now I can access properties of both console and Math objects directly!
      log(cos(PI));
    }
    

    is the same as console.log(Math.cos(Math.PI)), but in the reverse order:

    with (console) with (Math) {
      // now I can access properties of both console and Math objects directly!
      log(cos(PI));
    }
    

    since Math was most recently added to the top of the scope chain, and since Math.log exists, log will actually refer to that.

    Also you've got really bizarre edge case stuff, like this:

    var NS = {a: 3};
    with (NS) {
      var a = 5, b = 7;
      console.log(a, b); // 5 7
    }
    console.log(a, b);   // undefined 7
    console.log(NS);     // Object { a: 5 }
    

    (the variable b is created as you expect, but since NS.a exists, var a actually doesn't create a new variable; instead, it just updates the value of NS.a.)

    or this:

    with (Math) {
      var PI = 3;
      console.log(PI);    // 3.141592653589793
    }
    console.log(Math.PI); // 3.141592653589793
    console.log(PI);      // 3
    

    (because Math.PI is not a writable property, it does create a new variable called PI -- which cannot be accessed inside the with scope, because Math.PI exists!)


  • Considered Harmful

    @anotherusername said in JavaScript removing with() expressions:

    var a actually doesn't create a new variable

    :wtf: I thought the point of var declarations was to explicitly create a new variable, as opposed to assigning without a keyword.

    JavaScript is dumb.



  • @pie_flavor said in JavaScript removing with() expressions:

    @anotherusername said in JavaScript removing with() expressions:

    var a actually doesn't create a new variable

    :wtf: I thought the point of var declarations was to explicitly create a new variable, as opposed to assigning without a keyword.

    JavaScript is dumb.

    var declarations in JavaScript are scope-level, so they work like normal assignment if the variable is already declared in the scope.

    You can use variables before the var statement as well.



  • @pie_flavor said in JavaScript removing with() expressions:

    @anotherusername said in JavaScript removing with() expressions:

    var a actually doesn't create a new variable

    :wtf: I thought the point of var declarations was to explicitly create a new variable, as opposed to assigning without a keyword.

    Ah, but as far as Javascript is concerned, a already exists in the scope of the command -- so var a does not actually create a; it simply reassigns it. This is wrong; actually, var a = 5 does create a, but then it doesn't actually assign the same variable a that it created! Javascript's hoisting can be weird.

    Remember, you can use var a as many times as you like, and it'll only ever create the variable once (per scope) -- and only if it doesn't exist. This is even true in strict mode. This is perfectly fine:

    (function () {
      'use strict';
      var a = 5;
      console.log(a); // 5
      var a = 7;
      console.log(a); // 7
    })();
    

    Due to the way that var is hoisted, that's essentially:

    (function () {
      'use strict';
      var a;
      a = 5;
      console.log(a);
      a = 7;
      console.log(a);
    })();
    

    In fact, you could even write

    (function (a) {
      'use strict';
      var a = 5;
      console.log(a);
      var a = 7;
      console.log(a);
    })();
    

    ...since a already exists, it is not created; instead, the original a variable is overwritten.


  • Considered Harmful

    @anotherusername ... then that means that with doesn't create a scope, which is similarly stupid.



  • @pie_flavor said in JavaScript removing with() expressions:

    @anotherusername ... then that means that with doesn't create a scope, which is similarly stupid.

    In JavaScript, only functions create scopes.



  • @pie_flavor no, with doesn't create a scope. That's why the variable b still existed outside of the with in which it was created, in that same example in my post.

    ...holy fuck, I just realized something. var a does create a, but then it assigns NS.a!

    So this:

    (function () {
      var NS = {a: 3};
      with (NS) {
        var a = 5, b = 7;
        console.log(a, b);
      }
      console.log(a, b);
      console.log(NS);
    })();
    

    doesn't crash, but this does:

    (function () {
      var NS = {a: 3};
      with (NS) {
        let a = 5, b = 7;
        console.log(a, b);
      }
      console.log(a, b);
      console.log(NS);
    })();
    

    The first is equivalent to:

    (function () {
      var NS, a, b;
      NS = {a: 3};
      with (NS) {
        a = 5, b = 7;
        console.log(a, b);
      }
      console.log(a, b);
      console.log(NS);
    })();
    

    while the second is equivalent to:

    (function () {
      var NS;
      NS = {a: 3};
      with (NS) {
        let a = 5, b = 7;
        console.log(a, b);
      }
      console.log(a, b); // ReferenceError: a is not defined
      console.log(NS);
    })();
    

  • 🚽 Regular

    @ben_lubar said in JavaScript removing with() expressions:

    var declarations in JavaScript are scope-level, so they work like normal assignment if the variable is already declared in the scope.

    You can use variables before the var statement as well.

    Var declarations are "hoisted" (brought to the top of the closest function body, or the whole file if outside a function), much like function declarations.

    The alternative is let, which also declares variables without hoisting.

    :hanzo:


  • Considered Harmful

    @anotherusername any construct with braces should create a new scope, always.


  • 🚽 Regular

    In case anyone was curious, this is the repo:


Log in to reply