Hatena::ブログ(Diary)

vivid memo このページをアンテナに追加 RSSフィード

vivid code というサイトのメモ代わりに記事を書いていました。
現在ははてなブログに移行し、「ひだまりソケットは壊れない」 というブログで記事を書いています。 はてな id も id:nobuoka に変更しました。

2011-05-14

new を不当に貶める陰謀と JavaScript におけるクラスの継承構造の話

私は陰謀論者じゃないですし JavaScript の new 演算子が大好きなわけでも大嫌いなわけでもないです。 念のため。 本記事は Hiraku さんが書かれた下記記事への言及です。

new 演算子は使うな!?

newを封印するべき4つの理由」 でも new がいかに糞であるかが書かれていますし、その記事からも言及があるように Crockford さんが書かれた書籍 『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』 でも new 演算子は Bad Parts に分類されています。

new 演算子が忌避される理由はいろいろあるみたいですが、Hiraku さんの記事では

  • new を書き忘れてもコンストラクタ関数が実行されて意図しない動作になる
  • 継承を考慮する必要がある
  • 何をやっているのかわかりにくい
  • 読みづらい

という点が指摘されています。

常に new 演算子を使うことが最良の選択だとは思っていませんが、ことさら new 演算子を貶める言説も好きじゃないのでちょっと反論しておこうかな、というのがこの記事の主題です。

各継承方法のオブジェクトの関係図

はじめに、議論の土台として、純粋なプロトタイプ継承と、クラスの継承構造を持ち込んだ場合の継承方法について、オブジェクトの関係図を示します。

純粋なプロトタイプ継承

まずは JavaScript の純粋なプロトタイプ継承の形を見てみましょう。

ECMA-262 (5th edition) を満たしている JavaScript 処理系なら、Object.create メソッドで簡単にプロトタイプ継承が可能です。 Hiraku さんの記事で定義されている object 関数は、この Object.create メソッドと同じようなものです。

var baseObj = {
  name: "ベースオブジェクト", 
  printName: function(){ print( this.name ) }
};
// Object.create メソッドでプロトタイプ継承する
var subObj = Object.create( baseObj, { name: { value: "サブオブジェクト" } } );
// 使ってみる
subObj.printName() // サブオブジェクト

このような純粋なプロトタイプ継承による継承構造を図で表すと次のようになります。 非常に単純ですね。

f:id:vividcode:20110514151746p:image:w200

new 演算子とクラスの継承構造

次に、プロトタイプ継承と new 演算子によるオブジェクト生成の機構を利用して、クラスの継承構造を JavaScript で実現してみましょう。 詳しい実現方法は 「JavaScript におけるクラスの作成と継承」 の記事で述べましたので、ここでは詳細には立ち入りません。 基本的な思想としては、インスタンス (に相当するオブジェクト) で全てのインスタンス変数を保持し、プロトタイプ継承の先祖のオブジェクトでインスタンスメソッドを定義する、という形式です。 コンストラクタ関数オブジェクトがクラスそのものを表しています。

f:id:vividcode:20110514151745p:image:w440

少しややこしいですが、インスタンス生成には new 演算子、クラスの継承構造の生成には関数定義、という風に明確に分離しています。 また、クラスを表現するオブジェクト (コンストラクタ) とインスタンスメソッドを定義するオブジェクトが分かれているため、コンストラクタのプロパティによってクラスメソッドやクラス変数を実現できます。

Hiraku さんの記事におけるクラスの継承構造

最後に、Hiraku さんの記事に書かれている方法でクラスの継承構造を実現した場合のオブジェクトの関係を図に示します。

f:id:vividcode:20110514151747p:image:w310

JavaScript における純粋なプロトタイプ継承の継承構造に、無理やりクラスとインスタンスという概念を持ち込んでいるように私は思います。 以下のような問題点があるように思います。

  • クラスとインスタンスの区別が明確でない : クラスの継承もインスタンスの生成も完全に同じ機構で実現している。 同じプロトタイプ継承によって実現しているとしても、クラスとインスタンスという概念を持ち込むのであれば、区別するようにしたほうが良い
  • クラスメソッドとインスタンスメソッドのを区別が明確でない : クラスを表すオブジェクトとインスタンスメソッドを定義するオブジェクトが同一なので、クラスメソッドとインスタンスメソッドを区別できない
  • 明示的なコンストラクタ呼び出しが必要 : new 演算子によるオブジェクト生成の場合は暗黙的にコンストラクタ関数が呼び出されましたが、この方法では明示的にコンストラクタの呼び出しが必要です

