[js] 자바스크립트의 할당연산자 고찰

개요

컴파일 제어형 언어에는 대부분 베이직이나 파스칼 등에서는 할당(assignment)이 문(statement)로 처리되지만 스크립트언어는 많은 경우 식(expression)으로 처리됩니다.
즉 할당하고 난 값이 반환되는 식이죠. 자바스크립트의 = 연산자도 할당을 처리하는 식으로서 작동하여 값을 반환하게 됩니다.
간단히 다음의 식을 보죠.

var a, b, c;
a = b = c = 1;

자바스크립트에서 할당식은 매우 특이한 연산자로 모든 연산자가 좌에서 우로 해석되는데 비해 유일하게 우에서 좌로 해석되는 연산자입니다.
위의 식은 내부적으로는 아래와 같은 순서대로 평가됩니다.
assign0

  1. 할당식은 우선 좌변이 평가되고
  2. = 기준으로 우변이 평가됩니다.
  3. 따라서 처음 좌변의 a라는 식별자가 존재하는지 평가하고
  4. = 의 우변인 “b = c = 1″이 평가됩니다.
  5. 우변도 할당식이므로 b식별자가 평가되고
  6. 그 우변인 “c = 1″이 평가되며
  7. 마찬가지로 c가 먼저 식별자로 평가된 뒤 값인 1이 평가됩니다.
  8. 평가가 끝나면 c = 1 이 성립하여 이 결과 c에 할당된 값인 1이 이 식의 결과(1)가 되며
  9. 8번의 결과가 b = 1로 할당되면서 이 식도 결과적으로 1이 됩니다.
  10. 마지막으로 a에는 우변의 최종 값인 1이 할당되면서 전체식은 a에 할당된 1이 됩니다.

이러한 기본적인 할당식의 원리를 이용해 여러가지 상황을 설명해보도록하죠.

좌변이 우선 평가됨

좌변 우선 평가를 실습하기 위해 간단히 [key, value..] 쌍으로 되어있는 배열을 오브젝트로 변환하는 샘플을 작성해보도록 하겠습니다.
아래와 같은 배열이 있다고 생각해보죠.

var arr = ['name', 'hika', 'company', 'bsidesoft', 'home', 'https://www.bsidesoft.com'];

이제 이러한 키밸류쌍으로 되어있는 배열을 아래와 같은 오브젝트로 바꾸고 싶습니다.

var obj = {
  name:'hika',
  company:'bsidesoft',
  home:'https://www.bsidesoft.com'
}

이러한 변환을 처리하는 간단한 로직을 짜보죠.

var converter = function(arr){
  var result = {}, //결과객체
    i = 0, j = arr.length; //루프준비

  while(i < j){ //변환하여 처리
    result[arr[i++]] = arr[i++];
  }
  return result;
}
//실행!
var obj = converter(arr);

위의 converter함수에서 핵심적인 부분은 while안에 있는 result[arr[i++]] = arr[i++]; 입니다.

  1. 개요에서 언급한대로 좌변을 우선적으로 해석하기 때문에
  2. result[arr[i++]] 이 먼저 해석되며 최초 i는 0 이므로 result[i[0]] result[arr[0]]이 되고 i는 사후적으로 1이 됩니다.
  3. 이제 우변이 해석되는데 arr[i++] 에서 i는 좌변의 결과로 이미 1이 된 상태이므로 arr[1]이 되고 할당식이후에 2가 됩니다.
  4. 같은 형식으로 반복되면 result[arr[0]] = arr[1], result[arr[2]] = arr[3], result[arr[4]] = arr[5]… 라는 식으로 전개됩니다.

좌변이 먼저 해석되고 우변이 다음에 해석된 뒤 할당된다는 점만 기억하면 어렵지 않습니다.

좌변의 평가가 식별자 자체인 경우

우선 ECMAScript 5.1 스펙을 살펴보면 다음과 같습니다.

11.13.1 Simple Assignment ( = )

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

1. Let lref be the result of evaluating LeftHandSideExpression.
2. Let rref be the result of evaluating AssignmentExpression.
3. Let rval be GetValue(rref).
4. Throw a SyntaxError exception if the following conditions are all true:
* Type(lref) is Reference is true
* IsStrictReference(lref) is true
* Type(GetBase(lref)) is Environment Record
* GetReferencedName(lref) is either “eval” or “arguments”
5. Call PutValue(lref, rval).
6. Return rval.

