如何避免意外地隐式引用全局对象的属性?

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

是否可以在没有所有脚本似乎默认具有的隐式 with(global) 上下文的情况下执行代码块?例如,在浏览器中,是否有任何方法可以设置一个脚本,以便像

这样的行
const foo = location;

投掷

Uncaught ReferenceError: location is not defined

而不是访问 window.location,当 location 还没有首先声明时?缺少它,有没有一种方法可以使这种隐式引用导致某种警告?它可能是编写代码时出现错误的来源(见下文),因此有一种方法可以防止它出现。

(当然,由于普通的作用域规则,可以使用 constlet 或在内部块中声明另一个具有相同名称的变量,以确保使用该变量名称引用新变量而不是全局变量 属性,但这不是一回事。)

这可能类似于询问是否可以从 actual with 语句中停止引用 属性:

const obj = { prop: 'prop' };
with (obj) {
  // how to make referencing "prop" from somewhere within this block throw a ReferenceError
}

众所周知 with 一开始就不应该使用,但不幸的是,当涉及到 with(global) 时,我们似乎别无选择,偶尔会以牺牲几个字符为代价经常出现的令人困惑的错误: 3 4 5 6。例如:

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

(这里的问题:window.status 是保留的 属性 - 当赋值给它时,它会将赋值的表达式强制转换为字符串)

这些类型的错误与不鼓励或禁止显式使用 with 的原因相同,但隐式 with(global) 继续导致问题,即使在严格模式下也是如此,因此找出一种方法周围会有用。

如果您不处于严格模式,一种可能是遍历全局(或 withed)对象的 属性 名称,并从这些属性创建另一个对象,其setter 和 getter 都抛出 ReferenceErrors,然后将您的代码嵌套在该对象上的 另一个 with 中。请参阅下面代码中的注释。

这不是好的解决方案,但这是我能想到的唯一解决方案:

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;
}

注意事项:

  • 这显式使用了with,这在严格模式下是被禁止的
  • 这并不完全逃避隐含的with(global)作用域,或with(obj)作用域:外部作用域中的变量与a同名属性 将无法引用。
  • window有一个属性window,指的是windowwindow.window === window。因此,在 with 中引用 window 将会抛出。要么明确排除 window 属性,要么首先保存对 window 的另一个引用。

也许 稍微 清洁器 (YMMV) 是设置 getter 陷阱(就像你所做的那样),但在一个工人中,这样你就不会污染你的主要全局范围。虽然我不需要使用 with,所以也许这是一个改进。

工人"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}`);
  }
});

主要"Thread"

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


另一个可能值得探讨的选项是 Puppeteer

只需使用"use strict"。更多关于 Strict Mode.

在尝试回答这个问题之前,您需要考虑一些事项。

例如,取 Object constructor. It is a "Standard built-in object".

window.status is part of the Window interface.

很明显,你不想让status引用window.status,但是你想让Object引用window.Object吗?


解决无法重新定义问题的方法是使用 IIFE 或模块,这应该是您正在做的事情。

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

并且为了防止意外使用全局变量,我将设置您的 linter 以发出警告。强制使用像 with (fake_global) 这样的解决方案不仅会在 运行 时间出现错误,这可能不会被捕获,而且速度也会变慢。


特别是对于 ESLint,我似乎找不到 "good" 解决方案。启用浏览器全局变量允许隐式读取。

我建议 no-implicit-globals(因为无论如何你都不应该污染全局范围,并且它可以防止 var status 没有定义任何问题),并且也不会启用所有浏览器全局变量,仅,比如说,windowdocumentconsolesetInterval,等等,就像你在评论中说的那样。

查看 ESLint environments 以了解您要启用哪些。默认情况下,ObjectArray 之类的东西在全局范围内,但上面列出的那些和 atob 之类的东西不在全局范围内。

要查看全局变量的确切列表,它们由 this file in ESLint and the globals NPM package 定义。我会选择 "es6"、"worker" 或 "shared-node-browser".

(组合)

eslintrc 文件将包含:

{
    "rules": {
        "no-implicit-globals": "error"
    },
    "globals": {
        "window": "readonly",
        "document": "readonly"
    },
    "env": {
        "browser": false,
        "es6": [true/false],
        "worker": [true/false],
        "shared-node-browser": [true/false]
    }
}

比@CertainPerformance 的答案更容易实现,您可以使用 Proxy 来捕获对 window 以外所有内容的隐式访问。唯一的警告是你不能 运行 在严格模式下这样做:

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
}

请注意,即使 console 也必须有明确的引用才能不抛出。如果您想将其添加为另一个例外,只需使用

strictWindow 修改为另一个自己的 属性
Object.getOwnPropertyDescriptors({ window, console })

事实上,a lot of standard built-in objects 您可能想为其添加例外,但这超出了本答案的范围(无双关语意)。

在我看来,这提供的好处不及在严格模式下 运行ning 的好处。一个更好的解决方案是使用正确配置的 linter,在开发期间捕获隐式引用,而不是在非严格模式下 运行 时间。