4 つの new 批判に対する反論

それでは 「newを封印するべき4つの理由」 に対して反論してみます。

new を書き忘れてもコンストラクタが実行されてしまう問題
var Person = function Person( name ) {
    this.name = name;
};
Person.prototypte.sayHello = function sayHello() {
    print( "Hello, I'm " + this.name + "." );
};

// new を忘れて実行
var tarou = Person( tarou );

確かに、new を書き忘れてもコンストラクタ関数が実行されてしまいます。 それは認めましょう。 ですが、これの何が問題なんでしょうか? new を書き忘れることってありますか? 書き忘れたとして、それが発見困難なバグに繋がりますか?

私の感覚では、new の書き忘れは引数の個数不足での関数呼び出しと同じレベルの問題であると思います。 引数の個数や型チェックが必要なら関数内でチェックをするように、チェックする必要があるならコンストラクタ関数内で this が何を指しているのかチェックすればいいだけです。

new の書き忘れで時間を浪費するようなプログラマが居るならば、その人はプログラマに向いていないので転職を勧めてあげましょう。

継承を考慮する必要があるという問題

『この書き方は、object関数をインライン展開したのと同義です…。それならいっそのこと、常にobject関数を使うようにした方が楽だと思いませんか?』 とありますが、クラスの継承を行う関数を定義すればいいだけのことです。 例えば 「JavaScript におけるクラスの作成と継承」 の記事では、クラスの継承のための extend という関数を定義しました。

何をやっているのかわかりにくいという問題

『JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。』 って、new メソッドはプロトタイプ継承を応用してクラスの継承構造を JavaScript に持ち込むためのものなので当然でしょう。 クラスの継承構造を持ち込むのであれば、インスタンス化に new 演算子を使うことは他の言語との類似性から言ってもわかりやすい と思います。 もちろんクラスの継承構造を使わないのに new 演算子を使うことは混乱の元になると思いますが。

読みづらいという問題

個人的にはオブジェクトリテラルの中に関数式を書くのは好きじゃないのでどっちかというと

var Animal = {
  name: "動物"
, breathe: function(){alert("すーはー")}
, sayName: function(){alert(this.name)}
};

よりも

var Animal = function Animal( name ) {
    this.name = name;
};
Animal.prototype.breath = function breath() {
    print( "すーはー" );
};
Animal.prototypte.sayName = function sayName() {
    print( this.name );
};

の書き方が好きなんですが。 まあでも多くの人は前者の書き方の方が好みなんだろうなー、とは思います。

ていうかこの name ってなんなんでしょう。。 クラス変数?

そもそも new 演算子のどの部分を批難しているのか?

というわけで 4 つの問題点に対する反論 (になってるかどうかは微妙ですが) を書いてきましたが、そもそも new を封印する目的がいまいちよくわかりません。

  • 本当に、ただ純粋に new 演算子を使わないようにすべき、という主張?
  • JavaScript にクラスの継承構造を持ち込むな、という主張?

本当に、ただ純粋に new 演算子を使わないようにすべき、という主張は JavaScript に限らず Java などでもあります。 new 演算子によるインスタンス化は柔軟性に欠ける、というものです。 インスタンス化のための API としてユーザーにはファクトリーメソッドを提供し、new 演算子は表から見えないようにするべきであるという主張であり、『Effective Java 第2版 (The Java Series)』 などに書かれています。

しかし、今回の場合は単純に new 演算子を表から隠すだけでなく継承構造も変えていますし、単純に new 演算子を表から隠せ、という主張ではないように思います。

それでは、JavaScript にクラスの継承構造を持ち込むな、という主張なのかというとそれも違います。 Hiraku さんの記事では 「クラス(に相当するオブジェクト)を作る」 という表現が出てきていますし、クラスの継承構造を実現したいように思えます。

