ES6以上版本代码要不要转码成ES5?

Posted Jtag特工

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6以上版本代码要不要转码成ES5?相关的知识,希望对你有一定的参考价值。

ES6以上版本代码要不要转码成ES5?

为了兼容老的浏览器,尤其是IE系列,使用ES6以上规范的前端代码往往使用Babel等转码工具转码成ES5的代码。

距离发布ES6的2015年已经过去了6年了,现在浏览器对于ES6的兼容性如何呢?

我们来看下CanIUse的数据:

可以看到,有98.14%的浏览器支持ES6. 没有超过99%的原因是因为2015年发布的Opera Mini还有1.08%的使用率。
针对手机端,2016年以后发布的Safari on ios和Chrome等全部都支持ES6.
Safari on iOS 7-9.3目前的用户占比0.15%.
android从5版本开始WebView已经全线支持ES6.

从数据上看起来,因为数量很少的老设备导致近99%以上的设备能力没有应用起来,似乎并没有说服力。
另外,很多应用针对低端机有特殊的处理,中高端机一定是近期的老设备。至少针对中高端机,转码的兼容性必要性基本上是可以忽略的。

不过,ES6及以上版本是由多个功能组成的,不能简单抽象成6比5好。
我们将主要的功能转码和不转码做一个对比。

不转码效果更好

const

const是要带常量检查的。我们来个例子:

let f1 = () => 
  const a = 0;
  a = 2;
;
f1();

转码之后,Babel帮我们生成了一个_readOnlyError函数。

function _readOnlyError(name)  throw new TypeError("\\"" + name + "\\" is read-only"); 

var f1 = function f1() 
  var a = 0;
  2, _readOnlyError("a");
;
f1();

这个不用看字节码了,看源码就知道是哪个更好了。

数组拷贝

ES6之后,我们做数组拷贝使用扩展运算符"…".

  const a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  let a2 = [...a1];

Babel做的转码也不含糊,一个concat函数搞定:

  var a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  var a2 = [].concat(a1);

但是,从字节码角度来看,就不一样了。因为v8提供了CreateArrayFromIterable指令。
所以,转码之前,9个字节的指令就搞定了:

Bytecode length: 9
Parameter count 1
Register count 2
Frame size 16
OSR nesting level: 0
Bytecode Age: 0
         0x3c140829374e @    0 : 79 00 00 25       CreateArrayLiteral [0], [0], #37
         0x3c1408293752 @    4 : c4                Star0 
         0x3c1408293753 @    5 : 7a                CreateArrayFromIterable 
         0x3c1408293754 @    6 : c3                Star1 
         0x3c1408293755 @    7 : 0e                LdaUndefined 
         0x3c1408293756 @    8 : a9                Return 
Constant pool (size = 1)
0x3c1408293721: [FixedArray] in OldSpace
 - map: 0x3c1408002205 <Map>
 - length: 1
           0: 0x3c1408293715 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x3c14082936e5 <FixedArray[10]>>

转码之后就是函数调用了,还有生成一个空数组,一共需要21个字节:

         0x3c1408293696 @    0 : 79 00 00 25       CreateArrayLiteral [0], [0], #37
         0x3c140829369a @    4 : c4                Star0 
         0x3c140829369b @    5 : 7b 01             CreateEmptyArrayLiteral [1]
         0x3c140829369d @    7 : c1                Star3 
         0x3c140829369e @    8 : 2d f7 01 02       LdaNamedProperty r3, [1], [2]
         0x3c14082936a2 @   12 : c2                Star2 
         0x3c14082936a3 @   13 : 5e f8 f7 fa 04    CallProperty1 r2, r3, r0, [4]
         0x3c14082936a8 @   18 : c3                Star1 
         0x3c14082936a9 @   19 : 0e                LdaUndefined 
         0x3c14082936aa @   20 : a9                Return 
Constant pool (size = 2)
0x3c1408293665: [FixedArray] in OldSpace
 - map: 0x3c1408002205 <Map>
 - length: 2
           0: 0x3c1408293659 <ArrayBoilerplateDescription PACKED_SMI_ELEMENTS, 0x3c1408293629 <FixedArray[10]>>
           1: 0x3c1408202e9d <String[6]: #concat>

String.raw

对于String.raw,转码也是会多生成函数的。
比如转码前是这样:

let f1 = () => 
  String.raw`\\n`;
;

f1();

转码之后,Babel帮我们生成了一个_taggedTemplateLiteral函数:

var _templateObject;

function _taggedTemplateLiteral(strings, raw)  if (!raw)  raw = strings.slice(0);  return Object.freeze(Object.defineProperties(strings,  raw:  value: Object.freeze(raw)  )); 

var f1 = function f1() 
    String.raw(_templateObject || (_templateObject = _taggedTemplateLiteral(["\\n"])));
;

f1();

Symbol

Symbol是ES6新增的数据类型。
在ES6里,我们要判断其类型,直接使用typeof运算符就好。

