함수와 프로토타입 체이닝

Y·2020년 7월 20일
1

자바스크립트

목록 보기
8/20

함수 정의


자바스크립트에서 함수 생성 방법

  • 함수 선언문 (function statement)
  • 함수 표현식 (function expression)
  • 화살표 함수 표현식 ( arrow function
    expression)
  • Function() 생성자 함수

함수 리터럴


자바스크립트에서 함수도 일반 객체처럼 값으로 취급되기 때문에, 함수 리터럴을 이용해 함수를 생성 할 수 있다.

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

각 부분별로 쪼개서 알아보자!

  • function >> 함수 키워드
  • add >> 함수명
    : 함수명은 함수 몸체의 내부 코드에서 자신을 재귀적으로 호출하거나, 또는 자바스크립트 디버거가 해당 함수를 구분하는 식별자로 사용된다. 함수명은 선택 사항이다. 함수명이 없는 함수를 '익명 함수'라고 한다.
  • (x,y) >> 매개변수 리스트
    : 자바스크립트에서는 매개변수의 타입을 기술하지 않는다.
  • {return x+y;} >> 함수 몸체
    : 중괄호 내부의 코드들은 실제 함수가 호출됬을 때 실행되는 부분이다.

함수 선언문


함수 생성 방식은 함수 리터럴과 똑같지만, 함수 선언문으로 함수를 생성할 경우, 함수명이 필수적으로 정의되어있어야 한다.

function add(x,y){
  return x+y;
}
 console.log(add(3,4)); // 7

함수 표현식


함수를 변수에 할당하여 함수를 생성하는 것을 함수 표현식이라고 한다.

const add = function (x,y) {
  return x,y
}

const plus = add;
console.log(add(3,4));  // 7
console.log(plus(5,6)); // 11

함수표현식에서는 함수 이름이 선택 사항이며, 보통 사용하지 않는다.

  • add 는 함수명이 아니라 함수를 참조하는 함수변수다.

함수 표현식을 이용 할때 주의해야할 점이 있다.

const add = function sum(x,y){
  return x+y;
}
console.log(add(1,2)); //  3
console.log(sum(1,2)); // Error

함수 표현식을 사용 할때, 기명함수 표현식을 사용할 경우, 함수명은 외부에서 접근할 수 없다.
이 함수명은 주로 함수 내부에서 재귀적으로 이용되거나, 디버거 등에서 함수를 구분할 때 사용된다.

화살표 함수 표현식


화살표 함수 표현식은 함수 표현식에 비해 구문이 짧고, 자신의 this, argument, super 또는 new.target을 바인딩 하지 않는다. 따라서 화살표 함수 내부에서 arguments객체에 접근하면 외부의 함수의 arguments에 접근한다. 화살표 함수는 항상 익명이다. 메소드 함수가 아닌 곳에 가장 적합하다.

const justList= [
  'a',
  'ab',
  'abc'
];
console.log(justList.map(justList => justList.length); // Array [ 1, 2, 3]

기본적인 구문은 다음과 같다.

(param1, param2, ... , paramN) => {statements}
(param1, param2, ... , paramN) => expression
// { return expression; } 과 동일

매개변수가 하나뿐인 경우는 괄호는 안써도 된다.

자세한 내용은 여기서 더 공부 할 수 있다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/%EC%95%A0%EB%A1%9C%EC%9A%B0_%ED%8E%91%EC%85%98

Function() 생성자 함수


자바스크립트의 함수도 Function()이라는 기본 내장 생성자 함수로부터 생성된 객체라고 볼수 있다. 앞서 설명한 함수 선언문이나 함수 표현식 방식도 Funciton() 생성자 함수가 아닌 함수 리터럴 방식으로 함수를 생성하지만, 결국엔 이 또한 내부적으로는 Function() 생성자 함수로 함수가 생성된다고 볼 수 있다.

new Function (arg1, arg2, ... argN, functionBody)
  • argN - 함수의 매개변수
  • functionBody - 함수가 호출될 때 실행될 코드를 포함한 문자열
const add = new Function('x','y', 'return x+y');
console.log(add(3,4)); // 7

하지만 일반적으로 Function() 생성자 함수를 사용한 함수 생성 방법은 자주 사용되지 않는다.

함수 호이스팅


앞서 설명한 함수 생성 방식들은 동작 방식이 약간 다르다. 그중 하나가 바로 함수 호이스팅(Function Hoisting) 이다.

자바스크립트에서는 주로 함수 표현식만을 사용할 것을 권한다. 그 이유가 함수 호이스팅 때문이다.


add(4,5); // 9

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

add(2,3) // 5

맨 첫줄에서도 add 함수가 작동 하는 것을 볼 수 있다. 그 이유는, 함수 선언문으로 생성된 함수의 유효범위는 코드의 맨 처음부터 시작하기 때문이다. 이 개념이 바로 함수 호이스팅이다.

반면,

add(1,2); // Error
const add = function (x,y) {
  return x+y;
};
add(1,2); // 3

이와 같이 함수표현식으로 생성된 함수는, 첫번째 줄에서는 add 함수가 생성되기 전이므로, 에러가 발생한다. 반면, add 함수가 생성된 이후에는 정상적으로 출력된다.

함수 호이스팅 발생 원인


자바스크립트의 변수 생성(Instantiation) 과 초기화(initialization)의 작업이 분리돼서 진행되기 때문이다. 자세한 내용은 추후 글에 포스트 하겠다.

함수 객체


자바스크립트에서 함수도 역시 객체다. 동적으로 프로퍼티를 생성 할 수도 있다.

function add(x,y) {     //
  return x+y;		//   #1
}			//

add.result = add(3,2);  //   #2
add.status = 'OK';   	//   #3

console.log(add.result); // 5
console.log(add.status); // 'OK'
  • #1 : add() 함수를 생성할 때 함수 코드는 함수 객체의 [[Code]] 내부 프로퍼티에 자동으로 저장된다.
  • #2 : add() 함수에 마치 일반 객체처럼 프로퍼티를 동적으로 생성하고 , 값을 저장했다.
  • #3 : add() 함수 객체의 status 프로퍼티도 일반 객체에서의 접근 방식처럼 add.status를 이용해 접근 가능하다.

이처럼, 자바스크립트에서 함수도 일반 객체처럼 취급될 수 있기 때문에 다음과 같은 동작이 가능하다.

  • 리터럴에 의해 생성
  • 변수나 배열의 요소, 객체의 프로퍼티 등에 할당 가능
  • 함수의 인자로 전달 가능
  • 함수의 리턴값으로 리턴 가능
  • 동적으로 프로퍼티를 생성 및 할당 가능

이와 같은 특징이 있으므로 자바스크립트에서는 함수를 일급 객체 (First Class Object)라고 부른다.

함수 객체의 기본 프로퍼티


함수 객체만의 표준 프로퍼티가 정의되어 있는데, 예제를 통해 어떤 객체 형태로 되어 있는지 직접 확인해보자.

function add(x,y) {
  return x+y;
}
console.dir(add);

다음과 같은 많은 기본 프로퍼티가 존재한다.
add 함수도 역시 객체기 때문에, __proto__ 프로퍼티를 가지고있고, 이를 통해 자신의 부모 역할을 하는 프로토타입 객체를 가리킨다. 또한, add는 함수 객체의 부모역할을 하는 프로토타입객체이며, 함수 객체( Function.prototype ) 라고 한다.

prototype 프로퍼티


모든 함수는 객체로서 prototype 프로퍼티를 가지고 있다. 여기서 주의할 것은 함수 객체의 prototype 프로퍼티는 __proto__와는 다른 개념이다.

prototype 프로퍼티와 __proto__ 프로퍼티


function Add(x,y) {
  return x+y
};

const addf = new Add(1,2);
console.dir(addf);

두 프로퍼티 모두 프로토타입 객체를 가리킨다는점에서는 공통점이 있지만, 관점에 차이가 있다. 모든 객체에 있는 내부 프로퍼티 __proto__ 는 객체 입장에서 부모 역할을 하는 프로토타입 객체를 가리키는 반면, 함수 객체가 가지는 prototype 프로퍼티는 이 함수가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다. 자세한 내용은 이후에...

prototype 프로퍼티는 함수가 생성될 때 만들어지며(첫번째 __proto__), constructor 프로퍼티 하나만 있는 객체를 가리킨다. 그리고 prototype 프로퍼티가 가리키는 프로토타입 객체의 유일한 cunstructor 프로퍼티는 자신과 연결된 함수를 가리킨다. 즉, 자바스크립트에서 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 서로를 참조한다.

자세한 내용은 프로토타입체이닝 부분에서 공부할 것!

함수의 다양한 형태


콜백 함수


앞서 공부한 익명 함수의 대표적인 용도가 바로 콜백 함수이다. 콜백 함수는 코드를 통해 명시적으로 호출하는 함수가 아니라, 개발자가 단지 함수를 등록하기만 하고, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출되는 함수를 말한다. 또한, 특정 함수의 인자로 넘겨서, 코드 내부에서 호출되는 함수 또한 콜백 함수가 될 수 있다.

대표적인 콜백 함수의 사용 예가 자바스크립트에서의 이벤트 핸들러 처리다.

<body>
    <script>
        window.onload = function (){
            alert('ALERT!');

        }
    </script>
</body>

window.onlaod 이벤트 핸들러 예제 코드이다. 웹페이지가 로딩될 때, 우리가 등록한 이벤트 핸들러가 호출되면서 경고창이 뜨게 된다.

즉시 실행 함수


함수를 정의함과 동시에 바로 실행하는 함수를 즉시 실행 함수 ( immediate functions ) 라고 한다. 이 함수도 익명 함수를 응용한 형태이다. 예제를 보자.

(function (name) {
  console.log('This is the immediate function -->' + name);
})('foo');
// This is the immediate function --> foo

즉시 실행 함수를 만드는 방법은, 함수 리터럴을 괄호()로 둘러싸고, 정의함과 동시에 바로 호출될 수 있게 ()를 추가한다. 이 예제에서는 (function~)('foo'); 의 형태이다. 'foo' 값은 바로 즉시 실행 함수의 name 매개변수로 넘겨지게 된다.
주로 최초 한 번의 실행만을 필요로 하는 초기화 코드 부분 등에 사용할 수 있다.

내부 함수


자바스크립트에서는 함수 코드 내부에서도 다시 함수 정의가 가능하다. 이를 내부 함수 ( inner function ) 이라고 부른다. 내부 함수는 자바스크립트의 기능을 보다 강력하게 해주는 클로저를 생성하거나 부모 함수 코드에서 외부에서의 접근을 막고 독립적인 헬퍼 함수를 구현하는 용도 등으로 사용한다. ( 클로저의 자세한 내용은 이후 포스트에..)

// parent() 함수 정의
function parent () {
  var a = 100;
  var b = 200;
  
  // child() 내부 함수 정의
  function child(){
    var b =300;
    
  console.log(a); 
  console.log(b); 
  }
  child();
}

parent();
// 100
// 300
child();
// Error

기본적으로 내부함수에서 기억해야 할 것은,

  • 내부함수는 외부함수의 변수에 접근 할 수 있다.
  • 내부함수는 부모함수 외부에서 호출 할 수 없다.
    하지만, 클로저의 개념을 응용하면 부모함수 외부에서도 내부함수를 호출할 수 있다. 이 내용은 이후 포스트하겠다.

함수를 리턴하는 함수


자바스크립트에서는 함수도 일급 객체이므로, 일반 값처럼 함수 자체를 리턴할 수도 있다. 함수를 호출함과 동시에 다른 함수로 바꾸거나, 자기 자신을 재정의 하는 함수를 구현할 수 있다. 이처럼 자바스크립트는 유연성이 아주 높다.

var self = function() {
  console.log('a');
  return function () {
    console.log('b');
  }
}
self = self(); // a
self(); //b
  • 처음 self()함수가 호출됐을 떄는 'a'가 출력된다. 그리고 다시 self 함수 변수에 self() 함수 호출 리턴값으로 내보낸 함수가 저장된다.
  • 두번째로 self() 함수가 호출 됐을 때는 'b'가 출력된다. 즉, 첫번째 self()함수 호출 후에, self 함수 변수가 가리키는 함수가 원래 함수에서 리턴받은 새로운 함수로 변경된 것이다.

함수 호출과 this


함수의 기본적인 기능은 당연히 함수를 호출하여 코드를 실행하는 것이다. 자바스크립트에서 함수호출은 매우 자유롭다.

arguments 객체


자바스크립트에서는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지않는다. 다음 예제를 보자.

function func(arg1,arg2) {
  console.log(ar1,arg2);
}
func(); // undefined undefined
func(1); // 1 undefined
func(1,2); // 1 2
func(1,2,3); // 1 2
  • 자바스크립트에서는 인자를 어떻게 넘기더라도 함수를 호출할 때 에러가 발생하지 않는다.
  • 부족한 인수는 undefined 가 출력되고, 초과된 인자는 무시한다.

이러한 특성 때문에 함수 코드를 작성할 때 , 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해줘야 할 경우가 있다. 이를 가능 케 하는게 바로 arguments 객체다. 자바스크립트에서는 함수를 호출할 때 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달되기 때문이다. arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 의미한다. 이 객체는 실제 배열이 아닌 유사 배열 객체라는 점이다.

// add() 함수
function add(a,b) {
  //arguments 객체 출력
  console.dir(arguments);
  return a+b;
}
console.log(add(1)); //NaN
console.log(add(1,2)); // 3
console.log(add(1,2,3)); // 3 

  • 보면 유사배열객체임을 확인할 수 있고, 인자의 갯수만큼의 length를 가진다. 배열이 아닌 객체이기때문에, 배열 메서드를 쓸 수 없다는 것에 유의하자.
  • arguments 객체는 매개변수 개수가 정확하게 정해지지 않은 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 해줘야 하는 함수를 개발하는 데 유용하게 사용할 수 있다.
function sum() {
  var result = 0;
  
  for(let i=0; i < arguments.length; i++){
    result += arguments[i];
  }
  
  return result;
  
}
console.log(sum(1,2,3)); // 6
console.log(sum(1,2,3,4,5,6,7,8,9)); // 45
  • 이와 같이 인자의 개수에 따라 서로다른 처리를 해주는 함수를 구현할 수 있다.

호출 패턴과 this 바인딩


자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, 앞서 설명한 arguments 객체 및 this 인자가 함수 내부로 암묵적으로 전달된다. 여기서 특히, this 인자는 고급 자바스크립트 개발자로 거듭나려면 확실히 이해해야 하는 핵심 개념이다. this가 이해하기 어려운 이유는 자바스크립트의 여러가지 함수가 호출되는 방식(호출 패턴) 에 따라 this 가 다른 객체를 참조(this 바인딩)하기 때문이다.

객체의 메서드 호출할 때 this 바인딩


객체의 프로퍼티가 함수일 경우, 이 함수를 메서드라고 부른다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩 된다. 다음예제를 보자!

// myObject 객체 생성
var myObject = {
  name : 'foo',
  sayName : function() {
    console.log(this.name);
  }
};

// otherObject 객체 생성
var otherObject = {
  name: 'bar'
};

// otherObject.sayName() 메서드
otherObject.sayName = myObject.sayName;

// sayName() 메서드 호출
myObject.sayName(); // foo
otherObject.sayName(); // bar

  • myObject 객체와 otherObject 객체는 name 프로퍼티와 sayName() 메서드가 있다. sayName() 메서드는 this.name 값을 출력하는 간단한 함수로서 , myObjectotherObject 객체로부터 각각 호출된다. 이때 sayName() 메서드에서 사용된 this는 자신을 호출한 객체에 바인딩된다.

함수를 호출할 때 this 바인딩


자바스크립트에서는 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩된다. 브라우저에서 자바스크립트를 실행하는 경우 전역 객체는 window 객체가 된다.

전역객체란?

브라우저 환경에서 자바스크립트를 실행하는 경우, 전역 객체는 window 객체가 된다. 모든 전역변수와, 함수, 객체는 이 전역객체의 프로퍼티이다.

var foo = "I'm foo";
console.log(foo);// I'm foo
console.log(window.foo); // I'm foo

따라서 전역 변수는 전역 객체의 프로퍼티로도 접근할 수가 있다.
이제 함수를 호출할 때 this 바인딩이 어떻게 되는지를 다음 예제 코드를 살펴보자!

// 전역변수 test 선언
var test = "This is Test";
console.log(window.test);

//sayFoo() 함수
var sayFoo = function () {
  console.log(this.test);
  
// 출력값
// This is Test
// This is Test
  • test라는 전역변수를 생성했고, 전역객체인 window의 프로퍼티로 접근이 가능함을 확인할 수 있다.
  • sayFoo() 함수는 단순히 this.test를 출력하는 함수다. 자바스크립트에서 함수를 호출할때 this는 전역 객체에 바인딩된다고 했으므로, sayFoo()가 호출된 시점에서, thiswindow에 바인딩된다.
  • 여기서 주의할점은 내부함수를 호출했을 경우에도 그대로 적용되므로, 내부함수에서 this를 이용할때는 주의해야한다.

다음 예제를 통해 내부 함수의 this 바인딩 동작을 알아보자!

// 전역 변수 value 정의
var value = 100;

// myObject 객체 생성
var myObject = {
  value : 1,
  function1 : function() {
    this.value +=1 ;
    console.log('func1() called : this.value =' + this.value);
    
    // 내부함수 function2()
    function2 = function() {
      this.value +=1;
      console.log('function2() called : this.value =' +this.value);
      
      // 내부함수 function3()
      function3 = function() {
        this.value +=1;
        console.log('function3() called : this.value=' + this.value);
      }
      function3(); // 내부함수 function3() 호출
    }
    function2(); // 내부함수 function2() 호출
  }
};
myObject.function1(); // function1() 메서드 호출

// 출력값
// function1() called : this.value = 2
// function2() called : this.value = 101
// function3() called : this.value = 102
        
  • 함수호출 순서는 다음과 같다. function1() -> function2()-> function3()
  • 메서드로 호출할 때는, this는 메서드를 호출한 객체에 바인딩되므로, thismyObject객체에 바인딩된다.
  • function2()function1()을 부모 함수로 하기 때문에, thisfunction1() 과 같이 myObject에 바인딩된다고 생각할 수 있지만, 결과는 그렇지 않다.
  • 그 이유는 자바스크립트에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문이다. 내부 함수도 결국 함수이므로 이를 호출할 때는 함수 호출로 취급된다. 따라서 함수 호출 패턴에 따라 내부 함수의 this 는 전역객체에 바인딩 된다.

위의 내용을 숙지했다면, 다음 예시를 보자.

var value = 100;
var myObject ={
  value : 1,
  function1: function () {
    var that = this
    console.log('function1() called : this.value = 2' + this.value);
    
    function2 = function () {
      that.value += 1;
      console.log('function2() called : this.value =' + that.value);
      
      function3 = function () {
        that.value +=1;
        console.log('function3() called : this.value =' + that.value);
      }
      function3();
    }
    function2();
  }
};
myObject.function1(); // function1() 메서드 호출
  • 부모 함수인 function1()this 값을 새로운 변수 that 에 저장하고 내부함수에서 부모함수의 변수에 접근할 수 있는 특징을 이용하여, 내부함수에서도 function1()this가 바인딩된 객체인 myObject에 접근 가능하게 된다.

생성자 함수를 호출할 때 this 바인딩


자바스크립트의 생성자 함수는 말 그대로 자바스크립트의 객체를 생성하는 역할을 한다. 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다. 생성자 함수로 정의된 함수는 첫 글자를 대문자로 표기하는 방식이 널리 쓰인다.

자바스크립트에서는 이러한 생성자 함수를 호출할 때, 생성자 함수 코드 내부에서 this는 앞서 나온 메서드와 함수호출 방식에서의 this 바인딩과는 다르게 동작한다. 이를 이해하기 위해, 생성자 함수가 호출됐을 때 동작하는 방식을 알아보자.

생성자 함수가 동작하는 방식


new 연산자로 함수를 생성자로 호출하면, 다음과 같은 순서로 동작한다.

    1. 빈 객체 생성 및 this 바인딩
      생성자 함수 코드가 실행되기 전 빈 객체가 생성된다. 이 빈 객체가 생성자 함수가 새로 생성하는 객체이고, 이 객체는 this로 바인딩된다. 따라서, 이후에 쓰이는 this는 이 빈 객체를 가리킨다. 다만, 앞서 말했듯이 모든 객체는 prototype 프로퍼티를 가지고있다는 것을 명심해야하며, 생성자 함수가 생성한 빈 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. 즉 생성자의 프로토타입 프로퍼티가 프로토타입객체를 가리키고, 프로토타입 객체의 constructor프로퍼티가 생성자를 가리키는 순환구조의 형태이다.
    1. this를 통한 프로퍼티 생성
      이후에는 함수 코드 내부에서 this를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메서드를 생성할 수 있다.
    1. 생성된 객체 리턴
      리턴문이 동작하는 방식은 경우에 따라 다르다. 가장 일반적인 경우로 특별하게 리턴문이 없을 경우, this로 바인딩된 새로 생성한 객체가 리턴된다. (일반 함수에서는 리턴값이 없다면 undefined가 출력되니 주의) 하지만, 리턴값이 새로 생성한 객체(this)가 아닌 다른 객체를 반환하는 경우는 생성자 함수를 호출하였어도 this가 아닌 해당 객체가 리턴된다. 자세한 내용은 이후에..

예제를 통해 자세히 알아보자.

// Person() 생성자 함수
const Person = function (name) {
  // 함수 코드 실행 전 #1
  this.name = name ;
  // 함수 리턴        #2
};

// foo 객체 생성
const foo = new Person('foo')
console.log(foo.name); // foo
  • Person() 함수가 생성자로 호출되면, 함수 코드가 실행되기전(#1) 빈객체가 생성된다. 이때, 빈객체는 Person()생성자 함수의 prototype 프로퍼티가 가리키는 객체(Person.prototype 객체)를 __proto__ 로 연결해 자신의 프로토타입으로 설정한다. 그리고 이렇게 생성된 객체는 생성자 함수 코드에서 사용되느 this로 바인딩된다.

  • this가 가리키는 빈 객체에 name 이라는 동적 프로퍼티 생성

  • 리턴값이 특별히 없으므로, this로 바인딩한 객체가 생성자 함수의 리턴값으로 반환되어, foo 변수에 저장된다.

프로토타입 체이닝에대해 조금 더 직관적으로 이해하고 싶으면 https://velog.io/@sik2/JS-CoreJavaScript-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-%EC%B2%B4%EC%9D%B4%EB%8B%9DPrototype-Link-Prototype-Object 이분의 블로그를 참조하면 좋을것같다!

이러한 생성자 함수의 원리를 이용하면, 같은 형태의 여러 객체를 생성할 수 있다.

// Person() 생성자 함수
const Person = function(name,age){
  this.name = name;
  this.age = age;
};


const sam = new Person('sam','25');
const yong = new Person('yong','24');

console.log(sam);
console.log(yong);

  • 결과는 위와 같다. 같은 형태의 객체임을 확인할 수 있고, 이들의 프로토타입프로퍼티는 생성자 함수의 프로토타입 프로퍼티가 가리키는 프로토타입 객체를 가리키고 있음을 확인할 수 있다.

자세한 내용은 밑에 프로토타입 체이닝 파트에서 다룰 것이다.

call과 apply 메서드를 이용한 명시적인 this 바인딩


지금까지 본 this 바인딩은, 상황에 따라 this가 정해진 객체에 자동으로 바인딩된다는 것을 확인했다. 자바스크립트는 이러한 내부적인 this 바인딩 이외에도 this를 특정 객체에 명시적으로 바인딩시키는 방법도 제공한다. 이를 가능하게 하는 것이 바로 apply()call()메서드이다. 이 메서드들은 모든 함수의 부모객체인 function.prototype 객체의 메서드이므로, 모든 함수는 다음과 같은 형식으로 apply()메서드를 호출하는 것이 가능하다.

function.apply(thisArg, argArray)

참고로 call() 메서드는 apply()메서드와 기능이 같고 넘겨받는 인자의 형식만 다르다. 우선 apply()메서드에대해 알아보자!

  • apply()메서드를 호출하는 주체 함수 내부에서 사용될 thisthisArg 객체에 바인딩하겠다 라는 뜻이며, apply() 자체도 함수를 호출하는것이기 때문에, 함수에 넘길 인자들이 곧 argArray인 것이다.
// 생성자 함수
function Person(name,age,gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}

// foo 빈 객체 생성
const foo = {};

// apply 메서드 호출
Person.apply(foo,['foo', 30, 'man']);
console.dir(foo)

  • foo 라는 객체를 첫번째 인자로 넘겨 Person()함수에서 this로 바인딩되고, 그다음에 Person() 함수의 인자로 ['foo', 30, 'man'] 가 전달된다. 이 코드는 결국 Person('foo',30','man') 함수를 호출하면서, thisfoo 객체에 명시적으로 바인딩하는 것이다.

apply()메서드의 가장 대표적 용도는 arguments 객체에서 설명한 유사배열객체에서 배열 메서드를 사용하는 것이다.

function myFunc() {
  console.dir(arguments);
  
  const args = Array.prototype.slice.apply(arguments);
  console.dir(args);
};
myFunc(1,2,3);

  • Array.prototype.slice() 메서드를 호출하고, 메서드 내부에서 사용될 thisarguments 객체로 바인딩 시킨다.

  • 따라서, args에는 slice()메서드의 아무런 인자가 넘겨지지 않았을때 배열을 그대로 복사하는 성질을 이용하여 복사된 배열을 저장한다.

  • arguments 객체는 프로토타입객체가 Object이고, args 의 프로토타입객체가 Array인 것을 확인할 수 있다.

함수 리턴


자바스크립트 함수는 항상 리턴값을 반환한다. return 문을 사용하지 않더라도, 다음의 규칙으로 항상 리턴값을 전달하게 된다.

  1. 일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴된다.

  2. 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.

2번에서 예외가 존재하는데, 명시적으로 새로운 객체를 리턴할경우, 명시한 새로운 객체를 리턴한다. 하지만, 불린,숫자,문자열의 경우는 이러한 리턴값을 무시하고 this로 바인딩된 객체가 리턴된다.

프로토타입 체이닝


본 포스트 앞에서도 꾸준히 나왔던 프로토타입 개념. 아마 그냥 쭉 읽으면 이해가 안될수도 있다. 왜냐면 나도 책 읽으면서 그랬으니까.. 계속 자세한건 뒤에서 알아보쟤.. 이번 파트에서 정확하게 알아보자!

프로토타입의 두가지 의미


본 포스팅에서 '프로토타입(prototype) 프로퍼티'__proto__ 라고 사용한 개념에 대해서 알아보자!

자바스크립트는 C++이나 자바 같은 객체지향 프로그래밍 언어와는 다른 프로토타입 기반의 객체지향 프로그래밍을 지원한다. 이번 파트에서는 자바스크립트에서 OOP 상속에 근간이 되는 프로토타입프로토타입 체이닝의 기본 개념을 다룰 것이다.

자바스크립트에서는 타 언어의 클래스 개념이 없고, 객체 리터럴이나 생성자 함수로 객체를 생성한다. 이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체다. 부모라는 말을 쓰는것으로 보면 당연히 자식의 역할을 하는 부분도 있다는 뜻이다. 클래스 상속 개념과 마찬가지로 자식 객체는 부모 객체가 가진 프로퍼티 접근이나 메서드를 상속받아 호출하는 것이 가능하다.

앞서 계속 언급했듯 자바스크립트의 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티가 있다. ECMAScript에서는 이러한 링크를 암묵적 프로토타입 링크(Implicit Prototype Link)라고 부르며, 이러한 링크는 모든 객체의 [[Prototype]] 프로퍼티에 저장된다. 이 글에서 줄곧 언급해왔던 __proto__ 프로퍼티이다.

위의 함수 객체의 기본 프로퍼티인 prototype 프로퍼티와 혼동하면 안된다. 이 둘의 차이점을 알기 위해선 자바스크립트의 객체 생성 규칙을 알아야한다.

객체 생성 규칙


[[Prototype]] 링크 = __proto__ 프로퍼티

자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 [[Prototype]]링크로 연결한다.

다음 예제 코드를 살펴보자. Person() 생성자 함수를 정의하고, 이를 통해 foo객체를 생성하는 간단한 코드다.

// Person 생성자 함수
function Person(name) {
  this.name = name;
}

// foo 객체 생성
const foo = new Person('foo'); // #1

console.dir(Person); // #2
console.dir(foo); // #3
  • 다음 그림과 같은 상황이다.

  • Person() 생성자 함수는 prototype 프로퍼티로 자신과 링크된 프로토타입 객체를 가리킨다. Person() 생성자 함수로 생성된 foo 객체는 Person() 함수의 프로토타입 객체를 [[Prototype]]링크로 연결한다. 결국 prototype 프로퍼티와 [[Prototype]]링크 모두 같은 프로토타입 객체를 가리키고 있는 것이다. 이를 통해서 알 수 있는 것은, 객체를 생성하는 것은 생성자 함수이지만, 객체의 부모역할을 하는 건 생성자 자신이 아닌 생성자 함수의 프로토타입 프로퍼티가 가리키고 있는 프로토타입 객체이다.

객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝


객체는 자기 자신의 프로퍼티뿐만이 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 접근 가능하다. 이것이 바로 프로토타입 체이닝이다. 다음 예제를 보자!

const myObject = {
  name: 'foo',
  sayName: function () {
    console.log('My name is ' + this.name)
  }
};

myObject.sayName();
console.log(myObject.hasOwnPropety('name'));
console.log(myObject.hasOwnProperty('nickName'));
myObject.sayNickName();
// My name is Foo
// true
// false
// Uncaught TypeError: object #<Object> has no method 'sayNickName'
  • myObject 객체에는 hasOwnPropety 라는 프로퍼티가 없지만, 정상적으로 출력된다.
  • sayNickName 메서드는 myObject의 메서드로 존재하지않기 때문에 에러가 발생했지만 hasOwnProperty() 메서드는 에러가 아닌 false가 출력되는 이유가 프로토타입 체이닝 때문이다.
  • hasOwnPropety() 메서드는 이 메서드를 호출한 객체에 인자로 넘긴 문자열 이름의 프로퍼티나 메서드가 있는지 체크하는 자바스크립트의 표준 API 함수이기 때문이다.

객체 생성 파트에서 다루었듯이, 객체 리터럴 방식의 객체생성은 Object()라는 내장 생성자 함수로 생성된 것이다. 따라서, Object()생성자 함수도 함수 객체이므로 prototype 프로퍼티 속성이 있고, 위에 설명한 객체 생성 규칙의 prototype 프로퍼티와 [[Prototype]]링크의 구조가 존재한다.

  • 자바스크립트에서 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 [[Prototype]]링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색한다. 이것이 바로 프로토타입 체이닝 개념이다.

생성자 함수로 생성된 객체의 프로토타입 체이닝


생성자 함수로 객체를 생성하는 경우는 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 이뤄진다. 하지만 근본적으로 이루어지는 원칙은 똑같다.

자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수prototype 프로퍼티가 가리키는 프로토타입 객체를 자신의 프로토타입 객체(부모객체)로 취급한다. 다시한번 기억하자!

// Person() 생성자 함수
function Person(name,age) {
  this.name = name;
  this.age = age;
}

// foo 객체 생성
const foo = new Person('foo','25');

// 프로토타입 체이닝
console.log(foo.hasOwnProperty('name')); // true

// Person.prototype 객체 출력
console.dir(Person.prototype);

  • foo 객체의 생성자는 Person() 함수이다. 따라서, foo 객체의 프로토타입 객체는 Person 생성자 함수 객체의 prototype 프로퍼티가 가리키는 객체(Person.prototype) 가된다.
  • foo.hasOwnProperty()의 값이 정상적으로 출력된것은 역시 프로토타입 체이닝과정을 거쳤기 때문이다. 하지만 여기서 객체 리터럴 방식과 차이점은, Person() 생성자 함수의 프로토타입 객체에는 오직 constructor 프로퍼티 하나만 있으므로, Person.prototype 객체의 프로토타입 객체인 Object.prototype 객체에서 메서드를 찾는다.

프로토타입 체이닝의 종점


자바스크립트에서 객체는 부모역할의 프로토타입 객체를 갖고, 그 부모도 객체면 도대체 부모객체의 끝은 어딜까?

자바스크립트에서 Object.prototype 객체는 프로토타입 체이닝의 종점이다. 앞서 본 객체리터럴 방식이나, 생성자 함수 방식이나 Object.prototype 에서 프로토타입 체이닝이 끝나는 것을 알 수 있다.

이말은 즉, 모든 객체는 Object.prototype 의 프로퍼티와 메서드에 접근이 가능하고 서로 공유할 수 있다는 것이다. 때문에 자바스크립트 표준 built-in 객체인 Object.prototype에는 모든 객체에서 호출이 가능한 표준 메서드들이 정의되어 있다. ex) hasOwnProperty(),isPrototypeOf()

기본 데이터 타입 확장


객체 표준메서드 말고 기본타입들은?

기본타입인 숫자,문자열,배열 등에서 사용되는 표준 메서드들의 경우는 이들의 프로토타입인 Number.prototype, String.prototype, Array.prototype등에 정의되어 있다. 물론, 이러한 기본 내장 프로토타입 객체 또한 Object.prototype을 자신의 프로토타입으로 가지고있어서 프로토타입 체이닝으로 연결된다. ECMAScript 명세서를 보면 자바스크립트의 각 네이티브 객체별로 공통으로 제공해야 하는 메서드들을 각각의 프로토타입 객체 내에 메서드로 정의해야 한다고 기술하고 있다.

※네이티브 객체란?
네이티브 객체(Native objects or Built-in objects or Global Objects)는 ECMAScript 명세에 정의된 객체를 말하며 애플리케이션 전역의 공통 기능을 제공한다. 네이티브 객체는 애플리케이션의 환경과 관계없이 언제나 사용할 수 있다.

  • Object, String, Number, Function, Array, RegExp, Date, Math와 같은 객체 생성에 관계가 있는 함수 객체와 메소드로 구성된다.
  • 네이티브 객체를 Global Objects라고 부르기도 하는데 이것은 전역 객체(Global Object)와 다른 의미로 사용되므로 혼동에 주의하여야 한다.
  • 전역 객체(Global Object)는 모든 객체의 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미한다.

자바스크립트는 Object.prototype, String.prototype 등과 같이 표준 built-in 프로토아입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용한다. 다음 예제와 같은 코드를 작성하면, 모든 문자열에서 접근 가능한 새로운 메서드를 정의할 수 있다.

String.prototype.testMethod = function () {
  console.log('This is the String.prototype.testMethod()');
};

const str = "This is test";
str.testMethod();

console.dir(String.prototype);

  • 모든 문자열에대해서 새로운 기능을 하는 새로운 표준메서드처럼 사용할 수 있다.
  • 이것이 가능한 이유도 프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제할 수 있기 때문이다.

프로토타입 메서드와 this 바인딩


프로토타입 객체는 메서드를 가질수 있음을 확인했다(프로토타입 메서드라고 칭하겠다). 만약 프로토타입 메서드 내부에서 this를 사용한다면 이는 어디에 바인딩될 것인가?

이는 앞서 공부한 객체의 메서드를 호출할 때 this 바인딩 규칙과 같다. 메서드 호출 패턴에서의 this는 그 메서드를 호출한 객체에 바인딩 된다.

디폴트 프로토타입은 다른 객체로 변경이 가능하다


디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, 함수의 prototype 프로퍼티에 연결된다. 자바스크립트에서는 이러한 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능하다. 이러한 특징을 이용해서 객체지향의 상속을 구현한다. 이와 관련해서는 추후 포스트에 자세히 다룰 예정이다.

다만 주의할점이있는데, 생성자 함수의 디폴트 프로토타입이 변경되면 변경되기 이전에 생성된 객체는 변경되기 이전의 프로토타입 객체에 [[Prototype]]링크로 연결되고, 변경된 이후에 생성된 객체는 변경된 프로토타입 객체에 [[Prototype]]링크로 연결다는 점이다.

객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작한다.


객체의 특정 프로퍼티를 읽으려고 할 때, 프로퍼티가 해당 객체에 없는 경우 프로토타입 체이닝이 발생한다. 반대로 객체에 있는 특정 프로퍼티에 값을 쓰려고 한다면 이때는 프로토타입 체이닝이 일어나지 않는다. 지금까지 내용을 잘 이해했다면 정말 자명한 얘기다.

마무리


본 글은 송형주,고현준 "인사이드 자바스크립트 Inside Javascript (한빛미디어,2017)" 를 바탕으로 작성되었습니다.

지적 환영합니다.. 저도 공부하면서 정리한내용이라 제가 잘못 이해한부분이있을수있습니다 ㅠ

profile
연세대학교 산업공학과 웹개발 JavaScript

0개의 댓글