본문으로 건너뛰기

"JavaScript" 태그로 연결된 32개 게시물개의 게시물이 있습니다.

JavaScript 관련 포스트

모든 태그 보기

프로토타입

· 약 5분
준열
프론트엔드 개발자
정의

자바스크립트는 프로토타입 기반 언어입니다. 클래스 기반 언어에서 '상속'을 사용한다면, 프로토타입 기반 언어는 어떤 객체를 원형으로 삼아 이를 복제(참조)하는 방식으로 상속과 비슷한 효과를 만들어냅니다.

프로토타입의 개념 이해

constructor, prototype, instance

새로운 인스턴스를 생성할 때, __proto__라는 프로퍼티가 자동으로 부여됩니다.

let instance = new Constructor();

prototype__proto__는 모두 객체입니다. prototype 객체 내부에는 인스턴스가 사용할 메서드를 저장합니다.

let Person = function (name) {
this.name = name;
};

// Person.prototype에 getName 메서드 추가
Person.prototype.getName = function () {
return this.name;
};

let suzi = new Person("Suzi");
suzi.__proto__.getName(); // undefined
Person.prototype === suzi.__proto__; // true

위 코드에서 undefined가 출력되는 이유는 메서드 호출 시 바로 앞 객체가 this가 되기 때문입니다. __proto__ 객체에는 name 프로퍼티가 없으므로 undefined가 반환됩니다.

__proto__는 생략 가능합니다. 이 덕분에 인스턴스에서 prototype의 메서드를 자신의 것처럼 사용할 수 있습니다.

// 아래 세 표현은 동일합니다
suzi.__proto__.getName
suzi(.__proto__).getName
suzi.getName // __proto__ 생략!

prototype 내부에 없는 메서드는 인스턴스가 직접 호출할 수 없습니다. 생성자 함수에서 직접 접근해야 합니다.

let arr = [1, 2];
arr.forEach(function () {}); // (O) Array.prototype에 있음
Array.isArray(arr); // (O) true - Array 생성자 함수의 스태틱 메서드
arr.isArray(); // (X) TypeError - prototype에 없음

constructor 프로퍼티

prototype 내부에는 constructor 프로퍼티가 있습니다. 이는 생성자 함수 자신을 참조하며, 인스턴스가 자신의 원형을 알 수 있는 수단이 됩니다.

let arr = [1, 2];
Array.prototype.constructor === Array; // true
arr.__proto__.constructor === Array; // true
arr.constructor === Array; // true

// constructor로 새 인스턴스 생성 가능
let arr2 = new arr.constructor(3, 4);
console.log(arr2); // [3, 4]
constructor 변경 시 주의사항

constructor는 변경 가능하지만, 변경해도 인스턴스의 실제 타입이 바뀌지는 않습니다.

let NewConstructor = function () {
console.log("this is new constructor!");
};

let obj = {};
obj.constructor = NewConstructor;
console.log(obj.constructor.name); // 'NewConstructor'
console.log(obj instanceof NewConstructor); // false!

위 예시에서 constructor를 변경해도 instanceof는 여전히 false를 반환합니다. 참조 대상만 변경되었을 뿐 실제 원형이나 데이터 타입은 변하지 않습니다.

동일한 대상을 가리키는 표현:

[Constructor][instance].__proto__.constructor[instance].constructor;
Object.getPrototypeOf([instance]).constructor[Constructor].prototype
.constructor;

동일한 객체에 접근하는 방법:

[Constructor].prototype[instance].__proto__[instance];
Object.getPrototypeOf([instance]);

프로토타입 체인

메서드 오버라이드

인스턴스가 prototype과 동일한 이름의 메서드를 가지면 메서드 오버라이드가 발생합니다.

let Person = function (name) {
this.name = name;
};

Person.prototype.getName = function () {
return this.name;
};

let iu = new Person("지금");

// 인스턴스에 같은 이름의 메서드 추가
iu.getName = function () {
return "바로 " + this.name;
};

console.log(iu.getName()); // '바로 지금'

원본 메서드가 제거되는 것이 아니라 그 위에 덮어씌워지는 구조입니다. 자바스크립트 엔진은 다음 순서로 메서드를 찾습니다:

  1. 인스턴스 자신의 프로퍼티 검색
  2. 없으면 __proto__ 검색

