質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

92.50%

解決済 javascript のクラスって継承をしないときでも使ったほうがいいの?

  • JavaScript

    5445questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • オブジェクト指向

    95questions

    オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

  • ECMAScript

    40questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • 関数型プログラミング

    9questions

    関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

2016/10/05 16:47 投稿

2016/10/05 18:24 編集

mit0223 score 772

  • 3

    回答

  • 評価

  • 610

    view

ご意見募集系の質問が良くないのはわかってますが、ご容赦ください。はい、具体的な課題が含まれていません。いえ、決して銀のバッジが欲しいから質問数を稼いでるわけではありません。あー、ベストアンサーはできるかぎり選ばせていただきたいと思います。

クラス vs クロージャの塊

Javascript で、継承がない場合は、クラスで定義しなくても、クロージャをメンバに持つオブジェクトを生成する関数を作ることでコンストラクタを作れますよね?ここでは、このパターンをクロージャの塊と呼ぶことにします。

例を示します。ファイルから JSON オブジェクトの配列を読み込んで要素を追加して書き戻すだけのプログラムです。ES6で書いてます。

クラスで書いた場合:

import fs from 'babel-fs';

export default class Users {
    constructor(usersJSONPath) {
      this.usersJSONPath = usersJSONPath;
  }
    load() {
        return fs.readFile(this.usersJSONPath).then(data => {
            try {
                this.userList = JSON.parse(data);
                return this;
            } catch (err) {
                return Promise.reject(new Error('JSON parse error on reading file '+ this.usersJSONPath + ':' + err));
            }
        }, err => {
            return Promise.reject(new Error('Fail to read users data:' + err));
        });
    }
    flush() {
        return fs.writeFile(this.usersJSONPath, JSON.stringify(this.userList, '', ' ')).then( data => {
            return this;
        }, err => {
            return Promise.reject(new Error('Fail to write users data :' + err));
        });
    }
    getAll() {
        return this.userList;
    }
    add(user) {
        var userIndex = this.userList.findIndex(one => one.userid===user.userid);
        if (userIndex >= 0) {
            return Promise.reject(new Error('User id = "' + user.userid + '" is already registered.'));
        }
        this.userList.push(user);
        return this;
    }
}

Promise.resolve(new Users("test.json"))
  .then(users => {return users.load();})
  .then(users => {return users.add({userid:"xyz", passwd:"zzz"});})
  .then(users => {return users.add({userid:"abc", passwd:"ccc"});})
  .then(users => {return users.flush();})
  .then(users => {console.log(JSON.stringify(users.getAll(),'','  '))})
  .catch(err => console.error(err.stack));

クロージャの塊で書いた場合:

import fs from 'babel-fs';

export default function buildUsers(usersJSONPath) {
    var userList = [];
    var handlers = {
        load() {
            return fs.readFile(usersJSONPath).then(data => {
                try {
                    userList = JSON.parse(data);
                    return handlers;
                } catch (err) {
                    return Promise.reject(new Error('JSON parse error on reading file '+ usersJSONPath + ':' + err));
                }
            }, err => {
                return Promise.reject(new Error('Fail to read users data:' + err));
            });
        },
        flush() {
            return fs.writeFile(usersJSONPath, JSON.stringify(userList, '', ' ')).then( data => {
                return handlers;
            }, err => {
                return Promise.reject(new Error('Fail to write users data :' + err));
            });
        },
        getAll() {
            return userList;
        },
        add(user) {
            var userIndex = userList.findIndex(one => one.userid===user.userid);
            if (userIndex >= 0) {
                return Promise.reject(new Error('User id = "' + user.userid + '" is already registered.'));
            }
            userList.push(user);
            return handlers;
        }
  }
    return handlers;
}

Promise.resolve(buildUsers("test.json"))
  .then(users => {return users.load()})
  .then(users => {return users.add({userid:"xyz", passwd:"zzz"});})
  .then(users => {return users.add({userid:"abc", passwd:"ccc"});})
    .then(users => {return users.flush()})
  .then(users => {console.log(JSON.stringify(users.getAll(),'','  '))})
  .catch(err => console.error(err.stack));

質問1. クロージャの塊のパターンには呼び名がありますでしょうか? あれば教えて下さい。

