【译】JS继承常见误区扫盲(一)

原文链接:Common Misconceptions About Inheritance in JavaScript

作者:Eric Elliott

翻译:野草

本文首发于前端早读课【第911期】

什么鬼!!!当程序员遇到违反“最小惊讶原则”,违反他们直觉的时候,他们会不由自主地发出这句感叹!

举个栗子:

.1 + .2
0.30000000000000004

什么鬼!!!(+﹏+)~@

当我遇到一些资深前端开发,却不知道JavaScript原型继承(Prototypal inheritance)的时候,我也情不自禁地发出“什么鬼!!!”的感叹。原型继承可是计算机科学史上最重大的变革之一,也是JavaScript语言两大支柱之一。这对我来说就像,一位专业摄影师却还没学会曝光三角形(Exposure triangle)原理一样不可思议,这可是控制照片风格的基础啊。简而言之:

如果你不懂原型,那你根本就不懂JavaScript。

++问:类继承和原型继承不是同一回事儿吗,只是风格选择而已?++

++答:不是!++
类继承和原型继承不论从本质上还是从语法上来说,都是两个截然不同的概念。

二者之间有着区分彼此的本质性特征。要完全看懂本文,你必须牢牢记住以下几点:

类继承中,实例继承自模版(类),并且创建子类关系。换言之,你不能像使用实例一样使用类。实例由类创建出来,并且能调用类的方法,但是你不能直接在类上调用本身的方法。你必须创建一个实例,然后在实例上应用那些方法。

原型继承中,实例继承自其他的实例。它们使用的是原型委托(将实例的原型对象指向一个模板对象),这种方式被Kyle Simpson(你不知道的JS系列作者)称为对象关联(OLOO, Objects Linking to Other Objects)。使用这种关联继承,你只是将模板对象的属性拷贝到新的实例中而已。

理解上述区别至关重要。类继承的机制在创建子类的同时,也不小心创建了类的层级。

原型继承却可以避免创建类似的层级。建议原型链越短越好,其实很容易将很多原型扁平化为一个单委托原型。

总结:

  • 类是一个抽象的模版。

  • 原型是一个具体的对象实例。

++问:JavaScript中类不是创建对象的正确方式吗?++

++答:不是!++

JavaScript中创建对象有几种方式。最常见的一种是对象字面量方式。看个例子,用ES6语法写的对象:

// ES6 或称 ES2015, 因为发布于2015.

let mouse = {
  furColor: 'brown',
  legs: 4,
  tail: 'long, skinny',
  describe () {
    return `A mouse with ${this.furColor} fur,
      ${this.legs} legs, and a ${this.tail} tail.`;
  }
};

当然,对象字面量方式比ES6出来早多了,但之前的写法缺少对象中函数方法的简写方式,以及定义变量时你只能用var而用不了let。对了,describe()方法中的模板字符串在ES5中也是不能用的。

我们可以利用ES5中的Object.create()附上对象的委托原型:

let animal = {
  animalType: 'animal',

  describe () {
    return `An ${this.animalType}, with ${this.furColor} fur, 
      ${this.legs} legs, and a ${this.tail} tail.`;
  }
};

let mouse = Object.assign(Object.create(animal), {
  animalType: 'mouse',
  furColor: 'brown',
  legs: 4,
  tail: 'long, skinny'
});

让我们仔细分析这个例子。animal是委托原型,mouse是实例。当你尝试获取mouse对象上没有的属性时,JavaScript将会在animal(委托对象)上寻找这个属性。

Object.assign()是ES6的新特性,由Rick Waldron提出。其实它早已在一些知名的库中被实现,比如jQuery中的$.extend(),Underscore中的_.extend(),还有Lodash中的assign()。该方法传入一个目标对象,以及任何多个用逗号隔开的源对象,它将会从最后一个源对象开始拷贝所有的可枚举属性到目标对象。若存在属性名冲突,前者会被后者覆盖。

Douglas Crockford提出了ES5中的Object.create(),它能使我们在不用构造器和new关键词的情况下,设置对象的委托原型。

本文不涉及到构造函数,因为我非常不推荐这种方式。太多滥用构造函数的例子,以及太多由此引起的麻烦。值得一提的是,很多聪明人并不同意我的看法。没关系,聪明人想怎么做就怎么做。

总有明智的人会听取Douglas Crockford的意见:

如果某个特性有时会不靠谱,而且存在一个更好的选择,那么还是选择那个更好的方式。

++问:难道不需要构造函数来定义实例的行为,以及进行实例化吗?++

++答:不需要!++

任何函数均可创建并返回对象。当该函数不是用作构造函数来创建时,它被称为工厂函数(factory function)。

更佳选择

let animal = {
  animalType: 'animal',

  describe () {
    return `An ${this.animalType} with ${this.furColor} fur, 
      ${this.legs} legs, and a ${this.tail} tail.`;
  }
};

let mouseFactory = function mouseFactory () {
  return Object.assign(Object.create(animal), {
    animalType: 'mouse',
    furColor: 'brown',
    legs: 4,
    tail: 'long, skinny'
  });
};

let mickey = mouseFactory();

通常我不会将函数命名为factory,那只是一个形象的比喻。一般我就简单称之为mouse()

++问:不需要用构造函数来创造私有变量或者属性吗?++

++答:不需要++

JavaScript中,当你返回一个函数,该函数可以访问外部函数的变量。当你使用这个函数的时候,JS引擎创建了一个闭包。闭包是JavaSript中非常常见的模式,它通常用来创建私有变量。

闭包并不是构造函数独有的。任何函数均可创建闭包:

let animal = {
  animalType: 'animal',

  describe () {
    return `An ${this.animalType} with ${this.furColor} fur, 
      ${this.legs} legs, and a ${this.tail} tail.`;
  }
};

let mouseFactory = function mouseFactory () {
  let secret = 'secret agent';

  return Object.assign(Object.create(animal), {
    animalType: 'mouse',
    furColor: 'brown',
    legs: 4,
    tail: 'long, skinny',
    profession () {
      return secret;
    }
  });
};

let james = mouseFactory();

未完待续。