《 JavaScript程序设计》—— 第五章 函数

5.1 黑盒

从概念上讲,函数接受输入,进行计算,而后产生输出。下图是一个函数黑盒示意图,它计算一个帐户在t年以后的余额,其初始余额为p,年利率为r,每一年取n次复利。
黑盒函数
要使用这个函数,只需向函数发送四个数值,并在其回应信息中获取计算所得的余额。函数的用户看不到其“内在工做”,因此咱们把函数想象成黑盒子。javascript

这里书本给出一个关于什么是抽象的说明:在平常生活中到处能够看到相似的状况。咱们开车,但并不了解内燃机或者氢燃料电池;咱们用微波炉加热事食物,却不明白深层的物理学知识;咱们发送即便消息、推文、打电话,却对文字、声音的编码与传输方式一无所知。咱们把这种只看事物的主体部分而不关心细节的理念叫作抽象。函数则是对计算的抽象。html


5.2 定义和调用函数

在JavaScript中,函数类型值包含一个可执行的代码块,成为函数体,以及零个或多个输入,成为形参(parameter)。下面这个函数只有一个参数,它会计算此参数的三次方。前端

function (x) {return x*x*x;}

函数也是值,和数字、真值、字符串、数组及普通对象同样。所以,能够把函数类型值赋给变量。java

var cube = function (x) {return x*x*x;}

要运行一个函数(或者说调用一个函数),能够向它传递一个放在小括号中的列表,其中是零个或者多个实参(argument)。在被调用事,函数首先把每一个实参值赋给对应的形参,而后执行函数体。若是存在return语句,它会将计算结果传回给调用者。

下面的脚本展现一个函数定义以及对它的三次调用。程序员

// 定义函数 —— 这时不会运行函数体
        var cube = function (x) {
            return x*x*x;
        };
        
        // 进三次调用,将函数体运行三次
        alert(cube(-2));
        alert(cube(10));
        alert(("在一个魔方中有" + (cube(3) - 1) + "个立方体 "))

在第一次调用中,咱们向函数cube传递了-2,cube会把-2赋值给x,而后计算-2-2-2,并把结果值(-8)返回给调用处。这个值随后又被传给alert函数的调用。
函数也能够有名字。函数有了名字,在调用时,就不必定要将它赋值给变量了。web

function cube (x) {
            return x*x*x;
        };
        alert(cube(-2));    // -8

在JavaScript中,这种定义方式成为函数声明,相似于(但又不彻底等同于)把函数赋值给一个同名变量。尽管不少程序员喜欢函数声明的方式,但咱们更喜欢使用变量声明方式。咱们会在章尾讨论。编程

var diceRoll = function () {
            return 1+Math.floor(6*Math.random());
        };

要运行这个函数,必须写成diceRoll(),而不能写成diceRoll。前一个表达式会调用函数,然后一个就是函数自身。

var diceRoll = function () {
            return 1+Math.floor(6*Math.random());
        };
        alert(
            diceRoll()
        );
        alert(
            diceRoll
        );

图片描述

图片描述

这里都没什么问题,我想了想试了下以下函数:segmentfault

function test() {
            return "fn-test";
        };
        alert(test);

图片描述

可见,对函数名进行调用,返回的是函数定义语句,而我在对函数声明语句进行调用时与匿名函数赋值变量方式调用状况相同,是否可证实函数声明语句实际上隐式声明了一个变量(变量名就是函数名),而且将函数引用赋值给函数名(变量名)

若是一个函数完成了某主体的执行,却没有执行任何return语句,它会返回undefined值。这个undefined值真的只是一个技术术语,由于在调用一个没有return语句的函数时,主要是为了它产生的效果,而不是为了它产生的任何值。api

我对这就句话理解:若是函数定义时没有要求返回最终值,则默认返回undefined。调用一个没有返回值的函数后面还不是太理解...效果?何种效果?数组

var echo = function (message) {
            alert(message + ".");
            alert("I said: "+message+"!");
        };
        echo("Sanibonani");        // 调用这个函数最天然的方式
        var x =echo("Hello");      // 为x赋值undefined,但在实际中不会发生
        console.log(x);            // undefined

若是没有为函数传递足够了实参值,则额外的形参变量会被初始化为undefined。

var show = function (x,y) {
            alert(x+" "+y);
        };
        show(1);        // "1 undefined"

5.3 示例

5.3.1 简单的一行函数

// 返回半径为r的圆的面积
        var circleArea = function (r) {
            return Math.PI*r*r;
        };
        // 返回y可否被x整除
        var divides = function (x,y) {
            return y % x === 0;
        };

咱们能够利用本身编写的函数来构建其余函数。
...都是基本的例子略过。

须要注意的是:函数语句内隐式类型转换和优先级与结合性问题!


5.3.2 验证明参

略.......


