자바스크립트에서 함수 생성 방법
자바스크립트에서 함수도 일반 객체처럼 값으로 취급되기 때문에, 함수 리터럴을 이용해 함수를 생성 할 수 있다.
function add(x,y){
return x+y;
}
각 부분별로 쪼개서 알아보자!
function
>> 함수 키워드add
>> 함수명(x,y)
>> 매개변수 리스트{return x+y;}
>> 함수 몸체함수 생성 방식은 함수 리터럴과 똑같지만, 함수 선언문으로 함수를 생성할 경우, 함수명이 필수적으로 정의되어있어야 한다.
function add(x,y){
return x+y;
}
console.log(add(3,4)); // 7
함수를 변수에 할당하여 함수를 생성하는 것을 함수 표현식이라고 한다.
const add = function (x,y) {
return x,y
}
const plus = add;
console.log(add(3,4)); // 7
console.log(plus(5,6)); // 11
함수표현식에서는 함수 이름이 선택 사항이며, 보통 사용하지 않는다.
add
는 함수명이 아니라 함수를 참조하는 함수변수다. 함수 표현식을 이용 할때 주의해야할 점이 있다.
const add = function sum(x,y){
return x+y;
}
console.log(add(1,2)); // 3
console.log(sum(1,2)); // Error
함수 표현식을 사용 할때, 기명함수 표현식을 사용할 경우, 함수명은 외부에서 접근할 수 없다.
이 함수명은 주로 함수 내부에서 재귀적으로 이용되거나, 디버거 등에서 함수를 구분할 때 사용된다.
화살표 함수 표현식은 함수 표현식에 비해 구문이 짧고, 자신의 this
, argument, super 또는 new.target
을 바인딩 하지 않는다. 따라서 화살표 함수 내부에서 arguments
객체에 접근하면 외부의 함수의 arguments
에 접근한다. 화살표 함수는 항상 익명이다. 메소드 함수가 아닌 곳에 가장 적합하다.
const justList= [
'a',
'ab',
'abc'
];
console.log(justList.map(justList => justList.length); // Array [ 1, 2, 3]
기본적인 구문은 다음과 같다.
(param1, param2, ... , paramN) => {statements}
(param1, param2, ... , paramN) => expression
// { return expression; } 과 동일
매개변수가 하나뿐인 경우는 괄호는 안써도 된다.
자세한 내용은 여기서 더 공부 할 수 있다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/%EC%95%A0%EB%A1%9C%EC%9A%B0_%ED%8E%91%EC%85%98
자바스크립트의 함수도 Function()
이라는 기본 내장 생성자 함수로부터 생성된 객체라고 볼수 있다. 앞서 설명한 함수 선언문이나 함수 표현식 방식도 Funciton()
생성자 함수가 아닌 함수 리터럴 방식으로 함수를 생성하지만, 결국엔 이 또한 내부적으로는 Function()
생성자 함수로 함수가 생성된다고 볼 수 있다.
new Function (arg1, arg2, ... argN, functionBody)
argN
- 함수의 매개변수functionBody
- 함수가 호출될 때 실행될 코드를 포함한 문자열const add = new Function('x','y', 'return x+y');
console.log(add(3,4)); // 7
하지만 일반적으로 Function()
생성자 함수를 사용한 함수 생성 방법은 자주 사용되지 않는다.
앞서 설명한 함수 생성 방식들은 동작 방식이 약간 다르다. 그중 하나가 바로 함수 호이스팅(Function Hoisting) 이다.
자바스크립트에서는 주로 함수 표현식만을 사용할 것을 권한다. 그 이유가 함수 호이스팅 때문이다.
add(4,5); // 9
function add(x,y) {
return x+y;
};
add(2,3) // 5
맨 첫줄에서도 add
함수가 작동 하는 것을 볼 수 있다. 그 이유는, 함수 선언문으로 생성된 함수의 유효범위는 코드의 맨 처음부터 시작하기 때문이다. 이 개념이 바로 함수 호이스팅이다.
반면,
add(1,2); // Error
const add = function (x,y) {
return x+y;
};
add(1,2); // 3
이와 같이 함수표현식으로 생성된 함수는, 첫번째 줄에서는 add
함수가 생성되기 전이므로, 에러가 발생한다. 반면, add
함수가 생성된 이후에는 정상적으로 출력된다.
자바스크립트의 변수 생성(Instantiation) 과 초기화(initialization)의 작업이 분리돼서 진행되기 때문이다. 자세한 내용은 추후 글에 포스트 하겠다.
자바스크립트에서 함수도 역시 객체다. 동적으로 프로퍼티를 생성 할 수도 있다.
function add(x,y) { //
return x+y; // #1
} //
add.result = add(3,2); // #2
add.status = 'OK'; // #3
console.log(add.result); // 5
console.log(add.status); // 'OK'
add()
함수를 생성할 때 함수 코드는 함수 객체의 [[Code]]
내부 프로퍼티에 자동으로 저장된다.add()
함수에 마치 일반 객체처럼 프로퍼티를 동적으로 생성하고 , 값을 저장했다.add()
함수 객체의 status
프로퍼티도 일반 객체에서의 접근 방식처럼 add.status
를 이용해 접근 가능하다.이처럼, 자바스크립트에서 함수도 일반 객체처럼 취급될 수 있기 때문에 다음과 같은 동작이 가능하다.
이와 같은 특징이 있으므로 자바스크립트에서는 함수를 일급 객체 (First Class Object)라고 부른다.
함수 객체만의 표준 프로퍼티가 정의되어 있는데, 예제를 통해 어떤 객체 형태로 되어 있는지 직접 확인해보자.
function add(x,y) {
return x+y;
}
console.dir(add);
다음과 같은 많은 기본 프로퍼티가 존재한다.
add 함수도 역시 객체기 때문에, __proto__
프로퍼티를 가지고있고, 이를 통해 자신의 부모 역할을 하는 프로토타입 객체를 가리킨다. 또한, add
는 함수 객체의 부모역할을 하는 프로토타입객체이며, 함수 객체( Function.prototype
) 라고 한다.
모든 함수는 객체로서
prototype
프로퍼티를 가지고 있다. 여기서 주의할 것은 함수 객체의prototype
프로퍼티는__proto__
와는 다른 개념이다.
prototype
프로퍼티와 __proto__
프로퍼티function Add(x,y) {
return x+y
};
const addf = new Add(1,2);
console.dir(addf);
두 프로퍼티 모두 프로토타입 객체를 가리킨다는점에서는 공통점이 있지만, 관점에 차이가 있다. 모든 객체에 있는 내부 프로퍼티
__proto__
는 객체 입장에서 부모 역할을 하는 프로토타입 객체를 가리키는 반면, 함수 객체가 가지는 prototype 프로퍼티는 이 함수가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다. 자세한 내용은 이후에...
prototype 프로퍼티는 함수가 생성될 때 만들어지며(첫번째 __proto__
), constructor 프로퍼티 하나만 있는 객체를 가리킨다. 그리고 prototype
프로퍼티가 가리키는 프로토타입 객체의 유일한 cunstructor
프로퍼티는 자신과 연결된 함수를 가리킨다. 즉, 자바스크립트에서 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 서로를 참조한다.
자세한 내용은 프로토타입체이닝 부분에서 공부할 것!
앞서 공부한 익명 함수의 대표적인 용도가 바로 콜백 함수이다. 콜백 함수는 코드를 통해 명시적으로 호출하는 함수가 아니라, 개발자가 단지 함수를 등록하기만 하고, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출되는 함수를 말한다. 또한, 특정 함수의 인자로 넘겨서, 코드 내부에서 호출되는 함수 또한 콜백 함수가 될 수 있다.
대표적인 콜백 함수의 사용 예가 자바스크립트에서의 이벤트 핸들러 처리다.
<body>
<script>
window.onload = function (){
alert('ALERT!');
}
</script>
</body>
window.onlaod
이벤트 핸들러 예제 코드이다. 웹페이지가 로딩될 때, 우리가 등록한 이벤트 핸들러가 호출되면서 경고창이 뜨게 된다.
함수를 정의함과 동시에 바로 실행하는 함수를 즉시 실행 함수 ( immediate functions ) 라고 한다. 이 함수도 익명 함수를 응용한 형태이다. 예제를 보자.
(function (name) {
console.log('This is the immediate function -->' + name);
})('foo');
// This is the immediate function --> foo
즉시 실행 함수를 만드는 방법은, 함수 리터럴을 괄호()로 둘러싸고, 정의함과 동시에 바로 호출될 수 있게 ()를 추가한다. 이 예제에서는 (function~)('foo');
의 형태이다. 'foo'
값은 바로 즉시 실행 함수의 name
매개변수로 넘겨지게 된다.
주로 최초 한 번의 실행만을 필요로 하는 초기화 코드 부분 등에 사용할 수 있다.
자바스크립트에서는 함수 코드 내부에서도 다시 함수 정의가 가능하다. 이를 내부 함수 ( inner function ) 이라고 부른다. 내부 함수는 자바스크립트의 기능을 보다 강력하게 해주는 클로저를 생성하거나 부모 함수 코드에서 외부에서의 접근을 막고 독립적인 헬퍼 함수를 구현하는 용도 등으로 사용한다. ( 클로저의 자세한 내용은 이후 포스트에..)
// parent() 함수 정의
function parent () {
var a = 100;
var b = 200;
// child() 내부 함수 정의
function child(){
var b =300;
console.log(a);
console.log(b);
}
child();
}
parent();
// 100
// 300
child();
// Error
기본적으로 내부함수에서 기억해야 할 것은,
자바스크립트에서는 함수도 일급 객체이므로, 일반 값처럼 함수 자체를 리턴할 수도 있다. 함수를 호출함과 동시에 다른 함수로 바꾸거나, 자기 자신을 재정의 하는 함수를 구현할 수 있다. 이처럼 자바스크립트는 유연성이 아주 높다.
var self = function() {
console.log('a');
return function () {
console.log('b');
}
}
self = self(); // a
self(); //b
self()
함수가 호출됐을 떄는 'a'가 출력된다. 그리고 다시 self 함수 변수에 self()
함수 호출 리턴값으로 내보낸 함수가 저장된다.self()
함수가 호출 됐을 때는 'b'가 출력된다. 즉, 첫번째 self()
함수 호출 후에, self 함수 변수가 가리키는 함수가 원래 함수에서 리턴받은 새로운 함수로 변경된 것이다.함수의 기본적인 기능은 당연히 함수를 호출하여 코드를 실행하는 것이다. 자바스크립트에서 함수호출은 매우 자유롭다.
자바스크립트에서는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지않는다. 다음 예제를 보자.
function func(arg1,arg2) {
console.log(ar1,arg2);
}
func(); // undefined undefined
func(1); // 1 undefined
func(1,2); // 1 2
func(1,2,3); // 1 2
undefined
가 출력되고, 초과된 인자는 무시한다.이러한 특성 때문에 함수 코드를 작성할 때 , 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해줘야 할 경우가 있다. 이를 가능 케 하는게 바로 arguments
객체다. 자바스크립트에서는 함수를 호출할 때 인수들과 함께 암묵적으로 arguments
객체가 함수 내부로 전달되기 때문이다. arguments
객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 의미한다. 이 객체는 실제 배열이 아닌 유사 배열 객체라는 점이다.
// add() 함수
function add(a,b) {
//arguments 객체 출력
console.dir(arguments);
return a+b;
}
console.log(add(1)); //NaN
console.log(add(1,2)); // 3
console.log(add(1,2,3)); // 3
length
를 가진다. 배열이 아닌 객체이기때문에, 배열 메서드를 쓸 수 없다는 것에 유의하자.arguments
객체는 매개변수 개수가 정확하게 정해지지 않은 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 해줘야 하는 함수를 개발하는 데 유용하게 사용할 수 있다.function sum() {
var result = 0;
for(let i=0; i < arguments.length; i++){
result += arguments[i];
}
return result;
}
console.log(sum(1,2,3)); // 6
console.log(sum(1,2,3,4,5,6,7,8,9)); // 45
자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, 앞서 설명한 arguments
객체 및 this 인자가 함수 내부로 암묵적으로 전달된다. 여기서 특히, this 인자는 고급 자바스크립트 개발자로 거듭나려면 확실히 이해해야 하는 핵심 개념이다. this가 이해하기 어려운 이유는 자바스크립트의 여러가지 함수가 호출되는 방식(호출 패턴) 에 따라 this 가 다른 객체를 참조(this 바인딩)하기 때문이다.
객체의 프로퍼티가 함수일 경우, 이 함수를 메서드라고 부른다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩 된다. 다음예제를 보자!
// myObject 객체 생성
var myObject = {
name : 'foo',
sayName : function() {
console.log(this.name);
}
};
// otherObject 객체 생성
var otherObject = {
name: 'bar'
};
// otherObject.sayName() 메서드
otherObject.sayName = myObject.sayName;
// sayName() 메서드 호출
myObject.sayName(); // foo
otherObject.sayName(); // bar
myObject
객체와 otherObject
객체는 name
프로퍼티와 sayName()
메서드가 있다. sayName()
메서드는 this.name
값을 출력하는 간단한 함수로서 , myObject
와 otherObject
객체로부터 각각 호출된다. 이때 sayName() 메서드에서 사용된 this는 자신을 호출한 객체에 바인딩된다.자바스크립트에서는 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩된다. 브라우저에서 자바스크립트를 실행하는 경우 전역 객체는 window 객체가 된다.
브라우저 환경에서 자바스크립트를 실행하는 경우, 전역 객체는 window 객체가 된다. 모든 전역변수와, 함수, 객체는 이 전역객체의 프로퍼티이다.
var foo = "I'm foo";
console.log(foo);// I'm foo
console.log(window.foo); // I'm foo
따라서 전역 변수는 전역 객체의 프로퍼티로도 접근할 수가 있다.
이제 함수를 호출할 때 this 바인딩이 어떻게 되는지를 다음 예제 코드를 살펴보자!
// 전역변수 test 선언
var test = "This is Test";
console.log(window.test);
//sayFoo() 함수
var sayFoo = function () {
console.log(this.test);
// 출력값
// This is Test
// This is Test
test
라는 전역변수를 생성했고, 전역객체인 window
의 프로퍼티로 접근이 가능함을 확인할 수 있다.sayFoo()
함수는 단순히 this.test
를 출력하는 함수다. 자바스크립트에서 함수를 호출할때 this
는 전역 객체에 바인딩된다고 했으므로, sayFoo()
가 호출된 시점에서, this
는 window
에 바인딩된다.this
를 이용할때는 주의해야한다.다음 예제를 통해 내부 함수의
this
바인딩 동작을 알아보자!
// 전역 변수 value 정의
var value = 100;
// myObject 객체 생성
var myObject = {
value : 1,
function1 : function() {
this.value +=1 ;
console.log('func1() called : this.value =' + this.value);
// 내부함수 function2()
function2 = function() {
this.value +=1;
console.log('function2() called : this.value =' +this.value);
// 내부함수 function3()
function3 = function() {
this.value +=1;
console.log('function3() called : this.value=' + this.value);
}
function3(); // 내부함수 function3() 호출
}
function2(); // 내부함수 function2() 호출
}
};
myObject.function1(); // function1() 메서드 호출
// 출력값
// function1() called : this.value = 2
// function2() called : this.value = 101
// function3() called : this.value = 102
function1()
-> function2()
-> function3()
this
는 myObject
객체에 바인딩된다.function2()
는 function1()
을 부모 함수로 하기 때문에, this
가 function1()
과 같이 myObject
에 바인딩된다고 생각할 수 있지만, 결과는 그렇지 않다.this
는 전역객체에 바인딩 된다.위의 내용을 숙지했다면, 다음 예시를 보자.
var value = 100;
var myObject ={
value : 1,
function1: function () {
var that = this
console.log('function1() called : this.value = 2' + this.value);
function2 = function () {
that.value += 1;
console.log('function2() called : this.value =' + that.value);
function3 = function () {
that.value +=1;
console.log('function3() called : this.value =' + that.value);
}
function3();
}
function2();
}
};
myObject.function1(); // function1() 메서드 호출
function1()
의 this
값을 새로운 변수 that
에 저장하고 내부함수에서 부모함수의 변수에 접근할 수 있는 특징을 이용하여, 내부함수에서도 function1()
의 this
가 바인딩된 객체인 myObject
에 접근 가능하게 된다.자바스크립트의 생성자 함수는 말 그대로 자바스크립트의 객체를 생성하는 역할을 한다. 기존 함수에 new
연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다. 생성자 함수로 정의된 함수는 첫 글자를 대문자로 표기하는 방식이 널리 쓰인다.
자바스크립트에서는 이러한 생성자 함수를 호출할 때, 생성자 함수 코드 내부에서
this
는 앞서 나온 메서드와 함수호출 방식에서의this
바인딩과는 다르게 동작한다. 이를 이해하기 위해, 생성자 함수가 호출됐을 때 동작하는 방식을 알아보자.
new
연산자로 함수를 생성자로 호출하면, 다음과 같은 순서로 동작한다.
this
바인딩this
로 바인딩된다. 따라서, 이후에 쓰이는 this
는 이 빈 객체를 가리킨다. 다만, 앞서 말했듯이 모든 객체는 prototype
프로퍼티를 가지고있다는 것을 명심해야하며, 생성자 함수가 생성한 빈 객체는 자신을 생성한 생성자 함수의 prototype
프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. 즉 생성자의 프로토타입 프로퍼티가 프로토타입객체를 가리키고, 프로토타입 객체의 constructor
프로퍼티가 생성자를 가리키는 순환구조의 형태이다.this
를 통한 프로퍼티 생성this
를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메서드를 생성할 수 있다.this
로 바인딩된 새로 생성한 객체가 리턴된다. (일반 함수에서는 리턴값이 없다면 undefined
가 출력되니 주의) 하지만, 리턴값이 새로 생성한 객체(this
)가 아닌 다른 객체를 반환하는 경우는 생성자 함수를 호출하였어도 this
가 아닌 해당 객체가 리턴된다. 자세한 내용은 이후에..예제를 통해 자세히 알아보자.
// Person() 생성자 함수
const Person = function (name) {
// 함수 코드 실행 전 #1
this.name = name ;
// 함수 리턴 #2
};
// foo 객체 생성
const foo = new Person('foo')
console.log(foo.name); // foo
Person()
함수가 생성자로 호출되면, 함수 코드가 실행되기전(#1
) 빈객체가 생성된다. 이때, 빈객체는 Person()
생성자 함수의 prototype 프로퍼티가 가리키는 객체(Person.prototype
객체)를 __proto__
로 연결해 자신의 프로토타입으로 설정한다. 그리고 이렇게 생성된 객체는 생성자 함수 코드에서 사용되느 this
로 바인딩된다.
this
가 가리키는 빈 객체에 name
이라는 동적 프로퍼티 생성
리턴값이 특별히 없으므로, this로 바인딩한 객체가 생성자 함수의 리턴값으로 반환되어, foo
변수에 저장된다.
프로토타입 체이닝에대해 조금 더 직관적으로 이해하고 싶으면 https://velog.io/@sik2/JS-CoreJavaScript-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%B2%B4%EC%9D%B4%EB%8B%9DPrototype-Link-Prototype-Object 이분의 블로그를 참조하면 좋을것같다!
이러한 생성자 함수의 원리를 이용하면, 같은 형태의 여러 객체를 생성할 수 있다.
// Person() 생성자 함수
const Person = function(name,age){
this.name = name;
this.age = age;
};
const sam = new Person('sam','25');
const yong = new Person('yong','24');
console.log(sam);
console.log(yong);
자세한 내용은 밑에 프로토타입 체이닝 파트에서 다룰 것이다.
지금까지 본 this
바인딩은, 상황에 따라 this
가 정해진 객체에 자동으로 바인딩된다는 것을 확인했다. 자바스크립트는 이러한 내부적인 this
바인딩 이외에도 this
를 특정 객체에 명시적으로 바인딩시키는 방법도 제공한다. 이를 가능하게 하는 것이 바로 apply()
와 call()
메서드이다. 이 메서드들은 모든 함수의 부모객체인 function.prototype
객체의 메서드이므로, 모든 함수는 다음과 같은 형식으로 apply()
메서드를 호출하는 것이 가능하다.
function.apply(thisArg, argArray)
참고로
call()
메서드는apply()
메서드와 기능이 같고 넘겨받는 인자의 형식만 다르다. 우선apply()
메서드에대해 알아보자!
apply()
메서드를 호출하는 주체 함수 내부에서 사용될 this
를 thisArg
객체에 바인딩하겠다 라는 뜻이며, apply()
자체도 함수를 호출하는것이기 때문에, 함수에 넘길 인자들이 곧 argArray
인 것이다.// 생성자 함수
function Person(name,age,gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// foo 빈 객체 생성
const foo = {};
// apply 메서드 호출
Person.apply(foo,['foo', 30, 'man']);
console.dir(foo)
foo
라는 객체를 첫번째 인자로 넘겨 Person()
함수에서 this
로 바인딩되고, 그다음에 Person()
함수의 인자로 ['foo', 30, 'man']
가 전달된다. 이 코드는 결국 Person('foo',30','man')
함수를 호출하면서, this
를 foo
객체에 명시적으로 바인딩하는 것이다.
apply()
메서드의 가장 대표적 용도는arguments
객체에서 설명한 유사배열객체에서 배열 메서드를 사용하는 것이다.
function myFunc() {
console.dir(arguments);
const args = Array.prototype.slice.apply(arguments);
console.dir(args);
};
myFunc(1,2,3);
Array.prototype.slice()
메서드를 호출하고, 메서드 내부에서 사용될 this
를 arguments
객체로 바인딩 시킨다.
따라서, args
에는 slice()
메서드의 아무런 인자가 넘겨지지 않았을때 배열을 그대로 복사하는 성질을 이용하여 복사된 배열을 저장한다.
arguments
객체는 프로토타입객체가 Object
이고, args
의 프로토타입객체가 Array
인 것을 확인할 수 있다.
자바스크립트 함수는 항상 리턴값을 반환한다. return
문을 사용하지 않더라도, 다음의 규칙으로 항상 리턴값을 전달하게 된다.
일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined
값이 리턴된다.
생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.
2번에서 예외가 존재하는데, 명시적으로 새로운 객체를 리턴할경우, 명시한 새로운 객체를 리턴한다. 하지만, 불린,숫자,문자열의 경우는 이러한 리턴값을 무시하고 this
로 바인딩된 객체가 리턴된다.
본 포스트 앞에서도 꾸준히 나왔던 프로토타입 개념. 아마 그냥 쭉 읽으면 이해가 안될수도 있다. 왜냐면 나도 책 읽으면서 그랬으니까..
계속 자세한건 뒤에서 알아보쟤..이번 파트에서 정확하게 알아보자!
본 포스팅에서 '프로토타입(prototype) 프로퍼티' 와
__proto__
라고 사용한 개념에 대해서 알아보자!
자바스크립트는 C++이나 자바 같은 객체지향 프로그래밍 언어와는 다른 프로토타입 기반의 객체지향 프로그래밍을 지원한다. 이번 파트에서는 자바스크립트에서 OOP 상속에 근간이 되는 프로토타입과 프로토타입 체이닝의 기본 개념을 다룰 것이다.
자바스크립트에서는 타 언어의 클래스 개념이 없고, 객체 리터럴이나 생성자 함수로 객체를 생성한다. 이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체다. 부모라는 말을 쓰는것으로 보면 당연히 자식의 역할을 하는 부분도 있다는 뜻이다. 클래스 상속 개념과 마찬가지로 자식 객체는 부모 객체가 가진 프로퍼티 접근이나 메서드를 상속받아 호출하는 것이 가능하다.
앞서 계속 언급했듯 자바스크립트의 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티가 있다. ECMAScript에서는 이러한 링크를 암묵적 프로토타입 링크(Implicit Prototype Link)라고 부르며, 이러한 링크는 모든 객체의 [[Prototype]]
프로퍼티에 저장된다. 이 글에서 줄곧 언급해왔던 __proto__
프로퍼티이다.
위의 함수 객체의 기본 프로퍼티인 prototype
프로퍼티와 혼동하면 안된다. 이 둘의 차이점을 알기 위해선 자바스크립트의 객체 생성 규칙을 알아야한다.
※ [[Prototype]]
링크 = __proto__
프로퍼티
자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype
프로퍼티가 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 [[Prototype]]
링크로 연결한다.
다음 예제 코드를 살펴보자. Person()
생성자 함수를 정의하고, 이를 통해 foo객체를 생성하는 간단한 코드다.
// Person 생성자 함수
function Person(name) {
this.name = name;
}
// foo 객체 생성
const foo = new Person('foo'); // #1
console.dir(Person); // #2
console.dir(foo); // #3
Person()
생성자 함수는 prototype 프로퍼티로 자신과 링크된 프로토타입 객체를 가리킨다. Person()
생성자 함수로 생성된 foo
객체는 Person()
함수의 프로토타입 객체를 [[Prototype]]
링크로 연결한다. 결국 prototype
프로퍼티와 [[Prototype]]
링크 모두 같은 프로토타입 객체를 가리키고 있는 것이다. 이를 통해서 알 수 있는 것은, 객체를 생성하는 것은 생성자 함수이지만, 객체의 부모역할을 하는 건 생성자 자신이 아닌 생성자 함수의 프로토타입 프로퍼티가 가리키고 있는 프로토타입 객체이다.객체는 자기 자신의 프로퍼티뿐만이 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 접근 가능하다. 이것이 바로 프로토타입 체이닝이다. 다음 예제를 보자!
const myObject = {
name: 'foo',
sayName: function () {
console.log('My name is ' + this.name)
}
};
myObject.sayName();
console.log(myObject.hasOwnPropety('name'));
console.log(myObject.hasOwnProperty('nickName'));
myObject.sayNickName();
// My name is Foo
// true
// false
// Uncaught TypeError: object #<Object> has no method 'sayNickName'
myObject
객체에는 hasOwnPropety
라는 프로퍼티가 없지만, 정상적으로 출력된다.sayNickName
메서드는 myObject
의 메서드로 존재하지않기 때문에 에러가 발생했지만 hasOwnProperty()
메서드는 에러가 아닌 false
가 출력되는 이유가 프로토타입 체이닝 때문이다.hasOwnPropety()
메서드는 이 메서드를 호출한 객체에 인자로 넘긴 문자열 이름의 프로퍼티나 메서드가 있는지 체크하는 자바스크립트의 표준 API 함수이기 때문이다.객체 생성 파트에서 다루었듯이, 객체 리터럴 방식의 객체생성은 Object()
라는 내장 생성자 함수로 생성된 것이다. 따라서, Object()
생성자 함수도 함수 객체이므로 prototype
프로퍼티 속성이 있고, 위에 설명한 객체 생성 규칙의 prototype
프로퍼티와 [[Prototype]]
링크의 구조가 존재한다.
[[Prototype]]
링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색한다. 이것이 바로 프로토타입 체이닝 개념이다.생성자 함수로 객체를 생성하는 경우는 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 이뤄진다. 하지만 근본적으로 이루어지는 원칙은 똑같다.
자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의
prototype
프로퍼티가 가리키는 프로토타입 객체를 자신의 프로토타입 객체(부모객체)로 취급한다. 다시한번 기억하자!
// Person() 생성자 함수
function Person(name,age) {
this.name = name;
this.age = age;
}
// foo 객체 생성
const foo = new Person('foo','25');
// 프로토타입 체이닝
console.log(foo.hasOwnProperty('name')); // true
// Person.prototype 객체 출력
console.dir(Person.prototype);
foo
객체의 생성자는 Person()
함수이다. 따라서, foo
객체의 프로토타입 객체는 Person
생성자 함수 객체의 prototype
프로퍼티가 가리키는 객체(Person.prototype)
가된다.foo.hasOwnProperty()
의 값이 정상적으로 출력된것은 역시 프로토타입 체이닝과정을 거쳤기 때문이다. 하지만 여기서 객체 리터럴 방식과 차이점은, Person()
생성자 함수의 프로토타입 객체에는 오직 constructor
프로퍼티 하나만 있으므로, Person.prototype
객체의 프로토타입 객체인 Object.prototype
객체에서 메서드를 찾는다.자바스크립트에서 객체는 부모역할의 프로토타입 객체를 갖고, 그 부모도 객체면 도대체 부모객체의 끝은 어딜까?
자바스크립트에서 Object.prototype
객체는 프로토타입 체이닝의 종점이다. 앞서 본 객체리터럴 방식이나, 생성자 함수 방식이나 Object.prototype
에서 프로토타입 체이닝이 끝나는 것을 알 수 있다.
이말은 즉, 모든 객체는 Object.prototype
의 프로퍼티와 메서드에 접근이 가능하고 서로 공유할 수 있다는 것이다. 때문에 자바스크립트 표준 built-in
객체인 Object.prototype
에는 모든 객체에서 호출이 가능한 표준 메서드들이 정의되어 있다. ex) hasOwnProperty(),isPrototypeOf()
등
객체 표준메서드 말고 기본타입들은?
기본타입인 숫자,문자열,배열 등에서 사용되는 표준 메서드들의 경우는 이들의 프로토타입인 Number.prototype
, String.prototype
, Array.prototype
등에 정의되어 있다. 물론, 이러한 기본 내장 프로토타입 객체 또한 Object.prototype
을 자신의 프로토타입으로 가지고있어서 프로토타입 체이닝으로 연결된다. ECMAScript 명세서를 보면 자바스크립트의 각 네이티브 객체별로 공통으로 제공해야 하는 메서드들을 각각의 프로토타입 객체 내에 메서드로 정의해야 한다고 기술하고 있다.
※네이티브 객체란?
네이티브 객체(Native objects or Built-in objects or Global Objects)는 ECMAScript 명세에 정의된 객체를 말하며 애플리케이션 전역의 공통 기능을 제공한다. 네이티브 객체는 애플리케이션의 환경과 관계없이 언제나 사용할 수 있다.
Object, String, Number, Function, Array, RegExp, Date, Math
와 같은 객체 생성에 관계가 있는 함수 객체와 메소드로 구성된다.- 네이티브 객체를 Global Objects라고 부르기도 하는데 이것은 전역 객체(Global Object)와 다른 의미로 사용되므로 혼동에 주의하여야 한다.
- 전역 객체(Global Object)는 모든 객체의 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미한다.
자바스크립트는 Object.prototype
, String.prototype
등과 같이 표준 built-in
프로토아입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용한다. 다음 예제와 같은 코드를 작성하면, 모든 문자열에서 접근 가능한 새로운 메서드를 정의할 수 있다.
String.prototype.testMethod = function () {
console.log('This is the String.prototype.testMethod()');
};
const str = "This is test";
str.testMethod();
console.dir(String.prototype);
프로토타입 객체는 메서드를 가질수 있음을 확인했다(프로토타입 메서드라고 칭하겠다). 만약 프로토타입 메서드 내부에서 this를 사용한다면 이는 어디에 바인딩될 것인가?
이는 앞서 공부한 객체의 메서드를 호출할 때 this
바인딩 규칙과 같다. 메서드 호출 패턴에서의 this
는 그 메서드를 호출한 객체에 바인딩 된다.
디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, 함수의
prototype
프로퍼티에 연결된다. 자바스크립트에서는 이러한 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능하다. 이러한 특징을 이용해서 객체지향의 상속을 구현한다. 이와 관련해서는 추후 포스트에 자세히 다룰 예정이다.
다만 주의할점이있는데, 생성자 함수의 디폴트 프로토타입이 변경되면 변경되기 이전에 생성된 객체는 변경되기 이전의 프로토타입 객체에 [[Prototype]]
링크로 연결되고, 변경된 이후에 생성된 객체는 변경된 프로토타입 객체에 [[Prototype]]
링크로 연결다는 점이다.
객체의 특정 프로퍼티를 읽으려고 할 때, 프로퍼티가 해당 객체에 없는 경우 프로토타입 체이닝이 발생한다. 반대로 객체에 있는 특정 프로퍼티에 값을 쓰려고 한다면 이때는 프로토타입 체이닝이 일어나지 않는다. 지금까지 내용을 잘 이해했다면 정말 자명한 얘기다.
본 글은 송형주,고현준 "인사이드 자바스크립트 Inside Javascript (한빛미디어,2017)" 를 바탕으로 작성되었습니다.
지적 환영합니다.. 저도 공부하면서 정리한내용이라 제가 잘못 이해한부분이있을수있습니다 ㅠ