JavaScript가 많은 단점에도 불구하고 살아남은 이유는 JavaScript의 함수가 first-class이기 때문이라고 말씀드렸습니다. 비유를 하자면, first-class 함수는 무엇이든 만들어낼 수 있는 줄기 세포와 같아서 JavaScript의 부족한 부분을 채울 수 있었기 때문입니다.
블록 범위(block scoping)
아래 코드를 실행하면 콘솔에 1이 찍힐까요, 아니면 2가 찍힐까요?
var a = 1; { var a = 2; } console.log(a);
일반적인 기대와는 다르게 JavaScript는 콘솔에 2를 출력합니다. { } 블록이 Java나 C#의 블록 범위를 연상시키지만, JavaScript는 이런 일반적인 기대와 달리 함수 범위(function scoping)만 지원하기 때문에 같은 함수 내에서는 블록과 상관 없이 변수의 범위가 하나만 존재합니다.
하지만 실망할 필요는 없습니다. 다음과 같이 JavaScript의 익명 함수(anonymous function)를 정의한 후에 곧바로 호출하면 var a = 2
가 이 익명 함수 범위에 정의되므로 사실상 블록 범위를 지정한 것과 같은 효과를 낼 수 있습니다.
var a = 1; (function () { var a = 2; })(); console.log(a);
참고로 이 문제는 ES6에 let 키워드가 추가되면서 해결되었습니다.
비공개 필드(private field)
JavaScript는 Python, Ruby 등과 마찬가지로 동적 타이핑하는 언어이고, 동적 타이핑하는 언어는 일반적으로 메소드나 필드에 대한 접근 제한자(access modifier)를 제공하지 않습니다. 바꿔 말해, 모든 메소드나 필드가 공개(public)됩니다.
하지만 JavaScript에서는 함수를 이용하여 비공개 필드(private field)를 만들어 낼 수 있습니다. 생성자(constructor) 함수에서 지역 변수를 선언하고, 클로저(closure)로 정의한 메소드가 참조하게 만드는 방법입니다.
function Counter(init) { var count = 0; this.inc = function () { count++; console.log(count); }; this.dec = function () { count--; console.log(count); }; } var c = new Counter(); c.inc(); c.dec();
위 예제에서 count
변수는 로컬 변수이기 때문에 Counter
함수가 리턴되고 나면 더 이상 접근이 불가능하지만, inc
와 dec
메소드에서 참조하고 있기 때문에 두 메소드에서는 이후에도 계속 접근이 가능합니다. inc
, dec
메소드 외에는 이 변수에 접근할 방법이 없으므로 비공개 필드와 똑같은 효과를 냅니다.
참고로 이 문제는 ES6 Symbol 도입으로 해결하려고 했으나 아직 엄밀한 의미의 비공개 필드는 제공하지 않고 있습니다.
클래스
JavaScript는 일반적인 객체지향 프로그래밍 언어와 달리 프로토타입이라는 특이한 상속 모델을 가지고 있는 프로그래밍 언어입니다. 클래스 상속과 프로토타입 상속의 장단점을 차치하고, 일단 대부분의 개발자들에게 익숙치 않은 방식이라는 측면에서 프로토타입 상속 방식은 사실상 실패했다고 볼 수 있습니다. 다행인 것은 함수를 이용해 클래스 방식의 상속을 만들어낼 수 있다는 점입니다.
var __extends = this.__extends || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } __.prototype = b.prototype; d.prototype = new __(); }; var Animal = (function () { function Animal(name) { this.name = name; } Animal.prototype.move = function (meters) { alert(this.name + " moved " + meters + "m."); }; return Animal; })(); var Snake = (function (_super) { __extends(Snake, _super); function Snake(name) { _super.call(this, name); } Snake.prototype.move = function () { alert("Slithering..."); _super.prototype.move.call(this, 5); }; return Snake; })(Animal);
ES6에서는 class 지원이 포함되었기 때문에 위 코드는 아래와 같이 일반적인 객체지향 프로그래밍 스타일로 표현할 수 있습니다. 하지만 ES6에 class 지원이 공식적으로 포함되기 전에도 JavaScript 개발자는 위와 같이 first-class 함수를 이용해 이미 클래스 기반의 상속 메커니즘을 사용하고 있었습니다.
class Animal { constructor(public name: string) { } move(meters: number) { alert(this.name + " moved " + meters + "m."); } } class Snake extends Animal { constructor(name: string) { super(name); } move() { alert("Slithering..."); super.move(5); } }
정리
JavaScript는 1999년에 제정된 ES3 이후로 거의 10년이 넘게 새로운 표준을 제정하지 못하고 표류했습니다. 일반적인 프로그래밍 언어라면 이미 사장되고도 남을 충분한 시간이었습니다. 하지만 웹브라우저에서 사용할 수 있는 마땅한 대안 언어가 없었고, 다행히 JavaScript가 first-class 함수를 지원한 덕분에 언어의 부족한 면을 이 글에서 소개한 다양한 기술들을 이용해 메꾸어 왔습니다. ES 5, 6, 7 표준화를 통해 이런 부분들이 채워지면 위에서 소개한 방법들도 추억 속으로 사라지겠지만, 우리가 여전히 기억해야할 것은 “(first-class) 함수는 많은 것을 할 수 있다”라는 사실입니다.
Pingback: JavaScript 함수로 할 수 있는 일들 | 서광열의 코딩 스쿨