1

Objects and functions which I require only to execute once on a page load are wrapped inside an undefined check for the object. On Chrome on Windows/Linux which I usually use, the code works perfectly i.e. code only executes once. But on Safari on both iPad and MacBook, the undefined check doesn't work i.e. as per the browser, the object/function is already declared without even the code execution reaching there!

I've simplified my code to only include an if loop that checks if the nested function is already declared. Since it should not have been declared the first time, I've included someVariable inside the if that should never be undefined.

Run the same function on Chrome and Safari and see the difference.


if (typeof anObject == 'undefined') {

    function anObject(someParameter = 'someParameter') {
        var someProperty = 'someProperty';

        function someMethod(someParameter) {
            console.log(someParameter);
        }
    }

    console.log('Hi');
    var someVariable = 404;
}

On Chrome, you can see Console logging of 'Hi' as well someVariable as 404. But on Safari, there's no console logging and someVariable goes undefined.

If you put breakpoints to understand what's happening - the first undefined check never works actually. The anObject is defined even before the declaration.

I have tried searching about the differences regarding this between V8 (Chrome JS engine) and JavaScriptCore (Safari's engine) but haven't found anything solid. I assume this is something related to execution and function hoisting. It would be better if someone could explain to me the reason for this difference in execution. The behavior is the same on iPad that too even on Chrome!

Updates:

  1. I've found a similar problem regarding different execution. Seems like this is something related to function hoisting but couldn't find any solid source yet. Javascript Hoisting in Chrome And Firefox

  2. Looks like it is actually a hoisting behaviour now. This works by using function expression. In this case, simple replacing function anObject() with var anObject = function(). By doing this, I think, the variable doesn't get assigned the function reference even if the function is hoisted and evaluated before execution.

  3. As recommended by PhistucK, I have opened the issue on WebKit issue tracker (Bug #199823), Chromium Discuss and TC39 ECMA262 Github (Issue #1632).

  4. This is an existing Webkit bug reported in 2016 - Bug 163209 - [ES6]. Implement Annex B.3.3 function hoisting rules for global scope. I have now summarized the research in my answer.

Baahubali
  • 163
  • 3
  • 14
  • I am surprised such a difference still exists. Are you using the latest version of all of the browsers you tested? I think ECMAScript 2015+ was supposed to standardize the way this things work (I remember talks about functions within if-statements). A side note - Chrome for iOS (but not for macOS) does not use V8, it is basically just a skin of Safari with minimal additions injected into it (Web Payments, I think, as well as browser features like synchronization and automatic filling of stuff and some more), the web engine itself is still WebKit and JavaScriptCore. – PhistucK Jul 15 '19 at 11:13
  • And I suggest that you file an issue with either all of the engines or the ones you think are wrong and each will likely comment about whether their way is correct or not. If all of them state that their way is correct, you file an issue with TC39 (the committee of ECMAScript) about the differences and they should settle the argument. Once they do so, you can comment on the issues you filed about the decision (or file new issues for those that non-compliant). Yes, it is a bit of work, but it is worth it - you will fix one bit of the world. – PhistucK Jul 15 '19 at 11:18
  • @PhistucK, thank you so much for the guidance. I did file the issue on Chromium Discuss. I had actually planned initially to post this on different engine discussions but then thought I would be spamming many forms. I'll do this now. Thanks for clearing the doubt regarding Chrome for iOS - now I get that the issue is really about the engines. I'll also file the issue with the engines and TC39. I really appreciate how you've taken your time to let me know all of this. Happy to do my bit now. :) Chrome: 75.0.3770.100 Safari: Version 12.1.1 iPad Chrome: 75, iOS 12.3 – Baahubali Jul 15 '19 at 12:12
  • Those are the issue trackers of the major engines - https://webkit.org/b/ https://crbug.com/ https://bugzil.la/ and of TC39 - https://github.com/tc39/ecma262/issues – PhistucK Jul 15 '19 at 12:24
  • You'll want to check out [What are the precise semantics of block-level functions in ES6?](https://stackoverflow.com/q/31419897/1048572) Looks like JavaScriptCore has a bug here. – Bergi Jul 16 '19 at 16:49

2 Answers2

2

This behaviour relates to using sloppy mode in Webkit engines, which has a bug. Let me wrap up the research:

Specifically, there are three key aspects to the example: in non-strict mode code, a function is declared within a block and referenced prior to that block.

As the introduction to Annex B.3.3 explains, function declarations inside block statements were not originally part of the language spec; this was an extension that browsers often implemented, each in their own unique way. ES2015 sought to specify as much of this behavior as possible, but as the differences between browsers were not fully reconcilable, some existing code remained inevitably unportable.

"Here are things we were forced to specify because web browsers implemented this behavior and then pages start relying on it, but we aren't happy about it." - Annex B 3.3

In sloppy mode, JavaScriptCore does behave differently than the normal behaviour:

λ eshost -sx "if (typeof foo === 'undefined') { function foo() {} print('ok'); } else { print('hmm'); }"
#### ch, sm, v8, xs
ok

#### jsc
hmm

One solution is to use 'strict' mode:

λ eshost -sx "(function () { 'use strict'; if (typeof foo === 'undefined') { function foo() {} print('ok'); } else { print('hmm'); } })()"

#### ch, jsc, sm, v8, xs
ok

Also, this apparently only happens in Safari at the top level of scripts. In functions, as in

function g(){
  console.log(typeof f);
  {
    function f(){}
  }
}

g();

Safari conforms to the spec. This might well be because the behavior at the top level of scripts was only specified in ES2016, in 8582e81, as opposed to the behavior in functions, which was specified in ES2015.

Source: Comments posted by Ross Kirsling and Kevin Gibbons on GitHub issue #1632.

There has been an existing bug reported in 2016 related to this hoisting behaviour, Webkit Issue #16309: [ES6]. Implement Annex B.3.3 function hoisting rules for global scope. Here's a Test262 case that covers this.

To solve this, I have used Function Expressions:

That is I replaced function anObject() with var anObject() = function(). Run this code to understand the flow now:

if (typeof anObject == 'undefined') {

  if (typeof anObject == 'undefined') console.log('anObject not defined inside block')
  if (typeof someVariable == 'undefined') console.log('someVariable not defined as of now');

  var anObject = function(someParameter = 'someParameter') {
    var someProperty = 'someProperty';
  }

  console.log('anObject is now defined');
  var someVariable = 404;

  if (typeof someVariable == 'undefined') console.log('someVariable not defined as of now');

}

What's happening here?

The functions and variables are hoisted to the top level. But engines like V8 (Chrome), semantically defines the function name during code execution. However, in sloppy mode on Webkit browsers, even after the ECMA2015/16 standardization, function name is defined before the execution. Please note that on both engines, the function is actually defined (hoisted) before anything - this is just about the semantics regarding the function name. The code above assigns the anonymous function's reference (because it has no name now) to anObject during the execution and this will run fine on Safari as well. A good explanation about block scopes and hoisting on What are the precise semantics of block-level functions in ES6?.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Baahubali
  • 163
  • 3
  • 14
1

The other SO question you found actually answers this. In short:

Declaring functions inside conditional statements is non-standard, so do not do that.

Given that it's non-standard, browsers are free to have different behavior.

That aside, I don't think your technique gives any benefit. Just remove the "if undefined" check and define the functions in the top level unconditionally. They will only be assigned once anyway.

jmrk
  • 34,271
  • 7
  • 59
  • 74
  • Yes, already replaced that with a flag. Just trying to find reasons if any. This is a simplified code as I had mentioned. The source code actually checked multiple calling of same file with this check. In simple words, anObject would only get executed once no matter how many times the file is loaded. – Baahubali Jul 15 '19 at 09:43
  • @Bergi, thanks for the great explanation. I have posted the final answer that summarizes the research. I think you may want to improve it. Appreciated. :) – Baahubali Jul 17 '19 at 08:01