RubyistがJavascriptの基礎学習をしてみる

2024/12/13に公開

Rubyしか書けないし、Rubyしか書いてこなかったので、
それ以外の言語も触れるようになりたいなと思って、
リファレンス等を見ながら何となくでやってたところをしっかり理解しようという意図。

学習しながら書き足しているのでメモみたいなもの。

あまり参考にしないほうがいいかもしれない。

というか今までJavascriptよくわかってないのによく業務こなせてたな...。

雰囲気でコード書いてたのでウンコード量産してそう、すまん。

クラス

そもそもJavascriptのRubyでいうクラスメソッドみたいなのがよくわかっていなかった。

Example.foo();

みたいなやつ。
Javascriptって関数以外にメソッドも定義できるの?くらい何もわかってない。

クラス宣言とクラス式

クラスを用意するにはクラス宣言とクラス式の2つの定義方法があるみたい。

// クラス宣言
class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

// クラス式
let Rectangle = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

Rubyでいうところの以下みたいなものだろうか。

# クラス宣言にあたる記法
class Name; end

# クラス式にあたる記法
Name = Class.new

多分おそらくだけどクラス宣言で定義するのが一般的だと思うので、そちらに絞って学習を続ける。

ちなみにどちらもホイスティング問題というのがあるらしい。

クラスにアクセスする前に、そのクラスを宣言する必要があります。そうしないと、ReferenceError がスローされます

クラス本体の記述

クラス本体はStrictモードというもので実行されるらしい。
通常エラーにならないけど、バグや落とし穴になりそうなところでエラーが起きるようになるものみたい。

あとはちょっと高速だったり、定義予定の構文を禁止(将来の予約後となる名前の使用禁止ってこと?)したりするとか。

要は厳密なコードを書く必要がありますよ、といったところだろうか。

constructor

classによって生成されるオブジェクトの生成や初期化を行う特別なメソッドです。

Rubyでいうところのinitializeメソッドのことかな?

各クラスに1つしか定義できず、2回以上定義されるとSyntaxErrorを発生させる。

class Name {
    constructor(){}
    constructor(){}
}
// Uncaught SyntaxError: Classes may not have a field named 'constructor'
class Name
  def initialize; end
  def initialize; end
end
# SyntaxOK

Rubyは何度でも定義できるのでちょっと差異があるけど、同じようなものという認識でいていいかな。

class Name {
    constructor(foo){console.log(foo)}
}
new Name('Hello Javascript')
// Hello Javascript
class Name
  def initialize(foo)
    puts foo
  end
end
Name.new('Hello Ruby')
# Hello Ruby

戻り値の違いはあるものの同じ感じ。

プロトタイプメソッド

