본문으로 바로가기

name property of function

category JavaScript/ES6+ 2021. 2. 26. 18:45

이 글은 [인프런] Javascript ES6+ 제대로 알아보기 - 초급(정재남)를 토대로 작성되었음을 알려드립니다.


이번 장에서는 함수의  name 프로퍼티와  new.target 이라는 함수의 부가적인 내용들에 대해 알아보도록 하겠습니다.

 

 

name property of function

함수의  name 프로퍼티는 주로 디버깅하는 경우에 사용하지만  name 프로퍼티를 가지고 디버깅을 할 사항이 거의 없기 때문에 간단히 알아보고 넘아가 보겠습니다.

아래의 다양한 함수 리터럴 표기법에 따른  name 프로퍼티가 어떻게 달라지는지 알아보자.

js
// 함수 선언문
function a () { }
console.log(a.name);

// 함수 표현식(변수 b 에 익명함수를 할당)
const b = function () { };
// 기존에는 익명함수의 이름이 로그에서 확인할 수 없었지만
// 모던 브라우저에서는 name 프로퍼티를 확인할 수 있도록 지원하고 있다.
console.log( b.name );

// 기명 함수 표현식
const c = function cc () { };
console.log(c.name); // 기존부터 함수의 이름을 나타내고 있었음

// Arrow Function(화살표 함수)
const d = () => { };
console.log(d.name);

// ES6 메소드 축약 방식, 화살표 함수를 할당한 경우
const e = {
    om1: function () {},
    om2 () {},
    om3: () => {}
};
console.log(e.om1.name, e.om2.name, e.om3.name);

// ES6 Class 문법
class F {
    static method1 () {}
    method2 () {}
}
const f = new F();
console.log(F.method1.name, f.method2.name);

// ES5 Class 흉내
function G() { }
G.method1 = function () { };
G.prototype.method2 = function () { };
const g = new G();
console.log( G.method1.name, g.method2.name );

로그를 확인해 보면 마지막의  Class 를 흉내낸 패턴에서만 name 프로퍼티를 출력하지 않고 빈값으로 나오는 것을 확인할 수 있습니다.
추가된  ES6 의 클래스는  name 프로퍼티를 알려주지만 기존 방식에서는  name 프로퍼티가 없다라는 것을 알 수 있습니다.
중요한 사항은 아니지만 좀더  ES6에 이르러서 좀더 명확하고 견고하게 규정되었다는 정도로만 알고 넘어가자.

정리하면, name 프로퍼티를 확인하여 함수의 대상을 찾아서 어떤 정보를 담고 있는지를 알 수 있습니다.

 

 

 

 

new.target

new.target 을 알아보기 전에 다음의 코드를 살펴보자.

js
function Person (name) {
    if (this instanceof Person) {
        this.name = name;
    } else {
        throw new Error('new 연산자를 사용하세요.');
    }
}

var p1 = new Person('재희');
console.log(p1);

var p2 = Person('상윤');
console.log(p2);

var p3 = Person.call({}, '서비');
console.log(p3);

var p4 = Person.call(p1, '서비');
console.log(p4);

위 예제는 만약에  this 가  Person 의 인스턴스라면 전달받은  name 을  this.name 에 할당하고 그렇지 않다면 new 연산자를 사용하도록
에러를 던지는 코드로서  new 연산자없이 호출을 하면 에러를 내도록 한 것입니다.
이는  new 연산자를 강제하기 위한 방법중(ES5 때)에 한가지입니다.

좀더 풀이해 보자면,  new Person('재희'); 를 하면 생성될 인스턴스 자체가  this 가 되는데 그  this 가  Person 의 인스턴스냐라고 물었을 때 참이면  this.name = name; 이 실행되고, 이는  this.name 이라는 프로퍼티가 생성되서 반환이 될 것이고  new 연산자없이 호출하면  thiswindow 를 가리키게 되고  this는  Person 의 인스턴스가 아니기 때문에 에러를 발생하는 코드가 실행되는 것입니다.

 

위 예제를 하나씩 분석해 보자.

  • var p1 = new Person('재희');

    p1 은  Person 의 인스턴스이므로 에러가 나지 않는다.

  • var p2 = Person('상윤');

    p2 은  Person 의 인스턴스가 아니기 때문에 에러가 발생한다.

  • var p3 = Person.call({}, '서비');

    p3 에서  this 는  call 메소드로 인해  { } 인 빈 객체가 되고 이는  Person의 인스턴스가 아니므로 에러가 발생한다.

  • var p4 = Person.call(p1, '서비');

    call(p1, '서비'); 의  p1 은  this 가  Person의 인스턴스가 맞기 때문에  this.name 에 '서비'이 할당될 것이고,
     p4 는  call 메소드에  p1으로 인해  p1 을 바라보기 때문에  p4에는 반환하는 것이 없게 되어 undefined가 출력될 것입니다.
    그리고  p1의  name 에는  '서비'가 할당되어 있는 것을 확인할 수 있습니다.

 

