ログ日記

作業ログと日記とメモ

Closure Compiler の出力するJavaScriptを ES5(旧)とES6(新)で比べる

ふと、ES6よりES5で書いた方が軽くなるかと思ったので疑問を解消するために確かめる。
※ ES5、ES6と書きつつ、単に書き方の違いの調査になってきたので新・旧とする。



Closure Compiler と Closure Libraryをダウンロードする。

wget https://dl.google.com/closure-compiler/compiler-latest.zip

unar compiler-latest.zip
ln -s compiler-latest/closure-compiler-v20190325.jar closure-compiler.jar


https://github.com/google/closure-library/archive/v20190325.tar.gz
unar v20190325.tar.gz
ln -s closure-library-20190325 closure-library 

プログラムのディレクトリを作る。

mkdir js
mkdir js/es5
mkdir js/es6

Closure Compiler は何度も実行するのでコマンドを書いておく。
build.sh

#!/bin/bash


set -eux

function compile(){
    java -jar compiler.jar \
         --compilation_level=ADVANCED_OPTIMIZATIONS \
         --define 'goog.LOCALE=ja' \
         --define 'goog.DEBUG=false' \
         --create_source_map $1.js.map \
         --dependency_mode=STRICT \
         --entry_point=goog:entrypoint \
         'closure-library/closure/**.js' \
        '!closure-library/closure/**test.js' \
         "js/$1/**.js" \
         > $1.js

    echo >> $1.js
    echo "//@ sourceMappingURL=$1.js.map" >> $1.js
}

compile $1

まずは簡単なHello Worldから。

goog.provide('entrypoint');

function hello(v){
  console.log('hello ' + v);
}
function startApp(){
  hello('es5');
}

startApp();

goog.module('entrypoint');

const hello = (v) => console.log('hello ' + v);
const startApp = () => {
  hello('es6');
}

startApp();

まあこれぐらいなら基本なので全て削除されるかなと予想しつつコンパイル

chmld +x build.sh

./build.sh es5
./build.sh es6


結果。
app.js

console.log("hello es5");

console.log("hello es6");

ファイルを分けてみる。

// app.js
goog.provide('entrypoint');
goog.require('app.Foo');

window['startApp'] = app.Foo.startApp;

// foo.js
goog.provide('app.Foo');

app.Foo.hello = function(v){
  console.log('hello ' + v);
}
app.Foo.startApp = function(){
  app.Foo.hello('es5');
};

// app.js
goog.module('entrypoint');

const app = goog.require('app');
window['startApp'] = app.startApp;

// foo.js
goog.module('app');

const hello = (v) => console.log('hello ' + v);
const startApp = () => {
  hello('es6');
}

exports = {
  startApp
};


結果

window.startApp=function(){console.log("hello es5")};

window.startApp=function(){console.log("hello es6")};

どちらも無駄がない。



HTMLを作って呼び出す形にする。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Closure Test</title>

    <script src="es5.js"></script>
<!--    <script src="es6.js"></script> -->
  </head>
  <body>
    <h1>Closure Test</h1>
    <div id="contents">
      <div class="list"></div>
      <div class="list"></div>
      <div class="list"></div>
    </div>

    <script>
      startApp5();
      //startApp6();
    </script>
  </body>
</html>


// app.js
goog.provide('entrypoint');

goog.require('app.Foo');

window['startApp5'] = app.Foo.startApp;

// foo.js
goog.provide('app.Foo');
goog.provide('app.Bar');

goog.require('goog.dom');

app.Foo.hello = function(v){
  console.log('hello ' + v);
  
  var parentEl = goog.dom.getElement('contents');
  var children = goog.dom.getElementsByClass('list', parentEl);
  for (var i = 0; i < children.length; i++){
    goog.dom.setTextContent(children[i], 'list ' + i);
  }
}
app.Foo.startApp = function(){
  app.Foo.hello('es5');
};

// app.js
goog.module('entrypoint');

const app = goog.require('app');
window['startApp6'] = app.startApp;

// foo.js
goog.module('app');

const dom = goog.require('goog.dom');

const hello = (v) => {
  console.log('hello ' + v);

  const parentEl = dom.getElement('contents');
  const children = dom.getElementsByClass('list', parentEl);
  for (let i = 0; i < children.length; i++){
    dom.setTextContent(children[i], 'list ' + i);
  }
};
const startApp = () => hello('es6');


exports = {
  startApp
};


結果

var h = Array.prototype.indexOf ? function(a, b) {
    return Array.prototype.indexOf.call(a, b, void 0)
}
: function(a, b) {
    if ("string" == typeof a)
        return "string" == typeof b && 1 == b.length ? a.indexOf(b, 0) : -1;
    for (var c = 0; c < a.length; c++)
        if (c in a && a[c] === b)
            return c;
    return -1
}
;
window.startApp5 = function() {
    console.log("hello es5");
    var a, b = (a = document.getElementById("contents5")) || document;
    if (b.querySelectorAll && b.querySelector)
        a = b.querySelectorAll(".list");
    else {
        var c;
        b = document;
        a = a || b;
        if (a.querySelectorAll && a.querySelector)
            a = a.querySelectorAll(".list");
        else if (a.getElementsByClassName) {
            var d = a.getElementsByClassName("list");
            a = d
        } else {
            d = a.getElementsByTagName("*");
            var e = {};
            for (b = c = 0; a = d[b]; b++) {
                var g = a.className, f;
                if (f = "function" == typeof g.split)
                    f = 0 <= h(g.split(/\s+/), "list");
                f && (e[c++] = a)
            }
            e.length = c;
            a = e
        }
    }
    for (b = 0; b < a.length; b++)
        a[b].innerHTML = "list " + b
}
;

これもどちらも同じなので片方は省略。

goog.domを使ったのでindexOfやquerySelectorが無い場合の処理が入った。
querySelectorがある場合の処理だけ抜き出すと

window.startApp6 = function() {
    console.log("hello es6");
    var a, b = (a = document.getElementById("contents6")) || document;
    a = b.querySelectorAll(".list");
    }
    for (b = 0; b < a.length; b++)
        a[b].innerHTML = "list " + b
};

こうなる。
変数が再利用されていてvarとかletとかのレベルではなかった。




次にクラス。

// app.js
goog.provide('entrypoint');

goog.require('app.Bar');
goog.require('app.Baz');

var bar = new app.Bar('test');
bar.hello();
var baz = new app.Baz('test 2');
baz.hello();

// foo.js
goog.provide('app.Foo');
goog.provide('app.Bar');
goog.provide('app.Baz');

/**
 * @constructor
 */
app.Foo = function(value){
  this.value_ = value;
};
app.Foo.prototype.hello = function(){
  console.log('Foo > ' + this.value_);
};


/**
 * @constructor
 * @extends {app.Foo}
 */
app.Bar = function(value){
  app.Bar.base(this, 'constructor', value);
};
goog.inherits(app.Bar, app.Foo);

app.Foo.prototype.hello = function(){
  console.log('Bar > ' + this.value_);
};

/**
 * @constructor
 * @extends {app.Bar}
 */
app.Baz = function(value){
  app.Baz.base(this, 'constructor', value);
};
goog.inherits(app.Baz, app.Bar);

app.Foo.prototype.hello = function(){
  console.log('Baz > ' + this.value_);
};

// app.js
goog.module('entrypoint');

const app = goog.require('app');

const bar = new app.Bar('test');
bar.hello();
const baz = new app.Baz('test 2');
baz.hello();

// foo.js
goog.module('app');

class Foo {
  constructor(value){
    this.value_ = value;
  }

  hello(){
    console.log('Foo > ' + this.value_);
  }
}

class Bar extends Foo {
  constructor(value){
    super(value);
  }

  hello(){
    console.log('Bar > ' + this.value_);
  }
}

class Baz extends Bar {
  constructor(value){
    super(value);
  }

  hello(){
    console.log('Baz > ' + this.value_);
  }
}


exports = {
  Foo,
  Bar,
  Baz
}


結果

function c(a, d) {
    function g() {}
    g.prototype = d.prototype;
    a.f = d.prototype;
    a.prototype = new g;
    a.prototype.constructor = a;
    a.c = function(l, m, n) {
        for (var h = Array(arguments.length - 2), b = 2; b < arguments.length; b++)
            h[b - 2] = arguments[b];
        return d.prototype[m].apply(l, h)
    }
}
;function e(a) {
    this.a = a
}
e.prototype.b = function() {
    console.log("Foo > " + this.a)
}
;
function f(a) {
    this.a = a
}
c(f, e);
e.prototype.b = function() {
    console.log("Bar > " + this.a)
}
;
function k(a) {
    this.a = a
}
c(k, f);
e.prototype.b = function() {
    console.log("Baz > " + this.a)
}
;
(new f("test")).b();
(new k("test 2")).b();

var d = "function" == typeof Object.create ? Object.create : function(a) {
    function b() {}
    b.prototype = a;
    return new b
}
, e;
if ("function" == typeof Object.setPrototypeOf)
    e = Object.setPrototypeOf;
else {
    var f;
    a: {
        var g = {
            c: !0
        }
          , h = {};
        try {
            h.__proto__ = g;
            f = h.c;
            break a
        } catch (a) {}
        f = !1
    }
    e = f ? function(a, b) {
        a.__proto__ = b;
        if (a.__proto__ !== b)
            throw new TypeError(a + " is not extensible");
        return a
    }
    : null
}
var k = e;
function l(a, b) {
    a.prototype = d(b.prototype);
    a.prototype.constructor = a;
    if (k)
        k(a, b);
    else
        for (var c in b)
            if ("prototype" != c)
                if (Object.defineProperties) {
                    var m = Object.getOwnPropertyDescriptor(b, c);
                    m && Object.defineProperty(a, c, m)
                } else
                    a[c] = b[c];
    a.f = b.prototype
}
;function n(a) {
    this.a = a
}
n.prototype.b = function() {
    console.log("Foo > " + this.a)
}
;
function p(a) {
    this.a = a
}
l(p, n);
p.prototype.b = function() {
    console.log("Bar > " + this.a)
}
;
function q(a) {
    this.a = a
}
l(q, p);
q.prototype.b = function() {
    console.log("Baz > " + this.a)
}
;
(new p("test")).b();
(new q("test 2")).b();

class構文の方はトランスパイル後の共通コードが若干複雑になっているような。
ただし本体のコードはやはり同じだった。





その他、細かい動作等。
よくあるスコープミスのコード

function app()
{
  var a = [1, 2, 3, 5, 8];
  var b = [];
  for (var i = 0; i < a.length; i++){
    b.push(function(){
      console.log(i);
    });
  }
  for (var j = 0; j < b.length; j++){
    b[j]();
  }
}
app();
// compile
(function() {
    for (var a = [1, 2, 3, 5, 8], b = [], c = 0; c < a.length; c++)
        b.push(function() {
            console.log(c)
        });
    for (a = 0; a < b.length; a++)
        b[a]()
}
)();

単純なコードのままだけれどバグっている。
letを使う

function app()
{
  var a = [1, 2, 3, 5, 8];
  var b = [];
  for (let i = 0; i < a.length; i++){
    b.push(function(){
      console.log(i);
    });
  }
  for (let i = 0; i < b.length; i++){
    b[i]();
  }
}
app();
// compile
(function() {
    for (var a = [1, 2, 3, 5, 8], c = [], b = {
        a: 0
    }; b.a < a.length; b = {
        a: b.a
    },
    b.a++)
        c.push(function(d) {
            return function() {
                console.log(d.a)
            }
        }(b));
    for (a = 0; a < c.length; a++)
        c[a]()
}
)();

forEachを使うと安全。

goog.require('goog.array');

function app()
{
  var a = [1, 2, 3, 5, 8];
  var b = [];
  goog.array.forEach(a, function(a, i){
    b.push(function(){
      console.log(i);
    });
  });
  for (var j = 0; j < b.length; j++){
    b[j]();
  }
}
app();
// compile
var f = Array.prototype.forEach ? function(a, b) {
    Array.prototype.forEach.call(a, b, void 0)
}
: function(a, b) {
    for (var e = a.length, d = "string" == typeof a ? a.split("") : a, c = 0; c < e; c++)
        c in d && b.call(void 0, d[c], c, a)
}
;
(function() {
    var a = [];
    f([1, 2, 3, 5, 8], function(e, d) {
        a.push(function() {
            console.log(d)
        })
    });
    for (var b = 0; b < a.length; b++)
        a[b]()
}
)();

ただし素のJSで使おうとすると危険。

function app()
{
  var a = [1, 2, 3, 5, 8];
  var b = [];
  a.forEach(function(a, i){
    b.push(function(){
      console.log(i);
    });
  });
  for (let i = 0; i < b.length; i++){
    b[i]();
  }
}
app();
// compile
(function() {
    var a = [];
    [1, 2, 3, 5, 8].forEach(function(d, c) {
        a.push(function() {
            console.log(c)
        })
    });
    for (var b = 0; b < a.length; b++)
        a[b]()
}
)();

[].forEach を直接書くと、それはトランスパイル後もそのままになっている。