クラスの継承構造が不要な場面ではわざわざクラスの継承構造なんか作らずに、JavaScript の基本的な継承であるプロトタイプ継承を使いましょう!」 という主張なら納得するのですが、Hiraku さんの記事では結局のところ何が目的なのかよくわからなくなっており、「俺俺オブジェクト指向な気がする」 と言われても仕方ないかなー、と思います。

new 演算子を使うとわかりやすい? わかりにくい? (追記)

ちょっと重要なことなので追記。

本記事の上のほうでも 「クラスの継承構造を持ち込むのであれば、インスタンス化に new 演算子を使うことは他の言語との類似性から言ってもわかりやすい」 と述べましたし、id:teramako さんも 「僕はnew演算子は好きです。書き忘れが発生してorzすることはあるけど。何故好きかというと、コードを読んでいてインスタンスを作成していることが明確に分かるから」 と仰っています。 一方で、Hiraku さんは 「JavaScript自体はプロトタイプベースで作られているくせに、newを使うとクラスベースみたいな書き方をする必要があります。中でやっていることと、外側から見えるインターフェースが違いすぎる」 という立場です。

もちろん、どちらが間違っている、ということはありません。 とはいえどちらかの立場をとらなければいけないのであれば、「誰にとってわかりやすくすべきか」 という視点を取り入れるべきでしょう。

クラスという概念を取り入れる以上、クラスの設計者とクラスのユーザーという 2 人の視点で考える必要があります。 これはしばしば同一人物でありますが、同一人物ではない可能性もあります。 そして、クラスというものは クラスのユーザーにとってわかりやすい ものであるべきだと思います。 クラスの設計・実装者は当然 JavaScript に詳しいはずですので、プロトタイプ継承がどうとか、そういう点は問題なく認識しているはずです。 一方、クラスの使用者はクラスを提供されているだけなので、「クラスに見えるけどプロトタイプ継承で実装されている」 という点は認識していないかもしれません。 また、そのように認識させる必要もありません。 なぜなら 「クラス」 を提供しているのですからクラスのように扱えればいいのです。

例えば、Person というクラスを提供するとしましょう。 このクラスは人物を表すもので、人物の名前を引数にとってインスタンス化するものとしましょう。 こう聞くと、クラスのユーザーはほぼ間違いなく以下のようなコードでインスタンス化できるものと期待するはずです。

var tarou = new Person( "田中 太郎" );

「クラスをプロトタイプ継承したオブジェクトを作って、自分で初期化関数を呼んでください」 などと言われるとどうでしょう?

var tarou = Object.create( Person );
tarou.init( "田中 太郎" );

という感じでしょうか。 お世辞にも良いインターフェイスだとは言えません。 ではオブジェクトの生成と初期化を行う関数を Person オブジェクトに追加してみますか?

// ファクトリーメソッド
Person.newInstance = function newInstance( name ) {
    Object.create( this );
    this.init( name );
};
// インスタンス化
var tarou = Person.newInstance( "田中 太郎" );

すっきりしました。 これなら良いでしょう。 しかしこうなるともはや従来の手法でいい気がします。 クラスの実装方法に違いはありますが、上でも言ったようにクラスの設計・実装者には十分な知識があるはずなのでどちらの実装方法でもあまり問題にはなりません。 それよりも、ユーザーが標準的な使用方法でクラスを使用できるかどうか、が重要なのです。

標準的な使用方法で使えるのかどうかという点において、インスタンス化についてよりももっと大きな問題があります。 instanceof 演算子を実直に使えないという問題です。 (そもそも instanceof 演算子は使うべきではない、という主張もありますが、それはさておき。) ユーザーは、Person クラスをインスタンス化した tarou は、以下の式で true になると期待するでしょう。

tarou instanceof Person; //=> true

しかし、Hiraku さんとこの方法では

tarou instanceof type( Person ); //=> true

などとしなければいけません。 大規模なフレームワークとしてクラス機構を提供するのであれば新しい書き方を導入するのもひとつの手ではありますが、単一、または少数個のクラスを提供する際にこのような書き方をクラスのユーザーに強要するのは無理というものです。 様々なクラスの提供者がそれぞれ独自に type 関数のような関数を提供したらどうでしょう? 使いにくいですよね。 これが 「俺俺オブジェクト指向」 と呼ばれてしまったひとつの理由でしょう。

