JavaScript : 객체 ( Object ) 추가 정리 + ES6 클래스

Duboo·2022년 1월 4일
0

자바스크립트

목록 보기
6/7
post-thumbnail

자바스크립트에서 객체란?

자바스크립트는 객체 기반의 스크립트 언어이며 자바스크립트를 이루고 있는 대부분이 객체라고 한다.
여기서 거의 대부분이라는 것들은 이전에 설명한 원시 타입을 제외한 나머지들은 전부 객체라는 것이다.

일전에 자바스크립트의 객체는 키(key)와 값(value) 쌍으로 구성된 프로퍼티(property)들의 집합이라고 설명한바 있는데,
이 프로퍼티의 값으로 어떠한 값도 들어갈 수 있다. 여기서 이 어떠한 값에는 함수도 포함된다. 이 경우를 메서드라고 부른다.
자바스크립트에서의 함수는 1급 객체이기 때문에 가능하다. - 객체의 값으로 함수가 오는 경우는 메서드

1급 객체

다른 함수에 인자로 넘길 수 있다
리턴 값으로 함수를 쓸 수 있다.
변수에 함수를 넣을 수 있다.

이러한 프로퍼티는 프로퍼티 키(key)를 이용해서 식별할 수 있다.
즉, 프로퍼티의 키는 프로퍼티를 식별하기 위한 식별자 역할을 한다.
프로퍼티 키에 문자열이나 symbol 값 이외의 값을 지정하면 암묵적으로 문자열로 변환이 된다.
이미 있는 키를 중복 선언하면 뒤에 선언한 프로퍼티가 먼저 선언한 프로퍼티를 덮어쓴다.

정리
프로퍼티 키 : 빈 문자열을 포함하는 모든 문자열 또는 symbol 값을 넣을 수 있고 암묵적인 문자열 변환이 가능
프로퍼티 값 : 함수를 포함한 자바스크립트의 모든 값을 넣을 수 있다.

객체를 생성하는 다양한 방법

객체 리터럴

const obj = {}; // 빈객체 생성 방법

const person = {
  name: 'hyun',  //<- 키를 문자열로 만들지 않아도 자동으로 문자열로 타입 변환된다.
  age: '7',
  sayHi: function() {  // <- 객체 안에 함수가 있다? 이런 경우를 메서드라 부름
    console.log(`Hi ${this.name}`);
  } 
};

가장 기본적인 방법인 객체 리터럴 방식

간단하면서도 객체 안에 들어가 있는 값을 한 번에 보기가 편하다.

Object 생성자 함수

const person = new Object();  // <- 빈객체를 생성해준다. Object 생성자 함수이다.

// 이제 위의 person의 빈객체에 밑의 방법으로 추가해줄 수 있다

person.name = 'hyun'; // <- person 이라는 빈 객체의 키(key)로 name을 만들어 주고 
                      // <- person 빈객체에 name의 키의 값으로 'hyun'을 넣어준다.
person.age = 7;       // <- 같은 방법으로 다른 키와 값 프로퍼티를 넣어주고
person.sayHi = function() {  // 같은 방법으로 메소드를 추가해줄 수 있다.
  console.log(`Hi ${this.name}`);
}

console.log(person); // {name: "hyun", age: 7, sayHi: ƒ}

Object 생성자 함수를 사용해서 빈 객체를 만들어 주고 그 빈 객체 안을 따로 채워줬다.

왜 쓰는지 모르겠다. 이럴 거면 객체 리터럴 방식으로 한 번에 채워주는 게 더 좋아 보인다.

사실 객체 리터럴 방식이 위의 Object 생성자 함수로 객체를 생성하는 것을 단순화시킨 축약 표현이라고 한다.

즉, 자바스크립트 엔진이 객체 리터럴 방식으로 객체를 생성하면 자바스크립트 엔진이 알아서 Object 생성자 함수를

사용해준다고 한다. 고맙다.

생성자 함수

만약 같은 프로퍼티의 키를 가지고 있는데 프로퍼티의 값만 다르다고 해보자.

const person1 = {  // 값만 다르다
  name: 'hyun',
  age: 7        
  sayHi: function() {
    console.log(`Hi ${this.name}`);
  }
};

const person2 = {  // 값만 다르다
  name: 'yoon',  
  age: 9    
  sayHi: function() {
    console.log(`Hi ${this.name}`);
  }
};

위의 객체 리터럴 방식처럼 하나하나 만들어 줘야 한다. 겨우 프로퍼티 값 하나 바꾸는데 말이다.

ES5에서 함수로 정의한 클래스

이때 우리가 조금 더 편하게 객체를 만들 수 있는 방법이 생성자 함수를 사용하는 것이다.
생성자 함수의 사용 방법을 코드를 통해서 먼저 살펴보자

// 생성자 함수를 왜 써야 하는가 코드를 보자 얼마나 편한지

function Person(name, age) { // <- 클래스
  const something = 'who are you?'; // <- 이 변수는 나중에 설명
  this.name = name;    // <- 여기서 this의 역할이 중요
  this.age = age;
  this.sayHi = function() {   // <- 클래스 안에 메소드를 만들어 줬다. 나중에 차이를 살펴보자
    console.log(`Hi ${this.name}`);
  };
}

