2015년 5월 6일 수요일

[JavaScript] 자바스크립트 프로토타입(Prototype) 객체 이해하기

프로토타입(Prototype)이란?


프로토타입(Prototype), 이름에서 알 수 있듯 프로토타입이란 객체의 기본형인 객체를 뜻한다. 모든 객체는 프로토타입 객체를 가지며 프로토타입의 프로퍼티에 접근이 가능하다. 또한 이 프로토타입 객체도 객체인만큼 포로토타입 객체를 가진다. 즉 가기장 기본 Object 객체를 제외한 모든 객체들은 프로토타입 객체를 가지며 모든 객체의 최상위 프로토타입은 기본 Object.prototype 객체가 된다. 즉, 모든 객체들은 1개의 객체 인스턴스와 해당 인스턴스의 속성들을 공유하며 상속한다는 말이 된다.

이 프로토타입(Prototype)이 중요한 이유는 OOP의 상속 컨셉을 자바스크립트에서 구현할 수 있게하기 때문이다. 만약 객체가 뭔지 모른다면, 우선 이 글(클릭하기)을 읽고 와주기를 바란다.

우선 객체의 프로토타입은 ECMAScript 에서 지원하는 표준 메소드인 Object.getPrototypeOf() 메소드를 통해 접근할 수 있으며, 이 메소드는 프로토타입을 얻어(get)오는 메소드이므로 이메소드를 통해서 프로토타입을 수정하는것은 불가능하다.

*하지만 곧 ECMAScript 6에서 Object.setPrototypeOf() 메소드가 생긴다.

위의 표준 메소드와 별개로 Chrome, Internet Explorer, Mozilla FireFox 등에서는 __proto__따프로퍼티를 이용해 객체의 프로토타입에 접근할 수 있다.


var ob1 = {};
var ob2 = {};

console.log(ob1.__proto__); //Object
console.log(Object.getPrototypeOf(ob2)); //Object
console.log(ob1.__proto__ == Object.getPrototypeOf(ob2)); //true
//기본으로 생성되는 객체는 모두 같은 Object 프로토타입 객체 인스턴스을 참조한다.

여기서 ob1과 ob2 객체에 프로토타입을 설정해준다면


var protoObj = {
  getFirstName: function() {
    return this.firstName;
  }
}
var ob1 = {};
var ob2 = {};
ob1.__proto__ = protoObj;
ob2.__proto__ = protoObj;

ob1.firstName = "Jason";
ob2.firstName = "Natali";
console.log(ob1.getFirstName()); //Jason
console.log(ob2.getFirstName()); //Natali

protoObj.getFirstName = function(){
  return "Function changed";
};

console.log(ob1.getFirstName()); //Function changed
console.log(ob2.getFirstName()); //Function changed
console.log(ob1.__proto__==ob2.__proto__); //true
console.log(protoObj.__proto__ == Object.prototype); //true

이렇게 다른 언어의 객체 상속과 같은 결과를 낳게 된다. 또한 두 객체 모두 같은 프로토타입 객체 인스턴스를 참조하고 있으므로 주어진 프로토타입 객체의 수정은 해당 프로토타입 객체를 참조하는 모든 객체에 적용된다.

위 객체들의 최종 관계도를 UML 다이어그램으로 나타내자면 아래와 같다.

프로토타입 객체 인스턴스와 객체 인스턴스들의 관계도

앞서 말했듯, 모든 사용자 생성 객체는 생성될 때 Object.prototype 객체를 프로토타입으로 가진다.
그리고 ob1과 ob2는 같은 protoObj 인스턴스를 프로토타임으로 가지니, protoObj 인스턴스에 생기는 변화는 obj1과 obj2에 모두 적용된다.

그리고 Object.prototype에는


Object.prototype.constructor
Object.prototype.hasOwnProperty()
Object.prototype.isPrototypeOf()
Object.prototype.propertyIsEnumerable()
Object.prototype.toLocaleString()
Object.prototype.toString()

와 같이 생성되는 객체에 기본으로 탑재되는 메소드들이 프로퍼티로 저장되 있다.

*참고로 Object.prototype 객체 프로퍼티들의 속성은
Writable : false
Enumerable : false
Configurable : false
이니 어떤 방법을 써서도 코드 프로퍼티 리스트나 프로퍼티 수정이 불가능하다.


그렇다면 Object.prototype에서 Object란 무었일까? 이를 이해하기 위해서는 우선 constructor에 대해 알아야하니 모른다면 이 글(클릭하기)을 읽고 오자.

프로토타입과 생성자 함수(Constructor)


우리가 Constructor 라 부르는 함수들은 function 객체로 여러가지 프로퍼티를 가지는데, 그 중 prototype 이라는 프로퍼티가 존재한다. 이 prototype이라는 프로퍼티는 해당 생성자 함수에 의해 성성되는 객체들에 부여되는 프로토타입 객체를 참조한다.

*참고로 이 prototype 프로퍼티는 함수(Function) 객채의 프로퍼티이고, Object.getPrototypeOf 메소드로 얻어지는 프로토타입 객체는 함수(Function) 객체의 프로토타입이다. 자바스크립트에서는 함수도 객체라는것을 잊지 말자


function Foo(){};

var foo = new Foo();
var too = new Foo();

console.log(foo.__proto__ == Foo.prototype); //true
console.log(foo.__proto__ == Object.getPrototypeOf(Foo)); //false
console.log(Object.getPrototypeOf(Foo) == Function.prototype); //true
console.log(foo.__proto__ == too.__proto__); //true
console.log(Foo == Foo.prototype.constructor); //true

함수 Foo의 프로토타입 객체가 바로 Function.prototype 객체이며, 이는 Foo.prototype 프로퍼티가 참조하는 객체와는 다른 객체이다.

