ES6 javascript中Class类继承用法实例详解


Posted in Javascript onOctober 30, 2017

本文实例讲述了ES6 javascript中Class类继承用法。分享给大家供大家参考,具体如下:

1. 基本用法

Class 之间可以通过extends关键字实现继承, 这比 ES5 的通过修改原型链实现继承, 要清晰和方便很多。

class ColorPoint extends Point {}

上面代码定义了一个ColorPoint类, 该类通过extends关键字, 继承了Point类的所有属性和方法。 但是由于没有部署任何代码, 所以这两个类完全一样, 等于复制了一个Point类。 下面, 我们在ColorPoint内部加上代码。

class ColorPoint extends Point {
 constructor(x, y, color) {
  super(x, y); // 调用父类的 constructor(x, y)
  this.color = color;
 }
 toString() {
  return this.color + ' ' + super.toString(); // 调用父类的 toString()
 }
}

上面代码中, constructor方法和toString方法之中, 都出现了super关键字, 它在这里表示父类的构造函数, 用来新建父类的this对象。

子类必须在constructor方法中调用super方法, 否则新建实例时会报错。 这是因为子类没有自己的this对象, 而是继承父类的this对象, 然后对其进行加工。 如果不调用super方法, 子类就得不到this对象。

class Point { /* ... */ }
class ColorPoint extends Point {
 constructor() {}
}
let cp = new ColorPoint(); // ReferenceError

上面代码中, ColorPoint继承了父类Point, 但是它的构造函数没有调用super方法, 导致新建实例时报错。

ES5 的继承, 实质是先创造子类的实例对象this, 然后再将父类的方法添加到this上面( Parent.apply(this))。 ES6 的继承机制完全不同, 实质是先创造父类的实例对象this( 所以必须先调用super方法), 然后再用子类的构造函数修改this。

如果子类没有定义constructor方法, 这个方法会被默认添加, 代码如下。 也就是说, 不管有没有显式定义, 任何一个子类都有constructor方法。

constructor(...args) {
 super(...args);
}

另一个需要注意的地方是, 在子类的构造函数中, 只有调用super之后, 才可以使用this关键字, 否则会报错。 这是因为子类实例的构建, 是基于对父类实例加工, 只有super方法才能返回父类实例。

class Point {
 constructor(x, y) {
  this.x = x;
  this.y = y;
 }
}
class ColorPoint extends Point {
 constructor(x, y, color) {
  this.color = color; // ReferenceError
  super(x, y);
  this.color = color; // 正确
 }
}

上面代码中, 子类的constructor方法没有调用super之前, 就使用this关键字, 结果报错, 而放在super方法之后就是正确的。

下面是生成子类实例的代码。

let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

上面代码中, 实例对象cp同时是ColorPoint和Point两个类的实例, 这与 ES5 的行为完全一致。

2. 类的 prototype 属性和 __proto__ 属性

大多数浏览器的 ES5 实现之中, 每一个对象都有__proto__属性, 指向对应的构造函数的 prototype 属性。 Class 作为构造函数的语法糖, 同时有prototype 属性和__proto__属性, 因此同时存在两条继承链。

( 1) 子类的__proto__属性, 表示构造函数的继承, 总是指向父类。
( 2) 子类prototype属性的__proto__属性, 表示方法的继承, 总是指向父类的prototype属性。

class A {}
class B extends A {}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

上面代码中, 子类B的__proto__属性指向父类A, 子类B的prototype属性的__proto__属性指向父类A的prototype属性。

这样的结果是因为, 类的继承是按照下面的模式实现的。

class A {}
class B {}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

《对象的扩展》 一章给出过Object.setPrototypeOf方法的实现。

Object.setPrototypeOf = function(obj, proto) {
 obj.__proto__ = proto;
 return obj;
}

因此, 就得到了上面的结果。

Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

这两条继承链, 可以这样理解: 作为一个对象, 子类( B) 的原型( __proto__属性) 是父类( A); 作为一个构造函数, 子类( B) 的原型( prototype属性) 是父类的实例。

Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

3. Extends 的继承目标

extends关键字后面可以跟多种类型的值。

class B extends A {}

上面代码的A, 只要是一个有prototype属性的函数, 就能被B继承。 由于函数都有prototype属性( 除了Function.prototype函数), 因此A可以是任意函数。

下面, 讨论三种特殊情况。

第一种特殊情况, 子类继承 Object 类。

class A extends Object {}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true

这种情况下, A其实就是构造函数Object的复制, A的实例就是Object的实例。

第二种特殊情况, 不存在任何继承。