5.3.3 将对象引用做为参数传送

看一下向函数传递对象的状况

// 返回一个数组中全部元素之和
        var sum = function (a) {
            var result = 0;
            for (var i=0;i<a.length;i+=1) {
                result += a[i];
            }
            return result;
        };
        alert(sum([]));
        alert(sum([10,-3,8]));

再看另外一个例子,它使用了一种彻底不一样的风格。

// 把一个数组中全部字符串都转换成大写
        var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i+=1) {
                a[i]=a[i].toUpperCase();    
            }
        };

区别在于,函数sum返回一个值,而uppercaseAll根本没有包含return语句!相反,它修改了传递给它

// 把一个数组中全部字符串都转换成大写
        var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i+=1) {
                a[i]=a[i].toUpperCase();    
            }
        };
        var result = uppercaseAll(["a","b","c"]);
        alert(result);                        // undefined
        alert(uppercaseAll(["a","b","c"]));   // undefined

自我理解:调用函数后,传参,计算,由于没有return返回值,因此只是计算而已,则uppercaseAll(["a","b","c"])就会像章开头说的那样,默认返回undefined,而后赋值给变量result。(最后alert调用函数证实这一点)。


var uppercaseAll = function (a) {
            for (var i=0;i<a.length;i++) {
                a[i]=a[i].toUpperCase();
            }
        };
        var dogs = ["spike","spot","rex"];
        alert(uppercaseAll(dogs))       // undefined,此值并不表明函数没有执行,而是执行了未指定返回值,则返回默认值。
        alert(dogs);                    // ["SPIKE","SPOT","REX"],修改了传入对象的属性
  • 无返回值函数,按照文中描述,它修改的是传递给它的实参对象的属性。就是修改了实参的属性

  • 能够看到,直接向函数传参,而后被alert调用,结果是undefined由于调用函数并无返回结果,只会返回undefined虽然函数内部的确执行了大写转换操做,可是没有返回值然并卵),因此最后显示undefined

  • 可是dogs引用的数组对象已经被修改,即以前说的,它修改了传递给它的对象的属性。由于没有设置返回值,默认返回的undefined被上一个alert函数调用。dogs引用的数组被调用结束后因为没有返回值,避免了成为新数组被返回出去。因此大写字母保留下来。

另外一个:

var uppercaseAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result.push(a[i].toUpperCase())
            }
            return result;        // 返回的是一个新数组!
        };
        var dogs = ["spike","spot","rex"];
        alert(uppercaseAll(dogs));      // ["SPIKE","SPOT","REX"],这里alert调用的对象,是函数返回的新数组!不是dogs
        alert(dogs);                    // ["spike","spot","rex"]
  • 有返回值函数,直接调用、传参,由于有返回值,这个值被alert函数接收,显示处理结果:["SPIKE","SPOT","REX"]

  • 为何dogs引用的数组仍是小写呢?由于调用函数返回的最终值没有从新赋值给dogs。换句话说,alert函数调用的dogs数组,和uppercaseAll函数没有关系。uppercaseAll执行结束已经返回了一个新数组

也就是说alert(uppercaseAll(dogs)); 这段语句的结果,是大写字母仍是undefined,取决因而否对函数设置返回值!!!

确保你理解了最后这两个函数的区别,第一个函数修改了其实参的属性第二个函数没有改动实参,而是返回一个新的数组


5.3.4 先决条件

// 返回数组中的最大元素
        var max = function (a) {
            var largest = a[0];
            for (var i=0;i<a.lengt;ai++) {
                if (a[i]>largest) {
                    largest = a[i];
                }    
            }
            return largest;
        };
        
        max([7,19,-22,0]);
        max(["dog","rat","cat"]);

它能正常工做吗?
这个函数依靠>操做符一次比较数组中的连续值,跟踪当前找到的最大值(从第一个元素a[0]开始)。如今>知道如何比较数字与数字、字符串与字符串,但奇怪的是,除非>两边的值都是字符串,不然JavaScript会把这两个值都看做数字(隐式转换),而后进行相应比较。有时,这种作法是没问题的。

但若是有一个值被转换成NaN,那么状况就不妙了。若是xyNaN,表达式x>y会得出false3>NaNfalseNaN>3也是false!这就表示:

alert(max([3,"dog"]));            // 3
        alert(max(["dog",3]));            // "dog"

3"dog"实际上是不可比较的,因此计算这种数组的最大值基本上没有什么意义。那在这种状况下难道不该当抛出一个异常吗?不少语言都会这么作。其余语言甚至会拒绝运行包含这种比较的程序!而后,JavaScript很愉快地运行了这种比较,而后给出了没什么意义的结果,若是愿意的话,能够尝试在代码里探测这些问题。

/*返回数组中的最大元素。若是数组包含了不可比较的元素,函数会返回一个不肯定的任意值*/

