JavaScriptで楽しいパズルを解く

私たちの話は、トーマス・ラコマからのツイートから始まり ます。そこでは、そのような質問がインタビューであなたに会ったことを想像してみてください。







インタビューでのそのような質問に対する反応は、それが正確に何であるかに依存しているように私には思えます。質問が本当に値が何であるかである場合 tree



、コードをコンソールに挿入するだけで結果を得ることができます。



ただし、 この問題をどのように解決するかが問題である場合は、すべてが非常に興味深くなり、JavaScriptとコンパイラの複雑さに関する知識のテストにつながります。この記事では、この混乱をすべて整理し、興味深い結論を得ようとします。



この問題を解決するためのプロセスをTwitchでストリーミングしていました 放送は長いですが、そのような問題を解決するための段階的なプロセスをもう一度見ることができます。



一般的な推論



まず、コードをコピー可能な形式に変換しましょう。



let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
 + --d+ + +b-- +
 + +d*b+ +
 u
      
      





私はすぐにいくつかの特殊性に気づき、いくつかのコンパイラのトリックをここで使用できると判断しました。ご覧のとおり、JavaScriptは通常、中断できない式がない限り、各行の最後にセミコロンを追加します 。この場合、 +



各行の終わりで、この構築を中断する必要がないことをコンパイラーに通知します。



最初の行は、単に3つの変数を作成し、それらに値を割り当てます 3



3



はプリミティブ値であるため、コピーが作成されるたびに値によって作成さ れるため、すべての新しい変数は値を使用して作成されます。 3



..。JavaScriptが参照によってこれらの変数に値を割り当てる場合 、新しい各変数以前に使用さ変数 を指しますが、それ自体の値は作成しません。



追加情報



演算子の優先順位と結合性



これらは、この困難な課題を解決するための重要な概念です。つまり、JavaScript式の組み合わせが評価される順序を定義します。



オペレーターの優先順位



Q:これら2つの表現の違いは何ですか?



3 + 5 * 5
      
      





5 * 5 + 3
      
      





結果の観点からは、違いはありません。学校の数学の授業を覚えている人なら誰でも、足し算の前に掛け算が行われることを知っています。英語では、順序をBODMAS(Brackets Off Divide Multiply Add Subtract-ブラケット、次数、除算、乗算、加算、減算)として覚えています 。 JavaScriptには、Operator Precedenceと呼ばれる同様の概念があります。これは、式を評価する順序を意味します。最初3 + 5



計算を強制したい場合 は、次のようにします。



(3+5) * 5
      
      





演算子は演算子()



よりも優先順位が高い ため、括弧は式のこの部分を最初に評価するように強制します *







各JavaScript演算子が優先されるため、非常に多くの演算子が含まれ tree



ているため、それらが評価される順序を把握する必要があります。--



が値b



を変更する かが特に重要 d



であるため、これらの式が残りの式と比較していつ評価されるかを知る必要があり tree



ます。



重要: オペレーター優先順位表と追加情報



結合性



結合性は、同じ優先順位の演算子で式が評価される順序を決定するために使用されます。例えば:



a + b + c
      
      





演算子は1つしかないため、この式には演算子の優先順位はありません。では、どのように計算(a + b) + c



するのa + (b + c)



でしょうどのように、どのように計算 するのでしょう



結果は同じになることはわかっていますが、コンパイラーは、最初に1つの操作を選択してから計算を続行できるように、これを知る必要があります。この場合、正解は (a + b) + c



、演算子が+



結合性のままであるため です。つまり、左側の式を最初に評価します。



「なぜすべての演算子を結合性のままにしないのですか?」とあなたは尋ねるかもしれません。



さて、このような例を見てみましょう:



a = b + c
      
      





左の結合性の式を使用すると、次のようになります。



(a = b) + c
      
      





しかし、待ってください、これは奇妙に見えます、そしてそれは私が意図したことではありません。この式を左の結合性のみを使用して機能させたい場合は、次のようなことを行う必要があります。



a + b = c
      
      





これは(a + b) = c



、つまり最初にa + b



変換され 次にこの結果の値が変数に割り当てられます c







このように考える必要がある場合、JavaScriptははるかに混乱します。そのため、演算子ごとに異なる関連付けを使用します。これにより、コードが読みやすくなります。を読む a = b + c



と、すべてがより巧妙に内部に配置され、右結合と左結合のオペランドを使用しているにもかかわらず、計算の順序は自然に見えます。



あなたはおそらく結合性の問題に気づいたでしょう a = b + c



..。両方の演算子の結合性が異なる場合、最初に評価する式をどのようにして知ることができますか?回答:前のセクションのように、演算子の優先順位が高い方 この場合、 +



優先度が高いため、最初に計算されます。



記事の最後に詳細な説明を追加しました または、詳細を読む ことができます



ツリー式がどのように評価されるかを理解する



これらの原則を理解すると、問題の分析を開始できます。多くの演算子を使用しており、括弧がないため理解が困難です。それでは、括弧を追加して、使用されているすべての演算子とその優先順位および結合性をリストしてみましょう。



(変数xの演算子): 優先順位 結合性
x ++: 18 ない
バツ - : 18 ない
++ x: 17 正しい
- バツ: 17 正しい
+ x: 17 正しい
*: 15
x + y: 14
=: 3 正しい


括弧



ここで言及する価値があるのは、括弧を正しく追加するのは難しい作業です。各段階で答えが正しく計算されていることを確認しましたが、括弧が常に正しく配置されているとは限りません。ブレースの自動配置ツールをご存知の場合は、メールでお問い合わせください。



式が評価される順序を理解し、それを示すために括弧を追加しましょう。優先度の最も高い演算子から下に移動して、最終結果に到達した方法を段階的に説明します。



接尾辞++および接尾辞-



const tree = ++d * d*b * (b++) +
 + --d+ + +(b--) +
 + +d*b+ +
 u
      
      





単項+、接頭辞++および接頭辞-



ここで小さな問題がありますが、単項演算子を評価することから始め +



、次に問題のポイントに到達します。



const tree = ++d * d*b * (b++) +
 + --d+ (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





そして、これは困難が生じるところです。



+ --d+
      
      





--



+()



同じ優先順位持っています。それらを計算する順序をどのように知ることができますか?より簡単な方法で問題を定式化しましょう。



let d = 10
const answer = + --d
      
      





+



これは加算ではなく、単項プラス、つまり積極性であることを忘れないでください 。あなたはそれをとして知覚することができます -1



、ここでのみそれは +1



です。



この優先順位の演算子は右結合であるため、解決策は右から左に評価 することです。



したがって、式はに変換され + (--d)



ます。



これを理解するには、すべての演算子が同じであると想像してみてください。この場合には、 + +1



等しくなる (+ (+1))



で論理に従って 1 — 1 — 1



と等価 ((1 — 1) — 1)



..。括弧付きの表記での右側の結合演算子の結果は、左側の演算子の場合とは逆であることに注意してください。



同じロジックを問題点に適用すると、次のようになります。



const tree = ++d * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





そして最後に、後者の括弧を挿入することにより ++



、次のようになります。



const tree = (++d) * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





掛け算(*)



ここでも結合性を処理する必要がありますが、今回は同じ演算子を使用します。これは結合性のままです。前のステップと比較して、これは簡単なはずです!



const tree = ((((++d) * d) * b) * (b++)) +
 (+ (--d)) + (+(+(b--))) +
 (+(+((d*b) + (+u))))
      
      





すでに計算を開始できる段階に達しています。代入演算子に括弧を追加することは可能ですが、読みやすくなるよりも混乱するので、そうはしません。上記の式はもう少し複雑であることに注意してください x = a + b + c







単項演算子の一部を短縮することはできますが、重要な場合に備えて保存しておきます。



式をいくつかの部分に分割することで、計算の個々の段階を理解し、それらに基づいて構築することができます。



let b = 3, d = b, u = b;
 
const treeA = ((((++d) * d) * b) * (b++))
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





これで、さまざまな値の計算を検討し始めることができます。treeAから始めましょう。



TreeA



let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
      
      





ここで最初に評価されるのは、++d



を返し4



、インクリメント する式 です d







// b = 3
// d = 4
((4 * d) * b) * (b++)
      
      





次に実行され 4*d



ます。この段階では、dは4であるため、4*4



16であることわかり ます。



// b = 3
// d = 4
(16 * b) * (b++)
      
      





このステップの興味深い点は、bインクリメントする前にbを乗算する ため、計算が左から右に実行されることです。 16 * 3 = 48



..。



// b = 3
// d = 4
48 * (b++)
      
      





上記で++



は、よりも優先度が高い ものについて説明した *



ので、これはと書くことができますが 48 * b++



、ここには他のトリックがあります。戻り値b++



インクリメント後ではなくインクリメント前のです したがって、bが最終的に4になっても、乗算値は3になります。



// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
      
      





48 * 3



は等しい 144



ので、最初の部分を計算すると、bとdは4に等しくなり、式の結果は次のようになります。 144







let b = 4, d = 4, u = 3;
 
const treeA = 144
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeB



const treeB = (+ (--d)) + (+(+(b--)))
      
      





この時点で、単項演算子は実際には何もしないことがわかります。それらを短くすると、表現が大幅に簡略化されます。



// b = 4
// d = 4
const treeB = (--d) + (b--)
      
      





このトリックはすでに上で見ました。 --d



を返しますが 3



、を b--



返します 4



が、式が評価されるまでに、両方に値3が割り当てられます。



const treeB = 3 + 4
// b = 3
// d = 3
      
      





したがって、タスクは次のようになります。



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeC



そして、ほぼ完了です!



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
      
      





まず、これらの煩わしい単項演算子を取り除きましょう。



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
      
      





削除しましたが、ここではブラケットなどに注意する必要があります。



// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
      
      





今ではかなり簡単です。 3 * 3



等しい 9



9 + 3



等しい 12



、そして最後に、私たちは...



回答!



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
      
      





144 + 7 + 12



等しい 163



問題への答え: 163







結論



JavaScriptは、奇妙で楽しい方法であなたを困惑させる可能性があります。しかし、言語がどのように機能するかを理解することで、これの最も根本的な理由にたどり着くことができます。



一般的に言って、解決策への道は答えよりも有益である可能性があり、途中で見つかったミニ解決策は私たちに何かを教えることができます。



ブラウザコンソールを使用して作業を確認したことは言うまでもありません。基本的な原則に基づいて問題を解決するよりも、ソリューションをリバースエンジニアリングする方が面白かったです。



問題を解決する方法を知っていても、途中で対処する必要のある構文上のあいまいさがたくさんあります。そして、私たちの木の表現を見たときに、多くの人が気づいたと思います。それらのいくつかを以下にリストしましたが、それぞれが別々の記事の価値があります!



また、https://twitter.com/AnthonyPAliceaに感謝します。コースがなければ、すべてを理解することはできませんでした。この質問については、 https://twitter.com/tlakomyに感謝し ます



ノートと奇妙なこと



解決策を見つけるプロセスが透明なままになるように、途中で遭遇したミニなぞなぞを別のセクションで強調しました。



変数の順序の変更がどのように影響するか



このビデオを見る



let x = 10
console.log(x++ + x)
      
      





ここでいくつかの質問をすることができます。コンソールに何が出力さx



れ、2行目の値は何 ですか?



これが同じ数だと思うなら、すみません、私はあなたを裏切った。トリックは x++ + x



として計算されるもの (x++) + x



であり、JavaScriptエンジンが左側を計算する とき(x++)



、インクリメント x



を実行するため、に関しては + x



、xの値はに等しく 11



、ではありません 10







もう1つのトリッキーな質問-x++



それはどのような値を返し ますか?



私は答えが実際に何であるかについてかなり明白な手がかりを与えました 10







これは間の差である x++



++x



演算子の基礎となる関数を見ると、次のようになります。



function ++x(x) {
  const oldValue = x;
  x = x + 1;
  return oldValue;
}
function x++(x) {
  x = x + 1;
  return x
}
      
      





このように見ると、



let x = 10
console.log(x++ + x)
      
      





はそれx++



が返す ものを意味し 10



、評価時の + x



値は 11



です。したがって、コンソール 21



に出力され、値xはに等しくなり 11



ます。



この比較的単純なタスクは、コード全体で使用される一般的なアンチパターン(乱雑 な式副作用)を示しています。 詳細。



優先順位は同じで関連性が異な​​る2つの演算子が存在する可能性はありますか?



順番に移動して、今のところ「結合性」という言葉を忘れましょう。



演算子+



=



、を取り、状況を要約してみましょう 結合性のままであるため、 として計算された



ものの上に表示 a + b + c



されました それが正しい結合であるため として計算され ます。 変数に割り当てられた値を返すため、式を評価した後の値と等しくなることに注意してくださいオペランドをそれらの優先順位に置き換えましょう: (a + b) + c



+







a = b = c



a = (b = c)



=



=



a



b











a left b left c = (a left b) left c
a right b right c = a right (b right c)

  

a left b right c = ?
a right b left c = ?
      
      





2番目の例は論理的に不可能であることがわかりますか? a + b = c



+



優先される ためにのみ可能 =



であるため、パーサーは何をすべきかを知っています。2つの演算子の優先順位が同じで、結合性が異なる場合、構文パーサーはアクションを実行する順序を決定できません。



したがって、要約すると、いいえ、同じ優先順位を持つ演算子は異なる結合性を持つことはできません!



F#で関数の結合法則をその場で変更できるのは不思議です。そのため、私は気が狂うことなく結合法則について話すことができました。 詳細。



単項演算子



興味深い点は、計算の順序を解析するときに発見した +n



++n



数値を返すため



実行できず 、数値をインクリメントまたはデクリメントできず、意味があいまいなため 実行できません (これ または ?以下のコメントを参照)。ただし、これは実行できます。 -- -i



-



---i



---



-- -



- --







let i = 10
console.log(-+-+-+-+-+--i)
      
      





混乱した積極性



最も問題のある問題の1つは、+



JavaScriptのあいまいさでした 以下に示すように、同じ記号が4つの異なる機能で使用されます。



let i = 10
console.log(i++ + + ++i)
      
      





各オペランドには、独自の意味、優先度、および結合性があります。それは私に有名な単語パズルを思い出させます:



バッファローバッファローバッファローバッファローバッファローバッファローバッファローバッファロー



単項演算子または割り当て?



+



単項演算子または割り当てのいずれかを意味する場合があります。u



記事の冒頭から問題が発生した場合はどう なりますか?



... +
u
      
      





最終的に答えは...何であるかに依存します。すべてを1行で書いたら



... + u
      
      





そして、答えはで異なるだろう x + u



x - + u



前者の場合、記号は加算を意味し、後者の場合は単項を意味し +



ます。それが何を意味するのかを理解する唯一の方法は、表現する演算子が1つだけ残るまで、式の残りの部分を解析することです。






広告



最新のハードウェア、攻撃保護、および膨大な数のオペレーティングシステムを備えたプログラマー向けのVDS最大構成は、128 CPUコア、512 GB RAM、4000 GBNVMeです。






All Articles