30代からのプログラミング学び直し!

10年エンジニアやってるけどいまだになんもわからん

History: pushState()でページ遷移せずにURLを動的に変更する

やりたいこと

JavaScriptで非同期(ajax)でデータを取得している、あるWebページがありまして、ページング時にもURLは変わらない(ページ遷移しない)作りだったのですが。SEO的な観点で、ページングしたらページ番号をURLに付与したくなりました。いまさら非同期をやめてサーバサイドで処理をするのも嫌だったので、フロントだけでURLを動的に変更できないか調べたところ、いとも簡単にできたので備忘録です。

結論、History: pushState()を使う

History: pushState() メソッド - Web API | MDN

ページネーションの番号をクリックした後に呼ばれるコールバック関数内で、history.pushState()を呼ぶ。これだけ。

clickCallback(pageNum) {
      history.pushState(null, '', pageNum);    // URLを動的に変更
},

大事なのは第3引数のURLの部分ですが、相対パス絶対パスも指定できるようです。ドキュメントだけだといまいち分からなかったので試してみました。 https://example.com/aaa/bbb というパスに対して実行した場合

history.pushState(null, '', '1');    // https://example.com/aaa/1 に書き換わる
history.pushState(null, '', '/1');   // https://example.com/1 に書き換わる(ルート相対パス)

相対パスの場合、末尾のスラッシュがあるかないかでも挙動が変わるので注意。 https://example.com/aaa/bbb/ というパスに対して実行した場合

history.pushState(null, '', '1');    // https://example.com/aaa/bbb/1 に書き換わる

クエリパラメータにページ番号を追加する

あとはどのようにページ番号付きのURLを生成するかですが、 https://example.com/aaa/bbb?page=1 のようにクエリパラメータに追加することにしました。

clickCallback(pageNum) {
      let url = new URL(location.href);
      url.searchParams.set('page', pageNum);
      history.pushState(
        null,
        '',
        `${url.pathname}?${url.searchParams.toString()}`
      );
},

これであれば、https://example.com/aaa/bbb?c=cccのような、任意のクエリパラメータを持つURLであっても、 https://example.com/aaa/bbb?c=ccc&page=1 となって問題なくページ番号を付与することができます。

一見難しそうに見えましたがいとも簡単に実装できました!HTML5便利〜✨ ついでに言うとURLオブジェクトもなかなか便利ですね。

URL: URL() コンストラクター - Web API | MDN

参考にさせていただきました : ページ移動せずにアドレスバーのURLを変更する | GRAYCODE JavaScript

JavaScript 関数定義の書き方いろいろ

JavaScriptで関数を定義する時、書き方が色々あって、それぞれの特徴も色々あって、頭がごっちゃになることがあるので自分用にまとめたいと思います🧐

関数宣言文

function 宣言 - JavaScript | MDN

一番基本的な関数定義。

function square(x) {
  return x * x;
}
console.log(square(3));     // => 9

特徴 : 関数宣言文はスクリプトや関数の先頭に「巻き上げ」られるので、定義よりも前のコードで呼び出しができる!

console.log(square(3));    // 定義よりも前で呼び出してOK
function square(x) {
  return x * x;
}

関数式

関数式 - JavaScript | MDN

関数名を省略できる(無名関数)。

const square = function (x) {
  return x * x;
};
console.log(square(3));    // => 9

関数名は省略できるが、別に省略しなくてもいい。再帰の時とかに便利。

const f = function fact(x) {
  if (x <= 1) return 1;
  return x * fact(x - 1);
};
console.log(f(4));    // => 24

関数定義後すぐに呼び出すこともできる(IIFE:即時実行関数)。

const square = (function (x) {
  return x * x;
})(3);
console.log(square);    // => 9

特徴 : 関数宣言文とは違い、「巻き上げ」はされないので注意!定義よりも前のコードから関数を呼び出すことはできない。

アロー関数(ES6~)

アロー関数式 - JavaScript | MDN

最近はだいたいこれで書く。簡潔で便利。だけど制約も色々あるのでちゃんとリファレンスを読んだ方が良さそうだ。

const square = (x) => {
  return x * x;
};
console.log(square(3));    // => 9

特徴 : 関数が定義された環境のthisキーワードの値を継承する。→ 最後に説明

メソッドとして定義&呼び出し

ちょっと話はズレるけど、オブジェクトのプロパティ(メソッド)として定義した場合。

let o = {
  square: function (x) {
    return x * x;
  },
};
console.log(o.square(3));    // => 9

ES6から簡略記法が使える。

let o = {
  square (x) {
    return x * x;
  },
};

ちなみに、メソッド呼び出しでは、関数本体からthis キーワードを使ってオブジェクトoを参照できる。

let o = {
  square: function () {
    return this.x * this.x;
  },
  x: 3,
};
console.log(o.square());    // => 9

(余談)アロー関数のthis

メソッドがわかりやすい。 アロー関数以外では、入れ子型の関数は外側のthisの値を継承しない。外側の変数にアクセスすることはできるので、 let self = this; とかでthisの値を保管しておく方法がよく使われる。

let o = {
  m() {
    let self = this;
    console.log(this === o);    // => true : thisはオブジェクトoである
    f();
    function f() {
      console.log(this === o);    // => false : thisはオブジェクトoではない。
      console.log(self === o);    // => true : 外側で保管しておいたthis。
    }
  },
};

アロー関数では、thisがそのまま使える。(関数式は巻き上げられないことに注意)

let o = {
  m() {
    const f = () => {
      console.log(this === o2); // => true : thisはオブジェクトoである
    };
    f();
  },
};

整理するとだいたいこんなものでしょうか…!?

相変わらずこの本で勉強していますが、非常に学びが多いです😎

JavaScriptのオブジェクトでkey,valueを取り出したい時の最適解

長年付け焼き刃的な知識だけでJavaScriptを書いてしまっていることが、そろそろ看過できなくなってきたため、こちらの本で体系的に学び直しをしている最中です。

現在4分の1程度を読み終わったところですが、すごく、すごいです!ごくごく基本的なところでも知らなかった知識がバンバン出てくるし、経験上なんとなく身についてはいるけれど説明できないようなことも明快に言語化されています。特に、ES6以降の知識はインターネットで断片的にしか身につけてこなかったので、「こ、こんなに便利な記法があるなんて〜!」と感動します。逆に、今まで自分はなんてクソコードを生み出していたんだ…と恥ずかしくもなるのですが……。

そのうちの一つが今回のタイトルです。 「オブジェクトからkey,valueを取り出したい」というとても初歩的な処理でも学びがあったのでメモとして残しておきます。

オブジェクトからkey,valueを取り出したい時どうする?

今まで、私はこのようなコードを書いていました。(恥)

Object.keys(obj).forEach((k) => {
  console.log(k, obj[k]);
});

Object.keys()でkeyだけを取り出して、ループで回して、valueはkeyでアクセスする。 もちろんこれで問題なく動作はするのですが、今考えるとちょっと野暮ったいですね…。オブジェクトの変数名が長い時なんかはさらに冗長な感じがしてきます。

結論、最適解はこれ!
for (let [k, v] of Object.entries(obj)) {
  console.log(k, v);
}

これを知った時は、なんてスマートなんだ!と感動しました。

Object.entries()は[key,value]の配列を返してくれます。これはES2017からの機能のようです。

let obj = { a: "aaa", b: "bbb", c: "ccc" };
console.log(Object.entries(obj));    // => [ [ 'a', 'aaa' ], [ 'b', 'bbb' ], [ 'c', 'ccc' ] ]

これを、for/ofループで回しますが、その際、分割代入 let [k, v] で配列を2つの変数に展開します。for/ofループも分割代入も、ES6からの機能のようです。とても便利ですね。 それに、forEach()と違ってfor/ofではbreak文も使えるので、より処理の幅が広がりそうです。

Object.entries() - JavaScript | MDN

公式ドキュメントにも書いてありますが、これはfor/inループの反復処理と同じ挙動。ただし、for/inループはオブジェクトが継承しているプロパティも出力されるため、何も考えないと予期せぬ挙動をすることがあります。できれば、安全に独自プロパティだけ取り出せるこちらの記法が良さそうです。

たったこれだけのことなのだけど

たったこれだけのことなのですが、知っているのと知らないのとでは、スマートさが違うな〜と思いました。野暮ったい書き方をしていても動作はするので、基本的なことほど「調べて書く」をしなくなっていることを反省しました😔

引き続き勉強します!

CSSで要素を画面の最下部に固定したい、けど最後までスクロールしたらちょっと上に動かしたい

CSSはコーダーさんに任せっぱなしてあまり自分で書くことがなかったのですが、このままではいかんと思って、最近できるだけ自力で書くようにしています。学ぶことが多いです。

やりたいこと

要素(ボタン)を画面の最下部に固定したい。けど、最後までスクロールしたらちょっと上に動かして余白を持たせたい。 最下部にオーバーレイでバナーを表示したりすることもあるので、最後は余白があったほうが操作しやすいかと思って。

言葉で表すと分かりにくいですが、デモを見てもらえればわかると思います。 CSSを調べるときは、「ちょっと上に動かす」とかをなんて言葉でググったらいいん?と苦労します。笑

