欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JavaScript函数表达式实例讲解

程序员文章站 2023-11-23 18:19:04
函数表达式 函数的name属性:chrome、firefox、safari和opera都支持一个函数的非标准属性name,通过这个属性可以访问到函数指定的名字,name属性值永远...

函数表达式

函数的name属性:chrome、firefox、safari和opera都支持一个函数的非标准属性name,通过这个属性可以访问到函数指定的名字,name属性值永远等于function关键字后的函数标识符。

函数声明提升:可以先用函数再声明函数,类似于java的动态绑定(后期绑定)。

匿名函数(anonymous function,也叫拉姆达函数):即通过函数表达式定义的函数,function关键字后面不跟标识符,匿名函数的name属性是空字符串。

7.1 递归

函数的递归:一个函数在执行过程中调用自身。

在函数内部使用递归时,如果是通过函数名调用,一般不会出问题,但是遇到了如下就会出错了:

 function factorial(num) {

   if (num \<=1) {
   return 1;

   } else {

   return num \* factorial(num - 1);

   }

  }

   var anotherfactorial = factorial;

   factorial = null;

   //调用

   anotherfactorial(4); //出错

在函数执行之前将函数的标识符置为null,导致函数内部再也找不到这个函数,firefox报错:typeerror:factorial is not a function。

正确的做法应该是在使用递归时,在函数的内部使用arguments.callee代替函数名,这样就不会出错了。

7.2 闭包

首先要明确区分匿名函数和闭包是不同的概念。

关于匿名函数的概念上面讲过了,ctrl+f搜索匿名函数就能找得到。

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数的内部创建另一个函数。

要理解闭包,还要理解作用域链的概念,作用域链的概念在第4章。

变量对象:后台的每个执行环境都有一个表示变量的对象——变量对象。

scope属性:保存了外部环境中的作用域链。

作用域链的本质:一个指向变量对象的指针列表,它只引用但不包含变量对象。

一般说来,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保留全局作用域(全局执行环境的变量对象)。但是,闭包并非如此,在闭包还未销毁之前,其所在的外部函数在执行完毕后,这个外部函数的执行环境的作用域链会被销毁,但是其活动对象依然保留在内存中;直到闭包销毁后,闭包的外部函数的活动对象才会被销毁。示例:

   function createcomparisonfunction(propertyname) {

   return function (object1, object2) {

   var value1 = object1[propertyname]; //访问外部函数的变量

   var value2 = object2[propertyname]; //访问外部函数的变量

   if (value1 < value2) {

   return -1;

   } else if (value1 > value2) {

   return 1;

   } else {

   return 0;

   }

   }

   }

   var comparenames = createcomparisonfunction("name");

   var result = comparenames({name:"ethan"},{name:"greg"});

   alert(result); //-1

   comparenames = null; //销毁闭包,同时外部函数的活动对象也被销毁

由于闭包会携带包含它的函数作用域,因此会占用比其他函数更多的内存。过度使用闭包会导致内存占用过多,除非绝对必要,不要使用闭包。

闭包与变量

作用域链的配置机制引出了一个副作用:闭包只能取得包含它的函数中任何变量的最后一个值(外部函数运行期间无法获取到)。闭包保存的是整个变量对象而非某个特殊的变量。

关于立即执行函数

关于this对象

在闭包中使用this对象可能会导致一些问题。this对象是在运行时基于函数的执行环境绑定的;在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常是指window(还有一种情况就是使用call或apply改变了函数执行环境)。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。示例:

   var name = "this is my name";

   var object = {

   name: "ethan",

   getnamefunc: function () {

   return function () {

   return this.name;

   };

   }

   };

   alert(object.getnamefunc()()); //this is my name

注意:alert里面的函数有两个括号,第一个括号执行getnamefunc()方法,返回了一个匿名函数,如果没有第二个括号警告框将打印出匿名函数的内容(即getnamefunc()方法中return之后的内容);如果有第二个括号,说明执行了匿名函数,这是匿名函数是在全局环境下执行的,因此会在全局中查找名为name的属性并返回,在警告框中打印出来。

如何才能访问到外部函数中的活动对象?

在外部函数中需要定义一个变量保存,而不是使用this(arguments也是如此,如果想在闭包中访问外部函数中的arguments而不是另外一个运行环境中的arguments,就把定义时外部函数的arguments引用保存到另一个变量中,在闭包中使用这个变量访问包含闭包的外部函数的arguments)。如下:

var name = "this is my name";

   var object = {

   name: "ethan",

   getnamefunc: function () {

   that = this;

   return function () {

   return that.name;

   };

   }

   };


   alert(object.getnamefunc()()); //ethan