Person.prototype.bye = function() { // <- prototype을 이용해서 밖에 메소드를 만들 수 있다.
  console.log(`${this.name} Bye~`);
}

const person1 = new Person('hyun', 7); // <- 인스턴스
const person2 = new Person('yoon', 9); // <- 인스턴스

console.log(person1); // Person {name: "hyun", age: 7, sayHi: ƒ}
console.log(person2); // Person {name: "yoon", age: 9, sayHi: ƒ}
console.log(person1.name); // 'hyun'
//console.lgo(something); // Uncaught ReferenceError: something is not defined

// 함수도 제대로 작동하는지 보겠다.
console.log(person1.sayHi()); // Hi hyun 정상적으로 잘 작동한다.
console.log(person1.bye()); // hyun Bye~ 정상적으로 잘 작동한다.

위의 코드를 보면 알 수 있겠지만 대표인 녀석을 함수로 만들어 주고 그 뒤에 new 키워드를 사용해서
얼마든지 프로퍼티 값을 바꿔서 찍어낼 수 있다. 객체 리터럴 방식보다 훨씬 빠르고 좋은 방법이다.
객체를 찍어내는 공장 같은 느낌

++ 추가 정리
위에서 함수안에 메소드를 만들어주던, 밖에 prototype을 이용해서 메소드를 만들어주던 모두 정상적으로 작동된다.
하지만 다른 점은 새로운 인스턴스를 생성했을때 발생되는데 모든 인스턴스들이 안에 만들어준 메소드를 포함한다.
내가 생각하기로는 클래스 함수안에 메소드를 만들어주면 새로운 인스턴스를 생성할 때마다 메소드도 같이 생성이 되니
그만큼 메모리를 많이 사용하는듯 하다. 그렇기 때문에 공통적으로 사용할 메소드는 함수 밖에 prototype을 이용해서
만들어주면 새로운 인스턴스를 생성해도 메소드는 포함하지 않기 때문에 그만큼 메모리를 낭비하지 않아도 될듯하다.

이때 꼭 알고 넘어가야 할 것들

  • 생성자 함수의 이름은 대문자로 시작을 한다. // 우리가 생성자 함수라는 걸 인식하도록 도와준다. 즉, 약속이다.
  • 프로퍼티 또는 메서드 명 앞에 기술한 this는 생성자 함수가 생성할 인스턴스(instance)를 가리킨다.
  • this에 연결(바인딩)되어 있는 프로퍼티와 메서드는 외부(public)에서도 사용할 수 있다.
  • 생성자 함수 내에서 선언한 일반 변수 여기서는 something이라는 변수는 외부에서 사용할 수 없다. (private) # 생성자 함수 내부에서는 접근할 수 있지만 외부에서는 접근할 수 없다.

생성자 함수는 그 의미 그대로 객체를 생성해주는 함수이다.

여기서 자바스크립트의 생성자 함수는 자바와 같은 클래스 기반 객체지향 언어의 생성자(constructor)와는 다르게
형식이 정해져 있는 게 아니라 기존 함수와 동일한 방법으로 생성자 함수를 선언하고 new 연산자를 붙여서 호출하면
해당 함수가 생성자 함수로써 동작하도록 만든 것이다.

그렇다는 건 생성자 함수가 아닌 일반 함수에 new 연산자를 붙여 호출해도 생성자 함수와 동일하게 동작한다는 것이다.
그래서 우리는 '이건 생성자 함수입니다.'라고 알려주기 위해서 첫 문자를 대문자로 만든다고 설명한 것이다.

const person = {
  name: 'hyun',
  age: 7,
  gender: 'male'
};

console.log(person.name); // 'hyun'
console.log(person['name']); // 'hyun'

person.name = 'yoon';
console.log(person.name); // 'yoon'

delete person.gender;
console.log(person.gender); // undefined

만들어낸 프로퍼티 값을 접근하기 위한 방법은 이전에 설명한 닷 노테이션(.), 브라-켓 노테이션([])이 있다.
이미 있는 프로퍼티에 새로운 값을 할당하기도 가능하다. 대신 이전에 있었던 값은 없어진다.
프로퍼티를 삭제하고 싶다면 delete 연산자를 사용하자. - 피연산자는 프로퍼티 키이다.

Pass-by-reference - Pass-by-value

객체가 참조 타입인 건 이제 알고 있다. 원시 타입과 참조 타입의 차이는 이전 글을 보면 알 수 있다.

두 가지의 타입으로 나눈 건 모두 알고 있으니 이 두 가지의 타입이 값을 어떤 식으로 가지고 있는지 확인하자.

참조 타입은 객체의 모든 연산이 실제 값이 아닌 참조 값으로 처리가 된다고 하고,
원시 타입은 값이 한번 정해지면 변경할 수 없다고 합니다.

원시 타입 말의 뜻을 먼저 살펴보면 객체는 프로퍼티를 변경, 추가, 삭제가 가능한 변경 가능한 값이라는 것이고
이 말은 즉, 객체 타입은 동적으로 변화가 가능하고 원시 타입은 동적 변화가 불가능해 값 자체가 전달이 된다고 합니다.
즉, pass-by-value 의미 그대로 원시 타입은 값을 복사해서 전달한다고 합니다.

하지만 객체는 값을 복사해서 전달하는 게 아닌 객체의 참조 값을 저장한다고 하는데요.
이 말의 의미는 객체는 사실 값 자체를 자신이 가지고 있는 게 아닌, 생성이 된 객체의 주소를 기억하고 있는 것입니다.

코드를 살펴보면 이해가 더 쉽다.

const foo = { val: 10}; // foo, bar로 같은 같지만 서로 다른 객체를 만들어 줬다.
const bar = { val: 10}; 

console.log(foo.val, bar.val); // 10 10 <- 당연히 같은 값이 나온다.

console.log(foo === bar); // false <- 값은 같지만 값을 기억하고 있는 주소가 서로 다르다.

const baz = bar; // <- 다른 변수에 객체 할당 즉, 같은 주소를 가르킨다.

console.log(baz.val, bar.val); // 10 10 <- 둘다 값은 같은 10이다.
console.log(baz === bar);      // true <- 서로 같은 주소를 기억하고 있다.

마지막으로 pass-by-value 혹은 call-by-value 두 가지 방식으로 이를 칭하는데요.
이 둘의 차이점은 주어가 함수라면 call, 인수라면 pass를 사용한다고 합니다.
-정확한 건 아니라서 정확한 의미를 아시는 분이 있다면 알려주시면 감사하겠습니다...

++ES6에서 class라는 키워드를 이용해서 정의

하나의 모델이 되는 청사진(class)을 만들고, 그 청사진을 바탕으로 한 객체(instance)를 만드는 프로그래밍 패턴

클래스를 사용하는 방법

Class 선언 - class키워드와 함께 클래스의 이름을 선언한다.

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  //Person.prototype.sayHi 와 동일하다. class 함수안에 메소드를 정의해줬다.
  sayHi() {
    console.log(`Hi ${this.name}`);
  }
}

person1 = new Person('hyun', 7); // Person {name: "hyun", age: 7}

// person1 객체에 함수가 들어있지 않지만 정상적으로 사용이 가능하다.
person1.sayHi(); // Hi hyun 

console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(typeof Person);  // function
console.log(typeof Person.prototype.sayHi);  // function

instanceof 연산자는 생성자의 prototype 속성이 객체의 프로토타입 체인 어딘가 존재하는지 판별합니다.-출처: MDN

ES5에서의 함수로 정의한 클래스와는 다르게 ES6의 클래스 선언의 메서드 정의는 클래스 내부에 직접 생성자를 정의할 수 있고
클래스 메소드를 생성할 때 function키워드를 사용할 필요가 없어서 간결해진다.

Hoisting

ES5의 함수 선언과 ES6의 클래스 선언의 차이점은 함수 선언의 경우 호이스팅이 일어나지만, 클래스 선언은 그렇지 않다.
클래스를 사용하기 위해서는 클래스를 먼저 선언 해야 하며, 그렇지 않으면 에러가 나올 것이다. - 출처: MDN
즉, 클래스를 먼저 선언한 뒤 인스턴스를 만들어줄 수 있다. 그렇지 않으면 레퍼런스 에러가 나온다.

Class 표현식 - class를 정의하는 다른 방법

클래스 표현식은 클래스의 이름을 가질 수도 있고, 갖지 않을 수도 있다.

// 클래스 이름이 없는 클래스 표현식
const Person = class {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  sayHi() {
    console.log(`Hi ${this.name}`);
  }
};

const person1 = new Person('hyun', 7); // Person {name: "hyun", age: 7}
person1.sayHi(); // hyun Hi

// 클래스 이름이 있는 클래스 표현식
const Person = class Person2 {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  sayHi() {
    console.log(`Hi ${this.name}`);
  }  
};

const person1 = new Person('hyun', 7); // Person {name: "hyun", age: 7}
person1.sayHi(); // Hi hyun 

ES6 클래스 구문을 사용하는 이유는?

  • 실행이 선언에 도달할 때까지 Temporal dead zone에 존재합니다.
  • 클래스 선언의 모든 코드는 strict 모드로 자동 실행됩니다. 클래스의 strict 모드를 거부할 수있는 방법이 없습니다.
  • 모든 메서드는 Non-enumerable 입니다. Object.defineProperty()를 사용하여 메서드를 Non-enumerable하게 만드는 사용자 지정 타입과 다른 중요한 변경 사항입니다.
  • 모든 메서드는 내부 [[Construct]] 메서드가 없으며 new로 호출하려고 하면 에러가 발생합니다.
  • new를 사용하지 않고 클래스 생성자를 호출하면 오류가 발생합니다.
  • 클래스 메서드 내에서 클래스 이름을 덮어 쓰려고하면 오류가 발생합니다.

정리

ES5 클래스는 함수로 정의할 수 있다.
ES6 에서는 class라는 키워드를 이용해서 정의할 수도 있다.

profile
둡둡

0개의 댓글