class A {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

这种情况下, A 作为一个基类( 即不存在任何继承), 就是一个普通函数, 所以直接继承Funciton.prototype。 但是, A调用后返回一个空对象( 即Object实例), 所以A.prototype.__proto__指向构造函数( Object) 的prototype属性。

第三种特殊情况, 子类继承null。

class A extends null {}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true

这种情况与第二种情况非常像。 A也是一个普通函数, 所以直接继承Funciton.prototype。 但是, A 调用后返回的对象不继承任何方法, 所以它的__proto__指向Function.prototype, 即实质上执行了下面的代码。

class C extends null {
 constructor() {
  return Object.create(null);
 }
}

4. Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point
// true

因此, 可以使用这个方法判断, 一个类是否继承了另一个类。

5. super 关键字

super这个关键字, 有两种用法, 含义不同。

( 1) 作为函数调用时( 即super(...args)), super代表父类的构造函数。

( 2) 作为对象调用时( 即super.propsuper.method()), super代表父类。 注意, 此时super即可以引用父类实例的属性和方法, 也可以引用父类的静态方法。

class B extends A {
 get m() {
  return this._p * super._p;
 }
 set m() {
  throw new Error(' 该属性只读 ');
 }
}

上面代码中, 子类通过super关键字, 调用父类实例的_p属性。
由于, 对象总是继承其他对象的, 所以可以在任意一个对象中, 使用super关键字。

var obj = {
 toString() {
  return "MyObject: " + super.toString();
 }
};
obj.toString(); // MyObject: [object Object]

6. 实例的 __proto__ 属性

子类实例的 __proto__ 属性的 __proto__ 属性, 指向父类实例的 __proto__ 属性。 也就是说, 子类的原型的原型, 是父类的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

上面代码中, ColorPoint继承了Point, 导致前者原型的原型是后者的原型。

因此, 通过子类实例的__proto__.__proto__属性, 可以修改父类实例的行为。

p2.__proto__.__proto__.printName = function() {
 console.log('Ha');
};
p1.printName() // "Ha"

上面代码在ColorPoint的实例p2上向Point类添加方法, 结果影响到了Point的实例p1。

原生构造函数的继承

原生构造函数是指语言内置的构造函数, 通常用来生成数据结构。 ECMAScript 的原生构造函数大致有下面这些。

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

以前, 这些原生构造函数是无法继承的, 比如, 不能自己定义一个Array的子类。

function MyArray() {
 Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
 constructor: {
  value: MyArray,
  writable: true,
  configurable: true,
  enumerable: true
 }
});

上面代码定义了一个继承 Array 的MyArray类。 但是, 这个类的行为与Array完全不一致。

var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"

之所以会发生这种情况, 是因为子类无法获得原生构造函数的内部属性, 通过Array.apply() 或者分配给原型对象都不行。 原生构造函数会忽略apply方法传入的this, 也就是说, 原生构造函数的this无法绑定, 导致拿不到内部属性。

ES5 是先新建子类的实例对象this, 再将父类的属性添加到子类上, 由于父类的内部属性无法获取, 导致无法继承原生的构造函数。 比如, Array 构造函数有一个内部属性[[DefineOwnProperty]], 用来定义新属性时, 更新length属性, 这个内部属性无法在子类获取, 导致子类的length属性行为不正常。

下面的例子中, 我们想让一个普通对象继承Error对象。

var e = {};
Object.getOwnPropertyNames(Error.call(e))
// [ 'stack' ]
Object.getOwnPropertyNames(e)
// []

上面代码中, 我们想通过Error.call(e) 这种写法, 让普通对象e具有Error对象的实例属性。 但是, Error.call() 完全忽略传入的第一个参数, 而是返回一个新对象, e本身没有任何变化。 这证明了Error.call(e) 这种写法, 无法继承原生构造函数。

ES6 允许继承原生构造函数定义子类, 因为 ES6 是先新建父类的实例对象this, 然后再用子类的构造函数修饰this, 使得父类的所有行为都可以继承。 下面是一个继承Array的例子。

