Прототипы

Сага. Часть I

Гоголев Сергей

ВНИМАНИЕ

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

Студент


var student = {
    type: 'human',
    name: 'Daria',
    getName: function () {
        console.log(this.name);
    },
    sleep: function () {
        console.log('zzZZZ ...');
    }
}

student.getName();
// Daria

Преподаватель


var lecturer = {
    type: 'human',
    name: 'Sergey',
    getName: function () {
        console.log(this.name);
    }
    talk: function () {}
}

Объекты студента и предподавателя похожи

var student = {
    type: 'human',
    name: 'Daria',
    getName: function() {},
    sleep: function() {}
}
var lecturer = {
    type: 'human',
    name: 'Sergey',
    getName: function() {}
    talk: function() {}
}

Проблема: дублирование кода

Решение: выделить общие части

Личность


var person = {
    type: 'human',
    name: '?',
    getName: function () {}
}

И так – три, не связанных объекта:


var student = {
    name: 'Daria',
    sleep: function () {}
}

var lecturer = {
    name: 'Sergey',
    talk: function () {}
}

var person = {
    type: 'human',
    getName: function () {}
}

Задача: научить student пользоваться общим кодом, который вынесли в person

Заимствование метода


person.getName.call(student);
// Daria

А хотелось бы как раньше ...


student.getName();
// Daria

Как мы можем связать объекты student и person, чтобы это было возможным?

Для этого у каждого объекта в JS


var student = {
    name: 'Daria',
    sleep: function () {}
}

Есть скрытое поле [[Prototype]]


var student = {
    name: 'Daria',
    sleep: function () {},
    [[Prototype]]: <ссылка на объект>
}

Мы можем связать два объекта, записав в [[Prototype]] одного объекта student ссылку на другой person

Обратиться напрямую нельзя


student['[[Prototype]]'] = person;

// Так не работает

Но можно через специальный setter/getter __proto__


student.__proto__ = person;

var student = {
    name: 'Daria',
    sleep: function () {},
    [[Prototype]]: <ссылка на person>
}

Итак, связали два объекта


var student = {
    name: 'Daria',
    sleep: function () {},
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

Объект, на который указывает ссылка [[Prototype]], называется прототипом.

«person послужил прототипом для student»

Обращение к полю объекта


var student = {
    name: 'Daria',
    sleep: function () {},
    [[Prototype]]: <person>
}

console.log(student.name); // Поле есть в объекте
// Daria

console.log(student.type); // Поля нет в объекте
// ???

Поиск поля в прототипе


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

Если поля у объекта нет, то интерпретатор будет искать его в прототипе


console.log(student.type);

// human

Поиск метода в прототипе

var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {
     console.log(this.name);
    }
}

student.getName(); // Метод, которого нет у объекта

// Daria

this при этом будет ссылаться на student

Мы решили нашу задачу


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}

var lecturer = {
    name: 'Sergey',
    [[Prototype]]: <person>
}

var person = {
    getName: function () {}
}

student.getName();
// Daria
lecturer.getName();
// Sergey

Но когда поиск остановится?

Но когда поиск остановится?

Интепретатор будет идти по цепочке прототипов
в поиске поля или метода, пока не встретит null
в поле [[Prototype]]


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    [[Prototype]]: null //?
}

Null ??? Не совсем так!

Object.prototype


var person = {
    type: 'human',
    getName: function () {},
    [[Prototype]]: <Object.prototype>
}

Object.prototype – объект, который содержит самые общие методы для всех объектов

Object.prototype.toString()


Object.prototype = {
    toString: function () {
        // Сильная магия
    }
}

student.toString();
// [object Object]

console.log('Hello, ' + student);
// Hello, [object Object]

// :(

Object.prototype.toString()


var student = {
    name: 'Daria'
    toString: function () {
        return this.name;
    }
}

student.toString();
// Daria

console.log('Hello, ' + student);
// Hello, Daria

// :)

Object.prototype.valueOf()


Object.prototype = {
    toString: function () {},
    valueOf: function () {
        // Пытается преобразовать в примитив
    }
}

var width = { value: 30, units: 'px' }

