読者です 読者をやめる 読者になる 読者になる

せかいや

いまいるここを、おもしろく http://sekai-in-the-box.appspot.com/

【JavaScript 】プロトタイプを中心に。Object.prototype.toString.call の理由。

 
■topic summary
study about JavaScript prototype. (not prototype.js)


JavaScript 難しい。。

 

Objectプロトタイプによって継承されたプロパティはenumerableではない

enumerableではないため、for in文には出現しない。

var map={};
for (var key in map){
 console.log(key);
}

何も出力されない
 
存在を確認する場合はgetOwnPropertyNamesを使う。

Object.getOwnPropertyNames(Object.prototype);

■実行結果

["constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "__defineGetter__", "__lookupGetter__", "__defineSetter__", "__lookupSetter__", "__proto__"]

 
 

プロトタイプ継承したオブジェクトの作成方法

var Proto={x:2, y:3};
var obj=Object.create(Proto);
obj;
 ⇒Object {x: 2, y: 3}

 

Object.create内でプロパティ属性を指定する

こうやって書くと、プロパティ属性が有効になる。
ポイントは第二引数でプロパティ属性を指定しているところ。

var obj=Object.create(Object.prototype, {x:{value:30, enumerable:true}, y:{value:3, enumerable:false, writable:false}})
for (var key in obj){
 console.log(key);
}
obj.y = 10;
obj.y

 
■実行結果
enumerable:false、writable:falseが効いている

x
3

 
 

propertiesObjectの第二引数の形?

以下はエラー

var obj=Object.create(Object.prototype, {x:30, y:{value:3, enumerable:false, writable:false}});
obj;

 
■実行結果

TypeError: Property description must be an object

なぜかは分からない。。。

JavaScript は仕様が謎すぎて、
ちゃんと追いかける気力がでない。

 
(追記)
AJさんにコメントをもらったよ!
f:id:sekaiya:20131101122131j:plain
 
AJさんありがとう!
「Object.createの第一引数はただのオブジェクト
第二引数はディスクリプタの為の書式の決まったオブジェクトです」
について考えてみよう(擬似クラス名については以降に記載の内容)。

 

ディスクリプタってなんだ??

オブジェクトに存在するプロパティのディスクリプタは主に 2 種類あります: データディスクリプタと、アクセサディスクリプタです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

 
Object.createのドキュメントにもこう書いてある

第 2 引数はキーを *プロパティディスクリプタ* に対応づけることに注意してください

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create

なるほど。
第二引数は任意の連想配列を取るわけではなく、
「プロパティディスクリプタ」というものだけOKなんだ。

 

データディスクリプタ は値を持つプロパティで、その値は書き換え可能または不可能にできます。アクセサディスクリプタ は、関数の getter と setter の組で表されるプロパティです。ディスクリプタはこれら 2 種類のいずれかでなければなりません。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

ふんふん。
やってみよう。

データディスクリプタ

var obj=Object.create(Object.prototype, {x:{value:3, enumerable:false, writable:false}})
obj.x = 10;
obj.x   #<= 3

 

アクセサディスクリプタ

var obj=Object.create(Object.prototype, {x:{
  get:function(){console.log("getter"); return bValue;}, 
  set:function(val){ bValue = val; console.log("setter")}
}})
obj.x = 10;
obj.x

■実行結果

setter
getter
10

なるほど!
自分の書いていた

var obj=Object.create(Object.prototype, {x:30, y:{value:3, enumerable:false, writable:false}});

というコードは、
2種類あるディスクリプタのどちらの形式でもないから、エラーになっていたんだ。

 
 

下請けメソッドでthisは省略できない

this.doit2()をdoit2()と書くと、グローバル関数を探しにいってしまう

x=5;
var obj={
 x:3,
 doit:function(){console.log(this.x);this.doit2()},
 doit2:function(){console.log(x)}
}
obj.doit()

■実行結果

3
5

変数のスコープについては、記事を分けてまとめる予定。

 

to_Stringを使った型判定方法

jqueryprototype.jsの著名ライブラリは型判定を文字列ベースで行っています。つまりオブジェクトをtoStringで文字列化してパターンマッチしています。

そうなんだ?

と思ってやってみたものの、なんだかよく分からず。
toStringで文字列化してもクラス名が表示されるわけではなく。
{}で囲まれていたら連想配列、とかやってるのかな。

 
こんなの理解できる?

{a:"jj"}.toString()
  ⇒SyntaxError: Unexpected token .
var map = {a:"jj"};
map.toString()
  ⇒"[object Object]"

うー??

(追記)
この内容についてもAJさんにコメントをもらったよ!
f:id:sekaiya:20131101122131j:plain
 
「擬似クラス名を取得するハックはこうです
Object.prototype.toString.call(obj)」について考えてみよう。

toString.call

obj="jjj"
Object.prototype.toString.call(obj)

■実行結果

[object String]

なるほど!

AJさんの指摘の通り、
「著名なライブラリ」はこの判定方法を使っているみたい。

_.isFunction = function(obj) {
return toString.call(obj) == '[object Function]';
};
_.isString = function(obj) {
return toString.call(obj) == '[object String]';
};

http://stackoverflow.com/questions/10394929/why-does-underscorejs-use-tostring-call-instead-of-typeof

ふんふん。

call(obj)とは、

呼び出した関数内のthis参照を指定した任意のオブジェクト参照にできます

 
Java・・・関数呼び出しにおいてレシーバが常にthisとなる
JavaScript ・・・呼び出し方によってレシーバが異なる
 
この例ではレシーバをobjにしたら同じ結果になるのでは?

obj="jjj"
obj.toString()

■実行結果

"jjj"

だめだ。どうしてだ?

 

f.call(obj)とobj.f()は全然別の概念

function f(){console.log(this.x);}
var obj = {x:10};
f.call(obj);  #<= 10

 
obj.fと書きたいときは、こう定義しないといけない。

var obj = {
 x:10,
 f:function(){console.log(this.x)}
};
obj.f()

あ!
なるほど!
toStringメソッドがStringクラスで独自定義されているから、
"jjj",toString()としても、"[object String]"ではなく文字列が出力されたんだ!

JavaScript でクラスという概念はないけど、イメージとしては
StringクラスがObjectのtoStringをオーバーライドしている感じ。

String.prototype.hasOwnProperty('toString')

■実行結果

true

やっぱり。
 
StringクラスのtoStringを再定義しなおす事でも確認できる。

String.prototype.toString=function(){console.log("hoge")}
"jjj".toString()

■実行結果

hoge

 

String.prototype.toString=Object.prototype.toString
"jjj".toString()

■実行結果

"[object String]"

なるほどね。


 
だから、

toString.call(obj)

ではなく

Object.prototype.toString.call(obj)

と書くのが一般的なのは、
「ObjectプロトタイプのtoStringメソッドを呼んでいる」事を明確化したいからだ。


callメソッドを使うことで、
自分がオーバーライドする前の元々のメソッドも実行できるし、
任意のクラスのメソッドを実行できる。

すごい自由度だ。

 
さっきのコードを振り返ると。。

{a:"jj"}.toString()
  ⇒SyntaxError: Unexpected token .
var map = {a:"jj"};
map.toString()
  ⇒"[object Object]"

これは、こういう問題に置き換えられる。

{a:"jj"}.length
 ⇒SyntaxError: Unexpected token .

オブジェクトリテラル({a:"jj"})は振る舞いを持たないから、
SyntaxErrorが発生していたというわけ。

 
ちなみに

"jjj".length
 ⇒3

文字列値は暗黙的にStringオブジェクトに変換されるから、
"jjj".lengthはエラーにならない。

var a = "jj";
Object.getOwnPropertyNames(a)
TypeError: Object.getOwnPropertyNames called on non-object
var a = new String("jj");
Object.getOwnPropertyNames(a)
["0", "1", "length"]


うひょー。