类、复杂性和函数式编程

66 阅读3分钟

当涉及到旨在持久的应用程序时,我想我们都希望有简单的代码,更容易维护。我们经常有分歧的地方是如何实现这一目标。在这篇博文中,我将谈谈我如何看待函数、对象和类在这个讨论中的作用。

让我们看看一个类实现的例子来说明我的观点:

class Person {
  constructor(name) {
    // common convention is to prefix properties with `_`
    // if they're not supposed to be used. See the appendix
    // if you want to see an alternative
    this._name = name
    this.greeting = 'Hey there!'
  }
  setName(strName) {
    this._name = strName
  }
  getName() {
    return this._getPrefixedName('Name')
  }
  getGreetingCallback() {
    const {greeting, _name} = this
    return subject => `${greeting} ${subject}, I'm ${_name}`
  }
  _getPrefixedName(prefix) {
    return `${prefix}: ${this._name}`
  }
}
const person = new Person('Jane Doe')
person.setName('Sarah Doe')
person.greeting = 'Hello'
person.getName() // Name: Sarah Doe
person.getGreetingCallback()('Jeff') // Hello Jeff, I'm Sarah Doe

所以,我们已经声明了一个Person ,该类有一个构造函数来实例化一些成员属性以及一些方法。有了这些,如果我们在Chrome浏览器的控制台中输入person 对象,它看起来是这样的。

A Person instance with methods on proto

这里需要注意的真正好处是,这个person 的大部分属性都在prototype (在截图中显示为__proto__ )上,而不是在person 的实例上。这并非不重要,因为如果我们有一万个person 的实例,它们都能共享对相同方法的引用,而不是到处都有这些方法的一万份拷贝。

我现在想关注的是,为了真正理解这段代码,你必须学习多少个概念,以及这些概念给你的代码增加了多少复杂性:

  • 对象:很基本。绝对是入门级的东西。它们本身并没有增加多少复杂性。
  • 函数(和闭包)。这也是该语言的基础。闭包确实给你的代码增加了一些复杂性(如果你不小心的话,可能会造成一些问题),但是如果你不学习这些,你真的无法在JavaScript中走得太远。(在这里了解更多)。
  • 一个函数/方法的this 关键字。绝对是JavaScript中的一个重要概念。

我的论断是,this 是很难学的,而且会给你的代码库增加不必要的复杂性。

下面是MDNthis 的说法:

与其他语言相比,一个函数的this 关键字在JavaScript中的表现有些不同。它在严格模式和非严格模式之间也有一些区别。

在大多数情况下,this 的值是由函数的调用方式决定的。它不能在执行过程中通过赋值来设置,而且每次函数被调用时,它可能是不同的。ES5引入了 bind方法来设置一个函数的>的值this>的值,而不管它是如何被调用的,ES2015引入了箭头函数,其this 是词义范围的(它被设置为包围的执行上下文的this 值)。

也许不是火箭科学🚀,但这是一种隐含的关系,而且肯定比仅仅是对象和闭包更复杂。你无法摆脱对象和闭包,但我相信你往往可以在大多数时候避免使用类和this

这里有一个(特意设计的)例子,说明用this ,事情就会变得很糟糕。

const person = new Person('Jane Doe')
const getGreeting = person.getGreeting
// later...
getGreeting() // Uncaught TypeError: Cannot read property 'greeting' of undefined at getGreeting

核心问题是你的函数已经被"编译"到了它被引用的地方,因为它使用了this

对于这个问题的一个更真实的例子,你会发现这在React⚛️中特别明显。如果你使用过React一段时间,你可能和我一样曾经犯过这个错误。

class Counter extends React.Component {
  state = {clicks: 0}
  increment() {
    this.setState({clicks: this.state.clicks + 1})
  }
  render() {
    return (
      
        You have clicked me {this.state.clicks} times
      
    )
  }
}

当你点击按钮时,你会看到。Uncaught TypeError: Cannot read property 'setState' of null at increment

而这都是因为this ,因为我们把它传给了onClick ,而这个 并没有调用我们的increment 函数,而是与我们的组件实例绑定了this 。有各种方法可以解决这个问题(观看这个免费的🆓 egghead.io 视频💻了解如何解决)。

你必须考虑this ,这一事实增加了认知负担,如果能避免就好了。

那么,如果this 增加了这么多的复杂性(正如我所断言的),我们如何避免它而不给我们的代码增加更多的复杂性?我们不采用面向对象的类的方法,而是尝试更多的功能方法,怎么样?如果我们使用纯函数,事情会是这样的。

function setName(person, strName) {
  return Object.assign({}, person, {name: strName})
}

// bonus function!
function setGreeting(person, newGreeting) {
  return Object.assign({}, person, {greeting: newGreeting})
}

function getName(person) {
  return getPrefixedName('Name', person.name)
}

function getPrefixedName(prefix, name) {
  return `${prefix}: ${name}`
}

function getGreetingCallback(person) {
  const {greeting, name} = person
  return subject => `${greeting} ${subject}, I'm ${name}`
}

const person = {greeting: 'Hey there!', name: 'Jane Doe'}
const person2 = setName(person, 'Sarah Doe')
const person3 = setGreeting(person2, 'Hello')
getName(person3) // Name: Sarah Doe
getGreetingCallback(person3)('Jeff') // Hello Jeff, I'm Sarah Doe

