js对象——02_类的继承历史

142 阅读5分钟

一、简述

在提到js的类的时候,相信大部分人都习惯性地想到了class这个关键字。而我阅读过一些文章发现,class关键字并不是一开始就有的,是ES6才出现的。也就是意味着,以前是没有这个class的,那么就会想,以前是怎么实现类和它相关的继承的这个功能的呢?

二、类的继承发展

2.1 原型链实现

原理:通过直接附加在原型链上的属性或方法,往后可以直接提供给继承者使用

// 父类
function Person() {
  this.name = "yz";
  this.friends = [];
}
Person.prototype.eating = function() {
  console.log(this.name + " eating");
}
// 子类
function Student() {
  // 学号
  this.sno = 111
}

// 原型链实现
var p1 = new Person();
Student.prototype = p1;
Student.prototype.studying = function() {
  console.log(this.name + " studying");
}

var stu = new Student();
console.log(stu.__proto__ === p1) // true
console.log(stu.name);
stu.eating();
console.log(Person.prototype.constructor);

// 原型链实现继承的弊端
// 1.打印对象,继承的属性是看不到的
console.log(stu);

// 2.创建两个stu对象
var stu1 = new Student()
var stu2 = new Student()
// 获取引用,修改引用的值,会相互影响
stu1.friends.push("kobe");
console.log(stu1.friends);
console.log(stu2.friends);

在以上代码实现类的过程中,我们可以通过该系列的第一章——原型链,画出以下图

image.png

在前面实现类的过程中是有弊端的, 加上以下代码后,很容易发现弊端,共用的friends会同时影响两个新生成的对象stu1、stu2

// 原型链实现继承的弊端
// 1.打印对象,继承的属性是看不到的
console.log(stu);

// 2.创建两个stu对象
var stu1 = new Student()
var stu2 = new Student()
// 获取引用,修改引用的值,会相互影响
stu1.friends.push("kobe");
console.log(stu1.friends);
console.log(stu2.friends);
  1. 以上的studying和eating函数都没有传递参数
  2. 借用构造函数也是有弊端的,Person函数被调用两次
  3. stu对象上会多出属性,但是这些属性是没有必要存在的

2.2 原型链+借用构造函数实现

原理:通过函数所具有的call方法重新改变this的指向,并在子类中使用父类执行。即:用父类作为构造函数

// 父类
function Person(name, age, friends) {
  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 = sno;
}
// 直接将父类赋值给子类,这种方案是不正确的 
Student.prototype = new Person();
Student.prototype.studying = function() {
  console.log(this.name + " studying");
}
// 解决第3个弊端
var stu1 = new Student("yz", 18, ["kobe"], 111)
var stu2 = new Student("yz2", 20, ["james"], 130)

// 解决第1个弊端
console.log(stu1);
stu1.eating()

stu1.friends.push("haha");

// 解决第2个弊端
console.log(stu1.friends);
console.log(stu2.friends);

弊端

  1. Person函数至少被调用两次
  2. stu原型对象上会多余属性

2.3 父类原型赋值给子类(不正确的方案)

// 父类
function Person(name, age, friends) {
  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 = sno;
}
// 直接将父类赋值给子类,这种方案是不正确的
Student.prototype = Person.prototype;
// study被加上了person上面了
Student.prototype.studying = function() {
  console.log(this.name + " studying");
}

var stu = new Student("yz", 18, ["kobe"], 123);
console.log(stu);
stu.eating();

弊端:违背了继承的概念,将Student上的属性和方法会直接转移到Person上,不合继承的功能。

2.4 原型式继承

这种继承方式在出现方法Object.setPrototypeOf之后流行,通过一个工厂函数,传入父类并通过一个中介函数并让其的原型指向父类,然后创造并返回生成的新的对象

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

function createObject(o) {
  var newObj = {};
  // 有这个方法的时候就用这个方法实现
  Object.setPrototypeOf(newObj, o);

  return newObj;
}

// 2006年的时候没有setPrototypeOf
function createObject2(o) {
  function Fn() {}
  Fn.prototype = o;
  var newObj = new Fn();
  return newObj;
}

// var info = createObject(obj);
// 最新的提供的api
var info = Object.create(obj);
console.log(info);
console.log(info.__proto__);

2.5 寄生式继承

原理:通过Object.create创建出父类对象并作为子类对象的继承对象,以下写了一个以Student创造对象的工厂函数

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("yz");

2.6 寄生组合式继承

原理:整合寄生和组合的方式,关键在于inheritPrototype函数,指定并实现了父类和子类的继承关系

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.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("yz", 18, ["jam"], 111, 180);
console.log(stu);
stu.studying();
stu.running();
stu.eating();

三、总结全新class的使用

  1. 类声明:使用class关键字可以声明一个类,后面跟随类的名称。
class MyClass {
  // 类的定义
}
  1. 构造函数:类可以包含一个特殊的方法,称为构造函数,它在创建类的实例时被调用。
class MyClass {
  constructor() {
    // 构造函数代码
  }
}
  1. 实例方法:可以在类中定义方法,这些方法可以在类的实例上调用。
class MyClass {
  myMethod() {
    // 方法代码
  }
}
  1. 静态方法:类还可以定义静态方法,这些方法直接在类上调用,而不是在类的实例上调用。
class MyClass {
  static myStaticMethod() {
    // 静态方法代码
  }
}
  1. 继承:类可以通过extends关键字继承另一个类的属性和方法。
class ChildClass extends ParentClass {
  // 子类的定义
}
  1. super关键字:在子类中使用super关键字可以调用父类的构造函数和方法。
class ChildClass extends ParentClass {
  constructor() {
    super(); // 调用父类的构造函数
  }

  myMethod() {
    super.myMethod(); // 调用父类的方法
  }
}
  1. 访问修饰符:类中的属性和方法可以使用访问修饰符来定义其可访问性,包括publicprivateprotected
class MyClass {
  publicProperty = 123; // 公共属性
  #privateProperty = 456; // 私有属性

  publicMethod() {
    // 公共方法
  }

  #privateMethod() {
    // 私有方法
  }
}

js中并没有protected的属性,通过添加前缀_表示该属性属于protected属性

class MyClass { 
    _protectedProperty = 123; // 受保护的属性 
    _protectedMethod() {
    // 受保护的方法 
    } 
}

这些是JavaScript类的主要特性和用法。类提供了一种组织和封装代码的方式,使代码更易于理解和维护,并支持面向对象的编程范式。