let f2 = () => 
  let s1 = Symbol();
  return typeof s1;
;

这可难为Babel了,都得引入一个库才能解决:

function _typeof(obj)  "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj)  return typeof obj;  : function (obj)  return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; , _typeof(obj); 

var f1 = function f1() 
  var s1 = Symbol();
  return _typeof(s1);
;

rest参数

为了支持rest参数,v8提供了CreateRestParameter指令。不过,原有的arguments也是有CreateMappedArguments指令支持的。
二者算是打平。

不过,从源代码的角度来看,不转码的会短一些:

let f1 = (...values) => 
    let sum = 0;
    for (let v of values) 
        sum += v;
    
    return sum;
;
f1(1, 4, 9);

转码之后如下:

var f1 = function f1() 
    var sum = 0;

    for (var _len = arguments.length, values = new Array(_len), _key = 0; _key < _len; _key++) 
        values[_key] = arguments[_key];
    

    for (var _i = 0, _values = values; _i < _values.length; _i++) 
        var v = _values[_i];
        sum += v;
    

    return sum;
;

可选catch参数

这是ES2019的特性,可以省略catch中的错误类型。在2018年上半年即被safari所支持。

let f3 = f2 => 
  try 
    f2();
   catch 
    console.error("Error");
  
;

转码之后,Babel会给我们生成一个未使用的error变量_unused

var f1 = function f1(f2) 
    try 
        f2();
     catch (_unused) 
        console.error("Error");
    
;

带有error变量的情况下,v8通过CreateCatchContext为我们生成CatchContext,并且为catch块生成了一个CATCH_SCOPE:

         0x1937082936b6 @    0 : 19 ff fa          Mov <context>, r0
         0x1937082936b9 @    3 : 61 03 00          CallUndefinedReceiver0 a0, [0]
         0x1937082936bc @    6 : 8a 20             Jump [32] (0x1937082936dc @ 38)
         0x1937082936be @    8 : c3                Star1 
         0x1937082936bf @    9 : 82 f9 00          CreateCatchContext r1, [0]
         0x1937082936c2 @   12 : c4                Star0 
         0x1937082936c3 @   13 : 10                LdaTheHole 
         0x1937082936c4 @   14 : a6                SetPendingMessage 
         0x1937082936c5 @   15 : 0b fa             Ldar r0
         0x1937082936c7 @   17 : 1a f9             PushContext r1
         0x1937082936c9 @   19 : 21 01 02          LdaGlobal [1], [2]
         0x1937082936cc @   22 : c1                Star3 
         0x1937082936cd @   23 : 2d f7 02 04       LdaNamedProperty r3, [2], [4]
         0x1937082936d1 @   27 : c2                Star2 
         0x1937082936d2 @   28 : 13 03             LdaConstant [3]
         0x1937082936d4 @   30 : c0                Star4 
         0x1937082936d5 @   31 : 5e f8 f7 f6 06    CallProperty1 r2, r3, r4, [6]
         0x1937082936da @   36 : 1b f9             PopContext r1
         0x1937082936dc @   38 : 0e                LdaUndefined 
         0x1937082936dd @   39 : a9                Return 
Constant pool (size = 4)
0x19370829367d: [FixedArray] in OldSpace
 - map: 0x193708002205 <Map>
 - length: 4
           0: 0x193708293649 <ScopeInfo CATCH_SCOPE>
           1: 0x193708202741 <String[7]: #console>
           2: 0x193708202769 <String[5]: #error>
           3: 0x19370800455d <String[5]: #Error>

而对于可选catch参数的情况下,直接不生成CatchContext:

         0x19370829376a @    0 : 19 ff fa          Mov <context>, r0
         0x19370829376d @    3 : 61 03 00          CallUndefinedReceiver0 a0, [0]
         0x193708293770 @    6 : 8a 15             Jump [21] (0x193708293785 @ 27)
         0x193708293772 @    8 : 10                LdaTheHole 
         0x193708293773 @    9 : a6                SetPendingMessage 
         0x193708293774 @   10 : 21 00 02          LdaGlobal [0], [2]
         0x193708293777 @   13 : c2                Star2 
         0x193708293778 @   14 : 2d f8 01 04       LdaNamedProperty r2, [1], [4]
         0x19370829377c @   18 : c3                Star1 
         0x19370829377d @   19 : 13 02             LdaConstant [2]
         0x19370829377f @   21 : c1                Star3 
         0x193708293780 @   22 : 5e f9 f8 f7 06    CallProperty1 r1, r2, r3, [6]
         0x193708293785 @   27 : 0e                LdaUndefined 
         0x193708293786 @   28 : a9                Return 
Constant pool (size = 3)
0x193708293735: [FixedArray] in OldSpace
 - map: 0x193708002205 <Map>
 - length: 3
           0: 0x193708202741 <String[7]: #console>
           1: 0x193708202769 <String[5]: #error>
           2: 0x19370800455d <String[5]: #Error>

Generator

