前言

很久以前学习《Javascript语言精粹》时,写过一个关于js的系列学习笔记。

最近又跟别人讲什么原型和继承什么的,发现这些记忆有些模糊了,然后回头看自己这篇文章,觉得几年前的学习笔记真是简陋。

所以在这里将这篇继承重新更新一下,并且加上ES6的部分,以便下次又对这些记忆模糊了,能凭借这篇文章快速回忆起来。

本篇文章关于ES5的继承方面参考了《Javascript语言精粹》和《JS高程》,后面的ES6部分通过使用Babel转换为ES5代码,然后进行分析。

用构造函数声明对象

既然讲到继承,那么有必要先说一下对象是如何通过构造器构造的。

先看以下代码:

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}

当声明了Parent这个构造函数后,Parent会有一个属性prototype,这是Parent的原型对象。

可以相当于有以下这段隐式代码

Parent.prototype={constructor:Parent}

执行构造函数构造对象时,会先根据原型对象创造一个对象A,然后调用构造函数,通过this将属性和方法绑定到这个对象A上,如果构造函数中不返回一个对象,那么就返回这个对象A。

原型链

然后可以看下js最初的继承。

以下为最基本的原型链玩法

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
} function Child(){
this.name='儿子';
}
Child.prototype=new Parent();
var child=new Child(); console.info(child.name);//儿子
console.info(child.type);//人

通过以上发现,我们的child继承了原型对象的type。

借用构造函数:保证父级引用对象属性的独立性

原型链最大的问题在于,当继承了引用类型的属性时,子类构造的对象就会共享父类原型对象的引用属性。

我们先来看之前的例子,child不仅继承了parent的type,也继承了body。

修改一下以上的代码:

var child=new Child();
child.body.weight=30;
var man=new Child();
console.info(man.type);//30

这里可以看到,原型中的引用对象被child和man共享了。

子类构造函数构造child和man两个对象。

当他们读取父级属性时,读取的是同一个变量地址。

如果在子级对象中更改这些属性的值,那么就会在子级对象中重新分配一个地址写入新的值,那么就不存在共享了属性。

但是上面的例子中,是更改引用对象body里的值weight,而不是body。

这样的结果就是body的变量地址不变,导致父级引用对象被子级对象共享,失去了各个子级对象应该有的独立性(这里我只能用独立性来说明,为了避免和后面讲到的私有变量弄混)。

于是就有了借用构造函数的玩法:

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
} function Child(){
Parent.call(this);
this.name='儿子';
}
Child.prototype=new Parent();
var child=new Child(); console.info(child.name);//儿子
console.info(child.type);//人

实际上借用构造函数是在在子类的构造函数中借用父类的构造函数,然后就在子类中把所有父类的属性都声明了一次。

然后在子类构造对象后,获取属性时,因为子对象已经有了这个属性,那么就不会去查找原型链上的父对象的属性了,从而保证了继承时父类中引用对象的独立性。

组合继承:函数的复用

组合嘛,实际上就是利用了原型链来处理一些需要共享的属性或者方法(通常是函数),以达到复用的目的,又借用父级的构造函数,来实现属性的独立性

在上面的代码中加入

Parent.prototype.eat = function(){
// 吃饭
}

这样Child构造的对象就可以继承到eat这个函数

原型继承:道格拉斯的原型式继承

这个方式是道格拉斯提出来的,也就是写《JavaScript语言精粹》的那个人。

实际上就是Object.create。

它的最初代码如下:

Object.create=function(origin){
function F(){};
F.protoType=origin;
return new F();
} var child=Object.create(parent);

这个玩法的思路就是以后我们不要用js的那种伪类写法了,摆脱掉类这个概念,而是用对象去继承对象,也就是原型继承,因为相对于那些基于类的语言,js有自己进行代码重用的方式。

但是这个样子依然会有问题,就是在原型继承中,同一个父对象的不同子对象共享了继承自父对象的引用类型。

