高度なフロントエンドに関する JavaScript 面接の質問 8 つをシェアする

84b0d057f3bc8ac40600fca3beee1e8d.jpeg

英文 | https://levelup.gitconnected.com/8-advanced-javascript-interview-questions-for-senior-roles-c59e1b0f83e1

JavaScript は強力な言語であり、Web の主要な構成要素の 1 つです。この強力な言語にはいくつかの癖もあります。たとえば、0 === -0 は true と評価されること、または Number("") は 0 と評価されることをご存知ですか?

問題は、これらの癖のために、頭を悩ませたり、ブレンドン・アイヒが JavaScript を発明した日のことを疑問に思ったりすることがあることです。そうですね、重要なのは、JavaScript が悪いプログラミング言語であるとか、JavaScript を批判する人たちが言うような悪であるということではありません。すべてのプログラミング言語には何らかの癖があり、JavaScript も例外ではありません。

そこで、今日の記事では、JavaScript の面接での重要な質問について詳しく説明します。私の目標は、これらの面接の質問を徹底的に説明して、基本的な概念を理解し、できれば面接で他の同様の質問に対処できるようにすることです。

1. + 演算子と - 演算子を注意深く観察してください

console.log(1 + '1' - 1);

上記の状況で JavaScript の + 演算子と - 演算子がどのように動作するか推測できますか?

JavaScript は 1 + '1' を検出すると、+ 演算子を使用して式を処理します。+ 演算子の興味深い特性は、オペランドの 1 つが文字列である場合に文字列の連結が優先されることです。この場合、「1」は文字列であるため、JavaScript は暗黙的に値 1 を文字列に強制します。したがって、1 + '1' は '1' + '1' となり、文字列 '11' になります。

さて、方程式は「11」 - 1 となります。- 演算子はまったく逆の動作をします。オペランドの型に関係なく、数値の減算が優先されます。オペランドが数値型でない場合、JavaScript は暗黙的なキャストを実行して数値に変換します。この例では、「11」は数値 11 に変換され、式は 11 - 1 に簡略化されます。

すべてを一緒に入れて:

'11' - 1 = 11 - 1 = 10

2. 配列要素をコピーします

次の JavaScript コードを検討し、このコード内に問題があるかどうかを見つけてください。

function duplicate(array) {
  for (var i = 0; i < array.length; i++) {
    array.push(array[i]);
  }
  return array;
}


const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);

このコード スニペットでは、入力配列の繰り返し要素を含む新しい配列を作成する必要があります。最初に検査したところ、コードは元の配列 arr の各要素を複製して、新しい配列 newArr を作成しているように見えます。ただし、繰り返し機能自体に重要な問題が発生します。

repeat 関数は、ループを使用して、指定された配列内の各項目を反復処理します。ただし、ループ内では、push() メソッドを使用して、配列の最後に新しい要素を追加します。これにより、毎回配列が長くなり、ループが停止しないという問題が発生します。配列は大きくなり続けるため、ループ条件 (i < array.length) は常に true のままです。これによりループが永遠に続き、プログラムが停止してしまいます。

配列の長さが増大し続けて無限ループが発生するという問題を解決するには、ループに入る前に配列の初期長を変数に格納します。

その後、その初期長をループ反復の制限として使用できます。こうすることで、ループは配列内の元の要素に対してのみ実行され、重複の追加による配列の増大による影響を受けなくなります。コードの修正バージョンは次のとおりです。

function duplicate(array) {
  var initialLength = array.length; // Store the initial length
  for (var i = 0; i < initialLength; i++) {
    array.push(array[i]); // Push a duplicate of each element
  }
  return array;
}


const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);

出力には配列の末尾に繰り返し要素が表示され、ループによって無限ループが発生することはありません。

[1, 2, 3, 1, 2, 3]

3. プロトタイプと __proto__ の違い

プロトタイプ プロパティは、JavaScript のコンストラクターに関連するプロパティです。コンストラクターは、JavaScript でオブジェクトを作成するために使用されます。コンストラクターを定義するときに、そのプロトタイプのプロパティにプロパティとメソッドをアタッチすることもできます。

