Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript default arguments with block scoping fails on iOS only

try {
  const val = 'correct value';
  (() => {
    ((arg = val) => {
      const val = 'ignored value';
      alert(arg);
    })();
  })();
} catch (err) {
  alert(err.message || 'Unknown error');
}

On OS X Chrome, OS X Safari, Android Chrome, Windows Chrome, Windows Firefox, and even Windows Edge, it alerts "correct value". On iOS Safari and iOS Chrome, it alerts "Can't find variable: val".

The following snippets all work on iOS:

Not using default argument (snippet 2):

try {
  const val = 'correct value';
  (() => {
    alert(val);
    (() => {
      const val = 'wrong value';
    })();
  })();
} catch (err) {
  alert(err.message || 'Unknown error');
}

No nested functions (snippet 3):

try {
  const val = 'correct value';
  ((arg = val) => {
    const val = 'ignored value';
    alert(val || 'wrong value');
  })();
} catch (err) {
  alert(err.message || 'Unknown error');
}

Not overriding variable (snippet 4):

try {
  const val = 'correct value';
  (() => {
    ((arg = val) => {
      alert(arg);
    })();
  })();
} catch (err) {
  alert(err.message || 'Unknown error');
}

Block scope instead of function (snippet 5):

try {
  const val = 'correct value';
  {
    ((arg = val) => {
      const val = 'ignored value';
      alert(arg);
    })();
  }
} catch (err) {
  alert(err.message || 'Unknown error');
}

Based on the snippet 3, it's clear that the val in arg = val should come from the parent scope, not the scope of the inner function.

In the first snippet, the browser can't find val in the current scope, but instead of checking the ancestor scopes, it uses the child scope, which causes the temporal dead zone.

Is this an iOS bug or am I misunderstanding the proper JS behavior?

This bug is occurring in our Webpack+Babel+Terser output, so we can't just rewrite the code to avoid this bug.

like image 840
Leo Jiang Avatar asked Apr 06 '20 20:04

Leo Jiang


Video Answer


1 Answers

I think this is an unwanted consequence of a buggy implementation of the Param default values and their TDZs. I suspect that iOS Safari thinks you're trying to assign to something you have't initialized yet.

For reference -- the error location:

enter image description here


Workaround 1 Don't initialize an inner scope const w/ the same name as the default param & the outer scope's one

try {
    const val = 'correct value';
    (() => {
        ((arg = val) => {
            const val_ = 'ignored value';       // <----
            alert(arg);
        })();
    })();
} catch (err) {
    console.error(err);
    console.error('msg', err.message || 'Unknown error');
}

Workaround 2

Force const to let:

try {
    let val = 'correct value';                 // <----
    (() => {
        ((arg = val) => {
            const val = 'ignored value';
            alert(arg);
        })();
    })();
} catch (err) {
    console.error(err);
    console.error('msg', err.message || 'Unknown error');
}

Workaround 3 Don't reinitialize const val in the innermost closure at all:

try {
    const val = 'correct value';
    (() => {
        ((arg = val) => {
            // const val = 'ignored value';      // <--
            alert(arg);
        })();
    })();
} catch (err) {
    console.error(err);
    console.error('msg', err.message || 'Unknown error');
}
like image 179
Joe - Elasticsearch Handbook Avatar answered Sep 19 '22 22:09

Joe - Elasticsearch Handbook