Инкапсуляция — свойство системы, позволяющее объединить свойства (данные) и методы (поведение), работающие с ними, в классе или объекте с одновременным сокрытием внутренней структуры данных и реализации методов объекта от внешних обращений (от остальной программы): другим объектам доступен только интерфейс объекта, через который осуществляется все взаимодействие с ним.
Цель инкапсуляции:
- безопасная организация иерархической управляемости через сокрытие собственно реализации (чтобы было достаточно простой команды "что делать", без одновременного уточнения "как именно делать").
Инкапсуляция подразумевает сокрытие внутренней структуры данных и реализации методов объекта от остальной программы. Однако при этом другим объектам доступен интерфейс объекта, через который осуществляется всё взаимодействие с ним.
Варианты организации инкапсуляции в JavaScript:
- с помощью функции (замыкание);
- с помощью фабричной функции;
- с помощью модулей:
- на основе функции-конструктора;
- на основе немедленно вызываемой функции (IIFE - Immediately (немедленно) Invoked (вызываемое) Function (функциональное) Expression (выражение));
- на основе блока кода {...};
- с помощью классов.
Если переменная или объект в JavaScript не помещены внутрь какой-либо функции (блока кода), то они становятся глобальными, т.е. свойствами глобального объекта (для браузера это объект window).
Создание глобальных переменных, как правило, нежелательно:
- оно может привести к трудно обнаружимым ошибкам;
- усложняет перенос кода в другие приложения.
Реализация инкапсуляции с помощью функции (замыкания)
Функция в JavaScript создает область видимости, поэтому переменная, определенная исключительно внутри функции, не может быть доступна извне функции или внутри других функций.
Все переменные внутри функции в JavaScript - это свойства объекта LexicalEnvironment, так называемое лексическое окружение. Данный объект является внутренним и к нему нет доступа. Таким образом, оборачивание любого куска кода в функцию эффективно "скроет" любые вложенные определения переменных или функций от внешней области видимости во внутренней области видимости этой функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let func = function () { let x = 10; let y = true; if (y) { let x = 5; console.log(x); // 5 (переменные, объявленные через let, являются блочными) } console.log(x); // 10 (переменные, объявленные через let, являются блочными) }; func(); // доступа к контексту функции снаружи нет console.log(x); // ReferenceError: x is not defined console.log(y); // ReferenceError: y is not defined |
Функция может быть использована в качестве замыкания.
Замыкание функции — это комбинация функции и лексического окружения (LexicalEnvironment), в котором эта функция была определена; замыкание обеспечивает доступ внутренней функции к области видимости (Scope) внешней функции (при этом переменные внутренней функции для внешнего окружения недоступны).
Область видимости функции в JavaScript:
- функция, в которой она определена;
- вся программа, если функция объявлена на верхнем (глобальном) уровне.
Благодаря замыканию, внутренняя функция имеет доступ к контексту внешней (родительской) функции, может изменять его и возвращать значения во внешний код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function createGenerator(prefix) { let index = 0; return function genNewID() { index++; // значение переменной, объявленной во внешней функции, меняется, но вне функции genNewID() оно недоступно (изменить его нельзя) return prefix + index.toString(); }; } let generateNewID = createGenerator("вызов номер: "); console.log(generateNewID()); // вызов номер: 1 console.log(generateNewID()); // вызов номер: 2 console.log(generateNewID()); // вызов номер: 3 console.log(index); // ReferenceError: index is not defined |
Реализация инкапсуляции с помощью фабричной функции
Фабричная функция - это функция, которая возвращает объект. Этот шаблон удобен тем, что не требует дополнительного метода для инициализации объекта (как это обычно делается в функции-конструкторе).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function Count() { let count = 1; return { getCount: function () { return count++; }, setCount: function (value) { count = value; }, resetCount: function () { count = 1; }, }; } let counter = Count(); console.log(counter); // {getCount: ƒ, setCount: ƒ, resetCount: ƒ} (переменная count в объекте скрыта) console.log(counter.count); // undefined console.log(count); // ReferenceError: count is not defined console.log(counter.getCount()); // 1 console.log(counter.getCount()); // 2 counter.setCount(7); console.log(counter.getCount()); // 7 counter.resetCount(); console.log(counter.getCount()); // 1 |
Для полноценной реализации инкапсуляции в фабричной функции необходимо использовать метод Object.freeze() , который "замораживает" возвращаемый функцией объект, т.е. предотвращает добавление новых свойств к объекту, удаление старых свойств из объекта или изменение существующих свойств или значения их атрибутов перечисляемости, настраиваемости и записываемости.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function main(z) { let x = 5 + z; var y = 10; let user = { userName: "Alex", userAge: 30, }; function plusX(z) { return x + z; } return Object.freeze({ x: (x += 1), user, y, z, plusX }); } let newMain = main(4); console.log(newMain); // {x: 10, user: {…}, y: 10, z: 4, plusX: ƒ} console.log(newMain.plusX(3)); // 13 console.log(newMain.plusX); // ƒ plusX(z) // без Object.freeze() метод plusX() может быть изменен newMain.plusX = 5 console.log(newMain.plusX); // 5 |
Реализация инкапсуляции с помощью модуля
С помощью функции-конструктора создается объект, который и будет скрывать внутреннюю реализацию с помощью замыканий:
1 2 3 4 5 6 7 8 9 10 11 12 |
function Human(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; let age = 30; this.birthday = new Date().getFullYear() - age; } let user = new Human("Alex", "NAV"); console.log(user); // Human {firstName: 'Alex', lastName: 'NAV', birthday: 1991} console.log(user.birthday); // 1991 (age доступна внутри функции-конструктора) console.log(user.age); // undefined (недоступна) console.log(age); // ReferenceError: age is not defined |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let foo = (function oneModule() { // внутренняя логика let something = "cool"; let another = [1, 2, 3]; function doSomething() { console.log(something); } function doAnother() { console.log(another.join(" ! ")); } // внешний API return { doSomething: doSomething, doAnother: doAnother }; })(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3 console.log(something); // ReferenceError: something is not defined (внутренняя логика снаружи недоступна) |
В ECMAScript 6 const и let являются блочными или локальными (видны только внутри блока с областью видимости, ограниченной текущим блоком кода), поэтому вы можете использовать просто область блока, ограниченную {...} для реализации шаблона модуля: Приведенный выше код будет работать только в нестрогом режиме (в строгом режиме this вернет не window, а undefind).
Реализация инкапсуляции с помощью классов
Классы являются абстракцией («синтаксическим сахаром») над функциями-конструкторами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Human { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; let age = 30; this.birthday = new Date().getFullYear() - age; } } let user = new Human("Alex", "NAV"); console.log(user); // Human {firstName: 'Alex', lastName: 'NAV', birthday: 1991} (age в созданном экземпляре отсутствует) console.log(user.birthday); // 1991 (age доступна внутри конструктора) console.log(user.age); // undefined (недоступна снаружи) console.log(age); // ReferenceError: age is not defined |
Сравните с функцией-конструктором, возвращающей аналогичный результат:
1 2 3 4 5 6 |
function Human(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; let age = 30; this.birthday = new Date().getFullYear() - age; } |
При создании объектов мы хотим, чтобы одни свойства были открытыми (публичными), а другие закрытыми (частными или приватными).
Существует два способа это сделать:
- использование частных свойств по соглашению (с использованием префикса _ );
- использование частных полей, предоставляемых возможностями JS (с использованием префикса # - экспериментальная возможность).
Публичные и приватные поля - это экспериментальная особенность (stage 3), предложенная комитетом TC39 по стандартам языка Javascript. Поддержка браузерами ограничена, но это нововведение может быть использовано на моменте сборки, используя к примеру Babel. Подробнее...
В JavaScript частные переменные и свойства (которые не должны использоваться снаружи, за пределами класса) обычно обозначаются с помощью нижнего подчеркивания:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Car { constructor(model, fuel) { this._cargo = 300; // по соглашению if (fuel > 60) fuel = 60; this.model = model; this.fuel = fuel; this._weight = this._cargo + fuel; // по соглашению } } let Nissan = new Car("Note", 100); console.log(Nissan); // Car {_cargo: 300, model: 'Note', fuel: 60, _weight: 360} console.log(Nissan._cargo); // 300 (доступно снаружи) |
Переменные _cargo и свойство _weight в действительности не являются частными: они доступны извне.
Как правило, для управления частными свойствами создаются специальные методы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Car { constructor(model, fuel) { this._cargo = 300; // по соглашению if (fuel > 60) fuel = 60; this.model = model; this.fuel = fuel; this._weight = this._cargo + fuel; // по соглашению } getCargo() { return this._cargo; } } let Nissan = new Car("Note", 100); console.log(Nissan); // Car {_cargo: 300, model: 'Note', fuel: 60, _weight: 360} console.log(Nissan._cargo); // 300 (доступно снаружи) console.log(Nissan.getCargo()); // 300 (доступ через специально созданный метод) |
Для ограничения доступа к переменным следует использовать настоящие частные поля с использованием префикса # (экспериментальная возможность).
Классы позволяют создавать частные переменные с помощью префикса "#".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Car { #cargo = 300; // частные переменные с префиксом # должны определяться вне конструктора #weight; constructor(model, fuel) { if (fuel > 60) fuel = 60; this.model = model; this.fuel = fuel; this.#weight = this.#cargo + fuel; } getCargo() { return this.#cargo; } } let Nissan = new Car("Note", 100); console.log(Nissan); // Car {model: 'Note', fuel: 60, #cargo: 300, #weight: 360} console.log(Nissan.getCargo()); // 300 console.log(Nissan.#cargo); // SyntaxError: Private field '#cargo' must be declared in an enclosing class |
Частные переменные с префиксом # должны определяться вне конструктора.
Для управления частными полями нужны соответствующие методы, например, геттеры и сеттеры.