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

原文链接:Common Misconceptions About Inheritance in JavaScript

作者:Eric Elliott

翻译:野草

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

往期回顾:

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

++问:使用new关键词是否意味着类继承?++

++答:不是!++

new关键词的作用是调用构造函数,具体做了以下几件事:

  • 创建一个新实例
  • this绑定于该实例
  • 将该实例的委托[[Prototype]]指向构造函数的prototype属性所指的对象
  • 以构造函数来命名对象属性,通常在debug阶段你会注意到获得实例对象的属性时,你会得到[Object Foo]而不是[Object object]
  • 允许instanceof判断该实例的原型和构造函数的prototype属性是否指向同一个对象。
    不靠谱的instanceof

是时候重新思考instanceof了,或许你开始质疑它的作用。

注意:instanceof并不是我们所预期的像强类型语言那样进行类型检查。它只是检查对象的原型属性,而且很容易忽悠别人或者被忽悠。比如,它不能在不同执行环境下起作用(比如iframe中),这也是出bug的常见原因之一。

另外,利用instanceof很可能得到错误的结果。因为它仅仅是对目标对象的.prototype属性的身份检查,所以可能会出现以下奇怪的现象:

function foo(){}
var bar = { a: ‘a’};
foo.prototype = bar; // Object {a: “a”}
baz = Object.create(bar); // Object {a: “a”}
baz instanceof foo // true. oops.

最后一行的结果完全符合JavaScript对instanceof的定义。没有什么不对,仅仅是因为instanceof并不能保证结果的正确性罢了。它很容易得到错误的结果。

除此之外,强制代码强类型化,会让函数远离更有用的高复用度的类。

总而言之,instanceof限制了代码的可用性,也给程序带来了潜在的bug。

奇怪的new

什么?!new会返回一些奇怪的东西。如果你尝试返回一个基本数据类型,new做不到。倘若想返回其他任意对象,new可以做到,但这也意味着this被抛弃了,也就切断了所有能链接到this的引用(包括.call().apply()),同时返回东西和构造函数的prototype属性也没有了联系。

++问:类继承和原型继承性能差别大吗?++

++答:差别不大。++

你可能听说过hidden classes,认为用构造函数来创建实例会比Object.create()快很多。其实,有点夸大其词。

项目运行中只有很微少的时间是用来运行脚本的,然后花在获取对象属性上的时间更是微乎其微。事实上,当今最慢的计算机每秒也可访问上百万个属性。

所以,这并不是项目性能优化的瓶颈。你需要做的是仔细分析项目,去发现真正的性能瓶颈。我相信在你思考这些非常微小的优化之前,有数不尽的地方值得你去优化。

不相信?若想该微优化明显提升性能,你必须成千上万次地循环涉及的操作,而且微优化中你唯一需要关心的地方是那些跟数量级相关的代码。

经验之谈:仔细分析你的项目,尽量减少网络加载,文件读写,渲染等可能的瓶颈。然后你才应该开始考虑微优化的问题。

你能区别.0000000001秒和.000000001秒吗?不能吧?我也不能,但我能区别加载10个小图标和加载一个字体的时间长短。

如果你真的分析了你的项目,并且发现瓶颈真的出在创建对象上,最快的解决方式不是用new或者类继承,而是使用对象字面量。如果因为性能你觉得值得放弃原型面向对象,那也值得同时放弃原型链和继承转而直接使用字面量对象。

可谷歌说使用类更快。。。

什么?!我没听错吧!谷歌做的是JavaScript引擎,而你做的是实际项目。显然你们二者关心的不是同一件事情。就让谷歌那小子去处理微优化的摊子。你就担心担心你自己应用真正的瓶颈。我敢说,你担心什么都比担心原型继承带来的性能问题好。

++问:类继承和原型继承内存消耗区别大吗?++

++答:不大!++

两者均可使用委托原型使实例共享方法,同时,它们也可使用或者避免将一堆状态变量封装到闭包里。

实际上,如果你用的是工厂函数,你能更容易操作这些对象,因为你会更加谨慎地处理内存问题,也能避免时不时被垃圾回收器阻碍。想了解更多有关构造函数的尴尬,请看“使用new关键词是否意味着类继承”问答中的最后一段。

简而言之,如果你想更随心地进行内存管理,请使用工厂函数,而非构造器或者类继承。

++问:原始API使用构造函数,难道不是因为它们比工厂模式更常用吗?++

++答:不是!++

JavaScript中工厂模式是极其常用的。比如,一直以来最流行的jQuery库,使用的也是工厂模式。John Resig选择工厂和原型扩展,而不是类。因为他可不想每次开发者进行DOM选择的时候,都要用new来初始化。简直不忍直视!

/**
以类为设计核心的jQuery - 非常糟糕,可能jQuery就从此被埋没!
**/

// 看起来有点蠢,我们是在创造一个id为"foo"的DOM元素吗?错,我们是在选择这个现有的元素。
var $foo = new $('#foo');

// 重复冗余的输入
var $bar = new $('.bar');
var $baz = new $('.baz');

// 看下面这坨是什么鬼? 
var $bif = new $('.foo').on('click', function () {
  var $this = new $(this);
  $this.html('clicked!');
});

jQuery用工厂模式成功地避免了类似代码。

那还有哪些使用工厂模式的例子?

  • React中的React.createClass()是工厂函数;
  • Angular使用了类和工厂;
  • Ember中的Ember.Application.create()
  • Node中核心服务,如http.createServer()net.createServer()
  • Express也是一个工厂。

如上所见,几乎所有最流行的库和框架都使用了工厂函数。而JavaScript唯一比工厂还常见的对象实例化模式是对象字面量。

JavaScript内联函数使用的是构造函数,因为Brendan Eich设计语言的时候想让JavaScript设计得更像Java一点。考虑到自我连贯性,JavaScript就一直使用着构造函数的方式。现在想把所有东西都变成工厂,废弃构造函数就显得有点尴尬了。

但这并不意味着你的代码就要很糟糕。

结论是,工厂函数是个不错的选择。(译者话)