Меню

Классы в Java Script

08.05.2018 - java script, ЯП
Классы в Java Script

Classes in JS

В JS всё является объектами.
Объекты по своей сути это несортированная коллекция (ключ -> значение).
Объект является экземпляром класса.
В js выделяют три типа классов:
* object literals (not really classes)
* function (more like classes but still not)
* ES2015 classes

Object literals

По своей сути — функция, которая возвращает некоторый объект (это не класс)

function legoMan(name) {
    return {
        name: name,
        say: function(message) {
            console.log(`${this.name} message`);
    }
}}
var alex = legoMan('Alex');
alex.sayy("Hello, Kattie!"); //Alex: "Hello, Kattie!"
var kattie = legoMan("Kattie");
kattie.say("No"); //Kattie: "No"

Bad Functions

Обычная функция, отличие только обращение к контексту this

function LegoMan(name) {
    this.name = name;
    this.say = function(message) {
        console.log(`${this.name} : message`);
    }
}

Но когда мы вызываем эту функцию вместе с new, то JS вызывает её в качестве конструктора. Создавая и возвращая новый объект. Привязывая прототип.

var alex = new LegoMan('Alex');
alex.say("Hello, Kattie!");
var kattie = new LegoMan("Kattie");
kattie.say("No");

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

Good functions

Выносим методы в «общее место» — prototype;

function LegoMan(name) {
    this.name = name;
}
LegoMan.prototype.say = function(message) {
    console.log(this.name + ': "' + message + '"');
}

Prototype

prototype

[[Prototype]]

Наследование в JS

Синие — функции, Красные — объекты

Создавая любую функцию __proto__ будет ссылаться на Function.prototype

Object.__proto__ === Function.prototype; //true
Object.__proto__ === Function.__proto__; //true
Function.__proto__ === Function.prototype; //true
Object.__proto__ !== Object.prototype; //true
Object.prototype !== Function.prototype; //true
Object.prototype !== Function.__proto__; //true

Свойство Prototype есть только у функций.

function LegoMan(name) {
    this.name = name;
}
LegoMan.prototype.say = function(message) {
    console.log(this.name + ': "' + message + '"');
}
var alex = new LegoMan("Alex");

Оператор new создаст новый объект, создаст пустой объект this и создает __proto__ со ссылкой на прототип конструктора.

ES2015 Classes

class LegoMan {
    constructor(name) {
    this.name = name;
}
say(message) {
    console.log(`${this.name} message`);
}
}

const alex = new LegoMan('Alex');
alex.say("Hello, Kattie!");

Сеттеры и Геттеры ES2015 Classes

В классах добавили, так называемые сеттеры и геттеры

class LegoMan {
    constructor(name) {
        this.name = name;
        this.age = 0;
    }
    set newAge(value) {
        this.age = value;
    }
    get represent() {
        return `My name is ${this.name}. I am ${this.age} years old.`;
    }
}

Т.е. по сути это проперти — не функции, в которые можно записывать и с которых можно считывать.

const alex = new LegoMan('Alex');
alex.represent;
alex.newAge = 18;
alex.represent;

Также были добавлены статические методы:

class LegoMan {
    constructor(name) {
        this.name = name;       
    }
    static getInfo(man) {
        return `This is ${LegoMan.name}.`;
    }
}

В этих методах нет ссылки на instance (на экземпляры класса)

const alex = new LegoMan('Alex');
LegoMan.getInfo(alex); // This is Alex
alex.getInfo; // undefined

TYPEOF

typeof 132 // "number"
typeof 2.71 // "number"
typeof 'Alex' // "string"
typeof LegoMan // "function"
typeof true // "boolean"
typeof {} // "object"
typeof NaN // "number"
typeof new Number(132) // "object"
typeof [1,2,3] // "object"
typeof null // "object"

instanceof

Слева всегда объект, справа всегда функция или класс

class LegoMan {
    constructor(name) {
    this.name = name;
}
}
const alex = new LegoMan('Alex');
alex instanceof LegoMane //true
// LegoMan[@@hasInstance](alex)
alex instanceof Object //true

Inheritance (Наследование)

