函数表达式

函数表达式函数表达式是 javaScript 中一个既强大又容易令人困惑的特性 在 javaScript 中定义函数的方式有两种 一种是函数声明 一种是函数表达式 函数声明的语法是这样的 function functionName arg0 arg1 首先是 function 关键字 然后是函数的名字

大家好,我是讯享网,很高兴认识大家。

函数表达式是javaScript中一个既强大又容易令人困惑的特性,在javaScript中定义函数的方式有两种,一种是函数声明,一种是函数表达式。函数声明的语法是这样的。

function functionName(arg0, arg1) { 
    }

讯享网

首先是function关键字,然后是函数的名字,除过ie的主流浏览器都给函数定义了一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于跟在function关键字后面的标识符。

讯享网alert(functionName.name);//functionName

关于函数声明,他的一个重要特性就是函数声明提升,意思是在执行代码之前会读取函数声明。这就意味着可以把函数声明放在调用他的语句后面。

sayHi(); function sayHi(){ 
    alert("Hi"); }
讯享网var functionName = function (arg0, arg1, arg2) { 
    }
sayHi();//错误,函数不存在 var sayHi = function() { 
    alert("hi"); }

理解函数提升的关键就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果可能会让人意想不到

讯享网//不要这样做 if (conditon){ function sayHi(){ 
    alert("hi"); } } else { function sayHi(){ 
    alert("yo"); } }

表面上看,以上代码表示在condtion为true时,使用一个sayHi()的定义,否则就使用另外一个定义。实际上,这在ECMAScript中属于无效语法,javaScript引擎会尝试修正错误,将其转化为合理的状态,但问题是浏览器尝试修正错误的做法并不一致,大部分浏览器会返回第二个声明。ff会在conditions为true时返回第一个声明。因此使用这种声明方式很危险。不过使用函数表达式就没什么问题了。

递归

递归函数是在一个函数通过名字调用自身的情况下构成的,如下所示。

function factorial(num) { 
    if (num <= 1) { return 1; } else { return num * factorial(num-1); } }

这是个经典的递归阶乘函数,虽然这个函数表面看起来没什么错误,但是下面代码却可能会导致他出错。

讯享网var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4));//出错
function factorial(num) { 
    if (num <= 1) { return 1; } else { return num * arguments.callee(num-1); } }
讯享网var factorial = (function f(num){ 
    if (num <= 1) { return 1; } else { return num * f(num-1); } });

以上代码创建了一个名为f()的命名函数表达式,然后将他赋值给变量factorial。即使把函数赋值给了另一个变量,函数的.名字f仍然有效,所以递归调用照样能正确完成,这种方式在严格模式和非严格模式下都行得通。

闭包

有不少开发人员总是搞不清匿名函数和闭包两个概念,因此经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常用方式,就是在一个函数内部创建另一个函数,仍以前面的函数为例

function ceeateComparisonFunction(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定义的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName,即使这个内部函数被返回了,而且是在其他地方被调用了,但他仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域链中包含了createComparisonFunciton()的作用域,要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。
当某个函数被调用时,会创建一个执行环境及相应的作用域链,使用arguments和其他命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,。。。直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
作用域链本质上是一个指向变量对象的指针列表,他只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量,一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象),但是闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到他的作用域链中,因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数的createComparisonFunction()的活动对象。当ceateComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但他的活动对象仍然会留在内存中,知道匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。例如:

讯享网//创建函数 var compareNames = createComparisonFunction("name"); //调用函数 var result = compareNames({name: "Nicholas"},{name: "Greg"}); //接触对匿名函数的引用(以便释放内存) compareNames = null;

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子可以清晰的说明这个问题。

function createFunction() { 
    var result = new Array(); for(var i = 0; i < 10; i++){ result[i] = function() { 
    return i; } } return result; }

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置0的函数返回0,位置1的函数返回1,,,。但实际上,每个函数都返回10。因为每个函数的作用域链中都保存着createFunction()函数的活动对象,所以他们引用的都是同一个变量i。当createFunction()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10,但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。如下所示:

讯享网function createFunction(){ 
    var result = new Array(); for (var i = 0; i < 10; i++){ result[i] = function(num) { 
    return function() { 
    return num; } }(i); } return result; }

在重写了前面的createFunctions()函数后,每个函数就会返回各自不同的索引值,在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。这里的匿名函数哟一个参数num,也就是最终的函数要返回的值。在调用每个匿名函数时,我们传入了变量i。由于函数参数是按值传递的,所以就会把变量i的值复制给参数num,而在这个匿名函数内部,又创建并返回一个访问num的闭包,这样一来,result数组中每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。

关于this对象

在闭包中使用this对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。下面来看一个例子。

var name = "The window"; var object = { name: "Object", getNameFunc: function(){ 
    return function(){ 
    return this.name; }; } } alert(object.getNameFunc()());//"The window"(在非严格模式下)
