作为JS开发人员,这就是让我彻夜难眠的原因

本文概述

  • ES6类的麻烦
  • JavaScript流行测验#1:这些代码块之间的本质区别是什么?
  • JavaScript流行测验#2:以下代码做什么?
  • 原型与类
  • JavaScript流行测验#3:如何在课堂中实现隐私?
  • JavaScript流行测验#4:使用关键字类与上述内容等效?
  • JavaScript流行测验#5:SecretiveClass :: looseLips()的作用是什么?
  • JavaScript流行测验#6:经验丰富的JavaScript开发人员更喜欢哪种?原型还是类?
  • 寻找好零件
JavaScript是一种语言的怪胎。尽管受到Smalltalk的启发, 但它使用的是类似C的语法。它结合了过程, 功能和面向对象编程(OOP)范例的各个方面。它具有解决几乎所有可能的编程问题的许多方法, 通常是多余的方法, 并且对于哪种方法是优选的没有强烈的意见。它的输入方式弱而动态, 采用迷宫般的方式来强制输入, 甚至使经验丰富的开发人员也无法使用。
JavaScript也有其缺陷, 陷阱和可疑的功能。新程序员要面对一些较困难的概念(例如异步性, 闭包和吊装)。具有其他语言经验的程序员会合理地假设名称和外观相似的事物在JavaScript中的工作方式相同, 并且通常是错误的。数组不是真正的数组;这有什么用, 原型是什么, 新的实际作用是什么?
ES6类的麻烦 到目前为止, 最严重的违规行为是JavaScript的最新发行版ECMAScript 6(ES6):类的新增内容。围绕类的一些讨论坦率地令人震惊, 并揭示了对语言实际工作方式的根深蒂固的误解:
“ 现在有了类, JavaScript终于是一种真正的面向对象的语言!”
要么:
“ 类使我们摆脱了JavaScript破坏的继承模型的思考。”
甚至:
“ 类是在JavaScript中创建类型的更安全, 更轻松的方法。”
这些陈述不会打扰我, 因为它们暗示原型继承有问题。让我们搁置这些论点。这些陈述令我感到困扰, 因为它们都不是真的, 而且它们证明了JavaScript” 为所有人提供的一切” 方法在语言设计中的后果:它使程序员对语言的理解更多地失去了对语言的理解。在继续之前, 让我们举例说明。
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类语法的问题。
是的, 第二个例子更具表现力。仅出于这个原因, 你可能会认为类是对语言的一种不错的补充。不幸的是, 这个问题更加微妙。
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

如果回答不正确, 说明你实际上不知道什么是上课。这不是你的错就像Array一样, class不是语言功能, 它是句法晦涩的词。它试图隐藏原型继承模型和随之而来的笨拙习惯用法, 这暗示着JavaScript正在做某些事情, 而事实并非如此。
你可能已经被告知JavaScript引入了类, 以使来自Java之类的经典OOP开发人员更喜欢ES6类继承模型。如果你是这些开发人员之一, 那么该示例可能会使你感到恐惧。这应该。它表明JavaScript的class关键字没有提供类要提供的任何保证。它还演示了原型继承模型中的关键区别之一:原型是对象实例, 而不是类型。
原型与类 【作为JS开发人员,这就是让我彻夜难眠的原因】基于类的继承与基于原型的继承之间最重要的区别是, 类定义了可以在运行时实例化的类型, 而原型本身就是对象实例。
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.fooconsole.log(child.foo) // 'foo'parent.foo = 'baz'console.log(child.foo) // 'baz'

注意:在现实生活中, 你几乎永远不会写这样的代码-这是很糟糕的做法-但它简洁地演示了该原理。
在上一个示例中, 虽然未定义child.foo, 但它引用了parent.foo。一旦我们在child上定义了foo, child.foo的值就为’ bar’ , 但是parent.foo保留了其原始值。删除child.foo之后, 它将再次引用parent.foo, 这意味着当我们更改父项的值时, child.foo会引用新值。
让我们看一下发生了什么(为了更清楚地说明, 我们将它们假装成字符串而不是字符串文字, 这里的区别不重要):
作为JS开发人员,这就是让我彻夜难眠的原因

