Hatena::ブログ(Diary)

三等兵

2010-04-27

細かいJavaScriptの仕様や習慣やテク集

気づいたことやミスしたことなどメモしていたので確認作業。細かい仕様だったり暗黙のルールだったり、テクニックだったり。JSに慣れていたら当たり前なことばかりかもしらん。

追記のところはid:os0xさんより。ありがとうございます!


undefined

var a;
alert(a) // undefined

宣言だけだとundefined。undefinedというのは、宣言している変数に値が入っていませんよ、ということ。

そしてオブジェクトには無いプロパティとか参照すると出てくる。

var a = 100;
alert(a.length); // undefined

さらに引数も。

function func(val) {
  alert(val);
}
func()  // undefined

引数はCallオブジェクトってのに格納される。このオブジェクトはローカルの変数が格納されるオブジェクトでもある。変数宣言されたようなもんだから引数もundefinedになるんだろうね。


変数宣言の場所

変数宣言は関数の先頭というのがJSでは一般的。ブロックスコープないので。あとは、こういうミスの防止。

var a = 'global';
var func = function() {
alert(a);
var a = 'local';
};

func(); // undefined

これは、

var func = function() {
var a;
alert(a);
a = 'local';
};

こうなっている。とはいえこういったミスはそうしない。


データの型

変数の型はないけどデータの型はある。

  • 基本型
    • 文字列
    • 数値
    • 論理値
    • null
    • undefined
  • 参照型
    • 関数
    • オブジェクト(配列もここ)

文字列に関しては参照型という話もあるけど、人間が扱うに分に関しては基本型という認識で問題ない。


null値については、そのオブジェクトに値が無いという意味。undefinedと混同してしまうなこれ。

undefinedっていうのは未定義値って訳されている。だからundefinedは値が変数に設定されていない、定義されていないときなどに返ってくるもので、nullは値は定義されているんだけど(オブジェクトがない(値がない)ってことらしい)、その値が有効な値でないときに返ってくるもの、と私は理解している。

nullがよく返ってくる例としては、存在しない要素を参照しようとしたりすると返ってくる。

console.log(document.getElementById('hentainohanazono')); // null

id='hentainohanazono'の属性をもった要素があればnullじゃないけど。ヘンタイな人には馴染み深いid名ですね。wrapperの次に定番ですね。素晴らしいですね。


それからunllとundefinedは同じものでない。だのに等値演算子で比較するとこうなる。

null == undefined // true

でもこれは間違い。同じではない。だからこういう比較をしたいときは同値演算子にする。

null === undefined // false

面倒な暗黙の仕様。結構ひどいトラップ。こういう面倒なことが他にもあったりするので、JSでは同値演算子使えとよくいわれる。


それからnullは0ではない。でも数値に変換すると0にはなる。

Number(null) // 0

//比較
null == 0; // false

しかし比較するとfalseになる。実際はnullと0は同じでないので注意。


またnullはオブジェクト型。

typeof null // 'object'

nullやundefinedを区別する場合はtypeof演算子や同値演算子(===)を使う。


速い型変換

見辛いがこのやり方が一番速い。

var a = '12';
alert(typeof a+':'+a); // string:'12'
alert(typeof +a+':'+(+a)); // number:12
alert(typeof !!a+':'+(!!a)); // boolean:true

ただ読みづらくなる。コメント入れるのが良いかも。そこまで速さにこだわらんならparseInt使うかな。Numberは遅い。


parseInt

つながりで。基数を指定すると面倒なことならない。

parseInt('08', 10); // 8

parseInt('08'); // 0

配列のコピー

参照渡しだから代入しただけではコピーされない。また関数の引数でも参照が渡されているだけ。ということで、配列のコピーって結構面倒なことだったりする。普通に代入すると、

var alpha = [1,2,3,4,5,6];
var func = function(arr){
  arr.pop();
  return arr;
};
alert(func(alpha)); // 1,2,3,4,5
alert(alpha); // 1,2,3,4,5

元の配列まで変わる。参照だもの。


だから値をコピーしる。

var alpha = [1, 2, 3, 4, 5, 6];
var copy = [];
var func = function(arr){
  for(var i = 0; i < arr.length; i++) {
    copy[i] = arr[i];
  }
  copy.pop();
  return copy;
};
alert(func(alpha)); // 1,2,3,4,5
alert(alpha); // 1,2,3,4,5,6

さらに改良。

Array.prototype.arrayCopy = function() {
  return Array.apply(null,this)
};

var alpha = [1, 2, 3, 4, 5, 6];
var func = function(arr){
  var copy = arr.arrayCopy();
  copy.pop();
  return copy;
};

alert(func(alpha)); // 1,2,3,4,5
alert(alpha); // 1,2,3,4,5,6

追記

concatも良さげです。