これらのプロパティとメソッドは、そのコンストラクターから作成されたオブジェクトのすべてのインスタンスからアクセスできるようになります。したがって、プロトタイプ プロパティは、インスタンス間で共有されるメソッドとプロパティの共通リポジトリとして機能します。

次のコード スニペットを考えてみましょう。

// Constructor function
function Person(name) {
  this.name = name;
}


// Adding a method to the prototype
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};


// Creating instances
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");


// Calling the shared method
person1.sayHello();  // Output: Hello, my name is Haider Wain.
person2.sayHello();  // Output: Hello, my name is Omer Asif.

この例では、Person という名前のコンストラクターがあります。SayHello のようなメソッドで Person.prototype を拡張することで、このメソッドをすべての Person インスタンスのプロトタイプ チェーンに追加します。これにより、Personal の各インスタンスが共有メソッドにアクセスして利用できるようになります。各インスタンスがメソッドの独自のコピーを持つ代わりに。

一方、__proto__ プロパティ (多くの場合「ダンダー プロト」と発音されます) は、すべての JavaScript オブジェクトに存在します。JavaScript では、プリミティブ型を除くすべてのものをオブジェクトとみなすことができます。これらの各オブジェクトには、別のオブジェクトへの参照として使用されるプロトタイプがあります。__proto__ 属性は、このプロトタイプ オブジェクトへの単なる参照です。プロトタイプ オブジェクトは、元のオブジェクトにプロパティとメソッドがない場合に、プロパティとメソッドのフォールバック ソースとして使用されます。デフォルトでは、オブジェクトを作成すると、そのプロトタイプは Object.prototype に設定されます。

オブジェクトのプロパティまたはメソッドにアクセスしようとすると、JavaScript は検索プロセスに従ってそれを見つけます。このプロセスには、次の 2 つの主要な手順が含まれます。

オブジェクト自身のプロパティ: JavaScript はまず、オブジェクト自体が目的のプロパティまたはメソッドを直接所有しているかどうかを確認します。オブジェクト内でプロパティが見つかった場合は、直接アクセスして使用されます。

プロトタイプ チェーンの検索:オブジェクト自体にプロパティが見つからない場合、JavaScript はオブジェクトのプロトタイプ (__proto__ 属性によって参照される) を調べ、そこでプロパティを検索します。このプロセスは、プロパティが見つかるか、検索が Object.prototype に到達するまで、プロトタイプ チェーンを再帰的に続行します。

Object.prototype 内でもプロパティが見つからない場合、JavaScript はプロパティが存在しないことを示す未定義を返します。

4. 範囲

JavaScript コードを作成するときは、スコープの概念を理解することが重要です。スコープとは、コードのさまざまな部分にある変数のアクセス可能性または可視性を指します。例を続ける前に、ホイスティングと JavaScript コードの実行方法に詳しくない場合は、このリンクからそれについて学ぶことができます。これは、JavaScript コードがどのように動作するかをより詳しく理解するのに役立ちます。

コードスニペットを詳しく見てみましょう。

function foo() {
    console.log(a);
}


function bar() {
    var a = 3;
    foo();
}


var a = 5;
bar();

このコードでは、2 つの関数 foo() と bar() と、値 5 の変数 a を定義しています。これらの宣言はすべてグローバル スコープで行われます。bar() 関数内で、変数 a が宣言され、値 3 が割り当てられます。それでは、thebar() 関数が呼び出されたとき、a の値は何を出力すると思いますか?

JavaScript エンジンがこのコードを実行するとき、グローバル変数 a を宣言し、値 5 を割り当てます。次に、bar() 関数を呼び出します。bar() 関数内で、ローカル変数 a が宣言され、値 3 が割り当てられます。ローカル変数 a はグローバル変数 a とは異なります。その後、bar() 関数内から foo() 関数が呼び出されます。

foo() 関数内で、console.log(a) ステートメントは a の値をログに記録しようとします。ローカル変数 a は foo() 関数のスコープ内で定義されていないため、JavaScript はスコープ チェーンを検索して、a という名前の最も近い変数を見つけます。スコープ チェーンは、変数を検索して使用しようとするときに関数がアクセスできるさまざまなスコープをすべて参照します。

