JavaScript 基础与提高

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

2017-02-07  本文已影响42人  soojade

第四章 挥舞函数

在 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 属性不同。该属性的值等于该函数声明时所需要传入的形参数量。

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

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

函数重载的方法有:

/**
 * 定义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()的原因有两个:

上一篇下一篇

猜你喜欢

热点阅读