Интервьюер: Расскажите о цепочке прототипов и наследовании.

JavaScript
Интервьюер: Расскажите о цепочке прототипов и наследовании.

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

прототип

когда мыnewДоступ к новому экземпляру объекта можно получить напрямую, ничего не делаяtoString,valueOfи другие нативные методы. Так откуда же взялись эти методы? Ответ — прототипы.

Когда консоль выводит пустой объект, мы видим, что есть много методов, которые были «инициализированы», смонтированными на встроенном__proto__объект включен. это встроенный__proto__— это указатель на объект-прототип, который автоматически создается (явно или неявно) при создании нового объекта ссылочного типа и монтируется в новый экземпляр. Когда мы пытаемся получить доступ к свойству/методу объекта экземпляра, если есть свойство/метод объекта экземпляра, вернуть свойство/метод экземпляра, если нет, перейти к__proto__Найдите соответствующее свойство/метод в указанном объекте-прототипе. Вот почему мы пытаемся получить доступ к пустому объектуtoStringа такжеvalueOfПричина, по которой другие методы все еще могут быть доступны,JavaScriptФормально наследование осуществляется таким образом.

Конструктор

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

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

// 普通函数
function person () {}

// 构造函数,函数首字母通常大写
function Person () {}
const person = new Person();

Объект-прототип создается при объявлении конструктора. При объявлении конструктора также создается объект-прототип, который затем монтируется в объект конструктора.prototypeНа территории:

При создании объекта-прототипаconstructorсвойство, указывающее на конструктор, который его создал. Таким образом, отношения между ними тесно связаны.

Если вы будете внимательны, то можете обнаружить, что у объекта-прототипа также есть свои собственные__proto__, это и не удивительно, ведь все есть объект. __proto__ объекта-прототипа указывает наObject.prototype. ТакObject.prototype.__proto__Он существует? На самом деле его не существует.Если вы его распечатаете, то обнаружите, что этоnull. Это также доказываетObjectдаJavaScriptПроисхождение типа данных в .

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

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

После разговора о прототипе мы можем поговорить о цепочке прототипов.Если вы понимаете механизм прототипа, цепочку прототипов можно легко объяснить. На самом деле, на картинке выше__proto__связанные цепные отношения, называемыеСеть прототипов.

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

  • Доступ к атрибутам экземпляра объекта, возвращение, если есть, передача, если нет__proto__Перейдите к объекту-прототипу, чтобы найти его.
  • Если объект-прототип найден, он будет возвращен, если не найден, продолжить поиск по __proto__ объекта-прототипа.
  • слой за слоем был найденObject.prototype, если целевое свойство найдено, то оно будет возвращено, а если не найдено, то будет возвращеноundefined, дальше вниз смотреть не буду, потому что глядя вниз__proto__то естьnull.

Благодаря приведенному выше объяснению для экземпляра, сгенерированного конструктором, мы должны понять его объект-прототип. Все в JavaScript является объектом, поэтому конструктор также должен быть объектом.__proto__, то конструктор__proto__что это такое?

Можем распечатать и посмотреть:

Я только что вспомнил, что все функции можно использоватьnew Function()способ создания, то этот ответ очень естественный, немного интересный, а затем попробуйте другие конструкторы.

Это также доказывает, что все функцииFunctionпример. Подождите минутку, кажется, что-то не так, тогдаFunction.__proto__Не будет ли. . .

Согласно приведенной выше логике, если мы скажем так,FunctionТы не создал себя? На самом деле, мы не должны понимать это так, потому что как встроенный объект JS,FunctionОбъект уже существует до того, как будет сгенерирован ваш скриптовый файл. Где вы можете назвать себя? Эта вещь похожа на «Дао» и «Цянькунь» в метафизике. Можете ли вы объяснить, кто их создал? День рождения, солнце, луна и Гены не рождаются и не уничтожаются. . . Забудь, будет написано как культиватор когда я спущусь.=. знак равно

почемуFunction.__proto__равныйFunction.prototypeЕсть несколько поговорок:

  • Для согласованности с другими функциями
  • Чтобы проиллюстрировать отношения, такие как доказательство того, что все функцииFunctionпример.
  • функции можно назватьcall bindЭти встроенные API-интерфейсы могут быть написаны таким образом, чтобы экземпляры функций могли использовать эти API-интерфейсы.

будь осторожен:

Несколько замечаний о прототипах, цепочках прототипов и конструкторах:

  • __proto__Это нестандартное свойство, если вы хотите получить доступ к прототипу объекта, рекомендуется использовать новый ES6Reflect.getPrototypeOfилиObject.getPrototypeOf()метод, а не напрямуюobj.__proto__, потому что нестандартный атрибут означает, что атрибут может быть изменен или удален непосредственно в будущем. Точно так же при изменении прототипа объекта лучше всего также использоватьES6который предоставилReflect.setPrototypeOfилиObject.setPrototypeOf.
let target = {};
let newProto = {};
Reflect.getPrototypeOf(target) === newProto; // false
Reflect.setPrototypeOf(target, newProto);
Reflect.getPrototypeOf(target) === newProto; // true
  • функция будет иметьprototype,КромеFunction.prototype.bind()за пределами.
  • объекты будут иметь__proto__,КромеObject.prototypeснаружи (на самом деле он тоже существует, но онnull).
  • Все функции создаются из Function, т. е. их__proto__равныFunction.prototype.
  • Function.prototypeравныйFunction.__proto__.

Загрязнение прототипа

Загрязнение прототипа означает, что злоумышленник каким-то образом модифицирует прототип объекта JavaScript.

Что вы имеете в виду, принцип на самом деле очень прост. если мы положимObject.prototype.toStringИзмените его на это:

Object.prototype.toString = function () {alert('原型污染')};
let obj = {};
obj.toString();

Затем, когда мы запустим этот код, в браузере появится всплывающее окно.alert, родной объектtoStringМетод был переписан, и все объекты при вызовеtoStringбудут затронуты.

Вы можете сказать, как кто-то может быть настолько глуп, чтобы написать такой код в исходном коде, разве это не выстрел себе в ногу? Да, в исходниках такое никто бы не написал, но злоумышленник мог пройтиформаилиИзменить содержание запросаи другие способы использования прототипа загрязнения для атаки, рассмотрим следующую ситуацию:

'use strict';
 
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
 
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
 
function merge(a, b) {
    for (var attr in b) {
        if (isObject(a[attr]) && isObject(b[attr])) {
            merge(a[attr], b[attr]);
        } else {
            a[attr] = b[attr];
        }
    }
    return a
}
 
function clone(a) {
    return merge({}, a);
}
 
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
 
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
 
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
    var body = JSON.parse(JSON.stringify(req.body));
    var copybody = clone(body)
    if (copybody.name) {
        res.cookie('name', copybody.name).json({
            "done": "cookie set"
        });
    } else {
        res.json({
            "error": "cookie not set"
        })
    }
});
app.get('/getFlag', (req, res) => {
    var аdmin = JSON.parse(JSON.stringify(req.cookies))
    if (admin.аdmin == 1) {
        res.send("hackim19{}");
    } else {
        res.send("You are not authorized");
    }
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

Если приведенный выше фрагмент кода присутствует на сервере, злоумышленник может просто добавитьcookieустановлен в{__proto__: {admin: 1}}Вторжение в систему может быть завершено.

Прототипы решений по борьбе с загрязнением

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

Код очень прост, пока вы сталкиваетесьconstructorили__proto__Такие деликатные слова сразу выйдут из исполнения. Это, конечно, эффективное средство предотвращения загрязнения прототипа, и, конечно же, у нас есть другие средства:

  1. использоватьObject.create(null), метод создает прототип какnull, так что любое расширение прототипа не вступит в силу:
const obj = Object.create(null);
obj.__proto__ = { hack: '污染原型的属性' };
console.log(obj); // => {}
console.log(obj.hack); // => undefined
  1. использоватьObject.freeze(obj)Заморозить указанный объект, чтобы его свойства нельзя было изменить, сделав его нерасширяемым объектом:

    Object.freeze(Object.prototype);
    
    Object.prototype.toString = 'evil';
    
    console.log(Object.prototype.toString);
    // => ƒ toString() { [native code] }
    
  2. УчреждатьJSON schema, при разборе пользовательского ввода передатьJSON schemaФильтровать конфиденциальные имена ключей.

  3. Избегайте небезопасных рекурсивных слияний. это похоже наlodashМетод восстановления повышает безопасность операции слияния и пропускает обработку имен конфиденциальных ключей.

наследовать

Наконец, мы можем поговорить о наследовании.Давайте сначала рассмотрим концепцию наследования и посмотрим, что говорит Baidu:

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

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

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

Это неловко,JavaScriptОткуда берется класс, есть только объекты. Означает ли это, что чистое наследование невозможно? Отсюда вступительная фраза:Вместо наследования термин делегирование является более точным.

ноJavaScriptОна очень гибкая, и хотя эта особенность гибкости приносит ей много недостатков, она также создает много замечательных преимуществ. Неважно, что наследование классов не предусмотрено изначально, мы можем сделать это более разнообразным способом.JavaScriptНаследование в , например, использованиеObject.assign:

let person = { name: null, age: null };
let man = Object.assign({}, person, { name: 'John', age: 23 });
console.log(man);  // => { name: 'John', age: 23 }

использоватьcallа такжеapply:

let person = {
    name: null,
    sayName: function () {
        console.log(this.name);
    },
    sayAge: function () {
        console.log(this.age);
    }
};
let man = { name: 'Man', age: 23 };
person.sayName.call(man); // => Man
person.sayAge.apply(man); // => 23

Даже мы можем использовать глубокое копирование объектов для завершения операций, похожее на наследство ...JSСуществует множество способов реализации наследования в , но найти некоторые проблемы несложно, взглянув на приведенный выше код:

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

Есть ли способ решить эти проблемы? мы можем использоватьJavaScriptНаиболее распространенный способ наследования:прототипное наследование

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

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

// 父类
function SuperType (colors = ['red', 'blue', 'green']) {
    this.colors = colors;
}

// 子类
function SubType () {}
// 继承父类
SubType.prototype = new SuperType();
// 以这种方式将 constructor 属性指回 SubType 会改变 constructor 为可遍历属性
SubType.prototype.constructor = SubType;

let superInstance1 = new SuperType(['yellow', 'pink']);
let subInstance1 = new SubType();
let subInstance2 = new SubType();
superInstance1.colors; // => ['yellow', 'pink']
subInstance1.colors; // => ['red', 'blue', 'green']
subInstance2.colors; // => ['red', 'blue', 'green']
subInstance1.colors.push('black');
subInstance1.colors; // => ['red', 'blue', 'green', 'black']
subInstance2.colors; // => ['red', 'blue', 'green', 'black']

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

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

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

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

Объединение наследства с помощьюcallВызов конструктора родительского класса в конструкторе дочернего класса решает две указанные выше проблемы:

// 组合继承实现

function Parent(value) {
    this.value = value;
}

Parent.prototype.getValue = function() {
    console.log(this.value);
}

function Child(value) {
    Parent.call(this, value)
}

Child.prototype = new Parent();

const child = new Child(1)
child.getValue();
child instanceof Parent;

Однако у него все еще есть проблемы: конструктор родительского класса вызывается дважды (один раз при создании прототипа подкласса и один раз при создании экземпляра подкласса), что приводит к существованию атрибутов экземпляра родительского класса в прототипе подкласса, расточительному ОЗУ.

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

Ввиду недостатков комбинированного наследования возникло «паразитическое комбинированное наследование»: использованиеObject.create(Parent.prototype)Создайте новый объект-прототип и назначьте его подклассу, чтобы устранить недостатки наследования композиции:

// 寄生组合继承实现

function Parent(value) {
    this.value = value;
}

Parent.prototype.getValue = function() {
    console.log(this.value);
}

function Child(value) {
    Parent.call(this, value)
}

Child.prototype = Object.create(Parent.prototype, {
    constructor: {
        value: Child,
        enumerable: false, // 不可枚举该属性
        writable: true, // 可改写该属性
        configurable: true // 可用 delete 删除该属性
    }
})

const child = new Child(1)
child.getValue();
child instanceof Parent;

Модель паразитарного комбинированного наследования в настоящее время признана в отрасли относительно надежной.JSсхема наследования,ES6изclassунаследовано вbabelПосле экранирования нижний слой также реализуется путем наследования паразитных комбинаций.

Решение о наследственных отношениях

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

instanceof

мы можем использоватьinstanceofопределить, существуют ли между ними отношения наследования.instanceofБуквальное значение: является ли xx экземпляром xxx. верни, если даtrueв противном случае вернутьсяfalse:

function Parent () {}
function Child () {}
Child.prototype = new Parent();
let parent = new Parent();
let child = new Child();

parent instanceof Parent; // => true
child instanceof Child; // => true
child instanceof Parent; // => true
child instanceof Object; // => true

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

function _instanceof (obj, Constructor) {
    if (typeof obj !== 'object' || obj == null) return false;
    let construProto = Constructor.prototype;
    let objProto = obj.__proto__;
    while (objProto != null) {
        if (objProto === construProto) return true;
        objProto = objProto.__proto__;
    }
    return false;
}

Object.prototype.isPrototypeOf(obj)

так же доступноObject.prototype.isPrototypeOfЧтобы косвенно определить отношения наследования, этот метод используется для определения того, существует ли объект в цепочке прототипов другого объекта:

function Foo() {}
function Bar() {}
function Baz() {}

Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);

var baz = new Baz();

console.log(Baz.prototype.isPrototypeOf(baz)); // true
console.log(Bar.prototype.isPrototypeOf(baz)); // true
console.log(Foo.prototype.isPrototypeOf(baz)); // true
console.log(Object.prototype.isPrototypeOf(baz)); // true

Эта статья была включена вСтолбец «Руководство по фронтенд-интервью»

Связанные ссылки

Рекомендуемый прошлый контент

  1. Тщательно разбирайтесь в троттлинге и анти-тряске
  2. [Основные] Принципы и применение протоколов HTTP и TCP/IP
  3. [Combat] webpack4 + ejs + express перенесет вас в многостраничную архитектуру проекта приложения
  4. Цикл событий в браузере
  5. Интервьюер: Расскажите мне о контексте казни.
  6. Интервьюер: Расскажите о масштабе и закрытиях.
  7. Интервьюер: Расскажите о модульности в JS.
  8. Интервьюер: Расскажите о модульности в JS.