作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
贾斯汀·罗伯逊的头像

Justen罗伯逊

Justen有十年的全栈JavaScript经验,曾与Taylor Swift和Red Hot Chili Peppers等人合作.

工作经验

16

分享

JavaScript 是一种古怪的语言吗. 虽然受到Smalltalk的启发,但它使用了类似c的语法. 它结合了过程式、函数式和面向对象编程(OOP)范例的各个方面. 它有 众多, 通常冗余, 解决几乎所有可想到的编程问题的方法,并且不强烈地认为哪种方法是首选. 它是弱动态类型的, 使用迷宫式的类型强制转换方法,即使是经验丰富的开发人员也会绊倒.

JavaScript也有它的缺点、陷阱和有问题的特性. 新程序员会遇到一些更困难的概念——想想异步性, 闭包, 和提升. 具有其他语言经验的程序员会合理地假设具有类似名称和外观的东西在JavaScript中会以相同的方式工作,但这往往是错误的. 数组s 是n’t 真正的ly arrays; what’s the deal with 什么是原型,什么是原型 实际做的?

ES6类的问题

到目前为止,最糟糕的是JavaScript最新版本的新手, ECMAScript 6 (ES6): . 坦率地说,关于课程的一些讨论令人担忧,并揭示了对语言实际运作方式的根深蒂固的误解:

“JavaScript终于成为了 真正的 面向对象语言,现在它有了类!”

Or:

“类将我们从JavaScript破碎的继承模型中解放出来.”

甚至:

类是在JavaScript中创建类型的一种更安全、更简单的方法.”

这些说法并不困扰我,因为它们暗示着有什么问题 典型的继承; let’s set aside those arguments. 这些说法让我很困扰,因为它们都不是真的, 它们展示了JavaScript“为每个人提供一切”的语言设计方法的后果:它削弱了程序员对语言的理解,而不是它所能做到的. 在我进一步讨论之前,让我们来说明一下.

JavaScript突击测验#1:这些代码块之间的本质区别是什么?

函数原型typicalGreeting(greeting = "Hello", 名字。 = "World") {
  这.问候
  这.Name = Name
}

原型typicalGreeting.原型.Greet = function() {
  返回“$ {.祝福},{这美元.名称}!`
}

const greet原型 = 新 原型typicalGreeting("Hey", "folks")
控制台.日志(greet原型.问候())
ClassicalGreeting {
  构造函数(greeting = "Hello", 名字。 = "World") {
    这.问候
    这.Name = Name
  }

  问候(){
    返回“$ {.祝福},{这美元.名称}!`
  }
}

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

控制台.日志(classyGreeting.问候())

这里的答案是 没有一个. 它们实际上做同样的事情,只是是否使用了ES6类语法的问题.

没错,第二个例子更具表现力. 仅仅因为这个原因,你可能会说 class 是对语言的一个很好的补充吗. 不幸的是,问题更微妙一些.

JavaScript突击测验#2:下面的代码是做什么的?

函数原型() {
  这.名字。 = '原型'
  返回;
}

原型.原型.getName = function() {
  返回这.名字。
}

类MyClass扩展原型 {
  构造函数(){
    super ()
    这.名字。 = 'MyClass'
  }
}

const 实例 = 新 MyClass()

控制台.日志(实例.getName ())

原型.原型.getName = function(){返回'在原型中重写'}

控制台.日志(实例.getName ())

MyClass.原型.getName = function(){返回'在MyClass中被重写'}

控制台.日志(实例.getName ())

实例.getName = function(){返回'在实例中被重写'}


控制台.日志(实例.getName ())

正确的答案是打印到控制台:

> MyClass
> Overridden in 原型
> Overridden in MyClass
> Overridden in 实例

如果你答错了,说明你不明白是什么 class 实际上是. 这不是你的错. 就像 数组, class 不是一个语言特性,它是 语法蒙昧主义. 它试图隐藏原型继承模型和随之而来的笨拙的习惯用法, 这意味着JavaScript在做一些它没有做的事情.