質問2. javascript のクラスって継承をしないときでも使ったほうがいいのでしょうか?
C→C++→Java からきた私には、クラスのほうが自然なので、何か概念モデルがわかりにくいなぁと思いながらも、クラスでコーディングしようとしていました。しかし、Javascript で書かれたいろいろなプログラムを見ていると、結構クロージャの塊パターンが多くて、見慣れてくると Javascript にとっては、そっちのほうが自然なように見えてきました。
上の例でいいますと、usersJSONPath, userList, handlers が自由変数になっており、クロージャ間で共有されています。これらはクラスの場合だとインスタンス変数になるところですが、new せずにインスタンスができるところが、ラムダ式っぽくてスマートなように見えます。this も prototype も new も不要です。
もちろん、継承がある場合はクロージャの塊では対応できないと思いますが、継承のないクラスであれば、クロージャの塊のほうが Javascript にとっては自然なような気がしてきて質問しています。


頂いた回答から派生した質問を追加させていただきます。

質問3. クロージャの塊パターンでは関数オブジェクトが大量にできてしまい、性能負担になる場合がありそうなことを教えていただきました。その点を考慮して、シングルトンにしか使わなければ、クロージャの塊パターンでも問題は無いと考えて良いでしょうか?

質問4. オブジェクトを new を使わずに生成するパターンでも this を使って自由変数を避けるができることを教えていただきました。自由変数、クロージャを避けたほうが良い理由というのはありますでしょうか?

気になる質問をクリップする

クリップした質問に回答があった場合に通知・メールを受け取ることができます。

クリップした質問はマイページの「クリップ」タブからいつでも見ることができます。

良い質問の評価を上げる

以下のような質問は評価を上げましょう。

  • 質問内容が明確
  • 自分も答えを知りたい
  • 質問者以外のユーザにも役立つ

評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

質問の評価を上げたことを取り消します

この機能は開放されていません

評価を下げる条件を満たしてません

評価を下げる理由を選択してください

上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

質問の評価を下げる機能の利用条件

この機能を利用するためには、以下の事項を行う必要があります。

質問の評価を下げたことを取り消します

回答(全3件)

回答の評価を上げる

以下のような回答は評価を上げましょう。

  • 正しい回答
  • わかりやすい回答
  • ためになる回答

評価が高い回答ほどページの上位に表示されます。

6

この機能は開放されていません

評価を下げる条件を満たしてません

 クラスパターン

クラスパターンは instanceof 演算子や Object.getPrototypeOf で継承関係を確認することが出来ます。
users instanceof Users を実行することで Users.prototype 配下のプロパティが存在することを保証できます。

class Users {
  constructor(/* ...name */) {
    this.names = Array.from(arguments).map(String);
  }

  add (name) {
    var names = this.names;
    name = String(name);

    return names.includes(name) ? names.length : names.push(name);
  }

  remove (name) {
    var names = this.names, i;
    name = String(name);
    i = names.indexOf(name);

    if (i !== -1) {
      names.splice(i, 1);
    }
  }
}

var users = new Users('Ken', 'Alice', 'John');
console.log(users instanceof Users);                            // true
console.log(Object.getPrototypeOf(users) === Users.prototype);  // true

他に prototype 上にプロパティを定義することでメモリを節約することが出来ますが、これはシングルトンパターンを複数生成する場合でも実装することが出来ます。

 シングルトンパターン

オブジェクト初期化子で一つのオブジェクトにプロパティ/メソッドをまとめる事をシングルトンパターンと呼びます。

var users = {
  add: function add (name) {
    var names = this.names;
    name = String(name);

    return names.includes(name) ? names.length : names.push(name);
  },
  remove: function remove (name) {
    var names = this.names, i;
    name = String(name);
    i = names.indexOf(name);

    if (i !== -1) {
      names.splice(i, 1);
    }
  }
};

ご質問のコードではクロージャを使ってインスタンスを生成しているかのような設計にしていますが、基本的にはこの形の派生だと思います。

function createUsers (/* ...name */) {
  return {
    names: Array.from(arguments).map(String),
    add: function add (name) {
      var names = this.names;
      name = String(name);

      return names.includes(name) ? names.length : names.push(name);
    },
    remove: function remove (name) {
      var names = this.names, i;
      name = String(name);
      i = names.indexOf(name);

      if (i !== -1) {
        names.splice(i, 1);
      }
    }
  };
}

var array = [createUsers('Ken', 'Alice', 'John'), createUsers('Ken', 'Alice', 'John'), createUsers('Ken', 'Alice', 'John')];  // 3つのオブジェクトを生成したので3倍メモリを消費する

