ゆるふわ技術日誌

エンジニア見習いの悪戦苦闘日記

JavaScriptのインスタンスメソッドを変数に代入してはいけない

JSerにとっては当たり前の事なのかもしれないが、うっかりハマってしまったので備忘録として書いておく。


こんな感じのコードがあったして…

class User {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

const user = new User('mirin');

UserクラスのインスタンスであるuserのgetNameメソッドを呼び出します。

user.getName() // -> mirin

これは間違いなく、インスタンスかする時にコンストラクタで指定した名前が返ってきます。

しかし、以下のような感じで呼び出すと…

const gn = user.getName
gn() // -> Uncaught TypeError: Cannot read property 'name' of undefined

となる。

なぜか。

(ちょっとこの辺は正確さに欠ける表現があるかもしれないので、ご承知おきください)

JavaScriptthisは、呼び出したオブジェクトが入ります。(アロー関数を使った場合の挙動は別になります。要注意)

user.getName()の場合は、userがthisになるため、名前を取り出す事ができますが、メソッド自体を変数に代入して呼び出してしまうと、thisはundefinedとなってしまうため↑に示したエラーが起こります。

ちなみに

実際に僕がやらかしたのは、条件によって呼び出すメソッドを変える必要があるという、割と複雑なロジックを書いていた場面でした。

結局、それくらい複雑なことをやるなら関数分けた方がいいよね、となってやめたのですが、もしインスタンスメソッドを変数に代入したい場合は、

const gn = user.getName.bind(user)
gn() // -> mirin

という感じで、bindを使ってthisを束縛してやればOKです。もしくはメソッドの定義をアロー関数を使う、でもOKです。

TypeScripのkeyofについて

TypeScriptのHandbookに一通り目を通す、というチャレンジを今週はしております。

なんとも読みにくい英語だなーなんて思いながら読んでたらkeyofという知らない演算子が出てきたので、調べてみました。


keyofが出てきたのはこちらの「Using Type Parameters in Generic Constraints」というセクション。

www.typescriptlang.org

サンプルコードを引用すると、

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

こう。

どうやら、keyofにオブジェクトのキーを渡すと、そのオブジェクトのキーのいずれかを取るUnion Typeになるみたいです。

単純にするとこう

interface HogeInterface {
    a: string;
    b: number;
    c: boolean;
}


type Keys = keyof HogeInterface;

const hogeKey: Keys = 'a';
const fugaKey: Keys = 'd'; // error

Keys'a' or 'b'or'c'になります。

特定のオブジェクトのkeyの値しか許容しない、みたいなパターンの時に使えるって話なんだとは思うのだがこれがどこで使えるのか…というのはちょい謎。。。

誰か教えてほしい。


追記 2019/06/15

誰か教えて欲しい。

と書いていたら本当に教えてくれた方がいました。感謝。

shgam.hatenadiary.jp

結局ジェネレータって何に使うの…?って話

JavaScript基礎力強化を図っています。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

この本を読み返しながら、知識のないところを学習するシリーズ。


※先に結論を話すと、表題に関して答えを見つけられませんでした😉

ジェネレータについて話す前に、イテレータについて。

JSのイテレータについてざっくり

他の言語にあるイテレータ同様、繰り返しのための機構で、JSの場合Array/String/Map/SetなどがIterable(イテレート可能)なオブジェクトです。

イテレート可能なオブジェクトは[Symbol.iterator]という名前で、iteratorプロトコルに準拠したメソッドを実装しています。(Symbol.iteratorについては前回の記事参照)

developer.mozilla.org

詳しくは↑のMDNをみてもらうとして、ざっくり説明すると、

  • nextという名前のメソッドを持つ
  • 呼び出した時に{value: any, done: boolean}なオブジェクトを返す

というのがイテレータの条件です。

上記の条件を満たすことでfor..ofが使えたりします。もちろん自作のクラスに対して実装してあげてもOK。

で、ジェネレータは?

で、なぜイテレータについての説明を先にしたかというと、ジェネレータは呼び出すとイテレータを返す特殊な関数のことです。

こんな感じで定義します。

function* generator() {
  yield 'hoge'
  yield 'fuga'
  yield 'piyo'
  return
}

function*yieldという特別なキーワードを使って宣言するのが特徴で、この関数を呼び出すとイテレータがかえります。

const it = generator();
it.next(); // -> {value: 'hoge', done: false}
it.next(); // -> {value: 'fuga', done: false}
it.next(); // -> {value: 'piyo', done: false}
it.next(); // -> {value: undefined, done: true}

こんな感じ。

何が起こっているかというと、ジェネレータが返したイテレータを実行していくと、ジェネレータ内のyieldまで実行して、渡された値を返します。 実行はその時点で一旦ストップし、次に呼ばれたら、前回のyieldの位置の続きから実行して、次にyieldが出てきたら、渡された値を返します。

最後、returnが呼ばれるとイテレータはdone: trueを返すようになります。

また、イテレータのnextメソッドには引数を渡すことができ、

function* generator() {
  const name = yield 'What is your name?';
  console.log(`Your name is ${name}.`);
  return;
}

const it = generator();

it.next(); // -> {value: 'What is your name?', done: false}
it.next('mogami');
// Your name is mogami.
// -> {value: undefined, done: true}

みたいな感じで、前回実行したyieldの返り値がnextに渡した値になります。

何に使えるのか

やっとここで何に使えるのか、という話になります。

オライリー本では、イテレータ/ジェネレータの説明は12章で行われているのですが、14章 非同期プログラミングのところで再度ジェネレータが出てきます。

ジェネレータランナーという関数を用意することで、非同期処理が同期的に書けるという話。

本のソースコードを引用すると、

function grun(g) {
  const it = g();

  (function iterate(val) {
    const x = it.next();
    if (!x.done) {
      if (x.value instanceof Promise) {
        x.value.then(iterate).catch(err => it.throw(err));
      } else {
        setTimeout(iterate, 0, x.value);
      }
    }
  })();
}

こんな感じの関数を用意してやって

function* fileReadAndWrite() {
  const dataA = yield readFile('a.txt');
  const dataB = yield readFile('b.txt');
  const dataC = yield readFile('c.txt');  
  yield writeFile('d.txt', dataA+dataB+dataC);
}

(readFile/writeFileはいずれもPromiseを返す関数)

最後にこのジェネレータを最初に作ったジェネレータランナーに投げ込む

grun(fileReadAndWrite);

すると、非同期処理が同期的に書けて人間にみてもわかりやすいよね、と書いてありました。

とはいえ、コレって今となってはasync/awaitで解決しちゃう問題なんですよね…。

async function fileReadAndWrite() {
  const dataA = await readFile('a.txt');
  const dataB = await readFile('b.txt');
  const dataC = await readFile('c.txt');  
  await writeFile('d.txt', dataA+dataB+dataC);
}

fileReadAndWrite();

こうか?

で、よりわけわからなくなった。

じゃあもうジェネレータって何に使うのよ、って感じですね。

はぁ…。

RegExp.prototype.execが偶数回しか動かない謎に遭遇したハナシ

先日、既存のコードのテストをしていたところ、正規表現のマッチの結果が実行するごとに変わるという謎の現象に遭遇しました。

適当な例を作るとこんな感じ。

const regExp= /\*\*(.+?)\*\*/g; // MarkdownのBoldの箇所を抽出する正規表現
const markdown = "こんにちは。今日の気温は**33度!**とめっちゃ暑いので熱中症に注意しましょう!"

regExp.exec(markdown);

意図としては、Markdownの文章中からBoldのところ(アスタリスク2つで始まり、間になんらかの文字があって、アスタリスク2つで終わる)を抽出するという感じ。(もちろん実際のMarkdownパーサを書くならもっと考慮する事はありますが。。。)

実際には、これがUtil系の処理の一部として組み込まれていて、場合によっては数回叩かれるケースがある、というようなコードでした。

さて、これを実行してみると以下のような結果になります。

regExp.exec(markdown)
/* ↓結果
[ '**33度!**',
  '33度!',
  index: 12,
  input: 'こんにちは。今日の気温は**33度!**とめっちゃ暑いので**熱中症に注意**しましょう!',
  groups: undefined ]
*/

Node.jsで実行していますが、ブラウザでも同じだとおもいます。

見事「33度!」という値が取り出せているので、これで良さそうな感じがします。

しかしこれと全く同じコードをもう一度実行するとこうなります。

regExp.exec('/hoge?fuga=thisisvalue')
null

なんということでしょう…。

ちなみにこれ以降、もう一度実行すると成功し、さらにもう一度実行すると再びnullになるという挙動を繰り返します。

MDNのexecメソッドの説明をみると以下のように書いています。

developer.mozilla.org

exec() メソッドは、指定された文字列内で一致するものの検索を実行します。結果の配列、または null を返します。

実はこれが罠で、結果の配列を返すというのは、一致する全ての結果を配列で返すという意味ではないのです…。

今回正規表現gフラグが付いているのに気づいたでしょうか…?

これはグローバルフラグと呼ばれるもので、文字列中から正規表現に一致する全ての結果を得たいときに使います。今回の例の場合、これをつけるとexecは実行するたびに、前回ヒットした位置より後ろで、正規表現にマッチする箇所がないかを探すという挙動をします。

探した結果、存在しなければnullを返します。偶数回しかうまくいかなかった原因は、1回目は文字列を先頭から探して、ヒットする箇所があったので値が返却されましたが、2回目の実行では、前回のヒット位置より後ろ側にヒットする箇所がなかったためnullが返ります。これが今回遭遇したバグの原因となっていました。。。

どう解決するか

解決アプローチは複数あります。

まず1つ目。RegExpクラスのインスタンスは前回のヒットした位置をlastIndexというプロパティで持っています。

developer.mozilla.org

lastIndex は、次のマッチの始まりの位置を示す、正規表現インスタンスの読み書き可能な整数値のプロパティです。

とあるので、この値をexecの実行前に0に戻してあげれば、毎回同じ結果を返す事になります。

ただし、通常gフラグを使うという事は、複数マッチする事が想定されているはずなので、毎回0に戻すと、先頭のマッチしか取得できなくなってしまいます。

そこで2つ目のアプローチは、String.prototype.matchを使うというパターン。

developer.mozilla.org

こちらはexecとは異なり、gフラグが付いている正規表現を使用すると、一致する全ての箇所が配列となって返ってきます。

"**hoge** fuga piyo **puyo**".match(/\*\*(.+?)\*\*/g);
// -> [ '**hoge**', '**puyo**' ]

ただしこちらは、マッチした位置のindexなどの詳しい情報や、カッコを使ったキャプチャ(正規表現内でカッコを使うとカッコ内の文字を取得できる)を使う事ができないため、必要に応じてマッチした文字列をさらに加工する必要が出る可能性があります。

……あ、もちろん先頭のマッチ結果しかいらないならgフラグを外す事でexecの結果は冪等になりますことよ!

要件次第というところかなとは思いますが、一つ言えるのはgフラグが付いている正規表現を使うときはロジックを一度確認してみたほうが良いということですね。

なるべく副作用のない関数を書きたいと心がけているのですが、まさかこんなところに罠が潜んでいるとは…。という感じでした。おわり。

ES6で追加されたSymbolって結局なんなのか

JavaScript基礎力強化を図っています。

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

この本を読み返しながら、知識のないところを学習するシリーズ。


ES6から新たにSymbolという型のデータが追加されました。

developer.mozilla.org

Symbol() 関数は常に一意の値を返します。symbol 値はオブジェクトのプロパティ識別子として使われます。symbol 型はこの目的のためだけに使われます。

とのことなので、実際にどんな感じで使われるのかをみていこうと思います。

シンボルの特徴

シンボルはSymbol()関数で作ることができます。

先頭が大文字なのでクラスっぽいですが、new Symbol()は使えず、 エラーになります。

Symbol()は呼ばれる度に一意な値を返します。つまり

Symbol() === Symbol() // -> false

となります。

使われ方 その①

MDNにもあるように、シンボルはオブジェクトのプロパティ識別子(=キー)として用いることができます。具体的には

const sym = Symbol()
const obj = {
  [sym]: 'hoge'
}

ということができます。([sym]としないと、symという名前のキーになってしまうので注意)

これが具体的に役に立つケースは、既存のオブジェクトやクラスに対して、プロパティを追加したりするパターン。

Symbol登場以前は既存のオブジェクトやクラスに対してプロパティを追加する際に、キーが競合する可能性があるのを回避する手段がありませんでしたが、Symbolは作成する度に一意な値となることが保証されているので、安全に行うことができます。

const TATEGAKI = Symbol();

// Stringのインスタンスメソッドに縦書き(1文字ごとに改行)にするメソッドを追加する
String.prototype[TATEGAKI] = function () {
    return Array.from(this).join('\n')
}

console.log("文字列"[TATEGAKI]())

これでESの仕様で縦書きにするメソッドが実装されても(笑)自分の作った縦書きメソッドと衝突する恐れはないので、実行系が新しくなった瞬間に既存のコードが壊れるといった現象を回避することができます。

使われ方 その②

シンボルにはSymbol()を使って自分で作る値だけではなく、事前に定義された値があります。

その中の一つに、Symbol.iteratorという値があります。この値は、イテレート可能なオブジェクト(StringやArray、Map、Setなど)がイテレータを持つときのキーとして使われています。

"Stringはイテレータを持っています"[Symbol.iterator]; // -> function
new Date()[Symbol.iterator]; // -> undefined

なぜシンボルをキーに使うかについては、以下の記事が大変わかりやすかったです。

qiita.com

ざっくりとまとめると、

  • JSには特殊メソッド(使用者が上書きしてはいけないメソッド)を定義する方法がない
  • いきなり、イテレータを返すメソッドの名前を仕様で決め打ちにしてしまうと、正常に動かないプログラムが発生する可能性がある
  • そこで定義済みのシンボルを用意し、その値をキーとして用いることで、安全に特殊メソッドを実装することにした。

という感じ。言語の仕様を考えるような人たちって、本当に頭いいんですね…と思いました。

近況報告

なんとびっくり、ぴったり80日ぶりの更新です。

お久しぶりです。

ブログを書く習慣をつけるのは大変だったのに、書かなくなったら本当に書かなくてびっくりしました。

前回の更新である、2月23日から今日に至るまでに何があったかざっくり書いておきます。技術的要素はないです。


2月〜3月末

4月から入社する会社で、一足先にインターンとして働いていました。

内容としてはReact NativeとTypeScriptを使ったアプリ開発、という感じ。前回の更新がRNだったのも、業務の一環でつまづいたところとかのうち、一般的に役に立ちそうなものを書いたものでした。

無難に楽しく、週5日で働いてました。

人とコミュニケーションとるのがそれほど得意でない自分にとって、このインターン期間は既存社員とのコミュニケーションをとる良い機会になりました。

4月

入社。新元号発表のその日に入社式でした。

とはいえインターンで通い慣れていたので、いつも通り…と思って出勤したら入社書類を一式家に置いてあったことに30分くらい電車乗ってから気がつきました。

その場で引き返して、自宅最寄り駅から自宅までタクシーを使って、なんとか5分遅刻くらいで到着しました。入社式に遅刻しなかったのでセーフ。


そこから1ヶ月くらいは研修の日々。

最初の3日間くらいはビジネス職・エンジニア職合同の研修を受け、残りはエンジニア・デザイナーの研修。

f:id:uutarou:20190514224623j:plain
こんな絶景の会議室で研修を受けてました

世の中の研修って結構つまらない、ひたすら退屈なイメージが強かった(こんな記事があったり)が、幸いにもうちの会社の研修は、必要最低限を抑えた実践的かつ、普遍的に使える知識を身に着ける研修で大変良かった。そこは良かった。

同期と仲良くなれずに悩んで、病みそうにもなったりしたけど、なんとか無事に研修を修了。

と、同時に世の中はGWに突入。私は横浜の実家を離れ一人暮らしを開始しました。

大学も実家から通っていたので完全に人生初の一人暮らしです。

f:id:uutarou:20190514225234j:plain
引越し初日の様子

自宅の車がミニバンだったことと、実家が横浜とそれほど遠くないことから、自力での引越しを決断。

結局GWは荷物作っては車で運ぶというのを繰り返していたら終わってしまいました。

新卒1年目のGWとか、一番やる気に満ちてる時だったのに、有意義な時間を過ごせなかったのは残念。

一人暮らし話は引っ越して1ヶ月経ったくらいで書きたいな。ふとした瞬間(今とか)に死ぬほどさみしくなるけど、今のところはとっても楽しんでます。

5月

一人暮らし & 本格的な業務開始。

業務は結局インターン時代と同じチームに配属され、引き続きReact Nativeを使ったアプリ開発をやっています。

リリースからはすでに数年経っているものの、「とにかく機能開発を優先する」というフェーズからつい最近脱したアプリなので、手の入れどころ、リファクタリングのやりがいがあるプロジェクトだと個人的には感じています。

コード規模もまぁまぁ。利用者数も(正確な数字は知らないけど)下手すりゃ万単位で居る感じ。

JavaScript→TypeScriptの基礎力をこの辺りで一回ちゃんとつけておきたいと感じています。


一人暮らしの方はというと、やっと落ち着いてきて、生活のリズムもできてきたかなと思っているところです。

料理を作って写真を撮るということをなぜか趣味にしています。

f:id:uutarou:20190514230154j:plain
同期にめっちゃ好評だったやつ
f:id:uutarou:20190514230216j:plain
CGみたいと言われた朝飯
f:id:uutarou:20190514230239j:plain
with オライリーな朝食

こんな感じ?

今の所9割くらいの晩飯は自炊してます。クラシルには頭が上がらない。

あと、一人暮らしになって変わったことといえば、家のネット回線がとっても速くなりました。(実家はADSLだった)

f:id:uutarou:20190514230512p:plain

マンションタイプの光の割には速いと思うんですよね。

せっかく高い金払って光引いたのでちゃんと活用してお勉強しようと思います。

まとめ

まとめもクソもない。

せっかく家に課金したので、この環境(出社時間が超短い/インターネットが引かれている静かな部屋が24時間使い放題)を活かして圧倒的成長を遂げたいです。

今日は眠いので寝ようかしらね。

【ReactNative】AndroidとiOSではキーボードが開いた時の挙動が違う

※タイトルにRNとかいてますが、おそらくRNに関係ない話だと思います。多分。

ReactNativeを使ってて、キーボードが開いた時の挙動がOS間で違うのでは?と思って調べてみたらやっぱりそうだった、という話。

どっかに書いてるかな、と思ったけど見つけられなかったのでここに書き残す。

結論 どう違うのか

なにがどう違うかというのをアニメGIFで撮ったので貼ります。

(追記: いらんとは思うけどコードあげました。 GitHub - uutarou10/rn-view-height-test )

https://files-uploader.xzy.pw/upload/20190223001844_3577626371.gif

(width/heightのラベルはそれぞれが100%に指定してある親Viewの値です)

キーボードの分、Viewが縮むのがAndroidの挙動で、そうではないのがiOSの挙動です。

なので、キーボードが開いた時にキーボードの下に要素が隠れないようにするとか、そういうものを作ろうとするとちょっと工夫する必要が出てきます。

でも…

Androidのキーボードが開いた時の挙動はAndroidManifest.xmlというファイルにあるandroid:windowSoftInputModeという値で設定されているようです。(この辺、Android開発者ではないのでかなり怪しめです。)

react-native-cliで作ったアプリは、この値が最初adjustResizeになっています。(ちなみにAndroidManifest.xmlはRNの場合、android/app/src/main/AndroidManifest.xmlにあります)

developer.android.com

こちらのページに書いていますが、この値をadjustPanにすると、フォーカス位置が隠れないように画面をズラしてくれます。

adjustPanに設定した場合の挙動はこうなります。

https://files-uploader.xzy.pw/upload/20190223003501_4343677778.gif

ただ、画面をズラしてくれると書いたように、Inputがキーボードが出る位置の下にある場合、フォーカスが当たると全体的に画面がズレます。

https://files-uploader.xzy.pw/upload/20190223004013_6675353965.gif

当然画面上にある要素は見えなくなってしまうので、実際に使うには注意が必要です。

実際、先ほどのAndroid公式のドキュメントには、

一般にこの方法はサイズ変更に比べると望ましくありません。ユーザーがソフト キーボードを閉じて、ウィンドウの隠れた部分を操作する必要が生じる可能性があるためです。

と書いています。

じゃあお前どうすりゃええねん、という話はまた調査して書こうかなと。