JavaScript/자바스크립트는 왜 그 모양일까?

[JS] new 키워드를 사용하지 않고 객체 생성하기

개발자엄지희 2023. 7. 2. 17:40
반응형

"자바스크립트는 왜 그럴까?"를 읽던 중, new 키워드를 사용하지 말라는 말이 나왔다.
왜 우리는 new 키워드를 사용하면 안되며, 또 어떻게 new 키워드를 사용하지 않을 수 있을까?

자바스크립트는 왜 그럴까? 11p.

(아래의 velog는 읽기 전 읽으면 좋을, 생성자가 new 키워드에 대해 정리된 글이고
 그 아래의 webclub 티스토리는 이 글을 쓰기 위해 참고한 글임을 밝힙니다.)

 

JS 생성자와 new 키워드

객체란 서로 연관된 변수와 함수를 그룹핑한 그릇이라고 할 수 있다. 그리고 그 객체 내의 변수를 프로퍼티(property), 함수를 메소드(method)라고 부른다. 그리고 이렇게 생성된 객체는 일종의 독립

velog.io

 

자바스크립트 생성자 패턴 - new를 강제하는 패턴

javaScript 생성자 함수의 핵심 패턴 자바스크립트에는 클래스가 없기 때문에 상당히 유연합니다. 객체에 대해 사전에 알아두어야 하는 내용, 즉 클래스의 '청사진' 같은 것이 필요없기 때문입니다

webclub.tistory.com


 

객체를 생성하는 방법


1. 객체 리터럴 사용

let user = { name: "보라" };

2. 내장 생성자 사용

let user = new Object(); // 안티패턴
user.name = "보라";
안티패턴: 비효율적이거나 생산성이 저해되는, 다시 말해서 '권장사항'의 반대편에 있는 소프트웨어 설계 관행을 의미

 

내장 객체 생성자의 함정

// 경고 : 모두 안티패턴

// 빈 객체
var o = new Object();
console.log(o.constructor === Object); // true

// 숫자 객체
var o = new Object(1);
console.log(o.constructor === Number); // true
o.toFixed(2); // '1.00', 숫자 객체이므로 toFixed를 사용할 수 있다.

// 문자열 객체
var o = new Object('I am a String');
console.log(o.constructor === String); // true
// 일반적인 객체에는 substring() 이라는 메서드가 없자민 문자열 객체에는 있다.
console.log(typeof o.substring); // 'function', 문자열 객체이므로 substring을 사용할 수 있다.

// 불린 객체
var o = new Object(ture);
console.log(o.constructor === Boolean); // true
Warning:
Object 생성자는 인자를 받을 수 있기 때문에, 인자로 전달되는 값에 따라 생성자 함수가 다른 생성자에 객체 생성을 위임 수 있고, 따라서 기대한 것과는 다른 객체가 반환될 수 있다.
이와 같은 동작방식 때문에, 런타임이 결정하는 동적인 값이 생성자에 인자로 전될 경우 예기치 않은 결과가 반환될 수 있다. 따라서 new Object는 지양하는 것이 좋겠다. (차라리 더 간단하고 안정적인 객체 리터럴을 사용하자.)  

3. 사용자 정의 생성자 함수 사용

function User(name) {
  this.name = name;
  this.sayMyName = function () {
    console.log("안녕? 내 이름은 " + this.name);
  }
}

let user = new User("보라");

 

사용자 정의 생성자 함수: 내부에서 일어나는 일

1. 빈 객체(엄밀히 따지자면 빈 객체는 아니지만)가 생성된다.
    이 객에는 this라는 변수로 참조할 수 있고, 해당 함수의 프로토타입을 상속받는다.

2. this로 참조되는 객체에 프로퍼티와 메소드가 추가된다.

3. 마지막에 다른 객체가 명시적으로 반환되지 않을 경우, this로 참조된 이 객체가 반환된다.

function User(name) {
  // 1. 객체 리터럴로 새로운 객체를 생성한다.
  // var this = {};
  
  // 2. 프로퍼티와 메소드를 추가한다.
  this.name = name;
  this.sayMyName = function () {
    console.log("안녕? 내 이름은 " + this.name);
  }
  
  // 3. this를 반환한다.
  // return this;
}

let user = new User("보라");
메소드와 프로퍼티를 추가할 때마다, 메모리에 새로운 함수가 생성되게 됩니다.
say()라는 메소드는 인스턴스별로 달라지는 것이 아니므로, 이런 방식은 명백히 비효율적입니다.

 

조금 더 나은 방법

User.prototype.sayMyName = function() {
  console.log("안녕? 내 이름은 " + this.name);
};

 

생성자 내부의 this?

아까 생성자 내부의 이면에서 빈 객체가 생성된다고 했는데, 이 '빈' 객체가 실제로 텅 빈 것은 아닙니다.

var this = {};

이 객체는 User의 프로토타입을 상속받습니다.
즉, 다음 코드에 더 가깝습니다.

var this = Object.create(Person.prototype);

 

생성자의 반환값

var ObjectMark = function() {
  // 생성자가 다른 객체를 반환하기로 결정했기 때문에
  // 아래의 'name' 프로퍼티는 무시된다.
  this.name = 'This is it';
  
  // 새로운 객체를 생성하여 반환한다.
  var that = {};
  that.name = "And that's that";
  return that;
};

