HuangHongRui / Notebook

:pencil2: Yeah.. This's My NoteBook...:closed_book:
0 stars 0 forks source link

Es6_Javascript中的类 [继承与派生类] #27

Open HuangHongRui opened 7 years ago

HuangHongRui commented 7 years ago

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(3)
console.log( square.getArea()) // 9
console.log( square instanceof Square ) //true
console.log( square instanceof Rectangle ) //true
HuangHongRui commented 7 years ago

Square继承自Rectangle.. 这样做,必须用一个创建自Rectangle.prototype的新对象重写Square.prototype 并调用 Rectangle.call() 方法

[这些步骤即使是经验丰富的大牛也常在此出错]

类的出现..使得更轻松地实现继承功能.. 使用熟悉的 extends 关键字可以指定类继承的函数.. 原型会自动调整 通过调用 super() 方法即可访问基类的构造函数..

HuangHongRui commented 7 years ago
class Rectangle {
    constructor( length, width ) {
        this.length = length
        this.width = width
    }

    getArea() {
        return this.length * this.width
    }
}
class Square extends Rectangle {
    constructor (length) {
        super(length, length)
    }
}
let square = new Square(3)
console.log(square.getArea())
console.log(square instanceof Square)
console.log(square instanceof Rectangle)
HuangHongRui commented 7 years ago

Square类通过extends 关键字继承 Rectangle 类.. 在 Square 构造函数中通过 super() 调用 Rectangle 构造函数并传入相应参数..

需要注意的是, 跟Es5不同..标识符Rectangle只用于类声明(extends之后).

继承自其他类的类 被称作 派生类 如果在派生类中指定了构造函数则必须调用 super ..[ 如不这样做,程序即报错]

HuangHongRui commented 7 years ago

如果选择不使用构造函数.则当创建新的类实例时会自动调用 super() 并传入所有参数.

class Square extends Rectangle {
    // 无构造函数
}

// 等价于

class Square extends Rectangle {
    constructor ( ...args ) {
        super ( ...args );
    }
}

第二个类是所有派生类的等效默认构造函数, 所有参数按照顺序被传递给基类的构造函数.. 这里展示的功能不太正确,因为 Square 的构造函数只需要一个参数, 所以最好手动定义构造函数..

HuangHongRui commented 7 years ago

当使用 super 时切记的几个关键

  1. 只可在派生类的构造函数中使用super()..如果尝试在非派生类(不是用extends声明的类) 或函数中使用则会导致抛错.
  2. 在构造函数中访问this之前一定要调用super(), 它负责初始化this, 如果在调用super()之前尝试访问this 会导致抛错
  3. 如果不想调用super() ,唯一的方法是让类的构造函数返回一个对象..
HuangHongRui commented 7 years ago

类方法屏蔽

派生类中的方法总会覆盖基类中的同名方法.. 例子:

class Square extends Rectangle {
    constructor ( length ) {
        super ( length, length );
    }
//覆盖并遮蔽 Rectangle.prototype.getArea()方法..
    getArea() {
        return this.length * this.length
    }
}

现在..因为Square定义了getArea()方法.. 所以不能在Square的实例中调用Rectangle.prototype.getArea()方法..

HuangHongRui commented 7 years ago

如果想调用基类中的此方法..可调用 super.getArea()方法.

class Square extends Rectangle {
    constructor (length) {
        super(length, length);
    }
    //覆盖遮蔽后再调用 Rectangle.prototype.getArea()
    getArea() {
        return super.getArea()
    }
}

这种方法使用Super.. this值会被自动正确设置, 然后就可以进行简单的方法调用了.

HuangHongRui commented 7 years ago

静态成员在派生类中也可以用。

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) {
        super( length, length)
    }
}
var rect = Square.create(3, 4)
console.log(rect.getArea())  //  12
console.log(rect instanceof Rectangle)  //  true
console.log(rect instanceof Square)  //  false

静态方法被添加到Rectangle类中

HuangHongRui commented 7 years ago

派生类表达式的类

Es6 最强大的一面或许是从表达式导出类的功能了 只要表达式可以被解析为一个函数并且具有[[constructor]]属性和原型,那就可用extend进行派生。

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(3)
console.log(x.getArea())
console.log(x instanceof Rectangle)

Rectangle 是一个Es5风格构造函数,Square是一个类 由于Rectangle具有[[constructor]]属性和原型, 因此Square类可以直接继承它。

HuangHongRui commented 7 years ago

extends 强大的功能使得类可以继承自任意类型的表达式。[创造更多可能性]

HuangHongRui commented 7 years ago

动态地确定类的继承目标。

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 r = new Square(3)
console.log(r)
console.log(r.getArea())
console.log(r instanceof Rectangle)
console.log(r instanceof Square)

getBase()函数是类声明的一部分。 直接调用后返回 Rectangle 此例子跟前面Code例子等价。

HuangHongRui commented 7 years ago

因为可动态确定使用哪个基类, 所以可创建不同的继承方法。

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)  // 将每个传入的对象属性赋值给指定的base函数
  return base
}
// Square类再基于这个返回的base函数,用extends进行扩展
class Square extends mixin(AreaMixin, SerializableMixin) {  
  constructor(length) {
    super()  // 因使用了extends,所以在构造函数中需要调用super()
    this.length = length
    this.width = length
  }
}
var r = new Square(3)
console.log(r.getArea())
console.log(r.serialize())

使用了mixin函数代替传统的继承方法, 可接受任意数量的 mixin 对象作为参数。

Square的实例拥有来自2个对象的方法。(都是通过原型继承实现的) mixin()函数会用所有mixin对象的自有属性动态填充新函数的原型。(如多个mixin对象具有同样属性,那保留最后一个被添加的属性)

