JavaScript ES6 跨浏览器检测

85

我如何查找浏览器的Javascript引擎版本,并支持ECMAScript 6?

我正在使用navigator.appVersion来了解浏览器的版本,但不是引擎的版本。


你为什么想知道那个? - user663031
没有标准属性可以直接读取该信息。您可以构建一个已知平台的字典或使用功能测试。 - dandavis
2
如果你想要新的语法,使用一个可以输出与旧版本兼容的JavaScript的预编译器。如果你想要新的功能,就像Marco Bonelli所推荐的那样,使用特性检测。 - CodesInChaos
1
您可以使用feature-detect-es6库。 - Lloyd
11个回答

132

特性检测

我建议您使用特性检测而不是使用启发式方法来检测浏览器引擎。您可以简单地try {..} catch (e) {...}语句中包装一些代码,或者使用一些if (...)语句来实现这一点。

例如:

function check() {
    if (typeof SpecialObject == "undefined") return false;
    try { specialFunction(); }
    catch (e) { return false; }

    return true;
}

if (check()) {
    // Use SpecialObject and specialFunction
} else {
    // You cannot use them :(
}

为什么特性检测比浏览器/引擎检测更好?

有多个原因使得在大多数情况下,特性检测是最佳选择:

  • 你不必依赖于浏览器的版本、引擎或细节,也不必使用难以实现且相当巧妙的启发式方法来检测它们。

  • 你不会出现关于浏览器/引擎规范检测的错误。

  • 你不必担心特定于某个浏览器的功能:例如,WebKit 浏览器具有与其他浏览器不同的规范。

  • 一旦检测到某个特性,你可以确信能够使用它。

这些是我认为特性检测是最佳方法的主要原因。

特性检测 + 回退方案

当使用特性检测时,一种非常聪明的工作方式是,在你不确定哪些特性可以/不能使用时,采用多个特性检测和相应的回退方案(甚至可以从头开始创建这些方法),以防所需使用的特性不受支持。

一个简单的特性检测与回退方案示例可应用于window.requestAnimationFrame特性,该特性并非所有浏览器都支持,并且具有不同的前缀,具体取决于你正在使用的浏览器。在这种情况下,你可以轻松地进行检测和回退方案,如下所示:

requestAnimationFrame = 
   window.requestAnimationFrame       // Standard name
|| window.webkitRequestAnimationFrame // Fallback to webkit- (old versions of Chrome or Safari)
|| window.mozRequestAnimationFrame    // Fallback to moz- (Mozilla Firefox)
|| false;                             // Feature not supported :(

// Same goes for cancelAnimationFrame
cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || false;

if (!requestAnimationFrame) {
    // Not supported? Build it by yourself!
    requestAnimationFrame = function(callback) {
        return setTimeout(callback, 0);
    }

    // No requestAnim. means no cancelAnim. Built that too.
    cancelAnimationFrame = function(id) {
        clearTimeout(id);
    }
}

// Now you can use requestAnimationFrame 
// No matter which browser you're running
var animationID = requestAnimationFrame(myBeautifulFunction);

ECMAScript 6(Harmony)功能检测

现在,来到真正的问题:如果您想检测对ES6的支持,您将无法像上面所说的那样行事,因为相当一部分ES6特性基于新的语法和私有词汇,并且在ES5中使用会抛出SyntaxError,这意味着编写同时包含ES5和ES6的脚本是不可能的!

以下示例说明了此问题;下面的代码片段将无法工作,并且在执行之前将被阻止,因为它包含非法语法。

function check() {
    "use strict";

    try { eval("var foo = (x)=>x+1"); }
    catch (e) { return false; }
    return true;
}

if (check()) {
    var bar = (arg) => { return arg; }
    // THIS LINE will always throw a SyntaxError in ES5
    // even before checking for ES6
    // because it contains illegal syntax.
} else {
    var bar = function(arg) { return arg; }
}

