1

Why does it give an error?

!function() {
    var a = 3;
    setTimeout('console.log(a)');
}()

It works if to use a function instead of the string.

It also works with eval:

!function() {
    var a = 3;
    eval('console.log(a)');
}()

This question is about theory. No need to fix it.

The problem is with the scope. I don't understand why the scope is lost. Better - some sentences from a source (for example Mozilla site) where it is explained.

oleedd
  • 388
  • 2
  • 15

1 Answers1

2

Because you can imagine the setTimeout function as something like that

function setTimeout(callbackOrString, time) {
    if( typeof callbackOrString === 'string' ) {
        callbackOrString = eval(callbackOrString)
    }
    // pass callbackOrString to event queue 
}

The evaluation of the string happens inside of the setTimeout function, and not at the point when you pass it to the function. So at the point when the string is evaluated, there is no a in that scope.

And eval is not a regular function but more instruction for the engine, and its behaviour is defined here ECMAScript: 18.2.1 eval ( x )

setTimeout is not part of the language specification but part of an API, and just a "regular" function Living Standard: timer initialization steps (see 1. and 3.):

1. Let method context proxy be method context if that is a WorkerGlobalScope object, or else the WindowProxy that corresponds to method context.  
…  
7. Let task be a task that runs the following substeps:
   …
   2. Run the appropriate set of steps from the following list:
      - If the first method argument is a Function
        …
      - Otherwise
        …
        3. Let settings object be method context's environment settings object.

It is just as if you would write your own function, which results in the same error you get with setTimeout for the exact same reason:

function myFunc(code) {
  if (typeof code === 'string') {
    eval(code)
  } else if (typeof code === 'function') {
    code();
  }
}


!function() {
  var a = 3;

  // arrow function is passed to myFunc, closure is create so "a" is available
  myFunc(() => console.log(a)); 
  
  // function is passed to myFunc, closure is create so "a" is available
  myFunc(function() {
    console.log(a)
  });

  // the string "() => console.log(a)" is evaluated in the scope of "a" and the resulting arrow function is passed to myFunc, closure is create so "a" is available
  myFunc(eval('() => console.log(a)'));

  try {
    // does not work because only the string is passed to myFunc which is evaluated within myFunc
    myFunc('console.log(a)');
  } catch (err) {
    console.error(err);
  }
}()
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • But it works with eval. See the question update. I need explaining this for example on Mozilla site. – oleedd Jun 03 '20 at 11:07
  • @oleedd but `eval` is not called within the same scope. Since there is no mechanism to capture the value `a` there is no way to "transfer" that binding. – VLAZ Jun 03 '20 at 11:09
  • It is called because it works in my second example. – oleedd Jun 03 '20 at 11:11
  • @oleedd [this is what pretty much happens when you use a string in `setTimeout`](https://jsbin.com/melanuxewa/edit?js,console) – VLAZ Jun 03 '20 at 11:12
  • @oleedd it woks in your second example because `eval` *is called in the same scope!* `setTimeout` doesn't run in the same scope, it takes the string and `eval`s it *later*. – VLAZ Jun 03 '20 at 11:13
  • Ok. I knew this. So I wanted to read about this problem in some documentation. – oleedd Jun 03 '20 at 11:15
  • Not links with big material but some sentences where it is explained. – oleedd Jun 03 '20 at 11:16
  • @oleedd that's what I did `eval` is explicitly defined to behave that way (see specs). `setTimeout` is just a regular function provided by the API of the browser/node that calls `eval` inside, and is not different to a function that you would write yourself and in which you would use `eval`. – t.niese Jun 03 '20 at 11:21
  • Can you find some sentences in your links about this problem and copy them to the answer? Maybe from Mozilla documentation site. – oleedd Jun 03 '20 at 11:24
  • @oleedd `[…]An alternative syntax that allows you to include a string instead of a function, which is compiled and executed when the timer expires.[…]`. **compiled and executed when the timer expires** that's the same as I have written in my answer: `[…]The evaluation of the string happens inside of the setTimeout function, and not at the point when you pass it to the function.[…]` – t.niese Jun 03 '20 at 11:29
  • It doesn't explain close to the problem to understand. According to your code example: but is also doesn't work so: `function myFunc() { console.log(a) } !function() { var a = 3; myFunc(); }()`. But with `eval`, my example from the question works. So you can make "It is just as if you would write your own function" for cases with eval and usual function to compare them and understand. – oleedd Jun 03 '20 at 11:37
  • If `setTimeout` is an outer function then it shouldn't work with a function too. But it works. So for now I don't understand. – oleedd Jun 03 '20 at 11:44
  • In your example, `myFunc` is outer but `setTimeout` is not outer. – oleedd Jun 03 '20 at 11:57
  • @oleedd `setTimeout` **is** an outer function. The `setTimeout` function is part of the global object (in case of the browser the `window`) object. – t.niese Jun 03 '20 at 12:18
  • @oleedd the code I have shown with `myFunc` results in the same error as the `setTimeout` code, because of the same reason. – t.niese Jun 03 '20 at 12:20
  • But how can `setTimeout` work with a function in a local scope if it is outer? Can you make an example with your outer `myFunc` which works with a function instead of string? Seems that it can let understand. – oleedd Jun 03 '20 at 12:30
  • @oleedd that's is then regular closure over the variables in that scope, update the code snippet. – t.niese Jun 03 '20 at 12:52
  • Thanks. Now it is much better when there are things to compare. There is also some new for me in putting functions as arguments. +1 – oleedd Jun 03 '20 at 14:12