做为一名JS开发人员,是什么使我夜不能寐

做者:Justen Robertsonjavascript

翻译:疯狂的技术宅前端

原文:www.toptal.com/javascript/…java

未经容许严禁转载git

JavaScript 是一种奇怪的语言。虽然受到 Smalltalk 的启发,但它用了相似 C 的语法。它结合了程序、函数和面向对象编程(OOP)的方方面面。它有许多可以解决几乎任何编程问题的方法,这些方法一般是多余的,并无强烈推荐哪些是首选。它是弱动态类型,但采用了相似强制类型的方法,使经验丰富的开发人员也可使用。程序员

JavaScript 也有其瑕疵、陷阱和可疑的功能。新手程序员须要努力解决一些更为困难的概念 —— 异步性、封闭性和提高。具备其余语言经验的程序员合理地假设具备类似名称的东西,可是看上去与 JavaScript 相同的工做方法每每是错误的。数组不是真正的数组,什么是 this,什么是原型, new 实际上作了些什么?es6

ES6 类的麻烦

到目前为止,最糟糕的罪魁祸首是 JavaScript 的最新版本——ECMAScript 6(ES6)的。一些关于类的讨论坦率地使人震惊,并揭示了对语言实际运做机制的根深蒂固的误解:github

“JavaScript 如今终于成为一种真正的面向对象的语言,由于它有类!”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 而不是字符串字面量,这里的区别并不重要):

显示如何在JavaScript中处理缺乏的引用的原型链

它的工做方式,特别是 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 框架和库扩散到无限多的语言中。

它确实让我夜不能寐。

欢迎关注前端公众号:前端先锋,获取前端工程化实用工具包。

相关文章
相关标签/搜索