var a=[1,2];
var b=a.slice();
a.pop();
alert([a.length,b.length]);//1,2

三項演算子を見やすく

あまり好きではありませんが短くすむなら。

var x = 85;
var y = x > 70 ? '上':'下';

でもこれじゃあ見辛い。なので、

var x = 85;
var y = (x > 70) ? '上':'下';

こうする。代入せず式に含めるときは、

((x > 70) ? '上':'下');

括弧でくくる。まあ人それぞれ。


with文でブロックスコープ

with文は利用しないというのがJSでは一般的に知られている。だから使わなくても良いかもしれない。ただし時と場合によるんじゃないかということで使い方だけ覚えるようにした。

発展として、with文でブロックスコープができる。

with({i:0}) {
  i = 1;
}
alert(i); // エラー

少し難しい。無名のオブジェクトに値を代入している。


これを利用するとクロージャとなる。

for(var i = 0, func = []; i < 5; i++){
  with({i:i})
    func[i] = function() { return i; }
}

alert(func[0]()+','+func[1]()+','+func[2]()); // 0,1,2

ちなみにwithをとると、

for(var i = 0, func = []; i < 5; i++){
  func[i] = function() { return i; }
}

alert(func[0]()+','+func[1]()+','+func[2]()); // 5,5,5

結構前からあるテクニックみたいですが、あまり知られていなかったり。でも、そもそも使うときがあるかなというのはまた別の話。どっかのコード読むときには役立つかもしれない。


Argumentsオブジェクト

関数の引数情報を管理するオブジェクト。利用する際はargumentsと小文字で。argumentsプロパティは扱いが配列みたいだけど配列そのものではない。「配列に似せている」だけ。

var func = function() {
  alert(arguments[0]);
}

func(1); // 1;

引数を設定していないけどいける。

var func = function() {
  var leng = arguments.length;
  for(var i = 0, arr = []; i < leng; i++){
    arr[i] = arguments[i] * 10;
  }
  alert(arr);
};

func(1,2,3,4,5,6); // 10,20,30,40,50,60

argumentsはソース読み泣かせで最初はよく混乱した。なぜそこに引数があるのだ桜木花道ぃいい、みたいな。


calleeプロパティ

Argumentsオブジェクトにはcalleeプロパティというのもある。再帰ができる。

var func = function(n) {
  return (n != 0) ? n * arguments.callee(n-1) : 1;
};

alert(func(7)); // 5040

階乗。


参照の優先順位

分かりきったことといえばその通りなのですが。

Object.prototype.name = 'object';

var Parent = function() {
  this.name = 'parent';
};

Parent.prototype.name = 'parent_prototype';

var child = new Parent();

child.name = 'child';
alert(child.name);

順位としては、

1.child

2.parent

3.parent_prototype

4.object

の順位。prototypeはメソッド追加するぐらいだろと思っていて、そのとき読んだコードにプロパティがいろいろ混じっていて混乱した。初期化しない値もあるものね。prototypeはあくまでも最終の参照。当たり前といえばそうだけど分からなかったなあ。


オブジェクトで配列

JSの配列はオブジェクトを拡張したものっていわれている。だから遅いらしい。で、どういう形かなと。

arr = {
  0 : 'zero',
  1 : 'ichi',
  2 : 'ni',
  3 : 'san',
  length: 4
};

arr.__proto__ = Array.prototype;

配列というのはオブジェクトをこんな感じにしたものなんだと思う。

alert(arr[0]+', '+arr['0']) // zero,zero

//関数
alert(arr.reverse()); //  "san", "ni", "ichi", "zero"

配列の関数もこのように扱える。ただしfor in文だとlength出てきちゃうんだよなぁ。やっぱり違うのかな?


ということであっているか心配になって検索。

http://d.hatena.ne.jp/amachang/20070202/1170386546

コードまんまか。うーん、でも配列っぽく扱えるレベルってあたりか。オブジェクトにlength与えるとそれっぽく扱えるんでしょう。2007年の時点なので、なんか過去をなぞっているのが切なくなった。


よし、今度amachangさんの過去記事ストーキングしてくる。夜道に武装してほふく前進しながらモルヒネ片手にニヤついている奴がいるかも知らんぜ。


追記

配列のlengthにはDontEnum属性というものが設定されているのでfor inでは出てこないようになっています。ECMA-262 3rdではDontEnum属性をJavaScriptから操作することはできないので(ECMAScript5では可能になりましたが)、オブジェクトで完全な配列を再現することはできません。他にも、

var a=[];a[100]=1;alert(a.length);//101

このように配列は要素を入れるとlengthは自動で設定されますが、この挙動を再現することができません。

ちなみに最近のエンジンならもちろん配列のアクセスを最適化しているので、配列が遅いということはありません。


クラスっぽいもの

