深入理解 JavaScript 函數(shù)的特性與最佳實(shí)踐

函數(shù)用于指定對(duì)象的行為。所謂的編程询筏,就是將一組需求分解為一組函數(shù)和數(shù)據(jù)結(jié)構(gòu)的技能破花。

1 函數(shù)對(duì)象

JavaScript 函數(shù)就是對(duì)象。對(duì)象是名值對(duì)的集合玉控,它還擁有一個(gè)連接到原型對(duì)象的鏈接飞主。對(duì)象字面量產(chǎn)生的對(duì)象連接到 Object.prototype,而函數(shù)對(duì)象連接到 Function.prototype(這個(gè)對(duì)象本身連接到 Object.prototype)高诺。每個(gè)函數(shù)在創(chuàng)建時(shí)會(huì)附加兩個(gè)隱藏屬性:函數(shù)的上下文以及實(shí)現(xiàn)函數(shù)的代碼碌识。

函數(shù)對(duì)象在創(chuàng)建后會(huì)有一個(gè) prototype 屬性,它的值是一個(gè)擁有 constructor 屬性虱而、且值既是該函數(shù)的對(duì)象筏餐。

因?yàn)楹瘮?shù)是對(duì)象,所以可以被當(dāng)做參數(shù)傳遞給其他函數(shù)牡拇。它也可以再返回函數(shù)魁瞪。

2 函數(shù)字面量

函數(shù)可以通過字面量進(jìn)行創(chuàng)建:

var add = function (a, b) {
    return a + b;
}

這里沒有給函數(shù)命名穆律,所以稱它為匿名函數(shù)。

一個(gè)內(nèi)部函數(shù)除了可以訪問自己的參數(shù)和變量之外导俘,還可以訪問它的父函數(shù)的參數(shù)和變量众旗。通過函數(shù)字面量創(chuàng)建的函數(shù)對(duì)象包含一個(gè)連接到外部上下文的連接,這被稱為閉包趟畏。它是 JavaScript 強(qiáng)大表現(xiàn)力的來源贡歧。

3 調(diào)用

調(diào)用一個(gè)函數(shù)會(huì)暫停當(dāng)前函數(shù)的執(zhí)行,它會(huì)傳遞控制權(quán)和參數(shù)給這個(gè)被調(diào)用的函數(shù)赋秀。

當(dāng)函數(shù)的實(shí)際參數(shù)的個(gè)數(shù)與形式參數(shù)的個(gè)數(shù)不匹配時(shí)利朵,不會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。如果實(shí)際參數(shù)的個(gè)數(shù)過多猎莲,那么超出的參數(shù)會(huì)被忽略绍弟;如果實(shí)際參數(shù)的個(gè)數(shù)過少,那么缺失的值會(huì)是 undefined著洼。不會(huì)對(duì)參數(shù)類型進(jìn)行檢查樟遣,所以任何類型的值都可以被傳遞給任何參數(shù)。

3.1 方法調(diào)用模式

當(dāng)一個(gè)函數(shù)被保存為對(duì)象的一個(gè)屬性時(shí)身笤,就稱它為方法豹悬。當(dāng)方法被調(diào)用時(shí),this 被綁定到這個(gè)對(duì)象液荸。如果調(diào)用表達(dá)式包含一個(gè)提取屬性的動(dòng)作(即包含一個(gè)"." 點(diǎn)表達(dá)式或 "[]" 下標(biāo)表達(dá)式)瞻佛,那么它就是被當(dāng)做一個(gè)方法被調(diào)用。

var myObject = {
    value: 0,//屬性
    increment: function (inc) {//方法
        this.value += typeof inc === 'number' ? inc : 1;
    }
};

myObject.increment();
console.log(myObject.value);//1

myObject.increment(2);
console.log(myObject.value);//3

這里可以使用 this 來訪問自己所屬的對(duì)象娇钱。通過 this 可以取得它們所屬對(duì)象的上下文的方法被稱為公共方法伤柄。

3.2 函數(shù)調(diào)用模式

var add = function (a, b) {
    return a + b;
}
var sum = add(3, 4);//7;this 被綁定到全局對(duì)象

這里的 this 被綁定到全局對(duì)象文搂,這其實(shí)是語言設(shè)計(jì)上的失誤适刀!如果設(shè)計(jì)正確,那么當(dāng)內(nèi)部函數(shù)被調(diào)用時(shí)煤蹭,this 應(yīng)該被綁定到外部函數(shù)的 this 變量才是笔喉。可以這樣解決:為這個(gè)方法定義一個(gè)變量并給它賦值為 this疯兼,這樣內(nèi)部函數(shù)就可以通過這個(gè)變量訪問到 this 啦然遏,一般把這個(gè)變量命名為 that:

myObject.double = function () {
    var that = this;//讓內(nèi)部函數(shù)可以通過這個(gè)變量訪問到 this (myObject)

    var helper = function () {
        that.value = add(that.value, that.value);
    };

    helper();//以函數(shù)形式調(diào)用 helper
};

myObject.double();//以方法形式調(diào)用 helper
console.log(myObject.value);//6

3.3 構(gòu)造器調(diào)用模式

JavaScript 是基于原型繼承的語言,所以對(duì)象可以從其他對(duì)象繼承它們的屬性吧彪。

如果在函數(shù)之前加上 new 待侵,那么 JavaScript 就會(huì)創(chuàng)建一個(gè)連接到該函數(shù)的 prototype 屬性的新對(duì)象,而 this 會(huì)綁定到這個(gè)新對(duì)象姨裸。

/**
 * 構(gòu)造器調(diào)用模式(不推薦)
 */
var Quo = function (string) {//定義構(gòu)造器函數(shù)秧倾;按照約定怨酝,變量名首字母必須大寫
    this.status = string;//屬性
};

/**
 * 為 Quo 的所有實(shí)例提供一個(gè)名為 get_status 的公共方法
 * @returns {*}
 */
Quo.prototype.get_status = function () {
    return this.status;
};

var myQuo = new Quo("confused");//定義一個(gè) Quo 實(shí)例
console.log(myQuo.get_status());//"confused"

按照約定,構(gòu)造器函數(shù)被保存在以大寫字母命名的變量中那先。因?yàn)槿绻{(diào)用構(gòu)造器函數(shù)時(shí)沒有加上 new农猬,問題很大,所以才以大寫字母的命名方式讓大家記住調(diào)用時(shí)要加上 new售淡。

3.4 Apply 調(diào)用模式

因?yàn)?JavaScript 是函數(shù)式的面向?qū)ο笳Z言斤葱,所以函數(shù)可以擁有方法。

apply 方法可以構(gòu)建一個(gè)參數(shù)數(shù)組揖闸,然后再傳遞給被調(diào)用的函數(shù)揍堕。這個(gè)方法接收兩個(gè)參數(shù):要綁定給 this 的值以及參數(shù)數(shù)組。

//相加
var array = [3, 4];
var sum = add.apply(null, array);//7
console.log(sum);

//調(diào)用 Quo 的 get_status 方法汤纸,給 this 綁定 statusObject 上下文
var statusObject = {
    status: 'A-OK'
};

var status = Quo.prototype.get_status.apply(statusObject);
console.log(status);//'A-OK'

4 參數(shù)

當(dāng)函數(shù)被調(diào)用時(shí)衩茸,會(huì)有一個(gè) arguments 數(shù)組。它是函數(shù)被調(diào)用時(shí)贮泞,傳遞給這個(gè)函數(shù)的參數(shù)列表楞慈,包含那些傳入的、多出來的參數(shù)啃擦∧依叮可以利用這一點(diǎn),編寫一個(gè)無須指定參數(shù)個(gè)數(shù)的函數(shù):

//構(gòu)造一個(gè)能夠接收大量參數(shù)议惰,并相加的函數(shù)
var sum = function () {
    var i, sum = 0;
    for (i = 0; i < arguments.length; i += 1) {
        sum += arguments[i];
    }
    return sum;
};
console.log(sum(4, 5, 6, 7, 8, 9));//39

arguments 不是一個(gè)真正的數(shù)組慎颗,它只是一個(gè)類數(shù)組的對(duì)象,它擁有 length 屬性言询,但沒有數(shù)組的相關(guān)方法。

5 返回

return 語句可以讓函數(shù)提前返回傲宜。return 被執(zhí)行時(shí)运杭,函數(shù)會(huì)立即返回。

一個(gè)函數(shù)總會(huì)返回一個(gè)值函卒,如果沒有指定這個(gè)值辆憔,它就會(huì)返回 undefined。

如果使用 new 前綴來調(diào)用一個(gè)函數(shù)报嵌,那么它的返回值是:創(chuàng)建的一個(gè)連接到該函數(shù)的 prototype 屬性的新對(duì)象虱咧。

6 異常

異常是干擾程序正常流程的事故。發(fā)生事故時(shí)锚国,我們要拋出一個(gè)異常:

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

throw 語句會(huì)中斷函數(shù)的執(zhí)行腕巡,它要拋出一個(gè) exception 對(duì)象,這個(gè)對(duì)象包含一個(gè)用來識(shí)別異常類型的 name 屬性和一個(gè)描述性的 message 屬性血筑。也可以根據(jù)需要绘沉,擴(kuò)展這個(gè)對(duì)象煎楣。

這個(gè) exception 對(duì)象會(huì)被傳遞到 try 語句的 catch 從句:

var try_it = function () {
    try {
        add("seven");
    } catch (e) {
        console.log(e.name + ": " + e.message);
    }
};
try_it();

一個(gè) try 語句只會(huì)有一個(gè)捕獲所有異常的 catch 從句。所以如果處理方式取決于異常的類型车伞,那么我們就必須檢查異常對(duì)象的 name 屬性择懂,來確定異常的類型。

7 擴(kuò)充類型的功能

可以給 Function.prototype 增加方法來使得這個(gè)方法對(duì)所有的函數(shù)都可用:

/**
 * 為 Function.prototype 新增 method 方法
 * @param name 方法名稱
 * @param func 函數(shù)
 * @returns {Function}
 */
Function.prototype.method = function (name, func) {
    if (!this.prototype[name])//沒有該方法時(shí)另玖,才添加
        this.prototype[name] = func;
    return this;
};

通過這個(gè)方法困曙,我們給對(duì)象新增方法時(shí),就可以省去 prototype 字符啦O(∩_∩)O~

有時(shí)候需要提取數(shù)字中的整數(shù)部分谦去,我們可以為 Number.prototype 新增一個(gè) integer 方法:

Number.method('integer', function () {
    return Math[this < 0 ? 'ceil' : 'floor'](this);
});

它會(huì)根據(jù)數(shù)字的正負(fù)來決定是使用 Math.ceiling 還是 Math.floor赂弓。

然后再為 String 添加一個(gè)移除字符串首尾空白的方法:

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

這里使用了正則表達(dá)式。

通過為基本類型增加方法哪轿,可以極大地提高 JavaScript 的表現(xiàn)力盈魁。因?yàn)樵屠^承的動(dòng)態(tài)本質(zhì),新的方法立刻被賦予所有的對(duì)象實(shí)例上(甚至包括那些在方法被增加之前的那些對(duì)象實(shí)例)

基本類型的原型是公用的窃诉,所以在使用其他類庫時(shí)要小心杨耙。一個(gè)保險(xiǎn)的做法是:只在確定沒有該方法時(shí)才添加它。

Function.prototype.method = function (name, func) {
    if (!this.prototype[name])//沒有該方法時(shí)飘痛,才添加
        this.prototype[name] = func;
    return this;
};

8 遞歸

遞歸函數(shù)是會(huì)直接或間接地調(diào)用自身的函數(shù)珊膜。它會(huì)把一個(gè)問題分解為一組相似的子問題,而每一個(gè)子問題都會(huì)用一個(gè)尋常的解來解決宣脉。

漢諾塔的游戲規(guī)則是:塔上有 3 根柱子和一套直徑不相同的空心圓盤车柠。開始時(shí),源柱子上的所有圓盤都是按照從小到大的順序堆疊的塑猖。每次可以移動(dòng)一個(gè)圓盤到另一個(gè)柱子竹祷,但不允許把較大的圓盤放置在嬌小的圓盤之上。最終的目標(biāo)是把一堆圓盤移動(dòng)到目標(biāo)柱子上羊苟。我們可以用遞歸解決這個(gè)問題:

/**
 * 漢若塔
 * @param disc 圓盤編號(hào)
 * @param src 源柱子
 * @param aux 輔助用的柱子
 * @param dst 目的柱子
 */
var hanoi = function (disc, src, aux, dst) {
    if (disc > 0) {
        hanoi(disc - 1, src, dst, aux);
        console.log('Move disc ' + disc + ' from ' + src + ' to ' + dst);
        hanoi(disc - 1, aux, src, dst);
    }
};
hanoi(3, 'Src', 'Aux', 'Dst');

這里演示了如果圓盤數(shù)為 3 的解法:

這個(gè)問題可以分解為 3 個(gè)子問題塑陵。首先移動(dòng)一對(duì)圓盤中較小的圓盤到輔助的柱子上,從而露出下面較大的圓盤蜡励;然后再移動(dòng)下面的圓盤到目標(biāo)柱子令花。最后再將較小的圓盤從輔助柱子再移動(dòng)到目標(biāo)柱子上。通過遞歸調(diào)用自身來處理圓盤的移動(dòng)凉倚,就可以解決這些子問題兼都。

上面這個(gè)函數(shù)最終會(huì)以一個(gè)不存在的圓盤編號(hào)被調(diào)用,但它不執(zhí)行任何操作稽寒,所以不會(huì)導(dǎo)致死循環(huán)扮碧。

遞歸函數(shù)可以非常高效地操作樹型結(jié)構(gòu)。比如文檔對(duì)象模型(DOM),我們可以在每次遞歸調(diào)用時(shí)處理指定樹的一小段:

/**
 * 從某個(gè)節(jié)點(diǎn)開始瓦胎,按照 HTML 源碼中的順序芬萍,訪問該樹的每一個(gè)節(jié)點(diǎn)
 * @param node 開始的節(jié)點(diǎn)
 * @param func 被訪問到的每一個(gè)節(jié)點(diǎn)尤揣,會(huì)作為參數(shù)傳入這個(gè)函數(shù),然后這個(gè)函數(shù)被調(diào)用
 */
var walk_the_DOM = function walk(node, func) {
    func(node);
    node = node.firstChild;
    while (node) {
        walk(node, func);
        node = node.nextSibling;
    }
};
/**
 * 查找擁有某個(gè)屬性的元素
 * @param att 屬性名稱字符串
 * @param value 匹配值(可選)
 * @return 匹配的元素?cái)?shù)組
 */
var getElementsByAttributes = function (att, value) {
    var results = [];

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

注意: 深度遞歸的函數(shù)會(huì)因?yàn)槎褩R绯龆\(yùn)行失敗柬祠。比如一個(gè)會(huì)返回自身調(diào)用函數(shù)結(jié)果的函數(shù)北戏,它被稱為尾遞歸函數(shù)。

/**
 * 求階乘(帶尾遞歸的函數(shù))
 *
 * JavaScript 當(dāng)前沒有對(duì)尾遞歸進(jìn)行優(yōu)化漫蛔,所以如果遞歸過深會(huì)導(dǎo)致堆棧溢出
 * @param i
 * @param a
 * @returns {*} 返回自身調(diào)用的結(jié)果
 */
var factorial = function factorial(i, a) {
    a = a || 1;
    if (i < 2) {
        return a;
    }
    return factorial(i - 1, a * i);
};
console.log(factorial(4));

9 作用域

作用域控制著變量和參數(shù)的可見性以及生命周期嗜愈,它減少了名稱沖突,而且提供自動(dòng)內(nèi)存管理機(jī)制莽龟。

var foo = function () {
    var a = 3, b = 5;

    var bar = function () {
        var b = 7, c = 11;
        console.log("a:" + a + ";b:" + b + ";c:" + c);//a:3;b:7;c:11
        a += b + c;
        console.log("a:" + a + ";b:" + b + ";c:" + c);//a:21;b:7;c:11
    };

    console.log("a:" + a + ";b:" + b);//a:3;b:5
    bar();
    console.log("a:" + a + ";b:" + b);//a:21;b:5
};
foo();

JavaScript 支持函數(shù)作用域蠕嫁,但要注意一點(diǎn),就是在一個(gè)函數(shù)內(nèi)部的任何位置定義的變量毯盈,都這個(gè)函數(shù)的任何地方都是可見的剃毒!

注意:JavaScript 不支持塊級(jí)作用域。所以最好的做法是在函數(shù)體的頂部搂赋,聲明函數(shù)中可能會(huì)用到的所有變量赘阀。

10 閉包

作用域的好處是:內(nèi)部函數(shù)可以訪問定義它們外部函數(shù)的參數(shù)和變量(除了 this 和 arguments)。

注意:內(nèi)部函數(shù)擁有比它的外部函數(shù)更長的生命周期脑奠。

var myObject = (function () {
    var value = 0;//只對(duì) increment 與 getValue 可見

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

}());

通過一個(gè)函數(shù)的形式來初始化 myObject 對(duì)象基公,它會(huì)返回一個(gè)對(duì)象字面量。函數(shù)定義了一個(gè) value 變量宋欺,這個(gè)變量對(duì) increment 和 getValue 方法總是可見的轰豆,但它的函數(shù)作用域卻使得這個(gè)變量對(duì)其他的代碼來說是不可見的!

**
 * quo 構(gòu)造函數(shù)
 * @param status 私有屬性
 * @returns {{get_status: Function}}
 */
var quo = function (status) {
    return {
        get_status: function () {//方法
            return status;
        }
    };
};
var myQuo = quo("amazed");
console.log(myQuo.get_status());//amazed

當(dāng)我們調(diào)用 quo 時(shí)齿诞,它會(huì)返回一個(gè)包含 get_status 方法的新對(duì)象酸休,它的引用保存在 myQuo 中。所以即使 quo 函數(shù)已經(jīng)返回了掌挚,但 get_status 方法仍然享有訪問 quo 對(duì)象的 status 屬性的特權(quán)雨席。get_status 方法訪問的可是 status 屬性本身,這就是閉包哦O(∩_∩)O~

再看一個(gè)例子:

/**
 * 設(shè)置一個(gè) DOM 節(jié)點(diǎn)為黃色吠式,然后漸變?yōu)榘咨? * @param node
 */
var fade = function (node) {
    var level = 1;
    var step = function () {
        var hex = level.toString(16);//轉(zhuǎn)換為 16 位字符
        node.style.backgroundColor = '#FFFF' + hex + hex;
        if (level < 15) {
            level += 1;
            setTimeout(step, 100);
        }
    };
    setTimeout(step, 100);
};
fade(document.body);

fade 函數(shù)在最后一行被調(diào)用后已經(jīng)返回,但只要 fade 的內(nèi)部函數(shù)又需要抽米,它的變量就會(huì)持續(xù)保留特占。

注意:內(nèi)部函數(shù)能夠訪問外部函數(shù)的實(shí)際變量:

var add_the_handlers_error = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (e) {
            alert(i);//綁定的是變量 i 本身,而不是函數(shù)在構(gòu)造時(shí)的變量 i 的值T迫住J悄俊!
        }
    }
};

add_the_handlers_error 函數(shù)的本意是:想傳遞給每個(gè)事件處理器一個(gè)唯一的 i 值标捺,但因?yàn)槭录幚砥骱瘮?shù)綁定了變量 i 本身懊纳,而不是它的值揉抵!

/**
 * 給數(shù)組中的節(jié)點(diǎn)設(shè)置事件處理程序(點(diǎn)擊節(jié)點(diǎn),會(huì)彈出一個(gè)顯示節(jié)點(diǎn)序號(hào)的對(duì)話框)
 * @param nodes
 */
var add_the_handlers = function (nodes) {
    var helper = function (i) {//輔助函數(shù)嗤疯,綁定了當(dāng)前的 i 值
        return function (e) {
            alert(i);
        };
    };
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = helper(i);
    }
};

我們在循環(huán)之外向構(gòu)造一個(gè)輔助函數(shù)冤今,讓這個(gè)函數(shù)返回一個(gè)綁定了當(dāng)前 i 值的函數(shù),這樣就可以解決問題啦O(∩_∩)O~

11 回調(diào)

假設(shè)用戶觸發(fā)了一個(gè)請(qǐng)求茂缚,瀏覽器向服務(wù)器發(fā)送這個(gè)請(qǐng)求戏罢,然后最終顯示服務(wù)器的響應(yīng)結(jié)果:

request = prepare_the_request();
response = send_request_synchronously(request);
display(response);

這種方式的問題在于,網(wǎng)絡(luò)上的同步請(qǐng)求可能會(huì)導(dǎo)致客戶端進(jìn)入假死狀態(tài)脚囊。

所以建議使用異步請(qǐng)求龟糕,并為服務(wù)端的響應(yīng)創(chuàng)建一個(gè)回調(diào)函數(shù)。這個(gè)異步請(qǐng)求函數(shù)會(huì)立即返回悔耘,這樣我們的客戶端就不會(huì)被阻塞啦:

request = prepare_the_request();
send_request_asynchronously(request, function(response){
  display(response);
)};

一旦接收到服務(wù)端的響應(yīng)讲岁,傳給 send_request_asynchronously 的匿名函數(shù)就會(huì)被調(diào)用啦O(∩_∩)O~

12 模塊模式

模塊是一個(gè)提供接口但卻隱藏狀態(tài)與實(shí)現(xiàn)的函數(shù)〕囊裕可以使用函數(shù)和閉包來構(gòu)建模塊缓艳。通過函數(shù)來生成模塊,就可以不用全局變量啦泄鹏。

假設(shè)我們想給 String 增加一個(gè) deentityify 方法郎任。它會(huì)尋找字符串中的 HTML 字符,并把它們替換為對(duì)應(yīng)的字符备籽。這就需要在對(duì)象中保存字符實(shí)體的名字和它對(duì)應(yīng)的字符舶治。不能用全局變量,因?yàn)樗悄Ч沓碘∪绻x在函數(shù)內(nèi)部霉猛,那么就會(huì)帶來運(yùn)行時(shí)的損耗,因?yàn)槊看螆?zhí)行函數(shù)時(shí)珠闰,這個(gè)字面量就會(huì)被求值一次惜浅。所以理想的方式是把它放入閉包:

String.method('deentityify', function () {
    //字符實(shí)體表:映射字符實(shí)體的名字到對(duì)應(yīng)的字符
    var entity = {
        quot: '"',
        lt: '<',
        gt: '>'
    };

    //返回 deentityify 方法
    return function () {
        /**
         * 返回以'&'開頭 和 以';'結(jié)尾的子字符串
         */
        return this.replace(/&([^$;]+);/g, function (a, b) {
            var r = entity[b];//b:映射字符實(shí)體名字
            return typeof r === 'string' ? r : a;//a:原始字符串
        });
    };
}());

注意:最后一行是 (),所以我們是立即調(diào)用剛剛創(chuàng)建的函數(shù)伏嗜!

模塊模式利用函數(shù)的作用域和閉包來創(chuàng)建被綁定對(duì)象和私有成員的關(guān)聯(lián)坛悉。這個(gè)例子中只有 deentityify 方法才有權(quán)訪問 entity (字符實(shí)體表)。

模塊模式是一個(gè)定義了私有變量和函數(shù)的函數(shù)承绸。先利用閉包創(chuàng)建一個(gè)可以訪問私有變量和函數(shù)的特權(quán)函數(shù)裸影,最后返回這個(gè)函數(shù)军熏,或者把它保存到一個(gè)可以被訪問到的地方。

模塊模式做到了信息隱藏均践,是一種優(yōu)秀的設(shè)計(jì)實(shí)踐晤锹,特別適合于封裝應(yīng)用程序或者構(gòu)造單例哦O(∩_∩)O~

模塊模式還可以產(chǎn)生安全對(duì)象。比如我們想構(gòu)造一個(gè)能夠產(chǎn)生序列號(hào)的對(duì)象:

/**
 * 產(chǎn)生唯一字符串的對(duì)象(安全的對(duì)象)
 * 唯一字符串由 (前綴 + 序列號(hào)) 組成
 * @returns {{set_prefix: Function, set_seq: Function, gensym: Function}}
 */
var serial_number = function () {
    var prefix = '';
    var seq = 0;
    return {
        /**
         * 設(shè)置前綴
         * @param p
         */
        set_prefix: function (p) {
            prefix = String(p);
        },
        /**
         * 設(shè)置序列號(hào)
         * @param s
         */
        set_seq: function (s) {
            seq = s;
        },
        /**
         * 產(chǎn)生唯一的字符串
         * @returns {string}
         */
        gensym: function () {
            var result = prefix + seq;
            seq += 1;
            return result;
        }
    };
};

var seqer = serial_number();
seqer.set_prefix('Q');
seqer.set_seq(1000);
var unique = seqer.gensym();
console.log(unique);//Q1000

sequer 包含的方法沒有用到 this 或 that鞭铆,所以很安全葫慎。sequer 是一組函數(shù)的集合衔彻,只有那些特權(quán)函數(shù)才能夠獲取或修改私有屬性哦O(∩_∩)O~

如果把 sequer.gensym 作為值傳遞給第三方函數(shù),那么那個(gè)函數(shù)可以使用它產(chǎn)生唯一的字符串偷办,但卻不能通過這個(gè)函數(shù)改變 prefix 或 seq 的值艰额。因?yàn)檫@個(gè)函數(shù)的功能只是“產(chǎn)生唯一的字符串”呀柄沮!

13 級(jí)聯(lián)

如果我們讓某些方法返回 this废岂,那么就會(huì)啟動(dòng)級(jí)聯(lián)。在級(jí)聯(lián)中拯欧,我們可以在單條語句中依次調(diào)用同一個(gè)對(duì)象的多個(gè)方法,最著名的例子就是 jQuery 哦O(∩_∩)O~

形如:

getElement('myDiv')
  .move(350,150)
  .width(100);

級(jí)聯(lián)可以產(chǎn)生出極富表現(xiàn)力的接口镐作。

14 柯里化

柯里化指的是:把函數(shù)與傳遞給它的參數(shù)結(jié)合该贾,產(chǎn)生出新的函數(shù):

Function.method('curry', function () {
    var slice = Array.prototype.slice,
        args = slice.apply(arguments),//創(chuàng)建一個(gè)真正的數(shù)組
        that = this;
    return function () {
        return that.apply(null, args.concat(slice.apply(arguments)));
    };
});
var add1 = add.curry(1);
console.log(add1(6));//7

因?yàn)?arguments 不是真正的數(shù)組捌臊,所以沒有 concat 方法,因此我們使用 slice 創(chuàng)建出了真正的數(shù)組逞力。

15 記憶

有時(shí)候可以把先前計(jì)算過的結(jié)果記錄在某個(gè)對(duì)象中糠爬,避免重復(fù)計(jì)算。

假設(shè)我們想通過遞歸來計(jì)算 Fibonacci 數(shù)列。一個(gè) Fibonacci 數(shù)字是之前的兩個(gè) Fibonacci 數(shù)字之和殴玛,最前面的兩個(gè)數(shù)字是 0 和 1:

var fiboncci = function () {
    var memo = [0, 1];//存儲(chǔ)結(jié)果(已經(jīng)計(jì)算過的值)
    var fib = function (n) {
        var result = memo[n];
        if (typeof result !== 'number') {
            result = fib(n - 1) + fib(n - 2);
            memo[n] = result;
        }
        return result;
    };
    return fib;
}();

for (var i = 0; i <= 10; i += 1) {
    console.log(i + ": " + fiboncci(i));
}

我們把計(jì)算結(jié)果存儲(chǔ)在 memo 數(shù)組中,它被隱藏在閉包里寻仗。函數(shù)被調(diào)用時(shí)凡壤,它會(huì)先檢查計(jì)算結(jié)果是否已存在,如果存在就立即返回曹体。

我們對(duì)這個(gè)例子進(jìn)行擴(kuò)展硝烂,構(gòu)造一個(gè)帶記憶功能的函數(shù):

/**
 * 帶記憶功能的函數(shù)
 * @param memo 初始的 memo 數(shù)組
 * @param formula 公式函數(shù)
 * @returns {Function}
 */
var memoizer = function (memo, formula) {
    var recur = function (n) {
        var result = memo[n];
        if (typeof result != 'number') {
            result = formula(recur, n);
            memo[n] = result;
        }
        return result;
    };
    return recur;
};

這個(gè)函數(shù)會(huì)返回一個(gè)可以管理 meno 存儲(chǔ)參數(shù)和在需要時(shí)調(diào)用 formula 函數(shù)的 recur 函數(shù)滞谢。recur 函數(shù)和它的參數(shù)會(huì)被傳遞給 formula 函數(shù)(就是公式)。是不是感覺有點(diǎn)繞母截,我們來看看實(shí)例就會(huì)清楚啦橄教。

現(xiàn)在我們使用 memoizer 函數(shù)來重新定義 fibonacci 函數(shù):

/**
 * 斐波那契
 * @type {Function}
 */
var fibonacci = memoizer([0, 1], function (recur, n) {
    return recur(n - 1) + recur(n - 2);
});
console.log(fibonacci(10));//55

這樣清楚了吧颤陶,使用這個(gè)函數(shù)可以極大地減少我們的工作量哦,比如下面的這個(gè)階乘函數(shù):

/**
 * 階乘
 * @type {Function}
 */
var factorial = memoizer([1, 1], function (recur, n) {
    return n * recur(n - 1);
});
console.log(factorial(10));//3628800
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末垦江,一起剝皮案震驚了整個(gè)濱河市搅方,隨后出現(xiàn)的幾起案子姨涡,更是在濱河造成了極大的恐慌,老刑警劉巖涛漂,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異逢慌,居然都是意外死亡间狂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門忙菠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牛欢,“玉大人俭尖,你說我怎么就攤上這事⊙嫱” “怎么了已亥?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵虑椎,是天一觀的道長。 經(jīng)常有香客問我捆姜,道長泥技,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任簸呈,我火速辦了婚禮店茶,結(jié)果婚禮上贩幻,老公的妹妹穿的比我還像新娘两嘴。我一直安慰自己吃溅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赖歌,像睡著了一般功茴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上展父,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天栖茉,我揣著相機(jī)與錄音孵延,去河邊找鬼。 笑死惶凝,一個(gè)胖子當(dāng)著我的面吹牛留晚,可吹牛的內(nèi)容都是我干的章母。 我是一名探鬼主播缕陕,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼翘瓮,長吁一口氣:“原來是場噩夢啊……” “哼遍坟!你這毒婦竟也來了愿伴?” 一聲冷哼從身側(cè)響起电湘,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤鹅经,失蹤者是張志新(化名)和其女友劉穎瘾晃,沒想到半個(gè)月后幻妓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡强胰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年偶洋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了距糖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恩脂,死狀恐怖东亦,靈堂內(nèi)的尸體忽然破棺而出唬渗,到底是詐尸還是另有隱情,我是刑警寧澤壮啊,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布撑蒜,位于F島的核電站座菠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏浴滴。R本人自食惡果不足惜升略,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一屡限、第九天 我趴在偏房一處隱蔽的房頂上張望钧大。 院中可真熱鬧罩旋,春花似錦、人聲如沸劣挫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坎炼。三九已至,卻和暖如春檩淋,著一層夾襖步出監(jiān)牢的瞬間萄金,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工日戈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孙乖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓弯屈,卻偏偏與公主長得像恋拷,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酌住,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容