JavaScript系列----函数(Function)篇(4)

1.什么是函数?

        在W3C中函数的定义是这么说的:函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块。数组

  诚然,从这种抽象的定义中咱们得不到什么有价值的东西。下面,举例来列举出函数的几种定义方式:
浏览器

function add(num1, num2) {
  return num1 + num2;
}
var add = function (num1, num2) {
  return num1 + num2;
}//这是比较常见的两种

//下面两种比较少见
var add=new Function("num1","num2","return num1+num2");
var add=Function("num1","num2","return num1+num2");

   上面四种写法均是建立一个函数正确的语法。可是,常见的通常是前两种。由于相比于前两种,后两种存在着一些缺陷。
闭包

 

  1.  后两种比较繁琐不直观这点从上面的例子中能够看出。
  2.  后两种存在着一些致命的缺陷这种函数的建立方式,不能维持一个属于函数的做用域链,在任什么时候候下(new)Function的建立的函数,都至关于在全局做用域下建立的函 数。如下例子就能够证实这一点。
    var x = 1;
    var add = (function () { var x = 100; return function (num) { return num + x; } }()); console.log(add(0));//100
    var x = 1;
    var add = (function () { var x = 100; return new Function('num1', 'return num1+x'); }()); console.log(add(0));//1

     也就是说,后两种方式建立的函数,不能组成完整的函数做用域链(后面会讲到),也就不可能有所谓的闭包之说。函数

  3.  后两种的运行效率过低。
     首先,JS对字符串的解析算不上效率很高,而(new)Function均存在着大量的字符串。
     其次,JS的解释器,对于用function(){},这种形式的函数,都有必定形式的优化。好比下面这样

    var array = [
    ];
    for (var i = 0; i < 1000; i++) {
    array[i] = function () {
    return 'undefined';
    }
    }//第一种优化


    var array = [
    ];
    for (var i = 0; i < 1000; i++) {
    array[i] = new Function("return undefined");
    }//第二种,this

           这两种方式在运行效率上存在着很大的差距。对于,第一种只须要执行一次function(){},其余的999次都是赋值,然后一种要执行一千遍的函数建立并赋值。spa

   正是由于前面的三种缘由,才使得function(){}这种方式比较流行。指针

       另外,你可能也见过下面的这种,可是这种形式只是一种变体。rest

var add = new function () {
  return 1 + 2;
};
console.log(typeof add);//object
var result = add.constructor();/*调用时必须采用这种调用方式*/
console.log(result);//3

        这种形式,new function()建立的实质上是利用一个匿名函数建立一个对象。这个对象的一个constructor属性正好指向其构造函数,也就是这个匿名函数。因此实际上这是一种丑陋的写法。code

     到这里,咱们也就只是叙述了一下,定义函数的几种方式。经过比较,咱们知道前两种比较实用,可是即便这样,第一种和第二中的定义方式也存在着巨大的不一样。下一小节,咱们接着讲这两种方      式存在的差别。

 

2.函数声明和函数表达式

 

  函数声明:

  function 函数名称 (参数:可选){ 函数体 }

  函数表达式:

  function 函数名称(可选)(参数:可选){ 函数体 }

因此,能够看出,若是不声明函数名称,它确定是表达式,可若是声明了函数名称的话,如何判断是函数声明仍是函数表达式呢?ECMAScript是通 过上下文来区分的,若是function foo(){}是做为赋值表达式的一部分的话,那它就是一个函数表达式,若是function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。

  因此,咱们能够看出,在第一部分的前两种建立函数的方式分别为函数声明和函数表达式。

function add(num1, num2) {
  return num1 + num2;
}//函数声明 var add = function (num1, num2) {
  return num1 + num2;
}//函数表达式

 

  另外,还有一些比较容易和函数声明混淆的函数表达式。 

(function add(num1, num2) {
  return num1 + num2;
});//函数表达式
var add = function foo(num1, num2) {
  return num1 + num2; }//函数表达式

 

 

  ()在JS语言规则中是一个分组操做符,根据W3C标准分组操做符里面的会默认为是表达式。

  而下面一种则比较有意思,赋值表达式的左边是一个函数,关于这点不一样的解析器对此的处理不一样,有的认为这是函数声明,有的认为这是一个函数表达式,不一样的解析器对此的处理各不相同。

