JavaScript

8. 함수(생성자 함수, 일급 객체, 함수 호이스팅, 화살표 함수 ..)

문정훈 2022. 2. 9. 00:12

함수의 객체 생성 시점과 함수의 평가 과정이 어떻게 다르며 이들의 개념들이 어떻게 유기적으로 연결되는지 오랫동안 헷갈렸던 내용도 이 포스팅 중간에 정리해봤다.

 

1. 함수 리터럴이란?

함수 리터럴은 function 키워드, 함수 이름, 매개변수 목록, 함수 몸체로 구성된다. 

var f = function add(x, y) {
	return x+y;
}

위 식은 변수 f에 함수 리터럴을 대입했다고 할 수 있다. 

자바스크립트의 함수는 객체 타입의 값으로 함수 리터럴 또한 '평가'되어 값을 생성하는데 이 값은 객체이다. 

즉 함수는 객체이다. 

함수는 일반적인 객체와 다르게 호출할 수 있으며 일반 객체에는 없는 함수 객체만의 고유한 프로퍼티가 존재한다. 

 

 

2. 함수의 정의

함수를 정의한다함은 매개변수, 실행할 문, 반환할 값을 지정하는 것을 말한다. 

 

1) 험수 선언문

function func(x, y) {}

 

2) 함수 표현식

var f = function(x, y) {}

 

3) Function 생성자 함수

var f = new Function('x', 'y', 'return x+y');

 

4) 화살표 함수

var f = (x, y) => {}

 

 

3. 함수 호이스팅

1) 함수 선언문 -> 함수 호이스팅

함수 선언문이 작성된 스코프의 '평가'과정에서 (런타임이 아닌) 자바스크립트 엔진에 의해 함수 선언문의 객체가 생성되어 heap 영역에 함수 객체가 생성된다. 

즉 자바스크립트 엔진에 의해 함수 선언문이 작성된 스코프의 '평가' 과정 때 함수 이름과 동일한 이름을 식별자(key)로 하고 key의 값으로 객체를 생성하는데 이러한 객체는 heap영역에 저장된다. 

따라서 함수 선언문 이전에 함수를 참조하거나 호출하는 것이 가능하다. 그 이유는 참조 또는 호출은 런타임시 진행되는데 런타임 이전에 이미 함수의 객체가 생성되었기 때문이다. 

이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 특징을 함수 호이스팅이라고 한다.

 

2) 함수 표현식 -> 변수 호이스팅

소제목에서 알 수 있듯이 함수 표현식을 통해 작성된 함수는 함수 호이스팅이 아니라 변수 호이스팅이 발생한다. 

함수 표현식이 작성된 스코프의 '평가'과정에서 함수 표현식의 변수는 undefined로 초기화 된다. 

그리고 런타임시 undefined로 초기화되었던 변수에 함수 객체가 생성되면서 할당된다.

이것은 변수 호이스팅이다.

 

 

4. 화살표 함수(react에서 왜 화살표 함수를 더 선호하나?)

선행 개념1) 메소드란?

js에서 함수와 메소드의 차이점을 보면 메소드란 객체에 바인딩된 함수를 일컫는 의미로 ES6에서는 메소드를 다음과 같이 정의한다. "메소드란 메소드 축약 표현으로 정의된 함수만을 의미한다."

위 정의를 좀 더 확장해서 정리해보면

  1. 우선 메소드는 non-constructor하다. (생성자 함수로 호출 불가능)
  2. prototype 프로퍼티가 없으며 프로토타입 역시 생성하지 않는다. 
var obj = {
  value : 1,
  objFunc() { //객체 리터럴에 선언된 메소드
    console.log(">>"+this); //obj에 바인딩	
    function func4() {//메소드 안에 선언된 함수
        console.log(">>"+this); //this는 window
      }
      func4();
  }
}

obj.objFunc()

//실행 결과
//>>[object Object]
//>>[object global]

위 코드를 다시 보면 objFunc은 함수가 아니라 메소드이다.

메소드에 선언된 this는 자신을 선언한 객체인 obj에 할당된다.

메소드 내부에 선언된 func4는 함수이다. 어떠한 경우(메소드 내부, 콜백함수, 중첩함수) 이건 함수에서의 this는 window에 바인딩된다. 

 

 

선행개념2) js에서 이벤트로 전달된 값

<button onclick ="func1()">버튼</button>

위와 같이 onclick이벤트로 전달된 함수는 아래와 같이 처리된다. 

function onclick(event) {
	func1();
}

 

 


js함수를 정리하는 포스팅에서 4절은 react와 더 관련된 내용이다.

단순히 화살표 함수의 개념을 정리하는 것보다 react를 공부하며 화살표 함수가 실제로 왜 등장하게 되었는지 몸소 느낀점을 정리했다..

이 절을 읽기 전에 js에서 '메소드'와 '함수'가 어떻게 다른지와 일반 함수에서 this 할당 그리고 메소드에서 this 할당이 어떻게 다른지 알아야한다. (바로 위에 선행 개념1, 2를 알고 읽을 것을 권장) 

 

화살표 함수에 대해 간단히 설명하면

우선 화살표 함수는 non-constructor이며 (생성자 함수로 객체를 생성할 수 없다.)  prototype 프로퍼티가 없다. 즉 프로토타입을 생성하지 않는다. 

 

화살표 함수가 등장한 이유는 콜백 함수 내에서 this가 전역 객체에 바인딩 된다는 문제점을 해결하기 위해 나왔다. 

화살표 함수의 this바인딩은 화살표 함수가 선언된 상위 스코프이다.

 

class App extends React.Component {
    state = {...}
    
    func1() {
    	console.log(this); // this는 App를 가리킴
    }
}

위 클래스 내부의 func1는 함수가 아닌 메소드이다. 위와 같은 메소드에서 this는 당연히 클래스에 바인딩 된다.

(참고로 만약 func1 메소드 내부에 func2라는 "함수"가 선언된다면 함수 내부에서 this는 -> window에 바인딩이다.)

이제 아래 코드를 보자

 

 

class App extends React.Component {
	state = {...}
    
    func1() {
    	console.log(this); // this는 App를 가리킴
    }
    
    render() {
    	return (
            <div>
            	<button onClick = {this.func1}>클릭함</button>
            </div>
        )
    }
}

위 코드에서 App의 메소드인 func1()을 그냥 호출한다면 this는 App에 바인딩이 된다. 하지만

onClick 클릭했을 때 호출되는 func1 내부의 console.log(this) 코드의 this가 window에 바인딩된다는 문제점이 발생한다.

onClick의 값으로 전달하는 함수는 선행개념2에서 알 수 있듯이 콜백함수가 아니다.!! (만약 콜백함수 였다면 window 바인딩이 당연한데..)

그 이유는 아래와 같다고 추측한다.  아래 예시를 보자

 

예시) 

const obj = new Object();

obj.method1 = function() {
  console.log(">>"+this)
}

obj.method1();

const a = obj.method1

a();

//결과값
//>>[object Object]
//>>[object global]

위 코드를 실행해보면 위와 같은 결과가 나온다.

위 코드는 a = funciton() { console.log(">>" + this) } 

이것을 의미한다고 생각하면 된다.

 a = funciton() { console.log(">>" + this) }  이 코드만 봤을 때 this는 당연히 window에 할당된다는 것을 알 고 있다. 

(일반 함수의 this는 window에 할당된다.)

 

react 내부적으로 위 예시와 같은 동작을 수행하였다고 추측한다. 따라서

onclick의 값으로 전달된 메소드는 콜백함수가 아님에도 불구하고 func1 내부의 this가 window에 바인딩된다.

이러한 문제점 때문에 react에서 컴포넌트(클래스 컴포넌트)에서 메소드는 화살표 함수로 선언하는 것이 일반적이다. 

화살표 함수는 내부적으로 this의 바인딩이 존재하지 않는다.

따라서 화살표 함수에서 this호출은 화살표 함수의 상위 스코프의 this값을 읽게 된다. 

즉 화살표 함수에서의 this는 화살표 함수가 정의된 상위 스코프이다.!!!

 

 

 

5. 즉시 실행 함수

즉시 실행함수는 함수의 정의와 동시에 즉시 호출되는 함수를 말한다.

(function() {
	var a = 10;
    var b = 20;
    return a + b;
}());

위 형태는 익명 함수를 이용한 것으로 기명 즉시 함수보다 익명 즉시 함수를 사용하는 것이 일반적이다. 

 

(function func() {
	var a = 10;
    var b = 20;
    return a + b;
}());

