ブラウザJavaScriptの実行原理の解析

JavaScriptの実行原理

JavaScriptの実行フロー

  • 定義前に変数や関数を使用できる理由:変数ホイスティングとは、JavaScript エンジンが JavaScript 実行時に変数の宣言部分や関数の宣言部分をコードの先頭に持ち上げる「動作」のことを指します。コード。変数がプロモートされると、変数のデフォルト値が設定されます。デフォルト値は未定義です。
showName()//函数 showName 被执行
console.log(myname)//undefined
var myname = '时间'
function showName() {
    
    
    console.log('函数 showName 被执行');
}

JavaScript コードは実行前に JavaScript エンジンによってコンパイルする必要があり、コンパイルが完了すると実行フェーズに入ります。

1. コンパイルフェーズ

画像-20230405225212462

コードの一部を入力すると、コンパイル後に、実行コンテキスト (実行コンテキスト) と実行可能コードの2 つの部分が生成されます。

実行コンテキストは、JavaScript がコードを実行する実行環境です。たとえば、関数を呼び出すと、その関数の実行コンテキストに入り、この関数、変数、オブジェクト、関数の実行中に使用される関数が決定されます。関数。

2. 実装フェーズ

JavaScript エンジンが「実行可能コード」の実行を開始し、1 行ずつ順番に実行されます。

コードの一部で同じ名前の 2 つの関数が定義されている場合、最後の関数は最後の関数がコンパイルされたときに有効になり、後者が前者を上書きします。

実行コンテキスト

コードが実行されると、JavaScript エンジンはまずコードをコンパイルし、実行コンテキストを作成します。

どのような状況で、コードは「一部の」コードとみなされ、実行前にコンパイルされ、実行コンテキストが作成されます。一般的に、次の 3 つの状況があります。

  • JavaScript がグローバル コードを実行するとき、グローバル コードをコンパイルしてグローバル実行コンテキストを作成します。ページ全体のライフ サイクル中にグローバル実行コンテキストのコピーは 1 つだけ存在します。
  • 関数を呼び出すと、関数本体のコードがコンパイルされて関数実行コンテキストが作成されますが、通常、関数実行終了後、作成された関数実行コンテキストは破棄されます。
  • eval 関数を使用すると、eval コードもコンパイルされ、実行コンテキストが作成されます。

変化する環境

実行コンテキストには、変数プロモーションの内容を格納する変数環境オブジェクト(ViriableEnvironment) があります。

コールスタック

  • 関数が呼び出されるたびに、JavaScript エンジンはその実行コンテキストを作成し、その実行コンテキストをコール スタックにプッシュして、JavaScript エンジンが関数コードの実行を開始します。
  • 別の関数 B が関数 A で呼び出される場合、JavaScript エンジンは B 関数の実行コンテキストを作成し、B 関数の実行コンテキストをスタックの先頭にプッシュします。
  • 現在の関数の実行が終了すると、JavaScript エンジンはその関数の実行コンテキストをスタックからポップします。
  • 割り当てられたコール スタック スペースがいっぱいになると、「スタック オーバーフロー」問題が発生します。
var a = 2
function add(b,c){
    
    
  return b+c
}
function addAll(b,c){
    
    
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)

画像-20230405231204711

画像-20230405231245179

画像-20230405231315563

画像-20230405231327145

画像-20230405231335737

画像-20230405231342962

範囲

スコープは変数と関数のアクセス可能な範囲です。つまり、スコープは変数と関数の可視性とライフサイクルを制御します。

ES6 より前の ES には、グローバル スコープと関数スコープの 2 つのスコープしかありませんでした。

  • グローバルスコープオブジェクトはコード内のどこからでもアクセスでき、そのライフ サイクルにはページのライフ サイクルが伴います。
  • 関数スコープは関数内で定義された変数または関数であり、定義された変数または関数は関数内でのみアクセスできます。関数の実行が終了すると、関数内で定義された変数は破棄されます。

ES6 より前は、ブロックレベルのスコープはサポートされていませんでした

変数ホイスティングに関する問題

変数は気付かれずに簡単に上書きされます

//全局上下文中 myname=" 极客时间 ",function showName()
var myname = " 极客时间 "
function showName(){
    
    
  console.log(myname);
  if(1){
    
    
     //变量提升
   var myname = " 极客邦 "
  }
  console.log(myname);
}
//执行函数,创建函数上下文,函数中var myname 变量提升导致函数上下文中存在myname=undefined,如果没有变量提升,函数上下文中没有myname变量,则会去全局上下文中获取变量
showName()
//undefined
// 极客邦 

画像-20230405233137041

破壊されるべきだった変数が破壊されなかった

function foo(){
    
    
    // 由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。
  for (var i = 0; i < 7; i++) {
    
    
  }
  console.log(i); 
}
foo()

