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 가 발생합니다.
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 = classPeople{ 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를 실행한 곳으로 반환합니다. 과정은 다음과 같습니다.
new People(‘KimJongMin’)을 실행
new 연산자가 constructor를 호출하면서 파라미터 전달
constructor에 작성한 코드를 실행하기 전에 빈 Object 를 생성
constructor 코드를 실행
생성한 Object(인스턴스)에 property 할당 (인스턴스를 먼저 생성했기 때문에 this로 Object 참조 가능
생성한 Object 반환
다음은 생성된 인스턴스의 구조입니다.
1
console.dir(people);
people 인스턴스의 **_proto_**는 People Class 오브젝트와 함께 생성된 Prototype object를 가리키고 있습니다. 결국 Class 문법을 이용한 코드를 prototype 기반의 코드로 변경하면 다음과 같습니다.
1 2 3 4 5 6 7
functionPeople(name) { this.name = name; }
People.prototype.say = function () { console.log('My name is ' + this.name); };
Prototype 기반 상속(ES5)과 Class 기반 상속(ES6) 비교
먼저 ES5에서 Prototype을 사용하여 상속을 구현하는 방법을 살펴보고, 그 후 ES6에서 Class로 상속을 구현하는 형태를 보겠습니다.
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를 다시 할당해 줍니다.
ES6에서는 extends 키워드로 상속을 구현합니다. Cat 클래스를 상속받은 Lion 클래스의 구조는 다음과 같습니다.
위의 prototype을 통해 상속을 구현한 Lion 생성자 함수의 구조와 비교했을때 일치합니다. 추가적으로 new Lion(‘Samba’) 를 실행하면 다음의 과정을 거치게됩니다.
Lion 클래스의 constructor를 호출
Lion 클래스에 constructor를 작성하지 않았기 때문에 슈퍼 클래스의(Cat) constructor가 호출됨 (내부적으로 프로토타입 체인으로 인해)
슈퍼 클래스의 constructor에서 this는 현재의 인스턴스를 참조하므로 인스턴스의 name 프로퍼티에 파라미터로 전달받은 값을 설정
생성한 인스턴스를 lion에 할당
super 키워드
서브 클래스와 슈퍼 클래스에 같은 이름의 메서드가 존재하면 슈퍼 클래스의 메서드는 호출되지 않습니다. 이때 super 키워드를 사용해서 슈퍼 클래스의 메서드를 호출할 수 있습니다. (서브 클래스의 constructor에 super()를 작성하면 슈퍼 클래스의 constructor가 호출됩니다.)
static 키워드
static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 prototype에 연결되지 않고 클래스에 직접 연결되기 때문에 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 동일한 클래스 내의 다른 정적 메서드 내에서 정적 메서드를 호출하는 경우 키워드 this를 사용할 수 있다.
정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.
마치며
ES6의 Class 문법에 대해 정리해 보았다. JavaScript 언어를 약 1년전 Node.js 를 시작하며 처음 접하게 되었는데 사실 그 당시 Prototype과 상속에 대해 크게 다룰일이 없었다. (어쩌면 너무 무지해서 사용 필요성을 느끼지 못했을 수도…) 그 후 Node.js 버전을 올리고 ES6를 공부하며 Class 문법을 접하게 되었는데 JavaScript의 Prototype에 대한 이해와 지식이 부족하다 보니 이전에 공부했던 C++과 Java의 Class 처럼 이해했던 것 같다. 그래도 그 후 Prototype과 더불이 Class까지 공부하며 지금은 어느정도 이해하게 된것 같다. 결론은… 역시나 JavaScript에서 Prototype을 이해하는건 중요한것 같다.
자바 스크립트에서 함수는 객체이기 때문에 프로퍼티를 가질 수 있습니다. 그리고 언제든지 함수에 사용자 정의 프로퍼티를 추가할 수도 있습니다. 함수에 프로퍼티를 추가하여 결과(반환 값)을 캐시하면 다음 호출 시점에 복잡한 연산을 반복하지 않을 수 있습니다. 이런 활용 방법을 **메모이제이션 패턴**이라고 합니다.
다음 코드에서는 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 = {};
메모이제이션 적용하기
메모이제이션을 공부한 후 현재 진행중인 에밀리(개인 프로젝트)에 적용해 보았습니다. 어느 부분에 적용했는지 적용 전과 적용 후 얼마나 효율이 올라갔는지를 알아보겠습니다.
수정할 부분
메모이제이션 패턴을 적용할 코드가 현재 하고 있는 기능은 다음과 같습니다. 사용자의 요청(학생식당, 카페테리아, 사범대식당, 기숙사식당, 교직원식당)에 따라 미리 크롤링 후 DB에 저장되어 있는 데이터(메뉴)를 가져와 응답합니다.
메모이제이션 패턴을 사용함으로써 얻을 수 있는 이점은 비용이 많이 드는 결과를 캐싱하고 그 이후에 재사용함으로써 비용을 줄일 수 있다는 것입니다. 현재 코드에서 비용이 많이 드는 작업은 DB를 조회하는 부분입니다. DB에는 다음과 같이 미리 크롤링한 데이터가 날짜별로 저장되어 있습니다.
사용자의 요청에 따라 해당 날짜의 데이터를 조회한 후 사용자가 요청한 식당에 맞는 데이터를 결과로 반환합니다. 그렇기 때문에 해당 날짜의 최초 요청이 이루어진 후 그 하루 동안에는 계속해서 DB에 같은 쿼리를 통해 같은 결과를 얻게 됩니다. 이 부분이 비용이 많이 드는 작업이기 때문에 메모이제이션 패턴을 통해 개선해보았습니다.
메모이제이션 적용 전
먼저 메모이제이션 패턴을 적용하기 전 코드입니다. menuHandler 클래스의 getMenu 함수는 menuService를 통해 DB에서 해당 날짜의 식당 메뉴를 가져와 매개변수로 전달받은 식당의 이름을 사용하여 결과로 반환하는 역할을 합니다.
classmenuHandler{ staticgetMenu(place) { returnnewPromise((_s, _f) => { const today = newDate().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 함수를 통해 캐시에 저장된 데이터가 있는지 확인 후 분기하여 처리합니다.
메모이제이션 패턴을 적용하기 전과 적용한 후의 성능 차이는 아래 보이는것처럼 눈에 띄게 차이가 납니다. 메모이제이션 패턴을 적용한 코드는 첫번째 요청(캐싱 하기 전)때는 메모이제이션 패턴 적용 전과 응답속도가 비슷하지만 그 이후의 응답은 캐싱된 데이터를 이용하기 때문에 비교될 정도로 빨라졌습니다.
마치며
메모이제이션을 적용하기 전과 후 모두 같은 기능을 결과를 만들어내는 코드이지만 코드를 작성하는 방법에 따라 더욱 더 빠른 효율적인 서비스를 만들 수 있다는 것을 느끼게 되었습니다. 현재 제 상황에서는 캐싱된 데이터도 하루가 지나게되면 쓰이지 않고 계속 메모리에 남아있게 되는데 그 부분에 대한 처리를 추가해야할것 같습니다. 이번에 적용한 코드 뿐만 아니라 아직 프로잭트 내에 메모이제이션 패턴을 적용할 수 있는 부분이 더 있습니다. 앞으로 디자인패턴 공부를 계속하여 새로운 패턴들을 적용시키며 리팩토링을 해야겠습니다.
자바스크립트에서는 비동기 프로그래밍 해결을 위해 하나의 패턴으로 콜백을 사용했다. 그러나 콜백 패턴은 비동기 처리 중 발생한 오류를 예외 처리하기 힘들고 여러 개의 비동기 로직을 한꺼번에 처리하는 데도 한계가 있다. 즉 콜백 패턴은 그다지 유용한 패턴이 아니다. 이때 비동기 프로그래밍을 위한 또 다른 패턴으로 Promise가 등장했다.
**Promise**는 비동기 처리 로직을 추상화한 객체와 그것을 조작하는 방식을 말한다. Promise를 지원하는 함수는 비동기 처리 로직을 추상화한 promise 객체를 반환 한다. 그리고 객체를 변수에 대입하고 성공 시 동작할 함수와 실패 시 동작할 함수 를 등록해 사용한다.
함수를 작성하는 방법은 promise 객체의 인터페이스에 의존 한다. 즉, promise 객체에서 제공하는 메서드만 사용해야 하므로 전통적인 콜백 패턴처럼 인자가 자유롭게 결정되는 게 아니라 같은 방식으로 통일된다. Promise 라고 부르는 하나의 인터페이스를 이용해 다양한 비동기 처리 문제를 해결할 수 있다. 복잡한 비동기 처리를 쉽게 패턴화할 수 있다는 뜻이다. 이것이 Promise의 역할이며 Promise를 사용하는 많은 이유 중 하나다.
Promise 사용법
Promise는 new 연산자를 선언하여 Promise 인스턴스 객체를 생성한다.
1 2 3
const promise = newPromise(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 특징
Promise는 항상 비동기로 처리된다.
Promise.resolve()나 resolve()를 사용하면 promise 객체는 바로 Fulfilled 상태가 되기 때문에 then()으로 등록한 콜백 함수가 동기적으로 호출될 것이라 생각할 수 있다. 하지만 실제로는 then()으로 등록한 콜백 함수는 비동기적으로 호출된다. 동기적으로 처리 가능한 상황에서도 비동기적으로 처리하는 이유는 동기와 비동기가 혼재될때 발생하는 문제를 막기 위함이다.
새로운 promise 객체를 반환하는 then
promise.then(), catch()는 최초의 promise 객체에 메서드를 체인하는 것처럼 보이지만 실제로는 then()과 catch()는 새로운 promise 객체를 생성해 반환한다. Promise.all()과 Promise.race() 또한 새로운 promise 객체를 생성해 반환한다.
콜백-헬과 무관한 Promise
Promise는 callback-hell 을 해결할수는 없고 완화할 수 있을 뿐이다. 완화할 수 있는 이유는 단일 인터페이스와 명확한 비동기 시점 표현, 강력한 에러 처리 메커니즘 때문이다. 이는 비동기 처리 자체를 손쉽게 다룰 수 있도록 하는 것이므로 callback-hell 을 해결하는 방법으로 여기는건 바람직하지 않다.
그런 다음 페이지가 로드되면 프레임워크는 URL 표시줄을 보고 [ ‘/‘] 페이지에서 문자열을 가져 와서 div class = "container"> </ div>에 삽입합니다. 또한 링크를 클릭하면 프레임워크가 이벤트를 가로 채고 컨테이너에 새 문자열 (예 : 페이지 [ ‘/ foo’])을 삽입하고 브라우저가 정상적으로하는 것처럼 HTTP 요청을 실행하지 못하게 합니다.
검색 엔진 최적화(SEO)
웹 크롤러가 reddit.com 을 요청하기 시작했다고 가정해봅시다.
1 2 3 4 5 6 7 8 9 10
var request = require('request'); request.get('reddit.com', function (error, response, body) { // body looks something like this: // <html> // <head> ... </head> // <body> // <a href="espn.com">ESPN</a> // <a href="news.ycombinator.com">Hacker News</a> // ... other <a> tags ... });
그러면 크롤러는 응답 본문에있는 <a href> 항목을 사용해서 새 요청을 생성합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13
var request = require('request'); request.get('reddit.com', function (error, response, body) { // body looks something like this: // <html> // <head> ... </head> // <body> // <a href="espn.com">ESPN</a> // <a href="news.ycombinator.com">Hacker News</a> // ... other <a> tags ... request.get('espn.com', function () { ... }); request.get('news.ycombinator.com', function () { ... }); });
그 후 크롤러는 espn.com 및 _news.ycombinator.com_의 링크를 사용하여 크롤링을 계속함으로써 프로세스를 계속 진행합니다.
결국 다음과 같은 재귀 코드처럼 동작합니다.
1 2 3 4 5 6 7 8 9 10
var request = require('request'); functioncrawlUrl(url) { request.get(url, function (error, response, body) { var linkUrls = getLinkUrls(body); linkUrls.forEach(function (linkUrl) { crawlUrl(linkUrl); }); }); } crawlUrl('reddit.com');
크롤러가 www.example.com/page?query#!mystate 를 방문하면 www.example.com/page?query&_escaped_fragment_=mystate 로 변환됩니다. 이렇게하면 서버가 _escaped_fragment_를 사용하여 요청을 받으면 사람이 아닌 크롤러에서 요청을 받는다는 것을 알 수 있습니다.
그렇기때문에 요청이 크롤러에서 온 경우 <div class = "container"> ... </ div>를 제공할 수 있습니다. 일반적인 요청 인 경우 <div class = "container"> </ div>를 제공하고 JavaScript가 내용을 내부에 삽입하도록 할 수 있습니다.
그러나 문제가 있습니다. 서버가 <div class = "container"> </ div>안에 무엇이 들어가는지 알지 못하기 때문입니다. 내부에 무엇이 들어가는지 파악하려면 JavaScript를 실행하고 DOM을 만들고 DOM을 조작해야합니다. 전통적인 웹 서버는 이를 수행하는 방법을 모르기 때문에 Headless Browser로 알려진 서비스를 사용합니다.
더 똑똑해진 크롤러
6년 후, Google은 크롤러가 한층 더 똑똑해 졌다고 발표했습니다. Crawler 2.0에서 <script> 태그를 볼 때 웹 브라우저처럼 실제로 요청을하고 코드를 실행하고 DOM을 조작한다는 것입니다.
그래서 다음과 같은 코드가
1
<divclass="container"></div>
이제는 이렇게 보이는 것입니다.
1 2 3 4 5 6 7
<divclass="container"> ... ... ... ... ... </div>
Fetch as Google를 사용하여 Google 크롤러가 특정 URL을 방문했을 때 어떤 내용을 볼지 결정할 수 있습니다.
관련된 발표문의 내용 일부를 첨부합니다.
당시 우리 시스템은 자바 스크립트를 사용하여 사용자에게 콘텐츠를 제공하는 페이지를 렌더링하고 이해할 수 없었습니다. 크롤러는 동적으로 생성 된 콘텐츠를 볼 수 없었기 때문에 웹 마스터가 AJAX 기반 애플리케이션을 검색 엔진으로 인덱싱 할 수 있도록 일련의 방법을 제안했습니다.
시대가 바뀌 었습니다. 현재 Googlebot이 자바 스크립트 또는 CSS 파일을 크롤링하는 것을 차단하지 않는 한 일반적으로 최신 브라우저와 같이 웹 페이지를 렌더링하고 이해할 수 있습니다.
덜 똑똑한 크롤러
불행히도 Google 만이 유일한 검색 엔진이 아닙니다. Bing, Yahho, Duck Duck Go, Baidu 등도 있으며 실제로 사람들은 이러한 검색 엔진도 빈번하게 사용합니다.
두 세계(서버 측 렌더링, 클라이언트 측 렌더링)의 장점을 최대한 활용하려면 다음의 방법이 있습니다.
첫 번째 페이지 로드에는 서버 측 렌더링을 사용.
그 후 모든 후속 페이지 로드에는 클라이언트 측 렌더링을 사용.
이것이 의미하는 바를 생각해보세요.
첫 번째 페이지 로드의 경우 사용자가 콘텐츠를 보기 전에 두 번 왕복하지 않습니다.
후속 페이지 로드가 빨라집니다.
크롤러는 간단한 HTML을 얻습니다. 옛날처럼 JavaScript를 실행하거나 _escaped_fragment_를 처리할 필요가 없습니다.
그러나 이를 위한 설정을 하기위해서는 서버에서 약간의 작업이 필요합니다. Angular, React 및 Ember 모두 이 접근 방식으로 변경했습니다.
토론
먼저 고려해야 할 몇 가지 사항은 다음과 같습니다.
약 2%의 사용자가 JavaScript를 사용할 수 없게 설정되어 있는 경우 클라이언트 측 렌더링이 전혀 작동하지 않습니다.
웹 검색의 약 1/4은 Google 이외의 엔진으로 수행됩니다.
모두가 빠른 인터넷 연결을 사용하는 것은 아닙니다.
휴대 전화 사용자는 대개 빠른 인터넷 연결이 필요하지 않습니다.
너무 빠른 UI는 혼란 스러울 수 있습니다. 사용자가 링크를 클릭한다고 가정 해보세요. 앱에서 새로운 뷰로 이동합니다. 그러나 새로운 뷰는 이전의 뷰와 미묘하게 다릅니다. 그리고 변경 사항은 즉시 발생했습니다 (클라이언트 측 렌더링의 장점). 새로운 뷰가 실제로 로드 된 것을 사용자가 알지 못할 수도 있습니다. 또는 사용자가 주의를 기울 였지만 상대적으로 미묘하기 때문에 사용자는 전환이 실제로 발생했는지 여부를 감지하기 위해 약간의 노력을 기울여야합니다. 때로는 약간의 로딩 스피너와 전체 페이지 재 렌더링을 하는 것이 좋습니다.
캐싱이 중요합니다. 따라서 서버 측 렌더링을 사용하면 실제로 사용자가 실제로 모든 것을 서버로 가져갈 필요가 없습니다. 때로는 바다 건너편의 “공식”서버가 아닌 근처의 서버에 가면됩니다.
실제로 성과와 관련하여 때로는 중요하지 않습니다. 때로는 속도가 좋고 속도가 약간 올라가더라도 삶이 더 좋아지지는 않습니다.
대부분의 사용자는 인터넷 연결 상태가 좋으며 충분히 빠릅니다. 특히 Macbook Pro로 yuppies를 타겟팅하는 경우. 초기로드 시간이 너무 길어서 사용자를 잃을 염려가 없습니다. 사용자가 링크를 클릭 할 때 실제로 새 페이지가 로드된다는 사실을 사용자가 알지 못하는 사용성 문제에 대해 걱정할 필요가 없습니다.
그러나 초기 페이지 로드시 서버 측 렌더링을 사용하는 클라이언트 측 렌더링을위한 사용 사례는 확실합니다. 큰 회사의 경우 #perfMatters, 인터넷 연결 속도가 느린 사용자가 있고 최적화에 충분한 시간을 할애 할 수있는 충분한 엔지니어링 팀이있는 경우가 종종 있습니다.
앞으로 이 같은 형태의 웹 프레임 워크 (초기 페이지 로드시 서버 쪽 렌더링을 사용하고 후에는 클라이언트 측 렌더링을 수행)가 보다 안정되고 사용하기 쉬워지기를 기대합니다. 이 시점에서 추가 된 복잡성은 최소화 될 것입니다. 그러나 오늘날,이 모든 것은 매우 새롭고, 많은 추상화가있을 것으로 기대합니다. 앞으로 더 나아가 클라이언트 측 렌더링이 필요하지 않은 곳에 인터넷 연결이 충분해지기 때문에 추세가 다시 서버 측 렌더링으로 되돌아 갈 것으로 예상됩니다.
C++, Java와 같은 클래스 기반 객체지향 언어와 달리 자바스크립트는 프로토타입 기반 객체지향 언어입니다. 프로토타입을 사용하여 객체지향을 추구하기 때문에 자바스크립트를 사용함에 있어 프로토타입을 이해하는 것은 중요합니다. 최근 ECMA6 표준에서 Class 문법이 추가되었지만 C++, Java에서 말하는 클래스가 아닌 프로토타입을 기반으로 하여 만들어진 문법입니다.
자바스크립트의 프로토타입을 처음 공부하면서 prototype, [[prototype]], _proto_, 객체, 함수, prototype chain 과 같은 용어들을 접하게 되는데 공부할수록 서로 뒤엉켜지고, 모르는 것도 아닌 그렇다고 제대로 알고 있는것도 아닌 어중간한 상태가 됩니다.
자바스크립트를 사용한 경험이 있으시다면 아래의 코드와 같은 형태를 경험한적이 있으실겁니다. 지금부터 아래의 코드가 어떤 원리로 동작하게 되는지 알아보겠습니다.
먼저 프로토타입에 대해 이해하기 위해서는 객체(object)는 함수(function)로부터 시작된다라는 것을 알아야 합니다. 이는 prototype을 이해하는데 많은 도움을 줍니다.
객체(object)는 함수(function)로부터 시작된다
자바스크립트에서 primitive를 제외하고는 모두 객체(object)입니다. 앞으로 등장하는 Object와 Function은 function(즉, 생성자)입니다. object는 객체를 의미합니다.
다음의 코드를 분석하기 전 객체(object)는 함수(function)로부터 시작된다라는걸 다시한번 기억하겠습니다.
1 2
functionBook() { } // 함수 var jsBook = new Book(); // 객체 생성
위의 코드에서 Book이라는 함수를 통해서 jsBook이라는 객체를 생성했습니다. 이때 Book 함수를 생성자라고 합니다. 생성자는 새로 생성된 객체를 초기화하는 역할을 합니다. 코어 자바스크립트는 기본 타입에 대한 생성자를 내장하고 있는데 이는 다음 코드를 통해 확인이 가능합니다.
1
var cssBook = {}; // 생성자 선언 없이 객체 생성
위에서는 리터럴 방식을 사용하여 객체를 생성하였습니다. 리터럴 방식 또한 결과적으로는 함수를 통하여 객체를 생성하게 됩니다. 자바스크립트 엔진이 해당 리터럴을 다음과 같이 해석합니다.
1
var cssBook = newObject(); // 객체 생성
따라서 결과적으로는 리터럴 방식으로 객체를 생성할때도 Object라는 함수(생성자)를 통해서 객체를 생성하게 됩니다. Object 뿐만 아니라 Array, Function, Date, RegExp 모두 함수입니다.
배열도 객체이기 때문에(자바스크립트 배열은 객체의 특별한 형태입니다. 프로퍼티 이름이 정수로 사용되며, length 프로퍼티를 가집니다.) 객체를 생성할때와 마찬가지로 배열(객체)의 생성에도 함수가 관여하게 됩니다. 따라서 무심코 사용했던 배열의 리터럴 표현도 결국에는 자바스크립트 엔진이 다음과 같이 해석합니다.
1 2 3
var books = ['html', 'css', 'js']; // 배열(객체) 생성 // 엔진이 다음과 같이 해석합니다. var books = newArray('html', 'css', 'js'); // 배열(객체) 생성
이제 객체(object)는 함수(function)로부터 시작된다라는 것을 알 수 있습니다.
함수(function) 생성시 발생하는 일
객체(object)는 함수(function)로부터 시작되기 때문에 사용자가 객체를 생성하기 위해 먼저 함수를 정의하게 됩니다. 이때 발생하는 일에 대해 알아보겠습니다. 여기서는 2가지를 기억해야 합니다.
1.함수를 정의하면 함수가 생성되며 Prototype object가 같이 생성 됩니다. 생성된 Prototype object는 함수의 prototype 속성을 통해 접근할 수 있습니다. (Prototype object같은 경우 함수 생성시에만 됩니다. 일반 객체 생성시에는 생성되지 않습니다.)
2.함수의 생성과 함께 생성된 Prototype object는 constructor와 __proto__를 갖고 있습니다. (cover property를 추가한것 처럼 사용자 임의로 추가 가능합니다.) constructor는 생성된 함수를 가리키며(여기서는 function Book을 가리킵니다.) **_proto_**는 Prototype Link로서 객체가 생성될 때 사용된 생성자(함수)의 Prototype object를 가리킵니다. Prototype Link는 뒤에서 자세하게 알아보겠습니다.
다이어그램을 통해 확인하면 다음과 같습니다.
객체(object) 생성시 발생하는 일
이번에는 객체 생성시 발생하는 일에 대해 알아보겠습니다. 조금 전에 정의한 Book 함수(생성자)를 사용하여 jsBook이라는 객체를 생성해 보겠습니다.
생성자(함수)의 몸체 부분에 어떠한 코드도 작성하지 않았는데 이를 통해 생성한 jsBook 객체가 __proto__라는 프로퍼티를 갖고있습니다.
여기서 _proto_ 는 Prototype Link로서 **객체의 생성에 쓰인 생성자 함수의 Prototype object**를 가리키고 있습니다. 그렇기 때문에 Book 생성자 함수와 함께 생성된 Prototype object에 추가한 cover라는 프로퍼티가 보이는것을 확인할 수 있습니다.
조금 더 이해하기 쉽게 다이어그램으로 확인하면 다음과 같습니다.
다이어그램에서도 확인할 수 있다시피 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를 추가하지 않았는데도 결과가 출력되는지 이해할 수 있습니다. 다음과 같이 동작할 것입니다.
또한 다음과 같이 프로토타입 체인의 최상위는 Object이기 때문에 Object.prototype의 property들을 모두 사용할 수 있습니다. 자주 사용하는 toString()과 valueOf() 모두 Object.prototype에 선언되어 있습니다. (Book Prototype object는 객체이기 때문에 Object 생성자가 사용될 것입니다. 따라서 Book Prototype object의 **_proto_**는 Object Prototype object를 가리키게 됩니다.)
Prototype object와 __proto__ 그리고 프로토타입 체인에 대해 이해하였으니 다음과 같은 코드도 이해할 수 있습니다. 잘 이해가 되지 않는다면 위의 다이어그램을 참고해보시기 바랍니다.
번외
혹시 다이어그램을 보면서 function Book의 _proto_ 는 무엇을 가리키고 있는지 궁금해 하셨을 분들을 위해 추적해보았습니다.
다음 코드를 도식화 하면 다음과 같은 다이어그램이 나오게 됩니다.
간단하게 포스팅을 하려했는데 주제가 주제인지라 길어졌습니다. 저도 프로토타입을 처음 공부하면서 어려움을 많이 겪었는데 조금이나마 도움이 되었으면 좋겠습니다.
정규 표현식(regular expression)은 문자의 패턴을 나타내는 객체다. 자바스크립트의 RegExp 클래스는 정규 표현식을 표현하고, String과 RegExp에는 정규표현식을 사용하여 강력한 패턴 매칭을 수행하는 메서드와 텍스트상에서 특정 텍스트를 찾아서 바꾸는 함수가 정의되어 있다.
10.1 정규 표현식 정의
자바스크립트에서 정규 표현식은 RegExp 객체로 표현된다. RegExp 객체는 RegExp() 생성자를 사용하여 만들 수 있지만, RegExp() 생성자보다는 정규 표현식 리터럴 문법이 더 자주 사용된다. 정규 표현식 리터럴은 항 쌍의 슬래시(/) 문자 사이에 위치한 문자들이다.
1 2
var pattern1 = /s$/; var pattern2 = newRegExp("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() 메서드는 주어진 인자를 구분자로 삼아, 메서드가 호출된 문자열을 부분 문자열로 쪼갠다.
자바스크립트에서 클래스는 프로토타입 기반의 상속 메커니즘을 기반으로 하고있다. 두 객체가 같은 프로토타입 객체로부터 프로퍼티를 상속받았다면, 둘은 같은 클래스의 인스턴스다. 자바스크립트의 클래스와 프로토타입 기반 상속 메커니즘은 자바나 그와 비슷한 언어의 클래스 상속과는 상당히 다르다. 자바스크립트 클래스의 중요한 특징 중 하나는 동적으로 확장될 수 있다는 것이다. 클래스를 정의한 다는 말은 모듈화되고 재사용 가능한 코드를 작성한다는 뜻이다.
9.1 클래스와 프로토타입
자바스크립트의 클래스는 같은 프로토타입 객체로부터 프로퍼티를 상속받은 객체의 집합이다. 따라서 프로토타입 객체는 클래스의 핵심이다.
9.2 클래스와 생성자
생성자는 새로 생성된 객체를 초기화하는 용도로 사용되는 함수다. 생성자는 new 키워드를 사용하여 호출한다. 생성자를 호출하면 자동으로 새로운 객체가 생성되고, 생성자 함수 내부에서 새로 생성된 객체를 사용하기 때문에, 생성자 함수는 새 객체의 상태를 초기화하는 데만 신경 쓰면 된다. 생성자 호출의 핵심적인 특징은 생성자의 prototype 프로퍼티가 새 객체의 프로토타입으로 사용된다는 것이다. 이는 한 생성자를 통해 생성된 모든 객체는 같은 객체를 상속하고, 따라서 같은 클래스의 멤버임을 뜻한다. 클래스와 생성자 함수의 이름은 대문자로 시작하는 것은 매우 일반적은 코딩 규칙이다. 일반 함수와 메서드는 소문자로 이름을 시작한다. 새 객체는 생성자 함수가 실행되기 전에 자동으로 생성되고, 생성자 함수 내에서 this 값으로 접근할 수 있다. 생성자는 그저 새 객체를 초기화하기만 하면 되고 생성된 객체를 반환할 필요도 없다. 생성자를 호출하면 새 객체는 자동으로 생성되고, 새 객체의 메서드로서 생성자 함수가 호출된 다음, 초기화가 완료된 새 객체가 반환된다. 생성자 호출이 일반적인 함수 호출과 크게 다른것이 생성자 이름의 첫 글자를 대문자로 하는 또 하나의 이유다. 생성자는 new 키워드를 사용하여 호출된다고 가정하기 때문에, 일반적인 함수 호출처럼 호출하면 보통 제대로 작동하지 않는다.
9.2.1 생성자와 클래스 구별
프로토타입 객체는 클래스를 구별할 때 핵심적인 역할을 한다. 두 객체는 같은 프로토타입 객체를 상속한 경우에만 같은 클래스의 인스턴스다. 새로 생성된 객체의 상태를 초기화하는 생성자 함수는 클래스 구별의 핵심이 아니다. 서로 다른 두 생성자 함수라도 같은 프로토타입 객체를 가리키는 prototype 프로퍼티를 가질 수 있다. 그러면 두 생성자는 같은 클래스의 인스턴스를 만드는데 사용될 수 있다. 생성자가 prototype 만큼 객체 구별에 핵심적인 역할을 하지는 않더라도, 생성자는 클래스를 대표하는 역할을 한다. 생성자는 객체가 어떤 클래스에 속한 것인지 검사할 때 instanceof 연산자와 같이 사용된다.
1
r instanceof Rnage // r이 Range.prototype을 상속했다면 true를 반환한다.
instanceof 연산자는 실제로 r이 Range 생성자에 의해 초기화되었는지를 검사하지는 않고, r이 Range.prototype을 상속하는지를 검사한다.
9.2.2 constructor 프로퍼티
모든 자바스크립트 함수는 생성자로 사용될 수 있는데, 함수가 생성자로 호출되려면 prototype 프로퍼티가 있어야 한다. 따라서 모든 자바스크립트 함수에는 자동으로 prototype 프로퍼티가 설정된다. 이 prototype 프로퍼티의 값은 constructor 프로퍼티 하나만 가진 객체다. constructor 프로퍼티는 열거되지 않으며 constructor 프로퍼티의 값은 해당 함수 객체다.
1 2 3 4
var F = function() {}; // 함수 객체다. var p = F.prototype; // F와 연관이 있는 프로토타입 객체다. var c = p.constructor; // 프로토타입과 관련한 함수 객체다. c === F // => true: 모든 함수에 대해 F.prototype.constructor==F이다.
미리 정의된 프로토타입 객체가 있고 이 프로토타입 객체가 constructor 프로퍼티를 갖고 있다는 말은, 일반적으로 어떤 객체가 자기 자신의 생성자를 가리키는 constructor 프로퍼티 또한 상속하고 있음을 뜻한다. 별도로 정의한 프로토타입 객체에는 constructor 프로퍼티가 없다. 따라서 해당 클래스의 인스턴스에도 constructor 프로퍼티는 없을 것이다. 이 문제는 명시적으로 프로토타입 객체에 constructor 프로퍼티를 추가함으로써 해결한다. 일반적인 또 다른 기법은 constructor 프로퍼티가 미리 정의되어 있는 prototype 객체를 사용하는 것이다. 거기에 하나씩 메서드를 추가해가면 된다.
9.3 자바 스타일 클래스
자바스크립트가 자바와 다른 점 한가지는 함수가 값이라는 점이고, 따라서 메서드와 필드 사이에는 뚜렷한 구분이 없다. 프로퍼티 값이 함수라면 그 프로퍼티는 메서드이고, 함수가 아니라면 보통의 프로퍼티나 ‘필드’일 뿐이다. 자바스크립트가 자바 스타일의 클래스 멤버를 흉내 낼 수 있지만, 자바의 중요한 특징 중 자바스크립트가 지원하지 않는 것이 몇 가지 있다. 먼저 자바는 인스턴스 메서드 안에서 인스턴스 필드를 메서드의 지역 변수처럼 사용할 수 있고, this를 비롯한 어떤 접두사도 붙일 필요가 없다. 자바스크립트는 이를 지원하지 않지만, with문을 사용하여 비슷한 효과를 얻을 수 있다.(권장X) 자바에서는 final을 사용하여 상수 필드를 정의할 수 있다. 그리고 클래스 내부에서만 사용하고 외부에서 볼 수 없는 필드나 메서드는 private으로 정의할 수 있다. 자바스크립트에는 이런 키워드들이 없다. 따라서 힌트를 제공하는 표기 규칙을 사용한다.(값이 변경되면 안 되는 프로퍼티들은 이름이 대문자이고, 밑줄로 시작하는 이름의 프로퍼티는 클래스 외부에서 사용하면 안된다는 뜻이다.) private 프로퍼티는 클로저의 지역 변수로 흉내 낼 수 있고, 상수 프로퍼티는 ECMAScript 5에서는 사용 가능하다.
9.4 클래스 확장하기
자바스크립트의 프로토타입 기반 상속 메커니즘은 동적이다. 객체는 자신의 프로토타입에서 프로퍼티를 상속받는데, 심지어 이는 객체가 생성된 이후에 프로토타입이 변경되더라도 마찬가지다. 다시말해 자바스크립트 객체의 프로토타입에 메서드를 추가함으로써 간단히 자바스크립트 클래스를 확장할 수 있다는 뜻이다. Object.prototype에도 메서드를 추가할 수 있고, 그러면 모든 객체에서 추가된 메서드를 사용할 수 있지만 이 프로퍼티는 모든 for/in 루프에서 열거될 것이기 때문에 권장하지 않는다. 호스트 환경(웹브라우저 같은)에서 정의된 클래스를 이러한 방식으로 확장할 수 있는지는 호스트 환경의 구현체마다 다르다. 이 때문에 클라이언트 측 프로그래밍에서 이러한 기법의 사용은 몹시 제한받는다.
9.5 클래스와 자료형
9.5.1 instanceof 연산자
왼쪽 피연산자는 클래스를 판별하려는 객체이며, 오른쪽 피연산자는 생성자 함수여야 하는데, 이 생성자 함수의 이름이 곧 해당 클래스의 이름이다. 표현식 o instanceof c는 만약 o가 c.prototype을 상속한다면 true다.(상속은 직접적일 필요없고, 만약 o가 c.prototype을 상속한 어떤 객체를 상속한다해도 이 표현식은 true이다.) instanceof 연산자는 생성자 함수를 요구하지만, 실제로 instanceof 연산자는 객체가 어떤 프로토타입을 상속했는지를 검사하여 객체를 생성하는 데 어떤 생성자를 사용했는지 테스트하지는 않는다. 만약 생성자 함수를 검사의 기준으로 삼고 싶다면, isPrototype() 메서드를 대신 사용할 수 있다.
9.5.2 constructor 프로퍼티
어떤 객체의 클래스를 구별하는 또 다른 방법은 constructor 프로퍼티를 사용하는 것이다. constructor 프로퍼티를 사용하는 이 기법은 instanceof와 같은 문제가 있으며 자바스크립트 객체 가운데는 constructor 프로퍼티가 없는 것도 있을 수 있다.
9.6 자바스크립트의 객체 지향 기법
9.6.3 표준 변환 메서드
객체 자료형을 변환하는 데 사용되는 중요한 메서드들이 있고, 이 중 몇 가지 형 변환이 푤아할 때 자바스크립트 인터프리터에 의해 자동으로 호출된다. 작성한 클래스에 변환 메서드를 구현하지 않았다면, 이는 단순히 술수로 구현하지 않은 것이 아니라 의도적인 것이어야 한다. 먼저, 가장 중요한 메서드는 toString()이다. 이 메서드의 목적은 객체의 문자열 표현을 반환하는 것이다. 자바스크립트는 필요할 때 자동으로 이 메서드를 호출한다. 프로퍼티 이름 같이 문자열을 요구하는 곳에 객체를 사용했을 때 또는 문자열 결합을 위해 + 연산자를 사용했을 때 toString() 메서드가 호출된다. 만약 이 메서드를 구현하지 않으면, 클래스는 Object.prototype의 기본 구현을 상속할 것이고, 그 기본 구현 메서드는 쓸모 없는 문자열 **”[object Object]”**를 반환할 것이다. 다음 메서드는 valueOf()이다. 이 메서드는 객체를 원시 값으로 변환한다. 예를 들면, valueOf() 메서드는 객체가 숫자 컨텍스트에서 산술 연산자(+ 제외)나 비교 연산자와 함께 사용될 때 자동으로 호출된다. 객체 대부분은 원시 값으로 변환할 필요가 없으므로, 이 메서드를 정의하지 않는다. 다음 메서드는 toJson()이고 JSON.stringify()에 의해 자동으로 호출된다. JSON 형식은 데이터 구조를 직렬화하는데 사용되고 자바스크립트 원시 값, 배열, 일반 객체를 처리할 수 있다. 직렬화 할 때 객체의 프로토타입과 생성자는 무시된다.
9.6.4 비교 메서드
자바스크립트의 동치 연산자들은 객체를 비교할 때, 값이 아니라 참조를 사용한다. 즉, 주어진 두 참조가 같은 객체를 가리키고 있는지를 본다. 만약 어떤 클래스를 정의하고 이 클래스의 인스턴스를 비교하려면 비교하는 데 사용할 메서드를 정의해야 한다. 인스턴스를 동등 비교하려면, equals()라는 인스턴스 메서드를 정의해야 한다. equals() 메서드는 하나의 인자를 받고, 전달받은 인자와 equals 메서드를 가진 객체가 같다면 true를 반환한다. 클래스 컨텍스트상에서 ‘같다’라는 것이 어떤 의미인지는 구현에 달려있다. 객체를 어떤 순서에 따라 비교하는 것도 유용하다. 어떤 클래스의 한 인스턴스가 다른 인스턴스보다 ‘작다’ 또는 ‘크다’라고 말할 수 있게된다. 만약 <와 <=같은 관계 연산자에 객체를 사용하면 자바스크립트는 먼저 해당 객체의 valueOf() 메서드를 호출하고, 이 메서드가 원시 값을 반환하면 값을 비교한다. 그러나 대다수 클래스에는 valueOf() 메서드가 없다. 만약 valueOf() 메서드가 없는 객체들을 명시적으로 선택한 순서에 따라 비교하려면, compareTo() 메서드를 정의해야 한다. compareTo() 메서드는 하나의 인자를 받고, 메서드 호출 대상 객체와 인자를 비교한다. 만약 this 객체가 인자 객체보다 작으면 compareTo()는 0보다 작은 값을 반환해야 하며, this 객체가 인자 객체보다 크면 0보다 큰 값을 반환해야 한다. 만약 두 객체가 같으면 을 반환해야 한다. 가장 좋은 방법은 equals() 메서드와 compareTo() 메서드를 일관성 있게 작성하는 것이다. 클래스에 compareTo() 메서드를 정의하는 한 가지 이유는 해당 클래스의 인스턴스들로 구성된 배열을 정렬하기 위함이다. Array.sort() 메서드는 추가 인자로 인스턴스를 비교하는 함수를 받는데, 이 비교 함수는 compareTo() 메서드와 똑같은 반환 값 규칙을 사용한다.
1
ranges.sort(function(a, b) { return a.compareTo(b); });
9.6.6 private 상태
인스턴스를 생성할 때, 생성자 호출의 클로저에 포착된 변수(혹은 인자)를 사용하면 private 인스턴스 필드를 흉내 낼 수 있다. 그러면 생성자 내부에서 함수들을 정의하고, 이 함수들을 새로 생성한 객체의 프로퍼티로 할당해야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 예제) 시작점과 끝점에 대해 약한 캡슐화가 적용된 Range 클래스 functionRange(from , to) { // this 객체의 프로퍼티로 from, to를 저장하지 말 것. // 대신에 시작점과 끝점을 반환하는 접근자 함수를 정의한다. // 인자로 넘어온 from, to 값은 클로저에 저장된다. this.from = function() { returnfrom; } this.to = function() { return to; }
// 프로토타입의 메서드들은 생성자에 인자로 전달된 from, to를 직접 볼 수 없다. // 프로토타입의 메서드들은 다른 모든 것과 마찬가지로 접근자 메서드를 호출해야 한다. Range.prototype = { constructor: Range, includes: function(x) { returnthis.from() <= x && x <= this.to(); }; } };
이러한 캡슐화 기법에는 오버헤드가 있다. 상태를 캡슐화하도록 클로저를 사용하는 클래스는 그렇지 않은 클래스보다 확실히 느리고 크다.
9.7 서브클래스
객체 지향 프로그래밍에서 클래스 B는 다른 클래스 A를 확장(extend)하거나 클래스 A의 하위클래스가 될 수 있다. 이런 경우, 클래스 A를 슈퍼클래스라 하고 클래스 B를 서브클래스라고 한다. 클래스 B의 인스턴스는 클래스 A의 모든 인스턴스 메서드를 상속한다. 클래스 B의 메서드가 클래스 A의 메서드를 재정의했을 때, 클래스 B의 재정의된 메서드에서 클래스 A의 원래 메서드를 호출할 수가 있는데 이를 메서드 체이닝이라고 한다. 비슷하게 서브클래스의 생성자 B()가 슈퍼클래스의 생성자 A()를 호출할 필요가 있는데, 이는 생성자 체이닝이라고 한다.
이하 내용은 추후에 업데이트 예정… 다른 부분보다 확실히 이해가 많이 필요한 부분이라 몇 번 더 읽어봐야 할 것 같다.
자바스크립트의 기본 데이터 타입은 객체다. 객체는 일종의 복합체로, 여러 값(원시 타입, 다른 객체)들을 묶어 이름으로 저장하고, 값을 가져올 수 있다. 객체는 이름과 값으로 구성된 프로퍼티들의 정렬되지 않은 집합이다. 자바스크립트 객체는 객체가 가진 고유 프로퍼티를 유지하는 것 외에 **’프로토타입’**이라고 하는 다른 객체의 프로퍼티를 상속 받는다. 객체의 메서드들은 일반적으로 상속받은 프로퍼티이고, 이를 **'프로토타입 상속'**이라고 한다. 프로토타입 상속은 자바스크립트의 핵심적 특징이다.
자바스크립트 객체는 프로퍼티를 동적으로 추가하고 제거할 수 있기 때문에 동적이지만 자바스크립트의 객체는 정적 객체를 흉내 낼 수도 있고, 정적 타입 언어에서의 ‘구조체’처럼 사용할 수도 있다.
자바스크립트에서는 문자열(string)과 숫자(number), true/false와 null/undefined를 제외한 나머지는 객체다. 비록 문자열과 숫자, 불리언 값은 객체는 아니지만 변경 불가능한 객체(Wrapper 객체)처럼 동작한다.
객체의 각 프로퍼티는 ‘프로퍼티 속성’(쓰기, 열거, 설정) 이라고 하는 연관된 값을 갖는다.
프로퍼티뿐 아니라, 모든 개체는 세 가지의 속성을 갖는다.
prototype은 상속받은 프로퍼티들을 가진 객체를 참조한다.
class는 객체의 자료형(타입)을 특정짓는 문자열이다.
extensible 속성(ECMAScript 5)은 객체에 새 프로퍼티를 추가할 수 있는지를 결정한다.
세 부류의 자바스크립트 객체
**’네이티브 객체’**는 ECMAScript 명세에 정의된 객체 또는 그 객체의 클래스다. Array, Function, Date, 정규 표현식들은 전부 네이티브 객체다.
**’호스트 객체’**는 브라우저와 같이 자바스크립트 인터프리터가 내장된 호스트 환경에 정의된 객체다.
**’사용자 정의 객체’**는 자바스크립트 코드의 실행으로 생성된 객체다.
두 종류의 프로퍼티
**’고유 프로퍼티’**는 객체에 직접 정의된 프로퍼티다.
**’상속받은 프로퍼티’**는 객체의 프로토타입 객체가 정의한 프로퍼티를 말한다.
6.1 객체 생성하기
객체 리터럴을 통해 만들 수도 있고, new 키워드를 사용해 만들 수도 있으며, ECMAScript 5의 Object.create() 함수를 통해서도 생성할 수도 있다.
6.1.1 객체 리터럴
객체 리터럴은 평가될 때마다 새로운 객체를 생성하고 초기화하는 표현식이다. 각 프로퍼티의 값 또한 리터럴이 평가될 때마다 새롭게 계산된다. 따라서 하나의 객체 리터럴은 수많은 객체를 만들 수 있다. 객체 리터럴이 반복적으로 호출되는 함수 내부의 루프 몸체에 있는 경우, 매 순간 생기는 객체의 프로퍼티 값들은 서로 다를 것이다.
6.1.2 new를 사용해 객체 생성하기
new 연산자는 객체를 만들고, 초기화한다. new 키워드 다음에는 반드시 함수 호출문이 와야한다. 이때 호출되는 함수를 **생성자(constructor)**라고 한다. 새로 생성된 객체를 초기화하는 역할을 한다. 코어 자바스크립트는 기본 타입에 대한 생성자를 내장하고 있다.
6.1.3 프로토타입
자바스크립트의 모든 객체는 또 다른 자바스크립트 객체와 연관되어 있다. 이 두번째 객체는 프로토타입(prototype)으로 알려져있고, 이때 객체는 프로토타입으로부터 프로퍼티들을 상속받는다. 객체 리터럴로 생성된 모든 객체는 프로토타입 객체가 같으며, 자바스크립트 코드에서 이 프로토타입 객체는 Object.prototype으로 참조할 수 있다. new 키워드를 사용해 생성자를 호출하면, 생성자 함수의 프로토타입이 생성된 객체의 프로토타입이된다. 따라서 new Object()로 생성된 객체는 {}로 생성된 객체와 마찬가지로 Object.prototype를 상속받는다. 마찬가지로, new Array()로 생성된 객체는 Array.prototype을 객체의 프로토타입으로 사용하고, new Date()로 생성된 객체는 Date.prototype을 객체의 프로토타입으로 사용한다.
모든 내장 생성자는 (그리고 대부분의 사용자 정의 생성자는) Object.prototype을 상속하는 객체를 프로토타입으로 갖는다. 예를 들어, Date.prototype은 Object.prototype의 프로퍼티들을 상속받는다. 따라서 new Date()를 통해 생성한 Date 객체는 Date.prototype과 Object.prototype으로부터 프로퍼티를 상속받는다. 이처럼 프로토타입 객체들이 연결된 것을 **'프로토타입 체인'**이라고 한다.
6.1.4 Object.create()
ECMAScript 5는 객체를 생성하는 Object.create() 메서드를 지원한다. 이 메서드의 첫 번째 인자가 프로토타입 객체다. Object.create()는 새 객체의 프로퍼티 정보를 두 번째 인자로 받을 수 있는데, 이 인자는 생략할 수 있다.
Object.create()는 정적 함수로, 개별 객체를 통해 호출되는 메서드가 아니다. 함수를 사용하기 위해서는 단순히 프로토타입 객체를 넘기기만 하면 된다.
1
var o1 = Object.create({x:1, y:2}); // o1은 x, y 프로퍼티를 상속받는다.
프로토타입을 갖지 않는 새 객체를 만들기 위해서는 함수에 null을 전달하면 된다. 하지만 이 경우 새롭게 생성된 객체는 어떠한 객체도 상속받지 않기 때문에 toString() 메서드와 같은 기본적인 메서드조차 사용할 수 없다. 만약 {} 또는 new Object()가 만들어내는 것과 같은 일반적인 빈 객체를 만들고 싶다면, 함수에 Object.prototype을 전달한다.
1
var o2 = Object.create(Object.prototype); // o2은 {} 또는 new Object()와 같은 객체다.
6.2 프로퍼티 접근 및 설정
6.2.1 연관 배열로서의 객체
1
object["property"]
위와 같은 형태는 마치 문자열을 인덱스로 갖는 배열에 접근하는 형태와 유사하다. 이러한 형태의 배열을 **연관 배열**이라고 하고, 해시나 맵, 사전이라고도 한다. 모든 자바스크립트 객체는 연관 배열이다.
자바스크립트는 C나 C++, 자바에 비해 타입의 제약이 느슨하다. 프로그램은 객체 안에 수많은 프로퍼티들을 만들 수 있다. 하지만 마침표(.) 연산자를 사용해 객체의 프로퍼티에 접근할 때는 프로퍼티의 이름을 반드시 식별자로 표현해야 한다. 식별자는 자바스크립트 프로그램에 직접 타이핑해 넣은 이름이며 자료형이 없으므로 프로그램이 실행되는 도중이 변경할 수 없다.
반면에 [] 연산자를 사용해 객체의 프로퍼티에 접근할 때는 프로퍼티의 이름을 문자열로 표현한다. 문자열은 자바스크립트의 자료형이므로 프로그램 실행중에 생성하고 조작할 수 있다. 따라서 다음의 코드가 가능하다. 다음의 코드는 customer 객체의 address0, address1, address2, address3 프로퍼티 값을 읽고, 읽은 값을 addr 변수에 차례대로 이어 붙인다.
1 2 3 4
var addr = ""; for(i = 0; i < 4; i++) { addr += customer["address" + i] + '\n'; }
6.2.2 상속
자바스크립트 객체는 고유 프로퍼티들을 갖고 있고, 동시에 해당 객체의 프로토타입 객체로부터 여러 프로퍼티들을 상속받는다.
객체 o에서 프로퍼티 x를 찾는다고 했을때, 객체 o가 프로토타입 객체에 고유 프로퍼티 x가 없다면, 해당 프로토타입 객체에서 x를 찾는다. 만약 프로토타입 객체에 고유 프로퍼티 x가 없다면, 해당 프로토타입 객체가 역시 또 다른 프로토타입을 가진 경우, 그 또 다른 프로토타입 객체에서 프로퍼티 x를 찾는다. 이 작업은 프로퍼티 x를 찾거나 prototype이 null인 객체가 발견될 때까지 계속된다. 이처럼 객체의 prototype 속성은 프로퍼티가 계승되는 체인 또는 연결리스트를 생성한다.
다음은 객체 o의 프로퍼티 x에 값을 설정하는 경우이다. 객체 o가 상속받지 않은 고유 프로퍼티 x를 갖고 있는 경우에는 기존의 프로퍼티 값을 단순히 바꿀 수 있다. 프로퍼티 x를 갖고 있지않은 경우에는 객체 o에 프로퍼티 x를 만든 후 값을 설정한다. 따라서 만약 객체 o가 프로퍼티 x를 상속받은 상태였다면, 기존에 상속받은 프로퍼티의 x값은 새로 설정되는 값에 의해 가려지게 된다.
객체의 프로퍼티에 값을 설정할 때는 해당 프로퍼티에 값을 설정할 수 있는지 알아보기 위해 프로토타입 체인을 검사한다. 예를 들어, 객체 o가 상속한 프로퍼티 x가 읽기 전용이라면 해당 프로퍼티에는 값을 설정할 수 없다. 하지만 값 설정이 허용된다면 원래 객체에 새로운 프로퍼티가 만들어지거나 그 값이 설정되며, 프로토타입 체인은 결코 변경되지 않는다. 프로퍼티를 질의 할 때는 상속이 동작하지만 설정할 때는 그렇지 않다는 것은 자바스크립트의 중요한 특징 중 하나다. 계승된 프로퍼티를 선택적으로 재정의 할 수 있기 때문이다.
6.2.3 프로퍼티 접근 에러
프로퍼티 접근 표현식을 사용해도 항상 값을 얻을 수 있거나 값을 설정할 수 있는 것은 아니다.
존재하지 않는 프로퍼티에는 접근해도 에러가 발생하지 않는다. 존재하지 않는 프로퍼티는 undefined로 평가된다. 하지만 존재하지 않는 객체의 프로퍼티에 접근하려고 하면 에러가 발생한다. null과 undefined 값은 어떠한 프로퍼티도 갖지 않기 때문에 이들 값에 프로퍼티로 접근을 시도하면 에러가 발생한다.
6.3 프로퍼티 삭제하기
delete 연산자는 객체의 프로퍼티를 삭제한다. 이 연산자는 프로퍼티의 값을 지우는 것이 아니라 프로퍼티를 지운다. delete 연산자는 상속받은 프로퍼티가 아닌 고유 프로퍼티만 지울 수 있다. (상속받은 프로퍼티를 지우기 위해서는 해당 프로퍼티가 정의된 프로토타입 객체에서 지워야 하고, 삭제에 성공하면 프로토타입 객체를 상속한 모든 객체가 영향을 받는다.)
6.4 프로퍼티 검사하기
in 연산자 왼쪽에는 프로퍼티 이름이 문자열로 와야하고 오른쪽에는 개체가 와야한다. 객체에 해당 프로퍼티가 존재하면 true가 반환된다.
1 2 3
var o = { x: 1 } "x"in o; // 객체 o에 고유 프로퍼티 x가 존재하므로 true를 반환한다. "toString"in o; // 객체 o에 상속받은 프로퍼티 toString가 있기 때문에 true를 반환한다.
객체의 hasOwnProperty() 메서드는 주어진 이름의 프로퍼티가 객체에 존재하는지 검사한다. 상속받은 프로퍼티의 경우는 false를 반환한다.
1 2 3
var o = { x: 1 } o.hasOwnProperty("x"); // 객체 o에 고유 프로퍼티 x가 존재하므로 true를 반환한다. o.hasOwnProperty("toString"); // toString은 상속받은 프로퍼티이기 때문에 false를 반환한다.
propertyIsEnumerable() 메서드는 hasOwnProperty()보다 상세한 검사를 한다.
undefined가 아니지만 확인할 때는 in 연산자 대신 논리 연산자 !==를 사용하는 편이 훨씬 효과적이다.
1 2 3
var o = { x: 1 } o.x !== undefined; // true: 객체 o에 프로퍼티 x가 존재한다. o.y !== undefined; // false: 객체 o에 프로퍼티 y가 존재하지 않는다.
in 연산까지 사용하면 객체에 프로퍼티가 존재하지 않는 경우와 객체에 프로퍼티가 존재하지만 값이 undefined인 경우를 구별할 수 있다.
1 2 3 4 5
var o = { x: undefined } // 프로퍼티가 분명히 존재하지만 값이 undefined다. o.x !== undefined// false: 프로퍼티가 존재하지만 값이 undefined다. o.y !== undefined// false: 프로퍼티가 존재하지 않는다.하지만 값이 undefined다. "x"in o // true: 프로퍼티가 존재한다. "y"in o // false: 프로퍼티가 존재하지 않는다.
6.5 프로퍼티 열거하기
객체가 가진 모든 프로퍼티들을 순회하고 싶을 때는 보통 for/in 루프토 해결한다. 지정한 객체가 가진 고유 프로퍼티 또는 상속된 프로퍼티들 중 열거 가능한 프로퍼티들마다 for/in 루프의 몸체가 실행된다. 상속받은 내장 메서드는 열거할 수 없지만, 사용자가 임의로 추가한 프로퍼티들은 열거할 수 있다.(임의로 열거할 수 없도록 설정하는 함수를 사용할 수도 있다.)
1 2 3 4 5
var o = { x:1, y:2, z:3 } // 열거할 수 있는 3개의 고유 프로퍼티 o.propertyIsEnumerable("toString"); // => false: toString은 열거할 수 없는 프로퍼티 for(p in o) // 객체 o의 모든 프로퍼티에 대해 console.log(p); // 프로퍼티 이름을 출력. 결과는 x, y, z가 출력 // toString은 출력되지 않는다.
for/in 루프 말고도 ECMAScript 5에는 프로퍼티 이름을 열거하는 두가지 함수가 더 있다.
Object.keys() : 객체가 가진 고유 프로퍼티 중에 열거할 수 있는 프로퍼티 이름을 배열에 담아 반환한다.
Object.getOwnPropertyNames() : 프로퍼티를 열거하는 함수다. Object.keys()는 객체가 가진 ‘열거할 수 있는’ 고유 프로퍼티들을 배열에 담아 반환하지만, Object.getOwnPropertyNames()는 해당 객체가 가진 모든 고유 프로퍼티의 이름을 배열로 반환한다.
6.6 프로퍼티 Getter와 Setter
ECMAScript 5에서 프로퍼티의 값은 getter/setter 메서드로 대채할 수 있다. getter/setter 메서드로 정의된 프로퍼티는 단순히 값을 갖는 ‘데이터 프로퍼티’와는 다른 **'접근자 프로퍼티'**라고 한다.
프로그램이 객체의 접근자 프로퍼티의 값에 접근하면, 자바스크립트 엔진은 getter 메서드를 아무런 인자없이 호출하고 이때 반환 값이 프로퍼티 접근 표현식의 값이 된다. 프로그램이 프로퍼티의 값을 변경하려고 하면, 자바스크립트 엔진은 setter 메서드를 호출한다. 이때 할당자(=)의 오른쪽에 있는 값을 setter 메서드의 인자로 전달한다. setter 메서드는 프로퍼티의 값을 ‘설정’하는 것을 담당하고, 그 반환값은 무시된다.
데이터 프로퍼티가 writable(쓰기) 속성을 갖는 반면, 접근자 프로퍼티는 쓰기 속성이 없다. 만약 프로퍼티가 getter/setter 메서드를 모두 갖고 있으면, 읽기/쓰기 모두 가능한 프로퍼티인 것이고, 프로퍼티가 getter 메서드만 갖고 있다면, 읽기 전용 프로퍼티인 것이다. 프로퍼티가 setter 메서드만 갖고 있으면 쓰기 전용 프로퍼티고, 이때 읽기를 시도하면 항상 undefined가 반환된다. 접근자 프로퍼티는 다음과 같이 확장된 객체 리터럴 문법을 사용하여 쉽게 정의할 수 있다.
1 2 3 4 5 6 7
var o = { // 데이터 프로퍼티 data_prop: value, // 한 쌍의 함수로 정의된 접근자 프로퍼티 getaccessor_prop() { /* 함수 몸체 */ }, setaccessor_prop(value) { /* 함수 몸체 */ }, }
접근자 프로퍼티는 그 이름이 프로퍼티 이름과 같은 하나 또는 두 개의 함수이며, 함수 정의에 사용되는 function 키워드 대신 get/set을 사용한다. 자바스크립트는 getter/setter 함수를 객체의 메서드로서 호출한다. 이는 함수의 몸체 안에 사용된 this 키워드가 객체 자신을 가리킨다는 뜻이다. 접근자 프로퍼티는 데이터 프로퍼티와 마찬가지로 상속할 수 있다.
6.7 프로퍼티 속성
프로퍼티에는 프로퍼티로 할 수 있는 작업을 결정하는 세 가지 속성이 있다.
writable : 프로퍼티 값의 변경 가능 여부를 결정
enumerable : 프로퍼티가 열거될 수 있는지 여부를 결정
configurable : configurable 속성뿐 아니라 writable 속성과 enumerable 속성 값의 변경 가능 여부를 결정
접근자 프로퍼티의 getter/setter 메서드를 프로퍼티가 가진 속성으로 다룬다면 접근자 프로퍼티의 네 가지 속성은 get, set, enumerable, configurable이다. ECMAScript 5에서는 프로퍼티의 속성 값을 질의하고, 값을 설정할 수 있는 프로퍼티 디스크립터라는 객체를 제공한다. 이 객체의 프로퍼티 이름은 표현 대상 속성의 이름과 같다.
접근자 프로퍼티의 프로퍼티 디스크립터 객체의 프로퍼티 : get, set, enumerable, configurable 객체가 가진 특정 프로퍼티에 대한 프로퍼티 디스크립터 객체는 Object.getOwnPropertyDescriptor()를 통해 얻을 수 있다. (Object.getOwnPropertyDescriptor()는 객체의 고유 프로퍼티에서만 동작한다.)
프로퍼티의 속성을 설정하거나 임의의 속성으로 새 프로퍼티를 만들기 위해서는 Object.defineProperty()를 호출한다. 함수의 인자로, 수정할 객체와 추가하거나 변경할 프로퍼티 이름, 프로퍼티의 디스크립터 객체를 넘긴다.
1 2
var o = { }; Object.defineProperty(o, "x", {value: 1, writable: true, enumerable: false, configurable:true});
6.1 절에 있었던 ECMAScript 5 메서드인 Object.create() 메서드의 첫 번째 인자로는 새로 생성할 객체의 프로토타입 객체이며, 두 번째 선택 인자는 Object.defineProperty() 의 두 번째 인자와 같다. 이 두 번째 인자는, 생성된 객체에 프로퍼티로 추가된다.
// Object.getOwnPropertyDescriptor()와 Object.defineProperty() 메서드를 사용하여 프로퍼티가 가진 속성까지 복사하는 extend() 함수 // // Object.prototype에 열거되지 않는 메서드 extend()를 추가한다. // 이 메서드는 호출 시에 인자로 전달된 객체에서 프로퍼티들을 복사하여 객체를 확장한다. // 단순 프로퍼티의 값뿐 아니라 모든 프로퍼티 속성을 복사한다. // 인자로 넘긴 객체가 소유한 모든 고유 프로퍼티는 대상 객체에 같은 이름의 // 프로퍼티가 존재하지 않는 한 대상 객체에 복사된다. Object.defineProperty(Object.prototype, "extend", // Object.prototype.extend를 정의한다. { writable: true, enumerable: false, // 열거 불가능 configurable: true, value: function(o) { // Object.prototype.extend 메서드의 값은 함수다. // 열거되지 않는 프로퍼티들을 포함한 고유 프로퍼티에 대해 var names = Object.getOwnPropertyNames(o); for(var i = 0; i < names.length; i ++) { // this 객체에 이미 같은 이름의 프로퍼티가 존재하면 건너뛴다. if (names[i] inthis) continue; // 객체 o의 프로퍼티 디스크립터를 가져온다. var desc = Object.getOwnPropertyDescriptor(o, names[i]); // this 객체에 프로퍼티를 생성할 때 앞에서 가져온 디스크립터 객체를 사용한다. Object.defineProperty(this, name[i], desc); } } } );
6.8 객체 속성
모든 객체는 prototype, class, extensible 속성을 갖고 있다.
6.8.1 prototype 속성
prototype 속성은 객체가 만들어지는 시점에 설정된다. 객체 리터럴을 통해 만든 객체는 Object.prototype을 객체의 프로토타입으로 설정하고, new를 사용해 만든 객체는 생성자 함수의 prototype 프로퍼티값이 prototype이 된다. Object.create() 메서드로 만든 객체는 메서드의 첫 번째 인자가 프로토타입 속성의 값이 된다. 객체 A가 객체 B의 프로토타입(또는 프로토타입 체인의 일부)인지 알아보기 위해서는 isPrototypeOf() 메서드를 사용한다.
1 2 3 4
var p = { x: 1 } var o = Object.create(p); p.isPrototypeOf(o) // => true: 객체 o는 객체 p를 상속받는다. Object.prototype.isPrototypeOf(p) // => true: 객체 p는 Object.prototype을 상속받는다.
6.8.2 class 속성
객체의 class 속성은 객체의 타입에 대한 정보를 담고 있는 문자열이다. Object.prototype으로부터 상속되는 기본(default) toString() 메서드는 객체의 타입을 아래 형태의 문자열로 반환한다.
1
[object class]
따라서 객체의 클래스 정보를 알아보기 위해서는 객체의 toString() 메서드를 호출하면 된다.
6.8.3 extensible 속성
객체의 extensible 속성은 객체에 새 프로퍼티를 추가할 수 있는지 여부를 결정한다. extensible 속성의 목적은 ‘잠겨있는’ 객체의 상태를 고정하고, 외부에서 변경하는 것을 막는 것이다. ECMAScript 5에서는 모든 내장 객체와 사용자 정의 객체는 확장할 수 없게 바뀌지 않는한 확장 가능하고, 호스트 객체의 확장성은 구현체에 따라 다르다. 확장할 수 있는 객체인지 알아보려면 object.isExtensible() 함수에 해당 객체를 인자로 넘긴다. 객체를 확장할 수 없도록 하려면, Object.preventExtensions()에 해당 객체를 인자로 넘긴다. 해당함수를 사용하면 전 상태로 돌아갈 수 없다. 또한 extensible 속성 값이 false인 객체라도, 프로토타입에 새 프로퍼티를 추가하면, 추가된 프로퍼티는 해당 객체에 상속된다.
6.9 객체 직렬화하기
객체 직렬화는 객체의 상태를 문자열로 변환하는 과정을 말한다. ECMAScript 5는 자바스크립트 객체를 직렬화하는 JSON.stringify() 메서드와 직렬화한 문자열을 객체로 복원하는 JSON.parse() 메서드를 지원한다. 이 두 함수는 JSON 데이터 교환 형식을 사용한다. JSON은 ‘JavaScript Object Notation’의 줄임 표현이다. JSON 문법은 자바스크립트 문법의 부분 집합이기 때문에, 자바스크립트의 모든 값을 표현할 수는 없다. Function, RegExp, Error 객체와 undefined 값은 직렬화하거나 복원할 수 없다. JSON.stringify() 메서드는 객체가 가진 열거 가능한 고유 프로퍼티만 직렬화한다.
6.10 객체 메서드
모든 자바스크립트 객체는 Object.prototype의 프로퍼티를 상속받는다. 상속된 프로퍼티들은 대부분 메서드이고, 어느 객체에서도 사용할 수 있기 때문에 주요 메서드라고 할 수 있다.
6.10.1 toString() 메서드
toString() 메서드는 어떠한 인자도 받지 않고, 호출 대상 객체의 값을 어떠한 방식으로든 문자열로 만들어서 반환한다. 자바스크립트는 객체를 문자열로 변환해야 할 때 항상 toString() 메서드를 사용한다.
6.10.3 toJSON() 메서드
Object.prototype에는 toJSON() 메서드가 정의되어 있지 않다. 하지만 JSON.stringify() 메서드는, 직렬화할 객체에 toJSON() 메서드가 있는지 찾고, 만약 있다면, toJSON() 메서드가 호출되고 그 결과 값이 원래 객체 대신 직렬화된다.
6.10.4 vluaeOf() 메서드
valueOf() 메서드는 toString() 메서드와 매우 비슷하다. 이 메서드는 객체가 원시 타입 값을 필요로 하는 문맥 안에서 사용될 때, 자바스크립트는 valueOf() 메서드를 자동으로 호출한다.
자바스크립트의 배열은 타입이 고정되어 있지 않다. 같은 배열에 있는 원소 값의 타입은 서로 다를 수 있다. 배열의 원소는 객체가 될 수도 있고, 또 다른 배열이 될 수도 있다. 자바스크립트 배열은 동적이다. 배열의 크기가 필요에 따라 커지거나 작아질 수 있다. 배열을 생성하거나, 크기가 변경되어 다시 할당을 할 때도 배열 크기를 선언할 필요가 없다. 자바스크립트 배열은 밀집도가 높지 않고, 각 원소의 인덱스가 연속적이지 않아도 되고, 원소들 사이에 빈자리가 있어도 된다. 자바스크립트 배열에는 length 프로퍼티가 존재한다. 자바스크립트 배열은 자바스크립트 객체의 특별한 형태이고, 배열의 인덱스는 프로퍼티 이름인데 정수인 것이다. 일반적으로 배열은 객체 프로퍼티를 통해 원소에 접근하는 것보다 정수 첨자를 통해 원소에 접근하는 것이 훨씬 빠르도록 최적화 되어 있다. 배열은 Array.prototype의 프로퍼티들을 상속받는다.
7.1 배열 만들기
배열을 만드는 가장 쉬운 방법은 배열 리터럴을 사용하는 것이다. 배열 리터럴은 대괄호([,])안에 배열의 원소를 쉼표(,)로 구분해 나열한 것이다. 만약 배열 리터럴은 객체 리터럴 또는 다른 배열 리터럴을 포함할 수 있다. 만약 배열 리터럴에서 빠진 부분이 있다면 해당 부분의 원소 값은 undefined가 된다.
1
var undefs = [,,]; // 두 원소 모두 값은 undefined (배열 리터럴의 문법은 마지막 원소 다음에 쉼표 추가 가능)
배열을 만드는 또 다른 방법은 Array() 생성자를 이용하는 것이다. 생성자는 3가지 방법으로 호출 가능하다. – 인자 없이 호출 빈 배열을 생성하고 생성된 배열은 배열 리터럴 []과 동일하다.
1
var a = newArray();
– 배열의 길이를 의미하는 숫자를 인자로 주어 호출 배열에 저장될 원소의 크기를 알고, 미리 공간을 할당할 때 사용한다. 배열에는 어떠한 값도 저장되지 않고, 배열의 인덱스 프로퍼티 값(“0”, “1” …)도 존재하지 않는다.
1
var a = newArray(10);
– 두 개 이상의 원소, 또는 숫자가 아닌 원소 값 하나를 명시적으로 지정 생성자의 인자 값들이 배열의 원소가 된다. 리터럴을 사용하는편이 훨씬 더 간단하다.
1
var a = newArray(5, 4, 3, 2, 1, "test");
7.2 배열의 원소 읽고 쓰기
배열의 각 원소에 접근할 때에는 [] 연산자를 사용한다. 배열은 객체의 특별한 종류이다. 배열의 [] 구문은 객체 프로퍼티 접근 때 쓰는 []와 똑같이 동작한다. 자바스크립트는 사용자가 명시한 숫자 배열 인덱스를 문자열 형태로 바꿔서 프로퍼티 이름으로 사용한다. 배열의 인덱스와 객체 프로퍼티 이름을 올바르게 구별할 줄 알면 좋다. 모든 인덱스 값은 프로퍼티 이름이지만, 프로퍼티 이름은 0과 2의32승-1 사이의 정수여야만 인덱스가 될 수 있다. 모든 배열은 객체이므로, 어떤 이름의 프로퍼티라도 자유롭게 만들 수 있다. 하지만 배열에는, 프로퍼티 가운데 인덱스인 것들을 사용하면 length 프로퍼티의 값이 자동으로 갱신되는 특별한 기능도 갖추어져 있다.(배열이 일반 객체와 다른 점은 속성 이름으로 2의32승보다 작은 양수를 사용할 때, 자동으로 length 프로퍼티의 값을 바꾼다는 것이다.) 배열 첨자로 양의 정수가 담긴 문자열을 사용하면, 일반적으로 프로퍼티가 아닌 배열 인덱스로 쓰인다. (소수점 아래가 없는 부동 소수점 값도 마찬가지) 반면에 음수나, 정수 아닌 수들을 사용하면 숫자는 문자열로 변환되고, 변환된 문자열은 배열 객체의 프로퍼티 이름으로 사용된다. 객체에 존재하지 않는 프로퍼티 이름을 질의하면, 에러가 발생하지 않고 단순히 undefined값이 반환된다. 이러한 성질은 배열에도 적용된다.
1 2 3
a = [true, false]; // 두 개의 원소를 가진 배열을 생성한다. a[2] // => undefined. 해당 인덱스에 원소가 없어서. a[-1] // => undefined. '-1'이라는 속성 이름에 해당하는 값이 없어서.
모든 배열은 객체다. 따라서 배열은 객체의 프로토타입으로부터 원소들을 상속 받을 수 있다.
7.3 희소배열
희소배열은 배열에 속한 원소의 위치가 연속적이지 않은 배열을 말한다. 보통, 배열의 length 프로퍼티는 배열에 속한 원소의 개수를 의미한다. 그러나 희소배열의 경우, length 프로퍼티의 값은 원소의 개수보다 항상 크다.
1 2 3
a = newArray(5); // 원소가 없는 배열이지만 a.length의 값은 5다. a = [?]; // length 값이 0인 빈 배열을 생성한다. a[1000] = 0; // 하나의 원소를 할당했지만, length 값은 1001이 된다.
희소배열은 보통배열보다 일반적으로 느리고, 메모리를 많이 사용할 뿐 아니라, 원소를 찾는데 걸리는 시간이 일반 객체의 속성 값을 찾는 시간만큼 오래 걸린다. 배열 리터럴 사용 시 값을 명시하지 않는 방법으로는 희소배열을 만들 수 없다. 해당 원소의 값이 undefined가 되기 때문이다. 이는 배열에 원소가 아예 존재하지 않는 것과는 다르다. in 연산자를 사용하면 두 경우의 차이점을 알 수 있다.
1 2 3 4
var a1 = [,,,]; // 세 개의 원소가 undefined인 배열 var a2 = newArray(3); // 원소가 존재하지 않는 배열 0in a1 // => true: a1에는 0번 인덱스 위치에 원소가 존재한다. 0in a2 // => false: a2에는 0번 인덱스 위치에 원소가 존재하지 않는다.
7.4 배열의 길이
1 2 3 4
a = [1,2,3,4,5]; // 다섯 개의 원소를 가진 배열 a.length = 3; // length를 3으로 바꿨기 때문에 결과는 [1,2,3] a.length = 0; // length 값이 0이기 때문에 모든 element를 삭제, 결과는 [] a.length = 5; // length 값은 5이지만, 원소가 없다. new Array(5)와 같은 결과
7.5 배열에 원소를 추가하거나 삭제하기
배열에 원소를 추가하는 방법
배열의 새 인덱스에 값을 할당한다.
push() 메서드를 사용해 배열의 끝에 원소를 추가한다. (a[a.length]에 값을 할당하는것과 같다.)
unshift() 메서드를 사용하면 배열의 앞쪽에 원소를 추가할 수 있다.
배열에 원소를 삭제하는 방법
delete 연산자로 배열의 원소를 삭제할 수 있다. (배열의 length는 줄어들지 않는다.)
pop() 메서드를 사용해 배열의 앞에서 원소를 삭제한다.
배열의 특정 원소를 지우는 것은, 해당 원소에 undefined 값을 할당하는 것과 의미가 비슷하다. 원소가 지워지더라도 생기는 공백을 다른 원소가 대신하지 않으며, 해당 배열은 희소배열이 된다.
7.6 배열 순회하기
ECMAScript 5에는 배열을 순회하는 다양한 메서드가 추가되었다. 사용자가 정의한 함수에 배열의 원소가 인덱스 순서대로 하나씩 넘어오도록 하여 배열을 순회하는 형태다. forEach() 메서드가 대표적이다.
1 2 3 4 5
var data = [1,2,3,4,5]; var sumOfSquares = 0; data.forEach(function(x)) { sumOfSquares += x*x; });
배열을 다룰 때 forEach() 같은 순회 메서드는 간단하고 강력한 함수형 프로그래밍 스타일을 사용할 수 있게 한다.
7.7 다차원 배열
자바스크립트는 진정한 의미에서의 다차원 배열을 지원하지는 않는다. 그러나 배열의 배열을 사용해 다차원 배열을 흉내 낼 수 있다. 배열 내의 배열에 있는 원소에 접근하기 위해서는 단순히 [] 연산자를 두 번 사용하면 된다.
7.8 배열 메서드
7.8.1 join()
Array.join() 메서드는 배열의 모든 원소를 문자열로 변환하고, 변환한 문자들을 이어 붙인 결과를 반환한다. 결과로 반환되는 문자열에서 배열의 원소들을 구별하기 위해 구분자 문자열을 사용한다. 별도로 구분자 문자열을 지정하지 않으면 쉼표(,)가 기본 값으로 사용된다.
1 2 3 4 5 6
var a = [1, 2, 3]; a.join(); // => '1,2,3' a.join(" "); // => '1 2 3' a.join(""); // => '123' var b = newArray(10); // 길이가 10인 빈 배열 b.join('-') // => '---------': 아홉 개의 하이픈 문자열
Array.join() 메서드는 String.split() 메서드와는 반대로 작동한다. String.split() 메서드는 문자열을 조각들로 분리하고, 이 조각들을 원소로 하는 배열을 생성한다.
7.8.2 reverse()
Array.reverse() 메서드는 배열의 원소 순서를 반대로 뒤집어 반환한다. 순서가 뒤바뀐 새로운 배열을 생성하는 것이 아니라, 이미 존재하는 배열 안에서 원소들의 순서를 뒤바꾼다.
7.8.3 sort()
Array.sort() 메서드는 배열 안의 원소들을 정렬하여 반환한다. sort() 메서드는 별도의 인자전달 없이 호출하면, 배열 안의 원소들을 알파벳순으로 정렬한다. 배열에 undefined 원소들이 존재하면, 이 원소들은 배열의 끝부분으로 정렬된다. 알파벳순이 아니라 다른 순서로 배열을 정려하려면, sort() 메서드의 전달 인자를 통해 비교 함수를 직접 명시해주어야 한다. 비교 함수는 전달인자를 두 개 받아서, 정렬된 배열에서 어떤 것이 먼저 나타나야 하는지 판단한다. 만약 첫 번 째 인자가 두 번째보다 먼저 나타나야 한다면, 비교 함수는 0보다 작은 숫자를 반환해야 한다. 만약 두 값이 동등하다면 0을 반환해야 한다.
1 2 3 4 5 6
var a = [33, 4, 1111, 222]; a.sort(); // 알파벳순: 1111, 222, 33, 4 a.sort(function(a, b) { // 번호순: 4, 33, 222, 1111 return a-b; // 0보다 작은 값, 0, 또는 0보다 큰 값을 반환한다. }); a.sort(function(a,b) {return b-a}); // 내림차순 정렬
7.8.4 concat()
Array.concat() 메서드는 기존 배열의 모든 원소에 concat() 메서드의 전달인자들을 추가한 새로운 배열을 반환한다. 전달인자로 배열을 전달하면, 이 배열안의 원소들을 꺼내어 반환하는 배열에 추가한다. 하지만 중첩 배열일 경우에는 중첩된 배열의 원소까지는 꺼내지 않는다.
Array.slice() 메서드는 부분 배열을 반환한다. 부분 배열은 배열에서 잘라낸 원소들을 담은 새 배열이다. slice() 메서드는 전달인자를 두 개 받는데, 각 인자는 반환될 부분의 처음과 끝을 명시한다. 반환되는 배열은 첫 번째 전달인자가 지정하는 위치부터 두 번째 전달인자가 지정하는 위치 이전까지의 모든 원소를 포함한다. 만약 전달인자가 하나라면 그 위치에서 배열 끝까지의 모든 원소를 포함하는 부분 배열을 반환한다. 만약 전달인자가 음수라면, 배열의 마지막 원소에서부터의 상대적인 위치를 가리키는 것이다.
1 2 3 4 5
var a = [1,2,3,4,5]; a.slice(0, 3); // [1,2,3] a.slice(3); // [4,5] a.slice(1, -1); // [2,3,4] a.slice(-3, -2); // [3]
7.8.6 splice()
Array.splice() 메서드는 배열의 원소를 삽입하거나 원소를 제거하려 할 때 범용적으로 사용하는 메서드다. splice() 메서드는 slice()나 concat() 메서드와는 달리 호출 대상 배열을 바로 수정한다. splice()의 첫 번째 전달인자는 배열상에서 삽입 혹은 삭제 작업을 시작할 위치를 지정하고, 두 번째 전달인자는 배열에서 삭제할 원소의 개수를 지정한다. 두 번째 전달인자를 지정하지 않으면 첫 번째 전달인자로 지정한 배열의 시작 위치에서 마지막 원소까지 전부 삭제한다. splice()는 삭제한 배열을 반환하며, 만약 삭제된 원소가 하나도 없다면 빈 배열을 반환한다.
1 2 3 4
var a = [1,2,3,4,5,6,7,8]; a.splice(4); // [5,6,7,8]을 반환, a는 이제 [1,2,3,4] a.splice(1, 2); // [2, 3]을 반환, a는 이제 [1,4] a.splice(1, 1); // [4]를 반환, a는 이제 [1]
세 번째 전달인자부터는 배열에 새롭게 삽입할 원소들을 지정하는데 사용한다. 삽입 작업은 첫 번째 전달인자로 지정된 시작 위치부터 수행한다.
1 2 3
var a = [1,2,3,4,5]; a.splice(2,0, 'a', 'b'); // []를 반환, a는 이제 [1,2,'a','b',3,4,5] apsplice(2,2, [1,2], 3); // ['a', 'b']를 반환, a는 이제 [1,2,[1,2],3,3,4,5]
concat() 메서드와 달리 splice() 메서드는 전달인자로 배열이 전달되면, 그 배열의 원소들을 꺼내어 삽입하지 않고 배열 그 자체를 삽입한다.
7.8.7 push()와 pop()
push()와 pop() 메서드를 사용하면 배열을 마치 스택처럼 조작할 수 있다. (FILO (선입후출) 스택 구현 가능) push() 메서드는 하나 이상의 원소들을 배열의 끝 부분에 이어 붙이고, 배열의 새로운 length 값을 반환한다 pop() 메서드는 배열의 마지막 원소를 제거하고 배열의 length 값을 감소시킨 후, 배열에서 제거한 원소를 반환한다.
7.8.8 unshift()와 shift()
push(), pop()과 매우 유사하게 동작하는데, 배열의 끝이 아니라 배열의 맨 앞에서 원소를 추가하고 제거한다. unshift() 메서드는 하나 혹은 그 이상의 원소들을 배열의 맨 앞에 추가하고, 추가된 원소만큼 공간을 만들기 위해 기존 배열 원소들을 인덱스가 높은 방향으로 옮긴 후, 배열의 새로운 length 값을 반환한다. shift() 메서드는 배열의 첫 번째 원소를 제거한 후, 배열에서 제거한 원소를 반환한다.
7.8.9 toString()
배열의 toString() 메서드는 배열의 모든 원소를 문자열로 변환하고 이 문자열들을 쉼표(,)로 분리한 목록을 반환한다. 별도의 전달인자를 지정하지 않고 join() 메서드를 호출하면 toString()과 동일한 결과를 얻을 수 있다.
ECMAScript 5는 배열을 순회, 매핑, 필터링, 테스팅, 감소, 검색하기위한 아홉 가지 새로운 메서드를 정의한다. 대부분의 메서드들은 첫 번째 전달인자로 함수를 받는다. 이 함수는 배열의 각 원소마다 한 번씩 실행하거나 일부 원소들에 한해 실행된다. 만약 배열이 희소배열이라면, 빈 원소의 경우 함수를 호출하지 않는다. 대부분, 첫 번째 전달인자로 지정한 함수는 세 개의 전달인자를 갖고 호출되는데, 배열 원소의 값과 인덱스, 마지막으로 배열 그 자체다. 첫 인자로 함수를 받는 대부분의 ECMAScript 5의 배열 메서드들은 생략 가능한 두 번째 인자를 받는다. 두 번째 전달인자를 지정하면, 첫 번째 전달인자인 함수는 마치 두 번째 인자의 메서드인 것처럼 호출된다. 두 번째 인자는 첫 번째 전달인자인 함수 안에서 this 키워드의 값으로 사용된다. ECMAScript 5 배열 메서드는 호출 대상 배열을 수정하지 않는다. 메서드의 전달인자로 쓰인 함수 안에서는 배열을 수정할 수 있다.
7.9.1 forEach()
forEach() 메서드는 배열을 순회하는 메서드이다. 첫 번째 인자로 넘긴 함수를 각각의 원소를 대사응로 호출한다. forEach()는 첫 인자로 전달된 함수를 호출할 때 세가지 인자를 넘긴다. 각 인자는 배열의 원소 값과, 원소의 인덱스 값, 그리고 배열 그 자체다. forEach() 메서드는 배열의 모든 원소가 순회되기 전에는 종료도지 않는다. 루프에서 사용하는 break문은 사용할 수 없다. 루프를 중간에 종료시키려면, 예외를 발생시켜야 하고, forEach()는 try 블록 안에서 호출되어야 한다.
7.9.2 map()
map() 메서드는 배열의 각 원소를 메서드의 첫 번째 전달인자로 지정한 함수에 전달하고, 해당 함수의 반환 값을 배열에 담아 반환한다. map() 메서드에 전달한 함수는 forEach()에 전달한 함수와 동일한 형태로 호출되지만 map() 메서드에 인자로 전달된 함수는 반드시 값을 반환해야 한다. map() 메서드는 기존의 배열을 수정하지 않고, 새배열을 반환한다.
1 2
a = [1, 2, 3]; b = a.map(function(x) { return x*x; }); // b는 [1, 4, 9]
7.9.3 filter()
filter() 메서드는 배열의 일부분을 반환한다. 이 메서드에 전달하는 함수는 조건자 함수(항상 true 또는 false 값을 반환하는 함수)여야 한다. filter()의 조건 함수는 forEach()와 map() 메서드와 동일한 형태로 호출된다. 반환값이 true이거나 true로 변환되는 값이면 조건자 함수에 전달된 값은 filter가 반환할 배열에 추가된다.
every()와 some() 메서드는 배열 조건자 함수다. 두 메서드는 인자로 주어진 조건자 함수를 배열에 적용하여, 결과로 true나 false를 반환한다. every() 메서드는 전달인자로 넘긴 함수가 배열의 모든 원소에 대하여 true를 반환하는 경우, every() 메서드는 true를 반환한다. some() 메서드는 전달인자로 넘긴 함수가 배열의 일부 원소에 대해 true를 반환하는 경우, some() 메서드는 true를 반환한다. every()와 some() 메서드는 반환 값이 결정되면 배열의 원소 순회를 중단한다.
7.9.5 reduce()와 reduceRight()
reduce()와 reduceRight() 메서드는 인자로 주어진 함수를 사용하여 배열의 원소들을 하나의 값으로 결합한다. reduce() 메서드는 두 개의 인자를 갖는다. 첫 번째 인자는 배열 원소의 감소 작업을 하는 함수다. 이 감소 함수는 배열 원소 중 두 값을 하나로 결합하면서 크기를 줄이고, 마지막 남은 값을 반환한다. 두 번째 인자는 감소 함수에 전달할 시작 값이다. reduce()에 사용되는 함수는 forEach()와 map()과는 조금 다르다. reduce()에서 사용하는 함수의 첫 번째 인자는 함수를 사용해 계산된 값의 누적된 결과다. 그 초기 값은 reduce()의 두 번째 인자로 전달한 값이다. 이후의 호출에서는 전 단계 함수 호출에서 반환된 값을 함수의 첫 번째 인자로 사용한다. reduceRight() 메서드는 reduce()와 동작은 같지만, 배열의 끝부터 시작해 반대 방향으로 처리한다. 감소 함수의 피연사자들 중 오른쪽 피연산자의 우선순위가 높다면, reduceRight()를 사용해야 한다. reduce()와 reduceRight() 메서드는 감소 함수 호출 시 사용할 this 값을 선택인자로 지정할 수 없다. 선택 초기 값 인자만 지정할 수 있다. 만약 감소 함수를 특정 object의 메서드로 호출하고 싶다면, Function.bind() 메서드를 사용해야 한다.
7.9.6 indexOf()와 lastIndexOf()
indexOf()와 lastIndexOf() 메서드는 배열의 원소 중에서 특정한 값을 찾는다. 값이 존재하면 해당 값의 인덱스를 반환하고, 존재하지 않을 경우에는 -1을 반환한다. indexOf()는 배열의 처음부터 검색하고, lastIndexOf()는 배열의 끝에서부터 검색한다. indexOf()와 lastIndexOf()는 함수를 인자로 받지 않고, 첫 번째 인자에서 배열에서 찾고자 하는 값, 두 번째 인자에서 검색을 시작할 배열 인덱스를 지정할 수 있다.(생략가능)
7.10 배열 타입
ECMAScript 5에서는 Array.isArray()라는 함수를 통해 특정 객체가 배열인지 판단할 수 있다.
length 프로퍼티와 양의 정수 이름의 프로퍼티가 있는 객체는 일종의 배열로 취급할 수 있다. 이를 유사 배열 객체라고 한다. 자바스크립트 배열 메서드는 배열뿐 아니라 유사 배열 객체에도 적용이 가능 하도록 범용 메서드로 구현되었다. 유사 배열은 Array.prototype을 상속받지 않기 때문에, 배열 메서드를 해당 객체의 메서드로 호출할 수는 없지만 Function.call 메서드를 통해서 간접적으로 호출할 수 있다.
1 2
var a = {"0":"a", "1":"b", "2":"c", length:3}; // 유사 배열 객체 Array.prototype.join(a, "+") // => 'a+b+c'
7.12 문자열을 배열처럼 사용하기
문자열은 읽기 전용 배열처럼 동작한다. 문자열의 각 문자는 chatAt() 메서드로 접근할 수도 있지만 대괄호 []를 사용해 접근할 수도 있다. 문자열을 인덱스로 접근함으로써 얻을 수 있는 가장 큰 장점은 charAt() 메서드 호출을 단순하게 []로 대체 함으로써 코드가 전보다 간결해지고, 가독성이 높아지는 것이다. 문자열은 변하지 않는 값이라서, 읽기 전용 배열로만 다룰 수 있다. push(), sort(), reverse(), splice()와 같은 배열 메서드는 배열을 직접 수정하므로 문자열에서는 작동하지 않는다.
표현식(expression)은 자바스크립트 인터프리터가 값으로 평가하는 자바스크립트 구문이다.
4.1 기본 표현식
가장 간단한 형태의 표현식은 ‘기본 표현식’으로, 다른 표현식을 포함하지 않은 독립적 표현식이다. 자바스크립트에서 기본 표현식은 상수나 리터럴 값, 특정 키워드들 그리고 변수 참조를 말한다.
this는 상수가 아니고 프로그램 안에서 위치에 따라 각기 다른 값으로 평가된다. this 키워드는 객체 지향 프로그래밍에서 주로 사용된다. 메서드의 본문 안에서 this는 메서드를 호출한 객체로 평가된다. 자바스크립트에서는 프로그램 안에 존재하는 각 식별자들을 일단 변수라고 가정하고 값을 살펴보는데 해당 식별자를 이름으로 하는 변수가 없다면, 해당 표현식은 undefined 값으로 평가된다. 하지만 ECMAScript 5의 strict 에서는 존재하지 않는 변수를 평가하려고 하면 ReferenceError 예외가 발생한다.
4.2 객체와 배열의 초기화 표현식
객체와 배열 초기화 표현식은 새로 생성된 객체나 배열을 값으로 하는 표현식이다. 일반 리터럴과는 달리, 이들은 기본 표현식이 아니다. 이들 리터럴은 프로퍼티와 원소의 값을 지정하는 수많은 하위 표현식을 포함할 수 있기 때문이다.
4.4 프로퍼티 접근 표현식
두 프로퍼티 접근 표현식 모두 점(.)이나 대괄호([) 왼쪽 표현식이 먼저 평가된다. 만약 평가된 값이 null이나 undefined이면 이들 값은 프로퍼티를 갖지 않기 때문에 표현식은 TypeError 예외를 발생시킨다. 만약 값이 객체(또는 배열)가 아니면 객체로 변환된다. 객체에 해당 프로퍼티가 존재하지 않으면, 프로퍼티 접근 표현식의 값은 undefined가 된다.
‘. 식별자’ 문법이 두 방법 중 좀 더 간단하지만 접근하려는 프로퍼티 이름이 ‘합법적’ 식별자일 때만 사용 가능하고, 프로그램을 작성할 때 그 식별자를 미리 알고 있어야 한다. 만약 프로퍼티 이름이 예약어이거나, 구두점 문자나 공백을 포함, 숫자일 때는 반드시 대괄호를 사용해야 한다. 대괄호는 프로퍼티 이름이 고정되어 있지 않고, 그 이름 자체가 어떤 연산의 결과인 경우에도 사용할 수 있다.
4.5 호출 표현식
값을 반환하기 위해 return문을 사용하면, 그 값이 결국 호출 표현식의 값이 된다. 함수가 값을 반환하지 않는다면, 함수 표현식의 값은 undefined가 된다.
모든 호출 표현식은 한 쌍의 괄호 ()와, 괄호 앞에 오는 표현식으로 이뤄진다. 만약 그 표현식이 프로퍼티 접근 표현식이면, 호출 표현식은 메서드 호출이 된다. 메서드가 호출되면 함수의 몸체가 실행되는 동안 프로퍼티 접근 표현식이 가리키는 객체나 배열이 모두 this의 값이 된다.
메서드 호출이 아닌 호출 표현식은 보통 전역객체를 this 키워드의 값으로 사용한다. 하지만 ECMAScript 5에서는 메서드 호출이 아닌 호출 표현식을 ‘엄격모드’에서 사용할 경우, 전역 객체 대신 undefined가 this의 값이 된다.’
4.6 객체 생성 표현식
객체 생성 표현식은 새 객체를 생성하고 생성자라고 부르는 함수를 호출해 객체에 속한 프로퍼티들을 초기화한다. 객체 생성 표현식이 평가될 때, 자바스크립트 인터프리터는 먼저 새로운 빈 객체를 생성한다. 이때 생성된 객체는 객체 초기자 {}에 의해 생성되는 객체와 동일하다. 다음으로, 주어진 인자들과 함께 생성자를 호출하는데, 이때 방금 생성된 새 객체를 this 키워드의 값으로 설정하여 전달한다. 생성자 함수는 이 this 키워드를 사용해 새로 생성된 객체의 프로퍼티들을 초기화한다.
4.8 산술 표현식
수로 변환 불가능한 피연산자는 NaN 값으로 변환되며, 피연산자중 하나라도 NaN일 경우에는 연산 결과도 NaN이다.
/ 연산자는 첫 번째 피연산자를 두 번째 피연산자로 나눈다. 정수를 정수로 나누면 계산 값이 당연히 정수가 되리라고 예상하겠지만, 자바스크립트에서 모든 숫자는 부동소숫점 숫자로 취급된다. 따라서 모든 나눗셈 연산의 결과 또한 부동소숫점 숫자 값이 된다. 예를 들어 5/2는 2가 아니라 2.5로 평가된다. 값을 0으로 나누면 양의 무한도 또는 음의 무한대 값이 되고, 0/0의 값은 NaN로 평가된다.
% 연산자 결과의 부호는 첫 번째 피연산자의 부호와 동일하다.
4.8.1 덧셈 연산자 +
1 2 3 4 5 6 7
1 + 2// => 3: 덧셈 "1" + "2"// => '12': 이어붙이기 "1" + 2// => '12': 숫자를 문자열로 바꾼 후 이어붙이기 1 + {?} // => "1[object Object]": 객체를 문자열로 바꾼 후 이어붙이기 true + true// => 2: 불리언 값을 숫자로 바꾼 후 더하기 2 + null// => 2: null 값을 0으로 바꾼 후 더하기 2 + undefined// => NaN: undefined를 NaN으로 바꾼 후 더하기
4.8.2 단항 산술 연산자
증가(++)
표현식 ++x는 x=x+1과 항상 같지 않다. ++ 연산자는 절대 문자열 결합을 하지 않고, 항상 피연산자를 숫자로 바꾼 후에 값을 하나 증가시킨다. 예를 들어, x가 문자열 “1”이면 ++x는 숫자 2가 되지만 x+1은 문자열 “11”이 된다.
4.9 관계형 표현식
4.9.1 동치와 부등치 연산자
==와 === 연산자 모두 주어진 두 값이 같은지를 확인하는 데 쓰이지만 같음을 정의하는 기준이 서로 다르다. 두 연산자 모두 피연산자 타입을 가리지 않고, 주어진 피연산자들이 같으면 true, 다르면 false를 반환한다. === 연산자는 일치(엄격한 동치) 연산자로 알려져 있는데, 같음을 정의하는 기준을 매우 엄격하게 정의하여, 두 피연산자가 **’일치’**하는지 확인한다.
자바스크립트는 =(할당), ==(동치), ===(일치) 연산자를 지원하고 있다.
4.9.2 비교 연산자
비교 연산자는 피연산자 타입에 제한이 없다. 하지만 오직 숫자와 문자열만 비교할 수 있기 때문에, 숫자나 문자열이 아닌 피연산자는 먼저 변환된다.
문자열 비교는 대소문자를 구분한다. 모든 ASCII 대문자는 모든 ASCII 소문자보다 작다. 대소문자를 구분하지 않고 문자열을 비교하려면, 우선 String.toLowerCase()나 String.toUpperCase() 메서드를 사용해야 한다.
4.9.3 in 연산자
in 연산자는 좌변의 피연산자로 문자열(또는 문자열로 변환될 수 있는 것)을 받고, 우변의 피연산자로는 객체나 배열을 받는다. 좌변 값이 우변 객체의 프로퍼티 이름에 해당할 경우 연산 결과는 true이다.
1 2 3 4 5 6 7 8
var point = { x:1, y:1 }; // 객체 정의 "x"in point // => true: 프로퍼티 x가 있다. "z"in point // => false: 프로퍼티 z가 없다.. "toString"in point // => true: 상속된 프로퍼티 var data = [7,8,9]; // 원소가 0, 1, 2 위치에 차례로 7, 8, 9 "0"in data // => true: 배열에 0번째 원소가 있기 때문 1in data // => true: 배열에 1번째 원소가 있기 때문 3in data // => false: 배열에 4번째 원소가 없기 때문
4.9.4 instanceof 연산자
instanceof 연산자는 좌변의 피연산자로 객체를, 우변의 피연산자로 객체 클래스의 이름을 받는다. 자바스크립트에서 객체의 클래스는 객체를 초기화하는 생성자 함수로부터 정의된다. 그러므로 instanceof의 우변 피연산자는 함수가 되어야 한다.
1 2 3 4 5 6 7 8
var d = newDate(); // Date() 생성자로 새로운 객체를 생성한다. d instanceofDate; // => true: d는 Date()에 의해 생성되었다. d instanceofObject; // => true: 모든 객체는 Object의 인스턴스. d instanceofNumber; // => false: d는 Number의 객체가 아니다. var a = [1, 2, 3]; // 배열 리터럴 문법으로 새로운 배열을 생성한다. a instanceofArray; // => true: a는 배열이다. a instanceofObject; // => true: 모든 배열은 객체다. a instanceofRegExp; // => false: 배열은 정규 표현식이 아니다.
4.10 논리 표현식
4.10.1 논리 AND (&&)
&& 연산자의 피연산자로 반드시 불리언 값이 올 필요는 없다. 모든 자바스크립트 값은 true 또는 false로 평가될 수 있기 때문이다. 피연산자 모두 true로 평가되는 값이면, && 연산자는 true로 평가되는 값을 반환한다. 하지만 적어도 하나의 피연산자가 false로 평가될 경우에는 false로 평가되는 값을 반환한다.
&& 연산자의 특성을 ‘단축 평가’라고도 부르고 다음과 같이도 사용 가능하다.
1 2
if (a == b) stop(); (a == b) && stop();
&& 연산자는 우변 표현식을 평가할 수도, 하지 않을 수도 있기 때문에 && 우변에 부수 효과가 일어나는 표현식(하당, 증가, 감소, 함수 호출)을 사용할 때는 각별히 주의해야 한다. 이러한 부수 효과가 일어나는 표현식은 && 좌변 값에 따라 실행 여부가 결정되기 때문이다.
4.10.2 논리 OR (||)
|| 연산자는 일반적으로, 다음 코드와 같이 여러 값중에 최초로 true로 평가되는 값을 선택하는 경우에 사용된다.
1 2 3 4
// max_width가 정의되어 있으면 이것을 사용한다. // 이 외의 경우 preference 객체에 속한 값을 찾아본다. // 그것조차 정의되어 있지 않을 경우 하드 코딩된 상수를 사용한다. var max = max_width || preferences.max_width || 500;
4.13 기타 연산자들
4.13.2 typeof 연산자
typeof의 피연산자 값이 null일 때 연산자는 **”object”**를 반환한다. 만약 다른 Object들과 null을 구분하고 싶다면, typeof를 사용하기 보다는 명시적으로 null인지를 테스트해야한다.
typeof 연산자는 함수를 제외한 모든 객체와 배열을 “object”로 평가하기 때문에, 객체를 다른 원시 타입과 구분하는 용도로만 사용할 수 있다. 객체의 클래스를 구분하기 위해서는 instanceof 연산자나 class 속성 또는 constructor 프로퍼티와 같은 다른 수단을 사용해야 한다.
자바스크립트에서 함수는 객체의 일종이지만, typeof 연산자는 함수들이 자신의 반환 값을 가지고 있다는 점 때문에 일반 객체와는 충분히 다르다고 본다. 자바스크립트에서는 함수와 ‘호출 가능한 객체’ 사이에 미묘한 차이점이 있다. 모든 함수는 호출 가능하다. 하지만 실제로는 함수가 아닌데도 함수처럼 호출이 가능한 객체도 있을 수 있다.
ECMAScript 5 표준은 typeof 연산자가 모든 호출 가능한 객체에 대해 일반 개체든 호스트 객체든 관계없이 ‘function’을 반환하도록 명세를 확장했다.
4.13.3 delete 연산자
delete는 단항 연산자이며, 피연산자로 지정된 객체 프로퍼티, 배열 원소 또는 변수의 삭제를 시도한다. delete 연산자는 보통 연산자가 가진 부수 효과(프로퍼티 삭제) 때문에 사용하는 것이며, 연산자가 반환하는 값 때문에 사용하는 것이 아니다. (단, 배열의 원소를 delete 연산자를 사용하여 삭제해도 배열의 길이는 변하지 않는다.)
삭제된 프로퍼티나 배열의 원소는 단순히 undefined 값으로 설정된 것이 아니라는 점을 유의해야한다. 어떤 프로퍼티가 삭제되면 그 프로퍼티는 더 이상 존재하지 않는다. 그런데 존재하지 않는 프로퍼티에 접근하려고 해도 undefined가 반환되므로, 프로퍼티가 객체에 존재하는지 여부를 검사하려면 in 연산자를 쓰면 된다.
그러나 모든 변수나 프로퍼티를 삭제할 수는 없다. var 문으로 선언한 사용자 정의 변수나 내장 코어 프로퍼티, 클라이언트 측 프로퍼티는 삭제할 수 없다. function 문으로 정의한 함수와 함수 매개변수도 삭제할 수 없다.