原文链接:Common Misconceptions About Inheritance in JavaScript
作者:Eric Elliott
翻译:野草
本文首发于前端早读课【第911期】
往期回顾:
++问:使用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就一直使用着构造函数的方式。现在想把所有东西都变成工厂,废弃构造函数就显得有点尴尬了。
但这并不意味着你的代码就要很糟糕。
结论是,工厂函数是个不错的选择。(译者话)