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

IT技术 javascript scope with-statement
2021-03-19 10:16:39

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

const foo = location;

投掷

未捕获的 ReferenceError:未定义位置

而不是访问window.location,何时location尚未首先声明?缺少这一点,有没有办法使这种隐式引用可能导致某种警告?在编写代码时,它可能是错误的来源(见下文),因此有一种防范它的方法可能很有用。

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

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

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

众所周知,一开始with不应该使用 ,但不幸的是,当谈到 时,我们似乎别无选择with(global),它偶尔会节省一些字符,但代价是会经常弹出一些令人困惑的错误:1 2 3 4 5 6 . 例如:

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

(这里的问题:window.status是一个保留属性 - 分配给时,它将分配的表达式强制为字符串)

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

5个回答

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

Object构造函数为例它是一个“标准内置对象”

window.statusWindow界面的一部分

显然,您不想status引用window.status,但是您想Object引用window.Object吗?


您无法重新定义它的问题的解决方案是使用 IIFE 或module,无论如何您都应该这样做。

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

为了防止意外使用全局变量,我只会设置你的 linter 来警告它。使用类似的解决方案强制它with (fake_global)不仅会在运行时出现错误,这可能不会被捕获,而且速度会更慢。


特别是使用 ESLint,我似乎找不到“好的”解决方案。启用浏览器全局变量允许隐式读取。

我建议没有隐式的全局变量(如你不应该反正污染全球范围内,并防止var status不定义任何问题),也没有使所有的浏览器全局,只是,说windowdocumentconsolesetInterval,等,就像你在评论中所说的那样。

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

要看到全局的确切名单,它们被定义此文件中ESLintglobalsNPM包我会从(组合)“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]
    }
}
是的,脚本编写者可能希望能够继续隐式地引用某些全局属性(例如window.undefined,也许window.Object也是)。IIFE 有帮助(我在问题中提到了这个选项),但是有了它,仍然有可能意外引用全局属性,例如const someVar = location,然后“oop,我忘了location在块的前面定义,这行是指window.location”。
2021-04-20 10:16:39
我们离开browser: true,然后使用no-restricted-globals将导致我们问题的全局变量列入黑名单。"no-restricted-globals": ["error", "closed", "event", "fdescribe", "name", "length", "location", "parent", "top"]
2021-05-13 10:16:39
从对问题的反馈来看,一个好的综合运行时解决方案看起来很慢、丑陋或 hacky -运行时之前的 linter听起来是迄今为止最好的解决方案你介意把它充实一些吗?例如,要使用 ESLint 实现这一点,看起来必须确保禁用“浏览器”环境(否则允许隐式引用),而window将其指定为全局变量。
2021-05-17 10:16:39

如果在严格模式不是,一种可能性是遍历全球(或属性名称withED)的对象,并从这些特性,它的getter和setter方法都会抛出创建另一个对象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)作用域:外部作用域中与属性同名的变量将不可引用。
  • window有一个属性window,它指的是windowwindow.window === window. 所以,window在里面引用with会抛出。要么明确排除该window属性,要么先保存另一个引用window
您是否考虑过将一段代码作为字符串运行 linter 并且仅在它通过 linter 检查时才执行它?
2021-05-05 10:16:39
这只是我在写问题时想到的一个丑陋的黑客。它并非是的答案,我希望有一个更好的解决方案。
2021-05-09 10:16:39

比@CertainPerformance 的答案更容易实现,您可以使用 aProxy来捕获对除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
}

请注意,evenconsole必须具有显式引用才能不抛出。如果要将其添加为另一个异常,只需strictWindow使用另一个自己的属性进行修改

Object.getOwnPropertyDescriptors({ window, console })

事实上,您可能希望为许多标准内置对象添加例外,但这超出了本答案的范围(无意双关语)。

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

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

工人“线程”

//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}`);
  }
});

主“线程”

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

在此处输入图片说明


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

我很确定 OP 的目标是防止对全局属性的隐式访问(const foo = location失败),同时允许显式访问(const foo = self.location工作正常)。这里的方法可以防止两者。
2021-04-29 10:16:39
我知道了。是的,在那种情况下你是对的,这行不通。
2021-04-29 10:16:39
很酷,这让我意识到我有一个隐含的假设,即我不应该改变 native window,但这不一定是必需的。可以通过代理保留对全局对象的显式引用:jsfiddle.net/7t60Lu2b(如果需要,可以在 worker 内部)。丑陋而脆弱,但不需要with. worker 方法类似于 Ry- 的链接答案 - 现在已删除,但如果您愿意,可以在此处查看
2021-05-13 10:16:39

只需使用"use strict". 更多关于严格模式

在此处输入图片说明

'use strict'仅当您将要运行的所有代码严格放在同一行时才在控制台中有效,例如'use strict'; ('a').b = 'c';抛出错误,但将它们输入单独的行不会抛出,因为第二行不严格。
2021-05-04 10:16:39
b不是全局对象的属性,因此const a = b无论严格模式还是草率模式都会抛出。问题是关于像这样的行const a = location,它不会抛出,location全局对象上的一个属性在哪里,那个不小心提到的。
2021-05-13 10:16:39