ES6 Class 파헤치기

Index

  1. ES6 Class 문법
  2. Class 정의
  3. constructor
  4. Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교
  5. super 키워드
  6. static 키워드
  7. 마치며

ES6 Class 문법

JavaScript Class는 ECMAScript 6을 통해 소개되었습니다. ES6의 Class는 기존 prototype 기반의 상속을 보다 명료하게 사용할 수 있도록 문법을 제공합니다. 이를 Syntatic Sugar라고 부르기도 합니다.

Syntatic Sugar : 읽고 표현하는것을 더 쉽게 하기 위해서 고안된 프로그래밍 언어 문법을 말합니다.

JavaScript를 ES6를 통해 처음 접하시는 분들은 알아두셔야할 것이 JavaScript의 Class는 다른 객체지향 언어(C++, C#, Java, Python, Ruby 등…)에서 사용되는 Class 문법과는 다르다는 것입니다. JavaScript에는 Class라는 개념이 없습니다.
Class가 없기 때문에 기본적으로 Class 기반의 상속도 불가능합니다. 대신 다른 언어에는 존재하지 않는 프로토타입(Prototype)이라는 것이 존재합니다. JavaScript는 이 prototype을 기반으로 상속을 흉내내도록 구현해 사용합니다. Prototype을 처음 접하시는 분은 “Prototype 이제는 이해하자”를 참고하시면 도움이 될것같습니다.


Class 정의

JavaScript에서 Class는 사실 함수입니다. 함수를 함수 선언함수 표현식으로 정의할 수 있듯이 class 문법도 class 선언class 표현식 두가지 방법으로 정의가 가능합니다.

JavaScript 엔진은 function 키워드를 만나면 Function 오브젝트를 생성하듯, class 키워드를 만나면 Class 오브젝트를 생성합니다. class는 클래스를 선언하는 키워드이고 Class 오브젝트는 엔진이 class 키워드로 생성한 오브젝트입니다.

Class 선언

함수 선언과 달리 클래스 선언은 호이스팅이 일어나지 않기 때문에, 클래스를 사용하기 위해서는 먼저 선언을 해야합니다. 그렇지 않으면 ReferenceError 가 발생합니다.

1
2
3
4
5
6
7
8
9
class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

Class 표현식

Class 표현식은 이름을 가질 수도 있고 갖지 않을 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const People = class People {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

const People = class {
constructor(name) {
this.name = name;
}

say() {
console.log('My name is ' + this.name);
}
}

constructor

constructor는 클래스 인스턴스를 생성하고 생성한 인스턴스를 초기화하는 역할을 합니다. new People() 코드를 실행하면 People.prototype.constructor가 호출됩니다. 이를 default constructor라고 하며 constructor가 없으면 인스턴스를 생성할 수 없습니다.

1
const people = new People('KimJongMin');

new People(‘KimJongMin’)을 실행하면 People 클래스에 작성한 constructor가 자동으로 호출되고 파라미터 값으로 ‘KimJongMin’을 넘겨 줍니다.

new 연산자가 인스턴스를 생성하는 것처럼 보이지만, 사실 new 연산자는 constructor를 호출하면서 파라미터를 넘겨주는 역할만 합니다. 호출된 constructor가 인스턴스를 생성하여 반환하면 new 연산자가 받아 new를 실행한 곳으로 반환합니다. 과정은 다음과 같습니다.

  1. new People(‘KimJongMin’)을 실행
  2. new 연산자가 constructor를 호출하면서 파라미터 전달
  3. constructor에 작성한 코드를 실행하기 전에 빈 Object 를 생성
  4. constructor 코드를 실행
  5. 생성한 Object(인스턴스)에 property 할당 (인스턴스를 먼저 생성했기 때문에 this로 Object 참조 가능
  6. 생성한 Object 반환

다음은 생성된 인스턴스의 구조입니다.

1
console.dir(people);

people 인스턴스의 __proto__는 People Class 오브젝트와 함께 생성된 Prototype object를 가리키고 있습니다. 결국 Class 문법을 이용한 코드를 prototype 기반의 코드로 변경하면 다음과 같습니다.

1
2
3
4
5
6
7
function People(name) {
this.name = name;
}

People.prototype.say = function () {
console.log('My name is ' + this.name);
};

Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교

먼저 ES5에서 Prototype을 사용하여 상속을 구현하는 방법을 살펴보고, 그 후 ES6에서 Class로 상속을 구현하는 형태를 보겠습니다.

ES5 Prototype 기반 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Cat(name) {
this.name = name;
}

Cat.prototype.speak = function () {
console.log(this.name + ' makes a noise.');
};

function Lion(name) {
// `super()` 호출
Cat.call(this, name);
}

// `Cat` 클래스 상속
Lion.prototype = Object.create(Cat.prototype);
Lion.prototype.constructor = Lion;

// `speak()` 메서드 오버라이드
Lion.prototype.speak = function () {
Cat.prototype.speak.call(this);
console.log(this.name + ' roars.');
};

var lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

new Lion()을 실행하면 Lion()이 호출되고, default constructor를 호출합니다. 그래서 Lion()을 생성자(constructor) 함수라고 합니다.

생성자 함수가 있으면 Cat.prototype.speak와 같이 prototype에 메서드를 연결한 코드가 있습니다. 이와 같이 prototype에 작성하지 않으면 각각의 인스턴스에 메서드가 생성되게 됩니다. 이 형태가 ES5에서 인스턴스를 구현하는 기본 형태 입니다.

Object.create()를 통해 Cat.prototype에 연결된 메서드를 Lion.prototype.__proto__에 첨부합니다. Lion.prototype에는 constructor가 연결되어 있는데 prototype을 재 할당했기 때문에 지워진 constructor를 다시 할당해 줍니다.

결과적으로 Lion 생성자 함수의 구조는 다음과 같습니다.

ES6 Class 기반 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Cat {
constructor(name) {
this.name = name;
}

speak() {
console.log(this.name + ' makes a noise.');
}
}

class Lion extends Cat {
speak() {
super.speak();
console.log(this.name + ' roars.');
}
}

const lion = new Lion('Samba');
lion.speak();

[결과]
Sambda makes a noise.
Sambda roars.

ES6에서는 extends 키워드로 상속을 구현합니다. Cat 클래스를 상속받은 Lion 클래스의 구조는 다음과 같습니다.

위의 prototype을 통해 상속을 구현한 Lion 생성자 함수의 구조와 비교했을때 일치합니다. 추가적으로 new Lion(‘Samba’) 를 실행하면 다음의 과정을 거치게됩니다.

  1. Lion 클래스의 constructor를 호출
  2. Lion 클래스에 constructor를 작성하지 않았기 때문에 슈퍼 클래스의(Cat) constructor가 호출됨 (내부적으로 프로토타입 체인으로 인해)
  3. 슈퍼 클래스의 constructor에서 this는 현재의 인스턴스를 참조하므로 인스턴스의 name 프로퍼티에 파라미터로 전달받은 값을 설정
  4. 생성한 인스턴스를 lion에 할당

super 키워드

서브 클래스와 슈퍼 클래스에 같은 이름의 메서드가 존재하면 슈퍼 클래스의 메서드는 호출되지 않습니다. 이때 super 키워드를 사용해서 슈퍼 클래스의 메서드를 호출할 수 있습니다. (서브 클래스의 constructor에 super()를 작성하면 슈퍼 클래스의 constructor가 호출됩니다.)


static 키워드

static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 prototype에 연결되지 않고 클래스에 직접 연결되기 때문에 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 동일한 클래스 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우 키워드 this를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
class Lion {
static speak() {
console.log('Noise~');
}
}

Lion.speak();

[결과]
Noise~

정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.


마치며

ES6의 Class 문법에 대해 정리해 보았다. JavaScript 언어를 약 1년전 Node.js 를 시작하며 처음 접하게 되었는데 사실 그 당시 Prototype과 상속에 대해 크게 다룰일이 없었다. (어쩌면 너무 무지해서 사용 필요성을 느끼지 못했을 수도…) 그 후 Node.js 버전을 올리고 ES6를 공부하며 Class 문법을 접하게 되었는데 JavaScript의 Prototype에 대한 이해와 지식이 부족하다 보니 이전에 공부했던 C++과 Java의 Class 처럼 이해했던 것 같다. 그래도 그 후 Prototype과 더불이 Class까지 공부하며 지금은 어느정도 이해하게 된것 같다. 결론은… 역시나 JavaScript에서 Prototype을 이해하는건 중요한것 같다.

참고
ES6 Class는 단지 prototype 상속의 문법설탕일 뿐인가?
MDN - Classes
MDN - 상속과 프로토타입

메모이제이션 (Memoization)

Index

  1. 메모이제이션 이란?
  2. 메모이제이션 적용하기
  3. 결과 비교
  4. 마치며

메모이제이션 이란?

자바 스크립트에서 함수는 객체이기 때문에 프로퍼티를 가질 수 있습니다. 그리고 언제든지 함수에 사용자 정의 프로퍼티를 추가할 수도 있습니다. 함수에 프로퍼티를 추가하여 결과(반환 값)을 캐시하면 다음 호출 시점에 복잡한 연산을 반복하지 않을 수 있습니다. 이런 활용 방법을 메모이제이션 패턴이라고 합니다.

다음 코드에서는 myFunc 함수에 cache 프로퍼티를 생성합니다. 이 프로퍼티는 일반적인 프로퍼티처럼 myFunc.cache와 같은 형태로 접근할 수 있습니다. cache 프로퍼티는 함수로 전달된 param 매개변수를 키로 사용해서 계산의 결과를 값으로 가지는 객체(해시)입니다. 결과 값은 필요에 따라 복잡한 데이터 구조로 저장할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ...
// 비용이 많이 드는 수행 후 result에 결과 저장
// ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};

// 캐시 저장공간
myFunc.cache = {};

메모이제이션 적용하기

메모이제이션을 공부한 후 현재 진행중인 에밀리(개인 프로젝트)에 적용해 보았습니다. 어느 부분에 적용했는지 적용 전과 적용 후 얼마나 효율이 올라갔는지를 알아보겠습니다.

1. 수정할 부분

메모이제이션 패턴을 적용할 코드가 현재 하고 있는 기능은 다음과 같습니다. 사용자의 요청(학생식당, 카페테리아, 사범대식당, 기숙사식당, 교직원식당)에 따라 미리 크롤링 후 DB에 저장되어 있는 데이터(메뉴)를 가져와 응답합니다.
카카오톡 플러스친구 자동응답 봇
메모이제이션 패턴을 사용함으로써 얻을 수 있는 이점은 비용이 많이 드는 결과를 캐싱하고 그 이후에 재사용함으로써 비용을 줄일 수 있다는 것입니다. 현재 코드에서 비용이 많이 드는 작업은 DB를 조회하는 부분입니다. DB에는 다음과 같이 미리 크롤링한 데이터가 날짜별로 저장되어 있습니다.

DB 메뉴 table

사용자의 요청에 따라 해당 날짜의 데이터를 조회한 후 사용자가 요청한 식당에 맞는 데이터를 결과로 반환합니다. 그렇기 때문에 해당 날짜의 최초 요청이 이루어진 후 그 하루 동안에는 계속해서 DB에 같은 쿼리를 통해 같은 결과를 얻게 됩니다. 이 부분이 비용이 많이 드는 작업이기 때문에 메모이제이션 패턴을 통해 개선해보았습니다.

2. 메모이제이션 적용 전

먼저 메모이제이션 패턴을 적용하기 전 코드입니다. menuHandler 클래스의 getMenu 함수는 menuService를 통해 DB에서 해당 날짜의 식당 메뉴를 가져와 매개변수로 전달받은 식당의 이름을 사용하여 결과로 반환하는 역할을 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const dateUtil = require('util/dateUtil');
const menuService = require('services/menuService');

class menuHandler {
static getMenu(place) {
return new Promise((_s, _f) => {
const today = new Date().yyyymmdd();
let menu = '';

menuService.show(today)
.then(menuList => {
if (!menuList) {
menu = '데이터가 없습니다. 관리자에게 문의해주세요';
}

switch (place) {
case '학생식당':
menu = menuList.student.join('\n\n');
break;
case '카페테리아':
menu = menuList.cafeteria.join('\n\n');
break;
case '사범대식당':
menu = menuList.education.join('\n\n');
break;
case '기숙사식당':
menu = menuList.dormitory.join('\n\n');
break;
case '교직원식당':
menu = menuList.staff.join('\n\n');
break;
}

_s(menu);
})
.catch(err => {
_f(err);
});
});
}
}

module.exports = menuHandler;

3. 메모이제이션 적용 후

메모이제이션 패턴을 적용 한 후 코드입니다. setCache, getCache, pickMenu 함수가 추가되었고, getMenu 함수의 코드도 조금 변경되었습니다. 기존 코드의 getMenu 함수에서는 바로 DB를 조회하여 결과를 반환하였지만, 변경된 코드에서는 getCache 함수를 통해 캐시에 저장된 데이터가 있는지 확인 후 분기하여 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const dateUtil = require('util/dateUtil');
const menuService = require('services/menuService');

class menuHandler {
static setCache(param, menuList) {
menuHandler.cache[param] = menuList;
}

static getCache(param) {
if (menuHandler.cache[param]) {
return menuHandler.cache[param];
} else {
return null;
}
}

static pickMenu(menuList, place) {
let menu = '';

switch (place) {
case '학생식당':
menu = menuList.student.join('\n\n');
break;
case '카페테리아':
menu = menuList.cafeteria.join('\n\n');
break;
case '사범대식당':
menu = menuList.education.join('\n\n');
break;
case '기숙사식당':
menu = menuList.dormitory.join('\n\n');
break;
case '교직원식당':
menu = menuList.staff.join('\n\n');
break;
}

return menu;
}

static getMenu(place) {
return new Promise((_s, _f) => {
const today = new Date().yyyymmdd();
const cachedMenuList = this.getCache(today);
let menu = '';

if (cachedMenuList) {
menu = this.pickMenu(cachedMenuList, place);
_s(menu);
} else {
menuService.show(today)
.then(menuList => {
if (!menuList) {
menu = '데이터가 없습니다. 관리자에게 문의해주세요';
}

this.setCache(today, menuList);

menu = this.pickMenu(menuList, place);
_s(menu);
})
.catch(err => {
_f(err);
});
}
});
}
}

menuHandler.cache = {};

module.exports = menuHandler;

Line:70 에서 선언한 menuHandler의 cache Object에는 다음과 같이 데이터가 캐싱될 것입니다.

1
2
3
4
5
6
{
...
`2017-06-13`: { ... menuList ...}
`2017-06-14`: { ... menuList ...}
...
}


결과 비교

메모이제이션 패턴을 적용하기 전과 적용한 후의 성능 차이는 아래 보이는것처럼 눈에 띄게 차이가 납니다. 메모이제이션 패턴을 적용한 코드는 첫번째 요청(캐싱 하기 전)때는 메모이제이션 패턴 적용 전과 응답속도가 비슷하지만 그 이후의 응답은 캐싱된 데이터를 이용하기 때문에 비교될 정도로 빨라졌습니다.

메모이제이션 적용 전

메모이제이션 적용 후


마치며

메모이제이션을 적용하기 전과 후 모두 같은 기능을 결과를 만들어내는 코드이지만 코드를 작성하는 방법에 따라 더욱 더 빠른 효율적인 서비스를 만들 수 있다는 것을 느끼게 되었습니다.
현재 제 상황에서는 캐싱된 데이터도 하루가 지나게되면 쓰이지 않고 계속 메모리에 남아있게 되는데 그 부분에 대한 처리를 추가해야할것 같습니다. 이번에 적용한 코드 뿐만 아니라 아직 프로잭트 내에 메모이제이션 패턴을 적용할 수 있는 부분이 더 있습니다. 앞으로 디자인패턴 공부를 계속하여 새로운 패턴들을 적용시키며 리팩토링을 해야겠습니다.

다시 공부하는 Promise

Index

  1. Promise란?
  2. Promise 사용법
  3. Promise 상태
  4. Promise.resolve, Promise.reject
  5. Promise.prototype.then
  6. Promise.all, Promise.race
  7. Promise 특징

Promise란?

자바스크립트에서는 비동기 프로그래밍 해결을 위해 하나의 패턴으로 콜백을 사용했다. 그러나 콜백 패턴은 비동기 처리 중 발생한 오류를 예외 처리하기 힘들고 여러 개의 비동기 로직을 한꺼번에 처리하는 데도 한계가 있다. 즉 콜백 패턴은 그다지 유용한 패턴이 아니다. 이때 비동기 프로그래밍을 위한 또 다른 패턴으로 Promise가 등장했다.

Promise비동기 처리 로직을 추상화한 객체와 그것을 조작하는 방식을 말한다. Promise를 지원하는 함수는 비동기 처리 로직을 추상화한 promise 객체를 반환 한다. 그리고 객체를 변수에 대입하고 성공 시 동작할 함수와 실패 시 동작할 함수 를 등록해 사용한다.

함수를 작성하는 방법은 promise 객체의 인터페이스에 의존 한다. 즉, promise 객체에서 제공하는 메서드만 사용해야 하므로 전통적인 콜백 패턴처럼 인자가 자유롭게 결정되는 게 아니라 같은 방식으로 통일된다. Promise 라고 부르는 하나의 인터페이스를 이용해 다양한 비동기 처리 문제를 해결할 수 있다. 복잡한 비동기 처리를 쉽게 패턴화할 수 있다는 뜻이다. 이것이 Promise의 역할이며 Promise를 사용하는 많은 이유 중 하나다.


Promise 사용법

Promise는 new 연산자를 선언하여 Promise 인스턴스 객체를 생성한다.

1
2
3
const promise = new Promise(function(resolve, reject) {
// 비동기 처리 로직 후 resolve 또는 reject를 호출
});

new 연산자로 생성된 Promise 인스턴스 객체에는 성공(resolve), 실패(reject)했을 때 호출될 콜백 함수를 등록할 수 있는 Promise.then()이라고 하는 인스턴스 메서드가 있다.

1
promise.then(onFulfilled, onRejected)

성공했을 때는 onFulfilled가 호출되고 실패했을 때는 onRejected가 호출된다. promise.then()으로 성공 혹은 실패 시의 동작을 동시에 등록할 수 있다. 만약 오류 처리만 한다면 promise.then(undefined, onRejected)와 같은 의미인 promise.catch(onRejected)를 사용하면 된다.

1
promise.catch(onRejected)


Promise 상태

생성자 함수를 new 연산하여 생성된 Promise 인스턴스 객체에는 3가지 상태가 존재한다. promise 객체는 Pending 상태로 시작해 Fulfilled나 Rejected 상태가 되면 다시는 변화하지 않는다. (Event 리스너와는 다르게 then()으로 등록된 콜백함수는 한 번만 호출된다.)

  • Pending : 성공도 실패도 아닌 상태, Promise 인스턴스 객체가 생성된 초기상태
  • Fulfilled : 성공(resolve)했을 때의 상태, onFulfilled가 호출된다.
  • Rejected : 실패(reject))했을 때의 상태, onRejected 호출된다.

Promise.resolve, Promise.reject

Promise의 정적 메서드인 Promise.resolve()를 사용하면 new Promise() 구문을 단축해 표기할 수 있다. Promise.resolve()는 Fulfilled 상태인 promise 객체를 반환한다. 또한, Promise. resolve()는 thenable 객체를 promise 객체로 변환할 수 있다. 이것은 Promise.resolve()의 중요한 특징 중 하나다.

thenable은 ES6 Promises 사양에 정의된 개념이다. then()을 가진 객체 즉, 유사 promise 객체를 의미한다. length 프로퍼티를 갖고 있지만, 배열이 아닌 유사 배열 객체 Array-like Object와 같다. Promise.resolve()는 thenable 객체의 then() 이 Promise의 then()과 같은 동작을 할 것이라 기대하고 promise 객체로 변환한다.

Promise.reject()도 promise 객체를 반환한다. 따라서 에러 객체와 함께 catch()를 이용해 등록한 콜백 함수가 호출된다.


Promise.prototype.then

Promise에서는 메서드를 체인하여 코드를 작성할 수 있다. then()은 콜백 함수를 동록하기만 하는것이 아니라 콜백에서 반환된 값을 기준으로 새로운 promise 객체를 생성하여 전달하는 기능도 갖고 있다.


Promise.all, Promise.race

Promise.all()은 Promise 객체를 배열로 전달받고 객체의 상태가 모두 Fulfilled 됐을 때 then()으로 등록한 함수를 호출한다.
Promise.race()는 Promise.all()과 마찬가지로 promise 객체를 배열로 전달한다. Promise.all()과 달리 전달한 객체의 상태가 모두 Fulfilled가 될 때까지 기다리지 않고 전달한 객체 중 하나만 완료(Fulfilled, Rejected)되어도 다음 동작으로 넘어간다. Promise.race는 먼저 완료된 promise 객체가 있더라도 다른 promise 객체를 취소하지 않는다. (ES6 Promise 사양에는 취소라는 개념이 없다.)


Promise 특징

1. Promise는 항상 비동기로 처리된다.

Promise.resolve()나 resolve()를 사용하면 promise 객체는 바로 Fulfilled 상태가 되기 때문에 then()으로 등록한 콜백 함수가 동기적으로 호출될 것이라 생각할 수 있다. 하지만 실제로는 then()으로 등록한 콜백 함수는 비동기적으로 호출된다.
동기적으로 처리 가능한 상황에서도 비동기적으로 처리하는 이유는 동기와 비동기가 혼재될때 발생하는 문제를 막기 위함이다.

2. 새로운 promise 객체를 반환하는 then

promise.then(), catch()는 최초의 promise 객체에 메서드를 체인하는 것처럼 보이지만 실제로는 then()과 catch()는 새로운 promise 객체를 생성해 반환한다.
Promise.all()과 Promise.race() 또한 새로운 promise 객체를 생성해 반환한다.

3. 콜백-헬과 무관한 Promise

Promise는 callback-hell 을 해결할수는 없고 완화할 수 있을 뿐이다. 완화할 수 있는 이유는 단일 인터페이스와 명확한 비동기 시점 표현, 강력한 에러 처리 메커니즘 때문이다. 이는 비동기 처리 자체를 손쉽게 다룰 수 있도록 하는 것이므로 callback-hell 을 해결하는 방법으로 여기는건 바람직하지 않다.

Prototype 이제는 이해하자

Index

  1. prototype은 왜 어려울까?
  2. 객체(object)는 함수(function)로부터 시작된다
  3. 함수(function) 생성시 발생하는 일
  4. 객체(object) 생성시 발생하는 일
  5. 프로토타입 체인(Prototype Chain)
  6. 번외

prototype은 왜 어려울까?

C++, Java와 같은 클래스 기반 객체지향 언어와 달리 자바스크립트는 프로토타입 기반 객체지향 언어입니다. 프로토타입을 사용하여 객체지향을 추구하기 때문에 자바스크립트를 사용함에 있어 프로토타입을 이해하는 것은 중요합니다. 최근 ECMA6 표준에서 Class 문법이 추가되었지만 C++, Java에서 말하는 클래스가 아닌 프로토타입을 기반으로 하여 만들어진 문법입니다.

자바스크립트의 프로토타입을 처음 공부하면서 prototype, [[prototype]], __proto__, 객체, 함수, prototype chain 과 같은 용어들을 접하게 되는데 공부할수록 서로 뒤엉켜지고, 모르는 것도 아닌 그렇다고 제대로 알고 있는것도 아닌 어중간한 상태가 됩니다.

자바스크립트를 사용한 경험이 있으시다면 아래의 코드와 같은 형태를 경험한적이 있으실겁니다. 지금부터 아래의 코드가 어떤 원리로 동작하게 되는지 알아보겠습니다.

prototype에 property 추가

먼저 프로토타입에 대해 이해하기 위해서는 객체(object)는 함수(function)로부터 시작된다라는 것을 알아야 합니다. 이는 prototype을 이해하는데 많은 도움을 줍니다.


객체(object)는 함수(function)로부터 시작된다

자바스크립트에서 primitive를 제외하고는 모두 객체(object)입니다.
앞으로 등장하는 Object와 Function은 function(즉, 생성자)입니다. object는 객체를 의미합니다.

다음의 코드를 분석하기 전 객체(object)는 함수(function)로부터 시작된다라는걸 다시한번 기억하겠습니다.

1
2
function Book() { }                // 함수
var jsBook = new Book(); // 객체 생성

위의 코드에서 Book이라는 함수를 통해서 jsBook이라는 객체를 생성했습니다. 이때 Book 함수를 생성자라고 합니다. 생성자는 새로 생성된 객체를 초기화하는 역할을 합니다. 코어 자바스크립트는 기본 타입에 대한 생성자를 내장하고 있는데 이는 다음 코드를 통해 확인이 가능합니다.

1
var cssBook = {};                  // 생성자 선언 없이 객체 생성

위에서는 리터럴 방식을 사용하여 객체를 생성하였습니다. 리터럴 방식 또한 결과적으로는 함수를 통하여 객체를 생성하게 됩니다. 자바스크립트 엔진이 해당 리터럴을 다음과 같이 해석합니다.

1
var cssBook = new Object();        // 객체 생성

따라서 결과적으로는 리터럴 방식으로 객체를 생성할때도 Object라는 함수(생성자)를 통해서 객체를 생성하게 됩니다. Object 뿐만 아니라 Array, Function, Date, RegExp 모두 함수입니다.

Object, Array, Function, Date, RegExp - 모두 함수(function)이다

배열도 객체이기 때문에(자바스크립트 배열은 객체의 특별한 형태입니다. 프로퍼티 이름이 정수로 사용되며, length 프로퍼티를 가집니다.) 객체를 생성할때와 마찬가지로 배열(객체)의 생성에도 함수가 관여하게 됩니다. 따라서 무심코 사용했던 배열의 리터럴 표현도 결국에는 자바스크립트 엔진이 다음과 같이 해석합니다.

1
2
3
var books = ['html', 'css', 'js'];                  // 배열(객체) 생성
// 엔진이 다음과 같이 해석합니다.
var books = new Array('html', 'css', 'js'); // 배열(객체) 생성

이제 객체(object)는 함수(function)로부터 시작된다라는 것을 알 수 있습니다.


함수(function) 생성시 발생하는 일

객체(object)는 함수(function)로부터 시작되기 때문에 사용자가 객체를 생성하기 위해 먼저 함수를 정의하게 됩니다. 이때 발생하는 일에 대해 알아보겠습니다. 여기서는 2가지를 기억해야 합니다.

1.함수를 정의하면 함수가 생성되며 Prototype object가 같이 생성 됩니다. 생성된 Prototype object는 함수의 prototype 속성을 통해 접근할 수 있습니다. (Prototype object같은 경우 함수 생성시에만 됩니다. 일반 객체 생성시에는 생성되지 않습니다.)

함수 prototype property

2.함수의 생성과 함께 생성된 Prototype objectconstructor__proto__를 갖고 있습니다. (cover property를 추가한것 처럼 사용자 임의로 추가 가능합니다.) constructor는 생성된 함수를 가리키며(여기서는 function Book을 가리킵니다.) __proto__Prototype Link로서 객체가 생성될 때 사용된 생성자(함수)의 Prototype object를 가리킵니다. Prototype Link는 뒤에서 자세하게 알아보겠습니다.

함수와 함께 생성된 Prototype object

다이어그램을 통해 확인하면 다음과 같습니다.

함수와 Prototype object의 관계


객체(object) 생성시 발생하는 일

이번에는 객체 생성시 발생하는 일에 대해 알아보겠습니다. 조금 전에 정의한 Book 함수(생성자)를 사용하여 jsBook이라는 객체를 생성해 보겠습니다.

Book 함수로 생성한 객체

생성자(함수)의 몸체 부분에 어떠한 코드도 작성하지 않았는데 이를 통해 생성한 jsBook 객체가 __proto__라는 프로퍼티를 갖고있습니다.

여기서 __proto__Prototype Link로서 객체의 생성에 쓰인 생성자 함수의 Prototype object를 가리키고 있습니다. 그렇기 때문에 Book 생성자 함수와 함께 생성된 Prototype object에 추가한 cover라는 프로퍼티가 보이는것을 확인할 수 있습니다.

조금 더 이해하기 쉽게 다이어그램으로 확인하면 다음과 같습니다.

함수, Prototype object, 객체의 관계

다이어그램에서도 확인할 수 있다시피 prototype property(함수 생성시 함께 생성된 Prototype object를 가리킴)는 함수객체만 가지며 __proto__는 객체라면 모두 갖고 있습니다.

이제 프로토타입 체인(Prototype Chain)에 대해 이해할 수 있는 준비가 되었습니다.


프로토타입 체인(Prototype Chain)

결론부터 말씀드리면 프로토타입 체인은 객체의 property를 사용할때 해당 property가 없다면, __proto__ property를 이용해 자신의 생성에 관여한 함수(생성자 함수)의 Prototype object에서 property를 찾습니다. 만약 Prototype object에도 해당 property가 없다면 다시 Prototype object의 __proto__ property를 이용해 Prototype object에서 property를 찾습니다. 이렇게 계속 반복이 이루어지며 해당 property를 찾게 된다면 값을 반환하고 찾지 못한다면 undefined를 반환합니다. 이렇게 __proto__ property를 통해 상위 프로토타입과 연결되어 있는 형태를 프로토타입 체인(Chain)이라고 합니다.

프로토타입 체인에 대해 알게되었으니 다시한번 처음 코드를 살펴보겠습니다.

프로토타입 체인 예제

이제 어떻게 jsBook에 cover라는 property를 추가하지 않았는데도 결과가 출력되는지 이해할 수 있습니다. 다음과 같이 동작할 것입니다.

프로토타입 체인 예제(cover property 찾는 과정)

또한 다음과 같이 프로토타입 체인의 최상위는 Object이기 때문에 Object.prototype의 property들을 모두 사용할 수 있습니다. 자주 사용하는 toString()과 valueOf() 모두 Object.prototype에 선언되어 있습니다.
(Book Prototype object는 객체이기 때문에 Object 생성자가 사용될 것입니다. 따라서 Book Prototype object의 __proto__는 Object Prototype object를 가리키게 됩니다.)

프로토타입 체인의 최상위 object
Object.prototype

Prototype object__proto__ 그리고 프로토타입 체인에 대해 이해하였으니 다음과 같은 코드도 이해할 수 있습니다. 잘 이해가 되지 않는다면 위의 다이어그램을 참고해보시기 바랍니다.

__proto__와 Prototype object의 관계


번외

프로토타입 체인의 최상위 object
혹시 다이어그램을 보면서 function Book의 __proto__ 는 무엇을 가리키고 있는지 궁금해 하셨을 분들을 위해 추적해보았습니다.

function(생성자)의 __proto__ 추적

다음 코드를 도식화 하면 다음과 같은 다이어그램이 나오게 됩니다.

function(생성자)의 __proto__ 추적

간단하게 포스팅을 하려했는데 주제가 주제인지라 길어졌습니다. 저도 프로토타입을 처음 공부하면서 어려움을 많이 겪었는데 조금이나마 도움이 되었으면 좋겠습니다.

자바스크립트 완벽가이드 10장 (정규 표현식을 사용한 패턴 매칭)

정규 표현식(regular expression)은 문자의 패턴을 나타내는 객체다. 자바스크립트의 RegExp 클래스는 정규 표현식을 표현하고, String과 RegExp에는 정규표현식을 사용하여 강력한 패턴 매칭을 수행하는 메서드와 텍스트상에서 특정 텍스트를 찾아서 바꾸는 함수가 정의되어 있다.

10.1 정규 표현식 정의

자바스크립트에서 정규 표현식은 RegExp 객체로 표현된다. RegExp 객체는 RegExp() 생성자를 사용하여 만들 수 있지만, RegExp() 생성자보다는 정규 표현식 리터럴 문법이 더 자주 사용된다. 정규 표현식 리터럴은 항 쌍의 슬래시(/) 문자 사이에 위치한 문자들이다.

1
2
var pattern1 = /s$/;
var pattern2 = new RegExp("s$");

정규 표현식은 연속된 문자로 구성되어 있다. 영문자를 포함한 문자 대부분은 패턴에 적혀 있는 문자 그대로 매치된다. 몇몇 문자는 문자 그대로 매치되지 않으며, 특별한 의미를 지닌다. 정규 표현식 /s$/는 두 문자로 구성되었는데, 먼저 “s”는 문자 그대로 s와 매치되고, “$”는 특수 메타 문자로 문자열의 끝과 매치된다. 따라서 이 정규 표현식은 “s”로 끝나는 모든 문자열과 매치된다.

10.1.1 리터럴 문자

모든 영문자와 숫자는 그 문자 그대로 정규 표현식에서 매치된다 특정한 비-알파벳 문자들은 역슬래시로 시작하는 이스케이프 문자열을 통해 지원한다. (역슬래시 문자를 있는 그대로 넣으려면 역슬래시 자체를 이스케이프 해야한다. ( /\\/ )
몇가지 구두점 문자는 정규 표현식에서 특별한 의미를 지닌다. (^$.*+?=!:|\/()[]{})

10.1.2 문자 클래스

개별 리터럴 문자들은 그 문자들을 대괄호로 묶어서 문자 클래스로 다룰 수 있다. 문자 클래스는 해당 클래스 내의 모든 문자에 매치된다. /[abc]/는 a,b,c 중 아무 글자에나 매치된다. 부정 문자 클래스도 정의될 수 있는데 이는 대괄호 안에 있는 문자들을 제외한 모든 문자와 매치된다. 캐럿(^)을 사용한다. 정규표현식 /[^abc]/는 a,b,c를 제외한 모든 문자와 매치된다. 문자 클래스 하이픈(-)을 사용하여 문자의 범위를 지정할 수도 있다. /[a-z]/는 라틴 알파벳 소문자와 매치되며, 모든 라틴 알파벳 글자나 숫자와 매치되게 하려면 /[a-zA-Z0-9]/를 사용한다.

10.1.3 반복

복잡한 패턴을 작성할 때는 정규 표현식의 요소가 몇 번이나 반복되는지를 나타내는 문법을 사용해야 한다. 반복을 지정하는 문자는 언제나 반복을 지정할 패턴 뒤에 나온다.

  • {n,m} : 앞의 항목이 적어도 n번이상, m번 이하로 나타난다.
  • {n,} : 앞의 항목이 n번 이상 나타난다.
  • {n} : 앞의 항목이 정확하게 n번 나타난다.
  • ? : 앞의 항목이 0번 또는 한 번 나타난다. 앞의 항목이 나오지 않을 수도 있다. {0,1}과 동등
  • + : 앞의 항목이 한 번 이상 나타난다. {1,}과 동등
  • * : 앞의 항목이 0번 또는 그 이상 나타난다. {0,}과 동등
    와 ? 반복 문자를 사용할 때는 조심해야 한다. 예를 들면 정규 표현식 /a/는 실제로 문자열 “bbbb”와 매치된다.

10.1.4 대체, 그룹화, 참조

정규 표현식 문법은 대체 표현식, 부분 표현식 그룹화, 이전 부분 표현식을 참조 하는 특별한 문자를 포함하고 있다.
파이프 문자(|)는 대체 표현식을 구분한다. /ab|cd|ef/는 문자열 “ab” 또는 문자열 “cd” 또는 문자열 “ef”와 매치된다. 대체 표현식은 매치를 발견할 때까지 왼쪽에서 오른쪽으로 수행된다. 따라서 /a|ab/를 문자열 “ab”에 적용하면, 오직 첫 번째 글자 a만 매치된다.
정규 표현식에서 괄호는 여러 목적으로 사용된다. 하나는 여러 항목을 하나의 부분 표현식으로 묶고(그룹화(, 묶인 항목들을 |, *, +, ? 등이 하나의 단위로 취급할 수 있게 한다. /(ab|cd)+|ef/는 “ab” 또는 “cd”가 한 번 이상 반복되는 문자열 혹은 문자열 “ef”와 매치된다.
정규 표현식에서 괄호의 다른 목적은 전체 패턴 안에 부분 패턴을 정의하는 것이다. 정규 표현식이 대상 문자열에 성공적으로 매치되면, 괄호로 둘러싸인 특정 패턴과 매치되는 부분 문자열을 추출할 수 있다. 예를 들어 각 매치에 대해 끝에 있는 숫자에만 관심이 있다면, 패턴 일부를 /[a-z]+(\d+)/와 같이 괄호 안에 두고 찾아낸 매치 결과에서 숫자를 추출할 수 있다.

10.1.6 플래그(flag)

정규 표현식 플래그는 고차원 패턴 매칭 규칙을 지정한다. 다른 정규 표현식 문법과는 달리 플래그는 / 문자 쌍 바깥에, 즉 두 번째 슬래시 다음에 등장한다. 자바스크립트는 세 가지 플래그를 지원한다. 플래그들은 조합하여 지정될 수 있다.

  • i : 대소문자를 구별하지 않는 매칭을 수행한다.
  • g : 전역 매칭을 수행한다.. 즉, 처음 매치에서 끝내지 않고 모든 매치를 찾는다.
  • m : 여러 줄 모드

10.2 패턴 매칭을 위한 문자열 메서드

문자열에는 정규 표현식을 사용하는 메서드가 네 개 있다. 가장 간단한 것은 search() 메서드다. 이 메서드는 정규 표현식을 인자로 받고, 가장 처음 매칭되는 부분 문자열의 위치를 반환한다. 만약 매칭되는 문자열이 없다면 -1을 반환한다.

1
"JavaScript".search(/script/i);         // 4를 반환한다.

search()에 넘기는 인자가 정규 표현식이 아니라면, 이 인자는 먼저 RegExp 생성자로 넘겨지고 정규 표현식으로 변환된다. search()는 전역 검색을 지원하지 않기 때문에, 정규 표현식 인자의 g플래그는 무시된다.
replace() 메서드는 ‘검색 후 바꾸기’를 수행한다. 첫 번째 인자로는 정규 표현식을 받고, 두 번째 인자로는 교체할 문자열을 받는다. replace() 메서드는 g플래그 사용이 가능하다. replace()의 첫 번째 인자가 정규 표현식이 아니라 일반 문자열인 경우에는 RegExp() 생성자를 사용하여 정규 표현식으로 변환하지 않고 전달된 문자열을 문자열 그대로 찾는다.(search() 와는 반대)
match() 메서드는 정규 표현식 하나만을 인자로 받고 매치 결과를 배열로 반환한다. g플래그가 설정되어 있으면, 문자열 내의 모든 매치 부분을 배열로 반환한다. g플래그가 설정되어 있지 않으면, match()는 전역 검색을 수행하지 않고 단순히 첫 번째 매칭만 찾는다. 그러나 결과는 항상 배열로 반환된다.

1
2
3
4
5
6
7
8
9
var url = /(\w+):\/\/([\w.]+)\/(\S*)/;
var text = "Visit my blog at http://www.example.com/~david";
var result = text.match(url);
if (result != null) {
var fullurl = result[0]; // http:/www.example.com/~david
var protocol = result[1]; // http
var host = result[2]; // www.example.com
var path = result[3]; // ~david
}

split() 메서드는 주어진 인자를 구분자로 삼아, 메서드가 호출된 문자열을 부분 문자열로 쪼갠다.

1
"123,456,789".split(",")        // ["123", "456", "789"]



출처 : “JavaScript: The Definitive Guide, by David Flanagan (O’Reilly). Copyright 2011 David Flanagan, 978-0-596-80552-4”

자바스크립트 완벽가이드 8장 (함수)

함수는 한 번 정의하면 몇 번이든 실행할 수 있고 호출할 수 있는 자바스크립트 코드 블록이다. 함수 호출에는 전달인자 외에도 호출 컨텍스트가 포함되는데, this 키워드의 값이 바로 해당 컨텍스트다.
어떤 객체의 프로퍼티로 할당된 함수를 해당 객체의 메서드라 한다. 어떤 함수를 객체를 대상(on)으로, 또는 객체를 통해서(through) 호출하면, 이 객체는 해당 함수의 호출 컨텍스트, 즉 호출된 함수의 this 값이 된다. 새로 생성된 객체를 초기화하는 데 쓰이는 함수는 생성자(constructor)라고 한다.
자바스크립트 함수는 다른 함수 내에 중첩되어 정의될 수 있고, 중첩된 함수는 해당 함수가 정의된 유효범위 안의 어떤 변수에도 접근할 수 있다. 이는 자바스크립트 함수가 클로저(closure)이며, 클로저가 가능하게 하는 중요하고 강력한 프로그래밍 기법을 자바스크립트도 구사할 수 있음을 뜻한다.

8.1 함수 정의하기

함수는 function 키워드에 의해 정의되며, function 키워드는 함수 정의 표현식 또는 함수 선언문에서 사용된다.

1
2
3
4
5
6
7
8
// 정의된 함수를 변수에 할당할 수 있다.
var square = function(x) { return x*x; }
// 함수 표현식은 이름을 포함할 수 있다. 이러한 이름은 재귀 호출에 유용하게 사용된다.
var f = function fact(x) { if (x <= 1) return 1; else return x*fact(x-1); };
// 함수 표현식은 다른 함수의 전달인자로 사용 가능하다.
data.sort(function(a, b) { return a-b ; });
// 함수 표현식은 정의되는 즉시 호출 가능하다.
var tensquared = (function(x) {return x*x;}(10));

함수 정의 표현식에서 함수 이름은 옵션이다. 함수 선언문이 실제로 하는 일은, 어떤 변수를 정의하고 함수 객체를 그 변수에 할당하는 것이다. 함수 정의 표현식이 이름을 포함하면, 이 함수 몸체의 유효 범위에 해당 함수 객체에 연결된 이름이 포함된다. 사실상 그 함수 이름이 해당 함수의 지역 변수가 되는 것이다. 표현식 형태로 함수를 정의하는 것은 한 번만 사용되는 함수에 특히 적합하다.
함수 선언문은 그 함수를 둘러싼 스크립트나 함수의 맨 위로 끌어올려(hoisted)진다. 따라서 해당 함수는 이 함수가 정의된 위치보다 앞서 나오는 코드로부터 호출될 수 있다. 그러나 표현식으로 정의된 함수는 다르다. 함수를 호출하려면 먼저 호출할 함수를 참조할 수 있어야 하는데, 표현식으로 정의된 함수는 변수에 할당되기 전까지는 참조할 수 없다. 변수 선언은 끌어올려지지만, 변수 할당은 그렇지 않다. 그래서 표현식으로 정의된 함수는 정의되는 지점 위에서는 호출할 수 없다.
함수 대부분은 return문을 포함하고 있다. return 다음에 오는 표현식의 값을 호출자에게 반환하는데 return 다음에 표현식이 없다면 undefined 값을 반환한다. 함수가 return 문을 포함하지 않는다면, 함수 몸체 내의 각 구문을 실행 후 다음 호출자에게 undefined 값이 반환된다.


8.2 함수 호출하기

자바스크립트 함수는 네 가지 방법으로 호출할 수 있다.

  1. 일반적인 함수 형태
  2. 메서드 형태
  3. 생성자
  4. call()과 apply() 메서드를 통한 간접 호출

8.2.1 함수 호출

함수가 호출될 때는 먼저, 각각의 전달인자 표현식(괄호 사이에 있는 것)이 평가되고, 평가 결과 값이 해당 함수의 전달인자가 된다. 이 전달인자 값들은 함수 정의에 등장하는 형식인자 각각에 대응된다.
일반적인 함수 형태로 호출하도록 작성된 함수는 보통 this 키워드를 사용하지 않는다.

8.2.2 메서드 호출

메서드는 객체의 속성으로 저장된 자바스크립트 함수일 뿐이다. 그러나 메서드 호출함수 호출에 비해 한 가지 중요한 부분이 다른데, 바로 호출 컨텍스트다. 메서드 호출 표현식에서는 객체가 호출 컨텍스트가 되므로, 함수 몸체에서 this 키워드를 사용해서 객체를 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
var calculator = {
operand1: 1,
operand2: 1,
add: function() {
// 이 객체를 참조하기 위해 this 키워드를 사용
this.result = this.operand1 + this.operand2;
}
};
calculator.add();
calculator.result // => 2

메서드this 키워드는 자바스크립트 객체 지향 프로그래밍 패러다임의 중심이다. 메서드로 사용되는 함수는 메서드의 호출 대상 객체를 암시적 인자로 전달받는다.

메서드 체이닝
메서드 체이닝은 객체 이름은 한 번만 사용하고 메서드는 여러 번 호출할 수 있는 방식이다. (메서드가 객체를 반환하면, 메서드의 반환 값을 후속 호출의 일부로 사용)

변수와 달리, this 키워드에는 유효범위(scope)가 없고 중첩 함수는 호출자의 this 값을 상속하지 않는다. 만약 중첩 함수가 메서드 형태로 호출되면, 그 함수의 this 값은 그 함수의 호출 대상 객체다. 가장 흔한 실수는, 함수 형태로 호출된 중첩 함수가 바깥쪽 함수의 호출 컨텍스트를 획득하기 위해 this 값을 사용할 수 있다고 가정하는 것이다. 만약 바깥쪽 함수의 this 값에 접근하고 싶다면, 안쪽 함수의 유효범위에 바깥쪽 함수의 this 값을 별도의 변수로 저장해야 한다. 이러한 용도로 보통 self 변수를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
m: function() {
var self = this; // this 값을 변수에 저장
console.log(this === o); // true: this는 객체 o이다.
f(); // 헬퍼 함수 f() 호출
function f() { // 중첩 함수 f()
console.log(this === o); // false: this는 global 객체 또는 undefined이다.
console.log(self === o); // true: self는 바깥쪽 함수의 this 값이다.
}
}
};
o.m();

8.2.3 생성자 호출

함수나 메서드 호출 앞에 new 키워드가 있다면 생성자 호출이다. 생성자 호출은 일반 함수와 메서드 호출에 비해 매개변수, 호출 컨텍스트와 반환 값을 다루는 방식이 다르다.
생성자에 전달인자(매개변수) 목록이 없다면, 자바스크립트 생성자 호출 문법은 전달인자 목록과 괄호를 아예 생략하는 것을 허용한다.

1
2
3
var o = new Object();
var o = new Obejct;
// 위 두개는 같은 코드이다.

생성자를 호출하면 생성자의 prototype 프로퍼티를 상속받은 새로운 빈 객체가 생성된다. 생성자 함수는 객체를 초기화하고, 새로 생성된 이 객체는 생성자 함수의 호출 컨텍스트로 사용된다. 따라서 생성자 함수는 새로 생성된 객체를 this 키워드로 참조할 수 있다. 주의할 것은 생성자 호출이 마치 메서드 호출처럼 보일지라도, 메서드가 속한 객체가 아닌 새로 생성된 객체가 호출 컨텍스트로 사용된다는 점이다. (즉, new o.m()과 같은 표현식에서 o가 호출 컨텍스트로 사용되지는 않는다는 뜻)
생성자 함수는 보통 return 키워드를 사용하지 않는다. 일반적으로 생성자 함수는 새 객체를 초기화하고, 생성자 함수 몸체의 끝에 이르면 암시적으로 그 객체를 반환한다. 새 객체가 생성자 호출 표현식의 값이다.

8.2.4 간접 호출

자바스크립트 함수는 객체이고, 모든 자바스크립트 객체와 같이 함수에도 메서드가 있다. 이 메서드 중 call()apply()는 함수를 간접적으로 호출한다. 두 메서드 모두 호출 때 this 값을 명시적으로 지정할 수 있는데, 이는 어떤 함수든지 특정 객체의 메서드로 호출할 수 있다는 의미다. 심지어 함수가 실제로 그 객체에 속하지 않더라도 말이다.


8.3 함수 전달인자와 매개변수

8.3.1 생략 가능한 매개변수

정의된 것보다 적은 수의 전달인자로 함수가 호출되면, 나머지 매개변수는 undefined 값으로 설정된다.

1
2
3
4
5
function getPropertyNames(o, /* optional */ a) {
if (a === undefined) a = []; // 만약 undefined이면 새 배열을 사용한다.
for(var property in o) a.push(property);
return a;
}

getPropertyNames의 첫줄에는 if문 대신, 관용적으로 || 연산자를 사용하기도 한다.

1
a = a || [];

|| 연산자는 첫 번째 피 연산자가 true이거나 true로 변환될 수 있는 값이면 첫 번째 피 연산잘르 반환하고, 그렇지 않으면 두 번째 피 연산자를 반환한다. 따라서, 두 번째 인자가 생략된다면(또는 null이거나 false혹은 false로 변환될 수 있는 값이라면), 새로 생성된 빈 배열이 대신 사용될 것이다.

8.3.2 가변길이 전달인자 목록: Arguments 객체

함수가 호출될 때 정의된 매개변수보다 더 많은 인자가 전달되면, 매개변수 이름이 붙지 않은 인자 값을 직접적으로 참조할 방법은 없다. Arguments 객체는 이러한 문제에 대한 해결책이다. 함수 몸체 내에서 arguments 식별자는 해당 호출에 대한 Arguments 객체를 참조한다. Arguments 객체는 유사 배열 객체이고, 이름이 아니라 인덱스 숫자를 통해 함수의 전달인자를 얻어올 수 있다. (내장 함수 Math.max()가 Arguments를 사용하여 동작한다.)
arguments는 실제로는 배열이 아니라 Arguments 객체이다. 각 Arguments 객체는 숫자 인덱스가 붙은 배열 원소와 length 프로퍼티를 갖고 있다. 그러나 배열은 아니다. 어쩌다가 숫자로 된 프로퍼티를 갖고 있는 객채이다.
Arguments 객체의 배열 원소와 매개변수의 이름은 동일한 값을 가리키는 다른 두 이름이다. (Arguments 객체가 평범한 배열이라면, arguments[0]과 x는 같은 값을 가질 수 있지만, 하나를 변경하는 작업이 다른 하나에 영향을 미치지는 않을 것이다.)

1
2
3
4
5
function f(x) {
console.log(x); // 전달인자의 초기 값 출력
arguments[0] = null; // 배열 요소를 변경하면 x 또한 변경
console.log(x); // 이제 null을 출력
}

8.3.3 객체의 프로퍼티를 전달인자로 사용하기

어떤 함수에 세 개 이상의 매개변수가 있다면, 이 함수를 호출할때 인자의 올바른 순서를 기억하기가 어렵다. 따라서 전달인자를 순서에 상관없이 이름/값의 쌍으로 함수에 전달하는 편이 효과적일 수 있다. 단일 객체를 전달인자로 받는 함수를 정의하고, 함수의 사용자에게 함수에서 요구하는 이름/값 쌍을 가진 객체를 함수의 인자로 넘기도록 하면 된다.

8.3.4 전달인자 형식

자바스크립트 메서드의 매개변수에는 정의된 형식도 없고, 함수에 전달한 값에 대해서 자료형 검사도 하지 않는다. 한두 번만 사용하고 ‘버릴’함수가 아니라면, 인자 자료형을 검사하는 코드를 추가할 가치가 있다.
자바스크립트는 매우 유연하며 자료형을 느슨하게 처리하는 언어이기에, 때로는 인자 개수와 자료형에 유연한 함수를 작성하는 것이 바람직하다.


8.4 값으로서의 함수

자바스크립트에서 함수는 문법일 뿐만 아니라 값이기도 한데, 이는 함수가 변수에 할당될 수 있고 객체의 프로퍼티나 배열 원소로 저장될 수도 있으며, 다른 함수의 인자로 전달될 수도 있고, 기타 여러 방식으로 사용될 수 있음을 뜻한다.

8.4.1 자시만의 함수 프로퍼티 정의하기

자바스크립트에서 함수는 원시 값이 아니지만 특별한 종류의 객체이고 이는 함수가 프로퍼티를 가질 수 있음을 의미한다. 함수가 여러 번 호출되어도 그 값이 유지되어야 하는 ‘정적’ 변수가 필요할 때는, 전역 변수를 사용하는 것보다 함수의 프로퍼티를 사용하는 것이 편리한 경우가 많다.

1
2
3
4
5
6
7
8
9
10
// 팩토리얼을 계산하고 계산 결과를 함수 자신의 프로퍼티에 캐시한다.
function factorial(n) {
if (isFinite(n) && n>0 && n==Math.round(n)) {
if (!(n in factorial)) // 만약 캐시 해둔 결과가 없다면
factorial[n] = n * factorial(n-1); // 팩토리얼을 계싼하고, 계산 값 캐시
return factorial[n]; // 캐시 결과를 반환
}
else return NaN;
}
factorial[1] = 1; // 캐시를 기본 경우(1)에 대한 값으로 초기화


8.5 네임스페이스로서의 함수

자바스크립트는 함수 단위의 유효범위를 갖는다. 함수 내부에 정의된 변수는 해당 함수 내부(중첩 함수를 포함한)에서는 접근 가능하지만, 그 함수 바깥에는 존재할 수 없다. 함수 밖에서 정의된 변수는 전역 변수이고 자바스크립트 프로그램 전체에서 접근할 수 있다.

1
2
3
(function () {       // 이름이 없는 표현식으로 함수 작성
// 모듈 코드 위치
}()); // 함수 리터럴을 끝내고 바로 호출

단일 표현식으로 함수를 정의하고 호출하는 방식은 관용적으로 자주 사요되는 기법이다. 함수 앞의 시작 괄호는 반드시 필요한데, 만약 시작괄호가 없다면 자바스크립트 인터프리터는 function 키워드를 함수 선언문으로 해석하기 때문이다. 괄호가 있으면 인터프리터는 이것을 표현식 형태의 함수 선언으로 올바르게 인식한다. 괄호가 꼭 필요하지 않은 상황에서도, 정의하자마자 호출할 함수를 괄호로 둘러싸는 건 관용적인 방식이다.


8.6 클로저

자바스크립트는 다은 언어와 마찬가지로 어휘적 유효범위를 사용한다. 함수를 호출하는 시점에서의 변수 유효범위가 아니라, 함수가 정의된 시점의 변수 유효범위를 사용하여 함수가 실행된다는 뜻이다. 이러한 어휘적 유효범위를 구현하기 위해, 자바스크립트 함수 객체는 내부 상태에 함수 자체의 코드뿐만 아니라 현재 유효범위 체인에 대한 참조도 포함하고 있다. 함수 객체와 함수의 변수가 해석되는 유효범위(변수 바인딩의 집합)를 아울러 컴퓨터 과학 문헌에서는 클로저(closure)라고 부른다.(내부함수는 외부함수의 지역변수에 접근 할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근할 수 있다. 이러한 메커니즘이 클로저이다.)
기술적으로 자바스크립트 함수는 클로저이다. 함수는 객체이고 함수 자신과 관련된 유효범위 체인을 갖고있기 때문이다. 함수 대부분은 함수가 정의되었을 때의 유효범위 체인을 사용하여 호출되고, 클로저가 개입되었는지의 여보는 중요하지 않다.

1
2
3
4
5
6
7
var scope = "global scope";             // 전역 변수
function checkscope() {
var scope = "local scope"; // 지역 변수
function f() { return scope; } // 이 유효범위에 있는 값을 반환
return f;
}
checkscope()()

checkscope()는 중첩 함수를 객체 그 자체를 반환한다. 어휘적 유효범위의 기본적은 규칙을 기억해야한다. 자바스크립트 함수는 함수가 정의되었을 때의 유효범위 체인을 사용하여 실행된다. 중첩 함수 f()가 정의된 유효범위 체인에서 변수 scope는 “local scope”로 바인드되어 있다. f가 어디서 호출되든 상관없이, f가 실행될 때 이 바인딩은 항상 유효하다. 따라서 코드의 제일 마지막 줄은 “global scope”가 아니라 “local scope”를 반환한다. 이것이 클로저의 놀랍고 강력한 특성이다. 클로저는 자신을 정의한 바깥쪽 함수에 바인딩된 지역 변수(그리고 전달인자)를 포착한다.

1
2
3
4
var uniqueInteger = (function() {      // 함수를 정의하고 바로 호출
var counter = 0; // 아래 함수의 내부 상태
return function() { return counter++; };
}());

중첩 함수는 유효범위에 있는 변수에 접근하고, 바깥쪽 함수에 정의된 counter 변수를 사용할 수 있다. 바깥쪽 함수의 실행이 끝나면, 어떤 코드도 counter 변수를 볼 수 없다. 오직 안쪽 함수만 단독으로 counter 변수에 접근할 수 있을 뿐이다. counter와 같은 내부 변수는 여러 클로저가 공유할 수 있다. 즉, 같은 함수 안에 정의된 중첩 함수들은 같은 유효범위 체인을 공유한다.
클로저 기법과 getter/setter 프로퍼티들을 결합할 수 있다. 다음 예제는 내부 상태를 다루는 데 일반 객체 프로퍼티 대신 클로저를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function counter(n) {        // 함수 전달인자 n은 내부 변수다.
return {
// getter 메서드 프로퍼티는 counter 변수를 반환하고 증가시킨다.
get count() { return n++; },
// setter 메서드는 프로퍼티 n 값을 감소시키는 것을 허용하지 않는다.
set count(m) {
if ( m >= n) n = m
else throw Error("count는 오직 더 큰 값으로만 설정될 수 있습니다.");
}
};
}

var c = conter(1000);
c.count // => 1000
c.count // => 1001
c.count = 2000 // => 2000
c.c ount // => 2000
c.count = 2000 // => 에러!

counter() 함수는 지역 변수를 정의하지 않지만, 프로퍼티 접근 메서드들이 공유하는 내부 상태를 보관하기 위해 매개변수 n을 사용한다. 이로써 counter()를 호출하는 쪽에서 내부 변수의 초기 값을 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
// 0-9 값을 반환하는 함수들의 배열을 반환
function constfuncs() {
var funcs = [];
for(var i = 0; i < 10; i++)
funcs[i] = function() { return i; };
return funcs;
}

var funcs = constfuncs();
funcs[5]() // 무엇이 반환될까?

위의 코드는 열 개의 클로저를 생성하고, 생성한 클로저들을 배열에 저장한다. 모든 클로저는 같은 함수 내에서 정의되고, 따라서 클로저들은 변수 i에 대한 접근을 공유한다. constfuncs() 실해잉 끝나면, 변수 i의 값은 10이고, 열 개의 클로저 모두 이 값을 공유한다. 클로저와 연관된 유효범위 체인이 ‘살아 있다’는 사실을 기억해야 한다. 중첩 함수는 유효범위에 대한 내부 사본이나 변수 바인딩의 스냅샷 따위는 만들지 않는다.

더 자세한 내용은 다음을 참고
클로저(MDN)
자바스크립트의 스코프와 클로저


8.7 함수 프로퍼티, 메서드, 생성자

자바스크립트에서 함수는 일종의 값이다. typeof 연산자를 사용하면 “function” 문자열을 얻을 수 있지만, 함수는 정말 독특한 자바스크립트 객체다. 함수는 객체이기 때문에 프로퍼티와 메서드를 가질 수 있으며 Function() 이라는 생성자도 갖고 있다.

8.7.2 prototype 프로퍼티

모든 함수에는 prototype 프로퍼티가 있는데, 이 프로퍼티는 프로토타입 객체를 참조한다. 모든 함수는 서로 다른 프로토타입 객체를 갖고 있고, 함수가 생성자로 사용될 때, 새로 생성된 객체는 함수의 프로토타입 객체로부터 프로퍼티들을 상속받는다.

8.7.3 call()과 apply() 메서드

call()apply()는 어떤 함수를 다른 객체의 메서드인 것처럼 간접적으로 호출할 수 있도록 한다. call()과 apply()의 첫 번째 인자는 호출되는 함수와 관련이 있는 개체다. 이 첫 번째 인자는 호출 컨텍스트고 함수 몸체에서 this 키워드의 값이 된다. 함수 f()를 객체 o의 메서드로 호출하려면 다음과 같이 사용한다.

1
2
f.call(o);
f.apply(o);

call()의 첫 번째 호출 컨텍스트 다음에 있는 모든 인자는 호출되는 함수로 전달된다. apply() 메서드는 call() 메서드와 비슷하지만, 함수에 전달할 인자는 배열 형태여야 한다. apply()는 실제 배열과 마찬가지로 유사 배열 객체와도 잘 작동한다. 특히 arguments 배열을 직접 apply()에 넘김으로써, 다른 함수를 호출할 때 현재 함수에 전달된 인자와 같은 인자를 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
// 객체 o의 메서드 m을, 원본 메서드 호출 전후에 로그 메시지를 남긴다.
function trace(o, m) {
var original o[m]; // 원본 메서드를 클로저에 기억
o[m] = function() {
console.log(new Date(), "Entering:", m);
var result = original.apply(this, arguments); // 원본 메서드 호출
console.log(new Date(), "Exiting:", m);
return result;
};
}

8.7.4 bind() 메서드

bind() 메서드는 ECMAScript 5에 추가되었다. bind()의 주요 목적은 함수와 객체를 서로 묶는 것이다. 함수 f의 bind() 메서드를 호출하면서 객체 o를 전달하면, bind() 메서드는 새로운 함수를 반환한다. 반환된 새 함수를 호출하면, 원래 함수 f가 o의 메서드로 호출된다. 새로운 함수에 전달한 모든 인자는 원래 함수에도 전달된다.

1
2
3
4
function f(y) { return this.x + y; }     // 바인드되어야 하는 함수
var o = { x : 1 }; // 바인드 될 객체
var g = f.bind(o); // g(x)를 호출하면 o.f(x)가 호출된다.
g(2) // => 3

ECMAScript 5의 bind() 메서드는 함수를 객체에 바인딩하는 것보다 더 많은 일을 한다. bind()에 전달하는 인자 중 첫 번째 이후의 모든 인자는 this 값과 함께 해당 함수의 인자로 바인딩된다. 이를 커링(currying)이라 부르기도 한다.

1
2
3
4
var sum = function(x, y) { return x + y };
// 첫 번째 인자는 1로 바인딩된다. 새로운 함수는 단지 하나의 인자만 요구한다.
var succ = sum.bind(null, 1);
succ(2) // => 3: x는 1에 바인딩되고 y 인자로 2를 넘긴다.



출처 : “JavaScript: The Definitive Guide, by David Flanagan (O’Reilly). Copyright 2011 David Flanagan, 978-0-596-80552-4”

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×