第八章:Javascript函数

函数是这样一段代码,它只定义一次,但可能被执行或调用任意次。你可能从诸如子例程(subroutine)或者过程(procedure)这些名字里对函数概念有所了解。javascript

javascript函数是参数化的:函数定义会包括一个形参(parmeter)标识符列表。这些参数在函数中像局部变量同样工做。函数会调用会给形参提供实参的值。函数使用它们实参的值计算返回值,成为该函数的调用表达式的值。html

除了实参以外,么次调用还会拥有一个值——本地调用的上下文——这就是this关键字值java

若是函数挂载在一个对象上,做为对象的一个属性,就称为它为对象的方法。当经过这个对象来调用函数时,该对象就是这次调用的上下文(context),也就是该函数的this值。用于初始化一个新建立对象的函数称为构造函数(constructor).本文6节i会对构造函数进一步讲解:第9章还会再谈到它。node

在javascript中,函数即对象,程序可随意操做它们。好比,javascript能够把函数赋值给变量,或者做为参数传递给其余函数。由于函数就是对象,因此能够给他们设置属性,甚至调用它们的方法。程序员

javascript的函数能够嵌套在其余函数中定义,这样他们就能够访问它们被定义时所处的做用域变量。这意味着javascript函数构成了一个闭包(closere),它给javascript带来了很是强劲的编程能力。web

1.函数的定义。算法

函数使用function关键字来定义。它能够用在函数定义表达式(4.iii)或者函数声明语句里。在这两种形式中,函数定义都从function关键字开始,其后跟随这些部分编程

  1. 函数名称标识符:函数明川是函数声明语句必须的部分。它的用途就像是变量的名字,新定义的函数对象会赋值给这个变量。对函数定义表达式来讲,这个名字是可选的:若是存在,该名字只存在函数中,并代指函数对象自己。
  2. 一对圆括号:其中包含由0个或者多个逗号隔开的标识符组成的列表。这些标识符是函数的参数明川,它们就像函数体中的局部变量同样。
  3. 一对花括号,里边包含0条或者多条javascript语句。这些语句构成了函数体:一旦调用函数,就会执行这些语句。

下面的例子中分别展现了函数语句表达式两种方式的函数定义。注意:以表达式来定义函数只适用于它做为一个大的表达式的一部分,好比在赋值和调用的过程当中定义函数。数组

             //定义javascript函数
             //输出o的每一个属性的名称和值,返回undefined
            function printprops(o) {
                for (p in o)
                    console.log(p + ":" + o[p] + "\n")
            }

             //计算两个迪卡尔坐标(x1,y1)和(x2,y2)之间的距离
            function distance(x1, y1, x2, y2) {
                var dx = x2 - x1;
                var dy = y2 - y1;
                return Math.sqrt(dx * dx + dy * dy)
            }

             //计算递归函数(调用自身的函数)
             //x!的值是从x到x递减(步长为1)的值的累乘
            function factorial(x) {
                if (x <= 1) return 1;
                return x * factorial(x - 1);
            }

             //这个函数表达式定义了一个函数用来求传入参数的平方
             //注意咱们把它赋值了给一个变量
            var square = function(x) {
                return x * x
            }

             //函数表达式能够包含名称,这在递归时颇有用
            var f = function fact(x) {
                if (x <= 1) return 1;
                else return x * fact(x - 1);
            };
             //f(7)=>5040

             //函数表达式也能够做为参数传给其它函数
            data.sort(function(a, b) {
                return a - b;
            });

             //函数表达式有时定义后当即使用
            var tensquared = (function(x) {
                return x * x;
            }(10))

注意:以表达式定义的函数,函数的名称是可选的。一条函数声明语句实际上声明了一个变量。并把一个函数对象赋值给它。相对而言,定义函数表达式时并无声明一个变量。函数能够命名,就像上面的阶乘函数,它须要一个名称来指代本身。浏览器

若是一个函数定义表达式包含名称,函数的局部变量做用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。一般而言,以表达式方式定义函数时不须要名称,这会让定义它们的代码更紧凑。函数定义表达式特别适合用来那些只用到一次的函数,好比上面展现的最后两个例子。

在5.3.ii中,函数声明语句“被提早”到外部脚本或外部函数做用域的顶部,因此以这种方式声明的函数,能够被在它定义以前出现的代码所调用。不过,以表达式定义的函数就令当别论了。

为调用一个函数,必需要能引用它,而要使用一个表达式方式定义的函数以前,必须把它赋值给一个变量。变量的声明提早了(参见3.10.i),但给变量赋值是不会提早的。因此,以表达式定义的函数在定义以前没法调用。

请注意,上例中的大多数函数(但不是所有)包含一条return语句(5.6.iiii)。return语句致使函数中止执行。并返回它的表达式(若是有的话)的值给调用者。若是return语句没有一个与之相关的表达式,则它返回undefined值。若是一个函数不包含return语句。那它就执行函数体内的每条语句,并返回undefined值给调用者。

上面例子中的函数大可能是用来计算出一个值的,他们使用return把值返回给调用者。而printprops()函数不一样在于,它的任务是输出对象各属性的名称和值。不必返回值,该函数不包含return语句,printprops()的返回值始终是undefined.(没有返回值的函数有时候被称为过程)

嵌套函数

在javascript中,函数能够嵌套在其它函数里。例如

            function hyuse(a, b) {
                function square(x) {
                    return x * x
                }
                return Math.sqrt(square(a) + square(b));
            }

嵌套函数的有趣之处在于它的变量做用域规则:它们能够访问嵌套它们(或者多重嵌套)的函数的参数和变量。

例如上面的代码里,内部函数square()能够读写外部函数hyuse()定义的参数a和b。这些做用域规则对内嵌函数很是重要。咱们会在本文第6节在深刻了解它们。

5.2.ii曾经说过,函数声明语句并不是真正的语句。ECMAScript规范芝是容许它们做为顶级语句。它们能够出如今全局代码里,或者内嵌在其余函数中,但它们不能出如今循环、条件判断、或者try/cache/finally及with语句中(有些javascript并为严格遵循这条规则,好比Firefox就容许在if语句中出现条件函数声明)。注意:此限制仅适用于以语句形式定义的函数。函数定义表达式能够出如今javascript的任何地方

 2.函数调用

构成函数主题的javascript代码在定义之时并不会执行,只有调用该函数是,它们才会执行。有4种方式来调用javascript函数。

  • 做为函数
  • 做为方法
  • 做为构造函数
  • 经过它们的call()或apply()方法间接调用

i.函数调用

使用调用表达式能够进行普通的函数调用也能够进行方法调用(4.5)。一个调用表达式由多个函数表达式组成,每一个函数表达式都是由一个函数对象和左圆括号、参数列表和右圆括号组成,参数列表是由逗号分隔的逗号的零个或多个参数表达式组成。若是函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素。那么它就是一个方法调用表达式。下面展现了一些普通的函数调用表达式

            printprops({x: 1});
            var total = distance(0,0,2,1) + distance(2,2,3,5);
            var probality = factorial(5)/factorial(13);

在一个调用中,每一个参数表达式(圆括号之间的部分)都会计算出一个值,计算的结果做为参数传递给另一个函数。这些值做为实参传递给声明函数时定义的行参。在函数体中存在一个形参的调用,指向当前传入的实参列表,经过它能够得到参数的值。

对于普通的函数调用,函数的返回值成为调用表达式的值。若是该函数返回是由于解释器到达结尾,返回值就是undefined。若是函数返回是由于解释器执行到一条return语句,返回的值就是return以后的表达式值,若是return语句没有值,则返回undefined。

根据ECMAScript3和非严格的ECMAScript5对函数的调用规定,调用上下文(this的值)是全局对象。而后在严格模型下,调用上下文则是undefined、
以函数的形式调用的函数一般不使用this关键字。不过 ,“this”能够用来判断当前是否为严格模式。

             //定义并调用一个函数来肯定当前脚本运行是否为严格模式
            var strict = (function() {return !this;}())

ii.方法调用

一个方法无非是个保存在一个对象的属性里的javascript函数。若是有一个函数f和一个对象o,则能够用下面的代码给o定义一个名为m()的方法

            o.m = f;

给o定义的方法m(),调用它时就像这样

            o.m()

若是m()须要两个实参,调用起来像这样

            o.m(x,y)

上面的代码是一个调用表达式:它包括一个函数表达式o.m,以及两个实参表达式x和y,函数表达式的自己就是一个属性访问表达(4.4节),这意味着该函数被当作了一个方法,而不是做为一个普通的函数来调用。

对方法调用的参数和返回值的处理,和上面所描述的普通函数调用彻底一致。可是方法调用和函数调用有一个重要的区别,即:调用上下文。属性访问表达式由两部分组成:一个对象(本例中的o)和属性名称(m)。像这样的方法在调用表达式里,对象o成为调用上下文,函数体可使用关键字this引用该对象。以下是具体的一个例子

            var calcul = { //对象直接量
                oprand1: 1,
                oprand2: 1,
                add: function() {
                    //注意this关键字的用法,this指带当前对象
                    return this.result = this.oprand1 + this.oprand2;
                }
            };
            calcul.add(); //这个方法调用计算1+1的结果
            calcul.result; //=>2

