오늘은 Javascript에서의 함수 호출과 this에 대해 공부해보고자 합니다. 또한 프로토타입 체이닝에 대해서도 간단히 알아봅시다.
함수의 기본적인 기능은 당연하게도 함수를 호출하여 코드를 실행하는 것입니다. 저번 시간에서도 다루었듯이 Javascript는 문법 체크가 자유로운 언어이죠. 그렇다면 Javascript에서 함수 호출은 어떤 특징이 있을까요?
아래의 코드가 있다고 해봅시다.
function func(arg1, arg2) {
console.log(arg1, arg2);
}
func(); // 출력 : undefined undefined
func(1); // 출력 : 1 undefined
func(1, 2); // 출력 : 1 2
func(1, 2, 3); // 출력 : 1 2
func 함수는 인자로 arg1, arg2로 두 인자를 받고 있습니다. 만약 엄격한 문법 체크를 하는 언어라면 func 함수 호출 시 인자 2개를 주지 않으면 오류가 발생할것입니다. 하지만 Javascript에서는 그렇지 않습니다.
예시에서 볼 수 있듯이 함수의 인자의 양을 선언했던 인자 수 보다 적게 주거나, 더 많이 준다고해도 Javascript에서는 출력이 이루어지고 있습니다.
이처럼 Javascript의 함수는 호출 시 인자를 어떻게 주더라도 오류가 발생하지 않습니다.
그렇다면 만약 들어오는 인자 수에 따라서 함수의 동작을 제어해야한다면 어떻게 해야 할까요?
Javascript 함수는 호출 시 암묵적으로 arguments 객체도 함수 내부로 전달됩니다. arguments 객체는 함수에 전달된 모든 인자에 대해 배열과 같은 형태로 저장되어 있습니다. 우리는 이를 통해 전달받은 인자 수와 모든 인자들에 대해 처리를 수행할 수 있습니다.
사용 예시는 다음과 같습니다.
function sum(a, b){
var result = 0;
// 배열처럼 length 프로퍼티 존재
for(var 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)); // 출력 : 15
하지만 arguments 객체 사용에서 주의할 것이 있습니다. arguments 객체는 실제 배열이 아닌 유사 배열 객체입니다. 따라서 배열 객체에 사용하는 pop(), shift() 등의 함수는 사용할 수 없습니다. 하지만 이러한 메소드를 사용해야한다면 어떻게 해야할까요? 이는 조금 이후에 방법을 알아보고자 합니다.
함수 호출 시 arguments 객체와 함께 암묵적으로 전달되는 객체가 하나 더 있습니다. 그것은 바로 this 인자입니다.
this 인자는 함수를 호출한 객체로 바인딩됩니다. 이해를 돕기위해 아래의 코드를 봅시다.
var myObject = {
name: 'foo',
sayName: function () {
console.log(this.name);
}
};
var otherObject = {
name: 'bar'
};
otherObject.sayName = myObject.sayName;
var name = 'hi';
var sayName = myObject.sayName;
myObject.sayName(); // 출력 : foo
otherObject.sayName(); // 출력 : bar
sayName(); // 출력 : hi
otherObject.sayName과 객체 sayName은 모두 myObject의 sayName 함수와 동일합니다. 그럼에도 불구하고 세 함수를 모두 실행해보면 다른 결과가 나오는 것을 확인할 수 있습니다. 이는 세 함수 모두 바인딩된 this 인자가 다르기 때문입니다. 그럼 각각의 함수에 바인딩된 this는 무엇일까요?
코드를 보면서 눈치 채셨겠지만, 각각의 sayName() 함수 앞에 호출된 객체가 this로 바인딩되었습니다. 앞에 객체가 없는 sayName()의 경우 전역 객체로 바인딩되었기 때문에 hi가 출력되는 것을 확인할 수 있습니다.
브라우저 환경에서 전역객체는 window입니다.
이밖에도 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩된다는 특징이 있습니다. 아래의 예를 참고해서 알아봅시다.
var value = 100;
var myObject = {
value: 1,
func1: function() {
this.value += 1;
console.log('func1() called. this.value : ' + this.value);
func2 = function() {
this.value += 1;
console.log('func2() called. this.value : ' + this.value);
func3 = function() {
this.value += 1;
console.log('func2() called. this.value : ' + this.value);
}
func3(); // value ++
}
func2(); // value ++
}
};
myObject.func1();
이 코드를 작성한 사용자는 아래와 같은 형태로 코드가 돌아가기를 원했을 것입니다.
하지만 실제 출력은 다음과 같이 이루어질 것입니다.
myObject.func1();
/*
func1() called. this.value : 2
func2() called. this.value : 101
func3() called. this.value : 102
*/
이는 우리가 원했던 것과 달리, 아래와 같이 this가 바인딩되었기 때문입니다.
그러면 우리가 처음에 원하는대로 구현하려면 어떻게 해야할까요? 바로 func1 함수 내 다른 함수들이 접근할 수 있는 내부 변수에 this를 저장하여 사용하면 됩니다.
javascript에서 우리는 생성자 함수를 통해서도 객체를 생성할 수 있습니다. 사용 방법은 다음과 같습니다.
var Person = function (name) {
this.name = name;
}
var foo = new Person('foo');
console.log(foo.name); // 출력 : foo
이러한 방식의 객체 생성의 장점은 생성자 함수를 통해 같은 형태의 객체를 여러 개 생성할 수 있다는 것입니다.
이러한 생성자 함수에서도 주의해야하는 점이 있습니다.
아래의 코드를 봅시다.
var Person = function (name) {
this.name = name;
}
var foo = Person('foo');
console.log(foo); // 출력 : undefined
console.log(window.name); // 출력 : foo
위의 코드처럼 생성자 함수를 사용할 때 앞의 new 입력을 깜빡한다면, 생성자 함수 내 this가 전역객체로 바인딩되기 때문의 위와 같은 일이 발생하게 됩니다.
이러한 실수를 막기 위해 아래와 같은 방법을 사용하기도 합니다.
function A(arg){
if(!this instanceof A))
return new A(arg);
this.value = arg ? arg : 0;
}
var a = new A(100);
var b = A(10);
console.log(a.value); // 출력 : 100
console.log(b.value); // 출력 : 10
위에서 배운 것처럼 함수 호출 발생 시 각각의 상황에 따라서 this가 정해진 객체에 자동적으로 바인딩됩니다. 하지만 우리는 메소드를 통해 명시적으로 this를 바인딩할 수도 있습니다. 이러한 역할을 할 수 있는 것이 call()과 apply() 메서드입니다.
기본적으로 apply와 call 메서드를 호출하는 주체는 함수입니다. 또한 apply 또한 this를 특정 객체에 바인딩할 뿐 결국 본질적인 기능은 함수 호출입니다. 따라서 결론적으로 기본적인 기능은 자신을 호출한 함수를 호출하는 것입니다.
두 함수는 기능이 같기 때문에 우선은 apply() 메서드 먼저 이해해보겠습니다.
function.apply(thisArg, argArray);
첫 번째 인자인 thisArg는 this로 명시적으로 바인딩할 객체입니다. 그리고 두 번째 인자 argArray는 결론적으로 호출할 함수에 제공할 인자입니다. 아래의 예시를 통해 좀 더 자세히 이해해보겠습니다.
// 생성자 함수
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// foo 빈 객체 생성
var foo = {};
// apply() 메서드 호출
Person.apply(foo, ['foo', 30, 'man']);
console.log(foo); // 출력 : {name: 'foo', age: 30, gender: 'man'}
apply() 메서드를 사용한 것을 보면 이를 이용하여 생성자 함수 Person에서 호출하여 foo라는 객체에 프로퍼티를 할당하고 있는 것을 볼 수 있습니다.
call()의 경우 아래와 같은 방식으로 똑같이 사용할 수 있습니다.
Person.call(foo, 'foo', 30, 'man');
위에서 함수 호출 시 자동으로 전달되는 arguments 객체에 대해 다뤘었죠. arguments 객체는 배열과 비슷하지만 배열 객체는 아니기 때문에 표준 배열 메서드는 사용할 수 없다고 하였습니다. 하지만 call()이나 apply() 메서드를 이용하면 이러한 문제를 해결할 수 있습니다.
function myFunc() {
// args는 arguments 객체 내 인자를 똑같이 가진 배열 객체!
var args = Array.prototype.slice.apply(arguments);
}
위와 같이 코드를 작성하면 Array.prototype.slice 함수의 this가 arguments로 바인딩되어 결과적으로는 arguments.slice()와 같은 형태로 함수를 사용할 수 있습니다.
여기서 slice()함수는 인자를 따로 넘겨주지 않은 배열 전체가 복사되기 때문에 arguments 객체 내 모든 인자를 가진 배열을 args가 받을 수 있게 됩니다.
javascript의 함수는 항상 리턴값을 반환합니다. 이러한 반환은 아래의 규칙을 따릅니다.
1. 일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴된다.
2. 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.
2번 규칙의 경우는 이해를 돕고자 예시 코드를 보면 좋습니다.
// Person() 생성자 함수
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
// 명시적으로 다른 객체 반환
return {name:'bar', age:20, gender:'woman'};
}
var foo = new Person('foo', 30, 'man');
console.log(foo); // 출력 : {name: 'bar', age: 20, gender: 'woman'}
Javascript는 프로토타입 기반의 객체지향 프로그래밍을 지원합니다. 따라서 Javascript의 동작 과정을 이해하기 위해서는 프로토타입의 개념을 잘 알고 있을 필요가 있습니다.
Javascript에서는 따로 클래스 개념이 존재하지 않습니다. 대신 생성자 함수를 이용하여 객체를 생성할 수 있었죠. 이렇게 생성된 객체의 부모 객체가 바로 프로토타입 객체입니다. 프로토타입 객체 내 프로퍼티나 메서드는 자식 객체에서 호출하여 사용할 수 있습니다.
Javascript의 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 프로퍼티가 있습니다. 이는 [[Prototype]]으로, [[Prototype]]에 담겨진 링크는 암묵적 프로토타입 링크라고 부릅니다.
prototype 프로퍼티? [[Prototype]]?
함수객체의 prototype 프로퍼티와 객체의 [[Prototype]]는 구분할 수 있어야 합니다.
prototype 프로퍼티는 함수가 생성자 함수로 사용될 때, 이후 생성된 객체의 프로토타입 객체를 가리키는 것입니다.
반면 [[Prototype]]는 해당 객체의 부모의 링크를 저장하고 있는 프로퍼티입니다.
앞에서 말했듯, Javascript의 객체는 자신의 프로토타입 객체의 프로퍼티와 메서드를 자신의 것처럼 접근하는 것이 가능합니다. 이것이 가능하게끔 하는 것이 프로토타입 체이닝입니다.
객체는 생성 방식에 따라서 프로토타입 객체가 다를 수 있습니다. 예를 들어 리터럴 방식으로 생성된 객체는 프로토타입 객체가 Object()인 반면, 생성자 함수 방식으로 생성된 객체는 프로토타입 객체는 constructor 프로퍼티만 지닌 객체입니다.
따라서 리터럴 방식으로 생성된 객체는 프로토타입 체이닝에 의해 Object()가 지닌 메서드인 hasOwnProperty()를 사용할 수 있지만, 생성자 함수 방식으로 생성된 객체는 이를 사용할 수 없습니다.