`
shonelau
  • 浏览: 16973 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

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
4
分享到:
评论

相关推荐

    命名函数表达式探秘.pdf

    ### 命名函数表达式探秘 #### 函数表达式与函数声明 在JavaScript中,函数表达式和函数声明是两种创建函数的基本方式。两者的主要区别在于它们的语法结构和执行上下文。 - **函数声明**:这是一种较为常见的定义...

    js高端系列教程(26)——JavaScript探秘:强大的原型和原型链.docx

    在JavaScript中,原型和原型链的概念至关重要,它们是理解面向对象编程的基础之一。本文将深入探讨这些概念,并通过具体的代码示例来解释如何利用原型和原型链实现类的继承、封装等特性。 #### 原型对象的基本概念 ...

    计算机探秘基础

    ### 计算机探秘基础:黑客技能与编程学习 #### 黑客精神与技能的重要性 计算机技术作为一门不断发展的领域,对于想要深入了解其内部机制和技术的人来说,掌握一定的黑客技能是非常必要的。这里的“黑客”并非指...

    ajax hacks(ajax探秘) ajax探秘

    1. **XMLHttpRequest对象**:Ajax的基础是XMLHttpRequest(XHR)对象,它是JavaScript的一个内置对象,用于在后台与服务器进行通信。通过创建和使用XHR对象,我们可以发送HTTP请求并接收响应。 2. **异步请求**:...

    ajax探秘

    **Ajax 探秘** Ajax(Asynchronous JavaScript and XML)是一种在无需刷新整个网页的情况下,能够更新部分网页的技术。它的核心是JavaScript,通过XMLHttpRequest对象与服务器进行异步数据交互,结合DOM(Document ...

    位域探秘:C语言中的位域全解析

    1. **高级语言**:C语言是一种高级编程语言,设计用于编写系统软件和应用程序。 2. **结构化语言**:C语言支持结构化编程,允许使用条件语句、循环、函数等结构化编程概念。 3. **内存管理**:C语言提供了对内存的...

    枚举探秘:C语言中的枚举(enum)全解析

    1. **高级语言**:C语言是一种高级编程语言,设计用于编写系统软件和应用程序。 2. **结构化语言**:C语言支持结构化编程,允许使用条件语句、循环、函数等结构化编程概念。 3. **内存管理**:C语言提供了对内存的...

    16章全React18内核探秘:手写React高质量源码迈向高阶开发

    React是用于构建用户界面的JavaScript库,起源于Facebook的内部项目,该公司对市场上所有 JavaScript MVC框架都不满意,决定自行开发一套,用于架设Instagram的网站。于2013年5月开源。 2、React三大颠覆性的特点 ...

    Silverlight探秘系列课程(13):网络通信与开发示例

    【Silverlight探秘系列课程(13):网络通信与开发示例】 本课程主要探讨的是Silverlight在实现网络通信及开发示例方面的知识。Silverlight是微软推出的一款跨浏览器、跨平台的插件,主要用于增强Web应用的交互性和...

    vue源码解析.zip

    最后,"Vue源码探秘之AST抽象语法树"涉及到的是编译过程中的一个重要环节。AST是源代码的抽象表示,Vue将模板和脚本转换为AST,方便进行后续处理。通过AST,Vue可以轻松地分析和操作代码,例如提取指令、识别变量和...

    探秘ajax跨域请求

    浏览器加载并执行这段返回的JavaScript代码,相当于执行了一段客户端定义的函数,并将服务器数据作为参数传递进去。这样就实现了跨域的数据请求。 此外,还有一种解决方案是CORS(跨源资源共享)。CORS是一种新的...

    DOM事件探秘篇

    1. HTML事件处理程序:这是最基础的处理方式,直接在HTML标签内使用`on事件名`属性指定JavaScript函数,例如`onclick="myFunction()"`。这种方法简单直观,但不适合复杂的交互逻辑,且不推荐用于大型项目。 2. DOM0...

    pixelated:一个简单的小游戏,带有彩色像素

    《像素世界:JavaScript驱动的彩色像素游戏开发探秘》 在数字艺术与游戏设计领域,像素艺术因其独特的复古风格和无限的创意空间而备受青睐。"pixelated"是一款以彩色像素为主题的简单小游戏,它利用JavaScript编程...

    js实现QQ面板拖拽效果(DOM事件探秘)(全)

    在上述提供的内容中,代码首先定义了一个`getByClass`函数,该函数用于根据类名查找页面上的元素,此功能在处理动态元素或者需要对具有特定类名的元素进行批量操作时十分有用。 ```javascript function getByClass...

    Detective-Agency

    《侦探社:JavaScript技术探秘》 在信息技术的广阔领域中,JavaScript作为一门重要的脚本语言,扮演着不可或缺的角色。特别是在网页开发、前端交互、服务器端编程等方面,JavaScript以其灵活性和强大的功能,深受...

    Python开发的清华大学图书管理系统源码.zip

    《Python技术深度解析:清华大学图书管理系统源码探秘》 Python作为一门易学且功能强大的编程语言,在软件开发领域有着广泛的应用,特别是在Web开发、数据分析、人工智能等方面。本篇文章将聚焦于一个具体实例——...

    15puzzle:ML解决的15个益智游戏

    "15puzzle"是一款经典的益智游戏,它由15个可移动的数字方块和一个空格组成,玩家需要通过移动这些方块,使得它们按照1到15的顺序排列。现在,我们尝试用机器学习的方法来解决这个看似简单的15难题。在这个过程中,...

    C#探秘系列(四)——GetHashCode,ExpandoObject

    哈希函数的目标是将不同的对象映射到不同的哈希码,以实现高效查找。由于哈希冲突的存在,两个不同的对象可能会有相同的哈希码,但同一对象每次调用 `GetHashCode` 应该返回相同的结果。在示例代码中,我们看到两个...

Global site tag (gtag.js) - Google Analytics