内存泄漏

由于ie9之前的版本对jscript对象和com对象使用了不同的垃圾回收例程,因此闭包在ie的一些低版本中就存在一些问题。具体来说,如果闭包的作用域链中保存着一个html元素,那么就意味着该元素无法被销毁。示例:

  function assignelement() {

   var element = document.getelementbyid("someelement");

   element.onclick = function () {

   alert(element.id);

   };

   }

代码创建了一个作为element元素点击事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个assignhandler()的活动对象的引用,因此就会导致永远无法减少element的引用数,只要element存在,那么element的引用数最少也是1,因此element占用的内存就永远不会被回收。解决的方法就是用另一个变量保存闭包中引用的值(这样做可以消除循环引用):

   function assignelement() {

   var element = document.getelementbyid("someelement");

   var id = element.id;

   element.onclick = function () {

   alert(id);

   };

   element = null;

   }

这样闭包的外部函数就可以将element的值置为null以便回收内存,闭包保存的外部函数的

活动对象element的值为null,最终会被回收。

但是仅仅做到这一步,还是无法解决内存泄漏的问题。

7.3 模仿块级作用域

javascript没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是包含在函数中而非语句中创建的。示例:

  function outputnumbers(count) {

   for (var i = 0;i\

如果你像下面这样对i重新声明:

  function outputnumbers(count) {

   for (var i = 0; i \< count; i++) {

   alert(i);

   }

   var i;

   alert("i = " + i);

   }



   outputnumbers(3); //0,1,2,i = 3
;i++)>

结果依然是和没有重新声明前一样,原因就是javascript不会告诉你你是否多次声明了某个变量,如果你多次声明,后续的声明会被javascript引擎视而不见(仅仅是忽略声明,如果执行了初始化或者赋值操作,结果就会发生改变)。

匿名函数可以模仿块级作用域并避免这个问题,块级作用域(也叫做私有作用域),使用匿名函数模仿块级作用域的语法如下所示:

  (function () {

   //模仿私有作用域

   })();

定义并立即调用了一个匿名函数,把函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,而紧随其后的一对圆括号表示立即调用这个函数。如果不好理解,可以看下面的另一种表示方式:

   var somefunction = function () {

   //模仿私有作用域

   };

   somefunction();

首先通过函数表达式定义了一个匿名函数,然后紧接着立即调用这个函数。

将函数声明转换为函数表达式的方法:

   (function () {

   //模仿私有作用域

   })();

即在函数声明的外面加一层圆括号。

如果没有圆括号,javascript引擎会认为这是一个函数声明,但函数声明function后不能跟圆括号而要跟函数标识符,因此不加圆括号一定会报错。

无论在什么地方,只要需要一些临时变量,就可以使用私有作用域(块级作用域):

示例:

  function outputnumbers(count) {

   (function () {

   for (var i = 0; i \< count; i++) {

   alert(i);

   }

   })();

   alert("i = " + i); //错误!

   }

   outputnumbers(3);

firefox调试:referenceerror: i is not defined

这种技术经常被用在全局作用域,函数的外部,从而限制向全局作用域中添加过多的变量和函数,一般说来,应该尽量少向全局作用域中添加变量和函数。在一个大型应用程序中,过多的全局变量和函数容易导致命名冲突,而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。

这种方法可以减少闭包占用的内存问题,因为没有指向这些匿名函数(私有作用域)的引用,只要函数执行完毕,就可以立即销毁其作用域链了。

7.4 私有变量

严格来讲,javascript中没有私有成员的概念,所有的对象属性都是公有的,但是也有一个私有变量的概念:任何函数中定义的变量,都可以认为是私有变量,因为在函数外部无法访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。

如果在函数内部定义一个闭包,那么闭包通过自己的作用域链也可以访问到这些私有变量,而利用这一点就可以创建访问私有变量的公共方法。我们把有权访问私有变量和私有函数的方法称之为特权方法(privileged method),有两种在对象上创建特权方法的方式,第一种是在构造方法中创建特权方法:

   function myobject() {

   //定义私有变量

   var privatevariable = 10;

   //定义私有方法

   function privatefunction() {

   return false;

   }

   //特权方法

   this.publicmethod = function () {

   privatevariable++;

   return privatefunction();

   }

   }

除了使用创建的特权方法能够访问到函数内定义的私有变量和私有方法,没有别的途径可以访问得到。

除此之外,利用私有和特权成员,还可以隐藏那些不应该被直接修改的数据:

   function person(name) {

   this.getname = function () {

   return name;

   }

   this.setname = function (value) {

   name = value;

   }

   }



   var p = new person("ethan");

   alert(p.getname()); //ethan

   p.setname("greg");

   alert(p.getname()); //greg

但是缺点也很明显,将特权方法放在了构造函数中,意味着每创建一个实例,特权方法就重新构建一次,成为对象的实例方法,为了解决这个问题,就引出了静态私有变量来实现特权方法。

静态私有变量:指的是在私有作用域内定义私有变量和函数,同样也可以创建特权方法。

   (function () {

   //定义私有变量

   var privatevariable = 10;

   //定义私有方法

   function privatefunction() {

   return false;

   }

   //构造函数

   myobject = function () {



   };

   //公有/特权方法

   myobject.prototype.publicmethod = function () {

   privatevariable++;

   return privatefunction();

   }

   })();

在上面的代码中,可以看到定义构造函数时没有使用var关键字,在非严格模式下,使用未经声明的变量是可以的,但是这个变量将是全局变量,这正是我们想要的,如果使用var关键字(使用var关键字函数表达式)或者函数声明,那么这个构造函数就变成了一个局部函数,我们希望能够在全局环境下使用这个构造函数,为了使其变为全局构造函数,能够在外部访问并用其创建对象实例,没有使用var关键字(初始化未经声明的变量,总会创建一个全局变量)。不仅如此,还将要复用的方法写到了构造函数的原型中,这里体现了典型的原型模式。

构造函数中传入的参数可以当做实例属性处理。

另一个例子:

  (function () {

   var name = "";

   //构造函数

   person = function (value) {

   name = value;

   };

   //特权方法

   person.prototype.getname = function () {

   return name;

   };

   person.prototype.setname = function (v) {

   name = v;

   };

   })();

   var p1 = new person("ethan");

   alert(p1.getname()); //ethan

   p1.setname("gg");

   alert(p1.getname()); //gg



   var p2 = new person("greg");

   alert(p1.getname()); //greg

   alert(p2.getname()); //greg

多查找作用域链中的一个层次,就会在一定程度上影响查找速度,而这正是使用闭包和私有变量的不足之处。

模块模式:前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的是只有一个实例的对象。按照惯例,javascript使用对象字面量的方式来创建单例对象。示例:

   var singleton = {

   name: value,

   method : function () {

   //方法的代码

   }

   };

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,语法形式如下:

   var singleton = function () {

   //私有变量

   var privatevariable = 10;

   //私有方法

   function privatefunction() {

   return false;

   }

   //特权方法和特权变量

   return {

   publicproperty : true,

   publicmethod : function () {

   privatevariable++;

   return privatefunction();

   }

   };

   }();

这个模块模式使用了一个返回对象字面量的匿名函数。在匿名函数内部首先定义了私有变量和函数,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此他的公有方法有权访问私有变量和函数,而且,在这一匿名函数的最后加上了一对圆括号,令其立即执行,即初始化该函数后立即执行。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。

如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是object的实例,因为最终要通过一个对象字面量来表示它。

增强的模块模式

在返回一个对象字面量之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须要添加某些属性和(或)方法对其加以增强的情况。示例:

  var singleton = function () {

   //私有变量

   var privatevariable = 10;



   //私有方法

   function privatefunction() {

   return false;

   }



   //新建一个指定类型的实例

   var clone = new customtype();

   //特权方法和特权变量

   clone.publicproperty = true;

   clone.publicmethod = function () {

   privatevariable++;

   return privatefunction();

   };

   return clone;

   }();

注意:不要忘了初始化该匿名函数时要立即执行!

小结:

在后台的执行环境中,闭包包含它自己的作用域,包含函数的作用域以及全局作用域。

通常,函数的作用域会在函数执行完毕后被销毁。但是,当函数中存在一个闭包时,这个函数的作用域将一直存在于内存中,直到闭包不存在为止。

使用闭包可以在javascript中模仿块级作用域(javascript中没有真正意义上的块级作用域),步骤如下:

创建并立即调用一个函数,这样既可以立即执行函数里的代码,又不会在内存中留下该函数的引用。

结果就是函数内部的所有变量都会被立即销毁,除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

闭包还可以用于在对象中创建私有变量,相关概念及要点如下:

即使javascript中没有私有变量的概念,但是可以通过闭包来实现公有方法,而公有方法可以访问到包含作用域(即外部作用域)的变量(可以理解为私有变量)。

有权访问私有变量的公共方法称之为特权方法。

可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

过度使用闭包会导致占用大量内存。