JavaScript 2.0
根拠
その他
previousupnext

06/04/2003

この節では草案作成中に考えられていたその他の代替案を幾つか示す。

Object

型階層の根底は JavaScript 1.5 にあった Object 型が採られた。数値、文字列、論理値の各単純値 (プリミティブ値) は Object の派生クラスのインスタンスとなり、以前からあったラッパクラスは削除された。これは JavaScript 1.5 における単純値とオブジェクトの混同を無くすためである。

他に考えられていたものとしては新しい根底型に any を定義するものがあった。この型は単純な数値、文字列、論理値と同様 Object の祖先で、それぞれ自身の型 numberstringboolean を持つことができた。この筋書きの下では JavaScript 1.5 のクラス NumberStringBooleanObject の派生クラスのままだが、その単純値はそれらのクラスのメンバではなくなる — 例えば falseboolean のメンバだが Boolean のメンバではない。false をボクシングして得られたオブジェクトは Boolean クラスのメンバだが boolean のメンバではなく、if 文では真と (!) 評価されてしまう。

any 案は JavaScript 1.5 と互換性の高いものだが、新しく持ち込まれる複雑さと混乱は必要以上の互換性に見合ったものではない。代わりに JavaScript 2.0 はこの部分において JavaScript 1.5 の挙動よりも簡単で整理されたものになっている。

NeverVoid

Never 型と Void 型は異なる目的で使う。(関数の) 戻り値の型として使う場合、Never は関数が処理を返さないことを示し、Void は関数は値を返すが返される値は無効である (常に undefined) であることを示す。

以下の例は NeverVoid の使用法を示すものである:

// この関数は無効な値を返す。
// This function returns no useful value.
function display(message:String):Void {
  document.write("<\_P>" + message + "<\/P>\n");
}

// この関数は処理を返さない。
// This function cannot return.
function abort(message:String):Never {
  display(message);
  throw AbortException;
}

function chickenCount(myChickens:Array[Chicken]):Integer {
  if (notHatched(myChickens))
    abort("Can’t count the chickens yet");
  else
    return myChickens.length;
}

仮に関数 abort が戻り値の型を明示していなかったり Never 以外の型を指定している場合は、コンパイラは関数 chickenCount の内で警告を出すということに注意していただきたい。これは chickenCountInteger を返すように宣言されているにも関わらず、値を返さずに関数から抜けるコード経路 (if 文が偽になる場合) を含むからである。関数 abort の戻り値の型 NeverchickenCount 内にそのようなコード経路が存在しないということをコンパイラに知らせるものである。

コンパイラの警告の問題を解決するのに、その関数が処理を返さないことをコンパイラに検証させないためにということで、戻り値の型を Never にするアイデアは良いものだったかもしれない。

型付き配列

草案には元々他にも配列型が含まれていた (簡潔さのために除外された)。以下は言語の将来のバージョンで採用される可能性のあるものである:

値の集合
Array Array[Object] と同じ
Array[t] t の要素を格納する全ての (連続、非連続) 配列、及び null
List[t] t の要素を格納しする可変長連続配列、及び null
t[] t の要素を格納する固定長連続配列、及び null
ConstArray ConstArray[Object] と同じ
ConstArray[t] t の要素を格納する全ての定数配列、及び null

非連続配列は空洞を持つ。連続配列は必ず0から始まる添字を持つ連続した要素を持つ。可変長配列は length プロパティを設定することで長さが変化する。

AArray[t]List[t]t[]ConstArray 、及び ConstArray[t] のいずれかの型式である場合、以下の3つの操作で A 型の配列の作成、或いは変換が可能になる:

ConstArray 以外の配列型インスタンスが等しいのはそれらが同一のオブジェクトである場合だけである。ConstArray 配列型インスタンスが等しいのは要素が同じで要素の型 t も同じ場合である。

型式

他に以下の表のような型演算子が定義できた。st は型式である。

