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

第四章 挥舞函数

在 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()方法。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容