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

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

新規登録して質問してみよう
ただいま回答率
85.48%
ECMAScript

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

オブジェクト指向

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

JavaScript

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

関数型プログラミング

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

Q&A

解決済

3回答

7505閲覧

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

mit0223

総合スコア3401

ECMAScript

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

オブジェクト指向

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

JavaScript

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

関数型プログラミング

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

5グッド

14クリップ

投稿2016/10/05 07:47

編集2016/10/05 09:24

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

###クラス vs クロージャの塊

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

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

クラスで書いた場合:

javascript

1import fs from 'babel-fs'; 2 3export default class Users { 4 constructor(usersJSONPath) { 5 this.usersJSONPath = usersJSONPath; 6 } 7 load() { 8 return fs.readFile(this.usersJSONPath).then(data => { 9 try { 10 this.userList = JSON.parse(data); 11 return this; 12 } catch (err) { 13 return Promise.reject(new Error('JSON parse error on reading file '+ this.usersJSONPath + ':' + err)); 14 } 15 }, err => { 16 return Promise.reject(new Error('Fail to read users data:' + err)); 17 }); 18 } 19 flush() { 20 return fs.writeFile(this.usersJSONPath, JSON.stringify(this.userList, '', ' ')).then( data => { 21 return this; 22 }, err => { 23 return Promise.reject(new Error('Fail to write users data :' + err)); 24 }); 25 } 26 getAll() { 27 return this.userList; 28 } 29 add(user) { 30 var userIndex = this.userList.findIndex(one => one.userid===user.userid); 31 if (userIndex >= 0) { 32 return Promise.reject(new Error('User id = "' + user.userid + '" is already registered.')); 33 } 34 this.userList.push(user); 35 return this; 36 } 37} 38 39Promise.resolve(new Users("test.json")) 40 .then(users => {return users.load();}) 41 .then(users => {return users.add({userid:"xyz", passwd:"zzz"});}) 42 .then(users => {return users.add({userid:"abc", passwd:"ccc"});}) 43 .then(users => {return users.flush();}) 44 .then(users => {console.log(JSON.stringify(users.getAll(),'',' '))}) 45 .catch(err => console.error(err.stack));

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

javascript

1import fs from 'babel-fs'; 2 3export default function buildUsers(usersJSONPath) { 4 var userList = []; 5 var handlers = { 6 load() { 7 return fs.readFile(usersJSONPath).then(data => { 8 try { 9 userList = JSON.parse(data); 10 return handlers; 11 } catch (err) { 12 return Promise.reject(new Error('JSON parse error on reading file '+ usersJSONPath + ':' + err)); 13 } 14 }, err => { 15 return Promise.reject(new Error('Fail to read users data:' + err)); 16 }); 17 }, 18 flush() { 19 return fs.writeFile(usersJSONPath, JSON.stringify(userList, '', ' ')).then( data => { 20 return handlers; 21 }, err => { 22 return Promise.reject(new Error('Fail to write users data :' + err)); 23 }); 24 }, 25 getAll() { 26 return userList; 27 }, 28 add(user) { 29 var userIndex = userList.findIndex(one => one.userid===user.userid); 30 if (userIndex >= 0) { 31 return Promise.reject(new Error('User id = "' + user.userid + '" is already registered.')); 32 } 33 userList.push(user); 34 return handlers; 35 } 36 } 37 return handlers; 38} 39 40Promise.resolve(buildUsers("test.json")) 41 .then(users => {return users.load()}) 42 .then(users => {return users.add({userid:"xyz", passwd:"zzz"});}) 43 .then(users => {return users.add({userid:"abc", passwd:"ccc"});}) 44 .then(users => {return users.flush()}) 45 .then(users => {console.log(JSON.stringify(users.getAll(),'',' '))}) 46 .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 を使って自由変数を避けるができることを教えていただきました。自由変数、クロージャを避けたほうが良い理由というのはありますでしょうか?

mpyw, act823, LLman, BelkaStrelka, manzyun👍を押しています

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

クリップした質問は、後からいつでもMYページで確認できます。

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

クラスパターン

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

JavaScript

1class Users { 2 constructor(/* ...name */) { 3 this.names = Array.from(arguments).map(String); 4 } 5 6 add (name) { 7 var names = this.names; 8 name = String(name); 9 10 return names.includes(name) ? names.length : names.push(name); 11 } 12 13 remove (name) { 14 var names = this.names, i; 15 name = String(name); 16 i = names.indexOf(name); 17 18 if (i !== -1) { 19 names.splice(i, 1); 20 } 21 } 22} 23 24var users = new Users('Ken', 'Alice', 'John'); 25console.log(users instanceof Users); // true 26console.log(Object.getPrototypeOf(users) === Users.prototype); // true

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

シングルトンパターン

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

JavaScript

1var users = { 2 add: function add (name) { 3 var names = this.names; 4 name = String(name); 5 6 return names.includes(name) ? names.length : names.push(name); 7 }, 8 remove: function remove (name) { 9 var names = this.names, i; 10 name = String(name); 11 i = names.indexOf(name); 12 13 if (i !== -1) { 14 names.splice(i, 1); 15 } 16 } 17};

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

JavaScript

1function createUsers (/* ...name */) { 2 return { 3 names: Array.from(arguments).map(String), 4 add: function add (name) { 5 var names = this.names; 6 name = String(name); 7 8 return names.includes(name) ? names.length : names.push(name); 9 }, 10 remove: function remove (name) { 11 var names = this.names, i; 12 name = String(name); 13 i = names.indexOf(name); 14 15 if (i !== -1) { 16 names.splice(i, 1); 17 } 18 } 19 }; 20} 21 22var array = [createUsers('Ken', 'Alice', 'John'), createUsers('Ken', 'Alice', 'John'), createUsers('Ken', 'Alice', 'John')]; // 3つのオブジェクトを生成したので3倍メモリを消費する

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

JavaScript

1var createUsers = (function () { 2 function add (name) { 3 var names = this.names; 4 name = String(name); 5 6 return names.includes(name) ? names.length : names.push(name); 7 } 8 9 function remove (name) { 10 var names = this.names, i; 11 name = String(name); 12 i = names.indexOf(name); 13 14 if (i !== -1) { 15 names.splice(i, 1); 16 } 17 } 18 19 return function createUsers (/* ...name */) { 20 return { 21 names: Array.from(arguments).map(String), 22 add: add, 23 remove: remove 24 }; 25 } 26}());

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

Re: mit0223 さん

投稿2016/10/05 08:40

編集2016/10/05 08:42
think49

総合スコア18162

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mit0223

2016/10/05 09:15

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

2016/10/05 10:05

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

2016/10/05 10:27

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

2016/10/05 11: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 で書き換え可能という点に注意が必要です。
guest

0

ベストアンサー

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

質問1

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

質問2

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

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

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

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

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

LiveScript

1class User 2 ({@name, @age})-> 3 is_over_20: -> @age >= 20 4UserSeeds = 5 * name: \miyabi 6 age: 17 # 永遠の的な意味で 7 * name: \mit-san 8 age: 21 9 * name: \teratail 10 age: 2 11|> map -> new User it 12|> filter (.is_over_20!) 13# [User {name: \mit-san, age: 21}]

投稿2016/10/05 09:11

miyabi-sun

総合スコア21158

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mit0223

2016/10/05 09:48

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

2016/10/05 10:04

クロージャーかっこいいですよね。百戦錬磨の戦士のような渋さを感じます。 でも、実際問題ES6でClassが採用された背景には、誰もクロージャーで書ききれなかった(言い過ぎ)ことがあるのでしょうね。 いずれ三項演算子と共にコードゴルフ勢の嗜みになるんじゃないかなと思ってます。
guest

0

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

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

投稿2016/10/05 08:06

maisumakun

総合スコア145183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mit0223

2016/10/05 08:16

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問