3
var a = ({
    x: 10,
    foo: function () {
        function bar() {
            console.log(x);
            console.log(y);
            console.log(this.x);
        }
        with (this) {
            var x = 20;
            var y = 30;
            bar.call(this);
        }
    }
}).foo();

Results in undefined, 30, 20.

Would be greatly appreciated to get some step by step debug-style explanation of how this works.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Karen Grigoryan
  • 5,234
  • 2
  • 21
  • 35

2 Answers2

2

OK, lets first simplify the code a little. I've refactored out that foo is a method, which is not necessary do demonstrate the unexpected behaviour.

function foo(a) {
    // var x, y, bar - hoisting
    function bar() {
        console.log(x);
        console.log(y);
        console.log(a.x);
    }
    with (a) {
        var x = 20;
        var y = 30;
        bar();
    }
}
foo({x:10});

So what happens when we call foo?

  1. An execution context is set up and filled with the declared functions and variables. This is what colloquially is referred to as "hoisting". In foo, there are the function bar and the variables x and y (and the function foo itself and its argument a, only in my refactored version). This is the scope to which bar has access to.
  2. The with statement is executed. It exchanges the current lexical environment with one that is based on the a object - any properties of that are accessible like variables now.
  3. The value 20 is assigned to an x. What is this x? When resolving that identifier, the a object is checked and - oh - it has a binding with that name! So we put the value in that binding, which will put the 20 on the .x property of the object.
  4. The value 30 is assigned to an y. What is this y? Again, the current lexical environment is checked but we don't find an y property on the a object. So we go on to the parent environment, which is the one with the x, y, bar variables created above. Indeed, here we find an y variable, so we put the value 30 in that slot.
  5. The bar function is called. Again, a new execution context is set up (like above), with the one from step 1 as its parent scope (which was determined by the lexical position of bar in foo, when the bar function was instantiated - lexical closure). Now we log those three expression of interest:
    • x resolves to the variable x in the scope of foo, which still has the value undefined.
    • y resolves to the variable y in the scope of foo, which holds the value 30 that we just assigned.
    • a.x resolves to the x property of the a object, which holds the value 20 that we just assigned.
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I frankly can't get the point of x == undefined, I mean I know that when `with` statement terminates it deletes all the changes to the object it was operating on as a augmenting scope object, but in our case we don't terminate it we continue with calling a local method from our unnamed object, kinda confuses a lot... – Karen Grigoryan Apr 30 '14 at 19:57
  • Bergi: May be worth rewriting the code with the hoists in their ultimate destinations. – cookie monster Apr 30 '14 at 19:59
  • "*when with statement terminates it deletes all the changes to the object it was operating on*" - no, what makes you think that? Btw it makes no difference whether you move the `bar()` call out of the `with` statement. – Bergi Apr 30 '14 at 20:09
  • 1
    Not exactly. It changes the lexical environment back to original one, yes, but it does not revert the modifications that were done to either. The environment that was "plugged in" by the `with` is a child of the original one, and proxies the properties found on the object. See http://jsbin.com/faxapoho/2/edit – Bergi Apr 30 '14 at 21:02
1

It's generally recommended that you avoid with. It's confusing!

That said, it's probably simplest if we just annotate your code. I'll refer to your anonymous object as {} instead of this to avoid ambiguity. And the switch in the ordering here is solely for the sake of reading the code in execution order from top to bottom.

var a = ({
    x: 10,
    foo: function () {

        // entering `with(this)`: all variables are searched against `{}`
        // before the engine attempts to create a new variable:
        with (this) {

            // `var x` creates a local `x`. But, during assignment,
            // `x` matches `{}.x`, so `{}.x` is set. **local `x`** 
            // remains `undefined`.
            var x = 20;

            // `y` isn't found in `{}`, so it's a local variable
            var y = 30;

            // execute `bar()`
            bar.call(this);
        }

        // we're now in scope of `{}.foo()`, but not `with(this)`.
        function bar() {

            // local variable `x` was declared, but never defined.
            console.log(x);

            // local variable `y` exists in the scope of `{}.foo()`
            console.log(y);

            // we're still in the "macro" scope of `{}`. So, `this` refers
            // to `{}`, which was changed to 20.
            console.log(this.x);
        }

    }
}).foo();

Clear as mud? (Don't use with if you can avoid it!)

svidgen
  • 13,744
  • 4
  • 33
  • 58
  • Actually, there *is* an `x` variable (though not local to `bar`). – Bergi Apr 30 '14 at 19:40
  • @Bergi It's not a local variable though. That's the point. On account of the `with`, it's the property of an object. – svidgen Apr 30 '14 at 19:43
  • It's a local variable of `foo`. Check my answer for explanation :-) You could make `bar` use strict mode and it would not throw. – Bergi Apr 30 '14 at 19:44
  • @Bergi I could be mistaken on the terminology; but I think it's an identifier; not a variable. It's literally `undefined`. – svidgen Apr 30 '14 at 19:47
  • @Bergi But, I've edited out the "controversial" terminology for you anyway. – svidgen Apr 30 '14 at 19:49
  • No, (regardless of terminology) both `x` and `y` are *declared* in the scope of `foo`. It really exists, not only the property of the object. – Bergi Apr 30 '14 at 19:49
  • If there was no variable, it would cause a reference error. It seems that the `var x` gets hoisted to the top of the `foo` environment, and the `x = 20` remains inside `with`. That's bizarre. – cookie monster Apr 30 '14 at 19:49
  • @cookiemonster: That's not bizarre, that's hoisting :-) Same thing for `y`, if it wouldn't get hoisted then `bar` had no access to it actually (regardless of exception-throwing strict mode or `undefined`-yielding sloppy mode) – Bergi Apr 30 '14 at 19:51
  • @cookiemonster Good observation. ... I don't think either of our answers identify where `x` comes from then ... – svidgen Apr 30 '14 at 19:52
  • @Bergi Hoisting ... not sure that applies to variables. Only function declarations. – svidgen Apr 30 '14 at 19:53
  • @Bergi: You're right. It's just that when hoisting splits the declaration away from the assignment, you can usually rely on the assignment still relating to the original declaration. The bizarre part in my mind is that they hoist *and* change the `x` receiving the assignment. – cookie monster Apr 30 '14 at 19:53
  • 1
    @svidgen: It applies to variable declaration too. In any `var xyz = 123` type of operation, the `var xyz` part is hoisted and but the assignment remains in place. – cookie monster Apr 30 '14 at 19:54
  • @cookiemonster Interesting ... Noted. – svidgen Apr 30 '14 at 19:55