How to avoid accidentally implicitly referring to properties on the global object?

前端 未结 5 1782
面向向阳花
面向向阳花 2020-11-30 09:27

Is it possible to execute a block of code without the implicit with(global) context that all scripts seem to have by default? For example, in a browser, would t

相关标签:
5条回答
  • 2020-11-30 09:36

    If you're not in strict mode, one possibility is to iterate over the property names of the global (or withed) object, and create another object from those properties, whose setters and getters all throw ReferenceErrors, and then nest your code in another with over that object. See comments in the code below.

    This isn't a nice solution, but it's the only one I can think of:

    const makeObjWhosePropsThrow = inputObj => Object.getOwnPropertyNames(inputObj)
      .reduce((a, propName) => {
        const doThrow = () => { throw new ReferenceError(propName + ' is not defined!'); };
        Object.defineProperty(a, propName, { get: doThrow, set: doThrow });
        return a;
      }, {});
    
    // (using setTimeout so that console shows both this and the next error)
    setTimeout(() => {
      const windowWhichThrows = makeObjWhosePropsThrow(window);
      with (windowWhichThrows) {
        /* Use an IIFE
         * so that variables with the same name declared with "var" inside
         * create a locally scoped variable
         * rather than try to reference the property, which would throw
         */
        (() => { 
          // Declaring any variable name will not throw:
          var alert = true;  // window.alert
          const open = true; // window.open
          
          // Referencing a property name without declaring it first will throw:
          const foo = location;
        })();
      }
    });
    
    const obj = { prop1: 'prop1' };
    with (obj) {
      const inner = makeObjWhosePropsThrow(obj);
      with (inner) {
        // Referencing a property name without declaring it first will throw:
        console.log(prop1);
      }
    }
    .as-console-wrapper {
      max-height: 100% !important;
    }

    Caveats:

    • This explicitly uses with, which is forbidden in strict mode
    • This doesn't exactly escape the implicit with(global) scope, or the with(obj) scope: variables in the outer scope with the same name as a property will not be referenceable.
    • window has a property window, which refers to window. window.window === window. So, referencing window inside the with will throw. Either explicitly exclude the window property, or save another reference to window first.
    0 讨论(0)
  • 2020-11-30 09:46

    Perhaps slightly cleaner (YMMV) is to set getter traps (like you did), but in a worker so that you don't pollute your main global scope. I didn't need to use with though, so perhaps that is an improvement.

    Worker "Thread"

    //worker; foo.js
    addEventListener('message', function ({ data }) {
      try {
        eval(`
          for (k in self) {
            Object.defineProperty(self, k, {
              get: function () {
                throw new ReferenceError(':(');
              }
            });
          }
          // code to execute
          ${data}
        `);
        postMessage('no error thrown ');
      } catch (e) {
        postMessage(`error thrown: ${e.message}`);
      }
    });
    

    Main "Thread"

    var w = new Worker('./foo.js');
    w.addEventListener('message', ({data}) => console.log(`response: ${data}`));
    w.postMessage('const foo = location');
    


    Another option that may be worth exploring is Puppeteer.

    0 讨论(0)
  • 2020-11-30 09:53

    Somewhat simpler to implement than @CertainPerformance's answer, you can use a Proxy to catch implicit access to everything except window. The only caveat is you can't run this in strict mode:

    const strictWindow = Object.create(
      new Proxy(window, {
        get (target, property) {
          if (typeof property !== 'string') return undefined
          console.log(`implicit access to ${property}`)
          throw new ReferenceError(`${property} is not defined`)
        }
      }),
      Object.getOwnPropertyDescriptors({ window })
    )
    
    with (strictWindow) {
      try {
        const foo = location
      } catch (error) {
        window.console.log(error.toString())
      }
    
      // doesn't throw error
      const foo = window.location
    }

    Notice that even console has to have an explicit reference in order to not throw. If you want to add that as another exception, just modify strictWindow with another own property using

    Object.getOwnPropertyDescriptors({ window, console })
    

    In fact, there are a lot of standard built-in objects you may want to add exceptions for, but that is beyond the scope of this answer (no pun intended).

    In my opinion, the benefits this offers fall short of the benefits of running in strict mode. A much better solution is to use a properly configured linter that catches implicit references during development rather than at runtime in non-strict mode.

    0 讨论(0)
  • 2020-11-30 09:56

    There are some things you need to consider before trying to answer this question.

    For example, take the Object constructor. It is a "Standard built-in object".

    window.status is part of the Window interface.

    Obviously, you don't want status to refer to window.status, but do you want Object to refer to window.Object?


    The solution to your problem of it not being able to be redefined is to use a IIFE, or a module, which should be what you are doing anyways.

    (() => {
      var status = false;
      if (!status) {
        console.log('status is now false.');
      }
    })();
    

    And to prevent accidentally using global variables, I would just set up your linter to warn against it. Forcing it using a solution like with (fake_global) would not only have errors exclusively at run time, which might be not caught, but also be slower.


    Specifically with ESLint, I can't seem to find a "good" solution. Enabling browser globals allows implicit reads.

    I would suggest no-implicit-globals (As you shouldn't be polluting the global scope anyways, and it prevents the var status not defining anything problem), and also not enabling all browser globals, only, say, window, document, console, setInterval, etc., like you said in the comments.

    Look at the ESLint environments to see which ones you would like to enable. By default, things like Object and Array are in the global scope, but things like those listed above and atob are not.

    To see the exact list of globals, they are defined by this file in ESLint and the globals NPM package. I would would pick from (a combination of) "es6", "worker" or "shared-node-browser".

    The eslintrc file would have:

    {
        "rules": {
            "no-implicit-globals": "error"
        },
        "globals": {
            "window": "readonly",
            "document": "readonly"
        },
        "env": {
            "browser": false,
            "es6": [true/false],
            "worker": [true/false],
            "shared-node-browser": [true/false]
        }
    }
    
    0 讨论(0)
  • 2020-11-30 09:59

    Just use "use strict". More on Strict Mode.

    0 讨论(0)
提交回复
热议问题