JavaScript — мощный язык, который является одним из основных строительных блоков Интернета. Этот мощный язык также имеет некоторые свои особенности. Например, знаете ли вы, что 0 === -0
имеет значение true или что Number("")
возвращает 0?
Дело в том, что иногда эти причуды могут заставить вас почесать голову или даже задаться вопросом, был ли Брэндон Эйх под кайфом в тот день, когда он изобрел JavaScript. Ну, тут дело не в том, что JavaScript — плохой язык программирования или зло, как говорят его критики. Со всеми языками программирования связаны какие-то странности, и JavaScript не является исключением.
В этом сообщении блога мы увидим подробное объяснение некоторых важных вопросов на собеседовании по JavaScript. Моя цель — подробно объяснить эти вопросы на собеседовании, чтобы мы могли понять основные концепции и, надеюсь, решить другие подобные вопросы на собеседованиях.
1. Более внимательный взгляд на операторы + и –
console.log(1 + '1' - 1);
Можете ли вы угадать поведение операторов + и - JavaScript в ситуациях, подобных приведенной выше?
Когда JavaScript встречает 1 + '1'
, он обрабатывает выражение с помощью оператора +. Одним интересным свойством оператора + является то, что он предпочитает конкатенацию строк, когда один из операндов является строкой. В нашем случае «1» — это строка, поэтому JavaScript неявно преобразует числовое значение 1 в строку. Следовательно, 1 + '1'
становится '1' + '1'
, в результате чего получается строка '11'
.
Теперь наше уравнение '11' - 1
. Поведение оператора - совершенно противоположное. Он отдает приоритет числовому вычитанию независимо от типов операндов. Если операнды не имеют числового типа, JavaScript выполняет неявное приведение для преобразования их в числа. В этом случае '11'
преобразуется в числовое значение 11, а выражение упрощается до 11 - 1
.
Собираем все вместе:
'11' - 1 = 11 - 1 = 10
2-Дублирование элементов массива
Рассмотрим следующий код JavaScript и попытайтесь найти в нем проблемы:
function duplicate(array) { for (var i = 0; i < array.length; i++) { array.push(array[i]); } return array; } const arr = [1, 2, 3]; const newArr = duplicate(arr); console.log(newArr);
В этом фрагменте кода нам необходимо создать новый массив, содержащий дублированные элементы входного массива. При первоначальной проверке кажется, что код создает новый массив newArr
, дублируя каждый элемент исходного массива arr
. Однако внутри самой функции duplicate
возникает критическая проблема.
Функция duplicate
использует цикл для обхода каждого элемента данного массива. Но внутри цикла он добавляет новый элемент в конец массива, используя метод push()
. Это каждый раз делает массив длиннее, создавая проблему, при которой цикл никогда не останавливается. Условие цикла (i < array.length
) всегда остается истинным, поскольку массив продолжает увеличиваться. Это заставляет цикл продолжаться вечно, что приводит к зависанию программы.
Чтобы решить проблему бесконечного цикла, вызванную растущей длиной массива, вы можете сохранить начальную длину массива в переменной перед входом в цикл. Затем вы можете использовать эту начальную длину в качестве ограничения для итерации цикла. Таким образом, цикл будет выполняться только для исходных элементов массива, и на него не будет влиять рост массива из-за добавления дубликатов. Вот модифицированная версия кода:
function duplicate(array) { var initialLength = array.length; // Store the initial length for (var i = 0; i < initialLength; i++) { array.push(array[i]); // Push a duplicate of each element } return array; } const arr = [1, 2, 3]; const newArr = duplicate(arr); console.log(newArr);
В выводе будут показаны дублированные элементы в конце массива, и цикл не приведет к бесконечному циклу:
[1, 2, 3, 1, 2, 3]
3-Разница между прототипом и __proto__
Свойство prototype
— это атрибут, связанный с функциями-конструкторами в JavaScript. Функции конструктора используются для создания объектов в JavaScript. Когда вы определяете функцию-конструктор, вы также можете присоединять свойства и методы к ее свойству prototype
. Эти свойства и методы затем становятся доступными для всех экземпляров объектов, созданных из этого конструктора. Таким образом, свойство theprototype
служит общим хранилищем для методов и свойств, общих для разных экземпляров.
Рассмотрим следующий фрагмент кода:
// Constructor function function Person(name) { this.name = name; } // Adding a method to the prototype Person.prototype.sayHello = function() { console.log(`Hello, my name is ${this.name}.`); }; // Creating instances const person1 = new Person("Haider Wain"); const person2 = new Person("Omer Asif"); // Calling the shared method person1.sayHello(); // Output: Hello, my name is Haider Wain. person2.sayHello(); // Output: Hello, my name is Omer Asif.
В этом примере у нас есть функция-конструктор с именем Person
. Расширяя Person.prototype
методом, подобным sayHello
, мы добавляем этот метод в цепочку прототипов всех экземпляров Person
. Это позволяет каждому экземпляру Person
получать доступ и использовать общий метод. Вместо того, чтобы каждый экземпляр имел собственную копию метода.
С другой стороны, свойство __proto__
, часто произносимое как «прототип dunder», существует в каждом объекте JavaScript. В JavaScript все, кроме примитивных типов, можно рассматривать как объект. У каждого из этих объектов есть прототип, который служит ссылкой на другой объект. Свойство __proto__
— это просто ссылка на этот объект-прототип. Объект-прототип используется в качестве резервного источника свойств и методов, когда исходный объект ими не обладает. По умолчанию при создании объекта его прототипу присваивается значение Object.prototype
.
Когда вы пытаетесь получить доступ к свойству или методу объекта, JavaScript выполняет процесс поиска, чтобы найти его. Этот процесс включает в себя два основных этапа:
- Собственные свойства объекта: JavaScript сначала проверяет, обладает ли сам объект нужным свойством или методом. Если свойство обнаружено внутри объекта, к нему обращаются и используют напрямую.
- Поиск цепочки прототипов. Если свойство не найдено в самом объекте, JavaScript просматривает прототип объекта (на который ссылается свойство
__proto__
) и ищет там свойство. Этот процесс продолжается рекурсивно вверх по цепочке прототипов до тех пор, пока свойство не будет найдено или пока поиск не достигнетObject.prototype
.
Если свойство не найдено даже в Object.prototype
, JavaScript возвращает undefined
, указывая, что свойство не существует.
4-Области
При написании кода JavaScript важно понимать концепцию области видимости. Область действия означает доступность или видимость переменных в различных частях вашего кода. Прежде чем приступить к примеру, если вы не знакомы с подъемом и тем, как выполняется код JavaScript, вы можете узнать об этом по этой ссылке. Это поможет вам более детально понять, как работает код JavaScript.
Давайте подробнее рассмотрим фрагмент кода:
function foo() { console.log(a); } function bar() { var a = 3; foo(); } var a = 5; bar();
В коде определены две функции foo()
и bar()
и переменная a
со значением 5
. Все эти объявления происходят в глобальном масштабе. Внутри функции bar()
объявляется переменная a
, которой присваивается значение 3
. Итак, когда вызывается функцияbar()
, какое значение a
, по вашему мнению, она выведет?
Когда движок JavaScript выполняет этот код, объявляется глобальная переменная a
, которой присваивается значение 5
. Затем вызывается функция bar()
. Внутри функции bar()
объявляется локальная переменная a
, которой присваивается значение 3
. Эта локальная переменная a
отличается от глобальной переменной a
. После этого функция foo()
вызывается из функции bar()
.
Внутри функции foo()
оператор console.log(a)
пытается записать значение a
. Поскольку в области действия функции foo()
не определена локальная переменная a
, JavaScript просматривает цепочку области видимости, чтобы найти ближайшую переменную с именем a
. Цепочка областей действия относится ко всем различным областям видимости, к которым функция имеет доступ, когда пытается найти и использовать переменные.
Теперь давайте рассмотрим вопрос о том, где JavaScript будет искать переменную a
. Будет ли он просматривать область действия функции bar
или будет исследовать глобальную область видимости? Как выяснилось, JavaScript будет осуществлять поиск в глобальной области, и такое поведение обусловлено концепцией, называемой лексическая область видимости.
Лексическая область видимости относится к области действия функции или переменной на момент ее написания в коде. Когда мы определили функцию foo
, ей был предоставлен доступ как к собственной локальной, так и к глобальной области видимости. Эта характеристика остается неизменной независимо от того, где мы вызываем функцию foo
— внутри функции bar
или если мы экспортируем ее в другой модуль и запускаем там. Лексическая область видимости не определяется, где мы вызываем функцию.
Результатом этого является то, что выходные данные всегда будут одинаковыми: значение a
, найденное в глобальной области видимости, которое в данном случае равно 5
.
Однако если бы мы определили функцию foo
внутри функции bar
, возник бы другой сценарий:
function bar() { var a = 3; function foo() { console.log(a); } foo(); } var a = 5; bar();
В этой ситуации лексическая область действия foo
будет охватывать три различные области: собственную локальную область, область действия функции bar
и глобальную область. Лексическая область действия определяется тем, где вы размещаете свой код в исходном коде во время компиляции.
Когда этот код выполняется, foo
находится внутри функции bar
. Такое расположение меняет динамику прицела. Теперь, когда foo
пытается получить доступ к переменной a
, он сначала выполняет поиск в своей локальной области. Поскольку он не находит там a
, он расширит поиск до области действия функции bar
. И вот, там существует a
со значением 3. В результате оператор консоли напечатает 3
.
5-объектное принуждение
const obj = { valueOf: () => 42, toString: () => 27 }; console.log(obj + '');
Один из интригующих аспектов, который стоит изучить, — это то, как JavaScript обрабатывает преобразование объектов в примитивные значения, такие как строки, числа или логические значения. Это интересный вопрос, который проверяет, знаете ли вы, как приведение работает с объектами.
Это преобразование имеет решающее значение при работе с объектами в таких сценариях, как конкатенация строк или арифметические операции. Чтобы добиться этого, JavaScript использует два специальных метода: valueOf
и toString
.
Метод valueOf
является фундаментальной частью механизма преобразования объектов JavaScript. Когда объект используется в контексте, требующем примитивного значения, JavaScript сначала ищет метод valueOf
внутри объекта. В тех случаях, когда метод valueOf
отсутствует или не возвращает подходящее примитивное значение, JavaScript возвращается к методу toString
. Этот метод отвечает за предоставление строкового представления объекта.
Возвращаясь к исходному фрагменту кода:
const obj = { valueOf: () => 42, toString: () => 27 }; console.log(obj + '');
Когда мы запускаем этот код, объект obj
преобразуется в примитивное значение. В этом случае метод valueOf
возвращает 42
, который затем неявно преобразуется в строку из-за конкатенации с пустой строкой. Следовательно, вывод кода будет 42
.
Однако в тех случаях, когда метод valueOf
отсутствует или не возвращает подходящее примитивное значение, JavaScript возвращается к методу toString
. Давайте изменим наш предыдущий пример:
const obj = { toString: () => 27 }; console.log(obj + '');
Здесь мы удалили метод valueOf
, оставив только метод toString
, который возвращает число 27
. В этом сценарии JavaScript будет использовать метод toString
для преобразования объектов.
6-Понимание ключей объектов
При работе с объектами в JavaScript важно понимать, как ключи обрабатываются и назначаются в контексте других объектов. Рассмотрите следующий фрагмент кода и потратьте некоторое время, чтобы угадать результат:
let a = {}; let b = { key: 'test' }; let c = { key: 'test' }; a[b] = '123'; a[c] = '456'; console.log(a);
На первый взгляд может показаться, что этот код должен создать объект a
с двумя разными парами ключ-значение. Однако результат совсем другой из-за обработки ключей объекта в JavaScript.
JavaScript использует метод toString()
по умолчанию для преобразования ключей объекта в строки. Но почему? В JavaScript ключи объекта всегда являются строками (или символами) или автоматически преобразуются в строки посредством неявного приведения. Когда вы используете любое значение, отличное от строки (например, число, объект или символ), в качестве ключа в объекте, JavaScript внутренне преобразует это значение в его строковое представление, прежде чем использовать его в качестве ключа.
Следовательно, когда мы используем объекты b
и c
в качестве ключей в объекте a
, оба преобразуются в одно и то же строковое представление: [object Object]
. Из-за такого поведения второе назначение a[b] = '123';
перезапишет первое назначение a[c] = '456';
. Разберем код шаг за шагом:
let a = {};
: Инициализирует пустой объектa
.let b = { key: 'test' };
: Создает объектb
со свойствомkey
, имеющим значение'test'
.let c = { key: 'test' };
: определяет другой объектc
с той же структурой, что иb
.a[b] = '123';
: устанавливает значение'123'
для свойства с ключом[object Object]
в объектеa
.a[c] = '456';
: обновляет значение до'456'
для того же свойства с ключом[object Object]
в объектеa
, заменяя предыдущее значение.
Оба назначения используют одинаковую ключевую строку [object Object]
. В результате второе присвоение перезаписывает значение, установленное первым присвоением.
Когда мы регистрируем объект a
, мы наблюдаем следующий вывод:
{ '[object Object]': '456' }
7. Оператор двойного равенства
console.log([] == ![]);
Это немного сложно. Итак, как вы думаете, какой будет результат? Давайте оценим это шаг за шагом. Давайте сначала начнем с рассмотрения типов обоих операндов:
typeof([]) // "object" typeof(![]) // "boolean"
Для []
это object
, это понятно. Поскольку все в JavaScript является объектом, включая массивы и функции. Но как операнд![]
имеет тип boolean
? Давайте попробуем это понять. Когда вы используете !
с примитивным значением, происходят следующие преобразования:
- Ложные значения. Если исходное значение является ложным (например,
false
,0
,null
,undefined
,NaN
или пустая строка''
), применение!
преобразует его вtrue
. - Истинные значения. Если исходное значение является правдивым (любое значение, не являющееся ложным), применение
!
преобразует его вfalse
.
В нашем случае []
— это пустой массив, который является достоверным значением в JavaScript. Поскольку []
правдиво, ![]
становится false
. Таким образом, наше выражение принимает вид:
[] == ![] [] == false
Теперь давайте двинемся дальше и разберемся с оператором ==
. Каждый раз, когда два значения сравниваются с помощью оператора ==
, JavaScript выполняет алгоритм абстрактного сравнения. Алгоритм имеет следующие шаги:
Как видите, этот алгоритм учитывает типы сравниваемых значений и выполняет необходимые преобразования.
В нашем случае обозначим x
как []
, а y
как ![]
. Мы проверили типы x
и y
и обнаружили x
как объект и y
как логическое значение. Поскольку y — логическое значение, а x — объект, применяется условие 7 из абстрактного алгоритма сравнения на равенство:
Если Type(y) имеет логическое значение, верните результат сравнения x == ToNumber(y).
Это означает, что если один из типов является логическим, нам необходимо преобразовать его в число перед сравнением. Каково значение ToNumber(y)? Как мы видели, []
является истинным значением, отрицание делает его false
. В результате Number(false)
становится 0
.
[] == false [] == Number(false) [] == 0
Теперь у нас есть сравнение [] == 0
и на этот раз в игру вступает условие 8:
Если Type(x) — это String или Number, а Type(y) — Object, верните результат сравнения x == ToPrimitive(y).
Исходя из этого условия, если один из операндов является объектом, мы должны преобразовать его в примитивное значение. Именно здесь на сцену выходит алгоритм ToPrimitive. Нам нужно преобразовать x
, то есть []
, в примитивное значение. Массивы — это объекты в JavaScript. Как мы видели ранее, при преобразовании объектов в примитивы в игру вступают методы valueOf
и toString
. В этом случае valueOf
возвращает сам массив, который не является допустимым примитивным значением. В результате мы переходим к toString
для вывода. Применение метода toString
method к пустому массиву приводит к получению пустой строки, которая является допустимым примитивом:
[] == 0 [].toString() == 0 "" == 0
Преобразование пустого массива в строку дает нам пустую строку ""
, и теперь мы сталкиваемся с сравнением: "" == 0
.
Теперь, когда один из операндов имеет тип string
, а другой — тип number
, условие 5 выполняется:
Если Type(x) — строка, а Type(y) — число, верните результат сравнения ToNumber(x) == да.
Следовательно, нам нужно преобразовать пустую строку ""
в число, что даст нам 0
.
"" == 0 ToNumber("") == 0 0 == 0
Наконец, оба операнда имеют один и тот же тип и выполняется условие 1. Поскольку оба имеют одинаковое значение, окончательный результат:
0 == 0 // true
До сих пор мы использовали принуждение в последних нескольких рассмотренных нами вопросах, что является важной концепцией при освоении JavaScript и решении подобных вопросов на собеседовании, которых, как правило, задают много. Я очень рекомендую вам прочитать мою подробную публикацию в блоге о принуждении. Он объясняет эту концепцию ясно и подробно. Вот ссылка.
8-Закрытие
Это один из самых известных вопросов на собеседованиях, связанных с закрытием:
const arr = [10, 12, 15, 21]; for (var i = 0; i < arr.length; i++) { setTimeout(function() { console.log('Index: ' + i + ', element: ' + arr[i]); }, 3000); }
Если вы знаете результат, то хорошо. Итак, давайте попробуем понять этот фрагмент. На первый взгляд кажется, что этот фрагмент даст нам вывод:
Index: 0, element: 10 Index: 1, element: 12 Index: 2, element: 15 Index: 3, element: 21
Но здесь дело обстоит иначе. Из-за концепции замыканий и того, как JavaScript обрабатывает область видимости переменных, фактический результат будет другим. Когда обратные вызовы setTimeout
выполняются после задержки в 3000 миллисекунд, все они будут ссылаться на одну и ту же переменную i
, которая будет иметь конечное значение 4
после завершения цикла. В результате вывод кода будет таким:
Index: 4, element: undefined Index: 4, element: undefined Index: 4, element: undefined Index: 4, element: undefined
Такое поведение происходит потому, что ключевое слово var
не имеет области действия блока, а обратные вызовы setTimeout
фиксируют ссылку на ту же переменную i
. При выполнении обратных вызовов все они видят окончательное значение i
, то есть 4
, и пытаются получить доступ к arr[4]
, то есть undefined
.
Чтобы добиться желаемого результата, вы можете использовать ключевое слово let
для создания новой области видимости для каждой итерации цикла, гарантируя, что каждый обратный вызов захватывает правильное значение i
:
const arr = [10, 12, 15, 21]; for (let i = 0; i < arr.length; i++) { setTimeout(function() { console.log('Index: ' + i + ', element: ' + arr[i]); }, 3000); }
С этой модификацией вы получите ожидаемый результат:
Index: 0, element: 10 Index: 1, element: 12 Index: 2, element: 15 Index: 3, element: 21
Использование let
создает новую привязку для i
на каждой итерации, гарантируя, что каждый обратный вызов ссылается на правильное значение.
Часто разработчики знакомы с решением, использующим ключевое слово let
. Однако иногда собеседования могут пойти еще дальше и побудить вас решить проблему без использования let
. В таких случаях альтернативный подход предполагает создание замыкания путем немедленного вызова функции (IIFE) внутри цикла. Таким образом, каждый вызов функции имеет собственную копию i
. Вот как вы можете это сделать:
const arr = [10, 12, 15, 21]; for (var i = 0; i < arr.length; i++) { (function(index) { setTimeout(function() { console.log('Index: ' + index + ', element: ' + arr[index]); }, 3000); })(i); }
В этом коде немедленно вызываемая функция (function(index) { ... })(i);
создает новую область для каждой итерации, фиксируя текущее значение i
и передавая его в качестве параметра index
. Это гарантирует, что каждая функция обратного вызова получит свое собственное отдельное значение index
, что предотвращает проблемы, связанные с замыканием, и дает ожидаемый результат:
Index: 0, element: 10 Index: 1, element: 12 Index: 2, element: 15 Index: 3, element: 21
Спасибо за чтение. Я надеюсь, что этот пост поможет вам в подготовке к собеседованию. Если у вас есть дополнительные вопросы, не стесняйтесь обращаться к нам. Я всегда рад помочь.
Давайте соединим:
LinkedIn
Twitter
Повышение уровня кодирования
Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:
- 👏 Аплодируйте истории и подписывайтесь на автора 👉
- 📰 Дополнительную информацию смотрите в публикации Level Up Coding.
- 💰 Бесплатный курс интервью по программированию ⇒ Просмотреть курс
🔔 Следите за нами: Твиттер | Линкедин | "Новостная рассылка"
🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите потрясающую работу