knockoutのcomputed property

    この記事は、knockout.js Advent Calendar 2015の７日目の記事です。 先に６日目に目を通すことを推奨しています。
    knockout , knockout-es5 , knockout.punches環境を想定しています。

knockoutでも、あるpropertyに依存した別のpropertyを定義することができます。
例えば、商品の小計を考えてみましょう。
ここでの小計は``単価　×　数量``とします。
つまり、``小計``は``単価``と``数量``に依存しています。

これを**バニラ**のjavascriptで書くと、

```js:javascript
function Order(){
  this.unitPrice=150;
  this.quantity=3;
  Object.defineProperty(this,'price',{
    get:function(){
      return this.unitPrice * this.quantity;
    }
  });
}
```

このようになります。
これについての説明は、[MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)などを参照するといいです。

では、knockoutではどう書けばいいんでしょうか？

knockoutも**まったく同じで**も**OK**です。

もちろん、このpropertyにさらに依存したpropertyも定義できます。  

>なお、knockout es5では、knockout用にカスタムしたko.definePropertyという機構も提供しています。
`Object.defineProperty`ではなく、あえて`ko.defineProperty`を採用する利点は、
`Object.defineProperty`の場合は、propertyへアクセスする度に値が再計算されるのに対して、
`ko.defineProperty`は、依存先の値が変化するまでは、計算された値をキャッシュとして保持することで、余計な再計算を防ぐことができます。
複雑な計算や、膨大な計算量がある場合には、ko.definePropertyを使うべきでしょう。

----------------------------------------------------------------------------------

では、これまでの復習とより実践的な例として、次のようなものを考えてみます。

とあるFruitShopの商品注文を想定します。このShopでは、下記の要件をクリアする必要があります。

* Shopでは複数のItem（商品）を扱っている。
* Itemは、name（商品名）とprice（単価）の情報を持っている。
* Shopでは、１つのOrder（注文）を受け付けている。
* Orderは、複数のOrderDetail（注文内訳）を持っていて、そのtotalPrice（合計価格）の情報を持っている。
* OrderDetailは Itemとquantity（個数）とprice（小計）の情報を持っている。

これらを実現するサンプルが[**こちら**](http://jsfiddle.net/MKGaru/dkod52bw/)です。

```html:index.html
<h1>Fruit Shopping Order</h1>
<div>商品リスト：
    <table>
        <thead>
            <tr>
                <th>商品名</th>
                <th>単価</th>
                <th>注文</th>
            </tr>
        </thead>
        <tbody data-bind="foreach:items">
            <tr>
                <td>{{name}}</td>
                <td>{{price}}</td>
                <td>
                    <button type="button" data-bind="click:$parent.order.add">注文</button>
                </td>
            </tr>
        </tbody>
    </table>
</div>
<br>
<div>注文票：
    <table data-bind="with:order">
        <thead>
            <tr>
                <th>商品名</th>
                <th>単価</th>
                <th>数量</th>
                <th>小計</th>
                <th></th>
            </tr>
        </thead>
        <tbody data-bind="foreach:details">
            <tr>
                <td>{{item.name}}</td>
                <td>{{item.price}}</td>
                <td><input type="number" data-bind="value:quantity" min="1" style="width:3em;" /></td>
                <td>{{price}}</td>
                <td><button type="button" data-bind="on.click:$parent.details.splice($index(),1)">キャンセル</button></td>
            </tr>
        </tbody>
        <tbody data-bind="if:!details.length">
            <tr>
                <td colspan="5" style="text-align:center">Empty</td>
            </tr>
        </tbody>
        <tfooter>
            <tr>
                <td colspan="3">合計</td>
                <td>{{totalPrice}}</td>
            </tr>
        </tfooter>
    </table>
</div>
```

```js:script.js
function Item(name,price){
    this.name = name;
    this.price = price;
    ko.track(this);
}
function Shop(){
    this.items = [
        new Item("apple",60),
        new Item("banana",25),
        new Item("cinnamon",80),
        new Item("dragonfruit",120)
    ];
    this.order = new Order();
    ko.track(this);
}
function OrderDetail(item,quantity){
    this.item=item;
    this.quantity=quantity;
    ko.track(this);
    Object.defineProperty(this,'price',{get:function(){
        return this.item.price * this.quantity
    }});
}
function Order(){
    this.details = [];
    ko.track(this);
    Object.defineProperty(this,'totalPrice',{get:function(){
        var total = 0;
        this.details.forEach(function(detail){
            total += detail.price;
        });
        return total;
    }});
}
Order.prototype.add=function(item,event){
    var quantity = 1;
    this.details.push(new OrderDetail(item,quantity));
}

var vm = new Shop();

ko.punches.enableAll();
ko.applyBindings(vm);
```

--------------------------------------------------------

蛇足ですが、そろそろ型チェックが恋しくなってきたのではないでしょうか？　ぜひTypeScriptで書きましょう。

```ts:script.ts
class Item{
    constructor(
        public name:string,
        public price:number
    ){
        ko.track(this);
    }
}
class Shop{
    public items:Item[]=[];
    public order=new Order();
    constructor(){
        this.items = [
            new Item("apple",60),
            new Item("banana",25),
            new Item("cinnamon",80),
            new Item("dragonfruit",120)
        ];
        ko.track(this);
    }
}
class OrderDetail{
    public price:number;
    constructor(
        public item:Item,
        public quantity:number
    ){
        ko.track(this);
    }
    public get price():number{
        return this.item.price * this.quantity
    }
}
class Order{
    public totalPrice:number;
    public details:OrderDetail[] = [];
    constructor(){
        ko.track(this);
    }
    public get totalPrice():number{
        var total = 0;
        this.details.forEach((detail)=>{
            total += detail.price;
        });
        return total;
    }
    public add(item:Item,event){
        var quantity = 1;
        this.details.push(new OrderDetail(item,quantity));
    }
}

var vm = new Shop();

ko.punches.enableAll();
ko.applyBindings(vm);
```