今年年初 Douglas Crockford 的新書 How JavaScript Works 出版不久後,我買來看了。在 JavaScript: The Good Parts 出版後 10 年,並深遠影響了 JavaScript 語言以後,Douglas Crockford 對 JavaScript 這門語言依然有不少不滿,並認爲 the bad parts 更多了。javascript
固然我並不認同他的全部觀點,好比把箭頭函數和 async/await 也歸爲 the new bad parts。不過,他關於 this
和 class
的見解,以及他對這些見解的論證,我是贊成的。我認爲在遇到咱們不熟悉的觀點時,若是論述者足夠認真和嚴肅,咱們應該至少傾聽一下。Crockford 爲了證實「你不熟悉的東西不必定就是錯的」這個觀點,全書用 wun 來替代 one,由於 one 不符合任何英文發音規則。前端
首先須要說明的是,拒絕 this
和 class
和推崇函數式編程並無關係。若是你常常關注 Douglas Crockford 的話,你會知道他並不認爲 Monad 是解決問題的方案。他尋找的下一代編程語言依然是面向對象的,只不過不是 Java 和 C++ 那種。java
在我介紹 this
和 class
的問題以前,仍是先來看看啓發我寫這篇文章的一個小故事。git
前天在掘金看到一篇關於面試題的文章,看到這樣一題:程序員
// 寫一個 machine 函數達到以下效果
function machine() {}
machine('ygy').execute();
// start ygy
machine('ygy')
.do('eat')
.execute();
// start ygy
// ygy eat
machine('ygy')
.wait(5)
.do('eat')
.execute();
// start ygy
// wait 5s(這裏等待了5s)
// ygy eat
machine('ygy')
.waitFirst(5)
.do('eat')
.execute();
// wait 5s
// start ygy
// ygy eat
複製代碼
看到鏈式調用,可能不少人會想到原型鏈繼承。我一開始寫出的答案並無用原型鏈繼承,可是爲了省事仍是用了 this
:github
// 基於首次答案有微調
const defer = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
function machine(name) {
const tasks = [];
const initTask = () => {
console.log(`start ${name}`);
};
tasks.push(initTask);
function _do(str) {
const task = () => {
console.log(`${name} ${str}`);
};
tasks.push(task);
return this;
}
function wait(sec) {
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.push(task);
return this;
}
function waitFirst(sec) {
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.unshift(task);
return this;
}
function execute() {
tasks.reduce(async (promise, task) => {
await promise;
await task();
}, undefined);
}
return {
do: _do,
wait,
waitFirst,
execute,
};
}
複製代碼
來經過這段代碼看看 this
有什麼問題。看到 this
,若是你對 JS 比較熟悉,你想到的就是去找 this
所在函數的執行上下文。但是代碼中並無明顯而直觀的視覺提示 (visual cue) 來指引你去哪找,你只有當人肉解釋器去找 this
的動態綁定。這在我看來是不必的腦力浪費。而若是是新人看到這種代碼,會很是困惑和抓狂。WTF is this?!面試
來看看去除 this
的版本:編程
const defer = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
function machine(name) {
const context = {};
const tasks = [];
const initTask = () => {
console.log(`start ${name}`);
};
tasks.push(initTask);
function _do(str) {
const task = () => {
console.log(`${name} ${str}`);
};
tasks.push(task);
return context;
}
function wait(sec) {
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.push(task);
return context;
}
function waitFirst(sec) {
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.unshift(task);
return context;
}
function execute() {
tasks.reduce(async (promise, task) => {
await promise;
await task();
}, undefined);
}
// 用 Object.freeze 來防止調用者修改內部函數,保障安全
return Object.freeze(
Object.assign(context, {
do: _do,
wait,
waitFirst,
execute,
})
);
}
複製代碼
修改過的版本,全部的變量關係都是顯式的。看到 return context;
,你能很快跟蹤到 context
的引用,不用費力想就能明白 context
裏面有什麼。後端
我平時寫業務代碼時固然也會寫 this
,但我只是爲了順應開發生態。業餘練習我會盡可能避免 this
。而 Crockford 的觀點是,沒有 this
的 JavaScript 依然圖靈完備,並且會是更好的語言。下面我來介紹總結下他在 How JavaScript Works 這本書中關於 this
和 class
的觀點。數組
this
的問題提到 this
不得不提原型鏈繼承。最先採用原型鏈繼承的語言是 Self,它是 Smalltalk 的一個方言。Crockford 認爲,Self 的原型鏈繼承機制相對於笨重而高耦合的類繼承來講,像一陣清風。原型鏈繼承靈活,輕量,更有表達性。而 JavaScript 實現的原型鏈繼承則是一個古怪的變體。
在 JavaScript 中,當一個對象被建立時,咱們能夠同時指定這個變量的原型。原型上保存了目標對象的部分或全部內容。當咱們試圖訪問的某屬性或方法在一個對象中不存在時,咱們會獲得 undefined
,而當這個對象有原型時,原取值的結果會是在原型上取值的結果,若是在原型上取值失敗,會順着原型鏈繼續找,直到找到或者原型不存在。
一般使用原型鏈的場景是,當咱們須要在不一樣對象之間共享某些方法時,使用原型鏈會節省內存。
而原型鏈上的這些方法是怎麼知道它們做用於哪一個對象上的?這就要靠 this
來解決了。
當一個對象上的方法被執行時,這個方法接受的不只有實參,還有隱式傳入的形參 this
,this
被綁定在當前對象上。當一個方法內部存在函數時,內部這個函數訪問不到 this
,由於只有方法才能訪問到 this
,函數訪問不到。如:
old_object.bud = function bud() {
const that = this;
function lou() {
do_to_it(that);
}
lou();
};
複製代碼
因爲 this
綁定只做用於方法上,函數調用的狀況下,this
綁定會失敗:
// this 綁定有效
new_object.bud();
// 無效,失去了 this 綁定
const funky = new_object.bud;
funky();
複製代碼
看到上面的例子,想到了 React 裏面在能使用 class property 以前使人頭疼的 this 綁定了嗎?
this
最有問題的地方在於它的動態綁定。來看一個發佈訂閱例子:
function pubsub() {
const subscribers = [];
return {
subscribe: function(subscriber) {
subscribers.push(subscriber);
},
publish: function(publication) {
const length = subscribers.length;
for (let i = 0; i < length; i += 1) {
subscribers[i](publication);
}
},
};
}
複製代碼
因爲 subscribers[i](publication)
這行代碼的存在,每一個 subscriber
訂閱者函數都得到了 subscribers
數組的 this
綁定,這讓訂閱者函數能幹出很危險的事情,好比把 subscribers 數組清空,像這樣:
my_pubsub.subscribe(function(publication) {
this.length = 0;
});
複製代碼
若是把一個函數存在數組裏,當經過下標來調用這個函數時,實際上是在執行數組對象上的方法。此時函數得到了指向數組的 this
綁定。這在代碼安全性和可靠性規約上是很糟糕的。
上面提到的問題,能夠經過把 for
循環改爲 forEach
解決:
publish: function (publication) {
subscribers.forEach(function (subscriber) {
subscriber(publication);
})
}
複製代碼
全部變量都是靜態綁定的。靜態綁定能讓代碼更易理解,行爲更符合預期,更可靠安全。只有 this 是動態綁定的。動態綁定意味着函數的調用者,而不是定義者決定綁定的內容,這會引發困惑和混亂。
提到 class,不得不說面向對象編程。咱們如今認知中的主流的面向對象,和「面向對象」這個詞被髮明出來時所表達的意思已經相差太遠了。
I invented the term Object-Oriented, and I can tell you I did not have C++ in mind. -- Alan Kay
(我能夠告訴你,在我發明「面向對象」這個詞的時候,我想到的不是 C++ -- Alan Kay)
Alan Kay 設計了 Smalltalk。Smalltalk 雖然不是第一個面嚮對象語言,但現代面向對象編程思想始於 Smalltalk。Smalltalk 中面向對象編程的核心部分,是對象之間的消息傳遞。對象之間經過調用對方的方法來傳遞消息,而多態使得這種互相調用很是靈活和強大。這是面向對象最初所要強調的軟件設計思想。面向對象一開始和繼承沒有關係。
在處理的問題足夠簡單時,繼承能夠很方便複用代碼。可是現實世界是複雜的,多重繼承會形成代碼的高度耦合,改一個類,依賴這個類的相關的類所有受影響。
類繼承的問題已經有足夠多的論述,我再也不展開。我在初學 Python 的時候,學的教程是 Learn Python the Hard Way,書中專門留了一章來警告類繼承的問題。我想這個問題應該有足夠的共識。
既然已經知道了類繼承的問題,爲何還要在 JavaScript 中加入語法糖,提供假的繼承?一個可能的緣由是不少 Java 程序員要寫 JS 了,爲了方便這些開發者快速地把 Java 知識遷移到 JS 中來,EcmaScript 給全部 JS 開發者提供了 class 語法糖。
即便 JavaScript 中的 class 是基於原型鏈繼承的語法糖,它也有這些問題:
來看例子。
class Cat {
constructor(name) {
this.name = name;
}
meow() {
console.log(`${this.name} meows!`);
}
}
const Tom = new Cat('Tom');
Tom.meow(); // Tom meows!
Tom.name = 'Jerry';
Tom.meow(); // Jerry meows!
Tom.meow = null;
Tom.meow(); // TypeError: Tom.meow is not a function
複製代碼
可能你會想到正在 TC39 草案中的 private fields,而這在我看來是先製造問題,而後提供解決問題的方案。
用工廠函數就沒有這個問題:
function cat(name) {
function meow() {
console.log(`${name} meows!`);
}
return Object.freeze({
meow,
});
}
Tom.meow(); // Tom meows!
Tom.name = 'Jerry'; //TypeError: Cannot add property name, object is not extensible
Tom.meow = null; //TypeError: Cannot add property name, object is not extensible
複製代碼
在使用 class 的時候,要很是當心 this
失去上下文。上面已經講過了,再也不贅述。
第一個緣由正如剛剛提到的,有不少後端開發者要來寫前端,提供 class 可讓更多後端開發者快速上手 JS。
第二個緣由是原型鏈繼承能夠節省內存。當你要同時生成成千上萬個 UI 組件時,使用原型鏈繼承節省的內存是很可觀的。但我很懷疑這種策略的適用場景,你何時須要一個頁面渲染超過一百個組件了?Douglas Crockford 專門論述了內存佔用上的對比。在過去內存緊張的狀況下,原型鏈繼承節省的內存很重要;但如今,一臺手機的內存都用 G 來計量了,這點內存佔用差別能夠忽略不計。
若是你有興趣瞭解 Douglas Crockford 倡導的面向對象是什麼樣子,能夠看 MPJ 的這篇文章:The future is here: Classless object-oriented programming in JavaScript
另外,MPJ 有一期的 Fun Fun Function 講了對象組合。Composition Over Inheritance 他演示瞭如何用工廠函數來實現對象組合。
這裏還有一篇相似的:Object Composition in Javascript