在《初步了解浏览器》中,我们介绍了浏览器的组成部分,javascript解释器就是其中的一个组成部分:
interpreter有翻译者、翻译器的意思,在 javascrip中一般叫解释器、解析器、引擎。从中文的字面意思来理解,前端程序猿写了一大堆莫名其妙的代码,只有放到浏览器中,在js解释器翻译之后才具有一定的意义,解释器按照程序猿的规定去完成一个个任务,所有任务可以分成两种,同步任务(synchronous)和异步任务(asynchronous),这一节我们只讨论同步任务。
javascript代码本质上是一段文本,在浏览器宿主环境中需要被script标签包裹才能被正确的执行,这很好理解。JS引擎并不是编译器,不会将代码编译成机器码,JS引擎本身也是一种程序(可能是C、C++、Java编写的程序),它通过算法能识别javascript代码并执行,简单的说JS引擎其实就是能读懂javascript程序的程序。
以谷歌 V8 引擎为例,v8引擎包括两个主要组件:
关于Javascript执行我们要牢记,浏览器是多线程的,而javascript是单线程的。
为什么我要说原则上js解释器从上到下顺序执行代码,这是因为JS解释器执行代码不是按照书写代码的顺序执行的,而是有自己的执行规则,见下图:
js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:
分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误,停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。
例1:通过下面的代码我们发现语法错误b();只阻塞了代码块1的执行
例2:看这段代码,会报错吗?
var a=1;
function b(){
console.log(c);
}
console.log(a);
执行的结果是控制台打印了1,没有报错。函数b中明明有语法错误为什么不报错呢?这是因为JS引擎不会编译代码,而是一边解析,一边执行的,JS引擎没有执行b函数所以不会报错。
我们把代码改一下:
当调用了b函数,控制台报错了。这个因为在执行b函数的时候,开始检查b函数的语法,发现在b函数作用域中没有定义c,继续在全局作用域中寻找c,还是没找到,JS引擎就会抛出错误。
通过上面的代码,我们验证了一件事,就是JS解释器不会编译代码,而是边解析边执行的,对于语法错误,如果没有被执行到就不会报错。
语法检查检查之后,进入预解析阶段,js解释器在预解析阶段会在内存中创建执行上下文(Execution Context),也叫执行环境,以后我们会称之为执行环境,执行环境是什么呢?就是正在执行的某行或者某段代码的所处的环境。
在浏览器中,一般说JS的执行环境有2种,全局环境、函数环境。
执行环境是在预解析阶段被创建、代码执行阶段被重新赋值,代码执行完毕即出栈、等待被回收,这是执行环境的生命周期。
执行环境可以抽象成一个对象表示,其中包括三个属性:变量对象、作用域链、this指向
ECObj={
vo:{
}, //变量对象
scopeChain:{
}, //作用域链
this:{
} //this指向
}
变量对象(Variable Object – 简写 VO)是一个抽象的概念,指代与执行上下文相关的特殊对象,它存储着在上下文中声明的:
JS中的变量和函数并不总是可用的,有其使用的范围,这就是作用域。JS的作用域靠函数形成,函数内声明的变量是局部变量,在函数外不可访问。作用域链决定了哪些数据能被函数访问
全局环境
JS默认的代码执行环境,是最外围的一个执行环境,在web浏览器中,全局执行环境被认为是window对象。一旦代码被载入,引擎最先进入的是这个环境,全局环境不会被自动回收,只有在关闭浏览器窗口的时候才会被销毁,所以在定义全局变量一定要格外小心。
函数环境
JS解释器在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,函数环境是一个相对于全局环境的概念,调用任何一个函数一个新的执行环境就会被创建出来,执行函数,执行结束后返回全局环境,而创建的函数环境等待垃圾回收。
就像先有宇宙再有地球一样,js代码中一定是先创建全局环境,在预解析阶段创建全局环境。
如果在全局环境中调用函数,则对函数代码进行预解析,创建该函数的函数环境,执行函数,执行结束后返回全局环境(函数环境等待回收)。
函数环境与调用栈息息相关,下面我将用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');
处于调用栈的栈顶的函数就是当前执行的函数,该函数的执行环境就是当前环境,用一个对象抽象的表示如下。
预解析阶段的变量对象,用VO表示,包括三类属性:参数、变量声明、函数声明。
作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。可以用一个数组来表示作用域链,数组的第一项总是位于执行栈栈顶函数的上下文中的变量对象local,而数组的最后一项总是全局变量对象global。
其实函数作用域链是创建函数的时候就创建了,创建的时候其中只有全局变量对象,保存在函数的[[Scope]]属性中,函数执行时的,通过复制该属性中的对象来构建作用域链。
this指向是执行上下文被创建时确定的。在一个函数环境中,this由调用者决定,怎么理解这句话呢?
函数可以作为某个对象的方法调用,也可以独立调用,作为对象的方法调用时this指向这个对象,独立调用时this指向全局对象window。但在严格模式中,this的值是undefined。
执行代码阶段,变量对象VO会被重新赋予真实的值,即预解析阶段赋予的undefined值会被覆盖,赋值后的变量对象成为活动对象,用AO表示。
此阶段才是程序真正进入执行阶段,Javascript引擎会一行一行的读取并运行代码。
PS:假如变量是定义在函数内的,而函数从头到尾都没被调用的话,则变量值永远都是undefined值。
一旦函数的执行结束(会有返回值),该函数就会从执行栈弹出,执行栈中的下一个函数(如果没有则是全局代码)会继续执行,直到执行栈为空。
函数执行完毕,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的执行环境。
函数outer中返回了一个内联函数inner,并且在inner中引用了outer中的变量。inner就成为了闭包函数。可以说闭包是将函数内部和外部连接起来的一座桥梁。
在函数的内部可以调用函数,如果在函数的内部调用其自身,那么这个函数就是递归函数。递归函数用于让一个函数从其内部调用其本身。需要注意的是,如果递归函数处理不当,就会使程序陷入“死循环”。
如果运行下面这段代码,程序将陷入死循环,因为程序会不断的执行fun()函数。
function fun(){
fun();
}
fun();
在定义递归函数时,需要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