现在,由于您不能在同一个脚本中同时检查和执行ES6条件,您需要编写两个不同的脚本:一个仅使用ES5,另一个包含ES6功能。通过两个不同的脚本,只有在支持ES6时才能导入ES6脚本,而不会引发SyntaxErrors

ES6检测和条件执行示例

现在让我们举一个更具体的例子,假设您要在ES6脚本中使用以下功能:

  • 新的Symbol对象
  • 使用class关键字构建的类
  • 箭头((...)=>{...})函数

注意:只能使用eval()函数或其他等效函数(例如Function())对新引入的语法进行特性检测(如箭头函数),因为编写无效的语法将在执行之前停止脚本。这也是为什么您不能使用if语句检测类和箭头函数的原因:这些功能涉及关键字和语法,因此在try {...} catch (e) {...}块中包装的eval(...)将正常工作。

因此,来看真正的代码:

  • HTML Markup:

    <html>
        <head>
            <script src="es5script.js"></script>
        </head>
        <body>
            <!-- ... -->
        </body>
    </html>
    
  • Code in your es5script.js script:

    function check() {
        "use strict";
    
        if (typeof Symbol == "undefined") return false;
        try {
            eval("class Foo {}");
            eval("var bar = (x) => x+1");
        } catch (e) { return false; }
    
        return true;
    }
    
    if (check()) {
        // The engine supports ES6 features you want to use
        var s = document.createElement('script');
        s.src = "es6script.js";
        document.head.appendChild(s);
    } else {
        // The engine doesn't support those ES6 features
        // Use the boring ES5 :(
    }
    
  • Code in your es6script.js:

    // Just for example...
    "use strict";
    
    class Car { // yay!
       constructor(speed) {
           this.speed = speed;
       }
    }
    
    var foo = Symbol('foo'); // wohoo!
    var bar = new Car(320);  // blaze it!
    var baz = (name) => { alert('Hello ' + name + '!'); }; // so cool!
    

浏览器/引擎检测

正如我上面所说的,编写JavaScript脚本时,浏览器和引擎检测并不是最佳实践。我将为您提供一些关于这个主题的背景知识,以免让我的话成为“随意的个人意见”。

引用MDN文档 [链接]:

在考虑使用用户代理字符串来检测所使用的浏览器时,如果可能的话,请首先尝试避免这种做法。首先请尝试确定您为什么要这样做。

[...] 您是否试图检查某个特定功能的存在? 您的网站需要使用一项特定的Web功能,但某些浏览器尚不支持,因此您想将这些用户发送到拥有较少功能但可以正常工作的旧网站。这是使用用户代理检测的最糟糕的原因,因为很有可能其他所有浏览器最终都会达到相同的功能水平。在这种情况下,您应该尽力避免使用用户代理嗅探,而是使用 功能检测

而且,您说您使用navigator.appVersion,但请考虑使用另一种方法,因为那一个以及许多其他navigator属性已被废弃,并且不总是像您想象的那样运行。

所以,再次引用MDN文档 [链接]:

已弃用:此特性已从Web标准中删除。虽然某些浏览器仍可能支持它,但正在逐步停止使用。不要在旧或新项目中使用它。使用它的页面或Web应用程序可能随时会崩溃。

注意:不要依赖该属性返回正确的浏览器版本。在基于Gecko的浏览器(如Firefox)和基于WebKit的浏览器(如Chrome和Safari)中,返回的值以“5.0”开头,后面跟有平台信息。在Opera 10及更高版本中,返回的版本号也与实际的浏览器版本不匹配。


2
只是提醒一下:有一个表格显示ES6的支持情况 https://kangax.github.io/compat-table/es6/ - zaynetro
2
"特性检测"不起作用,因为你无法使用try/catch来解决像fat arrows、let和class这样的语法错误。它可以检查诸如"".startsWith之类的东西,但这一直很容易。要检测基于语法的功能,您需要使用eval()将无效代码隐藏在ES5解析器中。 - dandavis
@dandavis 再次感谢,是的我知道人们总是倾向于将 eval 与编程中的“那个不可言传的名字”联系在一起,但我也知道,如果你成功地以正确的方式实现它,它并不是如此恶劣的功能。 - Marco Bonelli
1
@user3412159 我编辑了我的回答!请查看新的回答,因为之前的完全错误了!谢谢。 - Marco Bonelli
1
使用上述方法是一种hack或解决方法,我认为浏览器应该告诉服务器要提供哪个文件,或者给出这样的选项。因此,应该有一个请求头,如engineVersion: es5engineVersion: es6来显示它是否支持,如果打开/关闭了flags,也应该起作用,这样您就不必依赖js script标签来加载资源。服务器应该能够提供正确的文件以供使用或遗留。但这仍然是一种观点 :) - Val
显示剩余8条评论

22

现在支持 ES6 模块的浏览器厂商提供了一种简单的功能检测方法:

Browser vendors that support ES6 modules now provide an easy way to do feature detection:
...
<head>
  <script nomodule>window.nomodules = true;</script>
  <script>console.log(window.nomodules)</script>
</head>
...

nomodule属性的脚本不会被支持<script type="module" ...>的浏览器执行。

您也可以像这样注入脚本:

const script = document.createElement('script');
script.setAttribute('nomodule', '');
script.innerHTML = 'window.nomodules = true;';
document.head.insertBefore(script, document.head.firstChild);
script.remove();

9

正如Marco Bonelli所说,检测ECMAScript 6语法的最佳方法是使用eval();。如果调用不会抛出错误,则表示支持“所有其他”功能,但我建议使用Function();

function isES6()
{
    try
    {
        Function("() => {};"); return true;
    }
    catch(exception)
    {
        return false;
    }
}

demo: https://jsfiddle.net/uma4Loq7/


5
使用 eval()Function() 与未指定 'unsafe-eval' 的内容安全策略不兼容。 - Damian Yerrick
如果您可以访问index.html,那么只需使用<script>标签来评估表达式即可。请参见下面的答案。 - Maciej Krawczyk
检测箭头函数支持并不能覆盖所有ES6功能。https://caniuse.com/es6-class,arrow-functions - Maciej Krawczyk

7

无需使用eval的ES6特性检测

你可以通过在自己的脚本块中插入检测代码并在结尾进行全局变量赋值来实现,而无需使用eval。如果脚本块中发生任何错误,变量赋值将不会运行。

<script>
    window.isES6 = false;
</script>
<script>
    // Arrow functions support
  () => { };
  
  // Class support
  class __ES6FeatureDetectionTest { };
  
  // Object initializer property and method shorthands
  let a = true;
  let b = { 
    a,
    c() { return true; },
    d: [1,2,3],
   };
  
  // Object destructuring
  let { c, d } = b;
  
  // Spread operator
  let e = [...d, 4];

  window.isES6 = true;
</script>

<script>
document.body.innerHTML += 'isES6: ' + window.isES6;
</script>

https://jsfiddle.net/s5tqow91/2/

请注意,ES6有许多功能,仅检查一个功能并不能保证你已经涵盖了所有功能。(上面的代码也没有覆盖所有内容,只是我认为我最常使用的功能)。

为什么不使用eval?

主要原因是安全性,并不是说调用eval进行特征检测本身就不安全。而是理想情况下,你应该禁止使用Content Security Policy中的eval,以便完全禁用它-这大大减少了攻击面。但如果你自己的代码使用了eval,你就无法这样做。

undefined 是一个假值。因此,在第一个 script 中您不需要初始化。 - ceving
你不需要那个,但通常你会期望该值是 boolean 类型,而不是 true | undefined - Maciej Krawczyk

4
  1. 检测 devicePixelRatio 属性,它是 WebKit 中的一个特殊属性。
  2. 检测 javaEnabled 函数的实现。