width.valueOf();
// {value: 30, units: "px"}

width < 45
// false

Object.prototype.valueOf()


var width = {
    value: 30,
    units: 'px',
    valueOf: function () { return this.value; }
}

width.valueOf();
// 30

width < 45
// true

// :)

Object.prototype.hasOwnProperty()


Object.prototype = {
    toString: function () {},
    valueOf: function () {},
    hasOwnProperty: function () {}
}

hasOwnProperty - определяет, принадлежит ли поле объекту лично (а не его прототипу)

Object.prototype.hasOwnProperty()


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

student.hasOwnProperty('name');
// true

student.hasOwnProperty('type');
// false

Object.prototype.hasOwnProperty()


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

Задача перечислить поля объекта student


for (var key in student) console.log(key);

// 'name', 'type', 'getName'

// :(

*Оператор in проверяет наличие свойства не только у объекта, но и в цепочке прототипов

Object.prototype.hasOwnProperty()


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

for (var key in student)
    if (student.hasOwnProperty(key))
       console.log(key);

// 'name'

// :/

Object.keys()


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

// Получаем массив ключей
var keys = Object.keys(student);

console.log(keys);

// 'name'

// :)

Но когда поиск остановится?


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    [[Prototype]]: <Object.prototype>
}

Object.prototype = {
    hasOwnProperty: function () {},
    [[Prototype]]: null
}

Таким образом поиск всегда остановится?


var lecturer = { name: 'Sergey'}
var student = { name: 'Daria' }

lecturer.__proto__ = student;
student.__proto__ = lecturer;

console.log(lecturer.abrakadabra);

Uncaught TypeError: Cyclic __proto__ value

Неужели нет способа по удобнее?


student.__proto__ = person;

Object.setPrototypeOf()

Цивилизованный способ связывать объекты


var student = {
    name: 'Daria',
    sleep: function () {}
}
var person = {
    type: 'human',
    getName: function () {}
}

Object.setPrototypeOf(student, person);

student.getName();
// Daira

Object.setPrototypeOf()


var student = {
    name: 'Daria',
    sleep: function () {}
}
var person = {
    type: 'human',
    getName: function () {}
}

Object.setPrototypeOf(student, 42);

TypeError: Object prototype may only be an Object or null

Object.getPrototypeOf()


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

Object.getPrototypeOf(student) === person;
// true

Object.getPrototypeOf(person) === Object.prototype;
// true

Object.getPrototypeOf(Object.prototype) === null;
// true

Итак, что мы знаем про объекты?

  1. В JS всё является объектом
  2. Их можно связывать через [[Prototype]]
  3. Объекты по умолчанию связаны с Object.prototype

Прототип у массивов?


var fruits = ['Apple', 'Banana', 'Potato'];

Object.getPrototypeOf(fruits);
// Array.prototype;

Object.getPrototypeOf(Array.prototype)
// Object.prototype;

Array.prototype.*


Array.prototype = {
    concat: function () {},
    slice: function () {},
    splice: function () {},
    forEach: function () {},
    filter: function () {},
    map: function () {},
    [[Prototype]]: <Object.prototype>
}

Array.prototype.*


var fruits = ['Apple', 'Banana', 'Potato'];

fruits.slice(1);
// ['Banana', 'Potato']

Прототип у функций?


function kawabanga () {
    console.log('Kawabanga!')
}

Object.getPrototypeOf(kawabanga);
// Function.prototype;

Function.prototype.*


Object.getPrototypeOf(kawabanga);
// Function.prototype;

Function.prototype = {
    call: function () {},
    apply: function () {},
    bind: function () {},
    [[Prototype]]: <Object.prototype>
}

Function.prototype.*


function kawabanga () {
    console.log('Kawabanga!')
}

kawabanga.call(null);
// Kawabanga!

Поведение полей объекта с учётом прототипов

Установка полей объекта


var student = {
    name: 'Daria',
    sleep: function () {}
}

console.log(student.name);
// Daira

student.name = 'Anna';

console.log(student.name);
// Anna

Установка не своих полей объекта


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

console.log(student.type); // human

student.type = 'pirat';

console.log(student.type); // pirat

console.log(person.type); // ???

Установка не своих полей объекта


student.type = 'pirat';

console.log(person.type); // ???

console.log(person.type); // 'human'

var student = {
    name: 'Daria',
    type: 'pirat',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

Такой эффект называется
затенением свойств

Свойства полей

writable – помечает поле как изменяемое (true, по умолчанию)

setter/getter - переопределение установки/чтения поля

enumerable – помечает поле как перечисляемое (true, по умолчанию)

Свойства полей

writable – помечает поле как изменяемое (true, по умолчанию)

Read-only поля


var student = { name: 'Daria' }

Object.defineProperty(student, 'gender', {
  writable: false,
  value: 'female',
});

console.log(student.gender); // female

student.gender = 'robot';
console.log(student.gender); // ???

// female

Read-only поля use strict;


'use strict;'
var student = { name: 'Daria' }
Object.defineProperty(student, 'gender', {
  writable: false,
  value: 'female'
});

student.gender = 'robot';

TypeError: Cannot assign to read only property 'gender' of object

Read-only поля в прототипах use strict;


var student = {
    name: 'Daria',
    [[Prototype]]: <person>
}
var person = {
    type: 'human',
    getName: function () {}
}

'use strict;'
Object.defineProperty(person, 'planet', {
  writable: false, value: 'Earth'
});

console.log(student.planet); // Earth

student.planet = 'Mars';

TypeError: Cannot assign to read only property 'planet' of object

Свойства полей

writable – помечает поле как изменяемое (true, по умолчанию)

setter/getter - переопределение установки/чтения поля

enumerable – помечает поле как перечисляемое (true, по умолчанию)

Setter поля


var student = {
    name: 'Daria'
    [[Prototype]]: <person>
}

Object.defineProperty(student, 'age', {
    set: function(age) { this._age = parseInt(age); },
    get: function() { return this._age; }
});

student.age = '20 лет';

console.log(student.age); // 20;

Не setter поля в прототипах

var student = {
    [[Prototype]]: <person>
}

var person = {
    age: null,
    type: 'human'
}

student.age = 20;

var student = {
    age: 20,
    [[Prototype]]: <person>
}
var person = {
    age: null,
    type: 'human'
}

Затенение свойства

Setter поля в прототипах


var student = {
    [[Prototype]]: <person>
}
var person = {
    type: 'human'
}

Object.defineProperty(person, 'age', {
    set: function(age) { this._age = parseInt(age); },
    get: function() { return this._age; }
});

student.age = '20 лет';

console.log(student.age); // 20;

student.hasOwnProperty(age); // false;

Свойства полей

writable – помечает поле как изменяемое (true, по умолчанию)

setter/getter - переопределение установки/чтения поля

enumerable – помечает поле как перечисляемое (true, по умолчанию)

Перечисляемые поля


var student = { name: 'Daria', age: 20 }

for (var key in student) console.log(key);

// name, age

Неперечисляемые поля


var student = { name: 'Daria' }

Object.defineProperty(student, 'age', {
  enumerable: false,
  value: '20'
});

for (var key in student) console.log(key);

// name

Object.keys(student);
// name

Неперечисляемые поля в прототипах


var student = {
    name: 'Daria'
    [[Prototype]]: <person>
}
var person = {
    type: 'human'
}


Object.defineProperty(person, 'age', {
    enumerable: false
});

for (var key in student) console.log(key);

// name, type

Домашнее задание

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

Хрюндель

Метки на гитхабе. Дедлайны

create-after-deadline - решение прислано после первого дедлайна

closed-after-deadline - задача принята уже после второго дедлайна

Метки на гитхабе. Оценка

accepted - задача принята на 1 балл

half-points - задача принята на 1/2 балла

duplicate - решение списано

Условия

Всего будет 10 задач

8 баллов – зачёт автоматом

5 баллов – минус задача/теория на зачёте

3 балла – допуск к зачёту

Домашнее задание

github.com/urfu-2015/javascript-tasks-7


Deadline
17 ноября
02:59:59.999