그리고 해당 함수의 prototype 프로퍼티가 참조하는 객체의 constructor 프로퍼티는 해당 함수를 참조한다.
관계도를 그린다면 아래와 같이 된다.

Constructor Foo로 생성되는 객체 인스턴스들과 프로토타입의 관계도

생성자 함수 Foo에 의해 생성된 객체 footoo는 모두 함수 Fooprototype 프로퍼티가 참조하는 객체 Foo.prototype을 프로토타입으로 두고 있다. 즉 Foo.prototype의 수정은 footoo 객체 모두에 영향을 끼치게 된다.

*단, 아래의 경우처럼 프로토타입을 새로이 어싸인(Assign)할 경우에는 해당 객체가 참조하는 프로토타입만이 바뀔 뿐, 원 프로는는토타입이나 같은 프로타입을 가진 다른 객체들은 아무런 영향을 받지 않는다.


function Foo() {};

var foo = new Foo();
var too = new Foo();

var newPrototype = new Object();

foo.__proto__ = newPrototype;

console.log(Foo.prototype == foo.__proto__); //false
console.log(Foo.prototype == too.__proto__); //true
console.log(foo.__proto__ == too.__proto__); //false

위의 코드가 실행되면 관계도는 아래처럼 변한다.

생성된 객체에 새로운 프로토타입 어싸인(Assign) 후 관계도

객체 foo에 새로이 어싸인된 프로토타입은 constructor로 기본 Object 함수를 참조하고 있는데, 이는 newPrototype이 사용자에 의해 생성된, 즉 Object() 생성자 함수를 이용해 생성되었기 때문이다.

저 constructor도 프로퍼티 이기 때문에
foo.__proto__.constructor, 또는 newPrototype 객체 생성시 newPrototype.constructor 프로퍼티를 이용해 원하는 생성자 함수를 어싸인해주면 된다.

Property Lookup(프로퍼티 검색)과 프로퍼티 overriding


어떤 객채이든 프로퍼티 검색은 해당 객체의 본체에서부터 시작된다. 아래의 코드를 보자.


var fooProto = {
  write: function() {
    return "write";
  },
  read: function() {
    return "read";
  }
};

function Foo() {
  this.write = function() {
    return "No write";
  }
}
Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.write()); //No write
console.log(foo.read()); //read

too = new Foo();
too.read = function() {
  return "No read";
}

console.log(too.write()); //No write
console.log(too.read()); //No read

delete too.read; //too.read 프로퍼티 삭제

console.log(too.read()); //read

객체의 프로퍼티 호출이 일어나게 되면 자바스크립트 엔진은 가장 먼져 호출이 된 해당 객체(base object)에서 프로퍼티를 검색한다. 그리고, 만약 찾지 못하면 해당 객체의 프로토타입 객체에서 프로퍼티를 검색하고, 그리고도 찾지못다면 프로토타입의 프로토타입 객체까지 검색을 이어간다. 이 검색은 프로퍼티가 발견되거나, 또는 더 이상 검색을 이어갈 프로토타입이 없을 때(Object.prototype까지 도달할 떄)까지 계속 이어진다. 즉 하위 객체에 해당 프로퍼티가 존재한다면 프로퍼티 Overriding이 가능하다는 것이다.

이 때문에 위의 코드에서도 foo.write() 메소드는 Foo함수에서 정의한대로 실행되고, foo.read() 메소드는 Foo함수 생성자의 프로토타입 프로퍼티가 참조하는 fooProto 객체에서 정의한대로 실행되는 것이다. 이는 too.write(), too.read()의 실행결과에서도 잘 알 수 있다.

또 위의 코드를 보면 too.read 프로퍼티를 삭제하게되면 too.read() 메소드가 프로토타입 객체에서 정의한대로 작동한다. 즉, 프로토타입 객체를 직접 호출하지 않고는 하위 객체에서 프로토타입의 프로퍼티를 수정하는 것이 불가능하다.

Object.prototype.hasOwnProperty() 메소드와 in Operator(연산자)


hasOwnProperty() 메소드는 해당 객체에  파라미터로 주어진 이름을 가진 프로퍼티가 있는지를 확인한다.

앞서 말했듯, 자바스크립트 엔진은 프로퍼티 호출이 발생하면, 해당 객체에서 먼져 프로퍼티를 검색한 후, 찾지 못한다면 프로토타입으로 검색을 이어간다. 이 때문에, 특정 프로퍼티가 호출되어도 개발자들은 해당 프로퍼티가 프로토타입에 속한 프로퍼티인지, 하위 객체에 속한 프로퍼티인지 알 수 없게 되는데, hasOwnProperty() 메소드는 그 점을 해결해주는 메소드 이다.

사용법은 간단하다. 확인을 원하는 객체와 프로퍼티 이름으로 메소드를 호출하면 Boolean(true||false) 값을 리턴한다.

hasOwnProperty()와는 달리 in 연산자는 해당 객체에서 간접적으로 접근이 가능한 모든 프로퍼티에 대해 true 값을 반환단다.


var fooProto = {
  a: "Letter A",
  b: "Letter B",
};

function Foo() {
  this.b = "Letter B";
  this.c = "Letter C";
}

Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.hasOwnProperty("a")); //false
console.log("a" in foo); //true
console.log(foo.hasOwnProperty("b")); //true
console.log("b" in foo); //true
console.log(foo.hasOwnProperty("c")); //true
console.log("c" in foo); //true

Object.prototype.isPrototypeOf() 메소드


isPrototypeOf 메소드는 해당 객체가 파라미터로 주어진 객체의 프로토타입인지를 확인하는 메소드 이다.

물론 Object.getPrototypeOf() 메소드 또는 __proto__ 프로퍼티와 '=' 연산자(equal operand)를 이용해서도 확인이 가능하지만, 이 방법을 대체할 수 있게 해주는 메소드 이다.


