Javascript一席谈之二: 函数内部探秘

该系列文章的内容主要来自: Pro JavaScript with Mootools.作者: Mark Joseph Obceca

 

本节,我们将揭开函数内部实现的帷幕一角,看一眼 js解释器遇到函数定义,函数调用时,它到底做了什么工作。我们不会深入到解释器的实现技术细节,我们主要关注那些帮助我们理解函数的定义和调用的内容。

 

注意:各家解释器的实现各不相同,但是ECMAScript规范描述了解释器实现函数的一般规则。我们根据ECMAScript的官方指导去深入函数内部,小探一把。

 

可执行代码 和 执行环境 (contexts:有翻译成环境的,有翻译成上下文的,我就把它翻成环境吧,可少敲一个字)

 

Javascript 把可执行代码分成了三类:

  • Global 代码: 在源程序中的顶级代码
  • Function代码:函数内部的代码
  • Eval 代码:我们传递给javascript eval函数执行的代码

说起来比较抽象,还是上例子吧:

//this is global code
var name='John';
var age =20;

function add(a,b){
    //this is function code
    var result=a+b;
    return result;
}

(function(){
   //this is function code
    var day='Tuesday';
    var time = function(){
        //this is also function code ,
        //but it is separate from the code  above
        return day;
    };
})();

//this is eval code
eval("alert('yay!');");
 

变量 name,age,和我们生成的大部分函数,都出现在顶级,也就是说 它们是Global代码。然而函数内部的代码,我们称之为Function代码。

 

当一个函数定义在其他函数的内部时,该函数的代码也会被看作一段独立的Function代码。

 

为什么Javascript把可执行代码分成不同的类型呢? 答案就是:解释器为了在解释代码时,能够保持跟踪当前解释执行到了代码的什么位置。具体的说就是:js解释器使用一种叫做: 执行上下文的内部机制来对代码的解释执行进行跟踪。

 

在执行一段脚本的过程中,js会生成和进入多个不同的执行环境,执行环境不仅跟踪代码的位置,同时也会保存当前的数据变量以保证代码的正确执行。


一个JS程序,至少有一个执行环境,一般称之为: global执行环境。当js解释器开始执行你编写的程序,解释器就进入了global执行环境,并且开始使用当前执行环境解释执行代码。当js解释器遇到函数调用,解释器就生成一个新的执行环境,然后进入该执行环境,并且使用这个环境解释执行函数代码。当函数执行结束或返回一个值,js解释器退出这个执行环境,返回上一个环境。

 

说起来有点乱,还是用示例代码会清楚些:

var a =1;

var add = function(a,b){
    return a+b;
};

var callAdd = function(a,b){
    return add(a,b);
};

add(a,2);
callAdd(1,2);

 让我们从js解释器的视角一步步的去执行上面的代码:

 

  1. 程序开始,解释器进入global执行环境开始执行当前环境的代码。解释器生成变量: a, add,callAdd,分别定义它们的值为:数子1, 一个函数,然后另一个函数。
  2. 解释器遇到一个add函数的调用。解释器生成一个新的执行环境,进入该环境。执行表达式a+b,然后返回这个表达式的值。返回值后,解释器离开这个新生成的执行环境,丢弃该环境,返回global执行环境。
  3. 解释器然后遇到一个callAdd函数的调用,和第二步一样,解释器在执行callAdd的函数体前,生成一个新的执行环境,并且进入该环境。当解释器执行callAdd时,解释器又遇到一个add函数调用,和其他的函数调用一样,js解释器再生成一个新的执行环境,进入该环境,到现在为止,我们有三个执行环境:global执行环境,callAdd执行环境,add执行环境。add执行环境是当前活动的执行环境。当add函数调用结束,add的执行环境被丢弃,返回callAdd的执行环境,然后callAdd调用结束,同样丢弃callAdd执行环境,返回global执行环境。

JS解释器中的几个内置的对象和执行环境联系紧密,直接影响脚本程序的执行。

 

变量和变量初始化

和执行环境联系紧密的第一个内置对象就是 variable object。 每个执行环境都有自己的variable object。该对象用来跟踪该执行环境下定义的全部变量。


js中生成变量的过程叫做变量初始化。应为js是一种文法作用域的语言。因此,变量的作用域根据变量在代码中初始化的位置确定。这个规则唯一的例外就是:由省略了var关键字定义的全局变量。

 

var friut = 'banana';

