WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

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

by WebSurfer 2024年2月12日 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

連動ドロップダウンリスト (ASP.NET Core MVC)

by WebSurfer 2024年2月8日 15:12

ASP.NET Core MVC での連動ドロップダウンリストの実装方法を備忘録として書いておきます。先の記事「連動ドロップダウンリスト (MVC5 版)」をターゲットフレームワーク .NET 8.0 の ASP.NET Core MVC ベースで書き直したものです。

ASP.NET Core MVC の連動 DropDownList

上の画像がそれで、先の記事と同様に、Microsoft の SQL Server サンプルデータベース Northwind の Categories テーブルと Products テーブルを使って、1 つめのドロップダウンリストで分類 (Categories) を選択すると、2 つめのドロップダウンリストにはその分類に属する製品 (Products) が絞り込まれて表示されるようにしています。

Categories テーブルと Products テーブルは以下の通りとなっています。(EDM のダイアグラムを表示したものです)

Categories  テーブルと Products テーブル

分類ドロップダウンリストの選択に応じて jQuery ajax を利用してアクションメソッドに要求を出し、選択された分類に該当するProducts テーブルの ProductID と ProductName を JSON 形式の応答として取得し、製品ドロップダウンリストの内容を書き換えるようにしています。

アプリの作り方の概要を以下に書きます。

(1) プロジェクトの作成

Visual Studio 2022 のテンプレートの中から「ASP.NET Core Web アプリ (Model-View-Controller)」を選び ASP.NET Core MVC アプリのプロジェクトを作成します。

テンプレート

この記事の例ではターゲットフレームワークを .NET 8.0 にしています。その他のバージョンではこの記事の内容と多少違いがあるかもしれません。

(2) コンテキストクラスとエンティティクラスの作成

Entity Framework を利用するので、リバースエンジニアリングという手法を使って、SQL Server データベース Northwind からコンテキストクラスとエンティティクラスを生成します。

作成方法は Microsoft のドキュメント「スキャフォールディング (リバース エンジニアリング)」を見てください。各パラメータについては、こちらの記事 Scaffold-DbContext がまとまっていて分かりやすいと思います。

参考に、EF Core パッケージマネージャーコンソール (PMC) ツールを使った場合の Scaffold-DbContext コマンドの例を下に載せておきます。

Scaffold-DbContext -Connection "Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=Northwind;Integrated Security=True" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Products, Categories, Suppliers, Customers, Orders, Employees, Shippers, "Order Details" -DataAnnotations

(3) コンテキストクラスを DI コンテナに追加

上のステップ (2) で作成したコンテキストクラス NorthwindContext を DI コンテナに追加します。Program.cs に以下のコードを追加してください。"NorthwindConnection" は接続文字列名で任意です。

builder.Services.AddDbContext<NorthwindContext>(options =>
    options.UseSqlServer(builder.Configuration
           .GetConnectionString("NorthwindConnection")));

appsettings.json に接続文字列を追加します。名前は上のコードで設定したもの(この例では "NorthwindConnection")とし、接続文字列本体はリバースエンジニアリングの Scaffold-DbContext コマンドで使ったものと同じにします。ただし、文字列中の \ は \\ にエスケープする必要があるので注意してください。

(4) PruductsController と View の作成

Visual Studio のスキャフォールディング機能を使って、Products テーブルの CRUD (Create / Read / Upadate / Delete) を行う Controller と View 一式を生成します。

アプリを実行してみて CRUD 機能が期待通り動くことを確認します。

(5) アクションメソッドの追加

この記事の一番上の画像の連動ドロップダウンリスト用のアクションメソッドを作成し、上のステップ (4) で作った PruductsController に追加します。

まずベースとなるビューモデルを作成します。コードは以下の通りです。クラス名は任意ですがここでは Sales としています。

using System.ComponentModel.DataAnnotations;

namespace MvcNet8App.Models
{
    public class Sales
    {
        public int Id { get; set; }