ここで、JavaScript が変数 a を検索する場所について説明します。bar 関数のスコープ内を調べるのでしょうか、それともグローバル スコープを調べますか? JavaScript はグローバル スコープで検索することがわかり、この動作は字句スコープと呼ばれる概念によって駆動されます。

字句スコープとは、コードに記述されている関数または変数のスコープを指します。foo 関数を定義すると、関数自体のローカル スコープとグローバル スコープへのアクセスが許可されました。この機能は、foo 関数をどこで呼び出しても、bar 関数内であっても、別のモジュールにエクスポートしてそこで実行しても変わりません。字句の範囲は、関数を呼び出す場所によって決まりません。

この結果、出力は常に同じになります。つまり、グローバル スコープで見つかった a の値、この場合は 5 になります。

ただし、bar 関数内で foo 関数を定義すると、別の状況が発生します。

function bar() {
  var a = 3;


  function foo() {
    console.log(a);
  }


  foo();
}


var a = 5;
bar();

この場合、 foo の字句スコープには、独自のローカル スコープ、bar 関数のスコープ、およびグローバル スコープという 3 つの異なるスコープが含まれます。字句の範囲は、コンパイル時にコードがソース コード内のどこに配置されるかによって決まります。

このコードが実行されると、foo は bar 関数内にあります。この配置により、音域のダイナミクスが変化します。ここで、foo が変数 a にアクセスしようとすると、まず独自のローカル スコープ内で検索します。が見つからないため、検索を bar 関数の範囲まで拡張します。なんと、 a は値 3 で存在します。したがって、コンソール ステートメントでは 3 が出力されます。

5. オブジェクトの強制

const obj = {
  valueOf: () => 42,
  toString: () => 27
};
console.log(obj + '');

検討する価値のある興味深い側面は、文字列、数値、ブール値などのプリミティブ値へのオブジェクトの変換を JavaScript がどのように処理するかです。これは興味深い質問で、キャストがオブジェクトに対してどのように機能するかを理解しているかどうかをテストします。

この変換は、文字列の連結や算術演算などのシナリオでオブジェクトを操作する場合に重要です。これを実現するために、JavaScript は valueOf と toString という 2 つの特別なメソッドに依存します。

valueOf メソッドは、JavaScript オブジェクト変換メカニズムの基本的な部分です。プリミティブ値を期待するコンテキストでオブジェクトが使用される場合、JavaScript は最初にオブジェクト内の valueOf メソッドを探します。

valueOf メソッドが存在しないか、適切なプリミティブ値を返さない場合、JavaScript は toString メソッドに戻ります。このメソッドは、オブジェクトの文字列表現を提供する役割を果たします。

元のコード スニペットに戻ります。

const obj = {
  valueOf: () => 42,
  toString: () => 27
};


console.log(obj + '');

このコードを実行すると、オブジェクト obj がプリミティブ値に変換されます。この場合、valueOf メソッドは 42 を返し、空の文字列との連結により暗黙的に文字列に変換されます。したがって、コードの出力は 42 になります。

ただし、valueOf メソッドが存在しない場合、または適切なプリミティブ値を返さない場合、JavaScript は toString メソッドにフォールバックします。前の例を変更してみましょう。

const obj = {
  toString: () => 27
};


console.log(obj + '');

ここでは、valueOf メソッドを削除し、数値 27 を返す toString メソッドだけを残しています。この場合、JavaScript はオブジェクト変換のために toString メソッドを使用します。

6. オブジェクトキーを理解する

JavaScript でオブジェクトを操作する場合、他のオブジェクトのコンテキストでキーがどのように処理され、割り当てられるかを理解することが重要です。次のコード スニペットを検討し、時間をかけて出力を推測してください。

let a = {};
let b = { key: 'test' };
let c = { key: 'test' };


a[b] = '123';
a[c] = '456';


console.log(a);

一見すると、このコードは 2 つの異なるキーと値のペアを持つオブジェクト a を生成するように見えます。ただし、JavaScript がオブジェクト キーを処理する方法により、結果はまったく異なります。