讯享网var name = "The window"; var object = { name: "My object", getNameFunc: function(){ 
    var that = this;' return function(){ return that.name; } } } alert(object.getNameFunct()());//"My Object"
var name = "The window"; var object = { name: "My Object", getName: function(){ 
    return this.name; } }

这里的getName()方法只简单地返回this.name的值,以下是几种调用object.getName()的方式以及各自的结果。

讯享网object.getName();//My object (object.getName)();//My object (object.getName = object,getName)();//The window,在非严格模式下

第一行和第二行this的值得到了维持,因为object.getName()和(object.getName)的定义是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结构。因此这个赋值表达式的值是函数本身(等于就是剥离出来),所以this的值不能得到维持,结果就返回了“The Window”。


讯享网

内存泄漏

由于IE9之前的版本Jscipt对象和com对象使用不同的垃圾收集例程,引用计数,因此闭包在ie的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个html元素,那么就意味着该元素无法被销毁。来看下面这个例子:

 function assignHandler () { 
    var element = document.getElementById("someElemet"); element.onclick = function () { 
    alert(element.id); } }

以上代码常见了一个作为elemens元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用,由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用至少也是1,因此他所占用的内存就永远不会回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。

讯享网 function assignHandler () { 
    var elements = document.getElementById("someElement"); var id = element.id; element.onclick = function () { 
    alert(id); }; element = null; }

在上面的代码中,通过把element.id的副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄露的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不能直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把elemet变量设置为null,这样就能够解除对dom对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模拟块级作用域

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

function outputNumbers(count){ 
    for (var i = 0; i < count; i++){ alert(i); } alert(i);//计数 }

这个函数中定义了一个for循环,而变量i的初始值被设置为0.在java和c++等语言中,变量i只会在for循环的语句块中有定义,循环一旦结束,变量i就会被销毁,可是在javaScript中,变量i是定义在outputNumbers()的活动对象中,因此从它有定义开始,就可以在函数内部随处访问它,即使像下面这样错误地重新声明同一个变量,也不会改变它的值。

讯享网function outputNumber (count) { 
    for (var i = 0; i < count; i++) { alert(i); } var i ;//重新声明变量 alert(i);//计数 }

javaScript从来不会告诉你是否多次声明了同一变量,遇到这种情况,它只会对后续的声明视而不见(不过,他会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。用块级作用域(通常称为私有作用域)的匿名函数的语法如下

(function(){ //这里是块级作用域 })()

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示他实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。如果感觉不好理解,来看下面这个例子

讯享网var count = 5; outputNumbers(count);

为了让代码更简洁,我们在调用函数时用5来代替变量count,如下所示:

outputNumbers(5);

这样做之所以可行,是因为变量只不过是值的另一种表现形式,因此用实际的值替换变量没有问题。再看下面的例子:

讯享网var someFunction = function() { } someFunction();

这个例子先定义了一个函数,然后立即调用了它。定义函数的方式是创建了一个匿名函数,并把匿名函数赋值给变量someFunction,而调用函数的方式是在函数名称后面添加一对圆括号,即someFunction(),通过前面的例子,我们知道可以用实际的值来取代变量count,那在这里是不是也可以用函数的值直接取代函数名呢?然而,下面的代码却会导致错误。

function () { 
    }()//出错

这段代码会导致语法错误,是因为javaScript将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。要将函数声明转化成函数表达式,只要像下面这样给它加上一对圆括号即可。

讯享网(function () { })();

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

 function outputNumbers (count) { 
    (function () { 
    for (var i = 0; i < count; i++) { alert(i); } })(); alert(i);//导致一个错误 }
讯享网(function () { var now = new Date(); if (now.getMonth() == 0 && now.getDate() == 1) { alert("happy new year"); } })()

把上面这段代码放在全局作用域中,可以用来确定哪一天是1月1日,如果到了这一天,就会向用户显示一条祝贺新年的消息。其中的变量now现在是匿名函数中的局部变量,而我们不必在全局作用域中创建它。
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

严格来讲,javaScript中没有私有成员的概念;所有对象属性都是共有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包含函数的参数,局部变量和在函数内部定义的其他函数。看下面例子:

 function add (num1, num2) { 
    var sum = num1 + num2; return sum; } 

在这个函数内部,有3个私有变量:num1、num2和sum。在函数内部可以访问这几个变量,但在函数外部不能访问他们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的共有方法成为特权方法,有两种在对象上创建特权的方式,第一种是在构造函数中定义特权方法,基本模式如下:

讯享网 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 person = new Person("Nicholas"); alert(person.getName());//"nicholas" person.setName("Greg"); alert(person.getName());//"Greg"

以上代码的构造函数中定义了两个特权方法,私有变量name在Person的每一个实例中都不相同,因为每次调用构造函数都会重新创建这两个方法。不过,在构造函数中定义特权方法也有一个缺点,就是你必须使用构造函数模式来达到这个目的。构造函数的缺点是针对每个实例都会创建一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,基本模式如下:

讯享网 (function () { 
    //私有变量和私有函数 var privateVariable = 10; function privateFunction () { 
    return false; } //构造函数 MyObject = function () { 
    }; //共有,特权方法 MyObject.prototype.publicMethod = function () { 
    privateVariable++; return privateFunction(); } })
(function () { 
    var name = ""; Person = function (value) { 
    name = value; }; Person.prototype.getName = function () { 
    return name; }; Person.prototype.setName = function (value) { 
    name = value; } })(); var person1 = new Person("Nicholas"); alert(person1.getName());//“nicholas” person1.setName("Greg"); alert(person1.getName());//"Greg" var person2 = new Person("Michael"); alert(person1.getName());//"Michael" alert(person2.getName());//"Michael"

这个例子中的Person构造函数与setName()和getName()方法一样,都有权访问私有变量name,在这种模式下,变量name,就变成了一个静态的,由所有实例共享的属性
多查找作用域链中的一个层次,就会在一定程度上影响查找的速度,而正式使用闭包和私有变量的一个明显的不足之处。

小讯
上一篇 2025-04-06 23:29
下一篇 2025-01-05 15:53

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/64606.html