자바스크립트 딥다이브 - 생성자 함수에 의한 객체 생성

ChoiYongHyeun·2023년 12월 11일
0

커어어어억

내용이 슬슬 어려워지기 시작한다.

객체가 생성되는 방법

우리는 여태 객체를 생성 할 때

객체 리터럴 방법을 이용해 객체를 생성했다.

var a = 'string';
var b = 123;
var c = true;
var d = function () {};
var e = {};

다음처럼 변수 명에 문자 리터럴, 숫자 리터럴, 불리언 리터럴 , 함수 리터럴 , 객체 리터럴 ...

리터럴 표기법을 이용해서 변수에 값을 할당하거나 객체를 설정했다.

console.log(a.constructor);
console.log(b.constructor);
console.log(c.constructor);
console.log(d.constructor);
console.log(e.constructor);
[Function: String]
[Function: Number]
[Function: Boolean]
[Function: Function]
[Function: Object]

이 때 각 변수들의 내부 메소드인 constructor 를 사용하면 뜬금없게도 [Function : 함수명] 이 나타난다.

여기서 constructor는 우리 말로 하면 생성자 로 해당 변수의 생성자는 Function 이면서, 함수명 이란 함수를 이용해 생성 되었다는 뜻이다.

결국 객체를 생성한다는 것은 생성자 함수 를 통해 만드는 것임을 알 수 있다.

객체 생성자

let person1 = new Object({
  name: 'lee',
  age: 16,
});

let person2 = { name: 'lee', age: 16 };

console.log(person1);
console.log(person2);

console.log(person1.constructor);
console.log(person2.constructor);
{ name: 'lee', age: 16 }
{ name: 'lee', age: 16 }
[Function: Object]
[Function: Object]

동일한 프로퍼티를 가진 두 객체를 생성 할 때 하나는 객체 생성자 함수를 이용하여 생성하고, 하나는 객체 리터럴 방법을 이용해서 생성하였다.

두 객체의 생성자를 확인해보니 Object 로 같음을 알 수 있다.

이렇게 보면 객체 생성자를 사용하기 보다 객체 리터럴 방법으로 사용 하는 것이 훨씬 간편해보이며

이는 사실이다.

하지만 객체 리터럴 방법으로 객체를 생성할 때 발생하는 단점이 있는데, 그 때는 객체 생성자 방법을 이용하는 것이 간편하다.

정리

객체를 생성하는 것은 객체 생성자 함수객체 선언new 키워드를 이용해 객체를 생성한다.

객체 리터럴을 이용 할 때의 단점

만약 내가 어떤 숫자가 주어졌을 때 실제 숫자의 값과, 홀수인지를 물어보는 함수를 가진 객체를 생성하고 싶다고 해보자

let a = {
  number: 2,

  isOdds() {
    if (this.number % 2) {
      return true;
    }
    return false;
  },
};

let b = {
  number: 3,

  isOdds() {
    if (this.number % 2) {
      return true;
    }
    return false;
  },
};

console.log(a); // { number: 2, isOdds: [Function: isOdds] }
console.log(a.isOdds()); // false
console.log(b); // { number: 3, isOdds: [Function: isOdds] }
console.log(a.isOdds()); // true

변수 a,b 모두 동일한 프로퍼티를 갖는 객체임에도 불구하고 객체 리터럴 방법을 이용 할 때에는 코드를 반복해서 작성해야 한다는 단점이 존재한다.

해당 객체 리터럴에서 객체에 함수도 들어간 모습을 볼 수 있는데
함수 또한 객체이기 때문에 객체에 함수가 존재 할 수 있다.

여기서 익숙치 않은 문법이 나오는데 그건 this 문법이다.

this

this 는 객체 자신의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수 (self-referencing variable) 이다. this 가 가리키는 값, 즉 this 바인딩 은 호출 방식에 따라 동적으로 결정된다.

호출 방식 this 바인딩
전역 함수에서 호출 전역 객체 (브라우저에서는 `window`)
메소드로서 호출 메소드가 속한 객체
함수로서 호출 전역 객체 또는 `undefined` (strict mode)
생성자 함수로서 호출 새로 생성된 객체
call 또는 apply 수동으로 지정한 객체

여기서 우리는 전역 함수에서 호출 , 메소드로서 호출 , 생성자 함수로서 호출 에 대해서만 알아보자

전역 함수에서의 호출

function foo() {
  console.log(this);
}