        [Display(Name = "分類")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int CategoryId { get; set; }

        [Display(Name = "製品")]
        [Required(ErrorMessage = "{0} の選択は必須")]
        public int ProductId { get; set; }

        [Display(Name = "コメント")]
        [Required(ErrorMessage = "{0} は必須")]
        public string? Comment { get; set; }
    }
}

連動ドロップダウンリスト用のアクションメソッド CascadingDropDown と、分類ドロップダウンリストの選択に応じて Products テーブルから選択された分類に該当する ProductID と ProductName を取得して JSON 形式で返すアクションメソッド GetProducts を作成し、PruductsController に追加します。

コードは以下の通りです。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet8App.Data;
using MvcNet8App.Models;

namespace MvcNet8App.Controllers
{
    public class ProductsController : Controller
    {
        private readonly NorthwindContext _context;

        public ProductsController(NorthwindContext context)
        {
            _context = context;
        }

        // 中略(スキャフォールディングで自動生成されたコード)

        public IActionResult CascadingDropDown()
        {
            ViewData["CategoryId"] = 
                new SelectList(_context.Categories, "CategoryId", "CategoryName");
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult CascadingDropDown(
            [Bind("ProductId,CategoryId,Comment")] Sales sales)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");
            }

            // 検証結果 NG で再描画する際は以下のように再度 ViewBag
            // に SelectList を設定しないと分類ドロップダウンの中身
            // が表示されないので注意
            ViewData["CategoryId"] = 
                new SelectList(_context.Categories, "CategoryId", "CategoryName");
            return View(sales);
        }

        // 製品ドロップダウンに表示する ProductID と ProductName を
        // JSON 形式で取得するアクションメソッド。引数 id が分類ドロ
        // ップダウンで選択された CategoryID
        public async Task<IActionResult> GetProducts(int id)
        {
            var products = _context.Products
                           .Where(p => p.CategoryId == id)
                           .Select(p => new
                           {
                               ProductId = p.ProductId,
                               ProductName = p.ProductName
                           });

            return Json(await products.ToListAsync());
        }
    }
}

上のアクションメソッド GetProducts が返す JSON 文字列は、デフォルトでは Camel Casing される ( {"name":"value"} の "name" の先頭の文字が小文字になる) ので注意してください。(詳しくは先の記事「JsonSerializer の Camel Casing (CORE)」を見てください)

Camel Casing されないようにするには、Program.cs のコードの中に AddControllersWithViews() メソッドが含まれているはずなので、それに以下のように .AddJsonOptions(opttions => ... 以下のコードを追加します。

builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

これをしないで、クライアントスクリプトで対応しても OK です。いずれにしても、対応しないと製品ドロップダウンリストの内容が undefined となってしまいますので注意してください。

(6) View の追加

上のステップ (5) で作成したアクションメソッド CascadingDropDown に対応する View をスキャフォールディング機能を利用して作成します。レイアウトページ _Layout.cshtml を利用しています。コードは以下の通りです。説明はコメントに書きましたので、それを見てください

@model MvcNet8App.Models.Sales

@{
    var data = ViewBag.CategoryID;
    
    ViewData["Title"] = "CascadingDropDown";    
}

<h1>CascadingDropDown</h1>

<div class="row">
    <div class="col-md-4">
        <form asp-action="CascadingDropDown">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="CategoryId" class="control-label"></label>

                @* 
                スキャフォールディングで生成される input を select に書き換え
                *@
                <select asp-for="CategoryId" class="form-control" 
                    asp-items="ViewBag.CategoryId">
                    <option value="">▼分類を選択してください▼</option>
                </select>

                <span asp-validation-for="CategoryId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ProductId" class="control-label"></label>

                @*
                スキャフォールディングで生成される input を select に書き換え
                asp-for="ProductId" が無いと検証が動かないので注意 
                *@
                <select asp-for="ProductId" class="form-control"></select>

                <span asp-validation-for="ProductId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Comment" class="control-label"></label>
                <input asp-for="Comment" class="form-control" />
                <span asp-validation-for="Comment" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

    <script type="text/javascript">
        //<![CDATA[

        $(function () {
            // 分類ドロップダウンの jQuery オブジェクトを取得。
            // html コードの id は html ヘルパの Id メソッドで取得
            var categoryDDL = $('#@Html.Id("CategoryId")');
            var productDDL = $('#@Html.Id("ProductId")');

            // 以下は初期画面および検証結果 NG での再描画の際必要。
            // 初期画面では categoryDDL.val() は上の option 要素の
            // value="" を取得するので if 文の条件は false となる。
            // 検証結果 NG で再描画される際は分類ドロップダウンの
            // 選択に応じて製品ドロップダウンの内容を書き換える
            if (categoryDDL.val() != "") {
                productDDL.children().remove();
                productDDL.append(
                    '<option value="">▼製品を選択してください▼</option>');
                productDDL.removeAttr('disabled');

                // 分類ドロップダウンの選択結果 (CategoryId) を
                // jQuery ajax でサーバー側の GetProducts アクション
                // メソッドに送信。アクションメソッドは Products テ
                // ーブルに SELECT クエリを発行し、CategoryId に属す
                // 製品の ProductId, ProductName を取得して JSON 文
                // 字列として返す。その JSON 文字列から option 要素
                // の文字列を組み立てて製品ドロップダウンに追加する
                $.ajax({
                    url: '/Products/GetProducts/' + categoryDDL.val(),
                    method: 'get',
                }).done(function (data) {
                    $.each(data, function (key, val) {
                        productDDL.append('<option value=' +
                            val.ProductId + '>' + val.ProductName +
                            '</option>');
                    });
                }).fail(function (jqXHR, textStatus, errorThrown) {
                    alert('Error getting products!');
                });
            } else {
                productDDL.children().remove();
                productDDL.attr('disabled', 'disabled');
            }

            // 分類ドロップダウンの選択が変更されると change イベント
            // が発生 するのでそのリスナで製品ドロップダウンの内容を
            // 分類に応じて書き換え。リスナの中身は上と同じコード
            categoryDDL.on("change", function () {
                if (categoryDDL.val() != "") {
                    productDDL.children().remove();
                    productDDL.append(
                        '<option value="">▼製品を選択してください▼</option>');
                    productDDL.removeAttr('disabled');

                    $.ajax({
                        url: '/Products/GetProducts/' + categoryDDL.val(),
                        method: 'get',
                    }).done(function (data) {
                        $.each(data, function (key, val) {
                            productDDL.append('<option value=' +
                                val.ProductId + '>' + val.ProductName +
                                '</option>');
                        });
                    }).fail(function (jqXHR, textStatus, errorThrown) {
                        alert('Error getting products!');
                    });
                } else {
                    productDDL.children().remove();
                    productDDL.attr('disabled', 'disabled');
                }
            });
        });

        //]]>
    </script>
}

