JavaScriptの勉強中:その5 Functions / Classes (途中まで)

今日も読むー。

bufferings.hatenablog.com

## Ch. 8 Functions

### 8.1 Defining Functions

関数宣言

function distance(x1, y1, x2, y2) {
  let dx = x2 - x1;
  let dy = y2 - y1;
  return Math.sqrt(dx*dx + dy*dy);
}
  • 関数につけた名前が変数名になる
  • 関数宣言ステートメントスクリプトや関数またはブロックの一番上に巻き上げられる(hoisted)。なので、定義してある場所より前でも呼び出すことができる。
  • ES6 より前は JavaScript ファイルまたは関数内のトップレベルでの定義しか許可されていなかったが、ES6 の strict mode ではブロック内での宣言が可能となった。そのブロック内からのみ利用可能となる。

関数式

const square = function(x) { return x*x; };
  • 関数名はオプショナル。関数宣言と違って、名前をつけても変数は登録されない。
  • 名前をつけると関数内のローカル変数になるので、再帰呼び出しで利用できる。
  • 関数式は hoist されないので定義前には利用できない。

アロー関数

  • ES6
// 全部書くとこう
const basic = (x, y) => { return x + y; };

// return ステートメントだけの場合は return と {} を省略できる
const simple = (x, y) => x + y;

// 引数が1つだけの場合は () を省略できる
const oneParam = x => x*x;

// 引数がない場合は () が必要
const noParam = () => 10;

// オブジェクトリテラルを返したい場合は曖昧になるので普通に書くか
const f = x => { return { value: x }; };
// () で囲む
const g = x => ({ value: x });
  • アローの前に改行を入れたらそこで終わってるって解釈されるからダメ
  • アロー関数は this を持たないので関数が定義されたコンテキストの this を参照する

this の扱いが一番大切そうだな。

### 8.2 Invoking Functions

  • 関数は「関数」または「メソッド」として呼び出される
  • return がなければ undefined が返される
  • 関数として呼び出された場合
    • this は、non-strict mode なら global object、strict mode なら undefined になる。
    • アロー関数は別。定義された場所の this を使う。
    • 基本的には関数では this は使わない。
  • メソッドは
    • オブジェクトのプロパティとして保持されている関数
    • そのオブジェクトを this で参照できる
    • メソッド内で定義した関数の中で this を使ってもメソッドのオブジェクトをは参照できないので注意。その場合は関数呼び出しとなるため。
"use strict";

let obj = {
    m: function() {
        console.log("mの中のthis=" + (this === obj));
        f();
        function f() {
            console.log("fの中のthis=" + this);
        }
    }
}
obj.m();

mの中のthis=true
fの中のthis=undefined

f() の中で this を使いたい場合は

  • アロー関数を使う
  • 一度ローカル変数に入れてそれを参照する
  • thisbind する

そういうときはアロー関数が一番読みやすそうね。

### 8.3 Function Arguments and Parameters

デフォルト値

  • デフォルト値の指定には以前は a = a || [] のように || オペレーターを使ってた
  • ES6 以降だとパラメーターにデフォルト値を指定できるのでそっちを使えばいい
    • デフォルト式には定数やリテラル以外にも変数や関数呼び出しも使える。その前のパラメーターも使える。へー。

Rest Parameters

  • ES6
  • Rest Parameters を使うと、数の決まっていないパラメーターを配列で受け取ることができる
function sample(first, ...rest) {
}
  • 3つのピリオド
  • 最後のパラメーターのみ
  • 空の配列にはなりうるが、 undefined になることはない

arguments

  • ES6 より前は arguments オブジェクトを使っていた
    • arguments オブジェクトは array-like オブジェクト
    • 引数名ではなくインデックスで参照

古いコードを読むときに出てくるかも、くらいだな。

Spread Operator

  • 関数呼び出し時に spread operator ... を使用することも可能
let numbers = [5, 2, 10];
Math.min(...numbers);

Destructuring assignment

  • パラメーターに destructuring assignment を使うことができる
// 配列
function vectorAdd([x1, y1], [x2, y2]) {
    return [x1 + x2, y1 + y2];
}
vectorAdd([1,2], [3,4]);

// オブジェクト
function vectorMultiply({x, y}, scalar) {
    return { x: x*scalar, y: y*scalar };
}
vectorMultiply({x: 1, y: 2}, 2);

// 違う変数名で受け取る
function vectorAdd2({x: x1, y: y1}, {x: x2, y: y2}) {
    return { x: x1 + x2, y: y1 + y2 };
}
vectorAdd2({x: 1, y: 2}, {x: 3, y: 4});

// デフォルト値を設定することもできる
function vectorMultiply2({x, y, z = 0}, scalar) {
    return { x: x*scalar, y: y*scalar, z: z*scalar };
}
vectorMultiply2({x: 1, y: 2}, 2);

// 配列の残りを rest parameter で受け取ることもできる
// 本当の rest parameter とは別物
function f([x, y, ...coords], ...rest) {
    return [x+y, ...rest, ...coords];
}
f([1,2,3,4],5,6); // => [ 3, 5, 6, 3, 4 ]

// (ES2018) オブジェクトに対しても rest parameter が使える
function vectorMultiply3({x, y, z=0, ...props}, scalar) {
    return { x: x*scalar, y: y*scalar, z: z*scalar, ...props };
}
vectorMultiply3({x: 1, y: 2, w: -1}, 2); // => { x: 2, y: 4, z: 0, w: -1 }

// オブジェクトと配列の混ざったやつもOK
function drawCircle({x, y, radius, color: [r, g, b]}) {
}
  • object の destructured parameters を使うと、名前付き引数に似たものを実現できる

### 8.4 Functions as Values

  • function はオブジェクトの一種なのでプロパティを持つことができる
f.counter = 0;
function f() {
    return f.counter++;
}
f(); // => 0
f(); // => 1

んー。面白いけど、Closureでやれば良さそう。

### 8.5 Functions as Namespaces

これ、jQueryでよく見たやつかな。

  • 関数内で定義されたローカル変数は外からは参照できないので汚さなくて済む
function chunkNamespace() {
    // ここに色々処理
}
chunkNamespace(); // 呼び出しを忘れずに

でもこれだと chunkNamespace という関数自体は登録されてしまうので、それもなくすとこうなる:

(function() { // 最初の丸括弧でこれが関数宣言じゃなくて関数式だとみなされる
    // ここに色々処理
}()); // 呼び出し

### 8.6 Closure

  • JavaScript はレキシカルスコープなので、関数が定義された場所のローカル変数がバインドされる。そういうのを Closure と呼ぶ。

あぁ、さっきのやつの Closure 版でてきた:

let f = (function() {
    let counter = 0;
    return function() { return counter++; };
}());
f(); // => 0
f(); // => 1
  • this を使いたいときはローカル変数にしてそれを参照することで、使うことができる。そういうときはだいたい self とか that って名前を使うのかな?
const self = this;

### 8.7 Function Properties, Methods, and Constructor

call() apply()

関数をあるオブジェクトに対するメソッドのように呼び出すことができる。第一引数に渡したオブジェクトが this になる。

f.call(obj);
f.apply(obj);
  • アロー関数の this はレキシカルスコープを使うから call()apply() では上書きされない。
  • call() は関数に渡したい引数をそのまま第二引数以降に指定する
  • apply() は関数に渡したい引数を第二引数に配列として指定する
f.call(obj, 1, 2);
f.apply(obj, [1, 2]);
  • ES5 までは spread operator がなかったから、任意の数の引数をとる関数に対しては apply() で配列を渡してた。
  • ES6 以降だと spread operator があるから call() だけでも良さそうかな。

bind()

  • 関数にオブジェクトをバインドして新しい関数を返す
  • this はそのオブジェクトになる
  • バインド済みの関数をオブジェクトのプロパティに設定しても、既にバインドしてるオブジェクトに対して呼び出される
  • アロー関数の this は上書きされない
  • bind() のよくある使い方は、アロー関数じゃない関数をアロー関数のようにしてしまうこと

でも bind() メソッドには単にバインドする以上のことができる

  • bind() の2番目以降の引数に値を渡すと、その引数もバインドされる(currying)
> let sum = (x,y) => x + y;
undefined
> let succ = sum.bind(null, 1);
undefined
> succ(2);
3

### 8.8 Functional Programming

読んで「面白いなー」と思ったけど、今はあんまり覚えなくていいかなぁと思ったので頭の片隅に入れとくくらいにしとく。

## Ch. 9 Classes

ES6 で class キーワードが導入されたりして書きやすくなったけど、まずは古いスタイルからたどっていく。

### 9.1 プロトタイプ

  • JavaScript でいう「クラス」は、同じプロトタイプオブジェクトからプロパティを継承しているオブジェクトのこと
  • Object.create() でプロトタイプオブジェクトを指定すれば JavaScript のクラスができる

こんな感じ

function range(from, to) {
    let r = Object.create(range.methods);
    r.from = from;
    r.to = to;
    return r;
}

// 関数オブジェクトのプロパティとして定義してる
range.methods = {
    includes(x) { return this.from <= x && x <= this.to; },
}

let r = range(1,3);
console.log(r.includes(2)); // => true
  • range() がファクトリー関数になってる
  • range.methods で定義してるメソッドの中で this を使って呼び出し元のオブジェクトにアクセスしてる

### 9.2 コンストラクター

// Range オブジェクトのコンストラクター関数
// コンストラクター関数は最初を大文字で始めるのが慣習
function Range(from, to) {
    // new で呼び出されると自動的に作成されたオブジェクトが this に入ってて
    // それが戻り値として返される
    this.from = from;
    this.to = to;
}

// コンストラクター関数の prototype プロパティがプロトタイプとして使用される
Range.prototype = {
    includes(x) { return this.from <= x && x <= this.to; },
}

let r = new Range(1,3);
console.log(r.includes(2)); // => true

instanceof

  • 2つのオブジェクトのプロトタイプオブジェクトが同じだったら、それは同じクラスのインスタンスだということ
  • コンストラクター関数が違っていてもプロトタイプが同じなら同じクラスだと言える
  • instanceofコンストラクター関数の prototype プロパティをオブジェクトが継承している場合に true を返す
r instanceof Range
range.methods.isPrototypeOf(r);

constructor プロパティ

  • さっきは Range.prototype を定義したけど、JavaScriptの関数は最初から prototype プロパティを持ってる
  • で、その事前に定義された prototype プロパティには constructor というプロパティがあって、それが元の関数を指している
  • なので、普通の関数を new を使って呼び出したら constructor というプロパティが存在して、それがコンストラクターとして使用した関数を指すことになる
> let F = function(){};
undefined
> let obj = new F();
undefined
> obj.constructor === F;
true

おー

  • さっき定義した Range 関数は prototype プロパティを上書きしてるから、ちゃんと constructor プロパティを定義しておく方がいいい
Range.prototype = {
    constructor: Range,

    includes(x) { return this.from <= x && x <= this.to; },
}
  • もうひとつ、古いコードで見られる方法は、事前に定義された prototype にメソッドを追加する方法
Range.prototype.includes = function(x) {
    return this.from <= x && x <= this.to;
}
Range.prototype.otherMethod = function() {
    // ...
}

### 9.3 class キーワード

ついにきた。ES6 で導入された class キーワード。

class Range {
    constructor(from, to) {
        this.from = from;
        this.to = to;
    }

    includes(x) { return this.from <= x && x <= this.to; }
}

let r = new Range(1,3);
console.log(r.includes(2)); // => true

これでさっきのやつと全く同じになる。

  • コンストラクター関数は constructor というキーワードで定義するけど Range という変数が使用されて、 Range というコンストラクター関数になる。
  • また constructor プロパティに Range 関数が入ってる
  • メソッドの定義が複数ある場合でもカンマは不要

大切なこと2つ

  • class 定義の中は暗黙的に strict mode になる
  • class 定義は関数定義と違って hoist されない

Static Methods

  • static キーワードをつけて static メソッドを定義できる
  • static メソッドは、プロトタイプオブジェクトのプロパティではなく、コンストラクターのプロパティとして定義される

Getter, Setter

  • オブジェクトのときと同じようにして Getter と Setter を定義できる

フィールド

  • ES6 ではメソッドの定義しか許可されていない
  • フィールドを定義したい場合は、コンストラクター関数かメソッドの中で生成しないといけない
  • static フィールドを定義したい場合は、クラスを定義した後に外側で定義しないといけない
class Range {
    constructor(from, to) {
        // フィールドを定義
        this.from = from;
        this.to = to;
    }

    includes(x) { return this.from <= x && x <= this.to; }
}

// static フィールドを定義
Range.MIN = -100;

新たに標準化が進んでいる書き方

現在、フィールドに対する標準化が進んでるところ。こんな風にかけるようになる。

class Range {
    from = 0;
    to = 0;

    includes(x) { return this.from <= x && x <= this.to; }
}
  • フィールドの初期化をコンストラクターの外に書くことができる
  • フィールドの初期値を書かなくても大丈夫。でも、その場合は undefined になるからちゃんと初期値を設定したほうがいい。

プライベートフィールドも定義できるようになる。 #プレフィックス。クラス内では使えるけど、外からは参照できない。

class Buffer {
    #size = 0;
    get size() { return this.#size; }
}

static フィールドはフィールド宣言の前に static キーワードをつければ良い。

眠くなってきたから今日はこれくらいにしとこ。次は 9.4 から。