JSにはクラスがないけどクラスっぽいものはある。というかオブジェクトなのですが。パっと見て分かりづらいので、JSではクラスっぽいものの名前の先頭は大文字にするルールみたいなのが定着している。

var Klass = function(x,y){
  this.prop1 = x;
  this.prop2 = y;
};

var ins = new Klass(2,2);

そしてこれはクラスっぽいものと言うと面倒なのでコンストラクタと言う。また人によってはクラスともいう。

実は正確な定義がない。なんでクラスがないのにクラスっていうかというと、説明するときに名称がないと説明しづらいからなんだろう。だからクラスってことにしたんだね。このあたりの定義の曖昧さは混乱の種。

今のJSにはクラスは存在しないので名称としてはコンストラクタで良いと思う。今後どうなるかわからないけど。人それぞれ言い方が違いますが、今のJSでクラスとコンストラクタは同じものを指しているよ。

それからfunction。もう見慣れてしまった。このあたりも混乱の種だよね。コンストラクタと関数の見分けが最初は辛い。大文字、thisが使われている、prototypeが使われている、このあたりでコンストラクタかどうか判断する。


追記

コンストラクタの見分けはnewされてるかどうかってのが重要。書いてなかった私はバカか。


thisキーワード

thisは少々やっかい。

var prop = 'global';
var Klass = function(){
  this.prop = 'prop';
  this.func = function() {
    alert(this.prop);
    (function() {
      alert(this.prop)
    })();
  }
}

var ins = new Klass();
ins.func(); // prop global

メソッドの内部関数ではthisがグローバルに羽ばたきやがる。実は関数内でthisを指定するとグローバルオブジェクトがセットされる。

function func() {
  alert(this); // [object Window]
};

うぬぅ。というわけで内部関数でその親オブジェクトを指定するためには、

var prop = 'global';
var Klass = function(){
  this.prop = 'prop';
  var self = this; // せるふりゃー
  this.func = function() {
    alert(this.prop);
    (function() {
      alert(self.prop)
    })();
  }
}

var ins = new Klass();
ins.func(); // prop prop

せるふりゃー。こうすることで自分自身を指している状態のthisを保存するようにする。

ここでthis入れたらselfはwindowが代入されているんじゃないのとか、this.propてwindow.porpにならないの?とか思っちゃうかもしれないけど、newされていることを忘れない。

インスタンス化すればthisはそのオブジェクト自身がセットされることになる。

だのに内部の関数のスコープに入るとグローバルにイン・ザ・スカイ!この関数のthisについては仕様ですと納得することに。言語設計のミスなのかどうかしらないが仕様は正義。だがあえて言おう。カスであると!(ガンダム芸人見て言いたかった)

これは簡単に回避できるのでそこまで頭痛めるようなことじゃない。


追記

thisは「一番内側の関数がどのように呼び出されたか」という条件のみによって決定されます。

>メソッドの内部関数ではthisがグローバルに

というあたりは誤解を招くかなと思います。

オブジェクトのメソッドとして呼び出されればそのオブジェクトがthisになり、call/applyで任意のオブジェクトにもできるというだけです。

var A={name:'A',f:function(){alert(this.name)}};

var B={name:'B'};

A.f(); // A

A.f.call(B);// B

ただ、関数の呼び出され方が特殊な場合(Event周りなど)ではそれに応じたthisが設定されるので、このあたりが少々ややこしいですね…

ちょっと狭い範囲でのthisのことをうじうじと書いてしまったようで。


obj={x:y}とobj.x=yの違い

恥ずかしながらこの違いに私は最近気づいた。違いもクソも別ものなんだけど。prototypeをほとんど使ったことないからわからないもんだな。簡単にいえばobj={}の{}はnew Objectのシンタックスシュガーだから、

var obj1 = {};
var obj2 = new Object();

同じ意味。新しくオブジェクトを生成しているんだよね。


ここまでは私も理解していた。でも、

var obj1 = { prop1:'prop1' };
var obj2 = obj1;

obj2.prop2 = 'prop2';
alert(obj2.prop1); // prop1

obj2 = {prop:'prop2'};
alert(obj2.prop1); // undefined

こうしてコピー(参照値の代入)がからんだら・・・あれれ?となった。そして、prototypeがからんでくるとさらにややこしいことになる。複雑なので日本語で。

var Obj = function(){};
Obj.prototype = { prop1:'1つめ' };

var obj1 = new Obj();
alert(obj1.prop1); // 1つめ

Obj.prototype = { prop1:'2つめ' };
alert(obj1.prop1) // 1つめ

var obj2 = new Obj();
alert(obj2.prop1) // 2つめ

Obj.prototype.prop1 = '3つめ';
alert(obj1.prop1) // 1つめ
alert(obj2.prop1) // 3つめ

この場合prototypeは暗黙の参照をする以外はただのオブジェクトだってこと、オブジェクトのコピーは参照渡しだということ、{}はnew Object()のシンタックスシュガーであること、obj.x=yは既存オブジェクトへの付け足しだということ、ということが理解できていれば納得できることだけどそこら辺が曖昧だと良く分からない。


obj={x:y}とobj.x=yは普通に違うのですが同じようなものとして認識してしまった言い訳をすると、入門書では、

var obj = {};
obj.prop1 = 'prop1';

と、

obj = { prop1:'prop1' };

は一緒だよと紹介されているので、そこから何を間違ったか区別がつかなくなったのです。

これだと確かに一緒なんだけど、obj={x:y}はobjという変数に新しいオブジェクトを代入していて、obj.x=yは既存のオブジェクトにプロパティを付け加えていることになるってのが良く分かっていなかったらしい。

こういうことはよくある。もともとは理解しているものでも、ちょっと形が変わったり考えたことがなかったものだと分からなくなったり。

単にお前がバカなんだといえばそうなんだけどこのやろうバズーカで星にするぞ☆


反省会

そろそろ実践的にがんばろうか。

os0xos0x 2010/04/28 03:04 こんにちはー。ちょこちょことコメントさせて頂きます。
>配列のコピー
sliceやconcatで一発です。
var a=[1,2];var b=a.slice();a.pop();alert([a.length,b.length]);//1,2
なお、浅いコピーなので、多重配列などの場合は中身のオブジェクトまでは複製されません。
>オブジェクトで配列
配列のlengthにはDontEnum属性というものが設定されているのでfor inでは出てこないようになっています。ECMA-262 3rdではDontEnum属性をJavaScriptから操作することはできないので(ECMAScript5では可能になりましたが)、オブジェクトで完全な配列を再現することはできません。他にも、
var a=[];a[100]=1;alert(a.length);//101
このように配列は要素を入れるとlengthは自動で設定されますが、この挙動を再現することができません。
ちなみに最近のエンジンならもちろん配列のアクセスを最適化しているので、配列が遅いということはありません。
>クラスっぽいもの
>コンストラクタと関数の見分け
ソースを読む際にはnewしているかどうかが最重要ポイントだと思います。
>this
thisは「一番内側の関数がどのように呼び出されたか」という条件のみによって決定されます。
>メソッドの内部関数ではthisがグローバルに
というあたりは誤解を招くかなと思います。
オブジェクトのメソッドとして呼び出されればそのオブジェクトがthisになり、call/applyで任意のオブジェクトにもできるというだけです。
var A={name:'A',f:function(){alert(this.name)}};
var B={name:'B'};
A.f(); // A
A.f.call(B);// B
ただ、関数の呼び出され方が特殊な場合(Event周りなど)ではそれに応じたthisが設定されるので、このあたりが少々ややこしいですね…

>obj={x:y}とobj.x=yの違い
var obj1 = { prop1:'prop1' };
var obj2 = obj1;
をコピーと表現されているのが気になりました。
obj1とobj2は同じオブジェクト1を参照しているだけなので、1つのオブジェクトに対して変数が2つあるだけです。こういうのはコピーとは言わないかなと。

ではでは。

makenekomakeneko 2010/04/28 09:42 null と undefined の違いについては、自分は

undefined : 意図しない未定義値
null : 意図した未定義値

と思うようにしてます。

dankogaidankogai 2010/04/28 21:18 +(+a)より0+aの方が短いし読みやすいかも

sandaisandai 2010/04/28 21:36 >os0xさん
コメントありがとうございます!
勉強になります。コメントの方を記事に追記させていただきました。
分かりやすく丁寧にご指摘していただきありがたい限りです!

>makenekoさん
コメントありがとうございます!
そのように見ることもできるのですね。

sandaisandai 2010/04/28 21:49 >dankogaiさん
ありがとうございます!
括弧は見づらいかなと思ってつけたのですが逆でしたか。
+だけでなく0をつけると確かに読みやすいですね。

KenjuKenju 2010/04/29 00:35 配列のコピーで .apply() を使った書き方がうまくいかないのは周知だと思っていたよ。
まぁ .concat() がいいだろうな。
//失敗
var a = [9];
var b = Array.apply(null, a);
alert(b[0]);

murky-satyrmurky-satyr 2010/04/29 03:50 0+'12'==='012'

sandaisandai 2010/05/01 11:33 >Kenjuさん
どうもです。私は既知じゃなかったみたいです><
そのような場合ダメなのですね。

>murky-satyrさん
そういえばそうだ気づかなかったorz
typeof(0+'12')

FeeFee 2011/03/08 08:29 私は非常にあなたのブログが好き

celladorsaikoucelladorsaikou 2013/05/11 02:07 なるほど

参考になりました

他の記事も拝見させていただきます

ありがとうございます

sunstarsunstar 2014/05/21 18:56 他の記事も拝見させていただきます

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証