JSにおける関数内部thisの探究

5699 ワード

本稿では,jsにおけるthisの指向と知覚回答,JavaScriptにおけるオブジェクト検索をリンクを参照して徹底的に理解する.
以下に述べるすべてが標準モードの場合、厳格モードのthisについては、別の文章の最後の部分を参照してください.
まず、thisの指向は関数定義時には確定できず、関数実行時にのみthisが誰を指しているのかを確定することができ、実際にthisの最終的な指向はその呼び出しのオブジェクトを指している.
これが最終的に呼び出されたオブジェクトを指す以上、まずJSの関数の呼び出しにはいくつかの状況があることを見てみましょう.
JSにおける関数呼び出しは4種類に分けられる

一:Function Invocation Pattern

foo()のような呼び出し形式はFunction Invocation Patternと呼ばれ、関数の最も直接的な使用形式であり、ここでfooは属性ではなく個別の変数として現れ、前に呼び出しオブジェクトはないことに注意してください.このモードではfoo関数体のthisは常にGlobalオブジェクトであり、ブラウザではwindowオブジェクトである.たとえば、次のコードがあります.
function foo(){
    var user = "   ";
    console.log(this.user); //undefined
    console.log(this); //Window
}
foo();

関数内部で呼び出されても、前に呼び出されたオブジェクトがないため、thisはwindowグローバルオブジェクトを指します.以下のコードなどです.
function printThis() {
  console.log(this)
  let print = function () { console.log(this) };
  print();  
}
printThis()  //       window,     window
printThis.call([1]);  //      [1],     window
printThis.call([2]);  //      [2],     window

上記のコードでは、1番目にprintThisを印刷するthisが、call関数を通過するとパラメータがthisとして関数に渡されるため、それぞれ[1]と[2]が印刷される.2つ目はprintを呼び出すオブジェクトがないため、windowを印刷します.printを匿名関数、例えば次のコードに変更しても、結果は変わりません.匿名関数はwindowから呼び出されます
function printThis() {
  console.log(this)
  (function () {
    console.log(this)
  })(); //         print  window  
}
printThis()  //       window,     window
printThis.call([1]);  //      [1],     window
printThis.call([2]);  //      [2],     window

しかしES 6では、矢印関数の導入に伴ってこのような状況が変化し、上の関数を以下のフォーマットに変更する.
function printThis() {
  console.log(this)
  let print = () => console.log(this);
  print();
}
printThis()  //       window,     window
printThis.call([1]);  //      [1],     [1]
printThis.call([2]);  //      [2],     [2]

この場合の結果は、矢印関数では、自分のthis属性を捨てて、閉じた実行コンテキストのthis値を直接使用するため、上記とは異なります.クローズド実行コンテキストとは,矢印関数が現れる場所のコードドメインである.この場合、1番目と2番目の印刷内容は同じです.矢印関数では、この原則はすべて、標準モードでも厳格モードでも有効ではありません.

二:Method Invocation Pattern

foo.bar()のような呼び出し形式はMethod Invocation Patternと呼ばれ、呼び出された関数がオブジェクトの属性として現れるのが特徴であることに注意する.あるいは「[]」のようなキー記号です.このモードではbar関数体のthisは永遠に「.」または「[」の前のオブジェクトは、前例のようにfooオブジェクトに違いありません.たとえば、次のコードです.
var o = {
  a:10,
  b:{
    a:12,
    fn:function(){
      console.log(this.a); //12
    }
  }
}
o.b.fn(); 

最初に呼び出されたオブジェクトはoであるが、最終的な関数fnはオブジェクトbの属性であり、したがってthis.aは12である.次のコードを見てみましょう
var o = {
  a:10,
  b:{
    a:12,
    fn:function(){
      console.log(this.a); //12
    }
  }
}
var j = o.b.fn;  //     ,      ,           j()
j(); 

この時のthis.aはいくらですか?このときのthisは実はwindowであり、this.aはundefinedである.どうしてそうなの?振り返ってみると、文章の最初に、thisの最終的な指向は、それを呼び出すオブジェクトであると述べています.o.b.fnを用いた場合,実行は呼び出されず,真の呼び出し実行は次のj()であり,この場合は第1の方法と同様である.

三:Constructor Pattern

new foo()という形式の呼び出しはConstructor Patternと呼ばれ、そのキーワードnewは問題を説明することができ、非常に識別しやすい.このモードでは、foo関数内部のthisは常にnew foo()が返すオブジェクトです.たとえば、次のコードがあります.
function Foo () {
  this.x = 1;
}
Foo.prototype.print = function () {
  console.log(this.x);
}

let foo = new Foo();
foo.print(); 
foo.print.call({x: 2});

このとき、第1の印刷は1であり、thisがfooのインスタンスオブジェクトであることを示す.this.xはconstructor関数に付与された値の1である.2番目の印刷は2であるため、{x: 2}をオブジェクトとして使用したfooのインスタンスオブジェクトのthisが置き換えられた.ここではFunctionがオブジェクトを作成する特別な状況に注意してください.
function Foo () {
  this.x = 1;
  return {x: 2};
}
Foo.prototype.print = function () {
  console.log(this.x);
}
let foo = new Foo();
console.log(foo.x);  // 2
foo.print();  // funciton print undefined

コンストラクション関数Fooがオブジェクトを返すと、fooはこのオブジェクトに置き換えられます.fooは{x: 2}で、x属性が2に等しいのは1つだけで、printというメソッド属性はありません.しかし、コードを変更して、次のフォーマットに変更します.
function Foo () {
  this.x = 1;
  return 1;
}
Foo.prototype.print = function () {
  console.log(this.x);
}
let foo = new Foo();
console.log(foo.x);  // 1
foo.print();  // 1

コンストラクション関数Fooは、オブジェクトではないオブジェクトを返すため、fooは置き換えられず、依然としてFooのインスタンスオブジェクトであり、このときのfoo.xおよびthis.xはすべて1である.

四:Apply Pattern

foo.call(thisObject)およびfoo.apply(thisObject)の形式はApply Patternと呼ばれ、組み込まれたcallおよびapply関数が使用される.このモードでは、callapplyの最初のパラメータがfoo関数内のthisであり、this Objectがnullまたはundefinedであればwindowオブジェクトになります.具体的なコードは、上記の3つのモードで説明したので、後述しません.
練習部分
次のコードを確認し、印刷結果を分析し、実際に実行して、結果と一致するかどうかを確認します.
var x = 0;
function Foo () {
  this.x = 1;
}
Foo.prototype.print = function () {
  console.log(this);
  console.log(this.x);
  (function () {
    console.log(this);
    console.log(this.x)
  })()
}

let foo = new Foo();
foo.print.call({x: 2});

コードを確認すると、まずFooオブジェクトのprintでは、3番目と4番目の印刷が匿名関数であることに気づきます.この場合、匿名関数の呼び出し元はwindowグローバル変数であるため、4番目の印刷のthis.xwindow.x = 0です.さらに下を見るとprint関数の{x:2}の代わりにthisが使用されているため、第1の印刷は{x: 2}、第2の印刷は2である.
コードを変更したらprintの定義を矢印関数に変更しますか?結果はどうだった?コードは次のとおりです.
var x = 0;
function Foo () {
  this.x = 1;
}
Foo.prototype.print = () => {
  console.log(this);
  console.log(this.x);
  (function () {
    console.log(this);
    console.log(this.x)
  })()
}

let foo = new Foo();
foo.print.call({x: 2});

まず、矢印関数にはthisが含まれていないことを知っています.thisは実行コンテキストのthisです.また、矢印関数の実行コンテキストの判定は、その定義の時点で決定され、スクリプトで定義されていることがわかり、このときの役割ドメインはグローバルであるため、この場合、前の2つの印刷と後の2つの印刷は同じように0とwindowである.