大多数方法调用使用点符号来访问属性,使用方括号(的属性访问表达式)也能够进行属性访问操做。下面两个例子都是函数的调用:

        o["m"](x,y) //o.m(x,y)的另一种写法
        a[0](z)//一样是一个方法调用(这里假设a[0]是一个函数)

方法调用可能包含更复杂的函数属性访问表达式:

            customer.surname.toUpperCase(); //调用customer.surname方法
            f().m(); //在f()调用结束后继续调用返回值中的方法m()

方法和this关键字是面向对象编程范例的核心。任何函数只要做为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。一般来说,基于那个对象的方法能够执行多种操做,方法调用的语法已经很清晰地代表了函数将基于一个对象进行操做。比较下面两行代码:

            rect.setSize(windth, height);
            setrectSize(rect, width, heigth);

咱们假设这两行代码的功能彻底同样,他们都做用域一个假定的对象rect。能够看出,第一行的方法调用语法很是清晰地代表了这个函数执行的载体是rect对象,函数中的全部操做都将基于这个对象

 方法链
当方法的返回值是一个对象,这个对象还能够再调用它的方法。这种方法调用序列中(一般称为“链”或者“级联”)每次的调用结果都是另一个表达式组成部分。好比基于jQuery(19章会讲到),咱们常这样写代码:
        //找到全部的header,取得他们的id的映射,转换为数组并给它们进行排序
        $(":header").map(function(){return this.id}).get().sort();
当方法并不须要返回值时,最好直接返回this。若是在设计的API中一直采用这种方式(每一个方法都返回this),使用API就能够进行“链式调用”风格的编程,在这种编程风格中,只要指定一次要调用的对象便可。余下的方法都看一基于此进行调用:
        shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();

须要注意的是,this是一个关键字,不是变量,也不是属性名。javascript的语法不容许给this赋值。

和变量不一样,关键字this没有做用域的限制,嵌套的函数不会从调用它的函数中继承this。若是嵌套函数做为方法调用,其this的值只想调用它的对象。若是嵌套函数做为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。不少人误觉得调用嵌套函数时this会指向调用外层函数的上下文。若是你想访问这个外部函数的this值,须要将this值保存在一个变量里,这个变量和内部函数都在一个做用域内。一般使用变量self来保存this。好比:

            var o = { //对象o
                m: function() { //对象中的方法m()
                    var self = this; //将this的值保存在一个变量中
                    console.log(this === o); //输出true,this就是这个对象o
                    f(); //调用辅助函数f()

                    function f() { //定义一个嵌套函数f()
                        console.log(this === o); //"false":this的值是全局对象undefied
                        console.log(self === o); //"true": slef指外部函数this的值
                    }
                }
            };
            o.m();//调用对象o的方法m

在8.7.iiii的例子中,有var self = this更切合实际的用法。

iii.构造函数的调用

若是函数或者方法以前带有关键字new,它就构成构造函数调用(构造函数掉在4.6节和6.1.ii节有简单介绍,第9章会对构造函数作更详细的讨论)。构造函数调用和普通的函数调用方法以及方法调用在实参处理、调用上下文和返回值各方面都不一样。
若是构造函数调用圆括号内包含一组实参列表,先计算这些实参表达式,而后传入函数内,这和函数调用和方法调用是一致的。但若是构造函数没有形参,javascript构造函数调用的语法是容许省略形参列表和圆括号的。凡是没有形参的构造函数均可以省略圆括号。以下文两个代码是等价的

            var o = Object();
            var o = Object;

构造函数调用建立一个新的 空对象,这个对象继承自构造函数prototype属性。构造函数试图初始化这个新建立的对象,并将这个对象用作起调用上下文,所以构造函数能够用this关键字来引用对象作起调用上下文,所以,构造函数可使用this关键字来引用这个新建立的对象。
注意:尽管构造函数看起来像一个方法调用,它依然会使用这个新对象做为调用上下文。也就是说,在表达式new o.m()中,调用上下文并非o

构造函数一般不使用return关键字,它们一般初始化新对象,当构造函数的函数体执行完毕时,它显式返回。这种状况下,构造函数调用表达式的计算结果就是这个新对象的值。然而,若是构造函数显式的使用了return语句返回一个对象,那么调用表达式的值就是这个对象。若是构造函数使用return语句但没有指定返回值。或者返回一个原始值,那么这时将忽略返回值。同时使用这个心对象做为调用结果。

iiii.间接调用

javascript中的函数也是对象,和其它javascript对象没有什么两样函数对象也能够包含方法。其中的两个方法call()和apply()能够用来间接的调用函数。两个方法都容许间接的调用函数。两个方法都容许显式指定调用所需的this值,也就是说,任何函数能够做为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法均可以指定调用的实参,apply()方法则要求以数组的形式传入参数。8.7.iii会有关这两种方法的详细介绍。

3.函数的实参和形参

javascript中的函数定义并未指定函数的形参类型,函数调用也未对实参作任何类型的检测。实际上javascript甚至不检查传入的形参的个数。下面几节将会讨论当调用函数时实参个数和声明的形参个数不匹配时出现的情况。一样说明了如何显式测试函数实参的类型,避免非法的实参传入函数。

i.可选形参

当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的的形参都将设置为undefined值。所以,在调用函数的时,形参是否可选以及是否可选以及是否能够省略应当保持 较好适应性。为了作到这一点,应当给省略的参数赋一个合理的默认值、来看这个例子:

            var xx = {x: 1,y: 2,z: 3};
            var zz = []
                //将对象o中的可枚举属性名追加到数组a中,并返回这个数组a
                //若是省略a,则建立一个新数组并返回这个新数组

            function getPropertyNames(o, /*optional*/ a) {
                if (a === undefined) a = []; //若是a未定义,则使用新数组
                for (var property in o) a.push(property);
                return a;
            }

             //这个函数调用时可使用两个实参
            getPropertyNames(xx); //将o的属性存储到一个新的数组中
            getPropertyNames(xx, zz); //将p的属性追加到数组a中

若是第一行代码中不使用,可使用“||”运算符,若是第一个实参是真值的话就返回第一个实参;不然返回第二个实参。在这个场景下。若是做为第二个实参传入任意对象,那么函数就会使用这个对象。若是省略掉第二个实参(或者传递null以及其余任意假值),那么就会建立一个新的空数组赋值给a。

(须要注意的是,使用“||”运算符代替if语句的前提是a必须先声明,不然表达式会报引用错误,在这个例子中a是做为形参传入的,至关于var a,既然已经声明a,因此这样用是没有问题的)

            a = a || [];

回忆"||"运算符,若是第一个实参是真值的话就返回第一个实参;不然返回第二个实参。在这个场景下,若是做为第二个实参传入任意对象。那么函数就会使用这个对象。

若是省略掉第二个实参(或者传递null或假值),那么就会建立一个空数组,赋值给a

须要注意的是,当用这种可选实参来实现函数时,须要将可选实参放在参数列表的最后。那行调用你的函数的人是没办法省略第一个实参传入第二个实参的(它必须将undefined显式传入,【注意:函数的实参可选时每每传入一个无心义的占位符,惯用的作法是传入null做为占位符,固然也可使用undefined】),一样要注意在函数定义中,使用注释/*optional*/来强调形参是可选的

ii.可变长的实参列表:实参对象

当调用函数的时候,传入的实参的个数大于函数定义的形参个数时,没有办法得到未命名值的引用。参数对象解决了这个问题。在函数体内,arguments是指向实参对象的引用,实参对象是一个类数组的对象(参照7章11节),这样能够经过数字下标就能访问传入函数的实参值。而不用非要经过名字来获得实参。

假设定义函数f,它只有一个实参x。若是调用这个函数时须要传入两个实参,第一个实参能够经过参数名x来得到,也能够经过arguments[0]来获得。第二个实参只能经过arguments[1]来获得。此外和真正的数组同样,arguments也包含一个length属性,用以表示其所包含元素的个数。所以,调用函数f()时传入两个参数,arguments.length的值就是2.

实参对象在不少地方都很是有用,下面的例子展现了使用它来验证明参的个数,从而调用正确的逻辑,由于javascript自己不会这样作:

            function f(x, y, z) {
                //首先验证传入实参的个数是否正确
                if (arguments.leng != 3) {
                    throw new Error("function f() called with" + arguments.length + "arguments,but it ecxpects 3 arguments");
                }
                //再执行函数的其它逻辑
            }

须要注意的是,一般没必要这样检查实参个数。大多数状况下,javascript的默认行为能够知足须要的:省略的实参都是undefined,多出的实参会自动省略

实参对象有一个重要的用处,就是让函数操做任意数量的实参。下面的函数就能够接受任意量的实参,并返回实参的最大值。(内置函数Max.max()的功能与之相似)

            function max( /*...*/ ) {
                var max = Number.NEGATIVE_INFINITY;
                //遍历实参,查找并记住最大值
                for (var i = 0; i < arguments.length; i++)
                    if (arguments[i] > max) max = arguments[i];
                    //返回最大值
                return max;
            }
            max(1, 10, 222, 100); //=>222

相似这样的函数能够接收任意个实参,这种函数也叫“不定参函数”(varargs function),来自古老的c语言

注意:不定实参函数的实参个数不能为零。arguments[]对象最适合的场景是在这样一类函数中,这类函数包含固定个数的命名和必须参数,以及随后个数不定的可选实参。

记住,arguments并非真正的数组。它是一个实参对象。能够这样理解:它是一个对象,碰巧有以数组索引的属性。

数组对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参别名,实参对象以数字索引,实参对象中以数字索引,而且形参名称能够能够认为是相同变量的不一样命名。经过实参名字来修改实参值的话,经过arguments[]数组也能够获取到更改后的值,下面的这个例子清楚的说明了这一点。

            function f(x) {
                console.log(x); //输出实参的初始值
                arguments[0] = null; //修改实参组的元素一样会修改x的内容
                console.log(x); //输“null”
            }
            f(11);

若是实参对象是一个普通的数组的话,第二条console.log(x)语句结果绝对不是null.这个例子中,arguments[]和x指代同一个值。

在ECMAScript5中移除了实参对象的这个特殊属性。在严格模型下还有一点(和非严格模式不一样),在非严格模式下,函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式下 函数没法使用arguments做为形参名或局部变量名,也不能给arguments赋值。

callee和caller属性

除了数组元素,实参对象还定义了callee和caller属性。在非严格模式下(严格模式下会有一系列错误),ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller属性是非标准的,但大多数浏览器都实现这个属性。它指代调运当前正在执行的函数的函数。经过方法caller属性能够访问调运栈。callee属性在某些时候很是有用,好比在匿名函数中经过callee来递归调用自身。

            var factorial = function(x) {
                if (x <= 1) return 1;
                return x * arguments.callee(x - 1);
            }

iii.将对象属性用做实参

当一个函数包含超过3个形参时,对于程序员来讲,要记住调用函数中实参的正确顺序实在让人头疼。每次调用这个函数时都不厌其烦的查阅文档,为了避免让程序员每次都要梳理,最好经过名/值对的形式传入参数。这样参数的顺序就可有可无。为了实现这样风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值才是真正须要的实参数据,以下例子,这样的写法容许在函数中设置省略参数的默认值

             //将原始值数组的length元素复制至目标数组
             //开始复制原始数组的from_start元素
             //而且将其复制到目标数组to_start中
             //要记住实现的顺序并不容易
            function arrayCopy( /*array*/ from, /*index*/ from_start, /*array*/ to, /*index*/ to_start, /*integer*/ length) {
                    //逻辑
                }
                //这个版本的实现效率有些低,但你没必要再记住实参的顺序
                //而且from_start和to_start都默认为0

            function easyCopy(args) {
                    arrayCopy(args.form,
                        args.form_start || 0, //注意,这里设置了默认值
                        args.to,
                        args.to_start || 0, args.length);
                }
                //来看如何调用easyCopy
            var a = [1, 2, 3, 4],
                b = [];
            easyCopy({
                from: a,
                to: b,
                length: 4
            });

iiii.实参类型

javascript方法的形参并未声明类型,在传入时也未作任何类型检查。能够在采用语义化的单词来给函数命名,像上个例子中,给实参作补充注释,以此使代码文档化。对于可选的实参来讲,能够在注释中补充下“这个实参是可选的”。当一个方法能够接收任意数量的实参时,可使用省略号。

            function max( /* number*/ ) { /*代码*/ }

3章8节提到,javascript会在必要的时候进行类型转换。所以,函数指望接收一个字符串实参,而调用函数时传入其它类型的值,所传入的值会在函数体内将其用作字符串方法转换为字符串类型。全部原始类型均可以转换为字符串,全部对象都包含toString()方法(尽管不必定有用),因此这种 场景下不会有任何错误。

然而事情不老是这样,上个例子中的arrayCopy()方法,这个方法指望他的第一个实参是一个数组,当传入一个非数组的值做为第一个实参时(一般会传入数组对象),尽管看起来没问题。但实际会出错。除非所写的函数是只用到一两次,用完即丢的那。你应当添加相似实参类型检查逻辑,由于宁愿程序在传入非法值时报错,也不肯意非法值致使程序报错。

相比而言,逻辑执行时的报错消息不甚清晰更难理解。下面的这个例子就作了这种类型检测。本节借用7章11节isArrayLike()函数

             //断定o是不是一个类数组对象
             //字符串和函数都length属性,可是他们能够有typeOf检测将其排除
             //在客户端javascript中,DOM文本节点也有length属性,须要用额外的o.nodetype != 3将其排除
            function isArrayLike(o) {
                if (o && //o非null、undefined等
                    typeof o === "object" && //o是对象
                    isFinite(o.length) && //o.length是有限数
                    o.length >= o && //o.length是非负数
                    o.length === Math.floor(o.length) && //o.length是整数
                    o.length < 4294967296) //o.length < 2^32
                    return true;
                else
                    return fasle; //不然它不是
            }

             //返回数组(或类数组对象)a的元素累加和
             //数组a中必须为数字/ null undefined的元素都将忽略
            function sum(a) {
                if (isArrayLike(a)) {
                    var total = 0;
                    for (var i = 0; i < a.length; i++) { //遍历全部元素
                        var element = a[i];
                        if (element == null) continue; //跳过null和undefiend
                        if (isFinite(element)) total += element;
                        else throw new Error("sum():elements must be a finte numbers");
                    }
                    return total;
                } else throw new Error("sun():arguments mustbe array-like")
            };
            
            a = [1,2,4,5,3,6,7];
            sum(a)

这里的sum()方法进行了很是严格的实参检查,当传入的非法的值的时候会抛出Error到控制台。但当涉及类数组对象和真正的数组(不考虑数组元素是不是null仍是undefied),这种作法带来的灵活性并不大。

javascript是一种很是灵活的弱类型语言,有时候适合编写实参类型和实参个数不肯定的函数。下面的flexisum()方法就是这样(有点极端),好比它能够接收任意数量的实参,并能够递归地处理实参是数组的状况,这样的话,它就能够用作不定实参函数或者是实参是数组的函数。此外,这个方法尽量在抛出错误在抛出错误以前将非数组转换为数字。

            function flexisum(a) {
                var total = 0;
                for (var i = 0; i < arguments.length; i++) {
                    var element = arguments[i],
                        n;
                    if (element == null) continue; //忽略null和undefined
                    if (isArray(element)) //若是实参是数组
                        n = flexisum.apply(this, element); //递归的计算累加和
                    else if (typeof element === "function") //不然,若是是函数...
                        n = Number(element()); //调用它并作类型抓换
                    else
                        n = Number(element); //直接作类型抓换
                    if (isNaN(n)) //若是没法转换为数字,则抛出异常
                        throw Error("flexisum():can nont convent" + element + "to number");
                    total += n; //不然,将n累加到total
                }
                return total;
            }

4.做为值的函数

函数能够定义,能够调用,这是函数最重要的特性。函数定义和调用是javascript词法特性,对于大多数编程语言来讲也是如此。然而在javascript中,函数不只是一种语法,也是值。也就是说,能够将函数赋值给变量。存储在对象的属性或数组的元素中,做为参数传入另一个函数等。

为了便于理解javascript中的函数是如何作数据的以及javascript语法,来看一个函数定义

            function square(x) {
                return x * x
            }

这个定义建立一个新的函数对象,并将其赋值给square。函数的名字其实是看不见的,它(square)仅仅是变量的名字。这个变量指代函数对象。函数还能够赋值给其它的变量,而且仍能够正常工做:

            var s = square; //如今s和sqare指代同一个函数
            square(4); //=>16
            s(4); //=>16

除了能够将函数赋值给变量,统一能够将函数赋值给对象的属性。当函数做为对象的属性调用时,函数就称为方法。

            var o = {
                square: function(x) {return x * x}
                }; //对象直接量
            var y = o.square(16);

函数甚至不须要名字,当把他们赋值给数组元素时:

            var a = [function(x) {return x * x},20];
            console.log(a[0](a[1])) //=>400

最后一句代码看起来很奇怪,但的确是合法的函数调用表达式。

             //在这里定义一些简单的函数
            function add(x, y) {return x + y;}

            function subtract(x, y) {return x - y;}

            function multiply(x, y) {return x * y;}

            function divide(x, y) {return x / y;}

             //这里的函数以上面的某个函数做为参数
             //并给它传入两个操做数而后调用它

            function operate(operator, operand1, operand2) {
                return operator(operand1, operand2)
            }

             //这行代码所示的函数调用了实际上计算了(2+3)+(4*5)的值
        var i = operate(add,operate(add,2,3) , operate(multiply,4,5));

                    //咱们为这个例子重复实现了一个简单的函数 
                    //此次实现使用函数量,这些函数直接量定义在一个对象直接量中

                    var operators = {
                        add: function(x, y) {return x + y;},
                        subtract: function(x, y) {return x - y;},
                        multiply: function(x, y) {return x * y;},
                        divide: function(x, y) {returnx / y},
                        pow:Math.pow()//使用预约义的函数
                    };
                    
                    //这个函数接受一个名字做为运算符,在对象中查找这个运算符
                    //而后将它做用于锁提供的操做数
                    //注意这里调用运算符函数语法
                    function operate2(operation,operand1,operand2){
                        if(typeof operators[operation] === "function")
                        return operators[operation](operand1,operand2);
                        else throw "unkown operators";
                    }
                    //这样来计算("hello" + "" + "world")的值
                    var j = operate2("add","hello",operate2("add","","world") );
                    //使用预约义的函数Math.pow()
                    var k = operate2("pow",10,2);

这里是将函数作值的另一个例子,考虑下Array.sort()方法,这个方法用来对数组元素进行排序。由于排序的规则有不少(基于数值大小,字母顺序,日期大小,从小到大等)。sort()方法能够接受一个函数做为参数,用来处理具体的排序操做。这个函数做用很是简单,对于任意两个值都返回一个值,以指定他们在爬行后的数组中的前后顺序。这个函数参数使得Array.sort()具备更完美的通用性和无线扩展性,它能够对任何类型的数据进行任意排序。7章8节iii有示例。

自定义函数属性

javascript中的函数并非原始值,而是一种特殊的对象,也就是说,函数能够拥有属性。当函数须要一个“静态”的变量来调用时保持某个值不变,最方便的方法就是给函数定义属性,而不是全局变量。显然定义全局变量会让命名空间变得更杂乱无章。
好比:你想写一个返回一个惟一整数的函数,无论在哪里调用的函数都会返回这个整数。而函数不能两次返回同一个芝。为了作到这一点,函数逼到可以跟踪它每次返回的值,并且这些值的信息须要在不一样函数调用过程当中持久化。能够将这些信息存放到全局变量中,但这并非碧玺的,由于这个信息仅仅是函数自己用到的。最好将这个信息保存到函数的一个属性中,下面这个例子就实现了这样的一个函数,每次调用函数都会返回一个惟一的整数:

             //初始化函数对象的计数器属性
             //因为函数声明被提早了,所以这个是能够在函数声明
             //以前给它的成员赋值的
            unInterger.counter = 0;

             //每次调用这个函数都会返回一个不一样的整数
             //它使用一个属性来记住下一次将要返回的值
            function unInterger() {
                  unInterger.counter++  ; //先返回计数器的值,而后计数器自增1
            }

来看另一个例子,下面这个函数factorrial()使用了自身属性(将自身当作数组来对待)来缓存上一次的计算结果:

             //计算阶乘,并将结果缓存在函数的属性中
            function factorrial(n) {
                if (isFinite(n) && n > 0 && n == Math.round(n)) { //有限的正整数
                    if (!(n in factorrial)) //若是没有缓存结果
                        factorrial[n] = n * factorrial(n - 1); //计算并缓存之
                    return factorrial[n];
                } else return NaN; //若是输入有误
            }
            factorrial[1] = 1; //初始化缓存以保存这种基本状况
            console.log(factorrial())

5.做为命名空间的函数

3章10节i介绍了函数做用域概念:

函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中),在函数的外部是不可见的
不在任何函数内声明的变量是全局变量,在整个javascript程序中都是可见的
在javascript中是没法声明只在一个代码块内可见的变量的(在客户端javascript中这种说法不彻底正确,在有些javascript扩展中就可使用let声明语句块内的变量,详细内容见11章),基于这个缘由,咱们经常简单定义一个函数用作临时命名空间,在这个命名空间内定义的变量不会污染的全局命名空间

好比,假设你写了一段javascript模块代码,这段代码将要用在不一样的javascript程序中(对于客户端javascript经常使用在各类网页中)。和大多数代码同样,假定这段代码定义了一个用以存储中间计算结果的变量。

这样,问题就来了,当模块代码放到不一样的程序中运行时,你没法得知这个变量是否已经建立了。若是已经存在这个变量,那么将会和代码发生冲突。

解决的办法固然是将代码放入一个函数内,而后调用这个函数。这样全局变量就编程了函数内的局部变量

            function mymodule() {
                //模块代码
                //这个模块全部使用的全部变量是局部变量
                //而不是污染全局命名空间
            }
            mymodule(); //不要忘了还要调用的这个函数

这段代码仅仅定义了一个单独的全局变量,名叫“mymodule”的函数。这样仍是太麻烦了,能够直接定义一个匿名函数,并在单个表达式中调用它: 

            (function() { //mymodule函数重写为匿名函数表达式
                //模块代码
            }()); //结束函数定义并当即调用它

这种定义匿名函数并当即在单个表达式中调用它的写法很是常见,已经成为一种惯用的用法了。注意上面代码的圆括号的用法,function以前的左括号是必须的,由于若是不写这个左圆括号,javascript解释器会试图将其解析为函数定义表达式。使用了它javascript解释器才会正确地将其解析为函数定义表达式。使用圆括号是习惯用法,尽管有些时候没有必要也不该当省略。这里定义的函数会当即调用。

下面的例子展现了这种命名空间技术,它定义一个返回extend()函数的匿名函数,此外这个匿名函数命名空间用来隐藏一组属性名。

 

            /**
             * Created by lenovo on 2015/2/11.
             */
             //在特定场景下返回带补丁的extend()版本
             //定义一个扩展函数,用来将第二个以及贵阳徐参数复制到第一个参数
             //这里咱们除了了IE bug:多ie版本中
             //若是o属性拥有一个不可枚举的同名属性,则for/in循环
             //不会枚举对象o的可枚举属性,也就是说 ,将不会挣钱的处理诸如toString的属性
             //除非咱们显式的检测它
            var extend = (function() { //将这个函数的返回值赋给extend
                    //在修复它以前,首先检测是否存在bug
                    for (var p in {
                        toString: null
                    }) {
                        //若是代码执行到这里,那么for/in循环会挣钱工做并返回
                        //一个简单版本的extend()函数
                        return function extend(o) {
                            for (var i = 1; i < arguments.length; i++) {
                                var soure = arguments[i];
                                for (var prop in soure) o[prop] = soure[prop];
                            }
                            return o;
                        };
                    }
                    //若是代码执行到这里,说明for/in循环 不会枚举对象的toString属性
                    //所以返回另一个版本的extend()函数,这个函数显式测试
                    //Object.prototype中的不可枚举属性
                    return function patched_extend(o) {
                        for (var i = 1; i < arguments.length; i++) {
                            var soure = arguments[i];
                            //复制全部能够枚举的属性
                            for (var prop in soure) o[prop] = soure[prop];
                            //如今检查特特殊属性
                            for (var j = 0; j < protoprops.length; j++) {
                                prop = protoprops[j];
                                if (soure.hasOwnproperty(prop)) o[prop] = soure[prop];
                            }
                        }
                        return o;
                    };
                    //这个列表列出看须要检查的特殊属性
                    var protoprops = ["toString", "valueOf", "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnummerable", "toLocaleString"];
                }
                ());

6.闭包

和大多数现代编程语言同样,javascript也采用词法做用域(lexical scoping),也就是说,函数的执行依赖于变量做用域这个做用域是在函数定义时决定的,而不是函数调用时决定的

为了实现这种词法做用域,javascript函数对象的内部状态不只包含函数的代码逻辑,还必须引用当前的做用域(在于都本章节以前,应当复习下3.10节和3.10.iii讲到的变量做用域和做用域链的概念)。

函数对象能够经过做用域相互关联起来,函数体内部的变量均可以保持在函数的做用域内,这种特性在计算机科学文献中称为“闭包”。(这种叫法很是古老,是指函数的变量能够隐藏于做用域链以内,所以看起来是函数将变量包裹了起来。)

从技术的做用域来说,全部的javascript函数都是闭包:它们都是对象,它们都关联到做用域链。定义大多函数时的做用域链在调用函数时依然有效,但这不影响闭包。当调用函数时闭包所指向的做用域链不是同一个做用域链时,事情就变得很是微妙。
当一个函数嵌套了另一个函数,外部嵌套的函数对象做为返回值返回的时候每每会发生这种事情。有不少强大的编程技术都利用到了这类嵌套的函数闭包,以致于这种编程模式在javascript中很是常见,当你第一次碰到很是让人费解,一旦你理解和掌握闭包以后,就能很是的自如的使用它了。理解这一点相当重要。

理解闭包首先须要了解嵌套函数的词法做用域规则,看一下这段代码

            var scope = "global scope"; //全局变量
            function checkscope() {
                var scope = "local scope"; //局部变量
                function f() {
                        return console.log(scope);
                    } //在做用域中返回这个值
                return f();
            }
            checkscope(); // local scope

  checkscope()函数声明了一个局部变量,并定于了一个函数f()返回了一个变量的值,最后将函数f()的执行结果返回便可,你应当很是清楚为何checkscope()会返回“local scope”如今咱们将代码改变下,你知道返回什么吗?

            var scope = "global scope"; //全局变量
            function checkscope() {
                var scope = "local scope"; //局部变量
                function f() {
                        return console.log(scope);
                    } //在做用域中返回这个值
                return f;
            }
            checkscope()(); // 

在这段代码中,咱们将函数内的一对圆括号移动到了checkscope()以后。checkscope()如今仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。在定义函数的做用域外面,调用这个嵌套的函数(包含最后一段代码和最后一对圆括号)会发生什么事情呢?

回想一下这个词法做用域的基本规则:javascript函数的执行用到了做用域链。这个做用域链是函数定义的时候建立的。嵌套的函数f()定义在这个做用域链里,其中的变量scope必定是局部变量,无论在什么时候何地都执行函数f(),这种绑定在执行f()时依然有效。所以,最后一行返回"local scope",而不是“global”.简言之,闭包的这个特性强大到让人吃惊:它能够捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到在其中定义他们的外部函数。

实现闭包
若是你了解了词法的做用域规则,你就能很容易地理解闭包:函数定义时的做用域链到函数执行时依然有效。然而不少程序员以为闭包很是难理解,由于在深刻和兴闭包的实现细节时将本身搞得晕头转向。他们以为在外部函数中定义的局部变量在函数返回后就不存在了(之因此这么说是由于不少人觉得函数执行结束后,与之相关的做用域链彷佛也不存在了,但在javascript中并不是如此),那么嵌套的函数如何能调用不存在的做用域链呢?若是你想搞清楚这个问题,你须要更深刻的了解相似c语言这种更底层的编程语言,了解基于栈的cpu构架;若是一个函数的局部变量定义在cpu的栈中,那么当函数返回时它们的确就不存在了。

但回想下3.10.iii节是如何定义做用域链的。咱们将做用域链描述为一个对象列表,不是绑定的栈。每次调用javascript函数的时候,都会为之建立一个新的对象用来保存局部变量,把这个对象添加至做用域链中。当函数返回的时候,就从做用域链中将这个绑定的变量的对象删除。若是不存在嵌套的函数,也没有其它引用指向这个绑定的对象,它就会被当作垃圾回收掉。若是定义了嵌套的函数,每一个嵌套的函数都各自对应一个做用域链,而且这个做用域链指向一个变量绑定对象。但若是这些嵌套的函数对象在外部函数中保留了下来,那么它们也会和所指向的变量绑定对象同样当作垃圾回收。可是若是这个函数定义了嵌套函数,并将它做为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的 函数,它就不会被当作垃圾回收,而且它所指向的变量绑定也不会被当作垃圾回收(做者在这里清楚地解释了闭包和垃圾回收以前的关系,若是使用不慎,闭包很容易形成“循环引用”,当DOM对象和javascript对象以前存在循环引用时须要格外当心,在某些浏览器下会形成内存泄漏)。

 本文4.i中定于了unInterger()函数,这个函数使用自身的一个属性来保存每次返回的值,以便每次调用都能跟踪上次的返回值。可是这种作法有一个问题,就是恶意代码可能将计数器重置或者把一个非整数赋值给它,致使unInterger()函数不必定能产生“惟一”的“整数”。而闭包能够捕捉到单个函数调用的局部变量,并将这些局部变量用作私有状态,咱们能够利用闭包重写这个函数

            var unInterger = (function() { //定义函数并当即调用
                var counter = 0; //函数的私有状态
                return function() {return counter++;};
            }());

要仔细阅读这段代码才能理解其含义,粗略来看,第一行代码看起来像将函数赋值给一个变量unInterger,实际上,这段代码定义了一个当即调用的函数(函数的开始带有左圆括号),所以是这个函数的返回值赋给变量unInterger。如今咱们来看函数体,这是一个嵌套的函数,咱们将它赋值给变量unInterger,嵌套函数是能够访问做用域内的变量的,并且能够访问外部函数中定义的counter变量。当外部函数返回以后,其它任何代码都没法访问counter变量,只有内部函数才能访问到它

像counter同样的私有变量不是只能用在一个单独的闭包内,在容一个外部函数内定义多个嵌套函数能够访问它,这个嵌套函数都共享一个做用域链,看一下这短代码:

        function counter(){
            var n =0;
            return{
                count:function(){return n++;},
                reset:function(){n = 0;}
            };
        }
        var c = counter(),d = counter(); //建立两个计数器
        console.log(c.count())        //=>0
        console.log(d.count())        //=>0
        console.log(c.reset())        // reset()和count方法共享状态 undefined
        console.log(c.count())        //=>0 由于咱们重置了c
        console.log(d.count())      //=>1 咱们没有重置d
        console.log(d.count())      //=>2 

counter()函数返回了一个“计数器”对象,这个对象包含两个方法:count()下返回一个整数,reset()将计数器重置为内部状态。

首先要理解,这两个方法都能访问私有变量n。再者,每次调用counter()会建立一个新的做用域链和一个新的私有变量。所以,若是调用counter()两次会获得两个计数器对象,并且彼此包含不一样的私有变量,调用其中一个计数器对象的count()或者reset()不会影响另一个对象。

从技术角度看,其实能够将这个闭包合并为属性存取器方法,getter和setter.下面这段代码所示的counter()函数是6章6节中代码的变种,所不一样的是,这里私有状态的实现是利用了闭包,而不是利用普通的对象属性来实现

            function counter(n) { //函数参数n是一个私有变量
                return {
                    //属性getter方法返回并给私有计数器var递增1
                    get count() {
                            return n++;
                        },
                        //属性setter方法不容许n递减
                        set count(m) {
                            if (m >= n) n = m;
                            else throw Error("count can only be set to a larger value");
                        }
                };
            }
            var c = counter(1000);
            console.log(c.count) //=>1000
            console.log(c.count) //=>1001
            console.log(c.count) //=>1002
            console.log(c.count = 2000) 
            console.log(c.count) //=>2000
            console.log(c.count) //=>2001
            console.log(c.count = 2000) //Error: count can only be set to a larger value

 须要注意的是,这个版本的counter()函数并未声明局部变量,而只是使用参数n来保存私有状态,属性存取器方法能够访问n。这样的话,调用counter()的函数就能够指定私有变量的初始值了。

下面的这个例子,利用闭包技术来共享私有状态的通用作法。这个例子定义了一个addPrivateProperty()函数,这个函数定义了一个私有变量,以及两个嵌套的函数来获取和设置这个私有变量的值。它将这些嵌套函数添加为所指定对象的方法。

利用闭包实现的私有属性存取器的方法

利用闭包实现的私有属性存取器的方法

             //这个函数给对象o增长了属性存取器方法
             //方法名称为get<name>和set<name>.若是提供了一个断定函数,setter方法就会用它来检测参数的合法性,而后在存储它。
             //若是断定函数返回false,setter方法抛出异常。
             //

             //这个函数有一个非同寻常之处,就是getter和setter函数
             //所操做的属性值并无存储在对象o中,相反,这个值仅仅是保存在函数中的局部变量中
             //getter和setter方法一样是局部函数,所以能够访问这个局部变量。也就是说,对于两个存取器方法来讲这个变量是私有的
             //就没有办法绕过存取器方法来设置或修改这个值
            function addPrivateProperty(o, name, predicate) {
                var value; //这是一个属性值
                //getter方法简单地将其返回
                o["get" + name] = function() {return value;};

                //setter方法首先检查值是否合法,若不合法就抛出异常,不然就将其存储起来
                o["set" + name] = function(v) {
                    if (predicate && !predicate(v))
                        throw Error("set" + name + ":invalid value" + v);
                    else
                        value = v;
                };
            }
            
            //下面展现了addPrivateProperty()方法
            var o ={};//设置一个空对象
            
            //增长属性存取器方法getName()和setName()
            //确保只容许添加字符串值
            
            addPrivateProperty(o,"Name",function(x){return typeof x == "string"; });
            
            o.setName("Frank"); //设置属性值
            console.log(o.getName());
            o.setName(o);//试图设置一个错误类型的值

咱们已经给出了不少例子,在同一个做用域链中定义两个闭包,这两个闭包共享一样的私有变量或变量。这是一种很是重要的技术,但仍是要当心那些不但愿共享的变量每每不经意间共享给了其它的闭包,了解这一点很是重要。看一下下面的这段代码:

             //这个函数返回一个老是返回v的函数
            function constfunc(v) {
                return function() {return v;}
            };

             //建立一个数组用来常数函数
            var funcs = [];
            for (var i = 0; i < 10; i++) funcs[i] = constfunc(i);

             //在第5个位置的元素所表示的函数返回值为5
            funcs[5]() //=>5