导致的结果就是一个子对象的值发生了改变,另外一个也就变了。

也就是我们说的引用属性独立性的问题。

寄生式继承:增强原型继承子对象

道格拉斯又提出了寄生式继承:

function createObject(origin){
var clone=Object.create(origin);
clone.eat=function(){
// 吃饭
}
}

这种继承你可以看做毫无意义,因为你一般会写成下面这样:

var clone=Object.create(origin);
clone.eat=function(){
// 吃饭
}

这个样子写也没毛病。

当然你可以认为这是一次重构,从提炼一个业务函数的角度去理解就没毛病了。比如通过人这个原型创造男人这个对象。

寄生组合式继承:保证原型继承中父级引用对象属性的独立性

组合继承的问题在于,会两次调用父级构造函数,第一次是创造子类型原型的时候,另一次是子类型构造函数内部去复用父类型构造函数。

对于一个大的构造函数而言,可能对性能产生影响。

而原型继承以及衍生出的寄生式继承的毛病就是,引用类型的独立性有问题。

那么堪称完美的寄生组合式继承就来了,但是在之前,我们先回顾下这段组合式继承的代码:

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
Parent.prototype.eat=function(){
//吃
}
Child.prototype=new Parent();
var child=new Child();

那么现在我们加入寄生式继承的修改:

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
}
Parent.prototype.eat=function(){
//吃
} function inheritPrototype(childType,parentType){
var prototype=Object.create(Parent.prototype);
prototype.constructor=childType;
childType.prototype=prototype
} inheritPrototype(Child,Parent)
var child=new Child();

或者我们把inheritPrototype写得更容易懂一点:

function inheritPrototype(childType,parentType){
childType.prototype=Object.create(Parent.prototype);
childType.prototype.constructor=childType;
}

记住这里不能直接写成

childType.prototype=Parent.prototype

表面上看起来可以,但是childType上原型加上函数,那么父级就会加上,这样不合理。

通过inheritPrototype,Child直接以Parent.prototype的原型为原型,而不是new Parent(),那么也就不会继承Parent自己的属性,而又完美继承了Parent原型上的eat方法。

通过借用构造函数又实现了引用属性的独立性。

那么现在我们来看就比较完美了。

只不过这种方式我平常都基本不用的,因为麻烦,更喜欢一个Object.create解决问题,只要注意引用对象属性的继承这个坑点就行。

函数化:解决继承中的私有属性问题

以上所有生成的对象中都是没有私有属性和私有方法的,只要是对象中的属性和方法都是可以访问到的。

这里为了做到私有属性可以通过函数化的方法。

function Parent(){
this.type='人'
this.name='爸爸';
this.body={
weight:50
}
}
function Child(){
Parent.call(this);
this.name='儿子';
} inheritPrototype(Child,Parent) var ObjectFactory=function(parent){
var name='troy';//私有变量
result=new Child();
result.GetMyName=function(){//这是要创建的对象有的特有方法
return 'myname:'+name;
}
return result;
};
var boy=ObjectFactory(anotherObj);

这个地方实际上用的是闭包的方式来处理。

拷贝继承

这里其实还有一种复制属性的玩法,继承是通过复制父对象属性到子对象中,但是这种玩法需要for in遍历,如果要保持引用对象独立性,还要进行递归遍历。

这里就不介绍了。

它有它的优点,简单,避开了引用对象独立性,并且避开了从原型链上寻找对象这个过程,调用属性的时候更快,缺点是这个遍历过程,对于属性多层级深的对象用这种玩法,不是很好。

关于ES5继承的一些说法

ES5一直都是有伪类继承(也就是通过构造函数来实现继承)和对象继承(我认为原型和拷贝都算这种)两种玩法,一种带着基于类的思想的玩法,一种纯粹从对象的角度去考虑这个事情。