ES6 は変数の巻き上げによって引き起こされる欠陥をどのように解決するか

ES6 では let キーワードと const キーワードが導入され、JavaScript も他の言語と同様にブロックレベルのスコープを持つことができるようになりました。

let キーワードで宣言された変数は変更できますが、const で宣言された変数は変更できません。どちらもブロックレベルのスコープを生成できます

function letTest() {
    
    
  let x = 1;
  if (true) {
    
    
    let x = 2;  // 不同的变量
    console.log(x);  // 2
  }
  console.log(x);  // 1
}

let キーワードはブロック レベルのスコープをサポートしているため、JavaScript エンジンは、コンパイル フェーズ中に if ブロックで let で宣言された変数を変数環境に保存しません。つまり、if ブロックで let で宣言されたキーの単語は保存されません。すべての関数に表示されるように昇格されます。したがって、if ブロック内の出力値は 2 で、ブロックの外に飛び出した後の出力値は 1 になります。これは私たちのプログラミング習慣と非常によく一致しています。アクション ブロックで宣言された変数は、ブロックの外側の変数には影響しません

JavaScript がブロック スコープをサポートする方法

同じコード部分で、ES6 は変数プロモーションの特性をサポートするだけでなく、ブロックレベルのスコープもどのようにサポートしますか?