var o = ObjectMark();
console.log(o.name); // "And that's that"
 생성자에서는 어떤 객체라도(객체이기만 한다면) 반환할 수 있습니다.
객체가 아닌 다른 것(Ex. 문자열, false 등)을 반환하려고 시도한다면, 에러가 발생하진 않지만
그냥 무시되고 this에 의해 참조된 객체가 대신 반환됩니다.

new를 강제하는 패턴


함수 호출문이 new로 시작하면 해당 함수는 생성자로서 호출되고, 그렇지 않으면 함수로서 호출됩니다.

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("보라"); // 생성자로서 호출
// User("보라"); // 함수로서 호출, 예기치 못한 결과 발생

 

그렇다면, 생성자를 호출할 때 new를 빼먹으면 어떻게 될까?

문법 오류나 런타임 에러가 발생하지는 않지만, 논리적인 오류가 생겨 예기치 못한 결과가 나올 수 있습니다.
new를 빼먹으면 생성자 내부의 this가 전역 객체를 가리키게 되기 때문입니다.
(브라우저라면 this는 window 객체를 가리킴)

// 생성자
function Coffee() {
  this.tastes = 'dalcom';
}

// 새로운 객체
var morning_coffee = new Coffee();
console.log(typeof morning_coffee); // 'object'
console.log(morning_coffee.tastes); // 'dalcom'
// 안티 패턴
// 'new'를 빼먹음
var morning_coffee = Coffee();
console.log(typeof morning_coffee); // 'undefined'
console.log(window.tastes); // 'dalcom'
ECMAScript5에서는 위와 같은 동작 방식의 문제에 대한 해결책으로, 스트릭트 모드에서는 this가 전역객체를 가리키지 않도록 했다. ES5를 쓸 수 없는 상황이라면, 생성자 함수가 new 없이 호출되어도 항상 동일하게 동작하도록 보장하는 방법을 써야한다.

 

대안 1: 명명규칙

생성자 함수명은 첫글자를 대문자로 쓰고 (파스칼 케이스: UserName, UserAge, PhoneNumber 등),
'일반적인' 함수와 메서드는 첫글자를 소문자를 사용한다. (카멜 케이스: userName, userAge, phoneNumber 등)

 

[Coding] 표기법 (카멜케이스, 파스칼케이스, 스네이크케이스, 케밥케이스)

표기법 필요성 프로그래밍 언어 가이드라인으로 공통된 표기법이 있으며, 개발자들이 개발하는데 있어서 코딩컨벤션이 존재하여 일관된 코딩스타일을 가져 생산성을 높히는것과 코드 분석에

velog.io

 

대안 2: that 사용

명명 규칙을 따르는 것도 꽤 도움이 되지만 이는 올바른 방식을 권고할 뿐, 강제하지는 못합니다.
또다른 대안은, this에 모든 멤버를 추가하는 대신, that에 모든 멤버를 추가한 후 that을 반환하는 것입니다.

function MyConstructor() {
  var that = {};
  that.name = "Eom Jihee";
  return that;
}
that이라는 변수명은 관습적인 것으로, 언어에 정의되어 있진 않아 어떤 이름이라도 쓸 수 있다.
흔히 사용되는 다른 변수명으로는 self와 me 등이 있다.
function MyConstructor() {
  return { name : "Eom Jihee" };
}
위 코드와 같이 간단한 객체라면 that이라는 지역 변수를 만들 필요도 없이 객체 리터럴을 통해 객체를 반환해도 됨.

 

아래와 같이 어떤 방법을 써도 문제없이 잘 나오지만,
위 패턴의 문제는 프로토타입과의 연결고리가 끊어지게 된다는 점입니다.
즉, MyConstructor() 프로토타입에 추가한 멤버를 객체에서 사용할 수 없게 됩니다.

// TEST
var first = new MyConstructor(),
	second = MyConstructor();

console.log(first.name); // "Eom Jihee"
console.log(second.name); // "Eom Jihee"

 

대안 2: 더 좋은 방법, 스스로를 호출하는 생성자

앞에서 언급한 프로토타입과의 연결고리가 끊어진다는 문제점을 해결하기 위해선, 다음의 접근법을 고려하면 되겠습니다.

function MyConstructor() {
  //this가 해당생성자의 인스턴스가 아니라면,
  // new와 함께 스스로를 재호출
  if(!(this instanceof MyConstructor)) {
    return new MyConstructor();
  }
  this.name = "Eom Jihee";
}

MyConstructor.prototype.anotherName = true;
// TEST
var first = new MyConstructor(),
	second = MyConstructor();

console.log(first.name); // "Eom Jihee"
console.log(second.name); // "Eom Jihee"

 

생성자 이름을 하드코딩하는 대신, arguments.callee와 비교하는 방법 또한 존재합니다.

function MyConstrutor() {
    if(!(this instanceof arguments.callee)) {
       return new arguments.callee();
    }
    this.name = "Eom Jihee";
}
모든 함수가 호출될 때는 내부적으로 arguments라는 객체가 생성되며, 이 객체는 함수에 전달된 모든 인자를 담고 있습니다. argumentscallee라는 프로퍼티는 호출된 함수를 가리킵니다.
Warning:
arguments.callee는 ES5의 스트릭트 모드에서는 허용되지 않습니다.
향후의 사용은 제한하는 것이 좋고, 기존 코드(레거시코드)에서 발견되는 경우는 제거해야 합니다.
반응형