对JS继承的研究--------------引用
问:类继承和原型继承不是同一回事儿吗,只是风格选择而已?
答:不是!
类继承和原型继承不论从本质上还是从语法上来说,都是两个截然不同的概念。
二者之间有着区分彼此的本质性特征。要完全看懂本文,你必须牢牢记住以下几点:
类继承中,实例继承自模版(类),并且创建子类关系。换言之,你不能像使用实例一样使用类。实例由类创建出来,并且能调用类的方法,但是你不能直接在类上调用本身的方法。你必须创建一个实例,然后在实例上应用那些方法。
原型继承中,实例继承自其他的实例。它们使用的是原型委托(将实例的原型对象指向一个模板对象),这种方式被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 的意见:
如果某个特性有时会不靠谱,而且存在一个更好的选择,那么还是选择那个更好的方式。
问:难道不需要构造函数来定义实例的行为,以及进行实例化吗?
答:不需要!
任何函数均可创建并返回对象。当该函数不是用作构造函数来创建时,它被称为工厂函数(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();
问:使用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就一直使用着构造函数的方式。现在想把所有东西都变成工厂,废弃构造函数就显得有点尴尬了。
但这并不意味着你的代码就要很糟糕。
结论是,工厂函数是个不错的选择。(译者话)
问:难道类继承不比原型继承更常用吗?
答:不是!
问:难道选择类继承还是原型继承不是决定于实际情况吗?
答:不是!
Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), & host of the documentary film-in-production, “Programming Literacy”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.
对JS继承的研究--------------引用的更多相关文章
- js继承实现
JS实现继承可以分为:对象冒充和原型链继承 其中对象冒充又包括:临时变量,call 和 apply 临时变量方法: function Person(name,sex){ this.name = nam ...
- js 继承的简单易懂小例子
js 继承 今天主要说原型链继承.构造继承.组合继承三种常用继承方式,分享一下我的理解. 原型链继承例子1 //原型继承function A(name){ this.name = name;}func ...
- JS继承的一些见解
JS继承的一些见解 js在es6之前的继承是五花八门的.而且要在项目中灵活运用面向对象写法也是有点别扭,更多的时候还是觉得面向过程的写法更为简单,效率也高.久而久之对js的继承每隔一段时间就会理解出现 ...
- JS继承的从入门到理解
开场白 大三下学期结束时候,一个人跑到帝都来参加各厂的面试,免不了的面试过程中经常被问到的问题就是JS中如何实现继承,当时的自己也是背熟了实现继承的各种方法,回过头来想想却不知道__proto__是什 ...
- underscore.js源码研究(2)
概述 很早就想研究underscore源码了,虽然underscore.js这个库有些过时了,但是我还是想学习一下库的架构,函数式编程以及常用方法的编写这些方面的内容,又恰好没什么其它要研究的了,所以 ...
- js 继承的几种方式
JS继承的实现方式: 既然要实现继承,那么首先我们得有一个父类,代码如下: function Animal(name) { // 属性 this.name = name || '小白'; // 实例方 ...
- JS继承方式详解
js继承的概念 js里常用的如下两种继承方式: 原型链继承(对象间的继承) 类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念.所以,要想实现 ...
- JS继承的实现方式
JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一.那么如何在JS中实现继承呢?让我们拭目以待. JS继承的实现方式 既然要实现继承,那么首先我们得有一个父类,代码如下: // 定义一个动物类 ...
- js继承机制的实现
js继承机制的实现 1. 继承的概念 说明继承的最经典的例子:几何形状.实际上,几何形状只有两种,即椭圆形(是圆形的)和多边形(具有一定数量的边).圆是椭圆的一种,它只有一个焦点.三角形.矩形和五边形 ...
随机推荐
- 学习shell的第三天
编程原理:1.编程介绍 早期编程: 驱动 硬件默认是不能使用的: 不同的厂家硬件设备之间需要进行指令沟通,我们需要驱动程序来进行“翻译”: 更趋近与硬件开发的工程师,要学习“汇编语言”:而“汇 ...
- C语言程序作业10
问题 答案 这个作业属于那个课程 C语言程序设计 这个作业要求在哪里 https://www.cnblogs.com/galen123/p/11957321.html 我在这个课程的目标是 在学好C语 ...
- 设置mysql 5.7 可以外部访问的办法
这里记录一下. 一台服务器上面的mysql出现了 无法外面连的情况解决办法: . 先尝试在宿主机上面登录 mysql -uroot -p 输入密码,可以登录. 然后 use mysql # 切换数据库 ...
- SpringMVC基础教程
1. 最简单的配置 首先是要有相应的配置文件: 文件内容: <context:component-scan base-package="com.imethsoft.server.*&q ...
- SSM @Autowired注入失败
1, Intellij IDEA中Mybatis Mapper自动注入警告的6种解决方案 https://blog.csdn.net/weixin_30945319/article/details/9 ...
- ProGuard 最全混淆规则说明
Input/Output Options 输入输出选项 -include filename 递归引入目录的配置文件 -basedirectory directoryname -injars class ...
- mysql-8.0.17-winx64 部署
1.官网下载mysql-8.0.17-winx64,选择Zip文件格式下载 2.解压到目标路径,我这里是E盘根目录,即E:\mysql8 3.根目录下创建my.ini,内容如下: [mysqld]#端 ...
- U盘重装系统
一.准备工作 (1)8G以上空间的U盘一个: (2)将U盘制作好启动工具: 1.下载启动工具制作软件(常用的有:大白菜.电脑店.老毛桃.快启动等等一系列软件,直接百度这些软件的名称,或者百度U盘启动制 ...
- react如何通过shouldComponentUpdate来减少重复渲染
转自:https://segmentfault.com/a/1190000016494335 在react开发中,经常会遇到组件重复渲染的问题,父组件一个state的变化,就会导致以该组件的所有子组件 ...
- 检查linux是否安装java、tomcat、mysql
linux下,查看安装软件 1.linux下的java Java -version 如果出现java版本,证明java安装成功. 2.linux下的tomcat 2.1.检查linux是否安装tomc ...