解析赋值这样使用迭代器的方式是下一节转码效率高的部分。
但是,针对Generator这样显式使用迭代器的,就是另一番情况了。

我们看一个最简单的Generator,我们只生成几个数字:

let f1 = () => 
  let obj1 = 
    *[Symbol.iterator]() 
      yield 1;
      yield 2;
      yield 3;
    
  ;
  [...obj1];
;

我们看到,转码后的结果,不但定义了几个函数,还需要regeneratorRuntime运行时的支持:

function _toConsumableArray(arr)  return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); 

function _nonIterableSpread()  throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 

function _unsupportedIterableToArray(o, minLen)  if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); 

function _iterableToArray(iter)  if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); 

function _arrayWithoutHoles(arr)  if (Array.isArray(arr)) return _arrayLikeToArray(arr); 

function _arrayLikeToArray(arr, len)  if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++)  arr2[i] = arr[i];  return arr2; 

var f1 = function f1() 
  var obj1 = 
    [Symbol.iterator]() 
      return /*#__PURE__*/regeneratorRuntime.mark(function _callee() 
        return regeneratorRuntime.wrap(function _callee$(_context) 
          while (1) 
            switch (_context.prev = _context.next) 
              case 0:
                _context.next = 2;
                return 1;

              case 2:
                _context.next = 4;
                return 2;

              case 4:
                _context.next = 6;
                return 3;

              case 6:
              case "end":
                return _context.stop();
            
          
        , _callee);
      )();
    

  ;

  _toConsumableArray(obj1);
;

如果将上面的代码用Node运行,会报错:

  var obj1 = _defineProperty(, Symbol.iterator, /*#__PURE__*/regeneratorRuntime.mark(function _callee() 
                                                               ^
ReferenceError: regeneratorRuntime is not defined

因为运行库没有被引入进来,我们还得加个库,先install:

npm install --save @babel/polyfill

然后把库引进来:

require("@babel/polyfill");
...
var f1 = function f1() 
    var obj1 = 
        [Symbol.iterator]() 
            return /*#__PURE__*/regeneratorRuntime.mark(function _callee() 
...

这增加了多少代码量,就留给读者自己去算吧。

虽然说class本质上跟Function的语法糖也差不多,但是,Babel转码生成出来的代码,可能比大多数同学想象的多。
我们看一个简单的例子:

  class Code 
    constructor(source) 
      this.source = source;
    
  
  code1 = new Code("test1.js");

转码之后效果如下,Babel为我们生成了_createClass,_classCallCheck和_defineProperties三个函数:

function _defineProperties(target, props)  for (var i = 0; i < props.length; i++)  var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor);  

function _createClass(Constructor, protoProps, staticProps)  if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype",  writable: false ); return Constructor; 

function _classCallCheck(instance, Constructor)  if (!(instance instanceof Constructor))  throw new TypeError("Cannot call a class as a function");  

var Code = /*#__PURE__*/_createClass(function Code(source) 
    _classCallCheck(this, Code);

    this.source = source;
);

code1 = new Code("test1.js");

依赖polyfill和runtime的内置对象

我们知道,ES6新增了不少对象还有原有对象的新属性。

比如你使用新增加的Set,Map,WeakSet,WeakMap等对象,或者是Number.isNaN这样的新方法,Babel并没有帮我们转码成ES5的语句。
那么,这些语句在老的游览器是如何支持的呢?
答案大家可能已经猜到了,就是我们之前Generator时用到的@babel/polyfill库。

Babel polyfill库实际上是基于两个开源的库:

  • 一个是facebook的regenerator库,目前运行时是一个700多行的文件:regenerator runtime .
  • 另一个是core-js库,大部分的内置对象的支持都靠它。

我们代码写成这样:

Array.from(new Set([1, 2, 3, 2, 1]));
[1, [2, 3], [4, [5]]].flat(2);
Promise.resolve(32).then(x => console.log(x));

实际上Babel/runtime执行的是这样的:

import from from 'core-js-pure/stable/array/from';
import flat from 'core-js-pure/stable/array/flat';
import Set from 'core-js-pure/stable/set';
import Promise from 'core-js-pure/stable/promise';

from(new Set([1, 2, 3, 2, 1]));
flat([1, [2, 3], [4, [5]]], 2);
Promise.resolve(32).then(x => console.log(x));

转码效果更好

凡事皆有特例,从某些功能来讲,也许转码实现更好。

解构赋值

我们引用阮一峰老师的变量交换的的例子:

let f1 = () => 
  let x = 1;
  let y = 2;
  [x, y] = [y, x];
;

f1();

转码之后变成这样:

var f1 =

以上是关于ES6以上版本代码要不要转码成ES5?的主要内容,如果未能解决你的问题,请参考以下文章

ES6以上版本代码要不要转码成ES5?

ES6以上版本代码要不要转码成ES5?

如何使用Babel将ES6转码为ES5?

涨知识|Gulp + Babel实现es6向es5转码

ECMAScript6转码器,ES6转ES5

如何使用Babel将ES6转码为ES5