var add = function(a,b){
    var localResult  = a+b;
    globalResult = localResult;
    return localResult;
};

add(1,2);

在上面的代码片段中,变量 friut和 add是全局作用域的。可以在整个脚本中使用。localResult和a,b都是局部作用域的。仅能在add函数内使用。而globalResult尽管定义在add函数中,但是省略了var 关键字,就成了全局作用域的变量了。


当js解释器进入一个执行环境时,它做的第一件事就是变量初始化。 解释器先生成一个variable object, 然后检查当前环境下的var 声明。接着这些变量被生成,添加到variable object的属性中,赋值为: undefined。对我们上面的例子代码而言,我们可以说: 变量friut 和add由global环境的variable object初始化。而变量a,b, localResult则由add函数的本地执行环境的variable object初始化。而变量 globalResult比较诡异,我们稍候讨论它的实现。


关于变量初始化,要牢记一点:它和执行环境休戚相关。若你还有印象的话,javascript把可执行代码分成了三类: global代码,function代码和eval代码。我们可以说: js 提供了三种执行环境: global 执行环境,function执行 环境和eval执行环境。


由于变量的初始化和执行环境的variable object有关,因此js中,我们有三种类型的变量: global 变量,function本地变量,和eval代码的变量。这引出了js中另外一个让很多人困惑的地方:js没有块左右域。在其他的类似于C的语言里,包含在一对括号里的代码,被称做 块,块有着自己独立的左右域。 而js中的是没有块左右域的,解释器进入新的执行环境,在当前执行环境定义的任何变量都会被初始化。不管是否在块中。


举例如下:

 

var x=1;

if(false){
    var y=2;
}

console.log(x);//1
console.log(y);//undefined
 

在一个有块作用域的语言中,console.log(y)一行的执行会报错,因为你试图访问一个没初始化的变量(因为 var y=2永远不会被执行)。但是js却并没报错,而是告诉我们y的值是undefined, undefined也就是一个变量被初始化了,但是没给值。js解释器的这种行为有点独特,是吧?


作用域链和闭包


可执行代码和执行环境是一一绑定的,从js解释器的角度看:


  • Global 代码-->Global执行环境
  • Funcition代码-->Function执行环境
  • Eval代码-->Eval执行环境

解释器每进入一种代码,就会生成一个当前代码的执行环境。每个执行环境都有自己的variable object属性去跟踪当前环境中定义的全部变量,在global执行环境中,variable object 又被称作 window 或 global对象, 当生成执行环境后,解释器,对当前环境定义的变量进行解析,把当前执行环境定义的变量放入variable object中,作为它的属性。当从一个执行环境,进入另外一个执行环境时,就发生了执行环境的嵌套,而执行环境用另外一个内置的属性scope chain去保存这个嵌套。


Scope chain:  global variable object<--outer variable object<--local variable object

 

执行环境对当前环境中变量的辨识就是通过scope chain进行。对于省略var 关键子定义的变量,由于不记录在local variabe object中,就顺着scope chain回溯到global中,若是还没找到,就在global中生成一个新的属性。

 

每个函数在定义时,该函数会生成一个内置的scope属性,该属性是定义该函数时,执行环境的variable object的嵌套。当调用这个函数时,解释器,生成该函数的执行环境,并用根据该函数的scope属性生成了这个执行环境的作用域链。而闭包的定义和调用一般是两个执行环境。这就是闭包产生的低层机理。


对于用new Function()产生的函数定义,该函数的scope属性里只有一个东西就是global variable object.

 

还是上示例回味吧:

var fruit = 'banana';
var animal = 'cat';
function sayFruit(){
    var fruit = 'apple';
    console.log(fruit); // 'apple'
    console.log(animal); // 'cat'
};
console.log(fruit); // 'banana'
console.log(animal); // 'cat'
sayFruit();
 
var fruit = 'banana';
function outer(){
     var fruit = 'orange';
    function inner(){
         console.log(fruit); // 'orange'
    };
    inner();
};
outer();
 
var fruit = 'banana';

function outer(){
    var fruit = 'orange';
    var inner = new Function('console.log(fruit);');
    inner(); // 'banana'
};
outer();
 
var fruit = 'banana';
var inner;
(function(){
    var fruit = 'apple';
    inner = function(){
        console.log(fruit);
    };
})();
console.log(fruit); // 'banana'
inner(); // 'apple'

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值