近年、フロントエンド開発ができるエンジニアが少しずつ増えてきています
フレームワーク(Vue, React)の書き方は知っているけれど、Javascript の基本的な言語仕様についてはあまり知らない人が増えているので
そんな人と、そうでもない人(=つまり全員)に向けたのおさらい
また、フロントエンドエンジニアのプロパーとして知っておくべき内容
Chromeのコンソールで以下を実行してみてください
var object = {
name: 'inoko'
};
オブジェクトをリテラル(直書き)で生成するだけです
そのオブジェクトを参照してみてください
__proto__
というオブジェクトがなぜか生成されています
フロントエンドの開発経験があるエンジニアなら、一度は見たことがある光景でしょう
👇 展開してみた
いろんなメソッドを持っていますが、見覚えはありませんか?
例えば hasOwnProperty
とか toString
とか。
この __proto__
オブジェクトが、プロトタイプです
継承(extends)されて、やって来ました
継承元は?
window.Object.prototype
確認のため、比較してみる
同じですね
この window.Object
は JavaScript の標準ビルトインオブジェクトになっています
MDN によると、継承の仕組みは以下のように説明されています
JavaScript におけるすべてのオブジェクトは Object に由来します。 すべてのオブジェクトは Object.prototype からメソッドとプロパティを継承しています
つまりJavaScriptでは、生成されたオブジェクトすべてにおいて
window.Object.prototype
から継承されたプロトタイプを __proto__
という名前で持ちます
実際にやってみましょう
まずは、生成して
var array = ['a', 'b', 'c'];
参照して、展開してみる
__proto__
いた
forEach
pop
push
など、配列を扱うときによく使う method が並んでいますね
勘のいい人はわかると思いますが
Array は window.Array.prototype
というオブジェクトを継承しています
String や Number も同様です
このように
すべてのJavaScriptオブジェクトには、もう一つの別のオブジェクトが関連付けられています
この もう一つの別のオブジェクトを、プロトタイプと呼びます
言い換えると
すべてのJavaScriptオブジェクトは、プロトタイプを持ちます
注)null の場合を除く
以下の場合は、プロトタイプを持ちません
var nullObject = Object.create(null);
例えば、こんな処理をしたことはありませんか?
var array = ['a', 'b', 'c'];
array.push('d');
array
という変数には、 push
というプロパティやメソッドを定義してないのに、どうして使えるのでしょう??🤔
ここで プロトタイプチェーン
が出てきます
以下を実行すると
array.push('d');
① array
という変数に push
というプロパティやメソッドがないかどうか調べる
array
には ['a', 'b', 'c']
という値しか代入していないので、もちろんないです
② なかった場合、__proto__
が参照する先を調べる
ここで、配列のプロトタイプの継承元である window.Array.prototype
の先を見に行きます
今回の例では、ここで push
が見つかり、実行されます
ちなみに、
③ __proto__
になかった場合、さらに遡って __proto__
の先へ先へと、永遠と調べ続ける
④ __proto__
が存在しなくなるまで探したが見つからない場合、 undefined
を返す
このように、__proto__
は鎖のように連鎖的に繋がっています
この プロトタイプの連鎖のことをプロトタイプチェーン と呼びます
もし、何らかのプロパティやメソッドが参照できなかった場合は、プロトタイプチェーンの最終リンクで見つからなかった、という事になります
何かの値を参照するときは毎回、 プロトタイプチェーンを遡っています
以下の場合を考えてみましょう
var object = {
age: 20
};
console.log(object.name); // undefined
① 変数 object
に name
が存在するかどうか調べる
ないので、次は
② object.__proto__
が参照する先を調べる
オブジェクトのプロトタイプは window.Object.prototype
でしたね
こちらにも、 window.Object.prototype.name
は存在しません
ですので、さらに遡ろうとします
③ object.__proto__.__proto__
があるか調べる
④ object.__proto__.__proto__
が存在しないので undefined
を返す
ES6 が登場して、JavaScriptがClass構文に対応しましたが
ES6 に対応していないブラウザ(IEとか)や、Class構文が登場する前は、どのように実装されていたのでしょうか?
以下のシンプルな Animal Class
を Babel
に通してみます
/**
Animal クラス
**/
class Animal {
constructor(name) {
this.name = name;
}
walk() {
console.log('walk method.');
}
eat() {
console.log('eat method.');
}
}
// Animal Class から dogインスタンス を作る
var dog = new Animal('John');
dog.walk(); // walk method.
dog.eat(); // eat method.
⬇︎ Babel を通す
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
/**
Animal クラス
**/
var Animal =
/*#__PURE__*/
function () {
function Animal(name) {
_classCallCheck(this, Animal);
this.name = name;
}
_createClass(Animal, [{
key: "walk",
value: function walk() {
console.log('walk method.');
}
}, {
key: "eat",
value: function eat() {
console.log('eat method.');
}
}]);
return Animal;
}(); // Animal Class から dogインスタンス を作る
var dog = new Animal('John');
dog.walk(); // walk method.
dog.eat(); // eat method.
今回の説明に不要そうなメソッドは取り除いてみます
/**
* プロパティを定義する
*/
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) {
descriptor.writable = true;
}
Object.defineProperty(target, descriptor.key, descriptor);
}
}
/**
* クラスを作る
*/
function _createClass(Constructor, protoProps) {
if (protoProps) {
_defineProperties(Constructor.prototype, protoProps);
}
return Constructor;
}
/**
Animal クラス
**/
var Animal =
/*#__PURE__*/
function () {
function Animal(name) {
this.name = name;
}
_createClass(Animal, [{
key: "walk",
value: function walk() {
console.log('walk method.');
}
}, {
key: "eat",
value: function eat() {
console.log('eat method.');
}
}]);
return Animal;
}();
// Animal Class から dogインスタンス を作る
var dog = new Animal('John');
dog.walk(); // walk method.
dog.eat(); // eat method.
みてみると、 prototype
を使っていますね
さらに、読みやすいように簡略化してコメントを書いてみます
/**
Animal クラス
**/
var Animal = function () {
function Animal(name) {
this.name = name;
}
// walk メソッドを定義
Animal.prototype.walk = function() {
console.log('walk method.');
}
// eat メソッドを定義
Animal.prototype.eat = function() {
console.log('eat method.');
}
return Animal;
}();
// Animal Class から dogインスタンス を作る
var dog = new Animal('John');
dog.walk(); // walk method.
dog.eat(); // eat method.
この状態が、JavaScriptのES5以前の仕様でクラスを実現しようとした時の書き方です
ES6 の Class構文 もあくまでラッパーなので、内部的にブラウザはこのように解釈しています
注目すべきは walk
,eat
メソッドを prototype
のメンバーに持たせている というところです
ここで、new
したあとの dogインスタンス
の中身を見てみましょう
eat
walk
メソッドはそれぞれ
dog.__proto__.eat
dog.__proto__.walk
プロトタイプに定義されています
以下を実行したときは、プロトタイプチェーンを遡り、dog.__proto__.walk
を実行することになります
dog.walk(); // walk method.
仮に、prototype
ではなく 以下のように実装するとします
/**
Animal クラス
**/
var Animal = function () {
function Animal(name) {
this.name = name;
this.walk = function() {
console.log('walk method.');
}
this.eat = function() {
console.log('eat method.');
}
}
return Animal;
}();
// Animal Class から dogインスタンス を作る
var dog = new Animal('John');
dog.walk(); // walk method.
dog.eat(); // eat method.
this
は Animal
を指すので、プロトタイプに値を持たせるのではなく
Animal.walk
のように Animal
の直下にメソッドを定義します
この状態でも、得られる結果は先ほどと、同様です
ここで、 new された後の dogインスタンス
の中身を見てみましょう
dog.__proto__.eat
ではなく、new した dog
の直下に定義されていますね
ここで、以下のように大量に new
した場合を考えましょう
var dog = new Animal('John');
var cat = new Animal('Emma');
var rabbit = new Animal('Olivia');
var tiger = new Animal('Ava');
var pig = new Animal('Isabella');
var squirrel = new Animal('Sophia');
var horse = new Animal('Charlotte');
// ...more
この場合、 walk
eat
メソッドが、new
するたびにメモリ空間を消費することになります
もし prototype
を使っていた場合は、継承元のメソッドのみでしかメモリを消費しません
つまり メモリの無駄遣いを防ぐために、プロトタイプ、プロトタイプチェーンの仕組みが使われています
例えば、window.Object
の持つメソッドを拡張したい場合、以下のようなメソッドを window.Object
のプロトタイプに追加していたとします
Object.prototype.hogeFunction = function() {
console.log('hoge');
};
この状態で、以下のようなオブジェクトを for in
で参照すると・・
var object = {
name: 'yamada',
age: 20
};
for(var key in object) {
console.log('key: ', key);
}
// 結果
// key: name
// key: age
// key: hogeFunction
このように、Object.prototype
に定義した hogeFunction
まで参照してしまいます
プログラマの意図した動作と違う動きをしてしまうので、for in
を使うときは注意が必要です
むしろ、本当に for in
でないと実現できない実装ではない限り、使わなくて良いと思います
for in
でhasOwnProperty
を使う
prototype
ではない値のみ参照する
for(var key in object) {
if (object.hasOwnProperty(key)) {
console.log('key: ', key);
}
}
// 結果
// key: name
// key: age
lodash
を使う(おすすめ)
array
にも object
にも forEach
が使えるようになっている
上記のプロトタイプの仕組みを理解した上で、 lodash
を使うのがおすすめ
ちなみに、lodash の forEach
のコードを見るとオブジェクトのキーの数だけ while
で処理している
while (++index < length) {
if (iteratee(iterable[index], index, iterable) === false) {
break
}
}
Lodash github forEach (baseEach)
-
すべてのJavaScriptオブジェクトは、プロトタイプを持つ
-
何かの値を参照するときは毎回、プロトタイプチェーンを遡っている
-
Class構文も内部的にはプロトタイプ、プロトタイプチェーンの仕組みを使われている、余分なメモリを消費しないため
prototypeをもたないオブジェクト https://qiita.com/yoshiwatanabe/items/338705a20e8475e4057e
JavaScriptはオブジェクトについて参照渡しだなんて、信じない https://qiita.com/mozisan/items/1b9d4bf5a1bb341dd354
JavaScript のデータ型とデータ構造
https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures#Strings
C言語 文字列 https://www.cc.kyoto-su.ac.jp/~yamada/programming/string.html
Babel https://babeljs.io/
Class構文 https://html5experts.jp/takazudo/17355/
プリミティブ型とオブジェクト https://murashun.jp/blog/20190401-08.html