プロパティへ値を代入するときの挙動

# 概要

プロトタイプチェーンはプロパティの読み込みだけじゃなくて代入するときも影響するという話。値を代入しようとしたときには以下の内部メソッドたちが呼ばれる、これは意外と複雑。

* [[DefineOwnProperty]]
* [[GetOwnProperty]]
* [[GetProperty]]
* [[CanPut]]
* [[Put]]

この記事では、これらの内部メソッドの詳細と、プロパティに値を代入するときに起こりえる挙動について説明する。

# 内部メソッドたちの挙動

仕様上での挙動とJavaScriptでのサンプルコードで解説(一部省略)。

## [[DefineOwnProperty]]\(P, Desc, Throw\)

オブジェクト *O* 自身のプロパティ名 *P* にプロパティディスクリプタ *Desc* を指定して定義する。 *Throw* はフラグで、strict modeのとき **true** となる。`Object.defineProperty`と同じようなもの。

## [[GetOwnProperty]]\(P\)

オブジェクト *O* 自身のプロパティ名 *P* であるプロパティのプロパティディスクリプタを返す。`Object.getOwnPropertyDescriptor`と同じようなもの。返り値は内部的に使われる。

## [[GetProperty]]\(P\)

オブジェクト *O* のプロパティ名 *P* である(プロトタイプチェーンから最初に見つかった)プロパティのプロパティディスクリプタを返す。返り値は内部的に使われる。

1. *prop* を プロパティ名を *P* として *O* の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
2. *prop* が **undefined** でない場合、 *prop* を返す
3. *proto* を *O* の[[Prototype]]内部プロパティ(プロトタイプのこと)とする
4. *proto* が **null** の場合、 **undefined** を返す
5. プロパティ名を *P* として *proto* の[[GetProperty]]内部メソッドを呼んだときに返り値を返す

```js:
function getProperty(O, P) {
  var prop, proto;
  prop = Object.getOwnPropertyDescriptor(O, P);
  if (prop !== undefined) return prop;
  proto = Object.getPrototypeOf(O);
  if (proto === null) return;
  return getProperty(proto, P);
}
```

## [[CanPut]]\(P\)

オブジェクト *O* のプロパティ名 *P* のプロパティに値を代入できるかどうかを真偽値で返す。返り値は内部的に使われる。

1. *desc* をプロパティ名を *P* として *O* の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
2. *desc* が **undefined** でない場合、
    1. *desc* がアクセサディスクリプタな場合、 
        1. *desc*.[[Set]]が **undefined** な場合、 **false** を返す
        2. 違う場合、 **true** を返す
    2. 違う場合は必ずデータディスクリプタになるので、 *desc*.[[Writable]]を返す
3. *proto* を *O* の[[Prototype]]内部プロパティとする
4. *proto* が **null** な場合、 *O* の[[Extensible]]内部プロパティの値を返す
5. *inherited* をプロパティ名を *P* として *proto* の[[GetProperty]]内部メソッドを呼んだときに返り値とする
6. *inherited* が **undefined** な場合、*O* の[[Extensible]]内部プロパティの値を返す
7. *inherited* がアクセサディスクリプタな場合、
    1. *inherited*.[[Set]] が **undefined** な場合、 **false** を返す
    2. 違う場合、 **true** を返す
8. *inherited* はデータディスクリプタになり、
    1. *O* の内部プロパティ[[Extensible]]内部プロパティが **false**の場合、 **false** を返す
    2. 違う場合、 *inherited*.[[Writable]]を返す

### Note

* アクセサディスクリプタとはアクセサプロパティのプロパティディスクリプタのこと
* データディスクリプタとはデータプロパティのプロパティディスクリプタのこと
* *hoge*.[[Set]]、 *hoge*.[[Writable]]等は実際のコード(`Object.getOwnPropertyDescriptor`で取得できるオブジェクト)では `hoge.set`、`hoge.writable`と読み替える
* *O*の[[Extensible]]内部プロパティは`Object.isExtensible(O)`で得られる

```js:
function isAccessorDescriptor(desc) {
  var hasOwn = Object.prototype.hasOwnProperty;
  return hasOwn.call(desc, 'get') && hasOwn.call(desc, 'set');
}

function isDataDescriptor(desc) {
  var hasOwn = Object.prototype.hasOwnProperty;
  return hasOwn.call(desc, 'value') && hasOwn.call(desc, 'writable');
}

function canPut(O, P) {
  var desc, proto, inherited;
  desc = getOwnProperty(O, P);
  if (desc !== undefined) {
    if (isAccessorProperty(desc)) return desc.set !== undefined;
    return desc.writable;
  }
  proto = Object.getPrototypeOf(O);
  if (proto === null) return Object.isExtensible(O);
  inherited = getProperty(proto);
  if (inherited === undefined) return Object.isExtendible(O);
  if (isAccessorProperty(inherited)) return inherited.set !== undefined;
  if (Object.isExtensible(O)) return false;
  return inherited.writable;
}
```

## [[Put]]\(P, V, Throw\)

オブジェクト *O* のプロパティ名 *P* のプロパティに値 *V* を代入するときに呼ばれる。 *Throw* はフラグでstrict modeのときに **true** となる。