원본 prototype 메서드에 접근하려면 명시적으로 호출해야 합니다:

// call로 this를 iu로 바인딩
console.log(iu.__proto__.getName.call(iu)); // '지금'

프로토타입 체인

배열의 내부 구조를 보면 __proto__ 내부에 다시 __proto__가 있습니다. prototype 객체도 객체이기 때문입니다.

let arr = [1, 2];

// __proto__ 생략 가능
arr.push(3); // arr.__proto__.push(3)와 동일

// 체인을 따라 올라감
arr.__proto__.__proto__.hasOwnProperty(2); // true
// arr → Array.prototype → Object.prototype
용어 정리
  • 프로토타입 체인: __proto__가 연쇄적으로 이어진 구조
  • 프로토타입 체이닝: 체인을 따라가며 검색하는 과정

메서드 오버라이드와 프로토타입 체이닝 예시:

let arr = [1, 2];

// Array.prototype의 toString
Array.prototype.toString.call(arr); // '1,2'

// Object.prototype의 toString
Object.prototype.toString.call(arr); // '[object Array]'

// 기본적으로는 Array.prototype의 toString 사용
arr.toString(); // '1,2'

// 메서드 오버라이드
arr.toString = function () {
return this.join("_");
};
arr.toString(); // '1_2'

어떤 생성자 함수든 prototype은 객체이므로, Object.prototype이 언제나 프로토타입 체인의 최상단에 존재합니다.

객체 전용 메서드의 예외사항

객체에서만 사용할 메서드를 Object.prototype에 정의하면 모든 데이터 타입에서 접근할 수 있게 되는 문제가 발생합니다.

// 잘못된 예시: Object.prototype에 메서드 추가
Object.prototype.getEntries = function () {
let res = [];
for (let prop in this) {
if (this.hasOwnProperty(prop)) {
res.push([prop, this[prop]]);
}
}
return res;
};

let data = [
["object", { a: 1, b: 2, c: 3 }], // [["a",1], ["b",2], ["c",3]]
["number", 345], // []
["string", "abc"], // [["0","a"], ["1","b"], ["2","c"]]
["boolean", false], // []
["array", [1, 2, 3]], // [["0",1], ["1",2], ["2",3]]
];

data.forEach(function (datum) {
console.log(datum[1].getEntries());
});

의도는 객체에만 사용하는 것이었지만, 모든 데이터 타입이 프로토타입 체이닝을 통해 getEntries에 접근할 수 있습니다.

이러한 이유로 객체 전용 메서드는 Object스태틱 메서드로 부여됩니다.

// 올바른 방식: 스태틱 메서드
Object.freeze(instance); // O
instance.freeze(); // X

// 인스턴스를 인자로 직접 전달
Object.keys(obj);
Object.values(obj);
Object.entries(obj);

생성자 함수 Object와 인스턴스 사이에는 this 연결이 불가능하므로, 대상 인스턴스를 인자로 받는 방식으로 구현됩니다.

Object.prototype에는 모든 데이터 타입에서 사용 가능한 범용 메서드만 존재합니다:

  • toString
  • hasOwnProperty
  • valueOf
  • isPrototypeOf

Object.create(null)

예외적으로 Object.create(null)을 사용하면 Object.prototype에 접근할 수 없는 객체를 만들 수 있습니다.

// __proto__가 없는 객체 생성
let _proto = Object.create(null);
_proto.getValue = function (key) {
return this[key];
};

// _proto를 __proto__로 하는 객체 생성
let obj = Object.create(_proto);
obj.a = 1;
console.log(obj.getValue("a")); // 1
console.dir(obj);

obj를 출력하면 __proto__에 오직 getValue 메서드만 존재하며, 일반적인 내장 메서드나 프로퍼티는 보이지 않습니다.

장단점:

  • 장점: 내장 메서드가 제거되어 객체가 가벼워지고 성능 향상
  • 단점: 기본 기능 제약 (toString, hasOwnProperty 등 사용 불가)

다중 프로토타입 체인

자바스크립트 기본 데이터 타입의 프로토타입 체인은 1-2단계로 끝나지만, 사용자가 더 긴 체인을 만들 수 있습니다.

