
* 모던 자바스크립트 Deep Dive을 토대로 공부한 것을 정리한 내용으로, 모든 인용문은 모던 자바스크립트 Deep Dive의 문구를 인용한 것입니다.
- 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
- 변수와 자료구조(객체, 배열 등)에 저장할 수 있다.
- 함수의 매개변수에 전달할 수 있다.
- 함수의 반환값으로 사용할 수 있다.
자바스크립트의 함수는 위의 조건을 모두 만족하므로 일급 객체이다. 함수가 일급 객체라는 것은 함수를 객체와 동일하게 사용할 수 있다는 의미이다. 객체는 값이므로 함수는 값과 동일하게 취급할 수 있다. 따라서 함수는 값을 사용할 수 있는 곳이라면 어디서든지 리터럴로 정의할 수 있으며, 런타임에 함수 객체로 평가된다. 때문에 함수를 일반 객체와 같이 함수의 매개변수에 전달할 수 있으며, 반환값으로도 사용할 수 있다. 이러한 특징 덕분에 자바스크립트에서 함수형 프로그래밍이 가능하다.
아래의 예제로 자바스크립트 함수가 위의 조건을 모두 만족하는지 알아보자.
// 1. 함수는 무명의 리터럴로 생성할 수 있다.
// 2. 함수는 변수에 저장할 수 있다.
// 런타임(할당 단계)에 함수 리터럴이 평가되어 함수 객체가 생성되고 변수에 할당된다.
const increase = function (num) {
return ++num;
};
const decrease = function (num) {
return --num;
};
// 2. 함수는 객체에 저장할 수 있다.
const auxs = { increase, decrease };
// 3. 함수의 매개변수에게 전달할 수 있다.
// 4. 함수의 반환값으로 사용할 수 있다.
function makeCounter(aux) {
let num = 0;
return function () {
num = aux(num);
return num;
};
}
// 3. 함수는 매개변수에게 함수를 전달할 수 있다.
const increaser = makeCounter(auxs.increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
// 3. 함수는 매개변수에게 함수를 전달할 수 있다.
const decreaser = makeCounter(auxs.decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
함수는 객체이지만 일반 객체와 차이점이 있다.
| 일반 객체 | 함수 |
|---|---|
| 호출 불가능 | 호출 가능 |
| 프로퍼티 없음 | 프로퍼티 소유 |
함수는 객체이므로 프로퍼티를 가질 수 있다. 아래 예제를 통해서 square 함수의 모든 프로퍼티의 프로퍼티 어트리뷰트를 Object.getOwnPropertyDescriptions 메서드로 확인해보자.
function square(number) {
return number * number;
}
console.log(Object.getOwnPropertyDescriptors(square));
/*
{
length: {value: 1, writable: false, enumerable: false, configurable: true},
name: {value: "square", writable: false, enumerable: false, configurable: true},
arguments: {value: null, writable: false, enumerable: false, configurable: false},
caller: {value: null, writable: false, enumerable: false, configurable: false},
prototype: {value: {...}, writable: true, enumerable: false, configurable: false}
}
*/
// __proto__는 square 함수의 프로퍼티가 아니다.
console.log(Object.getOwnPropertyDescriptor(square, '__proto__')); // undefined
// __proto__는 Object.prototype 객체의 접근자 프로퍼티다.
// square 함수는 Object.prototype 객체로부터 __proto__ 접근자 프로퍼티를 상속받는다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
위의 예제에서 확인할 수 있듯이, arguments, caller, lengthm name, prototype 프로퍼티는 모두 함수 객체의 데이터 프로퍼티이며 일반 객체에는 없는 함수 고유의 프로퍼티다.
그러나 __proto__는 접근자 프로퍼티이며, 함수 객체 고유의 프로퍼티가 아닌 Object.prototype 객체의 프로퍼티를 상속받은 것을 확인할 수 있다. 즉, Object.prototype 객체의 __proto__ 접근자 프로퍼티는 모든 객체가 사용할 수 있다.
함수 객체의 프로퍼티에 대해 간단하게 살펴보자.
함수 객체의 arguments 프로퍼티 값은 arguments 객체이다. 함수 호출 시 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체이며, 함수 내부에서 지역 변수처럼 사용된다. 즉, 함수 외부에서는 참조할 수 없다.
function multiply(x, y) {
console.log(arguments);
return x * y;
}
console.log(multiply()); // NaN
console.log(multiply(1)); // NaN
console.log(multiply(1, 2)); // 2
console.log(multiply(1, 2, 3)); // 2
ECMAScript 사양에 포함되지 않는 비표준 프로퍼티로, 이후 표준화될 예정도 없으니 사용하지 않는 게 좋다고 한다.
function foo(func) {
return func();
}
function bar() {
return 'caller : ' + bar.caller;
}
// 브라우저에서의 실행한 결과
console.log(foo(bar)); // caller : function foo(func) {...}
console.log(bar()); // caller : null
위의 예제를 살펴보면 bar 함수를 foo 함수 내에서 호출했다. 때문에 bar 함수의 caller 프로퍼티는 bar 함수를 호출한 foo 함수를 가리킨다. 그냥 함수 호출 bar()의 경우 bar 함수를 호출한 함수는 없기 때문에 caller 프로퍼티는 null을 반환한다.
caller는 ECMAScript 5의 strict mode에서 사용할 수 없으며, 대부분의 최신 JavaScript 엔진에서도 지원되지 않는다. 때문에 caller를 사용한 예제를 찾아보긴 어려웠다.
이러한 제한이 있는 이유는 arguments.caller가 함수 호출 스택을 조작하거나 디버깅을 어렵게 만들 수 있기 때문이다. 따라서 가능하면 arguments.caller 대신 함수 이름을 직접 호출하는 등의 다른 방법을 고려하여 함수 호출과 관련된 작업을 수행하는 것이 좋다고 한다.
length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킨다.
function foo() {}
console.log(foo.length); // 0
function bar(x) {
return x;
}
console.log(bar.length); // 1
function baz(x, y) {
return x * y;
}
console.log(baz.length); // 2
이때, 사용 시 주의해야 할 점은 arguments 객체의 length 프로퍼티는 인자의 개수를 가리키고, 함수 객체의 length 프로퍼티는 매개 변수의 개수를 가리키므로 주의해야 한다.
name 프로퍼티는 함수 이름을 나타낸다. name 프로퍼니틑 ES6 이전까지는 비표준이었다가 ES6 이후 정식 표준이 되었다.
// 기명 함수 표현식
var namedFunc = function foo() {};
console.log(namedFunc.name); // foo
// 익명 함수 표현식
var anonymousFunc = function() {};
// ES5: name 프로퍼티는 빈 문자열을 값으로 갖는다.
// ES6: name 프로퍼티는 함수 객체를 가리키는 변수 이름을 값으로 갖는다.
console.log(anonymousFunc.name); // anonymousFunc
// 함수 선언문(Function declaration)
function bar() {}
console.log(bar.name); // bar
위 예제에서도 알 수 있지만, ES5에서는 익명 함수 표현식의 경우 name 프로퍼티는 빈 문자열을 값으로 갖는다. 그러나, ES6에서는 함수 객체를 가리키는 식별자를 값으로 갖는다.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다. __proto__ 프로퍼티는 [[Prototype]] 내부 슬롯이 가리키는 프로토타입 객체에 접근하기 위해 사용하는 접근자 프로퍼티다. 내부 슬롯에는 직접 접근할 수 없으며, 간접적인 접근 방법을 제공하는 경우에 한하여 접근할 수 있다. 때문에 [[Prototype]] 내부 슬롯에도 직접 접근할 수 없으며 __proto__ 접근자 프로퍼티를 통해 간접적으로 프로토타입 객체에 접근할 수 있다.
JavaScript는 흔히 프로토타입 기반 언어(prototype-based language)라 부른다.
이는 모든 객체들이 메소드와 속성들을 상속받기 위한 명세로 프로토 타입 객체를 가진다는 의미이다. 그렇다면 프로토타입은 무엇일까?
JavaScript에는 클래스라는 개념이 없기 때문에, 기존의 객체를 복사하여 새로운 객체를 생성하는 프로토타입 기반의 언어이다. 프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어낸다. 이렇게 생성된 객체는 또 다른 객체의 원형이 될 수 있다.
JavaScript가 프로토타입을 기반으로 상속을 구현하는 이유는 여러가지가 있겠지만, 가장 큰 이유는 불필요한 중복을 제거하는 것이다. 중복을 제거하는 바법은 기존의 코드를 적극적으로 재사용하느 것인데, 코드 재사용은 개발 비용을 현저히 줄일 수 있는 잠재력이 있으며 코드를 재사용하는 방법이 프로토타입인 것이다.
프로토타입 객체란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다. 프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공한다. 프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조다. [[Prototype]]에 저장되는 프로토타입은 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장된다.
다시 __proto__ 접근자 프로퍼티로 돌아오면
const obj = { a: 1 };
// 객체 리터럴 방식으로 생성한 객체의 프로토타입 객체는 Object.prototype이다.
console.log(obj.__proto__ === Object.prototype); // true
// 객체 리터럴 방식으로 생성한 객체는 프로토타입 객체인 Object.prototype의 프로퍼티를 상속받는다.
// hasOwnProperty 메서드는 Object.prototype의 메서드다.
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('__proto__')); // false
위 예제에서 알 수 있듯이, hasOwnProperty 메서드는 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true를 반환하고 상속받은 프로토타입의 프로퍼티 키인 경우 false를 반환한다.
prototype 프로퍼티는 생성자 함수로 호출할 수 있는 constructor 만 소유할 수 있는 프로퍼티이며, 반대로 non-constructor 에는 존재하지 않는다.
prototype 프로퍼티는 함수가 객체를 생성하는 생성자 함수로 호출될 때 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.
// 함수 객체는 prototype 프로퍼티를 소유한다.
(function () {}).hasOwnProperty('prototype'); // -> true
// 일반 객체는 prototype 프로퍼티를 소유하지 않는다.
({}).hasOwnProperty('prototype'); // -> false
caller 프로퍼티는 처음 봤는데... 함수 자신을 호출한 함수를 가리키는 게 신기했다. 개발할 때 어느 경우에 사용하는지 조금 궁금했는데, 잠깐 찾아봤을 때는 비표준 프로퍼티라 그런지 개발에 사용한 것보단 간단한 예제만 소개되어 있어서 조금 아쉬웠다. 그렇다면 왜 비표준이고 표준화될 예정도 없지만 프로퍼티로 있는지 궁금해서 스터디 전까지 조금 찾아봐야겠다. name 프로퍼티도 왜 비표준이었다가 정식 표준이 된 건지 궁금하다.
세미나 때 일급 객체에 관한 내용이 있어서 관련하여 알아보면 좋겠다고 생각해 주제로 정했는데... 생각보다 일급 객체에 관한 내용보단 프로퍼티에 관한 내용이 더 많아서 조금 고민했다. 근데 프로퍼티를 뜯어본 기억은 없기도 하고 caller 프로퍼티도 지나쳐도 좋다고 써있긴 하나 처음 보는 프로퍼티라 다같이 알아보면 좋을 것 같아서 주제로 정하게 되었다. 그리고사실다른사람들이랑안겹치는주제를하고싶기도했다... 원래 19장에 프로토타입에 관한 내용이 있길래 프로토타입은 넘어가려고 했는데... 커리큘럼에 프로토타입이 없어서 간단하게 조사하게 되었다. 덕분에 더 이해가 잘된 것 같다! 이번 주에 유난히 공부하면서 계속 궁금해지는 게 많았는데 시간이 부족해 조사하지 못했다.... 부족한 게 많아서 그런지 궁금한 게 자꾸 늘어서 큰일이다 🥺
+) 프로토타입을 조사하면서 다른 웹사이트들을 참고했는데 모두 정리하긴 내용이 너무 길어질 것 같아 가장 도움이 되었던 사이트 첨부합니다!
JavaScript: 프로토타입 이해
"생각보다 일급 객체에 관한 내용보단 프로퍼티에 관한 내용이 더 많아서 조금 고민했다." 이 부분에서 고개 23748938901번 끄덕였씁니다.. 저두 원래 이 부분을 다룰까 하다가 프로퍼티 내용이 너무 심오하고,, 또 뭐 잘 안쓰인다고 하고,, (다 핑계고 무엇보다 이해하기 어려웠습니다) 하튼 그래서 과제하면서 헷갈렸던 부분을 주제로 잡았거든요! 근데 신지님,, 글 읽으니까 갑자기 쑝 이해됨. 진짜 정리를 너무 잘하시는 것 같아요. 또 궁금한 점을 파고들면서 공부하시는 모습이 너무 멋져서 저도 따라하기로 마음 먹었습니다. 조금 더 실질적으로 쓰일만한 부분들을 다같이 공부해보고 요 개념들을 많이 써먹으면 좋을 것 같아요 ㅎㅎㅎ!!! 좋은 아티클 감사합니다 ㅎㅎ
프로토타입에 대해 추가적으로 정리해주셔서 저도 프로토타입을 왜 쓰는지 간단하게 찾아보았습니다..!(댓글로 스장님이 스터디때 이야기 해본다길래 호델데해서 찾아보았습니다 ㅎㅎ),
1. 부모 객체 생성시 한번만 생성하게 되어 메모리 절약 가능
2. 생성자내에 property나 method를 함수 외부에서도 수정 가능
3. 특정 프로퍼티를 부모객체에만 갖게 할 수 있음.
요정도 찾아보았습니다. 말 자체는 이해가 되는데 와닿지는 않더라고요! (제 아티클 예시인데욥, 이번 장바구니 리스트 구현처럼 비슷한 객체 여러개 만들때 생성자 함수를 사용할 수 있다! 처럼 어떠한 경우에 활용할 수 있는지 와닿지 않았습니다..! 리액트 과제 제출하고 더 공부해보려구요..! 다른 이야기지만 스터디를 하며 와닿는 이해가 무엇인지 알게 되어서 좋아요!! 자스기본최고~)아무튼! 추가적인 정리해주셔서 저도 더 알게되었습니다. 추가적인 공부거리도 남겨주는 아티클 작성해주셔서 감사합니당! 최고~!~!!
+스장님 댓글 덧글로 작성되어 위치 수정했습니다..!
ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ다른 사람과 안겹치는 주제하고 싶으셨군욬ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
저는 아직 개발경험이 미숙해서 프로토타입을 왜 알아야되는지, 어떤 경우에 유용한 지식이 될지 솔직히 감이 안잡히더라구요! 이따 스터디 시간에 같이 이야기 나눠보면 좋을 것 같습니다 ㅎㅎ!!