深刻理解JS執行上下文的點點滴滴

前言

對於一名前端開發者來講,深刻理解JavaScript程序內部執行機制固然是頗有必要的,其中一個關鍵概念就是JavaScript的執行上下文和執行棧,理解這部份內容也有助於理解做用域、閉包等javascript

本次重點

  • 執行上下文概念、類型、特色
  • 執行上下文的生命週期
  • 關於變量提高
  • this指向問題
  • 執行上下文棧

基本概念:

所謂的JavaScript執行上下文就是當前JS代碼代碼被解析和執行時所在環境的抽象概念,js代碼都是在執行上下文中運行的前端

1、執行上下文類型

1.全局執行上下文

它的特色有如下幾個:java

a.它是最基礎、默認的全局執行上下文node

b.它會建立一個全局對象,而且將this指向全局對象,在瀏覽器中全局對象是window,在nodejs中全局對象是global面試

c.一個程序中只有一個segmentfault

2.函數執行上下文

它的特色有如下幾個:瀏覽器

a.有本身的執行上下文安全

b.能夠在一個程序中存在任意數量閉包

c.是函數被執行時建立app

3.eval函數執行上下文:

eval函數能夠計算某個字符串,並執行其中的js代碼,這樣就會存在一個安全性問題,在代碼字符串未知或者是來自於用戶輸入源的話,絕對不要使用eval函數

以上就是執行上下文的幾種類型和相應的特色,咱們能夠看下下面這段代碼:

裏面的三個函數都被執行了,因此是有三個函數執行上下文

// 全局執行上下文
var sayHello = 'Hello'
function someone() {   // 函數執行上下文
  var first = 'Tom', last = 'Ada'
  function getFirstName() {  // 函數執行上下文
    return first
  }
  function getLastName() {  // 函數執行上下文
    return last
  }
  console.log(sayHello + getFirstName() + getLastName())
}
someone()
複製代碼

2、執行上下文的生命週期

執行上下文的生命週期分了三個階段:

  • 建立階段
  • 執行階段
  • 回收階段
建立階段

對於函數執行上下文,函數被調用的時候,可是還未執行裏面的代碼以前,會作三件事情:

  • 建立變量對象:會初始化函數的參數,提高函數聲明和變量聲明

  • 建立做用域鏈:做用域鏈用於標識符解析,看下面代碼:

    f3函數被調用的時候,裏面的變量num要求被解析的時候,會在當前f3的做用域裏查找,若是沒找到,就會向上一層做用域中查找,直到在全局做用找到該變量爲30

var num = 30;
function f1() {
  function f2() {
    function f3() {
      console.log(num);
    }
    f3();
  }
  f2();
}
f1();
複製代碼
  • 肯定this指向:這個狀況比較多,會在下文統一介紹

在一個程序執行以前,要先解析代碼,會先建立全局執行上下文環境,把須要執行的變量和函數聲明都取出來並暫時賦值爲undefined,函數也要先聲明好待調用,這也是咱們下文中會講到的變量提高,以上幾步作完後,開始正式執行程序

執行階段

執行的變量賦值、函數調用等代碼執行

回收階段

執行上下文出棧,等待虛機垃圾回收執行上下文

3、變量提高

變量提高分爲兩種:

  • 變量聲明提高
  • 函數聲明提高

關於變量聲明提高,先看如下代碼片斷:

console.log(a)  // undefined
var a = 5
function test() {
  console.log(a)  // undefined
  var a = 10
}
test()
複製代碼

以上代碼中,第1個 a 是在全局執行上下文環境中,因爲在全局執行上下文建立的時候,把須要執行的變量和函數聲明都取出來並暫時賦值爲undefined,因此打印出來的就是undefined

第2個 a 是在test這個函數執行上下文環境中,同上,因此打印出來的就是undefined

var a
console.log(a)  // undefined
a = 5
function test() {
  var a
  console.log(a)  // undefined
  a = 10
}
test()
複製代碼

關於函數聲明提高,看如下代碼:

console.log(f1) // function f1() {}
function f1() {}
console.log(f2) // undefined
var f2 = function() {} 
複製代碼

打印結果在註釋中,因爲變量聲明和函數聲明提高原則能夠把代碼改爲以下:

function f1() {}
console.log(f1) // function f1() {}
var f2;
console.log(f2) // undefined
f2 = function() {} 
複製代碼

f1和f2不同的地方是:f1是普通函數聲明的方式,f2是函數表達式,在f2未被賦值的時候,它就是一個變量,這個時候變量提高,因此打印的f2爲undefined

若是一個變量既是函數聲明的方式,又是變量聲明的方式,代碼以下:

咱們發現函數聲明的優先級是高於變量提高的優先級的

function test(arg){
  console.log(arg);  // function arg(){console.log('hello world') }
  var arg = 'hello'; 
  function arg(){
    console.log('hello world') 
  }
  console.log(arg); // hello 
}
test('hi');
複製代碼

總結:變量提高的幾個特色:

  • 若是有形參,先給形參賦值
  • 函數聲明的優先級是高於變量提高的優先級的,但能夠從新賦值
  • 私有做用域代碼從上到下執行

4、肯定this指向問題

this指向問題一般會在一些面試題中出現,狀況比較多,先了解下它的一些特色:

  • this是執行上下文的一部分
  • 須要在執行時肯定
  • 瀏覽器中 this 指向 window, node中this指向global
對於非嚴格模式和es5的js程序中,this指向能夠分爲如下幾種狀況:

第一種:a()直接調用的方式,this === window

function a() {
  console.log(this.b)  
}
var b = 0
a()
複製代碼

打印出的值爲 0

第二種:誰調用了函數,誰就是this

