1. クロージャ
定義
- クロージャとは、関数が作成された時点のレキシカルスコープを記憶し、その関数が元のスコープ外で実行されたとしてもそのスコープにアクセスできる特性を指します。
クロージャの作成
- クロージャを作成する最も一般的な方法は、ある関数内で別の関数を定義し、その内部関数を通じて外側の関数のローカル変数にアクセスすることです。
利点と欠点
- 利点:
- 変数をメモリに長時間保持できる。
- オブジェクトのプライベートプロパティやプライベートメソッドをカプセル化し、グローバル変数の汚染を防ぐことができる。
- 欠点:
- メモリ消費が増える。
- 不適切に使用するとメモリリークを引き起こす可能性がある。
一般的な用途
- クロージャの主な用途の一つは、関数の外部から関数内部の変数にアクセスできるようにすることです。外部からクロージャ関数を呼び出すことで、関数内部の変数にアクセスしたり、プライベート変数を実現したりできます。
- もう一つの用途は、実行が終了した関数の実行コンテキスト内の変数オブジェクトをメモリに保持することです。これは、クロージャ関数がその変数オブジェクトへの参照を保持しているため、ガベージコレクションの対象とならないためです。
クロージャの実用例
1. よく使われるデバウンスやスロットリング
// デバウンス
function createDebounce(callbackFn, waitMs = 300) {
let timeoutId; // クロージャによって参照される外部変数
return function (...args) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
callbackFn.apply(this, args);
}, waitMs);
};
}
2. イベントハンドラにおけるクロージャ
document.addEventListener('click', function() {
console.log('ドキュメントがクリックされました!');
});
3. 非同期プログラミングにおけるコールバック
function retrieveData(resourcePath, onComplete) {
fetch(resourcePath)
.then((response) => response.json())
.then((data) => onComplete(data))
.catch((error) => console.error("エラー:", error));
}
retrieveData('https://api.example.com/data', (data) => {
console.log(data); // クロージャを使用して外部変数にアクセス
});
4. カリー化 (Currying)
function sumMaker(firstNum) {
return function(secondNum) {
return firstNum + secondNum;
};
}
const addFive = sumMaker(5);
console.log(addFive(3)); // 8
5. データカプセル化とオブジェクトプロパティのプライベート化
function privateDataHolder() {
let secretValue = "これは秘密の値です";
return {
retrieveValue: function() {
return secretValue;
},
updateValue: function(newValue) {
secretValue = newValue;
}
};
}
const myDataObj = privateDataHolder();
console.log(myDataObj.retrieveValue()); // プライベートプロパティにアクセス
myDataObj.updateValue("新しい秘密");
console.log(myDataObj.retrieveValue()); // 更新された値
頻出する質問ポイント
ループ内の非同期処理の課題と解決策
for(var i = 1; i <= 5; i ++){
setTimeout(function() {
console.log(i);
}, 0);
}
上記のコードを実行すると、コンソールには「6」が5回出力されます。面接では「なぜすべて6になるのか?」「1, 2, 3, 4, 5と出力させるにはどうすればよいか?」と問われることがよくあります。
この質問には、以下の点を中心に回答できます。
setTimeoutはマクロタスクであり、JavaScriptのシングルスレッドなイベントループメカニズムにより、同期タスクがすべて完了した後にsetTimeout内のコールバックが順番に実行されます。setTimeout関数もクロージャの一種であり、その親スコープチェーンはグローバルオブジェクト(window)です。ループが終了する時点で変数iはすでに6になっているため、最終的にすべてのコールバックで6が出力されます。
解決策1: 即時実行関数式 (IIFE) の利用
for(var i = 1; i <= 5; i++){
(function(currentVal){
setTimeout(function logTimer(){
console.log(currentVal);
}, 0);
})(i);
}
解決策2: ES6のletの使用
for(let i = 1; i <= 5; i++){
setTimeout(function() {
console.log(i);
}, 0);
}
解決策3: setTimeoutの第三引数を渡す
for(var i = 1; i <= 5; i++){
setTimeout(function(val) {
console.log(val);
}, 0, i); // 第3引数としてループ変数を渡す
}
2. スコープとスコープチェーン
スコープ
スコープとは、変数が定義され、アクセスできる領域のことです。ブラウザエンジンは、この規則に従って、現在のスコープおよびネストされたスコープ内で変数(識別子)を検索します。
種類
- グローバルスコープ
- 関数スコープ
- ブロックスコープ: ES6の
letやconstによって生成されます。ES6で導入されたブロックスコープにより、letキーワードで宣言された変数は、そのブロック内でのみアクセス可能です。また、「一時的なデッドゾーン(Temporal Dead Zone)」という特性を持ち、変数宣言前に使用することはできません。
スコープチェーン
スコープチェーンは、実行環境がアクセスできるすべての変数と関数の順序付けられたアクセスを保証する役割を持ちます。スコープチェーンを通じて、外側の環境の変数や関数にアクセスすることができます。
本質
スコープチェーンの本質は、変数オブジェクトへのポインタのリストです。変数オブジェクトには、実行環境内のすべての変数と関数が含まれます。スコープチェーンの先端は常に現在の実行コンテキストの変数オブジェクトであり、グローバル実行コンテキストの変数オブジェクト(つまりグローバルオブジェクト)は常にスコープチェーンの最後のオブジェクトです。
変数を検索する際、現在の実行環境で見つからない場合、スコープチェーンを遡って検索します。
3. JavaScriptのプロトタイプ、プロトタイプチェーン
プロトタイプ
JavaScriptのすべてのオブジェクトは、[[Prototype]]という内部プロパティを持っており、これがそのオブジェクトのプロトタイプに対応します。JavaScriptの関数オブジェクト(アロー関数を除く)は、[[Prototype]]の他にprototypeプロパティも持ちます。関数オブジェクトがコンストラクタとしてインスタンスを作成する際、このprototypeプロパティの値がインスタンスオブジェクトの[[Prototype]]となります。
プロトタイプオブジェクトは、デフォルトでconstructorプロパティを持ち、その値は対応するコンストラクタ関数です。また、[[Prototype]]プロパティはObject.prototypeを指します。
関連するプロパティ
prototype(明示的なプロトタイプ)- JavaScriptでは、すべてのコンストラクタ関数が
prototypeプロパティを持ち、それがオブジェクトを指します。newキーワードを使ってオブジェクト(コンストラクタ関数)を作成すると、その新しいオブジェクトの内部にある[[Prototype]]プロパティは、そのコンストラクタのprototypeオブジェクトを指します(Object.getPrototypeOf()メソッドでアクセス可能)。このオブジェクトは、プロトタイプ上のすべてのプロパティとメソッドを継承できます。 - アロー関数には
prototypeプロパティがありません。
- JavaScriptでは、すべてのコンストラクタ関数が
[[Prototype]](暗黙的なプロトタイプ)- すべてのオブジェクトは
[[Prototype]]プロパティを持ちます。コンストラクタ関数からオブジェクトをインスタンス化(new)すると、新しいオブジェクトの[[Prototype]]プロパティはコンストラクタのprototypeを指します。 - 関数の
[[Prototype]]はすべてFunction.prototypeを指します(すべての関数がnew Functionによって作成されたことを示唆しています)。 - コンストラクタが生成したオブジェクトの
[[Prototype]]は、そのコンストラクタのprototypeを指します。 - コンストラクタではない関数が生成したオブジェクトの
[[Prototype]]はObject.prototypeを指します(let a = {...}のようなオブジェクトは、new Objectによって作成されたとみなされます)。 Object.prototypeの[[Prototype]]はnullです。
- すべてのオブジェクトは
constructor- すべてのプロトタイプオブジェクトは自動的に
constructor(コンストラクタ関数)プロパティを取得します。このプロパティ(ポインタ)は、prototypeプロパティを持つ関数(例:Person)を指します。
- すべてのプロトタイプオブジェクトは自動的に
プロトタイプチェーン
プロトタイプチェーンは、プロパティやメソッドの継承を可能にする一連のプロトタイプオブジェクトのリンクです。あるオブジェクトのプロパティやメソッドにアクセスしようとしたとき、そのオブジェクト自体にそのプロパティやメソッドがない場合、JavaScriptインタープリタはプロトタイプチェーンを辿って検索し、プロパティやメソッドが見つかるか、プロトタイプチェーンの終端(通常は
Object.prototype、その終端はnull)に到達するまで検索を続けます。
役割
プロトタイプチェーンを通じて、オブジェクトは別のオブジェクトのプロパティやメソッドを継承できます。これはJavaScriptが継承を実現する主要な方法です。
関連関数
hasOwnProperty- オブジェクト自身が特定のプロパティを持っているかを確認するために使用でき、ブール値を返します。プロパティが存在しない場合、オブジェクトのプロトタイプチェーンを遡って検索することはありません。
hasOwnPropertyは、JavaScriptでプロトタイプチェーンを検索せずにプロパティを扱う唯一の関数です。
const myObject = { propA: 1 }; myObject.propB = 2; console.log(myObject.hasOwnProperty('propA')); // true console.log(myObject.hasOwnProperty('propB')); // true console.log(myObject.hasOwnProperty('toString')); // false, Object.prototypeから継承- オブジェクト自身が特定のプロパティを持っているかを確認するために使用でき、ブール値を返します。プロパティが存在しない場合、オブジェクトのプロトタイプチェーンを遡って検索することはありません。
Object.getOwnPropertyNames- オブジェクトのすべての自身のプロパティ(列挙可能かどうかに関わらず)の名前を取得できます。返り値は、オブジェクト自身のプロパティ名で構成される配列です。同様に、オブジェクトのプロトタイプチェーンを遡って検索することはありません。
const dataObject = { key1: 1, key2: 2 }; Object.defineProperty(dataObject, 'key3', { enumerable: false, value: 3 }); console.log(Object.getOwnPropertyNames(dataObject)); // ['key1', 'key2', 'key3']isPrototypeOf- あるオブジェクトが別のオブジェクトのプロトタイプであるかをチェックするメソッドです。
const baseProto = { value: 1 }; const instanceObj = Object.create(baseProto); console.log(baseProto.isPrototypeOf(instanceObj)); // trueObject.getPrototypeOf- オブジェクト
objのプロトタイプを返します。
const simpleObj = {}; console.log(Object.getPrototypeOf(simpleObj)); // 出力:{}- オブジェクト
なぜプロトタイプとプロトタイプチェーンが導入されたのか?
JavaScriptのプロトタイプは、オブジェクト間の関連性を実現し、コンストラクタ関数がデータを共有できない問題を解決するために導入された属性です。プロトタイプチェーンは、オブジェクト間の関連性、つまり継承を実現する主要な方法です。
プロトタイプチェーンの評価
以下の評価結果を記述してください。
Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;
解答
Object.prototype.__proto__; // null
Function.prototype.__proto__; // Object.prototype
Object.__proto__; // Function.prototype
Object instanceof Function; // true
Function instanceof Object; // true
Function.prototype === Function.__proto__; // true
4. イベント委譲 (Event Delegation)
イベント委譲(Event Delegation)は、イベントバブリングの特性を利用して、親要素で子要素のイベントを一元的に処理するテクニックです。
利点と欠点
- 利点:
メモリ消費の削減、パフォーマンスの向上: イベントハンドラの数を減らし、すべての子要素にイベントハンドラをバインドするコストを削減します。動的なコンテンツ処理: 動的に追加された子要素に対しても、イベントハンドラを再バインドする必要なく、イベント委譲が機能します。コードの簡素化: コードがより簡潔になります。
- 欠点:
イベントタイプの制限: イベント委譲はバブリングするイベントにのみ適用され、バブリングしないイベント(例:focusやblur)には適用できません。イベントターゲットの識別: イベントハンドラ内でevent.targetをチェックして、どの特定の子要素がイベントをトリガーしたかを判断する必要があり、これによりロジック処理が少し複雑になる可能性があります。イベント委譲チェーンの中断: イベントバブリングパス上のどこかの要素でイベントバブリングが停止された場合(event.stopPropagation()の呼び出しなど)、イベント委譲は機能しなくなります。
<ul id="itemList">
<li>リストアイテム 1</li>
<li>リストアイテム 2</li>
<li>リストアイテム 3</li>
</ul>
<script>
// イベント委譲を使用してクリックイベントをバインド
var itemList = document.getElementById('itemList');
itemList.addEventListener('click', function(event) {
// クリックされた要素が 'LI' タグであるか確認
if (event.target.matches('li')) {
console.log('クリックされたアイテム:', event.target.textContent);
}
});
</script>
5. JavaScriptでの継承
1. プロトタイプチェーン継承
プロトタイプチェーン継承では、子コンストラクタ関数のプロトタイプオブジェクトを親コンストラクタ関数のインスタンスに設定することで継承を実現します。
function BaseClass() {
this.objectName = 'Base';
}
BaseClass.prototype.greet = function() {
console.log('こんにちは');
};
function SubClass() {
this.objectName = 'Sub';
}
SubClass.prototype = new BaseClass();
const childInstance = new SubClass();
childInstance.greet(); // こんにちは
利点:子オブジェクトは親オブジェクトのプロトタイプチェーン上のプロパティとメソッドにアクセスできます。
欠点:すべての子オブジェクトが同じプロトタイプオブジェクトを共有するため、プロトタイプオブジェクトへの変更がすべての子オブジェクトに影響します。
2. コンストラクタ関数継承
コンストラクタ関数継承は、子コンストラクタ関数内で親コンストラクタ関数を呼び出すことで継承を実現します。この方法では、子コンストラクタ内でcall()またはapply()メソッドを使用し、親コンストラクタのコンテキストを子オブジェクトのコンテキストに設定することで継承します。
function BaseClass(className) {
this.className = className;
}
function SubClass(className) {
BaseClass.call(this, className); // 親コンストラクタを呼び出し
}
const childInstance = new SubClass('子クラス');
console.log(childInstance.className); // 子クラス
欠点:コンストラクタ関数継承は、親コンストラクタ関数のインスタンスプロパティのみを継承でき、親コンストラクタ関数のプロトタイプオブジェクト上のメソッドは継承できません。
3. 組み合わせ継承(プロトタイプチェーン継承 + コンストラクタ関数継承)
組み合わせ継承は、プロトタイプチェーン継承とコンストラクタ関数継承を組み合わせたもので、親コンストラクタ関数のプロパティとプロトタイプオブジェクト上のメソッドの両方を継承します。この方法では、親コンストラクタ関数を呼び出すことでプロパティを継承し、子コンストラクタ関数のプロトタイプオブジェクトを親コンストラクタ関数のインスタンスに設定することでメソッドを継承します。
function BaseEntity(entityName) {
this.entityName = entityName;
}
BaseEntity.prototype.displayInfo = function() {
console.log('Entity Name:', this.entityName);
};
function DerivedEntity(entityName, entityId) {
BaseEntity.call(this, entityName); // プロパティを継承
this.entityId = entityId;
}
DerivedEntity.prototype = new BaseEntity(); // メソッドを継承
const myEntity = new DerivedEntity('アイテム', 'ABC-123');
myEntity.displayInfo(); // Entity Name: アイテム
console.log(myEntity.entityId); // ABC-123
利点:親コンストラクタ関数のプロパティとプロトタイプオブジェクト上のメソッドの両方を継承できます。
欠点:子オブジェクトを作成する際に親コンストラクタ関数が2回呼び出されます。1回はプロトタイプを設定する際、もう1回は子オブジェクトを作成する際です。これにより、いくつかの不要なオーバーヘッドが発生します。
4. プロトタイプ継承
プロトタイプ継承は、Object.create()メソッドを使用して継承を実現します。
const baseObject = {
kind: 'Generic',
getInfo: function() {
console.log('This is a', this.kind);
}
};
const derivedObject = Object.create(baseObject);
derivedObject.kind = 'Specific'; // プロパティを上書き
console.log(derivedObject.kind); // Specific
derivedObject.getInfo(); // This is a Specific
プロトタイプ継承の本質は、新しいオブジェクトを作成し、そのプロトタイプオブジェクトを既存の別のオブジェクトを指すようにすることです。
欠点:この方法ではプロパティとメソッドの継承は実現できますが、コンストラクタ関数の引数を渡すことはできません。
5. 寄生型継承 (Parasitic Inheritance)
寄生型継承は、プロトタイプ継承をベースに、さらに追加の操作を行うものです。通常、新しいオブジェクトに追加のプロパティやメソッドを追加し、その新しいオブジェクトを返します。この方法はファクトリパターンに似ており、関数を通じてオブジェクトを作成し、その機能を拡張します。
function enhanceObject(sourceObject) {
// Object.create() を通じて新しいオブジェクトを作成し、そのプロトタイプは sourceObject を指す
const enhancedClone = Object.create(sourceObject);
// この新しいオブジェクトを強化
enhancedClone.displayEnhancedGreeting = function() {
console.log('強化されたオブジェクトからの挨拶です。');
};
// 強化されたオブジェクトを返す
return enhancedClone;
}
const originalData = {
dataType: 'Original',
displayInfo: function() {
console.log('オリジナルオブジェクトからの情報です。');
}
};
// 寄生型継承を使って新しいオブジェクトを作成
const clonedAndEnhanced = enhanceObject(originalData);
clonedAndEnhanced.displayEnhancedGreeting(); // 出力 "強化されたオブジェクトからの挨拶です。"
clonedAndEnhanced.displayInfo(); // 出力 "オリジナルオブジェクトからの情報です。"
利点:記述が簡単で、個別にコンストラクタ関数を作成する必要がありません。
欠点:寄生型継承でオブジェクトに関数を追加すると、関数の再利用が困難になります。この点はコンストラクタパターンに似ており、効率が低下します。
6. 寄生コンビネーション継承(プロトタイプ継承 + 寄生型継承)
組み合わせ継承は古典的な継承パターンとしてよく使用されますが、最大の課題は、常に親クラスのコンストラクタ関数が2回呼び出されることです(1回は子タイプの作成時、もう1回は子タイプのコンストラクタ内部)。寄生コンビネーション継承は、親クラスコンストラクタのオーバーヘッドを削減するために実装されました。
これは、コンストラクタを借用して親クラスのプロパティを継承し、プロトタイプチェーンのハイブリッド形式を通じてプロトタイプオブジェクト上のメソッドを継承します。本質的には、寄生型継承を使用してスーパタイプのプロトタイプを継承し、その結果をサブタイプのプロトタイプに割り当てます。
function setupInheritance(DerivedType, SuperTypeBase) {
const prototypeCopy = Object.create(SuperTypeBase.prototype); // 親クラスのプロトタイプコピーを作成
prototypeCopy.constructor = DerivedType; // コピーのコンストラクタを子クラスに設定
DerivedType.prototype = prototypeCopy; // 子クラスのプロトタイプを修正されたコピーに設定
}
// 親クラス SuperTypeBase を定義
function SuperTypeBase(name) {
this.name = name;
}
SuperTypeBase.prototype.logName = function() {
console.log(this.name);
};
// 子クラス DerivedType を定義
function DerivedType(name, age) {
// 親クラスのインスタンスプロパティを継承
SuperTypeBase.call(this, name);
this.age = age;
}
// 親クラスのメソッドと共有プロパティを継承
setupInheritance(DerivedType, SuperTypeBase);
DerivedType.prototype.logAge = function() {
console.log(this.age);
};
const myInstance = new DerivedType('アリス', 25);
myInstance.logName(); // 出力 "アリス"
myInstance.logAge(); // 出力 25
利点:親コンストラクタ関数を1回だけ呼び出す高効率性があり、子プロトタイプ上に不要な属性が作成されるのを防ぎます。同時に、プロトタイプチェーンは変更されません。
欠点:コードが複雑になります。
7. ES6クラス継承
ES6ではクラスの概念が導入され、classキーワードとextendsキーワードを使用してクラスの継承を実現できます。
class BaseEntity {
constructor(entityName) {
this.entityName = entityName;
}
speak() {
console.log('Hello');
}
}
class DerivedEntity extends BaseEntity {
constructor(entityName) {
super(entityName); // 親クラスのコンストラクタを呼び出す
}
}
const childObject = new DerivedEntity('子エンティティ');
console.log(childObject.entityName); // 子エンティティ
childObject.speak(); // Hello
6. thisの理解
JavaScriptのthisオブジェクトは、現在のコードが実行されているコンテキストを表します。thisの値は、関数がどのように呼び出されたかによって決まり、関数がどこで宣言されたかには依存しません。
以下に、thisの値が決定されるいくつかの一般的な方法を示します。
1. 通常関数の呼び出しでは、thisはグローバルオブジェクト(Window)を指します。
var globalName = 'グローバル';
function testFunc(){
var localName = 'ローカル';
console.log(this.globalName); // グローバル
console.log(this); // Windowオブジェクト
}
testFunc();
2. オブジェクトのメソッドの場合、thisはそのオブジェクトを指します。
単一階層のスコープチェーンの場合、thisはそのオブジェクトを指します。
var globalName = 'グローバル';
var myObject = {
globalName:'オブジェクト名',
describe:function(){
console.log(this.globalName); // オブジェクト名
}
}
myObject.describe();
多階層のスコープチェーンの場合、thisはメソッドに最も近いオブジェクトを指します。
var globalName = 'グローバル';
var outerObject = {
globalName:'外側オブジェクト',
innerObject:{
globalName:'内側オブジェクト',
executeMethod:function(){
console.log(this.globalName); // 内側オブジェクト
}
}
}
outerObject.innerObject.executeMethod();
もしouterObject.innerObject.executeMethodの結果が変数methodRefに代入された場合、methodRef()の値は「グローバル」になります。これは、代入操作時には関数呼び出しが行われず、methodRef()が真の呼び出しであり、この呼び出しの発生元がルートオブジェクトであるwindowであるため、thisはwindowを指し、this.globalNameは「グローバル」となるためです。
var methodRef = outerObject.innerObject.executeMethod;
methodRef(); // グローバル
3. コンストラクタ関数の呼び出しでは、thisは新しくインスタンス化されたオブジェクトを指します。
var globalValue = 'グローバル値';
function Animal(){
this.petName = 'タマ';
this.species = '猫';
}
var myAnimal = new Animal();
console.log(myAnimal); // 新しくインスタンス化されたオブジェクト Animal {petName: "タマ", species: "猫"}
console.log(myAnimal.petName); // タマ
4. applyとcallの呼び出しでは、thisは引数として渡されたオブジェクトを指します。
var valueName = 'デフォルト';
function logName(){
console.log(this.valueName);
}
var objCat = {
valueName:'ミケ',
}
var objDog = {
valueName:'ポチ',
}
logName.call(objCat); // ミケ
logName.call(objDog); // ポチ
5. 無名関数の呼び出しでは、グローバルオブジェクトを指します。
var globalVar = 'グローバル変数';
var containerObj = {
propName:'コンテナ値',
anonymousExec:(function(){
console.log(this.globalVar); // グローバル変数
})()
}
// containerObj.anonymousExec の評価結果は undefined (console.logの結果が返されるため)
6. タイマー内での呼び出しでは、グローバル変数を指します。
var globalVar = 'グローバル変数';
var timerId = setInterval(function(){
var localVal = 'ローカル値';
console.log(this.globalVar); // グローバル変数
clearInterval(timerId);
},500);
7. アロー関数の呼び出し
アロー関数は自身のthisコンテキストを持ちません。アロー関数は、その宣言されたコンテキストのthis値をキャプチャします。つまり、アロー関数を使用する場合、thisの値は、そのアロー関数が宣言されたスコープ内のthis値と同じになります。
var windowScopeName = 'window';
var userProfile = {
name: '田中',
performAction: function(){
// var self = this;
var arrowAction = () => {
// console.log(self.name);
console.log(this.name); // '田中' (performActionのthisを継承)
}
arrowAction();
},
anotherArrowAction: () => {
console.log(this.name); // 'window' (anotherArrowActionが定義されたグローバルスコープのthis)
}
}
userProfile.performAction(); // '田中'
userProfile.anotherArrowAction(); // 'window'
let tempFunc = userProfile.performAction;
tempFunc(); // 'window' (tempFuncはグローバルコンテキストで呼び出されるため)
7. イベントループ (Event Loop)
イベントループはJavaScriptの実行メカニズムであり、JavaScriptコードの実行順序を管理およびスケジュールし、非同期コードの正確な実行を保証します。JavaScriptにおいて、イベントループはシングルスレッドの実行モデルに基づいています。これは、すべてのJavaScriptコードが同じスレッドで実行されることを意味するため、アプリケーション全体をブロックしないように非同期コードをイベントループを通じて処理する必要があります。
イベントループの主要な構成要素
1. 実行スタック(Call Stack):
- JavaScriptコードが実行される際、すべての同期タスクは実行スタックで実行されます。
- 関数が実行されると、スタックの最上部に追加され、実行が完了するとスタックから削除されます。
2. イベントキュー(Event Queue):
- 非同期タスクが完了すると、コールバック関数はイベントキューに入れられ、実行を待ちます。
- イベントキューはFIFO(先入れ先出し)の原則に従います。
3. イベントループ(Event Loop):
- 実行スタックとタスクキューの状態を循環的にチェックし、次に実行するコードを決定する役割を担います。
- イベントループは実行スタックが空であるかをチェックし、空であればイベントキューから最初のタスクを取り出して実行スタックに入れ、実行します。
4. マクロタスクキュー(Macro Task Queue)/ミクロタスクキュー(Micro Task Queue):
- マクロタスクキューには、スクリプト全体コード、
setTimeout、setInterval、setImmediate、I/O処理、UIレンダリング、ユーザーインタラクションイベント(例:マウスクリック、スクロール、ズーム)、postMessage、MessageChannelなどが含まれます。 - ミクロタスクキューには、
Promiseコールバック、MutationObserver、async/await、process.nextTick、Object.observeなどが含まれます。 - 実行スタックが空になると、イベントループはまずミクロタスクキュー内のすべてのタスクを処理し、その後、次のマクロタスクを処理します。
JavaScriptエンジンとGUIレンダリングスレッドは排他的な関係にあるため、ブラウザはマクロタスクとDOMタスクを秩序立てて実行するために、1つのマクロタスクが実行された後、レンダリングの前に、その実行中に生成されたすべてのミクロタスクを実行し、その後GUIレンダリングスレッドがページのレンダリングを開始します。
マクロタスク -> ミクロタスク -> GUIレンダリング -> 次のマクロタスク -> ...
イベントループの動作フロー
- まず、JavaScriptはシングルスレッドで動作し、コード実行時には異なる関数の実行コンテキストが実行スタックにプッシュされ、コードの順序付けられた実行が保証されます。
- 同期コードの実行中に非同期イベントに遭遇した場合、JavaScriptエンジンは結果が返ってくるまで待つのではなく、そのイベントを保留状態にし、実行スタック内の他のタスクの実行を続けます。
- 同期イベントがすべて実行された後、非同期イベントに対応するコールバックが、現在の実行スタックとは異なる別のタスクキューに追加され、実行を待ちます。
- タスクキューはマクロタスクキューとミクロタスクキューに分けられます。現在の実行スタック内のイベントが完了した後、JavaScriptエンジンはまずミクロタスクキューに実行可能なタスクがあるかを確認します。あれば、ミクロタスクキューの先頭のイベントをスタックにプッシュして実行します。
- ミクロタスクキュー内のタスクがすべて実行された後、マクロタスクキュー内のタスクを評価します。
asyncとawait
asyncは関数定義時に使用され、asyncで定義された関数はデフォルトでPromiseインスタンスを返します(直接.thenを呼び出すことができます。また、asyncはオブジェクトのメソッドを定義することもできます)。もしasyncで定義された関数の実行結果がPromiseオブジェクトでない場合、その戻り値はPromiseオブジェクトにラップされます(戻り値はPromiseインスタンスのresolveメソッドの引数として渡されます)。
JavaScriptはawaitの左辺およびそれ以降のコードを、まとめて一つのミクロタスクとして待機タスクキューに入れます。
使用方法
- 1.
awaitの右辺が同期コードの場合、同期コードを実行します。もし関数を実行する場合、関数の戻り値がawaitの左辺の変数に割り当てられます。
let resultVar;
async function testAsyncFunc() {
resultVar = await 18; // awaitが同期値を待つ
console.log(resultVar); // 18
}
testAsyncFunc();
console.log(1); // 1
console.log(resultVar); // undefined (awaitの処理がミクロタスクキューに送られるため)
// 結果は 1, undefined, 18
JavaScriptは右から左に実行されます。まず18が評価され、awaitに遭遇すると、awaitの左辺およびそれ以降のコード全体がミクロタスクキューに送られます。そのため、外側のresultVarの出力はundefinedになり、同期コードがすべて実行された後、ミクロタスクが実行され、resultVarに値が代入され、出力されます。
- もし
awaitの右辺がPromiseインスタンス、またはPromiseインスタンスを返すメソッドの場合、awaitはPromiseインスタンスが解決されるのを待ちます。Promiseインスタンスが解決されるまで、await以降のコードは実行されません。また、Promiseが解決時に渡す値を受け取り、等号の左辺の変数に代入します。
async function promiseCreator() {
return 10;
}
async function asyncExecuter() {
let p = await promiseCreator(); // promiseCreatorはPromise.resolve(10)を返す
console.log(p); // 10
}
asyncExecuter();
console.log(1); // 1
// 出力は 1, 10
// 上記で述べたように、asyncで定義された関数がPromiseオブジェクトではなく通常の値を返す場合、
// デフォルトでPromiseオブジェクトにラップされ、その戻り値がawaitの左辺の変数に代入されます。
// もしasyncを使わずにPromiseオブジェクトを直接書く場合は以下のようになります。
function promiseFunc() {
return new Promise(((resolve, reject) => {
resolve(10);
}))
}
async function awaitPromiseExecuter() {
let p = await promiseFunc();
console.log(p);
}
awaitPromiseExecuter();
console.log(1);
setTimeout(function () {
console.log("1");
}, 0);
async function async1() {
console.log("2");
const data = await async2();
console.log("3");
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log("4");
resolve("async2の結果");
}).then((data) => {
console.log("5");
return data;
});
}
async1().then((data) => {
console.log("6");
console.log(data);
});
new Promise(function (resolve) {
console.log("7");
// resolve() // コメントアウトされているため、thenのコールバックは実行されない
}).then(function () {
console.log("8");
});
// 出力結果:2 4 7 5 3 6 async2の結果 1
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end') // awaitが解決されてから実行されるミクロタスク
}
async function async2() {
console.log('async2')
}
console.log('script start') // 同期処理
setTimeout(function () {
console.log('setTimeout') // マクロタスク
}, 0)
async1(); // async1実行
new Promise(function (resolve) {
console.log('promise1') // 同期処理
resolve();
}).then(function () {
console.log('promise2') // ミクロタスク
})
console.log('script end') // 同期処理
// 出力結果:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end (async2が解決し、async1内の残りの部分がミクロタスクとして実行)
// promise2 (Promiseのthenコールバックがミクロタスクとして実行)
// setTimeout (マクロタスクが最後に実行)
JavaScriptはシングルスレッドなのに、どうやって非同期処理を実現しているのか?
JavaScriptはシングルスレッドなので、一つのスレッドでしか実行できません。では、どのように非同期処理を実現しているのでしょうか?それはイベントループ(event loop)を通じてです。イベントループのメカニズムを理解すれば、JavaScriptの実行メカニズムを理解できます。
ブラウザのマルチスレッド
GUIレンダリングスレッド (ブラウザのHTML要素のレンダリングを担当)
JavaScriptエンジンメインスレッド (JavaScriptスクリプトプログラムの処理を担当。例: V8エンジン。GUIレンダリングスレッドとJavaScriptエンジンメインスレッドは排他的!)
イベントトリガースレッド(例:マウスクリック、AJAX非同期リクエストなど)
タイマーイベントスレッド(
setTimeout,setInterval)非同期HTTPリクエストスレッド
イベントポーリング処理スレッド (役割: メッセージキューをポーリングし、イベントループを管理)
8. new演算子の働き
new演算子は具体的に以下の処理を行います。
- 新しい空のオブジェクトを作成する:
new演算子はまず、空のオブジェクト{}を作成します。 - プロトタイプを設定する:この新しいオブジェクトのプロトタイプ(内部的な
[[Prototype]]プロパティ)は、コンストラクタ関数のprototypeプロパティが指すオブジェクトに設定されます。これにより、新しいオブジェクトはコンストラクタのprototypeに定義されたすべてのプロパティとメソッドを継承します。 thisを設定する:コンストラクタ関数内部のthisをこの新しいオブジェクトにバインドします。- コンストラクタ関数を実行する:コンストラクタ内部のコードが実行されます。この過程で、
thisキーワードは新しく作成されたオブジェクトを指し、コンストラクタが新しいオブジェクトにプロパティやメソッドを追加することを可能にします。 - 新しいオブジェクトを返す:コンストラクタ関数がオブジェクト型以外の値(つまり
undefinedまたは何も返さない)を返さない場合、new演算子はステップ1で作成された新しいオブジェクトを返します。もしコンストラクタ関数がオブジェクト型の値を返した場合、その返されたオブジェクトがnew演算子の結果として使用され、ステップ1で作成されたオブジェクトは破棄されます。
シンプルなnewメソッドの実装
function customNew(CreatorFunction, ...args) {
// 1. 新しい空のオブジェクトを作成
const instanceObj = {};
// 2. 新しいオブジェクトのプロトタイプをコンストラクタ関数のプロトタイプオブジェクトにリンク
Object.setPrototypeOf(instanceObj, CreatorFunction.prototype);
// 3 & 4. コンストラクタ関数のスコープを新しいオブジェクトに割り当て、実行
const ctorResult = CreatorFunction.apply(instanceObj, args);
// 5. コンストラクタが明示的にオブジェクトを返した場合、そのオブジェクトを返す。そうでなければ新しいオブジェクトを返す
return (typeof ctorResult === 'object' && ctorResult !== null) ? ctorResult : instanceObj;
}
customNewメソッドの使用例
function UserProfile(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
UserProfile.prototype.introduce = function() {
console.log('こんにちは、私の名前は ' + this.userName + ' です。');
};
var myUser = customNew(UserProfile, '山田', 30);
console.log(myUser.userName); // 出力: 山田
console.log(myUser.userAge); // 出力: 30
myUser.introduce(); // 出力: こんにちは、私の名前は 山田 です。
9. Ajaxの原理
Ajaxとは、JavaScriptスクリプトが直接サーバーにHTTP通信を発行し、サーバーから返されたデータに基づいてページ全体をリロードすることなく、ウェブページの一部を更新できる非同期通信方法です。
シンプルなAjaxの実装(素のJavaScript)
// 簡単なAjaxを自作
/** 1. 接続の作成 **/
var httpRequest = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); // IE6以下との互換性
/** 2. サーバーへの接続設定 **/
// 'get'メソッドでURLに非同期で接続
httpRequest.open('get', url, true);
/** 3. リクエストの送信 **/
httpRequest.send(null);
/** 4. レスポンスの受信 **/
httpRequest.onreadystatechange = function(){
// readyStateが4(リクエスト完了)になったら
if(httpRequest.readyState === 4){
// ステータスが200(成功)の場合
if(httpRequest.status === 200){
success(httpRequest.responseText); // 成功コールバックを実行
} else {
// 失敗の場合
fail && fail(httpRequest.status); // 失敗コールバックを実行
}
}
}
Promiseでのカプセル化
// Promise でのカプセル化実装:
function fetchJsonData(endpointUrl) {
// Promise オブジェクトを作成
let dataPromise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// HTTP リクエストを初期化
xhr.open("GET", endpointUrl, true);
// 状態の監視関数を設定
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return; // リクエストが完了していなければ何もしない
// リクエストが成功または失敗した場合、Promise の状態を変更
if (this.status === 200) {
resolve(this.response); // 成功時に解決
} else {
reject(new Error(this.statusText)); // 失敗時に拒否
}
};
// エラー監視関数を設定
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// レスポンスのデータタイプを設定
xhr.responseType = "json";
// リクエストヘッダー情報を設定
xhr.setRequestHeader("Accept", "application/json");
// HTTP リクエストを送信
xhr.send(null);
});
return dataPromise;
}
readyStateプロパティの値は以下の通りです。
- 0: リクエスト未初期化(
open()がまだ呼び出されていない)。 - 1: サーバー接続確立(
open()は呼び出されたが、send()はまだ呼び出されていない)。 - 2: リクエスト受信済み(
send()は呼び出され、応答ヘッダーと応答ステータスが利用可能)。 - 3: リクエスト処理中(応答ボディがダウンロード中で、まだ完全に受信されていない)。
- 4: リクエスト完了、応答準備完了(リクエストプロセス全体が終了し、サーバーの応答にアクセス可能)。
Ajaxの利点と欠点は何か?
利点
1. ユーザーエクスペリエンスの向上:ページの部分的な更新により、ページ全体をリロードする必要がなく、ユーザーはより速い応答速度を感じることができます。
2. サーバー負荷の軽減:サーバーには必要なデータのみをリクエストし、不要なデータ転送を削減することで、サーバーの負荷を軽減します。
3. データ処理の分離:フロントエンドとバックエンドのロジックを分離でき、フロントエンドはユーザーインターフェースとユーザーエクスペリエンスにのみ集中し、バックエンドはデータ処理にのみ集中できます。
4. ページパフォーマンスの向上:非同期リクエストにより、ページ全体の再読み込みを回避できるため、ページパフォーマンスが向上します。
5. メンテナンスの容易さ:フロントエンドとバックエンドの分離により、コードのメンテナンスと更新が容易になります。
欠点
1. ブラウザ互換性:古いバージョンのブラウザはAjaxをサポートしていないか、完全にサポートしていない場合があります。
2. セキュリティ問題:クロスサイトリクエストフォージェリ(CSRF)やクロスサイトスクリプティング(XSS)などのセキュリティ問題を考慮する必要があります。
3. 検索エンジン最適化(SEO)問題:コンテンツが動的にロードされるため、検索エンジン最適化(SEO)に影響を与える可能性があります。
10. クロスオリジン問題の解決策
1. JSONPによるクロスオリジン
JSONP(JSON with Padding)は非公式のプロトコルで、<script>タグがクロスオリジンリソースをリクエストできる特性を利用してクロスオリジンアクセスを実現します。リクエストURLにcallbackパラメータを追加し、サーバーが応答する際にこのパラメータを関数呼び出しとして使用し、データをその関数に引数として渡します。
この方法はGETリクエストのみをサポートし、XSS攻撃を受けやすいためセキュリティは低いです。
<script src="http://example.com/api?callback=dataProcessor"></script>
<script>
function dataProcessor(data) {
console.log(data);
}
</script>
2. CORS(Cross-Origin Resource Sharing)
CORSは、クロスオリジンリクエストを解決するための公式な標準方法です。サーバー側で応答ヘッダーを設定することで、クロスオリジンリクエストを許可するかどうかを制御します。サーバー側でAccess-Control-Allow-Originなどの関連ヘッダー情報を設定し、許可するオリジンドメインを指定します。
const express = require('express');
const app = express();
// すべてのドメインからのアクセスを許可
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*'); // 特定のドメインを指定することも可能: 'https://allowed-domain.com'
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // 許可するHTTPメソッド
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 許可するリクエストヘッダー
next();
});
// ルーティングと処理ロジック
app.get('/api/data', (req, res) => {
res.json({ message: 'CORSが許可されたデータです' });
});
app.listen(3000, () => {
console.log('サーバーがポート3000で稼働中');
});
3. プロキシサーバー
同一オリジンのサーバー上にプロキシサービスを設定し、リクエストをターゲットサーバーに転送します。プロキシサーバーは応答を受信した後、それをフロントエンドアプリケーションに転送し、ブラウザの同一オリジンポリシーを回避します。
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/api/external-data', (req, res) => {
// ターゲットサーバーにリクエストを送信
axios.get('http://api.external-example.com/data')
.then((response) => {
// ターゲットサーバーの応答をフロントエンドに返す
res.json(response.data);
})
.catch((error) => {
console.error('プロキシエラー:', error);
res.status(500).json({ error: 'エラーが発生しました' });
});
});
app.listen(3000, () => {
console.log('プロキシサーバーがポート3000で稼働中');
});
4. PostMessage
PostMessageはHTML5で定義されたクロスウィンドウ通信メカニズムで、異なるオリジンからのウィンドウ間で安全な通信を可能にします。PostMessageを通じて、あるウィンドウが別のウィンドウにメッセージを送信でき、両ウィンドウが同一オリジンであるかどうかに関わらず、クロスオリジン通信を実現します。
- メッセージの送信
targetWindowはターゲットウィンドウへの参照で、iframeのcontentWindowなどです。messageは送信するメッセージで、文字列やオブジェクトなどです。targetOriginはターゲットウィンドウのオリジンで、メッセージ送信のターゲットを制限するために使用されます。通常は具体的なオリジン(例:'https://example.com')を使用するか、'*'で任意のオリジンからのメッセージを受け入れることを示します。
// 例: 親ウィンドウからiframeにメッセージを送信
const targetIframe = document.getElementById('targetFrame');
if (targetIframe && targetIframe.contentWindow) {
const targetWindow = targetIframe.contentWindow;
const sentMessage = '別のドメインからのメッセージ!';
const allowedOrigin = 'https://example.com'; // iframeのオリジン
targetWindow.postMessage(sentMessage, allowedOrigin);
}
- メッセージの受信
window.addEventListener('message', function(event) {
// メッセージが信頼できるオリジンからのものであることを確認
if (event.origin !== 'https://sender-domain.com') {
console.warn('信頼できないオリジンからのメッセージをブロックしました:', event.origin);
return;
}
// event.data に送信されたデータが含まれる
console.log('受信したメッセージ:', event.data);
});
5. Nginxリバースプロキシ
Nginxでリバースプロキシを設定することで、クロスオリジンリクエストを同一オリジンインターフェースに転送し、ブラウザの同一オリジンポリシーの制限を回避します。
6. WebSocket
WebSocketを使用して双方向通信を行います。WebSocketは同一オリジンポリシーの制限を受けず、クロスオリジン通信を実現できます。
const socket = new WebSocket('ws://api.example.com/socket');
socket.onopen = () => {
console.log('WebSocket接続が確立されました。');
// データを送信
socket.send('サーバーへ、こんにちは!');
};
socket.onmessage = (event) => {
console.log('サーバーからメッセージを受信:', event.data);
};
socket.onclose = () => {
console.log('WebSocket接続が閉じられました。');
};
socket.onerror = (error) => {
console.error('WebSocketエラー:', error);
};
7. webpack-dev-serverプロキシ
webpack-dev-serverのプロキシ設定方法:
① Webpackとwebpack-dev-serverをインストール:
npm install --save-dev webpack webpack-dev-server
② webpack.config.jsを構成:
webpack.config.jsファイルで、devServerオブジェクトを構成し、proxyプロパティを設定できます。
module.exports = {
// その他の設定項目...
devServer: {
proxy: {
'/api': {
target: 'http://api.external.service.com', // プロキシのターゲットアドレスを設定
pathRewrite: { '^/api': '' }, // リクエストパスを書き換え、'/api'プレフィックスを削除
changeOrigin: true, // リクエストヘッダーのOriginをターゲットアドレスに変更
},
},
},
};
上記の例では、devServerのプロキシ機能を設定しています。/apiで始まるリクエストが発生した場合、http://api.external.service.comにプロキシされます。
この設定は開発環境のプロキシ用であり、本番環境のコードをビルドする際にはプロキシ設定は有効にならないことに注意してください。
11. モジュール化
JavaScriptには現在、比較的成熟した4つのモジュールロード方式があります。
- 最初の方法はCommonJS方式で、Node.jsで使用されるモジュール化仕様です。
requireメソッドでモジュールをロードし、module.exportsオブジェクトでモジュールをエクスポートします。このモジュールロード方式はサーバーサイドのソリューションであり、同期的にモジュールをインポートします。サーバーサイドではファイルはローカルディスクに保存されているため読み込みが非常に速く、同期ロードでも問題ありません。しかし、ブラウザサイドでは、モジュールのロードはネットワークリクエストを使用するため、非同期ロードの方が適切です。 - 2番目の方法はAMD方式で、非同期的にモジュールをロードします。モジュールのロードは後続のステートメントの実行に影響を与えず、このモジュールに依存するすべてのステートメントはコールバック関数内で定義され、ロードが完了した後にコールバック関数が実行されます。require.jsがAMD仕様を実装しています。
- 3番目の方法はCMD方式で、この方式もAMD方式と同様に非同期モジュールロードの問題を解決するために考案されました。sea.jsがCMD仕様を実装しています。require.jsとの違いは、モジュール定義時の依存関係の処理と、依存モジュールの実行タイミングの処理にあります。
- 4番目の方法はES6で提案された方式で、
importとexportの形式でモジュールをインポートおよびエクスポートします。
CommonJSとES6のモジュール化の違いは何か?
- CommonJSは同期インポート、ES6は非同期インポートです。
- CommonJSはエクスポート時に値のコピーを行います。エクスポートされた値が変更されても、インポートされた値は変更されません。したがって、値を更新したい場合は再インポートする必要があります。
- しかし、ES6はライブバインディング方式を採用しており、インポートされた値とエクスポートされた値は同じメモリ位置を指すため、インポートされた値はエクスポートされた値の変更に追従します。
- ES6モジュールは、実行時に
require/exportsにトランスパイルされることがあります。
AMDとCMDの仕様の違いは何か?
- 最初の違いは、モジュール定義時の依存関係の処理です。AMDは依存関係の事前定義を推奨しており、モジュールを定義する際にその依存モジュールを宣言します。一方、CMDは「必要になったら依存する」アプローチを推奨しており、特定のモジュールが必要になったときに初めて
requireを呼び出します。 - 2番目の違いは、依存モジュールの実行タイミングの処理です。AMDとCMDはどちらも非同期ロード方式ですが、モジュールの実行タイミングが異なります。AMDは依存モジュールがロードされるとすぐに実行し、依存モジュールの実行順序は記述順と一致しない場合があります。一方、CMDは依存モジュールがロードされてもすぐには実行せず、ダウンロードするだけです。すべての依存モジュールがロードされた後、コールバック関数のロジックに入り、
requireステートメントに遭遇したときにのみ対応するモジュールを実行するため、モジュールの実行順序は記述順と一致します。
モジュール化開発の利点
1. カプセル化:モジュール化開発は、関連する機能とデータをカプセル化し、モジュール内部に隠蔽し、必要なインターフェースのみを外部に公開します。このカプセル化により、グローバルスコープの汚染を回避し、名前の衝突を減らし、コードの安全性と保守性を向上させることができます。
2. 再利用性:モジュール化開発により、コードの再利用性が向上します。適切に設計されたモジュールは、異なるプロジェクトで何度も使用できるため、コードの重複記述を減らし、開発効率を向上させます。
3. 疎結合性:モジュール化開発は、モジュール間のインターフェースと依存関係を明確に定義することで、疎結合性を実現します。これは、あるモジュールの実装を変更しても他のモジュールに影響を与えないことを意味し、システムの結合度を下げ、コードの保守性と拡張性を向上させます。
4. 拡張性:システム要件の変化に伴い、モジュール化設計により新しい機能の追加が容易になります。
12. メモリリークを引き起こす操作
JavaScriptにおけるメモリリークを引き起こす一般的な操作は以下の通りです。
1. 意図しないグローバル変数:誤って作成されたグローバル変数は、ページが閉じられるまでメモリに残り続けます。
2. クロージャ:不適切なクロージャの使用により、特定の変数がメモリに残り続けてしまうことがあります。
3. イベントリスナー:要素にイベントリスナーを追加したが、その要素が破棄される前にリスナーを削除しなかった場合、これらのリスナーはメモリを占有し続けます。
4. タイマー:setTimeoutやsetIntervalが適切にクリアされなかった場合、それが参照する外部変数が解放されずにメモリに残り続けます。
5. 未解放のDOM要素への参照:DOMツリーから削除された要素に対してJavaScriptコードがまだ参照を保持している場合、その要素はガベージコレクションされません。
6. 循環参照:2つのオブジェクトが相互に参照し合い、ルートオブジェクトからアクセスできない場合、循環参照によりガベージコレクションされない可能性があります。
7. キャッシュ:localStorageやsessionStorageのようなキャッシュを使用する際、キャッシュデータがクリアされないとメモリリークを引き起こす可能性があります。
8. コンソールログ(console.log):開発ツールのコンソールを開いていると、console.logに渡されたオブジェクトへの参照が保持され、メモリリークにつながることがあります。
13. 一般的なWebセキュリティとその対策
1. SQLインジェクション
SQLインジェクションとは、SQLコマンドをWebフォームの送信、ドメイン入力、またはページリクエストのクエリ文字列に挿入することで、サーバーを騙して悪意のあるSQLコマンドを実行させる攻撃です。
対策:
- ユーザー入力の検証
- パラメータ化されたSQL(プリペアドステートメント)の使用
- データベース権限の制限
2. クロスサイトスクリプティング (XSS)
XSS(Cross-Site Scripting)攻撃とは、攻撃者が悪意のあるHTMLタグやJavaScriptコードをWebページに挿入する攻撃です。例えば、攻撃者がフォーラムに安全に見えるリンクを置いてユーザーを騙してクリックさせ、Cookie内のユーザーの機密情報を盗んだり、悪意のあるフォームを追加して、ユーザーがフォームを送信する際に、情報をユーザーが信頼していると思ったサイトではなく、攻撃者のサーバーに送信させたりします。
対策:
- ユーザー入力の適切なエスケープとサニタイズ
- 安全なテンプレートエンジンまたは自動エスケープ関数の使用
- HTTPヘッダーのContent Security Policy (CSP) の使用: CSPはホワイトリストメカニズムを通じて、ブラウザが読み込むコンテンツのソースを制限し、クロスサイトスクリプティング(XSS)、データインジェクション、クリックジャッキングなどのセキュリティリスクを効果的に軽減します。
3. クロスサイトリクエストフォージェリ (CSRF)
CSRF攻撃とは、攻撃者が被害者のログイン状態を利用して、サーバーに悪意のあるリクエストを送信し、ユーザーに代わって指定された操作を完了させる攻撃です。
対策:
- CSRFトークンによる検証
- リクエスト元(Origin)の検証
- HTTP Refererヘッダーの検証
// 例:CSRFトークン検証のミドルウェア
// 通常はフレームワークが提供するミドルウェアを使用
app.use((req, res, next) => {
// CSRFトークンを生成し、セッションまたはCookieに保存
// res.locals.csrfToken = generateCSRFToken();
next();
});
// 例:リクエスト元(Origin)の検証
// リクエストヘッダーのOriginが期待されるオリジンと一致するか確認
if (req.headers.origin !== 'https://expected-domain.com') {
// リクエストが期待されるオリジンからのものではない場合、処理を拒否
res.status(403).send('Forbidden: Invalid Origin');
return;
}
// 例:HTTP Refererヘッダーの検証
// リクエストヘッダーのRefererが期待される参照元と一致するか確認
if (req.headers.referer && !req.headers.referer.startsWith('https://expected-domain.com/')) {
// リクエストが期待される参照元からのものではない場合、処理を拒否
res.status(403).send('Forbidden: Invalid Referer');
return;
}
4. ファイルアップロードの脆弱性
攻撃者が悪意のあるファイルをサーバーにアップロードすることで、サーバーの権限を取得したり、悪意のあるコードを実行したりする可能性があります。
対策:
- アップロードファイルの種類とサイズの制限
- ファイルコンテンツのチェック
- ファイル保存パスの設定(ウェブサーバーから直接実行されない安全な場所)
5. クリックジャッキング
攻撃者がターゲットとなるウェブサイトを透過的なiframeに隠し、ユーザーを無害に見えるコンテンツをクリックするように誘い込み、実際には隠されたターゲットウェブサイトの操作をトリガーする攻撃です。
対策:
X-Frame-Options応答ヘッダーを使用して、ウェブページがiframeに埋め込まれるのを防ぐ。- Content Security Policy (CSP) を使用する。
- Framebustingスクリプトを使用して、ネストされるのを防ぐ。
6. 不安全なリダイレクトとジャンプ
悪意のあるリダイレクトは、ユーザーを詐欺サイトやマルウェア配布サイトに誘導する可能性があります。
対策:
- リダイレクトURLのホワイトリスト検証
- ジャンプリクエストの正当性検証
HTTP OnlyおよびSecureフラグ付きCookieの使用
7. セッションハイジャックとセッション固定
セッションハイジャックは、攻撃者がユーザーのセッション識別子を盗み、その識別子を悪用してユーザーになりすましてサーバーと通信し、ユーザー権限を乗っ取る攻撃です。
セッション固定は、攻撃者が事前にセッション識別子を設定し、被害者を騙してその識別子を使ってログインさせ、ユーザーセッションの制御権を得る攻撃です。
対策:
- 安全なセッション管理メカニズムの使用(HTTPSの使用、HTTP OnlyおよびSecureフラグ付きCookieの使用など)
- ランダムで複雑なセッションIDの生成
- セッションIDの定期的な更新
14. イベントモデル
イベントモデルには、キャプチャフェーズ、ターゲットフェーズ、バブリングフェーズの3つの段階があります。
1. キャプチャフェーズ(Capture Phase):イベントは最も外側の親ノードから下方に伝播し始め、ターゲット要素に到達するまで続きます。
2. ターゲットフェーズ(Target Phase):イベントがターゲット要素自体に到達し、ターゲット要素上のイベントハンドラをトリガーします。ターゲット要素に複数のハンドラがバインドされている場合、それらは追加された順序で実行されます。
3. バブリングフェーズ(Bubble Phase):イベントはターゲット要素から上方にバブリングし始め、親ノードに伝播し、最も外側の親ノードまたはルートノードに到達するまで続きます。
addEventListenerの第三引数は、イベントリスナーがキャプチャフェーズ(true)またはバブリングフェーズ(false、デフォルト値)でトリガーされるかを指定するために使用されます。
// キャプチャフェーズでトリガーされるイベントリスナーを登録
document.body.addEventListener('click', function(event) {
console.log('Body: キャプチャされたイベント');
}, true); // 第三引数を true に設定
// バブリングフェーズでトリガーされるイベントリスナーを登録
document.getElementById('actionButton').addEventListener('click', function(event) {
console.log('Button: バブリングされたイベント');
}, false); // 第三引数を false に設定するか、省略
15. offsetWidth/offsetHeight、clientWidth/clientHeight、scrollWidth/scrollHeightの違い
offsetWidth/offsetHeightはcontent + padding + border + スクロールバーを含む値を返します。これはe.getBoundingClientRect()と同じ効果です。clientWidth/clientHeightはcontent + paddingのみを返します。スクロールバーがある場合でもスクロールバーは含まれません。scrollWidth/scrollHeightはcontent + padding + オーバーフローコンテンツのサイズを返します。
16. 一般的な互換性問題
ウェブ開発において遭遇する一般的な互換性問題は以下の通りです。
1. ブラウザのボックスモデルの差異:異なるブラウザはボックスモデルの解釈に違いがあり、要素のサイズ計算が不一致になることがあります。CSSのbox-sizingプロパティを使用して制御できます。
2. ブラウザのCSSプロパティサポートの差異:異なるブラウザはCSSプロパティのサポートレベルが異なり、一部のプロパティは特定のブラウザで機能しない、または正しく解析されない場合があります。CSSベンダープレフィックス(-webkit-, -moz-など)を使用するか、互換性のあるソリューションで対処する必要があります。
3. JavaScript APIの差異:異なるブラウザはJavaScript APIのサポートに違いがあり、一部のメソッド、プロパティ、またはイベントは特定のブラウザで利用できない、または動作が異なる場合があります。互換性チェックを行い、代替ソリューションを使用するか、特定の処理を行う必要があります。
4. スタイルの互換性:異なるブラウザはスタイルの解析に違いがあり、ページ表示が不一致になる可能性があります。異なるブラウザ向けにスタイルを調整および最適化する必要があります。
5. 画像フォーマットの互換性:異なるブラウザは画像フォーマットのサポートに違いがあり、一部のフォーマットは特定のブラウザでサポートされない、または異常に表示される場合があります。要件に応じて適切な画像フォーマットを選択し、互換性処理を行う必要があります。
6. イベント処理の差異:異なるブラウザはイベント処理に違いがあり、例えばイベントオブジェクトのプロパティ、メソッド、座標取得などで差があります。互換性処理を行い、適切な方法でイベント関連情報を取得する必要があります。
17. Promiseの理解
定義:PromiseはJavaScriptにおける非同期操作を扱うためのメカニズムであり、コールバック地獄(callback hell)の問題をより洗練された方法で解決し、非同期コードの読みやすさと保守性を向上させます。
1. Promiseの3つの状態
1. 保留中(pending):初期状態。まだ完了も拒否もされていない。
2. 解決済み(fulfilled):操作が成功裡に完了した。
3. 拒否済み(rejected):操作が失敗した。
Promiseは状態マシンとして考えることができます。初期状態はpendingで、resolve関数とreject関数を通じてfulfilledまたはrejected状態に遷移できます。一度状態が変化すると、二度と変更することはできません。then関数またはcatch関数は新しいPromiseインスタンスを返します(前のインスタンスと同じではありません)。これにより、チェーン呼び出しが可能です。Promiseの仕様では、pending状態以外の状態は変更できないと規定されているため、もし同じインスタンスを返すのであれば、複数のthen呼び出しは意味を失います。
2. Promiseの静的メソッド
Promise.allメソッド- 構文:
Promise.all(iterable) - 引数:
Arrayなどのイテラブルオブジェクト。 - 説明:このメソッドは、複数の
Promiseの結果を集約するのに非常に役立ちます。ES6では、複数のPromise.all非同期リクエストを並行して操作し、結果は通常以下の2つの状況のいずれかになります。- すべての結果が成功して返された場合、リクエスト順序に従って成功結果を返します。
- いずれか一つでも失敗したメソッドがある場合、失敗メソッドに移行します。
// あるページで、カルーセルリストの取得、ショップリストの取得、カテゴリリストの取得という3つの操作を // 同時にリクエストしてページをレンダリングする必要がある場合、`Promise.all`を使うことで、より明確で一目瞭然な実装ができます。 // 1. カルーセルデータリストの取得 function fetchBannerData(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('カルーセルデータ') },300) }) } // 2. ショップリストの取得 function fetchShopData(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('ショップデータ') },500) }) } // 3. カテゴリリストの取得 function fetchCategoryData(){ return new Promise((resolve,reject)=>{ setTimeout(function(){ resolve('カテゴリデータ') },700) }) } function initializePage(){ Promise.all([fetchBannerData(), fetchShopData(), fetchCategoryData()]) .then(results=>{ console.log(results) // ['カルーセルデータ', 'ショップデータ', 'カテゴリデータ'] }).catch(error=>{ console.log(error) }) } initializePage();- 構文:
Promise.allSettledメソッドPromise.allSettledの構文と引数はPromise.allに似ており、Promiseの配列を引数として受け取り、新しいPromiseを返します。唯一の違いは、実行完了後に失敗することはないという点です。つまり、Promise.allSettledがすべて処理を完了した後、各Promiseが成功したか失敗したかに関わらず、その状態を取得できます。
const successPromise = Promise.resolve(2); const failurePromise = Promise.reject(-1); const allSettledResult = Promise.allSettled([successPromise, failurePromise]); allSettledResult.then(function (results) { console.log(results); }); // 返り値: // [ // { status: 'fulfilled', value: 2 }, // { status: 'rejected', reason: -1 } // ]Promise.anyメソッド- 構文:
Promise.any(iterable) - 引数:
Arrayなどのイテラブルオブジェクト。 - 説明:
anyメソッドはPromiseを返します。引数となるPromiseインスタンスのいずれか一つがfulfilled状態になった場合、最初に解決されたPromiseオブジェクトの値を返し、最終的にanyが返すインスタンスはfulfilled状態になります。もしすべての引数Promiseインスタンスがrejected状態になった場合、ラップされたインスタンスはrejected状態になります。
const successfulOutcome = Promise.resolve(2); const failedOutcome = Promise.reject(-1); const anyResult = Promise.any([successfulOutcome, failedOutcome]); anyResult.then(function (results) { console.log(results); }); // 返り値: // 2- 構文:
Promise.raceメソッド- 構文:
Promise.race(iterable) - 引数:
Arrayなどのイテラブルオブジェクト。 - 説明:
raceメソッドはPromiseを返します。引数となるPromiseのいずれか一つのインスタンスが先に状態を変化させた場合、raceメソッドの返り値の状態もそれに合わせて変化します。先に状態を変化させたPromiseインスタンスの返り値は、raceメソッドのコールバック関数に渡されます。
// 画像リソースのロード function loadImage(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } img.onerror = function(){ reject(new Error('画像のロードに失敗しました')); } img.src = 'http://www.baidu.com/img/flexible/logo/pc/result.png'; }); return p; } // タイムアウト関数、リクエストの計時用 function timeoutPromise(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject('画像リクエストタイムアウト'); }, 5000); }); return p; } Promise.race([loadImage(), timeoutPromise()]) .then(function(results){ console.log(results); // 成功した場合は画像オブジェクト、タイムアウトの場合はエラーメッセージ }) .catch(function(reason){ console.log(reason); // '画像リクエストタイムアウト' または '画像のロードに失敗しました' }); // 上記のコードからわかるように、Promiseを使って画像が正常にロードされたかを判断する方法は、Promise.raceメソッドの比較的好ましいビジネスシナリオです。- 構文:
Promiseの手書き実装、面接対応版:
function SimplePromise(executorFunc){
let _this = this; // コンテキストを保持
_this.currentState = "pending"; // 状態変更前の初期状態を定義
_this.resolvedValue = undefined; // resolved状態になったときの値を定義
_this.rejectedReason = undefined; // rejected状態になったときの理由を定義
// thenメソッドで登録されるコールバックを格納する配列
_this.onFulfilledCallbacks = [];
_this.onRejectedCallbacks = [];
function resolve(value){
// "pending"であることのチェックで、状態変更は不可逆であることを保証
if(_this.currentState === "pending"){
_this.resolvedValue = value;
_this.currentState = "fulfilled";
// 状態変更後、登録されたonFulfilledコールバックをすべて実行
_this.onFulfilledCallbacks.forEach(cb => cb(value));
}
}
function reject(reason){
// "pending"であることのチェックで、状態変更は不可逆であることを保証
if(_this.currentState === "pending"){
_this.rejectedReason = reason;
_this.currentState = "rejected";
// 状態変更後、登録されたonRejectedコールバックをすべて実行
_this.onRejectedCallbacks.forEach(cb => cb(reason));
}
}
// コンストラクタ内の例外をキャッチ
try{
executorFunc(resolve, reject);
}catch(e){
reject(e);
}
}
// チェーン呼び出し可能なthenメソッドを定義
SimplePromise.prototype.then = function(onFulfilled, onRejected){
let _this = this;
// 新しいPromiseを返すことでチェーンを可能にする
return new SimplePromise((resolve, reject) => {
const handleFulfilled = (value) => {
try {
const result = onFulfilled ? onFulfilled(value) : value;
resolve(result);
} catch (e) {
reject(e);
}
};
const handleRejected = (reason) => {
try {
const result = onRejected ? onRejected(reason) : Promise.reject(reason);
resolve(result); // rejectedハンドラが新しい値を返したらfulfilledになる
} catch (e) {
reject(e);
}
};
switch(_this.currentState){
case "fulfilled":
setTimeout(() => handleFulfilled(_this.resolvedValue), 0); // 非同期実行
break;
case "rejected":
setTimeout(() => handleRejected(_this.rejectedReason), 0); // 非同期実行
break;
case "pending":
// 状態がpendingの場合はコールバックを登録
_this.onFulfilledCallbacks.push(handleFulfilled);
_this.onRejectedCallbacks.push(handleRejected);
break;
}
});
}
Promiseについて話すとき、解決する問題点や一般的なAPIに加えて、イベントループ、ミクロタスク、マクロタスクの実行順序についても深く掘り下げて説明するのが良いでしょう。もしPromiseのソースコードを見たことがあるなら、ネイティブPromiseがどのように実装されているかについても触れると良いでしょう。Promiseの重要な点は、コールバックの2つの引数、
resolveとrejectです。また、Promiseのチェーン呼び出し(Promise.then()、各thenがそれぞれの責務を果たす)も重要なポイントです。
18. JSの基本データ型と参照データ型
基本データ型:
undefined: 未定義または未初期化の値を表します。null: 空の値または存在しないオブジェクトを表します。boolean: 論理的なtrueまたはfalseを表します。number: 数値を表し、整数と浮動小数点数を含みます。string: 文字列を表します。symbol: 一意で不変の値を表し、通常はオブジェクトのプロパティキーとして使用されます。BigInt:2^53 - 1よりも大きい整数を表します。
参照データ型:
object: 通常のオブジェクト{}、配列[]、関数function()、正規表現/regexp/、およびnew Date()のような特殊なオブジェクトを含みます。
違いと特徴:
- 格納方法:基本データ型は直接スタックメモリに格納されます。参照データ型の値は通常、ヒープメモリに格納されます。
- コピー挙動:基本データ型をコピーすると、新しいコピーが作成されます。参照データ型をコピーすると、参照アドレスがコピーされるため、2つの変数は同じオブジェクトを指します。
- メモリ使用量:異なる基本データ型がメモリを占有するサイズは固定されています。参照データ型のサイズは固定されておらず、オブジェクトのプロパティ数とサイズに依存します。
- 関数引数渡し:関数引数が基本データ型の場合、値のコピーが渡されます。参照データ型の場合、参照のコピーが渡されます。
19. nullとundefinedの違い
nullとundefinedの主な違いは以下の通りです。
1. 意味合い:nullは空の値または存在しないオブジェクトを明示的に表すために使用されます。undefinedは、変数が宣言されたがまだ値が割り当てられていない場合、またはプロパティが存在しない場合などを表します。
2. typeofの戻り値:null型をtypeof演算子でチェックすると、「object」が返されます。undefined型をtypeof演算子でチェックすると、「undefined」が返されます。
3. 使用シナリオ:nullは通常、変数が現在どのオブジェクトも指していないことを明示的に割り当てるために使用されます。undefinedは通常、変数が宣言された後にJavaScriptによって自動的に割り当てられ、変数がまだ値を持っていないことを示します。関数において、明確な戻り値がない場合、関数の戻り値はundefinedになります。
20. ["1", "2", "3"].map(parseInt)の解答
parseInt(str, radix)
- 文字列を解析し、10進整数を返します。
- 最初の引数
strは、解析する文字列です。 - 2番目の引数
radixは基数(進数)で、範囲は2〜36です。radix進数の規則に従ってstr文字列を解析します。不正なradixは解析失敗につながります。 radixが渡されない場合:strが0xで始まる場合、16進数として処理されます。strが0で始まる場合、8進数として処理されます(ただし、ES5では廃止され、一部の古いブラウザではまだ使用される可能性がありますが、通常は10進数として処理されます)。- その他の場合、10進数として処理されます。
eslintはparseIntに2番目の引数を含めることを推奨します(これは、0で始まる8進数の表記の不確実性(例:078)が原因で、10進数として処理される可能性があるためです)。
// 分解
const values = ["1", "2", "3"];
const results = values.map((item, index, array) => {
// 1回目の呼び出し: item: '1', index: 0
// 2回目の呼び出し: item: '2', index: 1
// 3回目の呼び出し: item: '3', index: 2
return parseInt(item, index);
// parseInt('1', 0) // 0はradixが渡されなかったのと同等で、10進数として処理され1を返す。parseInt('1')と同じ。
// parseInt('2', 1) // NaN。radix 1は2-36の範囲外。
// parseInt('3', 2) // NaN。2進数には'3'が存在しないため解析できない。
});
// 解答: [1, NaN, NaN]
21. JSONの理解
JSON(JavaScript Object Notation)は、軽量なデータ交換フォーマットです。JavaScriptオブジェクトのキーと値のペアに似た形式でデータを記述し、読み書きが容易であるとともに、マシンによる解析と生成も簡単です。
JSONには以下の特徴があります:
1. 簡潔性:JSON形式のデータは簡潔で、読み書きが容易です。
2. 多様なデータ型のサポート:JSONは、文字列、数値、ブール値、オブジェクト、配列、nullを含む基本的なデータ型をサポートしています。
3. クロスプラットフォームと言語:JSONは汎用的なデータ交換フォーマットであり、特定のプログラミング言語やプラットフォームに依存せず、様々なプログラミング言語で解析および生成できます。
JavaScriptでは、組み込みのJSONオブジェクトを使用して、JSON文字列とJavaScriptオブジェクトの間で変換を行うことができます。一般的なメソッドは以下の通りです。
JSON.parse():JSON文字列をJavaScriptオブジェクトに解析します。JSON.stringify():JavaScriptオブジェクトをJSON文字列に変換します。
// JSON文字列をJavaScriptオブジェクトに変換
var jsonText = '{"employeeName": "佐藤", "age": 30, "city": "東京"}';
var jsObject = JSON.parse(jsonText);
console.log(jsObject.employeeName); // 佐藤
// JavaScriptオブジェクトをJSON文字列に変換
var dataObject = {department: "開発部", yearsOfService: 5};
var jsonStringResult = JSON.stringify(dataObject);
console.log(jsonStringResult); // {"department":"開発部","yearsOfService":5}
22. deferとasync
defer
deferはスクリプトの実行を遅延させるために使用されます。スクリプトは並行してダウンロードされますが、ドキュメント全体の解析が完了するまで実行は待機します。複数のdefer属性を持つスクリプトは、ドキュメント内での出現順序で実行されます。これにより、スクリプトがDOMを操作する前にロードされることが保証され、ページのレンダリングがブロックされるのを防ぎます。defer属性は外部スクリプト(src属性を通じて導入されるスクリプト)のみが使用できることに注意が必要です。
async
async属性はスクリプトを非同期的にロードするために使用されます。スクリプトは並行してダウンロードされ、ダウンロード完了後すぐに実行されます。複数のasync属性を持つスクリプトの実行順序は不定であり、ダウンロードが先に完了したスクリプトが先に実行されます。これにより、スクリプトのロードパフォーマンスが向上しますが、スクリプト間の依存関係に問題が生じる可能性があります。同様に、async属性も外部スクリプトのみが使用できます。
<script src="script1.js" defer></script>
<script src="script2.js" async></script>
deferとasyncのどちらを使用するかは、スクリプトのロードと実行順序の重要性によって決まります。スクリプト間に依存関係があり、順序通りに実行する必要がある場合はdeferを使用すべきです。スクリプト間に依存関係がなく、並行してロードおよび実行できる場合は、asyncを使用してロードパフォーマンスを向上させることができます。
23. ES6の理解
ES6(ECMAScript 2015)はJavaScriptの6番目の主要バージョンで、開発者の効率とコード品質を向上させるために多くの新しい言語機能と改善が導入されました。ES6の重要な機能のいくつかを以下に示します。
- ブロックスコープ:
letとconstキーワードが導入され、ブロックスコープ内で変数を宣言できるようになり、変数ホイスティングとスコープ汚染の問題を解決しました。 - アロー関数:アロー(
=>)を使用して関数を定義し、関数の記述を簡素化し、thisを自動的にバインドします。 - テンプレートリテラル:バッククォート(`)で文字列を囲み、文字列内で変数や式を使用できるようになり、より柔軟な文字列結合とフォーマットを実現します。
- 分割代入:分割代入構文を使用すると、配列やオブジェクトから値を抽出し、対応する変数に代入できるため、変数代入操作が簡素化されます。
- デフォルト引数:関数はデフォルト引数値を定義できるため、関数呼び出し時の引数渡し操作が簡素化されます。
- レスト/スプレッド構文:3つのドット(
...)を使用して配列とオブジェクトを展開操作し、配列またはオブジェクトを個別の要素に分割したり、複数の配列またはオブジェクトを1つに結合したりできます。 - Promise:
Promiseオブジェクトが導入され、非同期操作をより適切に処理できるようになり、コールバック地獄の問題を解決し、より明確な非同期プログラミングパターンを提供します。 - クラスとモジュール:ES6はクラスの概念を導入し、
classキーワードを使用してクラスを定義でき、従来のオブジェクト指向プログラミングに近い方法を実現しました。同時に、ES6はモジュール化のサポートも提供し、importおよびexport構文を使用してモジュールをインポートおよびエクスポートできます。 - イテレーターとジェネレーター:イテレーターとジェネレーターの概念が導入され、カスタムイテレーターを通じてデータコレクションを走査し、ジェネレーター関数を使用してイテレーターを生成できるようになりました。
- パイプライン演算子:提案段階の機能で、パイプライン演算子(
|>)が導入され、式の結果を次の式の引数として渡すことができるため、関数呼び出しとメソッドチェーンの記述が簡素化されます。
24. オブジェクト指向プログラミングと手続き型プログラミング
手続き型プログラミング(Procedural Programming)とオブジェクト指向プログラミング(Object-Oriented Programming)は、異なるプログラミングパラダイムです。
手続き型プログラミングは、プロセス(関数、メソッド)を中心としたプログラミング方式で、プログラムを一連のステップとみなし、これらのステップを順次実行することで問題を解決します。手続き型プログラミングは、問題解決のステップとアルゴリズムを重視し、問題を異なるサブタスクに分割し、関数の呼び出しとデータの受け渡しを通じてタスク間の連携を実現します。
オブジェクト指向プログラミングは、オブジェクトを中心としたプログラミング方式で、プログラムを一連の相互に関連するオブジェクトとみなし、オブジェクト間の相互作用とメッセージパッシングを通じて問題を解決します。オブジェクト指向プログラミングは、物事(オブジェクト)の抽象化、カプセル化、継承、ポリモーフィズムを重視します。
相違点と共通点、利点と欠点:
- 相違点と共通点:
- プログラミングの思考様式とコードの組織化方法が異なります。手続き型は問題解決のステップとアルゴリズムに焦点を当て、オブジェクト指向はオブジェクトとオブジェクト間の関係と相互作用に焦点を当てます。
- 手続き型は問題を異なる関数に分解して実装しますが、オブジェクト指向は問題をオブジェクトとして抽象化し、オブジェクトのメソッドを通じて機能を実現します。
- 利点:
- 手続き型プログラミングの利点には、シンプルさ、直感性、高い実行効率が含まれます。
- オブジェクト指向プログラミングの利点には、高い再利用性、拡張性と保守性の容易さ、よりモジュール化された柔軟なコードが含まれます。
- 欠点:
- 手続き型プログラミングの欠点には、コードの重複性が高いこと、メンテナンスが困難であること、拡張性が低いことが含まれます。
- オブジェクト指向プログラミングの欠点には、複雑性が高いこと、学習曲線が急であること、ランタイムオーバーヘッドが増えることが含まれます。
25. JavaScriptで配列を判定する方法
1. Array.isArray() メソッド:
let isArrayResult = Array.isArray(variable);
2. instanceof Array 演算子:
instanceof演算子は、コンストラクタ関数のprototypeプロパティが、特定のインスタンスオブジェクトのプロトタイプチェーンに存在するかどうかを検出するために使用されます。
let isArrayResult = variable instanceof Array;
3. Object.prototype.toString.call() メソッド:
let isArrayResult = Object.prototype.toString.call(variable) === '[object Array]';
4. constructorプロパティのチェック:
この方法は、配列が異なるArrayコンストラクタ(例えば、異なるグローバルコンテキストから、またはArray.createを使用した場合)を通じて作成された場合、失敗する可能性があります。
let isArrayResult = variable.constructor === Array;
26. letとvarの違い
letとvarの主な違いは以下の通りです。
1. スコープ:
letで宣言された変数はブロックスコープを持ち、その変数が定義されたブロック内でのみ有効です。varで宣言された変数にはブロックスコープがなく、そのスコープは関数スコープまたはグローバルスコープです。
2. 変数ホイスティング:
letで宣言された変数もホイスティングされますが、初期化はされません。宣言前にこれらの変数にアクセスすると、ReferenceErrorが発生します。これは一時的なデッドゾーン(Temporal Dead Zone, TDZ)を持つためです。varで宣言された変数は変数ホイスティングが発生し、宣言前に使用できます(値はundefined)。
3. 重複宣言:
letで宣言された変数は重複宣言が許可されておらず、重複宣言するとエラーが発生します。varで宣言された変数は重複宣言が許可されており、エラーは発生せず、後続の宣言が前の宣言を上書きします。
4. グローバルオブジェクトのプロパティ:
letで宣言された変数はグローバルオブジェクト(window)のプロパティにはなりません。varで宣言された変数はグローバルオブジェクトのプロパティになります。
5. ループ内での違い:
letで宣言された変数はループ本体内でブロックスコープを持ち、各イテレーションで新しい変数が作成されます。varで宣言された変数はループ本体内でブロックスコープを持たず、変数は関数スコープまたはグローバルスコープになります。
27. 関数型プログラミングの理解
関数型プログラミングは、計算を数学関数の評価とみなすプログラミングパラダイムです。関数型プログラミングには以下の特徴があります。
1. 純粋関数(Pure Functions):関数の出力は入力のみによって決定され、副作用を生じません。つまり、同じ入力に対して常に同じ出力を返します。純粋関数は渡された引数を変更せず、外部の状態も変更しないため、コードの予測可能性が高まり、テストが容易になります。
2. 不変性(Immutability):データは一度作成されると変更できません。データへのあらゆる変更は、新しいデータのコピーを作成します。この不変性により、コードの安全性が高まり、潜在的なエラーを回避できます。
3. 高階関数(Higher-Order Functions):関数は他の関数に引数として渡されたり、戻り値として返されたりすることができます。この高階関数の能力は、関数の組み合わせ、カプセル化、抽象化に利用でき、コードの再利用性と可読性を向上させます。
4. 関数合成(Function Composition):複数の関数を新しい関数に組み合わせることで、より複雑なロジックを実現できます。関数合成は、ある関数の戻り値を別の関数の引数として渡し、複数の関数を連結して関数チェーンを形成することで実現できます。
5. 遅延評価(Lazy Evaluation):必要なときにのみ計算を行い、不要な計算を回避します。この遅延評価により、プログラムのパフォーマンスと効率が向上します。
28. アロー関数と通常関数の違い
アロー関数と通常の関数(functionキーワードで定義された関数)の主な違いは以下の通りです。
1. thisのバインディング: アロー関数は独自のthisを持たず、その関数が定義されたレキシカルスコープのthis値をキャプチャします。一方、通常の関数のthisは実行時に決定され、呼び出し方法によって異なります。
2. コンストラクタとしての使用不可: アロー関数はnewキーワードを使用してインスタンスを作成することはできません。独自のprototypeプロパティを持たず、インスタンス化できないためです。
3. argumentsオブジェクトの欠如: アロー関数は独自のargumentsオブジェクトを持ちません。代わりにRestパラメータ(...args)を使用できます。
4. yieldキーワードの欠如: アロー関数はジェネレータ関数として使用できず、yieldキーワードを使用して関数の実行を一時停止したり再開したりすることはできません。
29. JS配列とオブジェクトのイテレーション方法、および比較
配列のイテレーション方法:
1. forループ
const numbers = [10, 20, 30];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
2. forEachメソッド
const dataArray = ['apple', 'banana', 'cherry'];
dataArray.forEach((element) => {
console.log(element);
});
3. for...ofループ
for...ofステートメントは、イテラブルオブジェクトから値のシーケンスを処理するループを実行します。
const iterableArray = [1, 2, 3];
for (const val of iterableArray) {
console.log(val);
}
4. mapメソッド
const originalNumbers = [1, 2, 3];
const doubledNumbers = originalNumbers.map(num => num * 2);
console.log(doubledNumbers); // [2, 4, 6]
オブジェクトのイテレーション方法:
1. for...inループ
for...inステートメントは、継承された列挙可能なプロパティを含む、オブジェクトのすべての列挙可能な文字列プロパティ(Symbol以外)を反復処理します。
const myConfig = { settingA: 1, settingB: 2, settingC: 3 };
for (const key in myConfig) {
// オブジェクト自身のプロパティのみを処理するためにhasOwnPropertyチェックを追加するのがベストプラクティス
if (myConfig.hasOwnProperty(key)) {
console.log(key, myConfig[key]);
}
}
2. Object.keysメソッドとforEachメソッドの組み合わせ
Object.keys()静的メソッドは、指定されたオブジェクト自身の列挙可能な文字列キープロパティ名からなる配列を返します。
const dataMap = { id: 101, name: 'Alice', status: 'active' };
Object.keys(dataMap).forEach((key) => {
console.log(key, dataMap[key]);
});
3. Object.entriesメソッドとforEachメソッドの組み合わせ
Object.entries()静的メソッドは、指定されたオブジェクト自身の列挙可能な文字列キープロパティのキーと値のペアを含む配列を返します。
const propertyMap = { color: 'red', size: 'M', price: 50 };
Object.entries(propertyMap).forEach(([key, value]) => {
console.log(key, value);
});
30. let、var、constの違い
JavaScriptにおけるlet、var、constの主な違いは以下の通りです。
1. スコープ
letとconstはどちらもブロックスコープを持ちます。変数が宣言されたブロック内({}で囲まれた領域)でのみ有効です。varは関数スコープまたはグローバルスコープを持ちます。ブロックスコープは持ちません。
2. 変数の変更可能性
letとvarは、変数に新しい値を再代入することを許可します。constは、宣言後に再代入ができません。ただし、これは値が不変であることを意味するものではありません。もしconstでオブジェクトが宣言された場合、オブジェクト内部のプロパティは変更可能です。
3. 変数ホイスティングと一時的なデッドゾーン(TDZ)
varで宣言された変数は、その関数またはグローバルスコープの最上部にホイスティングされます。ただし、初期化(値の割り当て)はホイスティングされません。そのため、宣言前にアクセスするとundefinedになります。letとconstで宣言された変数もスコープの最上部にホイスティングされますが、初期化はされません。これらの変数は、宣言が実行されるまで「一時的なデッドゾーン(Temporal Dead Zone, TDZ)」に留まります。TDZ内でこれらの変数にアクセスしようとすると、ReferenceErrorが発生します。
4. 重複宣言
letとconstは、同じスコープ内での重複宣言を許可しません。重複宣言すると構文エラーになります。varは、同じスコープ内での重複宣言を許可します。エラーは発生せず、後続の宣言が前の宣言を上書きします。
31. ノードの追加、削除、移動、コピー、作成、検索方法
新しいノードの作成
document.createElement(tagName); // 指定されたタグ名の要素ノードを作成
document.createTextNode(text); // 指定されたテキストを含むテキストノードを作成
document.createDocumentFragment(); // 空のドキュメントフラグメントノードを作成
ノードの追加
parentNode.appendChild(newNode); // 新しく作成したノードを親ノードの子ノードリストの末尾に追加
parentNode.insertBefore(newNode, referenceNode); // 新しいノードを参照ノードの前に挿入
ノードの削除
parentNode.removeChild(node); // 親ノードから子ノードを削除
node.remove(); // 削除したいノード自体で直接呼び出し、DOMからノードを削除
ノードの検索
document.getElementById(id); // 要素のIDでノードを検索
document.getElementsByTagName(tagName); // タグ名でノードのコレクションを検索
document.getElementsByClassName(className); // クラス名でノードのコレクションを検索
document.querySelector(selector); // CSSセレクタで最初の一致するノードを検索
document.querySelectorAll(selector); // CSSセレクタで一致するすべてのノードを検索
ノードの置き換え
parentNode.replaceChild(newNode, oldNode); // 指定された古いノードを新しいノードで置き換え
ノードのコピー
node.cloneNode(true); // ノードとそのすべての子孫ノードをコピー(trueはディープコピーを意味)
node.cloneNode(false); // ノードをコピーするが子孫ノードは含まない(falseはシャローコピーを意味)
32. 正規表現
(コンテンツが提供されていません)
33. 配列の重複除去方法のまとめ
ES6 Set を利用した重複除去(ES6で最も一般的)
let dataItems = [1, 2, 2, 3, 4, 4, 5];
let uniqueItems = [...new Set(dataItems)]; // [1, 2, 3, 4, 5]
indexOf メソッドの使用
配列を走査し、indexOfメソッドを使って新しい配列に要素がすでに存在するかどうかをチェックして重複除去します。
let numbers = [1, 2, 2, 3, 4, 4, 5];
let distinctNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (distinctNumbers.indexOf(numbers[i]) === -1) {
distinctNumbers.push(numbers[i]);
}
}
console.log(distinctNumbers); // [1, 2, 3, 4, 5]
filter メソッドの使用
let values = [1, 2, 2, 3, 4, 4, 5];
let filteredValues = values.filter((item, index, self) => self.indexOf(item) === index);
console.log(filteredValues); // [1, 2, 3, 4, 5]
includes メソッドの使用
let sourceArray = [1, 2, 2, 3, 4, 4, 5];
let deduplicatedArray = [];
for (let i = 0; i < sourceArray.length; i++) {
if (!deduplicatedArray.includes(sourceArray[i])) {
deduplicatedArray.push(sourceArray[i]);
}
}
console.log(deduplicatedArray); // [1, 2, 3, 4, 5]
34. プロジェクトで行ったパフォーマンス最適化
以下は、ウェブページのロード速度とパフォーマンスを向上させるためにプロジェクトで採用できる一般的なパフォーマンス最適化策です。
1. HTTPリクエスト数の削減:CSS、JavaScriptファイルの結合と圧縮、スプライト画像やフォントアイコンの使用による画像リクエストの削減、不要なリソースリクエストの削減を行います。
2. DNSルックアップの削減:異なるドメインの使用を減らし、DNSルックアップの回数を削減します。
3. CDNの利用:静的リソースをCDNにデプロイし、より高速なアクセス速度を提供します。
<script src="https://cdn.example.com/script.js"></script>
4. リダイレクトの回避:ウェブページに余分なリダイレクトがないことを確認し、追加のネットワークリクエストを削減します。
5. 画像遅延読み込み (Lazy Loading):画像がビューポートに入ったときにのみロードするように遅延させます。
<!-- 遅延読み込み画像 -->
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload" alt="Description">
<script src="lazyload-lib.js"></script>
6. DOM要素数の削減:ページ構造を最適化し、DOM要素の数を減らしてレンダリングパフォーマンスを向上させます。
7. DOM操作の削減:頻繁なDOM操作を避け、操作を結合するか、DocumentFragmentを使用して一括操作を行います。
var containerElement = document.getElementById("container");
var fragment = document.createDocumentFragment(); // ドキュメントフラグメントを作成
for (var i = 0; i < 1000; i++) {
var div = document.createElement("div");
div.innerText = "要素 " + i;
fragment.appendChild(div); // フラグメントに追加
}
containerElement.appendChild(fragment); // 一括でDOMに追加
8. 外部JavaScriptおよびCSSの使用:JavaScriptおよびCSSコードを外部化し、ブラウザのキャッシュメカニズムを利用してページロード速度を向上させます。
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
9. ファイルの圧縮:JavaScript、CSS、フォント、画像などの静的リソースファイルを圧縮してファイルサイズを小さくします。
10. CSSスプライトの最適化:複数の小さなアイコンを1つの大きな画像に結合し、CSSで位置を指定することで画像リクエストを削減します。
.icon-home {
background-image: url("sprite.png");
background-position: -10px -20px; /* スプライト画像内の位置 */
width: 20px;
height: 20px;
}
11. iconfontの使用:アイコンフォントを代替画像として使用し、画像リクエストを削減し、レンダリングパフォーマンスを向上させます。
<i class="iconfont"></i>
12. フォントのサブセット化:ページで実際に使用されているフォント文字のみをロードし、フォントファイルのサイズを削減します。フォントツール(例:Fontello、IcoMoonなど)を使用してサブセット化を行う必要があります。
13. 複数ドメインでの配信:ウェブサイトのコンテンツを異なるドメインに分割し、同時リクエストの処理能力を向上させます。プロジェクト内で異なるドメインまたはサブドメインを設定する必要があります。
14. iframeの使用を減らす:iframeの頻繁な使用を避けます。追加のネットワークリクエストとページロード時間を増加させるためです。
15. 画像srcが空でないことを確認:<img>タグのsrc属性が空でないことを確認し、ブラウザが不要なリクエストを送信するのを防ぎます。
16. スタイルシートを<link>タグで配置:インラインスタイルを避け、スタイルシートを<link>タグ内に配置することで、ブラウザがスタイルとコンテンツを並行してロードできるようにします。
17. JavaScriptをページの最下部に配置:JavaScriptスクリプトをページの<body>タグの最後に配置することで、ページコンテンツが先にロードされ、ユーザーエクスペリエンスが向上します。
Webpackパフォーマンス最適化
1. プロダクションモードの使用:Webpack設定でmodeをproductionに設定すると、コード圧縮、スコープホイスティングなどの多くの組み込み最適化機能が有効になります。
2. コード分割(Code Splitting):Webpackのコード分割機能を使用し、コードを複数の小さなチャンクに分割してオンデマンドでロードすることで、巨大なファイルをバンドルするのを防ぎます。
3. 遅延読み込み(Lazy Loading):動的インポート(Dynamic Import)またはimport()関数を使用し、モジュールをオンデマンドでロードし、必要なときにのみ関連コードをロードします。
4. Tree Shaking:Webpackのoptimizationオプションを設定し、sideEffectsとusedExportsを有効にすることで、未使用のコード(dead code)を削除します。
5. キャッシュ:Webpackのchunkhashまたはcontenthashを使用してファイル名を生成し、キャッシュメカニズムを実現し、ブラウザがすでにロードしたファイルを再利用できるようにします。
6. 並行処理(Parallel Processing):thread-loaderやHappyPackプラグインを使用し、Webpackのビルドプロセスをマルチスレッド化してビルド速度を高速化します。
7. スコープの縮小(Narrowing the Scope):Webpackのresolveオプションを設定し、モジュールの解決範囲を縮小することで、不要な検索を減らします。
8. 外部依存関係の使用(External Dependencies):安定していて頻繁に修正されないライブラリやフレームワークをexternals設定で除外することで、バンドルサイズを削減し、CDN経由でロードします。
9. プラグインとローダーの使用:効率的なプラグインとローダーを選択し、それらのオプションを適切に設定して、ビルドプロセスとリソース処理を最適化します。
10. Webpack Bundle Analyzerの使用:Webpack Bundle Analyzerツールを使用してバンドルされたファイルを分析し、サイズが大きい、冗長な、または不要なモジュールを見つけて、さらに最適化を行います。
これらは一般的なWebpackのパフォーマンス最適化テクニックであり、具体的なプロジェクトの要件に応じて選択および設定し、ビルド速度を向上させ、出力結果を最適化できます。
Reactのパフォーマンス最適化戦略:
1. React.memo()またはPureComponentの使用:関数コンポーネメントの場合、React.memo()関数を使用するか、PureComponentクラスを継承してシャロー比較を行うことで、不要な再レンダリングを回避します。
2. リスト最適化のためのkey属性の使用:リストをレンダリングする際、各リストアイテムに一意のkey属性を提供することで、Reactがコンポーネントをより効率的に更新および再利用するのに役立ちます。
3. コンポーネントレンダリング制御のためのshouldComponentUpdateまたはReact.memo()の使用:クラスコンポーネントでは、shouldComponentUpdateライフサイクルメソッドを実装することで、コンポーネントの再レンダリングを制御できます。関数コンポーネントの場合、React.memo()でコンポーネントをラップし、カスタム比較関数を渡すことができます。
4. コンポーネントの遅延読み込み:大きなコンポーネントやページの場合、React.lazy()とSuspenseコンポーネントを使用してオンデマンドでロードし、初期ロード時間を短縮します。
5. 仮想化リストの使用:長いリストや大規模なデータセットの場合、仮想化リストライブラリ(例:react-virtualizedやreact-window)を使用して、表示されている部分のみをレンダリングし、DOM操作とメモリ使用量を削減します。
6. 計算のメモ化(Memoization)によるキャッシュ:メモ化技術を使用することで、計算結果をキャッシュし、重複する計算を回避してパフォーマンスを向上させることができます。メモ化ライブラリ(例:reselect)を使用して実装できます。
7. React Profilerによるパフォーマンス分析:React ProfilerはReactが提供するパフォーマンス分析ツールであり、アプリケーションのパフォーマンスボトルネックを特定し、最適化を行うのに役立ちます。
8. ESLintとコード分析ツールの使用:ESLintなどのコード規範ツールや静的コード分析ツールを使用することで、潜在的なパフォーマンス問題や最適化の機会を発見し、それに応じて調整を行うことができます。
35. ブラウザキャッシュ
ブラウザのキャッシュメカニズムとは、ユーザーがウェブサイトにアクセスした際、ブラウザがそのリソースをローカルキャッシュに保存する仕組みを指します。リソースの有効期間内に、そのリソースへの再リクエストがあった場合、ブラウザはサーバーにリクエストを送信するのではなく、キャッシュのコピーを直接使用します。これにより、ページのロード速度が向上し、サーバーの負担が軽減されます。
ウェブリソースのキャッシュポリシーは通常、サーバーによって指定され、2つの種類に分けられます:強力なキャッシュ(Strong Cache)と交渉キャッシュ(Negotiation Cache)。
クライアントが特定のリソースをリクエストする際のキャッシュ取得フローは以下の通りです。
- まず、そのリソースのいくつかのHTTPヘッダーに基づいて、強力なキャッシュにヒットするかどうかを判断します。ヒットした場合、サーバーにリクエストを送信せずに、ローカルからキャッシュリソースを直接取得します(応答ヘッダーに
Cache-ControlまたはExpiresフィールドが含まれており、キャッシュルールに合致する場合(例:max-ageの値が現在時刻より大きい場合)、そのリソースは強力なキャッシュにヒットしたと判断されます)。 - 強力なキャッシュにヒットしなかった場合、クライアントはサーバーにリクエストを送信します。サーバーは、他のリクエストヘッダー(
If-Modified-SinceやIf-None-Matchなど)を通じて、このリソースが交渉キャッシュにヒットするかどうかを検証します。これはHTTP再検証と呼ばれます。ヒットした場合、サーバーはリクエストに応答しますが、リソース自体は返さず、クライアントに直接キャッシュから取得するよう指示します。クライアントは応答を受け取った後、キャッシュからリソースを取得します。 - 強力なキャッシュと交渉キャッシュの共通点は、キャッシュにヒットした場合、サーバーがリソースを返さないことです。違いは、強力なキャッシュはサーバーにリクエストを送信しないのに対し、交渉キャッシュはリクエストを送信することです。
- 交渉キャッシュにもヒットしなかった場合、サーバーはリソースをクライアントに送信します。
Ctrl + F5で強制的にページをリフレッシュした場合、強力なキャッシュと交渉キャッシュの両方をスキップして、直接サーバーからロードします。
強力なキャッシュ (Strong Cache)
強力なキャッシュとは、ブラウザがリソースをリクエストする際、まずキャッシュルールに基づいて、サーバーにリクエストを送信することなく、ローカルキャッシュから直接リソースを取得できるかを判断するメカニズムです。強力なキャッシュポリシーは、HTTPヘッダー情報のExpires属性とCache-Control属性の2つの方法で設定できます。
Expires(HTTP 1.0の仕様で、値はGMT形式の絶対時間文字列で、キャッシュリソースの有効期限を表します):サーバーは応答ヘッダーにExpires属性を追加することで、リソースの有効期限を指定します。有効期限内であれば、そのリソースはキャッシュから使用でき、サーバーに再度リクエストを送信する必要はありません。この時間は絶対時間であり、サーバーの時間に依存するため、クライアントとサーバーの時間が一致しない、またはユーザーがクライアント時間を変更できる場合があり、キャッシュヒットの結果に影響を与える可能性があります。Cache-Control:max-age(HTTP 1.1の仕様で、強力なキャッシュはそのmax-age値を使用してキャッシュリソースの最大ライフサイクルを判断します。値の単位は秒):これはリソースがキャッシュされる時間を秒単位で指定する相対時間です。この値とリソースが最初にリクエストされた時間に基づいてリソースの有効期限が計算されるため、Expiresよりも効果的な方法です。no-cache:交渉キャッシュを意味し、リクエストごとにリソースが変更されたかどうかをサーバーと照合します(ETagまたはLast-Modifiedで比較します)。リソースが変更されていない場合、サーバーは304 Not Modifiedステータスコードを返し、クライアントにリクエストコンテンツの再送信は不要であり、キャッシュコンテンツを使用できることを伝えます。リソースが変更されている場合、200 OKステータスコードを返し、新しいリソースを返します。no-store:リクエストまたは応答のいかなる部分もキャッシュしないように指示します。
交渉キャッシュ (Negotiation Cache)
強力なキャッシュが無効になった場合、ブラウザは交渉キャッシュを使用します。交渉キャッシュポリシーを使用する場合、まずサーバーにリクエストを送信し、リソースが変更されていない場合は304ステータスコードを返し、ブラウザにローカルキャッシュのコピーを使用させます。リソースが変更されている場合は、変更後のリソースを返します。
Last-Modified:サーバーは応答ヘッダーにLast-Modified属性を追加することで、リソースが最後に変更された時刻を示します。If-Modified-Since:ブラウザが次にリクエストを発行する際、リクエストヘッダーにIf-Modified-Since属性を追加します。この属性の値は、前回リソースが返されたときのLast-Modifiedの値です。リクエストがサーバーに送信された後、サーバーはこの属性とリソースの最終変更時刻を比較し、リソースが変更されたかどうかを判断します。リソースが変更されていない場合、304ステータスコードを返し、クライアントにローカルキャッシュの使用を指示します。リソースが変更されている場合、変更後のリソースを返します。この方法には欠点があり、Last-Modifiedで示される最終変更時刻は秒単位までしか正確ではありません。もし一部のファイルが1秒以内に複数回変更された場合、ファイルは変更されてもLast-Modifiedは変更されないため、キャッシュヒットが不正確になる可能性があります。ETag:Last-Modifiedの潜在的な不正確さのため、HTTPでは別の方法としてETag属性が提供されています。サーバーがリソースを返す際、ヘッダー情報にETag属性を追加します。この属性はリソース生成時のユニークな識別子であり、リソースが変更されるとこの値も変更されます。If-None-Match:次にリソースをリクエストする際、ブラウザはリクエストヘッダーにIf-None-Match属性を追加します。この属性の値は、前回返されたリソースのETagの値です。サーバーはリクエストを受け取った後、この値とリソースの現在のETag値を比較し、リソースが変更されたかどうか、リソースを返す必要があるかどうかを判断します。この方法はLast-Modifiedの方法よりも正確です。
Last-ModifiedとETag属性が同時に存在する場合、ETagの優先度が高くなります。交渉キャッシュを使用する際、サーバーは負荷分散の問題を考慮する必要があります。複数のサーバー上のリソースのLast-Modifiedは一貫しているべきですが、各サーバー上のETagの値は異なるため、負荷分散を考慮する場合、ETag属性は設定しない方が良いでしょう。
強力なキャッシュ戦略と交渉キャッシュ戦略は、キャッシュにヒットした場合、どちらもローカルキャッシュのコピーを直接使用します。違いは、交渉キャッシュがサーバーに一度リクエストを送信する点のみです。キャッシュにヒットしなかった場合、どちらもサーバーにリクエストを送信してリソースを取得します。実際のキャッシュメカニズムでは、強力なキャッシュ戦略と交渉キャッシュ戦略は協力して使用されます。ブラウザはまずリクエスト情報に基づいて、強力なキャッシュがヒットするかどうかを判断し、ヒットした場合は直接リソースを使用します。ヒットしなかった場合は、ヘッダー情報に基づいてサーバーにリクエストを発行し、交渉キャッシュを使用します。交渉キャッシュがヒットした場合は、サーバーはリソースを返さず、ブラウザはローカルリソースのコピーを直接使用します。交渉キャッシュがヒットしなかった場合は、サーバーは最新のリソースをブラウザに返します。
36. シャローコピーとディープコピー
シャローコピー (Shallow Copy)
シャローコピーはオブジェクトの最初の階層のプロパティのみをコピーします。もしオブジェクト内部に別のオブジェクトや配列が含まれている場合、それらの内部構造は参照を通じて共有されたままになります。
オブジェクトのシャローコピー方法:
Object.assign()メソッドの使用
let originalData = { a: 1, b: { c: 2 } };
let copiedData = Object.assign({}, originalData);
console.log(copiedData); // { a: 1, b: { c: 2 } }
copiedData.b.c = 3; // コピーしたオブジェクトのネストされたプロパティを変更
console.log(originalData.b.c); // 3 - オリジナルも変更される
- スプレッド演算子 (
...) の使用
let sourceObject = { x: 1, y: { z: 2 } };
let shallowClonedObject = { ...sourceObject };
console.log(shallowClonedObject); // { x: 1, y: { z: 2 } }
shallowClonedObject.y.z = 4; // クローンしたオブジェクトのネストされたプロパティを変更
console.log(sourceObject.y.z); // 4 - オリジナルも変更される
配列のシャローコピー方法:
Array.prototype.slice()
let sourceArray = [1, { a: 2 }, 3];
let copiedArray = sourceArray.slice();
copiedArray[1].a = 99; // コピーした配列のオブジェクトのプロパティを変更
console.log(sourceArray[1].a); // 99 - オリジナルのオブジェクトも変更される
let originalList = [1, { val: 5 }, 7];
let shallowListCopy = [...originalList];
shallowListCopy[1].val = 100; // シャローコピーされた配列のオブジェクトプロパティを変更
console.log(originalList[1].val); // 100 - オリジナルも変更される
ディープコピー (Deep Copy)
ディープコピーは、完全に新しいオブジェクトを作成し、すべてのサブオブジェクトと配列を再帰的にコピーします。これにより、元のオブジェクトとコピーが完全に独立します。
function customDeepClone(sourceObj) {
if (sourceObj === null || typeof sourceObj !== 'object') {
return sourceObj; // プリミティブ値やnullはそのまま返す
}
// 配列かオブジェクトかを判断して初期化
let clonedObj = Array.isArray(sourceObj) ? [] : {};
// プロパティを再帰的にコピー
for (let propName in sourceObj) {
if (sourceObj.hasOwnProperty(propName)) { // 自身のプロパティのみをコピー
clonedObj[propName] = customDeepClone(sourceObj[propName]);
}
}
return clonedObj;
}
let objA = {
data: 1,
nested: { value: 2 }
};
let objB = customDeepClone(objA);
objB.nested.value = 3; // ディープコピーされたオブジェクトの値を変更
console.log(objA.nested.value); // 2 - オリジナルは変更されない
console.log(objB.nested.value); // 3
JSON.parse(JSON.stringify()) を使用したディープコピー
let dataSet1 = {
id: 1,
details: { code: 'ABC' }
};
let dataSet2 = JSON.parse(JSON.stringify(dataSet1));
dataSet2.details.code = 'XYZ';
console.log(dataSet1.details.code); // ABC - オリジナルは変更されない
console.log(dataSet2.details.code); // XYZ
JSON.parse(JSON.stringify()) を使用したディープコピーには限界があります。
undefinedを無視する:JSON.stringify()メソッドはオブジェクトをシリアライズする際にundefinedプロパティを無視し、シリアライズ後の結果にはそのプロパティが含まれません。- 関数をシリアライズできない:
JSON.stringify()メソッドはオブジェクトをシリアライズする際に関数プロパティを無視します。関数はJSON形式のデータ型に合致しないためです。シリアライズとデシリアライズの後、関数プロパティは失われます。 - 循環参照を持つオブジェクトを処理できない:オブジェクトに循環参照(オブジェクト内部が自身を参照している場合)がある場合、
JSON.stringify()メソッドは正しく処理できず、循環参照するプロパティがnullとしてシリアライズされます。 - 特殊なオブジェクトを処理できない:
JSON.stringify()メソッドは、Dateオブジェクト、正規表現、Map、Setなどの一部の特殊なオブジェクトをシリアライズできません。これらはシリアライズ中に空のオブジェクトに変換されるか、文字列に変換されます。
37. デバウンスとスロットリング
デバウンス(Debounce)
デバウンス関数の原理は、イベントがトリガーされた後、一定の遅延時間を待機することです。この遅延時間内にイベントが再びトリガーされた場合、遅延時間の計算を最初からやり直します。指定された時間間隔内にイベントが再度トリガーされなかった場合にのみ関数が実行されます。
function createDebouncedFunction(actionFunc, waitMs) {
let timeoutId = null;
return function(...args) {
if (timeoutId) {
clearTimeout(timeoutId); // 既存のタイマーをクリア
}
timeoutId = setTimeout(() => {
actionFunc.apply(this, args); // 指定された遅延後に実行
}, waitMs);
};
}
// 使用例
const searchInput = document.getElementById('searchBox');
const debouncedHandler = createDebouncedFunction(() => {
console.log('デバウンス関数が実行されました:', searchInput.value);
}, 500);
// イベントハンドラでデバウンス関数を使用
searchInput.addEventListener('input', debouncedHandler);
適用シナリオ
- テキスト入力の検証:連続して文字を入力した後、AJAXリクエストを送信して検証を行う場合、最後の入力のみを検証します。
- ボタン送信シナリオ:複数回ボタンが送信されるのを防ぎ、最後の送信のみを実行します。
- サーバーサイド検証シナリオ:フォーム検証でサーバーサイドの協力が必要な場合、連続する入力イベントの最後の一回のみを実行します。検索候補機能も同様です。
スロットリング(Throttle)
スロットリングは、指定された時間間隔内でイベントが何回トリガーされても、最初にトリガーされた関数のみが実行され、それ以降のトリガーは次の時間間隔まで無視されます。
スロットリングも、スクロールイベントやウィンドウのリサイズなど頻繁にトリガーされるイベントに適しており、一定期間内に関数が1回だけ実行されることを保証できます。
function createThrottledFunction(operationFunc, intervalMs) {
let throttleTimer = null; // スロットルタイマー
return function(...args) {
// タイマーがnullの場合(つまり、前回の実行からインターバル時間が経過している場合)
if (!throttleTimer) {
throttleTimer = setTimeout(() => {
operationFunc.apply(this, args); // 関数を実行
throttleTimer = null; // タイマーをリセットして次の実行を許可
}, intervalMs);
}
};
}
// 使用例
const scrollableElement = window; // または特定の要素
const throttledHandler = createThrottledFunction(() => {
console.log('スロットリング関数が実行されました');
}, 300);
// イベントハンドラでスロットリング関数を使用
scrollableElement.addEventListener('scroll', throttledHandler);
適用シナリオ
- ドラッグシナリオ:一定時間内に一度だけ実行し、超高頻度の位置変更を防ぎます。DOM要素のドラッグ機能の実装(
mousemove)。 - スケーリングシナリオ:ブラウザの
resizeイベントの監視。 - スクロールシナリオ:スクロールイベントを監視し、ページ下部に到達したかどうかの判断による自動読み込み。
- アニメーションシナリオ:短時間での複数回のアニメーショントリガーによるパフォーマンス問題を回避。
38. 変数ホイスティング
JavaScriptにおいて、変数ホイスティング(Variable Hoisting)とは、コード実行前にJavaScriptエンジンが変数と関数宣言を処理し、それらをそれぞれのスコープの最上部に移動させることを指します。これにより、変数と関数は宣言前に使用されても、参照エラー(ReferenceError)を発生させません。
変数ホイスティングのルール
- 関数宣言のホイスティング
- 関数宣言(Function Declaration)を使用して定義された関数は、それらが宣言されたスコープの最上部にホイスティングされます。これは、関数宣言の前にこれらの関数を呼び出すことができることを意味します。関数宣言は常に変数宣言よりも優先され、変数宣言が関数宣言の前にある場合でも、変数宣言は関数宣言によって上書きされます。
- 変数宣言のホイスティング
varを使用して宣言された変数は、関数またはグローバルスコープの最上部にホイスティングされます。ただし、初期化(値の割り当て)はホイスティングされません。宣言前に変数にアクセスすると、その値はundefinedになります。
letとconstのホイスティングletとconstで宣言された変数もスコープの最上部にホイスティングされますが、初期化はされません。これらの変数は、それらの宣言が実行されるまで「一時的なデッドゾーン(Temporal Dead Zone)」に留まります。
一時的なデッドゾーン(TDZ)
コードの実行が変数宣言の位置に到達するまで、letとconstで宣言された変数は不可視であり、この期間を「一時的なデッドゾーン」と呼びます。デッドゾーン内でこれらの変数にアクセスしようとすると、実行時エラーが発生します。
38. JavaScriptのシングルスレッドと非同期処理の関係
JavaScriptにおけるシングルスレッドとは、JavaScriptエンジンがコードを実行する際に、たった一つのメインスレッドしか持たないことを指します。つまり、一度に一つの命令しか実行できないということです。これは、JavaScriptコードが順序通りに実行され、前のコードが完了してから次のコードが実行されることを意味します。
- 非同期処理は、ノンブロッキング操作を処理するためのプログラミングモデルです。JavaScriptでは、コールバック関数、
Promise、async/awaitなどの方法で非同期プログラミングを実現できます。非同期操作はメインスレッドの実行をブロックしないため、プログラムの応答性能とユーザーエクスペリエンスが向上します。 - 非同期処理とシングルスレッドの関係は密接です。JavaScriptはシングルスレッドであるため、すべての操作が同期である場合、ネットワークリクエストやファイル読み込みなどの時間のかかる操作に遭遇すると、プログラム全体がブロックされ、ユーザーインターフェースも応答しなくなり、ユーザーエクスペリエンスが低下します。
- 非同期プログラミングモデルを使用することで、時間のかかる操作を他のスレッドやプロセスに委譲し、メインスレッドが他のタスクの実行を継続できるようにすることで、プログラムの並行性と応答性が向上します。非同期操作が完了すると、コールバック関数や
Promiseを通じてメインスレッドに通知され、メインスレッドは対応するコールバックロジックを実行します。
まとめ:
JavaScriptはシングルスレッドであり、コード実行のためのメインスレッドは一つだけです。- 非同期プログラミングは、ノンブロッキング操作を処理する方法であり、プログラムの応答性能とユーザーエクスペリエンスを向上させます。
- 非同期操作は、時間のかかるタスクを他のスレッドやプロセスに委譲し、メインスレッドは他のタスクの実行を継続できます。
- 非同期操作が完了すると、コールバック関数または
Promiseを通じてメインスレッドに通知されます。
39. なぜ同一オリジンポリシーが必要なのか?
同一オリジンポリシー(Same-Origin Policy)は、ブラウザにおける重要なセキュリティメカニズムであり、異なるオリジン(プロトコル、ドメイン、ポート)からのスクリプトが現在のドキュメントにアクセスする権限を制限するために使用されます。同一オリジンポリシーの主な目的は、ユーザーのプライバシーとセキュリティを保護し、悪意のあるウェブサイトが機密データを取得したり、クロスサイトスクリプティング(XSS)攻撃、CSRF攻撃、セッションハイジャックなどのセキュリティ問題を防止することです。
同一オリジンポリシーは、異なるオリジンのウェブページ間の相互作用を制限することで、同一オリジンのウェブページのみが互いのリソースにアクセスできるようにします。
同一オリジンポリシーは以下の行為を制限します。
1. スクリプトによるクロスオリジンドキュメントのDOMへのアクセス:ページに埋め込まれたiframe要素を通じてロードされたクロスオリジンドキュメントは、ターゲットドキュメントが明示的に許可しない限り、スクリプトを通じてそのDOMにアクセスできません。
2. スクリプトによるクロスオリジンドキュメントの内容の読み取り:ページに埋め込まれたiframe要素を通じてロードされたクロスオリジンドキュメントは、プロパティの読み取りやメソッドの実行を含め、スクリプトを通じてその内容を読み取ることができません。
3. スクリプトによるクロスオリジンAJAXリクエストの送信:スクリプトは直接クロスオリジンのAJAXリクエストを送信できず、同一オリジンのサーバーにのみリクエストを送信できます。
4. Cookie、LocalStorage、IndexedDBの制限:クロスオリジンのスクリプトは、他のオリジンのCookie、LocalStorage、またはIndexedDBデータにアクセスできません。
同一オリジンポリシーの存在により、ブラウザはユーザーのプライバシーとセキュリティをよりよく保護できます。しかし、JSONP、CORS、プロキシサーバーなどの方法を使用してクロスオリジンアクセスを行う必要があるシナリオもいくつかあります。
同一オリジンポリシーはブラウザでのみ適用され、サーバー間の通信を制限しないことに注意が必要です。サーバーは自由にクロスオリジンアクセスを行うことができます。
40. JavaScriptの構成要素
JavaScriptは主に以下の3つの構成要素から成り立っています。
1. ECMAScript(コア):ECMAScriptはJavaScriptの基盤であり、言語の構文、型、ステートメント、キーワードなどを定義しています。JavaScriptの基本的な構文、データ型、関数、演算子、制御フローなどのコア特性を規定し、配列、オブジェクト、文字列、正規表現などの操作メソッドと機能を提供します。ECMAScriptのバージョンはES6(ES2015)を基準としており、時間の経過とともに新しいバージョンのECMAScriptがより多くの言語機能と特性を導入しています。
2. DOM(Document Object Model):DOMはHTML、XMLドキュメントを表現および操作するためのインターフェースです。ドキュメントの構造、属性、メソッドを定義し、開発者がJavaScriptを通じてウェブページの内容、構造、スタイルにアクセスおよび変更することを可能にします。DOMはドキュメントをツリー構造として表現し、各ノードはドキュメント内の要素、属性、テキストなどを表します。開発者はDOMが提供するAPIを使用してこれらのノードを追加、削除、変更、検索する操作を行い、動的な更新とインタラクティブな効果を実現できます。
3. BOM(Browser Object Model):BOMはブラウザウィンドウと対話するためのインターフェースを提供します。ブラウザウィンドウへのアクセス、ウィンドウサイズの処理、ナビゲーション履歴、Cookieの処理、HTTPリクエストの送信などの機能を提供します。BOMのオブジェクトにはwindow、navigator、location、history、screenなどがあり、開発者はこれらのオブジェクトを使用してブラウザの動作を制御したり関連情報を取得したりできます。
これら3つの要素がJavaScript全体を構成し、ウェブページで豊富なインタラクションと動的な効果を実現できる強力なプログラミング言語となっています。
41. <script>タグの位置は初回の画面表示時間に影響するか
<script>の位置は、初回の画面表示時間に影響を与えます。ブラウザがHTMLを解析してDOMを生成する過程で、JavaScriptファイルは並行してダウンロードされますが、スクリプトの実行はページの解析とレンダリングをブロックします。- ブラウザが
<script>タグに遭遇すると、HTMLの解析を一時停止し、スクリプトのダウンロードと実行を開始します。スクリプトの実行が完了してから初めて、ブラウザはページの解析とレンダリングを続行します。 - もし
<script>タグが<head>タグ内に配置されている場合、スクリプトのダウンロードと実行がページのレンダリングより先に行われるため、初回の画面表示開始が遅延します。 - 初回の画面表示時間を改善するためには、通常、
<script>タグを<body>タグの最後に配置し、ほとんどのコンテンツが表示されてからスクリプトがロードおよび実行されるようにすることをお勧めします。これにより、ページがより早くユーザーに表示され、ユーザーエクスペリエンスが向上します。 - また、非同期ロード(例:
<script>タグにasync属性を追加)や遅延ロード(例:<script>タグにdefer属性を追加)の方法を使用することで、スクリプトがページのロードをブロックする影響を軽減できます。これにより、ページのレンダリングをブロックせずにスクリプトをロードおよび実行でき、初回の画面表示完了時間が短縮されます。
42. JavaScriptのガベージコレクション方法
現代のJavaScriptエンジンは、主に「マーク&スイープ(mark and sweep)」アルゴリズムを主要なガベージコレクション方法として使用します。参照カウント(reference counting)は、一部の古いJavaScriptエンジンで使用されることがあります。
マーク&スイープ(mark and sweep)アルゴリズム
- ガベージコレクターは実行時に、メモリに格納されているすべての変数にマークを付けます。最初はメモリ内のすべてのオブジェクトをガベージとみなし、すべて0とマークします。
- ガベージコレクターはルートオブジェクトから開始し、すべての参照を再帰的に走査し、それらを「環境内」としてマークし、ガベージではないノードを1に変更します。
- 走査が完了した後、ガベージコレクターはマークされていない変数をクリアし、つまりメモリ空間を回収します。
- クリアされたメモリ空間は、後続の変数に再割り当てされます。すべてのメモリ内オブジェクトのマークを0に変更し、次のガベージコレクションサイクルを待ちます。
利点
- マーク&スイープアルゴリズムの利点は、実装が比較的シンプルであることだけです。マーク付けは「付ける」か「付けない」かの2つの状態しかなく、1ビット(0と1)でマークできるため、非常にシンプルです。
欠点
メモリの断片化:空きメモリブロックが連続しておらず、多くの空きメモリブロックが発生しやすくなります。また、必要なメモリが大きすぎるオブジェクトを割り当てる際に適切なブロックが見つからない場合もあります。割り当て速度が遅い:First-fit戦略(サイズ以上のブロックが見つかったらすぐに返す)を使用しても、その操作はO(n)であり、最悪の場合、毎回最後まで走査する必要があります。また、断片化により、大きなオブジェクトの割り当て効率はさらに低下します。
結局のところ、マーク&スイープアルゴリズムの欠点は、クリア後に残ったオブジェクトの位置が変わらないことによって生じる空きメモリの不連続性です。この点を解決できれば、2つの欠点すべてを完璧に解決できます。
そして、マーク&コンパクション(Mark-Compact)アルゴリズム はこれを効果的に解決できます。そのマークフェーズはマーク&スイープアルゴリズムと変わりませんが、マーク終了後、マーク&コンパクションアルゴリズムは生存しているオブジェクト(つまり、クリーンアップ不要なオブジェクト)をメモリの一端に移動させ、最後に境界のメモリをクリーンアップします。
V8におけるGCの最適化
V8のガベージコレクション戦略は、主に世代別ガベージコレクションメカニズムに基づいています。V8ではヒープメモリを新生代(new generation)と老生代(old generation)の2つの領域に分け、異なるガベージコレクター、つまり異なる戦略でガベージコレクションを管理しています。
新生代のオブジェクトは生存期間の短いオブジェクト、簡単に言えば新しく生成されたオブジェクトで、通常1〜8MBの容量しかサポートしません。一方、老生代のオブジェクトは生存期間が長いオブジェクト、または常駐メモリのオブジェクトで、簡単に言えば新生代のガベージコレクションを経験してもまだ生存しているオブジェクトで、通常より大きな容量を持ちます。
これら新旧2つのメモリ領域のガベージコレクションを管理するために、V8は2つのガベージコレクターを採用しています。これを新生代ガベージコレクターと老生代ガベージコレクターと呼びましょう。
世代別ガベージコレクション (Generational Garbage Collection)
上記で述べたガベージクリーニングアルゴリズムは、ガベージコレクションのたびにメモリ内のすべてのオブジェクトをチェックします。これは、大きく、古く、生存期間の長いオブジェクトに対して、新しく、小さく、生存期間の短いオブジェクトと同じ頻度でチェックするのは良くありません。前者は時間がかかり、頻繁なクリーンアップは不要で、後者はその逆です。この点をどう最適化するか?そこで世代別が導入されます。
新生代ガベージコレクション
新生代オブジェクトは、Scavengeと呼ばれるアルゴリズムによってガベージコレクションされます。Scavengeアルゴリズムの具体的な実装では、主にCheneyアルゴリズムと呼ばれるコピー方式が採用されています。詳しく見ていきましょう。
Cheneyアルゴリズムでは、ヒープメモリを2つに分割します。1つは使用中のスペースで「使用領域」と呼び、もう1つはアイドル状態のスペースで「空き領域」と呼びます。以下の図を参照してください。
新しく追加されたオブジェクトはすべて使用領域に格納され、使用領域がいっぱいになりそうになると、ガベージクリーニング操作を実行する必要があります。
ガベージコレクションが開始されると、新生代ガベージコレクターは使用領域内のアクティブオブジェクトにマークを付けます。マークが完了した後、使用領域内のアクティブオブジェクトを空き領域にコピーしてソートし、その後ガベージクリーニングフェーズに入ります。つまり、非アクティブオブジェクトが占めるスペースをクリーンアップします。最後に役割を入れ替え、元の使用領域を空き領域に、元の空き領域を使用領域にします。
オブジェクトが複数回のコピー後も生存している場合、それは寿命の長いオブジェクトとみなされ、老生代に移動され、老生代のガベージコレクション戦略に従って管理されます。
もう一つの状況として、オブジェクトを空き領域にコピーする際、空き領域の占有率が25%を超えると、そのオブジェクトは直接老生代空間に昇格されます。25%の割合が設定されている理由は、Scavenge回収が完了した後、空き領域は使用領域に反転し、引き続きオブジェクトメモリの割り当てを行うため、占有率が大きすぎると、その後のメモリ割り当てに影響を与えるためです。
老生代ガベージコレクション
新生代と比較して、老生代のガベージコレクションは比較的理解しやすいです。上記で述べたように、ほとんどの大きく、生存期間の長いオブジェクトは老生代に割り当てられます。老生代のオブジェクトは通常大きいため、新生代のように領域を分割してコピーを繰り返す方法は非常に時間がかかり、結果として回収実行効率が低下します。そのため、老生代ガベージコレクターがそのガベージ回収実行を管理し、その全体のフローは前述のマーク&スイープアルゴリズムを採用しています。
まずマークフェーズでは、一連のルート要素から開始し、これらのルート要素を再帰的に走査します。走査中に到達できる要素はアクティブオブジェクトと呼ばれ、到達できない要素は非アクティブオブジェクトと判断できます。
クリアフェーズでは、老生代ガベージコレクターは非アクティブオブジェクト、つまりデータを直接クリーンアップします。
前述の通り、マーク&スイープアルゴリズムはクリーンアップ後に大量の不連続なメモリ断片を生成します。過剰な断片は大きなオブジェクトが十分な連続メモリを割り当てられない原因となります。V8では、この問題を解決し空間を最適化するために、前述のマーク&コンパクションアルゴリズムを採用しています。
43. JavaScriptの同一オリジンポリシーについて説明してください
同一オリジンポリシー(Same-Origin Policy)は、ブラウザに実装されている重要なセキュリティメカニズムであり、異なるオリジン(プロトコル、ドメイン、ポート)からのスクリプトが現在のドキュメントにアクセスする権限を制限するものです。このポリシーの主な目的は、ユーザーの機密情報を保護し、悪意のあるウェブサイトがユーザーのデータに不正にアクセスしたり、クロスサイトスクリプティング(XSS)攻撃、クロスサイトリクエストフォージェリ(CSRF)攻撃、セッションハイジャックなどのセキュリティ上の脅威からユーザーを保護することです。
具体的には、同一オリジンポリシーは以下の行為を制限します。
- DOMへのアクセス制限: あるオリジンからロードされたJavaScriptコードは、異なるオリジンからロードされたドキュメントのDOM要素に直接アクセスしたり、操作したりすることはできません。例えば、
iframeで埋め込まれた異なるオリジンのページの内容を親ページから読み取ることはできません。 - データ読み取り制限: 異なるオリジンからのスクリプトは、他のオリジンからのドキュメントの内容(例:
response.body)を直接読み取ることができません。これにはAJAXリクエストによるデータ取得も含まれます。 Cookie、LocalStorage、IndexedDBへのアクセス制限: 異なるオリジンからのスクリプトは、他のオリジンによって設定されたCookie、LocalStorage、IndexedDBのデータにアクセスすることはできません。これにより、セッション情報などが保護されます。
このポリシーは、ブラウザがユーザーのセキュリティを維持するための基本的なガードレールとして機能します。しかし、正当なビジネスニーズで異なるオリジン間で通信する必要がある場合もあります。そのための解決策として、CORS (Cross-Origin Resource Sharing)、JSONP、プロキシサーバー、window.postMessage()などが提供されています。
重要な点として、同一オリジンポリシーはブラウザによって強制されるものであり、サーバー間の通信には適用されません。サーバーは自由に異なるオリジンと通信できます。
44. Web Workerについて
Web WorkerはHTML5が提供するAPIで、JavaScriptをメインスレッドとは独立したバックグラウンドスレッドで実行することを可能にし、マルチスレッドプログラミングを実現してWebアプリケーションのパフォーマンスと応答速度を向上させます。
Web Workerの特性:
- メインスレッドからの独立
- Web Workerはバックグラウンドスレッドで実行できるため、メインスレッドの実行をブロックせず、Webアプリケーションの応答速度を向上させます。
- 並行実行が可能
- Web Workerのマルチスレッドモデルにより、複数のスレッドが並行して実行され、Webアプリケーションのパフォーマンスが向上します。
- DOMとBOMに直接アクセスできない
- Web WorkerはDOMとBOMに直接アクセスできません。これらのAPIはブラウザのメインスレッドで実装されているためです。しかし、
postMessageメソッドとonmessageイベントを通じてメインスレッドと通信し、データの受け渡しと対話を実現できます。
- Web WorkerはDOMとBOMに直接アクセスできません。これらのAPIはブラウザのメインスレッドで実装されているためです。しかし、
- 外部スクリプトのインポートが可能
- Web Workerは
importScriptsメソッドを通じて外部スクリプトをインポートし、その機能を拡張できます。
- Web Workerは
使用シナリオ
- 大量の計算集約型タスク
- Web Workerは、画像処理、オーディオ/ビデオのエンコード/デコードなど、大量の計算集約型タスクの実行に適しています。
- バックグラウンドデータ処理
- Web Workerはバックグラウンドスレッドでデータを処理できるため、メインスレッドの実行に影響を与えません。例えば、データの圧縮、解凍、暗号化などの操作にWeb Workerを使用できます。
- 非同期ネットワークリクエスト
- Web Workerはバックグラウンドスレッドで非同期ネットワークリクエストを実行できるため、メインスレッドのブロックを回避します。例えば、大量のWebSocket接続をWeb Workerで処理できます。
注意点
- Workerは現在の
windowとは異なるグローバルコンテキストで動作します。そのため、Worker内でwindowを通じてグローバルスコープ(selfではなく)を取得しようとするとエラーが返されます。 - Worker内では、DOMノードを直接操作することはできません。また、
windowオブジェクトのデフォルトメソッドやプロパティも使用できません。しかし、WebSocketsやIndexedDBなどのデータストレージメカニズムを含む、windowオブジェクト配下の多くのものを使用できます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Web Workerサンプル</title>
</head>
<body>
<label for="inputN">Nの値を入力:</label>
<input type="number" id="inputN">
<button id="calculateButton">計算</button>
<div id="resultOutput"></div>
<script>
// 新しいWorkerを作成するには、Worker() コンストラクタを呼び出し、Workerスレッドで実行するスクリプトのURIを指定します。
const myWorker = new Worker('worker.js');
// HTML要素を取得
const nInput = document.getElementById('inputN');
const calculateBtn = document.getElementById('calculateButton');
const resultDiv = document.getElementById('resultOutput');
// Web Workerからのメッセージをリッスン
myWorker.onmessage = function(event) {
resultDiv.innerHTML = `フィボナッチ数列の第${nInput.value}項は ${event.data} です。`;
};
// 計算ボタンがクリックされたら、Web Workerにメッセージを送信
calculateBtn.onclick = function() {
const nValue = parseInt(nInput.value, 10);
if (!isNaN(nValue)) {
myWorker.postMessage(nValue);
} else {
resultDiv.innerHTML = '有効な数値を入力してください。';
}
};
</script>
</body>
</html>
worker.jsの内容:
// メインスレッドから送信されたメッセージをリッスン
onmessage = function(event) {
const n = parseInt(event.data, 10);
const calculatedResult = fibonacci(n);
// メインスレッドにメッセージを送信
postMessage(calculatedResult);
};
// フィボナッチ数列の第n項を計算する関数
function fibonacci(n) {
if (n === 0 || n === 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Workerの終了
myWorker.terminate();
スクリプトとライブラリのインポート
Workerスレッドはグローバル関数importScripts()にアクセスでき、これを使用してスクリプトをインポートできます。この関数は、リソースをインポートするための0個以上のURIを引数として受け入れます。
importScripts(); /* 何もインポートしない */
importScripts("moduleA.js"); /* "moduleA.js" のみインポート */
importScripts("moduleA.js", "moduleB.js"); /* 2つのスクリプトをインポート */
importScripts("//example.com/utility.js"); /* 他のオリジンからスクリプトをインポートすることも可能 */
常用デザインパターン
ファクトリパターン
- 使用シナリオ:異なるパラメータに基づいて異なるタイプのオブジェクトを作成する必要がある場合に使用できます。例えば、ユーザーの選択に基づいて異なるタイプの支払い方法オブジェクトを作成する場合などです。
- 利点:オブジェクトの作成プロセスをカプセル化し、クライアントは必要なオブジェクトを取得するためにパラメータを渡すことだけに集中できるため、結合度が低下します。
- 欠点:コードの複雑さが増し、追加でファクトリメソッドを作成する必要があります。
const createPerson = (userRole) => {
if (userRole === 'admin') {
return { userRole, accessLevel: 'フルアクセス' };
} else {
return { userRole, accessLevel: '制限付きアクセス' };
}
};
const adminUser = createPerson('admin');
const guestUser = createPerson('guest');
console.log(adminUser.accessLevel); // フルアクセス
console.log(guestUser.accessLevel); // 制限付きアクセス
シングルトンパターン
- 使用シナリオ:システム全体で一つのインスタンスのみが必要な場合に使用できます。例えば、単一の構成オブジェクトやグローバル状態管理などです。
- 利点:一つのインスタンスのみが存在することを保証し、グローバルなアクセスポイントを提供し、インスタンスの重複作成を防ぎます。
- 欠点:拡張性に優れておらず、シングルトンのインスタンス化と使用が密接に結合しています。
const getUniqueInstance = (() => {
let singleInstance;
return () => {
if (!singleInstance) {
singleInstance = {}; // ここに実際のシングルトンオブジェクトの初期化ロジック
}
return singleInstance;
};
})();
const instance1 = getUniqueInstance();
const instance2 = getUniqueInstance();
console.log(instance1 === instance2); // true
パブリッシュ-サブスクライブパターン
- 使用シナリオ:複数のオブジェクト間で疎結合なメッセージ通信が必要な場合に使用できます。例えば、コンポーネント間の通信のためのイベントバスを実装する場合などです。
- 利点:オブジェクト間の通信を疎結合にし、購読者は関心のあるイベントにのみ集中し、発行者は具体的な購読者を気にする必要がありません。
- 欠点:メモリリークを引き起こしやすいです。手動で購読解除しないと、購読者が存在し続ける可能性があります。
const MessageHub = {
subscribers: {}, // イベントタイプごとに購読者リストを保持
subscribe(eventType, callbackFunction) {
if (!this.subscribers[eventType]) {
this.subscribers[eventType] = [];
}
this.subscribers[eventType].push(callbackFunction);
},
publish(eventType, dataPayload) {
if (this.subscribers[eventType]) {
this.subscribers[eventType].forEach(callback => callback(dataPayload));
}
},
unsubscribe(eventType, callbackFunction) {
if (this.subscribers[eventType]) {
this.subscribers[eventType] = this.subscribers[eventType].filter(cb => cb !== callbackFunction);
}
}
};
// 購読するコールバック関数
const handleUserLogin = (userData) => {
console.log('ユーザーログインイベントを受信:', userData.username);
};
// イベントを購読
MessageHub.subscribe('userLoggedIn', handleUserLogin);
// イベントを発行
MessageHub.publish('userLoggedIn', { username: '山田' });
// イベントを購読解除
MessageHub.unsubscribe('userLoggedIn', handleUserLogin);
MessageHub.publish('userLoggedIn', { username: '佐藤' }); // 購読解除されたため、このメッセージは処理されない
オブザーバーパターン
- 使用シナリオ:あるオブジェクトの状態が変化したときに、それに依存する他のオブジェクトに通知して対応する操作を行わせる必要がある場合に使用できます。例えば、データの双方向バインディング機能を実装する場合などです。
- 利点:オブジェクト間の関係を疎結合にし、被観測者と観測者が疎結合になり、観測者を動的に追加および削除できます。
- 欠点:オブジェクト間の相互依存関係が増加し、システムの複雑さが増す可能性があります。
function ObservableData() {
let value;
const listenerList = []; // 購読者リスト
return {
getValue: () => value,
setValue: (newValue) => {
value = newValue;
listenerList.forEach(listener => listener(value)); // 購読者に通知
},
subscribe: (listener) => {
listenerList.push(listener);
},
unsubscribe: (listener) => {
listenerList = listenerList.filter(l => l !== listener);
}
};
}
const myData = ObservableData();
const observerFunc = (data) => console.log('データが更新されました:', data);
myData.subscribe(observerFunc);
myData.setValue('フォームが更新されました'); // データが更新されました: フォームが更新されました
デコレータパターン
- 使用シナリオ:元のオブジェクトを変更せずに、オブジェクトに動的に追加機能を追加する必要がある場合に使用できます。例えば、基本的なコンポーネントにログ記録やパフォーマンス監視機能を追加する場合などです。
- 利点:オープン/クローズドの原則に従い、元のオブジェクトの構造を変更する必要がなく、機能を柔軟に追加または削除できます。
- 欠点:クラスの数が増加し、クラス階層が複雑になる可能性があります。
const coreOperation = () => 'コア機能';
// デコレータ関数: 既存の関数にログ記録機能を追加
const addLogging = (originalFunction) => () => {
console.log('ログ開始');
const result = originalFunction();
console.log('ログ終了');
return result + ' (ログ記録済み)';
};
const loggedOperation = addLogging(coreOperation);
console.log(loggedOperation());
// 出力:
// ログ開始
// ログ終了
// コア機能 (ログ記録済み)
ストラテジーパターン
シナリオ: 異なる条件に基づいて異なるアルゴリズムや振る舞いを選択する場合。例えば、異なるソートアルゴリズムなどです。
- 使用シナリオ:異なる状況に応じて異なるアルゴリズムや戦略を選択する必要がある場合に使用できます。例えば、ユーザーが選択した異なるソート方法に基づいてデータをソートする場合などです。
- 利点:条件文の複雑さを簡素化し、アルゴリズムを独立した戦略クラスにカプセル化し、拡張と保守を容易にします。
- 欠点:クラスの数が増加し、クラス階層が複雑になる可能性があります。
const DiscountStrategyA = function() {
console.log('戦略A: 10%割引を適用');
};
const DiscountStrategyB = function() {
console.log('戦略B: 固定額割引を適用');
};
const PricingContext = {
strategy: null, // 現在の割引戦略
setPricingMethod: function(discountStrategy) {
this.strategy = discountStrategy;
},
applyDiscount: function() {
if (this.strategy) {
this.strategy();
} else {
console.log('割引戦略が設定されていません。');
}
}
};
PricingContext.setPricingMethod(DiscountStrategyA);
PricingContext.applyDiscount(); // 戦略A: 10%割引を適用
PricingContext.setPricingMethod(DiscountStrategyB);
PricingContext.applyDiscount(); // 戦略B: 固定額割引を適用
プロキシパターン
シナリオ: 特定のオブジェクトに追加機能を追加したり、アクセスを制御したりする場合。例えば、フロントエンドでデータをリクエストする際にキャッシュを行う場合などです。
const RealDataLoader = () => ({
fetchActualData: () => {
console.log('実際のデータをロード中...');
return '実際のデータ';
}
});
const CachingDataLoaderProxy = (realService) => {
let dataCache = null; // キャッシュ領域
return {
fetchActualData: () => {
if (!dataCache) {
console.log('キャッシュがありません。実際のサービスからデータを取得します。');
dataCache = realService.fetchActualData();
} else {
console.log('キャッシュされたデータを返します。');
}
return dataCache;
}
};
};
const actualService = RealDataLoader();
const proxyService = CachingDataLoaderProxy(actualService);
console.log(proxyService.fetchActualData()); // 実際のデータをロード中... 実際のデータ
console.log(proxyService.fetchActualData()); // キャッシュされたデータを返します。 実際のデータ
プロトタイプパターン
- プロトタイプオブジェクトを通じて共通のプロパティとメソッドを共有します。
const baseHumanPrototype = {
eyes: 2,
nose: 1,
ears: 2,
// 共通のメソッドなどもここに定義
getInfo: function() {
console.log(`名前: ${this.name}, 年齢: ${this.age}`);
}
};
function HumanEntity(name, age) {
this.name = name;
this.age = age;
}
// HumanEntityのプロトタイプをbaseHumanPrototypeに設定
// これにより、HumanEntityのインスタンスがbaseHumanPrototypeのプロパティとメソッドを継承する
HumanEntity.prototype = baseHumanPrototype;
const person1 = new HumanEntity('太郎', 25);
console.log(person1.eyes); // 2
person1.getInfo(); // 名前: 太郎, 年齢: 25
アダプタパターン
シナリオ: 互換性のない2つのインターフェースを連携させる場合。例えば、旧版APIを新版APIに適合させる場合などです。
const LegacyApiClient = () => ({
fetchLegacyData: () => {
console.log('旧システムからデータを取得');
return '旧システムデータ';
}
});
const ModernApiHandler = () => ({
retrieveModernData: () => {
console.log('新システムからデータを取得');
return '新システムデータ';
}
});
// アダプタ: 旧インターフェースを新インターフェースに適合させる
const LegacyToModernAdapter = (newHandler) => ({
fetchLegacyData: () => newHandler.retrieveModernData() // 旧メソッド呼び出しを新メソッドに変換
});
const modernSystemHandler = ModernApiHandler();
const adaptedOldSystem = LegacyToModernAdapter(modernSystemHandler);
console.log(adaptedOldSystem.fetchLegacyData()); // 新システムからデータを取得 新システムデータ