《JavaScript语言精粹》学习笔记(函数(2))

《JavaScript语言精粹》学习笔记(函数(2))

函数(Functions)

参数(Arguments)

      当参数被调用时,会得到一个“免费”的参数数组arguments。函数可以通过此参数访问它被调用时传递给它的参数列表,包括那些没有被分配给函数形式参数的多余参数。这使得编写一个没有指定参数个数的函数成为可能:
      

    //访问一个将大量的值相加的函数
    //注意该函数内部定义的sum不会与函数外部定义的sum相冲突。
    //该函数只会看到函数内部的那个变量
    var sum = function (){
        var sum = 0;
        for (var i = 0; i<arguments.length; i++){
            sum += arguments[i];
        }
        return sum;
    };
    document.writeln(sum(4, 8, 15, 16, 23, 42));   //108

      这不是一个特别有用的模式。在第6章中,我们将会看到如何给数组添加一个相似的方法来达到同样的效果。
      因为语言的一个设计错误,arguments不是一个真正的数组。它是一个“类似数组(array-like)”的对象。arguments有一个length属性,但它没有任何数组的方法。在本章结尾将会看到这个设计错误导致的后果。


异常(Exceptions)

      JavaScript提供了一套异常处理机制。异常时干扰程序的正常流程的不寻常(但并非完全是出乎意料)的事故。当发现这样一个事故的时候,程序应该抛出一个异常:

    var add = funtion (a, b){
        if (typeof a !== 'number' || typeof b !== 'number'){
            throw {
                name: 'TypeError',
                message: 'add new numbers'
            };
        }
        return a + b;
    };

      throw语句中断函数的执行。它应该抛出一个exception对象,该对象包含一个用来识别异常类型的name属性和一个描述性质的message属性。你也可以添加其他的属性。
      该exception对象被传递到一个try语句的catch从句:

    //构造一个try_it函数,以不正确的方式调用之前的add函数
    var try_it = function (){
        try{
            add("try");
        }catch (e){
            document.writeln(e.name + ':' + e.message);
        }
    }

    try_it();

      如果在try代码块内抛出了一个异常,控制权就会交给它的catch从句。
      一个try语句只能包含一个catch从句。


扩充类型的功能(Augmenting Types)

      JavaScript允许给语言的基本类型扩充功能。在第3章中,我们已经看到,可以通过Object.prototype添加方法,可以让该方法对所有对象都有用。这样的方式对函数、数组、字符串、数字、正则表达式和 布尔值同样适用。

      举例来说,我们通过对Function.prototype增加方法来使该方法对所有函数可用:

    Function.prototype.method = function (name, func){
        this.prototype[name] = func;
        return this;
    };

      通过Function.prototype增加一个method方法,下次增加对象方法时便不必再输入prototype。

      JavaScript没有专门的整数类型,但有时候确实只需要数字中的整数部分。而JavaScript本身的取整方法略显丑陋。因此我们可以通过给Number.prototype增加一个integer方法来改善它。它会根据数字的正负来判断是使用Math.ceiling还是Math.floor。

    Number.method ('integer', function (){
        return Math[this < 0 ? 'ceil': 'floor'](this);
    });
    document.writeln((-10/3).integer());     // -3

      JavaScript缺少一个移除字符串首位空白的方法:

    String.method('trim', function (){
        return this.replace(/^\s+|\s+$/g, '');
    });
    document.writeln('"' + "  neat  ".trim() + '"');

      我们的trim方法使用了一个正则表达式,正则表达式的内容详见第七章的笔记。
      通过给基本类型增加方法,我们可以极大地提高语言的表现力。因为JavaScript原型继承的动态本质,新的方法立刻被赋予到所有的对象实例上,哪怕这个实例在这个新方法被增加之前就创造好了。
      基本类型的原型是公用结构,所以在类库混用时务必小心。保险的做法是只在确定没有改方法时才添加它:

    //符合条件时才增加方法。
    Function.prototype.method = function (name, func){
        if (!this.prototype[name]){
            this.prototype[name] = func;
        }
        return this;
    };

      另一个要注意的是for in 语句用在原型上时表现糟糕。第三章中提到减轻这种影响的方法:我们可以使用hasOwnProperty方法筛选出继承而来的属性,或者去查找特定的类型。


递归(Recursion)

      递归函数就是会直接或间接地调用自身的一种函数。一般来说,一个递归函数调用它自身去解决它的子问题。
      “汉诺塔”是一个著名益智游戏。塔上有三根柱子和一套直径各不相同的空心圆盘。开始时源柱子上所有圆盘都按照从小到大的顺序堆叠。目标是通过每次移动一个圆盘到另一根柱子,最终把一堆圆盘移动到目标桌子上,在操作过程中不能出现大圆盘在小圆盘之上的情况。这问题有一个寻常解:

    var hanoi = function (disc, src, aux, dst){
        if (disc > 0) {
            hanoi(disc - 1, src, dst, aux);
            document.writeln('Move disc ' + disc + ' from ' + src + ' to ' + dst);
            hanoi(disc - 1, aux, src, dst);
        }
    };
    hanoi(3, 'Src', 'Aux', 'Dst');

      圆盘数量为3时,它返回这样的解法:

    Move disc 1 from Src to Dst
    Move disc 2 from Src to Aux
    Move disc 1 from Dst to Aux
    Move disc 3 form src to Dst
    Move disc 1 from Aux to Src
    Move disc 2 from Aux to Dst
    Move disc 1 from src to Dst

      递归函数可以非常高效地操作树形结构,比如浏览器端的文档对象模型(DOM)。每次递归调用时处理指定的树的一小段。

    //定义walk_the_DOM函数,它从某个指定的节点开始,按HTML中的顺序访问
    //该树上的每个节点。
    //它会调用一个函数,并依此传递每个节点给它。walk_the_DOM再
    //调用自身去处理每一个节点。

    var walk_the_DOM = function walk(node, func){
        func(node);
        node = node.fisrtChild;
        while(node){
            walk(node, func);
            node = node.nextSibling;
        }
    };

    //定义getElementsByAttribute函数
    var getElementsByAttribute = function (att, value){
        var results = [];

        walk_the_DOM(document.body, function (node){
            var actual = node.nodeType === 1 && node.getAtrribute(att);
            if (typeof actual === 'string' && (actual === value || typeof value !== 'string')){
                results.push(node);
            }
        });
        return results;
    };

      一些语言提供了尾递归优化。JavaScript当前没有提供尾递归优化。

    //构建一个带尾递归的函数。因为它会返回自身调用的结果,所以它是尾递归。
    //JavaScript当前没有对这种形式的递归做出优化。
    var factorial = function factorial (i, a){
        a = a || 1;
        if (i < 2){
            return a;
        }
        return factorial(i - 1, a * i);
    };
    document.writeln(factorial(4));     //24



作用域(Scope)

       在编程语言中,作用域控制着变量与参数的可见性和生命周期。对程序员来说它是一项重要的服务,因为它减少了命名冲突,并且提供了自动内存管理。

    var foo = function (){
        var a = 3 , b = 5;
        var bar = function (){
            var b = 6, c = 8;
            //此时a是3,b是6,c是8。
            a += b + c;
            //此时a是17,b是6,c是8。
        };
        //此时a是3,b是5,c还没有定义。
        bar();
        //此时a是21,b是5。
    };

      大多数类C语言语法的语言都拥有块级作用域。尽管JavaScript的代码块语法貌似支持块级作用域,但实际上它并不支持。
      JavaScript确实有函数作用域。这意味着在函数中的变量和参数在函数外是不可见的,而在一个函数内任何位置定义的变量,在该函数内部任何地方都可见。


闭包(Closure)

      作用域的好处是内部函数可以访问定义它们的外部函数的变量和参数(this和arguments除外)。
      我们的getElementsByAttribute函数可以工作,是因为它函数中定义了一个results变量,而传递给walk_the_DOM的内部函数也可以访问results。
      更有趣的一个特点是,一个内部函数会比它的外部函数有更长的生命周期。
      之前我们构造了一个myObject对象,拥有一个value属性和一个increment方法。假定我们希望保护该值不会被非法更改。
      和以对象字面量形式去初始化myObject不同,我们通过一个函数调用的形式去初始化myObject,这个函数会返回一个对象字面量。函数里定义了一个value变量,它对于increment和getValue方法是可见的,但函数的作用域使得它对于其他函数是不可见的。

    var myObject = (function (){
        var value = 0;

        return {
            increment: function (inc){
                value += typeof inc === 'number' ? inc : 1;
            },
            getValue: function () {
                return value;
            }
        };
    }());

      在这里我们没有 把一个函数赋值给myObject。我们是把函数调用后的结果赋值给它。注意最后一行的()。
       本章之前的Quo构造器产生一个拥有status属性和一个get_status方法的对象。但好像没有太大意义,为什么要用一个getter方法去访问本来就能访问到的属性呢(第四章函数——构造器调用模式)?如果status是私有属性,这个访问status的方法才更有意义:

    //创建一个名为quo的构造函数。
    //它构造出带有get_status方法和status私有属性的一个对象。
    var quo = function (status){
        return {
            get_status: function (){
                return status;
            }
        };
    };
    var myQuo = new  quo("amazed");
    document.writeln(myQuo.get_status());

      这个quo被设计成无需其前面加上new,所以首字母没有大写。当我们调用quo时,它返回包含了get_status方法的一个新对象,该对象的一个引用保存在myQuo中。即使quo已经返回了,但是get_status仍然享有访问quo对象status属性的特权。get_status访问的不是该参数的副本,它访问的就是函数本身。这是可能的, 因为该函数可以访问它被创建时所处的上下文环境。这称作闭包。

来看下一个更有用的例子:

    //定义一个函数,它设置一个节点为黄色,然后再把它渐变成白色。
    var fade = function (){
        var level = 1;
        var step = function (){
            var hex = level.toString(16);
            node.style.background = '#FFFF' + hex + hex;
            if (level < 15){
                level += 1;
                setTimeout(step, 100);
            }
        };
        setTimeout(step, 100);
    };
    fade(document.body);

      我们调用fade,把document.body作为参数传递给它。fade函数设置level为1。它定义了一个step函数;接着调用setTimeout函数,并传递 step一个函数和一个时间(100毫秒)给它。然后它返回,fade函数结束。
      在大约0.1秒后,step函数被调用。它把fade函数的level变量转化成了16位字符。接着它修改fade函数获得节点的背景颜色。如果背景色仍未变成白色,那么增大level的值,然后用setTimeout来使它自己再次运行。
      step函数再次被调用,但这时fade函数的level值变成了2。fade函数在之前已经返回了,但只要fade的内部函数需要,它的变量就会继续保持。
正确的闭包:

    var add_the_handlers = function (node){
        var helper = function (i){
            return function (e){
                alert(i);
            };
        };
        var i;
        for (i = 0;i < node.length; i++){
            node[i].onclick = helper(i);
        }
    };

      避免在循环中创建函数,它可能只会带来无谓的计算,还会引起混淆。我们可以在循环之前先创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i的值的函数,这样就不会导致混淆了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值