v の暗黙の強制型変換
t[] t の固定長配列及び null undefined null
const t t (配列型でなければならない) を読み取り専用の配列型にする 無し
- t 型が t である null を除くあらゆる値
~ t t のあらゆる値及び undefined undefined undefined。型 t の他の暗黙の強制型変換には既に定義がある
+t t のあらゆる値及び null null nullundefined null (undefinedt のメンバでない場合)。型 t の他の暗黙の強制型変換には既に定義がある
s + t s と型 t のいずれか、或いは両方の全ての値 vs+t であれば v を使う。それ以外の場合で v as s が定義されていれば v as s を使う。これら以外の場合で v as t が定義されていれば v as t を使う
s * t s であり、型 t でもある全ての値 v as s as t が定義されていて、且つ s*t のメンバであれば v as s as t を使う
s / t s であるが型 t ではない全ての値 v as s が定義されていて、且つ s/t のメンバであれば v as s を使う。

添字演算子 [] は配列型を作成するのに使用できる。Array とは異なり、これらの配列には空洞が無く、整数値でのみインデクシング可能、固定長、そして派生不能である — t[]クラス修飾属性 final を持つ。

新しい単項演算子として const が追加できた。この演算子は PostfixExpressionOrSuper x をオペランドにとり、xConstArray 配列以外のインスタンスであれば const xx のコピーを ConstArray[T] で返す。ここに T は、x の各要素が T のメンバであることを最もよく表す型である。x が配列型であれば const x は対応する ConstArray 型となる。

多重コンストラクタ

以前の JavaScript 2.0 草案では、1つのクラスが複数のコンストラクタを異なる名前で持ち、静的関数と同じ構文で呼び出すことができた。これらは言語の単純さを保つために除外された。現在では各クラスは最高でも1つしかコンストラクタを持てず、同じクラスから別のコンストラクタを呼び出す this(args) 形式も除外された。コンストラクタは以下で述べるように定義されていた。

コンストラクタはクラス C の新しいインスタンスを作成する関数で、constructor 属性を使って定義する。コンストラクタは普通、クラスと同じ名前で与えられるが、その場合は constructor 属性は省略可能であり、コンストラクタはいわゆるデフォルトコンストラクタとして new C で呼び出し可能になる。コンストラクタの名前がクラス名と異なる場合はそのクラスの static 関数として呼ぶ:

class C {
  var a:String;

  constructor function C(p:String) {this.a = "New "+p}      // この constructor 属性は省略可能
  constructor function make(p:String) {this.a = "Make "+p}
  static function obtain(p:String):C {return new C(p)}
}

var c:C = new C("one");
var d:C = C.C("two");
var e:C = C.make("three");
var f:C = C.obtain("four");

c.a;     // "New one" を返す
d.a;     // "New two" を返す
e.a;     // "Make three" を返す
f.a;     // "New four" を返す

コンストラクタ内では this によりそのクラスのインスタンス変数を参照することができる。 クラス C をクラス B の派生クラスとすると C のインスタンス作成中に B のコンストラクタが呼ばれるわけだが、その際 B のコンストラクタは部分的に構築されているインスタンスでクラス C の仮想メソッドを呼び出すことができるだろう。同様に B のコンストラクタは this をグローバル変数 v に格納することができ、他の関数は部分的に構築されているオブジェクト vC のメソッドを呼び出すことが可能である。クラス C とその祖先クラスのいずれも上記のどちらかの動作をするコンストラクタを持たない場合に限ってクラス C のメソッドは C の完全に初期化されたインスタンスで呼ばれるだけということが保証され得る。

コンストラクタは自動的に新しく作成されたオブジェクトを返すので return 文で値を返すべきではない。コンストラクタの戻り値の型も記述してはいけない。

constructor 関数は常に新しいインスタンスを返す。一方、static 関数はそのクラスの既存のインスタンスを返すことができる。クラス C と同名の static 関数を定義することは可能で、そのような関数はクラスの外側からはコンストラクタのように見える (new C として呼び出すことができる) が、そのクラスの既存のインスタンスを呼び出し側に与えることができる。しかしながら、派生クラスには C がそのクラスのコンストラクタでないことが分かってしまうだろう。これは派生クラスのコンストラクタから C の擬似コンストラクタを呼び出すことができないためである。