这段代码利用循环建立了不少闭包 ,当写相似这种代码的时候每每会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数以内,看一下这段代码:

             //返回一个函数组成的数组,它们的返回值是0-9
            function constfuncs() {
                var funcs = [];
                for (var i = 0; i < 10; i++)
                    funcs[i] = function() {
                        return i;
                    };
                return funcs;
            }
            var funcs = constfuncs();
            console.log(funcs[5]()) //10

上面的这段代码建立了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,所以它们能够共享变量i。当constfuncs()返回时,变量i的值是10,全部的闭包都共享这一个值,所以,数组中的函数返回值都是同一个值,这不是咱们想要的结果。关联到闭包的做用域链都是“活动的”,记住这一点很是重要。嵌套的函数不会将做用域内的私有成员负责一份,也不会对所绑定的变量生成静态快照(static snapshot)。

书写闭包的时候还须要注意一件事情,this是javascript的关键字,而不是变量。正如以前讨论的,每一个函数调用都包含一个this值,若是闭包在外部的函数里是没法访问this【严格将,闭包内的逻辑是可使用this的,但这个this和当初定义函数的this不是同一个,即使是同一个this,this的值是随着调用栈的变化而变化的,而闭包里的逻辑所取到的this的值也是不肯定的,所以外部函数内的闭包是可使用this的,但要很是当心的使用才行,做者在这里提到的将this转存为一个变量的作法就能够避免this的不肯定性带来的歧义】,除非外部函数将this转存为一个变量:

            var self = this; //将this保存到一个变量中,以便嵌套的函数可以访问它

定arguments的问题与之相似。arguments并非一个关键字,但在调用每一个函数时都会自动声明它,因为闭包具备本身所绑定的arguments,所以闭包内没法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另一个变量中

        var outerArguments = arguments; //保存起来以便嵌套的函数能使用它

在本章接下来的例子中就利用了这种编程技巧来定义闭包,以便在闭包中能够访问外部函数的this和arguments值。

7.函数属性、方法和构造函数

咱们看到在javascript程序中,函数是值。对函数执行typeof运算会返回字符串“function”,可是函数是javascript特殊对象。由于函数也是对象,它们也能够拥有属性和方法,就像普通的对象能够拥有属性和方法同样。甚至能够用Function()构造函数来建立新的函数对象。接下来的几节就会着重介绍函数的属性和方法,以及Function()构造函数。在第三部分也会有关于这些内容的讲解。

i.length属性

在函数体里,arguments.length表示传入函数的实体的个数。而函数自己的length属性则有不一样的含义。函数的length属性是只读属性,它表明实参的数量,这里的参数是值“形参”而非“实参”,也就是定义函数时给出的实参个数,一般也是在函数调用时指望传入函数的实参个数。

下面代码定义一个名叫check()的函数,从另一个函数给它传入arguments数组,它比较arguments.length(实际传入的实参个数)和arguments.callee.length(指望传入的实参个数)来判断所传入的实参个数是否正确。若是个数不正确,则抛出异常。check()函数以后定义一个测试函数f(),用来展现check()用法:

             //这个函数使用arguments.callee,所以它不能再严格模式下工做
            function check(args) {
                var actual = args.length; //实参的真实个数
                var expected = args.callee.length; //指望的实参个数
                if (actual !== expected) //若是不一样则抛出异常
                    throw Error("Expected" + expected + "args; got" + actual)
            }

            function f(x, y, z) {
                check(arguments); //检查实参个数和指望的实参个数是否一致
                return x + y + z; //再执行函数的后续逻辑
            }

ii.prototype属性

每个函数都包含prototype属性,这个属性是指向一个对象的引用,这个对象称为原型对象(prototype object).每个函数都包含不一样原型对象。当将函数用做构造函数的时候,新建立的对象会从原型对象上继承属性。6.1.3节讨论了原型和prototype属性,在第9章会有进一步讨论。

iii.call()和apply()方法

咱们能够将call()和apply()看作是某个对象的方法,经过调用方法的形式来间接调用(8.2.iiii)函数(好比在6.8.ii中使用call()方法来调用一个对象的Object.prototype.toString方法,用以输出对象的类名称),call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内经过this来得到对它的引用。想要以对象o的方法来调用函数f(),能够这样使用call()和apply().

        f.call(o);
        f.apply(o);

上面的例子每行代码和下面代码的功能类型(假设对象o中预先不存在名为m的属性)

            o.m = f; //将f存储为o的临时方法
            o.m(); //调用它不传入参数
            delete o.m; //将临时方法删除

在ECMAScript5的严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的参数是原始值甚至是null或undefined。在ECMAScript3和非严格模式中,传入的null和undefined都会被全局变量替代,而其它原始值会被相应的包装对象(wrapper object)所替代

对于call()来讲,第一个调用上下文实参以后的全部实参就是要传入待调用的函数的值。好比,以对象o的方法形式调用函数f(),并传入两个参数,可使用这样的代码。

        f.call(o,1,2);

apply()方法和call()相似,但传入的实参的形式和call()有所不一样,它的实参都放入一个数组中:

            f.apply(0, [1, 2]);

若是一个函数的实参能够是任意数量,给apply()传入的参数数组能够是任意长度的。好比:为了找出数组中最大数组的元素,调用Math.max()方法的时候能够给apply()传入一个包含任意个元素的数组:

            var biggest = Math.max.apply(Math, array_of_numbers);

须要注意的是给apply()的参数数组能够是类数组对象也能够是真实数组。

实际上,能够将当函数的arguments数组直接传入(另外一个函数的)apply()来调用两一个函数,参照以下代码:

             //将对象o中名为m()的方法替换为令一个方法
             //能够在调用原始的方法以前和以后记录日志消息
            function trace(o, m) {
                var original = o[m]; //在闭包中保存原始方法
                o[m] = function() { //定义新的方法
                    console.log(new Date(), "entering:", m); //输出消息
                    var result = original.apply(this, arguments); //调用原始函数
                    console.log(new Date(), "exiting:", m);
                    return result;
                };
            }

trace()函数接收两个参数,一个对象和一个方法名,它将一个指定的方法替换为一个新方法,这个新方法是“包裹”原始方法的令一个泛函数(反函数也叫泛函,在这里特指一个函数)。这种动态修改已有方法有时候叫作"monkey - patching".

iiii.bind()方法
bind()方法是ECMAScript5中新增的方法,可是ECMAScript3中能够轻易模拟bind().从名字就能够看出,此方法的做用就是将函数绑定至某个对象。

当函数f()上调用bind()方法传入一个对象o做为参数,这个方法将返回一个新的函数。(以函数调用的方式)调用新的函数会把原始的函数f()当o的方法来调用。传入新函数的任何实参都将传入原始函数,好比:

            function f(y) {return this.x + y;} //这个是待绑定的函数
            var o = {x: 1}; //将要绑定的函数
            var g = f.bind(o); //经过g(x)来调用o.f(x)
            console.log(g(4)) // => 5

也能够经过如下代码实现轻松绑定

        //返回一个函数,经过它来调用o中的方法f(),传递它全部的实参
        function bind(f,o){    
            if(f.bind) return f.bind(o);//若是bind()方法存在的话,使用bind()方法
            else return function(){//不然这样绑定
                return f.apply(o,arguments);
            }
        }

ECMAScript5中的bind()方法不只仅是将函数绑定至一个对象,还附带一些其它的应用:除了第一个实参以外,传入bind()实参也会绑定至this,这个附带的应用是一种常见的函数编程技术,有时也被称为“柯里化”(currying)。参照下面的这个例子中的bind()方法的实现:

            var sum = function(x,y){return x + y};//返回练个个实参的值
            //建立一个相似sum的新函数,但this的值绑定到null
            //而且第一个参数绑定到1,这个新的参数指望只传入一个实参
            var succ = sum.bind(null,1);
            succ(5)     // =>6 x绑定到1,并传入2做为实例y
            
            function f(y,z) {return this.x + y + z}; //另一个左累加计算的函数
            var g = f.bind({x:1},2);  //绑定this和y
             g(3) //=>6:this.x绑定到1,y绑定到2,z绑定到3

们能够绑定this的值并在ECMAScript3中实现这个附带应用。例以下面的中的示例代码就模拟实现了标准的bind()方法

注意,咱们将这个方法另存为为Function.prototype.bind,以便全部的函数对象都继承它,这种技术会在9.4章节有详细介绍

ECMAScript3的Function.bind()方法

        if(!Function.prototype.bind){
            Function.prototype.bind() = function(o /*,args*/){
                //将this和arguments的值保存至变量中
                //以便在后面的嵌套函数中可使用他们
                var self = this,boundArgs = arguments;
                
                //bind()返回值是一个函数
                return function(){
                    //建立一个实参列表,将传入bind()的第二个及后续的实参都传入这个函数
                    var arg = [],i;
                    for(i=1;i<boundArgs.length;i++) args.push(boundArgs[i]);
                    for(i=0;i<arguments.length;i++) args.push(arguments[i]);
                    //如今讲self做为哦的方法来调用,传入这些实参
                    return self.apply(o,args);
                };
            };
        }

