JavaScriptを雰囲気で使っていたので勉強し直したら知らなかったこと

2022/05/31に公開
1

小ネタ。chromeで動作確認

何となく使っていたので改めて勉強してみたら知らなかったことなどのまとめ

this

関数の呼び出し元のオブジェクトがthisになる。ただし、呼び出し元のオブジェクトの指定がない場合は、windowオブジェクトがセットされる。ただし、厳格モードuse strictの場合はundefinedがセットされる

const foo = {
  bar: function() {
    // 呼び出し元はfoo.bar()
    console.log(this); // foo object
      
    const baz = function() {
      // 呼び出し元はbaz()でオブジェクトの指定がない
      console.log(this); // window object
    }
    baz();
    
    const qux = function() {
      'use strict';
      // 呼び出し元はqux()でオブジェクトの指定がないかつ厳格モード
      console.log(this); // undefined
    }
    qux();
  }
}
foo.bar();

また、classの場合はuse strictの指定なしでも厳格モードになる。以下は引用

this に値が付けられずに静的メソッドまたはプロトタイプメソッドが呼ばれると、this の値はメソッド内で undefined になります。たとえ "use strict" ディレクティブがなくても同じふるまいになります。なぜなら、class 本体の中のコードは常に Strict モードで実行されるからです。

class Foo {
  bar() {
    // 呼び出し元はfoo.bar()
    console.log(this); // Foo object
    
    const baz = function() {
      // 呼び出し元はbaz()でオブジェクトの指定がないかつクラス内なので厳格モード
      console.log(this); // undefined
    }
    baz();
  }
}
const foo = new Foo();
foo.bar();

call、apply、bindでthisを指定することができる

const foo = {
  bar: function() {
    const baz = function() {
      // 呼び出し元はbaz.apply(this)。
      // applyに指定しているthisの呼び出し元はfoo.bar()なのでfooとなる
      console.log(this) // foo object
    }
    baz.apply(this)

    const qux = function() {
      // 呼び出し元はqux()で、quxにはbind(this)が指定されている。
      // bindに指定されているthisの呼び出し元はfoo.bar()なのでfooとなる
      console.log(this) // foo object
    }.bind(this)
    qux();
  }
}
foo.bar();

また、アロー関数内のthisはアロー関数が定義されているスコープに基づく。以下は引用

アロー関数は、そのアロー関数が定義されているスコープに基づいて "this" を確立する

const foo = {
  bar: function() {
    const baz = () => {
      // 呼び出し元はbaz()
      // bazが宣言されているbar()の呼び出し元はfoo.bar()なのでfooとなる
      console.log(this); // foo object
    }
    baz();
  }
}
foo.bar();

シャローコピー

以下は引用の翻訳。引用にあるコピー操作ではシャローコピー(浅いコピー)となることがあり、コピー先を変更することでコピー元に影響を与えることがある

JavaScript では、標準的な組み込みのオブジェクトコピー操作 (スプレット構文, concat(), slice(), Array.from(), Object.assign(), Object.create()) はすべて、ディープコピーではなくシャローコピーを作成します。

以下の例では、nameは変わらないが、schoolは書きかわる

const user = {
  name: "user",
  school: { name: "school" }
}

const user2 = {...user}

user2.name = "user2"
user2.school.name = "school2"

console.log(user); // {name: "user", school: {name: "school2"} }
console.log(user2); // {name: "user2", school: {name: "shool2"} }

ディープコピー(深いコピー)を使用することで回避する。以下は引用

Javascript のオブジェクトのディープコピーを作成する一つの方法は、そのオブジェクトが シリアライズ 可能であれば JSON.stringify() でオブジェクトを JSON 文字列に変換し、 JSON.parse() で文字列から(完全に新しい) Javascript のオブジェクトに変換することです。

new演算子

クラスでなく、関数に対しても使うことができる。関数がnew演算子で呼ばれるとき、コンストラクタ関数という。コンストラクタ関数として呼び出すとオブジェクトが生成される

const Foo = function() {
  this.foo = "bar"
  return 1;
}

// 通常の関数として呼び出す
console.log(Foo()); // 1 

// コンストラクタ関数として呼び出す
console.log(new Foo()); // Foo{foo: "bar"}オブジェクト

以下はnew演算子のドキュメント

構文の引用

new constructor[([arguments])]
引数 constructor
オブジェクトインスタンスの型を指定するクラスまたは関数です。

解説の引用

  1. 空のプレーンな JavaScript オブジェクトを生成します。
  2. 新しいオブジェクトにプロパティ (proto) を追加し、コンストラクター関数のプロトタイプオブジェクトに結びつけます。
  3. 新しく生成されたオブジェクトインスタンスを this コンテキストとして結びつけます。 (すなわち、コンストラクター関数内の this へのすべての参照は、最初のステップで作成されたオブジェクトを参照するようになります。)
  4. 関数がオブジェクトを返さない場合は this を返します。

コンストラクタ関数の戻り値として、オブジェクトを返す場合とそうでない場合で挙動が異なる

オブジェクトを返さない例

function Foo() {
  this.bar = 1;
}

const foo = new Foo();
console.log(foo); // Foo { bar: 1 }

オブジェクトを返す例

function Foo() {
  this.bar = 1;
  return { baz: 1 };
}

const foo = new Foo();
console.log(foo); // { baz: 1 }
console.log(foo.bar) // undefined

プロトタイプベース

objectやarrayをconsole.logしたときに見かける[[Prototype]]__proto__の正体。知らないと困るというケースがあるかどうかはわからないが、以下あたりを1度読んでおくと雰囲気がわかる

図で理解するJavaScriptのプロトタイプチェーン - Qiita

継承とプロトタイプチェーン - JavaScript | MDN

ただ、classから作ったオプジェクトもプロトタイプベースであることは変わらないので理解はしておくべき

変数や関数の巻き上げ(Hoisting)

以下はドキュメントの引用

変数や関数の宣言が物理的にコードの先頭に移動されることを示唆していますが、実際にはそうではありません。変数や関数の宣言はコンパイル時にメモリに格納されますが、コード内で入力された場所は変わりません

ただし

定義のみが巻き上げられ、初期化はそうでありません。変数が使用された後に定義や初期化された場合、値は undefined になります

関数の場合、宣言の前に読んでもエラーにはならない

foo();

function foo() {
  console.log("foo");
}

変数の場合

foo(); // Uncaught TypeError: foo is not a function

var foo = () => console.log("foo");

引用のとおり定義のみが巻き上げられるので、実行時は以下のイメージとなり、変数としては存在するが、関数として呼ぼうとするとエラーになる

var foo;

foo(); // この時点でfoo は undefined

foo = () => console.log("foo")

その他

オブジェクトを返すアロー関数の書き方。以下は引用

オブジェクトリテラル式を返す場合は、式の周りに括弧が必要です。

// NG
const foo = () => {bar: "baz"}

// OK
const foo = () => ({bar: "baz"})

Discussion

haru427haru427

ちょうどつまづいたり、うっかり忘れでミス起きやすい部分でいい復習になりました。