JavaScript は、デフォルトの toString() メソッドを使用してオブジェクトのキーを文字列に変換します。しかし、なぜ?JavaScript では、オブジェクトのキーは常に文字列 (またはシンボル) であるか、暗黙的なキャストによって自動的に文字列に変換されます。文字列以外の値 (数値、オブジェクト、記号など) をオブジェクトのキーとして使用する場合、JavaScript は値をキーとして使用する前に内部でその値を文字列表現に変換します。

したがって、オブジェクト b とオブジェクト c をオブジェクト a のキーとして使用すると、両方とも同じ文字列表現 [object Object] に変換されます。この動作のため、2 番目の割り当て a[b] = '123'; は最初の割り当て a[c] = '456'; をオーバーライドします。

それでは、コードを段階的に分解してみましょう。

  • let a = {};: 空のオブジェクト a を初期化します。

  • let b = { key: 'test' };: プロパティ キーが 'test' であるオブジェクト b を作成します。

  • let c = { key: 'test' };: b と同じ構造を持つ別のオブジェクト c を定義します。

  • a[b] = '123';: オブジェクト a のキー [object Object] を持つプロパティを値 '123' に設定します。

  • a[c] = '456';: オブジェクト a のキー [object Object] の同じ属性の値を '456' に更新し、以前の値を置き換えます。

どちらの割り当てでも同じキー文字列 [object Object] を使用します。その結果、2 番目の割り当ては、最初の割り当てで設定された値をオーバーライドします。

オブジェクト a をログに記録すると、次の出力が観察されます。

{ '[object Object]': '456' }

7. == 演算子

console.log([] == ![]);

これはもう少し複雑です。それで、出力は何になると思いますか? 段階的に評価してみましょう。まず 2 つのオペランドの型を見てみましょう。

typeof([]) // "object"
typeof(![]) // "boolean"

[] の場合はオブジェクトであることは理解できます。JavaScript では、配列や関数を含むすべてがオブジェクトです。しかし、オペランド ![] はどのようにしてブール型を持つことができるのでしょうか? これを理解してみましょう。プリミティブ値に ! を使用すると、次の変換が行われます。

False: 元の値が false 値 (false、0、null、unknown、NaN、または空の文字列 '' など) の場合、! を適用すると true に変換されます。

true-value: 元の値が true 値 (false 以外の値) の場合、適用されます。falseに変換されます。

この場合、[] は空の配列であり、JavaScript では true の値です。[] が true であるため、![] は false になります。したがって、式は次のようになります。

[] == ![]
[] == false

それでは、== 演算子に移りましょう。JavaScript は、== 演算子を使用して 2 つの値を比較するたびに、抽象等価比較アルゴリズムを実行します。 

アルゴリズムには次の手順があります。

6aedf5d74cfce24fe2e17b79dd0afb25.png

ご覧のとおり、アルゴリズムは比較される値のタイプを考慮し、必要な変換を実行します。

この例では、x を [] と表し、y を ![] と表します。x と y の型を確認したところ、x はオブジェクト、y はブール値であることがわかりました。y はブール値、x はオブジェクトであるため、抽象等価比較アルゴリズムの条件 7 が適用されます。

Type(y) が Boolean の場合、x == ToNumber(y) の比較結果を返します。

これは、型の 1 つがブール値の場合、比較する前にそれを数値に変換する必要があることを意味します。ToNumber(y) の値は何ですか? ご覧のとおり、[] は真理値であり、否定すると偽になります。その結果、Number(false) は 0 になります。

[] == false
[] == Number(false)
[] == 0

これで、comparison[] == 0 となり、今度は条件 8 が適用されます。

Type(x) が String または Number で、Type(y) が Object の場合、比較 x == ToPrimitive(y) を返します。

この条件に基づいて、オペランドの 1 つがオブジェクトである場合、それをプリミティブ値に変換する必要があります。ここで ToPrimitive アルゴリズムが登場します。[]x をプリミティブ値に変換する必要があります。配列は JavaScript のオブジェクトです。前に見たように、オブジェクトをプリミティブに変換するときに、valueOf メソッドと toString メソッドが機能します。 

