JS 函數(shù)式編程思維簡述(五):閉包 02

  1. 簡述
  2. 無副作用(No Side Effects)
  3. 高階函數(shù)(High-Order Function)
  4. 柯里化(Currying)
  5. 閉包(Closure)
    -- JavaScript 作用域
    -- 面向?qū)ο箨P(guān)系
    -- this調(diào)用規(guī)則
    -- 配置多樣化的構(gòu)造重載
    -- 更多對象關(guān)系維護——模塊化
    -- 流行的模塊化方案
  6. 不可變(Immutable)
  7. 惰性計算(Lazy Evaluation)
  8. Monad

前言

? ? ? ?閉包是一個緩存多個函數(shù)內(nèi)部成員供函數(shù)外部引用的設計過程,思考這一設計過程時,方案的衍生、注意事項就變得尤為重要。

5.3 this調(diào)用規(guī)則

? ? ? ?無論是通過函數(shù)的方式,還是對象的方式進行數(shù)據(jù)封裝及導出,我們都不可避免的遇到一個問題——在函數(shù)/對象內(nèi)部,調(diào)用其他的內(nèi)部成員。

類方法中的 this

? ? ? ?支持面向?qū)ο?/strong>的編程語言通常都會有一個用于描述對象模板的結(jié)構(gòu),比如 JavaC# 中規(guī)定,以 class 作為關(guān)鍵字,聲明一個 作為對象模板。在 中可以聲明一些方法 (我們將隸屬于某對象的函數(shù)稱之為方法),而在 方法 的設計過程中,有的時候我們需要調(diào)用除 本方法 外的其他 類成員,需要使用關(guān)鍵字 this

// 以 Java 例舉
class User{
    
    private String username; // 登錄賬號
    private String password; // 登錄密碼
    
    /**
     * 模擬登錄方法
     */
    public void login(String u, String p){
        // 通過 this 關(guān)鍵字,可以調(diào)用成員變量
        this.username = u;
        this.password = p;
        
        // 通過 this 關(guān)鍵字,也可以調(diào)用其他成員方法
        this.print();
    }
    
    /**
     * 用于展示登錄信息
     */
    private void print(){
        // 將登錄信息輸出至控制臺
        System.out.println("歡迎您["+ this.username +"],您的密碼是:" + this.password);
    }
}

而在被外部調(diào)用時,可能是這樣的方式:

// 構(gòu)建用戶1:路飛
User user1 = new User();
user1.login("路飛", "lufei"); // 結(jié)果: 歡迎您[路飛],您的密碼是:lufei

// 構(gòu)建用戶2:特拉法爾加·羅
User user2 = new User();
user2.login("特拉法爾加·羅", "law"); // 結(jié)果: 歡迎您[特拉法爾加·羅],您的密碼是:law

在這個示例中,類模板 聲明了方法 login() ,這個方法在類中也只算是一個 模板方法,用于描述 對象 在真正調(diào)用時的行為。方法中,通過關(guān)鍵字 this 可以引用類中定義的其他成員。這個 this 關(guān)鍵字所表示的意義就是:代指當前真正調(diào)用者(對象)。比如:

  • 對象 user1 調(diào)用方法時,方法內(nèi)部的 this 就相當于是 user1
  • 對象 user2 調(diào)用方法時,方法內(nèi)部的 this 就相當于是 user2

在不同的語言環(huán)境中,對于描述 當前對象 的方式也有著不同的變體,比如:

  • PHP 中使用關(guān)鍵字 $this
  • Swift 的類中使用 self 關(guān)鍵字描述類的實例本身;
  • Python 的類方法的第一個形參即代表類實例,通常命名都是 self
  • Ruby 使用操作符 @ 來在類中引用成員;
  • 一般情況下,JavaScript 中使用 this 來表示調(diào)用當前方法的對象。
  • ...

JS 函數(shù)中的 this

? ? ? ?在 JavaScript 中,函數(shù)也可以獨立存在(不定義在類中)。同時,每一個函數(shù)中也可以使用 this 關(guān)鍵字,但單獨的函數(shù)中使用的 this 究竟代表了什么,卻是一件應用規(guī)則比較復雜的事情。JS 函數(shù)中的 this 會在不同場景下遵循如下規(guī)則:

  • 默認規(guī)則
  • 嚴格模式
  • 隱式綁定
  • 顯示綁定
  • new 綁定
  • 箭頭函數(shù)綁定

讓我們逐一領(lǐng)略一番...

默認規(guī)則

? ? ? ?默認規(guī)則是指,在默認情況下函數(shù)中使用 this 關(guān)鍵字時, this 所綁定的對象規(guī)則。在默認情況下, this 關(guān)鍵字指向的是全局對象

// 瀏覽器環(huán)境下
let foo = function() { 
    console.log(this);
}

foo(); // Window
// node.js 環(huán)境下
let foo = function() { 
    console.log(this);
}

foo(); // global

嚴格模式

? ? ? ?如果在嚴格模式(strict mode)下,這樣的調(diào)用就不會綁定全局環(huán)境的對象,this 所指向的將是 undefined 值:

// 嚴格模式下
'use strict';
let foo = function() { 
    console.log(this);
}

foo(); // undefined

隱式綁定

? ? ? ?隱式綁定是我們在 JavaScript 中最常見的 this 綁定機制。他是指,當前函數(shù)的調(diào)用者。 實際上默認綁定規(guī)則也是隱式綁定的一種表現(xiàn):因為在 JavaScript 環(huán)境中,如果未指定當前函數(shù)的調(diào)用者,其調(diào)用者就默認被當做是 全局對象

// 聲明一個函數(shù)
function printExample() {
    console.log('調(diào)用者是:', this.name);
}

// 聲明調(diào)用者 01
const user01 = {
    name: '娜美',
    print: printExample
};

// 聲明調(diào)用者 02
const user02 = {
    name: '烏索普',
    print: printExample
};

// 調(diào)用對象方法
user01.print(); // 調(diào)用者是:娜美
user02.print(); // 調(diào)用者是:烏索普

甚至于,當我們將 user02print() 方法賦值為 user01.print 時,只要最終調(diào)用的對象依然是 user02 那么結(jié)果也不會變化:

// 聲明調(diào)用者 02
const user02 = {
    name: '烏索普',
    print: user01.print
};

// 調(diào)用對象方法
user01.print(); // 調(diào)用者是:娜美
user02.print(); // 調(diào)用者是:烏索普

隱式綁定規(guī)則非常簡單,只要注意觀測函數(shù)的直接調(diào)用者是誰即可。讓我們來對上述代碼做一個變體:

// 聲明一個函數(shù)
function printExample() {
    console.log('調(diào)用者是:', this.name);
}

// 聲明調(diào)用者 01
const user01 = {
    name: '娜美',
    print: printExample
};

// 聲明調(diào)用者 02
const user02 = {
    name: '烏索普',
    print: printExample,
    user01: user01 // 此處,為對象 user02 添加 user01 作為屬性
};

// 調(diào)用對象方法
user02.user01.print(); // 調(diào)用者是:娜美

該例中,雖然 user01 作為 user02 的屬性,但最終調(diào)用時,依然是 user01 在調(diào)用 print() 方法,因此 this.name 獲取到的屬性值依然是 user01 對象中定義的值 娜美

顯式綁定

? ? ? ?隱式綁定規(guī)則描述的情況是——雖然我們沒刻意指定,但運行過程中隱式的幫我們做了 this 指定。那么相反的,顯式綁定規(guī)則則是指:明確的指定了函數(shù)的調(diào)用者是誰
? ? ? ?在顯式綁定規(guī)則中,我們通常使用函數(shù)對象的方法 bind()call()apply() 來進行描述:

  • <font color=red>Function.prototype.bind:</font> 函數(shù)用于創(chuàng)建一個新綁定函數(shù)(bound function,BF),在新函數(shù)中, this 關(guān)鍵字將始終以 bind(thisArg) 中的參數(shù) thisArg 作為綁定對象:
// 創(chuàng)建一個公共函數(shù)作為示例
function print() {
    console.log(this.name + '正在調(diào)用...');
}

// 定義對象
const user01 = {name : '索隆'};
const user02 = {name : '山治'};

// 使用 bind() 綁定調(diào)用對象,并將 print() 函數(shù)覆蓋
print = print.bind(user01);
// 為 user02 對象創(chuàng)建 print() 方法
user02.print = print;

// 雖然調(diào)用者看起來是 user02
// 但 print() 方法中已將 this 綁定為 user01
// 因此 調(diào)用的結(jié)果是: 索隆正在調(diào)用...
user02.print();
  • <font color=red>Function.prototype.call:</font> 函數(shù)用于調(diào)用另一個函數(shù),并且在調(diào)用時指定 this 值,以及傳遞參數(shù):
// 創(chuàng)建一個公共函數(shù)作為示例
function print(tricks) {
    console.log(this.name + '的絕招是:' + tricks);
}

// 定義對象
const user01 = {name : '索隆'};
const user02 = {
    name : '山治',
    print: print
};

// 調(diào)用 user02 對象的 print() 函數(shù),但 實際調(diào)用對象已經(jīng)指定了 user01
user02.print.call(user01, '三十六煩惱鳳'); // 索隆的絕招是:三十六煩惱鳳

// 將 print 函數(shù)的調(diào)用者綁定為 user02,并且調(diào)用
print.call(user02, '惡魔風腳'); // 山治的絕招是:惡魔風腳

// 普通調(diào)用時 this 指向了全局對象
print('龜派氣功'); // 的絕招是:龜派氣功
  • <font color=red>Function.prototype.apply:</font> 函數(shù)用于調(diào)用另一個函數(shù),并且在調(diào)用時指定 this 值,以及傳遞參數(shù)。與 call 方法不同的是,call() 方法的參數(shù)部分是逐一傳遞的,而apply()的參數(shù)是作為數(shù)組形式傳遞給方法的第二個參數(shù)
// call 方法的參數(shù)傳遞方式
// fun.call(thisArg, arg1, arg2, ...)

// apply 方法的參數(shù)傳遞方式
// fun.apply(thisArg, [argsArray])

new 綁定

? ? ? ?new 關(guān)鍵字用于調(diào)用一個構(gòu)造函數(shù),并締造一個實體對象。而當 JavaScript 中的類成員方法中使用 this 關(guān)鍵字時,使用 new 構(gòu)造的對象會綁定到方法中 主作用域this

// javascript 定義類的語法糖
class User{

    constructor(name){
        // 構(gòu)造方法
        this.name = name;
    }

    print(){
        // 定義的普通方法
        console.log('我的名字是: ' + this.name);
    }
}

const user01 = new User('伊麗莎白');
user01.print(); // 我的名字是伊麗莎白

常見綁定問題——函數(shù)嵌套

? ? ? ?在 JavaScript 中,我們經(jīng)常會遇到函數(shù)的嵌套語法。而且函數(shù)嵌套時,多個函數(shù)中的 this 關(guān)鍵字所指向的對象往往顯得復雜、混亂,this 究竟指向外層函數(shù)作用域,還是內(nèi)層的函數(shù)作用域呢?我們往往使用 代詞 來描述外層作用域,解決這個問題:

// 聲明一個對象
const obj = {
    arr: [ 1, 3, 5, 7, 9],
    seed: 3,
    calc: function(){
        // 通過當前對象的 arr 屬性作為數(shù)組模板
        return this.arr.map(function(e, ind){
            // 對偶數(shù)位(2,4,6,...)數(shù)據(jù)進行運算,生成新的集合
            return ind % 2 !== 0 ? e + this.seed : e ;
        });
    }
};

obj.calc(); // 結(jié)果: [1, NaN, 5, NaN, 9]

為什么會出現(xiàn)這樣的結(jié)果,偶數(shù)位數(shù)據(jù)運算后竟然都是 NaN ?原因很簡單,在 calc() 函數(shù)的主作用域中的 this ,因為調(diào)用時調(diào)用者就是 obj ,所以 this.arr 引用到了屬性 obj.arr 。而** this.arr.map 函數(shù)中,作為參數(shù)的函數(shù)也希望引用到 calc() 隱式綁定的 this 對象,但 map() 中的函數(shù)參數(shù)卻由于 每個函數(shù)內(nèi)部都擁有一個獨立的 this,因為調(diào)用時的就近原則導致 this.seed 無法指向外層函數(shù)所綁定的 this 對象。**因此,內(nèi)部的 this 指向了一個詭異的位置,而這個 this.seed 也并不是對象 objseed 屬性。如何改造呢?

// 聲明一個對象
const obj = {
    arr: [ 1, 3, 5, 7, 9],
    seed: 3,
    calc: function(){

        // 通過新的代詞描述外部作用域的 this 對象
        const self = this;

        // 通過當前對象的 arr 屬性作為數(shù)組模板
        return this.arr.map(function(e, ind){
            // 對偶數(shù)位(2,4,6,...)數(shù)據(jù)進行運算,生成新的集合
            // 注意:此處為了避免回調(diào)函數(shù)內(nèi)部 this 沖突
            // 顯式的調(diào)用了函數(shù)外層作用域中的 self 所代表的外層 this
            return ind % 2 !== 0 ? e + self.seed : e ;
        });
    }
};

obj.calc(); // 結(jié)果: [1, 6, 5, 10, 9]

箭頭函數(shù)綁定

? ? ? ?通過 Lambda表達式 定義的箭頭函數(shù),其應用自身獨有的一套規(guī)則,即:**捕獲函數(shù)定義位置作用域的 this,作為自己函數(shù)內(nèi)部的 this **。與此同時,其他 this 綁定規(guī)則 將無法影響箭頭函數(shù)中已捕獲的 this

// 全局作用域
window.bar = 'window 對象';

// 函數(shù) foo 通過箭頭函數(shù)定義
const foo = () => console.log(this.bar);

// 定義一個 baz 對象,添加函數(shù) foo 作為方法
const baz = {
    bar: 'baz 對象',
    foo: foo
}

foo();              // 結(jié)果:window 對象
baz.foo();          // 結(jié)果:window 對象
foo.call(baz);      // 結(jié)果:window 對象
foo.bind(baz)();    // 結(jié)果:window 對象

通過如上特性,如果我們遇到了嵌套函數(shù),也可以使用箭頭函數(shù)來描述子函數(shù)作用域引用外層的父作用域的 this

// 聲明一個對象
const obj = {
    arr: [ 1, 3, 5, 7, 9],
    seed: 3,
    calc: function(){

        // 通過當前對象的 arr 屬性作為數(shù)組模板
        // 子函數(shù)作用域定義箭頭函數(shù)的位置處于父函數(shù)的位置
        // 因此直接綁定了外層父函數(shù)作用域中的 this 引用
        return this.arr.map((e, ind) => {
            // 對偶數(shù)位(2,4,6,...)數(shù)據(jù)進行運算,生成新的集合
            return ind % 2 !== 0 ? e + this.seed : e ;
        });
    }
};

obj.calc(); // 結(jié)果: [1, 6, 5, 10, 9]

小結(jié):閉包是一種依賴于函數(shù)的設計過程,而在 JavaScript 函數(shù)中,對于 this 的引用經(jīng)常會讓不清楚規(guī)則的同學覺得很怪異。因此,對于 this 應用的了解,能夠幫助我們設計出更好的基于閉包環(huán)境的應用模塊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

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