기존의  ES5에서 new 연산자를 강제하기 위해서 위와 같은 조건문을 함수 내부에 작성하곤 했는데 이런 패턴은 var p4 = Person.call(p1, '서비'); 를 통해 확인했다시피 완벽히  new 를 강제하는 코드는 아니라는 것입니다.
이 코드는  new 를 강제하려고 했으나 본래 의도와는 달리 동작하게된 예외적인 경우입니다.
다시 말해,  new 연산자를 쓰지 않고도 에러가 나지 않는 상황이 발생된 예외적인 케이스인 것입니다.

 

 

new.target 의 등장

[MDN] new.target 바로가기

new.target은  new 연산자를 사용하여 함수를 실행했을 경우에 그 실행한 함수 자체가  new.target 이 되는 것을 말합니다.

예를 들어  new Person(43); 이라고 할 경우  new가 붙은  Person이  target이 된다는 의미입니다.

 

다음의 코드를 먼저 살펴보며 알아보도록 하자.

js
function Person (name) {
    console.dir(new.target);
    if (new.target !== undefined) {
        this.name = name;
    } else {
        throw new Error('new 연산자를 사용하세요.');
    }
}

const p1 = new Person('재희');
console.log(p1);

const p2 = Person('상윤');
console.log(p2);

const p3 = Person.call({}, '서비');
console.log(p3);

const p4 = Person.call(p1, '서비');
console.log(p4)

앞선 예제에서는  instanceof 연산자를 이용해 인스턴스를 판단했으나 위 예제에서는  new.target을 사용했습니다.

 

위 예제를 분석해 보자.

  • const p2 = Person('상윤');

    console.dir(new.target); 를 확인해 보면 생성자 함수 자체가 출력된 것을 확인할 수 있고,  new.target !== undefined 는  new.target 이 있으므로  this.name = name; 이 코드를 실행하게 됩니다.

  • const p2 = Person('상윤');

     new.target 이 없기 때문에 에러가 발생합니다.

  • const p3 = Person.call({}, '서비');

    this가 빈 객체를 가리키고, 일반 함수를 호출한 것과 동일하므로  undefined를 반환하게 되고  new.target이 없기 때문에 에러가 발생합니다.

  • const p4 = Person.call(p1, '서비');

    앞선 예제에서는 연산자를 쓰지 않아도 에러가 발생하지 않았으나 이 코드에서는 에러가 발생하게 됩니다.
    즉,  p1이 인스턴스이긴 하지만  new.target 으로 인해  new 연산자 자체를 사용하지 않으면 의도대로  new 연산자를 강제할 수 있습니다.

 

new.target 은 객체인가? 변수인가?

new.target 은 .target 의 형태로 보아  new라는 객체가 있을 것으로 보이고  new 객체에 프로퍼티로  target 이라는 것이 있는 형태로 보입니다. 하지만 콘솔 로그에서  console.log(new); 를 출력해 보면 읽어오질 못하는 것을 확인할 수 있습니다.
즉, 객체가 없다라고 할 수 있고 그렇다면  new.target 은 무엇일까?
new.target 은 함수 생성시 생기는 것으로 그 자체로서 변수로 볼 수 있습니다.
다시 말해, 함수 내부에서만 접근이 가능한  내부 변수라고 할 수 있습니다.

 

중첩된 함수가 화살표 함수일 경우 new.target 은 어떻게 될까?

화살표 함수 파트에서  this 바인딩뿐만 아니라  super,  new.target 도 바인딩하지 않는다고 언급했습니다.

다음의 예제를 살펴보자.

js
function Person (name) {
    const af = n => {
        this.name = n;
        console.log(new.target);
    };
    af(name);
}

const p1 = new Person('재남');
const p2 = Person('성훈');

위 예제는 내부함수로서 화살표 함수를 만들었고, 첫 번째 로그를 보면  Person 함수가 담겨있는 것을 확인할 수 있습니다.
이것은 화살표 함수가 바인딩이 되지 않기 때문에 위 코드상 상위 스코프의  Person 을 찾은 결과를 로그에서 확인할 수 있는 것입니다.

그리고  const p2 = Person('성훈');new 연산자없이 호출한 경우로 역시 화살표 함수는 바인딩을 하지는 않지만 일반 함수를 호출하는 형태로서 반환(return)값이 없는 상태이므로  undefined 가 출력되는 것입니다.

 

다른 상황을 다음의 예제를 통해 계속 알아보자.

js
function Person (name) {
    this.name = name;
}

function Android (name) {
    Person.call(this, name);
}

const p1 = new Android('시온봇');
console.log( p1 );

위 예제는  new Android('시온봇'); 로  new 연산자를 사용하긴 했으나  Android 내부에서  Person.call 를 이용해서 Person 함수를 빌려다 사용하고 있는 형태입니다.  new 로 사용해서 생성자 함수는  Android 이지만 내부에서 실행할 내용이  Person 에 있는 내용으로  p1(=this)을 하여  name 을 넘겨준 것으로 실행하라는 코드로 여기서 내부의  Person.call(this, name); 은 일반함수  Person 을 실행한 것이나 마찬가지입니다.
즉, 함수로서 실행이 된 것입니다.

이 내용을 좀더 명확히 알아보기 위해 다음의 예제를 살펴보도록 하겠습니다.

js
function Person (name) {
    this.name = name;
    return '복습 안하니까 따라오기 힘들지?';
}

function Android (name) {
    const res = Person.call(this, name);
    console.log( res );
}

const p1 = new Android('시온봇');
console.log( p1 );

p1 에 담긴  Android {name: "시온봇"} 내용은 달라질 것은 없지만  res 에 반환된 값이  '복습 안하니까 따라오기 힘들지?' 가 출력되는 것으로 보아 생성자 함수로서의 호출이 아니라 일반 함수로서 호출된 것을 명확히 알 수 있는 대목입니다.
즉, Person.call 은 생성자 함수로 호출된 것이 아니라 일반 함수로 호출된 것입니다.

위 같은 문제를 우회하기 위해 상단에서  new 연산자를 강제하기 위해 사용한 코드(new.target)를 이용하면 좋습니다.

js
function Person (name) {
    console.log( new.target );
    if (new.target !== undefined) {
        this.name = name;
    } else {
        throw new Error( 'Person 생성자함수를 new 로 호출해야 합니다 !!' );
    }
    this.name = name;
}

function Android (name) {
    Person.call(this, name);
}

const p2 = new Android('시온봇');
console.log( p2 );

위와 같이 작성하여 실행하면  new.target 에서 필터링되어  new Error 를 실행하게 됩니다.

위 코드를 좀더 견고하게 만들기 위해 다음과 같이 작성했습니다.

js
function Person (name) {
    console.log( new.target );
    if (new.target === Person) {
        this.name = name;
    } else {
        throw new Error( 'Person 생성자함수를 new 로 호출해야 합니다 !!' );
    }
    this.name = name;
}

function Android (name) {
    Person.call(this, name);
}

const p2 = new Android('시온봇');
console.log( p2 );

앞선 코드에서  new.target !== undefined를 비교하기 보다 좀더 명확하게 하기 위해서  Person이라고 비교하는 방법입니다.
사실  undefined와 비교하는 것과 생성자 이름과 비교하는 것이 별반 차이점은 없지만 명확하고 가독성 측면에서 좋을 수 있습니다.

 

 

지금까지 살펴본 안전 장치와 같은 코드는  Class 에서 사용할 경우에 그 의미가 있다고 할 수 있습니다.

아래 예제를 살펴보도록 하자.

js
class A {
    constructor () {
        console.log( new.target );
    }
}

class B extends A {
    constructor () {
        super();
    }
}

// 각각 해당 생성자를 가리킨다.
const b = new B();
const a = new A();

위 예제에서 클래스로 사용하여 로그를 확인해 보니 각각 올바르게 해당 생성자 함수를 출력하는데 이를 이용하면  추상 클래스를 흉내낼 수 있습니다.

추상 클래스 흉내내기

js
class A { // 추상클래스처럼 흉내
    constructor () {
        if (new.target === A) {
            throw new Error( 'A는 추상클래스입니다.' );
        }
    }
}

class B extends A {
    constructor () {
        super();
    }
}

// 각각 해당 생성자를 가리킨다.
const b = new B();
const a = new A();

위와 같이 작성하면  A 자체를  new 연산자로 생성자 함수를 호출할 수 없게 됩니다.
즉,  A 자체를 인스턴스로 호출할 수 없다는 의미로서 오직 B 에 의해서만 호출이 가능하게 되는 것입니다.

로그를 확인해 보면  b 에는 인스턴스가 있으나  a 는 아무런 값도 정의되어 있지 않다는 것을 확인할 수 있습니다.

클래스 자체를 인스턴스로 만드는 것은 얼마든지 가능하지만 자바스크립트에는 추상클래스 자체를 지원하고 있지 않기 때문에 위와 같은 조건문의 코드를 이용하여 인스턴스를 생성하려고 할 때 호출을 못하도록 막음으로서 오직 하위 서브 클래스인 즉, 상속받은 구체적인 클래스에 의해서만 동작을 할 수 있게 끔 동작을 제한할 수 있는 것입니다.

 

 

Jaehee's WebClub