1
1

More than 1 year has passed since last update.

[JavaScript]thisのスコープ理解していない人がいるわけない。(私は知らなかった)

Posted at

はじめに

画像アップロード機能を実装していたときにスコープに苦しめられた話です。

FileReaderやImageオブジェクトを使って苦手な非同期処理をしていたので、ハマっている原因が整理できずに泥沼化してました。。。

できごと

やりたいこと

  • <input type=’file’>で画像選択時にリアルタイムで選択した画像が表示される
  • 最終的に選択した画像のbase64の値をサーバーに送信したいので、ファイルが選択されたらcanvas→blob→base64の流れでデータを変換する

参考にした記事

'input type=file'から'canvas'への転写

ほぼこちらのソースコードをコピーで実装

ソースコード

upload-component.vue

<template>
    <div>
        <div>
            <input
                type="file"
                @change="uploadFile"
            >
            <!-- サーバーにリクエストする値 -->
            <input
                type="hidden"
                name="icon_base64"
                :value="iconBase64"
            >
            <canvas
                id="canvas"
            />
        </div>
    </div>
</template>

<script>
export default {
    data () {
        return {
            iconBase64: '',
        }
    },
    methods: {
        uploadFile(event) {
            const file = event.currentTarget.files[0];

            const readerForFile = new FileReader();
            readerForFile.onload = function() {
                // ファイルが読み込まれたあとの処理
                const image = new Image();
                image.onload = async function() {
                    // imageを転写するcanvasの取得、加工
                    const canvas = document.getElementById('canvas');
                    canvas.width = image.width;
                    canvas.height = image.height;
                    const context = canvas.getContext("2d");
                    // canvas(CanvasRenderingContext2D)に画像を転写
                    context.drawImage(image, 0, 0);

                    // canvas -> blobの変換
                    const blob = await this.canvasToBlob(canvas);
                    // blob -> base64の変換してプロパティに代入
                    this.iconBase64 = await this.readAsDataURL(blob);
                }
                // 画像がimageタグに読み込み->image.onloadイベント発火
                image.src = readerForFile.result;
            }
            // Fileオブジェクトを読み込む->readerForFile.onloadイベント発火
            readerForFile.readAsDataURL(file);
        },
        canvasToBlob(canvas) {
            return new Promise((resolve) => {
                canvas.toBlob(resolve, 'image/png');
            });
        },
        readAsDataURL(blob) {
            return new Promise((resolve) => {
                const reader = new FileReader();
                reader.onloadend = () => {
                    resolve(reader.result);
                };
                reader.readAsDataURL(blob);
            });
        },
    },
}
</script>

結果

以下エラーが発生

TypeError: canvasToBlob is not a function...

エラー箇所はココ。

// canvas -> blobの変換
const blob = await this.canvasToBlob(canvas);

でも、canvasToBlobは定義しているよ?ということでもう一度コードを読み返す。

結論

はい、そうです。thisのスコープでした。。。

今回以下のようにFileReaderやImageのonloadで非同期処理をしていて

readerForFile.onload = function() {
  ...
  image.onload = async function() {
    ...

その中で

// canvas -> blobの変換
const blob = await this.canvasToBlob(canvas);

これをやっていると。

実際にcanvasToBlobを定義しているのはVueComponent内(※)で定義しているので、そりゃthis.canvasToBlobでアクセスできないよねって話です。

※イメージ湧かない場合はvueのexport default内でconsole.log(this)やるとわかります。

対策

onload内でthis(=VueComponent)にアクセスしたい。でも、this.canvasToBlobでアクセスできない。

ということでthisを変数化してしまえばOK

参考記事

Vue.jsのdataオブジェクトを参照する際に Uncaught (in promise) TypeError: Cannot set property 'foo' of undefined

ソースコード(修正後)

    methods: {
        uploadFile(event) {
            const file = event.currentTarget.files[0];

			const vm = this;
            const readerForFile = new FileReader();
            readerForFile.onload = function() {
                // ファイルが読み込まれたあとの処理
                const image = new Image();
                image.onload = async function() {
										...
										...
                    // canvas -> blobの変換
                    const blob = await vm.canvasToBlob(canvas);
                    // blob -> base64の変換してプロパティに代入
                    this.iconBase64 = await vm.readAsDataURL(blob);
                }
                // 画像がimageタグに読み込み->image.onloadイベント発火
                image.src = readerForFile.result;
            }
            // Fileオブジェクトを読み込む->readerForFile.onloadイベント発火
            readerForFile.readAsDataURL(file);
        },
        canvasToBlob(canvas) {
						...
        },
				...
    },

const vm = this;でVueComponentを示すthisを変数に格納してonload内でもcanvasToBlobが呼べるようになりました!

おわりに

「こんだけ?」といわれるかもですが、すいません、すごくハマったのは事実なんです。。。

改めてJavaScriptのスコープの概念の理解不足が露呈した瞬間でした。。。

久しぶりにJavaScriptを触って、改めてJavaScriptに対してのアレルギーを感じることを実感したので、コツコツ言語仕様を理解しようかと思います。

最後までお読みいただきありがとうございます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1