JavaScript的ES3, ES5, ES6实现继承方式
前言
该篇博客涉及__proto__
, prototype
属性, 如果不是太了解, 参见下面这篇博客
https://blog.csdn.net/c_kite/article/details/78484191
ES3 继承
在JavaScript中,所谓的类就是函数,函数就是类。一般情况下,我们在函数的prototype上面定义方法,因为这样所有类的实例都可以公用这些方法;在函数内部(构造函数)中初始化属性,这样所有类的实例的属性都是相互隔离的。 我们定义ClassA和ClassB两个类,想让ClassB继承自ClassA。 ClassA代码如下所示:
function ClassA(name, age) {this.name = name;this.age = age;
}ClassA.prototype.sayName = function () {console.log(this.name);
};ClassA.prototype.sayAge = function () {console.log(this.age);
};
ClassA构造函数内部定义了name
和age
两个属性,并且在其原型上定义了sayName
和sayAage
两个方法。 ClassB如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
ClassB新增了job
属性,我们在其构造函数中执行ClassA.apply(this, [name, age]);
,相当于在Java类的构造函数中通过super()
调用父类的构造函数以初始化相关属性。
此时我们可以通过var b = new ClassB(“sunqun”, 28, “developer”);
进行实例化,并可以访问b.name
、b.age
、b.job
三个属性,但此时b还不能访问ClassA中定义的sayName
和sayAage
两个方法。
然后我们新增代码ClassB.prototype = ClassA.prototype;
,此时ClassB的代码如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
//新增
ClassB.prototype = ClassA.prototype;
当执行var b = new ClassB(“sunqun”, 28, “developer”);
时,b.__proto__
指向的是ClassB.prototype
,由于通过新增的代码已经将ClassB.prototype
指向了ClassA.prototype
,所以此时b.__proto__
指向了ClassA.prototype
。这样当执行b.sayName()
时,会执行b.__proto__.sayName()
,即最终执行了ClassA.prototype.sayName()
,这样ClassB的实例就能调用ClassA中方法了。
此时我们想为ClassB新增加一个sayJob
方法用于输出job
属性的值,如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
ClassB.prototype = ClassA.prototype;
//新增
ClassB.prototype.sayJob = function(){console.log(this.job);
};
此时问题出现了,我们为ClassB.prototype
添加sayJob
方法时,其实修改了ClassA.prototype
,这样会导致ClassA所有的实例也都有了sayJob
方法,这显然不是我们期望的。 为了解决这个问题,我们再次修改ClassB的代码,如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
// ClassB.prototype = ClassA.prototype;
//修改
ClassB.prototype = new ClassA();
ClassB.prototype.constructor = ClassB;
ClassB.prototype.sayJob = function(){console.log(this.job);
};
我们通过执行ClassB.prototype = new ClassA();
将ClassA实例化的对象作为ClassB的prototype,这样ClassB仍然能够使用ClassA中定义的方法,但是ClassB.prototype
已经和ClassA.prototype
完全隔离了。
我们的目的达到了,我们可以随意向ClassB.prototype
添加我们想要的方法了。有个细节需要注意,ClassB.prototype = new ClassA();
会导致ClassB.prototype.constructor
指向ClassA的实例化对象,为此我们通过ClassB.prototype.constructor = ClassB;
解决这个问题。
关于此处为什么要设置.constructor可参见这篇博客
https://blog.csdn.net/c_kite/article/details/78484191
一切貌似完美的解决了,但是这种实现还是存在隐患。我们在执行ClassB.prototype = new ClassA();
的时候,给ClassA传递的是空参数,但是ClassA的构造函数默认参数是有值的,可能会在构造函数中对传入的参数进行各种处理,传递空参数很有可能导致报错(当然本示例中的ClassA不会)。于是我们再次修改ClassB的代码如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
//修改
function ClassMiddle() {}
ClassMiddle.prototype = ClassA.prototype;
ClassB.prototype = new ClassMiddle();
ClassB.prototype.constructor = ClassB;
ClassB.prototype.sayJob = function () {console.log(this.job);
};
这次我们引入了一个不需要形参的函数ClassMiddle
作为ClassB和ClassA之间的中间桥梁。
1. ClassMiddle.prototype = ClassA.prototype;
: 将ClassMiddle.prototype
指向ClassA.prototype
,这样ClassMiddle可以访问ClassA中定义的方法。
2. ClassB.prototype = new ClassMiddle();
: 将ClassMiddle的实例化对象赋值给ClassB.prototype,这样就相当于执行了ClassB.prototype.__proto__ = ClassMiddle.prototype;
,所以ClassB就能使用ClassMiddle中定义的方法,又因为ClassMiddle.prototype
指向了ClassA.prototype
,所以ClassB.prototype.__proto__
也指向了ClassA.prototype
,这样ClassB能使用ClassA中定义的方法。
以上思路的精妙之处在于ClassMiddle是无参的,它起到了ClassB和ClassA之间的中间桥梁的作用。 现在我们为ClassA添加一些静态属性和方法,ClassA新增如下代码:
...//为ClassA添加静态属性
ClassA.staticValue = "static value";//为ClassA添加静态方法
ClassA.getStaticValue = function() {return ClassA.staticValue;
};
ClassA.setStaticValue = function(value) {ClassA.staticValue = value;
};
静态属性和方法不属于某一个实例,而是属于类本身。ClassA.prototype上面定义的方法是实例方法,不是静态的。静态属性和方法是直接添加在ClassA上的。 为了使ClassB也能继承ClassA的静态属性和方法,我们需要为ClassB添加如下代码:
...//ClassB继承ClassA的静态属性和方法
for (var p in ClassA) {if (ClassA.hasOwnProperty(p)) {ClassB[p] = ClassA[p];}
}
我们最终可以将上述继承代码的公共部分抽离成一个extendsClass方法,如下所示:
function extendsClass(Child, Father) {//继承父类prototype中定义的实例属性和方法function ClassMiddle() {}ClassMiddle.prototype = Father.prototype;Child.prototype = new ClassMiddle();Child.prototype.constructor = Child;//继承父类的静态属性和方法for (var p in Father) {if (Father.hasOwnProperty(p)) {Child[p] = Father[p];}}
}
我们只需要执行extendsClass(ClassB, ClassA);
就可以完成大部分继承的逻辑。 最终ClassA的完整代码如下所示:
function ClassA(name, age) {this.name = name;this.age = age;
}ClassA.prototype.sayName = function() {console.log(this.name);
};ClassA.prototype.sayAge = function() {console.log(this.age);
};ClassA.staticValue = "static value";ClassA.getStaticValue = function() {return ClassA.staticValue;
};ClassA.setStaticValue = function(value) {ClassA.staticValue = value;
};
ClassB的完整代码如下所示:
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}extendsClass(ClassB, ClassA);ClassB.prototype.sayJob = function() {console.log(this.job);
};
ES5继承
ES5.1规范中新增了Object.create()
方法,该方法会传入一个对象,然后会返回一个对象,返回的对象的原型指向传入的对象,比如执行代码var output = Object.create(input)
,相当于执行代码output.__proto__ = input;
,output的原型是input。我们可以简化之前的代码,不再需要ClassMiddle,只需要执行ClassB.prototype = Object.create(ClassA.prototype);
即可,相当于执行代码ClassB.prototype.__proto__ = ClassA.prototype;
。
而且ES5.1中新增了Object.keys()
方法用以获取对象自身的属性数组,我们可以用该方法简化继承父类静态属性和方法的过程。
根据以上两点,我们修改extendsClass方法如下所示:
function extendsClass(Child, Father) {//继承父类prototype中定义的实例属性和方法Child.prototype = Object.create(Father.prototype);Child.prototype.constructor = Child;//继承父类的静态属性和方法Object.keys(Father).forEach(function(key) {Child[key] = Father[key];});
}
ES6 继承
我们之前提到,ES6规范定义了Object.prototype.proto
属性,该属性既可读又可写,通过__proto__
属性我们可以直接指定对象的原型。于是在ES6中我们将extendsClass修改为如下所示:
function extendsClass(Child, Father) {//继承父类prototype中定义的实例属性和方法Child.prototype.__proto__ = Father.prototype;//暴力直接,利用__proto__属性设置对象的原型//继承父类的静态属性和方法Child.__proto__ = Father;
}
直接修改对象的__proto__
属性值不是最佳选择,ES6规范中还定义了Object.setPrototypeOf()
方法,通过执行Object.setPrototypeOf(b, a)
会将a对象作为b对象的原型,即相当于执行了b.__proto__ = a;
。为此我们利用该方法再次精简我们的extendsClass方法,如下所示:
function extendsClass(Child, Father) {//继承父类prototype中定义的实例属性和方法Object.setPrototypeOf(Child.prototype, Father.prototype);//继承父类的静态属性和方法Object.setPrototypeOf(Child, Father);
}
Object.setPrototypeOf(Child.prototype, Father.prototype);
相当于执行代码Child.prototype.__proto__ = Father.prototype;
,使得Child能够继承Father中的实例属性和方法。Object.setPrototypeOf(Child, Father);
相当于执行代码Child.__proto__ = Father;
,使得Child能够继承Father中的静态属性和方法。
由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]
在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.__proto__ = ...
语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]
已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]
。相反,你应该使用 Object.create()
来创建带有你想要的[[Prototype]]
的新对象。
ES6中引入了class
关键字,可以用class
直接定义类,通过extends
关键字实现类的继承,还可以通过static
关键字定义类的静态方法。
我们用class
等关键字重新实现ClassA和ClassB的代码,如下所示:
class ClassA{constructor(name, age){this.name = name;this.age = age;}sayName(){console.log(this.name);}sayAge(){console.log(this.age);}static getStaticValue(){return ClassA.staticValue;}static setStaticValue(value){ClassA.staticValue = value;}
}ClassA.staticValue = "static value";class ClassB extends ClassA{constructor(name, age, job){super(name, age);this.job = job;}sayJob(){console.log(this.job);}
}
ES6中不能通过static
定义类的静态属性,我们可以直接通过ClassA.staticValue = "static value";
定义类的静态属性。
需要注意的是,class
关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。
并不是所有的浏览器都支持class
关键字,在生产环境中,我们可以编写ES6的代码,然后用Babel或TypeScript将其编译为ES5等主流浏览器支持的语法格式。
ES6 Class和之前几种继承区别
一个很重要的区别在于, ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面, 例如上面的说的例子
function ClassB(name, age, job) {ClassA.apply(this, [name, age]);this.job = job;
}
ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
总结
执行
var x = new X()
;时,浏览器会执行x.__proto__ = X.prototype
,会将实例化对象的原型设置为对应的类的prototype
对象。实现类继承的关键是
Child.prototype.__proto__ = Father.prototype;
,这样会将Father.prototype
作为Child.prototype
的原型。Object.prototype.__proto__
属性是在ES6规范中所引入的,为了在ES3和ES5中需要通过各种方式模拟实现对Object.prototype.__proto__
进行赋值。通过执行
Child.__proto__ = Father;
可以实现继承父类的静态属性和方法。
参考链接:
https://blog.csdn.net/iispring/article/details/62219444
http://es6.ruanyifeng.com/#docs/class-extends
JavaScript的ES3, ES5, ES6实现继承方式相关推荐
- JavaScript中6种常见的继承方式
为什么需要继承? 在实际编码的过程中,如果有很多类似的方法都存放于构造函数中,这样会导致内存的浪费(内存泄漏),在这种情况下我们就需要用到继承. 继承是什么? 所谓继承就是通过某种方式让一个对象可以访 ...
- JavaScript中常见的几种继承方式
继承方式一:混入继承 "use strict";//object:A let Oa= {money: 'many money',resource: 'company,house', ...
- JavaScript面向对象——深入理解默认的继承方式原型链
描述: 正如我们所了解,JavaScript中的每个函数中都有一个指向某一对象的prototype属性.该函数被new操作符调用时会创建并返回一个对象,并且该对象中会有一个指向其原型对象的秘密链接,通 ...
- ES5和ES6的继承有哪些优劣?
突然看到继承,感觉对这个概念有点模糊,掌握的知识点不太全面牢固,所以才有了这篇博客. 在我的印象里,ES5的继承我只知道三种:通过构造函数继承.通过原型链继承.通过构造函数和原型链组合继承 对ES6的 ...
- JavaScript继承理解:ES5继承方式+ES6Class继承对比
JavaScript中实现继承 在JavaScript中实现继承主要实现以下两方面的属性和方法的继承,这两方面相互互补,既有共享的属性和方法,又有特有的属性和方法. 实例属性和方法的继承: 目的: ...
- (语法糖)ES6类class声明类的方式 -ES5类声明继承
本文将从以下几方面介绍类与继承 类的声明与实例化 如何实现继承 继承的几种方式 类的声明与实例化 类的声明一般有两种方式 // es5和es6声明类的区别,es5没有统一语法规范.es6有统一写法规范 ...
- JavaScript es6 五种常见继承方式
前言 参考: 原型和原型链 原型继承和 Class 继承 B站讲解 原型链 实例对象的隐式原型__proto__指向函数对象的显式原型prototype,原型的最终指向是Object的null 当我们 ...
- JavaScript 常见的六种继承方式
方式一.原型链继承 这种方式关键在于:子类型的原型为父类型的一个实例对象. -------------------------------------------------------------- ...
- JavaScript中的几种继承方式及优缺点分析
众所周知,继承是面向对象编程思想中的三大特点(封装,继承,多态)之一. 所谓继承,通俗来讲就是子类自动拥有父类的属性和方法, 继承可以提高代码的复用性. 继承也是前端里面的重要的一个知识点,在实际工作 ...
- JavaScript对象继承方式
一.对象冒充 其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式).因为构造函数只是一个函数,所以可使 Parent 构造函数 成为 Children 的方法, ...
最新文章
- Python实例浅谈之五Python守护进程和脚本单例运行
- SqlDataReader生成动态Lambda表达式
- 师范生计算机硬件技能怎么填,浅析对师范生教育技术能力培养与发展问题的思考的论文...
- 前端跨域问题解决方案
- 【Node】—系统模块
- MySQL数据库规范及解读
- liferay+portlet+开发实例
- Fiddler4安装与配置_偷懒的肥猫
- 基于web的电影院售票系统的设计与实现
- 倒立摆及其应用//2021-2-23
- 图灵之谜-《艾伦·图灵传》序
- 使用 Spring Data JPA 简化 JPA 开发
- 音标对照表—kk,88
- android模拟器的录屏,夜神安卓模拟器如何录制视频
- K-means算法详解及实现
- 移动端下拉加载更多DEMO(纯js实现)
- 【五六七人口普查】我国省市两级各行业门类人口及三次产业人口比重
- Google Play通话记录和短信权限使用限制
- 垃圾收集算法——新生代和老年代(JVM)
- 为什么你的字体和别人的字体不一样(下载的word启用编辑后字体发生变化)