サンプル

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // ゲッター
  get area() {
    return this.calcArea();
  }
  // メソッド
  calcArea() {
    return this.height * this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100

まぁ、サンプルパッと出されても何もわからないのでRubyに書き直して咀嚼する。

class Rectangle
  def initialize(height, width)
    @height = height
    @width = width
  end
  
  attr_reader :height, :width
  
  def area
    calc_area
  end
   
  def calc_area
    height * width
  end
end

square = Rectangle.new(10, 10)

print square.area # 100

こんな感じかな。ゲッターとある部分を単なるメソッドにしているのが意味合いを変えてしまっている気がしないでもないが...。

でもRubyのゲッターも単なるメソッドではあるので間違いではないか。

thisをどう解釈すればいいのかわからない、Rubyのselfではなさそう...。

thisってなんだ

ちょっと脱線するけど、thisがよくわからないので調べてみる。

thisって使われるタイミングで中身が左右されるみたいなのをみたことがある。

  1. メソッド呼び出しパターン
  2. 関数呼び出しパターン
  3. コンストラクタ呼び出しパターン
  4. apply,call呼び出しパターン

メソッド呼び出しパターン

メソッドの中で使われるthisのパターン。

const myObject = {
    value: 10,
    output: function() {
        console.log(this.value);
    }
};

myObject.output(); // 10

このサンプルはさっきまでの学習で知ったメソッドの定義と少し違うな...。

const myObject = {
    value: 10,
    output() {
        console.log(this.value);
    }
};

myObject.output(); // 10

これでも期待した値が出力できるのでSyntaxSugarなのかな。

後者が前者の簡略構文っぽい。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Method_definitions

結果から見るに、メソッド内のthisはそのメソッドが定義されているオブジェクト自身を参照するみたい。

関数呼び出しパターン

関数の中のthisはグローバルオブジェクトを参照するみたい。

グローバルオブジェクトのプロパティーに値をセットするとグローバル変数になるとか。

Javascriptは変数のスコープとかアクセスの種類とか覚えるのが難しそうだ。

function output(){
    console.log(this); // グローバルオブジェクト
    this.value = 1;
    console.log(this.value); // 1
}
output();

console.log(this.value); // 1
console.log(value); // 1

ここでJavascriptできない僕が気になったのが

const myObject = {
    value: 10,
    function output() {
        console.log(this.value)
    }
    output();
};

オブジェクトの中に直接関数定義した場合のthis

Javascriptちょっとわかる人ならすぐにわかると思うけど、これはSyntaxErrorだった。

オブジェクトの中に関数は直接定義できないみたい。

ただし...

const myObject = {
    value: 10,
    output() {
        console.log(this.value); // 10
        
        function output(){
            console.log(this.value); // undefined
            this.value = 1;
            console.log(this.value); // 1
        }
        output();
    }
};

myObject.output();
console.log(value); // 1

みたいなオブジェクトの中のメソッドの中には関数を定義できる。

関数の中のthisはたとえそれがメソッドの中であってもグローバルオブジェクトを参照するみたい。

難しいなぁ...。

もし、メソッド内の関数でオブジェクト自体を参照したい場合は、オブジェクトを参照している時点のthisを変数に入れて持っておくことが有効みたい。

その際によく使われる変数名がself,that,_thisらしい。

seifはブラウザ上だとwindow.selfを指すらしくて、それを上書きしてしまうのはどうなのかなという気もしないでもない。

とはいえ最も使われる変数名はselfみたいなのでそれに合わせたほうが良さそう。

const myObject = {
    value: 10,
    output() {
        console.log(this.value); // 10
        const self = this;
        function output(){
            console.log(self.value); // 10
        }
        output();
    }
};

myObject.output();
console.log(this.value); // undefined

ちなみに関数の定義は他にもアロー関数というものがあって、その中で使われるthisは関数呼び出しのパターンに当てはまらないみたい。

アロー関数の中のthisはその親のthisと等価みたい。

const myObject = {
    value: 10,
    output() {
        console.log(this.value); // 10
        output = () => {
            console.log(this.value); // 10
        };
        output();
    }
};

myObject.output();
console.log(this.value); // undefined
const myObject = {
    value: 10,
    output() {
        console.log(this.value); // 10
        getValue = () => { return this.value };
        function output(value) {
            console.log(value);
        };
        output(getValue()); // 10
    }
};

myObject.output();
const myObject = {
    value: 10,
    output() {
        console.log(this.value); // 10
        function output() {
            getValue = () => { return this.value };
            console.log(getValue()); // undefined
        };
        output();
    }
};

myObject.output();

アロー関数のthisは定義された場所から見て親のthisと同じものを参照するみたい。

実行された場所は関係ない。

アロー関数は構文も含めてちょっと難しいな...。

コンストラクタ呼び出しパターン

よくわからないのでまずはサンプルから

function MyObject(value) {
  this.value = value;
  this.increment = function() {
    this.value++;
  };
}
let myObject = new MyObject(0);
console.log(myObject.value); // 0

myObject.increment();
console.log(myObject.value); // 1

...なんだこれは、Javascriptはクラス以外もnewできるのか。

class MyObject {
    constructor(value) {
        this.value = value;
        this.increment = function() {
          this.value++;
        };
    }
}
let myObject = new MyObject(0);
console.log(myObject.value); // 0

myObject.increment();
console.log(myObject.value); // 1

これと等価っぽい。わからんけど。等価だと言ってくれ。

Javascript...わけがわからないな...。

関数定義であってもインスタンス化すると関数定義内のthisはインスタンス化された関数自身を指すようになるみたい。

apply,call呼び出しパターン

thisはこれを参照してね、というのを第一引数で指定してメソッドを実行できるものみたい。

let myObject = {
  value: 1,
  output: function() {
    console.log(this.value);
  }
};
let yourObject = {
  value: 3
};

myObject.output(); // 1

myObject.output.apply(yourObject); // 3
myObject.output.call(yourObject); // 3

関数に対しては使えないのかな...?

const myObject = {
  value: 1
};

function output(){
    console.log(this.value);
}

output.call(myObject) // 1

使えた。ふーん便利。

applycallの違いは第2引数以降の渡し方で、applyは配列で第2引数に丸ごと渡して、callは第2引数以降をそれぞれ渡すみたい。

ゲッター

get構文は、オブジェクトのプロパティを関数に結びつけ、プロパティが参照された時に関数が呼び出されるようにします。

まぁ当然(?)ながら「プロパティ」という概念すら雰囲気でやってきたのでよくわかっていないからそこから

プロパティ

JavaScript のオブジェクトは、自身に関連付けられたプロパティを持ちます。オブジェクトのプロパティは、オブジェクトに関連付けられている変数と捉えることができます。オブジェクトのプロパティは、オブジェクトに属するものという点を除けば、基本的に通常の JavaScript 変数と同じようなものです。

Rubyでいうインスタンス変数みたいなものかな。

class Sample {
    constructor(){
        this.foo = "Hello";
    }
}

let obj = new Sample;
console.log(obj.foo);
// Hello
class Sample
  def initialize
    @foo = "Hello"
  end
  
  attr_reader :foo # Rubyだと呼び出しのためのメソッド定義が必要
end

print Sample.new.foo
# Hello
let obj = {
    foo : "Hello"
};
console.log(obj.foo);
// Hello
obj = Class.new
obj.instance_variable_set('@foo', "Hello")
print obj.instance_variable_get('@foo')
# Hello

で、ゲッターの話に戻るけど、例のごとくサンプルをRubyに書き換えて咀嚼。

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    if (this.log.length == 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  }
};

console.log(obj.latest);
// expected output: "c"
obj = Class.new do
  @log = ['a', 'b', 'c']

  class << self

    attr_reader :log

    def latest
      return if log.size == 0
      
      log[log.size - 1]
    end
  end
end

print obj.latest
# expected output: "c"

こんな感じかな。

ゲッターとメソッドとの違いがいまいちよくわからない。

頭にgetをつけることで、呼び出し元で()を省略できますよってだけなんだろうか。

セッター

全くわからないけど、流れ的にRubyのattr_writer的なものかなとみる。

例のごとくサンプルをRubyに書き換えて咀嚼。

const language = {
  set current(name) {
    this.log.push(name);
  },
  log: []
};

language.current = 'EN';
language.current = 'FA';

console.log(language.log);
// expected output: Array ["EN", "FA"]
language = Class.new do
  @log = []

  class << self
  
    attr_reader :log

    def current=(name)
      @log << name
    end
   end
end

language.current = 'EN'
language.current = 'FA'

print language.log

こんな感じか。Rubyで書くと無理やりクラスを登場させているのでなんとなくわかりづらい感じもするが...。

これは個人のメモみたいなものなので気にせずにいこう。

話は戻ってJavascriptのゲッターとメソッドの違いを理解しないといけない。

ゲッターはRubyのメソッド呼び出しっぽく()を省略して呼べる。

メソッドはRubyっぽく呼び出すと、関数がそのまま返ってくる。

ちゃんと後ろに()をつけないと処理が行われない。

って感じかなぁ...。単に呼び出し方がちょっと違うだけなのかもしれない。

class Sample {
    method() {
        console.log("Hello Javascript")
    }
}

obj = new Sample;
obj.method;

// ƒ method() {
//     console.log("Hello Javascript")
// }

obj.method();
// Hello Javascript

現にゲッターのサンプルはget使わなくても

const obj = {
  log: ['a', 'b', 'c'],
  latest() {
    if (this.log.length == 0) {
      return undefined;
    }
    return this.log[this.log.length - 1];
  }
};

console.log(obj.latest()); // c

で期待した値を出力できるしなぁ...。

静的メソッド

staticキーワードでクラスに静的なメソッドを定義できるみたい。

静的メソッドは、クラスのインスタンス化なしで呼ばれ、インスタンス化されていると呼べません。

インスタンス化なしで呼ぶということは、これがRubyでいうクラスメソッドにあたるのかな。

いつもの

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  static distance(a, b) {
    const dx = a.x - b.x;
    const dy = a.y - b.y;

    return Math.hypot(dx, dy);
  }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.distance; //未定義
p2.distance; //未定義

