一文搞懂,JavaScript继承

知识是零碎的,整理归纳更方便学习,工作及面试。

本篇涉及到的所有理论知识均来自于《JavaScript高级程序设计》,仅为每种继承方式配备示例代码和应用场景分析。

前言

很多面向对象语言都支持两种继承:接口继承实现继承

前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。

原型链

定义和关系

基本思路:通过原型继承多个引用类型的属性和方法。

首先要清楚构造函数、原型和实例的关系:

  1. 每个构造函数都有一个原型对象
  2. 原型有一个属性指回构造函数
  3. 实例有一个内部指针指向原型

示例

原型链继承,代码示例:

javascript 复制代码
function Animal() {
  this.isAnimal = true;
}
Animal.prototype.getAnimal = function () {
  return this.isAnimal;
};

function Dog() {
  this.canFly = false;
}
Dog.prototype = new Animal(); // Animal实例赋值给Dog原型
Dog.prototype.getDog = function () {
  return this.canFly;
};

let dog = new Dog();
console.log(dog.getAnimal());  // true

上面代码定义了两个类型:AnimalDog

这两个类型分别定义了一个属性和一个方法,Animal定义一个属性isAnimal是否为动物,一个方法getAnimal返回是否为动物的结果。Dog定义了一个属性canFly能不能飞,明显狗不能飞,getDog方法返回不能飞的结果。

这两个类型的主要区别是,Dog通过创建 Animal的实例并将其赋给自己的原型 Dog.prototype,实现了对Aniaml继承 。这个赋值重写了Dog最初的原型,将其替换为Animal的实例。这表明Animal实例可以访问的所有属性和方法,也会存在于Dog的原型上,因此Dog的实例可以调用getAnimal方法,并返回了属性isAnimal

默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括 toString()valueOf()在内的所有默认方法的原因。

子类有时候需要覆盖父类的方法,或者增加父类没有的方法,这些方法必须在原型赋值之后再添加到原型上。

javascript 复制代码
function Animal() {
  this.isAnimal = true;
}
Animal.prototype.getAnimal = function () {
  return this.isAnimal;
};

function Dog() {
  this.isDog = true;
}
Dog.prototype = new Animal();
Dog.prototype.getDog = function () {
  return this.isDog;
};
// 覆盖原有方法
Dog.prototype.getAnimal = function () {
  return "这是动物";
};

let dog = new Dog();
console.log(dog.getAnimal());

let animal = new Animal();
console.log(animal.getAnimal());

Dog原型上覆写了getAnimal方法,在Dog实例化调用该方法时,调用的是覆写之后的方法。Animal的实例仍然是调用原有的方法,因为方法的覆写是在把原型赋值为Animal的实例之后定义的。

完整原型链图解关系:

问题

主要问题是出现在原型中包含引用值的时候。原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会 在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。

例如:

javascript 复制代码
function Colors() {
    this.colorList = ['red', 'green', 'yellow']
}
function Rainbow() {}
Rainbow.prototype = new Colors()

let myRainbow = new Rainbow()
myRainbow.colorList.push('indigo')
let yourRainbow = new Rainbow()
yourRainbow.colorList.push('black')

上面代码中,构造函数Colors定义了一个colorList属性,是一个引用类型的数组。每次Colors实例后都会把自己的color加到数组中。但是Rainbow是通过原型继承ColorsRainbow.prototype变成了Colors的一个实例,因此也获得了colorList属性,那Rainbow的每个实例都会共享colorList这个属性。

经典继承

又称为盗用构造函数,为了解决原型包含引用值导致的继承问题。

基本思路:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()call()方法以新创建的对象为上下文执行构造函数。

示例

csharp 复制代码
function Colors() {
  this.colorList = ["red", "green", "yellow"];
}
function Rainbow() {
  Colors.call(this);
}
let myRainbow = new Rainbow();
myRainbow.colorList.push("indigo");
let yourRainbow = new Rainbow();
yourRainbow.colorList.push("black");

通过使用 call()apply()方法,Colors构造函数在为 Rainbow 的实例创建的新对象的上下文中执行了。这相当于新的 Rainbow 对象上运行了Colors函数中的所有初始化代码。结果就是每个实例都会有自己的 colorList属性。

相较于原型链继承,经典继承的优点还有一个就是支持在子类构造函数中向父类构造函数传参。

javascript 复制代码
function Animal(lang) {
  this.saying = function () {
    console.log(lang);
  };
}
function Dog() {
  Animal.call(this, "汪汪汪");
}
const dog = new Dog();
const cat = new Animal('喵喵喵')

问题

必须在构造函数中定义方法,因此函数不能重用;子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

例如:

组合继承

又称为伪经典继承。综合了原型链和盗用构造函数(经典继承),将两者的优点集中了起来。

基本思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

示例

ini 复制代码
function Animal(lang) {
  this.lang = lang;
  this.colors = ["white", "black"];
}
Animal.prototype.saying = function () {
  console.log(this.lang);
};

function Dog(lang, age) {
  Animal.call(this, lang);
  this.age = age;
}
Dog.prototype = new Animal();
Dog.prototype.getAge = function () {
  console.log(this.age);
};

const dog1 = new Dog("汪汪汪", 3);
dog1.colors.push("yellow");

const dog2 = new Dog("吠吠吠", 5);
dog2.colors.push("gray");

