이번 글에서는 우리 모두가 알고 있는 arguments
객체에 대한 깊고도 깊은 이야기를 다룹니다.
본격적으로 글을 시작하기 전에, 잠시 arguments
객체와 관련된 자바스크립트 퀴즈를 풀어보도록 하지요.
1 | function problem1(a, b, c) { |
정답: [100, 2, 3]
1 | function problem2(a, b, c) { |
정답: false
1 | function problem3(a, b, c = 10) { |
정답: [1, 2, 10]
1 | function problem4(a, b, c = 10) { |
정답: 반환값 없음(TypeError
)
1 | function problem5() { |
정답: [1, 2]
자, 몇 개나 맞추셨나요?
위 퀴즈를 전부 맞추셨다면, 이 글은 안 읽으셔도 됩니다. 저 문제들의 답이 나오는 이유와, 그 비하인드 스토리를 다루는 게 이 글의 주제이기 때문이죠!
또한 실무에서 사용할 법한 지식을 원하신다면, 이 글을 안 읽으셔도 됩니다. 왜냐면 제가 할 이야기들은 실무와 많이 동떨어진 이야기이거든요!
그렇담, 지금까지 뒤로가기를 안 누르신 여러분들께 지금부터 arguments
의 속사정을 들려드리도록 하겠습니다!
What we already knows
먼저, 우리가 알고 있는 기본적인 상식들을 살펴보고 넘어가겠습니다.
arguments
객체는 함수의 전달인자(arguments)를 담고 있는 객체입니다.arguments
객체는 ArrayLike(정수가 key로서 사용되는 객체, 배열과 같이 데이터 간의 순서가 있는 객체)입니다.- 전달인자는 왼쪽부터 인덱스를 부여받습니다(0.. 1.. 2..).
그러므로 우리는 다음과 같은 방법을 통해 함수에게 전달된 값들을 읽을 수 있습니다:
1 | function example1(a) { |
또한 arguments
객체에 담기는 값들은 전달인자이기 때문에 매개변수의 정의 여부에 상관하지 않고 읽을 수 있습니다!
1 | function example2() { |
와우! 멋진 객체이군요!
Birth of arguments object
세상 모든 게 그렇듯, arguments
객체도 필요에 의해 만들어진 개념입니다.
도대체 어떤 이유로 이런 객체를 만들었는지 알아보도록 하지요.
Rest parameter
우리가 잘 알고 있는 Rest parameter 문법은 ES6(2015)에서 추가된 문법입니다.
여기서 여러분들은 자바스크립트의 최초 버전이 1997년에 나왔다는 걸 잊지 마셔야 합니다.
도대체 그 18년동안 자바스크립트 개발자들은 어떻게 살았었을까요? 나머지 매개변수를 구현했었을까요?
정답은.. 진짜로 그 18년동안 사람들은 arguments
객체를 사용해서 나머지 매개변수를 구현해서 사용했었습니다.
바로 이렇게요:
출처: Partial Application in JavaScript - John resig1
2
3
4
5Function.prototype.curry = function() {
var fn = this, args = Array.prototype.slice.call(arguments);
return function() {
return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
앞서 살펴보았듯, arguments
객체는 매개변수의 정의 여부에 상관하지 않기 때문에 이렇게 넘쳐버린 전달인자들도 담습니다.
그래서 저렇게 몇 개든 인자를 받아서 처리할 수 있는 것이지요.
Make anonymous function Recursive!
arguments
객체에는 callee
라는 특별한 프로퍼티가 하나 있습니다.
이 프로퍼티에는 현재 실행중인 함수가 담겨 있는데요, 굳이 함수의 이름을 쓰지 않고 왜 이 프로퍼티를 사용했었을까요?
그 이유는 시간을 거슬러 내려가, 자바스크립트의 초창기 버전(ES1~ES2)에서 찾을 수 있습니다.
ES3 이전에는, 자바스크립트에 함수 표현식이 존재하지 않았었습니다.
그래서 함수를 만들기 위해 프로그래머들은 함수 선언 혹은 Function
생성자를 사용했었습니다.
1 | function myFunction() {} // 함수 선언 |
이 중, Function
생성자로 만들어진 함수는 익명 함수입니다. 생성된 함수의 이름이 'anonymous'
이기 때문이죠.
(여담이지만, 자바스크립트의 익명 함수는 함수명(function.name
) 이 'anonymous'
인 함수와 ''
인 함수, 두 가지가 있습니다.
이 이야기는 다음 포스트에서 다루도록 하겠습니다.)
자, 여기서 퀴즈 하나 나갑니다. 익명 함수는 어떻게 재귀 호출을 할 수 있을까요?
1 | function myFunction1(a) { |
당연하게도 자신을 가르키는 이름이 없기 때문에 식별자를 통한 직접적인 참조가 불가능합니다.
이러한 문제를 해결하기 위해 자바스크립트는 arguments.callee
프로퍼티를 추가했습니다.
1 | var myFunction2 = new Function('return a ? a : a + arguments.callee(a - 1);', 'a'); |
하지만 다들 아시다시피, ES3에서 함수 표현식이 추가된 이후, Function
생성자의 사용 빈도가 급격히 줄었습니다.
그리고 함수 표현식은 선택적으로 식별자를 지정할 수 있게 해 주지요.
그래서 지금은 더 이상 안 쓰이는 테크닉입니다. 정말 희귀한 케이스가 아닌 이상 안 씁니다.
Seeing internal of arguments object
자, 지금부터 본격적으로 arguments
객체의 내부를 열어보도록 하겠습니다.
가장 먼저 arguments
객체가 생성되는 부분을 보도록 하겠습니다.
NOTICE: 이 글에서는 ECMA-262, 9th edition 을 해설 목적으로 사용하고 있습니다.
엇, arguments
객체를 생성하는 Abstract operation 이 두개네요?
네, 보시다시피 arguments
객체는 내부적으로 두 가지 종류가 있습니다.
하나는 Mapped Arguments 객체구요, 하나는 Unmapped Arguments 객체입니다.
Unmapped Arguments 객체는 함수가 strict mode 이거나, 함수의 파라매터 정의에 ES6의 feature(기본 매개변수, 디스트럭쳐링 바인딩 패턴, 나머지 매개변수)가 사용된 경우에 만들어지고,
그렇지 않은 경우에는 Mapped Arguments 객체가 만들어집니다.
두 객체의 차이를 한번 알아본 뒤, 명세를 이어서 읽어보도록 하겠습니다.
Feature | Mapped Arguments Object | Unmapped Arguments Object |
---|---|---|
2-way data binding | yes | no |
kind of object | exotic | ordinary |
uses [[ParameterMap]] | yes | no |
throw error when someone tried to access ‘callee’ property | no | yes |
자, 먼저 가장 간단한 Unmapped Arguments 객체를 생성하는 CreateUnmappedArgumentsObject
를 읽어보도록 하지요.
여기서 argumentList
는 전달인자들의 배열이라고 생각하셔도 무방합니다.(ex: f(1, 2)
라면 argumentList
는 [1, 2]
)
- 1.
len
이라는 변수를 만들고,argumentList
의 길이로 초기화합니다. - 2.
Object.prototype
을 프로토타입으로 갖는 객체를 만들고,[[ParameterMap]]
이라는 내부 프로퍼티를 추가합니다. 그 후 만들어진 객체를obj
라는 변수에 넣습니다. 이 객체는 차후 Unmapped Arguments 객체가 됩니다. - 3.
obj.[[ParameterMap]]
을undefined
로 초기화합니다. (왜냐면 Unmapped Arguments 객체니까요!) - 4.
obj
에 값이len
이고 키가length
인 프로퍼티를 정의합니다. - 5~6.
obj
에argumentList
에 있는 각 argument을 값으로, 각 인덱스를 키로 하는 프로퍼티들을 정의합니다. - 7.
obj
에Symbol.iterator
을 키로,Array.prototype.values
를 값으로 하는 프로퍼티를 정의합니다. - 8.
obj
에callee
라는 accessor 프로퍼티를 정의합니다. Getter와 Setter 모두 호출 즉시TypeError
를throw
하는 함수를 넣었습니다. - 9. 완성된 Unmapped Arguments 객체를 반환합니다.
이게 Unmapped Arguments 객체의 전부입니다! 간단하지요?
이제 본격적으로, 좀 더 어려운 녀석을 살펴보도록 하겠습니다.
바로 Mapped Arguments 객체지요.
여기서 func
는 호출된 함수 객체, formals
는 함수의 매개변수가 있는 코드 조각(소스 코드), argumentsList
는 매개변수들의 리스트(배열), env
는 여러분들이 잘 아시는 Environment Record 입니다.
- 2.
len
이라는 변수를 만들고,argumentList
의 길이로 초기화합니다. - 3.
obj
라는 변수에[[ParameterMap]]
이라는 내부 프로퍼티를 가지는 빈 객체를 넣습니다. - 4~9.
obj
객체에 객체의 기본적인 동작(프로퍼티 접근, 변경, 삭제 등등등)을 정의하는 메서드들을 주입합니다. - 10.
obj
의 프로토타입을Object.prototype
으로 설정합니다. - 11.
obj
를 확장할 수 있는 객체로 만듭니다. - 12.
map
이라는 변수를 만들고 빈 객체로 초기화합니다. - 13.
obj.[[ParameterMap]]
을map
으로 설정합니다. - 14.
parameterNames
라는 변수를 만들고,formals
에 있는 매개변수 식별자들의 리스트로 초기화합니다. - 15~17.
obj
에argumentList
에 있는 각 argument을 값으로, 각 인덱스를 키로 하는 프로퍼티들을 정의합니다. - 18.
obj
에 값이len
이고 키가length
인 프로퍼티를 정의합니다. - 19~21.
obj
와 호출된 함수의 스코프간의 양방향 데이터 바인딩을 구현하기 위한 코드들입니다. (이 부분은 잠시 후에 다시 살펴보겠습니다.) - 22.
obj
에Symbol.iterator
을 키로,Array.prototype.values
를 값으로 하는 프로퍼티를 정의합니다. - 23.
obj
에'callee'
을 키로, 호출된 함수를 값으로 하는 프로퍼티를 정의합니다. - 24. 완성된 Mapped Arguments 객체를 반환합니다.
자, 여기까지는 Mapped Arguments 객체가 생성되는 과정이였습니다.
그럼 이제 어떻게 양방향 바인딩이 구현되었는지 살펴보도록 하지요.
- 19.
mappedNames
라는 변수를 만들고 빈 리스트로 초기화합니다. - 20.
index
라는 변수를 만들고,numberOfParameters - 1
한 값을 넣습니다. <–numberOfParameters
의 값은parameterNames
의 길이입니다. - 21.
index
가 0보다 작아질때까지 아래의 단계를 반복합니다.- a.
name
이라는 변수를 만들고, 그 값을parameterNames[index]
(parameterNames
가 리스트이므로index
번째 요소를 가져오는 것 입니다.) 로 합니다. - b. 만약
name
이mappedNames
에 포함되있지 않다면,- i.
name
을mappedNames
의 가장 마지막 요소로서 추가합니다(push). - ii. 만약
index < len
이라면,- 1.
g
라는 변수를 만들고,MakeArgGetter(name, env)
의 결과값으로 초기화합니다. (MakeArgGetter
는 이따 살펴보겠습니다.) - 2.
p
라는 변수를 만들고,MakeArgSetter(name, env)
의 결과값으로 초기화합니다. (MakeArgSetter
도 이따 살펴보겠습니다.) - 3.
map
에String(index)
를 키로 하고,g
를 Getter로,p
를 Setter로 하는 접근자 프로퍼티를 정의합니다.
- 1.
- i.
- c.
index
의 값을1
줄입니다.
- a.
처음에 CreateMappedArgumentsObject
를 설명해드릴 때 생략한 부분인데요.
이 부분에서는 arguments
객체의 프로퍼티와, 호출된 함수의 스코프를 이어주는 접근자를 생성해주는 MakeArgGetter
와 MakeArgSetter
을 이용해
각 매개변수와 arguments
에 들어있는 전달인자의 값을 연결시켜주고 있습니다.
자, 그럼 이제 MakeArgGetter
와 MakeArgSetter
가 어떻게 둘을 이어주는 지 알아보도록 하겠습니다.
name
은 아시다시피 매개변수의 이름이구요, env
는 Running Execution Context의 Lexical Environment의 Environment Record 입니다.
(쉽게말해 현재 실행중인 코드의 스코프라는 이야기입니다.)
- 1.
steps
라는 변수를 만들고 아래에 있는ArgGetter
함수로 초기화합니다. - 2.
getter
이라는 변수를 만들고,CreateBuiltinFunction(steps, << [[Name]], [[Env]] >>)
의 결과값으로 초기화합니다.ArgGetter
을 호출할 수 있는 자바스크립트의 함수로 만들고, 내부 프로퍼티인[[Name]]
과[[Env]]
를 정의하는 겁니다.) - 3.
getter.[[Name]]
의 값을name
으로 설정합니다. - 4.
getter.[[Env]]
의 값을env
로 설정합니다. - 5.
getter
를 반환합니다.
아쉽게도 MakeArgGetter
에는 직접적으로 둘을 이어주는 로직은 없습니다. 그건 ArgGetter
의 역할이거든요.
자, 이제 ArgGetter
을 보도록 하겠습니다.(ArgGetter
에서 f
는 호출된 ArgGetter
를 의미합니다. arguments.callee
와 같게 보셔도 무방합니다.!)
- 1.
name
이라는 변수를 선언하고, 그f.[[Name]]
으로 초기화합니다. - 2.
env
이라는 변수를 선언하고, 그f.[[Env]]
으로 초기화합니다. - 3.
env.GetBindingValue(name, false)
의 결과값을 반환합니다.(아까 받은 스코프에서name
과 묶인 값을 찾는 부분입니다.)
그리 어렵지는 않지요? 그럼 이제 MakeArgSetter
를 보도록 하겠습니다.
여기서도 name
은 매개변수의 이름이구요, env
는 Running Execution Context의 Lexical Environment의 Environment Record 입니다.
- 1.
steps
라는 변수를 만들고 아래에 있는ArgSetter
함수로 초기화합니다. - 2.
setter
이라는 변수를 만들고,CreateBuiltinFunction(steps, << [[Name]], [[Env]] >>)
의 결과값으로 초기화합니다.ArgSetter
을 호출할 수 있는 자바스크립트의 함수로 만들고, 내부 프로퍼티인[[Name]]
과[[Env]]
를 정의하는 겁니다.) - 3.
setter.[[Name]]
의 값을name
으로 설정합니다. - 4.
setter.[[Env]]
의 값을env
로 설정합니다. - 5.
setter
를 반환합니다.
MakeArgSetter
에도 직접적으로 arguments
객체의 프로퍼티와 함수의 스코프에 있는 변수를 이어주는 로직은 없습니다.
그건 ArgSetter
의 역할이기 때문이죠.
자, 이제 ArgSetter
을 보도록 하겠습니다.(여기서도 f
는 호출된 ArgSetter
입니다., value
는 다들 아시는 setter의 매개변수(새로 set 될 값)입니다.)
- 1.
name
이라는 변수를 선언하고, 그f.[[Name]]
으로 초기화합니다. - 2.
env
이라는 변수를 선언하고, 그f.[[Env]]
으로 초기화합니다. - 3.
env.SetMutableBinding(name, value, false)
의 결과값을 반환합니다.(아까 받은 스코프에서name
과 묶인 값을 변경하는 부분입니다.)
자, 이렇게 우리는 기초적인 부분들을 살펴 보았습니다.
하지만 아직 여러분들이 보지 않은 부분이 있습니다. 바로 위에서 우리가 양방향 바인딩을 위해 만들고 arguments
에 붙인 접근자들이 호출되는 부분들이죠.
지금부터는 그 부분들을 찾아서 보도록 하겠습니다.
그 전에, 기반 지식에 대한 간단한 설명을 먼저 하도록 하겠습니다.
자바스크립트의 객체들의 시멘틱은 internal method라는 것에 의해 정의됩니다.
어떤 객체에 대한 동작/연산들이 internal method 라는 것을 통해 정의된다는 이야기인데요.
다만, 불규칙하게 정의되면 구현체 뿐만 아니라 명세에서도 각 객체를 위한 로직을 따로 작성해야 하므로 그 불편함을 없에기 위해 자바스크립트 명세에서는 기본적인 동작들을 위한 인터페이스를 정의하고, 최소한의 조건을 명시하고 있습니다.
또한, 자바스크립트 명세에서는 특별한 internal method가 필요하지 않은 객체들을 위해 앞서 언급한 최소한의 조건을 충족하는(또한 default behavior인) internal method들을 제공하고 있습니다.
만약, 한 객체의 기본적인 동작들을 구현하고 있는 internal method들이 전부 default behavior인 internal method라면, 그 객체를 ordinary 객체라고 부릅니다.
그렇지 않다면 Exotic 객체라고 부르지요. 위의 arguments
객체의 경우도 Unmapped arguments 객체는 ordinary 객체, Mapped arguments 객체는 Exotic 객체입니다.
Mapped arguments 객체는 Exotic 객체다. 감이 오지 않으시나요?
Mapped arguments 객체는 객체의 몇몇(Get/Set/Delete …etc) 연산이 default behavior가 아닌, 다른 말로 특별한 로직이 있는 객체입니다.
자, 그럼 지금부터 위에서 살펴본 접근자들을 호출하는 로직이 들어있는, arguments 객체의 프로퍼티 접근/수정 연산을 다루는 [[Get]]
internal method와 [[Set]]
internal method를 살펴보도록 하겠습니다.
여기서 P
는 접근할 프로퍼티 키, Receiver
는 접근할 프로퍼티가 접근자 프로퍼티라면, 적절한 접근자를 호출하게 되는데 이 때 접근자의 this
값으로서 사용되는 값 입니다.
- 1.
args
라는 변수를 선언하고, 이 internal method가 담겨있는arguments
객체로 초기화합니다. - 2.
map
이라는 변수를 선언하고,args.[[ParameterMap]]
으로 초기화합니다. - 3.
isMapped
라는 변수를 선언하고,! HasOwnProperty(map, P)
의 결과로 초기화합니다. (자바스크립트 코드로 표현하자면map.hasOwnProperty(P)
와 같은 로직입니다.) - 4~5. 만약
isMapped
가false
라면arg
(arguments
)객체에서 프로퍼티를 찾고 그 결과를 반환합니다. - 6. 그렇지 않은 경우,
map
객체에서 프로퍼티를 찾고 그 결과를 반환합니다.(프로퍼티를 찾을 때, 찾은 프로퍼티가 접근자 프로퍼티이면, 그 프로퍼티의 Get 접근자를 실행시킵니다. 이 부분이 바로 우리의ArgsGetter
접근자가 호출되는 부분입니다.)
짠, 거이 다 왔습니다. 힘내세요!
지금부터 볼 녀석은 [[Set]]
internal method 입니다.
여기서도 P
는 접근할 프로퍼티 키, Receiver
도 접근할 프로퍼티가 접근자 프로퍼티라면, 적절한 접근자를 호출하게 되는데 이 때 접근자의 this
값으로서 사용되는 값 입니다.
그리고.. V
는 새로 설정중인 값 입니다.
NOTICE: isMapped
변수의 스코프가 이상해 보이신다면, 정상이십니다. 저도 방금 알았네요. 이슈로 올려야겠어요.
- 1.
args
라는 변수를 선언하고, 이 internal method가 담겨있는arguments
객체로 초기화합니다. - 2 & 5.
SameValue(args, Receiver)
가false
라면args
객체에 키가P
이고 값이V
인 프로퍼티를 설정하고 그 결과를 반환합니다. - 3 & 4.
args.[[ParameterMap]]
객체에P
를 키로 갖는 프로퍼티가 있으면 그 프로퍼티에V
를 설정합니다(여기서ArgSetter
가 호출됩니다!) - 5.
args
객체에 키가P
이고 값이V
인 프로퍼티를 설정하고 그 결과를 반환합니다.
Time to explain!
자, 지금까지 우리는 arguments 객체의 종류, arguments 객체가 어떻게 생성되는 과정, 어떻게 양방향 데이터 바인딩을 구현했는지를 보았습니다.
이 정도면 여러분들 스스로를 arguments
학사 라고 칭하셔도 됩니다.(이 글에서 다루지 않은 디테일하지만 크게 중요하지는 않은 사항들이 남았거든요..ㅜㅜ)
자, 이제 마지막으로, 글의 서문에서 여러분들이 풀은 문제에 대한 해설을 지금까지 배운 지식들을 기반으로 해설해드리도록 하겠습니다.
1번 문제: Mapped arguments 객체는 자신의 프로퍼티의 변경사항을 매개변수에 반영시킵니다.1
2
3
4
5
6function problem1(a, b, c) {
arguments[0] = 100;
return [a, b, c];
}
problem1(1, 2, 3) // 반환값을 맞추어 보세요!
정답: [100, 2, 3]
간단한 문제입니다. problem1
에서의 arguments
객체는 Mapped Arguments 객체이기 때문에, arguments[0]
과 a
사이의 바인딩이 생기게 됩니다.
그래서 arguments[0]
을 수정했을 때, 그 변경사항이 a
에도 반영된 것이지요.
2번 문제: [[ParameterMap]]
가 공유한 프로퍼티는 arguments
객체의 own 프로퍼티가 아닙니다.1
2
3
4
5function problem2(a, b, c) {
return Object.hasOwnProperty(arguments, 0);
}
problem2(1, 2, 3) // 반환값을 맞추어 보세요!
정답: false
arguments
객체를 통해 arguments.[[ParameterMap]]
에 있는 프로퍼티를 접근할 수 있음에도 불구하고, 접근할 수 있는 프로퍼티들이 arguments
객체에 정의되있는 프로퍼티는 아니기에 false
가 나옵니다.
여담으로, 프로토타입 객체의 프로퍼티가 공유되는 방식은 arguments.[[ParameterMap]]
의 프로퍼티가 공유되는 방식과 같은 방식입니다.
3번 문제: Unmapped arguments 객체는 자신의 프로퍼티의 변경사항을 매개변수에 반영시키지 않습니다.1
2
3
4
5
6function problem3(a, b, c = 10) {
arguments[0] = 100;
return [a, b, c];
}
problem3(1, 2) // 반환값을 맞추어 보세요!
정답: [1, 2, 10]
problem3
의 매개변수 중, c
가 기본값 문법을 사용하고 있기 때문에 Mapped arguments 객체가 아닌 Unmapped arguments 객체가 생성됩니다.
그리고.. 아시다시피 Unmapped arguments 객체는 양방향 바인딩을 구현하고 있지 않지요. 그래서 arguments
객체의 프로퍼티를 수정하여도 그 변경사항이 problem3
의 매개변수에게 반영되지 않습니다.
4번 문제: arguments.callee
는 Unmapped arguments 객체에서 접근할 수 없습니다.1
2
3
4
5function problem4(a, b, c = 10) {
return arguments.callee;
}
problem4(1, 2) // 반환값을 맞추어 보세요!
정답: 반환값 없음(TypeError
)
여기서도 문제 3과 같은 이유로 Unmapped arguments 객체가 생성됩니다. 아시다시피 Unmapped arguments 객체의 callee
프로퍼티는
get 이든 set 이든 TypeError
를 일으키는 접근자로만 이루어진 접근자 프로퍼티입니다.
5번 문제: arguments
객체는 배열이 아닙니다.1
2
3
4
5
6function problem5() {
arguments[2] = 3;
return Array.prototype.slice.call(arguments);
}
problem5(1, 2); // 반환값을 맞추어 보세요!
정답: [1, 2]
아까 우리가 살펴 본 [[Set]]
internal method에는 프로퍼티의 수가 변경되었다고 해서 arguments
객체의 length
프로퍼티를 수정하는 로직을 가지고 있지 않았습니다.
하지만 Array.prototype.slice
메서드는 주어진 객체의 length
를 가지고 주어진 객체를 탐색하고 복사하지요.
그렇기에 Array.prototype.slice
는 length
(2)만 보고 arguments
객체의 '2'
을 키로 갖는 프로퍼티를 복사하지 않게 됩니다.
End
후우, 드디어 이 길고 긴 글이 끝을 맺게 되었네요.
부족한 필력에도 뒤로가기를 누르시지 않고 여기까지 읽어주셔서 감사합니다 :)
다음에는 더 좋은 자료로 돌아오도록 하겠습니다. 모두 즐거운 금요일 되세요!