如果加上类的概念的话,确实麻烦,如果是直接考虑原型继承而不用考虑类的话就简单很多。

所以对ES5而言可以更多从对象角度去考虑继承,而不是从类的角度。

ES6:进化,class的出现

在ES6中出现了class的玩法,这种类的玩法使得我在使用ES6的时候更愿意去站在类的角度去思考继承,因为基于类去实现继承更加简单了。

新的class玩法,并没有改变Javascript的通过原型链继承的本质,它更像是语法糖,只是让代码写起来更加简单明了,更加像是一个面向对象语言。

class Parent {
constructor(name){
super()
this.name = name
} eat(){
console.log('吃饭');
}
} class Child extends Parent {
constructor(name,gameLevel){
super(name)
this.gameLevel = gameLevel
} game(){
console.log(this.gameLevel);
}
}

我们可以通过Babel将它转化为ES5:

'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Parent = function () {
function Parent(name) {
_classCallCheck(this, Parent); this.name = name;
} _createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃饭');
}
}]); return Parent;
}(); var Child = function (_Parent) {
_inherits(Child, _Parent); function Child(name, gameLevel) {
_classCallCheck(this, Child); var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)); _this.gameLevel = gameLevel;
return _this;
} _createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]); return Child;
}(Parent);

然后在这里我们不考虑兼容性,提炼一下核心代码,并美化代码以便阅读:

'use strict';

var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key,descriptor);
}
}
return function (Constructor, protoProps, staticProps)
{
if (protoProps)
defineProperties(Constructor.prototype, protoProps);
if (staticProps)
defineProperties(Constructor, staticProps);
return Constructor;
};
}(); function _inherits(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.__proto__ = superClass;
} var Parent = function () {
function Parent(name) {
this.name = name;
} _createClass(Parent, [{
key: 'eat',
value: function eat() {
console.log('吃饭');
}
}]); return Parent;
}(); var Child = function (_Parent) {
_inherits(Child, _Parent); function Child(name, gameLevel) {
Child.__proto__.call(this, name);
var _this = this;
_this.gameLevel = gameLevel;
return _this;
} _createClass(Child, [{
key: 'game',
value: function game() {
console.log(this.gameLevel);
}
}]); return Child;
}(Parent);

在这里可以看到转化后的代码很接近我们上面讲述的寄生组合式继承,在_inherits中通过Object.create让子类原型继承父类原型(这里多了一步,Child.proto=Parent),而在Child函数中,通过Child.__proto__借用父级构造函数来构造子类对象。

而_createClass不过是把方法放在Child原型上,并把静态变量放在Child上。

总结

总的来说ES6中,用class就好,但是要理解这个东西不过是ES5的寄生组合式继承玩法的语法糖而已,而不是真的变成那种基于类的语言了。

如果有天还让我写ES5代码,父类中没有引用对象,那么使用Object.create是最方便的,如果有,那么可以根据实际情况考虑拷贝继承和寄生组合式继承。