クラス C がデフォルトコンストラクタ C.C を定義しない場合は C.C は自動的に定義される。この自動的に定義されるコンストラクタは C の基底クラスのデフォルトコンストラクタ B.B と同じ引数をとる。C.CB.B を呼び出した後、C の新しいインスタンスメンバをそのデフォルト値で初期化する。

基底コンストラクタの呼び出し

クラス C の基底クラスを B とする。コンストラクタ C.nthissuper にアクセスしたり呼び出し側に処理を戻す前にコンストラクタ B.mC.m を呼び出さなければならない。この呼び出しは明示的なものでも暗黙のものでもよい。仮に C.n が実行パス上でコンストラクタ B.mC.m いずれの呼び出しも含んでいなければ、自動的に C.n の文の先頭に B.B の引数無しの呼び出し文が挿入される。コンストラクタ C.n が例外を投げて処理を抜ける場合は他のコンストラクタを呼び出す必要は無い。C.n は2回以上コンストラクタ呼び出しを行ってはならない。

コンストラクタ C.n は以下の文を使って他のコンストラクタを呼び出すことができる:

super(args) B のデフォルトコンストラクタ B.B を呼ぶ。
super.m(args) B のコンストラクタ B.m を呼ぶ。mB のコンストラクタの1つを名前付ける QualifiedIdentifier でなければならない。
this(args) C のデフォルトコンストラクタ C.C を呼ぶ。
this.m(args) C のコンストラクタ C.m を呼ぶ。mC のコンストラクタの1つを名前付ける QualifiedIdentifier でなければならない。

上記の文は部分式などでない完全な文でなければならない。上の4つの形式の内、最初のものは確実に SuperStatement となるが、残りの3つは ExpressionStatement としてパースされる。以下の規則により、これら3つの形式 (super.m(args)this(args)this.m(args)) が式、コンストラクタ呼び出しのいずれとして扱われるか決まる:

オブジェクト作成中にクラス階層レベルを飛び越えたりすることはできない — 例えば C の基底クラスが B で、B の基底クラスが A のときは、C のコンストラクタは直接 A のコンストラクタを呼び出すことはできない。

クラス拡張

クラス拡張はどこか他の場所で定義されたクラスにメソッドを追加する機能である。クラス拡張は JavaScript 2.0 草案に含まれていたが、言語の単純さを保つために除外された。クラス拡張は有用なものだが、クラス設計は著しく複雑になり、厄介な問題 — パッケージ P がまずパッケージ Q から基底クラス C をインポートし、次に C の派生クラス D を定義、最後に D のメソッドと同じ名前、同じ名前空間で C を拡張した場合どうするのか — が発生する。

クラス拡張は以下で述べる extend 属性を使って行う。

extend 属性

extend 属性は1つの引数 C をとり、定義をクラス C に新メンバとして追加する。この引数はクラスとして評価されるコンパイル時定数式でなければならない。これによりクラス C が既にパッケージ P で定義されていても、既存の C にメソッドを追加できるようになる。これには幾つか制限がある:

以下は新しく作成した名前空間 StringExtension を使ってシステムクラス String にメソッドを追加する例である:

namespace StringExtension;

StringExtension extend(String) function scramble():String {...}
StringExtension extend(String) function unscramble():String {...}

use namespace(StringExtension);

var x:String = "abc".scramble();

クラス拡張が評価されるとメソッド scrambleunscrambleuse namespace(StringExtension) のスコープ内のコード中にある全ての文字列で使用可能になる。名前空間 StringExtension で限定すると名前 scrambleunscramble は特別な意味を帯びるため、クラス拡張により他の無関係のパッケージとの間に名前の衝突が起こる可能性は無い。

クラス拡張を他のパッケージにエクスポートするつもりが無ければ、デフォルト名前空間 internal の動作は大抵の場合素晴らしいものであり、上の例は簡単になる:

extend(String) {
  function scramble():String {...}
  function unscramble():String {...}
}

var x:String = "abc".scramble();

インターフェイス