class MyArray extends Array {
 constructor(...args) {
  super(...args);
 }
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined

上面代码定义了一个MyArray类, 继承了Array构造函数, 因此就可以从MyArray生成数组的实例。 这意味着, ES6 可以自定义原生数据结构( 比如Array、 String 等) 的子类, 这是 ES5 无法做到的。

上面这个例子也说明, extends关键字不仅可以用来继承类, 还可以用来继承原生的构造函数。 因此可以在原生数据结构的基础上, 定义自己的数据结构。 下面就是定义了一个带版本功能的数组。

class VersionedArray extends Array {
 constructor() {
  super();
  this.history = [
   []
  ];
 }
 commit() {
  this.history.push(this.slice());
 }
 revert() {
  this.splice(0, this.length, ...this.history[this.history.length - 1]);
 }
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.revert();
x // [1, 2]

上面代码中, VersionedArray结构会通过commit方法, 将自己的当前状态存入history属性, 然后通过revert方法, 可以撤销当前版本, 回到上一个版本。 除此之外, VersionedArray依然是一个数组, 所有原生的数组方法都可以在它上面调用。

下面是一个自定义Error子类的例子。

class ExtendableError extends Error {
 constructor(message) {
  super();
  this.message = message;
  this.stack = (new Error()).stack;
  this.name = this.constructor.name;
 }
}
class MyError extends ExtendableError {
 constructor(m) {
  super(m);
 }
}
var myerror = new MyError('ll');
myerror.message // "ll"
myerror instanceof Error // true
myerror.name // "MyError"
myerror.stack
 // Error
 // at MyError.ExtendableError
 // ...

注意, 继承Object的子类, 有一个行为差异。

class NewObj extends Object {
 constructor() {
  super(...arguments);
 }
}
var o = new NewObj({
 attr: true
});
console.log(o.attr === true); // false

上面代码中, NewObj继承了Object, 但是无法通过super方法向父类Object传参。 这是因为 ES6 改变了Object构造函数的行为, 一旦发现Object方法不是通过new Object() 这种形式调用, ES6 规定Object构造函数会忽略参数。

更多相关内容可查看本站专题:《ECMAScript6(ES6)入门教程》、《JavaScript数组操作技巧总结》、《JavaScript字符与字符串操作技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript错误与调试技巧总结》及《javascript面向对象入门教程》

希望本文所述对大家基于ECMAScript的程序设计有所帮助。

Javascript 相关文章推荐
如何让动态插入的javascript脚本代码跑起来。
Jan 09 Javascript
javascript打印输出json实例
Nov 11 Javascript
JS代码随机生成姓名、手机号、身份证号、银行卡号
Apr 27 Javascript
Zabbix添加Node.js监控的方法
Oct 20 Javascript
jQuery时间日期三级联动(推荐)
Nov 27 Javascript
Angular.Js的自动化测试详解
Dec 09 Javascript
详谈js中window.location.search的用法和作用
Feb 13 Javascript
seajs模块压缩问题与解决方法实例分析
Oct 10 Javascript
js 毫秒转天时分秒的实例
Nov 17 Javascript
NW.js 简介与使用方法
Feb 01 Javascript
vue-cli结合Element-ui基于cropper.js封装vue实现图片裁剪组件功能
Mar 01 Javascript
使用Vue 实现滑动验证码功能
Jun 27 Javascript
轻松理解vue的双向数据绑定问题
Oct 30 #Javascript
jQuery 禁止表单用户名、密码自动填充功能
Oct 30 #jQuery
移动端网页开发调试神器Eruda的介绍与使用技巧
Oct 30 #Javascript
ES6 javascript的异步操作实例详解
Oct 30 #Javascript
React Native 搭建开发环境的方法步骤
Oct 30 #Javascript
Bootstrap框架建立树形菜单(Tree)的实例代码
Oct 30 #Javascript
react实现一个优雅的图片占位模块组件详解
Oct 30 #Javascript
You might like
首页四格,首页五格For6.0(GBK)(UTF-8)[12种组合][9-18][版主安装测试通过]
2007/09/24 PHP
40个迹象表明你还是PHP菜鸟
2008/09/29 PHP
PHP读取RSS(Feed)简单实例
2014/06/12 PHP
php中使用PHPExcel读写excel(xls)文件的方法
2014/09/15 PHP
PHP也能干大事之PHP中的编码解码详解
2015/04/20 PHP
PHP面向对象程序设计之类与反射API详解
2016/12/02 PHP
js 兼容多浏览器的回车和鼠标焦点事件代码(IE6/7/8,firefox,chrome)
2010/04/14 Javascript
DOM和XMLHttpRequest对象的属性和方法整理
2012/01/04 Javascript
ExtJS4利根据登录后不同的角色分配不同的树形菜单
2014/05/02 Javascript
JS动态改变表格边框宽度的方法
2015/03/31 Javascript
详解js界面跳转与值传递
2016/11/22 Javascript
javascript中replace使用方法总结
2017/03/01 Javascript
jQuery选择器之表单元素选择器详解
2017/09/19 jQuery
vue获取当前激活路由的方法
2018/03/17 Javascript
Vue实现todolist删除功能
2018/06/26 Javascript
vue-router 源码之实现一个简单的 vue-router
2018/07/02 Javascript
CentOS7中源码编译安装NodeJS的完整步骤
2018/10/13 NodeJs
使用taro开发微信小程序遇到的坑总结
2019/04/08 Javascript
推荐几个不错的console调试技巧实现
2019/12/20 Javascript
Python中的Numeric包和Numarray包使用教程
2015/04/13 Python
python多线程抽象编程模型详解
2019/03/20 Python
python 实现绘制整齐的表格
2019/11/18 Python
python实现简单日志记录库glog的使用
2019/12/13 Python
pycharm新建Vue项目的方法步骤(图文)
2020/03/04 Python
Keras实现支持masking的Flatten层代码
2020/06/16 Python
css3实现波纹特效、H5实现动态波浪效果
2018/01/31 HTML / CSS
应用艺术毕业生的自我评价
2013/12/04 职场文书
数学检讨书1000字
2014/02/24 职场文书
中国梦我的梦演讲稿
2014/04/23 职场文书
计算机求职信
2014/07/02 职场文书
2015年民主生活会发言材料
2014/12/15 职场文书
新教师个人工作总结
2015/02/06 职场文书
简历中的自我评价应该这样写!
2019/07/12 职场文书
实体类或对象序列化时,忽略为空属性的操作
2021/06/30 Java/Android
mysql 联合索引生效的条件及索引失效的条件
2021/11/20 MySQL
Python循环之while无限迭代
2022/04/30 Python