NOTE When an assignment occurs within strict mode code, its LeftHandSide must not evaluate to an unresolvable reference. If it does a ReferenceError exception is thrown upon assignment. The LeftHandSide also may not be a reference to a data property with the attribute value {[[Writable]]:false}, to an accessor property with the attribute value {[[Set]]:undefined}, nor to a non-existent property of an object whose [[Extensible]] internal property has the value false. In these cases a TypeError exception is thrown.

(머래..^^;)
1. 간단히 요약하면 좌변의 식을 먼저 평가하고,
2. 우변의 할당할 식을 평가한 뒤
3. 좌변의 결과가 맘에 안들면 throw하고
4. 값을 할당한뒤
5. 우변의 평가값을 반환한다

라고 되어있습니다. NOTE에는 es5의 defineProperty로 할당된 속성의 경우에 대해서 다루고 있습니다.

헌데 ECMA2015에는 스펙이 더욱 추가되어서 다음과 같은 구문이 들어있습니다.

12.14.1 Static Semantics: Early Errors

AssignmentExpression : LeftHandSideExpression = AssignmentExpression

* It is a Syntax Error if LeftHandSideExpression is either an ObjectLiteral or an ArrayLiteral and the lexical token sequence matched by LeftHandSideExpression cannot be parsed with no tokens left over using AssignmentPattern as the goal symbol.
* It is an early Reference Error if LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral and IsValidSimpleAssignmentTarget of LeftHandSideExpression is false.

AssignmentExpression : LeftHandSideExpression AssignmentOperator AssignmentExpression

* It is an early Reference Error if IsValidSimpleAssignmentTarget of LeftHandSideExpression is false.

머 딴건 비슷한데 IsValidSimpleAssignmentTarget of LeftHandSideExpression is false. 라는 문구가 추가되었습니다.
그럼 IsValidSimpleAssignmentTarget는 머냐..그것도 스펙문서에 같이 있습니다.

12.13.2 Static Semantics: IsValidSimpleAssignmentTarget

ConditionalExpression : LogicalORExpression ? AssignmentExpression : AssignmentExpression
1. Return false.

별거 없습니다. 3항식이 안된다는 것입니다. 즉 이를 표현하면 아래와 같습니다.

//대상을 2개로 만든다.
var target1, target2;

//평가를 위한 임의의 변수
var a = 3;

//es5까지는 문제없고 es6에서부터는 문제가 됨
(a > 3 ? target1 : target2) = 3;

좌변에 3항표현식이 들어가서 할당할 대상을 규정하는 평가식이 오는 경우 es5까지는 문제없는 표현이었지만 es6부터는 오류가 됩니다.
실제 테스트해보면 IE11까지와 edge에서는 무사통과하지만 chrome에서는 Uncaught ReferenceError: Invalid left-hand side in assignment 으로 es6스펙 그대로 에러가 납니다.

하지만 3항식이 포함된 경우라도 대상이 단일하게 평가되는 경우는 문제없습니다.

var target1 = {}, target2 = {}, a = 3;

//문제없음
(a > 3 ? target1 : target2).value = 5;

위의 예제는 문제없습니다.(3항식이 평가된 이후엔 할당할 대상이 한개가 되기 때문에 내부적인 니모닉을 생성하는데 있어서 문제가 없기 때문일 것으로 ^^;)

우변을 할당한 결과가 다른 값인 경우의 결과값

일반적인 변수는 우변의 할당값이 같은 값 그대로 할당됩니다. 하지만 실제 그 값이 그대로 반영되는가는 객체의 속성에 대한 설정에 달려있습니다.
예를들어 es5에서 정의된 함수객체의 name속성을 예로 들어보죠.

//test라는 기명함수를 만들자!
var test = function test(){};
//test의 name은 'test'다
console.log(test.name); //'test'

//name을 바꿔보자
test.name = 'hika';

//하지만 바뀌지 않는다.
console.log(test.name); //'test'

이는 함수의 name속성이 함수표현식을 통해 생성할때의 값으로 고정되어 읽기 전용이 되는 내부 설정에 기인합니다.
(function.name(es6))

그럼 이 할당식의 결과를 다른 변수에 할당하면 어떻게 되는건가요?

var test = function test(){}, a;

a = test.name = 'hika';

console.log(a); //'test'일까 'hika'일까?

우선 정답부터 보면 ‘hika’입니다. 이유는 할당식의 결과는 우변의 평가결과값이지 좌변에 할당된 이후의 값이 아니기 때문입니다.
즉 다음과 같은 순서로 처리됩니다. 위의 섹션에서 다뤘던 es5의 공식문서를 다시 복습하면

  1. 우선 좌변의 a식별자가 확인됩니다.
  2. 우변의 test.name = ‘hika’ 가 평가됩니다.
  3. 이 식에서 우변의 값을 ‘hika’ 이므로 이 값을 test.name에 할당하려고 시도합니다. 하지만 이 식의 값은 우변의 값인 ‘hika’가 됩니다.
  4. 따라서 우변식의 결과인 ‘hika’가 a에 할당되고 최종적으로 식의 결과도 ‘hika’가 됩니다.

머야..쉽습니다. 걍 가장 오른쪽에 있는 넘이 이 식의 최종값이고 모든 할당 중간단계에 있는 식별자들에게 주어지는 값입니다.
즉 아래와 같은 복잡해보이는게 있다고 해도..

func1.name = a = func2.name = b = func3.name = 'hika';

그저 ‘hika’가 모든 식별자들에게 할당되려고 시도된다는 걸 알 수 있습니다.

컴마연산자와의 결합

할당연산자가 식이므로 컴마연산자와 결합하면 다중상태를 한 번에 변경할 수 있는 복잡한 상태수식으로 발전 합니다.
예를 들어 읽어들인 배열이 id, 속성, 값 형태가 연쇄된다고 생각해보죠.
그럼 우선 빈 오브젝트에 해당 id키가 존재하지않으면 id에 해당되는 오브젝트를 만들어준 뒤, 그 오브젝트의 키에 값을 할당해야 할 것입니다.
이는 다음과 같은 복합적인 할당식으로 해결할 수 있습니다.

var arr = ['hika', 'tel', '0000', 'jidolstar', 'tel', '1111', 'hika', 'addr', 'mapo'];
var result = {};
var i = 0, j = arr.length;
while(i < j ){
  result[result[arr[i]] || (result[arr[i]] = {}), arr[i++]][arr[i++]] = arr[i++];
}
/*
result = {
  'hika':{'tel':'0000', 'addr':'mapo'},
  'jidolstart':{'tel':'1111'}
}
*/

핵심이 되는 while안의 할당식만 살펴보겠습니다.

  1. 우선 식의 레이아웃을 보면 result[표현식][키] = 값 형태입니다.
  2. 이 중 표현식은 반드시 id 항목이 되어서 결국 result[아이디][키] = 값 이 되어야할 것입니다.
  3. 따라서 표현식의 정체는 result[아이디] 가 없으면 {}를 만든다, 아이디 라는 식입니다. 컴마연산자를 이용하면 뒤의 값만 반환되니까요.
  4. 그럼 컴마 앞 부분만 살펴보면 result[arr[i]] || (result[arr[i]] = {}) 입니다. 만약 result[아이디] 가 없으면 result[아이디] = {} 하는 식이죠.
  5. 그러고나서 arr[i++]을 반환하면서 다음 항목인 키를 가리키게 하고 있습니다.
  6. result[…][arr[i++]] 부분에서 키를 설정하고 다시 값을 가리키게 ++로 넘겨주고 있습니다.
  7. 마지막으로 우변에서도 i++을 통해 값을 할당값으로 내어주고 다음 레코드로 이동시켜주는 것이죠.

위의 할당식이 이제 그리 안어렵게 느껴진다면 자바스크립트가 할당을 처리하는 순서와 매커니즘을 거의 정복한 셈입니다(짝짝! 축하합니다!)

결론

자바스크립트의 식에는 많은 연산자가 등장하지만 이 중 우에서 좌로 진행되는 식은 오직 할당식 뿐입니다. 그런 만큼 모든 수식에서 특이점을 만들어내기 때문에 값으로 환원되는 명백한 식임에도 반드시 괄호를 묶어서 처리해야하는 상황이 자주 발생합니다. 다른 수식과는 달리 특성도 매우 독특하여 매커니즘을 따로 이해할 필요가 있는 어려운 연산자입니다.
많은 분들에게는 별개 아니겠지만..제게는 항상 어렵고 오류를 유발하는 언어의 특이점입니다.

자바나 c등의 알골60 컴파일계 자손들은 이를 문(statement)으로 처리하고 있는지도 모를 일입니다. 최근 델파이를 쓰다보니 혼동이 온듯. 페친께서 일께워주셔서 바로잡았습니다! 감사합니다.

%d 블로거가 이것을 좋아합니다: