Melchior

バンクーバーあたりで働くエンジニアの備忘録

classの継承についての考察

概要

ES6の継承が遅すぎると話題なので検証・最適化をしました。

どれくらい遅いのか

f:id:suguru03:20170428145927p:plain

こちらベンチマークによると、少しずつ改善は見られるものの、2.3-17倍程度遅い様子です。

実際に検証しました。

  • Node v6.10.1
  • benchmark.js v2.1.4

継承

// classes
function ParentES5(value) {
  this._value = value;
}
function ES5(value) {
  ParentES5.call(this, value);
}
util.inherits(ES5, ParentES5);

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
}
class ES6 extends ParentES6 {
  constructor(value) {
    super(value);
  }
}

// functions
const funcs = {
  'es5': function() {
    return new ES5(1);
  },
  'es6': function() {
    return new ES6(1);
  }
};
**** benchmark.js ****
[1] "es5" 0.000029ms [1.00]
[2] "es6" 0.000045ms [1.56]

単純にclassを使うだけでも1.5倍遅いようです。

super classメソッドの呼び出し

function ParentES5(value) {
  this._value = value;
}
ParentES5.prototype.get = function() {
  return this._value;
};
function ES5(value) {
  ParentES5.call(this, value);
}
util.inherits(ES5, ParentES5);

ES5.prototype.get = function() {
  return ParentES5.prototype.get.call(this) * 2;
};

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
  get() {
    return this._value;
  }
}
class ES6 extends ParentES6 {
  constructor(value) {
    super(value);
  }
  get() {
    return super.get() * 2;
  }
}
const es5 = new ES5(1);
const es6 = new ES6(1);

// functions
const funcs = {
  'es5': function() {
    return es5.get();
  },
  'es6': function() {
    return es6.get();
  }
}
**** benchmark.js ****
[1] "es5" 0.000021ms [1.00]
[2] "es6" 0.000078ms [3.79]

super classメソッドの呼び出しは非常に遅いので避けたいところです。

考察

super classメソッドの呼び出し

function ParentES5(value) {
  this._value = value;
}
ParentES5.prototype.get = function() {
  return this._value;
};
function ES5(value) {
  ParentES5.call(this, value);
}
util.inherits(ES5, ParentES5);

ES5.prototype.get = function() {
  return ParentES5.prototype.get.call(this) * 2;
};

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
  get() {
    return this._value;
  }
}
class ES6 extends ParentES6 {
  constructor(value) {
    super(value);
  }
  get() {
    return ParentES6.prototype.get.call(this) * 2;
  }
}
const es5 = new ES5(1);
const es6 = new ES6(1);

// functions
const funcs = {
  'es5': function() {
    return es5.get();
  },
  'es6': function() {
    return es6.get();
  }
}
**** benchmark.js ****
[1] "es6" 0.000020ms [1.00]
[2] "es5" 0.000020ms [1.02]

super.get()の代わりにsuper classメソッドをcallすることでes5と同等のパフォーマンスが得られました。これは使えそうです。

こちらのパターンも同じような結果が得られます。

function ParentES5(value) {
  this._value = value;
}
function getES5() {
  return this._value;
};
function ES5(value) {
  ParentES5.call(this, value);
}
util.inherits(ES5, ParentES5);

ES5.prototype.get = function() {
  return getES5.call(this) * 2;
};

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
}
function getES6() {
  return this._value;
};
class ES6 extends ParentES6 {
  constructor(value) {
    super(value);
  }
  get() {
    return getES6.call(this) * 2;
  }
}
const es5 = new ES5(1);
const es6 = new ES6(1);

// functions
const funcs = {
  'es5': function() {
    return es5.get();
  },
  'es6': function() {
    return es6.get();
  }
}
**** benchmark.js ****
[1] "es5" 0.000023ms [1.00]
[2] "es6" 0.000023ms [1.00]

こちらもなかなか使いどころがありそうです。

継承

ParentES6.prototype.constructorへのアクセスは禁止されているため、classをextendsしsuperを呼び出す必要があります。

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
}

class ES6 extends ParentES6 {
  constructor(value) {
    super(value);
  }
}
// エラー
class ES6 {
  constructor(value) {
    ParentES6.prototype.constructor.call(this, value); 
  }
}
// TypeError: Class constructor ParentES6 cannot be invoked without 'new'

今のところたどり着いた最速の継承はこちらです。

function ParentES5(value) {
  this._value = value;
}
function ES5(value) {
  ParentES5.call(this, value);
}
util.inherits(ES5, ParentES5);

class ParentES6 {
  constructor(value) {
    this._value = value;
  }
  get() {
    return this._value;
  }
}
function ES6(value) {
  this._value = value;
}
ES6.prototype = ParentES6.prototype;

const es5 = new ES5(1);
const es6 = new ES6(1);
console.log(es6.get()); // 1
console.log(es6 instanceof ParentES6); // true

// functions
const funcs = {
  'es5': function() {
    return es5.get();
  },
  'es6': function() {
    return es6.get();
  }
}
[1] "es6" 0.000020ms [1.00]
[2] "es5" 0.000026ms [1.33]

es5の継承よりも高速な継承が可能となりました。もう少し検証が必要ですが、instanceofがtrueとみなされるのでこちらは意外と使えるかもしれません。 こちらの案はthamminさんがベースを提供してくれました。

まとめ

superクラスの呼び出しを避ける考察でした。ns,μsレベルのチューニングなので普通にsuperを呼んでいただければと思います。

JavaScriptっておもしろい。

リンク