[JS] ECMAScript 6 - Inheritance : compare with c#

这一章,估计是js最操蛋的一部分内容。

现代方法:

 

远古方法: 

  * 《Javascript面向对象编程(一):封装》【可略,已看】

  * 《Javascript面向对象编程(二):构造函数的继承》

  * 《Javascript面向对象编程(三):非构造函数的继承》

 


 

热身一

调用Object.create(..) 会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你指定的对象。

既然是新对象,也就没有必要让o1的属性继承Base里的this.a。

 

热身二

var animal = function() {};
var dog    = function() {};  // 本质是 new Function(),

animal.price = 2000;     // 原型预备役,先赋值
dog.prototype = animal;  // want to share properties in animal
console.log(dog.price)   // undefined, 其实没有“继承”到
/**
* 原型链是依赖于__proto__,而不是prototype!
* 所以,dog.prototype赋值,有屁用!
*/
原理:
dog's __proto__ ----> dog的构造函数的原型,也就是Function的原型
dog.__proto__ == Function.prototype

Function.prototype没有price, 当然dog.price就没有继承到东西


var tidy = new dog(); console.log(tidy.price)  // 2000, 反而“继承”到了

Jeff: tidy.__proto__ == dog.prototype == animal

 

可见,通过_proto__,让tidy与dog有关;但dog与animal之间却没有建立起这层关系。

也就是说:dog不能继承,但dog的实例反而能继承animal的属性。

 

 


对象之间的"继承"的五种方法

 

父类 - 构造函数形式

function Animal(){
  this.species = "动物";
}

 

一、 构造函数绑定

Ref: [JS] Topic - hijack this by "apply" and "call"

  function Cat(name,color){

    Animal.apply(this, arguments);  // 如果是当前对象中没有的属性,就改变this,在"父类"中找

    this.name = name;
    this.color = color;
  }

  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

  

二、 prototype模式

  Cat.prototype = new Animal();    // 同理“热身二”

/**
* Cat.prototype.constructor 指向变化: Cat --> Animal
*/
  Cat.prototype.constructor
= Cat; // 有必要么?但是,第二行又是什么意思呢?   var cat1 = new Cat("大毛","黄色");   alert(cat1.species); // 动物

  

极为重要的一点:prototype改变后,需要再调整回来,为了安全,避免继承链的紊乱。

原型对象的constructor属性,原本就是指向正确的,

现在Cat.prototype都变为了new的新对象,那么也要至少保证Cat.prototype.constructor仍然是指向Cat的。

这个潜规则。

 

三、 直接继承prototype

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。

缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

function Animal(){ }
Animal.prototype.species = "动物";

Cat.prototype = Animal.prototype;

...
...

 

四、 利用空对象作为中介

 由于"直接继承prototype"存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。

var F = function(){};         // F是空对象,所以几乎不占内存
F.prototype = Animal.prototype;   // 这里的技巧: 利用空对象 做了过渡

Cat.prototype = new F();        // 不会影响到Animal的prototype对象

Cat.prototype.constructor = Cat;

封装成一个函数,便于使用。

  function extend(Child, Parent) {
    
var F = function(){};     F.prototype = Parent.prototype;     Child.prototype = new F();     Child.prototype.constructor = Child;     Child.uber = Parent.prototype;   }

为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层")这等于在子对象上打开一条通道,可以直接调用父对象的方法。

这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

 