プロトタイプ継承という機構を隠してしまったことの弊害

しかしながら、JavaScript が 「クラスの継承構造のような機構」 を提供し、プロトタイプ継承という機構を隠してしまったがための弊害もあります。 Hiraku さんが仰るように 「実際の挙動 (プロトタイプ継承) と見た目 (クラスの継承やインスタンス化) が異なっている」 というものです。

JavaScript で new Date() のようにインスタンス化を行ったことがある人はたくさんいると思いますが、プロトタイプ継承という継承方法を知っている人は思っている以上に少ないのではないか、と思います。 プロトタイプ継承という機構が裏に隠れてしまっているために、(誰かが作ったクラスを使うだけなら) プロトタイプ継承について勉強する必要が無いためです。 特に JavaScript はプログラマだけでなく、web デザイナーなど本職がプログラマじゃない人にも使われていますので、ちゃんと勉強してない人も多いはずです。

そのような 「クラスを使ったことはあるけどプロトタイプ継承はよく知らない」 というような人がクラスを設計・実装しようとするとよくわからなくなって混乱してしまいます *1。 そして、「JavaScript のクラスってよくわからない」 ということになってしまうのではないかと思います。

この点はどうしようもないかなーという気がするのですが、とりあえず言えることは 「他の言語と同じようにクラスを使えるからといってろくに勉強せずクラスの設計・実装をしようとすると痛い目を見るので、まず JavaScript の勉強をしましょう」 ということですね。

私の主張

最後に JavaScript におけるオブジェクトに関して私の主張をちょこっとしておきます。

new 演算子がしばしば攻撃の対象になるのは、他の言語との類推から JavaScript においてもなんでもかんでも new 演算子を使ってオブジェクト生成するような人がいるからだと思います。 他の言語、例えば Java などではオブジェクトを生成するために基本的 *2new を使います。 一方、JavaScript では new 演算子が必要となる場面はそれほど多くありません。

JavaScript は弱い動的型付け言語であり、使い捨てのオブジェクトをオブジェクトリテラルで生成することができます。 なんらかのオブジェクトを継承したい場合、クラスの継承などしなくてもプロトタイプ継承によりオブジェクトそのものを継承することができます。 特に web サイト上で動く JavaScript は使い捨てのオブジェクトを使うことが多く、クラスの継承構造が必要になることはあまり多くないでしょう。

一方で、クラスの継承構造を利用すると便利な場面もあります。 同じメソッドを持ち、異なるデータを保持する多くのオブジェクトを使いたい場面では、クラスの継承構造が役に立ちます。 そういうところでは、クラスの継承構造をつくり、new 演算子 *3 を使ってインスタンス化する、ということをすれば良いでしょう。

使いどころを考えて、使うべきところで使うべき方法をとるというのが重要です。

*1:Hiraku さんが仰っている 「よくわからない」 という指摘はこの点なんじゃないかと思います。

*2:ファクトリーメソッドなどもありますが。。

*3new じゃなくてファクトリーメソッドなどを使うべき、という議論もありますが、それはまた別のお話ということで。

HirakuHiraku 2011/05/14 23:36 非常にわかりやすい解説ありがとうございます。
確かに、攻撃的に書きすぎました…。安易にdisるのは良くないですね。

元記事で書きたかったのは、「プロトタイプ的継承さえあれば、オブジェクト指向でよく解説される機能ほとんど(それこそクラスとインスタンスさえ)を実現できる」ことをまとめておきたかった、というところです。あ、new関係なかったですね…
主張がもう少し明確になるよう、注釈を書いてみます。

vividcodevividcode 2011/05/15 00:06 Hiraku さん

コメントどうもです! こちらこそ読み返してみると攻撃的に書いてしまったきらいがあってすみません。 別に怒ってるわけではなくて、反論する糸口があんまり無かったのでちょっとムキになってしまって。。

> 「プロトタイプ的継承さえあれば、オブジェクト指向でよく解説される機能ほとんど(それこそクラスとインスタンスさえ)を実現できる」

そうですね、従来の方法でのクラス・インスタンスの実現もプロトタイプ継承を使っているだけ (クラス A を継承したクラスも、クラス A のインスタンスも、両方ともクラス A のコンストラクタの prototype プロパティの参照先オブジェクトをプロトタイプ継承したもの) なので、結局のところ重要なのはプロトタイプ継承なんですよね。

個人的には、本記事にも書いたように、クラスというものを JavaScript に導入するのであればクラスとインスタンスとを明確に分けて、インスタンス化の際には new 演算子 (またはファクトリーメソッド) を使うようにして、ユーザーが 「これはインスタンス化である」 ということを認識しやすいようにした方がわかりやすいのではないか、と考えています。 ただ、裏で何をやっているのか知らない JavaScript プログラマにとっては、Hiraku さんの仰るように 「実際にやっていること (プロトタイプ継承) と見た目 (クラスの継承やインスタンス化)」 が違うというのは混乱の元なのかもしれないなーとも思います。

そういう意味では確かにプロトタイプ継承を前面に出すことも重要ですね。 new 演算子が実際にどういう処理をするのかを知らない JavaScript プログラマは意外と多いと思うので、それを解説するのが重要なのかも、と思ったりもします。

botchbotch 2011/11/15 18:46 vividcodeさんに賛成です。
new を使わない流儀に疑問を感じてこのサイトにたどりつきました。
プログラマ暦35年少々の爺です。オブジェクト指向プログラミングは30年やってます。(経験年数自慢じゃなくて、素人ではないという意味で)
私はオブジェクトを作る工場抑止論者です。(廃止とまでは言わない)
継承やインスタンスを理解しないで、ちょっと目にした本に書いてあることを真の流儀と思い込んでいるプログラマが多いのにうんざりしています。

vividcodevividcode 2011/11/22 00:46 > botch さん

コメントありがとうございます。 返答が遅くなってしまいすみません。

『オブジェクトを作る工場抑止論者』 とのことですが、これは JavaScript に限らず、new 演算子を用いない方法でのオブジェクト生成 API (いわゆる Factory Method パターンなど) を良しとしない、ということでしょうか? もしそうなのでしたら、すみませんが本記事の趣旨とは少しずれてしまいます。

本記事の趣旨としては、「JavaScript の new 演算子を忌避して、変な継承構造を作るのは良くないのではないか」 という事を言っておりまして、new 演算子を使わずにオブジェクトを生成するメソッド等を批判しているわけではないのです。 new 演算子を用いないオブジェクト生成手法は (特に C++ や Java などの静的型付けの言語において)、オブジェクト指向プログラミングの利点である実装とインターフェイスの分離や多態性といった点を活用するために有用であると思っております。
(私の方が何か勘違いしてしまっているとしたら申し訳ありません。)

『継承やインスタンスを理解しないで、ちょっと目にした本に書いてあることを真の流儀と思い込んでいるプログラマが多いのにうんざり』
確かにそうですね。 イマイチな内容の書籍も多いですしね。。

kenken 2013/07/30 10:01 4 つの new 批判に対する反論が反論に足らないのに、Hiraku さんの主張の妥当性が毀損されている、と感じるのでコメント致します。


1.「 new を書き忘れることってありますか? 書き忘れたとして、それが発見困難なバグに繋がりますか?」グローバル変数を意図せず書き換える危険性に言及されており、これは発見困難なバグに繋がる。「チェックする必要があるならコンストラクタ関数内で this が何を指しているのかチェックすればいいだけです。new の書き忘れで時間を浪費するようなプログラマが居るならば、その人はプログラマに向いていないので転職を勧めてあげましょう。」言語仕様の欠陥について指摘されているのに、その不備を「XXすればいいだけ」「その不備に適応できないのならプログラマに向いていない」などと、言語仕様の問題を人のスキルに転嫁している。

2. 「クラスの継承を行う関数を定義すればいいだけのことです。 」これもその欠陥への余剰な対応を「すればいいだけ」とよくわからない正当化。

3.「もちろんクラスの継承構造を使わないのに new 演算子を使うことは混乱の元になると思いますが。」その通りです。混乱の元になっています。

あとその他諸々ありますが、総論として、言語仕様の欠陥の話に、「言語の機構を深く理解せずに安易に使うプログラマが悪い」という論調は、そもそもの言語仕様の欠陥の指摘の理路のちゃぶ台返しに等しいと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証