function foo(){
    
    
    var a = 1
    let b = 2
    {
    
    
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

最初のステップは、実行コンテキストをコンパイルして作成することです

画像-20230405235210912

  • 関数内で var によって宣言された変数はすべて、コンパイル段階で変数環境に保存されます。
  • let で宣言された変数は、コンパイル中に **字句環境 (LexicalEnvironment)** に保存されます。
  • 関数のスコープ内では、let で宣言された変数は字句環境に格納されません。

2 番目のステップでは、引き続きコードの実行が行われます。コード ブロックが実行されると、変数環境の a の値は 1 に設定され、字句環境の b の値は 2 に設定されます。関数の実行コンテキストは次の図に示すとおりです。

画像-20230405235512956

関数のスコープ ブロックに入ると、スコープ ブロック内の let で宣言された変数は、字句環境の別の領域に保存されます。この領域の変数は、スコープ ブロックの外側の変数には影響しません。関数 変数 b はスコープ外で宣言されており、変数 b はスコープブロック内でも宣言されており、スコープ内で実行されると、これらはすべて独立して存在します。

  • 字句環境内では、小さなスタック構造が維持されます。スタックの一番下は、関数の最も外側の変数です。スコープ ブロックに入った後、スコープ ブロック内の変数はスタックの一番上にプッシュされます。実行が完了すると、スコープ情報がスタックの最上位からポップされます。これがレキシカル環境の構造です。ここで話している変数は、let または const によって宣言された変数を指すことに注意してください。

スコープ ブロック内のconsole.log(a)このコード行が実行されると、字句環境と変数環境で変数 a の値を検索する必要があります。具体的な検索方法は次のとおりです。字句環境のスタック トップをクエリします。字句環境では の特定のブロックで見つかった場合はそのままJavaScriptエンジンに返しますが、見つからなかった場合は変数環境で検索を続けます。

画像-20230405235901688

スコープ ブロックが実行されると、その内部で定義された変数が字句環境のスタックの最上位からポップされ、最終的な実行コンテキストが次の図に示されます。

画像-20230405235939923

語彙範囲

字句スコープとは、コード内の関数宣言の位置によってスコープが決定されることを意味します。したがって、字句スコープは静的スコープであり、これにより、実行中にコードがどのように識別子を探すかを予測できます。

画像-20230406001407041

字句スコープはコードの位置に従って決定されます。main 関数には bar 関数が含まれ、bar 関数には foo 関数が含まれます。JavaScript スコープ チェーンは字句スコープによって決定されるため、字句スコープ チェーン全体の順序は:foo 関数のスコープ—> bar 関数のスコープ—> main 関数のスコープ—> グローバル スコープです。

スコープチェーン

  • JavaScript の実行中、そのスコープ チェーンは字句スコープによって決定されます

  • 字句スコープはコードの段階で決定され、関数の呼び出し方法とは関係ありません

function bar() {
    
    
    console.log(myName)
}
function foo() {
    
    
    var myName = " 极客邦 "
    bar()
}
var myName = " 极客时间 "
foo()//极客时间

画像-20230406000712516

各実行コンテキストの変数環境には、外部の実行コンテキストを指す外部参照が含まれており、これを アウター と呼びます

コードの一部が変数を使用する場合、JavaScript エンジンはまず「現在の実行コンテキスト」で変数を検索します。たとえば、
上記のコードが myName 変数を検索するときに、現在の変数環境で見つからない場合は、次に、JavaScript エンジンは、outer によって指定された実行コンテキストで検索を続けます。

bar 関数と foo 関数の外側は両方ともグローバル コンテキストを指します。つまり、外部変数が bar 関数または foo 関数で使用される場合、JavaScript エンジンはグローバル実行コンテキストで検索します。この検索チェーンをスコープ チェーンと呼びます

ブロックスコープでの変数のルックアップ

function bar() {
    
    
    var myName = " 极客世界 "
    let test1 = 100
    if (1) {
    
    
        let myName = "Chrome 浏览器 "
        console.log(test)
    }
}
function foo() {
    
    
    var myName = " 极客邦 "
    let test = 2
    {
    
    
        let test = 3
        bar()
    }
}
var myName = " 极客时间 "
let myAge = 10
let test = 1
foo()

画像-20230406002103082

閉鎖

function foo() {
    
    
    var myName = " 极客时间 "
    let test1 = 1
    const test2 = 2
    var innerBar = {
    
    
        getName:function(){
    
    
            console.log(test1)
            return myName
        },
        setName:function(newName){
    
    
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())

foo 関数内のreturn innerBarこのコード行が実行されると、コール スタックはどうなるか

画像-20230406002412783

  • innerBar は、getName と setName の 2 つのメソッドを含むオブジェクトです。
  • これら 2 つのメソッドは、内部で 2 つの変数 myName と test1 を使用します。

字句スコープの規則に従って、内部関数 getName および setName は外部関数 foo 内の変数に常にアクセスできるため、 foo 関数が実行されていても innerBar オブジェクトがグローバル変数 bar に返されると、getName および setName は関数は引き続き foo 関数で変数 myName と test1 を使用できます。したがって、 foo 関数の実行が完了すると、呼び出しスタック全体の状態が次の図に示されます。

画像-20230406002532008

foo 関数の実行が完了すると、その実行コンテキストはスタックの最上位からポップされますが、foo 関数内の変数 myName と test1 は返される setName メソッドと getName メソッドで使用されるため、これら 2 つの変数は依然として想い出。これは、setName メソッドと getName メソッドによって運ばれる専用のバックパックによく似ており、setName メソッドと getName メソッドがどこで呼び出されても、foo 関数の専用バックパックを運びます。このナップザックはfoo 関数のクロージャと呼ばれます

JavaScript では、字句スコープの規則に従い、内部関数は外部関数で宣言された変数に常にアクセスできます。外部関数を呼び出して内部関数が返された場合、外部関数が実行された後であっても、内部関数参照は外部関数の変数は引き続きメモリに格納されており、これらの変数のコレクションをクロージャと呼びます。

myName = "极客邦"bar.setName メソッドのコードが実行されると、JavaScript エンジンは「現在の実行コンテキスト -> foo 関数クロージャー -> グローバル実行コンテキスト」の順序で myName 変数を検索します。

画像-20230406002923421

画像-20230406003122056

クロージャ生成の中核には 2 つのステップがあります: 最初のステップは内部関数を事前スキャンすることであり、2 番目のステップは内部関数によって参照される外部変数をヒープに保存することです。

この仕組み

var bar = {
    
    
    myName:"time.geekbang.com",
    printName: function () {
    
    
        console.log(myName)
    }    
}
function foo() {
    
    
    let myName = " 极客时间 "
    return bar.printName
}
let myName = " 极客邦 "
let _printName = foo()
_printName()//极客邦
bar.printName()//极客邦

printName 関数で使用される変数 myName はグローバル スコープに属しているため、最終的に出力される値は「Geek State」になります。これは、JavaScript 言語のスコープ チェーンが字句スコープによって決まり、字句スコープがコード構造によって決まるためです。

常識によれば、bar.printNameメソッドを呼び出すとき、メソッド内の変数 myName は bar オブジェクトを使用する必要があります。これらは全体であるため、ほとんどのオブジェクト指向言語はこのように設計されています。

オブジェクトの内部メソッドで内部オブジェクト プロパティを使用することは、非常に一般的な要件ですただし、JavaScript のスコープ メカニズムはこれをサポートしていないため、この要件に基づいて、JavaScript はこのメカニズムの別のセットを作成しました。

これはJavaScriptでは何ですか

画像-20230406224236490

this は実行コンテキストにバインドされています。つまり、各実行コンテキストに this が 1 つあります。

実行コンテキストは主にグローバル実行コンテキスト、関数実行コンテキスト、eval実行コンテキストの3種類に分かれており、対応するthisはグローバル実行コンテキストのthis、functionのthis、evalのthisの3種類のみとなります。

これはグローバル実行コンテキスト内で

コンソールに と入力してconsole.log(this)、これをグローバル実行コンテキストで出力します。最終出力はウィンドウ オブジェクトです。したがって、グローバル実行コンテキストにおける this は window オブジェクトを指していると結論付けることができますこれは、this とスコープ チェーン間の唯一の交差点でもあり、スコープ チェーンの下端にはウィンドウ オブジェクトが含まれており、グローバル実行コンテキストの this もウィンドウ オブジェクトを指します。

これは関数実行コンテキスト内で

function foo(){
    
    
  console.log(this)
}
foo()//window 

デフォルトでは、関数を呼び出すとき、実行コンテキスト内の this もウィンドウ オブジェクトを指します。

関数の呼び出しメソッドを通じて、実行コンテキストで this の値を設定します。

call メソッドに加えて、bind メソッドapplyメソッドを使用してこれを関数実行コンテキストに設定することもできますが、それらの使用法にはまだいくつかの違いがあります。

関数のcallメソッドを使用して、関数実行コンテキストの this point を設定できます。たとえば、次のコードは foo 関数を直接呼び出すのではなく、foo の call メソッドを呼び出し、bar オブジェクトをパラメータとして使用します。呼び出しメソッドの

let bar = {
    
    
  myName : " 极客邦 ",
  test1 : 1
}
function foo(){
    
    
  this.myName = " 极客时间 "
}

foo.call(bar)
console.log(bar)
console.log(myName)

このコードを実行して出力結果を観察すると、foo 関数内の this が bar オブジェクトを指していることがわかります。これは、bar オブジェクトを出力することで、bar の myName 属性が「geek state」から変更されていることがわかります。 "geek time" に変更すると、グローバル実行コンテキストで myName を出力するときに、JavaScript エンジンは変数が未定義であることを示すプロンプトを表示します。

オブジェクト呼び出しメソッドで設定
var myObj = {
    
    
  name : " 极客时间 ", 
  showThis: function(){
    
    
    console.log(this)
  }
}
myObj.showThis()

name 属性と showThis メソッドで構成される myObj オブジェクトを定義し、myObj オブジェクトを通じて showThis メソッドを呼び出します。このコードを実行すると、最終出力のこの値は myObj を指します。

オブジェクトを使用してその内部のメソッドを呼び出します。このメソッドの this はオブジェクト自体を指します

JavaScript エンジンが次myObject.showThis()のように変換すると考えることもできます。

myObj.showThis.call(myObj)

呼び出しメソッドを少し変更し、showThis をグローバル オブジェクトに割り当ててから、そのオブジェクトを呼び出します。これはグローバル ウィンドウ オブジェクトを指します。

var myObj = {
    
    
  name : " 极客时间 ",
  showThis: function(){
    
    
    this.name = " 极客邦 "
    console.log(this)
  }
}
var foo = myObj.showThis
foo()
  • グローバル環境で関数を呼び出す場合、関数内の this はグローバル変数ウィンドウを指します。
  • オブジェクトを通じて内部メソッドを呼び出すには、メソッドの実行コンテキスト内の this がオブジェクト自体を指します。
コンストラクターによって設定される
function CreateObj(){
    
    
  this.name = " 极客时间 "
}
var myObj = new CreateObj()

newキーワードは次のことを行います。

  • 空の単純な JavaScript オブジェクト (つまり{}) を作成します。
  • 手順 1 で新しく作成したオブジェクトにプロパティを追加し__proto__、そのプロパティをコンストラクターのプロトタイプ オブジェクトにリンクします。
  • ステップ 1 で新しく作成したオブジェクトをthisコンテキストとして使用します。
  • 関数がオブジェクトを返さない場合に戻りますthis

new CreateObj() を実行すると、JavaScript エンジンは次の 4 つのことを実行します。

  • 継承した新しいCreateObj.prototypeオブジェクト tempObj が作成されます。
  • 指定された引数を使用してコンストラクターを呼び出しCreateObjthis新しく作成されたオブジェクトにバインドします。
    • 最初に CreateObj.call メソッドを呼び出し、call メソッドのパラメータとして tempObj を使用します。これにより、CreateObj の実行コンテキストが作成されるときに、this は tempObj オブジェクトを指します。
    • 次に、CreateObj 関数を実行します。このとき、CreateObj 関数の実行コンテキスト内の this は tempObj オブジェクトを指します。
  • コンストラクターによって返されるオブジェクトは、new式の結果です。コンストラクターが明示的にオブジェクトを返さない場合は、手順 1 で作成した tempObj オブジェクトが使用されます。

これの設計上の欠陥と解決策

ネストされた関数の this は外部関数から継承されません

var myObj = {
    
    
  name : " 极客时间 ", 
  showThis: function(){
    
    
    console.log(this)//函数 showThis 中的 this 指向的是 myObj 对象
    function bar(){
    
    
        console.log(this)//函数 bar 中的 this 指向的是全局 window 对象
    }
    bar()
  }
}
myObj.showThis()

この問題を解決するには、ちょっとしたトリックを使用します。showThis関数で変数 self を宣言してthis を保存し、bar 関数で self を使用します。このメソッドの本質は、this システムをスコープ システムに変換することです。

var myObj = {
    
    
  name : " 极客时间 ", 
  showThis: function(){
    
    
    console.log(this)
    var self = this
    function bar(){
    
    
      self.name = " 极客邦 "
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

ES6 のアロー関数を使用してこの問題を解決することもできます。ES6 のアロー関数は独自の実行コンテキストを作成しないため、アロー関数のこれは外部関数に依存します。

var myObj = {
    
    
  name : " 极客时间 ", 
  showThis: function(){
    
    
    console.log(this)
    var bar = ()=>{
    
    
      this.name = " 极客邦 "
      console.log(this)
    }
    bar()
  }
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)

これには変数とは異なりスコープ制限がないため、ネストされた関数はそれを呼び出す関数からこれを継承せず、多くの直感的でないコードが発生します。この問題を解決するには、次の 2 つのアイデアが考えられます。

  • 1 つ目は、これを自己変数として保存し、変数スコープ メカニズムを使用して入れ子関数に渡すことです。
  • 2 つ目は、this を引き続き使用しますが、ネストされた関数をアロー関数に変更します。これは、アロー関数には独自の実行コンテキストがないため、呼び出し関数で this が継承されるためです。

通常の関数では、これはデフォルトでグローバル オブジェクト ウィンドウを指します。

デフォルトで関数が呼び出されるとき、実行コンテキスト内の this はデフォルトでグローバル オブジェクト ウィンドウを指します。

実際の作業では、関数実行コンテキストで this がデフォルトでグローバル オブジェクトを指すことは望ましくありません。これは、データ境界を壊し、誤操作を引き起こすためです。関数実行コンテキストでこれをオブジェクトを指すようにしたい場合、最良の方法は、call メソッドを通じて呼び出しを表示することです。

JavaScriptとはどのような言語ですか

  • 使用前に変数のデータ型を確認する必要がある静的言語
  • 逆に、実行時にデータ型をチェックする必要がある言語は、 動的言語 と呼ばれます
  • 暗黙的な型変換をサポートする言語は、弱い型付け言語と呼ばれます。
  • 暗黙的な型変換をサポートしない言語は、強く型付けされた言語と呼ばれます

JavaScript は型付けが弱い動的言語です

JavaScriptのデータ型

変数の型を調べるには、「typeof」演算子を使用します。

var bar
console.log(typeof bar)  //undefined
bar = 12 
console.log(typeof bar) //number
bar = " 极客时间 "
console.log(typeof bar)//string
bar = true
console.log(typeof bar) //boolean
bar = null
console.log(typeof bar) //object
bar = {
    
    name:" 极客时间 "}
console.log(typeof bar) //object

画像-20230407213627277

  • typeof を使用して Null 型を検出すると、Object が返されます。
  • Object 型は特殊で、上記 7 つの型から構成されるキーと値のペアを含むデータ型です。
  • 最初の 7 つのデータ型はプリミティブ型と呼ばれ、最後のオブジェクト型は参照型と呼ばれます。

メモリ空間

画像-20230407213811682

JavaScript の実行中、コード スペース、スタック スペースヒープ スペースという 3 つの主なタイプのメモリ スペースが存在します。

スタック領域とヒープ領域

プリミティブ型のデータ値は「スタック」に直接格納され、参照型の値は「ヒープ」に格納されます

function foo(){
    
    
    var a = " 极客时间 "
    var b = a
    var c = {
    
    name:" 极客时间 "}
    var d = c
}
foo()

画像-20230407214350350

画像-20230407214218850

画像-20230407214326608

JavaScript のガベージ コレクション

コールスタック内のデータがどのようにリサイクルされるか

function foo(){
    
    
    var a = 1
    var b = {
    
    name:" 极客邦 "}
    function showName(){
    
    
      var c = " 极客时间 "
      var d = {
    
    name:" 极客时间 "}
    }
    showName()
}
foo()

画像-20230407220434206

showName 関数が実行されると、JavaScript エンジンは showName 関数の実行コンテキストを作成し、showName 関数の実行コンテキストをコール スタックにプッシュします。showName 関数が最終的に実行されるとき、そのコール スタックは次のようになります。上の図。同時に、現在の実行状態を記録するポインター (ESP と呼ばれる)もあり、コール スタック内の showName 関数の実行コンテキストを指し、showName 関数が現在実行中であることを示します。

次に、showName 関数が実行された後、関数実行フローは foo 関数に入り、その後、showName 関数の実行コンテキストを破棄する必要があります。このとき ESP が役に立ちます。JavaScript は ESP を foo 関数の実行コンテキストに移動します。このダウン操作は showName 関数の実行コンテキストを破棄するプロセスです

画像-20230407220604077

関数の実行が終了すると、JavaScript エンジンは ESP を下に移動することによって、スタックに保存されている関数の実行コンテキストを破棄します

ヒープ内のデータがどのようにリサイクルされるか

画像-20230407220726966

ヒープ内のガベージ データをリサイクルするには、JavaScript でガベージ コレクターを使用する必要があります。

世代仮説

ガベージ コレクションの分野では重要な用語であり、その後のガベージ コレクション戦略は仮説に基づいています。

世代間仮説には次の 2 つの特徴があります。

  • 1 つ目は、ほとんどのオブジェクトがメモリ内に存在するのは短期間であるということです。簡単に言えば、多くのオブジェクトはメモリが割り当てられるとすぐにアクセスできなくなります。
  • 2 つ目は、長生きするアンデッド オブジェクトです。

JavaScript エンジン V8 はガベージ コレクションを実装します (Java と同様)

V8 では、ヒープが若い世代古い世代の2 つの領域に分割され、新世代には生存期間の短いオブジェクトが格納され、古い世代には生存期間の長いオブジェクトが格納されます

新しく誕生したエリアは通常 1 ~ 8M の容量しかサポートしませんが、古いエリアがサポートする容量ははるかに大きくなります。これら 2 つの領域について、V8 は 2 つの異なるガベージ コレクターを使用して、ガベージ コレクションをより効率的に実装します。

  • セカンダリ ガベージ コレクタは、主に新しい世代のガベージ コレクションを担当します。
  • メイン ガベージ コレクターは、主に古い世代のガベージ コレクションを担当します。
ガベージ コレクターのワークフロー

ガベージ コレクターの種類に関係なく、それらはすべて共通の実行プロセスを持っています

  • 最初のステップは、スペース内のアクティブなオブジェクトと非アクティブなオブジェクトにラベルを付けることです。
  • 2 番目のステップは、非アクティブなオブジェクトによって占有されているメモリを再利用することです。
  • 3 番目のステップは、記憶の整理を行うことです。一般に、オブジェクトが頻繁にリサイクルされると、メモリ内に大量の不連続な領域が発生し、このような不連続なメモリ領域をメモリの断片化と呼びます。メモリ内に多数のメモリ フラグメントが出現した場合、大量の連続メモリを割り当てる必要がある場合、メモリが不足する可能性があります。したがって、最後のステップではこれらのメモリフラグメントを整理する必要がありますが、一部のガベージコレクタはメモリフラグメントを生成しないため、このステップは実際にはオプションです

画像-20230407221144597

セカンダリ ガベージ コレクター

セカンダリ ガベージ コレクタは、主に新しいエリアでのガベージ コレクションを担当します。通常の状況では、ほとんどの小さなオブジェクトが新しい領域に割り当てられるため、この領域は大きくありませんが、ガベージ コレクションは依然として比較的頻繁に行われます。

新世代では、処理にScavenge アルゴリズムが使用されますいわゆる Scavenge アルゴリズムは、新しい世代の空間を半分の 2 つの領域に分割し、その半分がオブジェクト領域で、もう半分が空き領域です。

新しく追加されたオブジェクトはオブジェクト領域に格納されますが、オブジェクト領域がほぼいっぱいになった場合は、ガベージ クリーン操作を実行する必要があります。

ガベージ コレクション プロセスでは、最初にオブジェクト領域内のガベージにマークを付ける必要があります。マークが完了すると、ガベージ クリーニング段階に入り、セカンダリ ガベージ コレクタはこれらの生き残ったオブジェクトを空き領域にコピーし、また、これらのオブジェクトは整然と配置されているため、このコピー処理はメモリの仕上げ操作を完了したことに相当し、コピー後の空き領域にメモリの断片化は発生しません。

コピーが完了すると、オブジェクト領域と空き領域の役割が逆転し、元のオブジェクト領域が空き領域となり、元の空き領域がオブジェクト領域となる。このようにして、ゴミオブジェクトのリサイクル操作が完了すると同時に、この役割逆転操作により、これら 2 つの領域を新世代で無期限に再利用することもできます

新世代で使用される Scavenge アルゴリズムにより、クリーンアップ操作が実行されるたびに、生き残ったオブジェクトをオブジェクト領域から空き領域にコピーする必要があります。ただし、コピーには時間的コストがかかり、新領域の空き容量を大きくしすぎると毎回のクリーンアップに時間がかかりすぎるため、実行効率を考慮して新領域の空き容量を設定するのが一般的です。比較的小さいこと

スポーンエリアのスペースは狭いため、エリア全体を生き物で埋めるのは簡単です。この問題を解決するために、JavaScript エンジンはオブジェクト プロモーション戦略を採用しています。つまり、2 回のガベージ コレクション後もまだ生きているオブジェクトは古い領域に移動されます。

メインガベージコレクター

メインガベージコレクターは主に旧エリアのガベージコレクションを担当します。新入生エリアで宣伝されるオブジェクトに加えて、いくつかの大きなオブジェクトが旧学生エリアに直接割り当てられます。したがって、古い地域の物体は、大きな空間を占めるということと、長く生き続けるという2つの特徴を持っています。

古い領域のオブジェクトは比較的大きいため、古い領域でのガベージ コレクションに Scavenge アルゴリズムを使用する場合、これらの大きなオブジェクトをコピーするには時間がかかり、非効率的なリサイクルが行われ、領域の半分が無駄になります。 。したがって、メインのガベージ コレクターは、ガベージ コレクションに **Mark-Sweet** アルゴリズムを使用します。

マーキングフェーズはルート要素群から開始され、ルート要素群を再帰的に横断することになりますが、この横断過程で到達できる要素はアクティブオブジェクトと呼ばれ、到達できない要素はガベージデータと判断されます。

画像-20230407221551899

次はゴミの除去作業です

画像-20230407221624865

ただし、メモリのブロックに対してマーククリア アルゴリズムを複数回実行すると、多数の不連続なメモリ フラグメントが生成されます。断片化が多すぎると、大きなオブジェクトに十分な連続メモリを割り当てることができなくなります。そのため、別のアルゴリズムであるmark-compact (Mark-Compact) は、このマーク プロセスは mark-clear アルゴリズムと同じですが、次のステップは異なります。リサイクル可能なオブジェクトを直接クリーンアップしますが、残っているすべてのオブジェクトを一方の端に移動してから、端の境界の外側のメモリを直接クリーンアップします。

画像-20230407221732340

フルストップ (ストップ・ザ・ワールド)

V8 では、セカンダリ ガベージ コレクターとメイン ガベージ コレクターを使用してガベージ コレクションを処理しますが、JavaScript はメイン スレッドで実行されるため、ガベージ コレクション アルゴリズムが実行されると、ガベージ コレクションが完了するまで実行中の JavaScript スクリプトを一時停止する必要があります。スクリプトの実行を再開します。この動作をStop-The-Worldと呼びます

画像-20230407221821794

V8 新世代のガベージ コレクションでは、スペースが小さく、生き残るオブジェクトが少ないため、完全一時停止の影響は大きくありませんが、旧世代は異なります。上の図に示すように、ガベージ コレクション中にメイン スレッドが長時間占有されると、200 ミリ秒かかり、この 200 ミリ秒の間、メイン スレッドは他のことを行うことができなくなります。たとえば、ページが JavaScript アニメーションを実行している場合、ガベージ コレクターが動作しているため、アニメーションは 200 ミリ秒以内に実行できず、ページがフリーズします。

旧世代のガベージコレクションによる遅延を軽減するため、V8ではマーキング処理を1つずつサブマーキング処理に分割し、同時にマーキングフェーズが終了するまでガベージコレクションのマーキングとJavaScriptアプリケーションロジックを交互に実行させます。このアルゴリズムを「増分インクリメンタル マーキング アルゴリズム」と呼びます以下に示すように:

画像-20230407221918344

V8 の仕組み

コンパイラとインタプリタ

プログラムが実行される前に、コンパイルされた言語はコンパイラのコンパイル プロセスを経る必要があり、マシンが理解できるバイナリ ファイルはコンパイル直後に保持されるため、プログラムを実行するたびにバイナリ ファイルを保存できます。プログラムを再起動せずに直接実行します

インタプリタ言語で書かれたプログラムは、実行されるたびにインタプリタによって動的に解釈され、実行される必要がありますたとえば、Python、JavaScript などはすべてインタープリタ型言語です。

画像-20230407222240373

V8 が JavaScript コードを実行する仕組み

画像-20230407222504311ソースコードを抽象構文ツリーに変換し、実行コンテキストを生成します。

var myName = " 极客时间 "
function foo(){
    
    
  return 23;
}
myName = "geektime"
foo()

生成された AST 構造は次のとおりです。

画像-20230407223617967

コンパイラまたはインタプリタの後続の作業は、ソース コードではなく AST に依存する必要があります。

AST は非常に重要なデータ構造です

Babel の動作原理は、まず ES6 ソース コードを AST に変換し、次に ES6 構文 AST を ES5 構文 AST に変換し、最後に ES5 AST を使用して JavaScript ソース コードを生成します。

最初の段階はトークン化であり、字句解析とも呼ばれ、その機能はソース コードの行をトークンに逆アセンブルすることです。いわゆるトークンとは、構文的に細分化できない最小の単一文字または文字列を指します。

画像-20230407224224779

第 2 段階は、文法分析とも呼ばれる解析 (解析) です。その機能は、前のステップで生成されたトークン データを文法規則に従って AST に変換することです。ソース コードが文法規則に準拠している場合、このステップは正常に完了します。ただし、ソース コードに構文エラーがある場合、このステップは終了し、「構文エラー」がスローされます。

AST を使用すると、V8 はコードの実行コンテキストを生成します。

AST と実行コンテキストを使用すると、次の 2 番目のステップはインタプリタ Ignition です。これは、AST に基づいてバイトコードを生成し、そのバイトコードを解釈して実行します。

V8 は当初、バイトコードを持たず、AST を直接機械語に変換していましたが、機械語の実行効率が非常に高いため、リリース後しばらくはこの方式が非常に有効でした。しかし、携帯電話、特に 512M メモリで動作する携帯電話での Chrome の普及に伴い、V8 は変換されたマシンコードを保存するために大量のメモリを消費する必要があるため、メモリ使用量の問題も露呈しています。メモリ使用量の問題を解決するために、V8 チームはエンジン アーキテクチャを大幅にリファクタリングし、バイトコードを導入し、以前のコンパイラを放棄し、最終的に現在のアーキテクチャを実現するまでに 4 年近くかかりました。

バイトコードはASTとマシンコードの間のコードです。ただし、これは特定のタイプのマシンコードとは関係がなく、バイトコードは実行する前にインタプリタによってマシンコードに変換される必要があります。

画像-20230407230212540

マシンコードはバイトコードよりもはるかに多くのスペースを占有するため、バイトコードを使用するとシステムのメモリ使用量を削減できます。

初めて実行されるバイトコードがある場合、インタプリタ Ignition がそれを 1 つずつ解釈して実行します。バイトコードの実行過程で、何度も繰り返し実行されるコードなどのホットスポット コード (HotSpot) がある場合、これはホットスポット コードと呼ばれ、バックグラウンド コンパイラ TurboFan はホットスポットのバイトを配置します。コードは効率的なマシンコードにコンパイルされ、この最適化されたコードが再度実行されるときは、コンパイルされたマシンコードのみを実行する必要があるため、コードの実行効率が大幅に向上します。

バイトコードは、Java や Python などのインタプリタおよびコンパイラ テクノロジと連携し、仮想マシンもジャストインタイム コンパイル (JIT)と呼ばれるこのテクノロジに基づいて実装されます

「文法上の誤り」。

AST を使用すると、V8 はコードの実行コンテキストを生成します。

AST と実行コンテキストを使用すると、次の 2 番目のステップはインタプリタ Ignition です。これは、AST に基づいてバイトコードを生成し、そのバイトコードを解釈して実行します。

V8 は当初、バイトコードを持たず、AST を直接機械語に変換していましたが、機械語の実行効率が非常に高いため、リリース後しばらくはこの方式が非常に有効でした。しかし、携帯電話、特に 512M メモリで動作する携帯電話での Chrome の普及に伴い、V8 は変換されたマシンコードを保存するために大量のメモリを消費する必要があるため、メモリ使用量の問題も露呈しています。メモリ使用量の問題を解決するために、V8 チームはエンジン アーキテクチャを大幅にリファクタリングし、バイトコードを導入し、以前のコンパイラを放棄し、最終的に現在のアーキテクチャを実現するまでに 4 年近くかかりました。

バイトコードはASTとマシンコードの間のコードです。ただし、これは特定のタイプのマシンコードとは関係がなく、バイトコードは実行する前にインタプリタによってマシンコードに変換される必要があります。

【外部リンク画像転送...(img-bWRQAvjQ-1682523193368)】

マシンコードはバイトコードよりもはるかに多くのスペースを占有するため、バイトコードを使用するとシステムのメモリ使用量を削減できます。

初めて実行されるバイトコードがある場合、インタプリタ Ignition がそれを 1 つずつ解釈して実行します。バイトコードの実行過程で、何度も繰り返し実行されるコードなどのホットスポット コード (HotSpot) がある場合、これはホットスポット コードと呼ばれ、バックグラウンド コンパイラ TurboFan はホットスポットのバイトを配置します。コードは効率的なマシンコードにコンパイルされ、この最適化されたコードが再度実行されるときは、コンパイルされたマシンコードのみを実行する必要があるため、コードの実行効率が大幅に向上します。

バイトコードは、Java や Python などのインタプリタおよびコンパイラ テクノロジと連携し、仮想マシンもジャストインタイム コンパイル (JIT)と呼ばれるこのテクノロジに基づいて実装されます

画像-20230408000306073

おすすめ

転載: blog.csdn.net/weixin_46488959/article/details/130396808