Tags:

CORE

JavaScript の console.log()

by WebSurfer 2024年1月31日 20:15

JavaScript の console.log() で開発者ツールの Console に JavaScript オブジェクトや html 要素 (DOM) を出力すると、Console を開いた時点での内容 (下の (1) の時点での内容ではなくて) が表示されるという話を書きます。

開発者ツールの Console 出力

Qiita の質問「javascriptとブラウザのコンソールについて」で調べたことですが、忘れそうなので自分のブログに備忘録として残しておこうと思った次第です。

上の画像の JavaScript のコードの (1) を見てください。obj = { prop: 123 } という JavaScript オブジェクトを作成してから即 console.log(obj) とし、その後で prop の値を 456 に書き換えています。しかしながら、コンソールを開いて見ると prop が書き換えられた後の 456 という結果が表示されています。

これは、MDN のドキュメント console: log() static method の「Logging objects」のセクションに書いてあるように、"Information about an object is lazily retrieved. This means that the log message shows the content of an object at the time when it's first viewed, not when it was logged." ということによります。

コード上で console.log() とした時点での JavaScript オブジェクトのログを取りたい場合は、MDN のドキュメントに書いてあるように "A common way is to JSON.stringify() and then JSON.parse() it" とするのが良さそうです。それが上の画像の JavaScript のコード (2) です。

Console を開いた時点での内容が表示されるというのは、JavaScript オブジェクトだけでなく、 html 要素 (DOM) を JavaScript で書き換えても同じことが起こります。

上の画像の (4) を見てください。JavaScript で html の p 要素を生成して id と textContent を設定してから即 console.log(p) し、その後で id と textContent を書き換えています。しかし、Console には書き換えた後の結果が表示されています。

コード上で console.log() とした時点での p 要素のログを取りたい場合は、p.outerHTML を出力するのが良さそうです。それが上の画像の (5) です。

JavaScript はブラウザ依存ですが、Windows 10 PC の Edge 121.0.2277.83, Chrome 121.0.6167.140, Firefox 122.0, Opera 106.0.4998.66 で試して同じ結果となることは確認しました。

Tags: , ,

JavaScript

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar