2025年5月5日月曜日

JSON.parse(JSON.stringify())で深いコピーをしないでという話

javascriptで深いコピーをする方法について

前回は浅いコピーの話をしたので今回は深いコピーの話

■JSON.parse(JSON.stringify(配列))

let originalarr= Array(36).fill().map(() => Array(8).fill(0));
let copiedarr= JSON.parse(JSON.stringify(originalarr));
copiedarr[0][0] = 1; console.log(originalarr[0][0]); // 出力: 0 (影響を受けない)

 この方法は有名で簡潔に深いコピーを実現できます
ただし、JSON.parse(JSON.stringify(配列)) は、手軽に深いコピーを実現できる反面、undefined、関数、Date オブジェクト、RegExp オブジェクト、Map、Set、循環参照などの特定のデータ型を正しく扱えないという大きな制約があります。


この方法でコピーするのがおすすめのパターン

以下のような比較的単純な構造のオブジェクトや配列を深いコピーする場合には手軽で有効です。
・プリミティブな値 (number, string, boolean, null) のみを含む配列やオブジェクト
・プレーンな JavaScript オブジェクト (POJO - Plain Old JavaScript Object) で、上記のプリミティブな値と基本的なオブジェクト、配列のみで構成されている場合


そもそもこれは何をやっているのか

使い方と使う場面を知ったら次は仕組みの理解。

■そもそもJSONってなに?
JSON 「JavaScript Object Notation」の略です。
JavaScript のオブジェクトを軽量なテキスト形式で表現するための標準的なデータ形式です。その名前が示す通り、JavaScript のオブジェクトの記法をベースにしていますが、言語に依存しないため、様々なプログラミング言語やシステム間でデータを交換する際に広く利用されています。


JSON.stringify() メソッド

まずは内部から理解しましょう。
構文は以下パターン。
JSON.stringify(value)
JSON.stringify(value, replacer)
JSON.stringify(value, replacer, space)

「JSON 文字列に変換する」というのが根本的な機能です。
例外のTypeErrorが発生するのは「value が循環参照を含む場合・長整数値に遭遇した場合」です。
上記に記載している通り色々な言語間のデータをやり取りなどが可能な文字列形式にするという事です。ですので、JSON.stringify(配列)を行うとstringを出力します。 


JSON.parse() 静的メソッド

JSON 文字列を JavaScript のオブジェクトや配列に変換します。
つまり、逆の事を行います。JSON 文字列を解析して、元の配列やオブジェクトにします。


■つまり?

つまり、「JSON.parse(JSON.stringify(配列))」というのは配列を一旦文字列にしてそれを再度配列に戻すという事を行っています。
一旦文字列になる事で配列ではなくなるので新たに再変換されて出力される時に新しい配列になるので多次元配列がディープコピーできるのです。
また、この処理を行う時に文字列に上手く変換できない値達は欠損するという事です。


■素直に配列でコピーする

関数化する

function deepCopyMultiDimensionalArray(arr) {
if (!Array.isArray(arr)) {
return arr; // 配列でなければそのまま返す (プリミティブな値やオブジェクトなど)
} const newArray = [];
for (let i = 0; i < arr.length; i++) {
newArray[i] = deepCopyMultiDimensionalArray(arr[i]); // 要素が配列なら再帰的にコピー
}
return newArray;
} //ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー // 使用例 let originalMulti = [1, [2, [3, 4]], 5, { a: 6 }];
let copiedMulti = deepCopyMultiDimensionalArray(originalMulti); copiedMulti[1][1][0] = 99;
copiedMulti[3].a = 100; console.log("オリジナル:", originalMulti);
// 出力: オリジナル: [ 1, [ 2, [ 3, 4 ] ], 5, { a: 6 } ]
console.log("コピー:", copiedMulti);
// 出力: コピー: [ 1, [ 2, [ 99, 4 ] ], 5, { a: 100 } ] let originalWithObject = [1, [2, { b: 7 }]];
let copiedWithObject = deepCopyMultiDimensionalArray(originalWithObject); copiedWithObject[1][1].b = 101;
console.log("オリジナル (オブジェクトあり):", originalWithObject);
// 出力: オリジナル (オブジェクトあり): [ 1, [ 2, { b: 7 } ] ]
console.log("コピー (オブジェクトあり):", copiedWithObject);
// 出力: コピー (オブジェクトあり): [ 1, [ 2, { b: 101 } ] ]

こんな感じ


■二次元配列のコピーでいい場合

まぁでも多次元配列なんてあんまり使う物でもないと思いますので(多次元で深くネストすると人間も何処に何があるか分かり難くなるので)、大半は二次元配列とかが多い(セルや座標)と思います。

その程度であれば1次配列を[...配列]で展開して回すだけでも十分です。
function deepcopy2d(oa) {return oa.map(ar=>[...ar]);}


■structuredClone(obj)

実はもっと簡単な方法があります。

let original = {
  date: new Date(),
  regex: /abc/g,
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3]),
  arrayBuffer: new ArrayBuffer(8),
  circular: {}
}; original.circular.self = original;
let cloned = structuredClone(original);
console.log(cloned.date); // 元の Date オブジェクトとは異なる新しい Date オブジェクト console.log(cloned.regex); // 元の RegExp オブジェクトとは異なる新しい RegExp オブジェクト
console.log(cloned.map);  // 元の Map オブジェクトとは異なる新しい Map オブジェクト
console.log(cloned.set);  // 元の Set オブジェクトとは異なる新しい Set オブジェクト
console.log(cloned.arrayBuffer); // 元の ArrayBuffer とは異なる新しい ArrayBuffer
console.log(cloned.circular.self === cloned); // true (循環参照も正しくコピー)
cloned.date.setTime(0);
console.log(original.date.getTime() === cloned.date.getTime()); // false (深いコピー)