函数在这个注释中承诺:只要调用者仅传递有意义的参数,那它就返回最大值;不然契约失效。函数对实参提出了这些约数条件称为先决条件函数自身不会检查先决条件,没有知足先决条件只是会致使未指明的行为。先决条件是编程圈子很是熟悉并且深入理解的一个术语,因此咱们将为引入先决条件的注释采用一种约定。

// 返回数组中的最大元素。先决条件:数组中的全部元素必须是能够互相比较的

5.3.5 关注点分离

下面要举的例子几乎会出如今全部介绍编程的书中 ———— 一个质数判断函数。该函数接受一个输入值,而后返回它是否为质数。4.5.1节给出了一份完整的质数脚本,可是如今要学习的内容要多得多了,因此下面将对该脚本进行重构重构重构就是对代码作结构性的调整,让其变得更好,通常(但不必定)是将大而混乱的代码分解成较小的组成部分。在这个案例中咱们要将用户交互与主要计算区分开来,将主要计算部分包装成一个漂亮的函数。

//  返回n是否为质数。先决条件:n是一个大于或等于2的整数,在JavaScript可表示的整数范围以内。
        var isPrime = function (n) {
            for (var k=2,last=Math.sqrt(n);k<=last;k+=1) {
                if (n%k===0) {
                    return false;
                }
            }
            return true;
        };

请务必注意:这个函数只会返回它的实参是否是质数,并不会弹出一条说明判断结果的消息!其他脚本负责提示输入、检查错误、报告结果。

var SMALLEST = 2,BIGGEST = 9E15;
        var n = prompt("输入一个数组,我会检查它是否是质数");
        if (isNaN(n)) {
            alert("这不是个数字");
        } else if (n<SMALLEST) {
            alert("我不能检测这么小的数字");
        } else if (n>BIGGEST) {
            alert("这个数字对我来讲太大了,没法检测");
        } else if (n%1!==0) {
            alert("我只能测试整数");
        } else {
            alert(n+"是"+(isPrime(n)? "质数" : "合数"));
            // 注意这里若是去掉三目运算符的括号,则会先计算字符串链接符,永远弹出:"质数"
        }

重构后的代码体现了关注点的分离,这是一种很优秀的编程作法,它主要有两点好处。

  • 分离关注点可让复杂系统变得容易理解。对于像航天飞机或金融服务系统这样额大型系统,要理解或诊断其中的某个问题,必须可以肯定一些具备明确行为的子系统。若是只是把一个大型系统当作一系列语句的集合,那就永远没法真正理解它。

  • 将质数计算放到它本身的函数中,就能生成一段能够重复使用的代码,能够将它放到咱们未来编写的任意脚本中。咱们已经体验过函数的复用性了:咱们已经调用过alertMath.sqrt,却不须要本身去编写其中的细节。

但咱们这个质数函数的复用性到底如何呢?调用这个函数的脚本作了不少错误检查。若是真的但愿这个函数只需编写一次,却能被数百个、数千个脚本调用,那期待这些“调用者”来作一样的错误检查是否公平呢?固然不公平了。咱们能够在函数中检查错误。

// 返回实参是否为2到9e15之间的质数。
        // 若是实参不是整数或者超出2到9e15的范围,则会抛出异常。
        var isPrime = function (n) {
            if (n%1!==0 || n<2 || n>9e15) {
                throw "这个数组不是整数或者超出范围";
            };
            for (var k=2,last=Math.sqrt(n);k<last;k++) {
                if (n%k===0) {
                    return true;
                }
            }
            return false;
        };

注意,这个函数在遇到问题时会抛出异常,而不是弹出错误提示!这是很关键的。要使函数真正实现可复用,它永远都不该接管用户交流的责任。我理解为错误不与交互模块混用
函数的不一样用户对错误报告可能会有不一样的要求。有些人会把错误写到网页的某个位置,有些人可能会把错误收集到一个数组中,有些人可能想用别的某种语言博报告错误,预测用户可能使用的每种语言不是这个函数的任务。

当编写为调用者计算数值的函数时,应当经过抛出异常来指示错误。


5.3.6 斐波那契数列

本节最后一个例子是一个生成斐波那契数列的函数。斐波那契数列是一个很是值得注意的数列,在天然、音和金融市场中都会出现它的属性。这个数列的开头以下:

0,1,1,2,3,5,8,13,21,34,55,89,144,...

数列中每一个值(前两个值除外)都是前两个值之和。

f(n)=f(n-1)+f(n-2)

咱们的函数会构造一个数组f,从[0,1]开始,而后不停地把最后一个元素(f[f.length-1])和倒数第二个元素(f[f.length-2])相加。由于函数只能处理整数,因此咱们必须确保结果只不会超过JavaScript能够连续表达的整数范围,大约是9e15。就目前来讲,咱们先作个弊,只生成其中的前75的数字,由于我知道这些数字是安全的。

// 返回一个数组,其中包含斐波那契数列的前75个数字。即f.length = 75
        
        var fibonacciSequence = function () {
            var f = [0,1];
            
            for (var i=0;i<=75;i++) {
                f.push(f[f.length-1]+f[f.length-2]);
            }
            alert(f);
        };
        fibonacciSequence();

练习

  • 改写这个斐波那契数列,使其接受一个实参,代表要产生多少个斐波那契数。若是传入的参数不是介于0到75之间(包含)的整数,则抛出一个异常。

<!DOCTYPE HTML>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>     
    <body>
        <script type="text/javascript">
            /*计算*/
            function fbnqFn(n) {
                var fbnqArr = [0,1];
    
                for (var i=0;i<n;i++) {
                    fbnqArr.push(fbnqArr[fbnqArr.length-2]+fbnqArr[fbnqArr.length-1]);
                }
                
                fbnqArr.length = n;
                return fbnqArr;
            };
            /*交互*/
            function client() {
                var Max = prompt("须要生成多少个斐波那契数?(不能超过150)");
                if (isNaN(Max)===true || Max%1!==0 || Max<0 || Max>150) {
                    throw "输入数字不能是非数字、整数、负数且不能超过150";
                }
                var test = fbnqFn(Max);
                console.log(test+" | "+test.length);    
            };
            // start
            client();
        </script>
    </body>
</html>

5.4 做用域

利用函数,能够将其任意的计算进行打包,在调用者看来,就是一条单独地简单命令。请看如下计算阶乘的函数:

// 返回n的阶乘。先决条件:n是一个介于0到21之间的整数(包含0和21)。超过21,返回近似值
        var factorial = function (n) {
            var result = 1;
            for (var i=1;i<=n;i++) {
                result *= i;
            }
            return result;
        };

这个函数声明了一个形参n,以及它本身的两个变量:i和result。在函数内部声明的变量成为局部变量,和形参同样,属于函数本身,与脚本其余位置的同名变量彻底无关。这点很是好,请看:

var result = 100;
        alert(factorial(5));          // 120
        alert(result);                // 100

咱们不会希全局变量result仅仅由于咱们计算了一次阶乘就发生改变。脚本的不一样部分每每是由不一样人编写的。编写函数调用部分的做者彻底不知道在函数中会用到哪些变量。若是你调用了alert函数,而它改变了你的某些变量,你确定会不高兴。
在JavaScript中,在函数内部声明的变量以及函数的形参均拥有函数做用域,而在函数以外声明的变量则拥有全局做用域,成为全局变量拥有函数做用域的变量只在声明它们的函数中可见,与外部世界隔离,就像咱们前面看到的那样。下面这段很是简短的脚本更清地代表了这一点。

var message = "冥王星只是一个矮行星";            
        var warn = function () {
            var message = "你立刻要看到一些争议性的东西";
            alert(message);
        };
        warn();            // "你立刻要看到一些争议性的东西"
        alert(message);    // "冥王星只是一个矮行星"

这里有两个恰巧同名的不一样变量。全局变量的做用域开始于它的声明位置,一直延伸到脚本结束,而局部变量的做用域则是声明它的函数体内部。在这种状况下,局部变量和全局变量的名字相同(message),其做用域重叠。在重叠区域中,最内层的声明优先。

局部变量对外部是隐藏的,没法从外部引用,而全局变量则能在函数中看到,除非你特意隐藏它们。

var warning = "不要双击提交按钮"; 
        var warn = function () {
            alert(warning);        // 这里能够看到全局变量
        };    
        warn();                    // "不要双击提交按钮"
        alert(warning);            // "不要双击提交按钮"

能在函数访问全局变量并无什么使人惊讶的。实际上,咱们已经用过了不少全局变量:alertpromptisNaNMath等等。若是不容许在函数中用它们,要完成任何事情都会面临巨大的阻碍。可是,这也意味着一个潜在的问题。

var message = "新游戏的时间";
        var play = function () {
            message = "正在玩";        // 没有声明
            alert(message);
        };        
        alert(message);
        play();
        alert(message);
        play();

上面脚本定义了一个message变量,它的值由函数更新。在函数中修改全局变量几乎总被认为是很是差的编程实践:脚本中的函数进行"相互交流"的正确作法是经过函数实参和返回值,而不是经过全局变量。程序应当尽可能少的使用全局变量:

尽可能减小全局变量的使用。具体来讲,函数应该经过参数和返回值进行"交流",而不是经过更新全局变量。

JavaScript中局部变量的做用域包含了声明它们的整个函数体,这一事实又会致使另外一种可能状况:全局变量是在声明以后才会出现,而局部变量则是在其函数开始执行时就立刻存在的,即使变量是在函数体中间声明的。考虑如下代码:

var x = 1;
        // 在此处,全局变量x已经存在,而全局变量y则还没有存在
        // 在此处使用y则会抛出一个ReferenceError引用错误
        var y = 2;
        // 此时全局变量y已经存在
        var f = function () {
            alert(z);            // 没有错误,显示undefined
            var z = 3;
            alert(y+3);            // 5
        };
        f();

其实上面例子有一个变量提高的问题,根据变量提高机制,var会提高到当前做用域的顶端,z的做用域是f所包含的区块,因此你的代码等价于

var x = 1;
        var y = 2;
        var f = function () {
            var z; 
        // 会把 var 声明提高到最高的位置 这种特性叫作 变量提高  此时声明了 z 可是为定义值 因此z的值是 undefined     
            alert(z);            
            z = 3;
            alert(y+3);        
        };
        f();

当调用函数时,JavaScript引擎会在该处建立一个对象,用以保存函数的形参和局部变量。形参会被当即初始化,得到调用时所传实参值的副本,全部局部变量会被马上初始化为undefined(这里不是先初始化再赋值的?)。上面例子里,在z声明前就引用了它,但并无抛出ReferenceError,其缘由就在于此。

可是,尽管你知道局部变量在声明以前便可调用,但这并不意味着就应该使用处于未定义状态的局部变量。事实上,故意在定义变量以前就使用它们,几乎可让全部阅读你代码的人产生混淆,因此这被认为是很是差的风格。不少JavaScript风格指南甚至直接认定这是一种错误;JSLint甚至包含了一项设置,专门用于检查这一状况。


练习(包含变量声明提高和函数声明提高问题)

  • 请定义术语做用域

    1. 函数内部定义的变量只对函数内部可见,对外部不可见

    2. 或,函数内部定义的变量、对象。使其能被外部发现,使用的范围。


  • 下面的脚本中,弹出什么提示内容?

var x = 1;
        var f = function (y) {
            alert(x+y);
        };
        f(2);        // 3

那么按照变量提高,其实是:

var x = 1;
        var f = function (y) {
            var y;        // 声明提高
            y = 2;        // 得到调用函数传入的实参
            alert(x+y);   // 这里的y引用的是全局变量y
        };
        f(2);

若是把形参y更名为x,脚本会提示什么?

var x = 1;
        var f = function (x) {
            alert(x+y);
        };
        f(2);

实际上会报错,由于变量y没有定义

var x = 1;
        var f = function (x) {
            var x;
            x = 2;
            alert(x+y);
        };

关于变量提高和块级做用域问题能够看几位大神的文章:



5.5 做为对象的函数

JavaScript是每个值,只要它不是undefined、null、布尔值、数字和字符串,那它就是一个对象。所以,函数值也是对象,并且跟全部对象同样,也能够有属性。它们还能够像其余值同样,其自己是其余对象的属性。

5.5.1 函数的属性

知道函数是对象以后,天然会问,函数有那些属性?
函数属性的其余用途包括:计算生成特定结果的次数、记住函数在给定实参下的返回值,以及定义与特定对象集合相关的数据。

当建立了函数对象以后,JavaScript会其初始化两个属性。第一个是length,初始值为函数的形参个数

var average = function (x,y) {
            return (x+y)/2;
        };
        alert(average.length);    // 2,一个用于x,一个用于y

第二个预约义属性是prototype,以后在讨论


5.5.2 做为属性的函数

因为函数也是值,因此能够做为对象的属性。把函数放在对象内部有两个主要理由,第一个理由是把许多相关函数放在一组。例如:

var geometry = {
            circleArea:function (radius) {
                return Math.PI*radius*radius;
            },
            circleCircumference:function (radius) {
                return 2*Math.PI*radius;
            },
            sphereSurfaceArea:function (radius) {
                return 4*Math.PI*radius*radius;
            },
            boxVolume:function (length,width,depth) {
                return length*width*depth;
            }
        };

把许多函数组合到单个对象中,有助于组织和理解大型程序。人类不但愿去尝试理解一个拥有数百个甚至数千个函数的系统,若是一个系统只有数十个软件组成部分,那咱们理解起来会容易不少。例如,在一个游戏程序中,咱们会很天然地为玩家、地貌、物理属性、消息传递、装备、图像等分别建立出子系统,每一个都是一个很大的对象。

将函数做为属性的第二个理由是让程序从面向过程转向面向对象。例如,咱们不必定要将函数看做对形状执行操做,将函数存储为形状的属性。将函数放在对象的内部,可让人们专一于这些函数,让函数扮演对象行为的角色。

var circle = {
            radius:5,
            area:function () {
                return Math.PI*this.radius*this.radius;
            },
            circumference:function () {
                return 2*Math.PI*this.radius;
            }
        };
        alert(circle.area());            // 78.53981633974483
        circle.radius = 1.5;
        alert(circle.circumference());    // 9.42477796076938

这个例子引入了JavaScript的this表达式,这是一个至关强大的表达式,能够根据上下文表达出不一样含义。当一个调用中引入了包含函数的对象时(就如上面的circle.area()),this指的就是这个包含函数的对象。

使用this表达式的函数属性称为方法。所以,咱们说circle有一个area方法和一个circumference方法。

练习

  • 将函数值用做对象属性的两个主要理由是什么?

    谈谈本身理解,有大神有别的建议欢迎评论

    1. 更好的运用面向对象编程思惟

    2. 对象在建立时自带操做函数(属性),存储在对象内部,做为对象的一部分存在。


  • 下面的脚本会提示什么?为何?

var x = 2;
        var p = {
            x:1,
            y:1,
            z:function () {
                return x + this.x;    // x引用的是全局变量,this.x指向的是p.x
            },
        };
        alert(p.z());    // 3

5.5.3 构造器

在上一节,咱们仅定义了一个circle圆对象。但若是须要不少个圆,怎么办?

// 错误的示范
        var Circle = function (r) {
            return {
                radius:r,
                area:function () {
                    return Math.PI*this.radius*this.radius;
                },
                circumference:function () {
                    return 2*Math.PI*this.radius;
                }
            };
        };
        var c1 = Circle(2);        // 建立一个半径为2的圆
        var c2 = Circle(10);    // 建立一个半径为10的圆
        alert(c1.area())        // "314.1592653589793"

这段代码表面上看没问题,但有一个缺陷。每次建立一个圆,也另行建立了额外的面积和周长方法。
图片描述
在建立多个圆时,会浪费大量的内存来保存面积和周长函数的冗余副本——这是很糟糕的事情,由于内存资源是有限的。当脚本耗尽内存就会崩溃。型号,JavaScript的原型prototype提供了一种解决方案。

// 一个圆的原型,其设计目的是做为下面用Circle函数建立的全部圆的原型
        var protoCircle = {
            radius:1,
            area:function () {return Math.PI*this.radius*this.radius;},
            circumference:function () {return 2*Math.PI*this.radius;}
        };
        // 建立具备给定半径的圆
        var Circle = function (r) {
            var c= Object.create(protoCircle);    // 将protoCircle原型建立到变量c中
            c.radius = r;                         // c的_proto_指向protoCircle对象
            return c;
        };

每一个经过调用Circle建立的圆都有本身的radius属性和一个隐藏连接,指向一个惟一的共享原型,其中包含了areacircumference函数(分别只有一个)。这是极好的,不过还只是有小小缺陷。咱们使用了两个全局变量CircleprotoCircle。若是只有一个就更好了,这样可让咱们的原型圆做为Circle函数的一个属性。咱们如今就有了一模式,用于很方便的定义一系列同种"类型"的对象。

/* 一个圆数据类型。概要:
         * 
         * var c = Circle(5);
         * c.radius => 5
         * c.area() => 25pi
         * c.circumference() => 10pi
         */
        
        var Circle = function (r) {
            var circle = Object.create(Circle.prototype);
            circle.radius = r;
            return circle;
        };
        Circle.prototype = {
            area:function () {return Math.PI*this.radius*this.radius},
            circumference:function () {return 2*Math.PI*this.radius},
        };

咱们能够应用这一模式,生成一个用于建立矩形的函数。

/* 矩形数据类型。概要:
         * 
         * var r = Rectangle(5,4);
         * r.width => 5
         * r.height => 4
         * r.area() => 20
         * r.perimeter() => 18
         */
        
        var Rectangle = function (w,h) {
            var rectangle = Object.create(Rectangle.prototype);
            rectangle.width = w;
            rectangle.height = h;
            return rectangle;
        };
        Rectangle.prototype = {
            area:function () {return this.width*this.height};
            perimeter:function () {return 2*(this.width+this.height)}
        };

全新方式:JavaScript中的每一个函数对象都自动包含一个prototype属性,prototype是函数两个预约义属性中的第二个,第一个length。只要函数一经定义,它的prototype属性就会被初始化为一个全新对象。(这个全新对象有本身的一个属性,叫作constructor)。
下图展现了一个新鲜出炉的函数,用于算两个值的平均值。
图片描述

其次在使用函数建立对象时,只要是用来魔法操做符new,就无需明确链接原型,也无需返回新建立对象。当你在函数调用以前加上了new时,会发生三件事情。

  • JavaScript会建立一个全新的空对象,而后使用引用这个新对象的表达式this来调用此函数。

  • 该构造对象的原型被设定为函数的prototype属性。

  • 该函数会自动返回新的对象(除非你明确要求函数返回其余东西)
    这些规则看上去很复杂,但看一个例子就清楚了。