var fooProto = {
  a: "Letter A",
  b: "Letter B",
};

function Foo() {
  this.b = "Letter B";
  this.c = "Letter C";
}

Foo.prototype = fooProto;

foo = new Foo();

console.log(foo.isPrototypeOf(fooProto)); //false

//아래 3가지 확인법들은 모두 같은 결과를 반환한다.
console.log(fooProto.isPrototypeOf(foo)); //true
console.log(fooProto == Object.getPrototypeOf(foo)); //true
console.log(fooProto == foo.__proto__); //true


2015년 4월 27일 월요일

[JavaScript] 자바스크립트 생성자(Constructor) 함수와 객체

객체(Object)는 Object Literal Notation을 사용해서 생성할 수도 있을 뿐 아니라, Constructor 라는 함수를 이용해서도 생성이 가능하다. 이 Constructor 함수는 Object.constructor 프로퍼티를 이용해 접근이 가능하며, 우리가 일반적으로 new Object() 또는 Object Literal Notation을 이용해 생성한 객체들은 모두 자바스크립트 내장 함수인 Object() 라는 객체 생성자(Constructor)함수를 이용해 생성되는 것 이다.


var person1 = {
  firstName: "Jason",
  lastName: "Bourne",
  age: 25  
};

var person2 = new Object();

console.log(person1.constructor); //function Object() 
console.log(person2.constructor); //function Object() 
console.log(person1.constructor == person2.constructor); //true

이렇듯 person1 객체나 person2 객체 모두 같은 constructor에 의해 생성된다. 또 number, boolean string, array, regex 등도 각각의 constructor에 의해 생성된다.


var array = [];
var number = new Number(3);
var bool = new Boolean(true);
var str = new String("String");

console.log(array.constructor); //function Array()
console.log(number.constructor); //function Number()
console.log(bool.constructor); //function Boolean()
console.log(str.constructor); //function String()

즉 constructor(생성자)는 함수일 뿐이며, constructor 함수는 언제든지 생성될 수 있다는 뜻 이기도 하다.
예를들어, 위의 person 객체의 생성자 함수를 만든다 하면 아래와 같이 만들 수 있다.


function Person(firstName, lastName, age) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.age = age;
  this.getInfo = function() {
    return "Name : " + firstName + " " + lastName + "\nAge : " + age;
  }
}

var person1 = new Person("Jason", "Bourne", 33);
var person2 = new Person("Jenny", "Laurence", 18);

console.log(person1.constructor); //function Person
console.log(person2.constructor); //function Person

new 키워드없이 생성자(Constructor)함수를 호출하게 되면 this 키워드때문에 윈도우 객체에 변수들이 추가되므로 주의하도록 하자(자세한건 this 키워드 관련 글을 참고하기).

2015년 4월 15일 수요일

[JavaScript] 자바스크립트 객체(Object)의 프로퍼티(Property)와 속성(Property Attribute)

예전에 적었던 자바스크립트 객체 소개글에서 말했던것 처럼, 객체란 여러가지 자료(Data)들과 함수(Function)들의 집합이다. 매우 간단하게 선언할 수 있고 접근될 수 있다. 이 글에서는 객체의 프로퍼티(Property)에 대해 이야기 해볼까 한다.

프로퍼티(Property)란 객체에 속한 데이터(Data)를 뜻한다. 아래의 코드에서는 name, age, occupationperson 객체의 프로퍼티가 되겠다.


var person = {
 name: "Jason",
 age: 25,
 occupation: "Student",
 getPersonProfile: function() {
  return "Name : " + this.name +
   "\nAge : " + this.age +
   "\nOccupation : " + this.occupation;
 }
};
person.newProperty = "newProperty";

console.log(person);
// { name: 'Jason',
//   age: 25,
//   occupation: 'Student',
//   getPersonProfile: [Function],
//   newProperty: 'newProperty' }

delete person.newProperty;

console.log(person);
// { name: 'Jason',
//   age: 25,
//   occupation: 'Student',
//   getPersonProfile: [Function] }

console.log(person.getPersonProfile());
// Name : Jason
// Age : 25
// Occupation : Student


위의 코드를 보면 person 객체 내부에 저 값들이 모두 저장된것 같지만, 사실 name, age, occupation 은 모두 값(Data)들이 저장된 메모리 위치를 가리킬 뿐이다. 그러므로, 프로퍼티에 저장된된 Data의 수정, 변경등은 모두 프로퍼티가 가리키는 메모리 위치에서 이루어 지는 일이다.

우리가 흔히 메소드(Method)라 부르는 항목은, 객체 내부에서 프로퍼티로 선언된 함수(Function)을 뜻하며, 위의 코드에서는 returnPersonProfile 프로퍼티가 바로 메소드 이다.

프로퍼티는 객체 선언시점에서와, 선언된 이후 시점 모두에서 추가가 가능하며, delete 키워드를 이용해서 삭제가 가능하다.

프로퍼티는 named data property, named accessor property 2가지 종류로 나뉘지며 named data propertynamed accessor property든, 프로퍼티들은 총 4가지의 속성(Attribute)으로 이루어지는데, 이러한 속성(Attribute)들은 특정 조건에서 프로퍼티의 행동양식(Behavior)을 저장하며, 또한 프로퍼티의 값(Value)를 저장한다.

Data Property와 Accessor Property는 2개의 같은 속성들(Configurable, Enumerable)과 2개의 다른 다른 속성들([Writable, Value], [Get, Set])로 구성된다

Named Data Property(데이터 프로퍼티)


