一篇文章图文并茂地带你轻松学完 JavaScript 继承

JavaScript 继承

在阅读本文章之前,已经默认你了解了基础的 JavaScript 语法知识,基础的 ES6 语法知识 。

继承种类

简单的继承种类可以分为

  1. 构造函数继承
  2. 原型链继承
  3. class继承
  4. 寄生继承

其中 class 继承是 ES6 后提供的一种语法糖,方便其他面向对象语言的程序员更好的接受 JavaScript 中的继承,本质上还是原型链继承。

1. 构造函数继承

function Person() {
    this.name = "name";
    this.eat = function() {
        console.log("eat");
    }
}

function Student() {
    Person.call(this); // 继承
    this.age = 20;
}

const student = new Student();
console.log(student);

2. 原型链继承

原型与原型链前置相关内容可以参考 点这里

function Person() {
    this.name = "name";
}

function Student() {
    this.age = 20;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;	// 指利用 Student 构造函数 进行实例初始化

const stu = new Student();
console.log(stu.name); // "name"
console.log(stu);

利用在原型和原型链所学知识

Student 实例对象的 __proto__ 将会指向 Person 实例,从而实现继承的效果

stu:

3. class继承

constructor 是构造函数,可以结合原型链中的 constructor 属性看

class People {
  constructor() {
    this.name = "name";
  }
}

class Student extends People {
  constructor() {
    super()
    this.age = 20;
  }
}

console.log(new Student())

可以发现,其实就是基于原型链继承,只不过 constructor 是 class Student

4. 寄生继承

在 JavaScript 设计模式中,有 工厂模式 ,具体可以上网查询

工厂模式 意味着只要传入适当的参数 (加工),就会给予一个实例,就像工厂制造东西一样。

而寄生继承,用的就是工厂模式的思想

function People() {}

People.prototype.eat = function() {
  console.log("eat");
}

function createInstance() {
  const obj = Object.create(People.prototype)
  Object.assign(obj, ...arguments);
  return obj;
}

const stu1 = createInstance({ age: 20 });
console.log(stu1);
const stu2 = createInstance({ age: 30 });
console.log(stu2);

下面是 stu1 的打印结果

继承优化

1. 构造函数继承

利用 Student 构造出来的实例,属性和方法是不共享的

function People(name) {
  this.name = name;
  this.eat = function () {
    console.log("eat");
  };
}

function Student(name) {
  People.call(this, name);
  this.age = 20;
}

const stu1 = new Student("huro");
const stu2 = new Student("lero");
console.log(stu1.name === stu2.name);	// false
console.log(stu1.eat === stu2.eat);		// false

对于方法来说我们希望是共享的,否则实际上浪费了很多内存。

2. 组合继承

基于原型的方法是实例共享的,我们将方法放入原型,而属性放在构造函数内,这样就叫做组合继承,组合继承可以解决浪费多余内存的问题。

function People(name) {
  this.name = name;
}

People.prototype.sayName = function() {
  console.log(this.name);
}

function Student() {
  People.call(this);
  this.age = "20";
}

Student.prototype = new People();
Student.prototype.constructor = Student;

const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.sayName === stu2.sayName);

然而,还是有个缺点,我们打印 stu1

在 __proto__ 中 有个 name 属性,这个属性其实我们是不需要的,我们希望每个实例能够独享属性,这个 name 属性的存在不但加大了内存开销,还导致当 delete stu1.name 的时候,出现还能使用 stu1.name 的情况,这是我们不想要的

3. 组合寄生继承

顾名思义,组合寄生继承就是结合组合继承和寄生继承

function People(name) {
  this.name = name;
}

People.prototype.sayName = function() {
  console.log(this.name);
}

function Student() {
  People.call(this);
  this.age = "20";
}

Student.prototype = Object.create(People.prototype); // 实际上只变化这一行
Student.prototype.constructor = Student;

const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.sayName === stu2.sayName);

通过这种方式创造的继承,弥补了组合继承的不足,节省了内存,并且使得实例共享方法独享属性。

那么 ES6 语法提供的 class 是否也有这种 "聪明" 的设计呢?如果有的话,我们直接利用 class 就可以了

  1. class 继承
class People {
  constructor() {
    this.name = "name";
  }
  eat() {
    console.log("eat");
  }
}

class Student extends People {
  constructor() {
    super()
    this.age = 20;
  }
}

const stu1 = new Student();
const stu2 = new Student();
console.log(stu1.eat === stu2.eat); // true

extends 继承的是原型链的方法

super 继承的是独享的属性和方法

可以发现其实是和组合寄生继承类似的

哦哦,那肯定啊,不然 ES6 不被喷死啊。

继承优势 (选择)

用 ES6 的 class 语法有什么优势呢?

  1. 最大的优势是在于可以继承原生构造函数

原生构造函数

  1. Boolean
  2. Number
  3. String
  4. Array
  5. Date
  6. Function
  7. RegExp
  8. Error
  9. Object

在 ES5 语法中,你无法原生构造函数的属性,你可能会尝试这样写

const MyArray() {
    Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.constructor = MyArray;

当用这种方式继承的时候,你会发现他与 Array 这个类的行为完全不一致

const names = new MyArray();
names[0] = "huro";
console.log(names.length); // 0

原生构造函数无法绑定 this

而 class继承 可以

class MyArray extends Array {}

const names = new MyArray();
names[0] = "huro";
console.log(names.length); // 1
  1. 是否一定具有 __proto__

在原型和原型链章节中,我们说到实例的 __proto__ 指向构造函数的 prototype

实际上并不是所有浏览器都是支持 __proto__ 的,而 class 作为构造函数的语法糖,一定有这两个属性。

  1. 更严格的控制
function People(name) {
  this.name = name;
  this.eat = function () {
    console.log("eat");
  };
}

function Student(name) {
  People.call(this, name);
  this.age = 20;
}

const stu1 = Student("huro"); // new?
console.log(stu1);

利用构造函数实例化对象的时候,如果忘传了 new 会怎么样,这个时候显然也成立,因为会被当做一个函数看待,由于是全局调用,因此 this 在浏览器环境下就是 window

这样相当于给 window 赋值了 name 和 eat

这个时候解释器也不会报错,因为没有任何方法可以区分一个函数是否是构造函数,因此可能出现意想不到的错误。

而用 class 方式继承,好处就是如果不写 new 直接报错。

class MyArray extends Array {}

const names = MyArray(); // class constructor MyArray cannot be invoked without "new"

除此之外,在继承的构造函数里,如果没写 super 关键字或 super 不在构造函数顶部也会报错

class MyArray extends Array {
  constructor(){
	// Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  }
}

总结

更严格的语法检查,更多的优化,使得 class继承 应该是目前来看最为优质的继承方式。 为了能看懂他人的代码,以及更好的兼容性,其他的继承方式也要有所了解。

posted @ 2021-02-04 14:04  Huro~  阅读(103)  评论(0)    收藏  举报