foo();
<ref *1> Object [global] {
  global: [Circular *1],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  clearInterval: [Function: clearInterval],
  ....

전역에서 생성된 영역에서 전역 함수의this 를 로그하면 전역 환경에 대한 내용이 로그 된다.

전역 함수에서 this 가 가리키는 것은 전역 객체 이다.

메소드로서 호출

메소드로서의 호출은 함수를 객체의 메소드로서 사용될 때를 의미한다.

function foo() {
  console.log(this);
}

let obj = { name: 'lee', foo };
obj.foo(); // { name: 'lee', foo: [Function: foo] }

다음 같은 경우는 foo 라는 함수가 obj 라는 객체의 프로퍼티로 들어갔다. 메소드로서 말이다.

메소드로서 호출되면 foo 는 전역 객체를 가리키는 것이 아닌 메소드가 선언된 객체를 가리킨다.

생성자 함수로서의 호출

이부분은 추후 밑에서 설명하도록 한다.

function foo() {
  console.log(this);
}

let obj = new foo();
obj; // foo {}

new 키워드와 함께 사용하여 생성자 함수로서 호출하게 되면 생성자 함수가 (미래에) 생성할 인스턴스를 반환한다.

인스턴스

생성자 함수로 인해 생성된 객체를 인스턴스라고 한다.

생성자 함수를 이용하여 객체 생성하기

함수도 객체이기 때문에 프로퍼티를 가질 수 있다.

function foo() {
  console.log('foo!');
}

console.log(foo); // [Function: foo]

foo.age = 16;
foo.address = 'korea';
console.log(foo); // [Function: foo] { age: 16, address: 'korea' }

그렇기 때문에 함수에 프로퍼티를 추가하면 프로퍼티에 객체가 추가되는 모습을 볼 수 있다.

이것은 예시를 위해 사용했을 뿐 실제로 함수에 객체를 추가하는 행위는 권장되지 않는다.

그럼 위에서 표현했던

let a = {
  number: 2,

  isOdds() {
    if (this.number % 2) {
      return true;
    }
    return false;
  },
};

let b = {
  number: 3,

  isOdds() {
    if (this.number % 2) {
      return true;
    }
    return false;
  },
};

를 생성자 함수를 이용하여 표현해보자

function Naming(num) {
  this.num = num;
  this.isOdds = function () {
    if (this.num % 2) return true;
    else return false;
  };
}

위의 로직을 표현하는 함수 Naming 을 만들어주자

이 때 함수 몸체 안에 this 를 이용하여 Naming 함수의 객체 this.num , this.isOdds 를 설정해주었다.

이것이 의미하는 것은 Naming 함수에 매개변수 값 num 이 들어오면 Naming 함수가 새로운 객체 {} 를 만들고 this 는 방금 만든 새로운 객체 {} 를 가리킨다.

this.num = num , this.isOdds = function(){ ... } 를 통해

방금 만든 새로운 객체 {} 의 프로퍼티와 값으로 설정해주겠다는 뜻이다.

let number2 = new Naming(2);
let number3 = new Naming(3);

console.log(number2);
console.log(number2.isOdds());
console.log(number3);
console.log(number3.isOdds());
Naming { num: 2, isOdds: [Function: isOdds] }
false
Naming { num: 3, isOdds: [Function: isOdds] }
true

이후 생성자 함수를 통해 반복되는 로직을 가진 객체를 생성할 수 있다.

생성자 함수의 인스턴스 생성 과정

스텝 바이 스텝으로 찾아보자

생성자 함수의 역할은 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 템플릿(클래스)으로서 동작하여 인스턴스를 생성하는 것과 생성된 인스턴스를 초기화 하는 것이다.

1. 인스턴스 생성과 this 바인딩

function Naming(num) {
  // 인스턴스 생성 및 바인딩
  this.num = num;
  this.isOdds = function () {
    if (this.num % 2) return true;
    else return false;
  };
}

new Naming 을 실행하는 순간 빈 객체은 {} 가 생성된다.

이 때 생성된 객체를 인스턴스 라고 하며 , Naming 내부에 존재하는 this 는 방금 생성된 인스턴스 를 가리킨다.

이러한 행위를 바인딩 이라고 한다.

바인딩

식별자와 값을 연결하는 행위
위에서는 this 라는 식별자가 인스턴스를 가리킨다.

2. 인스턴스 초기화

function Naming(num) {
  // 인스턴스 생성 및 바인딩
  this.num = num;
  this.isOdds = function () {
    if (this.num % 2) return true;
    else return false;
  };
  // 인스턴스 초기화 및 할당
}

이후 Naming 내부에 존재하는 로직을 통해 인스턴스{} 이자 식별자는 this 의 프로퍼티들을 설정한다.

3. 암묵적 인스턴스 반환

생성자 함수 내부에서 모든 처리가 끝나면 완성된 this 를 암묵적으로 반환한다.

this 를 반환한다는 것은 프로퍼티가 설정된 인스턴스 를 반환한다는 것과 같다.

만약 함수 내에 반환문(return)이 존재하며 객체를 반환한다면 this 가반환되지 못하고 명시한 객체가 반환된다.

function Naming(num) {
  this.num = num;
  this.isOdds = function isOdds() {
    if (this.num % 2) return true;
    else return false;
  };

  return { name: 'lee' }; // 객체를 반환
}

let number2 = new Naming(2);

console.log(number2); // { name: 'lee' }

하지만 만약 객체가 아닌 원시값을 반환한다면 무시한다.

그러니 생성자 함수는 객체를 반환하지 않는 한 암묵적으로 생성한 인스턴스를 반환한다.

new 빼면 어떻게 되는데 ?

function Naming(num) {
  this.num = num;
  this.isOdds = function isOdds() {
    if (this.num % 2) return true;
    else return false;
  };
}

let number2 = Naming(2);

console.log(number2); // undefined

생성자 선언문인 new 를 제거하면 함수는 생성자 함수가 아닌 일반 함수로서 호출되기 때문에

블록문 내의 결과 값을 return 한다.

이 때는 return 될 값이 없기에 undefined 가 반환되었다.

new를 통해 함수가 일반 함수인지, 생성자 함수인지를 구분하는구나 !

내부 메소드 [[call]][[Construct]]

일반적으로 함수는 호출되어 기능을 구현하는 역할을 한다.

하지만 new 를 사용하면 생성자 함수 로서의 역할을 한다.

함수는 상황에 따라 호출되어 발생하는 함수, 생성자 함수로서의 역할을 해야 하는데

그건 자바스크립트 엔진이 어떻게 평가할까 ?

이전 챕터에서 객체는 내부 어트리뷰트와 내부 메소드를 갖는다고 하였는데 함수 또한 객체이기 때문에 내부 어트리뷰트와 내부 메소드를 갖는다.

내부 어트리뷰트

내부 어트리뷰트 설명
[[Call]] 함수가 호출될 때 수행되는 내부 메소드
[[Construct]] 생성자로서 호출될 때 수행되는 내부 메소드

내부 메소드

내부 메소드 설명
[[GetPrototypeOf]] 객체의 프로토타입을 반환하는 내부 메소드
[[SetPrototypeOf]] 객체의 프로토타입을 설정하는 내부 메소드
[[GetOwnProperty]] 객체의 속성을 반환하는 내부 메소드
[[HasProperty]] 객체가 특정 속성을 가지고 있는지 확인하는 내부 메소드

보다 많은 메소드들이 있으나 우리가 주목해야 할 부분은 내부 어트리뷰트이다.

함수가 호출 될 때 생성자 없이 호출되면 내부 어트리뷰트가 call 인 상태로 호출되어

함수 블록문 내부에 존재하는 로직을 평가한다.

하지만 생성자와 함께 호출되면 내부 어트리뷰트가 Construct 인 상태로 호출되어

생성자 함수로서 작동한다.

중요한 포인트는 모든 함수들은 호출 할 수 있기 때문에 call 이 가능하다. 이러한 함수의 특징을 callable (호출가능한) 한 특성을 갖는다고 한다.

하지만 함수별로 생성자 함수로서 이용가능 할수도 있고, 없을 수도 있다.

생성자 함수로서 이용 가능한 함수를 constructor 이라 하고 , 불가능한 함수를 non-constructor 이라고 한다.

constructornon-constructor

생성자 함수

함수 설명
function Example() {} 기본 생성자 함수
class MyClass {} ES6+ 클래스 문법은 또한 생성자 함수를 만듭니다

비-생성자 함수

함수 설명
const arrowFunction = () => {}; 화살표 함수는 생성자를 갖지 않습니다
function regularFunction() {} `class` 또는 `function` 키워드 없이 작성된 일반 함수는 생성자가 아닙니다

new.target

위에서 생성자 함수로 사용하기 위해선 생성자 선언문인 new 를 사용해야 한다고 하였다.

근데 만약 내가 생성자 함수를 만들어두고 생성자 선언문을 사용하지 않고 코드를 작성하면

예기치 못한 오류가 발생 할 수 있다.

이를 막기 위해 ES6 에서는 new.target 을 지원한다.

new.target 은 함수의 내부 어트리뷰트에 따라서 값이 변하낟.

만약 함수의 내부 어트리뷰트가 call 이라면 (일반 함수로서 호출된다면) new.targetundefined 값을 가리키게 된다.

하지만 내부 어트리뷰트가 constructor 라면 (생성자 함수로서 호출된다면) new.target 은 함수 자체를 가리키게 된다.

function Example() {
  if (new.target) { // undefined 값은 데이터 타입 변환으로  falsy 한 값이다. 
    console.log('이 함수는 new 키워드로 호출되었습니다.');
  } else {
    console.log('이 함수는 new 키워드로 호출되지 않았습니다.');
  }
}

new Example(); // "이 함수는 new 키워드로 호출되었습니다."
Example(); // "이 함수는 new 키워드로 호출되지 않았습니다."

new.target 을 이용해 생성자 함수로 사용되지 않은 함수를 찾아낼 수 있다.

이를 이용하여 재귀적으로 일반 호출로서 표현된 함수도 생성자 함수로 사용 할 수 있다.

function Naming(num) {
  if (!new.target) {
    console.log('new 를 까먹었구나?');
    return new Naming(num);
  }

  this.num = num;
  this.isOdds = function isOdds(num) {
    if (num % 2) return true;
    else return false;
  };
}

let number2 = Naming(2);
console.log(number2);
// new 를 까먹었구나?
// Naming { num: 2, isOdds: [Function: isOdds] }
profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글