7 реализаций наследования JavaScript, которые сможет понять даже соседский ребенок

внешний интерфейс

Эта статья была впервые опубликована сообществом Tencent IMWEB:imweb.io/

В JavaScript нет классов, цепочка прототипов для достижения наследования

Потому что первым языком, с которым я столкнулся в школе, был cpp, который является статически типизированным языком, а ключевое слово class напрямую используется для реализации объектно-ориентированного, и оно говорит только об идее проектирования объектно-ориентированного, которая мешает мне понять механизм наследования языка javascript.

JavaScript не является понятием «подкласс» и «родительский», нет различий между «классами» (класс) и «экземпляром» (экземпляром), спасибо "Сеть прототипов"(цепочка прототипов) реализует наследование.

Когда я учился, мне очень хотелось пожаловаться, я потратил столько усилий на моделирование классов, почему js не разработал ключевое слово class в начале, а использовал class только в качестве зарезервированного слова в начале? (После ES6 есть ключевое слово class, которое является синтаксическим сахаром для прототипов)

В то время я всегда задавался вопросом: «Это недостаток дизайна, что в js нет классов?»

Оказалось, что в начале дизайна JavaScript все типы данных в дизайне былиобъект(объект), поначалу JavaScript просто хотел проектироваться как простой скриптовый язык.Дизайнер JavaScript полон объектов, и должен быть механизм соединения всех объектов, но если вводится понятие "класс", то это слишком «формально», что усложняет начало работы.

Чтобы реализовать наследование, но не хотите использовать классы, что мне делать?

Брендан Эйх, дизайнер JavaScript, обнаружил, что его можно использовать в таких языках, как C++ и Java.newкоманда для создания экземпляра.

Поэтому в JavaScript была введена новая команда для создания экземпляра объекта из объекта-прототипа. Но в JavaScript нет «классов», так как же должны быть представлены объекты-прототипы?

В это время он думал, что когда c++ и java используют новую команду, они вызовут "класс"Конструктор(конструктор), поэтому он сделал упрощенный дизайн.В JavaScript за новой командой следует не класс, а конструктор.

Одним из недостатков использования конструкторов для создания объектов-экземпляров является невозможность совместного использования свойств и методов.

Каждый экземпляр объекта имеет собственную копию своих свойств и методов. Это не только невозможно обмениваться данными, но и огромная трата ресурсов.

Помня об этом, Брендан Эйх решил задать конструкторусвойство прототипа.

Это свойство содержитпрототип объекта(Да, значением свойства прототипа является объект-прототип), все свойства и методы, которыми должны делиться объекты-экземпляры, помещаются в этот объект, а те, которыми не нужно делиться, помещаются в конструктор.

Как только объект экземпляра создан, на свойства и методы объекта-прототипа будут автоматически ссылаться, то есть свойства и методы объекта экземпляра делятся на два типа: один является локальным, а другой — ссылочным.

Поскольку все объекты-экземпляры используют один и тот же объект-прототип, снаружи объект-прототип кажется прототипом объекта-экземпляра, а объект-экземпляр кажется «наследующим» объект-прототип.

Если вы не знаете C++, Java или другие языки программирования, я думаю, вы заснете после прочтения вышеизложенного! Хорошо, давайте перейдем непосредственно к коду~

Наследование цепочки прототипов

//原型链继承

// 父类
// 拥有属性 name
function parents(){
    this.name = "JoseyDong";
}

// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
    console.log(this.name);
}

//子类
function child(){
}

//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()

// 创建一个子类的实例对象,如果它有父类的属性和方法,那么就证明继承实现了
let child1 = new child();

child1.getName(); // => JoseyDong

Когда есть только один экземпляр объекта подкласса, мы не видим никаких проблем. Однако в реальных сценариях мы создадим множество объектов-экземпляров для наследования родительского класса, ведь чем больше наследования, тем больше копируется кода~

//原型链继承

// 父类
// 拥有属性 name
function parents(){
    this.name = ["JoseyDong"];
}

// 在父类的原型对象上添加一个getName方法
parents.prototype.getName = function(){
    console.log(this.name);
}

//子类
function child(){
}

//子类的原型对象 指向 父类的实例对象
child.prototype = new parents()

// 创建一个子类的实例对象,如果它有父类的属性和方法,那么就证明继承实现了
let child1 = new child();

child1.getName(); // => ["JoseyDong"]

// 创建一个子类的实例对象,在child1修改name前实现继承
let child2 = new child();

// 修改子类的实例对象child1的name属性
child1.name.push("xixi");

// 创建子类的另一个实例对象,在child1修改name后实现继承
let child3 = new child();

child1.getName();// => ["JoseyDong", "xixi"]
child2.getName();// => ["JoseyDong", "xixi"]
child3.getName();// => ["JoseyDong", "xixi"]

Во многих случаях значения в наших объектах экземпляра будут меняться в зависимости от конкретной сцены. Например, в это время, в дополнение к Joseydong, друг нашего ребенка1 дал ей новое имя xixi, и мы изменили значение имени child1. И ребенок1, ребенок2, ребенок3 — это три независимых личности, но, наконец, выяснилось, что у трех детей есть новые имена!

Это означает, что в наследовании цепочки прототипов используются значения в одной и той же памяти, поэтому изменение значений в памяти изменит значения в других унаследованных экземплярах подкласса.

Это не тот эффект, которого мы хотим, в конце концов, только child1 было дано новое имя. Кроме того, если я хочу передать параметры родительскому классу через объект экземпляра дочернего класса, это невозможно.

Заемный конструктор

// 构造函数继承


function parents(){
    this.name = ["JoseyDong"];
}

// 在子类中,使用call方法构造函数,实现继承
function child(){
    parents.call(this);
}

let child1 = new child();
let child2 = new child();

child1.name.push("xixi");

let child3 = new child();

console.log(child1.name);// => ["JoseyDong", "xixi"]
console.log(child2.name);// => ["JoseyDong"]
console.log(child3.name);// => ["JoseyDong"]

Используя метод конструктора, мы изменили только имя дочернего элемента1, а атрибуты имени дочернего элемента2 и дочернего элемента3 не были затронуты~

В то же время, поскольку call() поддерживает передачу параметров, мы также можем передавать параметры родителю в дочернем~

// 构造函数实现继承
//子类向父类传参

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

//call方法支持传递参数
function child(name){
    parents.call(this,name)
}

let child1 = new child("I am child1");

let child2 = new child("I am child2");

console.log(child1.name);// => I am child1
console.log(child2.name);// => I am child2

Что ж, теперь мы реализуем наследование через конструкторы, чтобы компенсировать недостатки использования цепочек прототипов для реализации наследования, а также преимущество реализации наследования через конструкторы:

1. Избегайте того, чтобы свойства ссылочных типов были общими для всех экземпляров.

2. Вы можете передать параметры родителю в дочернем

Однако у этого подхода есть и недостатки, поскольку методы определяются в конструкторе, а метод создается каждый раз при создании экземпляра.

наследование композиции

Мы обнаружили, что наследование через цепочку прототипов повторно использует одни и те же атрибуты и методы, а наследование через конструкторы — это независимые атрибуты и методы. Поэтому мы решили воспользоваться этим, объединив два подхода: определение методов на прототипахповторное использование функциис помощью конструктора, чтобы убедиться, что каждый экземпляр имеет егоСобственные свойства.

Позвольте дать вам еще один каштан, чтобы все ощутили преимущества комбинированного наследования~

//组合继承

// 偶像练习生大赛开始报名了
// 初赛,我们找了一类练习生
// 这类练习生都有名字这个属性,但名字的值不同,并且都有爱好,而爱好是相同的
// 只有会唱跳rap的练习生才可进入初赛
function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

// 我们在student那类里面找到更特殊的一类进入复赛
// 当然,我们已经知道初赛时有了name属性了,而不同练习生名字的值不同,所以使用构造函数方法继承
// 同时,我们想再让练习生们再介绍下自己的年龄,每个子类还可以自己新增属性
// 当然啦,具体的名字年龄就由每个练习生实例来定
// 类只告诉你,有这个属性

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

// 而大家的爱好值都相同,这个时候用原型链继承就好啦
// 每个对象都有构造函数,原型对象也是对象,也有构造函数,这里简单的把构造函数理解为谁的构造函数就要指向谁
// 第一句将子类的原型对象指向父类的实例对象时,同时也把子类的构造函数指向了父类
// 我们需要手动的将子类原型对象的构造函数指回子类
greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;

