从原型继承到类继承详解

构造函数的产生

  • 对象是一个容器,封装了属性(property)和方法(method)

  • 生成对象,需要一个模板.

  • JavaScript 语言使用构造函数(constructor)作为对象的模板。

  • 所谓”构造函数”,就是专门用来生成实例对象的函数。

new 命令的原理

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码。
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) {
  // 将 arguments 对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的 prototype 属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回 context 对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);

⚠️如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

this的产生

var obj = { foo:  5 };
{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}
  • JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

  • 原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。

var obj = { foo: function () {} };
{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}
  • 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。
var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2
  • 由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了。
注意点1
var obj ={
  foo: function () {
    console.log(this);
  }
};
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

⚠️上面代码中,obj.foo就是一个值。JavaScript 引擎内部,objobj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。

注意点2
var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined

⚠️如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

Function.prototype.bind()

⚠️bind()方法每运行一次,就返回一个新函数。

//错误的写法
element.addEventListener('click', o.m.bind(o));
// ...
element.removeEventListener('click', o.m.bind(o));
// 正确的写法
var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);
//bind()可以结合call()方法使用

[1, 2, 3].slice(0, 1) // [1]
// 等同于 :本质是在[1, 2, 3]上面调用Array.prototype.slice()方法
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]

//等同于: call()实质上是调用Function.prototype.call()方法
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]

原型链

  • JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……

  • 修改原型对象时,一般要同时修改constructor属性的指向。

构造函数的继承

// 继承

// 父类
function Super() {
    this.x = 1;
    this.y = 2;
    this.z = 100;
    this.xxx = function () {
        console.log('父类实例享方法');
    }
    this.arr = [1,2,3];
}
Super.prototype.x = 8;
Super.prototype.fff = function () {
    console.log('父类共享方法');
};
Super.hhh = function () {
    console.log('父类静态方法');
};
// 子类
function Sub() {
    Super.call(this); // 子类只具有父类实例的属性和方法  ,这样导致 Sub实例为Sub { x: 3, y: 4, z: 100, xxx: [Function] }
    this.x = 3;
    this.y = 4;
}

Sub.prototype = Object.create(Super.prototype);  // 子类只具有父类原型的属性和方法
// Sub.prototype = new Super(); // 子类会具有父类原型的属性和方法 也会具有父类实例的属性和方法,但是 Sub实例依旧为Sub { x: 3, y: 4 }

// Sub.prototype.__proto__ = Super.prototype; 等价于 Sub.prototype = Object.create(Super.prototype);  和  Sub.prototype.constructor = Sub;

Sub.prototype.constructor = Sub;
Sub.prototype.sss = function () {
    console.log('子类共享方法');
}
// 静态属性方法继承
Sub.__proto__ = Super;

// Object.create(Super.prototype) 返回一个实例,实例的原型是Super.prototype,也就是说实例继承自Super.prototype。
//Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。



const sub1 = new Sub();
console.log(sub1); //Sub { x: 3, y: 4 }

sub1.fff();
console.log(sub1.x); // 3

console.log(sub1.z); // undefined   |  100
// sub1.xxx(); //sub1.xxx is not a function |  父类实例享方法

const sub2 = new Sub();
sub2.arr.push(4);
console.log(sub1.arr); // [ 1, 2, 3 ]  |  [ 1, 2, 3, 4 ]

Sub.hhh();

console.log(Sub.prototype.constructor);


/**
 * 问题1:继承父类是要部分继承(只原型)还是整体继承? 觉得Super.call(this);和 Sub.prototype = Object.create(Super.prototype); 搭配的最好
 * 问题2:先创造子类实例的this,再将父类的属性方法添加到this上(Super.call(this))
 * 问题3:模拟了静态属性和方法的继承
 */

  • 类的数据类型就是函数

  • 类的所有方法都定义在类的prototype属性上

取值函数(getter)和存值函数(setter)

⚠️目的:拦截该属性的存取行为

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

类继承

  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

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

  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

// 父类
class Super {
    constructor(){
        this.x = 1;
        this.y = 2;
        this.z = 100;
        this.xxx = function () {
            console.log('父类实例方法');
        };
        this.arr = [1,2,3];
    }

    fff() {
        console.log(this); //  原型方法中的this 为 子类实例
        console.log('父类共享方法');
    };

    static hhh() {
        console.log(this); // 静态方法中的this 为 子类
        console.log('父类静态方法');
    };
}


/**
    Sub.prototype = {
        constructor: Sub
        toString() {},
    };
 */
// 子类
class Sub extends Super{
    // 类似ES5 的构造函数Sub
    // 类似ES5 constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
    constructor(x, y) {
      super(); // 继承 父类的 实例属性和方法 ; 即 Super.prototype.constructor.call(this)。
      // 实例属性
      this.x = x;
      this.y = y;
    }

    // 实例属性 新写法
    // 那么constructor方法还有什么用?1.调用super() 2.传入参数x,y ;新写法则无法接受
    bar = 'hello';

    // ES5 原型上的方法
    sss() {
        console.log(this); // 原型方法中的this 为 实例
        super.fff(); // 在子类普通方法中通过`super`调用父类的方法时,方法内部的`this`指向当前的子类实例。
        console.log('子类共享方法');
    }

    // 静态方法
    static staticFun() {
        console.log(this === Sub); // 静态方法中的this 为 类
        super.hhh(); //
        return 'hello';
    }

    // 静态属性
    static myStaticProp = 42;

    // 私有属性
    #count = 0;

    // 私有方法
    #sum() {
        return this.#count;
    }
}

const sub1 = new Sub(3,4);
console.log(sub1); //{x: 3, y: 4, z: 100, #sum: ƒ, #count: 0,arr: (3) [1, 2, 3],bar: "hello"}

console.log(sub1.z);
sub1.xxx();

sub1.fff();
sub1.sss();

console.log(Sub.staticFun());
console.log(Sub.myStaticProp);
Sub.hhh(); // 父类的静态方法,可以被子类继承。

const sub2 = new Sub(33,44);
sub2.arr.push(55);
console.log(sub1.arr); // [1,2,3]

// 综上, 子类 继承了 父类的实例属性和方法 ,继承了 父类的共享属性和方法 ,还继承了 静态属性和方法
// 先将父类实例对象的属性和方法,加到`this`上面(所以必须先调用`super`方法),然后再用子类的构造函数修改`this`

判断一个类是否继承了另一个类

Object.getPrototypeOf(ColorPoint) === Point

super 关键字

  • super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)

  • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

  • 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。 即ES5的Sub.prototype = Object.create(Super.prototype);

  • 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。实际上执行的是super.print.call(this)

  • 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

  • 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

子类的__proto__ 和子类的prototype

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

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

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 继承 A 的静态属性 (比ES5多的,也是(1)的解释)
Object.setPrototypeOf(B, A);

const b = new B();

⚠️这句话有意思:

子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

【子类(`B`)的原型对象(`prototype`属性)】是【父类的原型对象(`prototype`属性)的实例】。
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B
// 等同于
子类(`B`)的原型对象(`prototype`属性)是 【父类的原型对象(`prototype`属性)】的实例。
B.prototype.__proto__ = A.prototype;

实例的__proto__

b.__proto__= B.prototype
B.prototype.__proto__ = A.prototype;

拓展

  1. Object.setPrototypeOf(A,B)方法为参数对象A设置原型B,返回该参数对象A
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
  1. 判断一个类是否继承了另外一个类
Object.getPrototypeOf(ColorPoint) === Point
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值