JavaScriptの関数とオブジェクトについて
今回はJavaScriptにおける関数と、オブジェクトについて解説していきます。
まず知っておくべきことは、関数もオブジェクトであるということです。
例えば、以下のように関数の対してもプロパティやメソッドを追加することができます。
function fn(a) {
console.log(a);
}
fn.prop = "b";
fn.method = () => {
console.log("method");
};
// b
console.log(fn.prop);
// method
fn.method();
そして、()
は実行可能なオブジェクトを実行するという記号になります。
これを理解してると、様々な挙動も理解できるようになるので覚えておいてください。
引数
まず、関数の引数について解説していきます。
引数とは何か、関数はどのように書くのかなどの基本的な説明は今回省きます。
JavaScriptにおいて、呼び出し側の引数の数が違ってもエラーにならずに、undefinedが入るようになっています。
function fn(a) {
// undefined
console.log(a);
}
fn();
ちなみに、アロー関数で書いた場合も同じ挙動になります。
また、引数は指定しなくても、argumentsに実引数が配列のような形で入ってきます。
実際のコードで確認すると、以下のようになります。
function fn() {
// {0: "a"}
console.log(arguments);
}
fn("a");
後ほど詳しく説明しますが、argumentsはアロー関数の中で使おうとするとエラーになります。
this
次に、thisについて解説していきます。
このthisは使われるコンテキストによって、中身が変わってくるのでしっかりと理解しておきましょう。
thisは呼び出し元のオブジェクトへの参照を保持します。
なので以下の場合、thisはpersonを参照することになり結果は次の通りになります。
const person = {
name: "shinya",
hello: function () {
console.log("hello," + this.name);
}
};
//hello,shinya
person.hello();
ただ、以下のように参照のコピーを行うと思ったような挙動になりません。
const person = {
name: "shinya",
hello: function () {
console.log("hello," + this?.name);
}
};
//hello,undefined
const ref = person.hello;
ref();
こちらは呼び出し元のオブジェクトがpersonではなくなってしまったからです。
つまり、personのメソッドとして呼ばれたのではなく、単にhelloという関数を単体で呼び出した形となっています。
そして、呼び出し元のオブジェクトがない場合、thisはグローバルスコープを参照します。
なのでグローバルスコープにnameが定義されていないため、undefined となっているわけです。
bind, call, apply
先ほどのような時は、bindやcall、apply関数を使うことで解決します。
まずはbindから説明していきます。
この関数を使うことで、thisの値を束縛できるようになるので、挙動の勘違いによるエラーを防げます。
const person = {
name: "shinya",
hello: function () {
console.log("hello," + this?.name);
}
};
//hello,shinya
const ref = person.hello.bind(person);
ref();
こう書くことで、thisが呼び出された際に、personを参照するようになります。
またこの関数は、引数と同時に渡せます。
コードにすると以下の通りです。
const person = {
name: "shinya",
hello: function (a) {
console.log("hello," + this?.name);
console.log("hello," + a);
}
};
//hello,shinya
//hello,johon
const ref = person.hello.bind(person, "john");
ref();
次に、callとapplyについて説明していきます。
bindは飽くまで、thisを束縛した関数を生成するだけでした。
けれど、callとapplyはthisを束縛し、関数の実行まで行います。
const person = {
name: "shinya",
hello: function (a) {
console.log("hello," + this?.name);
}
};
//hello,shinya
person.hello.apply(person);
//hello,shinya
person.hello.call(person);
それぞれ、機能的にはほぼ一緒ですが、関数の引数の渡し方だけが異なります。
const person = {
name: "shinya",
hello: function (a, b) {
console.log("hello," + this?.name);
console.log("hello," + a);
console.log("hello," + b);
}
};
//hello,shinya
person.hello.apply(person, ["john", "tim"]);
//hello,shinya
person.hello.call(person, "john", "tim");
このようにcallの場合は、bindと同じように渡しますが、applyの場合は配列で渡すことになります。
なので、配列で扱うようなデータを引数で渡したいときはapplyを使うと良いでしょう。
アロー関数
次に、アロー関数について説明します。
このアロー関数は、ES6から追加された記法になります。
この記法を使うことで、端的に無名関数を書くことができます。
実際のコードで見ると、次の通りです。
function fn(a){
console.log(a)
}
const fn2 = (a) => {
console.log(a)
}
ちなみに、めちゃくちゃ省略すると、次のような書き方もできます。
const fn3 = _ => console.log("hello")
このようにアロー関数は無名関数を省略して書けますが、厳密に機能が同じ訳ではありません。
例えばこのアロー関数内では、this,argumentsを使うことができません。
なので、一応このことを頭に入れておく必要があります。
コンストラクタ関数
次に、コンストラクタ関数について解説していきます。
こちらはnew演算子を使って使用できる、新たなオブジェクトを生み出すための関数になります。
そして、新たに作られたオブジェクトのことをインスタンスと言い、new演算子を使ってインスタンスを作ることをインスタンス化と言います。
コンストラクタ関数の書き方は次の通りです。
function Person(name, age) {
this.name = name;
this.age = age;
}
const shinya = new Person("shinya", 27);
const john = new Person("john", 32);
// {name: "shinya", age: 27}
console.log(shinya);
// {name: "john", age: 32}
console.log(john);
このコンストラクタ関数は慣習的に1文字目は大文字とします。
こちらの処理が内部的にどうなっているかは、後ほど解説します。
プロトタイプ
次にプロトタイプについて解説していきます。
こちらは、オブジェクトに存在する特別なプロパティです。
コンストラクタ関数と合わせて使用されます。
実際の使い方は次の通りです。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.hello = function () {
console.log("hello," + this.name);
};
const shinya = new Person("shinya", 27);
// hello,shinya
shinya.hello();
このようにprototypeオブジェクトに新たなメソッドを追加することで、独自のメソッドを定義できます。
ただ、このようにしなくても、Personオブジェクトにhelloメソッドを追加すれば同じことができます。
けれど、前者は飽くまでプロトタイプオブジェクトへの参照をコピーしてるだけなので、この方法だとメモリを節約することができます。
つまり、プロトタイプを使ったメソッドは、全てのインスタンスから全て同じメモリを参照することになる訳です。
なので、次のような結果になります。
// true
console.log(john.hello === shinya.hello);
ここは少し難しい話をしてるので、一旦は理解できなくてもOKです。
new
new演算子は、コンストラクタ関数からインスタンスを作成するために使用する演算子です。
このnew演算子は、コンストラクタ関数の戻り値によって挙動が変わります。
まず、戻り値がオブジェクトの場合は、そのオブジェクトを新しいインスタンスとして作成します。
逆に、戻り値がオブジェクト以外の時または何も無い時は、プロトタイプメソッドを__proto__にコピーして、thisを返します。
文字だけだと分かりづらいと思うので、コードにすると次のようになります。
function Person(name, age) {
this.name = name;
this.age = age;
return {};
}
Person.prototype.hello = function () {
console.log("hello," + this.name);
};
const shinya = new Person("shinya", 27);
// {}
console.log(shinya);
function Person(name, age) {
this.name = name;
this.age = age;
return 1; // returnなしでも同じ
}
Person.prototype.hello = function () {
console.log("hello," + this.name);
};
const shinya = new Person("shinya", 27);
// {age: 27,name: "shinya",{[[Prototype]]: {hello: ƒ ()}}}
console.log(shinya);
ただ、この挙動を知っておく必要はあまりないので、new演算子を使えばインスタンス化ができるということを認識しておけばOKです。
プロトタイプチェーン
次にプロトタイプチェーンについて解説していきます。
こちらはプロトタイプの多重形成のことになります。
つまり、インスタンス化したものが他のprototypeも継承してる状態を言います。
例えば、次のような感じです。
この状態の時にメソッドを使用した時は、階層の浅いprototypeから該当のメソッドを探しに行くという挙動になります。
つまり、コードで例えると次のようになります。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.hello = function () {
console.log("Person: hello," + this.name);
};
Object.prototype.hello = function () {
console.log("Object: hello," + this.name);
};
const shinya = new Person("shinya", 27);
// Person: hello,shinya
shinya.hello()
ついでにhasOwnPropertyとinについて解説します。
まず、hasOwnPropertyは自身のオブジェクトのプロパティだけを見に行きます。
けれど、inはprototypeも遡って見にいきます。
なので、次のような挙動の違いが生じます。
// false
console.log(shinya.hasOwnProperty("hasOwnProperty"));
// true
console.log("hasOwnProperty" in shinya);
こちらはしっかりと覚えておきましょう。
instanceof
instanceofを使うことで、あるインスタンスがどのコンストラクタ関数から作られたのかを特定できます。
使い道としては、オブジェクトの種類を調べる時などに使います。
挙動としては、指定したコンストラクタのprototypeと、インスタンスの__prototype__の等価性を確認します。
ちなみに、こちらもプロトタイプチェーンを遡って調べに行くので、次のような結果になります。
// true
console.log(shinya instanceof Object);
クラス
このクラスは、今までに説明してきたコンストラクタ関数をクラス表記で書けるようにしたものです。
つまり、このクラスはコンストラクタ関数のシンタックスシュガーとなります。
コードにすると以下のようになります。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
hello() {
console.log("hello," + this.name);
}
}
//function Person(name, age) {
// this.name = name;
// this.age = age;
//}
//
//Person.prototype.hello = function () {
// console.log("hello," + this.name);
//};
const shinya = new Person("shinya", 27);
constructorの中身が、今まで書いていたコンストラクタ関数と同じ挙動になります。
そして、その外側にメソッドを追加することで、prototypeにメソッドを追加することができます。
このように、class表記にすることでシンプルにコードを記述できるようになります。
継承
また、クラス構文はextendsとsuperを使うことで、継承も簡単に書くことができます。
コードにすると以下の通りです。
class Japanese extends Person{
constructor(name, age, gender) {
super(name, age)
this.gender = age;
}
}
Extendsとは継承を表すためのキーワードとなり、superを実行することで継承先のコンストラクタ関数を実行することができます。
継承することで、親のprotoypeも引き継ぐことができます。
ちなみに、superはコンストラクタ関数以外の継承元の関数も使用することができますが、あまり使わない方法なので頭の片隅に置いておけばOKです。
super.hello()
その他の様々な設定
オブジェクトには、様々な設定をすることができます。
具体的には、以下のようなものが存在します。
- value:値を格納できます
- configurable:設定を変更出来るかどうかを設定できます
- enumerable:列挙することができるかを設定できます
- writable:値を書き換え可能にするかを設定できます
- set:値をセットする時に動かすメソッドを設定できます
- get:値を取得する時に動かすメソッドを設定できます
あまり使わないので、頭の片隅に置いておけばOKです。
まとめ
今回は、JavaScriptにおけるオブジェクトと関数について解説してきました。
こちらをきちんと理解できてる人は少ないので、この記事を参考にして他の人との差をつけていきましょう。
宣伝
0からエンジニアになるためのノウハウをブログで発信しています。
また、YouTubeでの動画解説も始めました。
YouTubeのvideoIDが不正ですインスタの発信も細々とやっています。
興味がある方は、ぜひリンクをクリックして確認してみてください!
Discussion