也许有人告诉过你 class 引入JavaScript是为了让来自Java等语言的经典OOP开发人员更适应JavaScript/ES6类继承模型. 如果你 其中一个开发者,这个例子可能会吓到你. 它应该. 它显示了JavaScript class 关键字没有提供任何类应该提供的保证. 它还演示了原型继承模型中的一个关键区别:原型是 对象实例,而不是 类型.

JavaScript原型vs. 类

JavaScript基于类的继承和基于原型的继承之间最重要的区别是,一个类定义了一个 类型 哪个可以在运行时实例化,而原型本身是一个对象实例.

ES6类的子类是另一种 类型 定义,它用新的属性和方法扩展父类, 哪些可以在运行时实例化. 原型的子对象是另一个对象 实例 哪个将没有在子进程上实现的属性委托给父进程.

旁注:您可能想知道为什么我提到了类方法,而没有提到原型方法. 这是因为JavaScript没有方法的概念. 函数是 一流的 它们可以有属性,也可以是其他对象的属性.

类构造函数创建类的实例. JavaScript中的构造函数只是一个返回对象的普通函数. JavaScript构造函数的唯一特殊之处在于,当使用 关键字,它将自己的原型赋值为返回对象的原型. 如果这听起来让你有点困惑, 你并不孤单——确实如此, 这也是为什么原型很难被理解的重要原因.

明确一点,原型的子元素并不是 复制 它的原型,也不是一个相同的对象 形状 作为它的原型. 这孩子有活生生的参照 to 原型, 任何在子节点上不存在的原型属性都是对原型上同名属性的单向引用.

考虑以下几点:

让家长。 = {喷火: '喷火'}
Let 孩子 = {}
Object.set原型类型Of(孩子、家长)

控制台.日志(孩子.Foo) // ' Foo '

孩子.Foo = “酒吧”

控制台.日志(孩子.Foo) // “酒吧”

控制台.日志(父.Foo) // ' Foo '

删除的孩子.喷火

控制台.日志(孩子.Foo) // ' Foo '

家长。.Foo = 'baz'

控制台.日志(孩子.Foo) // 'baz'
注意:在现实生活中,您几乎不会编写这样的代码——这是一种糟糕的实践——但它简洁地演示了该原则.

在前面的例子中,while 孩子.喷火未定义的,它参考了 家长。.喷火. 只要我们定义 喷火 on 孩子, 孩子.喷火 有价值 “酒吧”,但 家长。.喷火 保留了原来的价值. 一旦我们 删除的孩子.喷火 它又指的是 家长。.喷火,这意味着当我们改变父节点的值时, 孩子.喷火 指新的值.

让我们看看刚刚发生了什么(为了更清楚地说明), 我们要假装这些是 字符串 而不是字符串字面量,这里的区别并不重要):

遍历原型链,展示在JavaScript中如何处理缺少的引用.

它的工作原理,尤其是它的特点 ,这是另一个话题,但Mozilla已经做到了 一篇关于JavaScript原型继承链的详细文章 如果你想读更多.

关键的收获是,原型并没有定义 类型; they 是 themselves 实例 它们在运行时是可变的.

还在我身边? 让我们继续剖析JavaScript类.

JavaScript突击测验#3:如何在类中实现隐私?

我们上面的原型和类属性并没有像“悬在窗外”那样被“封装”.“我们应该解决这个问题,但如何解决呢?

这里没有代码示例. 答案是你不能.

JavaScript没有任何隐私的概念,但它有闭包:

函数Secretive原型() {
  const secret = "这个类是一个谎言!"
  这.spillTheBeans = function() {
    控制台.日志(秘密)
  }
}

const 长舌者 = 新 Secretive原型()
尝试{
  控制台.日志(长舌者.秘密)
}
抓住(e) {
  //类型错误:SecretiveClass.Secret没有定义
}

长舌者.spillTheBeans () // "这个类是一个谎言!"

你明白刚才发生了什么吗? 如果不是,说明您不理解闭包. 没关系, 真的,他们不像人们想象的那么可怕, 它们非常有用, 你应该 花点时间去了解他们.

让我们看看JavaScript类与函数问题的另一半.

JavaScript突击测试#4:使用 class 关键字?

抱歉,这又是一个刁钻的问题. 你基本上可以做同样的事情,但它看起来像这样:

类SecretiveClass {
  构造函数(){
    我是一个谎言!"
    这.spillTheBeans = function() {
      控制台.日志(秘密)
    }
  }

  looseLips () {
    控制台.日志(秘密)
  }
}

const 骗子 = 新 SecretiveClass()
尝试{
  控制台.日志(骗子.秘密)
}
抓住(e) {
  控制台.log(e) //类型错误:SecretiveClass . log.Secret没有定义
}
骗子.spillTheBeans () // "我是一个谎言!"

让我知道这看起来是否更容易或更清楚 Secretive原型. 在我个人看来,它更糟糕——它打破了习惯用法 class 它的工作方式不像您期望的那样来自Java. 这将通过以下方式加以说明:

JavaScript突击测验#5:What Does SecretiveClass: looseLips () Do?

让我们来看看:

尝试{
  骗子.looseLips ()
}
抓住(e) {
  // ReferenceError: secret没有定义
}

嗯,那太尴尬了.

JavaScript突击测试#6:经验丰富的JavaScript开发人员更喜欢哪个-原型还是类?

你猜对了, 这是另一个棘手的问题——有经验的JavaScript开发人员会尽量避免这两个问题. 这里有一个很好的方法用惯用的JavaScript来完成上面的工作:

函数secretFactory() {
  const secret = "喜欢组合胜过继承,'新'被认为是有害的,末日临近了。!"
  const spillTheBeans = () => 控制台.日志(秘密)

  返回{
    spillTheBeans
  }
}

const 泄密者 = secretFactory()
泄密者.spillTheBeans ()

这不仅仅是为了避免继承固有的丑陋,或者强制封装. 想想你还能做些什么 secretFactory泄密者 这在原型或类中是不容易做到的.

首先,你可以解构它因为你不用担心上下文 :

const {spillTheBeans} = secretFactory()

spillTheBeans () //组合优于继承,(...)

这很好. 除了避免 它允许我们与CommonJS和ES6模块互换使用我们的对象. 它也使构图更容易:

函数spyFactory(渗透目标){
  返回{
    漏出:infiltrationTarget.spillTheBeans
  }
}

const 黑帽 = spyFactory(泄密者)

黑帽.漏出() //选择组合而不是继承,(...)

控制台.日志(统称.//未定义(看起来我们成功了)

的客户 黑帽 不用担心在哪里 漏出 来自,并且 spyFactory 不需要浪费时间 功能::绑定 上下文杂耍或深度嵌套属性. 提醒你一下,我们不用太担心 用简单的同步过程代码, 但是它会在异步代码中引起各种问题,这些问题最好避免.

稍微想一下, spyFactory 可以发展成一个高度复杂的间谍工具,可以处理各种渗透目标,或者换句话说, a 外观.

当然,你也可以在类中这样做, 或者更确切地说, 各种各样的课程, 所有这些都继承自 抽象类 or 接口除了JavaScript没有任何抽象或接口的概念.

让我们回到迎宾器的例子,看看我们如何用工厂来实现它:

函数greeterFactory(greeting = "Hello", 名字。 = "World") {
  返回{
    greet: () => `${greeting}, ${名称}!`
  }
}

控制台.日志(greeterFactory(“嘿”,“人”).问候()) //嘿,伙计们!

你可能已经注意到,随着我们的进行,这些工厂变得越来越简洁, 不过别担心——它们的作用是一样的. 伙计们,训练的轮子要脱落了!

这已经比相同代码的原型或类版本更少的样板文件了. 其次,它更有效地实现了其属性的封装. 也, 在某些情况下,它具有较低的内存和性能占用(乍一看可能不是这样), 但是JIT编译器在幕后悄悄地工作,以减少重复和推断类型)。.

所以它更安全,通常更快,编写这样的代码也更容易. 我们为什么还要上课? 哦,当然是可重用性. 如果我们想要不开心和热情的迎宾员,会发生什么呢? 如果我们用 ClassicalGreeting 类的时候,我们可能会直接开始想象一个类的层次结构. 我们知道我们需要参数化标点符号, 所以我们将做一些重构并添加一些子节点:

//问候类
ClassicalGreeting {
  构造函数(greeting = "Hello", 名字。 = "World", punctuation = "!") {
    这.问候
    这.Name = Name
    这.标点符号
  }

  问候(){
    返回“$ {.祝福},{这美元.名称}$ {.标点符号}’
  }
}

//不愉快的问候
UnhappyGreeting类扩展ClassicalGreeting {
  构造函数(greeting, 名字。) {
    Super(问候,名字,":("))
  }
}

const classsyunhappygreeting = 新 UnhappyGreeting("Hello", "everyone")

控制台.日志(classyUnhappyGreeting.问候()) //大家好:(

//热烈的问候
ClassicalGreeting类扩展ClassicalGreeting {
  构造函数(greeting, 名字。) {
	超级问候,名字,”!!")
  }

  问候(){
	返回超级.问候().toUpperCase ()
  }
}

const greetingWithEnthusiasm = 新 EnthusiasticGreeting()

控制台.日志(greetingWithEnthusiasm.问候()) // HELLO, WORLD!!

这是一个很好的方法, 直到有人出现,并要求一个不能完全符合层次结构的功能,整个事情就变得没有意义了. 当我们尝试用工厂编写相同的功能时,先把这个想法放在一边:

