WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

JavaScript の関数内での this (その 2)

by WebSurfer 12. February 2024 18:20

JavaScript の関数内での this」の続きです。先の記事ではクラス内に定義した関数の話を書きましたが、この記事ではオブジェクト内に定義した関数について書きます。

クラス内でもオブジェクト内でも同じかと思っていたのですが、実はアロー関数の場合は this に設定されるものが違ってくるということを知ったので、調べたことをまとめて備忘録として書いておくことにした次第です。

下の画像のように Person オブジェクトをグローバルコンテキストで定義した場合、Visual Studio 2022 のインテリセンスで f2 に設定したアロー関数内の this を見ると this: typeof globalThis となっています。

オブジェクトの場合

MDN のドキュメント「globalThis」によると "globalThis はグローバルプロパティで、グローバルオブジェクトと同等であるグローバルな this が格納されています" とのことで、ブラウザでは window になるそうです。実際 Person.f2() を実行するとコンソールには window と出力されました。

MDN のドキュメント「this」に以下の説明があり、上の画像の (1), (2) はその通りの結果になっています。

(1) 「オブジェクトのメソッドとして」のセクション: 関数がオブジェクトのメソッドとして呼び出されるとき、その this にはメソッドが呼び出されたオブジェクトが設定されます。

(2) 「アロー関数」のセクション: アロー関数では、this はそれを囲む構文上のコンテキストの this の値が設定されます。グローバルコードでは、グローバルオブジェクトが設定されます。

アロー関数で this を使うことのメリットについては、MDN のドキュメント「アロー関数式」の「例」のセクションに以下の説明があり、setTimeout を使ったコード例が載っています。

"おそらくアロー関数を使う最大の利点は、 DOM レベルのメソッド(setTimeout() や EventTarget.prototype.addEventListener())で、通常は何らかのクロージャ、call()、apply()、bind() を使用して、関数が適切なスコープで実行されることを確認する必要があることです。"

addEventListener() を使った場合はどういう利点があるか、自分が考えた話なのでイマイチかもしれませんが、以下に例を書きます。

<body>
    <button type="button" id="button1">button</button>
</body>
<script>
    document.getElementById("button1")
            .addEventListener("click", listener);

    function listener() {
        const Person1 = {
            name: "Person1",
            f1: function () { console.log(this); },
            f2: () => { console.log(this); }
        }

        Person1.f1();  // Object
        Person1.f2();  // <button type="button" id="button1">button</button>
}
</script>

関数がイベントハンドラとして使用された場合 this はリスナーがアタッチされている DOM に設定されることを期待するはずです。上のコード例ではアロー関数の this は期待通りとなりますが、function() { ... } 内の this は Person1 オブジェクトが設定されます。

もう一つ、Promise オブジェクトのメソッド then() の中に設定され、非同期で実行されるコールバックの場合もアロー関数を使う利点があると思います。その例を以下に書きます。

const WeatherData = {
    result: "",
    setData: function (data) { this.result = data; },        
    fetchData: function() {
        fetch('jsonSample.json')
            .then(function (response) {
                return response.json();
            })
            //.then(function (data) {
            //    this.setData(data[0].name); // this は window
            //});
            .then(data => {
                this.setData(data[0].name);  // this は WeatherData
                console.log("fetchData:", this);
            });
    }
}

WeatherData.fetchData();

上のコードで、コメントアウトした方の this には window が設定されるので this.setData(data[0].name); で "Uncaught (in promise) TypeError: this.setData is not a function" というエラーになります。

一方、その下のアロー関数を使った場合、this には WeatherData オブジェクトが設定され、期待通り this.setData(data[0].name); で WeatherData オブジェクトの result プロパティが書き換えられます。

ちなみにですが、クラスの場合はアロー関数内の this は下の画像のようになり、クラス本体の this すなわちクラスのインスタンスが設定されるようです。

クラスの場合

これに関しては、MDN のドキュメント「アロー関数式」の「メソッドとしては使用不可」のセクションに以下の説明があります。

"クラスの本体は this コンテキストを持っているので、クラスフィールドのようなアロー関数はクラスの this コンテキストを閉じ、アロー関数の本体の中の this はインスタンス(または静的フィールドの場合はクラス自体)を正しく参照します。しかし、これは関数自身のバインディングではなく、クロージャであるため、 this の値が実行コンテキストによって変わることはありません。"

以下に検証に使ったコードを載せておきます。

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <button type="button" id="button1">button</button>
</body>
<script>
    // グローバルコンテキストでオブジェクトを定義
    const Person = {
        name: "Person",
        f1: function () { console.log(this); },
        f2: () => { console.log(this); }
    }

    Person.f1();    // Object -------- (1)
    Person.f2();    // Window -------- (2)

    // addEventListener で設定するリスナでアロー関数を使う
    document.getElementById("button1")
            .addEventListener("click", listener);

    function listener() {
        const Person1 = {
            name: "Person1",
            f1: function () { console.log(this); },
            f2: () => { console.log(this); }
        }

        Person1.f1();    // Object
        Person1.f2();    // <button type="button" id="button1">button</button>
    }

    // 以下は蛇足でクロージャーとクラスの例

    // クロージャー
    const Person2 = function () {
        this.Name = "クロージャー";
        this.f1 = function () { console.log(this); }
        this.f2 = () => { console.log(this); }
    }

    let p2 = new Person2();
    p2.f1();    // Person2
    p2.f2();    // Person2

    // クラス
    class Example {
        Name = "クラス";
        f1 = function () { console.log(this); }
        f2 = () => { console.log(this); }
    }    

    let e = new Example();
    e.f1();    // Example -------- (3)
    e.f2();    // Example -------- (4)

    // then() の中に設定するコールバックでアロー関数を使う
    const WeatherData = {
        result: "",
        setData: function (data) { this.result = data; },        
        fetchData: function() {
            fetch('jsonSample.json')
                .then(function (response) {
                    return response.json();
                })
                //.then(function (data) {
                //    this.setData(data[0].name); // this は Window
                //});
                .then(data => {
                    this.setData(data[0].name);  // this は WeatherData
                    console.log("fetchData:", this);
                });
        },

        fetchData2: async function () {
            const response = await fetch('jsonSample.json');
            const data = await response.json();
            this.setData(data[0].name);
            console.log("fetchData2:", this);
        }
    }

    WeatherData.fetchData();

    WeatherData.fetchData2();