Классическое (C++, C#, Java)

Прототипное (Lua, JS)

Наследование в JS

Crockford way

function extend(Child, Parent) {
    var F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.superclass = Parent.prototype; //optional
}

ES5 Way

function extend(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    Child.superclass = Parent.prototype; //optional
}

function LegoBatMan(name) {
    LegoBatMan.superclass.constructor.call(this, name);
}
extend(LegoBatMan, LegoMan);

Чем Object.create() отличается от {}?

Object.create() создает пустой объект, в качестве proto положит первый параметр, который получит.
А LegoBatman.prototype будет удалён GC, т.к. после того как мы привязали к другому прототипу, на родной прототип больше ничего не ссылается

ES6 Way

class LegoBatMan extends LegoMan {
    constructor(name) {
        super(name);
    }
    say(message) {
        console.log('|\\_/|');
        super.say(message);
        console.log('|\\_/|');
    }
}

Почему Classes это хорошо

Почему Classes плохо

Композиция против Наследование

Наследование — это когда вы проектируете ваши типы вокруг того чем они являются
Композиция — это когды вы проектируете ваши типы вокруг того, что они делают
Композиция:
* более гибкая
* более мощная
* легче
Пример композиции:

const barker = state => ({
    bark : () => console.log(`Woof, I am ${state.name}`);
});

const driver = state => ({
    drive: () => state.position = state.position + state.speed;
});

barker({name: 'karo'}).bark(); //Woof, I am karo

const murderRobotDog = name => {
    let state = {
        name,
        speed: 100,
        position: 0
    }
    return Object.assign(
        {},
        barker(state),
        driver(state),
        killer(state)
    )
}

murderRobotDog('sniffles').bark(); //"Woof, I am sniffles";

Классы (YDKJS)

class Foo {
    constructor(a,b) {
    this.x = a;
    this.y = b;
}
gimmeXY() {
    return this.x * this.y;
    }
 }  

Как это выглядело раньше

function Foo(a,b) {
    this.x = a;
    this.y = b;
}
Foo.prototype.gimmeXY = function() {
    return this.x * this.y;
}

Extends

class Bar extends Foo {
    constructor(a,b,c) {
        super(a,b);
        this.z = c;
    }
    gimmeXYZ() {
        return super.gimmeXY() * this.z;
    }
}
var b = new Bar(5,15,25);
b.x; //5
b.y; //15
b.z; //25
b.gimmeXYZ(); //1875

Имейте в виду, что super динамически не является, и не управляется контекстом. Когда конструктор или метод с его помощью создает внутри себя ссылку во время объявления ()в теле класса), слово super статически связывается с иерархией конкретного класса и не допускает переопределения:
Т.е. если вы хотите взять метод одного «класса» и заимствовать для другого класса, переопределяя ключевое слово this, например с помощью методов call() или apply(), то при наличии в заимствуемом методе ключевого слова super вы можете натолкнуться с сюрпризами:

class ParentA {
    constructor(){this.id = "a"}
    foo(){console.log("ParentA:", this.id);}
}
class ParentB {
    constructor() {this.id = "b"}
    foo() {console.log("ParentB:", this.id);}
}
class ChildA extends ParentA {
    foo() {
        super.foo();
        console.log("ChildA:", this.id);
    }
}
class ChildB extends ParentB {
    foo(){
        super.foo();
        console.log("ChildB:", this.id);
    }
}
var a = new ChildA();
a.foo();
var b = new ChildB();
b.foo();
b.foo.call(a); 
//ParentB: a
//ChildB: a

Как мы увидим в последнем случае, ссылка super.foo() функции b.foo() динамически не поменялась. Поэтому мы увидели ParentB вместо ParentA
Здесь нужно сузить процесс проектирования объектов до статических иерархий — ключевые слова class, extends и super в этом случае будут прекрасно работать. Еще один вариант — отказаться от имитации классов и использовать динамические гибкие бесклассовые объекты и делегирование [[Prototype]].

Конструктор подкласса

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

constructor(...args) {
    super(...args);
}

Внимание!

Доступ к ключевому слову this в таком конструкторе появляется только после вызова метода super(..). Именно родительский конструктор создает и инициализирует значение this для экземпляра.
Для иллюстрации

function Foo(){
    this.a = 1;
}
function Bar() {
    this.b = 2;
    Foo.call(this);
}
//Bar расширяет Foo
Bar.prototype = Object.create(Foo.prototype);

