Javascript函数进阶-执行顺序和执行环境

javascript解释器概述

在《初步了解浏览器》中,我们介绍了浏览器的组成部分,javascript解释器就是其中的一个组成部分:
Javascript函数进阶-执行顺序和执行环境_第1张图片
interpreter有翻译者、翻译器的意思,在 javascrip中一般叫解释器、解析器、引擎。从中文的字面意思来理解,前端程序猿写了一大堆莫名其妙的代码,只有放到浏览器中,在js解释器翻译之后才具有一定的意义,解释器按照程序猿的规定去完成一个个任务,所有任务可以分成两种,同步任务(synchronous)和异步任务(asynchronous),这一节我们只讨论同步任务。

javascript代码本质上是一段文本,在浏览器宿主环境中需要被script标签包裹才能被正确的执行,这很好理解。JS引擎并不是编译器,不会将代码编译成机器码,JS引擎本身也是一种程序(可能是C、C++、Java编写的程序),它通过算法能识别javascript代码并执行,简单的说JS引擎其实就是能读懂javascript程序的程序。

内存堆和调用栈

以谷歌 V8 引擎为例,v8引擎包括两个主要组件:

  • 内存堆 — 这是发生内存分配的地方
  • 调用栈 — 这是代码执行的地方
    V8 引擎主要是通过这两个组件执行代码的。
    Javascript函数进阶-执行顺序和执行环境_第2张图片

javascript解释器执行过程

关于Javascript执行我们要牢记,浏览器是多线程的,而javascript是单线程的。

  1. 浏览器内部是多线程的。I/O操作、定时器的计时和事件监听(click, keydown…)等都是由浏览器提供的其他线程来完成的。
  2. JS是单线程语言,浏览器只分配给js一个主线程,用来执行任务。原则上js引擎从上到下顺序执行代码,当一个任务在执行的时候,其他未执行的任务都在排队。

为什么我要说原则上js解释器从上到下顺序执行代码,这是因为JS解释器执行代码不是按照书写代码的顺序执行的,而是有自己的执行规则,见下图:
Javascript函数进阶-执行顺序和执行环境_第3张图片

一、语法检查

js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:
分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误,停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。

例1:通过下面的代码我们发现语法错误b();只阻塞了代码块1的执行
Javascript函数进阶-执行顺序和执行环境_第4张图片
例2:看这段代码,会报错吗?

    var a=1;
    function b(){
     
        console.log(c);
    }
    console.log(a);

执行的结果是控制台打印了1,没有报错。函数b中明明有语法错误为什么不报错呢?这是因为JS引擎不会编译代码,而是一边解析,一边执行的,JS引擎没有执行b函数所以不会报错。
我们把代码改一下:
Javascript函数进阶-执行顺序和执行环境_第5张图片
当调用了b函数,控制台报错了。这个因为在执行b函数的时候,开始检查b函数的语法,发现在b函数作用域中没有定义c,继续在全局作用域中寻找c,还是没找到,JS引擎就会抛出错误。

通过上面的代码,我们验证了一件事,就是JS解释器不会编译代码,而是边解析边执行的,对于语法错误,如果没有被执行到就不会报错。

二、预解析

语法检查检查之后,进入预解析阶段,js解释器在预解析阶段会在内存中创建执行上下文(Execution Context),也叫执行环境,以后我们会称之为执行环境,执行环境是什么呢?就是正在执行的某行或者某段代码的所处的环境。

在浏览器中,一般说JS的执行环境有2种,全局环境、函数环境。

执行环境的生命周期

执行环境是在预解析阶段被创建、代码执行阶段被重新赋值,代码执行完毕即出栈、等待被回收,这是执行环境的生命周期。
Javascript函数进阶-执行顺序和执行环境_第6张图片

执行环境可以抽象成一个对象表示,其中包括三个属性:变量对象、作用域链、this指向

ECObj={
     
	vo:{
     }//变量对象
	scopeChain:{
     },	//作用域链
	this:{
     }     //this指向
}

1、变量对象