</script>
</html>

上のコードを実行したコンソール出力は以下の通りです。最後の 2 つは button をクリックして listener() を起動して出力させたものです。

コンソール出力

Tags: , , ,

JavaScript

JavaScript の関数内での this

by WebSurfer 6. November 2023 17:41

JavaScript の関数内での this の使い方を備忘録として残しておきます。

MDN ドキュメント「this」に JavaScript の this についていろいろ書いてあります。関数内での this については「関数コンテキスト」のセクションに書いてあるように "関数の呼び出し方によって異なります" ということです。ただし、アロー関数の場合はそれは当てはまらないようです。

というわけで、関数内で this を使う場合は、常に「その this は何なのか?」ということを考えてコードを書く必要がありそうです。

どういうことか具体例を以下に書きます。

下の画像は、Visual Studio 2022 のテンプレートを使って作成した React + Web API のプロジェクトを実行し、React 側から fetch API を使って Web API から天気予報データを取得してブラウザに表示したものです。

天気予報データを取得しブラウザに表示

Visual Studio が自動生成した React 側の JavaScript のコードは以下の通りです。React の componentDidMount のタイミングでコードの下の方にある populateWeatherData メソッドが呼び出されると、fetch API を使って Web API から天気予報データを取得し、取得したデータを this.setState メソッドを使って state に設定しています。

import React, { Component } from 'react';

export class FetchData extends Component {
    static displayName = FetchData.name;

    constructor(props) {
        super(props);
        this.state = { forecasts: [], loading: true };
    }

    componentDidMount() {
        this.populateWeatherData();
    }

    static renderForecastsTable(forecasts) {
        return (
            <table className="table table-striped" aria-labelledby="tableLabel">
                <thead>
                    <tr>
                        <th>Date</th>
                        <th>Temp. (C)</th>
                        <th>Temp. (F)</th>
                        <th>Summary</th>
                    </tr>
                </thead>
                <tbody>
                    {forecasts.map(forecast =>
                        <tr key={forecast.date}>
                            <td>{forecast.date}</td>
                            <td>{forecast.temperatureC}</td>
                            <td>{forecast.temperatureF}</td>
                            <td>{forecast.summary}</td>
                        </tr>
                    )}
                </tbody>
            </table>
        );
    }

    render() {
        let contents = this.state.loading
            ? <p><em>Loading...</em></p>
            : FetchData.renderForecastsTable(this.state.forecasts);

        return (
            <div>
                <h1 id="tableLabel">Weather forecast</h1>
                <p>This component demonstrates fetching data from the server.</p>
                {contents}
            </div>
        );
    }

    async populateWeatherData() {
        const response = await fetch('weatherforecast');
        const data = await response.json();
        this.setState({ forecasts: data, loading: false });
    }
}

上のコードの一番最後の行の this.setState メソッドの this が、関数の書き方によってどう変わってくるかを以下に書きます。

上のコード例で populateWeatherData メソッドは FetchData クラスのメソッドなので、MDN のドキュメント this の Class context のセクションに "the this value is the object that the method was accessed on." と書いてあるように、this には FetchData オブジェクトが設定されます。

this は FetchData オブジェクト

結果、Web API から取得したデータは this.setSate メソッドによって FetchData オブジェクトの state に設定され、この記事の一番上の画像のように天気予報データがブラウザに表示されます。

では、以下のように async/await は使わないで then() の中でコールバックを呼ぶように変更し、コールバックを function () { ... } という形にして、その function の中で this を使うとどうなるでしょう?

this は undefined

上の画像の通り this は undefined となります。結果、Web API から取得したデータの state への設定は失敗しますので、初期画面で Loading... と表示された状態で止まってしまいます。

MDN のドキュメント「関数コンテキスト」に書いてありますが、(1) strict モードでは、実行コンテキストに入るときに this 値が設定されていないと、undefined のままになり、(2) strict モードでない場合は、this は既定でグローバルオブジェクトすなわち window となるそうです。then() の中で非同期に実行される場合は「実行コンテキストに入るときに this 値が設定されていない」ということになるようで、上の画像の例では (1) という結果になっています。

次に、上の画像の例のコールバック function () { ... } を以下のようにアロー関数に代えて、その中で this を使うとどうなるでしょうか?

this は FetchData オブジェクト

上の画像の通り this は FetchData オブジェクトとなります。結果、Web API から取得したデータは FetchData オブジェクトの state に設定され、期待通り天気予報データがブラウザに表示されます。

MDN のドキュメント「アロー関数」によると "アロー関数では、this はそれを囲む構文上のコンテキストの this の値が設定されます" ということで、this には FetchData オブジェクトが設定されたということのようです。

アロー関数というのは従来の function() { ... } の簡潔な代替構文に過ぎないと思っていたのですが、MDN のドキュメント「アロー関数式」に書いてあるように意図的な使用上の制限があるそうです。その中の一つが this で、アロー関数自身では this の結び付けを行わず、包含する構文上のコンテキストの this の値を保持するのだそうです。

Tags: , , ,

JavaScript

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2024  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar