深刻理解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执行上下文和执行栈

相关文章
相关标签/搜索