1. プロパティ名を *P* として *O* の[[CanPut]]内部メソッドを呼んだときの返り値が **false** の場合、
    1. *Throw* が **true** な場合、 **TypeError** 例外を投げる
    2. 違う場合、終了
2. *ownDesc* をプロパティ名を *P* として *O* の[[GetOwnPropery]]内部メソッドを呼んだときの返り値とする
3. *ownDesc* がデータプロパティの場合、
    1. *valueDesc* をプロパティディスクリプタ　{[[Value]]: *V*}とする
    2. 引数を *P* 、 *valueDesc* 、 *Throw* として *O* の[[DefineOwnProperty]]を呼ぶ([[Value]]だけ上書きする)
    3. 終了
4. *desc* をプロパティ名を *P* として *proto* の[[GetProperty]]内部メソッドを呼んだときに返り値とする
5. *desc* がアクセサディスクリプタな場合、
    1. *setter* を *desc*.[[Set]]とする(**undefined** の場合は終了?)
    2. **this** を *O* 引数を *V* として *setter* の [[Call]]内部プロパティを呼ぶ
6. 違う場合、
    1. *newDesc* をプロパティディスクリプタ{[[Value]]: *V*, [[Writable]]: **true**, [[Enumerable]]: **true**, [[Configurable]]: **true**}とする
    2. 引数を *P* 、 *newDesc* 、 *Throw* として *O* の[[DefineOwnProperty]]を呼ぶ
7. 終了

### Note

[[Call]]内部プロパティは`Function.prototype.call`みたいなもの(厳密には違う)

```js:
// strict modeは割愛
function put(O, P, V) {
  if (!canPut(O, P)) return;
  var ownDesc, valueDesc, desc, setter, newDesc;
  ownDesc = Object.getOwnPropertyDescriptor(O, P);
  if (isDataDescriptor(ownDesc)) {
    valueDesc = { value: V };
    Object.defineProperty(O, P, valueDesc);
    return;
  }
  desc = getProperty(O, P);
  if (isAccessorDescriptor(desc)) {
    setter = desc.set;
    if (settter === undefined) return;
    setter.call(O, V);
  } else {
    newDesc = {
      value: V,
      writable: true,
      enumerable: true,
      configurable: true
    };
    Object.defineProperty(O, P, newDesc);
  }
}
```

# オブジェクト自身にプロパティがないときに新しいプロパティが作られない状況

オブジェクトにプロパティを代入しようとしたとき(`foo.bar = baz`)に新しくプロパティが定義されない状況は大きく分けて3パターンある

## Object.isExtensible(foo)がfalse

これは単純に新しくプロパティを追加できないという状態。でも、こういう状況はあまりない気がする。

## プロトタイプチェーン上から得たプロパティがデータプロパティで書き込み不可能

プロトタイプチェーン上のプロパティであっても[[Writable]]が **false** であれば新しくプロパティは作られない。

```js:
var proto = Object.create(null, {
  bar: { value: 1, writable: false }
});
var foo = Object.create(proto);
foo.bar = 10;
console.log(Object.prototype.hasOwnProperty.call(foo, 'bar')); // false
console.log(foo.bar); // 1
```

## プロトタイプチェーン上から得られたプロパティがアクセサプロパティ

プロトタイプチェーン上から得られたプロパティがアクセサプロパティな場合、セッターがある場合はセッターが呼ばれ、ない場合は何もしない。

```js:
var proto = {
  _bar: 1,
  set bar(x) { this._bar = x }
}
var foo = Object.create(proto);
foo.bar = 10;
console.log(Object.prototype.hasOwnProperty.call(foo, 'bar')); // false
console.log(Object.prototype.hasOwnProperty.call(foo, '_bar')); // true
console.log(foo._bar); // 10
```

プロトタイプにアクセサプロパティを定義するのはクラス風の書き方ではけっこう使えそう。

```js:
function BitReader(bytes) {
  this.bytes = bytes;
  this.bitOffset = 0;
}

Object.defineProperty(BitReader.prototype, 'byteOffset', {
  get: function() { return this.bitOffset >>> 3; },
  set: function(byteOffset) { this.bitOffset = byteOffset << 3; }
});

var bitReader = new BitReader(new Uint8Array(1024));
bitReader.byteOffset = 512;
console.log(bitReader.bitOffset); // 4096
```

ちなみに、TypeScriptではこのテクニックが使われている。[サンプルコード](http://www.typescriptlang.org/Playground/#src=class%20Foo%20%7B%0A%09private%20_bar%3B%0A%09%0A%09get%20bar()%20%7B%0A%09%09return%20this._bar%3B%0A%09%7D%0A%09%0A%09set%20bar(v)%20%7B%0A%09%09this._bar%20%3D%20v%3B%0A%09%09%0A%09%7D%0A%7D)

## それでも新しくプロパティを定義したいは

`Object.defineProperty`を使おう。終わり。

# 参考

* [ECMAScript Language Specification - ECMA-262 Edition 5.1](http://www.ecma-international.org/ecma-262/5.1/)