なんとなく position: sticky; とかを使えばいいのかな〜とは思ったのですが、上に固定するのはよく出てくるけど下に固定はあまり出てこない…。

結局、こうやった

See the Pen sticky-bottom by aya-cat-g (@tungtun) on CodePen.

参考にさせていただきました: 【CSS】position: stickyを使ってボタンをfooterと被らずにスマホ下部に固定する方法 | YUJIRO BLOG

参考サイトを元に自分で考えてみました。キモとなるのはこの部分。

.submit-button {
  width: 100%;
  position: sticky;
  bottom: 0;
  padding: 1rem;
}
.blank {
  height: 8rem;
  width: 100%;
}

position: sticky;bottom: 0; で要素を下に固定していますが、その下に空のdivを置いてheightを指定しました。 空のdivを置いているところが、あんまり綺麗なやり方ではないとは思うのですが…。他にいい方法があったら教えていただきたいです!😢

それにしても、stickyとか、最近のCSSは便利ですね…✨

JavaScriptでnew Date()を使って時間の計算をするときにはタイムゾーンに注意

new Date()の罠

ある残り時間を表示するための以下のコードで、海外のユーザーから「残り時間がずれているよ!」と指摘をいただきました。

NG

let endDate = "2024/03/12 22:00:00";      // この値は実際にはAPIから取得した値なので変更不可
// タイムスタンプに直して、終了時間から現在時間の差分を求める
let diff = new Date(endDate).getTime() - new Date().getTime();
// タイムスタンプの差分から「あと○時間○分」に直す処理は省略

endDateは日本時間だけど、new Date()で取得される現在時刻はユーザーが設定しているタイムゾーンになってしまうので、この処理ではまずいことが分かります。(言い訳ですがこのコードを書いたのは私ではない。)

であれば、日本時間の現在時刻を表示すればいいではないか、と思いDateのリファレンスを読んでみたのですが、どうも一筋縄ではいかなそうでした。Dateはタイムゾーンを自由に設定できるわけではないのですね…(不便)

発想を変えて、どちらも同じタイムゾーンにすればいいだろう!と思ってUTCに変換する処理を入れてみました。

NG

let endDate = "2024/03/12 22:00:00";
let diff = Date.parse(new Date(endDate).toISOString()) - Date.now();

toISOString()UTC時刻に変換してから、Date.parseでタイムスタンプを取得しています。Date.now()は常にUTCのタイムスタンプを返すようです。

が、これでもうまく動作せず。よく考えたら、endDateタイムゾーンの情報がないので、new Date(endDate)の時点で、ユーザーが設定しているタイムゾーンとして解釈されてしまうようです。

であるならば、タイムゾーンを明示的に指定してから初期化しよう、ということで以下の方法で解決しました。 ISO形式(2024-03-12T22:00:00+09:00)で記述するために、dayjsを使いました。

OK

let endDate = "2024/03/12 22:00:00";   // 実際には変更不可の値
let jstDate = dayjs(endDate).format("YYYY-MM-DDTHH:mm:ss+09:00");
let diff = Date.parse(new Date(jstDate).toISOString()) - Date.now();

めでたしめでたし。タイムゾーンは頭が混乱してしまいますね。

とても参考にさせていただきました: JavaScript における Date のタイムゾーンの挙動と対策

ブラウザでタイムゾーンを変更する方法

結論、Chrome DevToolsの「センサー(Sensors)」を使うのが一番手軽で便利でよかったです!こんな便利な機能があったとは!

センサー: デバイス センサーをエミュレートする  |  DevTools  |  Chrome for Developers

Macの システム設定>一般>日付と時刻 で端末の時間を変更するのは、タイムゾーンの問題解決には意味がなかったです。タイムゾーンではなく時間自体がずれてしまうので意図した動作にならず。最初これをやってしまって、無駄に混乱してしまいました……。

技術書読んだメモ:シェル・ワンライナー160本ノック

少し前にこちらの本を拝読しました。

読もうと思ったわけ

サーバーを触ることはあるので、基本的なコマンド操作はできるのですが、|とか>とか&とかが出てくる長めのコマンドになると「何か分からん…とりあえずコピペ……」状態になってしまう自分を少しでも変えたいと思ったので手に取ってみました。あと、ちょっとした操作をシェルスクリプトで自動化できる同僚がかっこいいな〜と思っていたからです。

結果

本の題名には「半年以内に習得」と書いてありますが、私は約4ヶ月で読み切ることができました。一日、少ない日は0ですが多い日は10個くらいをまとめて進めました。セクションが細かく分かれているので、「とりあえず一つだけやろう…」という低いハードルで初めてけっきょく3つくらいできちゃったり、モチベーションを保ちやすかったです。私は、問題を読んで→1分くらい一応考えて→分からなかったら解説を読みながらコマンドを真似て打ってみて納得する、という感じで1問20~30分で終わらせるようにしました。(そうしないと、途中で挫折しそうで…。)自分で考えながら進めた場合は、もっとたくさん時間がかかると思います。解説が丁寧で、痒いところに手が届く感じだったので、本に書いてある内容だけでほぼ理解できました。

