JavaScriptの使われ方には種類がある
- ブラウザで実行
- 流行りのSPAバリバリのアプリ
- ちょっとJavaScriptでスパイスを入れる
- Node.jsで実行
- サーバサイドアプリケーション
- デスクトップアプリ
たいていのページはこんな感じ
- ちょっとJavaScriptでスパイスを入れる
- だからjQueryでちょちょっとやればいいよね。
- と思ってたけど、育って大きくなっちゃった😝
ハマりがちな例
- セレクタ地獄
- イベント地獄
- スコープ地獄
- タイミング地獄
セレクタ地獄
症状
一度この状態にハマってしまうと、どこでなにを触っているのかがわからなくなり、処理の追加はできても処理の変更が怖くなってしまう。
原因
querySelectorが便利すぎて、セレクタを多用してしまう。
対策
参照する要素を変数に予め格納しておく。
- 要素に変数名をつけることができる
- 親子関係を明示できる
- 要素を再利用できる
サンプルソースコード
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
.form-group > label:first-child {
display: inline-block;
width: 120px;
text-align: right;
vertical-align: top;
}
.form-group > div.checkbox {
display: inline-block;
width: 120px;
}
</style>
</head>
<body>
自宅・携帯・Eメールの内、入力した優先連絡先を有効にする
<form id="registration-form">
<div class="form-group">
<label for="name">名前</label>
<input type="text" name="name"/>
</div>
<div class="form-group">
<label for="home-tel">自宅電話番号</label>
<input type="text" name="home-tel"/>
</div>
<div class="form-group">
<label for="mobile-tel">携帯電話番号</label>
<input type="text" name="mobile-tel"/>
</div>
<div class="form-group">
<label for="email">Eメール</label>
<input type="email" name="email"/>
</div>
<div class="form-group">
<label>優先連絡先</label>
<div class="checkbox">
<label style="display: block;"><input type="checkbox" name="access-home-tel"/>自宅電話番号</label>
<label style="display: block;"><input type="checkbox" name="access-mobile-tel"/>携帯電話番号</label>
<label style="display: block;"><input type="checkbox" name="access-email"/>Eメール</label>
</div>
</div>
</form>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
</body>
</html>
Before
$(function() {
$('#registration-form [name="home-tel"]').on('change', function() {
var value = $(this).val();
$('#registration-form [name="access-home-tel"]').prop('disabled', value === '');
});
$('#registration-form [name="mobile-tel"]').on('change', function() {
var value = $(this).val();
$('#registration-form [name="access-mobile-tel"]').prop('disabled', value === '');
});
$('#registration-form [name="email"]').on('change', function() {
var value = $(this).val();
$('#registration-form [name="access-email"]').prop('disabled', value === '');
});
// disabledに初期化
$('#registration-form [name="access-home-tel"]')
.prop('disabled', $('#registration-form [name="home-tel"]').val() === '');
$('#registration-form [name="access-mobile-tel"]')
.prop('disabled', $('#registration-form [name="mobile-tel"]').val() === '');
$('#registration-form [name="access-email"]')
.prop('disabled', $('#registration-form [name="email"]').val() === '');
});
After
$(function() {
// フォーム要素
var $form = $('#registration-form');
// 連絡方法の入力欄
var $homeTel = $form.find('[name="home-tel"]');
var $mobileTel = $form.find('[name="mobile-tel"]');
var $email = $form.find('[name="email"]');
// 優先連絡先のチェックボックス
var $accessHomeTel = $form.find('[name="access-home-tel"]');
var $accessMobileTel = $form.find('[name="access-mobile-tel"]');
var $accessEmail = $form.find('[name="access-email"]');
$homeTel.on('change', function(e) {
var value = $(e.target).val()
$accessHomeTel.prop('disabled', value === '');
});
$mobileTel.on('change', function(e) {
var value = $(e.target).val()
$accessMobileTel.prop('disabled', value === '');
});
$email.on('change', function(e) {
var value = $(e.target).val()
$accessEmail.prop('disabled', value === '');
});
// disabledに初期化
$accessHomeTel.prop('disabled', $homeTel.val() === '');
$accessMobileTel.prop('disabled', $mobileTel.val() === '');
$accessEmail.prop('disabled', $email.val() === '');
});
イベント地獄
症状
changeイベントなどが各所に記載されてしまい、どこでなにが実行されているのか、どの順番で実行されているのかがわからなくなる。
同時にタイミング地獄も発症する。
原因
Aの要素が変更されたら実行する関数、Bの要素を変更されたら実行する関数というように考えると、いろいろなところにイベントハンドラを追加することになってしまう。
また、少しずつ処理を追加していくとこの状況に陥りやすい。
対策
更新処理は一箇所に集める。Reactなどのモダンなライブラリでも更新処理は一箇所に集まっている。
こうすることで以下のメリットがある
- 処理を共通化でできる
- 処理順番が明快になる
Before
$(function() {
// フォーム要素
var $form = $('#registration-form');
// 連絡方法の入力欄
var $homeTel = $form.find('[name="home-tel"]');
var $mobileTel = $form.find('[name="mobile-tel"]');
var $email = $form.find('[name="email"]');
// 優先連絡先のチェックボックス
var $accessHomeTel = $form.find('[name="access-home-tel"]');
var $accessMobileTel = $form.find('[name="access-mobile-tel"]');
var $accessEmail = $form.find('[name="access-email"]');
$homeTel.on('change', function(e) {
var value = $(e.target).val()
$accessHomeTel.prop('disabled', value === '');
});
$mobileTel.on('change', function(e) {
var value = $(e.target).val()
$accessMobileTel.prop('disabled', value === '');
});
$email.on('change', function(e) {
var value = $(e.target).val()
$accessEmail.prop('disabled', value === '');
});
// disabledに初期化
$accessHomeTel.prop('disabled', $homeTel.val() === '');
$accessMobileTel.prop('disabled', $mobileTel.val() === '');
$accessEmail.prop('disabled', $email.val() === '');
});
After
$(function() {
// フォーム要素
var $form = $('#registration-form');
// 連絡方法の入力欄
var $homeTel = $form.find('[name="home-tel"]');
var $mobileTel = $form.find('[name="mobile-tel"]');
var $email = $form.find('[name="email"]');
// 優先連絡先のチェックボックス
var $accessHomeTel = $form.find('[name="access-home-tel"]');
var $accessMobileTel = $form.find('[name="access-mobile-tel"]');
var $accessEmail = $form.find('[name="access-email"]');
$homeTel.on('change', update);
$mobileTel.on('change', update);
$email.on('change', update);
// 更新処理
function update() {
$accessHomeTel.prop('disabled', $homeTel.val() === '');
$accessMobileTel.prop('disabled', $mobileTel.val() === '');
$accessEmail.prop('disabled', $email.val() === '');
}
// disabledに初期化
update();
});
スコープ地獄
症状
明確なスコープがわからなくなってしまい、影響範囲がよくわからなくなってしまう。
原因
明確なスコープを切っていないことが原因。
小さいソースコードから中規模のソースコードに育っていくとほぼ必ず通る道。
対策
処理の責任範囲を明確に区切る。コンポーネントクラスの考え方を持ち込むとすんなりいく。
- 変数・関数の責任範囲が明確になる
- すべての処理にメソッド名がつくので処理が明確になる
Before
$(function() {
// フォーム要素
var $form = $('#registration-form');
// 連絡方法の入力欄
var $homeTel = $form.find('[name="home-tel"]');
var $mobileTel = $form.find('[name="mobile-tel"]');
var $email = $form.find('[name="email"]');
// 優先連絡先のチェックボックス
var $accessHomeTel = $form.find('[name="access-home-tel"]');
var $accessMobileTel = $form.find('[name="access-mobile-tel"]');
var $accessEmail = $form.find('[name="access-email"]');
$homeTel.on('change', update);
$mobileTel.on('change', update);
$email.on('change', update);
// 更新処理
function update() {
$accessHomeTel.prop('disabled', $homeTel.val() === '');
$accessMobileTel.prop('disabled', $mobileTel.val() === '');
$accessEmail.prop('disabled', $email.val() === '');
}
// disabledに初期化
update();
});
After
$(function() {
// コンストラクタ
function RegistrationForm() {
this.buildElementReference();
this.attachEvents();
}
Object.assign(RegistrationForm.prototype, {
// フォームの要素をプロパティ化する
buildElementReference: function() {
// フォーム要素
var $form = $('#registration-form');
this.$form = $form;
// 連絡方法の入力欄
this.$homeTel = $form.find('[name="home-tel"]');
this.$mobileTel = $form.find('[name="mobile-tel"]');
this.$email = $form.find('[name="email"]');
// 優先連絡先のチェックボックス
this.$accessHomeTel = $form.find('[name="access-home-tel"]');
this.$accessMobileTel = $form.find('[name="access-mobile-tel"]');
this.$accessEmail = $form.find('[name="access-email"]');
},
// イベントの割当
attachEvents: function() {
this.$homeTel.on('change', this.update.bind(this));
this.$mobileTel.on('change', this.update.bind(this));
this.$email.on('change', this.update.bind(this));
},
// 更新処理
update: function() {
this.$accessHomeTel.prop('disabled', this.$homeTel.val() === '');
this.$accessMobileTel.prop('disabled', this.$mobileTel.val() === '');
this.$accessEmail.prop('disabled', this.$email.val() === '');
},
});
var registrationForm = new RegistrationForm();
registrationForm.update();
});
タイミング地獄
症状
イベントのコールバックなどを各所に記載することで、処理が期待した順番で実行されなくなってしまい、setTimeoutを乱用するようになる
原因
JavaScriptのイベントの実行順序は正しく決まっていない。そのため、イベント処理が散らばっていると実行順序を制御できなくなる。
対策
DOMの更新処理は一箇所にまとめる。
ソースコード
http://jsbin.com/pifuponote/1/edit?html,css,js
Before。全角で入力したときに正しく計算されないことがある
$(function() {
var $numericOnly = $('.numeric-only');
var $sum = $('.sum');
// 合計する
$numericOnly.on('change', function(e) {
var sum = $numericOnly.toArray().reduce(function(prevVal, curVal) {
return prevVal + (parseInt(curVal.value, 10) || 0);
}, 0);
$sum.val(sum);
});
// 全角を半角に変換する
$numericOnly.on('change', function(e) {
var str = $(e.target).val();
var hankakuStr = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) - 65248);
});
$(e.target).val(hankakuStr);
});
});
After。setTimeoutを利用して無理やり回避したもの
$(function() {
var $numericOnly = $('.numeric-only');
var $sum = $('.sum');
// 合計する
$numericOnly.on('change', function(e) {
setTimeout(function() {
var sum = $numericOnly.toArray().reduce(function(prevVal, curVal) {
return prevVal + (parseInt(curVal.value, 10) || 0);
}, 0);
$sum.val(sum);
}, 0);
});
// 全角を半角に変換する
$numericOnly.on('change', function(e) {
var str = $(e.target).val();
var hankakuStr = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) - 65248);
});
$(e.target).val(hankakuStr);
});
});
After。正しく修正したもの。
$(function() {
var $numericOnly = $('.numeric-only');
var $sum = $('.sum');
function update(e) {
var str = $(e.target).val();
var hankakuStr = str.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) - 65248);
});
$(e.target).val(hankakuStr);
var sum = $numericOnly.toArray().reduce(function(prevVal, curVal) {
return prevVal + (parseInt(curVal.value, 10) || 0);
}, 0);
$sum.val(sum);
}
// numericOnlyを更新する処理
$numericOnly.on('change', update);
});
まとめ
- 使用する要素は予め変数に入れておく
- DOMを更新する処理は一箇所にまとめる
- コンポーネントクラスを作ってスコープを管理する
- イベントの実行順は制御できないことを考慮する