JS的继承到class类的理解

216 阅读9分钟

本文是学习coderwhy老师课程所整理 参考文章阮一峰

继承是什么?

为什么需要继承?

复用

继承的进化

原型链继承

// 父类: 公共属性和方法
function Person() {
  this.name = "why"
  this.friends = []
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student() {
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

图片.png


缺点:

  • 1、打印stu对象, 继承的属性是看不到的
  • 2、创建出来两个stu的对象
    • 直接修改对象上的属性, 是给本对象添加了一个新属性
    • stu1.name = "kobe"
    • 获取引用, 修改引用中的值, 会相互影响
    • stu1.friends.push("kobe")
  • 3、在前面实现类的过程中都没有传递参数
    • var stu3 = new Student("lilei", 112)

借用构造函数继承

解决上面那个方法的第三个缺点:(解决了第三个,12就随着都解决了)因为真是开发的时候,是要接收参数,创建不同的,所以这里不满足,解决办法有如下:


其实是想把2个东西继承下来,

  • 1是父类中对属性赋值的相关代码
  • 2是父类原型上的fun

图片.png

这个属于公共的东西,放在子类里来写,是不合适不对的,

那应该怎么做呢?Person.call(this, name, age, friends)

// 父类: 公共属性和方法
function Person(name, age, friends) {
  // this = stu
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
  Person.call(this, name, age, friends)
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

var stu = new Student("why", 18, ["kobe"], 111)

缺点:

  • Person函数至少被调用了两次
  • stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要

父类原型赋值给子类

// 直接将父类的原型赋值给子类, 作为子类的原型
Student.prototype = Person.prototype

这种方法可以运行吗?可以的,但是如果后期在子类原型上添加一些东西,就都加到父类上了,如果有一堆子类,也都加到父类上了,从道理来说,明明是给子类添加的,现在添加到父类了,也是不对的

图片.png

原型式继承函数(对象)

不用new Father(),

新创建一个对象,用原来某个对象作为新创建对象的原型

var obj = {
  name: "why",
  age: 18
}

var info = Object.create(obj)

// 原型式继承函数
function createObject1(o) {
  var newObj = {}
  Object.setPrototypeOf(newObj, o)
  return newObj
}

function createObject2(o) {
  function Fn() {}
  Fn.prototype = o
  var newObj = new Fn()
  return newObj
}

// var info = createObject2(obj)

var info = Object.create(obj)

console.log(info)
console.log(info.__proto__)

弊端:

  • 扩展的时候

图片.png

寄生式继承(对象)

把原生式继承,放到一个工厂函数里,在工厂函数里对原来的某一个对象做扩展,

var personObj = {
  running: function() {
    console.log("running")
  }
}

function createStudent(name) {
  var stu = Object.create(personObj)
  stu.name = name
  stu.studying = function() {
    console.log("studying~")
  }
  return stu
}

var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

弊端:

  • 方法在每个实例里都有一份,不ok
  • 实例通过函数创建的,不是new出来的,不ok

寄生组合式继承

第一步,继承属性:

function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}
Person.prototype.running = function() {
  console.log("running~")
}

function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  this.sno = sno
  this.score = score
}
Student.prototype.studying = function() {
  console.log("studying~")
}

关键的来咯 Student.prototype = Object.create(Person.prototype);


function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}
Person.prototype.running = function () {
  console.log("running~");
};

function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends);
  this.sno = sno;
  this.score = score;
}

Student.prototype = Object.create(Person.prototype);
Object.defineProperties(Student.prototype, "constructor", {
  enumerable: false,
  configurable: true,
  writable: true,
  value: Student,
});

Student.prototype.studying = function () {
  console.log("studying~");
};

抽一下


function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Objec.create(SuperType.prototype);
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType,
  });
}

function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}
Person.prototype.running = function () {
  console.log("running~");
};
Person.prototype.eating = function () {
  console.log("eating~");
};

function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends);
  this.sno = sno;
  this.score = score;
}

inheritPrototype(Student, Person);

Student.prototype.studying = function () {
  console.log("studying~");
};

var stu = new Student("why", 18, ["kobe"], 111, 100);
console.log(stu);
stu.studying();
stu.running();
stu.eating();

思考

这种方法是创建了一个新对象,让这个新对象的原型指向父类的原型,

那么我的疑惑是:为什么不可以把子类的原型的原型直接指向父类的原型呢?

这样就可以少创建一个空对象

图片.png

图片.png

红的就是那个寄生组合继承,我一开始想的是黄的,但是因为__proto__不推荐,我改成了蓝色的那个

完全行得通,但是没有人这样做,为什么呢?

这样就可以少创建一个空对象啊。

补充:

JS中proto和 prototype 存在的意义是什么?

引用自:www.zhihu.com/question/56…

先有object还是现有function

class

定义


定义类(不存在变量提升,与继承有关,子类必须在父类后面)

// 类的声明
class Person {

}

//类的表达式
var Animal = class {
  
}


图片.png


类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。


构造函数

添加参数

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。

一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。


  • 当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:

  • p1.在内存中创建一个新的对象(空对象);

  • p2.这个对象内部的[[prototype]]属性会被赋值为该类的prototype属性;

  • p3.构造函数内部的this,会指向创建出来的新对象;

  • p4.执行构造函数的内部代码(函数体代码);

  • p5.如果构造函数没有返回非空对象,则返回创建出来的新对象;


实例方法

定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。

静态方法

相当于实例的原型,在类中定义的方法,都会被实例继承。

如果在一个方法前,加上static关键字,就表示该方法不会被实例继承

而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。

父类的静态方法,可以被子类继承。

访问器方法

我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的

静态属性

静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
//——————————————————————
// 老写法
class Foo {
  // ...
}
Foo.prop = 1;

// 新写法
class Foo {
  static prop = 1;
}

实例属性

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

class IncreasingCounter {
  constructor() {
    this._count = 0;
  }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性this._count定义在constructor()方法里面。另一种写法是,这个属性也可以定义在类的最顶层,其他都不变。

class IncreasingCounter {
  _count = 0;
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

上面代码中,实例属性_count与取值函数value()increment()方法,处于同一个层级。这时,不需要在实例属性前面加上this

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

class foo {
  bar = 'hello';
  baz = 'world';

  constructor() {
    // ...
  }
}

私有方法和私有属性

私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

new.target

new是从构造函数生成实例对象的命令。

ES6 为new命令引入了一个new.target属性

该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。

如果构造函数不是通过new命令或Reflect.construct()调用的

new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

上面代码确保构造函数只能通过new命令调用。

为什么呢?

可是为什么要有这么多不同的方法,各自的应用场景又是什么呢?老师没讲,我觉得只记住这几种不难,但是理解了为什么要有这么多不同的方法,可以理解本质

我搜索了大量文章,借鉴了如下,静态方法和实例方法的区别以及如何恰当使用

  • 如果打算创建静态方法将使用的任何静态属性,请考虑创建实例方法。它将允许您使用多个实例。它还允许您控制何时可以释放绑定的内存。
  • 如果您认为将来有机会添加条件逻辑,请使用实例方法。通过利用Redefinition,利用多态来使设计更加灵活
  • 静态仅应用于对象创建的设计模式,例如Singleton,Factory Method,Abstract Factory,Singleton Factory,以方便对象创建。
  • 静态应该用于纯实用程序类,而不用于助手类。

但是我没理解他说了点啥

class继承

super

super这个关键字,既可以当作函数使用,也可以当作对象使用。


super作为函数调用时


代表父类的构造函数。子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

super()在这里相当于A.prototype.constructor.call(this)

super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

class B extends A {
  m() {
    super(); // 报错
  }
}

作为对象


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

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.p()); // 2
  }
}

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

ES6 -> ES5