文章图片
它的工作方式, 尤其是new和this的特性, 是另一天的话题, 但是如果你想了解更多, Mozilla会提供一篇有关JavaScript原型继承链的详尽文章。
关键的一点是, 原型没有定义类型。它们本身就是实例, 它们在运行时是可变的, 具有所有隐含和必然性。
还在我这儿?让我们回到剖析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 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中类声明的惯用用法, 并且效果不像你期望的那样, 例如Java。这将通过以下内容清楚地表明:
JavaScript流行测验#5:SecretiveClass :: looseLips()的作用是什么? 让我们找出:
try { liar.looseLips() } catch(e) { // ReferenceError: secret is not defined }

好吧……那很尴尬。
JavaScript流行测验#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()

这不仅仅是避免固有的继承丑陋或强制封装。考虑一下你可能会对secretFactory和leaker做什么, 而原型或类则很难做到。
一方面, 你可以对其进行重组, 因为你不必担心其上下文:
const { spillTheBeans } = secretFactory()spillTheBeans() // Favor composition over inheritance, (...)

很好除了避免新的和这种愚弄之外, 它还允许我们将对象与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的客户不必担心漏洞的来源, 而spyFactory则不必乱搞Function :: bind上下文杂乱或深层嵌套的属性。请注意, 我们在简单的同步过程代码中不必为此担心, 但是它会导致异步代码中的各种问题, 而这些问题最好避免。
稍加思考, 即可将spyFactory开发为一种高度复杂的间谍工具, 可以处理各种渗透目标(换句话说, 是立面)。
当然, 你也可以使用一个类, 或者说一个各种各样的类来做到这一点, 所有这些类都继承自抽象类或接口……除了JavaScript没有任何抽象或接口的概念。
让我们回到问候示例, 看看如何在工厂中实现它:
function greeterFactory(greeting = "Hello", name = "World") { return { greet: () => `${greeting}, ${name}!` } }console.log(greeterFactory("Hey", "folks").greet()) // Hey, folks!

你可能已经注意到, 随着我们的发展, 这些工厂变得越来越简洁, 但请不要担心-他们是同一件事。伙计们, 训练轮脱落了!
这已经比相同代码的原型或类版本要少。其次, 它更有效地实现了其特性的封装。而且, 在某些情况下, 它具有较低的内存和性能占用空间(乍看之下似乎并不像它, 但是JIT编译器正在悄悄地进行幕后工作, 以减少重复和推断类型)。
这样更安全, 通常更快, 编写这样的代码也更容易。为什么我们需要再次上课?哦, 当然是可重用性。如果我们想要不快乐和热情的迎宾者变体会怎样?好吧, 如果我们使用的是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!!

尽管这段代码短了一点, 但它显然不是更好的代码。实际上, 你可能会争辩说它很难阅读, 也许这是一种过时的方法。我们不能只拥有一个不快乐的GreetFactory和一个热情的GreetFactory吗?
然后, 你的客户说:” 我需要一个不高兴的新迎宾员, 并希望整个房间都知道这一点!”
console.log(enthusiastic(unhappy(greeterFactory))().greet()) // HELLO, WORLD :(

如果我们需要多次使用这种热情不满的问候语, 我们可以使自己更轻松:
const aggressiveGreeterFactory = enthusiastic(unhappy(greeterFactory))console.log(aggressiveGreeterFactory("You're late", "Jim").greet())

有一些方法可以与原型或类一起使用。例如, 你可以重新考虑将UnhappyGreeting和EnthusiasticGreeting作为装饰器。与上面使用的功能样式方法相比, 它仍然需要更多样板, 但这就是你为实型类的安全性和封装所付出的代价。
关键是, 在JavaScript中, 你不会获得这种自动安全性。强调类使用的JavaScript框架在解决此类问题方面具有很多” 魔力” , 可以迫使类表现自己。我敢, 有一段时间请看Polymer的ElementMixin源代码。这是JavaScript至上的怪癖级别, 我的意思是没有讽刺或讽刺意味。
当然, 我们可以用Object.freeze或Object.defineProperties解决以上讨论的某些问题, 以或多或少地产生效果。但是, 为什么在不使用功能的情况下模仿表单, 而忽略工具, JavaScript确实为我们提供了我们在Java等语言中找不到的工具?当你的工具箱旁边有真正的螺丝刀时, 你会使用标有” 螺丝刀” 的锤子来驱动螺丝吗?
寻找好零件 JavaScript开发人员经常在口语上和参考同名书时都强调该语言的优点。我们试图避免由其更可疑的语言设计选择所设置的陷阱, 并坚持让我们编写简洁, 可读, 错误最小化, 可重用代码的部分。
关于JavaScript的哪些部分合格, 有合理的论据, 但我希望我已经说服你该类不是其中之一。失败的话, 希望你理解JavaScript中的继承可能会造成混乱, 并且该类既不能解决该问题, 也使你不必去理解原型。如果你发现有关面向对象的设计模式在没有类或ES6继承的情况下可以很好地工作的提示, 则格外值得赞扬。
我并不是要你完全避免上课。有时你需要继承, 而类提供了更简洁的语法来实现这一点。特别是, 类X扩展Y比旧的原型方法好得多。除此之外, 许多流行的前端框架都鼓励使用它, 你应该避免仅凭原则编写怪异的非标准代码。我只是不喜欢这要去哪里。
在我的噩梦中, 整个JavaScript库都是使用类编写的, 期望它的行为与其他流行语言相似。发现了全新的bug类(双关语)。如果我们不小心落入类陷阱中, 很可能会遗留在格式错误的JavaScript墓地中的旧版本。经验丰富的JavaScript开发人员为这些怪物所困扰, 因为流行的并不总是善良的。
最终, 我们所有人都放弃了挫败感, 开始重新发明Rust, Go, Haskell或其他人知道的轮子, 然后编译为Wasm进行网络使用, 新的Web框架和库激增为多语言的无限。
的确使我彻夜难眠。

    推荐阅读