美文网首页JavaScript 基础与提高
JavaScript忍者秘籍笔记——挥舞函数

JavaScript忍者秘籍笔记——挥舞函数

作者: soojade | 来源:发表于2017-02-07 18:43 被阅读42次

    第四章 挥舞函数

    在 Web 程序开发中,函数式编程风格是我们所要做事情的重要核心。为了不必要的函数名称污染全局命名空间,将创建大量小型函数进行传递。利用匿名函数进行函数式编程,可以解决在 JavaScript 开发时所面临的很多挑战。

    递归

    当函数调用自身,或调用另外一个函数,但这个函数的调用树种的某个地方又调用了自己时,递归就发生了。

    利用递归检测回文

    • 单个和零个字符都是一个回文。
    • 如果字符串的第一个字符和最后一个字符相同,并且除了两个字符以为剩余的其它字符也是一个回文的话,原字符串是一个回文。
    function isPalindrome(text) {
        if (typeof text === "null" || typeof text === "undefined") return false;
        var text = text.toString();
        if (text.length <= 1) return true;
        if (text.charAt(0) != text.charAt(text.length - 1)) return false;
        return isPalindrome(text.substr(1, text.length - 2));
    }
    

    函数递归的两个条件:引用自身,并且有终止条件。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>使用命名函数发出啾啾声</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
        <script>
            function chirp(n){
                return n>1 ? chirp(n-1)+"-chirp" : "chirp";
            }
    
            assert(chirp(3) === "chirp-chirp-chirp", "Calling the named function comes naturally.");
    
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    方法中的递归

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>对象中的方法递归</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
        <script>
            var ninja = {
                chirp: function(n){
                    return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
                }
            }
    
            assert(ninja.chirp(3) === "chirp-chirp-chirp", "An object property isn't too confusing,erither.");
    
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    我们在函数上使用了非直接引用——也就是ninja对象的chirp属性——所以才能进行递归。但这会有问题。

    修改一下代码,添加一个新的对象samurai,该对象也引用了ninja对象上的匿名递归函数。

    var ninja = {
        chirp: function(n){
            return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
        }
    }
    
    var samurai = {chirp: ninja.chirp};
    // 重新定义ninja对象,去除所有属性
    ninja = {};
    
    try{
        assert(samurai.chirp(3) === "chirp-chirp-chirp", "Is this going to work?");
    }catch(e){
        assert(false, "Uh,this isn't good! where'd ninja.chirp go?");
    }
    

    上述代码中,重新给ninja定义一个空对象,但匿名函数仍然存在,而且可以通过samurai.chirp属性进行引用,但是ninja.chirp属性却已不存在。修复这个问题,可以在匿名函数中不再使用显式的ninja引用,而是使用函数上下文this进行引用,如下所示:

    var ninja = {
        chirp: function(n){
            // return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
            // 修复引用丢失 使用 this 进行引用
            return n>1 ? this.chirp(n-1)+"-chirp" : "chirp";
        }
    }
    

    不管是不是作为递归进行调用,当一个函数作为方法被调用时,函数上下文 this 指的是调用该方法的那个对象。

    内联命名函数

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>使用内联函数进行递归</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
        <script>
            var ninja = {
                chirp: function signal(n){ // 内联函数
                    return n>1 ? signal(n-1)+"-chirp" : "chirp";
                }
            }
            assert(ninja.chirp(3) === "chirp-chirp-chirp", "Works as we would expect it to!");
    
            // 创建新对象
            var samurai = {chirp: ninja.chirp};
            // 重新定义ninja对象,去除所有属性
            ninja = {};
    
            assert(samurai.chirp(3) === "chirp-chirp-chirp", "The method correctly calls itself.");
            
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    还可以将内联函数用于普通的变量赋值:

    // 声明一个命名内联函数,并将其赋值给一个变量
    var ninja = function myNinja(){
        // 在内联函数中,验证两个名字是等价的
        assert(ninja === myNinja, "This function is named two things at once!");
    };
    ninja(); // 调用函数执行内部验证
    
    assert(typeof myNinja === "undefined", "But myNinja isn't defined outside of the function.");
    

    上述代码展示了内联函数最重要的一点:尽管可以给内联函数进行命名,但这些名称只能在自身函数内部可见。内联函数的名称和变量名称有点像,它们的作用域仅限于声明它们的函数。

    这就是为什么要将全局函数作为 window 的方法进行创建的原因。不使用 window 的属性,没有办法引用这些函数。

    已经被移除的callee属性

    警告:ECMAScript 5 禁止在严格模式中使用 arguments.callee()。当一个函数必须调用自身的时候,假如它是函数表达式则给它命名,或者使用函数声明,避免使用 arguments.callee()

    var ninja = {
        chirp: function(n){
            return n>1 ? arguments.callee(n-1)+"-chirp" : "chirp";
        }
    };
    

    将函数视为对象

    有时候,我们可能需要存储一组相关又独立的函数,事件回调管理是最明显的例子。向这个集合添加函数时,面临的挑战是要确定哪些函数在集合中不存在,应该添加,哪些函数已经存在而不需要添加。

    利用函数的属性特性,给函数添加一个附加属性从而实现上述目的。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>存储一组独立的函数</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
        <script>
           var store = {
               nextId: 1,
               cache: {}, // 创建一个对象作为缓存,存储函数
               // 向缓存中添加函数,但只有缓存不存在的情况下才能添加成功
               add: function(fn){
                if(!fn.id){
                    fn.id = store.nextId++;
                    return !!(store.cache[fn.id] = fn); // !!可以将任意js表达式转化为bool值
                }
               }
           };
           function ninja(){}
           assert(store.add(ninja), "Function was safely added.");
           assert(!store.add(ninja), "But it wwas only added once.");
           
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    另一有用的技巧是,通过暴露函数属性,可以对函数自身进行修改。缓存记忆是构建函数的过程,这种函数能够记住先前计算的结果。以下以判断素数作为演示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>记忆之前计算过的值</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
        <script>
           function isPrime(value){
               if(!isPrime.answers) isPrime.answers = {}; // 创建缓存
               // 检查缓存过的值
               if(isPrime.answers[value] != null){
                   return isPrime.answers[value];
               }
               var prime = value != 1;
               for(var i=2; i<value; i++){
                    if(value % i == 0){
                        prime = false;
                        break;
                    }
               }
               // 保存计算出的值
               return isPrime.answers[value] = prime;
           }
    
           assert(isPrime(5), "5 is prime!");
           assert(isPrime.answers[5], "The answer was cached!");
    
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    缓存记忆DOM元素:

    function getElements(name){
        if(!getElements.cache) getElements.cache = {};
        return getElements.cache[name] = getElements.cache[name] || document.getElementsByTagName(name);
    }
    

    伪造数组方法

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>模拟类似数组的方法</title>
        <style>
            li.pass{color: green;}
            li.fail{color: red;text-decoration:line-through; }
        </style>
    </head>
    <body>
        <ul id="results"></ul>
    
        <input type="text" id="first">
        <input type="text" id="second">
        <script>
           var elems = {
               length: 0, // 保存元素个数
               // 添加元素
               add: function(elem){
                   Array.prototype.push.call(this,elem);
               },
               // 根据id查找元素并添加到集合中
               gather: function(id){
                   this.add(document.getElementById(id));
               }
           };
           elems.gather("first");
    
           assert(elems.length == 1 && elems[0].nodeType, "Verify that we have an element in our stash");
    
           elems.gather("second");
           assert(elems.length == 2 && elems[1].nodeType, "Verify The other insertion");
    
            // 创建断言函数
            function assert(value,desc){
                var li = document.createElement('li');
                li.className = value ? 'pass' : 'fail';
                li.appendChild(document.createTextNode(desc));
                document.getElementById("results").appendChild(li);
            }
        </script>
    </body>
    </html>
    

    可变长度的参数列表

    使用apply()将数组作为一个可变长度的参数列表,实现判断数组的最大值与最小值。

    function smallest(array){
        return Math.min.apply(Math,array);
    }
    
    function largest(array){
        return Math.max.apply(Math,array);
    }
    

    使用arguments实现函数重载:

    function merge(root){
        // 跳过第一个参数,索引从1开始
        for(var i=1; i<arguments.length; i++){
            for(var key in arguments[i]){
                root[key] = arguments[i][key];
            }
        }
        return root;
    }
    

    对arguments列表进行切片:

    function multiMax(multi){
        // 由于arguments并不是数组,所以不能直接使用slice
        return multi * Math.max.apply(Math, Array.prototype.slice.call(arguments,1));
    }
    

    函数的 length 属性

    函数的 length 属性和 arguments 的 length 属性不同。该属性的值等于该函数声明时所需要传入的形参数量。

    对于一个函数,在参数方面可以确定两件事:

    • 通过 length 属性,可以知道形参数量。
    • 通过 arguments.length,可以知道实参数量。

    利用参数个数进行函数重载

    函数重载的方法有:

    • 根据传入参数的类型执行不同的操作。
    • 通过某些特定参数是否存在来进行判断。
    • 通过传入参数的个数进行判断。
    /**
     * 定义addMethod方法接收三个参数:
     * 1. 要绑定方法的对象
     * 2. 绑定发放所用的属性名
     * 3. 要绑定的方法
     */
    function addMethod(object, name, fn) {
        // 保存原有的函数,调用的时候可能不匹配传入的参数个数
        var old = object[name];
    
        object[name] = function () {
            // 如果该匿名函数的形参个数和实参个数匹配,就调用该函数
            if (fn.length == arguments.length) {
                return fn.apply(this, arguments);
            } else if (typeof old === 'function') {
                return old.apply(this, arguments);
            }
        }
    }
    
    var ninja = {};
    addMethod(ninja, 'whatever', function () {/**/ });
    addMethod(ninja, 'whatever', function (a) {/**/ });
    addMethod(ninja, 'whatever', function (a, b) {/**/ });
    

    函数判断

    function ifFunction(fn){
        return Object.prototype.toString.call(fn) === "[object Function]";
    }
    

    访问Object.prototype的内部toString()方法。在默认情况下,这个特殊方法是用来返回表示一个对象的内部描述的字符串(如Function或String)。利用该方法,我们可以在任何对象上调用它,从而获得对象真正的类型。这种技术不仅可以判断是不是函数,还可以判断 String、RegExp、Date或其他对象。

    上述代码中不直接调用 fn.toString()的原因有两个:

    • 不同的对象可能有自己的toString()方法实现。
    • JavaScript中的大多数类型都已经有一个预定义的toString()方法覆盖了Object.prototype提供的toString()方法。

    相关文章

      网友评论

        本文标题:JavaScript忍者秘籍笔记——挥舞函数

        本文链接:https://www.haomeiwen.com/subject/hmlpittx.html