(function() {
  var v8string = 'function%20javaEnabled%28%29%20%7B%20%5Bnative%20code%5D%20%7D';
  var es6string = 'function%20javaEnabled%28%29%20%7B%0A%20%20%20%20%5Bnative%20code%5D%0A%7D';

  if (window.devicePixelRatio) //If WebKit browser
  {
    var s = escape(navigator.javaEnabled.toString());
    if (s === v8string) {
      alert('V099787 detected');
    } else if (s === es6string) {
      alert('ES6 detected')
    } else {
      alert('JSC detected');
    }
  } else {
    display("Not a WebKit browser");
  }

  function display(msg) {
    var p = document.createElement('p');
    p.innerHTML = msg;
    document.body.appendChild(p);
  }

})()


1
这很好,但我想知道在火狐浏览器上我在哪里可以直接看到引擎版本? - RoliCo
5
如果 (window.devicePixelRatio) // 如果是 WebKit 浏览器 <= 这很糟糕。请参考 Marco Bonelli 的回答,了解为什么不应该使用浏览器检测,而应该使用特性检测。 备注:已按要求进行翻译,并未添加任何解释或额外内容。 - Arturo Torres Sánchez

1

目前还没有确切的方法来检测ES6,但是如果在当前浏览器中测试其功能,则可以确定引擎是否为ES6。我的 esx 库通过进行语法测试和方法检查来检测ECMAScript版本。它可以检测ECMAScript 3、5、6和7(未经测试的ES7应该可以工作),如果没有匹配到ECMAScript测试,则返回null作为结果。

使用我的库的示例:

if (esx.detectVersion() >= 6) {
    /* We're in ES6 or above */
}

3
本仓库中 esdetect.js 文件第 46 行的 eval() 使用与未指定 'unsafe-eval' 的内容安全策略不兼容。 - Damian Yerrick

0
将不兼容的语法代码(例如包含箭头函数)放入自己的脚本块中,并使用兼容的语法代码进行填充。
<script>
        // This script block should not compile on incompatible browsers, 
        // leaving the function name undefined.
        // It can then be polyfilled with a function containing compatible syntax code.
        function fame() {
            /* incompatible syntax code such as arrow functions */
        }
</script>

<script>
    if (typeof fame !== "function") {
        // alert("polyfill: fame");
        function fame() {
            /* compatible syntax code */
        }
    }
</script>

<script>
    // main code
    fame();
</script>

这个解决方案没有问题。然而,如果我是你,我会将回退代码放入一个外部文件中,并使用 document.write("<script src=/path/to/file.js></script>") 来加载它,这样那些拥有现代浏览器的人就不会被强制下载他们不需要的代码。 - PHP Guru

0

此函数在 Chrome 98.0.4758.80 和 Firefox 97.0.2 中返回 true(已测试)。它可能无法在其他浏览器和早期版本的 Chrome/Firefox 上工作(会出现错误的负面结果)

function hasAsyncSupport () {
    return Object.getPrototypeOf(async function() {}).constructor.toString().includes('Async')
}

0

我认为最好的方法是将所有ES6脚本都编写为模块,并对任何回退使用nomodule脚本标记。尝试编写内联检测是不必要的。

当然,这引出了一个问题,如何检测来自ES6后版本的新语法,例如"??"和"?."运算符。


0

使用UAParser.js检测浏览器是否支持ECMAScript 6。

function supportsES6(engine) {
  if (typeof engine.name === 'undefined' || typeof engine.version === 'undefined') {
    return false;
  }
  if (engine.name in MinimumForES6) {
    return parseInt(engine.version) >= MinimumForES6[engine.name];
  }
  return false;
}

/*
ref:
- https://caniuse.com/es6
- https://www.lambdatest.com/web-technologies/es6
*/
var MinimumForES6 = {
  "Blink": 51,
  "EdgeHTML": 15,
  "Gecko": 54,
  "WebKit": 602
};

window.addEventListener('DOMContentLoaded', function() {
  var ua_parser = new UAParser();
  var ua_result = ua_parser.getResult();
  window['isES6'] = supportsES6(ua_result.engine);

  console.log('isEs6: '+ window.isES6);
});
<script src="https://cdn.jsdelivr.net/npm/ua-parser-js@1.0.33/src/ua-parser.min.js"></script>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接