こんな感じです。
見ての通り、より多くのデータ型をサポート: Date オブジェクト、RegExp オブジェクト、Map、Set、ArrayBuffer、DataView、ImageBitmap、ImageData など、JSON.stringify() では正しく扱えなかった多くの型を適切にコピーできます。

循環参照を処理: structuredClone() は循環参照のあるオブジェクトグラフも正しくコピーできます。内部的にコピー済みのオブジェクトを追跡し、無限ループを防ぎます。

Transferable オブジェクトの転送: ArrayBuffer などの Transferable オブジェクトは、コピーではなく所有権が移動するため、より効率的なデータの受け渡しが可能です(元のオブジェクトでは使用できなくなります)。

より自然なコピー: JSON.stringify() のように一旦文字列にシリアライズするステップがないため、より直接的で自然なコピー処理が行われます。


しかもwindow.のグローバル関数なのでこれでオールOK!
実装されたのは2020年頃でnodeも2021年頃実装しています。

2025年4月28日月曜日

ランドメイク&全イベント統合シミュレーターを作ろうかなって話

「 Hey!Scripting Cat!全イベントのシミュレーションもしたいよ!」

HAHAHA!なかなか無理をいいますね。この令和の時代に需要が何処にあるんですか。
ひとまずは「ランドメイクシミュレーター」は作ってあるからそれを使ってね
現状でもAFイベントはセットになってるからプレイ時に可能な配置をシミュレーションできるよ。
とは言え確かにあそこまでやるならもう全部のイベントの消滅条件とかまで管理しちゃえばいいんじゃない?という気もする。あと連続(アンドゥ・リドゥ)描画をした時にちょっと遅いのでそこを解消したいですね。

ランドメイク&全イベント管理を考える

まずは現在のランドメイクシミュレーターの機能と追加したい機能

■現在のランドメイクシミュレーターの機能
・全体マップを選択する
・ランドメイクマップを生成6x6(地形と土地マナ)
・ポスト入手→配置→積み木入手→配置
積み木まではイベントがない為配置と同時にAF取得
以下ループ
┏┓
┃┃
┃■イベント開始
┃┃
┃■イベント終了
┃┃
┃■AF入手
┃┃
┗┛
・常に工程を復元用ログに保存

■現在サブ機能
・全体マップの表示非表示
・全体マップ選択範囲クリック選択
・全体マップ選択範囲矢印で操作
・推奨エリアの直接選択
・配置とイベント両方のログ
・配置とイベント両方のアンドゥリドゥ

ーーーーーーーーーーー

■初期化
・Fマップ生成
・Lマップ生成
・リスナー設置
 ┗Fマップ 25x25
 ┗Lマップ 6x6
 ┗ボタン類 12個前後
 ┗AF 26 
 ┗イベント 約68x2
 ┗チェック 2程度

■操作
Fマップ選択>インデックス変更判定>変更時再初期化
繰り返し
 ┗イベント開始or終了をチェック
 ┗AFを選択
 ┗AFを設置
 ┗イベントの取得

■イベント
・開始または終了チェック時にAF取得判定
・終了チェックでランドロック解除
・履歴加算

