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が参照されています。ここで、get aaaの関数(ゲッタ)が呼ばれるのです。

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

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

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

さて、セッタが何もしなかったので、

a.aaa=5;

の行では何も起こらなかったということになります。

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

ゲッタ・セッタの利用

さて、さっきのサンプルでは、aaaがプロパティとしての役割を果たしていませんでした。これらを、クロージャのときのようにアクセスの制限のために使うのにはどうすればよいのでしょう。

例えば、ゲッタとセッタで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倍してみました。するとログは「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演算子(十一章第五回)を使う方法もあります。

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

また、配列のlengthも、もともとあるやつなので全く同じではないかもしれませんが、同じ感じで処理されているようです。したがって、lengthは重いと言われることもあります。

例えば、

for(var i=0;i<aaa.length;i++){
}

これは普通に配列の1つ1つに処理する感じのforループです。aaaが配列変数ですね。ここで、ループ一回ごとにi<aaa.lengthが処理されるので、aaa.lengthが処理されることになります。したがって、無駄ということになるので、次のような書き方がされることがあります。

for(var i=0,l=aaa.length;i<l;i++){
}

最初にlengthを変数に入れておいて、あとはそれを使うということです。

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

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

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;
        

今回のポイントは、ゲッタで_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;
      }
    }
  };
})();

おなじみの、無名関数を作って即座に呼び出す形ですね。無名関数の返り値がオブジェクトであり、変数objに代入されます。

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