咱们注意到,bind()方法返回的函数是一个闭包,在这个闭包的外部函数中声明了self和boundArgs变量,这两个变量在闭包里用到。尽管定义闭包的内部函数已经从外部函数中返回,并且调用这个闭包逻辑的时刻要在外部函数返回以后(在闭包中照样能够争取访问这两个变量)。

ECMAScript5定义的bind()方法也有一些特性是上述ECMAScript3代码没法模拟的。首先,真正的的bind()方法返回一个函数对象,这个对象的length属性是绑定函数的形参减去绑定实参的个数(length值不能小于0)。再者,ECMAScript5的bind()方法能够顺带作构造函数,将忽略传入bind()的this,原始函数就会以构造函数的形式调用,其实参也已经绑定(意思是在运行时将bind()所返回的函数用作构造函数时,所传入的实参会原封不动的传入原始函数)。由bind()方法返回的函数并不包含prototype属性(普通函数的固有的prototype属性是不能删除的),而且将这些绑定的函数用作构造函数时锁建立的对象从原始值的未绑定的构造函数中继承prototype。一样在使用instanceof运算符时,绑定构造函数和未绑定构造函数并没有两样

iiiii.toString

和全部的javascript对象同样,函数也有toString()方法,ECMAScript规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。实际上,大多数(非所有)的toString()方法的实现都返回函数的完整源码。内置函数每每返回一个"[native code]"的字符串做为函数体

 iiiiii.Function()构造函数

无论是经过函数定义仍是函数直接量表达式,函数的定义都要使用function关键字。但函数还能够经过Function()构造函数来定义,好比:

            var f = new Function("x","y","return x*y");

这一行代码建立一个新的函数,这个函数和经过下面代码定义的函数几乎等价:

            var f = function(x, y) {return x * y;}

Function()构造函数能够传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体;它能够包含任意的javascript语句,每两条语句之间用分号分隔。传入构造函数的其余全部的实参字符是指定函数的形参名字的字符串。若是定义的函数不包括任何参数,只须给构造函数简单地传入一个字符串--函数体--便可

注意:Function()构造函数并不须要经过传入实参以指定函数名。就像函数直击量同样,Function()构造函数建立一个匿名函数。

关于Function()构造函数有几点须要注意:

Function()构造函数容许javascript在运行时动态的建立并编译函数。

每次Function()构造函数都会解析函数体,并建立新的函数对象。若是是在一个循环或者屡次调用的函数中执行这个构造函数,执行效率会受影响。相比之下 ,循环制的嵌套函数和函数定义表达式则不会每次执行时都从新编译。

最后一点,也是关于Function()构造函数很是重要的一点,就是它所建立的函数并非使用词法的做用域。想法,函数体代码的编译老是会在顶层函数(也就是全局做用域)执行,正以下面代码所示:

            var scope = "global";

            function constructFunction() {
                    var scope = "local";
                    return new Function("return scope"); //没法捕捉局部做用域
                }
                //    这行代码返回global,由于经过Function()构造函数所返回的战术使用的不是局部做用域
            constructFunction()(); //=>"global"

咱们能够将Function()构造函数任务是在全局做用域执行eval()(参照4.12.ii节),eval()能够在本身的私有做用域内定义新变量和函数,Function()构造函数在实际编程过程当中不多用到。

iiiiiii.可调用的对象

咱们在7.11节中提到“类数组对象”并非真正的数组,但大部分场景下能够将其当作数组来对待。对于函数也存在相似状况。“可调用的对象”(callable object)是一个对象,能够在函数调用表达式中调用这个对象。全部的函数都是可调用的,但非全部的可调用对象都是函数。

截止目前,可调用对象在两个javascript实现中不能算做函数。首先,IE web浏览器(ie8及之前的版本)实现了客户端方法(诸如window.alert()和document.getElementsById()),使用了可调用的宿主对象,而不是内置函数对象。IE的这个方法在其它浏览器中也都存在,但他们本质不是Function对象。IE9将它们实现为真正的函数,所以这类可调用的对象愈来愈罕见。

另一个常见的可调用对象是RegExp对象(在众多浏览器中均有实现),能够直接调用RegExp对象,这笔调用它的exec()方法更编辑一些。在javascript这是一个彻头彻尾的非标准对象最开是由Netscape提出,后背其它浏览器厂商所复制,仅仅是为了和Netscape兼容。代码最好不要对可调用的RegExp对象有太多依赖,这个特性在不久的未来可能会废除并删除。对RegExp执行typeof运算结果并不统一,有些浏览器中返回“function”,有些返回“object”。

若是想检测一个对象是不是真值的函数对象(而且具备函数方法),能够参照代码检测它的class属性(6章8节ii)

            function isFunction(x) {
                return Object.prototype.toString.call(x) === "[object Function]"
            }

注意,这里的isFunction()函数和7.10节的isArray()极其相似。

8.函数式编程

和lisp、Haskell不一样,javascript并不是函数式编程语言,但在javascript中能够像操做对象同样操控函数,也就是说能够在javascript中应用函数式编程成绩。ECMAScript5中的数组方法(诸如map()和reduce())就能够很是适合用于函数式编程风格。接下来的几节将着重介绍javascript中的函数式编程技术。对javascript函数的探讨会让人倍感兴奋,你会体会到javascript函数很是强大,而不只仅是学习一种编程风格而已(若是你对这部份内容感兴趣,推荐你使用一下(至少阅读一下)奥利弗·斯蒂尔(Oliver Steele)的函数式javascript库)。

i.使用函数处理数组

假设有一个数组,数组的元素都是数字,咱们想要计算这些元素的平均值和标准差。若使用非函数式编程风格的话,代码是这样:

            var data = [1, 1, 3, 5, 5, 6]; //这里待处理的数组
             //平均数是全部元素的累加值和除以元素的个数
            var total = 0;
            for (var i = 0; i < data.length; i++) total += data[i]
             var mean = total / data.length; //=>3.5

             //计算标准差,首先计算每一个数减去平均数减去平均数以后误差的平方而后求和
            total = 0;
            for (var i = 0; i < data.length; i++) {
                var deviation = data[i] - mean;
                total += deviation * deviation;
            }
            var stddev = Math.sqrt(total / (data.length - 1)); // 2.16794833886788 标准差的值

可使用数组方法,map()和reduce()来实现一样的计算,这种实现极其简洁(参照7.9节来查看这些方法):

            //首先先简单定义两个简单函数
            var sum = function(x,y){return x+y;};
            var square = function(x) {return x*x;};
        
            //而后将这些函数和数组方法配合使用计算出平均数和标准差
            var data = [1, 1, 3, 5, 5, 6]; //这里待处理的数组
            var mean =data.reduce(sum)/data.length;
            var deviations = data.map(function(x){return x-mean;});
            var stddev = Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));

若是咱们基于ECMAScript3来如何实现呢?由于ECMAScript3并不包含这些数组方法,若是不存在内置方法咱们能够自定义map()和reduce()函数:

             //对于每一个数组元素调用函数f(),并返回一个结果数组
             //若是Array.prototype.map定义了的话,就使用这个方法

            var map = Array.prototype.map ? function(a, f) {
                    return a.map(f);
                } //若是已经存在map()方法,就直接使用它

            : function(a, f) { //不然就本身实现一个
                var result = [];
                for (var i = 0, len = a.length; i < len; i++) {
                    if (i in a) result[i] = f.call(null, a[i], i, a);
                    return result;
                }
            };

             //使用函数f()和可选的初始值将数组a减至一个值
             //若是Array.prototype.reduce存在的话,就使用这个方法
            var reduce = Array.prototype.reduce ? function(a, f, initial) { //若是reduce()方法存在的话
                if (arguments.length > 2)
                    return a.reduce(f, initial); //若是成功的传入了一个值
                else return a.reduce(f); //不然没有初始值
            }
            :function(a,f,initial){//这个算法来自ECMAScript5规范
                var i =0,len =a.length,accumulator;
                
                //以特定的初始值开始,不然第一个值取自a
                if(arguments.length>2) accumulator = initial;
                else {//找到数组中第一个已经定义的索引
                    if(len == 0) throw TypeError();
                    while(i<len){
                        if(i in a){
                            accumulator = a[i++];
                            break;
                        }else i++;
                    }if(i == len) throw TypeError();
                }
                //对于数组中剩下的元素一次调用f()
                while(i<len){
                    if(i in a)
                    accumulator = f.call(undefined,accumulator,a[i],i,a);
                }
                return accumulator;
            };

使用定义的map()和reduce()函数,计算平均值和标准差的代码看起来像这样:

        var data = [1,2,35,6,3,2];
        var sum =function(x,y){return x+y;};
        var square = function(x){return x*x;};
        var mean =reduce(data,sum)/data.length;
        var deviations = map(data,function(x){return x-mean;});
        var stddev = Math.sqrt(reduce(map(deviations,square),sum)/(data.length-1));

ii.高阶函数