// 决赛 kunkun和假kunkun进入了决赛
let kunkun = new greatStudent('kunkun','18');
let fakekun = new greatStudent('fakekun','28');

// 有请两位选手介绍下自己的属性值
console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies) // => fakekunkun 28 ["sing", "dance", "rap"]

// 这个时候,kunkun选手说自己还有个隐藏技能是打篮球
kunkun.hobbies.push("basketball");

console.log(kunkun.name,kunkun.age,kunkun.hobbies) // => kunkun 18 ["sing", "dance", "rap", "basketball"]
console.log(fakekun.name,fakekun.age,fakekun.hobbies)// => fakekun 28 ["sing", "dance", "rap"]

// 我们可以看到,假kunkun并没有抄袭到kunkun的打篮球技能
// 并且如果这个时候新来一位选手,从初赛复赛闯进来的一匹黑马
// 可以看到黑马并没有学习到kunkun的隐藏技能
let heima = new greatStudent('heima','20')
console.log(heima.name,heima.age,heima.hobbies) // => heima 20 ["sing", "dance", "rap"]

Можно видеть, что составное наследование позволяет избежать недостатков наследования цепочки прототипов и наследования конструктора и сочетает в себе преимущества обоих, становясь наиболее часто используемым методом наследования в javascript.

прототипное наследование

Идея этого наследования заключается в использовании переданного объекта в качестве прототипа создаваемого объекта.

function createObj(o){
  function F(){};
  F.prototype = o;
  return new F();
}

Давайте реализуем прототипное наследование и посмотрим, есть ли проблемы

// 原型式继承

function createObj(o){
    function F(){};
    F.prototype = o;
    return new F();
}

let person = {
    name:'JoseyDong',
    hobbies:['sing','dance','rap']
}

let person1 = createObj(person);
let person2 = createObj(person);

console.log(person1.name,person1.hobbies) // => JoseyDong ["sing", "dance", "rap"]
console.log(person2.name,person2.hobbies) // => JoseyDong ["sing", "dance", "rap"]

person1.name = "xixi";
person1.hobbies.push("basketball");

console.log(person1.name,person1.hobbies) //xixi ["sing", "dance", "rap", "basketball"]
console.log(person2.name,person2.hobbies) //JoseyDong ["sing", "dance", "rap", "basketball"]

В это время мы обнаружили, что значение увлечений лица 1 изменилось, а значение увлечений лица 2 также изменилось.

Это связано с тем, что значения свойств, которые содержат ссылочные типы, всегда будут совместно использовать соответствующие значения, как наследование цепочки прототипов~

Значение person1.name было изменено, но значение person2.name не изменилось не потому, что person1 и person2 имеют независимые значения имени, а потому, что оператор person1.name = "xixi" должен добавить экземпляр объекта person1. Имеется атрибут name, и значение name в его объекте-прототипе не изменилось, поэтому имя person2 не изменилось. Потому что, когда мы ищем свойства объекта, мы всегда сначала ищем экземпляр объекта, а затем ищем свойства объекта-прототипа, если не находим его. Если в объекте-экземпляре и в объекте-прототипе есть свойство с одинаковым именем, значение объекта-экземпляра всегда берется первым.

ESMAScript5 добавляет метод Object.create() для стандартизации наследования прототипов~

паразитарное наследование

Создайте функцию, которая просто инкапсулирует процесс наследования, внутренне улучшает объект в той или иной форме и, наконец, возвращает объект.

//寄生式继承

function createObj(o){
    let clone = Object.create(o);
    clone.sayName = function(){
        console.log('hi');
    }
    return clone
}

let person = {
    name:"JoseyDong",
    hobbies:["sing","dance","rap"]
}

let anotherPerson = createObj(person);
anotherPerson.sayName(); // => hi

Конечно, использование паразитного наследования для добавления функций к объектам, например заимствование шаблона конструктора, создает методы каждый раз при создании объекта.

Паразитическая композиционная наследственность

Как мы уже говорили ранее, наследование композиции является наиболее часто используемым шаблоном наследования в JavaScript. Здесь мы сначала рассмотрим код композиционного наследования:

//组合继承

function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

greatStudent.prototype = new student();
greatStudent.prototype.constructor = greatStudent;

let kunkun = new greatStudent('kunkun','18');

Самый большой недостаток наследования композиции заключается в том, что родительский конструктор вызывается дважды.

Один раз при установке прототипа экземпляра подкласса:

greatStudent.prototype = new student();

Один раз при создании экземпляра подтипа:

let kunkun = new greatStudent('kunkun','18');

В этом примере, если мы напечатаем объект kunkun, мы обнаружим, что и greatStudent.prototype, и kunkun имеют свойство, называемое хобби.

На самом деле это связано с тем, что значения атрибута в объекте-экземпляре и объекте-прототипе повторяются, и при поиске значения атрибута, если значение атрибута найдено в объекте-экземпляре, оно не будет найдено в объекте-прототипе, и значение этой части объекта-прототипа - настоящая трата места для хранения.

Так как же нам продолжать совершенствоваться и избегать этого повторного вызова?

Что, если мы не будем использовать greatStudent.prototype = new student(), а позволим greatStudent.prototype напрямую обращаться к student.prototype?

Посмотрите, как это реализовано:

// 寄生组合式继承

function student(name){
    this.name = name;
    this.hobbies = ["sing","dance","rap"];
}

function greatStudent(name,age){
    student.call(this,name);
    this.age = age;
}

//关键的三步 实现继承
// 使用F空函数当子类和父类的媒介 是为了防止修改子类的原型对象影响到父类的原型对象
let F = function(){};
F.prototype = student.prototype;
greatStudent.prototype = new F();

let kunkun = new greatStudent('kunkun','18');
console.log(kunkun);

распечатать результат:

Как видите, свойство hobbies больше не существует в объекте-прототипе экземпляра kunkun.

Наконец, мы инкапсулируем этот унаследованный метод:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    let prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

Цитируя похвалу паразитическому композиционному наследованию в «Advanced JavaScript Programming»:

Эффективность этого подхода заключается в том, что он вызывает конструктор Parent только один раз и, таким образом, позволяет избежать создания ненужных избыточных свойств в Parent.prototype. При этом цепочка прототипов остается неизменной, поэтому можно нормально использовать instanceof и isPrototypeOf. Разработчики в целом согласны с тем, что паразитное композиционное наследование является идеальной парадигмой наследования для ссылочных типов.

В общем, этот js-способ реализации наследования — лучший.

ES6 реализует наследование

Однако позже ES6 реализует наследование с помощью ключевого слова extends.

// ES6 

class parents {
    constructor(){
        this.grandmather = 'rose';
        this.grandfather = 'jack';
    }
}

class children extends parents{
    constructor(mather,father){
    //super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。
        super();
        this.mather = mather;
        this.father = father;
    }
}

let child = new children('mama','baba');
console.log(child) // =>
// father: "baba"
// grandfather: "jack"
// grandmather: "rose"
// mather: "mama"

Подклассы должны вызывать метод super в методе конструктора, иначе при создании экземпляра будет сообщено об ошибке. Это связано с тем, что подкласс не имеет собственного объекта this, а наследует объект this родительского класса и затем обрабатывает его.

Ключевое слово this можно использовать только после вызова super, иначе будет сообщено об ошибке. Это связано с тем, что создание экземпляра подкласса основано на обработке экземпляра родительского класса, и только суперметод может вернуть экземпляр родительского класса.

Суть наследования ES5 заключается в том, чтобы сначала создать экземпляр объекта this подкласса, а затем добавить к этому метод суперкласса (Parent.call(this)).

Суть механизма наследования ES6 заключается в том, чтобы сначала создать объект-экземпляр this родительского класса (поэтому сначала должен быть вызван метод super()), а затем использовать конструктор подкласса для его изменения.

Основной код для es6 для реализации наследования выглядит следующим образом:

function _inherits(subType, superType) {
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superType) {
    Object.setPrototypeOf 
    ? Object.setPrototypeOf(subType, superType) 
    : subType.__proto__ = superType;
  }
}

подклассprotoАтрибут: указывает на наследование конструкторов, всегда указывая на родительский класс. Свойство прототипа подклассаprotoСвойство: указывает на наследование метода, всегда указывая на свойство прототипа родительского класса.

Кроме того, ES6 может настраивать подклассы нативных структур данных (таких как Array, String и т. д.), чего не может сделать ES5.