■AF設置時
・AF選択時 他の操作を停止、AF情報表示、配置可能エリア光らせる
・(キャンセル時:各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・AF配置 AFがランドになる際に周囲4つのマナ合計÷2を加算して配置、周囲4つのランドはAFのマナを直加算
・配置終了時(各操作停止解除、AF情報を閉じる、配置エリアを戻す)
・設置終了時イベントを解放
・設置終了時履歴加算
 
■履歴操作
・各操作でイベント操作を記憶
・アンドゥ、肯定を1つ戻す
・リドゥ、履歴範囲内で履歴を一つ進める
・履歴操作中に履歴加算がある場合現在の位置を履歴の末端に変更

■状態の保存復元
SaveURLに現在の状態をパラメータで保存
URLにアクセスするだけで復元可能

鶏が先か卵が先か

・イベントが終了するとAF取得(ない時もある)
・AFを設置してランド生成でイベント生成

ランドが無いとイベントがそもそも発生しないのでAFが先
ポストを配置→ホーム生成→ナンバリング0イベント草人→AF入手

2025年4月27日日曜日

onclickを廃止した話

「Hey!Scripting Cat!モジュール読み込みにしたらボタンのonclickで関数が実行できなった!どうすればいい?」

なるほど良くある話ですね。HTML の onclick 属性は、イベント発生時にグローバルスコープで評価されるため、モジュールスコープ内で定義された関数を直接参照しようとすると、その関数が見つからず実行できません。

じゃあonclickが使えなくなるのかと言えばそうではありません。

モジュール読み込みでボタンから関数を実行する方法

web上のjavascriptにはグローバル領域に直接スクリプトを読み込む方法とモジュール(module)で読み込む方法の主に二種類があります。
今回の目標はボタンを押して関数を実行する事が目的です。
工程は2つあります「ボタンを押す」「関数実行」。これを実現する方法は2つ存在します。


1.window.領域に関数を持ってくる(非推奨)

今までの感覚を変更しないのであれば一番簡単です。
変更するのは関数側です。

■js
function moduleFunction() {console.log('関数実行');}
window.moduleFunction = moduleFunction;

これだけです。htmlも変更不要で簡単ですね。
現在onclickが動かないというのはつまりグローバル領域に関数が居ない為です。
じゃあ何処にいるのかというとモジュールスコープ領域に居ます。

つまり、window.関数名に現在の関数名を代入すればモジュール内の関数をhtml側からonclickで実行できます。折角モジュール読み込みするのであれば(スコープを気にし始めたのなら)一応非推奨ではありますが他の関数等はモジュールにいるので今までの通りの方法「<script src="script.js"></script>」でただjsを読み込むだけより全然良いと思います。
onclickが出来ないならレガシーな通常読み込みに戻そうと考えてしまうより一旦はこの方法で良いと思います。モダンへの入り口は広い方がいい。


モジュールに移行する理由

個人や小規模なサイトであれば必ずしもモジュール読み込みの形式にする必要はありません。ただ、そもそもモジュールで読み込む方法が生まれたという事は理由が、つまり利点があるから実装されました。

最大の理由はおそらく名前の衝突です。グローバルスコープに全ての変数や関数があると衝突して正しく動かなくなる可能性高くなります。

<script type="module" ></script>で直接モジュール領域を作ったり
<script type="module" src="script.js"></script>モジュール読み込みを行うとグローバルスコープを侵さずに衝突しない領域を作ることができます。

次に個々に領域を分断した事でそれをつなぐインポートとエクスポートが便利だからです。
これも結局はスコープの制御がしやすいという話に帰結します。


2.addEventListenerでボタン待ちをする

さて、次はよりモダンな方法です。
まずはボタンにidを付けます(他にも方法はある後述)。
■html
<button id="mybutton">ボタン</button>
■js
function moduleFunction() {console.log('関数実行');}
document.getElementById("mybutton").addEventListener('click', moduleFunction)

これだけです。それほど難しくはなりません。


モジュールのトップレベルのコードは、モジュールが最初に読み込まれ、評価される際に一度だけ実行されます。このタイミングで addEventListener を呼び出すことで、ボタン要素にクリックイベントのリスナーが登録され、ボタンがクリックされるのを待機する状態になります。

これによってボタンが押されるとidで対象のボタンが特定されクリックが実行されたことでmoduleFunctionを実行できます。
少し注意点としては「addEventListener('click', moduleFunction)」の関数名部分(この場合「moduleFunction」)には「()」をつけてはいけません。
関数登録に括弧が付くとその位置で即時関数が実行されてしまいます。

※ボタンを特定する他の方法
id 以外に、nameやclassやタグ名で要素を取得できます。
・class(例: document.querySelector('.mybutton'))
・タグ(例: document.querySelectorAll('button'))


引数を渡したい場合

括弧が無ければじゃあ引数渡せないのかというとそうではなく
document.getElementById("mybutton").addEventListener('click', ()=>{moduleFunction(1)})
とすれば引数を渡す事が出来ます。
これでonclickとはさよならできますね。

関数に飛ばさず直接処理する場合

document.getElementById("mybutton").addEventListener('click', ()=>{ここに処理を記述する})

こうするだけでOK。
さらっと流してるアロー関数部分は前の記事を参照してください。(https://blackstraycatreboot.blogspot.com/2025/04/blog-post.html)
要するに先ほどアロー関数に関数名を記述してるのは無名関数にする事で処理領域を確保して、この領域で改めて関数にキックしてたわけです。
ですから、ここに関数を記述せずに処理を記述すればOK!



余談

ちなみに「addEventListener」は実行時にイベントオブジェクトを渡します。
この場合、'click' イベントに対するリスナーなので、event は クリックイベント に関する情報を保持します。
今回のclick イベントの場合はeventの型はMouseEvent オブジェクト。
MouseEvent は Event クラスの派生型で、クリック固有のプロパティ(例: マウス座標)を含みます。


function moduleFunction(arg, event) {
  console.log(`引数: ${arg}`);
  // イベントオブジェクトの詳細を確認
  console.log(`クリックされた要素:`, event.target);
  console.log('イベントオブジェクト:', event);
  console.log('type:', event.type); // "click"
  console.log('target:', event.target); // <button id="mybutton">...
  console.log('clientX/Y:', event.clientX, event.clientY); // 例: 150, 200
  console.log('ctrlKey:', event.ctrlKey); // 例: false
}
document.getElementById('mybutton').addEventListener('click', (event) => {
  moduleFunction(1, event); // event オブジェクトを渡す
});

こういった情報取得も可能です。
ただまぁ今回は「ボタンを押す」「関数実行」が主題なので今回はこれで終了。

2025年4月24日木曜日

location.search/href/hash:urlから複数のデータを取得する方法

「Hey,Scripting Cat!URLパラメータの取得の仕方を教えて!」


はい、urlからパラメータを取得する方法はいくつかあります
location.href フル URL(プロトコル、ホスト、パス、クエリ、ハッシュ
location.search 「?」以降の全てクエリ名を取得
location.hash 「#」以降の値を取得

URLの指定方法と取り出し方

http://url/a.htm?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1#1z2z3
上記の場合取得できるのは

【href】http://url/a.htm?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1#1z2z3
【search】?q=z3z7z9z9z9z9z0z9z8z34z0z9z9z14z1
【hash】#1z2z3

ハッシュは必ず最後にする必要があります。ハッシュの後にクエリ―パラメータを入れるとハッシュの内容として処理される為クエリを取得出来ません
ちなみに、処理でもハッシュ(hash)ですが、フラグメントと呼んだりもします。

クエリとハッシュの扱い方の違い

さて、取り出し方の前にそもそものurlパラメータに指定方法について
既に上に書いてはいますが2種類あります。

■クエリ(?クエリ名=aaa&クエリ名=bbb)

基本的にはクエリ指定は別のページとして扱います
つまりサーバーや検索エンジンは別々のURL(コンテンツが違うものとして)判定しようとします。
もっと言うとクエリが指定されているのにページの内容が全く同じか少ししか変化しない場合グーグル等検索エンジンなどからは複数のページで同じ内容のコンテンツが提供されていると判断されてランキング低下やインデックス優先度低下のリスクが上がります。
phpなのでページ内容が直接変更されるものは良いですが、javascriptのみで内容を変更するような場合はjavascriptが無い状態で基本ページを判定されるので注意が必要です(同じページなのにurlパラメータ違いで全てが別ページ扱いになり、重複ページのペナルティの可能性が高まる)
他には、次に説明するハッシュと違いリアルタイムでシームレスなURL変更は出来ません。
「location.search = "?q=ppp"」みたいにするとページをリロードをしてパラメータが更新されます。

■ハッシュ(#aaa)

クエリとは逆に同じページ内のリンクとして扱います
アンカーなどでページを上下させる方法などが一般的です。
ハッシュを使ってコンテンツをタブなどで切り替えるなどもありです。
ブラウザの履歴などには一応urlの履歴が残りますが、クライアント側(ブラウザ側)で処理している為、サーバー側からはハッシュの遷移が取得出来ません。
そのためページ遷移の判定を得るためにクエリを指定するパターンも結構あります。
しかし先に説明した通りグーグルなどはクエリ違いの同じページを嫌います。
また、ハッシュはページをリロードせずにリアルタイムでURLハッシュを変更可能です。

■URLの正規化(canonical)

canonicalタグ(カノニカルタグ)を使う事で(基本的にはパラメータなしを指定)パラメータの有無についてこのページが(コンテンツが重複してるページの)正規のページです。と、知らせるタグがあります。
ただし、完全な指定をするというものではないのでurl違いの同じ内容のページはない方が良いです。検索エンジンに知らせるだけで検索エンジンが申告通り判定するかは別問題)。


データの取り出し方

まぁなんも考えず上記のurlをの指定方法をみれば想像つくと思いますが「str.split(”z”)」すれば良いです。もちろん取り出したいデータが数値じゃなくアルファベットなら固定数値でsplitすれば良いです。
ちなみにhrefからパラメータを取り出す事も出来ますが、基本的にはsearch/hashで取り出しましょう。
そうしないとクエリを取り出したかったのにハッシュが取り込まれたりする可能性があります。

■パラメータの頭に区切りを入れる手法
ちなみに上記で区切りの頭にzを入れてますがこれは「q=z3z7z9z9z9z9z0」この「クエリ名=」が邪魔だからです。パラメータの一つ目[0]を必ず捨てる事でパラメータを直接splitして1番目から配列を回すだけで良い。必要なら0番目の要素を破棄も可能。
普通は「q=3z7z9z9z9z9z0」ですが、こうした場合は「=」以降で切り出しした後にsplitをするか配列後0番目の要素から要素[0].split("=")[1]とかでもこの要素0が空指定だった場合、要素[0].split("=")[1]ではエラーするので有無を判定してから切り出す必要がでたりします。
下記で推奨する正しいパラメータの抜き出し方もありますがクエリ名を知ってる必要があります(普通は知ってる)。今回の方法であればクエリ名が何であっても0番目を捨てるので雑にデータを抜き出せます。


正しいURLパラメータと正しい値の取り出し方

正直手間なのですが正しい指定と取り出し方もあります。
ちゃんするならこうした方が良いです。
http://url/a.htm?q1=aaa&q2=bbb&q3=10&q4=30&q5=200#abcd

データの取り出し方

// 現在の URL のクエリ文字列を取得
const url = new URL(location.href);
// 例: http://url/a.htm?q1=aaa&q2=bbb&q3=10&q4=30&q5=200#abcd
// クエリパラメータを取得
const params = new URLSearchParams(url.search);
// 各パラメータの値を取得
const q1 = params.get("q1"); // "aaa"
const q2 = params.get("q2"); // "bbb"
const q3 = params.get("q3"); // 10 (数値)
const q4 = params.get("q4"); // 30 (数値)
const q5 = params.get("q5"); // 200 (数値)
// フラグメントを取得
const fragment = url.hash.slice(1); // "abcd" (# を除去)
// 確認
console.log({ q1, q2, q3, q4, q5, fragment });

厳密な指定や取得が必要な場合はこうしてください。SEO的にもこちらが正しいです。
ただ、やはり少し手間なんですよね。20個以上パラメータが必要だったりするときに全てクエリ名を入れる必要があるし。

ちなみにクエリは重複指定も可能です
http://url/a.htm?q1=aaa&q1=bbb
みたいなパターンです。
const params = new URLSearchParams(url.search);
const q1 = params.get("q1"); // "aaa"
これだと実は1つ目しか取得出来ません。二つ目以降は無視されます。
勿論対処方法もあります。

const q1 = params.getAll("q1"); // ["aaa","bbb"]
手動で切り出す方法はいくらでもありますが基本的にはこの取り出し方が一番きれいだと思います。
なお、getAll は常に配列を返します。1つの中身でも配列です["要素値"]、無ければ[](空配列)です。

非推奨な話

一番最初に説明している通り実は「location.search」は「「?」以降の全てクエリ名を取得します。なので実は「?クエリ名=」って記述しなくてもパラメータを指定出来るし、urlからパラメータを取り出す事も可能です。
例えば上部ではでは「z」で区切っていたけど数字もアルファベットも使い切りたい時に区切り文字がない。そんな時に実はURLに「a.html?aaa?BBB?ddd?123?saf?777777?ss」みたいなURLを作っても別にページにアクセス出来なくなったりはしない。
さらに「location.search.split("?")」みたいな形でデータを取り出す事も可能。

まぁでも「やろうと思えば出来る」だけであって勿論非推奨です。
当然ながら一般的なURLパラメータ以外は検索エンジンにも嫌がられます。

2025年4月23日水曜日

bsc_wrapper.jsを作ってコードを圧縮した話

今回はコードのラップについてのお話です。
少し前の「document.getElementByIdを毎回書くのを止めようかなって話」のほぼ続きです。

オリジナルラッパーを作るyo!

後でモジュールにしようとは思いますがいったんグローバルで。
はい、こちら「ビスクラッパー.js」です。

//bsc_wrapper.js

function wrapElement(element) { const hasProp = (prop) => prop in element; const wrapped = { get si() { return hasProp('selectedIndex') ? element.selectedIndex : undefined; }, set si(value) { if (hasProp('selectedIndex')) element.selectedIndex = value; }, get sl() { return hasProp('selected') ? element.selected : undefined; }, set sl(value) { if (hasProp('selected')) { element.selected = value; // <option> の selected 変更時に親 <select> の selectedIndex を更新 if (element.parentElement && element.parentElement.tagName.toLowerCase() === 'select') { const index = Array.from(element.parentElement.options).indexOf(element); if (value && index !== -1) { element.parentElement.selectedIndex = index; // 選択 } else if (!value && index === element.parentElement.selectedIndex) { element.parentElement.selectedIndex = -1; // 選択解除 } } } }, get ds() { return hasProp('disabled') ? element.disabled : undefined; }, set ds(value) { if (hasProp('disabled')) element.disabled = value; }, get ck() { return hasProp('checked') ? element.checked : undefined; }, set ck(value) { if (hasProp('checked')) element.checked = value; }, get vl() { return hasProp('value') ? element.value : undefined; }, set vl(value) { if (hasProp('value')) element.value = value; }, get ih() { return hasProp('innerHTML') ? element.innerHTML : undefined; }, set ih(value) { if (hasProp('innerHTML')) element.innerHTML = value; }, get cn() { return hasProp('className') ? element.className : undefined; }, set cn(value) { if (hasProp('className')) element.className = value; }, get tc() { return hasProp('textContent') ? element.textContent : undefined; }, set tc(value) { if (hasProp('textContent')) element.textContent = value; }, get hd() { return hasProp('hidden') ? element.hidden : undefined; }, set hd(value) { if (hasProp('hidden')) element.hidden = value; }, get ro() { return hasProp('readOnly') ? element.readOnly : undefined; }, set ro(value) { if (hasProp('readOnly')) element.readOnly = value; }, get rq() { return hasProp('required') ? element.required : undefined; }, set rq(value) { if (hasProp('required')) element.required = value; }, get ty() { return hasProp('type') ? element.type : undefined; }, set ty(value) { if (hasProp('type')) element.type = value; }, get nm() { return hasProp('name') ? element.name : undefined; }, set nm(value) { if (hasProp('name')) element.name = value; }, get id() { return hasProp('id') ? element.id : undefined; }, set id(value) { if (hasProp('id')) element.id = value; }, get st() { return { get bc() { return element.style.backgroundColor; }, set bc(value) { element.style.backgroundColor = value; }, get dp() { return element.style.display; }, set dp(value) { element.style.display = value; }, get co() { return element.style.color; }, set co(value) { element.style.color = value; }, get fs() { return element.style.fontSize; }, set fs(value) { element.style.fontSize = value; }, get wd() { return element.style.width; }, set wd(value) { element.style.width = value; }, get ht() { return element.style.height; }, set ht(value) { element.style.height = value; }, get mg() { return element.style.margin; }, set mg(value) { element.style.margin = value; }, get pd() { return element.style.padding; }, set pd(value) { element.style.padding = value; } }; }, element, getElement() { return element; }, on(type, listener) { element.addEventListener(type, listener); }, off(type, listener) { element.removeEventListener(type, listener); } }; return new Proxy(wrapped, { get(target, prop) { if (/^\d+$/.test(prop) && hasProp('options')) { const index = parseInt(prop); const option = element.options[index]; if (!option) { console.warn(`wrapElement: options[${index}] は存在しません`); return undefined; } return wrapElement(option); } if (prop in target) return target[prop]; const value = element[prop]; return typeof value === 'function' ? value.bind(element) : (hasProp(prop) ? value : undefined); }, set(target, prop, value) { if (prop in target) { target[prop] = value; return true; } if (hasProp(prop)) { element[prop] = value; } return true; } }); } function $i(id) { const element = document.getElementById(id); if (!element) return null; return wrapElement(element); } function $n(name) { const elements = document.getElementsByName(name); return Array.from(elements).map(element => wrapElement(element)); } function $$(query, { preferId = false } = {}) { const idElement = document.getElementById(query); const nameElements = Array.from(document.getElementsByName(query)); if (idElement && nameElements.length > 0) { console.warn(`idとnameに同じ名称あり: "${query}"`); if (preferId) return wrapElement(idElement); return null; } if (idElement) return wrapElement(idElement); if (nameElements.length > 0) return nameElements.map(element => wrapElement(element)); return null; } const $c = console.log.bind(console);

/* // グローバル互換性(オプション) window.$i = $i; window.$n = $n; window.$$ = $$; window.$c = $c; window.apc = apc; window.finishset0 = finishset0; //モジュールの場合(エクスポート) export { $i, $n, $$, $c }; */

こんな感じです。
前回は「const $ = (id) => document.getElementById(id);」で圧縮!!って話をしました。
これだけでもだいぶ平和になるんですが今回はそれを更に便利にした感じです。

例えば

  function apc(){
	var ea=getmaxessence()
	var ma=document.getElementById("material"); var msi=ma.selectedIndex
	var eq=document.getElementById("equipment");var esi=eq.selectedIndex
	var s1=document.getElementById("sp1");      var s1i=s1.selectedIndex
	var s2=document.getElementById("sp2");      var s2i=s2.selectedIndex
	var s3=document.getElementById("sp3");      var s3i=s3.selectedIndex
	var fi=document.getElementById("finish");   var fii=fi.selectedIndex
	
	var pmap=document.getElementsByName("pmap")
	var equp=document.getElementsByName("equp")
	var sp1p=document.getElementsByName("sp1p")
	var sp2p=document.getElementsByName("sp2p")
	var sp3p=document.getElementsByName("sp3p")
	var finp=document.getElementsByName("finp")
	var perp=document.getElementsByName("performance")
	var per2=document.getElementsByName("performance2")
    上記を
    …
    function apc(){
	var ea=getmaxessence()
	var ma=$i("material"); var msi=ma.si
	var eq=$i("equipment");var esi=eq.si
	var s1=$i("sp1");      var s1i=s1.si
	var s2=$i("sp2");      var s2i=s2.si
	var s3=$i("sp3");      var s3i=s3.si
	var fi=$i("finish");   var fii=fi.si
	
	var pmap=$n("pmap")
	var equp=$n("equp")
	var sp1p=$n("sp1p")
	var sp2p=$n("sp2p")
	var sp3p=$n("sp3p")
	var finp=$n("finp")
	var perp=$n("performance")
	var per2=$n("performance2")
    

$i ← document.getElementById
$n ← document.getElementsByName
どちらも二文字でアクセスできるようにラップしました。
varなのはDWが古くてletとか構文エラー入るので…直近のコードはだいたいletかconstです。

今回の目的「無くてもいいけどあったら楽」

jquery使ったことは無いんですがそちらより短くアクセス出来ると思います。
勿論様々なラップがされてるjqueryには機能面では劣ります。

今回の目的としては通常のjavascriptの記述方式の流れを汲む事と短縮しても何のプロパティにアクセスしてるかプロパティ名を知ってれば分かる事。
このラッパーを使うのを止めた場合でもjavascriptの記述に戻れる(と思う)様に考えてます。

jqueryとかはメソッド方式らしく「obj.method()」みたいな感じで動かすらしいです。
ただその記述方法は独特なものになるので今回はオリジナルのプロパティを設定する形にしたかった事もあり、ゲッターとセッターを使ったラップにしてあります。拡張も容易です。

ちなみに、jqueryでメソッド方式の優位点はチェーンを作れるところ
「obj.method().プロパティ1().プロパティ2().プロパティ3()」みたいにする事で一度の記述で複数のプロパティを書き換えたり出来るそうです。

そういうオリジナルの形に進んじゃうと便利だけど普通の記述の構成忘れちゃいそうなので今回のラッパーはjqueryは違う方向性って事です。

ゲッターとセッター

今回実は初めて使いました。
オブジェクトを作って
・引数無しならそのプロパティの値を取得=ゲッター
・引数を代入したらその値をセット=セッター
ざっくり言えばそんな感じです。

他にも一応方法はあって、生エレメントDOMを設定を直接書き換える方法もあるにはあります「Object.defineProperties(HTMLElement.prototype…」みたいな感じです。
直接的な挙動に短縮プロパティを設定する方法なのでこちらなら色々面倒なことを考えなくても良くなりますが危険度の方が高いと思います。まぁ使わなかった方法の話はいいでしょう。閑話休題。

前回の記事の方法であれば「const $i = (id) => document.getElementById(id);」
関数名を合わせるとこの様になり「$i("idname")」でアクセスできます。
今回の方法でも同様に「$i("idname")」でアクセスできますが戻ってくるものが違います。
let a=$i("idname")
こうした場合、前回と今回ではaに入るものが違います。
前回の方法は生エレメントを直接取得していますが今回はオブジェクトを取得しています。

つまり、前回の方法なら全てのメソッドやプロパティにアクセス出来ますが、オブジェクト側は設定しないと使えないのです。
最初にゲッターセッターを作ったあとに通常プロパティ名でアクセスしたら何も取得出来ませんでした
let a=$i("idname")
console.log(a.selectedIndex)
こう記述した時にaの中身は
前回:a=生エレメント
今回:a=オブジェクト
という事です。セッターゲッターに通常のプロパティと同じ名前のプロパティ名を設定すればどちらでも使えるように出来ますが全て列挙するのは現実的ではありません。

ではどうするかというとゲッターセッターに短縮プロパティを登録して、登録外のものは生エレメントに転送する方法です。
となると、生エレメントは常に持っておかなければならないという結論に。
さらに言えばオブジェクトから生エレメントを取り出せるようにしました。
「let b=$i(id).element」といった感じですね。

命名規則の話

今回の目的としてはとにかく頻度が高く、単語が長いものを短くするという目的があるので
基本的に二文字に圧縮しようと決めていました
・単語が二つあるものはその頭をとる(innerHTMLならih)
・母音頭2文字(di sa bledみたいに区切りってds)
・母音が無ければ先頭2文字(styleとかならst)
基本的にこのルールによってプロパティ名を決めました。


結局ラッパーって何?

簡単に言えばオリジナルオブジェクトです$i(id)は生エレメントをオリジナルオブジェクトで包み込んでそれにプロパティを生やしてるだけです。
指定のプロパティでアクセスがきたらそれを内部のエレメントに投げて結果貰ったりDOMを書き換えたりするって事。

2025年4月22日火曜日

script type="module"に変更して外部ファイルを読み込む

今回のお題はメインとなるjsの可読性を上げようかなって話
まぁそういう意味では前回の「document.getElementById」を毎回書くのを止めようかなって話と同じですね。

javascriptで長いコードを外部化する方法

まず、古来からのjavascriptの読み込み方法というのは

<script type="text/JavaScript" src="./main.js"></script>

こんな感じです。
ですがこの読み込み方法では外部ファイルjsを参照出来ません。
jsのファイルの読み込み方法から変更する必要があります。

<script type="module" src="main.js"></script>

この様にタイプの指定をmoduleという指定方法でjavascriptを読み込みます。

①従来の読み込み方法

違いとしては古来の方法は「window.」というブラウザのグローバル空間で定義されていています。例えば
window.a=10
var a=10
と、どちらで定義しても同じです。「window.」のプロパティの形で登録されていて暗黙的に省略してるだけです。
通常の関数もそうです。

function aaa() { console.log("こんにちは");}
console.log(window.aaa); // 関数 aaa
aaa(); 


じゃあ、node.jsの場合は?となる人も居るかと思いますがnodeでは「global.」オブジェクトに定義されます。ですので、

global.myVar = 10;
console.log(global.myVar); // 10
console.log(myVar); // 10

nodeではこんな感じになります。


話しを戻して、例えば、
<script type="text/JavaScript" src="./main1.js"></script>
<script type="text/JavaScript" src="./main2.js"></script>
と、二つ読み込んだ場合はどちらにコードを記載してもどちらかでも変数にも関数にもアクセスできます。
main1に「function a(){ console.log("test");}」と記述しているならば、main2で「a()」と書くだけでアクセスできます。

ファイル自体は分かれていても、コードの処理領域・スコープが同じなのです。

更に言えば読み込んでいるhtml側もそうです。
例えば、
<button onclick="a()">ぼたん</button>
はボタンを押すだけで「a()」を実行できます。

②モジュールの読み込み方法

次にmoduleですが、こちらは処理領域がjsのファイル内に閉じられています。
<script type="module" src="main1.js"></script>
<script type="module" src="main2.js"></script>

古来の方法通り普通にhtmlに読み込んでるように見えますがhtml側とモジュール側では領域が断絶されています。jsのトップレベルにあるコードは実行されますがそれだけです。

例えばmain1.jsに「function a(){ console.log("test");}」記述されている時に
html側から「<button onclick="a()">ぼたん</button>」を押しても実行出来ません。
「window.」領域にモジュールの内容がいない為html側からアクセスできません。
対応については順番に説明します。

②-1:モジュールのスコープ

例えば、main1.jsに「var b=10」を定義した場合、この変数bを使えるのはmain1.jsだけです。html側からも他のjsファイルもアクセス出来ません。
関数を定義した場合も同様で、「const c = (x) => x+1」のように定義した場合、html側からも他のjsファイルからもアクセス出来ません。


②-2:他のjsファイルへのアクセス

htmlとのやり取りの前に先に他のjsとのやり取りを先に説明します
現状ではmain1.jsは完全に独立していて他のjsに影響されないしmain1.jsも他のjsへは影響を与える事は出来ません。
どうするかというと、他のjsへアクセスするにはエクスポートとインポートが必要になります。
インポートはエクスポートされているものしか参照出来ません。
つまりjsのファイル間で合意が必要になります。「使って良い処理を出す側」と「使いたい処理を受ける側」これが一致して初めて外部のファイルを参照して利用できます。
インポート側が一方的に好きな変数や関数を参照出来ないという事です。
この方式はコーディング的にスコープ周りは強固ですが面倒臭いのは否めない。


②-2-1:エクスポートする側

まず、エクスポートして貰わないとそもそもインポートも出来ない形式なので先にエクスポートの方法について。

1.外部参照させたい変数や関数の頭全てに「export 」を記述する方法
export const aa=1
export const bb=2
const cc=3
export function dd() {}
export function ee() {}
function ff() {}
「export 」が付いていないものは外部からは参照出来ません。
コードを流してみた時に頭に「export 」があるかどうかで外部から見れるかが直ぐに分かります。

2.一番最後に「export 」対象を列挙する方法①
const aa=1
const bb=2
const cc=3
function dd() {}
function ee() {}
function ff() {}
export { aa, bb, ee,ff};//まとめて指定
こちらは最後に対象の変数や関数を指定する方法です。各定義の頭に「export 」を付ける必要はありません。既存のコードがある場合は全ての対象にexport を付けなおす必要はないので楽かもしれません。
逆に言えば流し見した時にどれがエクスポート対象かは分かりません。最後のリストから名前で検索して内容を確認する必要があるかも。
また、ある程度量がある場合列挙するのがそれはそれで面倒かもしれない。

3.一番最後に「export 」対象を列挙する方法②「デフォルトエクスポート」
const aa=1
const bb=2
const cc=3
function dd() {}
function ee() {}
function ff() {}
export { aa, bb, ee,ff};//まとめて指定
export default { bb, dd, ee,ff}; // デフォルトエクスポート
モジュールにつき1回だけ可能な指定方法、手間的には対象を列挙①と変わりません。
インポート時に楽が出来るかどうかの違いです。詳細はインポート側で説明。

②-2-2:インポートする側

さて、エクスポート対象が決まったらやっとインポート側で参照できるようになります。
エクスポート側では対象全てをエクスポートするみたいな事は出来ませんがインポート側は多少は柔軟でエクスポートを一括で取り込むことも可能

1.エクスポートされた変数関数をそのまま使えるようにする
import { aa, bb, dd } from './db.js';
console.log(aa);
console.log(dd());
エクスポートされた変数や関数の名前を指定して名前を変えずそのままそれを使えるようにします

2.一括取り込み①「export default」読み込み
import datas from './db.js'; 
console.log(datas.aa);
console.log(datas.dd());
オブジェクトの様に一括で取り込み、プロパティの様に対象全てを利用する事が出来ます。
オブジェクト名は好きな名前を付ける事が出来ます。
記述時に「{}」が不要な所は注意
モジュール1つにつき1回だけ可能。

3.一括取り込み②名前空間で「*」全指定する
import * as datas2 from './db.js';
console.log(datas2.aa);
console.log(datas2.dd());
エクスポート対象を*で全て対象にしてasで名前を付けて纏めています
こちらもオブジェクトとプロパティの様に対象対象全てを利用できます

4.エクスポートされた変数関数の名前を変更して取り込む
import { aa as zz, dd as yy} from './db.js';
console.log(zz);
console.log(yy());
エクスポートされた変数や関数の名前を指定した後にasで変数関数の名前を変更して取り込みます。

5.一旦全て取り込んで個別に取り出す(分割代入)
import * as datas2 from './db.js';
const { aa, dd } = datas2 ;
console.log(aa);
console.log(dd());
一旦全て取り込むのは既に紹介しましたがオブジェクトとプロパティのようなアクセスでした。分割代入を使えば元の名前で変数関数を取り出して使う事ができます。

html側からアクションを起こす方法

コード側のやり取りの説明が終わったので今度はhtmlからのアクションはどうするのかについてです。
「window.」空間外にjavascriptがいる為アクセス出来ません。しかもjsと違ってhtml側からではインポートの構文が使えません。解決方法は主に2つ。

①「window.」空間に来てもらう

html側からアクセス出来ないのは「window.」空間に対象の変数も関数も居ない為アクセスできないのですから、js側で「window.」に定義してhtml側の空間に下りてきて貰えばいいのです。
■js(module)
function dd() {}
window.dd=dd;
■html
<button onclick="dd()">ボタンをクリック</button>
こうする事でモジュール空間にあった変数や関数を「window.」空間に定義してhtml側からでもアクセスできるようになります

②イベントリスナーでjs側からhtml側アクション待ちで待機する

■js(module)
function dd() {}
document.getElementById("pp").addEventListener("click", dd);
■html
<button id="pp">ボタンをクリック</button>
onclickと違い一方方向にアクションが進んでいくのではなくjs側で待ち状態にしてhtml側のアクションの後にjs側でアクションされた対象を特定して実行する
どのボタンかの特定が必要に足る為idなどの識別子は必ず必要になる。

③「window.」空間の上にオブジェクトの空間を挟む

■js(module)
function dd() {}
window.obj1= window.App || {};
window.obj1.dd= dd;
■html
<button onclick="obj1.dd()">ボタンをクリック</button>
これは①のちょっと安全版みたいなもので完全なグローバル空間の「window.」の上にオブジェクトを作成して、一階層浮かせたグローバル空間で処理をする方法です。
一階層浮かせているのでonclickで実行する時にオブジェクト名も必要になります。

━━━━━━━━━━━━━
■終わりに
現状のjavascriptの一部を分離するために色々と調べたんだけど今から分割するのは手間が掛かり過ぎるかも・・・うーん困った。
調べる前は外部ファイルに定義して必要なものだけ引っ張ってこればいいのかなと思っていたけどエクスポートとインポートが相互で処理が必要で思ったより簡単ではなかった。
修正するかもしれないけど一旦保留にして次何か作る時はちょっとモジュール思考で考えてみよう。

2025年4月21日月曜日

document.getElementByIdを毎回書くのを止めようかなって話

document.なんちゃら長すぎもう無理・・・

document.getElementById("idname")、document.getElementsByName("name")
・・・長いよ!!こんなの毎回書くの面倒すぎる。
こんなもの・・・これをこうしてこう!!

const $ = (id) => document.getElementById(id);

はい。今回は最終的にこういうことが出来るというお話です。
タイトル通り、 document.getElement・・・をいくつも書くのが面倒なので関数にすればいいかぁとは思ってたけど何か短く書くかーとなって最終形態が上記通りです。

javascriptの「$」はどんな存在なの?

「$は変数定義出来る数少ない記号なだけでアルファベットと同様にただの識別子に使用可能なただの文字扱い」です。
なので、
const $ = (id) => document.getElementById(id);
const d = (id) => document.getElementById(id);
ただの変数名ですのでこの二つに差はないです。記号なせいで特別に見えますがアルファベットと同じです。一旦$にしたのは変数名が重なる事がないからです。別にdでもconst idgetとかでもなんでも良いです。

シングルクォート囲みの$には干渉しないの?

しません。
「`文字と${aaa}を一緒に記述出来る`」使い方はこんな感じですね。
シングルクォート囲みの「${変数や処理}」は全く干渉しませんletやvarとはまぁちょっと違いますが特定の条件下の決められた記述なので変数定義とは全く関係ないです。

変数の識別子に使える文字列ってそもそも何?

$以外に変数に使える記号とかってあるの?って話ですよね。
一応あります。折角なのでそもそもの変数の定義可能な文字列について。

■JavaScript の変数名
【a-z, A-Z】アルファベット
【0-9】数字(0-9)※ただし、最初の文字には使えない。
【_】アンダースコア
【$】ドル記号
【Unicode 文字】(例: 日本語のひらがな、カタカナ、漢字なども可)。

■ルール:
①数字以外は変数名の1文字目に使える(そのためこの中ではある意味数字は特殊)
②予約語(例: function, let, const など)は変数名に出来ない
③大文字小文字の区別される(例:nekoとneKoは別の変数として定義される)

という事は
const _ = (id) => document.getElementById(id);

って記述も可能って事ですね。
さらには日本語も使えるので実は
const 取得 = (id) => document.getElementById(id);

って記述も可能です。なんなら絵文字でも可能。
ただまぁ、可読性がどうかはさておき。
変数に日本語が使える=Unicode 文字が使えるっていうのはjavascriptは最初からそういう仕様だったそうで…、だいぶjavascript使ってるのに先ほど初めて知りました。

逆にそれ以外の記号関係は変数名に使用できない

@, #, %, &, *, +, -, /, !, ?, =, <, > などがありますが
「@」はデコレーター
(デコレーターは、クラスやメソッドなどの宣言の前に @ を付けて、その宣言に機能を追加したり装飾したりするための特別な宣言。クラスやメソッドに、ある機能を付与するような処理を記述し、その処理をデコレーターでアタッチすることで、再利用可能な形で機能を追加できる機能)
「#」はプライベートなプロパティやメソッドの宣言
「%」は余り「+」は加算「‐」は減算「!」はnotなどなど比較や計算の演算子となっていて他の記号は変数に使えません。

そうしなければならないという事ではないが記号文字の使い道

つまるところ「$」と「_」の二つですね。

■アンダースコア(_):
プライベート変数や一時変数を表す。
例:_private, _temp, _(ループで使わない変数)。

■ドル記号($):
DOM 操作やユーティリティ関数のショートカット。
例:$, $element。

だいたいそんな感じで使われていますが別にそう決められているわけでは無いです。
またアンダーが最初に来るときは意味がある場合が多いですが「best_cat」みたいな変数の単純な単語区切りにも使われてます。こういう時は特別な意味はないです。

jQuery には気を付ける

有名なjqueryは既に$が定義されています。なのでjqueryを使う予定があるなら「$」単品で変数名を定義をするのはやらない方が無難です。特にグローバル変数なので混在するとグローバル範囲が汚染される可能性があります。
まぁ上記で説明した通り「$」自体は別に特別な機能って事は無くただの変数名である以上別の変数名で上書きして書き換える事も出来るにはできると思いますがそれはそれで大変だと思うのでよしなに。