所谓高阶函数(higer-order function)就是操做函数的函数,它接收一个或多个函数做为参数,并返回一个新函数,看这个例子:

             //这个高阶函数返回一个新的函数,这个新函数将它的实参传入f()
             //并返回f的返回值逻辑非
             function not(f){
                 return function(){//返回一个新的函数
                     var result = f.apply(this,arguments);//调用f()
                     return !result; //对结果求反
                 };
             }
             var even = function (x){//判断a是否为偶数的函数
                 return x % 2 === 0;
             };
             
             var odd = not(even); //判断一个新函数,和even()相反
             [1,1,3,5,5].every(odd); //=>true 每一个元素为奇数

上面的not()函数就是一个高阶函数,由于它接收一个函数做为参数,并返回一个新函数。令外一个例子,来看下面的mapper()函数,它也是接收一个函数做为参数,并返回一个新函数,这个新函数 将一个数组映射到另外一个使用这个函数的数组上。这个函数使用了以前定义的map()函数,但首先要理解这两个函数有所不一样的地方,理解这一点相当重要。

            var map = Array.prototype.map ? function(a, f) {
                    return a.map(f);
                } //若是已经存在map()方法,就直接使用它

            : function(a, f) { //不然就本身实现一个
                var result = [];
                for (var i = 0, len = a.length; i < len; i++) {
                    if (i in a) result[i] = f.call(null, a[i], i, a);
                    return result;
                }
            };

             // 所返回的函数的参数应当是一个实参数组,并对每一个函数数组元素执行函数f()
             // 并返回全部的计算结果组成数组
             // 能够对比下这个函数和上下文提到的map()函数
            function mapper(f) {
                return function(a) {
                    return map(a, f);
                };
            }
            var increment = function(x) {return x + 1;};
            var incrementer = mapper(increment);

            incrementer([1, 2, 3]) // => [2,3,4]

这里是一个更常见的例子,它接收两个函数f()和g(),并返回一个新的函数用以计算f(g()):

             //返回一个新的可计算f(g(...))的函数
             //返回的函数h()将它全部的实参传入g(),而后将g()的返回值传入f()
             //调用f()和g()时的this值和调用h()时的this值是同一个this
             function compose(f,g){
                 return function(){
                     //须要给f()传入一个参数,因此使用f()的call方法
                     //须要给g()传入不少参数,因此使用g()的apply()方法
                     return f.call(this,g.apply(this,arguments));
                 };
             }
             var square = function(x){return x*x;};
             var sum = function(x,y){return x+y;};
             var squareofsum = compose(square,sum);
             squareofsum(2,10) //=>144

本章后几节中定义了partial()和memozie函数,这两个函数都是很是重要的高阶函数。

iii.不彻底函数

函数f()(见8.7.iiii)的bind()方法返回一个新函数,而后给新函数传入特意的上下文和一组指定的参数,让调用函数f()。咱们说它把函数“绑定至”对象并传入一个部分参数。bind()方法只是将实参放在(完整参数列表的)左侧,也是说传入的bind()的实参都是放在传入原始函数的实参列表开始的位置。但有时咱们指望传入bind()实参放在(完整实参列表)右侧:

             // 实现一个工具函数将类数组对象(或对象)转换为正真的数组
             // 在后面示例代码中用到了这个方法将arguments对象转化为正真的数组
            function array(a, n) {return Array.prototype.slice.call(a, n || 0);}

             //这个函数的实参传递至左侧
            function partialLeft(f /*,...*/ ) {
                var args = arguments; //保存外部实参数组
                return function() { //并返回这个函数
                    var a = array(args, 1); //开始处理外部的地图份额args
                    a = a.concat(array(arguments)); //而后增长内全部内部实参
                    return f.apply(this, a); //而后基于这个实参列表调用f()
                };
            }

             //这个函数的实参传递至右侧
            function partialRight(f /*,...*/ ) {
                var args = arguments; //保存外部实参数组
                return function() { //返回这个函数
                    var a = array(arguments); //从内部参数开始
                    a = a.concat(array(args, 1)); //而后从外部第一个args开始添加
                    return f.apply(this, a); //而后基于这个实参列表调用f()
                };
            }

             //这个函数的实参被用作模板
             //实参列表中的undefeined值都被填充
            function partial(f /*,...*/ ) {
                    var args = arguments; //保存外部实参数组
                    return function() {
                        var a = array(args, 1); //从外部的args开始
                        var i = 0,
                            j = 0;
                        //遍历args,从内部实参填充undefined值
                        for (; i < a.length; i++)
                            if (a[i] === undefined) a[i] = arguments[j++];
                            //如今将剩下的内部实参都追加进去
                        a = a.concat(array(arguments, j))
                        return f.apply(this, a);
                    };
                }
            //这个函数带有三个实参
            var f = function(x, y, z) {
                return x * (y - z);
            };
             //注意三个不彻底调用以前的区别
             partialLeft(f, 2)(3, 4) //=>-2: 绑定第一个实参:2*(3-4)
             partialRight(f, 2)(3, 4) //=>6: 绑定最后一个实参:3*(4-2)
             partial(f, undefined, 2)(3, 4) //=>-6 绑定中间的实参:3*(2-4)

利用这种不彻底函数的编程技巧,能够编写一些有意思的代码,利用已有的函数定义新的函数。参照下, 这个例子

            var increment = partialLeft(sum,1);
            var cuberoot = partialRight(Math.pow,1/3);
            String.prototype.first = partial(String.prototype.charAt,0);
            String.prototype.last = partial(String.prototype.substr,-1,1);

当不彻底调用和其余高阶函数整合在一块儿的时候,事情就变得格外有趣了。好比这个理例子定义了not()函数,它用到了刚才提到不彻底调用:

            var not = partialLeft(compose,function(x){return !x;});
            var even = function(x) {return x % 2 === 0;};
            var odd = not(even);
            var isNumber = not(isNaN)

咱们也可使用不彻底调用的组合来从新足足求平均数和标准差的代码,这种编码风格是很是纯粹的函数式编程:

var data = [1,1,3,5,5]
            var sum =function(x,y){return x+y;}; //两个初等函数
            var product =function(x,y){return x*y;};
            var neg = partial(product-1);
            var square = partial(Math.pow,undefined,2);
            var sqrt = partial(Math.pow,undefined,.5);
            var reciprocal = partial(Math.pow,undefined,-1);

咱们也可使用不彻底调用的组合来从新足足求平均数和标准差的代码,这种编码风格是很是纯粹的函数式编程:

            //如今来计算平均值和标准差,全部的函数调用都不带运算符
            //这段代码看起来很像lisp代码
            
            var mean = product(reduce(data,sum),reciprocal(data.length));
            var stddev = sqrt(product(reduce(map(data,
                compose(square,
                    partial(sum,neg(mean))))
            ,sum),
            reciprocal(sum(data.length,-1))));
            
            console.log(mean)

 iiii.记忆

在8.4.i中定义了一个阶乘函数,它能够将上次的计算结果缓存起来。在函数式编程当中,这种缓存技巧叫“记忆”(memorization)。下面代码展现了一个高阶函数,memorize()接受一个函数做为实参,并返回带有以及能力的函数。(须要注意的是,记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的事件复杂度,在客户端javascript中的代码的执行时间复杂度每每成为瓶颈,所以在大多数场景下,这种牺牲空间换取事件的作法以提高程序执行效率的作法是很是可取的。)

            function memorize(f) {
                var cache = {}; //将值保存在闭包内
                return function() {
                    //将实参转换为字符串形式,并将其用作缓存的键
                    var key = arguments.length + Array.prototype.join.call(arguments, ",");
                    if (key in cache) return cache[key];
                    else return cache[key] = f.apply(this, arguments);
                };
            }

memorize()函数建立一个新的对象,这个对象被当作缓存(的宿主)并赋值给一个局部变量,所以对于返回的函数来讲它是私有的(在闭包中)。所返回的函数将它的实参转换为字符串,并将字符串用作缓存对象的属性名。若是在缓存中存在这个值,则直接返回它。
不然,就调用既定的函数对实参进行计算,将结果缓存起来并返回,下面的代码展现了如何使用memorize():

            //返回两个整数的最大公约数
            //使用欧吉利德算法
            function gcd(a,b){//这里省略对a和b的类型检查
                var t;
                if (a>b) t=b,b=a,a=t; //确保a>=b
                while(b !=0) t=b, b= a%b, a=t; //这里是求最大公约数的欧几里德算法
                return a;
            }
            var gcdmemo = memorize(gcd);
            gcdmemo(85,187); //=>17
            
            //注意,咱们写一个递归函数时,每每须要实际记忆功能
            //咱们更但愿调用了实现了记忆功能的递归函数,而不是原递归函数
            var factorial = memorize(function(n){
                return(n <= 1)?1:n *factorial(n-1);
            });
            factorial(5) //=>120 对4-1的值也有缓存

(本文完结,临近春节,祝你们新年快乐。欢迎你们关注第9章内容:javascript类和模块

相关文章
相关标签/搜索