可是目前在主流浏览器上默认的是函数表达式,并且foo做为函数标识符,只在其函数内部能被识别。看下面的例题:

var add=function foo(n){
  if(n==1)return 1;
  else return n+foo(n-1);
};

console.log(add(3));//6
console.log(foo(3));//error, foo is not defined

  那么函数声明和函数表达式有什么区别呢?

   回答这些问题,就涉及到函数被调用时的状况了。

 

3.函数的执行环境和做用域   

 

  咱们都知道,函数运行时是运行在新开辟的栈里的。那么函数在运行时,代码的执行环境是什么样的呢? 

  函数被调用时发生了什么? 

function add(num1, num2) {
  var num3 = 300;
  return num1 + num2 + num3;
}
var result = add(100, 200);
console.log(result);

这一段代码在执行到第5行的时候会调用咱们声明的add函数,add函数在被调用时会作如下处理:

1.add函数形参的声明并赋值。

2.add函数内函数的声明(若函数变量与函数形参的变量同名,则函数声明会覆盖形参声明)。

3.add函数内变量的声明。----->函数按顺序执行。

下面的几个例子能够证实:

例题一:函数内的函数的声明会覆盖函数形参的声明。

function add(num1, num2) {

  console.log(typeof num1);//function
function num1() {}
}
var result = add(100, 200);

例题二:函数内变量的声明不会覆盖形参的声明和函数内函数的声明

function add(num1, num2) {
  console.log(typeof num1);//number
   var num1="23";
   
}
var result = add(100, 200);

   补充:

   所谓变量的声明都是相似 var x, y; 这种状况。而var x=1;

  实际上是 var x ; x=1;分两步执行。

  变量声明老是这样: var  x;

 

函数的执行环境:

  由上述咱们知道,函数执行的时候开辟一个新的栈,而栈内保存着函数内部声明的变量,变量的值在函数代码运行以前按照刚才所讨论的三步赋值。也就是说,当一个函数被调用时,在代码运行以前其栈中已经存在着函数运行时所需的全部的变量。这些变量加一块儿则构成函数的执行环境。

若是解释器真的把函数内部全部的声明都放在栈中的话,那么解释器在开辟栈的时候就应该能够肯定所开辟栈空间的大小。可是若是栈空间大小肯定之后,有如下几个问题就须要解决了:

  • 变量的类型在运行时被改变。此时栈空间的大小须要不断调整。
  • 每一个函数在声明的时候会维持一个属于本身的做用域链,若是做用域链上的变量所占用的空间大小改变的话,须要对整个做用域链上的栈调整。
  • 须要针对这种结构重写垃圾回收机制(标记--清理不适用了)。

改进:引进函数变量

  当函数被调用的时候,并不是将变量的声明保存在栈中,而是保存在一个对象中。而将这个对象的引用保存在栈中。而这个对象存储在堆中,具体的工做原理以下: 

function add(num1, num2) {
  var num3 = 300;
  return num1 + num2 + num3;
}
var result = add(100, 200);

 

当函数被调用时(未执行以前),解释器建立一个addReference对象:

addReference={

  num1:100;

  num2:200;

  num3:undefined;

};

addReference对象的引用被压入栈中,而对象自己则存在于堆中。

 

补充:

 函数在运行时,栈中还保存着返回值以及this指针。在函数执行完毕退出时,会清空栈,若存在对此函数变量引用的函数,则将此函数变量加入引用函数的做用域链上,不然过一段时间,若垃圾回收机制为未发现有此函数变量的引用,则将该函数变量删除。

 

改进后:

  • 栈空间大小肯定: 函数运行以前,栈所须要的空间已经能被肯定。只存在三个元素: this指针,函数变量的引用,返回值。(返回值也保存在一个变量中,由解释器管理)
  • 函数在运行时,若建立一个新的函数。则只须要将函数变量对象加入新函数的做用域链上便可。
  • 只须要根据此函数变量的引用计数是否为0就能够管理内存,而不须要重写垃圾回收机制。

 

 

4.函数的做用域链 