저장된 Data 값에 관련된 프로퍼티이며 Value를 제외한 모든 속성(Attribute)들이 true||false, boolean 값으로 이루어져 있다. 위의 Person 객체에서는 name, age, occupation 프로퍼티들이 데이터 프로퍼티에 속한다
  • Configurable
    • 해당 프로퍼티가 delete 키워드 등으로 재정의(Redefine)될 수 있음을 가리킨다
    • 객체 프로퍼티 생성시 기본값으로 true 값을 가지며 false로 변경할 경우,  해당 프로퍼티 삭제되지 못하며, 이는 해당 프로퍼티의 속성(Attribute)에도 적용된다.
    • 객체 선언시 정의되지 않은 프로퍼티 같은경우에는 기본으로 false 값을 가진다.
  • Enumerable
    • 객체가 for-in 루프를 통과할시, 해당 프로퍼티가 for-in 루프에서 반환되어야(읽혀져야) 할지를 가리킨다.
    • 기본값으로는 true 값을 가진다.
    • 객체 선언시 정의되지 않은 프로퍼티 같은경우에는 기본으로 false 값을 가진다.
  • Writable
    • 프로퍼티의 Data 값이 수정될 수 있음을 가리킨다.
    • 기본값으로는 true 값을 가진다.
  • Value
    • 프로퍼티의 Data 값이다.
    • Value가 정해지지 않은경우에는 undefined값을 가진다.
    • 위의 Person 객체의 name 프로퍼티의 value 값은 "Jason"이다.

Named Accessor Property(접근 프로퍼티)


접근 속성은 프로퍼티의 데이터 값(Value)에 대한 접근에 관련된 속성이다. 데이터를 읽어(Retrieve)오는 Get 속성과 데이터를 저장하는 Set 속성을 가지며, 프로퍼티를 읽어오고 수정하는 기능을 한다. 위의 Person 객체에서는 getPersonProfile 메소드가 바로 Get의 역할을 한다 할 수 있다.

  • Configurable
    • 해당 프로퍼티가 delete 키워드 등으로 재정의(Redefine)될 수 있음을 가리킨다
    • 프로퍼티 생성시 기본값으로 true 값을 가지며 false로 변경할 경우,  해당 프로퍼티 삭제되지 못하며, 이는 해당 프로퍼티의 속성(Attribute)에도 적용된다.
    • 객체 선언시 정의되지 않은 프로퍼티 같은경우에는 기본으로 false 값을 가진다.
  • Enumerable
    • 객체가 for-in 루프를 통과할시, 해당 프로퍼티가 for-in 루프에서 반환되어야(읽혀져야) 할지를 가리킨다.
    • 기본값으로는 true 값을 가진다.
    • 객체 선언시 정의되지 않은 프로퍼티 같은경우에는 기본으로 false 값을 가진다.
  • Get
    • 프로퍼티가 읽어질 때, 호출되는 함수를 정한다. 이 속성을 통해 프로퍼티가 읽어져 반환되기 전에 어떠한 과정을 수행할지를 정할 수 있다.
    • Get 속성에 어싸인된 함수는 Parameter를 받을 수가 없다.
    • 기본값은 undefined.
  • Set
    • 프로퍼티가 수정될 때, 호출되는 함수를 정한다. 이 속성을 통해 프로퍼티가 수정되기 전에 어떠한 과정을 수행할지를 정할 수 있다.
    • Set 속성에 어싸인된 함수는 2개 이상의 Parameter를 받을 수가 없다.
    • 기본값은 undefined.

이러한 속성을 이용해서 아래와 같이 getPersonProfile 메소드를 대체할 수 있다.


var person = {
  name: "Jason",
  age: 25,
  occupation: "Student",
};

Object.defineProperty(person, "profile", {
  get: function() {
    return "Name : " + this.name +
      "\nAge : " + this.age +
      "\nOccupation : " + this.occupation;;
  }
});

console.log(person.profile);
// Name : Jason
// Age : 25
// Occupation : Student


프로퍼티(Property) 속성(Attribute) 정의하기


프로퍼티의 속성은
  • Object.defineProperty()
  • Object.defineProperties()
두가지 메소드를 이용해서 정의가 가능하다. 두 메소드의 차이는 단일 프로퍼티를 정의하는지(defineProperty), 여러가지 프로퍼티를 정의하는지(defineProperties)이다.

Object.defineProperty()

Object.defineProperty() 메소드는 프로퍼티를 소유한 객체, 프로퍼티의 이름, 프로퍼티 속성(Attribute) 정의객체(Descriptor Object), 이렇게 3개의 매개변수(Parameter)를 받는다.

사용법은 아래와 같다.


var person = {
  name: "Jason",
  age: 25,
  occupation: "Student",
  returnPersonProfile: function() {
    return "Name : " + this.name +
      "\nAge : " + this.age +
      "\nOccupation : " + this.occupation;
  }
};

//"name" 프로퍼티 속성을 정의한다
Object.defineProperty(person, "name", {
  configurable: true,
  enumerable: false,
  writable: false,
  value: "Laura"
});

console.log(person.name); //값이 Jason에서 Laura로 변경됬다

//프로퍼티 정의 객체 이기때문에 이렇게 객체를 선언해 사용할 수도 있다
var propertyDescriptor = {
  configurable: false,
  enumerable: false,
  writable: false,
  value: "New Property Value"
};

Object.defineProperty(person, "newProperty", propertyDescriptor);

//프로퍼티를 원하는 속성으로 생성할 수도 있다.
console.log(person.newProperty);

// configurable 을 false로 설정했기 때문에 아래 코드는 에러를 발생시킨다.
delete person.newProperty;
Object.defineProperty(person, "newProperty", {
  enumerable : true
});


Object.defineProperties()


defineProperties()가 여러개의 프로퍼티를 한꺼번에 정의한다. 매개변수로는 프로퍼티를 정의할 객체, 그리고 프로퍼티들의 속성 정의객체.

사용법은 아래와 같다.


var person = {
  name: "Jason",
  age: 25,
  occupation: "Student",
};

Object.defineProperties(person, {
  name: {
    configurable: false,
    enumerable: true,
    writable: false,
    value: "Laura"
  },

  profile: {
    get: function() {
      return "Name : " + this.name +
        "\nAge : " + this.age +
        "\nOccupation : " + this.occupation;;
    }
  }
});

console.log(person.profile);
// Name : Laura
// Age : 25
// Occupation : Student


프로퍼티 속성 읽어오기


프로터티의 속성은 Object.getOwnPropertyDescriptor() 메소드를 이용해 읽어올 수 있다. 매개변수로는 프로퍼티가 속한 객체와, 프로퍼티의 이름을 받는다.


var person = {
  name: "Jason",
  age: 25,
  occupation: "Student",
};

Object.defineProperty(person, "profile", {
  get: function() {
    return "Name : " + this.name +
      "\nAge : " + this.age +
      "\nOccupation : " + this.occupation;;
  }
});

var propertyDescriptor_name = Object.getOwnPropertyDescriptor(person, "name");

console.log(propertyDescriptor_name);
// { value: 'Jason',
//   writable: true,
//   enumerable: true,
//   configurable: true }

var propertyDescriptor_age = Object.getOwnPropertyDescriptor(person, "age");
console.log(propertyDescriptor_age);
// { value: 25,
//   writable: true,
//   enumerable: true,
//   configurable: true }


var propertyDescriptor_occupation = Object.getOwnPropertyDescriptor(person, "occupation");
console.log(propertyDescriptor_occupation);
// { value: 'Student',
//   writable: true,
//   enumerable: true,
//   configurable: true }

var propertyDescriptor_Profile = Object.getOwnPropertyDescriptor(person, "profile");
console.log(propertyDescriptor_Profile);
// { get: [Function],
//   set: undefined,
//   enumerable: false,
//   configurable: false }
// 객체 선언시 내부에서 정의된 프로퍼티가 아니기 때문에 configurable 과 enumerable 값이 모두 false 이다.

2015년 4월 2일 목요일

[JavaScript] 자바스크립트 this 키워드의 모든것

this 키워드를 이해하기 위해서는 Execution Context를 이해해야하니, 혹시 Execution Context를 모른다면 이 글(클릭하기)을 읽어 봐 주길 바란다.

자바스크립트에서는 매번 함수의 호출마다 새로운 Execution Context가 생성된다. 이 Execution Context는 Lexical Environment, Variable Environment, 그리고 ThisBinding에 대한 정보를 가지고 있는데, 바로 this 키워드가 이 이 thisbinding이라고 할 수 있겠다. this값은 해당 함수가 현재 어느 Execution Context에서 구동하고 있는지를 알려준다.

Context는 함수 호출에 의해 새로이 생성되며, thisBinding 값은 함수 호출시점에 정해지게 된다. 정확히 말하자면 ECMAScript 내부에서 Function call 발생할 때, 함수의 Arguments List와 thisArg라 불리는 두 객체를 전달하게 되는데, ECMAScript® Language Specification 10.4.3에 따르면 여기서 아래의 과정을 거친후 thisBinding의 값이 결정 된다.
  1. 만약 함수가 strict mode에서 정의된 함수인지를 확인한 후, thisBinding 값은 전달된 thisArg값이 된다.
  2. (strict mode에서 정의된 함수가 아니고) thisArg 값이 null 이나 undefined라면면,  thisBinding 값은 global object가 된다.
  3. 전달된 Type(thisArg) 연산으로 얻어진 thisArg 값의 Typeobject(객체)가 아니라면, thisBinding 값은 toObject(thisArg) 오퍼레이션을 통해 객체화된 객체가 된다.
    • Type() 함수는 ECMA스크립트 내부 함수로, 주어진 변수의 Type을 연산합니다.
    • toObjecT() 함수는 주어진 변수를 Boolean, Number, String, Object 형으로 변환 합니다
  4. 위 모든 과정에서 thisBinding 값이 정해지지 않았다면, 전달된 thisArgthisBinding이 된다.

참고로 funciton.prototype.call, function.prototype.apply 와 같은 this 값을 수정 가능케 하는 기능들은 모두 thisArg 값을 사용자가 전달할 수 있게 함으로써 가능한 기능들이다.

그렇다면, 일반적으로는 이 thisArg 값이 어떻게 결정 되는 것 일까? 이는 함수 호출(Function Call) 과정을 설명한 ECMAScript® Language Specification 11.2.3 에 잘 정의 되어 있다 - 6번과 7번에 주목하자.

*우선 이전에 몇가지 알아야 될것들이 있다

  • 함수가 저장된 주소인 reference 값은 base value, referenced name, strict reference flag, 이 3가지 요소로 이루어져 있다
    • referenced namestring 값으로 저장된 function identifier이다
    • strict reference flagboolean 값으로, strict mode 에서 실행되는 코드인지를 표시한다
    • base valuereference가 속한 context object를 가리킨다

thisArg값은 함수 호출(Function Call)의 6번째 과정에서에서 결정되며, 함수 호출의 전체 과정은 아래와 같다.

  1. 함수 객체의 이름(MemberExpression)을 해석해서 함수가 저장된 레퍼런스 값인 ref를 얻는다.
  2. GetValue(ref) 오퍼레이션을 이용, 함수 func을 불러온다.
  3. 주어진 arguments 들로 argList를 생성한다.
  4. Type(func) 오퍼레이션을 이용, 함수 func의 타입을 확인한후, 객체타입이 아니라면 TypeError Exception 을 발생시킨다.
  5. IsCallable(func) 오퍼레이션을이용, 함수 func의 호출이 가능한지를 확인한 후, 불가능하다면 TypeError Exception을 발생시킨다.
  6. Type(ref)를 이용, ref의 타입을 확인한 후, reference 타입이라면
    1. IsPropertyReference(ref) 오퍼레이션을 이용, ref가 object 또는 primitive wrapper object(String, Boolean, Number 등의 객체)에 속한 property 라면 thisArg값은 getBase(ref) 오퍼레이션에서 얻어진 해당 객체 인스턴스가 된다.
    2. 그 이외의 경우에는 ref가 속한 Environment Record(Variable Environment Object)라면 getBase(ref)에서 얻어진 값의 ImplicitThisValue()가 바로 thisArg가 된다.
  7. 만약 Type(ref)를 이용해 확인한 ref의 타입이 reference 가 아니라면, thisArg값은 undefined가 된다.
  8. Call 오퍼레이션을 이용, thisArg 값과 argList를 전달해 함수를 호출한 후 그 결과값을 리턴한다.

