Javascript ES6 跨浏览器检测

Javascript ES6 cross-browser detection

如何找到浏览器的 Javascript 引擎版本和对 ECMAScript 6 的支持?

我使用 navigator.appVersion 只是为了知道浏览器的版本,而不是引擎的版本。

  1. 检测WebKit中特殊的属性devicePixelRatio。
  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);
  }

})()

特征检测

我建议您使用特征检测,而不是使用启发式方法检测浏览器的引擎。为此,您可以简单地 将一些代码包装在 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 :(
}

为什么特征检测比 browser/engine 检测更好?

在大多数情况下,有多种原因使得特征检测成为最佳选择:

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

  • 您不会陷入browser/engine规格检测的错误。

  • 您不必担心特定于浏览器的功能:例如 WebKit 浏览器的规格与其他浏览器不同。

  • 您可以确定,一旦检测到某个功能,您就可以使用它。

这些是恕我直言使特征检测成为最佳方法的主要原因。

特征检测+回退

使用特征检测时,当您不确定要使用哪些特征时,这是一种非常聪明的工作方式can/cannot使用包括 几个特征检测和随后的 回退 到更基本的方法 (甚至从头开始创建这些方法)以防您想要的特征不支持使用。

带有回退的特征检测的一个简单示例可以应用于 window.requestAnimationFrame 特征,并非所有浏览器都支持该特征,并且有几个不同的前缀取决于在您正在使用的浏览器上。在这种情况下,您可以像这样轻松检测和 fallback

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,并且不会导致 SyntaxErrors 被抛出。

ES6检测和条件执行示例

现在让我们做一个更相关的例子,假设你想在你的 ES6 脚本中使用这些特性:

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

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

那么,进入真正的代码:

  • HTML 标记:

    <html>
        <head>
            <script src="es5script.js"></script>
        </head>
        <body>
            <!-- ... -->
        </body>
    </html>
    
  • 您的 es5script.js 脚本中的代码:

    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 :(
    }
    
  • 您的代码 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!
    

Browser/engine检测

就像我上面说的,浏览器和引擎检测不是编程某些 JavaScript 脚本时的最佳实践。我会给你一些关于这个话题的背景知识,只是不要把我的话留作 "random personal opinion".

引自 MDN 文档 [link]:

When considering using the user agent string to detect which browser is being used, your first step is to try to avoid it if possible. Start by trying to identify why you want to do it.

[...] Are you trying to check for the existence of a specific feature? Your site needs to use a specific Web feature that some browsers don't yet support, and you want to send those users to an older Web site with fewer features but that you know will work. This is the worst reason to use user agent detection, because odds are eventually all the other browsers will catch up. You should do your best to avoid using user agent sniffing in this scenario, and do feature detection instead.

此外,您说您使用 navigator.appVersion,但请考虑使用另一种方法,因为该方法与许多其他导航器属性一起 已弃用 ,并且并不总是像你想象的那样。

因此,再次引用 MDN 文档 [link]:

Deprecated: this feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped. Do not use it in old or new projects. Pages or Web apps using it may break at any time.

Note: Do not rely on this property to return the correct browser version. In Gecko-based browsers (like Firefox) and WebKit-based browsers (like Chrome and Safari) the returned value starts with "5.0" followed by platform information. In Opera 10 and newer the returned version does not match the actual browser version, either.

目前还没有一个准确的方法来检测ES6,但是如果你在当前浏览器中测试它的特性,你可以判断引擎是不是ES6。我的 esx 库通过语法测试和方法检查来检测 ECMAScript 版本。因为知道它可以检测 ECMAScript 3、5、6 和 7(ES7 未测试,但应该可以),如果没有匹配的 ECMAScript 测试,它会给出 null 作为结果。

使用我的库的示例:

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

将不兼容的语法代码(例如包含箭头函数)放入它自己的脚本块中,并使用兼容的语法代码对其进行填充。

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

正如 Marco Bonelli 所说,检测 ECMAScript 6 语言语法的最佳方法是使用 eval();。如果调用没有抛出错误,支持 "all other" 功能,但我推荐 Function();.

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

演示: https://jsfiddle.net/uma4Loq7/

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

...
<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();

正如 Damian Yerrick 所提到的,使用 eval() 或 Function() 与未指定 'unsafe-eval'.

的内容安全策略不兼容

如果浏览器支持 Worker,那么您可以通过在 worker 中实现该语法并检查错误或成功来检测对任何 ES6 语法的支持,例如检测对箭头函数的支持:

worker.js

// If ES6 arrow functions are supported then the worker listener will receive true, otherwise it will receive an error message
(() => {
    postMessage(true);
})();

index.js

if (typeof (Worker) !== "undefined") {

    var myWorker = new Worker('worker.js');

    myWorker.onmessage = function (e) {
        // arrow functions must be supported since we received message from the worker arrow function
    }

    myWorker.onerror = function (e) {
        // the worker triggered an error so arrow function not supported (could explicitly check message for syntax error)
    }
}

没有 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,你就不能这样做了。

此功能 returns 在 Chrome 98.0.4758.80 和 Firefox 97.0.2(刚刚测试)中为真。它可能不适用于其他浏览器和 Chrome/Firefox 的早期版本(假阴性结果)

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