방법: 생성자 함수의 prototype이 다른 생성자 함수의 인스턴스를 바라보게 합니다.

// 유사배열객체를 반환하는 생성자 함수
let Grade = function () {
let args = Array.prototype.slice.call(arguments);
for (let i = 0; i < args.length; i++) {
this[i] = args[i];
}
this.length = args.length;
};

let g = new Grade(100, 80);

g는 배열 형태(인덱스와 length 프로퍼티)를 가졌지만, 배열 메서드는 사용할 수 없는 유사배열객체입니다.

배열 메서드를 사용 가능하게 만들기:

// Grade.prototype을 배열 인스턴스로 교체
Grade.prototype = [];

console.log(g); // Grade(2) [100, 80]

// 이제 배열 메서드 사용 가능
g.pop();
console.log(g); // Grade(1) [100]

g.push(90);
console.log(g); // Grade(2) [100, 90]

이제 g의 프로토타입 체인은 다음과 같습니다:

g 객체
→ Grade.prototype (배열 인스턴스)
→ Array.prototype
→ Object.prototype

g는 자신의 멤버, Grade.prototype, Array.prototype, Object.prototype의 모든 멤버에 접근할 수 있습니다.

정리

  • 생성자 함수를 new 연산자와 함께 호출하면 인스턴스에 __proto__ 프로퍼티가 자동 부여되며, 이는 생성자 함수의 prototype을 참조합니다.

  • __proto__는 생략 가능하여, 인스턴스는 prototype의 메서드를 자신의 것처럼 호출할 수 있습니다.

  • prototype의 constructor 프로퍼티는 생성자 함수 자신을 가리키며, 인스턴스가 자신의 생성자 함수를 알 수 있게 합니다.

  • 프로토타입 체인: __proto__가 연쇄적으로 이어진 것
    프로토타입 체이닝: 체인을 따라 검색하는 것

  • 객체 전용 메서드는 Object.prototype이 아닌 Object에 스태틱 메서드로 부여됩니다. 모든 데이터 타입이 Object.prototype을 상속받기 때문입니다.

클로저

· 약 4분
준열
프론트엔드 개발자

개념

정의

클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상입니다.

function outer() {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
}

var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
  1. outer 함수 내부에 변수 a와 함수 inner를 선언합니다
  2. inner 함수는 외부 함수의 변수 a를 참조합니다
  3. outer 함수는 inner 함수 자체를 반환합니다
  4. outer() 호출로 반환된 inner 함수를 outer2에 저장합니다
  5. outer 함수 실행이 종료되었지만, outer2() 호출 시 여전히 변수 a에 접근 가능합니다

동작 원리

  1. 내부 함수가 외부 함수의 변수를 참조
  2. 내부 함수가 외부로 전달됨
  3. 가비지 컬렉터가 참조된 변수를 메모리에서 유지

메모리 관리

메모리 해제

클로저 사용이 완료되면 참조를 해제한다.

var closure = outer();
// 사용 완료 후
closure = null;
  1. outer() 함수를 호출하여 클로저를 생성합니다
  2. 클로저 사용이 완료되면 closure 변수에 null을 할당합니다
  3. 참조 카운트가 0이 되어 가비지 컬렉터가 메모리를 회수합니다

메모리 해제를 왜 해야할까?

클로저는 외부 함수의 변수를 참조하고 있어서 가비지 컬렉터가 해당 변수들을 메모리에서 제거하지 않는다.

클로저를 계속 사용하지 않는데도 참조가 남아있다면.

  • 메모리 누수 발생: 불필요한 변수들이 메모리에 계속 남아있음
  • 성능 저하: 메모리 사용량이 계속 증가
  • 브라우저 크래시 위험: 메모리 부족으로 인한 문제

특히 이벤트 리스너나 타이머 함수에서 클로저를 사용할 때 제대로 정리하지 않으면 메모리가 계속 쌓인다.

활용 사례

콜백 함수에서 외부 데이터 사용

function attachListener(name) {
return function () {
console.log(name + " 클릭됨");
};
}

button.addEventListener("click", attachListener("버튼1"));
  1. attachListener 함수는 name 매개변수를 받습니다
  2. 내부에서 익명 함수를 반환하는데, 이 함수는 name 변수를 참조합니다
  3. attachListener("버튼1") 호출 시 반환된 함수가 이벤트 리스너로 등록됩니다
  4. 클릭 이벤트 발생 시 클로저로 인해 name 값이 유지되어 "버튼1 클릭됨"이 출력됩니다

정보 은닉

외부에서 접근할 수 없는 private 변수를 구현합니다.

function createCounter() {
var count = 0;

return {
increment: function () {
return ++count;
},
decrement: function () {
return --count;
},
getCount: function () {
return count;
},
};
}

var counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
  1. createCounter 함수 내부에 count 변수를 선언합니다
  2. 객체를 반환하는데, 이 객체의 메소드들이 count를 참조합니다
  3. count 변수는 외부에서 직접 접근할 수 없고, 오직 반환된 메소드를 통해서만 조작 가능합니다
  4. counter.increment() 호출 시 클로저로 인해 count 값이 증가하고 결과를 반환합니다

부분 적용 함수

일부 인자를 미리 고정하고 나머지 인자를 나중에 전달합니다.

function partial(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}

function add(a, b, c, d, e) {
return a + b + c + d + e;
}

var add3 = partial(add, 1, 2, 3);
console.log(add3(4, 5)); // 15
  1. partial 함수는 원본 함수 fn과 미리 적용할 인자들 presetArgs를 받습니다
  2. 새로운 함수를 반환하는데, 이 함수는 나머지 인자들 laterArgs를 받습니다
  3. add3add 함수에 1, 2, 3을 미리 적용한 부분 적용 함수입니다
  4. add3(4, 5) 호출 시 모든 인자(1, 2, 3, 4, 5)가 합쳐져 원본 add 함수가 실행됩니다

디바운스 함수

연속된 함수 호출을 제어하여 마지막 호출만 실행되도록 합니다.

function debounce(eventName, func, wait) {
var timeoutId = null;
return function (event) {
var self = this;
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
};
}

document.body.addEventListener("mousemove", debounce("move", handleMove, 500));
  1. debounce 함수는 이벤트명, 실행할 함수, 대기 시간을 받습니다
  2. timeoutId 변수는 클로저로 유지되어 타이머 상태를 추적합니다
  3. 반환된 함수가 호출될 때마다 기존 타이머를 취소(clearTimeout)합니다
  4. 새로운 타이머를 설정하여 wait 시간 후 func를 실행합니다
  5. 연속 호출 시 타이머가 계속 재설정되어 마지막 호출만 실행됩니다

커링 함수

여러 인자를 받는 함수를 하나의 인자씩 받는 함수로 변환합니다.

function curry5(func) {
return function (a) {
return function (b) {
return function (c) {
return function (d) {
return function (e) {
return func(a, b, c, d, e);
};
};
};
};
};
}
  1. curry5 함수는 5개 인자를 받는 함수를 커링으로 변환합니다
  2. 각 단계에서 하나의 인자를 받아 다음 함수를 반환합니다
  3. 마지막 인자를 받으면 모든 인자를 원본 함수에 전달하여 실행합니다

ES6 화살표 함수로 간소화:

const curriedAdd = (a) => (b) => (c) => a + b + c;

console.log(curriedAdd(1)(2)(3)); // 6
  1. 화살표 함수로 더 간결하게 표현 가능합니다
  2. curriedAdd(1)b => c => 1 + b + c 함수를 반환합니다
  3. curriedAdd(1)(2)c => 1 + 2 + c 함수를 반환합니다
  4. curriedAdd(1)(2)(3)은 최종 결과 6을 반환합니다
부분 적용 함수 vs 커링 함수
  • 부분 적용 함수: 여러 인자를 한 번에 전달 가능
  • 커링 함수: 한 번에 하나의 인자만 전달, 마지막 인자 전달 시 실행

요약

  • 클로저는 내부 함수가 외부 함수의 변수에 접근하는 현상
  • 가비지 컬렉션으로 인해 외부 함수 종료 후에도 변수가 유지됨
  • 정보 은닉, 콜백 처리, 함수형 프로그래밍에 활용
  • 사용 완료 후 null 할당으로 메모리 해제 필요