ご質問のコードを含めて素直にこの実装をすると生成したオブジェクトの数だけ addremove 関数オブジェクトを生成するという問題があります。
この問題はコードを少し書き換えることで解消することが可能です。

var createUsers = (function () {
  function add (name) {
    var names = this.names;
    name = String(name);

    return names.includes(name) ? names.length : names.push(name);
  }

  function remove (name) {
    var names = this.names, i;
    name = String(name);
    i = names.indexOf(name);

    if (i !== -1) {
      names.splice(i, 1);
    }
  }

  return function createUsers (/* ...name */) {
    return {
      names: Array.from(arguments).map(String),
      add: add,
      remove: remove
    };
  }
}());

ただし、苦労に見合う対価がないのでここまで苦労するならクラスパターンで書いた方がスマートだと私は思います。

Re: mit0223 さん

2016/10/05 17:40 投稿

2016/10/05 17:42 編集

コメント(4)

2016/10/05 18:15

なるほど、シングルトンパターンですか。
その名の通り、シングルトンにしか使わなければ、関数オブジェクトの数の問題は無いと考えて良いでしょうか?
あと、this を使って自由変数を避けておられますが、自由変数を避けたほうが良い理由というのはありますでしょうか?
→質問の方を編集しておきます。

2016/10/05 19:05

> その名の通り、シングルトンにしか使わなければ、関数オブジェクトの数の問題は無いと考えて良いでしょうか?
問題ないと思います。

> あと、this を使って自由変数を避けておられますが、自由変数を避けたほうが良い理由というのはありますでしょうか?
this は実行コンテキストに入るときに決定する為、Funcion#call や Function#bind を使って this 値を変更出来るようになり、他のインスタンスメソッドに移植することも容易になります。
例えば、Array.prototype.forEach.call(document.querySelector('p>.hoge')) は比較的よく知られていますね。

2016/10/05 19:27

あー、「関数はオブジェクトだよー」、「メソッドもオブジェクトだよー」と言いながら、持って回るんですね。勉強になります。
ただ、クロージャがいいなと思ったのは this を使わなくてすむからというのもあるんです。Javascript の this って、 callback をインラインで書いてたりすると、ぱっとみて何を指してるのかわかりにくいですよね。Promise の then に渡す関数も受け渡すデータを this にしてくれるという手もあったと思うのですが、第一引数に渡されるのは、マニアックコーディングを避ける方向ではないでしょうか。
miyabi-san との議論にもありますが、Javascript のマニアックコーディングは避けられて、排除されていくのではないでしょうか。
クロージャを使わずに class で書く。引数を受け取って返却値で返す。他の言語から来た人に対してハードルをさげる大事なことかもしれません。と思い始めました。

2016/10/05 20:46 編集

> Javascript の this って、 callback をインラインで書いてたりすると、ぱっとみて何を指してるのかわかりにくいですよね。
他言語習得者がJavaScriptを学び始めた時に同様の感想はよく聞きます。
ただ、「分かりにくいから this を使わないようにしよう」は可能性の幅を狭めるだけで「JavaScript の this も使いこなそう」とするのが真の JavaScripter だと思っています。
先述の Array.prototype.forEach も含めて既存のビルトイン関数の多くは内部的に this を使っており、this を学ばずして「JavaScript を理解できた」とはいえないとも思います。
JavaScript は基本的に後方互換性を捨てない方向で進化を遂げてきたので「ある時、いきなり this を使わない言語になる事」はまずありません。
ES6 で this を束縛しないアロー関数が出てきましたが、アロー関数とて上位スコープの this を参照する仕組みがあります。

> Promise の then に渡す関数も受け渡すデータを this にしてくれるという手もあったと思うのですが
Promise はコールバック関数なので this 値に束縛する理由がなかったのだと思います。
コールバック関数の this 値は原則として束縛されません(addEventListener 等の例外はあります)。
また、this は Function#callやFunction#bind で書き換え可能という点に注意が必要です。

ベストアンサー

回答の評価を上げる

以下のような回答は評価を上げましょう。

  • 正しい回答
  • わかりやすい回答
  • ためになる回答

評価が高い回答ほどページの上位に表示されます。

4

この機能は開放されていません

評価を下げる条件を満たしてません

結論からいうと、ES6ならclassを使った方が良いでしょうね。

質問1

これがそのままクロージャーだと思ってます(あまり自信ない)
即時実行関数で変数を閉じ込めてプライベートなプロパティを宣言してますしね。