a.forEach(function(a, i){

の箇所を

a.myforEach(function(a, i){

にするとコンパイルエラーになるから判別は出来ているんだけど。
直接arrayの関数を呼び出す場合はブラウザ対応を開発者に任せるということかな。




クラスやthisが入ると長くなるのかなと思いきや、そうでもない。

const PREFIX = 'prefix_';
const SUFFIX = '_suffix';

const plusPrefix = (v) => PREFIX + v;
const plusSuffix = function(v){
  return v + SUFFIX;
};

class Foo
{
  constructor(val){
    this.val_ = val;
  }

  setValue(val){
    this.val_ = val;
  }
  getValue(){
    const v1 = plusPrefix(this.val_);
    return plusSuffix(v1);
  }

}

const foo = new Foo('val');
console.log(foo.getValue());
// compile
console.log("prefix_val_suffix");

あれ?クラスは?
驚愕の圧縮率。



何度も使うと結果が変わる。

const PREFIX = 'prefix_';
const SUFFIX = '_suffix';

const plusPrefix = (v) => PREFIX + v;
const plusSuffix = function(v){
  return v + SUFFIX;
};

class Foo
{
  constructor(val){
    this.val_ = val;
  }

  setValue(val){
    this.val_ = val;
  }
  getValue(){
    const v1 = plusPrefix(this.val_);
    return plusSuffix(v1);
  }

}

const foo = new Foo('val');
console.log(foo.getValue());

const foo2 = new Foo('val 2');
console.log(foo2.getValue());

const foo3 = new Foo('val 3');
console.log(foo3.getValue());
// compile
function a(b) {
    this.a = b
}
console.log("prefix_" + (new a("val")).a + "_suffix");
console.log("prefix_" + (new a("val 2")).a + "_suffix");
console.log("prefix_" + (new a("val 3")).a + "_suffix");

それでも動的な部分以外は定数になっている。


setValueを呼び出しても結果が変わる。

const PREFIX = 'prefix_';
const SUFFIX = '_suffix';

const plusPrefix = (v) => PREFIX + v;
const plusSuffix = function(v){
  return v + SUFFIX;
};

class Foo
{
  constructor(val){
    this.val_ = val;
  }

  setValue(val){
    this.val_ = val;
  }
  getValue(){
    const v1 = plusPrefix(this.val_);
    return plusSuffix(v1);
  }

}

const foo = new Foo('val');
foo.setValue('val2');
console.log(foo.getValue());
// compile
var a = new function() {
    this.a = "val"
}
;
a.a = "val2";
console.log("prefix_" + a.a + "_suffix");

なんかこのコンパイル結果を再びコンパイルすると綺麗に消せそうだけれども。
一度しか利用されていない変数やメソッドはインライン展開されるということかな。



クラス定数はどうか。

const PREFIX = 'prefix_';
const SUFFIX = '_suffix';

const plusPrefix = (v) => PREFIX + v;
const plusSuffix = function(v){
  return v + SUFFIX;
};

class Foo
{
  constructor(val){
    this.val_ = val;
  }

  setValue(val){
    this.val_ = val;
  }
  getValue(){
    const v1 = plusPrefix(this.val_);
    const v2 = plusSuffix(v1);
    const v3 = this.plusPrefix(v2);
    return Foo.plusSuffix(v3);
  }

  plusPrefix(v) { return this.CLASS_PREFIX + v; }
  get CLASS_PREFIX() { return 'classprefix_'; }

  static plusSuffix(v) { return v + Foo.CLASS_SUFFIX; };
  static get CLASS_SUFFIX() { return '_classsuffix'; }

}

const foo = new Foo('val');
console.log(foo.getValue());
// compile
var a = "undefined" != typeof window && window === this ? this : "undefined" != typeof global && null != global ? global : this;
function b(d) {
    this.b = d
}
a.Object.defineProperties(b.prototype, {
    a: {
        configurable: !0,
        enumerable: !0,
        get: function() {
            return "classprefix_"
        }
    }
});
a.Object.defineProperties(b, {
    a: {
        configurable: !0,
        enumerable: !0,
        get: function() {
            return "_classsuffix"
        }
    }
});
var c = new b("val");
console.log(c.a + ("prefix_" + c.b + "_suffix") + b.a);

なんか増えた。
素直にクラス外で定数にした方が良さそう。



効率良いトランスパイル結果を得ようと思ったら、 goog.module でモジュールにしつつthisを使わないシンプルな関数や定数の組み合わせでコードを書くのが良さそうだ。
まあそもそもthisを使うためにnewするのだから、thisを使わないのにクラスメソッドにするのは無駄な設計とも言えるね。