Недопустимость такого в ES6

class Foo {
    constructor() {this.a = 1}
}
class Bar extend Foo {
    constructor() {
        this.b = 2; //недопустио до вызова `super()`
        super(); //для исправления нужно поменять местами эти два оператора
    }
}

Расширение встроенных объектов

class MyCoolArray extends Array {
    first() {return this[0];}
    last() {return this[this.length - 1];}
}

var a = new MyCoolArray(1,2,3);
a.length; //3
a; //[1,2,3]
a.first(); //1
a.last(); //3

Подкласс на ошибки (Error)

class Oops extends Error {
    constructor(reason) {
        this.oops = reason;
    }
}
//Позднее;
var ouch = new Oops("I messed up!");
throw ouch;

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

Свойство new.target

Новая концепция в ES6 — метасвойства. Имеет форму new.target.
Оно представляет из себя значение, доступное во всех функциях (в обычных оно всегда undefined). В любом конструкторе new.target всегда будет указывать на конструктор, который непосредственно вызвал оператор new, даже если тот располагается в параллельно классе и был делегирован через вызов super() из дочернего конструктора.
Пример:

class Foo {
    constructor() {
        console.log("Foo: ", new.target.name);
  }
}
class Bar extends Foo {
    constructor() {
        super();
        console.log("Bar: ", new.target.name);
    }
    baz() {
        console.log("baz: ", new.target);
    }
}
var a = new Foo(); //Foo: Foo
var b = new Bar();
//Foo: Bar <-- учитывает сторону, вызвавшую 'new'
//Bar: Bar

b.baz(); //baz: undefined

Метасвойство new.target в конструкторах классов не имеет особого назначения, кроме обеспечения доступа к свойству/методу static.
Если new.target равняется undefined — Значит функция с помощью new не вызывалась.

Static

Чтобы добавить статические методы непосредственно в объект-функцию, а не в объект-прототип, используются статические методы

class Foo {
    static cool(){console.log("cool");}
    wow() {console.log("wow");}
}
class Bar extends Foo {
    static awesome() {
        super.cool();
        console.log("awesome");
    }
    neat() {
    super.wow();
    console.log("neat");
    }
}
Foo.cool(); //cool
Bar.cool(); //cool
Bar.awesome(); //cool
var b = new Bar();
b.neat();// wow neat
b.awesome(); //undefined
b.cool(); //undefined

Symbol.species
Это свойство позволяет дочернему классу передать в родительский информацию о том, каким конструктором следует пользоваться, в том случае когда вы не хотите использовать конструктор самого дочернего класса. Когда какой-либо метод родительского класса должен породить новый экземпляр.
Например если мы решим расширить класс массива, но хотим, чтобы, например, в результате метода map возвращались экземляры родительского класса, а не дочернего, то

class MyCoolArray extends Array {
    //Принудительно возвращаем `species` в родительский конструктор
    static get [Symbol.species]() {return Array;}
}
var a = new MyCoolArray(1,2,3),
    b = a.map(v => v * 2);
b instanceof MyCoolArray; //false
b instanceof Array; //true

Как правильно наследоваться в современном JS

Возьмем пример:

function Parent(name) {
    this.name = name || 'Eugene';
}
Parent.prototype.say = function() {
    return this.name;
}
function Child(name) {
    Parent.apply(this, arguments);
}

let kid = new Child("Patrik");
console.log(kid.name); //Patrik
typeof kid.say; //undefined

Как это было в ES3

Сохранение суперкласса с указателем на конструктор

function inherit (Child, Parent) {
    let F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;
    Child.prototype.constructor = Child;
}

Ну и такой способ, чтобы не создавать каждый раз временную функцию, можно использовать ее в замыкании.
Лучший вариант из старых:

let inherit = (function() {
    let F = function(){};
    return function(Child, Parent) {
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.uber = Parent.prototype;
        Child.constructor = Child;
    }
}());

Как это было в ES5

Этот метод принимает второй параметр — объект, свойства которого будут добавлены во вновь созданный объект, как собственные свойства.
Если простой объект.

let child = Object.create(parent, {
    age : {value : 2}
});
child.hasOwnProperty("age"); //true

Если класс

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Как это в ES6

Object.setPrototypeOf(Child.prototype, Parent.prototype);

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *