JavaScriptはPHPとよく似たシンタクスを持っています。PHPerにとっては親近感を感じる言語かもしれません。しかし、両者の言語仕様の違いはおそらくPHPerの想像以上です。『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』の著者、Douglas Crockford 氏が「JavaScriptはCの皮をかぶったLISP」と表現するくらいです。
PHPとJavaScriptが似ている(ように見える)が故に、はまりそうなポイントとその対策について簡単にまとめてみました。より詳しく知りたい方は『JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス』や『JavaScriptパターン ―優れたアプリケーションのための作法』を読んでみることをオススメします。
あと、自分も経験が浅いところがありますので間違いなどあればご指摘くださいm(_ _)m 他にも、「こんなのではまった」というエピソードも募集してます。
1. for-in は foreach じゃない
配列のループに便利なPHPの foreach
ですが、同じ感覚でJavaScriptの配列(PHPで言う添字配列)に for-in
を使うのは、論理的なバグの原因になります。
<?php
$arr = array(1,2,3,4,5);
foreach ( $arr as $num ) {
var_dump($num);
}
/*
int(1)
int(2)
int(3)
int(4)
int(5)
*/
Array.prototype.foo = 'foo'; // ライブラリ、広告、ブログガジェット、未来の改修などあなたの知らないところで
var arr = [1,2,3,4,5];
for ( i in arr ) {
console.log(arr[i]);
}
/*
1
2
3
4
5
"foo" (←あれっ)
*/
対策
配列では、 for-in
ではなく for
を使う。
for ( var i = 0, max = arr.length; i < max; i++ ) {
console.log(arr[i]);
}
余談ですが、 for-in
は配列の順番も保証されないので、なおのこと for
を使うことが重要です。
2. ついつい var を忘れる
文末にセミコロンを用いない言語に慣れている人が、PHPでセミコロンをついつい忘れてしまうのと同じように、PHPに慣れている人は var
宣言をつい忘れてしまうことがあります。
しかし、JavaScriptはvar
宣言していない変数は、暗黙のうちにグローバル変数にしてしまいます。なんの警告もありません。
<?php
$result = '';
function func() {
$result = 'foo';
return $result;
}
func();
var_dump($result); // string(0) ""
var result = '';
function func() {
result = 'foo'; // var 忘れ!
return result;
}
func();
console.log(result); // "foo"
対策
関数の最初にローカル変数をリストアップする。そうすることを習慣化する。
function func() {
var result,
foo = 'foo',
bar = 'bar';
result = foo + bar;
return result;
}
3. 巻き上げ
PHPでは、変数初期化より先に変数を参照すると Notice エラーになります。ところが、JavaScriptには「巻き上げ」と呼ばれる振る舞いにより、PHPでは考えられない動きになることがあります。
<?php
function func() {
var_dump($foo); // NULL, Notice: Undefined variable: foo in ... on line ...
$foo = 'foo';
var_dump($foo); // string(3) "foo"
}
func();
var foo = 'hoge';
function func() {
console.log(foo); // undefined (とくにエラーにもならず、グローバルのfooを参照しているわけでもない)
var foo = 'foo';
console.log(foo); // "foo"
}
func();
var
宣言は関数のどこであっても行うことができます。「巻き上げ」とは、関数の途中で var
宣言している変数を、関数の最初で宣言したことにしていまう振る舞いのことです。つまり、上の例は、下記のコードと等価になります。
function func() {
var foo;
console.log(foo);
foo = 'foo';
console.log(foo); // "foo"
}
対策
対策としては、「var忘れ」と同じく、関数の頭でローカル変数をリストアップすることです。
4. var宣言してもローカル変数にならない
var
でリストアップするのは有効です。ただし、下記のように代入を連鎖することはハマリパターンになります。これは、式が右から順に評価されるためです。 下の例では、 bar = '1'
がグローバル変数になっていまいます。
<?php
function func() {
$foo = $bar = '1';
}
func();
var_dump($foo, $bar); // NULL, NULL (Noticeが出ます)
function func() {
var foo = bar = '1';
}
func();
console.log(foo); // "foo is not defined" (例外が飛びます)
console.log(bar); // "1"
対策
変数代入を連鎖する場合は、var
宣言だけ先にやっておきましょう。
function func() {
var foo, bar
foo = bar = '1';
}
5. for-in と prototype
オブジェクト(連想配列)に関しては、for-in
を使うべきです。ところが、JavaScript は prototype を変更することで、すべての関連するオブジェクトにその変更が行き渡ります。その結果、自分のメンバ変数ではない変数を for-in
が拾うことがあります。
<?php
$assoc = array(
'foo' => 1,
'bar' => 2,
'baz' => 3,
);
foreach ( $assoc as $key => $value ) {
var_dump($key, $value);
}
/*
string(3) "foo" int(1)
string(3) "bar" int(2)
string(3) "baz" int(3)
*/
Object.prototype.hoge = function() {}; // ライブラリ、広告、ブログガジェット、未来の改修などあなたの知らないところで
var assoc = {
foo: 1,
bar: 2,
baz: 3
};
for ( var i in assoc ) {
console.log(i, assoc[i]);
}
/*
foo 1
bar 2
baz 3
hoge function () {} // あれ?
*/
対策
hasOwnProperty()
を呼んで、自分自身のメンバ変数かを確認する必要があります。
for ( var i in assoc ) {
if ( assoc.hasOwnProperty(i) === true ) {
console.log(i, assoc[i]);
}
}
6. 最後のカンマ
PHPは配列の列挙をカンマ(,)で終わることができます。プロジェクトによっては、コーディング規約で、必ずカンマで終わることを推奨しているところもあるでしょう。一方、JavaScriptでは、カンマで終わってしまうとIEでパースエラーになります。デバッグ環境が劣悪なIEでこの手のエラーが起こると、膨大なコードをデバッグするのも一苦労ですね。
<?php
$arr = array(
'127.0.0.1',
'192.168.0.1',
'192.168.0.2',
'192.168.0.3',
);
$assoc = array(
'foo' => 1,
'bar' => 2,
'baz' => 3,
);
var arr = [
'127.0.0.1',
'192.168.0.1',
'192.168.0.2',
'192.168.0.3', // このカンマでパースエラー
]
var assoc = {
foo: 1,
bar: 2,
baz: 3, // このカンマでパースエラー
};
対策
最後にカンマを付けないように注意するにこしたことはないですが、うっかりつけてしまうことがあると思います。こうした場合のチェックは、チェックツールに任せるのがおすすめです。Google Closure Compiler や JSLint などのツールを使うと、この手のエラーを発見してくれます。
7. intval() のつもりで parseInt()
文字列を数値型にすることは、PHPであれJavaScriptであれ、しばしば行うことがあります。ググルと、parseInt()
を使った例が出てきたりします。これを intval()
的な感覚で使うと、0で始まる文字列が8進数として解釈されるなど、バグの原因になることがあります。
<?php
var_dump(intval('1')); // int(1)
var_dump(intval('12 MB')); // int(12)
var_dump(intval('0775')); // int(775)
console.log(parseInt('1')); // 1
console.log(parseInt('12 MB')); // 12
console.log(parseInt('0775')); // 14509 (なんだとぉ!?)
対策
対策としては、第二引数に進数を明示する他に、
console.log(parseInt('0775', 10)); // 775
計算をする方法などもあります。
console.log(+'0775'); // 775
console.log('0775' * 1); // 775
console.log(Number('0775')); // 775
こちらのほうが parseInt()
よりも速くなることがあるそうです。
8. 字下げスタイルによる問題
PHPのプロジェクトによっては、字下げスタイルにBSD/オールマンスタイルを起用しているケースがあります。オールマンスタイルは、開始の波括弧を次の行に置くのが特徴的です。
<?php
if ( $foo )
{
// code...
}
else
{
// code...
}
PHPとJavaScriptで似た字下げスタイルをコーディング規約に盛り込むことは、プロジェクトとして一貫性があり迷いが少なくなります。しかし、JavaScriptにオールマンスタイルを適用する場合は注意が必要です。JavaScriptは、行が正しく終っていないと判断した場合、暗黙のうちにセミコロンを挿入するからです。
<?php
function func()
{
return array(
'name' => 'Alice',
);
}
var_dump(func()); // array(1)
function func()
{
return
{
name: 'Alice'
}
}
console.log(func()); // undefined
上記のコードは、暗黙のうちに次のコードと解釈されます。
function func()
{
return; // セミコロン自動挿入。ここから下は到達できないコードになる。
{
name: 'Alice'
}
}
対策
オールマンスタイルをやめて、K&Rスタイルに統一するか、return については特別な例外を設け、チームメイトに注意喚起する。
9. 配列もオブジェクトのひとつ
PHPは配列がとても便利です。array()
と書いてしまえばあらゆる変数を格納できます。JavaScriptには、もっと短い配列リテラル []
とオブジェクトリテラル {}
があり、これまた便利です。PHPの添字配列は []
に、連想配列は {}
に直訳できますが、JavaScriptの配列もオブジェクトもあくまで オブジェクト ということを忘れてはいけません。
<?php
$arr = array(1, 2, 3);
$assoc = array(
'foo' => 'Alice',
'bar' => 'Bob',
'baz' => 'Carol',
);
// 別変数にコピーする
$arrCopy = $arr;
$assocCopy = $assoc;
// コピーを修正する
$arrCopy[0] = 99999;
$assocCopy['foo'] = 'Dave';
// 当然ながら、元の配列は変化なし
var_dump($arr[0]); // int(1)
var_dump($assoc['foo']); // string(5) "Alice"
var arr = [1, 2, 3];
var assoc = {
foo: 'Alice',
bar: 'Bob',
baz: 'Carol'
};
// 別変数にコピーする
var arrCopy = arr;
var assocCopy = assoc;
// コピーを修正する
arrCopy[0] = 99999;
assocCopy.foo = 'Dave';
// !!!??
console.log(arr[0]); // 99999
console.log(assoc.foo); // "Dave"
余談ですが、上のJSコードを厳密にPHPで再現するとしたら、次のようになります。
<?php
$arr = new ArrayObject(array(1, 2, 3));
$assoc = new ArrayObject(array(
'foo' => 'Alice',
'bar' => 'Bob',
'baz' => 'Carol',
));
$arrCopy = $arr;
$assocCopy = $assoc;
$arrCopy[0] = 99999;
$assocCopy['foo'] = 'Dave';
var_dump($arr[0]); // int(99999)
var_dump($assoc['foo']); // string(4) "Dave"
対策
for
で回してコピーする。
function copyArray(arr) {
var arrCopy = []
for ( var i = 0, max = arr.length ; i < max; i++) {
arrCopy[i] = arr[i];
}
return arrCopy;
}
function copyObject(assoc) {
var assocCopy = {}, i;
for ( i in assoc ) {
if ( assoc.hasOwnProperty(i) === true ) {
assocCopy[i] = assoc[i];
}
}
return assocCopy;
}
slice()
をたくみに使う。
var arrCopy = arr.slice(0);
jQueryを使ってコピーする。
// Shallow copy
var newObject = jQuery.extend({}, oldObject);
// Deep copy
var newObject = jQuery.extend(true, {}, oldObject);
10. メンバ変数の扱い
PHPのクラスでは、non-staticなメンバ変数が他のオブジェクトをいじったことで、影響をうけることはありません。(この例はちょっと長いですが我慢してください。)
<?php
class User
{
public $profile = array(
'name' => null,
'age' => null,
);
public $hobbies = array();
/**
* コンストラクタ
*/
public function __construct()
{
}
/**
* 名前をセットする
* @param string $name 名前
*/
public function setName($name)
{
$this->profile['name'] = $name;
}
/**
* 趣味を追加する
* @param string $hobby 趣味
*/
public function addHobby($hobby)
{
$this->hobbies[] = $hobby;
}
}
$alice = new User();
$bob = new User();
// ボブの属性を変更
$bob->setName('Bob');
$bob->addHobby('Ski');
// 当然ながら、アリスのデータは何も変わってない
var_dump($alice->profile->name); // NULL
var_dump($alice->hobbies); // array(0) {}
一方、JavaScriptでは、次のようなパターンでメンバ変数を扱った場合に、他のオブジェクトに影響を与えます。
/**
* Userオブジェクトを作成する
* @constructor
* @class User
*/
var User = function() {
}
User.prototype = {
/**
* @protected
* @var {Object}
*/
profile: {
name: null,
age: null
},
hobbies: [],
/**
* 名前をセットする
* @public
* @param {String} name 名前
*/
setName: function (name) {
this.profile.name = name;
},
/**
* 趣味を追加する
* @public
* @param {String} hobby 趣味
*/
addHobby: function (hobby) {
this.hobbies.push(hobby);
}
};
var alice = new User();
var bob = new User();
// ボブの属性を変更
bob.setName('Bob');
bob.addHobby('Ski');
// ボブに対して適用したと思っていた変更がアリスにも適用されている
console.log(alice.profile.name); // "Bob"
console.log(alice.hobbies); // ["Ski"]
(厳密には、JavaScriptにはクラスがありません。また、PHP5~のオブジェクトも参照のように振舞いますし、JavaScriptの配列/オブジェクトは、PHPのオブジェクトとして対応付けるほうがより理解が進むかと思います。)
対策
オブジェクトごとに異なるメンバ変数はコンストラクタで初期化する。
/**
* Userオブジェクトを作成する
* @constructor
* @class User
*/
var User = function() {
this.profile = {
name: null,
age: null
};
this.hobbies = [];
}
User.prototype = {
/**
* 名前をセットする
* @public
* @param {String} name 名前
*/
setName: function (name) {
this.profile.name = name;
},
/**
* 趣味を追加する
* @public
* @param {String} hobby 趣味
*/
addHobby: function (hobby) {
this.hobbies.push(hobby);
}
};
var alice = new User();
var bob = new User();
bob.setName('Bob');
bob.addHobby('Ski');
console.log(alice.profile.name); // null
console.log(alice.hobbies); // []
ちなみに、JavaScriptの new
は「既存のオブジェクトをもとに新しいオブジェクトをつくる」という意味なので、PHPの clone
に近いと思っておいたほうがいいでしょう。また、JavaScriptの prototype
に格納されているメンバ変数は、そのままではPHPで言うところの public static $value
のような状態にあると考えるとなんとなくイメージできるのではないでしょうか。
11. 正規表現をコメントアウト
PHPでは正規表現は文字列ですが、JavaScriptでは正規表現リテラル//
を使うことができます。また、PHPと同じようにJavaScriptのコメントは /* */
を使うことができます。ところが、コメントの終端*/
と /abc.*/
のような正規表現を区別するほどコンパイラは賢くありません。次の例は、パースエラーになります。
/* ちょっとコメントアウト^^
var string = "0123456789abcdef".replace(/abc.*/, '');
*/
// Uncaught SyntaxError: Unexpected token ,
対策
コメントは //
のみを使う。RegExp
オブジェクトを使う。