console.log(Point.distance(p1, p2)); // 7.0710678118654755
class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
  
  attr_reader :x, :y

  class << self
    def distance(a, b)
      dx = a.x - b.x
      dy = a.y - b.y

      Math.hypot(dx, dy)
     end
  end
end

p1 = Point.new(5, 5)
p2 = Point.new(10, 10)
# p1.distance # NoMethodError
# p2.distance # NoMethodError

print Point.distance(p1, p2) # 7.0710678118654755

Math.hypot(dx, dy)のところとか全く同じものが使えるの面白い。

最初に睨んだ通り、静的メソッドはRubyのクラスメソッドみたいなものだった。

プロトタイプと静的メソッドによるボクシング

ボクシングっていうのがよくわからなかったので調べた。

Javascriptにはオブジェクト型とプリミティブ型の2種類があるらしい。

プリミティブ型は全6種類

  • 文字列('文字列')
  • 数値(3.14)
  • 真偽地(true)
  • シンボル(Symbol())
  • Null値(null)
  • 未定義(undefined)

これらはメソッドやプロパティを持たない。

対してRubyは全てがオブジェクトであり、全てのオブジェクトはメソッドを持っているので馴染みづらいな。

で、ボクシングというのはこれらプリミティブ型をオブジェクト型に変換してくれることを指すみたい。

ボクシングすることでオブジェクト型のように扱うことができ、メソッドやプロパティーを持たせることができる。

で、話は戻ってプロトタイプと静的メソッドによるボクシングの話になるが、クラスを書くときのStrictモードでは、
このボクシングが自動で行われないみたい。

なので、thisの値がundefinedの場合、メソッド内でもundefinedのままらしい。

クラスの外はStrictモードではないので、自動ボクシングが行われる。

自動ボクシングの際、最初のthisundefinedの場合、thisにはグローバルオブジェクトが入ります。

function Animal() { }

Animal.prototype.speak = function() {
  return this;
};

Animal.eat = function() {
  return this;
};

let obj = new Animal();
let speak = obj.speak;
speak(); // グローバルオブジェクト

let eat = Animal.eat;
eat(); // グローバルオブジェクト

クラスの外でも明示的にStrictモードを使用している場合は、自動ボクシングが行われないのでundefinedのまま。

'use strict';

function Animal() { }

Animal.prototype.speak = function() {
  return this;
};

Animal.eat = function() {
  return this;
};

let obj = new Animal();
let speak = obj.speak;
speak(); // undefined

let eat = Animal.eat;
eat(); // undefined

インスタンスのプロパティ

インスタンスのプロパティはクラスのメソッドの中で定義しなければなりません

class Rectangle {
  constructor(height, width) {    
    this.height = height;
    this.width = width;
  }
}

ということは、後からプロパティーを追加できないってことだろうか?

class Rectangle {}

let obj = new Rectangle;
obj.height = 10;
obj.width = 20;
console.log(obj); // Rectangle {height: 10, width: 20}

...できるじゃん。これはどういうことなんだ?

クラスのインスタンスのプロパティはメソッド内で定義しないとSyntaxErrorになっちゃいますよってことかな...。

thisの話に付随するところがありそう。

クラスに付随する静的なプロパティやプロトタイプのプロパティは、クラス本体の宣言の外で定義しなければなりません

class Rectangle {}
Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;

まぁこれもそうだろうなって感じ。

ただプロトタイプのプロパティってなんだろう...。

プロトタイプは JavaScript オブジェクトが機能を互いに継承するメカニズムです。

へぇ〜

Object.prototype.width = 50;
class Rectangle {}
Rectangle.width; // 50
Rectangle.prototype.width; // 50
Rectangle.prototype.height = 100;
Object.height; // undefined
Object.prototype.height = 200;
Rectangle.height; //200
Rectangle.prototype.height; // 100

なんとなくわかったけど、継承元の静的なプロトタイププロパティが継承先の静的なプロパティやプロトタイププロパティとして使えるっぽい。

で、もちろん上書きができるけど、プロトタイププロパティはそのインスタンスのプロパティには影響しないのか。

class Object{
    constructor() {    
        this.prototype.height = 10;
    }
}
class Rectangle {}
let obj = new Rectangle;
obj.width; // undefined

インスタンスプロパティには使えないっぽい。

一旦おしまい。続きはまた今度。

GitHubで編集を提案

Discussion