有了这个方案,我们就没有了对this 的引用。我们不必考虑这个问题。因此,它更容易理解。只有函数和对象。通过这些函数,基本上没有任何你需要在脑子里保持的状态,这使得它非常好!而人的对象只是数据,所以更容易思考。

The person3 object with just greeting and name

函数式编程的另一个很好的特性是它很容易进行单元测试,这一点我不会深入研究。你只需用一些输入调用一个函数,并对其输出进行断言。你不需要事先设置任何状态。这是一个非常方便的特性!

请注意,函数式编程更多的是让代码更容易理解,只要它 "足够快"。尽管执行速度不是重点,但在某些情况下,你可以获得一些非常好的perf胜利(例如,可靠的=== 对象的平等检查)。更多的时候,你对函数式编程的使用往往会在使你的应用程序变慢的瓶颈列表中排在很后面。

使用class 并不是坏事。它绝对有它的位置。如果你有一些非常"热 "的代码,是你的应用程序的瓶颈,那么使用class 可以真正加快事情。但99%的时候,情况并非如此。而且我不认为classes和this 的附加复杂性在大多数情况下是值得的(我们甚至不要开始考虑原型继承)。我还没有遇到过为了性能而需要classes的情况。所以我在React组件中使用它们,因为如果你需要使用状态/生命周期方法,你就必须这么做(但也许将来不会)。

类(和原型)在JavaScript中有它们的位置。但它们是一种优化。它们不会让你的代码更简单,而是让它更复杂。最好是把你的注意力集中在那些不仅简单易学而且简单易懂的东西上:函数和对象。

See you on twitter!

朋友们再见!

这里有一些额外的东西供你观看 :)

模块模式

另一种避免复杂的this ,利用简单的对象和函数的方法是模块模式。你可以从Addy Osmani的《学习JavaScript设计模式》一书中了解更多关于这个模式的信息,这本书可以在这里免费阅读。下面是基于Addy的 "Revealing Module Pattern"的person 类的实现。

function getPerson(initialName) {
  let name = initialName
  const person = {
    setName(strName) {
      name = strName
    },
    greeting: 'Hey there!',
    getName() {
      return getPrefixedName('Name')
    },
    getGreetingCallback() {
      const {greeting} = person
      return subject => `${greeting} ${subject}, I'm ${name}`
    },
  }
  function getPrefixedName(prefix) {
    return `${prefix}: ${name}`
  }
  return person
}

const person = getPerson('Jane Doe')
person.setName('Sarah Doe')
person.greeting = 'Hello'
person.getName() // Name: Sarah Doe
person.getGreetingCallback()('Jeff') // Hello Jeff, I'm Sarah Doe

我喜欢这一点的原因是,需要理解的概念很少。我们有一个函数,它创建了几个变量并返回一个对象--简单。基本上只有对象和函数。作为参考,如果你在Chrome DevTools中展开,这个人的对象是这样的。

chrome devtools showing the object

只是一个有几个属性的对象。

上述模块模式的一个缺陷是,每个person ,每个属性和函数都有自己的副本。 例如。

const person1 = getPerson('Jane Doe')
const person2 = getPerson('Jane Doe')
person1.getGreetingCallback === person2.getGreetingCallback // false

即使getGreetingCallback 函数的内容是相同的,但它们在内存中都会有自己的该函数的副本。大多数时候,这并不重要,但如果你打算制造一大堆这样的实例,或者你希望创建这些实例的速度更快,这可能是个问题。在我们的Person 类中,我们创建的每个实例都会有一个对完全相同的方法的引用getGreetingCallback

const person1 = new Person('Jane Doe')
const person2 = new Person('Jane Doe')
person1.getGreetingCallback === person2.getGreetingCallback // true
// and to take it a tiny bit further, these are also both true:
person1.getGreetingCallback === Person.prototype.getGreetingCallback
person2.getGreetingCallback === Person.prototype.getGreetingCallback

模块模式的好处是,它避免了我们在上面看到的调用点的问题。

const person = getPerson('Jane Doe')
const getGreeting = person.getGreeting
// later...
getGreeting() // Hello Jane Doe

在这种情况下,我们根本不需要关心this 。还有一些严重依赖闭包的问题需要注意。这都是权衡利弊的问题。

类的私有属性

如果你真的想使用class ,并拥有闭包的私有能力,那么你可能会对这个提议感兴趣(目前处于第二阶段,但不幸的是没有babel支持)。

class Person {
  #name
  greeting = 'hey there'
  #getPrefixedName = prefix => `${prefix}: ${this.#name}`
  constructor(name) {
    this.#name = name
  }
  setName(strName) {
    #name = strName
    // look at this! shorthand for:
    // this.#name = strName
  }
  getName() {
    return #getPrefixedName('Name')
  }
  getGreetingCallback() {
    const {greeting} = this
    return subject => `${this.greeting} ${subject}, I'm ${#name}`
  }
}
const person = new Person('Jane Doe')
person.setName('Sarah Doe')
person.greeting = 'Hello'
person.getName() // Name: Sarah Doe
person.getGreetingCallback()('John') // Hello John, I'm Sarah Doe
person.#name // undefined or error or something... Either way it's totally inaccessible!
person.#getPrefixedName // same as above. Woo! 🎊 🎉

所以我们已经通过该提案解决了隐私问题。然而,它并没有让我们摆脱this 的复杂性,所以我可能只在我真正需要class 的性能提升的地方使用它。

我还应该注意到,你也可以使用WeakMap来获得类的隐私,就像我在es6-workshopWeakMap练习中演示的那样。