const greeterFactory = (greeting = "Hello", 名字。 = "World",标点符号= "!") => ({
  greet: () => `${greeting}, ${名称}${标点符号}’
})

//让迎宾员不高兴
const unhappy = (greeter) => (greeting, 名字。) => greeter(greeting, 名字。, ":(")

控制台.日志(不开心(greeterFactory)(“你好”,“每个人”).问候()) //大家好:(

//使迎宾员热情
const enthusiastic = (greeter) => (greeting, 名字。) => ({
  greet: () => greeter(greeting, 名字。, "!!").问候().toUpperCase ()
})

控制台.日志(热情(greeterFactory) ().问候()) // HELLO, WORLD!!

这段代码是否更好并不明显,尽管它更短一些. 事实上,你可能会说它更难读,也许这是一种迟钝的方法. 我们就不能 unhappyGreeterFactory 和一个 enthusiasticGreeterFactory?

然后你的客户走过来说, “我需要一个不开心的新迎宾员,要让整个房间的人都知道!”

控制台.日志(热情(不开心(greeterFactory)) ().问候()) // HELLO, WORLD:(

如果我们需要不止一次地使用这个热情而不开心的问候, 我们可以让自己更轻松:

热情的(不高兴的)

控制台.“你迟到了”,“吉姆”.问候())

对于这种组合风格,有一些方法可以与原型或类一起工作. 例如,你可以重新思考 UnhappyGreetingEnthusiasticGreeting as 修饰符. 与上面使用的函数式方法相比,它仍然需要更多的样板文件, 但这是你为安全性和封装所付出的代价 真正的 类.

问题是,在JavaScript中,你无法获得自动安全性. JavaScript框架强调 class usage做了很多“魔法”来掩盖这类问题,并强制JS类按照自己的方式行事. 看看聚合物的 ElementMixin 源代码 总有一天,我敢打赌. 这是JavaScript奥秘的高级向导级别,我的意思不是讽刺或讽刺.

当然,我们可以解决上面讨论的一些问题 Object.冻结 or Object.defineProperties 或大或小的影响. 但是为什么要模仿没有函数的表单,而忽略JavaScript工具呢 为我们提供了在Java等语言中可能找不到的功能? 你会用一把标有“螺丝刀”的锤子来拧螺丝吗, 当你的工具箱旁边放着一把螺丝刀的时候?

找到好的部分

JavaScript开发人员经常强调该语言的优点, 口语化的和指涉的 同名的书. 我们试图避免由其更有问题的语言设计选择所设置的陷阱,并坚持让我们编写干净的部分, 可读的, error-minimizing, 可重用代码.

关于JavaScript的哪些部分符合条件,有一些合理的争论, 但我希望我已经说服了你 class 不是其中之一吗. 如果做不到这一点, 希望你能理解JavaScript中的继承是一团混乱 class 既不能修复它,也不能让你不必去理解原型. 如果您注意到面向对象的设计模式在没有类或ES6继承的情况下也能很好地工作,那就额外加分了.

我不是告诉你要避免 class 完全. 有时你需要继承 class 为此提供了更清晰的语法. 特别是, 类X扩展Y 比旧的原型方法更好吗. 旁边,, 许多流行的前端框架都鼓励使用它,你应该避免编写奇怪的非标准代码. 我只是不喜欢事情的发展方向.

在我的噩梦中,整整一代JavaScript库都是使用 class期望它的行为与其他流行语言类似. 全新类型的bug(双关语)被发现. 旧的东西被复活了,如果我们没有不小心陷入 class 陷阱. 有经验的JavaScript开发人员被这些怪物所困扰, 因为流行的东西并不总是好的.

最终,我们都在沮丧中放弃,开始在Rust中重新发明轮子, Go, Haskell, 或者谁知道还有什么, 然后编译成web版本的Wasm, 新的web框架和库激增到多语言的无限.

这确实让我夜不能寐.

了解基本知识

  • ECMAScript 6是什么?

    ES6是ECMAScript的最新稳定实现, JavaScript所基于的开放标准. 它为该语言增加了许多新特性,包括一个官方模块系统, 块作用域的变量和常量, 箭头功能, 以及其他许多新关键词, 语法, 内置对象.

  • ES6是最新的ECMAScript版本吗?

    ES6 (ES2015)是最新的标准,在主流浏览器和其他JS环境的最新版本中是稳定和完全实现的(除了适当的尾部调用和一些细微差别). ES7 (ES2016)和ES8 (ES2017)也都是稳定的规范,但实现却很复杂.

  • ES6是面向对象的吗?

    JavaScript对面向对象编程有强大的支持, 但是与大多数流行的OO语言(使用经典继承)相比,它使用了不同的继承模型(原型)。. 它还支持过程式和函数式编程风格.

  • 什么是ES6类?

    在ES6, “class”关键字和相关特性是创建原型构造函数的一种新方法. 从某种意义上说,它们不是大多数其他面向对象语言的用户所熟悉的真正的类.

  • 哪些关键字可以用来在ES6中实现继承?

    在JavaScript ES6中,可以通过“class”和“extends”关键字实现继承. 另一种方法是通过“构造器”函数习惯加上函数和静态属性赋值到构造器的原型.

  • 什么是JavaScript中的原型继承?

    原型遗传, 原型是对象实例,子实例将未定义的属性委托给它. 与此形成鲜明对比的是, 经典继承中的类是类型定义, 子类在实例化期间从中继承方法和属性.

聘请Toptal这方面的专家.
现在雇佣
贾斯汀·罗伯逊的头像
Justen罗伯逊

位于 波特兰,俄勒冈,美国

成员自 2018年4月23日

作者简介

Justen有十年的全栈JavaScript经验,曾与Taylor Swift和Red Hot Chili Peppers等人合作.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

16

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.

<喷火ter class="_31netn7Q _30nEAbwv _2U548_p3">

Footer

®
世界顶尖人才,点播 ®

Toptal, LLC版权所有