javascript中的函数,闭包简单介绍

来源:互联网 发布:淘宝怎么看几颗心 编辑:程序博客网 时间:2024/06/05 04:48

javascript函数:

学习 JavaScript 最重要的就是要理解对象和函数两个部分。最简单的函数就像下面这个这么简单:

function add(x, y) {    var total = x + y;    return total;}

这个例子包括你需要了解的关于基本函数的所有部分。一个 JavaScript 函数可以包含 0 个或多个已命名的变量。函数体中的表达式数量也没有限制。你可以声明函数自己的局部变量。return 语句在返回一个值并结束函数。如果没有使用return 语句,或者一个没有值的 return 语句,JavaScript 会返回 undefined

已命名的参数更像是一个指示而没有其他作用。如果调用函数时没有提供足够的参数,缺少的参数会被 undefined 替代。

add(); // NaN // 不能在 undefined 对象上进行加法操作

你还可以传入多于函数本身需要参数个数的参数:

add(2, 3, 4); // 5 // 将前两个值相加,4被忽略了

这看上去有点蠢。函数实际上是访问了函数体中一个名为

arguments 的内部对象,这个对象就如同一个类似于数组的对象一样,包括了所有被传入的参数。让我们重写一下上面的函数,使它可以接收任意个数的参数:

function add() {    var sum = 0;    for (var i = 0, j = arguments.length; i < j; i++) {        sum += arguments[i];    }    return sum;}add(2, 3, 4, 5); // 14

这跟直接写成 2 + 3 + 4 + 5 也没什么区别。接下来创建一个求平均数的函数:

function avg() {    var sum = 0;    for (var i = 0, j = arguments.length; i < j; i++) {        sum += arguments[i];    }    return sum / arguments.length;}avg(2, 3, 4, 5); // 3.5

这个很有用,但是却带来了新的问题。avg() 函数处理一个由逗号连接的变量串,但如果想得到一个数组的平均值该怎么办呢?可以这么修改函数:

function avgArray(arr) {    var sum = 0;    for (var i = 0, j = arr.length; i < j; i++) {        sum += arr[i];    }    return sum / arr.length;}avgArray([2, 3, 4, 5]); // 3.5

但如果能重用我们已经创建的那个函数不是更好吗?幸运的是 JavaScript 允许使用任意函数对象的 apply()方法来调用该函数,并传递给它一个包含了参数的数组。

avg.apply(null, [2, 3, 4, 5]); // 3.5

传给 apply() 的第二个参数是一个数组,它将被当作 avg() 的参数使用,至于第一个参数 null,我们将在后面讨论。这也正说明一个事实——函数也是对象。

JavaScript 允许你创建匿名函数:

var avg = function() {    var sum = 0;    for (var i = 0, j = arguments.length; i < j; i++) {        sum += arguments[i];    }    return sum / arguments.length;};

这个函数在语义上与 function avg() 相同。你可以在代码中的任何地方定义这个函数,就像写普通的表达式一样。基于这个特性,有人发明出一些有趣的技巧。与 C 中的块级作用域类似,下面这个例子隐藏了局部变量:

var a = 1;var b = 2;(function() {    var b = 3;    a += b;})();a; // 4b; // 2

JavaScript 允许以递归方式调用函数。递归在处理树形结构(比如浏览器 DOM)时非常有用。