function a() {
  console.log(this)
}
var obj = {a: a}
obj.a()
複製代碼

打印出的值爲obj這個對象

第三種:構造函數模式下,this指向當前執行類的實例

function getPersonInfo(name, age) {
  this.name = name
  this.age = age
  console.log(this)
}
var p1 = new getPersonInfo('linda', 13)
複製代碼

打印出來的值是:

getPersonInfo{ name: 'linda', age: 13 }

第四種:call/apply/bind調用函數的方式,this指向第一個參數

function add (b, c) {
  console.log(this)
  return this.a + b + c
}
var obj = {a: 3}
add.call(obj, 5, 7)
add.call(obj, [10, 20])
複製代碼

打印出來的值就是obj的值

對於嚴格模式的js程序中,this指向對於直接調用的方式有所不一樣:

嚴格模式下,函數直接調用的方式中this指向undefined

'use strict'
function a() {
  console.log(this)  
}
a()
複製代碼

這個時候函數裏的this打印出 undefined

對於箭頭函數

箭頭函數沒有自身的this關鍵字,看外層是否有函數,若是有函數,外層函數的this就是內部箭頭函數的this,若是沒有,this就是指向window

能夠看如下幾種狀況:

var person = {
  myName: 'linda', 
  age:1, 
  clickPerson: function() { 
  	var show = function() {
  		console.log(`Person name is ${this.myName}, age is ${this.age}`)
  	}
    show()
  }
}
person.clickPerson()
複製代碼

打印結果:Person name is undefined, age is undefined

裏面的函數show被調用的時候,是普通函數調用的狀況,因此this指向window,而全局函數中沒有myName和age,因此打印出來是undefined

能夠換成箭頭函數:

var person = {
  myName: 'linda', 
  age:1, 
  clickPerson: function() { 
  	var show = () => {
  		console.log(`Person name is ${this.myName}, age is ${this.age}`)
  	}
    show()
  }
}
person.clickPerson()
複製代碼

打印出的結果是:Person name is linda, age is 1

對於箭頭函數自身沒有this關鍵字,因此看外層函數,而外層函數中是咱們前面說到的第二種狀況,this指向person這個對象,因此是有myName和age的值

若是把clickPerson也換成箭頭函數:

var person = {
  myName: 'linda', 
  age:1, 
  clickPerson: () => { 
  	var show = () => {
  		console.log(`Person name is ${this.myName}, age is ${this.age}`)
  	}
    show()
  }
}
person.clickPerson()
複製代碼

咱們發現打印的結果是:Person name is undefined, age is undefined

因爲都是箭頭函數,最後找到了全局的window,因此this指向window,而全局函數中沒有myName和age,因此打印出來是undefined

再看另一個例子:

function getPersonInfo(name,age){
  this.myName = name;
  this.age = age;
  this.show = function() {
    console.log(`Person name is ${this.myName}, age is ${this.age}`)
  }
}
getPersonInfo.prototype.friend = function(friends) {
    var array = friends.map(function(friend) {
        return `my friend ${this.myName} age is ${this.age}`
    });
    console.log(array);
}

var person1 = new getPersonInfo("linda",18);
person1.show()
person1.friend(['Ada', 'Tom'])
複製代碼

show()函數調用結果打印:Person name is linda, age is 18

friend()函數調用打印結果:["my friend undefined age is undefined", "my friend undefined age is undefined"]

對於friend函數內部,this指向的是當前的getPersonInfo這個構造函數初始化的實例,可是在內部使用map是一個閉包函數,且內部是普通函數的調用方式,因此內部this是指向了window,能夠把裏面普通函數調用的方式改爲箭頭函數的方式便可

function getPersonInfo(name,age){
  this.myName = name;
  this.age = age;
  this.show = function() {
    console.log(`Person name is ${this.myName}, age is ${this.age}`)
  }
}
getPersonInfo.prototype.friend = function(friends) {
    var array = friends.map((friend) => {
        return `my friend ${this.myName} age is ${this.age}`
    });
    console.log(array);
}

var person1 = new getPersonInfo("linda",18);
person1.show()
person1.friend(['Ada', 'Tom'])
複製代碼

此次打印結果就是["my friend linda age is 18", "my friend linda age is 18"]就是咱們預想的了

總結:(非嚴格模式下)能夠按照下圖規律查找this的指向

5、執行上下文棧

js建立了執行上下文棧來管理執行上下文,咱們經過以下一段代碼和進棧出棧順序圖來理解執行上下文棧

var name = 'Tom';
function father() {
  var sonName = 'Anda';
  function son() {
    console.log('son name is ', sonName)
  }
  console.log('father name is ', name)
  son();
}
father();
複製代碼

過程:

1.全局執行上下文進棧

2.調用函數father,father函數執行上下文進棧

3.father函數內部代碼執行,son函數被執行,son函數執行上下文進棧

4.son函數執行完畢,son函數的執行上下文出棧

5.father函數執行完畢,father函數的執行上下文出棧

6.瀏覽器關閉時,全局執行上下文出棧

執行上下文棧特色:

  • 先建立全局執行上下文,並壓入棧頂
  • 函數執行時建立函數執行上下文,再壓入棧頂
  • 函數執行完函數的執行上下文出棧,等待垃圾回收
  • JS執行引擎老是訪問棧頂的執行上下文
  • js代碼是單線程的,代碼是排隊執行
  • 全局執行上下文在瀏覽器關閉時出棧
參考資料:

揭祕JavaScript中「神祕」的this關鍵字

全面瞭解JS做用域

js執行上下文棧/做用域鏈

深刻理解js執行上下文

深刻理解JavaScript執行上下文和執行棧

相關文章
相關標籤/搜索