Strange things happen when using Object.defineProperty with let or var

590 Views Asked by At
  1. Can anyone explain why testVariable has two different ouputs when using let. And why there isn't any runtime error when varibles with the same name are defined in window object?
    Object.defineProperty(window, 'testVariable', {
      value: 22
    })

    let testVariable = 12

    console.log(window.testVariable)   // result: 22
    console.log(testVariable)          // result: 12
  1. But when using var, the outputs are the same.
    Object.defineProperty(window, 'testVariable', {
      value: 22
    })

    var testVariable = 12

    console.log(window.testVariable)   // result: 12
    console.log(testVariable)          // result: 12
  1. Why following code runs correctly
  <script>
    Object.defineProperty(window, 'a', {
      value: 33
    })

    let a = 13
  </script>

  <script>
    console.log(a)    // result: 13
  </script>
  1. But the following throws an error.
  <script>
    Object.defineProperty(window, 'a', {
      value: 33
    })
  </script>

  <script>
    let a = 13
    console.log(a)   // Uncaught SyntaxError: Identifier 'a' has already been declared
  </script>
1

There are 1 best solutions below

3
On

When a variable is declared with var on the top level, as soon as the script tag starts, it's assigned as a writable property of the global object:

console.log(Object.getOwnPropertyDescriptor(window, 'testVariable'));
var testVariable = 12;

The same is not true for variables declared with let - they don't get put onto the global object:

console.log(Object.getOwnPropertyDescriptor(window, 'testVariable'));
let testVariable = 12;

When you use Object.defineProperty to define a property that's already defined on the object, and you pass a value, like with

Object.defineProperty(window, 'testVariable', {
    value: 22
})

The prior value that existed on the object gets overwritten. So with your second code, you're defining a writable property named testVariable on the global object which then gets overwritten, and both testVariable and window.testVariable evaluate to 12.

In contrast, with your first code, top-level variables declared with let create a global identifier, but do not get assigned to properties of the global object, so

Object.defineProperty(window, 'testVariable', {

and

let testVariable =

are referring to different things.


Why following code runs correctly, but the following throws an error

This one is quite interesting. According to the specification, when the environment prepares to execute a new <script> tag, it runs GlobalDeclarationInstantiation to set things up. It does, among other things:

1. Let envRec be env's EnvironmentRecord.
2. Assert: envRec is a global Environment Record.
3. Let lexNames be the LexicallyDeclaredNames of script.
4. Let varNames be the VarDeclaredNames of script.
5. For each name in lexNames, do
 - If envRec.HasVarDeclaration(name) is true, throw a SyntaxError exception.
 - If envRec.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.
 - Let hasRestrictedGlobal be ? envRec.HasRestrictedGlobalProperty(name).
 - If hasRestrictedGlobal is true, throw a SyntaxError exception.
6. For each name in varNames, do
 - If envRec.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.

Where envRec is the global Environment Record. (An Environment Record, or Lexical Environment, is a record of which variable names in a given scope refer to which values.)

With your code, the section of GlobalDeclarationInstantiation that is throwing is this section of 5:

If envRec.HasLexicalDeclaration(name) is true, throw a SyntaxError exception.

In your third code, before the first script tag starts, when GlobalDeclarationInstantiation runs, the global environment record doesn't have a variable named a, so no error is thrown. In contrast, in your fourth code, when the second script tag starts, and GlobalDeclarationInstantiation runs, a variable named a declared with let already exists in the global environment record, so the HasLexicalDeclaration call returns true, and an error is thrown. (Variables declared with let create lexical declarations; variables declared with var create var declarations.)