- 簡述
- 無副作用(No Side Effects)
- 高階函數(shù)(High-Order Function)
- 柯里化(Currying)
- 閉包(Closure)
-- JavaScript 作用域
-- 面向?qū)ο箨P(guān)系
-- this調(diào)用規(guī)則
-- 配置多樣化的構(gòu)造重載
-- 更多對象關(guān)系維護——模塊化
-- 流行的模塊化方案- 不可變(Immutable)
- 惰性計算(Lazy Evaluation)
- 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),比如 Java
和 C#
中規(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)用者是:烏索普
甚至于,當我們將 user02
的 print()
方法賦值為 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
也并不是對象 obj
的 seed
屬性。如何改造呢?
// 聲明一個對象
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)境的應用模塊。