js 在 es6 之前的继承是五花八门的. 而且要在项目中灵活运用面向对象写法也是有点别扭, 更多的时候还是觉得面向过程的写法更为简单, 效率也高. 久而久之对 js 的继承每隔一段时间就会理解出现困难. 所以这次我要把对对象的理解写下来, 这样应该就深刻一点了.
我们先来看看一个对象是怎么生成的
- // 三种创建对象的方法
- var obj = {}
- var obj2 = new Object()
- var obj3 = Object.create(null)
- // 创建一个空字符串对象
- var obj4 = new String()
- obj.constructor === obj2.constructor // true
- obj.__proto__ === obj2.__proto__ === Object.prototype // true
- obj4.__proto__ === String.prototype // true
obj4.__proto__.__proto__ === Object.prototype // ture
这三种方法, 前面两种是一样的. 它们创建的空对象都具有原型, 而第三种方式创建的是一个真正意义上的空对象, 它不继承任何属性; 而 obj4 呢是一个字符串对象. 看下图的对比:
下面对象除了 obj3 都有个__proto__的隐性属性, 这个属性指向的是该创建该实例的构造函数的原型.
这里 obj 和 obj2 虽然是空对象, 不过它们具有 Object 构造函数的属性的方法, 而 obj4 除了有 obj 拥有的属性和方法, 它还具备了 String 拥有的属性和方法; 而 obj3 是真正的空对象, 它的__proto__指向的是 null.
我们再将 obj4 全部展开看看:
再来看看它们之间的关系:
如上图, 我举得例子是 Object 和 String 这两个原生具有的构造器. 它们的关系和我们平时写的父类和子类之间的关系是一样的, 所有的类最终都会指向 Object.
在 es5 的时代, 我们用到的继承最简单的就是原型链继承了
- // 我们先创建一个父类
- function Super (name) {
- this.name = name
- this.type = '我是父类'
- }
- Super.prototype.move = function () {
- console.log(this.name + '在跑')
- }
- // 子类继承父类
- function Child (name) {
- this.name = name
- }
- Child.prototype = new Super()
- // 原型链继承, 子类原型的私有方法和属性要在继承之后添加, 不然会被覆盖
- Child.prototype.constructor = Child // 修复对构造函数的指向
- Child.prototype.eat = function () {
- // 做点啥
- }
原型链继承就是将子类的原型等于父类的实例, 然后再添加子类原型的属性和方法. 这样的缺点就是只能继承一个父类. 而且在创建子类的实例的时候, 不能传参给父类. 在做单一继承而且父类不需要传参的时候, 这种写法是最好的选择了.
下面在介绍一种我个人觉得已经很不错的继承方式(组合继承)
- // 子类继承父类
- function Child (name) {
- Super.call(this)
- this.name = name
- }
- Child.prototype = new Super()
- Child.prototype.constructor = Child // 修复对构造函数的指向
- Child.prototype.eat = function () {
- // 做点啥
- }
这种继承方式就是粗暴的将父类 Super 构造函数利用对象冒充的方式将父类的实例属性复制到子类里. 在用原型继承的方式继承父类原型的属性和方法.
这样的继承方式, 你会发现子类的实例的属性和__proto__下都有父类的属性, 而子类实例的把子类原型的覆盖了.
这样的方式可以实现对多个父类 (非原型部分) 的继承. 因为原型的继承是链式的, 所以只能继承一个.
一般来说这种继承方式已经很好了, 代码量少. 继承性好, 弊端也没有影响. 这就是 ES6 之前的继承. 下面再来看看 ES6 是怎么操作的.
ES6 的 class
ES6 提供了更接近传统语言的写法, 引入了 Class(类)这个概念, 作为对象的模板. 通过 class 关键字, 可以定义类.
基本上, ES6 的 class 可以看作只是一个语法糖, 它的绝大部分功能, ES5 都可以做到, 新的 class 写法只是让对象原型的写法更加清晰, 更像面向对象编程的语法而已. 上面的代码用 ES6 的 class 改写, 就是下面这样.
上面这两句话是引用阮一峰对 ES6 的 class 的简介(Class 的基本语法 http://es6.ruanyifeng.com/#docs/class ). 阮大神的 ECMAScript 6 入门真的是对 es6 初学者居家必备的好东西. 我在这里就谈谈我的理解好了.
- // 写一个 class
- class Point {
- constructor(x, y) {
- this.x = x;
- this.y = y;
- }
- toString() {
- return '(' + this.x + ',' + this.y + ')';
- }
- }
- var point = new Point(2, 3) // new 一个实例
下面看下 point 内部是怎样的, 可以看出 constructor,toString 都是在 "proto" 内的.
在这里会等价于构造函数的什么写法呢
- // 写一个 class
- function Point (x, y) {
- this.x = x
- this.y = y
- }
- Point.prototype.toString = function () {
- return '(' + this.x + ',' + this.y + ')';
- }
- var point = new Point(2, 3) // new 一个实例
在不考虑 class 静态属性的情况下, 可以简单理解为 constructor 内 this 下的新属性和方法都属于构造函数内的, 而 constructor 外的方法都是 prototype 下的.
在看看 class 的继承
- class ChildPoint extends Point {
- constructor (x, y, z) {
- super(x, y)
- this.z = z
- }
- toString () {
- return '(' + this.x + ',' + this.y + ','+ this.z +')';
- }
- changeX (x) {
- this.x = x
- }
- }
- var childPoint = new ChildPoint(2, 3, 4) // new 一个 ChildPoint 的实例
- console.log(childPoint.x) // 2
- console.log(childPoint.y) // 3
- console.log(childPoint.z) // 4
- console.log(childPoint.toString()) // 2,3,4
- childPoint.changeX(5)
- console.log(childPoint.x) // 5
- console.log(childPoint.__proto__ === ChildPoint.prototype) // true
- console.log(childPoint.__proto__.__proto__ === Point.prototype) // true
- console.log(childPoint.constructor === ChildPoint) // true
这里 class 是通过 extends 继承父类的, 而子类的 constructor 方法内需要调用 super()方法, 这相当与调用了父类的 constructor()方法.
下面看下 childPoint 的内部情况:
从图片和上面打印的情况可以看出, es6 的继承和组合继承很是相似. 唯一的不同点就是 es6 的继承没有组合继承产生的多余的一份实例属性在原型里. 看起来很顺眼. 不过相对的 es6 的不能多继承(虽然组合继承只能多继承多个构造函数, 并不能继承多个原型).
其实组合继承还可以进一步升级成 es6 继承那个样子的(寄生组合继承):
- // 我们先创建一个父类
- function Super (name) {
- this.name = name
- this.type = '我是父类'
- }
- Super.prototype.move = function () {
- console.log(this.name + '在跑')
- }
- // 子类继承父类
- function Child (name) {
- Super.call(this)
- this.name = name
- }
- (function() {
- // 创建一个没有 Super 实例的中间类
- var MiddleSuper = function() {}
- MiddleSuper.prototype = Super.prototyoe // 跟 Super 共享原型
- // 在这里 MiddleSuper 和 Super 的区别就是有没有实例了.
- // 然后在正常进行组合继承的操作
- Child.prototype = new MiddleSuper()
- })()
- Child.prototype.constructor = Child // 修复对构造函数的指向
- Child.prototype.eat = function () {
- // 做点啥
- }
经过这一系列复杂的操作后我们就得到了一份和 es6 一样的结构了. 不过 es6 只需要一个 extends, 而 es6 之前我们就得长篇大论的写才能实现, 而且还特别绕. 所以一句话, 快去学 es6 吧.
该文纯属个人见解, 有什么问题请指出.
本文参考了:
幻天芒的 JS 继承的实现方式
阮一峰的 ECMAScript 6 入门 http://es6.ruanyifeng.com/#docs/class
来源: https://www.cnblogs.com/suyuanli/p/8866918.html