위 형태는 기명 즉시 함수이다. 

두 즉시 실행함수 모두 함수 리터럴로 평가되며 함수의 이름은 함수 몸체에서만 참조할 수 있다.

따라서 즉시 실행 함수를 다시 호출할 수 없다. 

 

 

6. 내가 헷갈렸던 내용 정리

1) 함수 객체 생성 시점 

자바스크립트에서 함수는 (일급)객체로 간주된다.

함수 선언문이 작성된 스코프를 A라고 하겠다. A라는 것이 이 절에서 계속 언급할 것이다.

A의 평가 과정이 진행되면 함수 선언문의 함수 이름을 key로 가지며 그 값으로 함수의 "객체"가 생성된다. 

key와 값(객체)는 당연히 heap영역에 저장된다. 

이때 함수의 객체가 생성된다함은 무엇인가???

 

※ 개념1

우선 함수 객체가 생성되면 함수의 [[Environment]]의 내부 슬롯의 값이 결정되는데 이 값이란 함수 스코프의 상위 스코프이다. 자바스크립트에서는 렉시컬 스코프를 가진다. 즉 함수가 어디서 호출되었는가가 아닌 함수가 어느 스코프에서 선언되었는가에 따라 자신의 상위 스코프를 결정하고 그 상위 스코프는 변경되지 않는다.

A의 평가 과정에서 함수 객체가 생성되는 시점에 결정된다. 함수의 상위 스코프가 결정된다. 그래서 함수가 선언된 A를 함수의 상위 스코프로 가지게 되는 것이다. 

 

※ 개념2

그렇다면 함수 객체가 생성되는 것과 함수의 '평가' 과정이 진행된 후 런타임이 진행되는 것과는 무엇이 다르며 어떤 개념적 연결고리가 있을까?

흐름을 정리해보면,

우선 다른 예시를 들자면 자바스크립트에서 객체 리터럴로 생성된 obj = { a : 10} 이러한 객체가 있다고 해보자.

이러한 객체를 포함하는 스코프의 '평가' 과정이 진행되면 객체 리터털의 객체가 생성되고 obj라는 변수는 heap영역에 실제 객체가 저장된 메모리 주소를 가리킨다.

heap영역에는 { a : 10 }이라는 값이 저장되어있는데 이러한 값은 역시 메모리 구조 내에서 a라는 key로 그 값이 10인 공간을 메모리에서 차지하는 것이다.

 

함수 역시 마찬가지이다.

함수 자체가 객체이며 함수 내부에 a = 10;  b = function() {}... 이와 같은 key와 value의 값들이 존재한다고 해보자.

함수 선언문이 작성된 스코프인 A의 평가가 진행되면 함수 객체가 생성되고 그 객체는 heap영역에 생성된다.

실제 heap 메모리 영역에 a 라는 key의 값이 10이라는 2진수 값이 저장되게 된다. 

그리고 [[Environment]]라는 내부 슬롯의 값이 상위 스코프인 A를 참조하게 된다. 

 

이러한 상황에서 A의 평가과정이 종료 된 후 A의 실행 과정으로 (런타임) 함수의 호출이 이루어진다고 해보자. 

그럼 함수의 실행 컨텍스트가 실행 컨텍스트 스택에 push 되면서 함수 코드의 '평가' 과정이 진행된다. 

이때 함수의 실행 컨텍스트 스택의 a라는 값에는 undefined라는 값이 저장된다. 

그리고 함수의 런타임 과정에서 a = 10이라는 값이 할당되게 된다. (평가와 실행과정의 상세한 내용은 7.3 실행 컨텍스트 포스팅에서 정리함)

그리고 함수의 실행이 종료되면 클로저가 아닌 경우 함수는 실행 컨텍스트 스택에서 완전히 제거된다.

 

이 과정에서 heap영역에 저장된 함수 객체와 실행 컨텍스트에서 생성되는 함수의 실행 컨텍스트의 차이점이 정리된다.

 

 

2) 마무리..

사실 모든 내용을 이해한 지금 시점에서 내가 무엇이 헷갈렸는지 정리가 잘 안된다..

내가 헷갈렸었던 점은 함수 객체가 생성되는 시점이 함수 선언문이 작성된 스코프의 평가 과정이라는 것은 알겠지만 함수 객체가 생성된다함은 정확히 무엇을 말하는 것인지 이해가 잘 안됐다. 그래서 일단은 함수 객체가 생성되구나.. 그리고 함수 객체가 생성 될 때 함수의 상위 스코프도 결정되구나 생각하고 넘어갔었다.

이렇게만 이해한 채로 함수의 평가 과정이 진행되면서 함수의 a의 값이 undefined로 저장되었다가 런타임때 그 값이 할당되는 것이 당연하게 이해되지 못했다.  함수의 객체가 이미 생성되었는데 왜 또 함수의 값들이 초기화 되었다가 값이 할당되는지 받아들여지지 못했다.

하지만 함수의 객체가 heap영역에 어떻게 생성되는지를 객체 리터럴이 어떻게 heap영역에 생성되는지에 매칭하여 생각해보니 함수역시 거창하게 들리지만 객체 리터럴과 같이 key와 value를 가지는 객체 리터럴과 다를것이 없다는 것을 생각했다. 

 

 

7. 생성자 함수

1) 생성자 함수 설명

생서자 함수가 필요한 이유는 객체 리터럴로 생성된 객체는 하나의 독립적 단위의 객체이다.

생성자 함수(빵틀)를 new 연산자를 통해 객체(빵)을 생성한다. 자바에서 클래스와 객체로 이해하면 된다. 

생성자 함수 역시 함수이다. 

일반 함수 내부에서 this의 사용은 일반적으로 사용하지 않았다. (일반함수는 this바인딩이 전역 객체이기 때문이다.)

하지만 생성자 함수의 this바인딩은 생성자 함수로 생성될 미래의 객체이다.

생성자 함수 자체 역시 일급 객체이므로 heap영역에 객체로 존재하며 이런 생서자 함수(객체)로 생성한 객체 역시 함수로써 일급 객체이다. (마찬가지로 heap영역에 저장)

 

2) 내부 메소드 [[Call]], [[Construct]]

함수 역시 객체이다. 따라서 객체가 가지는 내부 슬롯 및 내부 메소드를 함수 역시 가진다. 

함수 객체는 일반 객체가 가지는 것과 더해서 함수 객체만이 가지는 [[Environment]], [[FormalParameters]] 내부슬롯을 가지며 

[[Call]], [[Construct]] 내부 메소드를 추가로 가진다. 

함수가 일반 함수로 호출되면 함수 객체의 내부 메소드 [[Call]]가 호출된다. 

함수가 생성자 함수로 호출되면 함수 객체의 내부 메소드 [[Construct]]가 호출된다. 

 

이때 내부 메소드 [[Call]]을 가지는 함수를 callable이라고 하며 [[Construct]]를 갖는 함수를 constructor라고 하며

[[Construct]]를 가지지 않는 함수를 non-contructor라고 한다. 

 

함수 자체는 두 가지 경우가 있다. 

경우1) callable하면서 동시에 constructor 한 경우

경우2) callable 하면서 동시에 non-constructor한 경우

 

경우1은 함수가 일반 함수로 호출되면 callable로 동작하고 생성자 함수로 호출되면 constructor로 동작한다.

경우2는 함수가 일반 함수로 호출되면 callable하게 동작하지마 생성자 함수로 호출된다면 [[Construct]]를 실행해야하는데 [[Construct]] 내부 메소드가 없는 non-constructor이기 때문에 생성자 함수로는 동작할 수 없는 함수를 뜻한다. 

 

이제 경우1과 경우2에 해당하는 함수들은 어떤 함수들인지 정리할 것이다. 

 

● callable + constructor

함수 선언문, 함수 표현식, 클래스

 

● callable + non-constructor

메소드(함수라고 안함), 화살표 함수

이 경우에서 메소드란 표현이 에매할 것이다.  아래 예시 코드를 보면

const obj = {
	x: function() {}
};

new obj.x(); //오류

위와 같이 객체 내부에 선언된 저 x를 함수라 하지 않고 메소드라고 지칭한다. 

즉 함수를 프로퍼티의 값으로 사용하면 일반적으로 메소드라고 통칭한다. 

 

일반 함수와 생성자 함수의 특별한 형식적 차이는 없다. 따라서 생성자 함수는 함수의 첫 문자를 대문자로 쓰는 노력으로 일반함수와 생성자 함수를 구별하도록 (관용적으로 구별) 한다.