Pergunta

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.

Foi útil?

Solução

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.

Outras dicas

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!)

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top