かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

javascript thisはトラップが多いよねー大井っちー

第1回 JavaScriptの基礎を見直す:聞いたら一生の宝,プログラミングの基礎の基礎 |gihyo.jp … 技術評論社 という記事を読みました。

jsってthisのトラップ多いよねーと思いながら読んでいたのですが気になる所があったので検証してみました。

thisの使い分けができていますか?
JavaScriptのthisは呼び出し元によって意味が変わります。

1. グローバルオブジェクトとしてのthis
2. コンストラクタ内のthis
3. メソッドに所属しているthis
4. apply,callから変えられるthis

といった内容の箇所にサンプルコードが載っていて

やりたいこと
下のコードでresizeTouphone.ratio.wを正常に出力したい

var resizeToiphone = {
  wWidth  : document.documentElement.clientWidth,
  wHeight : document.documentElement.clientHeight,
  iWidth  : 320,
  iHeight : 568,
  ratio   : {
    w : this.iWidth/this.wWidth,
    h : this.iHeight/this.wHeight
  }
}

現在のコードでresizeToiphone.ratio.wを出力するとundefinedになってしまいます。

とあります。

記事内にも書いてあるようにthisWindowなので、this.iWidthundefinedかと思いますが、undefined / undefinedNaNになったような気がします。
もしかして、この計算ってブラウザで異なるのでしょうか???

※以下Chromeで確認しています。

resizeToiphone.ratio.wはundefinedなのか?

var resizeToiphone = {
  wWidth  : document.documentElement.clientWidth,
  wHeight : document.documentElement.clientHeight,
  iWidth  : 320,
  iHeight : 568,
  ratio   : {
    self: this, // thisが何かちぇっく。
    iWidth: this.iWidth,
    w : this.iWidth/this.wWidth,
    h : this.iHeight/this.wHeight
  }
}
console.log(resizeToiphone.ratio);
// -> Object {self: Window, iWidth: undefined, w: NaN, h: NaN}

resizeToiphone.ratioオブジェクト内のthisWindowthis.iWidthundefinedで合ってました。
そして予想通りundefined / undefinedNaNなので、resizeToiphone.ratio.wresizeToiphone.ratio.hNaNになっているようです。

undefinedの計算とかをしらべてみる

undefined + undefined;  // NaN
undefined - undefined;  // NaN
undefined * undefined;  // NaN
undefined / undefined;  // NaN

// 数値にしてみる
Number(undefined);       // NaN
parseInt(undefined,10);  // NaN
undefined - 0;           // NaN
+undefined;              // NaN

// 文字列にしてみる
String(undefined);     // "undefined"
undefined.toString();  // TypeError: Cannot read property 'toString' of undefined    
undefined + '';        // "undefined"

// Bool値にしてみる
Boolean(undefined);  // false
!!undefined;         // false

ことごとくNaNです!



さて、元の記事にもどります。
まぁさっきのコードは上手くいかないよってことで下記のような解決策のコードが載っています。

var resizeToiphone = {
  wWidth  : document.documentElement.clientWidth,
  wHeight : document.documentElement.clientHeight,
  iWidth  : 320,
  iHeight : 568,
  ratio   : function () {
    return {
      w : this.iWidth/this.wWidth,
      h : this.iHeight/this.wHeight
    };
  }
};

他にもコンストラクタ内のthisとして扱う方法もありますが, シンプルに行うのは上記でしょう。

resizeTouphone.ratio.wを正常に出力したいという事でしたので、これ解決になってないような気がします。。。
まずresizeToiphone.ratiofunctionままだと思います。
なので、このままresizeToiphone.ratio.wとした場合functionオブジェクトのwプロパティが返されるような気がします。Function.wと同じ?(すみませんここハッキリ理解してるわけじゃないので明言できないのですが...) なので結果はundefinedではないでしょうか?
resizeToiphone.ratio.wではなくresizeToiphone.ratio().wとすれば、function内のthisはresizeToiphoneになるので値が取得できそうな気がします。取得する方法を書き忘れてたのかな?