質問2

実質クラスとクロージャーは実行される内容は同じです。
(JavaScriptはクラスベースではなく、プロトタイプベースのオブジェクト指向言語なので)

さて、C言語等からの流れでリストをfor文で扱う事はバグの温床になっているので、
モダンな言語では「for文使うのやめい!foreachを使え!」という流れが広がっています。

クロージャーを使って書くやり方も人の数だけ実装があり、
同じくバグの温床になっているのでES6で書く場合は出来るだけClassで統一するべきでしょう。

ES5ではクラスが無いのが問題となりますが、
CoffeeScriptやTypeScriptなどのAltJSでは、Class構文が自動的にクロージャーに展開されるのでプログラマーがクロージャーを書く必要がなくなります。
表現も簡素でキレイになりますので、ES5なら出来るだけAltJSを使うべきでしょう。

下記はCoffeeScriptから派生して生まれたLiveScriptの簡単なスクリプトです。
公式サイトの右上のテキストエリアにソースコードを貼り付けて、
CompileボタンやRunボタンをクリックするとソースや結果が見れますので、ES5的な解決策の一つとして答えになるのではないかと思います。

class User
  ({@name, @age})->
  is_over_20: -> @age >= 20
UserSeeds =
  * name: \miyabi
    age: 17 # 永遠の的な意味で
  * name: \mit-san
    age: 21
  * name: \teratail
    age: 2
|> map -> new User it
|> filter (.is_over_20!)
# [User {name: \mit-san, age: 21}]

2016/10/05 18:11 投稿

コメント(2)

2016/10/05 18:48

なるほど、マニアックなコーディングを避ける意味があるのですね。「クロージャこそ Javascript の真髄だー!」とか言っていてはだめなんですね。
LiveScript は難しいですが、なんとなく雰囲気はわかりました。(そんなに若いわけないじゃないですか・・・)

2016/10/05 19:04

クロージャーかっこいいですよね。百戦錬磨の戦士のような渋さを感じます。
でも、実際問題ES6でClassが採用された背景には、誰もクロージャーで書ききれなかった(言い過ぎ)ことがあるのでしょうね。

いずれ三項演算子と共にコードゴルフ勢の嗜みになるんじゃないかなと思ってます。

回答の評価を上げる

以下のような回答は評価を上げましょう。

  • 正しい回答
  • わかりやすい回答
  • ためになる回答

評価が高い回答ほどページの上位に表示されます。

2

この機能は開放されていません

評価を下げる条件を満たしてません

大きな違いとして、class構文ではプロトタイプにメソッドをセットする、ということがあります。そのため、同じ種類のインスタンスを多数作っても、メソッドになる関数オブジェクトは1つで済みます。

オブジェクトに直接関数オブジェクトを割り当てていくと、同じようなオブジェクトを大量に作っても、そのたびごとに関数オブジェクトが生成されてしまう、ということになりかねません(優秀な処理系なら最適化してくれるかもしれませんが)。

2016/10/05 17:06 投稿

コメント(1)

2016/10/05 17:16

なるほど!
クロージャを大量に生成すると、メモリとJITコンパイラに負担がありそうですね。考えがいたってませんでした。ありがとうございます。

15分調べてもわからないことは、teratailで質問しよう!

ただいまの回答率

92.50%

関連した質問

同じタグがついた質問を見る

  • JavaScript

    5445questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

  • オブジェクト指向

    95questions

    オブジェクト指向プログラミング(Object-oriented programming;OOP)は「オブジェクト」を使用するプログラミングの概念です。オブジェクト指向プログラムは、カプセル化(情報隠蔽)とポリモーフィズム(多態性)で構成されています。

  • ECMAScript

    40questions

    ECMAScriptとは、JavaScript類の標準を定めるために作られたスクリプト言語です。

  • 関数型プログラミング

    9questions

    関数型プログラミングとは、関数を用いて演算子を構築し、算出し、コンピュータプログラムを構成する枠組みです。

閲覧数の多いJavaScriptの質問

関連した質問

teratail × エキスパート

メールアドレスで登録

利用規約、及び 個人情報の取り扱いに関する要項をご確認のうえ同意いただける場合は「同意して登録」ボタンをクリックしてください。

SNSアカウントで登録

  • Facebook
  • Twitter
  • Google
  • Github
  • Hatena

思考するエンジニアのためのQ&Aサイト「teratail」について詳しく知る