Функциональное и объектное программирование в JavaScript
Выбери формат для чтения
Загружаем конспект в формате pdf
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Лекция 10. Функциональное и объектное программирование в JavaScript
При первоначальном знакомстве с лекцией можно прочитать только пп. 10.1-10.3,
10.10-10.13.
10.1. Функции
Функции представляют собой набор инструкций, выполняющих определенное
действие или вычисляющих определенное значение.
Синтаксис определения функции:
function имя_функции ([параметр [, ...]]){
// Инструкции
}
Определение функции начинается с ключевого слова function, после которого
следует имя функции. Наименование функции подчиняется тем же правилам, что и
наименование переменной: оно может содержать только цифры, буквы, символы
подчеркивания и доллара ($) и должно начинаться с буквы, символа подчеркивания
или доллара.
После имени функции в скобках идет перечисление параметров (аргументов). Даже
если параметров у функции нет, то пустые скобки всё равно указываются. Затем в
фигурных скобках следует тело функции, содержащее набор инструкций.
Определим простейшую функцию:
function display() {
document.write("функция в JavaScript");
}
Данная функция называется display(). Она не принимает никаких параметров и
все, что она делает, это пишет на web-страницу строку.
Однако простого определения функции еще недостаточно, чтобы она заработала. Её
нужно ещё вызвать:
Необязательно давать функциям определенное имя. Можно использовать анонимные
функции:
var display = function(){ // определение функции
document.write("функция в JavaScript");
}
display();
Фактически мы определяем переменную display и присваиваем ей ссылку на
функцию. А затем по имени переменной функция вызывается.
Также мы можем динамически присваивать функции для переменной:
function goodMorning() {
document.write("Доброе утро");
}
function goodEvening() {
document.write("Добрый вечер");
}
var message = goodMorning;
message(); // Доброе утро
message = goodEvening;
message(); // Добрый вечер
Рассмотрим передачу параметров:
function display(x) { // определение функции
var z = x * x;
document.write(x + " в квадрате равно " + z);
}
display(5); // вызов функции
Функция display принимает один параметр - x. Поэтому при вызове функции мы
можем передать для него значение, например, число 5, как в данном случае.
Если функция принимает несколько параметров, то с помощью spread-оператора ...
(многоточие) мы можем передать набор значений для этих параметров из массива:
function sum(a, b, c) {
let d = a + b + c;
console.log(d);
}
sum(1, 2, 3);
let nums = [4, 5, 6];
sum(...nums);
Во втором случае в функцию передается числа из массива nums. Но чтобы
передавался не просто массив, как одно значение, а именно числа из этого массива,
применяется spread-оператор ... (многоточие).
Функция может принимать множество параметров, но при этом часть или все
параметры могут быть необязательными. Если для параметров не передается значение,
то по умолчанию они имеют значение "undefined".
function display(x, y) {
if (y === undefined) y = 5;
if (x === undefined) x = 8;
let z = x * y;
console.log(z);
}
display(); // 40
display(6); // 30
display(6, 4) // 24
Здесь функция display принимает два параметра. При вызове функции мы можем
проверить их значения. При этом, вызывая функцию, необязательно передавать для
этих параметров значения. Для проверки наличия значения параметров используется
сравнение со значением undefined.
Есть и другой способ определения значения для параметров по умолчанию:
function display(x = 5, y = 10) {
let z = x * y;
console.log(z);
}
display();
// 50
display(6);
// 60
display(6, 4)
// 24
Если параметрам x и y не передаются значения, то они получаются в качестве
значений числа 5 и 10 соответственно. Такой способ более лаконичен и интуитивен,
чем сравнение с undefined.
При этом значение параметра по умолчанию может быть производным, то есть,
представлять собой выражение:
function display (x = 5, y = 10 + x) {
let z = x * y;
console.log(z);
}
display();
// 75
display(6);
// 96
display(6, 4)
// 24
В данном случае значение параметра y зависит от значения x.
При необходимости мы можем получить все переданные параметры через глобально
доступный массив arguments:
function display (){
var z = 1;
for (var i=0; i
Здесь переменные x и d являются глобальными. Они доступны из любого места
программы.
А вот переменная z глобальной не является, так как она определена внутри функции.
Переменная, определенная внутри функции, является локальной:
function displaySquare() {
var z = 10;
console.log(z);
let b = 8;
console.log(b);
}
Переменные z и b являются локальными, они существуют только в пределах функции.
Вне функции их нельзя использовать:
function displaySquare() {
var z = 10;
console.log(z);
}
console.log(z); //ошибка, так как z не определена
Когда функция заканчивает свою работу, то все переменные, определенные в
функции, уничтожаются.
Представим, что у нас есть две переменных - одна глобальная, а другая локальная,
которые имеют одинаковое имя:
var z = 89;
function displaySquare() {
var z = 10;
console.log(z); // 10
}
displaySquare(); // 10
В этом случае в функции будет использоваться та переменная z, которая определена
непосредственно в функции. То есть локальная переменная скроет глобальную.
При использовании оператора let каждый блок кода определяет новую область
видимости, в которой существует переменная. Например, мы можем одновременно
определить переменную на уровне блока и на уровне функции:
let z = 10;
function displayZ() {
let z = 20;
{
let z = 30;
console.log("Block:", z);
}
console.log("Function:", z);
}
displayZ();
console.log("Global:", z);
Здесь внутри функции displayZ определен блок кода, в котором определена
переменная z. Она скрывает глобальную переменную и переменную z, определенную
на уровне функции. В реальной программе блок мог быть предеставлять вложенную
функцию, блок цикла for или конструкции if. Но в любом случае такой блок
определяет новую область видимости, вне которого переменная не существует.
И в данном случае мы получим следующий консольный вывод:
Block: 30 Function: 20 Global: 10
С помощью оператора var мы не можем определить одновременно переменную с
одним и тем же именем и в функции, и в блоке кода в этой функции:
function displaySquare(){
var z = 20;
{
var z = 30; // Ошибка ! Переменная z уже определена
console.log("Block:", z);
}
console.log("Function:", z);
}
То есть с помощью var мы можем определить переменную с одним именем либо на
уровне функции, либо на уровне блока кода.
Все, что относится к оператору let, относится и к оператору const, который
позволяет определить константы. Блоки кода задают область видимости констант, а
константы, определенные на вложенных блоках кода, скрывают внешние константы с
тем же именем:
const z = 10;
function displayZ() {
const z = 20;
{
const z = 30;
console.log("Block:", z);
// 30
}
console.log("Function:", z);
// 20
}
displayZ();
console.log("Global:", z); // 10
Если мы не используем это ключевое слово при определении переменной в функции,
то такая переменная будет глобальной. Например:
function bar(){
foo = "25";
}
bar();
console.log(foo);
// 25
Несмотря на то, что вне функции bar переменная foo нигде не определяется, тем не
менее она доступна вне функции во внешнем контексте.
Иначе было бы, если бы мы не только присваивали значение переменной, но и
определяли её:
function bar(){
var foo = "25";
}
bar();
console.log(foo);
// ошибка
Определение глобальных переменных в функциях может вести к потенциальным
ошибкам. Чтобы их избежать используется строгий режим или strict mode:
"use strict";
function bar(){
foo = "25";
}
bar();
console.log(foo);
В этом случае мы получим ошибку SyntaxError: Unexpected identifier,
которая говорит о том, что переменная foo не определена.
Установить режим strict mode можно двумя способами:
добавить выражение "use strict" в начало кода JavaScript, тогда strict
mode будет применяться для всего кода
добавить выражение "use strict" в начало тела функции, тогда strict
mode будет применяться только для этой функции
10.3. Замыкания и функции IIFE
Замыкание (closure) представляют собой конструкцию, когда функция, созданная в
одной области видимости, запоминает свое лексическое окружение даже в том случае,
когда она выполняет вне своей области видимости.
Замыкание технически включает три компонента:
внешняя функция, которая определяет некоторую область видимости и в
которой определены некоторые переменные - лексическое окружение
переменные (лексическое окружение), которые определены во внешней
функции
вложенная функция, которая использует эти переменные
function outer() {
// внешняя функция
var n;
// некоторая переменная
return inner() {
// вложенная функция
// действия с переменной n
}
}
Рассмотрим замыкания на простейшем примере:
function outer() {
let x = 5;
function inner() {
x++;
console.log(x);
};
return inner;
}
let fn = outer();
// fn = inner, так как функция outer
возвращает функцию inner
// вызываем внутреннюю функцию inner
fn();
// 6
fn();
// 7
fn();
// 8
Здесь функция outer задает область видимости, в которой определены внутренняя
функция inner и переменная x. Переменная x представляет лексическое окружение
для функции inner. В самой функции inner инкрементируем переменную x и
выводим ее значение на консоль. В конце функция outer возвращает функцию
inner.
Далее вызываем функцию outer:
let fn = outer();
Поскольку функция outer возвращает функцию inner, то переменная fn будет
хранить ссылку на функцию inner. При этом эта функция запомнила свое окружение
- то есть внешнюю переменную x.
Далее мы фактически три раза вызываем функцию inner, и видим, что переменная x,
которая определена вне функции inner, инкрементируется:
fn();
// 6
fn();
// 7
fn();
// 8
То есть несмотря на то, что переменная x определена вне функции inner, эта
функция запомнила свое окружение и может его использовать, хотя она вызывается
вне функции outer, в которой была определена. В этом суть замыканий.
Рассмотрим еще один пример:
function multiply(n) {
var x = n;
return function(m){ return x * m;};
}
var fn1 = multiply(5);
var result1 = fn1(6); // 30
console.log(result1); // 30
var fn2= multiply(4);
var result2 = fn2(6); // 24
console.log(result2); // 24
Итак, здесь вызов функции multiply() приводит к вызову другой внутренней
функции. Внутренняя же функция:
function(m) { return x * m; };
запоминает окружение, в котором она была создана, в частности, значение переменной
x.
В итоге при вызове функции multiply определяется переменная fn1, которая и
представляет собой замыкание, то есть объединяет две вещи: функцию и окружение, в
котором функция была создана. Окружение состоит из любой локальной переменной,
которая была в области действия функции multiply во время создания замыкания.
То есть fn1 — это замыкание, которое содержит и внутреннюю функцию
function(m) { return x * m;}, и переменную x, которая существовала во
время создания замыкания.
При создании двух замыканий: fn1 и fn2, для каждого из этих замыканий создается
свое окружение.
При этом важно не запутаться в параметрах. При определении замыкания:
var fn1 = multiply(5);
число 5 передается для параметра n функции multiply.
При вызове внутренней функции:
var result1 = fn1(6);
число 6 передается для параметра m во внутреннюю функцию
function(m) { return x * m;};.
Также мы можем использовать другой вариант для вызова замыкания:
function multiply(n) {
var x = n;
return function(m){ return x * m;};
}
var result = multiply(5)(6); // 30
console.log(result);
Обычно определение функции отделяется от ее вызова: сначала мы определяем
функцию, а потом вызываем. Но это необязательно. Мы также можем создать такие
функции, которые будут вызываться сразу при определении. Такие функции еще
называют Immediately Invoked Function Expression (IIFE) или "немедленно
вызываемые".
(function() {
console.log("Привет мир");
}());
(function (n) {
var result = 1;
for(var i=1; i<=n; i++)
result *=i;
console.log("Факториал числа " + n + " равен " + result);
})(4);
Подобные функции заключаются в скобки, и после определения функции в скобках
выполняется передача параметров.
10.4. Простейший модуль
Паттерн "модуль" базируется на замыканиях и состоит из двух компонентов: внешняя
функция, которая определяет лексическое окружение, и возвращаемый набор
внутренних функций, которые имеют доступ к этому окружению.
Определим простейший модуль:
let foo = (function() {
let obj = { greeting: "hello" };
return {
display: function() {
console.log(obj.greeting);
}
}
})();
foo.display(); // hello
Здесь определена переменная foo, которая представляет результат анонимной
функции. Внутри подобной функции определен объект obj с некоторыми данными.
Сама анонимная функция возвращает объект, который определяет функцию display.
Возвращаемый объект определяет общедоступый API, через который мы можем
обращаться к данным, определенным внутри модуля.
return {
display: function(){
console.log(obj.greeting);
}
}
Такая конструкция позволяет закрыть некоторый набор данных в рамках функциимодуля и опосредовать доступ к ним через определенный API - возвращаемые
внутренние функции.
Рассмотрим чуть более сложный пример:
let calculator = (function() {
let data = { number: 0};
return {
sum: function(n){
data.number += n;
},
subtract: function(n){
data.number -= n;
},
display: function(){
console.log("Result: ", data.number);
}
}
})();
calculator.sum(10);
calculator.sum(3);
calculator.display();
// Result: 13
calculator.subtract(4);
calculator.display();
// Result: 9
Данный модуль представляет примитивный калькулятор, который выполняет три
операции: сложение, вычитание и вывод результата.
Все данные инкапсулированы в объекте data, который хранит результат операции.
Все операции представлены тремя возвращаемыми функциями: sum, subtract и
display. Через эти функции мы можем управлять результатом калькулятора извне.
10.5. Рекурсивные функции
Среди функций отдельно можно выделить рекурсивные функции. Их суть состоит в
том, что функция вызывает саму себя.
Например, рассмотрим функцию, определяющую факториал числа:
function getFactorial(n) {
if (n === 1) {
return 1;
}
else {
return n * getFactorial(n - 1);
}
}
var result = getFactorial(4);
console.log(result); // 24
Функция getFactorial() возвращает значение 1, если параметр n равен 1, либо
возвращает результат нового вызова функции getFactorial, в которую передается
значение n-1. Например, при передаче числа 4, у нас образуется следующая цепочка
вызовов:
var result = 4 * getFactorial(3);
var result = 4 * 3 * getFactorial(2);
var result = 4 * 3 * 2 * getFactorial(1);
var result = 4 * 3 * 2 * 1; // 24
Рассмотрим другой пример - определение чисел Фибоначчи:
function getFibonachi(n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
else {
return getFibonachi(n - 1) + getFibonachi(n - 2);
}
}
var result = getFibonachi(8); //21
console.log(result); // 21
Анонимную функцию можно вызвать рекурсивно с помощью arguments.callee:
console.log((function(n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return arguments.callee(n - 1) + arguments.callee(n - 2);
}
})(8));
10.6. Переопределение функций
Функции обладают возможностью для переопределения поведения. Переопределение
происходит с помощью присвоения анонимной функции переменной, которая
называется так же, как и переопределяемая функция:
function display() {
console.log("Доброе утро");
display = function(){
console.log("Добрый день");
}
}
display(); // Доброе утро
display(); // Добрый день
При первом срабатывании функции действует основной блок операторов функции, в
частности, в данном случае выводится сообщение "Доброе утро". И при первом
срабатывании функции display также происходит ее переопределение. Поэтому при
всех последующих вызовах функции срабатывает ее переопределенная версия, а на
консоль будет выводиться сообщение "Добрый день".
Но при переопределении функции надо учитывать некоторые нюансы. В частности,
попробуем присвоить ссылку на функцию переменной и через эту переменную
вызвать функцию:
function display() {
console.log("Доброе утро");
display = function(){
console.log("Добрый день");
}
}
// присвоение ссылки на функцию до переопределения
var displayMessage = display;
display(); // Доброе утро
display(); // Добрый день
displayMessage(); // Доброе утро
displayMessage(); // Доброе утро
Здесь переменная displayMessage получает ссылку на функцию display до ее
переопределения. Поэтому при вызове displayMessage() будет вызываться
непереопределенная версия функции display.
Но допустим, мы определили переменную displayMessage уже после вызова
функции display:
display(); // Доброе утро
display(); // Добрый день
var displayMessage = display;
displayMessage(); // Добрый день
displayMessage(); // Добрый день
В этом случае переменная displayMessage будет указывать на переопределенную
версию функции display.
10.7. Hoisting
Hoisting представляет процесс доступа к переменным до их определения. Возможно,
данная концепция выглядит немного странно, но она связана с работой компилятора
JavaScript. Компиляция кода происходит в два прохода. При первом проходе
компилятор получает все объявления переменных, все идентификаторы. При этом
никакой код не выполняется, методы не вызываются. При втором проходе собственно
происходит выполнение. И даже если переменная определена после
непосредственного использования, ошибки не возникнет, так как при первом проходе
компилятору уже известны все переменные.
То есть, происходит "поднятие" кода с определением переменных и функций вверх до
их непосредственного использования. Поднятие на английский переводится как
hoisting, поэтому данный процесс так называется.
Переменные, которые попадают под hoisting, получают значение undefined.
Например, возьмем следующий простейший код:
console.log(foo);
Его выполнение вызовет ошибку ReferenceError: foo is not defined
Добавим определение переменной:
console.log(foo);
// undefined
var foo = "Tom";
В этом случае консоль выведет значение "undefined". При первом проходе
компилятор узнает про существование переменной foo. Она получает значение
undefined. При втором проходе вызывается метод console.log(foo).
Рассмотрим другой пример:
var c = a * b;
var a = 7;
var b = 3;
console.log(c); // NaN
Здесь та же ситуация. Переменные a и b используются до определения. По умолчанию
им присваиваются значения undefined. А если умножить undefined на
undefined, то получим Not a Number (NaN).
Все то же самое относится и к использованию функций. Мы можем сначала вызвать
функцию, а потом уже ее определить:
display();
function display(){
console.log("Hello Hoisting");
}
Здесь функция display благополучно отработает, несмотря на то, что она
определена после вызова.
Но от этой ситуации надо отличать тот случай, когда функция определяется в виде
переменной:
display();
var display = function (){
console.log("Hello Hoisting");
}
В данном случае мы получим ошибку TypeError: display is not a
function. При первом проходе компилятор также получит переменную display и
присвоет ей значение undefined. При втором проходе, когда надо будет вызывать
функцию, на которую будет ссылаться эта переменная, компилятор увидит, что
вызывать нечего: переменная display пока еще равна undefined. И будет
сгенерирована ошибка.
Поэтому при определении переменных и функций следует учитывать особенности
hoisting.
10.8. Передача параметров по значению и по ссылке
Строки, числа, логические величины передаются в функцию по значению. Иными
словами, при передаче значения в функцию эта функция получает копию данного
значения. Рассмотрим, что это значит в практическом плане:
function change(x) {
x = 2 * x;
console.log("x in change:", x);
}
var n = 10;
console.log("n before change:", n); // n before change: 10
change(n);
// x in change: 20
console.log("n after change:", n); // n after change: 10
Функция change получает некоторое число и увеличивает его в два раза. При вызове
функции change ей передается число n. Однако после вызова функции мы видим, что
число n не изменилось, хотя в самой функции произошло увеличение значения
параметра. Потому что при вызове функция change получает копию значения
переменной n. И любые изменения с этой копией никак не затрагивают саму
переменную n.
Объекты и массивы передаются по ссылке. То есть, функция получает сам объект
или массив, а не их копию.
function change(user) {
user.name = "Tom";
}
var bob = {
name: "Bob"
};
console.log("before change:", bob.name);
// Bob
change(bob);
console.log("after change:", bob.name);
// Tom
В данном случае функция change получает объект и меняет его свойство name. В
итоге мы увидим, что после вызова функции изменился оригинальный объект bob,
который передавался в функцию.
Однако если мы попробуем переустановить объект или массив полностью,
оригинальное значение не изменится.
function change(user) {
// полная переустановка объекта
user= {
name:"Tom"
};
}
var bob = {
name: "Bob"
};
console.log("before change:", bob.name);
change(bob);
console.log("after change:", bob.name);
То же самое касается массивов:
function change(array) {
array[0] = 8;
}
function changeFull(array) {
array = [9, 8, 7];
}
// Bob
// Bob
var numbers = [1, 2, 3];
console.log("before change:", numbers);
change(numbers);
console.log("after change:", numbers);
changeFull(numbers);
console.log("after changeFull:", numbers);
// [1, 2, 3]
// [8, 2, 3]
// [8, 2, 3]
10.9. Стрелочные функции
Стрелочные функции (arrow functions) представляют сокращенную версию обычных
функций. Стрелочные функции образуются с помощью знака стрелки (=>), перед
которым в скобках идут параметры функции, а после - собственно тело функции.
Например:
let sum = (x, y) => x + y;
let a = sum(4, 5);
// 9
let b = sum(10, 5);
// 15
В данном случае функция (x, y) => x + y осуществляет сложение двух чисел и
присваивается переменной sum. Функция принимает два параметра - x и y. Ее тело
составляет сложение значений этих параметров. И поскольку после стрелки
фактически идет конкретное значение, которое представляет сумму чисел, то функция
возвращает это значение. И мы можем через переменную sum вызвать данную
функцию и получить ее результат в переменные a и b.
Если после стрелки следует операция или выражение, которое возвращает значение,
то это значение фактически возвращается из стрелочной функции. Но также в качестве
тела функции может примяться выражение, которое ничего не возвращает и просто
выполняет некоторое действие:
let sum = (x, y) => console.log(x + y);
sum(4, 5);
// 9
sum(10, 5);
// 15
В данном случае функция console.log() ничего не возвращает, и соответственно
функция sum также не возвращает никакого результата.
Если функция принимает один параметр, то скобки вокруг него можно опустить:
var square = n => n * n;
console.log(square(5));
// 25
console.log(square(6));
// 36
console.log(square(-7));
// 49
Если тело функции представляет набор выражений, то они заключаются в фигурные
скобки:
var square = n => {
let result = n * n;
return result;
}
console.log(square(5));
// 25
Для возвращения результата из функции в таком случае применяется стандартный
оператор return.
Особо следует остановиться на случае, когда стрелочная функция возвращает объект:
let user = (userName, userAge) => ({name: userName, age:
userAge});
let tom = user("Tom", 34);
let bob = user("Bob", 25);
console.log(tom.name, tom.age);
// "Tom", 34
console.log(bob.name, bob.age);
// "Bob", 25
Объект также определяется с помощью фигурных скобок, но при этом он заключается
в круглые скобки.
Если стрелочная функция не принимает никаких параметров, то ставятся пустые
скобки:
var hello = ()=> console.log("Hello World");
hello();
// Hello World
hello();
// Hello World
Формальное отличие стрелочных функций от function в том, что они не имеют
своего указателя this.
В стрелочных функциях указатель this получает значение на момент объявления
функции, а в function он устанавливается во время вызова, и может быть утерян в
зависимости от контекста вызова. Также стрелочные функции не имеют своего
прототипа, соответственно, не могут использоваться в качестве конструктора.
10.10. Объекты
Объектно-ориентированное программирование на сегодняшний день является одной
из господствующих парадигм в разработке приложений, и в JavaScript мы также
можем использовать все преимущества ООП. В то же время применительно к
JavaScript
объектно-ориентированное
программирование
имеет
некоторые
особенности.
В прошлых темах мы работали с примитивными данными - числами, строками, но
данные не всегда представляют примитивные типы. Например, если в нашей
программе нам надо описать сущность человека, у которого есть имя, возраст, пол и
так далее, то естественно мы не сможем представить сущность человека в виде числа
или строки. Нам потребуется несколько строк или чисел, чтобы должным образом
описать человека. В этом плане человек будет выступать как сложная комплексная
структура, у которого будут отдельные свойства - возраст, рост, имя, фамилия и т.д.
Для работы с подобными структурами в JavaScript используются объекты. Каждый
объект может хранить свойства, которые описывают его состояние, и методы, которые
описывают его поведение.
Есть несколько способов создания нового объекта.
Первый способ заключается в использовании конструктора Object:
var user = new Object();
В данном случае объект называется user. Он определяется также, как и любая
обычная переменная с помощью ключевого слова var.
Выражение new Object() представляет вызов конструктора - функции, создающей
новый объект. Для вызова конструктора применяется оператор new. Вызов
конструктора фактически напоминает вызов обычной функции.
Второй способ создания объекта представляет использование фигурных скобок:
var user = {};
На сегодняшний день более распространенным является второй способ.
После создания объекта мы можем определить в нем свойства. Чтобы определить
свойство, надо после названия объекта через точку указать имя свойства и присвоить
ему значение:
var user = {};
user.name = "Tom";
user.age = 26;
В данном случае объявляются два свойства name и age, которым присваиваются
соответствующие значения. После этого мы можем использовать эти свойства,
например, вывести их значения в консоли:
console.log(user.name);
console.log(user.age);
Также можно определить свойства при определении объекта:
var user = {
name: "Tom",
age: 26
};
В этом случае для присвоения значения свойству используется символ двоеточия, а
после определения свойства ставится запятая (а не точка с запятой).
Кроме того, доступен сокращенный способ определения свойств:
var name = "Tom";
var age = 34;
var user = { name, age };
console.log (user.name);
// Tom
console.log (user.age);
// 34
В данном случае названия переменных также являются и названиями свойств объекта.
И таким образом можно создавать более сложные конструкции:
var name = "Tom";
var age = 34;
var user = { name, age };
var teacher = { user, course: "JavaScript" };
console.log (teacher.user);
// {name: "Tom", age: 34}
console.log (teacher.course);
// JavaScript
Методы объекта определяют его поведение или действия, которые он производит.
Методы представляют собой функции. Например, определим метод, который бы
выводил имя и возраст человека:
var user = {};
user.name = "Tom";
user.age = 26;
user.display = function() {
console.log(user.name);
console.log(user.age);
};
// вызов метода
user.display();
Как и в случае с функциями методы сначала определяются, а потом уже вызываются.
Также методы могут определяться непосредственно при определении объекта:
var user = {
name: "Tom",
age: 26,
display: function() {
console.log(this.name);
console.log(this.age);
}
};
Как и в случае со свойствами, методу присваивается ссылка на функцию с помощью
знака двоеточия.
Чтобы обратиться к свойствам или методам объекта внутри этого объекта,
используется ключевое слово this. Оно означает ссылку на текущий объект.
Также можно использовать сокращенный способ определения методов, когда
двоеточие и слово function опускаются:
var user = {
name: "Tom",
age: 26,
display() {
console.log(this.name, this.age);
},
move(place) {
console.log(this.name, "goes to", place);
}
};
user.display();
// Tom 26
user.move("the shop"); // Tom goes to the shop
Существует также альтернативный способ определения свойств и методов с помощью
синтаксиса массивов:
var user = {};
user["name"] = "Tom";
user["age"] = 26;
user["display"] = function() {
console.log(user.name);
console.log(user.age);
};
// вызов метода
user["display"]();
Название каждого свойства или метода заключается в кавычки и в квадратные скобки,
затем им также присваивается значение. Например, user["age"] = 26.
При обращении к этим свойствам и методам можно использовать либо нотацию точки
(user.name), либо обращаться так: user["name"]
Также следует отметить, что названия свойств и методов объекта всегда представляют
строки. То есть мы могли предыдущее определение объекта переписать так:
var user = {
"name": "Tom",
"age": 26,
"display": function(){
console.log(user.name);
console.log(user.age);
}
};
// вызов метода
user.display();
С одной стороны, разницы никакой нет между двумя определениями. С другой
стороны, бывают случаи, где заключение названия в строку могут помочь. Например,
если название свойства состоит из двух слов, разделенных пробелом:
var user = {
name: "Tom",
age: 26,
"full name": "Tom Johns",
"display info": function(){
console.log(user.name);
console.log(user.age);
}
};
console.log(user["full name"]);
user["display info"]();
Только в этом случае для обращения к подобным свойствам и методам мы должны
использовать синтаксис массивов.
Выше мы посмотрели, как можно динамически добавлять новые свойства к объекту.
Однако также мы можем удалять свойства и методы с помощью оператора delete. И
как и в случае с добавлением мы можем удалять свойства двумя способами. Первый
способ - использование нотации точки:
delete объект.свойство
Либо использовать синтаксис массивов:
delete объект["свойство"]
Например, удалим свойство:
var user = {};
user.name = "Tom";
user.age = 26;
user.display = function(){
console.log(user.name);
console.log(user.age);
};
console.log(user.name); // Tom
delete user.name; // удаляем свойство
// альтернативный вариант
// delete user["name"];
console.log(user.name); // undefined
После удаления свойство будет не определено, поэтому при попытке обращения к
нему, программа вернет значение undefined.
10.11. Вложенные объекты и массивы в объектах
Одни объекты могут содержать в качестве свойств другие объекты. Например, есть
объект страны, у которой можно выделить ряд свойств. Одно из этих свойств может
представлять столицу. Но у столицы мы также можем выделить свои свойства,
например, название, численность населения, год основания:
var country = {
name: "Германия",
language: "немецкий",
capital:{
name: "Берлин",
population: 3375000,
year: 1237
}
};
console.log("Столица: " + country.capital.name); // Берлин
console.log("Население: " + country["capital"]["population"]);
// 3375000
console.log("Год основания: " + country.capital["year"]);
// 1237
Для доступа к свойствам таких вложенных объектов мы можем использовать
стандартную нотацию точки:
country.capital.name
Либо обращаться к ним как к элементам массивов:
country["capital"]["population"]
Также возможен смешанный вид обращения:
country.capital["year"]
В качестве свойств также могут использоваться массивы, в том числе массивы других
объектов:
var country = {
name: "Швейцария",
languages: ["немецкий", "французский", "итальянский"],
capital:{
name: "Берн",
population: 126598
},
cities: [
{ name: "Цюрих", population: 378884 },
{ name: "Женева", population: 188634 },
{ name: "Базель", population: 164937 }
]
};
// вывод всех элементов из country.languages
document.write("
"); // вывод всех элементов из country.cities document.write("
"); В объекте country имеется свойство languages, содержащее массив строк, а также свойство cities, хранящее массив однотипных объектов. С этими массивами мы можем работать так же, как и с любыми другими, например, перебрать с помощью цикла for. При переборе массива объектов каждый текущий элемент будет представлять отдельный объект, поэтому мы можем обратиться к его свойствам и методам: country.cities[i].name В итоге браузер выведет содержимое этих массивов: 10.12. Проверка наличия и перебор методов и свойств При динамическом определении в объекте новых свойств и методов перед их использованием бывает важно проверить, а есть ли уже такие методы и свойства. Для этого в JavaScript может использоваться оператор in: var user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; var hasNameProp = "name" in user; console.log(hasNameProp); // true - свойство name есть в user var hasWeightProp = "weight" in user; console.log(hasWeightProp); //false - в user нет свойства или метода под названием weight Оператор in имеет следующий синтаксис: "свойство|метод" in объект - в кавычках идет название свойства или метода, а после in - название объекта. Если свойство или метод с подобным именем имеется, то оператор возвращает true. Если нет - то возвращается false. Альтернативный способ заключается на значение undefined. Если свойство или метод равен undefined, то эти свойство или метод не определены: var hasNameProp = user.name!==undefined; console.log(hasNameProp); // true var hasWeightProp = user.weight!==undefined; console.log(hasWeightProp); // false И так как объекты представляют тип Object, а значит, имеет все его методы и свойства, то объекты также могут использовать метод hasOwnProperty(), который определен в типе Object: var hasNameProp = user.hasOwnProperty('name'); console.log(hasNameProp); // true var hasDisplayProp = user.hasOwnProperty('display'); console.log(hasDisplayProp); // true var hasWeightProp = user.hasOwnProperty('weight'); console.log(hasWeightProp); // false С помощью цикла for мы можем перебрать объект как обычный массив и получить все его свойства и методы и их значения: var user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; for(var key in user) { console.log(key + " : " + user[key]); } И при запуске консоль браузера отобразит следующий вывод: name : Tom age : 26 display : function (){ console.log(user.name); console.log(user.age); } 10.13. Объекты в функциях Функции могут возвращать значения. Но эти значения не обязательно должны представлять примитивные данные - числа, строки, но также могут быть сложными объектами. Например, вынесем создание объекта user в отдельную функцию: function createUser(pName, pAge) { return { name: pName, age: pAge, displayInfo: function() { document.write("Имя: " + this.name + " возраст: " + this.age + "
"); } }; }; var tom = createUser("Tom", 26); tom.displayInfo(); var alice = createUser("Alice", 24); alice.displayInfo(); Здесь функция createUser() получает значения pName и pAge и по ним создает новый объект, который является возвращаемым результатом. Преимуществом вынесения создания объекта в функцию является то, что далее мы можем создать несколько однотипных объектов с разными значениями. Кроме того объект может передаваться в качестве параметра в функцию: function createUser(pName, pAge) { return { name: pName, age: pAge, displayInfo: function() { document.write ("Имя: " + this.name + " возраст: " + this.age + "
"); }, driveCar: function(car){ document.write (this.name + " ведет машину " + car.name + "
"); } }; }; function createCar(mName, mYear){ return { name: mName, year: mYear }; }; var tom = createUser("Том", 26); tom.displayInfo(); var bentley = createCar("Бентли", 2004); tom.driveCar(bentley); Здесь используются две функции для создания пользователей и объекта машины. Метод driveCar() объекта user в качестве параметра принимает объект car. В итоге браузер нам выведет: Имя: Том возраст: 26 Том ведет машину Бентли 10.14. Конструкторы объектов Кроме создания новых объектов JavaScript предоставляет нам возможность создавать новые типы объектов с помощью конструкторов. Так, одним из способов создания объекта является применение конструктора типа Object: var tom = new Object(); После создания переменной tom она будет вести себя как объект типа Object. Конструктор позволяет определить новый тип объекта. Тип представляет собой абстрактное описание или шаблон объекта. Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке - наличие двух рук, двух ног, головы, пищеварительной, нервной системы и т.д. Есть некоторый шаблон этот шаблон можно назвать типом. Реально же существующий человек является объектом этого типа. Определение типа может состоять из функции конструктора, методов и свойств. Для начала определим конструктор: function User(pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; } Конструктор - это обычная функция за тем исключением, что в ней мы можем установить свойства и методы. Для установки свойств и методов используется ключевое слово this: this.name = pName; В данном случае устанавливаются два свойства name и age и один метод displayInfo. Как правило, названия конструкторов, в отличие от названий обычных функций начинаются с большой буквы. После этого в программе мы можем определить объект типа User и использовать его свойства и методы: var tom = new User("Том", 26); console.log(tom.name); // Том tom.displayInfo(); Чтобы вызвать конструктор, то есть создать объект типа User, надо использовать ключевое слово new. Подобным образом мы можем определить и другие типы и использовать их вместе: // конструктор типа Car function Car(mName, mYear){ this.name = mName; this.year = mYear; this.getCarInfo = function() { document.write ("Модель: " + this.name + " Год выпуска: " + this.year + "
"); }; }; // конструктор типа User function User(pName, pAge) { this.name = pName; this.age = pAge; this.driveCar = function(car) { document.write (this.name + " ведет машину " + car.name + "
"); }; this.displayInfo = function(){ document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; }; var tom = new User("Том", 26); tom.displayInfo(); var bentley = new Car ("Бентли", 2004); tom.driveCar (bentley); Оператор instanceof позволяет проверить, с помощью какого конструктора создан объект. Если объект создан с помощью определенного конструктора, то оператор возвращает true: var tom = new User("Том", 26); var isUser = tom instanceof User; var isCar = tom instanceof Car; console.log(isUser); // true console.log(isCar); // false 10.15. Расширение объектов. Prototype Кроме непосредственного определения свойств и методов в конструкторе мы также можем использовать свойство prototype. Каждая функция имеет свойство prototype, представляющее прототип функции. То есть свойство User.prototype представляет прототип объектов User. И любые свойства и методы, которые будут определены в User.prototype, будут общими для всех объектов User. Например, после определения объекта User нам потребовалось добавить к нему метод и свойство: function User(pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; }; User.prototype.hello = function(){ document.write(this.name + " говорит: 'Привет!'
"); }; User.prototype.maxAge = 110; var tom = new User("Том", 26); tom.hello(); var john = new User("Джон", 28); john.hello(); console.log(tom.maxAge); // 110 console.log(john.maxAge); // 110 Здесь добавлены метод hello и свойство maxAge, и каждый объект User сможет их использовать. Но важно заметить, что значение свойства maxAge будет одно и то же для всех объектов, это разделяемое статическое свойство. В отличие, скажем, от свойства this.name, которое хранит значение для определенного объекта. В то же время мы можем определить в объекте свойство, которое будет назваться так же, как и свойство прототипа. В этом случае собственное свойство объекта будет иметь приоритет перед свойством прототипа: User.prototype.maxAge = 110; var tom = new User("Том", 26); var john = new User("Джон", 28); tom.maxAge = 99; console.log(tom.maxAge); // 99 console.log(john.maxAge); // 110 И при обращении к свойству maxAge JavaScript сначала ищет это свойство среди свойств объекта, и если оно не было найдено, тогда обращается к свойствам прототипа. То же самое касается и методов. 10.16. Инкапсуляция Инкапсуляция является одним из ключевых понятий объектно-ориентированного программирования и представляет сокрытие состояния объекта от прямого доступа извне. По умолчанию все свойства объектов являются публичными, общедоступными, и мы к ним можем обратиться из любого места программы. function User (pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age); }; }; var tom = new User("Том", 26); tom.name=34; console.log(tom.name); Но мы можем их скрыть от доступа извне, сделав свойства локальными переменными: function User (name, age) { this.name = name; var _age = age; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + _age + "
"); }; this.getAge = function() { return _age; } this.setAge = function(age) { if (typeof age === "number" && age >0 && age<110) { _age = age; } else { console.log("Недопустимое значение"); } } } var tom = new User("Том", 26); console.log(tom._age); // undefined - _age - локальная переменная console.log(tom.getAge()); // 26 tom.setAge(32); console.log(tom.getAge()); // 32 tom.setAge("54"); // Недопустимое значение В конструкторе User объявляется локальная переменная _age вместо свойства age. Как правило, названия локальных переменных в конструкторах начинаются со знака подчеркивания. Для того, чтобы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод - setAge, который еще называется сеттер (setter), предназначен для установки значения переменной _age. Плюсом такого подхода является то, что мы имеем больший контроль над доступом к значению _age. Например, мы можем проверить какие-то сопутствующие условия, как в данном случае проверяются тип значение (он должен представлять число), само значение (возраст не может быть меньше 0). 10.17. Функция как объект. Методы call и apply В JavaScript функция тоже является объектом - объектом Function и тоже имеет прототип, свойства, методы. Все функции, которые используются в программе, являются объектами Function и имеют все его свойства и методы. Например, мы можем создать функцию с помощью конструктора Function: var square = new Function('n', 'return n * n;'); console.log(square(5)); В конструктор Function может передаваться ряд параметров. Последний параметр представляет собой само тело функции в виде строки. Фактически строка содержит код JavaScript. Предыдущие аргументы содержат названия параметров. В данном случае определяется функция возведения числа в квадрат, которая имеет один параметр n. Среди свойств объекта Function можно выделить следующие: arguments: массив аргументов, передаваемых в функцию length: определяет количество аргументов, которые ожидает функция caller: определяет функцию, вызвавшую текущую выполняющуюся функцию name: имя функции prototype: прототип функции С помощью прототипа мы можем определить дополнительные свойства: function display(){ console.log("привет мир"); } Function.prototype.program ="Hello"; console.log(display.program); // Hello Среди методов надо отметить методы call() и apply(). Метод call() вызывает функцию с указанным значением this и аргументами: function add(x, y) { return x + y; } var result = add.call(this, 3, 8); console.log(result); // 11 this указывает на объект, для которого вызывается функция - в данном случае это глобальный объект window. После this передаются значения для параметров. При передаче объекта через первый параметр, мы можем ссылаться на него через ключевое слово this: function User (name, age) { this.name = name; this.age = age; } var tom = new User("Том", 26); function display() { console.log("Ваше имя: " + this.name); } display.call(tom); // Ваше имя: Том В данном случае передается только одно значение, поскольку функция display не принимает параметров. То есть функция будет вызываться для объекта tom. Если нам не важен объект, для которого вызывается функция, то можно передать значение null: function add(x, y) { return x + y; } var result = add.call (null, 3, 8); console.log (result); // 11 На метод call() похож метод apply(), который также вызывает функцию и в качестве первого параметра также получает объект, для которого функция вызывается. Только теперь в качестве второго параметра передается массив аргументов: unction add (x, y) { return x + y; } var result = add.apply (null, [3, 8]); console.log (result); // 11 10.18. Наследование JavaScript поддерживает наследование, то позволяет нам при создании новых типов объектов при необходимости унаследовать их функционал от уже существующих. Например, у нас может быть объект User, представляющий отдельного пользователя. И также может быть объект Employee, который представляет работника. Но работник также может являться пользователем и поэтому должен иметь все его свойства и методы. Например: // конструктор пользователя function User (name, age) { this.name = name; this.age = age; this.go = function(){ document.write(this.name + " идет
"); } this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; } User.prototype.maxage = 110; // конструктор работника function Employee(name, age, comp) { User.call(this, name, age); this.company = comp; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "; компания: " + this.company + "
"); }; } Employee.prototype = Object.create(User.prototype); var tom = new User("Том", 26); var bill = new Employee("Билл", 32, "Google"); tom.go(); bill.go(); tom.displayInfo(); bill.displayInfo(); console.log(bill.maxage); Здесь в начале определяет конструктор User и к его прототипу добавляется свойство maxage. Затем определяется тип Employee. В конструкторе Employee происходит обращение к конструктору User с помощью вызова: User.call(this, name, age); Передача первого параметра позволяет вызвать функцию конструктора User для объекта, создаваемого конструктором Employee. Благодаря этому все свойства и методы, определенные в конструкторе User, также переходят на объект Employee. Кроме того, необходимо унаследовать также и прототип User. Для этого служит вызов: Employee.prototype = Object.create(User.prototype); Метод Object.create() позволяет создать объект прототипа User, который затем присваивается прототипу Employee. При этом при необходимости в прототипе Employee мы также можем определить дополнительные свойства и методы. При наследовании мы можем переопределять наследуемый функционал. Например, Employee переопределяет метод displayInfo(), унаследованный от User, чтобы включить в вывод этого метода новое свойство company. В итоге браузер предоставит следующий вывод: Том идет Билл идет Имя: Том; возраст: 26 Имя: Билл; возраст: 32; компания: Google Для кода User.prototype.maxage = 110; Employee.prototype = Object.create(User.prototype); Employee.prototype.maxage = 120; var tom = new User("Том", 26); console.log(tom.maxage); //110 объект Employee унаследует прототип от User, а оба прототипа будет указывать на разные сущности. А в случае User.prototype.maxage = 110; Employee.prototype = User.prototype; Employee.prototype.maxage = 120; var tom = new User("Том", 26); console.log(tom.maxage); //120 оба прототипа будут указывать на один и тот же объект. Изменив прототип Employee, мы также поменяем прототип User, что не очень правильно. 10.19. Ключевое слово this Поведение ключевого слова this зависит от контекста, в котором оно используется, и от того, в каком режиме оно используется - строгом или нестрогом. В глобальном контексте this ссылается на глобальный объект. В данном случае поведение не зависит от режима (строгий или нестрогий): this.alert ("global alert"); this.console.log ("global console"); var currentDocument = this.document; Но в данном случае использвание this избыточно. Если скрипт запускается в строгом режиме (директива "use strict"), то this ссылается непосредственно на контекст функции. Иначе this ссылается на внешний контекст. Например: function foo(){ var bar = "bar2"; console.log(this.bar); } var bar = "bar1"; foo(); // bar1 В приницпе, смысла использования this в данном случае нет, так как мы могли бы напрямую использовать локальную переменную. Если бы мы использовали строгий режим, то this в этом случае имело бы значение undefined: "use strict"; function foo(){ var bar = "bar2"; console.log(this.bar); } var bar = "bar1"; foo(); // ошибка - this - undefined В контексте объекта, в том числе в его методах, ключевое слово this ссылается на этот же объект: var o = { bar: "bar3", foo: function(){ console.log(this.bar); } } var bar = "bar1"; o.foo(); // bar3 Рассмотрим более сложный пример: function foo(){ var bar = "bar2"; console.log(this.bar); } var o3 = {bar:"bar3", foo: foo}; var o4 = {bar:"bar4", foo: foo}; var bar = "bar1"; foo(); // bar1 o3.foo(); // bar3 o4.foo(); // bar4 Здесь определена глобальная переменная bar. И также в функции foo определена локальная переменная bar. Значение какой переменной будет выводиться в функции foo? Функция foo выводит значение глобальной переменной, так как данный скрипт запускается в нестрогом режиме, а значит ключеое слово this в функции foo ссылается на внешний контекст. Иначе дело обстоит с объектами. Они определяют свой собственный контекст, в котором существует свое свойство bar. И при вызове метода foo внешним контекстом по отношению к функции будет контекст объектов o3 и o4. Подобное поведение может привести к некоторому непониманию в отдельных случаях. Так, рассмотрим другую ситуацию: var o1 = { bar: "bar1", foo: function(){ console.log(this.bar); } } var o2 = {bar: "bar2", foo: o1.foo}; var bar = "bar3"; var foo = o1.foo; o1.foo(); // bar1 o2.foo(); // bar2 foo(); // bar3 Несмотря на то, что объект o2 использует метод foo из объекта o1, тем не менее функция o1.foo также будет искать значение для this.bar во внешнем котексте, то есть в контексте объекта o2. А в объекте o2 это значение равно bar: "bar2". То же самое с глобальной переменной foo, которая ссылается на ту же функцию, что и метод o1.foo. В этом случае также будет происходить поиск значения для this.bar во внешним контексте, то есть в глобальном контексте, где определена переменная var bar = "bar3". Однако если мы вызываем функцию из другой функции, вызываемая функция также будет использовать внешний контекст: var bar = "bar2"; function daz(){ var bar = "bar5"; function maz(){ console.log(this.bar); } maz(); } daz(); // bar2 Здесь функция daz в качестве this.bar использует значение переменной bar из внешнего контекста, то есть значение глобальной переменной bar. Функция maz также в качестве this.bar использует значение переменной bar из внешнего контекста, а это значение this.bar из внешней функции daz, которое в свою очередь представляет значение глобальной переменной bar. Поэтому в итоге консоль выведет "bar2", а не "bar5". С помощью методов call() и apply() можно задать явную привязку функции к определенному контексту: function foo(){ console.log(this.bar); } var o3 = { bar: "bar3" } var bar = "bar1"; foo(); // bar1 foo.apply(o3); // bar3 // или foo.call(o3); Во втором случае функция foo привязывается к объекту o3, который и определяет ее контекст. Поэтому во втором случае консоль выведет "bar3". Метод f.bind(o) позволяет создать новую функцию с тем же телом и областью видимости, что и функция f, но с привязкой к объекту o: function foo(){ console.log(this.bar); } var o3 = {bar: "bar3"} var bar = "bar1"; foo(); // bar1 var func = foo.bind(o3); func(); // bar3 При работе с несколькими контекстами мы вынуждены учитывать, в каком контексте определяется переменная. Например, возьмем следующий код: var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function() { this.courses.forEach(function(course){ console.log(this.title, course); }) } } school.printCourses(); Функция printCourses проходит по всем курсам из массива и при их выводе предваряет их значением свойства title. Однако на консоли при запуске программы мы увидим следующее: undefined "JavaScript" undefined "TypeScript" undefined "Java" undefined "Go" Мы видим, что значение this.title не определено, так как this как контекст объекта замещается глобальным контекстом. В этом случае нам надо передать подобное значение this.title или весь контекст объекта. var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function(){ var that = this; this.courses.forEach(function(course){ console.log(that.title, course); }) } } school.printCourses(); Стрелочные функции также позволяют решить данную проблему: var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function(){ this.courses.forEach ((course)=>console.log(this.title, course)) } } school.printCourses(); Контекстом для стрелочной функции в данном случае будет выступать контекст объекта school. Соответственно, нам недо определять дополнительные переменные для передачи данных в функцию. 10.20. Декомпозиция Декомпозиция (destructuring) позволяет извлечь из объекта отдельные значения в переменные: let user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; let {name, email} = user; console.log(name); // Tom console.log(email); // tom@gmail.com Для декомпозиции объекта переменные помещаются в фигурные скобки и им присваивается объект. Сопоставление между свойствами объекта и переменными идет по имени. Мы можем указать, что мы хотим получить значения свойств объекта в переменные с другим именем: let user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; let {name: userName, email: mailAddress} = user; console.log (userName); // Tom console.log (mailAddress); // tom@gmail.com В данном случае свойство name сопоставляется с переменной userName, а поле email - с переменной mailAddress. Также можно декомпозировать массивы: let users = ["Tom", "Sam", "Bob"]; let [a, b, c] = users; console.log(a); // Tom console.log(b); // Sam console.log(c); // Bob Для декомпозиции массива переменные помещаются в квадратные скобки и последовательно получают значения элементов массива. При этом, мы можем пропустить ряд элементов массива, оставив вместо имен переменных пропуски: let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; let [first,,,,fifth] = users; console.log (first); // Tom console.log (fifth); // Alice Выражение first,,,,fifth указывает, что мы хотим получить первый элемент массива в переменную first, затем пропустить три элемента и получить пятый элемент в переменную fifth. Подобным образом можно получить, например, второй и четвертый элементы: let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; let [,second,,forth] = users; console.log(second); // Sam console.log(forth); // Ann Можно совместить получение данных из массива и объекта: let people = [ {name: "Tom", age: 34}, {name: "Bob", age: 23}, {name: "Sam", age: 32} ]; let [,{name}] = people; console.log(name); // Bob В данном случае получаем значение свойства name второго объекта в массиве. Если в функцию в качестве параметра передается массив или объект, то его также можно подобным образом декомпозировать: function display({name:userName, age:userAge}){ console.log(userName, userAge); } function sum([a, b, c]){ var result = a + b + c; console.log(result); } let user = {name:"Alice", age:33, email: "alice@gmail.com"}; let numbers = [3, 5, 7, 8]; display(user); // Alice 33 sum(numbers); // 15 10.21. Классы С внедрением стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса. Для определения класса используется ключевое слово class: class Person { } После слова class идет название класса (в данном случае класс называется Person), и затем в фигурных скобках определяется тело класса. Также можно определить анонимный класс и присвоить его переменной: let Person = class {} После этого мы можем создать объекты класса с помощью конструктора: class Person {} let tom = new Person(); let bob = new Person(); Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передается никаких аргументов. Также мы можем определить в классе свои конструкторы. Также класс может содержать свойства и методы: class Person{ constructor(name, age){ this.name = name; this.age = age; } display(){ console.log(this.name, this.age); } } let tom = new Person("Tom", 34); tom.display(); // Tom 34 console.log(tom.name); // Tom Конструктор определяется с помощью метода с именем constructor. По сути это обычный метод, который может принимать параметры. Основная цель конструктора инициализировать объект начальными данными. В данном случае конструктору передаются два значения - для имени и возраста пользователя. Для хранения состояния в классе определяются свойства. Для их определения используется ключевое слово this. В данном случае в классе два свойства: name и age. Поведение класса определяют методы. В данном случае определен метод display(), который выводит значения свойств на консоль. Поскольку мы определили конструктор, который принимает два параметра, то соответственно мы можем передать в конструктор два значения для инициализации объекта: new Person("Tom", 34). Одни классы могут наследоваться от других. Наследование позволяет сократить объем кода в классах-наследниках. Например, определим следующие классы: class Person{ constructor(name, age){ this.name = name; this.age = age; } display(){ console.log(this.name, this.age); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } display(){ super.display(); console.log("Employee in", this.company); } work(){ console.log(this.name, "is hard working"); } } let tom = new Person("Tom", 34); let bob = new Employee("Bob", 36, "Google"); tom.display(); bob.display(); bob.work(); Для наследования одного класса от другого в определении класса применяется оператор extends, после которого идет название базового класса. То есть в данном случае класс Employee наследуется от класса Person. Класс Person еще называется базовым классом, классом-родителем, суперклассом, а класс Employee классом-наследником, подклассом, производным классом. Производный класс, как и базовый, может определять конструкторы, свойства, методы. Вместе с тем с помощью слова super производный класс может ссылаться на функционал, определенный в базовом. Например, в конструкторе Employee можно вручную не устанавливать свойства name и age, а с помощью выражения super (name, age); вызвать конструктор базового класса и тем самым передать работу по установке этих свойств базовому классу. Подобным образом в методе display в классе Employee через вызов super.display() можно обратиться к реализации метода display класса Person. Консольный вывод программы: Tom 34 Bob 36 Employee in Google Bob is hard working Статические методы вызываются для всего класса в целом, а не для отдельного объекта. Для их определения применяется оператор static. Например: class Person{ constructor(name, age){ this.name = name; this.age = age; } static nameToUpper(person){ return person.name.toUpperCase(); } display(){ console.log(this.name, this.age); } } let tom = new Person("Tom Soyer", 34); let personName = Person.nameToUpper(tom); console.log(personName); // TOM SOYER В данном случае определен статический метод nameToUpper(). В качестве параметра он принимает объект Person и переводит его имя в верхний регистр. Поскольку статический метод относится классу в целом, а не к объекту, то мы не можем использовать в нем ключевое слово this и через него обращаться к свойствам объекта. Задание к лабораторной работе 10. Решить задачи из своего варианта задания. Задача 1 предполагает написание и тестирование функции, обрабатывающей свои аргументы. Часть вариантов может предполагать применение функции-замыкания. Задача 2 предполагает создание объекта JavaScript со свойствами, описывающими объект из Вашего варианта задания и методами, выполняющими основные действия над данными объекта. Варианты задачи 1 1. Создайте функцию repeat(str, n), которая возвращает строку, состоящую и n повторений строки str. Аргумент n по умолчанию равен 2, str — пустой строке. 2. Создайте функцию rgb(), которая будет принимать три числовых аргумента и возвращать строку вида «rgb(23,100,134)». Если аргументы не заданы, считать их равными нулю. 3. Создайте функцию avg() , которая будет находить среднее значение по всем своим аргументам (аргументы величины числовые) 4. Напишите функцию operation(m,n,o), в которой m и n — числовые переменные, а o — функциональный литерал, который берет два аргумента и выполняет математическую операцию над ними 5. Напишите функцию, которая возвращает квадратную матрицу размерностью n x n, последовательно заполненную значениями 1, 2, …, n2 6. Напишите функцию, возвращающую пирамидку из N строк вида, показанного далее: 7. Напишите функцию, возвращающую строку вида '123…N', где N – аргумент функции 8. Напишите функцию, проверяющую корректность даты ДД.ММ.ГГГГ 9. Напишите функцию, определяющую номер дня в году 10.Напишите функцию, определяющую, сколько полных дней осталось до конца года 11.Напишите функцию, определяющую, какого числа в выбранном году будет первое воскресенье октября (День Учителя) 12.Напишите функцию, определяющую знак числа (-1, 0 или 1). Примените функцию для определения знаков всех элементов двумерной матрицы. 13.Напишите функцию, формирующую список всех простых чисел в диапазоне от a до b, a и b – натуральные числа, aЗадача 1
Вывод приложения (всплывающее окно):
Пример реализации задачи 2. Объект: треугольник, заданный длинами сторон.
Задача 3
Официальные языки Швейцарии
"); for (var i=0; i < country.languages.length; i++) document.write(country.languages[i] + ""); // вывод всех элементов из country.cities document.write("
Города Швейцарии
"); for (var i=0; i < country.cities.length; i++) document.write(country.cities[i].name + ""); В объекте country имеется свойство languages, содержащее массив строк, а также свойство cities, хранящее массив однотипных объектов. С этими массивами мы можем работать так же, как и с любыми другими, например, перебрать с помощью цикла for. При переборе массива объектов каждый текущий элемент будет представлять отдельный объект, поэтому мы можем обратиться к его свойствам и методам: country.cities[i].name В итоге браузер выведет содержимое этих массивов: 10.12. Проверка наличия и перебор методов и свойств При динамическом определении в объекте новых свойств и методов перед их использованием бывает важно проверить, а есть ли уже такие методы и свойства. Для этого в JavaScript может использоваться оператор in: var user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; var hasNameProp = "name" in user; console.log(hasNameProp); // true - свойство name есть в user var hasWeightProp = "weight" in user; console.log(hasWeightProp); //false - в user нет свойства или метода под названием weight Оператор in имеет следующий синтаксис: "свойство|метод" in объект - в кавычках идет название свойства или метода, а после in - название объекта. Если свойство или метод с подобным именем имеется, то оператор возвращает true. Если нет - то возвращается false. Альтернативный способ заключается на значение undefined. Если свойство или метод равен undefined, то эти свойство или метод не определены: var hasNameProp = user.name!==undefined; console.log(hasNameProp); // true var hasWeightProp = user.weight!==undefined; console.log(hasWeightProp); // false И так как объекты представляют тип Object, а значит, имеет все его методы и свойства, то объекты также могут использовать метод hasOwnProperty(), который определен в типе Object: var hasNameProp = user.hasOwnProperty('name'); console.log(hasNameProp); // true var hasDisplayProp = user.hasOwnProperty('display'); console.log(hasDisplayProp); // true var hasWeightProp = user.hasOwnProperty('weight'); console.log(hasWeightProp); // false С помощью цикла for мы можем перебрать объект как обычный массив и получить все его свойства и методы и их значения: var user = {}; user.name = "Tom"; user.age = 26; user.display = function(){ console.log(user.name); console.log(user.age); }; for(var key in user) { console.log(key + " : " + user[key]); } И при запуске консоль браузера отобразит следующий вывод: name : Tom age : 26 display : function (){ console.log(user.name); console.log(user.age); } 10.13. Объекты в функциях Функции могут возвращать значения. Но эти значения не обязательно должны представлять примитивные данные - числа, строки, но также могут быть сложными объектами. Например, вынесем создание объекта user в отдельную функцию: function createUser(pName, pAge) { return { name: pName, age: pAge, displayInfo: function() { document.write("Имя: " + this.name + " возраст: " + this.age + "
"); } }; }; var tom = createUser("Tom", 26); tom.displayInfo(); var alice = createUser("Alice", 24); alice.displayInfo(); Здесь функция createUser() получает значения pName и pAge и по ним создает новый объект, который является возвращаемым результатом. Преимуществом вынесения создания объекта в функцию является то, что далее мы можем создать несколько однотипных объектов с разными значениями. Кроме того объект может передаваться в качестве параметра в функцию: function createUser(pName, pAge) { return { name: pName, age: pAge, displayInfo: function() { document.write ("Имя: " + this.name + " возраст: " + this.age + "
"); }, driveCar: function(car){ document.write (this.name + " ведет машину " + car.name + "
"); } }; }; function createCar(mName, mYear){ return { name: mName, year: mYear }; }; var tom = createUser("Том", 26); tom.displayInfo(); var bentley = createCar("Бентли", 2004); tom.driveCar(bentley); Здесь используются две функции для создания пользователей и объекта машины. Метод driveCar() объекта user в качестве параметра принимает объект car. В итоге браузер нам выведет: Имя: Том возраст: 26 Том ведет машину Бентли 10.14. Конструкторы объектов Кроме создания новых объектов JavaScript предоставляет нам возможность создавать новые типы объектов с помощью конструкторов. Так, одним из способов создания объекта является применение конструктора типа Object: var tom = new Object(); После создания переменной tom она будет вести себя как объект типа Object. Конструктор позволяет определить новый тип объекта. Тип представляет собой абстрактное описание или шаблон объекта. Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке - наличие двух рук, двух ног, головы, пищеварительной, нервной системы и т.д. Есть некоторый шаблон этот шаблон можно назвать типом. Реально же существующий человек является объектом этого типа. Определение типа может состоять из функции конструктора, методов и свойств. Для начала определим конструктор: function User(pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; } Конструктор - это обычная функция за тем исключением, что в ней мы можем установить свойства и методы. Для установки свойств и методов используется ключевое слово this: this.name = pName; В данном случае устанавливаются два свойства name и age и один метод displayInfo. Как правило, названия конструкторов, в отличие от названий обычных функций начинаются с большой буквы. После этого в программе мы можем определить объект типа User и использовать его свойства и методы: var tom = new User("Том", 26); console.log(tom.name); // Том tom.displayInfo(); Чтобы вызвать конструктор, то есть создать объект типа User, надо использовать ключевое слово new. Подобным образом мы можем определить и другие типы и использовать их вместе: // конструктор типа Car function Car(mName, mYear){ this.name = mName; this.year = mYear; this.getCarInfo = function() { document.write ("Модель: " + this.name + " Год выпуска: " + this.year + "
"); }; }; // конструктор типа User function User(pName, pAge) { this.name = pName; this.age = pAge; this.driveCar = function(car) { document.write (this.name + " ведет машину " + car.name + "
"); }; this.displayInfo = function(){ document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; }; var tom = new User("Том", 26); tom.displayInfo(); var bentley = new Car ("Бентли", 2004); tom.driveCar (bentley); Оператор instanceof позволяет проверить, с помощью какого конструктора создан объект. Если объект создан с помощью определенного конструктора, то оператор возвращает true: var tom = new User("Том", 26); var isUser = tom instanceof User; var isCar = tom instanceof Car; console.log(isUser); // true console.log(isCar); // false 10.15. Расширение объектов. Prototype Кроме непосредственного определения свойств и методов в конструкторе мы также можем использовать свойство prototype. Каждая функция имеет свойство prototype, представляющее прототип функции. То есть свойство User.prototype представляет прототип объектов User. И любые свойства и методы, которые будут определены в User.prototype, будут общими для всех объектов User. Например, после определения объекта User нам потребовалось добавить к нему метод и свойство: function User(pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; }; User.prototype.hello = function(){ document.write(this.name + " говорит: 'Привет!'
"); }; User.prototype.maxAge = 110; var tom = new User("Том", 26); tom.hello(); var john = new User("Джон", 28); john.hello(); console.log(tom.maxAge); // 110 console.log(john.maxAge); // 110 Здесь добавлены метод hello и свойство maxAge, и каждый объект User сможет их использовать. Но важно заметить, что значение свойства maxAge будет одно и то же для всех объектов, это разделяемое статическое свойство. В отличие, скажем, от свойства this.name, которое хранит значение для определенного объекта. В то же время мы можем определить в объекте свойство, которое будет назваться так же, как и свойство прототипа. В этом случае собственное свойство объекта будет иметь приоритет перед свойством прототипа: User.prototype.maxAge = 110; var tom = new User("Том", 26); var john = new User("Джон", 28); tom.maxAge = 99; console.log(tom.maxAge); // 99 console.log(john.maxAge); // 110 И при обращении к свойству maxAge JavaScript сначала ищет это свойство среди свойств объекта, и если оно не было найдено, тогда обращается к свойствам прототипа. То же самое касается и методов. 10.16. Инкапсуляция Инкапсуляция является одним из ключевых понятий объектно-ориентированного программирования и представляет сокрытие состояния объекта от прямого доступа извне. По умолчанию все свойства объектов являются публичными, общедоступными, и мы к ним можем обратиться из любого места программы. function User (pName, pAge) { this.name = pName; this.age = pAge; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age); }; }; var tom = new User("Том", 26); tom.name=34; console.log(tom.name); Но мы можем их скрыть от доступа извне, сделав свойства локальными переменными: function User (name, age) { this.name = name; var _age = age; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + _age + "
"); }; this.getAge = function() { return _age; } this.setAge = function(age) { if (typeof age === "number" && age >0 && age<110) { _age = age; } else { console.log("Недопустимое значение"); } } } var tom = new User("Том", 26); console.log(tom._age); // undefined - _age - локальная переменная console.log(tom.getAge()); // 26 tom.setAge(32); console.log(tom.getAge()); // 32 tom.setAge("54"); // Недопустимое значение В конструкторе User объявляется локальная переменная _age вместо свойства age. Как правило, названия локальных переменных в конструкторах начинаются со знака подчеркивания. Для того, чтобы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод - setAge, который еще называется сеттер (setter), предназначен для установки значения переменной _age. Плюсом такого подхода является то, что мы имеем больший контроль над доступом к значению _age. Например, мы можем проверить какие-то сопутствующие условия, как в данном случае проверяются тип значение (он должен представлять число), само значение (возраст не может быть меньше 0). 10.17. Функция как объект. Методы call и apply В JavaScript функция тоже является объектом - объектом Function и тоже имеет прототип, свойства, методы. Все функции, которые используются в программе, являются объектами Function и имеют все его свойства и методы. Например, мы можем создать функцию с помощью конструктора Function: var square = new Function('n', 'return n * n;'); console.log(square(5)); В конструктор Function может передаваться ряд параметров. Последний параметр представляет собой само тело функции в виде строки. Фактически строка содержит код JavaScript. Предыдущие аргументы содержат названия параметров. В данном случае определяется функция возведения числа в квадрат, которая имеет один параметр n. Среди свойств объекта Function можно выделить следующие: arguments: массив аргументов, передаваемых в функцию length: определяет количество аргументов, которые ожидает функция caller: определяет функцию, вызвавшую текущую выполняющуюся функцию name: имя функции prototype: прототип функции С помощью прототипа мы можем определить дополнительные свойства: function display(){ console.log("привет мир"); } Function.prototype.program ="Hello"; console.log(display.program); // Hello Среди методов надо отметить методы call() и apply(). Метод call() вызывает функцию с указанным значением this и аргументами: function add(x, y) { return x + y; } var result = add.call(this, 3, 8); console.log(result); // 11 this указывает на объект, для которого вызывается функция - в данном случае это глобальный объект window. После this передаются значения для параметров. При передаче объекта через первый параметр, мы можем ссылаться на него через ключевое слово this: function User (name, age) { this.name = name; this.age = age; } var tom = new User("Том", 26); function display() { console.log("Ваше имя: " + this.name); } display.call(tom); // Ваше имя: Том В данном случае передается только одно значение, поскольку функция display не принимает параметров. То есть функция будет вызываться для объекта tom. Если нам не важен объект, для которого вызывается функция, то можно передать значение null: function add(x, y) { return x + y; } var result = add.call (null, 3, 8); console.log (result); // 11 На метод call() похож метод apply(), который также вызывает функцию и в качестве первого параметра также получает объект, для которого функция вызывается. Только теперь в качестве второго параметра передается массив аргументов: unction add (x, y) { return x + y; } var result = add.apply (null, [3, 8]); console.log (result); // 11 10.18. Наследование JavaScript поддерживает наследование, то позволяет нам при создании новых типов объектов при необходимости унаследовать их функционал от уже существующих. Например, у нас может быть объект User, представляющий отдельного пользователя. И также может быть объект Employee, который представляет работника. Но работник также может являться пользователем и поэтому должен иметь все его свойства и методы. Например: // конструктор пользователя function User (name, age) { this.name = name; this.age = age; this.go = function(){ document.write(this.name + " идет
"); } this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "
"); }; } User.prototype.maxage = 110; // конструктор работника function Employee(name, age, comp) { User.call(this, name, age); this.company = comp; this.displayInfo = function() { document.write ("Имя: " + this.name + "; возраст: " + this.age + "; компания: " + this.company + "
"); }; } Employee.prototype = Object.create(User.prototype); var tom = new User("Том", 26); var bill = new Employee("Билл", 32, "Google"); tom.go(); bill.go(); tom.displayInfo(); bill.displayInfo(); console.log(bill.maxage); Здесь в начале определяет конструктор User и к его прототипу добавляется свойство maxage. Затем определяется тип Employee. В конструкторе Employee происходит обращение к конструктору User с помощью вызова: User.call(this, name, age); Передача первого параметра позволяет вызвать функцию конструктора User для объекта, создаваемого конструктором Employee. Благодаря этому все свойства и методы, определенные в конструкторе User, также переходят на объект Employee. Кроме того, необходимо унаследовать также и прототип User. Для этого служит вызов: Employee.prototype = Object.create(User.prototype); Метод Object.create() позволяет создать объект прототипа User, который затем присваивается прототипу Employee. При этом при необходимости в прототипе Employee мы также можем определить дополнительные свойства и методы. При наследовании мы можем переопределять наследуемый функционал. Например, Employee переопределяет метод displayInfo(), унаследованный от User, чтобы включить в вывод этого метода новое свойство company. В итоге браузер предоставит следующий вывод: Том идет Билл идет Имя: Том; возраст: 26 Имя: Билл; возраст: 32; компания: Google Для кода User.prototype.maxage = 110; Employee.prototype = Object.create(User.prototype); Employee.prototype.maxage = 120; var tom = new User("Том", 26); console.log(tom.maxage); //110 объект Employee унаследует прототип от User, а оба прототипа будет указывать на разные сущности. А в случае User.prototype.maxage = 110; Employee.prototype = User.prototype; Employee.prototype.maxage = 120; var tom = new User("Том", 26); console.log(tom.maxage); //120 оба прототипа будут указывать на один и тот же объект. Изменив прототип Employee, мы также поменяем прототип User, что не очень правильно. 10.19. Ключевое слово this Поведение ключевого слова this зависит от контекста, в котором оно используется, и от того, в каком режиме оно используется - строгом или нестрогом. В глобальном контексте this ссылается на глобальный объект. В данном случае поведение не зависит от режима (строгий или нестрогий): this.alert ("global alert"); this.console.log ("global console"); var currentDocument = this.document; Но в данном случае использвание this избыточно. Если скрипт запускается в строгом режиме (директива "use strict"), то this ссылается непосредственно на контекст функции. Иначе this ссылается на внешний контекст. Например: function foo(){ var bar = "bar2"; console.log(this.bar); } var bar = "bar1"; foo(); // bar1 В приницпе, смысла использования this в данном случае нет, так как мы могли бы напрямую использовать локальную переменную. Если бы мы использовали строгий режим, то this в этом случае имело бы значение undefined: "use strict"; function foo(){ var bar = "bar2"; console.log(this.bar); } var bar = "bar1"; foo(); // ошибка - this - undefined В контексте объекта, в том числе в его методах, ключевое слово this ссылается на этот же объект: var o = { bar: "bar3", foo: function(){ console.log(this.bar); } } var bar = "bar1"; o.foo(); // bar3 Рассмотрим более сложный пример: function foo(){ var bar = "bar2"; console.log(this.bar); } var o3 = {bar:"bar3", foo: foo}; var o4 = {bar:"bar4", foo: foo}; var bar = "bar1"; foo(); // bar1 o3.foo(); // bar3 o4.foo(); // bar4 Здесь определена глобальная переменная bar. И также в функции foo определена локальная переменная bar. Значение какой переменной будет выводиться в функции foo? Функция foo выводит значение глобальной переменной, так как данный скрипт запускается в нестрогом режиме, а значит ключеое слово this в функции foo ссылается на внешний контекст. Иначе дело обстоит с объектами. Они определяют свой собственный контекст, в котором существует свое свойство bar. И при вызове метода foo внешним контекстом по отношению к функции будет контекст объектов o3 и o4. Подобное поведение может привести к некоторому непониманию в отдельных случаях. Так, рассмотрим другую ситуацию: var o1 = { bar: "bar1", foo: function(){ console.log(this.bar); } } var o2 = {bar: "bar2", foo: o1.foo}; var bar = "bar3"; var foo = o1.foo; o1.foo(); // bar1 o2.foo(); // bar2 foo(); // bar3 Несмотря на то, что объект o2 использует метод foo из объекта o1, тем не менее функция o1.foo также будет искать значение для this.bar во внешнем котексте, то есть в контексте объекта o2. А в объекте o2 это значение равно bar: "bar2". То же самое с глобальной переменной foo, которая ссылается на ту же функцию, что и метод o1.foo. В этом случае также будет происходить поиск значения для this.bar во внешним контексте, то есть в глобальном контексте, где определена переменная var bar = "bar3". Однако если мы вызываем функцию из другой функции, вызываемая функция также будет использовать внешний контекст: var bar = "bar2"; function daz(){ var bar = "bar5"; function maz(){ console.log(this.bar); } maz(); } daz(); // bar2 Здесь функция daz в качестве this.bar использует значение переменной bar из внешнего контекста, то есть значение глобальной переменной bar. Функция maz также в качестве this.bar использует значение переменной bar из внешнего контекста, а это значение this.bar из внешней функции daz, которое в свою очередь представляет значение глобальной переменной bar. Поэтому в итоге консоль выведет "bar2", а не "bar5". С помощью методов call() и apply() можно задать явную привязку функции к определенному контексту: function foo(){ console.log(this.bar); } var o3 = { bar: "bar3" } var bar = "bar1"; foo(); // bar1 foo.apply(o3); // bar3 // или foo.call(o3); Во втором случае функция foo привязывается к объекту o3, который и определяет ее контекст. Поэтому во втором случае консоль выведет "bar3". Метод f.bind(o) позволяет создать новую функцию с тем же телом и областью видимости, что и функция f, но с привязкой к объекту o: function foo(){ console.log(this.bar); } var o3 = {bar: "bar3"} var bar = "bar1"; foo(); // bar1 var func = foo.bind(o3); func(); // bar3 При работе с несколькими контекстами мы вынуждены учитывать, в каком контексте определяется переменная. Например, возьмем следующий код: var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function() { this.courses.forEach(function(course){ console.log(this.title, course); }) } } school.printCourses(); Функция printCourses проходит по всем курсам из массива и при их выводе предваряет их значением свойства title. Однако на консоли при запуске программы мы увидим следующее: undefined "JavaScript" undefined "TypeScript" undefined "Java" undefined "Go" Мы видим, что значение this.title не определено, так как this как контекст объекта замещается глобальным контекстом. В этом случае нам надо передать подобное значение this.title или весь контекст объекта. var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function(){ var that = this; this.courses.forEach(function(course){ console.log(that.title, course); }) } } school.printCourses(); Стрелочные функции также позволяют решить данную проблему: var school ={ title: "Oxford", courses: ["JavaScript", "TypeScript", "Java", "Go"], printCourses: function(){ this.courses.forEach ((course)=>console.log(this.title, course)) } } school.printCourses(); Контекстом для стрелочной функции в данном случае будет выступать контекст объекта school. Соответственно, нам недо определять дополнительные переменные для передачи данных в функцию. 10.20. Декомпозиция Декомпозиция (destructuring) позволяет извлечь из объекта отдельные значения в переменные: let user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; let {name, email} = user; console.log(name); // Tom console.log(email); // tom@gmail.com Для декомпозиции объекта переменные помещаются в фигурные скобки и им присваивается объект. Сопоставление между свойствами объекта и переменными идет по имени. Мы можем указать, что мы хотим получить значения свойств объекта в переменные с другим именем: let user = { name: "Tom", age: 24, phone: "+367438787", email: "tom@gmail.com" }; let {name: userName, email: mailAddress} = user; console.log (userName); // Tom console.log (mailAddress); // tom@gmail.com В данном случае свойство name сопоставляется с переменной userName, а поле email - с переменной mailAddress. Также можно декомпозировать массивы: let users = ["Tom", "Sam", "Bob"]; let [a, b, c] = users; console.log(a); // Tom console.log(b); // Sam console.log(c); // Bob Для декомпозиции массива переменные помещаются в квадратные скобки и последовательно получают значения элементов массива. При этом, мы можем пропустить ряд элементов массива, оставив вместо имен переменных пропуски: let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; let [first,,,,fifth] = users; console.log (first); // Tom console.log (fifth); // Alice Выражение first,,,,fifth указывает, что мы хотим получить первый элемент массива в переменную first, затем пропустить три элемента и получить пятый элемент в переменную fifth. Подобным образом можно получить, например, второй и четвертый элементы: let users = ["Tom", "Sam", "Bob", "Ann", "Alice", "Kate"]; let [,second,,forth] = users; console.log(second); // Sam console.log(forth); // Ann Можно совместить получение данных из массива и объекта: let people = [ {name: "Tom", age: 34}, {name: "Bob", age: 23}, {name: "Sam", age: 32} ]; let [,{name}] = people; console.log(name); // Bob В данном случае получаем значение свойства name второго объекта в массиве. Если в функцию в качестве параметра передается массив или объект, то его также можно подобным образом декомпозировать: function display({name:userName, age:userAge}){ console.log(userName, userAge); } function sum([a, b, c]){ var result = a + b + c; console.log(result); } let user = {name:"Alice", age:33, email: "alice@gmail.com"}; let numbers = [3, 5, 7, 8]; display(user); // Alice 33 sum(numbers); // 15 10.21. Классы С внедрением стандарта ES2015 (ES6) в JavaScript появился новый способ определения объектов - с помощью классов. Класс представляет описание объекта, его состояния и поведения, а объект является конкретным воплощением или экземпляром класса. Для определения класса используется ключевое слово class: class Person { } После слова class идет название класса (в данном случае класс называется Person), и затем в фигурных скобках определяется тело класса. Также можно определить анонимный класс и присвоить его переменной: let Person = class {} После этого мы можем создать объекты класса с помощью конструктора: class Person {} let tom = new Person(); let bob = new Person(); Для создания объекта с помощью конструктора сначала ставится ключевое слово new. Затем собственно идет вызов конструктора - по сути вызов функции по имени класса. По умолчанию классы имеют один конструктор без параметров. Поэтому в данном случае при вызове конструктора в него не передается никаких аргументов. Также мы можем определить в классе свои конструкторы. Также класс может содержать свойства и методы: class Person{ constructor(name, age){ this.name = name; this.age = age; } display(){ console.log(this.name, this.age); } } let tom = new Person("Tom", 34); tom.display(); // Tom 34 console.log(tom.name); // Tom Конструктор определяется с помощью метода с именем constructor. По сути это обычный метод, который может принимать параметры. Основная цель конструктора инициализировать объект начальными данными. В данном случае конструктору передаются два значения - для имени и возраста пользователя. Для хранения состояния в классе определяются свойства. Для их определения используется ключевое слово this. В данном случае в классе два свойства: name и age. Поведение класса определяют методы. В данном случае определен метод display(), который выводит значения свойств на консоль. Поскольку мы определили конструктор, который принимает два параметра, то соответственно мы можем передать в конструктор два значения для инициализации объекта: new Person("Tom", 34). Одни классы могут наследоваться от других. Наследование позволяет сократить объем кода в классах-наследниках. Например, определим следующие классы: class Person{ constructor(name, age){ this.name = name; this.age = age; } display(){ console.log(this.name, this.age); } } class Employee extends Person{ constructor(name, age, company){ super(name, age); this.company = company; } display(){ super.display(); console.log("Employee in", this.company); } work(){ console.log(this.name, "is hard working"); } } let tom = new Person("Tom", 34); let bob = new Employee("Bob", 36, "Google"); tom.display(); bob.display(); bob.work(); Для наследования одного класса от другого в определении класса применяется оператор extends, после которого идет название базового класса. То есть в данном случае класс Employee наследуется от класса Person. Класс Person еще называется базовым классом, классом-родителем, суперклассом, а класс Employee классом-наследником, подклассом, производным классом. Производный класс, как и базовый, может определять конструкторы, свойства, методы. Вместе с тем с помощью слова super производный класс может ссылаться на функционал, определенный в базовом. Например, в конструкторе Employee можно вручную не устанавливать свойства name и age, а с помощью выражения super (name, age); вызвать конструктор базового класса и тем самым передать работу по установке этих свойств базовому классу. Подобным образом в методе display в классе Employee через вызов super.display() можно обратиться к реализации метода display класса Person. Консольный вывод программы: Tom 34 Bob 36 Employee in Google Bob is hard working Статические методы вызываются для всего класса в целом, а не для отдельного объекта. Для их определения применяется оператор static. Например: class Person{ constructor(name, age){ this.name = name; this.age = age; } static nameToUpper(person){ return person.name.toUpperCase(); } display(){ console.log(this.name, this.age); } } let tom = new Person("Tom Soyer", 34); let personName = Person.nameToUpper(tom); console.log(personName); // TOM SOYER В данном случае определен статический метод nameToUpper(). В качестве параметра он принимает объект Person и переводит его имя в верхний регистр. Поскольку статический метод относится классу в целом, а не к объекту, то мы не можем использовать в нем ключевое слово this и через него обращаться к свойствам объекта. Задание к лабораторной работе 10. Решить задачи из своего варианта задания. Задача 1 предполагает написание и тестирование функции, обрабатывающей свои аргументы. Часть вариантов может предполагать применение функции-замыкания. Задача 2 предполагает создание объекта JavaScript со свойствами, описывающими объект из Вашего варианта задания и методами, выполняющими основные действия над данными объекта. Варианты задачи 1 1. Создайте функцию repeat(str, n), которая возвращает строку, состоящую и n повторений строки str. Аргумент n по умолчанию равен 2, str — пустой строке. 2. Создайте функцию rgb(), которая будет принимать три числовых аргумента и возвращать строку вида «rgb(23,100,134)». Если аргументы не заданы, считать их равными нулю. 3. Создайте функцию avg() , которая будет находить среднее значение по всем своим аргументам (аргументы величины числовые) 4. Напишите функцию operation(m,n,o), в которой m и n — числовые переменные, а o — функциональный литерал, который берет два аргумента и выполняет математическую операцию над ними 5. Напишите функцию, которая возвращает квадратную матрицу размерностью n x n, последовательно заполненную значениями 1, 2, …, n2 6. Напишите функцию, возвращающую пирамидку из N строк вида, показанного далее: 7. Напишите функцию, возвращающую строку вида '123…N', где N – аргумент функции 8. Напишите функцию, проверяющую корректность даты ДД.ММ.ГГГГ 9. Напишите функцию, определяющую номер дня в году 10.Напишите функцию, определяющую, сколько полных дней осталось до конца года 11.Напишите функцию, определяющую, какого числа в выбранном году будет первое воскресенье октября (День Учителя) 12.Напишите функцию, определяющую знак числа (-1, 0 или 1). Примените функцию для определения знаков всех элементов двумерной матрицы. 13.Напишите функцию, формирующую список всех простых чисел в диапазоне от a до b, a и b – натуральные числа, a
Вывод приложения - в консоли...
Вывод приложения (консоль):