上面代码,Animal构造函数定义了一个lang属性,在它的原型上定义了个saying方法。Dog构造函数调用了Animal构造函数,传入lang属性,定义了自己的属性ageDog.prototype被赋值为Animal的实例,又在原型上上添加了新方法getAge

基于Dog创建两个实例dog1dog2,两个实例都有自己的属性,共享相同的方法。

问题

存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。

原型式继承

适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

Object.create()方法将原型式继承的概念规范化。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。

示例

Object.create()方法只有一个参数时。

ini 复制代码
let animal = {
    name: 'dog',
    behavior: ['eat', 'run', 'jump']
}
let anotherAnimal = Object.create(animal)
anotherAnimal.name = 'cat'
anotherAnimal.behavior.push('climb')
let yetAnotherAnimal = Object.create(animal)
yetAnotherAnimal.name = 'bird'
yetAnotherAnimal.behavior.push('fly')

animal对象定义了另一个对象也应该共享的信息,新对象的原型是animal,意味着它的原型上既有原始值属性又有引用值属性。这也意味着animal.behavior不仅是 animal 的属性,也会跟 anotherAnimalyetAnotherAnimal 共享。这里实际上克隆了两个 animal

Object.create()的第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。

css 复制代码
let animal = {
    name: 'dog',
    behavior: ['eat', 'run', 'jump']
}
let anotherAnimal = Object.create(animal, {
    name: {
        value: 'cat'
    },
    behavior: {
        value: ['eat', 'fly']
    }
})

寄生式继承

基本思想:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。

示例

javascript 复制代码
function createAnother(original){ 
 let clone = object(original); // 通过调用函数创建一个新对象
 clone.sayHi = function() { // 以某种方式增强这个对象
 console.log("hi"); 
 }; 
 return clone; // 返回这个对象
}

let animal = { 
 name: "dog", 
 friends: ["eat", "run", "jump"] 
}; 
let anotherAnimal = createAnother(animal); 
anotherAnimal.sayHi(); // "hi"

上面代码基于animal对象返回了一个新对象。新返回的anotherAnimal对象具有 animal 的所有属性和方法,还有一个新方法叫 sayHi()

object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。

寄生式组合继承

旨在解决组合式继承的问题:多次调用构造函数。

寄生式组合继承通过盗用构造函数 继承属性,但使用混合式原型链继承方法。

基本思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

基本模式:

ini 复制代码
function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建对象
  prototype.constructor = subType; // 增强对象 
  subType.prototype = prototype; // 赋值对象
}

这个 inheritPrototype()函数实现了寄生式组合继承的核心逻辑。

这个函数接收两个参数:子类构造函数父类构造函数 。在这个函数内部,第一步是创建父类原型的一个副本。然后,给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型。

示例

javascript 复制代码
// 定义父类
function Animal(name, lang) {
  this.name = name;
  this.lang = lang;
}
// 父类原型方法
Animal.prototype.saying = function () {
  console.log(`${this.name},${this.lang}`);
};
// 继承方法
function inherit(superType, subType) {
  function inheritFn() {
    this.constructor = subType;
  }
  inheritFn.prototype = superType.prototype;
  subType.prototype = new inheritFn();
}
// 子类Dog
function Dog(name, lang, age) {
  Animal.call(this, name, lang); // 借用构造函数
  this.age = age; // Dog类特有属性
}
// 继承父类
inherit(Animal, Dog);
Dog.prototype.dogAge = function () {
  console.log(`${this.name}今年${this.age}岁了`);
};
// 子类Cat
function Cat(name, lang, color) {
  Animal.call(this, name, lang);
  this.color = color;
}
inherit(Animal, Cat);
Cat.prototype.catColor = function () {
  console.log(`${this.name}是${this.color}的`);
};

const dog = new Dog("狗", "汪汪汪", "2");
const cat = new Cat("猫", "喵喵喵", "灰色");

上面代码定义了一个父类Animal动物类,父类有两个属性分别是名称name和语言lang,并在父类原型上定义一个方法saying输出这个动物是怎么叫的。

实现一个继承方法,在new inheritFn时将构造函数指向子类,将子类的原型指向父类的一个副本。

定义两个子类DogCat,在将子类Dog的原型指向父类Animal原型的副本之后,也就是调用了继承方法之后,才可以在子类Dog的原型上定义自己的方法,Cat类似。但是DogCat都有自己的属性和自己的方法,同时继承了父类Animal的属性和方法。

寄生式组合继承的高效体现在它只调用了一次Animal构造函数。

类extends

以上都是使用ES5的特性模拟类,实现继承的代码也显得非常冗长和混乱。ES6新增的class关键字,具有正式定义类的能力。

需要注意的是,类(class)是ES6中新的基础性语法糖结构,表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。

示例

类可以包含构造函数方法、实例方法、获取函数、设置函数和静态类方法,但这些都不是必需的,空的类定义照样有效。

javascript 复制代码
class Animal {
  constructor(name, lang) {
    this._name = name;
    this._lang = lang;
  }
  saying() {
    console.log(`${this._name},${this._lang}`);
  }
}

class Dog extends Animal {
  constructor(name, lang, age) {
    super(name, lang);
    this._age = age;
  }
  dogAge() {
    console.log(`${this._name}今年${this._age}岁了`);
  }
}
const dog = new Dog("狗", "汪汪汪", 2);
dog.saying();
dog.dogAge();
相关推荐
还是大剑师兰特39 分钟前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解39 分钟前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django
张张打怪兽1 小时前
css-50 Projects in 50 Days(3)
前端·css