ES6里关于类的拓展(二):继承与派生类
继承与派生类
在ES6之前,实现继承与自定义类型是一个不小的工作。严格意义上的继承需要多个步骤实现
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function Square(length) {
Rectangle.call(this, length, length);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
value:Square,
enumerable: true,
writable: true,
configurable: true
}
});
var square = new Square();
console.log(square.getArea()); //
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
Square继承自Rectangle,为了这样做,必须用一个创建自Rectangle.prototype的新对象重写Square.prototype并调用Rectangle.call()方法。JS新手经常对这些步骤感到困惑,即使是经验丰富的开发者也常在这里出错
类的出现让我们可以更轻松地实现继承功能,使用熟悉的extends关键字可以指定类继承的函数。原型会自动调整,通过调用super()方法即可访问基类的构造函数
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
class Square extends Rectangle {
constructor(length) {
// 与 Rectangle.call(this, length, length) 相同
super(length, length);
}
}
var square = new Square();
console.log(square.getArea()); //
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
这一次,square类通过extends关键字继承Rectangle类,在square构造函数中通过super()调用Rectangle构造函数并传入相应参数。请注意,与ES5版本代码不同的是,标识符Rectangle只用于类声明(extends之后)
继承自其他类的类被称作派生类,如果在派生类中指定了构造函数则必须要调用super(),如果不这样做程序就会报错。如果选择不使用构造函数,则当创建新的类实例时会自动调用super()并传入所有参数
class Square extends Rectangle {
// 没有构造器
}
// 等价于:
class Square extends Rectangle {
constructor(...args) {
super(...args);
}
}
示例中的第二个类是所有派生类的等效默认构造函数,所有参数按顺序被传递给基类的构造函数。这里展示的功能不太正确,因为square的构造函数只需要一个参数,所以最好手动定义构造函数
注意事项
使用super()时有以下几个关键点
1、只可在派生类的构造函数中使用super(),如果尝试在非派生类(不是用extends声明的类)或函数中使用则会导致程序抛出错误
2、在构造函数中访问this之前一定要调用super(),它负责初始化this,如果在调用super()之前尝试访问this会导致程序出错
3、如果不想调用super(),则唯一的方法是让类的构造函数返回一个对象
1、类方法遮蔽
派生类中的方法总会覆盖基类中的同名方法。比如给square添加getArea()方法来重新定义这个方法的功能
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重写并屏蔽 Rectangle.prototype.getArea()
getArea() {
return this.length * this.length;
}
}
由于为square定义了getArea()方法,便不能在square的实例中调用Rectangle.prototype.getArea()方法。当然,如果想调用基类中的该方法,则可以调用super.getArea()方法
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
// 重写、屏蔽并调用了 Rectangle.prototype.getArea()
getArea() {
return super.getArea();
}
}
以这种方法使用Super,this值会被自动正确设置,然后就可以进行简单的方法调用了
2、静态成员继承
如果基类有静态成员,那么这些静态成员在派生类中也可用。JS中的继承与其他语言中的继承一样,只是在这里继承还是一个新概念
class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
static create(length, width) {
return new Rectangle(length, width);
}
}
class Square extends Rectangle {
constructor(length) {
// 与 Rectangle.call(this, length, length) 相同
super(length, length);
}
}
var rect = Square.create(3, 4);
console.log(rect instanceof Rectangle); // true
console.log(rect.getArea()); //
console.log(rect instanceof Square); // false
在这段代码中,新的静态方法create()被添加到Rectangle类中,继承后的Square.create()与Rectangle.create()的行为很像
注意:虽然是Square.create(),但是还是return的new Rectangle(length, width);,所以他是Rectangle的实例,这点需要注意。
3、派生自表达式的类
ES6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有[[Construct]属性和原型,那么就可以用extends进行派生
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
class Square extends Rectangle {
constructor(length) {
super(length, length);
}
}
var x = new Square();
console.log(x.getArea()); //
console.log(x instanceof Rectangle); // true
Rectangle是一个ES5风格的构造函数,Square是一个类,由于Rectangle具有[[Construct]]属性和原型,因此Square类可以直接继承它
extends强大的功能使类可以继承自任意类型的表达式,从而创造更多可能性,例如动态地确定类的继承目标
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function() {
return this.length * this.width;
};
function getBase() {
return Rectangle;
}
class Square extends getBase() {
constructor(length) {
super(length, length);
}
}
var x = new Square();
console.log(x.getArea()); //
console.log(x instanceof Rectangle); // true
getBase()函数是类声明的一部分,直接调用后返回Rectangıe(这还是挺有趣的),此示例实现的功能与之前的示例等价。由于可以动态确定使用哪个基类,因而可以创建不同的继承方法
let SerializableMixin = {
serialize() {
return JSON.stringify(this);
}
};
let AreaMixin = {
getArea() {
return this.length * this.width;
}
};
function mixin(...mixins) {
var base = function() {};
Object.assign(base.prototype, ...mixins);
return base;
}
class Square extends mixin(AreaMixin, SerializableMixin) {
constructor(length) {
super();
this.length = length;
this.width = length;
}
}
var x = new Square();
console.log(x.getArea()); //
console.log(x.serialize()); // "{"length":3,"width":3}"
这个示例使用了mixin函数代替传统的继承方法,它可以接受任意数量的mixin对象作为参数。首先创建一个函数base,再将每一个mixin对象的属性值赋值给base的原型,最后minxin函数返回这个base函数,所以Square类就可以基于这个返回的函数用extends进行扩展。由于使用了extends,因此在构造函数中需要调用super()
Square的实例拥有来自AreaMixin对象的getArea()方法和来自SerializableMixin对象的serialize方法,这都是通过原型继承实现的,mixin()函数会用所有mixin对象的自有属性动态填充新函数的原型。如果多个mixin对象具有相同属性,那么只有最后一个被添加的属性被保留
注意:在extends后可以使用任意表达式,但不是所有表达式最终都能生成合法的类。如果使用null或生成器函数会导致错误发生,类在这些情况下没有[[Consturct]]属性,尝试为其创建新的实例会导致程序无法调用[[Construct]]而报错
4、内建对象的继承
自JS数组诞生以来,一直都希望通过继承的方式创建属于自己的特殊数组。在ES5中这几乎是不可能的,用传统的继承方式无法实现这样的功能
// 内置数组的行为
var colors = [];
colors[] = "red";
console.log(colors.length); //
colors.length = ;
console.log(colors[]); // undefined
// 在 ES5 中尝试继承数组
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
var colors = new MyArray();
colors[] = "red";
console.log(colors.length); //
colors.length = ;
console.log(colors[]); // "red"
这段代码最后console.log()的输出结果与预期不符,MyArray实例的length和数值型属性的行为与内建数组中的不一致,这是因为通过传统JS继承形式实现的数组继承没有从Array.apply()或原型赋值中继承相关功能
ES6类语法的一个目标是支持内建对象继承,因而ES6中的类继承模型与ES5稍有不同,主要体现在两个方面:
(1)在ES5的传统继承方式中,先由派生类型(如MyArray)创建this的值,然后调用基类型的构造函数(如Array.apply()方法)。这也意味着,this的值开始指向MyArray的实例,但是随后会被来自Array的其他属性修饰
(2)ES6中的类继承则与之相反,先由基类(Array)创建this的值,然后派生类的构造函数(MyArray)再修改这个值。所以一开始可以通过this访问基类的所有内建功能,然后再正确地接收所有与之相关的功能
class MyArray extends Array {
// 空代码块
}
var colors = new MyArray();
colors[] = "red";
console.log(colors.length); //
colors.length = ;
console.log(colors[]); // undefined
MyArray直接继承自Array,其行为与Array也很相似,操作数值型属性会更新length属性,操作length属性也会更新数值型属性。于是,可以正确地继承Array对象来创建自己的派生数组类型,当然也可以继承其他的内建对象
5、Symbol.species属性
内建对象继承的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。所以,如果有一个继承自Array的派生类MyArray,那么像slice()这样的方法也会返回一个MyArray的实例
class MyArray extends Array {
// 空代码块
}
let items = new MyArray(, , , ),
subitems = items.slice(, );
console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true
正常情况下,继承自Array的slice()方法应该返回Array的实例,但是在这段代码中,slice()方法返回的是MyArray的实例。在浏览器引擎背后是通过Symbol.species属性实现这一行为
Symbol.species是诸多内部Symbol中的一个,它被用于定义返回函数的静态访问器属性。被返回的函数是一个构造函数,每当要在实例的方法中(不是在构造函数中)创建类的实例时必须使用这个构造函数。以下这些内建类型均己定义Symbol.species属性
Array
ArrayBuffer
Map
Promise
RegExp
Set
Typed arrays
列表中的每个类型都有一个默认的symbol.species属性,该属性的返回值为this,这也意味着该属性总会返回构造函数
// 几个内置类型使用 species 的方式类似于此
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
在这个示例中,Symbol.species被用来给MyClass赋值静态访问器属性。这里只有一个getter方法却没有setter方法,这是因为在这里不可以改变类的种类。调用this.constructor[Symbol.species]会返回MyClass,clone()方法通过这个定义可以返回新的实例,从而允许派生类覆盖这个值
class MyClass {
static get [Symbol.species]() {
return this;
}
constructor(value) {
this.value = value;
}
clone() {
return new this.constructor[Symbol.species](this.value);
}
}
class MyDerivedClass1 extends MyClass {
// 空代码块
}
class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}
let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("bar"),
clone2 = instance2.clone();
console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false
在这里,MyDerivedClass1继承MyClass时未改变Symbol.species属性,由于this.constructor[Symbol.species]的返回值是MyDerivedClass1,因此调用clone()返回的是MyDerivedClass1的实例;MyDerivedClass2继承MyClass时重写了Symbol.species让其返回MyClass,调用MyDerivedClass2实例的clone()方法时,返回值是一个MyClass的实例。通过Symbol.species可以定义当派生类的方法返回实例时,应该返回的值的类型
数组通过Symbol.species来指定那些返回数组的方法应当从哪个类中获取。在一个派生自数组的类中,可以决定继承的方法返回何种类型的对象
class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(, , , ),
subitems = items.slice(, );
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false
这段代码重写了MyArray继承自Array的Symbol.species属性,所有返回数组的继承方法现在将使用Array的实例,而不使用MyArray的实例
一般来说,只要想在类方法中调用this.constructor,就应该使用Symbol.species属性,从而让派生类重写返回类型。而且如果正从一个已定义Symbol.species属性的类创建派生类,那么要确保使用那个值而不是使用构造函数
6、在类的构造函数中使用new.target
new.target及它的值根据函数被调用的方式而改变。在类的构造函数中也可以通过new.target来确定类是如何被调用的。简单情况下,new.target等于类的构造函数
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
// new.target 就是 Rectangle
var obj = new Rectangle(, ); // 输出 true
这段代码展示了当调用new Rectangle(3.4)时等价于Rectangle的new.target。类构造函数必须通过new关键字调用,所以总是在类的构造函数中定义new.target属性,但是其值有时会不同
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
class Square extends Rectangle {
constructor(length) {
super(length, length)
}
}
// new.target 就是 Square
var obj = new Square(); // 输出 false
Square调用Rectangle的构造函数,所以当调用发生时new.target等于Square。这一点非常重要,因为每个构造函数都可以根据自身被调用的方式改变自己的行为
// 静态的基类
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("This class cannot be instantiated directly.")
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
var x = new Shape(); // 抛出错误
var y = new Rectangle(, ); // 没有错误
console.log(y instanceof Shape); // true
在这个示例,每当new.target是Shape时构造函数总会抛出错误,这相当于调用new Shape()时总会出错。但是,仍可用Shape作为基类派生其他类,示例中的Rectangle便是这样。super()调用执行了Shape的构造函数,new.target与Rectangle等价,所以构造函数继续执行不会抛出错误
注意:因为类必须通过new关键字才能调用,所以在类的构造函数中,new.target属性永远不会是undefined
ES6里关于类的拓展(二):继承与派生类的更多相关文章
- 不可或缺 Windows Native (21) - C++: 继承, 组合, 派生类的构造函数和析构函数, 基类与派生类的转换, 子对象的实例化, 基类成员的隐藏(派生类成员覆盖基类成员)
[源码下载] 不可或缺 Windows Native (21) - C++: 继承, 组合, 派生类的构造函数和析构函数, 基类与派生类的转换, 子对象的实例化, 基类成员的隐藏(派生类成员覆盖基类成 ...
- C++学习之路—继承与派生(二):派生类的构造函数与析构函数
(根据<C++程序设计>(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明) 由于基类的构造函数和析构函数是不能被继承的,所以 ...
- 实现Square类,让其继承自Rectangle类,并在Square类增添新属性和方法,在2的基础上,在Square类中重写Rectangle类中的初始化和打印方法
实现Square类,让其继承自Rectangle类,并在Square类增添新属性和方法,在2的基础上,在Square类中重写Rectangle类中的初始化和打印方法 #import <Found ...
- C++中的类继承(2)派生类的默认成员函数
在继承关系里面, 在派生类中如果没有显示定义这六个成员 函数, 编译系统则会默认合成这六个默认的成员函数. 构造函数. 调用关系先看一段代码: class Base { public : Base() ...
- [C++]变量存储类别,指针和引用,类与对象,继承与派生的一些摘要
C++中共有四种存储类别标识符:auto/static/register/extern 1.auto 函数或分程序内定义的变量(包括形参)可以定义为auto(自动变量).如果不指定存储类别,则隐式定义 ...
- C++ 继承 - 在派生类中对基类初始化
构造函数与基类的其他成员不同,不能被派生类继承,因此为了初始化基类中的成员变量,需要在派生类中调用基类的构造函数(即显式调用),如果派送类没有调用则默认调用基类的无参构造函数(即隐式调用). 显式调用 ...
- C#A类派生类强转基类IL居然还是可以调用派生类中方法的例子
大家都知道在C#中,如果B类继承自A类,如果一个对象是B类型的但是转换为A类型之后,这个对象是无法在调用属于B类型的方法的,如下例子: 基类A: public class A { } 派生类B: pu ...
- ES6里关于字符串的拓展
一.子串识别 自从 JS 引入了 indexOf() 方法,开发者们就使用它来识别字符串是否存在于其它字符串中.ES6 包含了以下三个方法来满足这类需求: 1.includes():该方法在给定文本存 ...
- day35-1 类的三大特性---继承,以及类的派生
目录 类的继承 继承的特性 类的派生 类的组合 类的继承 继承是为了拿到父类的所有东西 继承的特性 减少代码的冗余 Python中父类和子类的对应关系是多对多 使用__bases__方法获取对象继承的 ...
随机推荐
- 【bzoj1927】[Sdoi2010]星际竞速 有上下界费用流
原文地址:http://www.cnblogs.com/GXZlegend/p/6832464.html 题目描述 10年一度的银河系赛车大赛又要开始了.作为全银河最盛大的活动之一,夺得这个项目的冠军 ...
- [洛谷P1903][国家集训队]数颜色
题目大意:有$n$支画笔,有两个操作 $Q\;l\;r:$询问$[l,r]$中有几种颜色 $R\;p\;Col:$把第$p$支画笔的颜色改成$Col$ 题解:带修莫队,分为$n^{\frac{1}{3 ...
- GYM - 101147 A.The game of Osho
题意: 一共有G个子游戏,一个子游戏有Bi, Ni两个数字.两名玩家开始玩游戏,每名玩家从N中减去B的任意幂次的数,直到不能操作判定为输.问谁最终能赢. 题解: 当Bi为奇数的时候,显然Bi的所有次幂 ...
- DB2设置code page(日文943)
为了便于 DB2 在执行 DB2 命令或语句之后显示错误.警告和指示性消息,必须安装您期望使用的语言的 DB2 消息文件集.因为 DB2 有基于语言分组的不同分发版,您必须验证安装 CD-ROM 上有 ...
- linux中shell变量$#,$@,$0,$1,$2
linux中shell变量$#,$@,$0,$1,$2的含义解释: 变量说明: $$ Shell本身的PID(ProcessID) $! Shell最后运行的后台Process的PID $? 最后运行 ...
- python类中两个列表实例如何相加或相减
如下 import numpy a = [1, 2, 3, 4] b = [5, 6, 7, 8] a_array = numpy.array(a) b_array = numpy.array(b) ...
- .net 过滤 sql防注入类,省地以后每次都要重新弄!
/// <summary> /// 过滤不安全的字符串 /// </summary> /// <param name="Str"&g ...
- GPS经纬度的表示方法及换算
想要认识GPS中的经纬度,就必须先了解GPS,知道经纬度的来源: 1. GPS系统组成 GPS是 Gloabal Positioning System 的简称,意为全球定位系统,主要由地面的控制站.天 ...
- QML与Qt C++ 交互机制探讨与总结(转)
原文转自 https://www.cnblogs.com/aoldman/p/4103510.html 介绍 QML和 C++对象可以通过,signals,slots和 属性修改进行交互.对于一个C+ ...
- python笔记-冒泡排序【转载】
本篇转自博客:上海-悠悠 原文地址:http://www.cnblogs.com/yoyoketang/tag/python/ 前言 面试的时候经常有面试官喜欢问如何进行冒泡排序?这个问题相信能难倒一 ...