Наследственность, как базовый навык и обязательная пробная площадка для собеседований, должна быть освоена в совершенстве. Небольшие компании могут разрешить вам писать наследование только от руки (обычно
寄生组合式继承Вы можете), большая фабрика должна попросить вас полностью проанализировать преимущества и недостатки каждого наследства. Эта статья даст вам всестороннее представление о наследовании JavaScript, его преимуществах и недостатках, чтобы оставаться непобедимым в холодную зиму.
Наследование цепочки прототипов
предыдущий пост«О прототипе / цепочке прототипов с точки зрения восприятия»Расскажите, что такое прототип и цепочка прототипов. Кратко напомним взаимосвязь между конструкторами, прототипами и цепочками прототипов: каждый конструктор имеетprototypeсвойство, которое указывает на объект-прототип, а все объекты-прототипы имеют указатель на конструкторconstructor, все экземпляры объектов содержат внутренние указатели на объекты-прототипы.[[prototype]]. Если мы сделаем объект-прототип равным экземпляру другого конструктора, то этот объект-прототип будет содержать указатель на другой прототип. Таким образом, слой за слоем, шаг за шагом формируется цепочка прототипов.
Согласно приведенному выше введению, мы можем написать原型链继承.
function Vehicle(powerSource) {
this.powerSource = powerSource;
this.components = ['座椅', '轮子'];
}
Vehicle.prototype.run = function() {
console.log('running~');
};
function Car(wheelNumber) {
this.wheelNumber = wheelNumber;
}
Car.prototype.playMusic = function() {
console.log('sing~');
};
// 将父构造函数的实例赋值给子构造函数的原型
Car.prototype = new Vehicle();
const car1 = new Car(4);
В приведенном выше примере сначала определите交通工具Конструктор , он имеет два свойства, которые驱动方式а также组成部分, и метод-прототип, который跑; следующее определение называется汽车конструктор, который имеет轮胎数量свойства и播放音乐метод. мы будемVehicleЭкземпляр назначаетсяCarпрототип и создатьcar1пример.
Но у этого подхода есть несколько недостатков:
-
Операции над ссылочными типами несколькими экземплярами будут изменены
-
Свойство конструктора прототипа подтипа переопределяется.
-
Добавление свойств и методов к прототипу подтипа должно выполняться после замены прототипа
-
При создании экземпляра подтипа невозможно передать аргументы конструктору супертипа
Недостаток 1
Как видно из рисунка выше, к прототипу экземпляра добавляется атрибут экземпляра родительского класса, когда атрибут прототипа является ссылочным типом, происходит подделка данных.
Мы добавили новый экземпляр под названиемcar2, и датьcar2.componentsДобавить новый элемент. Распечататьcar1,Обнаружитьcar1.componentsтакже изменился. Это так называемая операция нескольких экземпляров по ссылочному типу, которая будет изменена.
const car2 = new Car(8);
car2.components.push('灯具');
car2.components; // ['座椅', '轮子', '灯具']
car1.components; // ['座椅', '轮子', '灯具']
Недостаток 2
Этот путь приводит кCar.prototype.constructorпереопределяется, он указывает наVehicleвместоCar. Поэтому вам нужно вручнуюCar.prototype.constructorуказать назадCar.
Car.prototype = new Vehicle();
Car.prototype.constructor === Vehicle; // true
// 重写 Car.prototype 中的 constructor 属性,指向自己的构造函数 Car
Car.prototype.constructor = Car;
Недостаток 3
потому чтоCar.prototype = new Vehicle();Переопределяет объект прототипа автомобиля, поэтому приводит кplayMusicМетоды перезаписываются, тем самым добавляя к методу прототипа должен подкласс замены прототипа после.
function Car(wheelNumber) {
this.wheelNumber = wheelNumber;
}
Car.prototype = new Vehicle();
// 给子类添加原型方法必须在替换原型之后
Car.prototype.playMusic = function() {
console.log('sing~');
};
Минусы 4
Очевидно, создатьcarЭкземпляр не может передавать параметры конструктору родительского класса, то есть не может быть инициализированpowerSourceАтрибуты.
const car = new Car(4);
// 只能创建实例之后再修改父类的属性
car.powerSource = '汽油';
Заимствование наследования конструктора
Этот метод также называется伪造对象или经典继承. его суть在创建子类实例时调用父类的构造函数.
function Vehicle(powerSource) {
this.powerSource = powerSource;
this.components = ['座椅', '轮子'];
}
Vehicle.prototype.run = function() {
console.log('running~');
};
function Car(wheelNumber) {
this.wheelNumber = wheelNumber;
// 继承父类属性并且可以传参
Vehicle.call(this, '汽油');
}
Car.prototype.playMusic = function() {
console.log('sing~');
};
const car = new Car(4);
Преимущество использования классического наследования в том, что вы можете передавать параметры в родительский класс, и этот метод не будет переопределять прототип подкласса, поэтому он не повредит метод прототипа подкласса. Кроме того, поскольку каждый экземпляр будет копировать свойства родительского класса, проблема множественных изменений ссылочного типа не возникнет (поскольку свойства экземпляра родительского класса отсутствуют в прототипе).
Однако недостатки также очевидны, мы не можем найти ни одногоrunТень метода, поскольку этот метод может наследовать только свойства и методы экземпляра родительского класса и не может наследовать свойства и методы прототипа.
Вспоминая функцию-конструктор, упомянутую в предыдущей статье, чтобы поместить общедоступные методы в место, доступное для всех экземпляров, мы обычно помещаем их в прототип функции-конструктора. И если пусть借用构造函数继承Для работы очевидно, что公有方法Напишите его в конструкторе вместо прототипа, что будет расточительно при создании нескольких экземпляров.
наследование композиции
Комбинаторное наследование поглощает преимущества двух вышеупомянутых методов: оно использует цепочку прототипов для реализации наследования методов-прототипов и использует конструктор для реализации наследования свойств экземпляра.
function Vehicle(powerSource) {
this.powerSource = powerSource;
this.components = ['座椅', '轮子'];
}
Vehicle.prototype.run = function() {
console.log('running~');
};
function Car(wheelNumber) {
this.wheelNumber = wheelNumber;
Vehicle.call(this, '汽油'); // 第二次调用父类
}
Car.prototype = new Vehicle(); // 第一次调用父类
// 修正构造函数的指向
Car.prototype.constructor = Car;
Car.prototype.playMusic = function() {
console.log('sing~');
};
const car = new Car(4);
Хотя этот метод может успешно наследовать свойства и методы родительского класса, он дважды вызывает родительский класс. Когда конструктор родительского класса вызывается в первый раз,Car.prototypeполучитеpowerSourceа такжеcomponentsдва свойства; при вызовеCarКогда конструктор сгенерирует экземпляр, он будет вызван сноваVehicleКонструктор, который будет создан на этом инстансе в это времяpowerSourceа такжеcomponents. Согласно правилам цепочки прототипов, эти два свойства экземпляра будут маскировать два одноименных свойства в цепочке прототипов.
прототипное наследование
Этот метод создает новые объекты на основе существующих объектов с помощью прототипов.
Сначала создайте файл с именемobjectфункцию, затем создайте пустую функцию внутриF, а функцияprototypeУказывает на переданный объект и, наконец, возвращает экземпляр функции. По сути,object()сделано один раз для переданного объекта浅拷贝.
function object(proto) {
function F() {}
F.prototype = proto;
return new F();
}
const cat = {
name: 'Lolita',
friends: ['Yancey', 'Sayaka', 'Mitsuha'],
say() {
console.log(this.name);
},
};
const cat1 = object(cat);
Несмотря на то, что этот подход хорош, все же есть некоторые проблемы. потому что原型式继承эквивалентно浅拷贝, так что это приведет к引用类型подделаны несколькими экземплярами. В следующем примере мы даемcat1.friendsдобавляет элемент, но приводит кcat.friendsподделаны.
cat1.friends.push('Hachi');
cat.friends; // ['Yancey', 'Sayaka', 'Mitsuha', 'Hachi']
если ты читаешьObject.create()Полифилл не должен быть незнаком с приведенным выше кодом. Этот метод нормализует прототипное наследование и принимает два параметра: первый параметр передает объект, который будет использоваться в качестве прототипа нового объекта, а второй параметр передает объект дескриптора свойства или null. Подробную документацию по этому API можно найти здесьObject.create() | Полный анализ JavaScript API
const cat = {
name: 'Lolita',
friends: ['Yancey', 'Sayaka', 'Mitsuha'],
say() {
console.log(this.name);
},
};
const cat1 = Object.create(cat, {
name: {
value: 'Kitty',
writable: false,
enumerable: true,
configurable: false,
},
friends: {
get() {
return ['alone'];
},
},
});
паразитарное наследование
Этот подход создает функцию, которая просто инкапсулирует процесс наследования, каким-то образом внутренне дополняет объект и, наконец, возвращает объект, как если бы он действительно выполнял всю работу.
const cat = {
name: 'Lolita',
friends: ['Yancey', 'Sayaka', 'Mitsuha'],
say() {
console.log(this.name);
},
};
function createAnother(original) {
const clone = Object.create(original); // 获取源对象的副本
clone.gender = 'female';
clone.fly = function() {
// 增强这个对象
console.log('I can fly.');
};
return clone; // 返回这个对象
}
const cat1 = createAnother(cat);
а также原型式继承так как этот метод приведет к引用类型подделаны несколькими экземплярами, кроме того,flyметод существует в实例вместо原型, так函数复用Не о чем говорить.
Паразитическая композиционная наследственность
Выше мы говорили о组合继承, его недостатком является то, что родительский класс будет вызываться дважды, поэтому атрибут экземпляра родительского класса будет создан на экземпляре подкласса и его прототипе, что приведет к тому, что атрибут экземпляра закроет атрибут с тем же именем на цепочка прототипов.
хорошо, что у нас есть寄生组合式继承, что по существу через寄生式继承чтобы наследовать прототип родительского класса, а затем присвоить результат прототипу дочернего класса. Можно сказать, что это лучший способ наследования до выхода ES6, и он не использовался в интервью.
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建父类原型的副本
prototype.constructor = child; // 将副本的构造函数指向子类
child.prototype = prototype; // 将该副本赋值给子类的原型
}
Затем мы пытаемся написать пример.
function Vehicle(powerSource) {
this.powerSource = powerSource;
this.components = ['座椅', '轮子'];
}
Vehicle.prototype.run = function() {
console.log('running~');
};
function Car(wheelNumber) {
this.wheelNumber = wheelNumber;
Vehicle.call(this, '汽油');
}
inheritPrototype(Car, Vehicle);
Car.prototype.playMusic = function() {
console.log('sing~');
};
Посмотрите на картинку выше, чтобы понять, почему это лучший подход. Он вызывает суперкласс только один раз, что позволяет избежать создания избыточных свойств в прототипе подкласса, а структура цепочки прототипов остается неизменной.
Если настаивать на недостатках, то добавление свойств и методов к прототипу подтипа все же нужно поместить вinheritPrototypeпосле функции.
Наследование ES6
С точки зрения утилитаризма, после того, как в ES6 был добавлен синтаксис класса, вышеупомянутые методы были сведены только к интервью. Конечно, класс — это просто синтаксический сахар, его основная идея по-прежнему寄生组合式继承, давайте посмотрим, как реализовать наследование с использованием синтаксиса ES6.
class Vehicle {
constructor(powerSource) {
// 用 Object.assign() 会更加简洁
Object.assign(
this,
{ powerSource, components: ['座椅', '轮子'] },
// 当然你完全可以用传统的方式
// this.powerSource = powerSource;
// this.components = ['座椅', '轮子'];
);
}
run() {
console.log('running~');
}
}
class Car extends Vehicle {
constructor(powerSource, wheelNumber) {
// 只有 super 方法才能调用父类实例
super(powerSource, wheelNumber);
this.wheelNumber = wheelNumber;
}
playMusic() {
console.log('sing~');
}
}
const car = new Car('核动力', 3);
Следующий код представляет собой унаследованный полифилл, идеи и寄生组合式继承Последовательный.
function _inherits(subType, superType) {
// 创建对象,创建父类原型的一个副本
subType.prototype = Object.create(superType && superType.prototype, {
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true,
},
});
if (superType) {
// 指定对象,将新创建的对象赋值给子类的原型
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: (subType.__proto__ = superType);
}
}
Сравнение наследования ES5 и ES6
ES5 создает классы с конструкторами, так бывает函数提升; и класс похож на let и const, поэтому вы не можете сначала создать экземпляр, а затем объявить класс, иначе сразу будет сообщено об ошибке.
// Uncaught ReferenceError: Rectangle is not defined
let p = new Rectangle();
class Rectangle {}
Наследование в ES5 заключается в том, чтобы сначала создать экземпляр объекта подкласса, а затем добавить к нему метод суперкласса, то естьParent.call(this).
Наследование в ES6 отличается: по сути, сначала создается объект экземпляра this родительского класса, а затем для его изменения используется конструктор подкласса. Поскольку у подкласса нет собственного объекта this, сначала должен быть вызван метод super() суперкласса, иначе новый экземпляр сообщит об ошибке.
наконец
Добро пожаловать в мой публичный аккаунт WeChat: начало атаки
Ссылаться на
Расширенное программирование с помощью JavaScript (третье издание) - Николас С. Закас
[Продвинутый уровень 5-2] Цепочка графических прототипов и ее наследование