extends 后可以使用任意表达式, 但不是所有表达式最终都能生成合法的类。 如果使用 null 或生成器函数 会导致错误,类在这些情况下没有[[constructor]]属性,尝试为其创建新的实例会因程序无法调用[[constructor]]而抛错。

HuangHongRui commented 7 years ago

【内建对象的继承】

自 Js 数组诞生以来,开发者一直都希望通过继承的方式创建属于直接的特殊数组。 在Es5或前版本中几乎不可能,用传统的继承方式无法实现这样的功能: 如下例子

var colors = []
colors[0] = 'red'
console.log(colors.length) // 1

colors.length = 0 
console.log(colors[0])  // undefined

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[0] = 'blue'
console.log(colors.length) // 0

colors.length = 0
console.log(colors[0])  //  'blue'

MyArray 实例的length和数值型属性的行为与内建数组中的不一致,这是因为通过传统Js继承形式实现的数组继承没有从Array.apply()或原型赋值中继承相关功能

HuangHongRui commented 7 years ago

Es6 类语法的一个目标就是支持内建对象继承, 因而Es6中的类继承模型与Es5和之前版本略有不同(两方面)。

  1. Es5传统继承方式中,先有派生类型创建this的值(如MyArray),然后调用基类型的构造函数(如Array.apply()方法),这也意味,this的值开始指向的是MyArray的实例,但随后会被来自Array的其他属性所修饰。
  2. Es6中的类继承则与之相反,先由基类(Array)创建this的值, 再派生类的构造函数(MyArray)修改这个值。所以一开始可通过this访问基类的所有內建功能,然后再正确地接收所有与之相关的功能。

例子: 一个基于类生成特殊数组的实践:

class MyArray extends Array {}
var colors = new MyArray()
colors[0] = 'Yellow'
console.log(colors.length) // 1
colors.length = 0
console.log(colors[0]) //undefined

MyArray直接继承自Array。 操作数值型属性会更新length属性,操作length属性也会更新数值型属性。 于是可以正确地继承Array对象来创建直接的派生数组类型,也可继承其他的内建对象。

HuangHongRui commented 7 years ago

【Symbol.species 属性】

内建对象继承的一个实用之处: 原本在內建对象中返回实例自身的方法 将自动返回派生类的实例 所以如有一个继承自Array的派生类MyArray,那么像slice()这样的方法也会返回一个MyArray的实例:

class MyArray extends Array {}

let items  = new MyArray(1,2,3,4),
    subitems = items.slice(1,3);

console.log( items instanceof MyArray )  //  true
console.log( subitems instanceof MyArray )  //  true

继承自Array的slice()方法应该返回Array的实例 但是这段Code中,slice()返回的是MyArray的实例 在浏览器引擎背后是通过Symbol.speclies属性实现了这一行为。

HuangHongRui commented 7 years ago

Symbol.species是诸多内部Symbol中的一个 它被用于定义返回函数的静态访问器属性。

被返回的函数是一个构造函数,每当要在实例的方法中(不是构造函数中)创建类的实例时必须使用这个构造函数。

Array / ArrayBuffer / Map / Promise / RegExp / Set / Typed arrays 这些內建类型均已定义该属性 该属性的返回值为this,意味着该属性总会返回构造函数。

如在自定义的类中实现此功能,Code如下:

class MyClass {
  static get [Symbol.species]() {
    return this
  }
  construtor(value) {
    this.value = value
  }
  clone() {
    return new this.constructor[Symbol.species](this.value)
  }
}

此例, Symbol.species 被用来给MyClass赋值静态访问器属性 这里只有一个getter方法却没有setter方法,因为这里不可改变类的种类。 调用`this.constructor[Symbol.species]会返回MyClass clone() 方法通过这个定义可以返回新的实例,从而允许派生类覆盖这个值

Next Code:


class MyClass {
static get [Symbol.species]() {
return this
}
construtor(val) {
this.value = val
}
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('Raine'),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2('Rui'),
clone2 = instance2.clone();

console.log(clone1 instanceof MyClass) // true console.log(clone1 instanceof MyDerivedClass1) // true console.log(clone2 instanceof MyClass) // ture console.log(clone2 instanceof MyDerivedClass2) // false


MyDerivedClass1 继承MyClass时未改变Symbol.species属性,
由于this.constructor[Symbol.species] 的返回值是 MyDerivedClass1,
因此调用clone()返回的是MyDerivedClass1的实例。

MyDerivedClass2 继承Myclass前重写了Symbol.species,让其返回 MyClass
在调用clone()时,返回是一个MyClass的实例。
**通过Symbol.species可以定义当派生类的方法返回实例时,应该返回的值的类型。**
HuangHongRui commented 7 years ago

数组通过 Symbol.species 来指定那些返回数组的方法应当从哪个类中获取。 在一个派生自数组的类中, 可以决定继承的方法返回何种类型的对象

Code:


class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}
let items = new MyArray(1,2,3,4),
subitems = items.slice(1,3);

console.log(items instanceof Array) // true console.log(items instanceof MyArray) // true console.log(subitems instanceof Array) // true console.log(subitems instanceof MyArray) // false


`MyArry` 继承自 Array的 `Symbol.species` 属性,
所有返回数组的继承方法现在将使用Array的实例。

**要想在类方法中调用 `this.constructor`,就该使用 `Symbol.species` 属性, 让派生类重写返回类型。**
如果正从一个已定义 `Symbol.species` 属性的类创建派生类,
要确保使用这个值而不是使用构造函数