この場合、valueOf は配列自体を返しますが、これは有効なプリミティブ値ではありません。そこで、出力として toString を使用します。toString メソッドを空の配列に適用すると、空の文字列が生成されます。これは有効なプリミティブです。

[] == 0
[].toString() == 0
"" == 0

空の配列を文字列に変換すると、結果は空の文字列 "" になります。ここで、比較 "" == 0 に直面します。

ここで、オペランドの 1 つが文字列型で、もう 1 つが数値型である場合、条件 5 が成立します。

Type(x) が String で、Type(y) が Number の場合、比較 ToNumber(x) == y を返します。

したがって、空の文字列 "" を数値 0 に変換する必要があります。

"" == 0
ToNumber("") == 0
0 == 0

最後に、両方のオペランドの型が同じであり、条件 1 が true です。両方の値が同じであるため、最終的な出力は次のようになります。

0 == 0 // true

これまで検討してきた最後のいくつかの質問では、キャストを使用しました。これは、JavaScript をマスターし、面接でよく聞かれるこの種の質問に取り組む上で重要な概念です。 

8. 締めくくり

これは、閉店に関する最も有名な面接の質問の 1 つです。

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('Index: ' + i + ', element: ' + arr[i]);
  }, 3000);
}

出力がわかれば大丈夫です。それでは、この断片を理解してみましょう。表面的には、このコード スニペットにより次の出力が得られます。

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

しかし、ここではそうではありません。実際の出力は、クロージャの概念と JavaScript が変数スコープを処理する方法によって異なります。setTimeout コールバックが 3000 ミリ秒の遅延後に実行されると、両方とも同じ変数 i を参照し、ループ完了後の最終値は 4 になります。結果として、コードの出力は次のようになります。

Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined

この現象は、var キーワードにブロック スコープがなく、setTimeout コールバックが同じ i 変数への参照をキャプチャするために発生します。コールバックが実行されると、すべてのコールバックは i の最終値 (4) を確認し、未定義の arr[4] にアクセスしようとします。

目的の出力を実現するには、 let キーワードを使用してループの反復ごとに新しいスコープを作成し、各コールバックが i の正しい値を確実に取得できるようにします。

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('Index: ' + i + ', element: ' + arr[i]);
  }, 3000);
}

この変更により、期待どおりの出力が得られます。

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

let を使用すると、反復ごとに i の新しいバインディングが作成され、各コールバックが正しい値を参照するようになります。

多くの場合、開発者は let キーワードを含むソリューションにすでに精通しています。ただし、面接ではさらに一歩進んで、let を使用せずに問題を解決することが求められることがあります。この場合のもう 1 つのアプローチは、ループ内で関数 (IIFE) をすぐに呼び出してクロージャを作成することです。このようにして、各関数呼び出しには i の独自のコピーが含まれます。あなたはこれを行うことができます:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  (function(index) {
    setTimeout(function() {
      console.log('Index: ' + index + ', element: ' + arr[index]);
    }, 3000);
  })(i);
}

このコードでは、すぐに呼び出される関数 (function(index) { ... })(i); が反復ごとに新しい範囲を作成し、i の現在値を取得してインデックス パラメーターとして渡します。これにより、各コールバック関数が独自の個別のインデックス値を持つようになり、クロージャに関連する問題が回避され、期待どおりの出力が得られます。

Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21

最終的なまとめ

以上が JS に関するフロントエンド面接の 8 つの質問であり、今日この記事で共有したいと思います。この記事が面接の準備に役立つことを願っています。 

まだご質問がある場合は、メッセージ領域にメッセージを残してください。学習の進捗状況を一緒に交換します。

最後に、読んでフォローしていただきありがとうございます。今後も質の高い記事をお届けいたします。

ファンの特典

Tailwind Css に基づいた NodeJs バックグラウンド テンプレートのソース コードを共有します。nodejs と tailwindcss を学習したい場合は、このソース コードをお見逃しなく。

もっとスキルを学ぶ

以下の公開番号をクリックしてください

6040a5181fe547da96563d41de21b6eb.gif

おすすめ

転載: blog.csdn.net/Ed7zgeE9X/article/details/132702997