变量对象(Variable Object – 简写 VO)是一个抽象的概念,指代与执行上下文相关的特殊对象,它存储着在上下文中声明的:

  • 变量(var)
  • 函数声明 (function declaration,简写 FD)
  • 函数的形参(arguments)

2、作用域链

JS中的变量和函数并不总是可用的,有其使用的范围,这就是作用域。JS的作用域靠函数形成,函数内声明的变量是局部变量,在函数外不可访问。作用域链决定了哪些数据能被函数访问

3、this指向

创建执行环境

全局环境
JS默认的代码执行环境,是最外围的一个执行环境,在web浏览器中,全局执行环境被认为是window对象。一旦代码被载入,引擎最先进入的是这个环境,全局环境不会被自动回收,只有在关闭浏览器窗口的时候才会被销毁,所以在定义全局变量一定要格外小心。
函数环境
JS解释器在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,函数环境是一个相对于全局环境的概念,调用任何一个函数一个新的执行环境就会被创建出来,执行函数,执行结束后返回全局环境,而创建的函数环境等待垃圾回收。

1、创建全局环境

就像先有宇宙再有地球一样,js代码中一定是先创建全局环境,在预解析阶段创建全局环境。

  1. JS解释器会先做变量和函数的声明(js引擎会进行变量提升)
  2. 作用域链是个数组,在全局环境中很简单,作用域链只有全局作用域Global
  3. this指向window。

2、创建函数环境

如果在全局环境中调用函数,则对函数代码进行预解析,创建该函数的函数环境,执行函数,执行结束后返回全局环境(函数环境等待回收)。

函数环境与调用栈息息相关,下面我将用Chrome浏览器的开发者工具的Sources面板打断点来演示一下函数f执行环境的生命周期,也可以结合Firefox的调试器来查看一下,会更加的清楚。

function f(arg_f1,arg_f2)
{
     
  var v_f1=10;
  function ff(){
     
    console.log('ff');
  }
  ff();
  console.log(v_f1);
}
f(1);
console.log('Go on');

Javascript函数进阶-执行顺序和执行环境_第7张图片
处于调用栈的栈顶的函数就是当前执行的函数,该函数的执行环境就是当前环境,用一个对象抽象的表示如下。

Javascript函数进阶-执行顺序和执行环境_第8张图片

1、生成变量对象

预解析阶段的变量对象,用VO表示,包括三类属性:参数、变量声明、函数声明。

  • 在执行阶段之前参数的属性名是形参,属性值是实参,如果没有传参则是undefined;
  • 函数的函数名是函数名,属性值是函数的定义。
  • 变量声明的变量名是属性名,属性值是undefined;
    Javascript函数进阶-执行顺序和执行环境_第9张图片
2、建立作用域链

作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。可以用一个数组来表示作用域链,数组的第一项总是位于执行栈栈顶函数的上下文中的变量对象local,而数组的最后一项总是全局变量对象global。

其实函数作用域链是创建函数的时候就创建了,创建的时候其中只有全局变量对象,保存在函数的[[Scope]]属性中,函数执行时的,通过复制该属性中的对象来构建作用域链。
Javascript函数进阶-执行顺序和执行环境_第10张图片

3、确定this指向

this指向是执行上下文被创建时确定的。在一个函数环境中,this由调用者决定,怎么理解这句话呢?
函数可以作为某个对象的方法调用,也可以独立调用,作为对象的方法调用时this指向这个对象,独立调用时this指向全局对象window。但在严格模式中,this的值是undefined。
Javascript函数进阶-执行顺序和执行环境_第11张图片

三、解释执行

Javascript函数进阶-执行顺序和执行环境_第12张图片
执行代码阶段,变量对象VO会被重新赋予真实的值,即预解析阶段赋予的undefined值会被覆盖,赋值后的变量对象成为活动对象,用AO表示。
Javascript函数进阶-执行顺序和执行环境_第13张图片
此阶段才是程序真正进入执行阶段,Javascript引擎会一行一行的读取并运行代码。
PS:假如变量是定义在函数内的,而函数从头到尾都没被调用的话,则变量值永远都是undefined值。
一旦函数的执行结束(会有返回值),该函数就会从执行栈弹出,执行栈中的下一个函数(如果没有则是全局代码)会继续执行,直到执行栈为空。
Javascript函数进阶-执行顺序和执行环境_第14张图片
函数执行完毕,JS的垃圾回收机制将会销毁执行环境和活动对象。