読んでみた感想

awksedがすごい

この本を読むまでは存在すら知らなかったawksedだけで大体のことが解決できると知れてよかったです。8割くらいawksedなのでは…!?世の問題のほとんどは「入力に対して、加工して、出力する」なのでawksedが有効、という点を実感として得られました。膨大なコマンドを覚える必要はない、という気づきでシェルに対するハードルが下がりました。

Macユーザーはつまづきポイントが多いかも

私はMacでコマンドを実行していたのですが、本はLinuxを前提で書かれているので、たまに動かないことがありました。最初は諦めていたのですが、そのうちMacBSD系で、LinuxGNU系なのでコマンドの挙動が異なるということを知りました。

この辺りのGNU系をインストールしたと思います。

brew install coreutils    # gdateなど
brew install grep         # ggrep
brew install gawk         # gawk
brew install gnu-sed      # gsed

本の通りに動かない時は、とりあえずコマンドの先頭にgをつけてみると動く!ことが多かったです。これが分かってから捗るようになりました。

あと、システムのファイルを使う際にもどうしてもファイル構成が異なってしまうので、Dockerで簡易的なUbuntu環境を作ってその中でコマンド実行をしたりしました。

docker pull queeno/ubuntu-desktop
docker run -itd -p 5901:5901 --name ubuntu-desktop --hostname ubuntu:remote queeno/ubuntu-desktop
docker exec -it ubuntu-desktop /bin/bash    # Docker内に入る 
シェルがちょっと好きになった

一番よかったのは、毎日のようにひたすらコマンドを打つことで、コマンドを恐れる気持ちがなくなったことです。そして、シェルを愛する人々はこういう問題に日々取り組んでいるのか〜という今まで知らなかった世界を垣間見れたことも面白かったです。奥深い世界なんだなと思います。パズルのような感覚で、頭の体操にもなりました。

この本を読んだことで、じゃあ業務の様々なことを自動化できるようになったか?というと残念ながら私はその域には達せていないと思います…。実用性を求めるのであれば、ワンライナーではなく、シェルスクリプトを書く練習をした方がいいと思いますし。しかし、シェルに関する基礎的な知識を学んで、数をこなして慣れたことで、日常でコマンドを使う頻度は多くなりましたし、機会があればシェルスクリプトも書いてみようという気持ちになりました!読んでよかったです。

JavaScriptで配列と文字列を比較した時の挙動

長年JavaScriptを使っていても、いまだに、基本的なところで「あれ!?」と思う挙動にぶち当たることがあります。

配列(オブジェクト)同士の比較
let array1 = ["a", "b", "c"];
let array2 = ["a", "b", "c"];
console.log(array1 == array2);   // false

配列(オブジェクト)は、中身が同じでも等価ではない。これは経験からなんとなく予想できます。 オブジェクトはデータの参照元(メモリの位置)で比較しているからのようです。ちょっと不便ではあります。

配列と文字列の比較
let array1 = ["a", "b", "c"];
let string1 = "a,b,c";
console.log(array1 == string1);   // true

今回驚いたのはこの2つが等しいことです。当然falseだと思っていました。

等価 (==) - JavaScript | MDN

オペランドの型が異なる場合は、比較前に同じ型に変換を試みます。 オペランドのうちの一方がオブジェクトで、もう一方が数値または文字列である場合は、そのオブジェクトの valueOf() および toString() メソッドを使用してプリミティブに変換を試みます。

ちゃんと書いてありました。配列と文字列を比較する時、配列の方は、toString()でカンマ区切りの文字列となって比較されるみたいです。

let array1 = ["a", "b", "c"];
let string1 = "a,b,c";
console.log(array1 === string1);   // false

厳密演算子だともちろんfalseになります。

厳密等価 (===) - JavaScript | MDN

この演算子と等価 (==) 演算子の最も顕著な違いは、オペランドの型が異なる場合、 == 演算子は比較前に同じ型に変換しようとすることです。

なるほど〜。感覚的には理解してたけど、ちゃんと文章にするとこうなるのですね。 型が違う値を比較しようと思うことが今まであまりなかったからか、知らなかった!(恥)ドキュメントはちゃんと読みましょう。

JavaScriptは今までなんとなく使っていてちゃんと学んだことがなかったので、基礎から体系的に勉強したいな…ということで、この本を購入してみました。勉強します。