寫在前面:本文為JavaScript新手之作沮榜,大牛輕噴
要說當下編程社區(qū)里最火的語言是什么慌洪,如果JavaScript稱第二拣宰,怕是沒其他語言敢稱第一柄冲。不過這幾年JS實在發(fā)展得太快,而在ES5之后辽幌,好像除了統(tǒng)一的模塊機制增淹,我其實并沒有覺得JS的發(fā)展給前端加上了什么非用不可的語言特性——嗯,是的我覺得前端不太用得上 async 和 await(什么乌企?class虑润?我沒聽過誒)。
不過JS仍然是一個很好玩的語言加酵,和Lua一樣拳喻,有著Scheme一樣的風骨。所以竊以為猪腕,要理解一些JS的設計思路和編碼習慣冗澈,還是要搞點函數(shù)式才行。網(wǎng)上有一套開源的書叫 You Don't Know JS陋葡,我之前小翻了前幾本亚亲,覺得寫得很不錯。但是仔細想想它解釋一些概念的說法腐缤,還是可能對命令式編程背景的讀者不是那么友好捌归。前兩天組里有一個同事問我能不能對JS里的 this
給一個相對淺顯的理解,我第一反應就是把 YDKJS 里讓我印象深刻的說法丟出來:
this
相當于JS靜態(tài)作用域里的一個動態(tài)變量
——呃岭粤,這個定義是挺準確的惜索,但是真的好理解嗎?
好吧绍在,展開來寫一篇我自己是怎么一步一步地理解JS里的 this
這個磨人的小妖精
靜態(tài)作用域和動態(tài)作用域
變量作用域其實和所有編程語言都相關(guān)门扇,但是函數(shù)式編程語言為了閉包的支持,對這個概念尤其看重偿渡。來一個簡單的栗子——我們有如下兩個函數(shù)一個變量和一次調(diào)用:
var v = 1;
function f1() {
console.log(v);
}
function f2() {
var v = 2;
f1();
}
f2();
我們知道JS是一個靜態(tài)作用域的語言臼寄,如果把上面代碼貼到瀏覽器的 console 里執(zhí)行一下,會輸出 1
溜宽。這說明 v
這個變量的綁定是在我們寫 f1
的定義時就確定的吉拳,也可以說 v
的作用域是靜態(tài)的,所以這樣的設計叫靜態(tài)作用域(也有人叫詞法作用域)适揉。但是如果JS是一個動態(tài)作用域的語言留攒,v
的綁定就是在 f1
運行的時候確定的,在 f2
里這個值指向了上面那句 var v = 2
嫉嘀,最終輸出的結(jié)果就會是 2
炼邀。這個問題實在太重要了,因為正是作用域在函數(shù)間的隔離剪侮,給了函數(shù)足夠的抽象能力來實現(xiàn)各種對邏輯和數(shù)據(jù)的封裝拭宁。
Now this
is the main dish
好了,現(xiàn)在我們理解了靜態(tài)和動態(tài)作用域以后瓣俯,就可以貼點復雜一些但是更能說明問題的例子了——首先杰标,假設我們的App有這么一個工具集:
var utils = {
base: 0,
accumulate: function(list, acc) {
var result = this.base;
for (var i of list) result = acc(result, i);
return result;
},
};
系不系很簡單?accumulate
函數(shù)就是拿一列數(shù)字和一個 acc
函數(shù)彩匕,然后把這個函數(shù)對這列數(shù)字層層套上(如果腦海里飛過left fold/reduce這樣的概念腔剂,請給自己一朵小紅花)。現(xiàn)在假設我們的App就是一個用來把1驼仪、2掸犬、3、4加起來的簡單計算器——
var app = {
list: [1, 2, 3, 4],
simpleSum: function() {
var result = utils.accumulate(this.list, function(x, y) { return x + y });
console.log(result);
},
};
app.simpleSum();
是不是很明顯會log一個10绪爸?現(xiàn)在我們來讓邏輯復雜一些登渣,求這幾個數(shù)的平方和:
app.square = function(x) { return x * x };
app.sqrSum = function() {
console.log(utils.accumulate(
this.list,
function(x, y) { return x + this.square(y) } // <-- watch this
));
};
app.sqrSum(); // Hmm...
發(fā)現(xiàn)了嗎,傳遞給 app.sqrSum
的函數(shù)里毡泻,this
指向的并不是 app
胜茧。問題出在哪里呢?問題就在于仇味,this
在函數(shù)里的作用域不是跟著定義走呻顽,而是在運行的時候被動態(tài)分發(fā)的(敲黑板!劃重點5つ)廊遍。是不是看起來和動態(tài)作用域簡直一毛一樣?
所以我們怎么繞開這個坑呢贩挣?這就是在遙遠的ES5時代及以前喉前,大家經(jīng)常用的一個辦法:把 this
存成另一個變量没酣,再把這個變量傳進子函數(shù)里:
app.sqrSum = function() {
var self = this; // some may prefer using `that`
console.log(utils.accumulate(
this.list,
function(x, y) { return x + self.square(y) } // now self is referenced correctly
));
};
把 this
賦值給另一個變量再往里傳是不是其實有點蛋疼?確實有點卵迂。JS社區(qū)里大家也這么覺得裕便,所以才為此在ES6里搞了個箭頭函數(shù),在箭頭函數(shù)定義里的 this
就是跟著詞法作用域走的见咒。這個特性在網(wǎng)上已經(jīng)有太多討論偿衰,一搜一大把,這里就不展開了改览。
更好的JS
不管是箭頭函數(shù)還是重新賦值下翎,其實都不是我心中最好的解決方式——我其實更偏向于直接用函數(shù),而不是用對象來封裝數(shù)據(jù)和邏輯宝当。學JS的朋友大多都讀過或者聽過 JavaScript the Good Parts 這本書的大名吧视事,作者 Douglas Crockford 在14年曾經(jīng)談過他覺得JavaScript里怎樣做到更好的設計模式,這個Talk名字就叫 JavaScript the Better Parts庆揩。其中有一點我特別特別贊同:完全不要用this郑口,因為……根本不需要!JS里的函數(shù)是比對象更全能的抽象方法盾鳞。
再復用剛才的那個例子犬性,我就不需要再特別解釋,直接把用函數(shù)做抽象的代碼貼上來一看就懂了:
var utils = (function utils() {
var base = 0;
function accumulate(list, func) {
var result = base;
for (var i of list) result = func(result, i);
return result;
}
return { base: base, accumulate: accumulate };
})();
var app = (function app() {
var list = [1, 2, 3, 4], square = function(x) { return x * x };
return {
simpleSum: function() {
console.log(utils.accumulate(
list,
function(x, y) { return x + y }
));
},
sqrSum: function() {
console.log(utils.accumulate(
list,
function(x, y) { return x + square(y) }
));
},
};
})();
是不是沒有了 this
以后整個邏輯清晰不少腾仅?再也不用糾結(jié)指的是哪一層對象的字段了乒裆。而且用函數(shù)來做封裝還有另外一個好處,不放在返回對象字段里的變量對外不可見推励,相當于一個私有變量鹤耍。如果不習慣IIFE的寫法,網(wǎng)上的資料也挺多的验辞,搜一下不難稿黄,這里就略過不表咯。至于那些對象實例的 call
和 bind
這些用法細節(jié)跌造,在把 this
拿掉之后杆怕,也一樣沒有必要去深究了——是不是好棒棒?
但是殘酷的現(xiàn)實是寫一個網(wǎng)頁App一般都會用個框架壳贪,框架里一般都會用上 this
陵珍,實在是……(啊不,像我這樣沒有能力寫一個框架的小菜雞违施,是不會抱怨的)
P.S. 如果有大牛想吐槽我的API里犯了跟Lodash和Underscore一樣的錯——那就吐吧反正這種科普貼只是我分享自己的入門心得互纯,又不是給大牛看的吼吼……