試してみましょう。

修正されたコードのresizeToiphone.ratio.wで値は取れるのか?

var resizeToiphone = {
  wWidth  : document.documentElement.clientWidth,
  wHeight : document.documentElement.clientHeight,
  iWidth  : 320,
  iHeight : 568,
  ratio   : function () {
    return {
     self: this, // 関数内のthisが何になってるかしらべる。
      w : this.iWidth/this.wWidth,
      h : this.iHeight/this.wHeight
    };
  }
};

// resizeToiphone.ratio.w
console.log(resizeToiphone.ratio);      // function() {...
typeof(resizeToiphone.ratio);           // "function"
console.log(resizeToiphone.ratio.w);   // undefined
console.log(resizeToiphone.ratio.self); // undefined

// Function も見てみる
console.log(Function);   // function Function() { [ native code ] }
console.log(Function.w); // undefined

// resizeToiphone.ratio().w
console.log(resizeToiphone.ratio().w);  // 0.xxxx 欲しい値!
console.log(resizeToiphone.ratio().self);
// -> Object {wWidth: XXX, wHeight: XXX, iWidth: 320, iHeight: 568, ratio: function} 

やはり、resizeToiphone.ratio.wundefinedでした。
resizeToiphone.ratio().wと関数を実行して返されるオブジェクトの値を見れば欲しい値が取れています。特にresizeToiphone.ratio()を実行する度にwWidthwHeightは再計算される訳じゃないのでresizeToiphone.ratio().wresizeToiphone.ratio().hで取れる値はこの変数が実行された時の値のまま一定です。
レスポンシブなサイトで使うとかなら関数のままにしておいて都度再計算させるのが良い様に思いますが、最初に計算しておいて常に固定の値が欲しいのであれば、値を取りに行く度に関数が実行されるので少しアレな気もします。


即時関数にすればいい?

即時関数になってない。みたいなコメントも見たのですが、即時関数にした所で即時関数内のthisWindowのままなのでダメな気がします。

私気になります!
はい。調べます。

var resizeToiphone = {
  wWidth  : document.documentElement.clientWidth,
  wHeight : document.documentElement.clientHeight,
  iWidth  : 320,
  iHeight : 568,
  // ↓ 即時関数にしてみるよ。
  ratio   : (function () {
    return {
      self: this, // 即時関数内のthisが何かしらべる
      w : this.iWidth/this.wWidth,
      h : this.iHeight/this.wHeight
    };
  })()
};

console.log(resizeToiphone.ratio.w); // NaN
console.log(resizeToiphone.ratio);
// -> Object {self: Window, w: NaN, h: NaN}

はい。やはり即時関数にしてもthisWindowなのでダメでした。 あ。
即時関数の書き方によってはratioの値が変わってしまうことがあるので注意が必要です。
過去の記事ですが参考までに



僕ならどうするか考えてみた。

case1
  • resizeToiphone.ratio.wは初回に計算しちゃって常に固定値を返せばOK
  • resizeToiphone.ratio.wresizeToiphone.ratio.hだけ取れればOK
  • resizeToiphone.wWidthなどは外から取れなくてOK
var resizeToiphone = (function(d) {
  var wWidth= d.documentElement.clientWidth,
       wHeight= d.documentElement.clientHeight,
       iWidth= 320,
       iHeight= 568;
  return {
    ratio: {
      w: iWidth/wWidth,
      h: iHeight/wHeight
    }
  };
})(document); 

console.log(resizeToiphone.ratio);
// -> Object {w: 0.XXXXX, h: 0.XXXXXXXX}
case2
  • あくまでthisを使いたい
  • resizeToiphone.wWidth なども外から取れるようにしたい
var ResizeToiphone = function() {
  this.wWidth = document.documentElement.clientWidth;
  this.wHeight = document.documentElement.clientHeight;
  this.iWidth = 320;
  this.iHeight = 568;
  this.ratio = {
    w: this.iWidth / this.wWidth,
    h: this.iHeight / this.wHeight
  };
  return this;
}

var resizeToiphone = new ResizeToiphone();

console.log(resizeToiphone);
// -> ResizeToiphone {wWidth: XXX, wHeight: XXX, iWidth: 320, iHeight: 568, ratio: Object}
console.log(resizeToiphone.ratio);
// -> Object {w: 0.XXXXX, h: 0.XXXXXXXX}
case3
  • CLASSみたいに使いまわさないので、ResizeToiphoneって変数をglobalに定義したくない

よろしい。ならば無記名関数を使ってやる!

var resizeToiphone = (new function(d) {
  this.wWidth = d.documentElement.clientWidth;
  this.wHeight = d.documentElement.clientHeight;
  this.iWidth = 320;
  this.iHeight = 568;
  this.ratio = {
    w: this.iWidth / this.wWidth,
    h: this.iHeight / this.wHeight
  };
  return this;
}(document));

console.log(resizeToiphone);
// -> Object {wWidth: XXX, wHeight: XXX, iWidth: 320, iHeight: 568, ratio: Object}
console.log(resizeToiphone.ratio);
// -> Object {w: 0.XXXXX, h: 0.XXXXXXXX} 

キモい!!

少し別件ですが即時関数でnew function()とする時は即時関数の閉じ方を気をつけないと変数を渡せないというか、エラーになるっぽいです!少しハマりましたw

// 変数を渡せない。というかエラーになるパティーン
var a = (new function(w, d) {
  console.log(w, d);
})(window, document);
// -> TypeError: object is not a function

// 変数を渡せるパティーン
var b = (new function(w, d) {
  console.log(w, d); // Window, #document
}(window, document));

即時関数でnew function()するのは気をつけないとダメなケースがあるので、即時関数の中でnewして返すのが見通しが良さそうな気がします。

var resizeToiphone = (function(d) {
  var ResizeToiphone = function() {
    this.wWidth = d.documentElement.clientWidth;
    this.wHeight = d.documentElement.clientHeight;
    this.iWidth = 320;
    this.iHeight = 568;
    this.ratio = {
      w: this.iWidth / this.wWidth,
      h: this.iHeight / this.wHeight
    };
    return this;
  };
  return new ResizeToiphone();
})(document);

console.log(resizeToiphone);
// -> ResizeToiphone {wWidth: XXX, wHeight: XXX, iWidth: 320, iHeight: 568, ratio: Object}
console.log(resizeToiphone.ratio);
// -> Object {w: 0.XXXXX, h: 0.XXXXXXXX} 

/* ↓ var ResizeToiphoneの定義もなくしちゃう */

var resizeToiphone = (function(d) {
  return new function() {
    this.wWidth = d.documentElement.clientWidth;
    this.wHeight = d.documentElement.clientHeight;
    this.iWidth = 320;
    this.iHeight = 568;
    this.ratio = {
      w: this.iWidth / this.wWidth,
      h: this.iHeight / this.wHeight
    };
    return this;
  };
})(document);

console.log(resizeToiphone);
// -> Object {wWidth: XXX, wHeight: XXX, iWidth: 320, iHeight: 568, ratio: Object}
console.log(resizeToiphone.ratio);
// -> Object {w: 0.XXXXX, h: 0.XXXXXXXX} 

はい。私気になります!で色々と調べてみる事ができて楽しかったです。

やはりjavascriptのthisはトラップが多いので、ぼくはオブジェクトや関数内で使う時は最初にvar self = this;としてしまって最初のthisを変数にしておくことが多いです。変数にしておけば関数内に関数を作って実行しする際に元のthisを使いたい時とかselfという変数になっているので解りやすいし間違いも少なくなると思っています。

CoffeeScriptとか使えばいいじゃん!って?
まぁそうなのですが、元のjavascriptでどうなっているか知っていた方がCoffeeScript使った時にも間違いは少ないと思うの、なのです!


[参考]

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス

JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス