uhyohyo.net

JavaScript初級者から中級者になろう

九章第六回 ゲッタとセッタ

このページの最終更新日:

今回紹介するゲッタセッタは、オブジェクトのプロパティを参照したり変更しようとするときに、関数を呼ぶというものです。このとき呼ばれる関数が、ゲッタやセッタです。参照されたときに呼ばれるのがゲッタで、値を変更するときに呼ばれるのがセッタです。

したがって、ゲッタやセッタは関数です。それでは、ゲッタやセッタをどう作るのか解説します。

作り方

作り方の1つは、オブジェクトリテラル(すなわちオブジェクトを作るとき)に特殊な記法によってゲッタやセッタを作る方法です。以前、 { プロパティ:値, プロパティ:値 } という形でオブジェクトを作ることを解説しました。ここに、プロパティ:値ではない特殊な形を入れます。

次のサンプルを見てみましょう。


var a = {
  get aaa(){ return 3; },
  set aaa(){}
};
console.log(a.aaa);
a.aaa=5;
console.log(a.aaa);

2回とも3と表示されるはずです。

オブジェクトaをつくるとき、プロパティ:値のかわりに、getとsetの形で書かれています。この形は、 function 関数名(){ 〜 } のfunctionがget,setに変わった形と見ることができるかもしれません。ただし、書く場所は前述のようにオブジェクトリテラルの中です。

まずa.aaaが参照されています。ここでオブジェクトaはaaaというゲッタを持っているので、ゲッタ(get aaaの関数)が呼ばれるのです。

そして、ゲッタの戻り値がそのままa.aaaの値として扱われます。今回ゲッタは無条件で3を返すので、a.aaaは3ということになり、3が表示されました。

次では、aaaに5を代入しようとしています。このとき呼ばれるのがセッタ(set aaa)です。

今回セッタの中身は空っぽです。つまり、何も処理をしないということです。ちなみに、今回は使ってませんが、代入されようとしている値は第一引数に入っているので、それを利用できます。

さて、セッタが何もしなかったので、a.aaa=5;の行では何も起こらなかったということになります。

次にまたa.aaaを表示していますが、ゲッタは相変わらず3を返すので3が表示されます。

結局、今回ゲッタとセッタによってできたプロパティaaaは常に3が入っているような振る舞いをすることになります。

ゲッタ・セッタの利用

さて、さっきのサンプルはあまり実用的ではありませんでした。これらを、クロージャのときのようにアクセスの制限のために使うのにはどうすればよいのでしょう。

例えば、ゲッタとセッタでaというプロパティを作りたいとします。そうすると、実際のaの値を別のところに保存しておけばいいのです。


var obj = {
  _a : 0,
  get a(){return this._a; },
  set a(n){this._a = n; }
};

obj.a=3;
console.log(obj.a);

この例では、プロパティaが参照されるときには_aの値がそのまま返され、代入するときには_aにそのまま代入されます。つまり、_aがそのままaとしての役割を果たしており、ゲッタとセッタが仲介しています。このようにして内容を保存することができます。では、これに手を加えてみましょう。


var obj = {
  _a : 0,
  get a(){return this._a; },
  set a(n){this._a = n*2; }
};

obj.a=3;
console.log(obj.a);

こんどは、_aに代入するときに2倍してみました。すると、3を代入したのに6が入っているという変なプロパティになりました。

もう少し実用的な例としては、真偽値以外代入できないというのを作ってみましょう。真偽値とは、trueまたはfalseのことですね。

つまり、値がtrueまたはfalseかどうか判定して、その場合のみ代入すればいいのです。


var obj = {
  _a : false,
  get a(){return this._a; },
  set a(n){
    if(n==false || n==true){
      this._a = n;
    }
  }
};

obj.a=true;
console.log(obj.a);
obj.a="あいうえお";
console.log(obj.a);

この場合、最初にobj.a=true;ではtrueが_aに代入され、console.logではtrueが表示されます。

次に"あいうえお"を代入しようとしますが、これはtrueまたはfalseではないので_aに代入されません。したがって、_aは変わらずtrueのままとなります。

(注: 真偽値かどうかの判定にはtypeof演算子を使うか===演算子を使うかしたほうがよいでしょう。詳しくは今度紹介しますが、実は0 == falseなのでこの例ではobj.aに0を入れることができます。)

これまでの例で見たように、ゲッタとセッタは応用次第でいろいろ使い道があります。前回のクロージャのパターンではいちいち関数を使わないといけませんでしたが、これなら普通のプロパティと同じ感覚で扱えます。ただ、プロパティを扱うたびに関数が処理されるので、普通のプロパティより処理が重いと思われます。

既存のオブジェクトに設定する方法

さて、ゲッタとセッタにはもうひとつ設定方法があります。いままでの方法は、新しくオブジェクトを作っていましたが、すでにあるオブジェクトにこれらを追加したいときはどうするのでしょう。実は、そのためのメソッドがありました


var obj = { _a : false };
obj.__defineGetter__("a", function(){ return this._a; });
obj.__defineSetter__("a", function(n){
  if(n==true || n==false){
    this._a = n;
  }
});

obj.a=true;
console.log(obj.a);
obj.a="あいうえお";
console.log(obj.a);

__defineGetter__と__defineSetter__が、それぞれゲッタとセッタを設定するメソッドです。第一引数がプロパティ名で、第二引数が関数となっています。このサンプルは、さっきのをこれらを使って書き換えたもので、同じ動作をします。

ところで、「ありました」と過去形なのは、今ではこれは使われないからです。代わりに、のちのち紹介するdefinePropertyが今では使われます。ここで紹介した__defineGetter__と__defineSetter__は昔はこれが使われていたという程度に覚えておきましょう。

というか、そもそもメソッド名が怪しいですね。アンダースコアで囲まれています。これは、このメソッドがそもそも非標準だったことが理由です。

ゲッタ・セッタの応用

ゲッタ・セッタは応用がききます。実は、九章で解説してきたprototypeと組み合わせることができます。すなわち、あるオブジェクトのprototypeにセッタ・ゲッタを設定すると、そのインスタンス全てで使うことができるのです。

ところで、次の問題が分かりますか。


//問題:最終的にobj._aの値は何になるか
var obj = {
  _a : 0,
  get a(){
    this._a++;
    return this._a;
  },
  set a(n){
    this._a=n;
  }
};

obj.a=3;
obj.a+=2;
obj.a;

console.log(obj._a);  // ?

今回のポイントは、objはゲッタで_aの値が変動するふざけたオブジェクトであるということです。

まず、obj.a=3;の行で、_aは3になります。次の行が問題です。

obj.a+=2;

これは、obj.aに2を足すということです。これを詳しく見ると、「obj.aを参照して、2を足した値を代入する」ということです。この過程で、一度obj.aが参照されます。ここでゲッタが呼ばれるのです。

ゲッタでは、_aが1たされて4になります。それを返すので、参照した結果は4です。2を足すので6になり、それがセッタで_aに代入されます。

最後にobj.a;ですが、書いてあると、とりあえず参照されます。そこでゲッタが呼ばれ、_aは1増えて7となります。

したがって、答えは7です。

ところで、書いてあると参照されると述べましたが、最初のobj.a=3;やその次では参照されないのでしょうか。実は、この場合はまず真ん中にある代入演算子がされるので、この時点では処理されません。

代入演算子の働きは左に右を代入することですが、これはやや特殊であり、特に左(ここではobj.a)を参照はしません。

そして、代入演算子は右の値を返します。したがって、この文は

3;

となります。ここで終了します。次の行も同じ感じです。

一方、obj.a;は、文を処理し始めると、いきなりobj.aが出てきます。ここで、処理の対象はobj.aそのものとなるので、参照されるのです。

だから、例えば

b = obj.a;

というように右側にある場合は、まず代入演算子が処理されますが、左に右を代入する過程で、右の値が処理の対象となり、即ち参照されます。ここでゲッタが呼ばれます。

この後代入演算子が値を返し、

obj.a;

の形になりもう一度呼ばれると思えるかもしれませんが、そういうことはありません。b = obj.a;を処理するときにまずobj.aを参照してb = (値);のように具体的な値になってしまうからです。

クロージャの利用

ところで、さっきから実際の値を保存しておくプロパティとして_aを使ってきました。しかしこれでは、_aを直接参照されてしまうと意図しない動作をすることになります。これを回避するために、前回解説したクロージャを使うことができます。


var obj=(function(){
  var _a=false;
  return {
    get a(){return _a;},
    set a(n){
      if(n==true || n==false){
        _a=n;
      }
    }
  };
})();

無名関数によって新しいスコープを作り、その中で作ったオブジェクトをreturnで外に出して変数objに代入しています。

今回、プロパティaの実体として使われているのは無名関数内のローカル変数_aです。返り値として新しく作られたオブジェクトのゲッタやセッタも、クロージャとして無名関数の中の環境を利用可能です。これならは、ゲッタとセッタを介す以外に_aにアクセスする方法がないので安全になりました。