【JS复习笔记】03 继承(从ES5到ES6)的更多相关文章

  1. JS自学笔记03

    JS自学笔记03 1.函数练习: 如果函数所需参数为数组,在声明和定义时按照普通变量名书写参数列表,在编写函数体内容时体现其为一个数组即可,再传参时可以直接将具体的数组传进去 即 var max=ge ...

  2. 【JS复习笔记】03 继承

    关于继承 好吧,说到底JS还是原型继承的,而不是类继承.所以在这个上面要经常用到prototype去继承另一个对象. 所有的构造器函数都约定命名为首字母大写的形式,并且不以首字母大写的形式拼写任何其它 ...

  3. 【JS复习笔记】07 复习感想

    好吧,其实<JavaScript语言精粹>后面还简单介绍了代码风格,优美特性,以及包含的毒瘤.糟粕. 但我很快就看完了,发现其实都在前面讲过了,所以就不写了. 至今为止已经算是把JavaS ...

  4. 【JS复习笔记】02 对象与函数

    好吧,因为很重要的事情,几天没写笔记了. 关于对象: ||可以用来填充默认值,如:myApp.name || "无" &&可以用来避免错误,myApp.NameOb ...

  5. 【JS复习笔记】00 序

    作为一个前端苦手,说是复习,你就当我是重学好了. 好吧,我当然不可能抱着一个砖头去复习,所以捡了本薄的来读——<JavaScript语言精粹>. 当初带我的人说这本书挺好,就看这本书好了. ...

  6. js类的继承,es5和es6的方法

    存在的差异:1. 私有数据继承差异 es5:执行父级构造函数并且将this指向子级 es6:在构造函数内部执行super方法,系统会自动执行父级,并将this指向子级2. 共有数据(原型链方法)继承的 ...

  7. JavaScript语言精粹 笔记03 继承

    继承伪类对象说明符原型函数化部件 继承 JS不是基于类的,而是基于原型的,这意味着对象直接从其他对象继承. 1 伪类 JS提供了一套丰富的代码重用模式,它可以模拟那些基于类的模式,因为JS实际上没有类 ...

  8. Servlet&JSP复习笔记 03

    1.Servlet的声明周期 容器如何创建Servlet对象,如何为Servlet对象分配资源,如何调用Servlet对象的方法来处理请求,以及如何销毁Servlet对象的过程. a.实例化 容器调用 ...

  9. 【JS复习笔记】05 正则表达式

    好吧,正则表达式,我从来没记过.以前要用的时候都是网上Copy一下的. 这里还是扯一下吧,以后要是有要用到的正则表达式那么就收集到这个帖子里.(尽管我认为不会,因为我根本就不是一个专业的前端,我只是来 ...

随机推荐

  1. Javascript数组系列一之栈与队列

    所谓数组(英语:Array),是有序的元素序列. 若将有限个类型相同的变量的集合命名,那么这个名称为数组名. 组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量. ---百度百科 ...

  2. C# 如何使用 Elasticsearch (ES)

    Elasticsearch简介 Elasticsearch (ES)是一个基于Apache Lucene(TM)的开源搜索引擎,无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进.性能最好 ...

  3. Scala依赖注入

    控制反转(Inversion of Control,简称IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合程度.其中最常见的方式叫做依赖注入(Dependency Inject ...

  4. Oracle数据库忘记用户名密码的解决方案

    1.windows+r输入sqlplus 2.依次输入: sys/manager as sysdba #创建新用户 SQL> create user c##username(自己的用户名) id ...

  5. SQL Server死锁的解决过程

    某现场报一个SQL死锁,于是开启了1222跟踪: dbcc traceon(1222,-1) 一段时间之后拷贝ERROR文件查找相关信息,比较有用的摘录出来如下: 语句一: select study_ ...

  6. CENTOS7错误:Cannot find a valid baseurl for repo: base/7/x86_6

    CENTOS7错误:Cannot find a valid baseurl for repo: base/7/x86_6 解决办法: 1.进入/etc/sysconfig/network-script ...

  7. malloc和calloc用法

    malloc和calloc用法 #include <stdio.h> #include <stdlib.h> int main(){ int n; printf("i ...

  8. oracle 分组函数执行分析

    先上例了: select job as "JOB1", avg(sal) as "avg sal" from scott.emp group by " ...

  9. Docker 从入门到实践(二)Docker 三个基本概念

    一.Docker 的三个进本概念? 了解 Docker 的三个基本概念,就可以大致了解 Docker 的生命周期. 镜像(Image) 容器(Container) 仓库(Repository) 二.镜 ...

  10. 4、爬虫系列之mongodb

    mongodb mongo简介 简介 MongoDB是一个基于分布式文件存储的数据库.由C++语言编写.旨在为WEB应用提供可扩展的高性能数据存储解决方案.MongoDB是一个介于关系数据库和非关系数 ...