[JS] 코딩 인터뷰를 꿰뚫는 JS 코어 개념 총정리
목차
JS 타입 시스템
원시타입 · 참조타입 · 래퍼타입
원시타입(Primitive Type)의 특징
- 변수에 할당하면 값이 복사되어 들어감
- 원시값이 할당된 변수들은 각자 고유한 값을 독립적으로 가짐
- 종류:
string,number,boolean,null,undefined
typeof
- 원시값의 종류를 확인하는 연산자
typeof null === "object"— null은 예외이므로 주의
참조타입(Reference Type)의 종류
| 타입 | 리터럴 |
|---|---|
| 객체 | {} |
| 배열 | [] |
| 함수 | function |
| 날짜 | Date |
| 정규표현식 | RegExp |
원시타입을 제외한 모든 값은 참조타입이다.
참조타입의 특징
- 변수에 값을 직접 저장하지 않음
- 변수에 저장되는 것은 메모리 상 객체 위치를 가리키는 포인터
- “무엇이 저장되느냐”가 원시타입과 참조타입의 핵심 차이
원시 래퍼타입(Wrapper Type)
원시타입도 래퍼타입을 통해 객체처럼 사용할 수 있다: String, Number, Boolean
오토박싱(autoboxing): 원시타입을 객체처럼 사용할 때 JS 내부에서 임시 인스턴스를 생성하고, 코드 실행 후 즉시 파괴하는 과정.
var one = 1;
var two = 2;
one = two;
one = 3;
console.log(two);
console.log(typeof 1); // "number"
console.log(typeof "hi"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof null); // "object" ← 주의!
console.log(typeof undefined);// "undefined"
var objOne = {one:1};
var objTwo = {two:2};
objTwo = objOne;
objTwo.one = 3;
console.log(objOne); // {one: 3} ← 같은 객체를 참조하므로 함께 변경됨
console.log(objTwo); // {one: 3}
var str = "hello world";
console.log(str.length);
var name = "test";
console.log(name.concat("coin"));
var name2 = "test";
var temp = new String(name);
console.log(temp.concat("coin"));
// 임시 변수를 만들어서 메서드 실행 후 바로 해제 -> JS 내부에서 벌어지는 오토박싱
temp = null;
var name3 = "test";
name3.coin = "coin";
console.log(name3.coin); // undefined — 원시타입을 참조타입처럼 사용 시 내부적으로 temp 사용
// JS 내부 동작
var name3 = "test";
var temp = new String(name);
temp.coin = "coin";
temp = null; // 객체 해제
var temp = new String(name);
console.log(temp.coin); // undefined
this 바인딩 — call / apply / bind
this가 필요한 이유
객체 안에서 변수명을 하드코딩하면 이름 변경이나 재사용 시 문제가 생긴다.
var myDiner = {
name: "김치찌개",
menu: function() {
console.log("오늘저녁은 " + myDiner.name); // 변수명이 바뀌면 전부 수정해야 함
}
};
myDiner.menu();
this를 사용하면 함수를 여러 객체에서 재사용할 수 있다.
function menuGlobal() {
console.log("오늘 저녁은 " + this.name);
}
var myDiner = { name: "김치찌개", menu: menuGlobal };
var yourDiner = { name: "된장찌개", menu: menuGlobal };
yourDiner.menu(); // "오늘 저녁은 된장찌개"
this는 일반적으로 함수를 호출한 객체에 자동으로 할당된다.
call()
this 값을 지정하면서 인수를 하나씩 전달해 함수를 실행한다.
function menuGlobal(item) {
console.log(item + this.name);
}
var myDiner = { name: "김치찌개" };
var yourDiner = { name: "된장찌개" };
menuGlobal.call(myDiner, "오늘 저녁은 ");
menuGlobal.call(yourDiner, "오늘 저녁은 ");
apply()
this 값을 지정하면서 인수를 배열로 묶어 한 번에 전달한다.
function menuGlobal(item1, item2) {
[item1, item2].forEach(function(el) {
console.log("오늘 저녁은 " + el + " " + this.name);
}, this);
}
var myDiner = { name: "김치찌개" };
var yourDiner = { name: "된장찌개" };
menuGlobal.apply(myDiner, ["묵은지", "삼겹살"]);
menuGlobal.apply(yourDiner, ["두부", "애호박"]);
| 메서드 | 인수 전달 방식 | 공통점 |
|---|---|---|
call | 개별 인수 나열 | this 지정 후 즉시 실행 |
apply | 배열로 묶어 전달 | this 지정 후 즉시 실행 |
bind()
ES5에서 추가. this가 고정된 새 함수를 반환한다(즉시 실행하지 않음).
function menuGlobal(item) {
console.log("오늘 저녁은 " + this.name);
}
var myDiner = { name: "김치찌개" };
var yourDiner = { name: "된장찌개" };
var menuGlobalForMe = menuGlobal.bind(myDiner); // myDiner로 고정
var menuGlobalForYou = menuGlobal.bind(yourDiner); // yourDiner로 고정
console.log(menuGlobal("삼겹살")); // undefined (전역 this에는 name 없음)
myDiner.meneMine = menuGlobalForYou;
myDiner.meneMine("묵은지"); // "오늘 저녁은 된장찌개" — bind된 객체 유지
화살표 함수와 this
화살표 함수는 **호출 객체가 아닌 상위 스코프의 this**를 그대로 사용한다.
function menuGlobal(item1, item2) {
console.log(this);
[item1, item2].forEach((el) => {
console.log("오늘 저녁은 " + el + " " + this.name); // 외부 함수의 this 사용
});
}
var myDiner = { name: "김치찌개" };
var yourDiner = { name: "된장찌개" };
menuGlobal.apply(myDiner, ["묵은지", "삼겹살"]);
정리
| 방법 | 동작 |
|---|---|
call / apply | 호출 시 this 지정 |
bind | this가 고정된 새 함수 반환 |
| 화살표 함수 | 상위 스코프의 this 상속 |
유효범위(Scope)
var func1 = function(){
var a = 1;
var b = 2;
console.log(a+b);
return a+b;
};
var a = 20;
console.log(b);
func1();
JS의 스코프 종류는 세 가지다.
| 스코프 | 설명 |
|---|---|
| 전역 스코프 | 스크립트 어디서든 접근 가능. 협업·라이브러리 사용 시 충돌 위험 |
| 함수 스코프 | 함수 내부 변수는 외부에서 접근 불가. 함수 어디서든 접근 가능 |
| 블록 스코프 (ES6) | {} 안에서만 접근 가능. 블록 종료 시 변수 해제 |
함수 스코프 예시
var func1 = function() {
var a = 1;
var b = 2;
var func2 = function() {
var b = 5; // func1의 b와 별개
var c = 6;
a = a + b + c;
console.log(a); // 12
};
func2();
};
func1();
블록 스코프 예시
function test() {
val = "hello"; // var/let 선언 없이 사용하면 전역 변수가 됨
var val2 = "world";
}
test();
console.log(val); // "hello" (전역으로 누출됨)
if (true) {
var value = "hello"; // var → 블록 밖에서도 접근 가능
}
console.log(value); // "hello"
if (true) {
let value = "world"; // let → 블록 스코프, 밖에서 접근 불가
}
console.log(value); // "hello" (if 블록 안의 let value와 별개)
스코프는 변수의 접근성과 생존 기간을 제어하며, 이름 충돌 방지와 자동 메모리 관리를 가능하게 한다.
클로저(Closure)
클로저는 **함수와 함수가 선언될 때의 환경(스코프)**으로 이루어진다. 외부 함수가 실행 종료된 이후에도 그 환경은 계속 유지된다.
function outer() {
var outerVar = "I'm from outer!";
function inner() {
console.log(outerVar);
}
return inner;
}
var closureFunc = outer(); // outer 실행 → inner 반환
closureFunc(); // "I'm from outer!" 출력 — outer가 종료됐어도 outerVar 접근 가능
내부 함수가 외부 함수보다 오래 살아있는 경우
var outer = function() {
var a = 1;
var inner = function() {
var b = 5;
var c = 6;
a = a + b + c;
console.log(a);
};
inner();
};
outer();
inner를 반환하면 outer 종료 후에도 a에 접근할 수 있다.
var outer = function() {
var a = 1;
var inner = function() {
var b = 5;
var c = 6;
a = a + b + c;
console.log(a); // 12
};
return inner;
};
var newInner = outer(); // outer 실행 결과(inner 함수) 저장
newInner(); // inner 실행 — outer의 a에 여전히 접근 가능
클로저의 세 가지 특징
- 내부 함수: 외부 함수 내부에서 선언되며, 외부 함수의 지역 변수를 참조할 수 있다.
- 환경 기억: 선언 시점의 외부 스코프(변수·매개변수)에 대한 참조를 유지한다.
- 외부 함수에서 반환: 반환된 후에도 외부 함수의 스코프 참조가 살아 있어, 외부 함수의 지역 변수가 사라지지 않는다.
클로저에서 외부 함수의 변수는 내부 함수에서 읽기·쓰기 모두 가능하다.
결국 클로저는 “폐쇄된 공간에 대한 접근 권한을 가진 함수”다.
활용: 비공개 데이터를 가진 객체
// 즉시 실행 함수(IIFE)로 클로저를 생성하고 결과를 person에 할당
var person = (function() {
var age = 15; // 외부에서 직접 접근 불가
return {
name: "Wade",
getAge: function() {
console.log(age);
return age;
},
setAge: function(val) {
age = val;
console.log(age);
}
};
})();
person.age = 30; // 프로퍼티 추가일 뿐, 내부 age는 변경되지 않음
person.getAge(); // 15
- JS는 내부 함수에서 자신을 포함하는 외부 함수의 스코프에 접근 가능
- 내부 함수가 살아있는 상태에서 외부 함수가 파괴되면, 외부 함수 변수에 대한 접근 권한은 내부 함수만 가지게 된다
- 이것이 클로저다
생성자(Constructor)
new 연산자가 붙은 함수를 생성자라 하며, 인스턴스를 만들 수 있다. new Object(), new Array() 등은 JS 내장 생성자다.
var myArray = new Array(1, 2, 3);
console.log(myArray);
생성자 만드는 법
// 생성자 함수: 이름은 관례상 대문자로 시작
function Person(name) {
this.name = name;
}
// new 연산자로 인스턴스 생성
var p = new Person("Wade");
생성자와 인스턴스의 관계는 instanceof와 constructor로 확인한다.
function MyOwn() {}
var myObj = new MyOwn();
console.log(myObj instanceof MyOwn); // true
console.log(myObj.constructor === MyOwn); // true
생성자를 쓰는 이유
동일한 프로퍼티와 메서드를 가진 객체를 대량 생산할 수 있다.
function Food(taste) {
this.taste = taste;
this.smell = function() {
console.log(this.taste + " 냄새가 난다");
};
}
var myFood1 = new Food("특제 파스타");
var myFood2 = new Food("해물 파스타");
var myFood3 = new Food("알리오 파스타");
myFood1.smell();
myFood2.smell();
myFood3.smell();
new 유무 | this 참조 | 반환값 |
|---|---|---|
| 있음 | 새 인스턴스 | 자동으로 인스턴스 반환 |
| 없음 | 전역 객체 | undefined |
프로토타입(Prototype)
위 방식에서는 인스턴스마다 smell 함수가 별도로 생성된다. 메모리 낭비다.
function smell() {
console.log(this.name + " 냄새가 난다");
}
function Food(name) {
this.name = name;
this.smell = smell; // 외부 함수를 참조해도 인스턴스별 프로퍼티는 생성됨
}
var myFood = new Food("로제 파스타");
var myFood2 = new Food("창란젓");
console.log(myFood.smell === myFood2.smell); // true (같은 함수 참조)
prototype에 등록하면 모든 인스턴스가 하나의 함수를 공유한다.
Food.prototype.smell = function() {
console.log(this.name + " 냄새가 난다");
};
function Food(name) {
this.name = name;
// smell을 따로 정의할 필요 없음
}
var myFood = new Food("로제 파스타");
var myFood2 = new Food("청국장");
myFood.smell();
myFood2.smell();
console.log(myFood.smell === myFood2.smell); // true
프로토타입 체인 구조:
인스턴스 → [[Prototype]] → 생성자.prototype → Object.prototype.constructor
프로토타입 체인: 인스턴스에서 생성자의
prototype을 타고 올라가며 프로퍼티를 탐색하는 현상.
- JS는 생성자의
prototype프로퍼티를 통해 타입의 특징을 정의한다 - 모든 인스턴스는 내부에
[[Prototype]]프로퍼티를 가지며 이를 통해 생성자의 프로토타입을 추적한다
서브타입(Subtype) & 슈퍼타입(Supertype)
Sausage와 FireSausage를 각각 정의하면 공정 코드가 중복된다.
function Sausage(el1, el2) {
this.inside1 = el1;
this.inside2 = el2;
}
Sausage.prototype.taste = function() {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
};
// 불맛 소시지 — Sausage와 중복 코드 발생
function FireSausage(el1, el2, el3) {
this.inside1 = el1;
this.inside2 = el2;
this.inside3 = el3;
}
FireSausage.prototype.taste = function() {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
};
FireSausage.prototype.flavor = function() {
return this.inside3 + "의 풍미도 있다!";
};
생성자 훔치기(Constructor Stealing)
call()을 사용해 상위 생성자의 프로퍼티를 하위 타입에서 재사용한다.
function Sausage(el1, el2) {
this.inside1 = el1;
this.inside2 = el2;
}
Sausage.prototype.taste = function() {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
};
function FireSausage(el1, el2, el3) {
Sausage.call(this, el1, el2); // Sausage의 프로퍼티를 FireSausage 인스턴스에 복사
this.inside3 = el3;
}
var myNewSausage = new FireSausage("돼지고기", "마늘", "불맛");
console.log(myNewSausage.inside1); // "돼지고기"
console.log(myNewSausage.inside2); // "마늘"
console.log(myNewSausage.inside3); // "불맛"
Sausage.call(this, ...)—this는FireSausage의 인스턴스이므로,Sausage생성자가FireSausage인스턴스에 프로퍼티를 세팅하게 된다.
프로토타입 상속(Prototype Inheritance)
생성자 훔치기만으로는 Sausage.prototype의 메서드(taste)를 상속받지 못한다. Object.create()로 프로토타입 체인을 연결한다.
function Sausage(el1, el2) {
this.inside1 = el1;
this.inside2 = el2;
}
Sausage.prototype.taste = function () {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
}
var mySausage = new Sausage("돼지고기", "마늘");
console.log(mySausage.taste());
function FireSausage(el1, el2, el3) {
Sausage.call(this, el1, el2);
this.inside3 = el3;
}
var myNewSausage = new FireSausage("돼지고기", "마늘", "불맛");
FireSausage.prototype = Object.create(Sausage.prototype);
FireSausage.prototype.constructor = FireSausage;
FireSausage.prototype.flavor = function () {
return this.inside3 + "의 풍미도 있다!";
}
Object.create() 메서드는 프로토타입에 참조할 생성자의 프로토타입 프로퍼티를 설정한다.
function Sausage(el1, el2) {
this.inside1 = el1;
this.inside2 = el2;
}
Sausage.prototype.taste = function() {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
};
function FireSausage(el1, el2, el3) {
Sausage.call(this, el1, el2);
this.inside3 = el3;
}
// Sausage.prototype을 프로토타입으로 하는 객체를 FireSausage.prototype에 할당
FireSausage.prototype = Object.create(Sausage.prototype);
FireSausage.prototype.constructor = FireSausage; // constructor 복원
FireSausage.prototype.flavor = function() {
return this.inside3 + "의 풍미도 있다!";
};
var myNewSausage = new FireSausage("돼지고기", "마늘", "불맛");
console.log(myNewSausage.taste()); // Sausage.prototype에서 상속
console.log(myNewSausage.flavor()); // FireSausage.prototype에 정의
코드가 짧을 때는 차이가 작아 보이지만, 규모가 커질수록 상속은 매우 유용하다.
| 용어 | 의미 |
|---|---|
| 하위타입(Subtype) | 프로퍼티를 상속받는 타입 (FireSausage) |
| 상위타입(Supertype) | 프로퍼티를 상속해 주는 타입 (Sausage) |
call/apply로 상위 생성자를 호출해 프로퍼티를 상속받는 것을 생성자 훔치기라 한다Object.create()로 인스턴스의 프로토타입 대상을 지정할 수 있다
클래스(Class)
클래스는 **생성자 + 프로토타입 패턴을 다른 언어처럼 보기 좋게 개선한 문법(Syntactic Sugar)**이다. 내부 동작은 동일하다.
class User {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this);
console.log(this.name);
}
}
클래스 없이 만들기 위해서는 아래의 작업을 거쳐야 한다.
var me = new User("jaehyun");
me.sayName();
// constructor 메서드는 Object.prototype.constructor와는 다르다.
// 클래스의 생성자 함수라고 할 수 있다.
function UserOld(name) {
this.name = name;
}
UserOld.prototype.sayName = function() {
console.log(this);
console.log(this.name);
};
var user = new UserOld("jaehyun");
user.sayName();
클래스의
constructor는Object.prototype.constructor와 다르다. 클래스 자체의 생성자 함수다.
클래스 상속 — extends
extends 연산자로 상위 타입의 프로퍼티를 간결하게 상속받을 수 있다. call + Object.create() 조합보다 명시적이다.
class Sausage {
constructor(el1, el2) {
this.inside1 = el1;
this.inside2 = el2;
}
taste() {
return this.inside1 + "와 " + this.inside2 + " 맛이 난다!";
}
}
class FireSausage extends Sausage {}
var classicFireSausage = new FireSausage("소고기", "파");
console.log(classicFireSausage.inside1); // "소고기"
console.log(classicFireSausage.taste()); // "소고기와 파 맛이 난다!"
super — 부모 생성자 호출
자식 클래스에서 constructor를 선언하면 부모의 constructor를 덮어쓰게 된다. super()로 부모 생성자를 명시적으로 호출해야 한다.
아래 두 코드 중 위는 에러, 아래는 정상이다. 자식 클래스에 constructor 선언 시 부모의 constructor를 덮어쓰기 때문이다.
// 에러: super() 없이 this 사용 → ReferenceError
class FireSausage extends Sausage {
constructor(el1, el2, el3) {
this.inside3 = el3;
}
flavor() {
return this.inside3 + "의 풍미도 있다!";
}
}
var classicFireSausage = new FireSausage("소고기", "파", "불맛");
console.log(classicFireSausage.taste());
console.log(classicFireSausage.flavor());
// 정상: super()로 부모 constructor 먼저 호출
class FireSausage extends Sausage {
constructor(el1, el2, el3) {
super(el1, el2); // Sausage의 constructor 실행
this.inside3 = el3;
}
flavor() {
return this.inside3 + "의 풍미도 있다!";
}
}
var classicFireSausage = new FireSausage("소고기", "파", "불맛");
console.log(classicFireSausage.taste()); // Sausage에서 상속
console.log(classicFireSausage.flavor()); // FireSausage 고유 메서드
| 키워드 | 역할 |
|---|---|
extends | 상위 타입의 프로퍼티·메서드 상속 |
super() | 부모 클래스의 constructor 호출 (자식 constructor 내 필수) |
호이스팅(Hoisting)
함수 선언과 변수 선언은 코드 실행 시 해당 스코프의 최상단으로 끌어올려진다. 이 현상을 호이스팅이라 한다.
케이스 1 — 함수 선언문 vs 함수 표현식
console.log(test()); // "test" — 함수 선언문은 전체가 호이스팅됨
console.log(testValue()); // TypeError: testValue is not a function
function test() {
return "test";
}
var testValue = function() {
return "testValue";
};
var testValue 선언은 끌어올려지지만, 함수 표현식 값은 끌어올려지지 않아 호출 시점에 undefined다.
test // "test" (함수 선언문은 전체가 호이스팅됨)
testValue() // Uncaught TypeError: testValue is not a function
// (var 선언만 끌어올려지고 함수 표현식 값은 아직 undefined)
케이스 2 — 변수 선언만 끌어올려짐
console.log(testValue); // undefined (선언은 올라가지만 값은 아직 없음)
console.log(undeclared); // ReferenceError: undeclared is not defined
var testValue = 100;
console.log(testValueVar()); // TypeError: testValueVar is not a function
var testValueVar = function testValue() {
return "hoist test";
};
케이스 3 — if 블록 안의 var 선언
console.log(test); // undefined
var condition = false;
if (condition) {
var test = "this is test"; // var → 블록 무관, 전역으로 선언됨
}
var test 선언이 전역 스코프 최상단으로 끌어올려져 undefined가 출력된다.
케이스 4 — 함수 스코프 안의 변수
console.log(test()); // "hoisttest"
console.log(value); // ReferenceError: value is not defined
function test() {
var value = "hoist"; // 함수 스코프 안에서 최상단으로 올라감
return value + "test";
}
함수 선언은 호이스팅되어 test() 호출이 가능하지만, value는 함수 스코프 내부에 있어 외부에서 접근할 수 없다.
케이스 5 — let / const / class는 호이스팅 안 됨
console.log(test1); // ReferenceError
console.log(test2); // ReferenceError
console.log(Tester); // ReferenceError
let test1 = "let value";
const test2 = "const value";
class Tester {
constructor() {
this.name = "test";
}
}
let tester = new Tester();
let, const, class는 **TDZ(Temporal Dead Zone, 일시적 사각지대)**에 배치된다. 선언이 실행된 후에야 TDZ에서 제거되어 사용 가능해진다.
정리
| 선언 방식 | 호이스팅 | 초기값 | 비고 |
|---|---|---|---|
function 선언문 | O | 함수 전체 | 선언 전에 호출 가능 |
var | O | undefined | 값은 끌어올려지지 않음 |
let / const | X | — | TDZ 적용 |
class | X | — | TDZ 적용 |