function countChars(elm) {    if (elm.nodeType == 3) { // 文本节点        return elm.nodeValue.length;    }    var count = 0;    for (var i = 0, child; child = elm.childNodes[i]; i++) {        count += countChars(child);    }    return count;}

这里需要说明一个潜在问题——既然匿名函数没有名字,那该怎么递归调用它呢?在这一点上,JavaScript 允许你命名这个函数表达式。你可以命名立即调用的函数表达式(IIFES——Immediately Invoked Function Expressions),如下所示:

var charsInBody = (function counter(elm) {    if (elm.nodeType == 3) { // 文本节点        return elm.nodeValue.length;    }    var count = 0;    for (var i = 0, child; child = elm.childNodes[i]; i++) {        count += counter(child);    }    return count;})(document.body);

如上所提供的函数表达式的名称的作用域仅仅是该函数自身。这允许引擎去做更多的优化,并且这种实现更可读、友好。该名称也显示在调试器和一些堆栈跟踪中,节省了调试时的时间。

需要注意的是 JavaScript 函数是它们本身的对象——就和 JavaScript 其他一切一样——你可以给它们添加属性或者更改它们的属性,这与前面的对象部分一样。

自定义对象

在经典的面向对象语言中,对象是指数据和在这些数据上进行的操作的集合。与 C++ 和 Java 不同,JavaScript 是一种基于原型的编程语言,并没有 class 语句,而是把函数用作类。那么让我们来定义一个人名对象,这个对象包括人的姓和名两个域(field)。名字的表示有两种方法:“名 姓(First Last)”或“姓, 名(Last, First)”。使用我们前面讨论过的函数和对象概念,可以像这样完成定义:

function makePerson(first, last) {    return {        first: first,        last: last    }}function personFullName(person) {    return person.first + ' ' + person.last;}function personFullNameReversed(person) {    return person.last + ', ' + person.first}s = makePerson("Simon", "Willison");personFullName(s); // Simon WillisonpersonFullNameReversed(s); // Willison, Simon

上面的写法虽然可以满足要求,但是看起来很麻烦,因为需要在全局命名空间中写很多函数。既然函数本身就是对象,如果需要使一个函数隶属于一个对象,那么不难得到:

function makePerson(first, last) {    return {        first: first,        last: last,        fullName: function() {            return this.first + ' ' + this.last;        },        fullNameReversed: function() {            return this.last + ', ' + this.first;        }    }}s = makePerson("Simon", "Willison");s.fullName(); // Simon Willisons.fullNameReversed(); // Willison, Simon

上面的代码里有一些我们之前没有见过的东西:关键字 this。当使用在函数中时,this 指代当前的对象,也就是调用了函数的对象。如果在一个对象上使用 点或者方括号来访问属性或方法,这个对象就成了this。如果并没有使用“点”运算符调用某个对象,那么 this 将指向全局对象(global object)。这是一个经常出错的地方。例如:

s = makePerson("Simon", "Willison");var fullName = s.fullName;fullName(); // undefined undefined

当我们调用 fullName() 时,this 实际上是指向全局对象的,并没有名为 firstlast 的全局变量,所以它们两个的返回值都会是 undefined

下面使用关键字 this 改进已有的 makePerson函数:

function Person(first, last) {    this.first = first;    this.last = last;    this.fullName = function() {        return this.first + ' ' + this.last;    }    this.fullNameReversed = function() {        return this.last + ', ' + this.first;    }}var s = new Person("Simon", "Willison");

我们引入了另外一个关键字 new,它和this 密切相关。它的作用是创建一个崭新的空对象,然后使用指向那个对象的 this 调用特定的函数。注意,含有this 的特定函数不会返回任何值,只会修改 this 对象本身。new 关键字将生成的this 对象返回给调用方,而被 new 调用的函数成为构造函数。习惯的做法是将这些函数的首字母大写,这样用 new 调用他们的时候就容易识别了。

不过这个改进的函数还是和上一个例子一样,单独调用fullName() 时会产生相同的问题。

我们的 Person 对象现在已经相当完善了,但还有一些不太好的地方。每次我们创建一个 Person 对象的时候,我们都在其中创建了两个新的函数对象——如果这个代码可以共享不是更好吗?

function personFullName() {    return this.first + ' ' + this.last;}function personFullNameReversed() {    return this.last + ', ' + this.first;}function Person(first, last) {    this.first = first;    this.last = last;    this.fullName = personFullName;    this.fullNameReversed = personFullNameReversed;}

这种写法的好处是,我们只需要创建一次方法函数,在构造函数中引用它们。那是否还有更好的方法呢?答案是肯定的。

function Person(first, last) {    this.first = first;    this.last = last;}Person.prototype.fullName = function() {    return this.first + ' ' + this.last;}Person.prototype.fullNameReversed = function() {    return this.last + ', ' + this.first;}

Person.prototype 是一个可以被Person的所有实例共享的对象。它是一个名叫原型链(prototype chain)的查询链的一部分:当你试图访问一个Person 没有定义的属性时,解释器会首先检查这个 Person.prototype 来判断是否存在这样一个属性。所以,任何分配给Person.prototype 的东西对通过 this 对象构造的实例都是可用的。

这个特性功能十分强大,JavaScript 允许你在程序中的任何时候修改原型(prototype)中的一些东西,也就是说你可以在运行时(runtime)给已存在的对象添加额外的方法:

s = new Person("Simon", "Willison");s.firstNameCaps();  // TypeError on line 1: s.firstNameCaps is not a functionPerson.prototype.firstNameCaps = function() {    return this.first.toUpperCase()}s.firstNameCaps(); // SIMON

有趣的是,你还可以给 JavaScript 的内置函数原型(prototype)添加东西。让我们给 String 添加一个方法用来返回逆序的字符串:

var s = "Simon";s.reversed(); // TypeError on line 1: s.reversed is not a functionString.prototype.reversed = function() {    var r = "";    for (var i = this.length - 1; i >= 0; i--) {        r += this[i];    }    return r;}s.reversed(); // nomiS

定义新方法也可以在字符串字面量上用(string literal)。

"This can now be reversed".reversed(); // desrever eb won nac sihT

正如我前面提到的,原型组成链的一部分。那条链的根节点是 Object.prototype,它包括 toString() 方法——将对象转换成字符串时调用的方法。这对于调试我们的Person 对象很有用:

var s = new Person("Simon", "Willison");s; // [object Object]Person.prototype.toString = function() {    return '<Person: ' + this.fullName() + '>';}s.toString(); // <Person: Simon Willison>

你是否还记得之前我们说的 avg.apply() 中的第一个参数 null?现在我们可以回头看看这个东西了。apply() 的第一个参数应该是一个被当作this 来看待的对象。下面是一个 new 方法的简单实现:

function trivialNew(constructor, ...args) {    var o = {}; // 创建一个对象    constructor.apply(o, args);    return o;}

这并不是 new 的完整实现,因为它没有创建原型(prototype)链。想举例说明 new 的实现有些困难,因为你不会经常用到这个,但是适当了解一下还是很有用的。在这一小段代码里,...args(包括省略号)叫作 剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。

因此调用

var bill = trivialNew(Person, "William", "Orange");

可认为和调用如下语句是等效的

var bill = new Person("William", "Orange");

apply() 有一个姐妹函数,名叫 call,它也可以允许你设置this,但它带有一个扩展的参数列表而不是一个数组。

function lastNameCaps() {    return this.last.toUpperCase();}var s = new Person("Simon", "Willison");lastNameCaps.call(s);// 和以下方式等价s.lastNameCaps = lastNameCaps;s.lastNameCaps();

内部函数

JavaScript 允许在一个函数内部定义函数,这一点我们在之前的 makePerson() 例子中也见过。关于 JavaScript 中的嵌套函数,一个很重要的细节是它们可以访问父函数作用域中的变量:

function betterExampleNeeded() {    var a = 1;    function oneMoreThanA() {        return a + 1;    }    return oneMoreThanA();}

如果某个函数依赖于其他的一两个函数,而这一两个函数对你其余的代码没有用处,你可以将它们嵌套在会被调用的那个函数内部,这样做可以减少全局作用域下的函数的数量,这有利于编写易于维护的代码。

这也是一个减少使用全局变量的好方法。当编写复杂代码时,程序员往往试图使用全局变量,将值共享给多个函数,但这样做会使代码很难维护。内部函数可以共享父函数的变量,所以你可以使用这个特性把一些函数捆绑在一起,这样可以有效地防止“污染”你的全局命名空间——你可以称它为“局部全局(local global)”。虽然这种方法应该谨慎使用,但它确实很有用,应该掌握。

闭包

下面我们将看到的是 JavaScript 中必须提到的功能最强大的抽象概念之一:闭包。但它可能也会带来一些潜在的困惑。那它究竟是做什么的呢?

function makeAdder(a) {    return function(b) {        return a + b;    }}var x = makeAdder(5);var y = makeAdder(20);x(6); // ?y(7); // ?

makeAdder 这个名字本身应该能说明函数是用来做什么的:它创建了一个新的 adder 函数,这个函数自身带有一个参数,它被调用的时候这个参数会被加在外层函数传进来的参数上。

这里发生的事情和前面介绍过的内嵌函数十分相似:一个函数被定义在了另外一个函数的内部,内部函数可以访问外部函数的变量。唯一的不同是,外部函数被返回了,那么常识告诉我们局部变量“应该”不再存在。但是它们却仍然存在——否则adder 函数将不能工作。也就是说,这里存在 makeAdder 的局部变量的两个不同的“副本”——一个是 a 等于5,另一个是 a 等于20。那些函数的运行结果就如下所示:

x(6); // 返回 11y(7); // 返回 27

下面来说说到底发生了什么。每当 JavaScript 执行一个函数时,都会创建一个作用域对象(scope object),用来保存在这个函数中创建的局部变量。它和被传入函数的变量一起被初始化。这与那些保存的所有全局变量和函数的全局对象(global object)类似,但仍有一些很重要的区别,第一,每次函数被执行的时候,就会创建一个新的,特定的作用域对象;第二,与全局对象(在浏览器里面是当做window 对象来访问的)不同的是,你不能从 JavaScript 代码中直接访问作用域对象,也没有可以遍历当前的作用域对象里面属性的方法。

所以当调用 makeAdder 时,解释器创建了一个作用域对象,它带有一个属性:a,这个属性被当作参数传入makeAdder 函数。然后 makeAdder 返回一个新创建的函数。通常 JavaScript 的垃圾回收器会在这时回收makeAdder 创建的作用域对象,但是返回的函数却保留一个指向那个作用域对象的引用。结果是这个作用域对象不会被垃圾回收器回收,直到指向makeAdder 返回的那个函数对象的引用计数为零。

作用域对象组成了一个名为作用域链(scope chain)的链。它类似于原形(prototype)链一样,被 JavaScript 的对象系统使用。

一个闭包就是一个函数和被创建的函数中的作用域对象的组合。

闭包允许你保存状态——所以它们通常可以代替对象来使用。

内存泄露

使用闭包的一个坏处是,在 IE 浏览器中它会很容易导致内存泄露。JavaScript 是一种具有垃圾回收机制的语言——对象在被创建的时候分配内存,然后当指向这个对象的引用计数为零时,浏览器会回收内存。宿主环境提供的对象都是按照这种方法被处理的。

浏览器主机需要处理大量的对象来描绘一个正在被展现的 HTML 页面——DOM 对象。浏览器负责管理它们的内存分配和回收。

IE 浏览器有自己的一套垃圾回收机制,这套机制与 JavaScript 提供的垃圾回收机制进行交互时,可能会发生内存泄露。

在 IE 中,每当在一个 JavaScript 对象和一个本地对象之间形成循环引用时,就会发生内存泄露。如下所示:

function leakMemory() {    var el = document.getElementById('el');    var o = { 'el': el };    el.o = o;}

这段代码的循环引用会导致内存泄露:IE 不会释放被 elo 使用的内存,直到浏览器被彻底关闭并重启后。

这个例子往往无法引起人们的重视:一般只会在长时间运行的应用程序中,或者因为巨大的数据量和循环中导致内存泄露发生时,内存泄露才会引起注意。

不过一般也很少发生如此明显的内存泄露现象——通常泄露的数据结构有多层的引用(references),往往掩盖了循环引用的情况。

闭包很容易发生无意识的内存泄露。如下所示:

function addHandler() {    var el = document.getElementById('el');    el.onclick = function() {        el.style.backgroundColor = 'red';    }}

这段代码创建了一个元素,当它被点击的时候变红,但同时它也会发生内存泄露。为什么?因为对 el 的引用不小心被放在一个匿名内部函数中。这就在 JavaScript 对象(这个内部函数)和本地对象之间(el)创建了一个循环引用。

这个问题有很多种解决方法,最简单的一种是不要使用 el 变量:

function addHandler(){    document.getElementById('el').onclick = function(){        this.style.backgroundColor = 'red';    };}

有趣的是,有一种窍门解决因闭包而引入的循环引用,是添加另外一个闭包:

function addHandler() {    var clickHandler = function() {        this.style.backgroundColor = 'red';    };    (function() {        var el = document.getElementById('el');        el.onclick = clickHandler;    })();}

内部函数被直接执行,并在 clickHandler 创建的闭包中隐藏了它的内容。

另外一种避免闭包的好方法是在 window.onunload 事件发生期间破坏循环引用。很多事件库都能完成这项工作。注意这样做将使 Fixefox 中的bfcache无法工作。所以除非有其他必要的原因,最好不要在 Firefox 中注册一个onunload 的监听器。

注:摘自:JavaScript|MDN

原创粉丝点击