使用方式:

  extend(Cat,Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

 

五、 拷贝继承

  function Animal(){}
  Animal.prototype.species = "动物";

  function extend2(Child, Parent) {

    var p = Parent.prototype;
    var c = Child.prototype;

    for (var i in p) {
      c[i] = p[i];
    }

    c.uber = p;
  }

  extend2(Cat, Animal);
  var cat1 = new Cat("大毛","黄色");
  alert(cat1.species); // 动物

 

 

 


父类 - 普通对象形式

var Chinese = {
    nation:'中国'
};

两个对象都是普通对象,不是构造函数,无法使用构造函数方法实现"继承"。

可以使用:非构造函数"的继承。

 

一、object()方法

function object(o) {
  function F() {}
  F.prototype = o;  // 把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
  return new F();
}

1. 第一步先在父对象的基础上,生成子对象
var Doctor = object(Chinese);    // 这里没有在外部显式的new,跟构造函数的继承有点用法的区别

2. 然后,再加上子对象本身的属性
Doctor.career = '医生';

3. 子对象已经继承了父对象的属性
alert(Doctor.nation); //中国

 

  

二、浅拷贝 

这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

详见原链接:Javascript面向对象编程(三):非构造函数的继承

  function extendCopy(p) {
    var c = {};
    
for (var i in p) {       c[i] = p[i];     }     c.uber = p;     return c;   }   var Doctor = extendCopy(Chinese);   Doctor.career = '医生';   alert(Doctor.nation); // 中国

 

 

三、深拷贝

目前,jQuery库使用的就是这种继承方法。

  function deepCopy(p, c) {

    var c = c || {};

    for (var i in p) {

      if (typeof p[i] === 'object') {

        c[i] = (p[i].constructor === Array) ? [] : {};

        deepCopy(p[i], c[i]);

      } else {

         c[i] = p[i];

      }
    }

    return c;
  }

使用方式:

var Doctor = deepCopy(Chinese);

// 现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:
Chinese.birthPlaces = ['北京','上海','香港'];
Doctor.birthPlaces.push('厦门');

// 这时,父对象就不会受到影响了。
alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
alert(Chinese.birthPlaces); //北京, 上海, 香港

 

 

 

 

 让我们来瞧瞧,有了Class类之后会如何?


 

1. 子类需要得到this对象

2. 子类必须在constructor方法中调用super方法

3. 只有调用super之后,才可以使用this关键字,否则会报错

4. 父类的静态方法,也会被子类继承

Parent类:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

Child类:

class ColorPoint extends Point {
constructor(x, y, color) {    // 会被默认添加 super(x, y);
// 调用父类的constructor(x, y), 这里的x,y是父类的,所以要super中作为了参数 this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } }

 

 

Object.getPrototypeOf方法

可以用来从子类上获取父类

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

 

 

super 关键字

注意,

super虽然代表了父类A的构造函数,但是返回的是子类B的实例。

super指向父类的原型对象

super内部的this指的是B,因此super()在这里相当于:

A.prototype.constructor.call(this)。

 

  • super不是指向父类!

例一:super.p无法调用到this.p = 2 

例二:只能调用父类的prototype上

例三:this的不变性【子类普通方法中通过super调用父类的方法时,虽然是父类方法内部的this,却其实是指向当前的子类实例】

例四:超级变态,带我娓娓道来!

class A {
  constructor() {
    this.x = 1;
  }
}

class B extends A {
  constructor() {
    super();
    this.x = 2;
    super.x = 3;  // 其实是把上面的x变了,因为在这种情况下,super.x == this.x
    console.log(super.x); // undefined
    console.log(this.x); // 3
  }
}

let b = new B();

附加题:对于super.x的设计已经无语。

  • super指向父类!

例五:用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

------------------------------------- class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); } } Child.myMethod(
1); // static 1 var child = new Child(); child.myMethod(2); // instance 2

 

 

类的 prototype 属性和__proto__属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。 

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

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

 

这算是子类沿着线索找寻父类的一个办法。

class A {
} class B extends A {
} B.__proto__
=== A // true (1)构造函数的继承 B.prototype.__proto__ === A.prototype // true (2)方法的继承 

貌似是讲述本质,实现原理。但哥不是很关心。

class A {
} class B {
}
// B 的实例 继承 A 的实例 Object.setPrototypeOf(B.prototype, A.prototype);  // ----> // B 继承 A 的静态属性 Object.setPrototypeOf(B, A); const b = new B();

解释:

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

 

这两条继承链,可以这样理解:【记住这个就可以了】

 (1) 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);

 (2) 作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

 

 

extends 的继承目标

先复习一下:

 

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

 

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

 

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

 

 

原生构造函数的继承

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

 

ECMAScript 的原生构造函数大致有下面这些。

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

 

自定义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
//     ...

 

  

Mixin 模式的实现

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。

const a = {
  a: 'a'
};
const b
= { b: 'b' };
const c
= {...a, ...b}; // {a: 'a', b: 'b'}

下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

function mix(...mixins) {
  class Mix {}

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝实例属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== "constructor"
      && key !== "prototype"
      && key !== "name"
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

 有点难,没看懂。

 

(完)

posted @ 2018-04-15 15:30  郝壹贰叁  阅读(204)  评论(0编辑  收藏  举报