产生一个圆的函数,如何使用new操做符来调用该函数,建立的圆的实例

/*一个圆数据类型。概要:
         * var c = new Circle(5);
         * c.radius => 5
         * c.area() => 25pi
         * c.circumference() => 10pi
         */
        var Circle = function (r) {
            this.radius = r;
        };
        Circle.prototype.area = function () {
            return Math.PI*this.radius*this.radius;
        };
        Circle.prototype.circumference = function () {
            return 2*Math.PI*this.radius;
        };
        var c1 = new Circle(2);        // 建立半径为2的圆
        var c2 = new Circle(10);    // 建立半径为10的圆
        alert(c2.area());            // "314.1592653589793"

此脚本先建立一个函数对象,咱们将用变量Circle引用它。和全部函数同样,建立它时,拥有一个第二对象,这个对象被prototye属性引用。随后,咱们向这个原型对象添加areacircumference函数。接下来咱们调用new Circle建立一对圆对象。操做符new建立新的对象,这个对象其原型为Circle.prototype
图片描述

根据设计,诸如Circle这样的函数就是要用new调用的,这种函数称为构造器。根据约定,咱们用大写首字母命名,并省略return语句,优先使用JavaScript的自动功能返回新建立的对象。之因此要约定使用大写首字母,缘由在下一节给出。

没有return语句的构造器调用将返回对象,而不是返回一般的undefined,新建立对象的原型将被神奇地指定给一个历来不会显式建立的对象。

这种方法不够直接,这多是JavaScript语言中要添加Object.create的缘由之一。一些JavaScript程序员建议对于新脚本仅使用Object.create,由于这样可让对象与其原型之间的连接更为明确。明确的代码更易读易懂易于处理。坚持使用Object.create的另外一个缘由多是出于哲学考虑:咱们能够直接用对象来考虑问题,而不用另行引用“类型”的概念。

可是,咱们不能放弃构造器和操做符new。JavaScript从一开始就在使用它们,数以千计的现有脚本中都使用了它们,JavaScript的许多内置对象都是经过这些方式构建的,因此咱们须要真正理解它们。经过一些练习能够熟悉它们,对目前来讲,请复习如下步骤。

使用操做符new建立和使用一种自定义数据类型,好比圆:

  • 编写一个构造器函数,经过体会this.radius = r这样的赋值语句,为每一个圆初始化一个独有的属性;

  • 将全部圆共享的方法指定给Circle.prototype

  • 经过调用new Circle()来建立特定圆。对于如此建立的每一个圆,其原型将自动变为Circle.prototype


5.6 上下文(applycall

前面的在JavaScript——this、全局变量和局部变量混谈中已经给出前两种规则(全局做用域和函数做用域下的this引用),接下来要说一个注意点。

规则3:当用一个以new操做符调用的函数中时,this引用指的是新建立的对象。

var Point = function (x,y ) {
            this.x = x;
            this.y = y;
        };
        var p = new Point(4,-5);        // 新的实例
        var q = Point(3,8);                // 这里修改了全局变量x和y!

上面的最后一行代表,咱们必定要很是注意,老是以new来调用构造器,以避免修改了已有的全局变量,致使脚本运行失控。为减小发生这种意外的可能性,JavaScript程序员用大写字母书写构造器的名字。可使用一些工具(JSLint)来扫描代码,不要调用函数而不使用new前缀,很危险!


规则4:利用函数方法applycall,能够专门定义一个但愿用做this值的对象。

var f = function (a,b,c) {
            this.x += a+b+c;            
        };
        var a = {x:1,y:2};
        
        f.apply(a,[10,20,5]);            // 调用f(10,20,5),以"a"为this            
        f.call(a,3,4,15);                // 调用f(3,4,15),以"a"为this
        alert(a.x);                        // 58
        
        var Point = function (x,y) {
            this.x = x;
            this.y = y;
        };
        var p = {z:3};
        Point.apply(p,[2,9]);            // 如今p为{x:2,y:9,z:3}
        Point.call(p,10,4);                // 如今p为{x:10,y:4,z:3}

这些方法容许借用(或劫持)现有的方法和构造器,将它们用于一些原本没打算为其使用的对象。这些方法稍有不一样:call会传送其实参,而apply会将实参打包放在一个数组中。


练习:

  • this引用有哪四种应用?

分别对应不一样的做用域和上下文中,全局做用域、被当作方法调用、new构造函数调用、applycall


  • 如下脚本会输出什么?

var p = {
            x:1,
            f:function (y) {
                this.x += y;
                return this.x;
            }
        };            
        var q = {x:5};
        
        alert(p.f(1));
        alert(p.f.call(q,3));

先分析第一个alert:既然是p.f,接收方对象是p,则this引用指向p。且本来的p对象中,局部变量x值为1,执行函数传参后,1 += 1;因此最后p.x的值为2。

第二个alert一样是p.f,只是此次用了call方法,这个方法能够借用现有的方法和构造器,也就是说,p.f这个方法被借用了,给谁呢?对了括号内的q对象,并传参3,此时this引用指向了q对象(注意当调用的一瞬间,this已经指向了q对象),且q对象已经有q.x=5,传参相加,最终结果q.x值为8。


5.7高阶函数

考虑下面两个函数:

var squareAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=a[i]*a[i];
            }
            return result;
        };
        
        var capitalizeAll = function (a) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=a[i].toUpperCase();
            }
            return result;
        };