インターフェイスは JavaScript 2.0 で検討されたが、言語の単純さを保つために除外された — 動的な型付けが起こる言語では静的な型付けが起こる言語ほどの必要性は無い。

インターフェイスは以下の構文とセマンティクスの追加により定義されただろう。

インターフェイスの定義

InterfaceDefinition 
   interface Identifier ExtendsList Block
|  interface Identifier Semicolon
ExtendsList 
   «empty»
|  extends TypeExpressionList
TypeExpressionList 
   TypeExpressionallowIn
|  TypeExpressionList , TypeExpressionallowIn

インターフェイス II を実装するクラス C の基底型ではないという点を除けば、インターフェイスはクラスのようなものである。その代わり C のインスタンス c は型 I暗黙に強制型変換可能で、I のインスタンス i が得られ、i を型 C暗黙の強制型変換すると元のインスタンス c が得られる。これは c == i が成立するかどうかを示すものではない。

インターフェイスは具象メンバ、抽象メンバの両方を持つことができるが、コンストラクタを持つことはできない。

名前の衝突が無ければ、インターフェイス I のメンバは I を実装するクラス C のあらゆるインスタンス c のプロパティとしてアクセスできる。しかしながらインターフェイス I のメンバ m をクラス C のメンバと同じ名前で定義し、なお2つのメンバを区別することは正しいことである。また1つのクラスが両方とも名前 m のメンバを持つ2つのインターフェイス IJ を実装し、2つの m を区別するのも正しいことである。メンバ探索 c.m を処理するときどちらのメンバが得られるかは、c が最後にどちらのインターフェイス、或いはオブジェクト型に変換されたかによる。

クラス定義

クラス定義の Inheritance 節はインターフェイスを許容するために implements 節にインターフェイスのリストを指定するものに修正される:

ClassDefinition  class Identifier Inheritance Block
Inheritance 
   «empty»
|  extends TypeExpressionallowIn
|  implements TypeExpressionList
|  extends TypeExpressionallowIn implements TypeExpressionList

新しく定義したクラスは、基底クラスと基底インターフェイスから (もしあれば) インスタンスメンバと静的メンバを継承する。基底クラスとインターフェイスで衝突がある場合は、基底クラスを優先する。2つの基底インターフェイスで衝突がある場合はどちらかを優先させること無く、探索ではインスタンスメンバについては基底インターフェイス名で限定し、静的メンバについては基底インターフェイス名で直接限定しなければならない。

属性

primitive

以前の草案では primitive クラス修飾属性が考えられていた。この属性を使用するとそのクラス型の変数が格納できる値の集合から null を排除することができた。例えばクラス Cprimitive 属性を使って定義すると null は型 C のメンバではなくなる (型 C の未初期化の変数にデフォルト値を指定する方法が無ければならない)。この属性によりユーザ定義クラスに Number などの定義済みクラスのような振る舞いを持たせることが可能になる。

この属性が現在の草案が除外されたのは、未定義クラスの循環問題のためである。仮にクラス C が未定義でも型 C の変数を作成することは可能なのだ (この変数は null で初期化される)。C が単純 (プリミティブ) クラスであることが分かると、この値を遡って変更しなければならない。

パッケージのインポ−ト

[訳注: この節の内容は「コア言語」の「パッケージ - パッケージの参照」とほとんど同じである]

以下に述べるように import ディレクティブにより多くのオプションを使って、より良い識別子、名前空間の管理が可能なように拡張できる:

パッケージ Pimport ディレクティブを使うことで異なるパッケージ Q を参照できる:

ImportDirective 
   import ImportBinding IncludesExcludes
|  import ImportBinding , namespace ParenListExpression IncludesExcludes
ImportBinding 
   PackageName
|  Identifier = PackageName
IncludesExcludes 
   «empty»
|  , exclude ( NamePatterns )
|  , include ( NamePatterns )
NamePatterns 
   «empty»
|  NamePatternList
NamePatternList 
   QualifiedIdentifier
|  NamePatternList , QualifiedIdentifier

ParenListExpression が与えられた場合、それはパッケージから提供される名前空間のリストであるべきである。これらの名前空間は import 文により use される。パッケージ間の名前の衝突を解決するために、IncludesExcludes はどの名前をインポートするかを決めるためのきめ細かい制御を可能にする。include 節、exclude 節はどの名前の集合が最上位変数として共有されるかを決定する。include を使った場合はリストに挙げられた名前だけがアクセス可能になる。逆に exclude の場合はリストに含まれていない全ての名前がアクセス可能になる。例えば:

package My.P1 {
  explicit namespace N;

  N const a = "global a";
  N const b = "global b";
  N class C {

    static var x = 2;
  }
  N const c = new C(i:5);     // c.i を 5 で初期化する
  const x = "global x";
}

package My.P2 {
  import P = My.P1, namespace(N), exclude(N::b, x);  // My.P1 をインポートし、名前空間 N を use する。ただし N::b と x は除外する
  c;                          // OK。クラス C  のインスタンスとして評価される
  N;                          // エラー: N は explicit なので見つからない
  P.N;                        // OK。パッケージ My.P1 内の名前空間 N として評価される
  a;                          // OK。"global a" として評価される
  b;                          // エラー: N::b は除外されているので見つからない
  P.b;                        // OK。"global b" として評価される
  (P.N)::b;                   // エラー: N::b は除外されているので見つからない
  x;                          // エラー: グローバルな x は除外されているので見つからない
  C.x;                        // OK。2 として評価される
}

include 節も exclude 節も無ければ、exclude() としたことと同じになる。

import ディレクティブは以下の処理を行う:

パッケージ Ppublic な最上位定義 n を持っており、パッケージ Qimport PkgP = PP をインポートすると、パッケージ Qn 或いは PkgP.n のいずれでも n を参照することができる。短い形式の n は他の n と衝突する場合は使用できない。その最上位スコープの汚染を避けるためにパッケージ Qimport PkgP = P, include() 或いは import PkgP = P, exclude(n) を使ってパッケージ P をインポートすることができる。この場合、PkgP.n を使う以外にパッケージ Qn を参照する方法は無い。

パッケージ Pexplicit な最上位定義 n を持ち、パッケージ QP をインポートした場合、PkgP.n を使う以外にパッケージ Qn を参照する方法は無い。

パッケージ P が名前空間 N 内に最上位定義 n を持ち、パッケージ Qimport PkgP = PP をインポートすると、パッケージ QPkgP.N::n 或いは N::n のいずれでも n を参照することができる (いずれの場合も名前 N も同様にアクセス可能でなければならない。N のアクセス性が public でない場合は名前の限定を、N のアクセス性が explicit である場合は n ではなく (PkgP.N) の使用を必要とする可能性がある)。パッケージ Q は代わりに import PkgP = P, namespace(N) を使ってパッケージ P をインポートすることで、名前の衝突が無ければただ n と書くだけで n を参照可能になる。 他にも、パッケージ Q は後ろに use namespace(N) (或いは use namespace(PkgP.N)) が続く import PkgP = P を実行することで同様の効果を得ることができる。

wrap モード

この草案の初期のドラフトには、(型の) 範囲を超えた整数に対してエラーを生成する代わりに、その整数を暗黙の内に整数マシン型に変換する wrap プラグマが含まれていた。このプラグマの有効範囲はプラグマの字句的な有効範囲と同じである。このプラグマの基本的な機能は結果が左辺値に収まらないときにエラーを生成する代わりに、sbytebyteshortushortintuintlong 、及び ulong において算術的な折り返しを行うことである。

このプラグマは効率重視のコードでは有用なものだが、言語の単純さを保持するために除外された。折り返しは整数マシン型のいずれかに明示的にキャストすることでも実現できる。


Waldemar Horwat
最終更新: 2003年6月4日 (水)
previousupnext
訳者: exeal <exeal@student.interq.or.jp>
このドキュメントのオリジナルは mozilla.org において英語で公布されています。
この和訳は、利用者の利便のために Mozilla Japan 翻訳部門 によって提供されています。
内容に関してご不明な点がありましたら webmaster までお問い合わせください。