在第三部分咱们讨论出,函数的做用域上保存的都是函数变量。下面咱们经过这个例子来讲明这种现象。

 1 var fun;
 2 (function fun1() {
 3   var x = 1;
 4   (function fun2() {
 5     var y = 2;
 6     fun = function () {
 7       return x + y;
 8     }
 9   }());
10 }())
11 
12 var result=fun();
13 console.log(result)//3

 

根据上例,咱们来一步步分析函数执行时都发生了什么?

  1.  在全局做用域中的变量对象。
    globalReference = {
      .....//之前存在的对象 好比 Object,Math,Date之类 fun: undefined; result: undefined; }
  2. 当函数运行至第二行时,fun1的变量对象
    fun1Reference = {
      x: undefined;
    }

    //执行至第4行的时候,
    fun1Reference = {
    x: 1;
    }
    //fun1的做用域链: globalReference
  3. 当函数运行至第四行时,fun2的变量对象
    fun2Reference = {
      y: undefined;
    }
    
    //执行至第5行的时候,
    fun2Reference = {
    y:
    2;
    }
    //fun1的做用域链: globalReference--->fun1Reference
  4. 当函数运行至第6行时,fun的做用域链
    //fun的做用域链: globalReference--->fun1Reference--->fun2Reference
  5. fun2执行完毕退栈--->fun1执行完毕退栈。
  6. 当函数运行至第12行时,fun被调用。
                        globalReference = {
                                    | ....... | fun: undefined; | result: undefined; | } fun1Reference ={x:1;} | | | | fun2Reference={y:2} | | | | funReference:{};
  7. fun执行完毕,result=6;13行,输出结果。


上述,咱们已经模拟一遍函数的执行时的过程。下面咱们来介绍一下全局做用域对象。

 首先,先明确一点,全局做用域也是一个变量对象。

globalReference = {
  Object:内置的对象构造函数对象;
  Array:内置的数组构造函数对象;
  Function:内置的函数构造函数对象;
  Math:内置的Math对象;
  Date:内置的日期构造函数对象;
  .......;
  window:globalReference
}

 window对象保持这对全局对象的引用。
  补充:在控制台下执行的代码都是在eval()函数中执行的,这个函数可以使用而且能改变当前函数所在的执行环境。

 

 5.变量和属性的区别 

一、变量:能够被改变的量。只有前面加var(非函数)的才能称为变量。函数变量有本身独特的变量声明方式。   

var x = 1,y = 2;
var z = 3;
//相似上面这种才是变量

xxx=1;//这样的不是变量,下面会讲到这种形式

 

二、属性:一个对象内的变量。

var object={
  x:1,   y:2,   z:3 }; //x,y,z均为属性。

 

上面的两种都很容易区分,可是下面这种又该如何解释呢?

rest=1;//rest是属性仍是变量?

这句话通常是在函数执行时候,常常性遇到,这样写有很大的弊端。

  1. 查找较慢。rest前面没有var,则确定不是变量。那么,在执行的时候就会沿做用域链一直向上查找,直至到全局做用域中的变量对象。此时未找到,则根据规则将其做为全局做用域变量对象的属性。

  2.改变了全局做用域变量对象。通常来讲,咱们在执行代码的时候应该尽可能避免改变全局做用域对象。

   也就是说,若是咱们使用一个前面没有加var的“变量”,则在执行期间,会将该“变量”当作全局做用域变量的属性。

 

三、变量和属性的区别:

  •   属性(配置为可删除的状况下)能够经过其所在的对象被删除。

    好比:

var object={
x:1
}
delete object.x; //true
y=2;
delete window.y(或者delete y);//true 
  • 变量在声明的时候会被做为函数变量的属性。原则上也是能够被删除,可是由于咱们不能获得函数变量这个对象(window是一个特例),因此在实际操做中,也就致使不可能被删除。
  • 变量通常是针对声明时期,而属性通常针对执行时期。二者在本质上,意义就不同。 
  • 查找普通对象的属性,若是未找到不会抛出错误。可是,查找变量对象的属性,若是未找到则会抛出错误。 
    var object = {
    };
    console.log(object.z);//undefined,-----在普通变量中查找
    console.log(window.v);//undefined -----在普通变量中查找
    console.log(z);//error;           -----在做用域链上的变量对象中查找,未找到则报错。   
相关文章
相关标签/搜索