这两个函数只有很小的一点不一样,他们都是向一个数组中的每一个元素应用一个函数,并收集结果;可是,第一个函数是计算这些元素的平方,而第二个函数则是将这些元素变为大写。咱们能不能仅为共同结构编写一次代码,而后用参数来实现它们之间的小小区别?

var collect = function (a,f) {
            var result = [];
            for (var i=0;i<a.length;i+=1) {
                result[i]=f(a[i]);
            }
            return result;
        };

对每一个数组元素实际执行的函数(好比求平方或转换为大写)如今做为实参传送。

var square = function (x) {return x*x};
        var capitalize = function (x) {return x.toUpperCase();};
        
        var squareAll = function (a) {return collect(a,square);};
        var capitalizeAll = function (a) {return collect(a,capitalize)};

对于这些小小的square和capitalize函数,咱们甚至能够不为其声明变量。

var squareAll = function (a) {
            return collect(a,function (x) {return x*x};)
        };
        var capitalizeAll = function (a) {
            return collect(a,function (x) {return x.toUpperCase();});
        };

好,来看看它们如何工做的。

var arr1 = [-2,5,0];        
        var arr2 = ["hi","ho"];
        
        alert(squareAll(arr1));
        alert(capitalizeAll(arr2));

函数f接受一个另外一个函数g做为其实参(并在本身体内调用g),这种函数f称为高阶函数。函数collect称为高阶函数,内置的sort也是如此。咱们能够向sort传送一个比较函数,使它采用不一样的排序方式。比比较函数就是咱们本身编写的一个两实参函数,当第一个实参小于第二个时返回一个负值,当两个实参相等时返回0,当第一个实参较大时则返回一个正值。

var a = [3,6,10,1,40,25,8,73];            
        alert(a.sort());                                // 按字母排序
        alert(a.sort(function (x,y) {return x-y;}));    // 按数值递增排序
        alert(a.sort(function (x,y) {return y-x;}));    // 按数值递减排序

由于咱们能够告诉sort函数,按照咱们喜欢的任意方式来比较元素,因此能够编写一些代码,用几种不一样方式对一组对象进行排序。


在web页上设置定时器、与用户操做进行交流时,常常会传送函数。它也是人工智能编程中最为重要的程序设计范例之一。且有助于构建很是大的分布式应用程序。

高阶函数一词不只适用于以函数为实参的函数,还适用于返回函数的函数。

var withParentheses = function (s) {return "("+s+")";};
        var withBrackets = function (s) {return "["+s+"]";};
        var withBraces = function (s) {return "{"+s+"}";};

这三个函数很是相似。能够怎样进行重构呢?这三个函数中的每个均可以由另外一函数构造而成,只需告诉构造者要使用那种分隔符便可。

var delimitWith = function (prefix,suffix) {
            return function (s) {return prefix+s+suffix;}
        };
        var withParentheses = delimitWith("("+s+")");
        var withBrackets = delimitWith("[","]");
        var withBraces = delimitWith("{","}");

withParentheses、withBrackets、withBraces这三个函数都成为闭包。粗略的说,JavaScript闭包是一种函数,它的函数体使用了来自外围(enclosing)函数的变量。闭包在一些很是高级复杂的java结构中扮演着不可或缺的角色。


5.8函数声明与函数表达式

function circleArea(x) {
            return Math.PI*Math.pow(x,2)
        };

这种形式的函数官方名称为函数声明,它的工做方式与前者很是类似,可是这两种定义形式是不一样的。

  • 函数声明不能出如今代码中的某些地方。

  • 经过函数声明引入的变量遵循不一样于普通变量的做用域规则。

具体来讲,函数声明只能出如今脚本中的全局位置,或者出如今一个函数体的"顶级",不容许只出如今语句内部。根据官方的EA规范,下面代码出现移一处语法错误:

if (true) {
            function successor() {return x+1;}        // 不容许
        }

这里的戒律是:即使浏览器容许,也绝对不要将函数声明放在一条语句内。不管是否选择使用函数声明,它们的存在都会影响咱们编写特定表达式的方式,由于函数声明一以单词function开头,因此JavaScript的设计者决定任何语句都不能以这个单词开头,以避免读者混淆。

相关文章
相关标签/搜索