Глубокое понимание прототипов JavaScript

JavaScript

предисловие

Прототипы, как фронтенд-разработчики, более-менее на слуху. Возможно, вы хотели узнать об этом, но не сделали этого по разным причинам, следуйте за мной сейчас и исследуйте это вместе. В этой статье мы пойдем от простого к более глубокому и шаг за шагом демистифицируем прототип JavaScript. (требуются базовые знания объектов JavaScript)

Исходный код:GitHub

прототип

1. Что такое прототип?

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

Все мы знаем, что JavaScript — это объектно-ориентированный язык сценариев, но в нем нет концепции классов, поэтому объекты в JavaScript отличаются от объектов в таких языках, основанных на классах, как Java. Объекты в JavaScript — это неупорядоченные наборы свойств, которые могут содержать примитивные значения, объекты или функции.Это больше похоже на набор пар ключ-значение, но на самом деле очень похоже. С объектами само собой разумеется, что есть наследование, иначе между объектами нет связи, и она действительно сводится к набору пар ключ-значение. Так как же JavaScript без классов реализует наследование?

Мы знаем, что в JavaScript вы можете использовать синтаксис конструктора (функции, вызываемые через new, часто называются конструкторами) для создания нового объекта, например:

// 构造函数,无返回值
function Person(name) {
  this.name = name;
}
// 通过 new 新建一个对象
var person = new Person('Mike');

Это похоже на синтаксис создания объектов в общих объектно-ориентированных языках программирования (Java или C++), но это упрощенный дизайн,newДалее следует не класс, а конструктор. Конструктор здесь можно рассматривать как тип, точно так же, как класс в объектно-ориентированном языке программирования, но объекты, созданные таким образом, не имеют никаких других связей, кроме свойств, а свойства и методы не могут быть общими между объектами. Всякий раз, когда мы создаем новый объект, выделяется новый кусок памяти для методов и свойств, что является огромной тратой ресурсов. Имея это в виду, дизайнер JavaScript Брендан Эйх решил установить свойство в конструкторе. Это свойство указывает на объект.Все свойства и методы, которые должны быть общими для объектов экземпляра, помещаются в этот объект, а те, которые не нужно использовать совместно, помещаются в конструктор. После создания объекта-экземпляра автоматически выполняется ссылка на свойства и методы этого объекта. Другими словами, свойства и методы объектов-экземпляров делятся на два типа: один является локальным и не используется совместно, а другой используется по ссылке и является общим. Этот объект является прототипом (прототипом) объекта, именуемым прототипом.

Функции, которые мы создаем с помощью объявлений функций или выражений функций, имеют свойство прототипа (прототипа), которое является указателем на объект, который является прототипом экземпляра объекта, созданного путем вызова конструктора.В частности, в спецификации ECMA-262 функции, созданные через Function.prototype.bind, не имеют свойства прототипа.. Прототип может содержать свойства и методы, общие для всех экземпляров, то есть, пока у прототипа есть свойства и методы, экземпляр объекта, сгенерированный вызовом конструктора, будет иметь эти свойства и методы. См. код ниже:

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

Person.prototype.age = '20';
Person.prototype.sayName = function() {
  console.log(this.name);
}

var person1 = new Person('Jack');
var person2 = new Person('Mike');

person1.sayName(); // Jack
person2.sayName(); // Mike
console.log(person1.age); // 20
console.log(person2.age); // 20

В этом коде мы объявляемPersonфункцию, и добавил к прототипу этой функцииageсвойства иsayNameметод, а затем создать два экземпляра объектаperson1а такжеperson2, каждый из этих двух экземпляров имеет свои собственные свойстваnameи свойства прототипаageи методsayName. Все объекты-экземпляры совместно используют свойства и методы объекта-прототипа, поэтому кажется, что объект-прототип подобен классу, и мы можем использовать прототип для достижения наследования.

2. конструктор и [[Prototype]]

Мы знаем, что у каждой функции есть свойство прототипа, которое указывает на прототип функции, поэтому, когда мы получаем функцию, мы можем определить прототип функции. И наоборот, если нам дан прототип функции, как мы узнаем, какой функции принадлежит этот прототип? Это говорит о свойстве конструктора прототипа:

По умолчанию все объекты-прототипы автоматически получают свойство-конструктор (constructor), которое содержит указатель на функцию, в которой находится свойство-прототип.

То есть у каждого прототипа есть свойство конструктора, указывающее на функцию, в которой находится прототип.В предыдущем примере Person.prototype.constructor указывает на Person. Ниже приведена иллюстрация взаимосвязи между конструкторами и прототипами:

давай, давай поговорим[[prototype]].

Когда мы вызываем конструктор для создания нового экземпляра (нового объекта), например, в приведенном выше примереperson1, внутри экземпляра будет указатель (внутреннее свойство) на прототип конструктора. Этот указатель называется [[Prototype]] в 5-м издании ECMA-262. Мы можем связать график с функцией обновления и прототипом:

Хотя нет стандартного способа получить доступ к [[Prototype]] в сценариях, но в Firefox, Safari и Chrome вы можете__proto__доступ к собственности. В других реализациях это свойство полностью невидимо для скрипта. Однако очень важно четко понимать, что связь существует между экземпляром и объектом-прототипом конструктора, а не между экземпляром и конструктором.

Когда в VSCode включен режим отладки, мы можем увидеть следующие отношения:

Из приведенного выше рисунка мы видим, чтоPersonизprototypeсвойства иperson1из__proto__свойства точно такие же,Person.prototypeсодержитconstructorатрибут, указывающий наPersonфункция.这些可以很好的印证我们上面所说的构造函数、原型、constructorтак же как__proto__Отношение между.

3. Экземпляр и прототип объекта

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

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

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

мы можем пройтиisPrototypeOf()Метод оценивает, есть ли связь между прототипом и экземпляром объекта, или мы также можем использовать методы, добавленные в ES5.Object.getPrototypeOf()получить экземпляр объекта__proto__Стоимость имущества. См. пример ниже:

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true

2. Доступ к свойствам и методам экземпляра объекта

Всякий раз, когда код считывает свойство объекта, выполняется поиск свойства с заданным именем. Сначала поиск начинается с самого экземпляра объекта. Если свойство с заданным именем найдено в экземпляре объекта, возвращается значение этого свойства. Если не нашли, продолжайте поиск__proto__Объект-прототип, на который указывает указатель, ищет свойство с заданным именем в объекте-прототипе и возвращает значение свойства, если свойство найдено в объекте-прототипе. Если он не сможет его найти, он будет продолжать искать прототип прототипа, пока не достигнет верхнего уровня. Это является обоснованием для нескольких экземпляров объекта, совместно использующих свойства и методы, содержащиеся в прототипе.

Хотя вы можете получить доступ к значению, хранящемуся в прототипе, через экземпляр объекта, вы не можете переопределить значение в прототипе через экземпляр объекта. Свойство, которое мы добавляем в экземпляр, будет маскировать доступное для записи свойство с тем же именем в прототипе.Если свойство доступно только для чтения, в строгом режиме будет вызвана ошибка, но ее нельзя замаскировать в нестрогом режиме. Кроме того, поhasOwnPropertyМетод может определить, существует ли свойство в экземпляре объекта (он не может определить, существует ли свойство в прототипе объекта). Рассмотрим следующий пример:

function Person() {}

Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

// 设置 phone 属性为不可写
Object.defineProperty(person1, 'phone', {
  writable: false,
  value: '100'
});

// 新增一个访问器属性 address
Object.defineProperty(person1, 'address', {
  set: function(value) {
    console.log('set');
    address = value;
  },
  get: function() {
    return address;
  }
});

// 注意,此处不能用 name,因为函数本身存在 name 属性
console.log(person1.hasOwnProperty('age')); // false
console.log(Person.hasOwnProperty('age')); // false

person1.name = 'Greg';
console.log(person1.hasOwnProperty('name')); // true
console.log(person1.name); //'Greg'——来自实例
console.log(person2.name); //'Nicholas'——来自原型

person1.phone = '123'; // 严格模式下报错
person1.address = 'china hua'; // 调用 set 方法,输出 'set'
console.log(person1.address); // 'china hua'
console.log(person1.phone); // 100

3. в операторе

Существует два способа использования оператора in:

  • Использовать отдельно

    При отдельном использовании оператор in возвращает значение true, если данное свойство доступно через объект, независимо от того, существует ли это свойство в экземпляре или прототипе.

  • Используется в циклах for-in.

    При использовании цикла for-in возвращаются все перечисленные свойства, к которым можно получить доступ через объект, включая свойства, существующие в экземпляре, и свойства, существующие в прототипе. Если вам нужно получить все свойства (включая неперечислимые свойства), вы можете использовать метод Object.getOwnPropertyNames().

См. пример ниже:

function Person(){
  this.name = 'Mike';
}

Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){ console.log(this.name); };

var person = new Person();

for(var item in person) {
  console.log(item); // name age job sayName
}

console.log('name' in person); // true - 来自实例
console.log('age' in person); //  true - 来自原型

4. Динамичность прототипов

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

var person = new Person();

Person.prototype.sayHi = function(){ console.log("hi"); };
person.sayHi(); // "hi"

В приведенном выше коде сначала создаетсяPersonэкземпляр и сохраните его вpersonсередина. Затем следующее утверждение вPerson.prototypeдобавил метод вsayHi(). хотяpersonЭкземпляр создается до добавления нового метода, но он по-прежнему имеет доступ к этому новому методу. При вызове этого метода он сначала ищетpersonЕсть ли этот метод в экземпляре, найти нет, а затем перейти кpersonПоиск в объекте-прототипе , этот метод существует в прототипе, и поиск завершается. ;

Но результат следующего кода совершенно другой:

function Person() {}

var person = new Person();

Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

person.sayName(); // error

Присмотревшись к приведенному выше коду, мы напрямую используем синтаксис литерала объекта, чтобы датьPerson.prototypeКажется, что нет никаких проблем. Но мы должны знать, что синдром размером с текст сгенерирует новый объект, а это значит, чтоPerson.prototypeэто новый объект иpersonиз__proto__Свойства больше не имеют значения. В этот момент мы пытаемся вызватьsayNameметод сообщит об ошибке, потому чтоpersonиз__proto__Атрибут указывает на исходный объект-прототип, а исходный объект-прототип — нет.sayNameметод, поэтому будет сообщено об ошибке.

Сеть прототипов

1. Прототип прототипа

В предыдущем примере мы добавляем свойства и методы непосредственно к прототипу или присваиваем новый объект прототипу, что, если мы сделаем объект-прототип равным экземпляру другого типа?

function Person() {
  this.age = '20';
}

Person.prototype.weight = '120';

function Engineer() {
  this.work = 'Front-End';
}

Engineer.prototype = new Person(); // 此时 Engineer.prototype 没有 constructor 属性
Engineer.prototype.constructor = Engineer;

Engineer.prototype.getAge = function() {
  console.log(this.age);
}

var person = new Person();
var engineer = new Engineer();

console.log(person.age); // 20
engineer.getAge(); // 20
console.log(engineer.weight); // 120
console.log(Engineer.prototype.__proto__ == Person.prototype); // true

В приведенном выше коде есть два конструктораPersonа такжеEngineer, можно рассматривать как два типа,EngineerПрототипPersonэкземпляр ,то естьEngineerПрототип указывает наPersonПрототип(обратите внимание на последнюю строку кода выше). Затем мы создаем новыйPersonа такжеEngineerобъект экземпляра, вы можете видетьengineerОбъекты экземпляра могут получить доступPersonизageа такжеweightсвойство, которое легко понять:EngineerПрототипPersonобъект экземпляра,PersonОбъект экземпляра содержитageсвойства, при этомweightсобственностьPersonсвойства объекта-прототипа,PersonОбъекты-экземпляры могут естественным образом обращаться к свойствам прототипа и аналогичным образомEngineerэкземпляр объектаengineerтакже может получить доступEngineerДоступ к свойствам прототипа также можно получить косвенно.PersonСвойства прототипа.

Кажется, что отношения немного сложны, это не имеет значения, мы используем картинку, чтобы объяснить эти отношения:

Понятно ли сразу, по красной линии на рисунке,engineerОбъекты экземпляра могут быть получены гладкоPersonсвойства экземпляра иPersonСвойства прототипа. На данный момент основа почти завершена.После того, как мы понимаем прототип прототипа, легко понять цепочку прототипов.

2. Цепочка прототипов

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

Объект-прототип может содержать указатель на другой прототип (прототип 2), и, соответственно, другой прототип (прототип 2) также может содержать указатель на соответствующий конструктор (конструктор прототипа 2). Если другой прототип (прототип 2) является экземпляром другого типа (конструктор прототипа 3), то вышеуказанная связь остается в силе, и так далее, формируется цепочка экземпляров и прототипов. Это основная концепция так называемой цепочки прототипов.

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

Реализуя цепочку прототипов, вы существенно расширяете механизм поиска прототипов, упомянутый ранее: при доступе к свойству экземпляра в режиме чтения сначала выполняется поиск этого свойства в экземпляре. Если свойство не найдено, продолжается поиск прототипа экземпляра. В случае наследования по цепочке прототипов процесс поиска может продолжаться вверх по цепочке прототипов. В случае, когда свойство или метод не найдены, процесс поиска всегда проходит цикл за этапом до конца цепочки прототипов перед остановкой.

Так чем же заканчивается цепочка прототипов? Мы должны знать, что все функции默认原型являются экземплярами Object, поэтому прототип по умолчанию будет содержать внутренний указатель наObject.prototype. Мы можем добавить строку кода в конец приведенного выше кода для проверки:

console.log(Person.prototype.__proto__ == Object.prototype); // true

ЭтоObject.prototypeЧто является прототипом того, что не может быть без конца? Умные друзья могли догадаться, да, этоnull, null означает, что здесь не должно быть значения, которое является конечной точкой. Мы можем проверить это в консоли Chrome или Node:

console.log(Object.prototype.__proto__); // null

Обновим диаграмму отношений:

Пока все понятно, поговорим о полезности цепочки прототипов.

наследовать

Наследование — очень распространенная концепция в объектно-ориентированных языках.В процессе чтения предыдущего кода мы фактически реализовали простое отношение наследования, которое, возможно, было обнаружено нашими внимательными друзьями. В JavaScript реализация наследования в основном достигается за счет использования цепочки прототипов.

1. Реализация цепочки прототипов

Простая реализация наследования на основе цепочки прототипов выглядит так:

// 父类型
function Super(){
    this.flag = 'super';
}

Super.prototype.getFlag = function(){
    return this.flag;
}
// 子类型
function Sub(){
    this.subFlag = 'sub';
}
// 实现继承
Sub.prototype = new Super();
Sub.prototype.getSubFlag = function(){
    return this.subFlag;
}

var instance = new Sub();

console.log(instance.subFlag); // sub
console.log(instance.flag); // super

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

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

  2. При создании экземпляра подтипа нельзя передавать аргументы конструктору супертипа.
    На самом деле следует сказать, что нет способа передать параметры конструктору родительского типа, не затрагивая все экземпляры объекта, и передаваемые нами параметры станут свойствами всех экземпляров.

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

2. Заимствование конструкторов

Для решения вышеуказанных проблем используется метод, называемый借用构造函数的技术. Основная идея этой техники проста:apply()илиcall()метод, который вызывает конструктор супертипа внутри конструктора подтипа, так что подтип имеет свойства и методы супертипа.

function Super(properties){
  this.properties = [].concat(properties);
  this.colors = ['red', 'blue', 'green'];
}

function Sub(properties){
  // 继承了 Super,传递参数,互不影响
  Super.apply(this, properties);
}

var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red, blue, green, black'
console.log(instance1.properties[0]); // 'instance1'

var instance2 = new Sub();
console.log(instance2.colors); // 'red, blue, green'
console.log(instance2.properties[0]); // 'undefined'

Заимствование конструктора действительно может решить две проблемы, упомянутые выше: экземпляры не будут иметь общих свойств, и вы также можете передавать параметры родительскому типу, однако с этим методом все еще есть некоторые проблемы: подтипы не могут наследовать свойства в прототипе родителя. тип. Мы только вызываем конструктор супертипа в конструкторе подтипа, и больше ничего не делаем, а прототип подтипа и супертип не имеет никакой связи. Имея это в виду, техника заимствования конструктора также редко используется в одиночку.

3. Композиционное наследование

Вышеупомянутые два метода могут дополнять недостатки друг друга.Объединяя эти два метода, мы можем решить проблему идеально.Это комбинированное наследование. Идея заключается в том, чтобы использовать цепочку прототипов для реализации наследования свойств и методов прототипов, а также для реализации наследования свойств экземпляров путем заимствования конструкторов. Таким образом, повторное использование функций достигается за счет определения методов в прототипе, и можно гарантировать, что каждый экземпляр будет иметь свои собственные свойства, таким образом используя сильные стороны обоих. Посмотрите на простую реализацию:

function Super(properties){
  this.properties = [].concat(properties);
  this.colors = ['red', 'blue', 'green'];
}

Super.prototype.log = function() {
  console.log(this.properties[0]);
}

function Sub(properties){
  // 继承了 Super,传递参数,互不影响
  Super.apply(this, properties);
}
// 继承了父类型的原型
Sub.prototype = new Super();
// isPrototypeOf() 和 instance 能正常使用
Sub.prototype.constructor = Sub;

var instance1 = new Sub(['instance1']);
instance1.colors.push('black');
console.log(instance1.colors); // 'red,blue,green,black'
instance1.log(); // 'instance1'

var instance2 = new Sub();
console.log(instance2.colors); // 'red,blue,green'
instance2.log(); // 'undefined'

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

4. Наследование паразитарного состава

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

// 用于继承的函数
function inheritPrototype(child, parent) {
  var F = function () {}
  F.prototype = parent.prototype;
  child.prototype = new F();
  child.prototype.constructor = child;
}
// 父类型
function Super(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function () {
  console.log(this.name);
};
// 子类型
function Sub(name, age) {
  // 继承基本属性和方法
  SuperType.call(this, name);
  this.age = age;
}

// 继承原型上的属性和方法
inheritPrototype(Sub, Spuer);

Sub.prototype.log = function () {
  console.log(this.age);
};

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

Ссылаться на

  1. Продвинутое программирование на JavaScript
  2. Идея дизайна механизма наследования Javascript