결국 각각의 경우에 따라 thisBinding의 값이 달라지니 좀 헷깔린다. 그래서 자주 보게되는 상황들을 Case By Case 로 설명을 해보려 한다.

* 참고로 this 키워드는 객체이기 대문에 '.'(Dot Notation) 또는 '[', ']'(Bracket Notation) 를 이용해서도 this 객체의 변수나 함수를 호출 할 수 있다.
* 위 과정을 읽었으면 알겠지만 strict 모드에서는 함수 호출시 thisArg값은 non-strict mode 일때와는 다르다.


1. Declaration environment record에 존재하는 함수 호출(Function Call)


  • 위의 6-2번 과정이다. ImplicitThisValue() 는 모든 declaration에 대해 undefined를 리턴한다
  • 이 때문에 this 키워드는 strict mode에서는 undefined 값을, non-strict mode에서는 global object(window object)값을 참조하게 된다.


function invokeFunction() {
    console.log("invokeFunction");
    console.log(this);//Window
    demo1();
    function demo1() {
 console.log("demo1");
 console.log(this);//Window

 var demo2 = function() {
     console.log("demo2");
     console.log(this);//Window

     var demo3 = function() {
  console.log("demo3");
  console.log(this);//Window
     };
     demo3();
 };
 demo2();
    }
}
console.log(this);//Window
invokeFunction();


2. new 키워드를 이용해 객체(object)로 인스턴스화 될 때, this 값은 언제나 인스턴스화된 객체를 참조한다.

  • 위 과정의 6-1번에 속하는 경우이다. 호출된 함수가 객체의 property인 경우이다


function thisObject(name, property) {
    this.name = name;
    this.property = property

    this.thisValue = this;

    this.f = function() {
 console.log("this in function f \n" + this); //object
    }
}

var demo = new thisObject("testObject", 0);
console.log("demo.thisvalue is " + demo.thisValue); //object demo


3. 객체의 메소드로써 호출될 때, this 값은 언제나 인스턴스화된 객체를 참조한다.


  • 메소드란 객체의 프로퍼티(property)로 존재하는 함수를 한다.
  • 이 경우 또한 위 과정의 6-1번의 경우가 되겠다.


var a = {
    test : function() {
 return this;
    }
};

a.test();// object 'a'

var b = {};
b.test = a.test;

b.test()// object 'b'


4. function.prototype.call 또는 function.prototype.apply 메소드를 이용해 호출될 때


  • thisArg의 값은 사용자에 의해 주어진다.


function test() {
    return this;
}
test();// windows

var a = {};
test.apply(a);// object 'a'

test.call(a);// object 'a'

2015년 3월 10일 화요일

[JavaScript] 자바스크립트 Array(배열)

자바스크립트는 여러가지 built-in Object를 내장하고 있는데, 그중 Array(배열) 개체은 사실상 가장 기초적이며 가장 다양하게 쓰이는 개체일 것 이다.
같은 타입(Type)의 자료를 저장해야하는 타 프로그래밍 언어와는 달리, 자바스크립트(ECMAScript)는 배열에 다른 자료형들을 저장하는게 가능하다. 즉, Array(배열) 1개에, boolean 값, string 값, number 값, 등을 모두 저장할 수 있다는 말이다. Array 자료형은 총 4294967295개의 데이터를 저장할 수 있다. 만약 이 이상을 저장하려 한다면 에러가 발생하게 될 것이다.

Array(배열)은 2가지 방식으로 선언될 수 있다.

// Array Constructor 를 이용하는 방법

var arrayConstructorDemo1 = new Array(); // empty Array를 생성
var arrayConstructorDemo2 = new Array(3); // 3의 크기를 가진 Array를 생성
var arrayConstructorDemo3 = new Array("test", true, 33); // 원하는 데이터로 생성

// Array Literal Notation을 사용하는 방법
var arrayLiteralDemo1 = []; // empty Array를 생성한다.
var arrayLiteralDemo2 = [ "test", true, 33 ];
// "test", true, 33 3개의 데이터를 가진
// 어레이를 생성 ArrayConstructorDemo3와 같은 결과

// 아래의 Array Literal Notation 사용은 작동하나, 브라우져 환경에 따라 에러를 생성할 수 있으니, 사용을 자제하도록하자.

var arrayLiteralDemo3 = [ 1, 2, 3, ]; 
// 1,2,3값을 가진 크기 3 또는 4(네번째는 undefined 값)의 배열을 생성한다.

var arrayLiteralDemo4 = [ , , , ]; 
// 3개 또는 4개의 undefined 값을 가진 array를 생성한다

Array(배열)의 선언 후에 내부 값에 접근(읽기/쓰기)하기 위해서는 [,] 사각괄호안에 접근하고 싶은 값의 index를 넣으면 된다.
참고로 배열의 index는 언제나 0 부터 시작하며, 배열의 크기는 length프로퍼티에 접근하여 알 수 있다.


var arrayConstructorDemo = new Array("test", true, 33);