函数的嵌套

嵌套函数,就是在一个函数的内部定义另外一个函数。在一个内部定义的函数只能在其内部调用,不能在外部调用。

function f(arg_f1,arg_f2)
{
     
  var v_f1=10;
  function ff(){
     
    console.log('ff');
  }
}
ff();	//Uncaught ReferenceError: ff is not defined

这是因为执行环境通过作用域链只能一级一级的往上查询函数,不能往下级搜索函数。也就是说只有函数的内部嵌套的函数可以访问函数的作用域。

那么,那怎么做才能在外部环境调用函数内部的函数呢?可以把内部函数作为外部函数的返回值赋值给一个外部的变量。

"use strict";
function f(arg_f1)
{
     
  function ff() {
     
    console.log('内部函数');
  }
  return ff;
}
var a=f(1);
a();	//内部函数
a();	//内部函数
a();	//内部函数

闭包

这段代码中内部函数ff没有引用外部函数f的变量和参数,如果内部函数引用了外部函数的变量使用到就会产生一个闭包Closure
使用闭包可以让外部函数执行完毕后,其内部的活动变量(AO)停留在内存中,不被垃圾回收机制回收(前面说了函数执行完毕,JS的垃圾回收机制将会销毁执行环境和活动对象。)。

"use strict";
function outer(arg_f1)
{
     
  var n='test';
  function inner() {
     
    console.log(++arg_f1);
  }
  return inner;
}
var fun=outer(1);
fun();  //2
fun();  //3
fun();  //4

上面的代码中外部函数outer执行完毕,返回它的内部函数inner,并赋值给了变量fun,从此fun作为指针指向一个函数,调用fun,就相当用调用了内部函数inner,我们用两种浏览器来观察第一次执行inner的执行环境。

  • inner出现在调用栈的栈顶,它的下面并没有outer,说明outer已经执行结束出栈了。
  • inner的作用域链中出现了一个包含outer函数中变量的作用域,这意味着通过这个方法即使outer函数执行完毕,仍然可以访问outer函数的内部变量。变量占用的内存不能被释放和回收,这也是一种内存泄漏,不要过度使用闭包。
    Javascript函数进阶-执行顺序和执行环境_第15张图片
    在上图中我们看到,似乎Chrome认为outer是闭包,但是在JavaScript高级程序设计(第3版)中这样描述:闭包是指有权访问另一个函数作用域中的变量的函数,这样说起来则inner是闭包,其实不管谁是闭包,道理是一样的。
    我们使用JavaScript高级程序设计的说法
  1. 闭包是一个函数
  2. 通过闭包函数,可以在函数的外部访问到函数内部的局部变量

函数outer中返回了一个内联函数inner,并且在inner中引用了outer中的变量。inner就成为了闭包函数。可以说闭包是将函数内部和外部连接起来的一座桥梁。

递归函数

在函数的内部可以调用函数,如果在函数的内部调用其自身,那么这个函数就是递归函数。递归函数用于让一个函数从其内部调用其本身。需要注意的是,如果递归函数处理不当,就会使程序陷入“死循环”。
如果运行下面这段代码,程序将陷入死循环,因为程序会不断的执行fun()函数。

function fun(){
     
  fun();
}
fun();

在定义递归函数时,需要2个必要条件:

  1. 一个结束递归的条件;
  2. 一个递归调用的语句;

下面的例子中fun函数将被调用4次,第4次调用时num<1条件成立,结束递归。

//递归计算阶乘
function fun(num){
     
  if(num<1){
     
    return 1;
  }else{
     
    return fun(num-1)*num;
  }
}
console.log(fun(3));  //6

Javascript函数进阶-执行顺序和执行环境_第16张图片

你可能感兴趣的:(前端进阶之路,Javascript,执行上下文,执行环境,函数嵌套,闭包,递归函数)