做爲一名JS開發人員,是什麼使我夜不能寐

做者:Justen Robertson

翻譯:瘋狂的技術宅javascript

原文:https://www.toptal.com/javasc...前端

未經容許嚴禁轉載java

JavaScript 是一種奇怪的語言。雖然受到 Smalltalk 的啓發,但它用了相似 C 的語法。它結合了程序、函數和麪向對象編程(OOP)的方方面面。它有許多可以解決幾乎任何編程問題的方法,這些方法一般是多餘的,並無強烈推薦哪些是首選。它是弱動態類型,但採用了相似強制類型的方法,使經驗豐富的開發人員也可使用。git

JavaScript 也有其瑕疵、陷阱和可疑的功能。新手程序員須要努力解決一些更爲困難的概念 —— 異步性、封閉性和提高。具備其餘語言經驗的程序員合理地假設具備類似名稱的東西,可是看上去與 JavaScript 相同的工做方法每每是錯誤的。數組不是真正的數組,什麼是 this,什麼是原型, new 實際上作了些什麼?程序員

ES6 類的麻煩

到目前爲止,最糟糕的罪魁禍首是 JavaScript 的最新版本——ECMAScript 6(ES6)的。一些關於類的討論坦率地使人震驚,並揭示了對語言實際運做機制的根深蒂固的誤解:es6

「JavaScript 如今終於成爲一種真正的面向對象的語言,由於它有類!」github

要麼是:web

「讓咱們從 JavaScript 中被破壞的繼承模型中解脫出來。」面試

甚至是:編程

「在 JavaScript 中建立類型是一種更安全、更簡單的方法。」

這些言論並無影響到我,由於它們暗示了原型繼承中存在問題,讓咱們拋開這些論點。這些話讓我感到困擾,由於它們都不是真的,它們證實了 JavaScript 的「everything for everyone」的語言設計方法的後果:它削弱了程序員對語言的理解。在我進一步說明以前,先舉一個例子。

JavaScript 小測驗 #1:這些代碼塊之間的本質區別是什麼?

function PrototypicalGreeting(greeting = "Hello", name = "World") {
  this.greeting = greeting
  this.name = name
}

PrototypicalGreeting.prototype.greet = function() {
  return `${this.greeting}, ${this.name}!`
}

const greetProto = new PrototypicalGreeting("Hey", "folks")
console.log(greetProto.greet())
class ClassicalGreeting {
  constructor(greeting = "Hello", name = "World") {
    this.greeting = greeting
    this.name = name
  }

  greet() {
    return `${this.greeting}, ${this.name}!`
  }
}

const classyGreeting = new ClassicalGreeting("Hey", "folks")

console.log(classyGreeting.greet())

這裏的答案是並非惟一的。這些代碼確實有效,它只是一個是否使用了 ES6 類語法的問題。

沒錯,第二個例子更具表現力,所以你可能會認爲 class 是語言的一個很好的補充。不幸的是,這個問題會變得更加微妙。

JavaScript 小測驗 #2:如下代碼有什麼做用?

function Proto() {
  this.name = 'Proto'
  return this;
}

Proto.prototype.getName = function() {
  return this.name
}

class MyClass extends Proto {
  constructor() {
    super()
    this.name = 'MyClass'
  }
}

const instance = new MyClass()

console.log(instance.getName())

Proto.prototype.getName = function() { return 'Overridden in Proto' }

console.log(instance.getName())

MyClass.prototype.getName = function() { return 'Overridden in MyClass' }

console.log(instance.getName())

instance.getName = function() { return 'Overridden in instance' }


console.log(instance.getName())

正確的答案是它打印到控制檯的輸出:

> MyClass
> Overridden in Proto
> Overridden in MyClass
> Overridden in instance

若是你回答錯誤,就意味着不明白 class 到底是什麼。但這不是你的錯。就像Arrayclass不是語言特徵同樣,它是矇昧的語法。它試圖隱藏原型繼承模型和隨之而來的笨拙的慣用語法,這意味着 JavaScript 正在作的事情並不是是你想的那樣。

你可能已經被告知在 JavaScript 中引入了 class,以使來自 Java 等語言的經典 OOP 開發人員更加熟悉 ES6 類繼承模型。若是你是這樣的開發者,那個例子可能會讓你感到恐懼。例子代表 JavaScript 的 class 關鍵字沒有提供類所須要的任何保證。它還演示了原型繼承模型中的一個主要差別:原型是對象實例,而不是類型

原型與類

基於類和基於原型的繼承之間最重要的區別是類定義了一個類型,它能夠在運行時實例化,而原型自己就是一個對象實例。

ES6 類的子類是另外一個類型定義,它使用新的屬性和方法擴展父類,而後能夠在運行時實例化它們。原型的子代是另外一個對象實例,它將任何未在子代上實現的屬性委託給父代。

旁註:你可能想知道爲何我提到了類方法,但沒有提到原型方法。那是由於 JavaScript 沒有方法的概念。函數在 JavaScript 中是一流的,它們能夠具備屬性或是其餘對象的屬性。

類構造函數用來建立類的實例。 JavaScript 中的構造函數只是一個返回對象的普通函數。 JavaScript 構造函數惟一的特別之處在於,當使用 new 關鍵字調用時,它會將其原型指定爲返回對象的原型。若是這對你來講聽起來有點混亂,那麼你並不孤單 —— 它就是原型很難理解的緣由。

爲了說明一點,原型的子代不是原型的副本,也不是與原型相同的對象。子代對原型有生命參考,而且子代上不存在的原型屬性是對原型上具備相同名稱屬性的單向引用。。

思考如下代碼:

let parent = { foo: 'foo' }
let child = { }
Object.setPrototypeOf(child, parent)

console.log(child.foo) // 'foo'

child.foo = 'bar'

console.log(child.foo) // 'bar'

console.log(parent.foo) // 'foo'

delete child.foo

console.log(child.foo) // 'foo'

parent.foo = 'baz'

console.log(child.foo) // 'baz'
注意:你幾乎不會在現實中寫這樣的代碼 —— 這是一種可怕的作法 —— 但它簡潔地證實了這一原則。

在前面的例子中,當 child.fooundefined 時,它引用了 parent.foo。一旦在 child 上定義了 foochild.foo 的值爲 'bar',但 parent.foo 保留了原始值。一旦咱們 delete child.foo,它將會再次引用 parent.foo,這意味着當咱們更改父項的值時,child.foo 指的是新值。

讓咱們來看看剛纔發生了什麼(爲了更清楚地說明,咱們假設這些是 Strings 而不是字符串字面量,這裏的區別並不重要):

clipboard.png

它的工做方式,特別是 newthis 的特色是另外一個主題,但若是你想學到更多的內容,能夠查閱 Mozilla 的關於 JavaScript 的原型繼承鏈的一篇詳盡的文章

關鍵的一點是原型沒有定義 type,它們自己就是 instances ,而且它們在運行時是可變的。

還有勇氣往下讀嗎?接下來讓咱們再回過頭來剖析 JavaScript 類。

JavaScript 小測驗 #3:如何在類中實現私有?

上面的原型和類屬性並無被「封裝」爲外部不可訪問的私有成員。應該怎樣解決這個問題呢?

這裏沒有代碼示例。答案是,你作不到。

JavaScript 沒有任何私有的概念,可是它有閉包:

function SecretiveProto() {
  const secret = "The Class is a lie!"
  this.spillTheBeans = function() {
    console.log(secret)
  }
}

const blabbermouth = new SecretiveProto()
try {
  console.log(blabbermouth.secret)
}
catch(e) {
  // TypeError: SecretiveClass.secret is not defined
}

blabbermouth.spillTheBeans() // "The Class is a lie!"

你明白剛纔發生了什麼嗎?若是不明白的話就沒搞懂閉包。好吧,可是它們並不那麼使人生畏,並且很是有用,你應該花一些時間來了解它們

JavaScript 小測驗 #4:怎樣用 class 關鍵字寫出與上面功能相同的代碼?

對不起,這是另外一個技巧問題。你能夠作一樣的事情,但它看起來是這樣的:

class SecretiveClass {
  constructor() {
    const secret = "I am a lie!"
    this.spillTheBeans = function() {
      console.log(secret)
    }
  }

  looseLips() {
    console.log(secret)
  }
}

const liar = new SecretiveClass()
try {
  console.log(liar.secret)
}
catch(e) {
  console.log(e) // TypeError: SecretiveClass.secret is not defined
}
liar.spillTheBeans() // "I am a lie!"

若是你以爲這看起來比 SecretiveProto 更簡單或更清晰,那麼請告訴我。在我我的看來,它有點糟糕 —— 它打破了 JavaScript 中 class 聲明的習慣用法,而且它不像你指望的那樣來自 Java。這將經過如下方式代表:

JavaScript 小測驗#5: SecretiveClass::looseLips() 是作什麼用的?

咱們來看看這段代碼:

try {
  liar.looseLips()
}
catch(e) {
  // ReferenceError: secret is not defined
}

嗯……這很尷尬。

JavaScript Pop Quiz#6:經驗豐富的 JavaScript 開發人員更喜歡原型仍是類?

你猜對了,這又是一個關於技巧問題 —— 經驗豐富的 JavaScript 開發人員傾向於儘量避免二者。如下是使用 JavaScript 執行上述操做的慣用的好方法:

function secretFactory() {
  const secret = "Favor composition over inheritance, `new` is considered harmful, and the end is near!"
  const spillTheBeans = () => console.log(secret)

  return {
    spillTheBeans
  }
}

const leaker = secretFactory()
leaker.spillTheBeans()

這不只僅是爲了不繼承的醜陋或強制封裝。想想你能用 secretFactoryleaker 作些什麼,你用原型或類作可不能輕易的作到。

首先,你能夠解構它,由於你沒必要擔憂 this 的上下文:

const { spillTheBeans } = secretFactory()

spillTheBeans() // Favor composition over inheritance, (...)

這真是太好了。除了避免使用 newthis 作蠢事以外,它還容許咱們將對象與 CommonJS 和 ES6 模塊互換使用。它還使開發更容易:

function spyFactory(infiltrationTarget) {
  return {
    exfiltrate: infiltrationTarget.spillTheBeans
  }
}

const blackHat = spyFactory(leaker)

blackHat.exfiltrate() // Favor composition over inheritance, (...)

console.log(blackHat.infiltrationTarget) // undefined (looks like we got away with it)

使用 blackHat 的程序員沒必要擔憂 exfiltrate 來自哪裏,spyFactory 也沒必要亂用 Function::bind 的上下文小伎倆或深層嵌套屬性。請注意,咱們無需在簡單的同步過程代碼中擔憂 this,但它會致使異步代碼中的各類問題。

通過一番思考,spyFactory 能夠發展成爲一種高度複雜的間諜工具,能夠處理各類滲透目標 - 換句話說,就是外觀模式

固然你也能夠用類來作,或者更確切地說,是各類各樣的類,全部類都繼承自 abstract classinterface 等,不過 JavaScript 沒有任何抽象或接口的概念。

讓咱們用一個更好的例子來看看如何用工廠模式實現它:

function greeterFactory(greeting = "Hello", name = "World") {
  return {
    greet: () => `${greeting}, ${name}!`
  }
}

console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!

這比原型或類的版本更簡潔。它能夠更有效地實現其屬性的封裝。此外,它在某些狀況下具備較低的內存和性能影響(乍一看彷佛不太可能,但 JIT 編譯器正悄悄地在幕後作了減小重複和推斷類型的工做)。

所以它更安全,一般狀況下也更快,而且編寫這樣的代碼更容易。爲何咱們又須要類了呢?哦,固然是可重用性。若是咱們想要一個unhappy 且 enthusiastic 的 greeting會怎樣?好吧,若是咱們用的是 ClassicalGreeting 類,可能會直接跳到夢想中的類層次結構中。咱們知道本身須要參數化符號,因此會作一些重構並添加一些子類:

// Greeting class
class ClassicalGreeting {
  constructor(greeting = "Hello", name = "World", punctuation = "!") {
    this.greeting = greeting
    this.name = name
    this.punctuation = punctuation
  }

  greet() {
    return `${this.greeting}, ${this.name}${this.punctuation}`
  }
}

// An unhappy greeting
class UnhappyGreeting extends ClassicalGreeting {
  constructor(greeting, name) {
    super(greeting, name, " :(")
  }
}

const classyUnhappyGreeting = new UnhappyGreeting("Hello", "everyone")

console.log(classyUnhappyGreeting.greet()) // Hello, everyone :(

// An enthusiastic greeting
class EnthusiasticGreeting extends ClassicalGreeting {
  constructor(greeting, name) {
    super(greeting, name, "!!")
  }

  greet() {
    return super.greet().toUpperCase()
  }
}

const greetingWithEnthusiasm = new EnthusiasticGreeting()

console.log(greetingWithEnthusiasm.greet()) // HELLO, WORLD!!

這是一個很好的方法,直到有人出現並要求實現一個不能徹底適合層次結構的功能,整個事情都沒有任何意義。當咱們嘗試用工廠模式編寫相同的功能時,在這個想法中放一個引腳:

const greeterFactory = (greeting = "Hello", name = "World", punctuation = "!") => ({
  greet: () => `${greeting}, ${name}${punctuation}`
})

// Makes a greeter unhappy
const unhappy = (greeter) => (greeting, name) => greeter(greeting, name, ":(")

console.log(unhappy(greeterFactory)("Hello", "everyone").greet()) // Hello, everyone :(

// Makes a greeter enthusiastic
const enthusiastic = (greeter) => (greeting, name) => ({
  greet: () => greeter(greeting, name, "!!").greet().toUpperCase()
})

console.log(enthusiastic(greeterFactory)().greet()) // HELLO, WORLD!!

雖然它的代碼更短,但獲得的好處並不明顯。實際上你可能會以爲它更難以閱讀,也許這是一種遲鈍的方法。難道咱們不能只有一個 unhappyGreeterFactory 和一個 passionsticGreeterFactory

而後你的客戶出現並說:「我須要一個不開心的新員工,但願整個辦公室都能認識它!」

console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(

若是咱們須要不止一次地使用這個 enthusiastically 且 unhappy 的 greeter,能夠更容易實現:

const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory))

console.log(aggressiveGreeterFactory("You're late", "Jim").greet())

這種合成風格的方法適用於原型或類。例如,你能夠將 UnhappyGreetingEnthusiasticGreeting 從新考慮爲裝飾器。它仍然須要比上面的函數風格方法更多的樣板,但這是你爲真正的類的安全性和封裝所付出的代價。

問題是,在 JavaScript 中,你沒有獲得自動安全性。強調 class 用法的 JavaScript 框架會對這些問題變不少「魔術」,並強制類使用本身的行爲。看看 Polymer 的 ElementMixin 源代碼,我敢說。它簡直是 JavaScript 神器級別的代碼,我沒有任何諷刺的意思。

固然,咱們能夠用 Object.freezeObject.defineProperties 來解決上面討論的一些問題,以達到更大或更小的效果。可是爲何要在沒有函數的狀況下模仿表單,而忽略了 JavaScript 自己爲咱們提供的工具?當你的工具箱旁邊有真正的螺絲刀時,你會用一把標有 「螺絲刀」 的錘子來驅動螺絲嗎?

找到好的部分

JavaScript 開發人員常常強調語言的優勢。咱們選擇試圖經過堅持編寫乾淨、可讀、最小化、可重用的代碼來避免其可疑的語言設計和陷阱。

關於 JavaScript 的哪些部分是合理的,我但願已經說服了你,class 不是其中之一。若是作不到這一點,但願你能理解 JavaScript 中的繼承多是混亂且使人困惑的。並且 class 既不去修復它,也不會讓你不得不去理解原型。若是你瞭解到面向對象的設計模式在沒有類或 ES6 繼承的狀況下正常工做的提示,則可得到額外的好處。

我並無告訴你要徹底避免 class。有時你須要繼承,而 class 爲此提供了更清晰的語法。特別是,class X extends Y 比舊的原型方法更好。除此以外,許多流行的前端框架鼓勵使用它,你應該避免在原則上單獨編寫奇怪的非標準代碼。我只是不喜歡它的發展方向。

在個人噩夢中,整整一代的 JavaScript 庫都是使用 class 編寫的,指望它的行爲與其餘流行語言相似。即便咱們沒有不當心掉進 class 的陷阱,它也可能復活在錯誤的 JavaScript 墓地之中。經驗豐富的JavaScript開發人員常常受到這些怪物的困擾,由於流行的並不老是好的。

最終咱們都沮喪地放棄了,開始從新發明 Rust、Go、Haskell 或者其它相似這樣的輪子,而後爲 web 編譯爲Wasm,新的 Web 框架和庫擴散到無限多的語言中。

它確實讓我夜不能寐。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索