Прототипическое наследование
За исключением null
и undefined
, каждый примитивный тип данных имеет прототип — соответствующую объектную обертку, предоставляющую методы для работы со значениями. Когда для примитива вызывается поиск метода или свойства, JavaScript незаметно оборачивает примитив и вызывает метод или вместо этого выполняет поиск свойств в объекте-оболочке.
Например, строковый литерал не имеет собственных методов, но вы можете вызвать для него метод .toUpperCase()
благодаря соответствующей оболочке объекта String
:
"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL
Это называется прототипным наследованием — наследование свойств и методов от соответствующего конструктора значения.
Number.prototype
> Number { 0 }
> constructor: function Number()
> toExponential: function toExponential()
> toFixed: function toFixed()
> toLocaleString: function toLocaleString()
> toPrecision: function toPrecision()
> toString: function toString()
> valueOf: function valueOf()
> <prototype>: Object { … }
Вы можете создавать примитивы, используя эти конструкторы, вместо того, чтобы просто определять их по их значению. Например, использование конструктора String
создает строковый объект, а не строковый литерал: объект, который содержит не только наше строковое значение, но и все унаследованные свойства и методы конструктора.
const myString = new String( "I'm a string." );
myString;
> String { "I'm a string." }
typeof myString;
> "object"
myString.valueOf();
> "I'm a string."
По большей части результирующие объекты ведут себя в соответствии со значениями, которые мы использовали для их определения. Например, несмотря на то, что определение числового значения с помощью new Number
приводит к созданию объекта, содержащего все методы и свойства прототипа Number
, вы можете использовать математические операторы для этих объектов так же, как и для числовых литералов:
const numberOne = new Number(1);
const numberTwo = new Number(2);
numberOne;
> Number { 1 }
typeof numberOne;
> "object"
numberTwo;
> Number { 2 }
typeof numberTwo;
> "object"
numberOne + numberTwo;
> 3
Вам очень редко придется использовать эти конструкторы, поскольку встроенное в JavaScript прототипное наследование означает, что они не несут никакой практической пользы. Создание примитивов с помощью конструкторов также может привести к неожиданным результатам, поскольку результатом является объект, а не простой литерал:
let stringLiteral = "String literal."
typeof stringLiteral;
> "string"
let stringObject = new String( "String object." );
stringObject
> "object"
Это может усложнить использование операторов строгого сравнения:
const myStringLiteral = "My string";
const myStringObject = new String( "My string" );
myStringLiteral === "My string";
> true
myStringObject === "My string";
> false
Автоматическая вставка точки с запятой (ASI)
При анализе сценария интерпретаторы JavaScript могут использовать функцию, называемую автоматической вставкой точки с запятой (ASI), чтобы попытаться исправить случаи пропущенных точек с запятой. Если анализатор JavaScript обнаруживает недопустимый токен, он пытается добавить точку с запятой перед этим токеном, чтобы исправить потенциальную синтаксическую ошибку, если выполняется одно или несколько из следующих условий:
- Этот токен отделяется от предыдущего токена разрывом строки.
- Этот токен
}
. - Предыдущий токен —
)
, а вставленная точка с запятой будет завершающей точкой с запятой оператораdo
…while
.
Для получения дополнительной информации обратитесь к правилам ASI .
Например, пропуск точки с запятой после следующих операторов не приведет к синтаксической ошибке из-за ASI:
const myVariable = 2
myVariable + 3
> 5
Однако ASI не может учитывать несколько операторов в одной строке. Если вы пишете более одного оператора в одной строке, обязательно разделяйте их точкой с запятой:
const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier
const myVariable = 2; myVariable + 3;
> 5
ASI — это попытка исправления ошибок, а не своего рода синтаксическая гибкость, встроенная в JavaScript. Обязательно используйте точки с запятой там, где это необходимо, чтобы не полагаться на них для создания правильного кода.
Строгий режим
Стандарты, управляющие написанием JavaScript, развились далеко за пределы того, что рассматривалось на ранних стадиях разработки языка. Каждое новое изменение ожидаемого поведения JavaScript не должно вызывать ошибок на старых веб-сайтах.
ES5 решает некоторые давние проблемы с семантикой JavaScript, не нарушая при этом существующие реализации, вводя «строгий режим» — способ выбрать более ограничительный набор языковых правил либо для всего скрипта, либо для отдельной функции. Чтобы включить строгий режим, используйте строковый литерал "use strict"
, за которым следует точка с запятой, в первой строке скрипта или функции:
"use strict";
function myFunction() {
"use strict";
}
Строгий режим предотвращает определенные «небезопасные» действия или устаревшие функции, выдает явные ошибки вместо обычных «тихих» ошибок и запрещает использование синтаксиса, который может конфликтовать с будущими функциями языка. Например, ранние проектные решения, касающиеся области видимости переменных, повышали вероятность того, что разработчики ошибочно «загрязняли» глобальную область видимости при объявлении переменной, независимо от содержащего ее контекста, опуская ключевое слово var
:
(function() {
mySloppyGlobal = true;
}());
mySloppyGlobal;
> true
Современные среды выполнения JavaScript не могут исправить это поведение без риска взлома любого веб-сайта, который использует его, по ошибке или намеренно. Вместо этого современный JavaScript предотвращает это, позволяя разработчикам выбирать строгий режим для новой работы и включать строгий режим по умолчанию только в контексте новых функций языка, где они не нарушают устаревшие реализации:
(function() {
"use strict";
mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal
Вы должны написать "use strict"
в качестве строкового литерала . Литерал шаблона ( use strict
) не будет работать. Вы также должны включить "use strict"
перед любым исполняемым кодом в его предполагаемом контексте. В противном случае интерпретатор игнорирует это.
(function() {
"use strict";
let myVariable = "String.";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal
(function() {
let myVariable = "String.";
"use strict";
console.log( myVariable );
sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope
По ссылке, по значению
Любая переменная, включая свойства объекта, параметры функции и элементы массива , набора или карты , может содержать либо примитивное значение, либо ссылочное значение .
Когда примитивное значение присваивается одной переменной другой, механизм JavaScript создает копию этого значения и присваивает ее переменной.
Когда вы присваиваете объект (экземпляры классов, массивы и функции) переменной, вместо создания новой копии этого объекта переменная содержит ссылку на сохраненную позицию объекта в памяти. По этой причине изменение объекта, на который ссылается переменная, изменяет сам объект, на который ссылается, а не только значение, содержащееся в этой переменной. Например, если вы инициализируете новую переменную переменной, содержащей ссылку на объект, а затем используете новую переменную для добавления свойства к этому объекту, свойство и его значение добавляются к исходному объекту:
const myObject = {};
const myObjectReference = myObject;
myObjectReference.myProperty = true;
myObject;
> Object { myProperty: true }
Это важно не только для изменения объектов, но и для выполнения строгих сравнений, поскольку строгое равенство между объектами требует, чтобы обе переменные ссылались на один и тот же объект, чтобы получить значение true
. Они не могут ссылаться на разные объекты, даже если эти объекты структурно идентичны:
const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};
myObject === myNewObject;
> false
myObject === myReferencedObject;
> true
Распределение памяти
JavaScript использует автоматическое управление памятью, а это означает, что память не нужно явно выделять или освобождать в ходе разработки. Хотя детали подходов движков JavaScript к управлению памятью выходят за рамки этого модуля, понимание того, как распределяется память, дает полезный контекст для работы со ссылочными значениями.
В памяти есть две «области»: «стек» и «куча». В стеке хранятся статические данные — примитивные значения и ссылки на объекты, поскольку фиксированный объем пространства, необходимый для хранения этих данных, может быть выделен до выполнения сценария. В куче хранятся объекты, которым требуется динамически выделяемое пространство, поскольку их размер может меняться во время выполнения. Память освобождается процессом, называемым «сборкой мусора», который удаляет из памяти объекты без ссылок.
Основная тема
JavaScript — это по сути однопоточный язык с «синхронной» моделью выполнения, что означает, что он может выполнять только одну задачу за раз. Этот контекст последовательного выполнения называется основным потоком .
Основной поток используется другими задачами браузера, такими как анализ HTML, рендеринг и повторный рендеринг частей страницы, запуск CSS-анимации и обработка взаимодействий пользователя, начиная от простых (например, выделение текста) до сложных (например, взаимодействие с формой). элементы). Поставщики браузеров нашли способы оптимизировать задачи, выполняемые основным потоком, но более сложные сценарии по-прежнему могут использовать слишком много ресурсов основного потока и влиять на общую производительность страницы.
Некоторые задачи могут выполняться в фоновых потоках, называемых Web Workers , с некоторыми ограничениями:
- Рабочие потоки могут работать только с отдельными файлами JavaScript.
- У них значительно ограничен или вообще отсутствует доступ к окну браузера и пользовательскому интерфейсу.
- Они ограничены в том, как могут взаимодействовать с основным потоком.
Эти ограничения делают их идеальными для целенаправленных ресурсоемких задач, которые в противном случае могли бы занять основной поток.
Стек вызовов
Структура данных, используемая для управления «контекстами выполнения» — активно выполняемым кодом — представляет собой список, называемый стеком вызовов (часто просто «стеком»). Когда скрипт выполняется впервые, интерпретатор JavaScript создает «глобальный контекст выполнения» и помещает его в стек вызовов, при этом операторы внутри этого глобального контекста выполняются по одному, сверху вниз. Когда интерпретатор встречает вызов функции во время выполнения глобального контекста, он помещает «контекст выполнения функции» для этого вызова на вершину стека, приостанавливает глобальный контекст выполнения и выполняет контекст выполнения функции.
Каждый раз, когда вызывается функция, контекст выполнения функции для этого вызова помещается на вершину стека, чуть выше текущего контекста выполнения. Стек вызовов работает по принципу «первым вошел — первым вышел», что означает, что самый последний вызов функции, который является самым высоким в стеке, выполняется и продолжается до тех пор, пока он не будет разрешен. Когда эта функция завершена, интерпретатор удаляет ее из стека вызовов, а контекст выполнения, содержащий этот вызов функции, снова становится самым высоким элементом в стеке и возобновляет выполнение.
Эти контексты выполнения фиксируют любые значения, необходимые для их выполнения. Они также устанавливают переменные и функции, доступные в области действия функции, на основе ее родительского контекста, а также определяют и устанавливают значение ключевого слова this
в контексте функции.
Цикл событий и очередь обратных вызовов
Такое последовательное выполнение означает, что асинхронные задачи, включающие функции обратного вызова, такие как получение данных с сервера, ответ на взаимодействие с пользователем или ожидание таймеров, установленных с помощью setTimeout
или setInterval
, будут либо блокировать основной поток до завершения этой задачи, либо неожиданно прерывать ее. текущий контекст выполнения в момент добавления контекста выполнения функции обратного вызова в стек. Чтобы решить эту проблему, JavaScript управляет асинхронными задачами, используя управляемую событиями «модель параллелизма», состоящую из «цикла событий» и «очереди обратных вызовов» (иногда называемой «очередью сообщений»).
Когда асинхронная задача выполняется в основном потоке, контекст выполнения функции обратного вызова помещается в очередь обратного вызова, а не поверх стека вызовов. Цикл событий — это шаблон, который иногда называют реактором , который постоянно опрашивает состояние стека вызовов и очереди обратных вызовов. Если в очереди обратного вызова есть задачи и цикл событий определяет, что стек вызовов пуст, задачи из очереди обратного вызова помещаются в стек по одной для выполнения.