arguments 객체
자바스크립트는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지 않는다.
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(), func(1) 호출처럼 함수의 인자보다 적게 함수를 호출할 경우, 넘겨지지 않은 인자에는 undefined값이 할당된다. 이와 반대로 인자가 초과된 경우 초과된 인수는 무시된다.
자바스크립트의 이런 특성 때문에 함수 코드를 작성할 때, 런타임 시 호출된 인자의 개수를 확인하고 이데 따라 동작을 다르게 해줘야 하는 경우가 생긴다. 이를 가능하게 하는 게 arguments라는 객체다.
자바스크립트에서는 함수를 호출할 때 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달 된다. arguments 객체는 함수를 호출할 때 넘긴 인자들의 배열 형태로 저장된 객체를 의미한다. 여기서 주의할 점은 진짜 배열이 아니고 배열과 유사한 동작하는 객체이다.
arguments 객체는 매개변수 개수가 정확하게 정해지지 않은 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 해줘야 하는 함수를 개발하는데 유용하게 사용될 수 있다.
function sum() {
var result = 0;
for(var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
}
console.log(sum(1,2,3));
console.log(sum(1, 2, 3, 4, 5, 6, 7, 8));
호출 패턴과 this 바인딩
자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, arguments 객체 및 this 인자가 함수 내부로 암묵적으로 전달된다. 여기서 this는 자바스크립트의 여러 가지 함수가 호출되는 방식에 따라 this가 다른 객체를 참조하기 때문에 중요한 개념이다.
객체의 메서드를 호출할 때 this 바인딩
객체의 프로퍼티가 함수일 경우, 이 함수를 '메서드'라 부른다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩된다.
var myObject = {
name: 'foo',
sayName: function () {
console.log(this.name);
}
};
var otherObject = {
name: 'bar'
}
otherObject.sayName = myObject.sayName;
myObject.sayName(); // foo
otherObject.sayName(); // bar
위 코드에서 sayName() 메서드에서 사용된 this는 자신을 호출한 객체에 바인딩된다. 따라서 myObject.sayName()의 this는 myObject 객체를 가리킨다.
함수를 호출할 때 this 바인딩
자바스크립트에서 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩 된다. 즉 브라우저에서 자바스크립트를 실행하면 전역 객체는 window객체가 된다. 자바스크립트의 모든 전역 변수는 실제로 이러한 전역 객체의 프로퍼티들이다.
var foo = "I'm foo";
console.log(foo);
console.log(window.foo);
위 에서의 전역변수 foo는 전역 객체(window)의 프로퍼티로 된다. 즉 window.foo로 접근 가능해진다.
var test = 'This is test';
console.log(window.test);
var sayFoo = function() {
console.log(this.test); // sayFoo() 호출시 this는 전역 객체에 바인딩 됨.
}
sayFoo();
위 코드에서 전역 변수 test를 선언하고 전역 객체 window의 프로퍼티로 접근했다. sayFoo()가 호출되면 this는 전역 객체에 바인딩되므로, window에 바인딩된다. 그래서 this.test는 window.test를 의미하게 된다.
여기서 주의할 점이 있다. 내부 함수에서 this를 사용할 때 주의를 해야 한다.
var myObject = {
value: 100,
func1: function() {
this.value += 1; // 메서드에서 this는 호출하는 것을 가리키므로 myOject를 가리킴
console.log('func1() called ' + this.value);
// 내부함수
func2 = function() {
this.value += 1; // myObject를 가리킬까???
console.log('func2() called ' + this.value);
// 내부함수
func3 = function() {
this.value += 1; // this가 myObject를 가리킬까??
console.log('func3() called ' + this.value);
}
func3(); // 호출
}
func2(); // 호출
}
};
myObject.func1();
위 코드에서 앞에서 설명했던 것처럼 this는 메서드가 호출한 것을 가리키니 func1, func2, func3의 this가 모두 myObject를 가리킬 것만 같은 느낌이다. 그래서 사람의 두뇌는 다음을 예상한다.
func1() called 2
func2() called 3
func3() called 4
하지만 실제론 다음의 결과가 나온다.
func1() called 2
func2() called 101
func3() called 102
왜 그런고 하니 자바스크립트에선 내부 함수 호출 패턴을 정의해 놓지 않았기 때문이다. 내부 함수는 메서드가 아닌 함수이므로, 이를 호출할 때 함수로 취급된다. 따라서 내부 함수의 this는 전역 객체(window)에 바인딩된다.
이렇게 내부 함수가 this를 참조하는 한계를 극복하려면 부호 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용된다. 보통 관례상 that이라는 이름으로 지정한다.
// 전역 변수 value
var value = 100;
var myObject = {
value: 1,
func1: function() {
var that = this; // func1()의 this값을 that에 저장.
this.value += 1; // 메서드에서 this는 호출하는 것을 가리키므로 myOject를 가리킴
console.log('func1() called ' + this.value);
func2 = function() {
that.value += 1;
console.log('func2() called ' + that.value);
func3 = function() {
that.value += 1;
console.log('func3() called ' + that.value);
}
func3(); // 호출
}
func2(); // 호출
}
};
myObject.func1();
이제 결과는 처음 예상한 데로 다음과 같다.
func1() called 2
func2() called 3
func3() called 4
생성자 함수를 호출할 때 this 바인딩
자바스크립트에선 기존 함수에 new 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작한다. 여기서 문제는 일반 함수에 new를 붙여 호출하면 의도치 않는 생성자 함수처럼 동작하게 된다는 것이다. 그래서 자바스크립트 스타일 가이드에서는 생성자 함수를 일반 함수와 분류하기 위해 생성자 함수의 첫 문자를 대문자로 쓰라고 권하고 있다.
// Person() 생성자 함수
var Person = function(name) {
this.name = name;
};
var foo = new Person('foo');
console.log(foo.name); // foo
생성된 Person 객체는 생성자 함수 코드에서 사용되는 this로 바인딩된다.
객체 리터럴 방식과 생성자 함수를 통한 객체 생성 방식의 차이
가장 큰 차이는 객체 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성할 수 없다는 점이다.
객체 리터럴의 경우 자신의 프로토타입 객체는 Object가 되고, 생성자 함수의 경우는 실제 생성한 객체(위 코드에서 Person)로 서로 다르다.
// 객체 리터럴 방식으로 foo 생성
var foo = {
name: 'foo',
age: 35,
ggender: 'man'
};
console.dir(foo);
// 생성자 함수
function Person(name, age, gender, position) {
this.name = name;
this.age = age;
this.gender = gender;
}
var bar = new Person('bar', 33, 'man');
console.dir(bar);
var baz = new Person('baz', 25, 'woman');
console.dir(baz);
이런 차이가 발생하는 이유는 자바스크립트 객체 생성 규칙 때문이다. 자바스크립트 객체는 자신을 생성한 생성자 함수 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. 즉 리터럴 방식에선 객체 생성자 함수는 Object()이고, 생성자 함수 방식의 경우 생성자 함수 자체(Person)이므로 두 가지 방식이 다른 프로토타입 객체를 가리킨다.
생성자 함수를 new를 붙이지 않고 호출할 겨우
일반 함수 호출과 생성자 함수를 호출할 때 this 바인딩 방식이 다르다. 일반 함수 호출의 경우 this가 전역 객체(window)에 바인딩되고, 생성자 함수 호출의 경우 this는 새로 생성된 빈 객체에 바인딩된다.
// new 없이 호출
var qux = Pserson('qux', 20, 'man');
console.log(qux); // undefined
console.log(window.name); // qux
console.log(window.age); // 20
위 코드에서 new 없이 일반 함수를 호출한 경우, this는 함수 호출이 되므로 전역 객체인 window 객체로 바인딩된다. 따라서 this.name이 window.name이 되어버린다. 위 코드에서 전역 객체로 name, age, gender가 선언되진 않았지만, Person이 호출되면서 window 객체에 동적으로 name, age, gender 프로퍼티가 생성된다.
call과 apply 메서드를 이용한 명시적인 this 바인딩
apply()나 call() 메서드는 this를 원하는 값으로 명시적으로 매핑해 특정 함수나 메서드를 호출할 수 있다.
// 생성자 함수
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
var foo = {}; // 빈객체 생성
// apply() 메서드
Person.apply(foo, ['foo', 30, 'man']); // 두번째 인자에 배열로 넘김.
console.dir(foo);
// call() 메서드
Person.call(foo, 'foo', 30, 'man');
console.dir(foo);
함수 리턴
자바스크립트 함수는 항상 리턴값을 반환한다.
규칙 1) 일반 함수나 메서드는 리턴값을 지정하지 않은 경우, undifined 값이 리턴된다.
var noReturnFunc = function() {
console.log('Hello');
};
var result = noReturnFunc();
console.log(result); // Hello undefined 찍힘
규칙 2) 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.
위 규칙으로 생성자 함수에선 일반적으로 리턴값을 지정하지 않는다. 만약 생성자 함수의 리턴값으로 불린, 숫자, 문자열이 온다면 이 리턴값을 무시하고 this로 바인딩된 객체를 리턴한다.
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
return 100; // 객체가 아닌 숫자를 리턴하고 있음
}
var foo = new Person('foo', 30, 'man');
console.log(foo); // 100이 아닌 Person 객체 찍힘