console.log(arrayConstructorDemo[0]); //"test"
console.log(arrayConstructorDemo[1]); //true
console.log(arrayConstructorDemo[2]); //33

//같은 방식으로 값을 변경할 수 있다.

arrayConstructorDemo[0] = "complete";
arrayConstructorDemo[1] = 13;
arrayConstructorDemo[2] = false;

console.log(arrayConstructorDemo[0]); // complete
console.log(arrayConstructorDemo[1]); // 13
console.log(arrayConstructorDemo[2]); // false

console.log(arrayConstructorDemo.length);// 배열의 크기 확인

//배열의 new index에 새로운 값을 삽입한다.
arrayConstructorDemo[4] = "new Index"; // index 4에 새로운 값을 삽입한다
console.log(arrayConstructorDemo[4]); // new Index
console.log(arrayConstructorDemo.length);// 5

//배열의 크기를 2로 줄여보자.
arrayConstructorDemo.length = 2;
console.log(arrayConstructorDemo[4]); // undefined
console.log(arrayConstructorDemo[2]); // undefined

//다시 배열의 크기를 5로 늘릴경우
arrayConstructorDemo.length = 5;
console.log(arrayConstructorDemo[4]); // undefined
console.log(arrayConstructorDemo[3]); // undefined
console.log(arrayConstructorDemo[2]); // undefined

Array의 크기는 매우 동적이며, 만약 위의 코드처럼, 사이즈 3의 배열에 존재하지 않는 인덱스인 4에 새로운 값을 삽입하려 한다면, 자바스크립트 엔진은 자동으로 배열의 크기를 새로운 index에 알맞게 증가 시키고, 값을 삽입하게 된다.

length 프로퍼티는 read-only가 아니기 때문에, array의 크기를 조절하는데도 사용이 가능하다.
배열의 크기를 줄일 경우에는, 줄어든 배열에 포함되지 않는 index 값에 접근하게되면 언제나 undefined 값을 반환하게 된다.

배열의 크기를 늘리게 될 경우에도 마찬가지로 배열에 새로들어오게된 index들은 undefined 값을 가지게 된다.

Array.isArray() 메소드


Array.isArray() 메소드는 괄호안에 주어진 변수가 Array 개체인지를 확인하는 메소드 이다. 이 메소드는 ECMAScript 5에서부터 적용되기 시작했다. 이 메소드가 적용된 가장 큰 이유는 instanceof 연산자가 가지는 한계 때문이였는데, 1개의 global context를 전제하는 instanceof 연산자는 대부분 multi-frame DOM 환경에서 제대로 작동하지 않는다는 한계를 가지고 있었다. 이 때문에 총 2개의 global context를 가지는 경우에는, instanceof 연산자로 다른 global context의 Array 개채를 계산하게 되면, false 값을 얻게 된다. 이 한계를 넘기 위해 나온게 바로 Array.isArray 메소드 이다.

아래와 같이 사용할 수 있다.


if (Array.isArray(arrayConstructorDemo)) {
    console.log("This is Array");
}

Stack & Queue


자바스크립트 Array(배열)은 StackQueue 설정을 제공한다. 즉, 기본적으로 제공되는 메소드들로 배열을 Stack 또는 Queue로 사용하는게 가능하다. Stack 과 Queue는 LIFO, FIFO로 표현되는 자료 구조로, 그 컨셉을 자세히 알고 싶으면 앞의 링크를 클릭 해보도록 하자.

Stack

push 메소드를 이용해서 새로운 데이터를 삽입해 array 의 크기값을 반환하고, pop 메소드를 이용해서 가장 최근에 삽입된 데이터를 빼내온다.


var arrayStackDemo = new Array();

var size = arrayStackDemo.push("Demo1", "Demo2");

console.log(size);// 2

size = arrayStackDemo.push("Demo3");

console.log(size);// 3

var topStack = arrayStackDemo.pop();

console.log(topStack); // Demo3

console.log(arrayStackDemo.length); // 2


Queue

Stack과 같이 push 메소드를 이용해 배열의 끝에 새 데이터를 삽입한다.
원래 Queue의 값을 얻어오는 메소드도 이름이 "pop"이나 이미 Stack을 구현하는데 사용되버렸기 때문에 "shift"메소드를 이용해 배열에 값을 삽입힌다.
또한, "unshift" 메소드를 이용해 배열의 처음에 새 데이터를 삽입할 수도 있다


var arrayQueueDemo = new Array("Demo0");

var size = arrayQueueDemo.push("Demo1", "Demo2");

console.log(size);// 3

size = arrayQueueDemo.push("Demo3");

console.log(size);// 4

var firstQueue = arrayQueueDemo.shift();

console.log(firstQueue); // Demo0

console.log(arrayQueueDemo.length); // 3

size = arrayQueueDemo.unshift("newValue");

console.log(size);// 4

firstQueue = arrayQueueDemo.shift();

console.log(firstQueue); // newValue

Sort & Reverse


자바스크립트의 Array 개체는 기본적으로 배열을 재정렬할 수 있는 메소드들을 제공한다.

Reverse

Reverse 메소드는 이름에서도 알 수 있듯이, 배열을 뒤짚는 메소드 이다.


var a = [ 0, 1, 2, 3, 4, 5 ];
a.reverse();

console.log(a);// [ 5, 4, 3, 2, 1, 0 ]

Sort

Sort 메소드는 Array를 정렬한다. 하지만 기본적으로 Sort 메소드는 배열의 개체들이 String 타입이라 전제하에 정렬하기 때문에, 수의 경우에도 마찬가지로 문자열처럼 [1, 12, 13, 2] 정렬된다.


var a = [ 0, 1, 12, 13, 2 ];
a.sort();

console.log(a);// [ 0, 1, 12, 13, 2 ]

이를 해결하기 위해서 sort메소드의 파라미터로 비교함수를 넣어 줄 수 있다.
사용법은 아래와 같다.


function comparator(a, b) {
    if (a > b) {
 return 1;
    } else
 (a < b)
    {
 return -1;
    }
    return 0;
}

var a = [ 0, 1, 12, 13, 2, 13 ];
a.sort(comparator);

console.log(a);// [ 0, 1, 12, 13, 13, 2 ]

Concat


각기 다른 여러개의 Array를 합쳐 새로운 Array 개체를 생성한다


var a = [ "a", "b", "c" ];
var b = [ "d", "e", "f" ];

var c = a.concat(b, "g", [ "e", "f", "g" ])

console.log(a);// [ 'a', 'b', 'c' ]
console.log(b);// [ 'd', 'e', 'f' ]
console.log(c);// [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'e', 'f', 'g' ]

Splice


Array 개체에서 주어진 index의 데이터들을 가져와 새로운 Array를 생성한다. 새로운 Array를 생성하며 추가적으로 원래 Array에 새로운에에 데이터를 삽입할 수도 있다. 즉 원래 Array에서는 데이터를 삭제하며, 새로운 삭제된 데이터들로 새로운 Array를 생성한다.


array.splice(start, deletecount, items)

start에 삭제할 범위의 첫번째 index
deletecount에는 첫번째 index인 start를 포함해 삭제할 엘리맨트의 갯수
items에는 삭제후 삭제된 index에 삽입할 데이터 어레이, 또는 데이터.

var a = [ "a", "b", "c" ];
var b = [ "d", "e", "f" ];

var c = a.splice(0, 2);
b.splice(0, 2)

console.log(a);// [ 'c' ]
console.log(b);// [ 'f' ]
console.log(c);// [ 'a', 'b' ]

c.splice(0, 0, "new VAL");

console.log(c);// [ 'new VAL', 'a', 'b' ]

indexOf & lastIndexOf


indexOf와 lastIndexOf 메소드는 둘다 array에서 주어진 데이터를 검색 후, 그 데이터가 저장된 위치, 즉 index를 리턴한다. 아무것도 찾을 수 없을시에는 -1값을 리턴한다.

*indexOf는 배열의 처음(0)부터 검색하지만, lastIndexOf는 배열의 끝(length-1)부터 검색하게 된다.


var a = [ "a", "b", "c", "a" ];

console.log(a.indexOf("a"));// 0
console.log(a.lastIndexOf("a"));// 3
console.log(a.indexOf("t"));// -1

반복문(Iterative Method)


자바스크립의 Array는 기본적으로 5가지 함수 반복문을 제공한다.

  • foreach()
    • Array의 모든 엘리멘트를 파라미터로 주어진 함수에 따라 계산한다. 하지만 어떠한 값도 반환하지 않는다
  • every()
    • Array의 모든 엘리멘트를 파라미터로 주어진 함수에 따라 계산한다. 만약 모든 계산에서 true 값이 나왔다면 true 값을 리턴한다
  • some()
    • Array의 모든 엘리멘트를 파라미터로 주어진 함수에 따라 계산한다. 그 중 1개의  계산에서 true 값이 나왔다면 true 값을 리턴한다
  • filter()
    • Array의 모든 엘리멘트를 파라미터로 주어진 함수에 따라 계산한다. 계산에서 true 값을 리턴하는 엘리멘트의 배열을 리턴한다
  • map()
    • Array의 모든 엘리멘트를 파라미터로 주어진 함수에 따라 계산한다. 각각 계산의 결과값으로 만들어진 배열을 리턴한다

파라미터로 주어지는 함수의 시그니쳐는 언제나


function name(item(데이터값), index(인덱스), array(배열))

이며 사용법은 아래와 같다.


function func(item, index, array) {
    if (item % 2 == 0) {
 return true;
    }
    return false;
}

var a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
console.log(a);// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

var b = a.every(func);
console.log(b);// false

var c = a.filter(func);
console.log(c); // [ 2, 4, 6, 8, 10 ]

var d = a.some(func);
console.log(d);// true

var e = a.map(func);
console.log(e);// [ false, true, false, true, false, true, false, true, false, true ]

var f = a.forEach(func);
console.log(f);// undefined
//아무것도 반환하지 않기 때문에 f의 값은 undefined 이다.


Reduce & ReduceRight


Reduce 메소드란 ECMAScript 5에서 새로 추가된 기능으로 배열의 모든 데이터에 대해 파라미터로 주어진 함수를 반복해, 최종값을 얻어내는 메소드 이다.
Reduce 메소드는 Array의 처음부터 함수를 시작하며, ReduceRight 메소드는 Array의 끝부터 메소드를 시작한다.
Reduce 와 ReduceRight 메소드 둘 다 파라미터로 받는 함수는 다음과 같은 Function Signature를 가진다.


function func(prev, cur, index, array)
//prev 는 바로 전 반복문까지의 최종 값이며, cur 은 현재 반복문의 순서에 있는 배열내의 데이터값이다.
//index 는 현재 데이터값의 배열 인덱스를 의미하며, array는 현재 배열 개체를 의미한다.

사용법은 아래와 같다.


function func(prev, cur, index, array) {
    if (cur % 2 == 0) {
 return prev - cur;
    }
    return prev + cur;
}

var a = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
console.log(a.reduce(func));// -5
// 위 reduce메소드의 계산과정은 아래와 같다.
// 1 - 2 - 1 + 3 + 2 - 4 - 2 + 5 + 3 - 6 - 3 + 7 + 4 - 8 - 4 + 9 + 5 - 10

console.log(a.reduceRight(func));// 15
// 위 reduceRight메소드의 계산과정은 아래와 같다.
// 10 + 9 + 19 - 8 + 11 + 7 + 18 - 6 + 12 + 5 + 17 - 4 + 13 + 3 + 16 - 2 + 14 + 1