
이 글은 프론트엔드 직무 면접 질문과 관련된 Github 저장소 front-end-interview-handbook 번역본으로, 저장소에 Translations 폴더의 내용과 동일합니다.
프론트엔드 면접 질문 - JS 질문에 대한 해설입니다.
Pull Request를 통한 제안, 수정 요청 환영합니다.
function foo() {}와 var foo = function() {} 사이에서 foo 사용의 차이에 대해 설명하세요.let, var, const를 사용하여 생성된 변수들의 차이점은 무엇인가요?동기 함수는 블로킹인 반면, 비동기 함수는 그렇지 않습니다. 동기 함수에서는 다음 명령문이 실행되기 전에 앞 명령문이 완료됩니다. 이 경우, 프로그램은 명령문의 순서대로 정확하게 평가되고 명령문 중 하나가 매우 오랜 시간이 걸리면 프로그램 실행이 일시중지됩니다.
비동기 함수는 일반적으로 파라미터를 통해서 콜백을 받고, 비동기 함수가 호출된 후 즉시 다음 줄 실행이 계속됩니다. 콜백은 비동기 작업이 완료되고 호출 스택이 비어 있을 때만 호출됩니다. 웹 서버에서 데이터를 로드하거나 데이터베이스를 쿼리하는 등의 무거운 작업을 비동기식으로 수행하여, 메인 스레드가 긴 작업을 완료할 때까지 블로킹하지 않고 다른 작업을 계속할 수 있습니다(브라우저의 경우 UI가 중지됨).
이벤트 루프는 콜 스택을 모니터하고 태스크 큐에서 수행할 작업이 있는지 확인하는 단일 스레드 루프입니다. 콜 스택이 비어 있고 태스크 큐에 콜백 함수가 있는 경우, 함수는 큐에서 제거되고 실행될 콜 스택으로 푸시됩니다.
Philip Robert의 talk on the Event Loop를 아직 확인하지 않은 경우 확인하십시오. JavaScript 분야에서 가장 많은 조회수를 기록한 동영상 중 하나입니다.
function foo() {}와 var foo = function() {} 사이에서 foo 사용의 차이에 대해 설명하세요.전자는 함수 선언인 반면, 후자는 함수 표현식입니다. 주요한 차이점은 함수 선언은 함수바디가 호이스트되지만, 함수 표현식의 바디는 호이스트되지 않습니다(변수와 동일한 호이스팅 동작을 가짐). 호이스팅에 대한 자세한 설명은 질문 위의 호이스팅을 참조하십시오. 함수 표현식을 정의하기 전에 호출하려고 하면 Uncaught TypeError : XXX is not function 에러가 발생합니다.
함수 선언
foo() // 'FOOOOO'
function foo() {
console.log('FOOOOO')
}
함수 표현식
foo() // Uncaught TypeError: foo는 함수가 아닙니다
var foo = function() {
console.log('FOOOOO')
}
let, var, const를 사용하여 생성된 변수들의 차이점은 무엇인가요?var 키워드를 사용하여 선언된 변수는 함수가 생성된 함수나 함수 밖에서 생성된 함수에 전역 오브젝트로 적용됩니다. let과 const는 블록 스코프입니다. 즉, 가장 가까운 중괄호(function, if-else 블록, for-loop) 내에서만 접근할 수 있습니다.
function foo() {
// 함수 내에서 모든 변수에 접근할 수 있습니다.
var bar = 'bar'
let baz = 'baz'
const qux = 'qux'
console.log(bar) // "bar"
console.log(baz) // "baz"
console.log(qux) // "qux"
}
console.log(bar) // ReferenceError: bar is not defined
console.log(baz) // ReferenceError: baz is not defined
console.log(qux) // ReferenceError: qux is not defined
if (true) {
var bar = 'bar'
let baz = 'baz'
const qux = 'qux'
}
// var로 선언된 변수는 함수 스코프의 어디에서나 접근할 수 있습니다.
console.log(bar) // "bar"
// let과 const로 정의된 변수는 정의된 블록 외부에서 접근할 수 없습니다.
console.log(baz); // ReferenceError: baz is not defined
console.log(qux); // ReferenceError: qux is not defined
var는 변수가 호이스트되도록 허용합니다. 즉, 변수가 선언되기 전에 코드에서 참조될 수 있습니다. let과 const는 이를 허용하지 않고 대신 에러를 던집니다.
console.log(foo) // undefined
var foo = 'foo'
console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization
let baz = 'baz';
console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization
const bar = 'bar'
var을 사용하여 변수를 다시 선언해도 에러가 발생하지 않지만, let과 const는 에러를 발생시킵니다.
var foo = 'foo'
var foo = 'bar'
console.log(foo) // "bar"
let baz = 'baz'
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared
let은 변수의 값을 재할당할 수 있지만, const는 재할당할 수 없다는 점이 다릅니다.
// 괜찮습니다
let foo = 'foo'
foo = 'bar'
// 예외가 발생합니다
const baz = 'baz'
baz = 'qux'
먼저 각각의 예를 살펴보겠습니다.
// ES5 함수 생성자
function Person(name) {
this.name = name
}
// ES6 클래스
class Person {
constructor(name) {
this.name = name
}
}
간단한 생성자의 경우에는 매우 유사합니다.
생성자의 주요 차이점은 상속을 사용할 때 발생합니다. Person의 하위 클래스이면서 studentId 필드를 추가로 가지고 있는 Student 클래스를 만들고자 한다면, 이것이 우리가 추가로 해야할 일입니다.
// ES5 함수 생성자
function Student(name, studentId) {
// 수퍼 클래스의 생성자를 호출하여 수퍼 클래스에서 상속된 멤버를 초기화합니다.
Person.call(this, name)
// 서브 클래스의 멤버를 초기화합니다.
this.studentId = studentId
}
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student
// ES6 클래스
class Student extends Person {
constructor(name, studentId) {
super(name)
this.studentId = studentId
}
}
ES5에서 상속을 사용하는 것이 훨씬 더 불편하며, ES6 버전이 이해하고 기억하기가 더 쉽습니다.
화살표 함수의 한 가지 분명한 이점은 function 키워드를 사용하지 않고도 함수를 생성하는데 필요한 문법을 단순화하는 것입니다.
또한, 화살표 함수 내의 this는, this가 함수를 호출하는 객체에 의해 결정되는 일반 함수와 다르게, 주변 스코프에에 묶입니다.
렉시컬스코프 this는 특히 React 컴포넌트에서 콜백을 호출할 때 유용합니다.
생성자 내부에서 화살표 함수를 메소드로 사용하는 주된 장점은, 함수 생성시 this의 값이 설정되고 그 이후에는 변경할 수 없다는 것입니다.
따라서, 생성자가 새로운 객체를 생성하는데 사용될 때, this는 항상 그 객체를 참조할 것입니다.
예를 들어, 우리가 인수로 first name을 받고, 그 이름을 console.log로 출력하는 Person 생성자가 있다고 해봅시다. 하나는 일반 함수이고, 다른 하나는 화살표 함수일 때,
const Person = function(firstName) {
this.firstName = firstName;
this.sayName1 = function() { console.log(this.firstName); };
this.sayName2 = () => { console.log(this.firstName); };
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// 일반 함수의 'this'값은 변경할 수 있지만, 화살표 함수는 변경할 수 없습니다.
john.sayName1.call(dave); // Dave (because "this" is now the dave object)
john.sayName2.call(dave); // John
john.sayName1.apply(dave); // Dave (because 'this' is now the dave object)
john.sayName2.apply(dave); // John
john.sayName1.bind(dave)(); // Dave (because 'this' is now the dave object)
john.sayName2.bind(dave)(); // John
var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1(); // undefined (because 'this' is now the window object)
var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2(); // John
여기에서 주요 장점은 this는 일반 함수에 대해 변경될 수 있지만, 컨텍스트는 항상 화살표 함수에 대해 동일하게 유지된다는 것입니다. 따라서 화살표 함수를 앱의 다른 부분으로 전달하는 경우에도 컨텍스트 변경에 대해 걱정할 필요가 없습니다.
이는 특히 React 클래스 컴포넌트에서 유용할 수 있습니다. 일반 함수를 사용하는 클릭 핸들러와 같은 클래스 메소드를 정의한 다음, 해당 클릭 핸들러를 하위 컴포넌트의 prop으로 전달하면 상위 컴포넌트의 생성자에서 this도 바인드해야합니다.
대신 화살표 함수를 사용하면, 메소드가 this값을 주위 렉시컬 컨텍스트에서 자동으로 가져오기 때문에 this를 바인딩할 필요가 없습니다.
(좋은 데모, 샘플 코드는 이 기사를 참조하세요.)
고차 함수는 다른 함수를 매개 변수로 사용하여 어떤 데이터를 처리하거나, 결과로 함수를 반환하는 함수입니다. 고차 함수는 반복적으로 수행되는 어떤 연산을 추상화하기 위한 것입니다. 전형적인 예시는 배열과 함수를 인수로 취하는 map입니다. map은 고차 함수를 사용하여 배열의 각 항목을 변환하고, 변환된 데이터로 새로운 배열을 반환합니다. JavaScript에서 흔히 볼 수 있는 다른 예로 forEach, filter, reduce가 있습니다. 다른 함수에서 함수를 반환하는 많은 사용사례가 있기 때문에 고차 함수는 배열을 조작할 필요가 없습니다. Array.prototype.bind는 JavaScript에서 그러한 예시 중 하나입니다.
Map
각 요소를 대문자 문자열로 변환해야하는 이름들을 가진 배열이 있다고 가정해 보겠습니다.
const names = ['irish', 'daisy', 'anna']
일반적인 방법은 다음과 같습니다.
const transformNamesToUppercase = function(names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
.map(transformerFn)을 사용하면 코드가 더 짧아지고 선언적이어집니다.
const transformNamesToUppercase = function(names) {
return names.map(name => name.toUpperCase());
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
디스트럭쳐링은 ES6에서 사용할 수 있는 표현식으로 객체나 배열의 값을 추출하여 다른 변수에 배치하는 간결하고 편리한 방법을 제공합니다.
배열 디스트럭쳐링
// 변수 할당.
const foo = ['one', 'two', 'three']
const [one, two, three] = foo
console.log(one) // "one"
console.log(two) // "two"
console.log(three) // "three"
// 변수 교환
let a = 1
let b = 3
;[a, b] = [b, a]
console.log(a) // 3
console.log(b) // 1
객체 디스트럭쳐링
// 변수 할당.
const o = { p: 42, q: true }
const { p, q } = o
console.log(p) // 42
console.log(q) // true
템플릿 리터럴을 사용하면 문자열 보간을 하거나 문자열에 변수를 포함하는 작업을 간단하게 수행할 수 있습니다. ES2015 이전에는 아래와 같이하는 것이 일반적이었습니다.
var person = { name: 'Tyler', age: 28 };
console.log('Hi, my name is ' + person.name + ' and I am ' + person.age + ' years old!');
// 'Hi, my name is Tyler and I am 28 years old!'
템플릿 리터럴을 사용하면, 대신 이렇게해도 같은 출력을 만들 수 있습니다.
const person = { name: 'Tyler', age: 28 };
console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`);
// 'Hi, my name is Tyler and I am 28 years old!'
템플릿 리터럴을 사용하고 있으며 ${} 플레이스홀더 안에 표현식을 삽입할 수 있다는 것을 나타내기 위해 따옴표(')가 아닌 백틱(`)을 사용한다는 것에 유의하세요.
두번째 유용한 사용사례는 다중행 문자열을 만드는 것입니다. ES2015 이전에는 아래와 같이 다행의 문자열을 만들 수 있었습니다.
console.log('This is line one.\nThis is line two.');
// This is line one.
// This is line two.
또는 코드에서 여러 줄로 나눠진 긴 문자열을 읽기 위해 텍스트 편집기에서 오른쪽으로 스크롤 할 필요가 없도록하려면 다음과 같이 작성할 수 있습니다.
console.log('This is line one.\n' +
'This is line two.');
// This is line one.
// This is line two.
그러나 템플릿 리터럴은 당신이 추가한 간격 그대로 유지됩니다. 예를 들어, 위에 작성한 것과 동일한 다중행 출력을 작성하려면 다음과 같이하면 됩니다.
console.log(`This is line one.
This is line two.`);
// This is line one.
// This is line two.
템플릿 리터럴의 또 다른 사용사례는 간단한 변수 보간을 위한 템플릿 라이브러리의 대체품으로 사용하는 것입니다.
const person = { name: 'Tyler', age: 28 };
document.body.innerHTML = `
<div>
<p>Name: ${person.name}</p>
<p>Name: ${person.age}</p>
</div>
`
.innerHTML을 사용하면 코드가 XSS의 영향을 받을 수 있습니다. 사용자로부터 입력받은 데이터인 경우 표시하기 전에 데이터를 안전하게 만드세요!
currying은 둘 이상의 매개 변수가 있는 함수가 여러 함수로 분리된 패턴으로, 직렬로 호출하면, 필요한 모든 매개 변수가 한 번에 하나씩 누적됩니다. 이 기법은 함수형 스타일로 작성된 코드를 읽고, 합성하기 더 쉬워진 경우 유용할 수 있습니다. 함수를 currying하려면, 하나의 함수로 시작하여, 하나의 매개 변수를 취하는 일련의 함수로 분리해야 합니다.
function curry(fn) {
if (fn.length === 0) {
return fn
}
function _curried(depth, args) {
return function(newArgument) {
if (depth - 1 === 0) {
return fn(...args, newArgument)
}
return _curried(depth - 1, [...args, newArgument])
}
}
return _curried(fn.length, [])
}
function add(a, b) {
return a + b
}
var curriedAdd = curry(add)
var addFive = curriedAdd(5)
var result = [0, 1, 2, 3, 4, 5].map(addFive) // [5, 6, 7, 8, 9, 10]
ES6의 spread 문법은 함수형 패러다임에서 코딩할 때 매우 유용합니다. 왜냐하면 Object.create, slice나 라이브러리 함수를 사용하지 않고도 배열이나 객체의 복사본을 쉽게 만들 수 있기 때문입니다. 이 언어 기능은 Redux나 Rx.js를 사용하는 프로젝트에서 많이 사용됩니다.
function putDookieInAnyArray(arr) {
return [...arr, 'dookie']
}
var result = putDookieInAnyArray(['I', 'really', "don't", 'like']) // ["I", "really", "don't", "like", "dookie"]
var person = {
name: 'Todd',
age: 29
}
var copyOfTodd = { ...person }
ES6의 rest 구문은 함수에 전달할 임의의 수의 인수를 포함하는 약식을 제공합니다. 이는 데이터의 배열을 채우기보다는 데이터를 가져와서 배열로 채우는 spread 구문의 반대와 비슷하며, 배열이나 객체 디스트럭쳐링 할당뿐만 아니라 함수 인수에서도 작동합니다.
function addFiveToABunchOfNumbers(...numbers) {
return numbers.map(x => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]
const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const { e, f, ...others } = {
e: 1,
f: 2,
g: 3,
h: 4,
}; // e: 1, f: 2, others: { g: 3, h: 4 }
이것은 Javascript 환경에 따라 다릅니다.
클라이언트(브라우저 환경)에서는, 변수/함수가 전역 스코프(window)에 선언되어있는 한 모든 스크립트가 이를 참조할 수 있습니다. 또는, 보다 모듈형 접근 방식을 위해 RequireJS를 통해 비동기 모듈 정의(AMD)를 이용합니다.
서버(Node.js)에서 일반적인 방법은 CommanJS를 사용하는 것입니다. 각 파일은 모듈로 취급되며, 변수와 함수를 module.exports 객체에 붙여서 내보낼 수 있습니다.
ES2015에서는 AMD 및 commonJS를 모두 대체하기 위한 모듈 문법을 정의합니다. 이 기능은 브라우저 및 노드 환경 모두에서 지원됩니다.
정적 클래스 멤버(속성/메서드)는 클래스의 특정 인스턴스와 묶이지 않으며, 어떤 인스턴스가 이를 참조하는지에 관계없이 동일한 값을 가집니다. 정적 속성은 일반적으로 설정(configuration) 변수이며 정적 메서드는 일반적으로 인스턴스의 상태에 의존하지 않는 순수 유틸리티 함수입니다.
좋은 글 번역 감사합니다