• [Inside JS] 2. 함수와 프로토타입 체이닝

    2019. 9. 10.

    by. 나나 (nykim)

     

     

    글과 일부 이미지의 출처는 [ 인사이드 자바스크립트(송형주, 고현준 저) ]에 있음을 밝힙니다 :-)

     

     


     

    1. 함수 정의

     

    우선 어떻게 함수를 만드는지 살펴봅시다.

     

     

    1) 함수 리터럴

     

    JS에서 함수는 뭐다? 객체다!

    객체 리터럴 방식으로 객체를 생성했듯, 함수 리터럴 방식으로 함수를 생성할 수 있습니다.

     

    function add(a,b) {
      return a+b;
    }

     

     

    위 리터럴의 구조를 살펴보면, [ function 키워드 + 함수 이름 + 매개 변수 + 함수 몸체 ] 로 이루어져있음을 알 수 있습니다.

    여기서 함수 이름과 매개변수는 선택사항입니다. JS는 이름이 없는 '익명함수'를 생성할 수가 있거든요.

     

    2) 함수 선언문

     

    함수 선언문 방식은 함수 리터럴 형태와 같은데, 함수 선언문으로 정의된 함수는 반드시 함수명이 정의되어야 있어야 합니다.

     

     

    2) 함수 표현식

     

    JS에서 함수는 뭐다? 어떠한 값이다!

    따라서 함수를 변수에다 할당하는 것도 물론 가능합니다.

    이렇게 변수에 할당해 함수를 생성하는 것을 함수 표현식이라고 합니다.

     

    var add = function(a,b) {
      return a+b;
    }
    
    var plus = add;
    
    console.log( add(1,2) ); //출력값: 3
    console.log( plus(2,4) ); //출력값: 6

     

    여기서 변수 add는 함수를 '참조'하고 있는 변수일 뿐함수 이름이 아님에 주의합시다.

    변수 add는 함수의 참조값을 갖고 있으므로 또 다른 변수인 plus에도 그 값을 그대로 할당할 수가 있습니다.

    또, 함수 표현식으로 생성된 함수는 변수 이름으로 호출합니다.

     

     

     

     

    위 예제에서 변수 add가 참조하고 있는 함수에는 이름이 없는데, 이러한 함수를 익명 함수라고 부릅니다.

    즉, 익명 함수 표현식이라고 할 수 있습니다.

     

     

    그런데 함수에 이름이 있다면요? 이 경우 기명 함수라고 합니다.

    이때, 기명 함수 표현식 사용에 있어 주의해야 할 점이 있습니다.

     

    var add = function sum(a,b) {
      return a+b;
    };
    
    console.log( add(1,2) ); //출력값: 3
    console.log( sum(2,4) ); //출력값: Uncaught ReferenceError: sum is not defined

     

    위 코드 예제에서 sum()함수를 호출하니 에러가 발생했네요;ㅁ;

    이유는 함수 표현식에서 사용된 함수 이름은 외부 코드에서 접근할 수 없기 때문입니다.

     

    함수 표현식에 사용된 함수 이름은 정의된 함수 내부에서 해당 함수를 재귀호출하거나, 디버거 등에서 함수를 구분할 때 사용됩니다.

    따라서 외부에서 호출하면 정의되어 있지 않다는 에러가 발생합니다.

    기명 함수 표현식의 예제로 재귀 호출을 잠깐 살펴봅시다.

     

    var factorialVar = function factorial(n) {
       if (n <= 1) {
          return 1;
       }
       return n * factorial(n-1); / * 함수 내부에서의 재귀호출은 factorial() 함수 이름으로 처리 가능! */
    };
    
    console.log( factorialVar(3) ); //출력값: 6
    console.log( factorial(3) ); //출력값: Uncaught ReferenceError: factorial is not defined

     

     

    음?? 그럼 함수 선언식은 어떻게 함수 이름으로 호출한 거죠??

    함수 선언문 형식으로 정의된 함수는 관대하신 JS께서 다음과 같이 바꿔주십니다.

     

    var add = function add(a,b) {
      return a+b;
    };

     

    함수 이름과 함수 변수 이름이 같으므로, 함수 이름으로 호출되는 것처럼 보이는 거죠!

    참고로, 함수 표현식 방식에서는 세미콜론(;)을 사용할 것을 권장한다고 합니다 :>

     

     

    4) Function() 생성자 방식

     

    JS에서 함수는 Function()이라는 기본 내장 생성자 함수로부터 생성된 객체라 볼 수 있습니다.

    함수 선언문이나 함수 표현식 방식도 함수 리터럴 방식으로 함수를 만들긴 하지만,

    결국엔 이 또한 내부적으로는 Function() 생성자 함수로 함수가 생성된다고 볼 수 있습니다.

     

    단, 우리가 함수를 만들 때 이 방식은 자주 안 쓴다고 합니다.

    그래서 스루할게여^_^

     

     

    5) 함수 호이스팅

     

    함수 선언문function abc(){} 또는 함수 표현식var a = function(){}으로 JS의 함수를 만들 수 있다는 걸 알았습니다.

    그런데 이들의 동작 방식에는 약간의 차이가 있는데, 그중 하나가 함수 호이스팅입니다.

    요거 때문에 함수 표현식 사용을 권장한다고 합니다 :0

     

    JS에서는 변수 생성과 초기화 작업이 분리돼서 진행됩니다.

    간단히 말하면, 내가 끝에서 선언했더라도 알아서 꼭대기에 가져다놓는 습관(...)입니다.

     

    add(5,2); //출력값: 10
    
    function add(x,y) {
      return x*y;
    }

     

    보면 함수 add가 아직 정의되지 않았는데도 호출한 것을 볼 수 있습니다.

    함수 선언문 형태로 정의한 함수의 유효범위는 코드의 맨 처음이 되는데, 이걸 함수 호이스팅이라고 합니다.

     

    이것은 함수 선언 -> 사용이라는 흐름을 무시하기 때문에 가능한 쓰지 않는 게 좋다는군요.

    그럼 함수 표현식 형태로 정의한 함수는 어떨까요?

     

    add(5,2); //출력값: uncaught type error
    
    var add = function (x,y) {
      return x*y;
    }

     

    넵. 호이스팅이 일어나지 않은 걸 볼 수 있습니다. 생성되지 않은 함수를 호출했기에 에러가 뙇 나는 모습이네요.

     

     


     

     

    2. 함수 객체: 함수도 객체다

     

    1) JS에서 함수는 객체다

     

    이전에도 썼듯이, JS에서는 함수도 객체입니다.

    즉, 함수도 프로퍼티를 가질 수 있습니드아아아앜!!!

     

    var add = function (x,y) {
       return x*y
    };
    
    add.result = add(3,2);
    add.status = 'Good';
    
    console.log(add.result); //출력값: 6
    console.log(add.status); //출력값: 'Good'

     

    위 예제에서 함수 코드는 함수 객체의 [[CODE]] 내부 프로퍼티에 자동으로 저장된 상태입니다.

    result와 status라는 프로퍼티도 일반 객체처럼 .을 이용해 접근 가능한 것을 볼 수 있습니다.

     

     

    2) JS에서 함수는 값이다

     

    다시 한 번 소리 질러~~!!! (함수는 객체다아아ㅏㅏ!!!)

    그럼 뭐다? (일반 객체처럼 취급될 수 있다!!!!)

     

    그래서 다음과 같은 동작이 가능합니다.

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

     

    이런 게 다- 되기 때문에 JS에서는 함수를 일급 객체라고 부른다고 합니다.

    아무튼 JS에서 함수는 객체란 사실을 꼭 기억해둡시다.

     

     

    2-1] 변수나 프로퍼티의 값으로 할당

     

    자, 그럼 함수를 직접 할당해보죠!

     

    /* 변수에 함수 할당 */
    var foo = 100;
    var bar = function(){ return 100; };
    console.log( bar() ); //출력값: 100
    
    /* 프로퍼티에 함수 할당 */
    var obj = {};
    obj.baz = function(){ return 200; }
    console.log( obj.baz() ); //출력값: 200

     

    변수 foo에 100이란 값을 할당하는 것처럼, 변수 bar에 함수 리터럴로 생성한 함수를 할당할 수 있습니다.

    둘의 차이점은, 변수 bar는 함수의 '참조값'을 저장하고 있다는 거죠. 그래서 bar()라고 써서 실제 함수를 호출할 수 있습니다.

    그밖에 baz처럼 객체 프로퍼티, 또는 배열의 원소 등에서 할당이 가능합니다.

    왜냐구요? 함수도 객체니까요!

     

     

    2-2] 함수 인자로 전달

     

    함수는 다른 함수의 인자로도 전달이 가능합니다. 

     

    var foo = fuction(func) {
       func(); //인자로 받은 func()함수를 호출
    };
    
    foo(function(){
       console.log('Function can be used as the argument.');
    });

     

    위 예제를 찬찬히 살펴봅시다.

     

    foo()함수는 어떤 기능을 할까요? 인자로 받은 함수를 호출하고 있습니다. 그러니까 얘는 그냥 소환사에요.

    그래서 foo()함수를 호출하고, 그 인자로서 '익명함수'를 넘겼습니다. 그 익명함수는 콘솔에다 뭐라뭐라 찍는 기능을 하죠.

    결국 foo() 함수는 콘솔에다 뭐라뭐라 찍는 함수를 인자로 전달받아서 그걸 호출하는 함수인 셈이죠!

     

     

    2-3] 리턴값으로 활용

    함수는 할당될 수 있고, 인자로 전달될 수 있다는 걸 배웠습니다. 여기에 하나 더 알아둡시다. 

    함수는 다른 함수의 리턴값으로 활용할 수도 있습니다.

     

    var foo = function(){
      return function(){
        console.log('This function is the return value.');
      };
    };
    
    foo(); //호출되지 않음
    
    var bar = foo();
    bar(); //출력값: 'This function is....'

     

     

    3) 함수 객체의 기본 프로퍼티

     

    일단 한 번 더 소리지르고 시작합시다 (함수 역시 객체다ㅏㅏㅏ!!)

    이건 함수가 일반적인 객체의 기능을 갖는 것은 물론, 호출됐을 때 정의된 코드를 실행하는 기능을 가지고 있다는 뜻이죠.

    또, 일반 객체랑은 다르게 빠스뜨끌라스잖아요? 추가로 함수 객체만의 표준 프로퍼티가 정의되어 있습니다.

     

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

     

     

     

    콘솔창을 확인해보면 add() 함수에 arguments, caller, length와 같은 여러 프로퍼티가 기본적으로 생성된 것을 확인할 수 있습니다. 이러한 프로퍼티들이 함수 생성 시 포함되는 표준 프로퍼티입니다.

     

     

    3-1] length 프로퍼티

     

    ECMA5 명세서에서는 모든 함수가 length와 prototype 프로퍼티를 가져야 한다고 합니다. 우리가 만든 add() 함수도 잘 가지고 있는 게 보이네요. 아, 함수에서 length는 함수의 정의된 인수 개수를 말합니다. (함수가 실행될 때 기대되는 인자의 개수라고도 말할 수 있겠네요!)

     

     

    3-2] prototype 프로퍼티

     

    아까 말했듯 모든 함수는 객체로서 prototype 프로퍼티를 가지고 있습니다. 이때 주의할 점은, 함수 객체의 protytpe 프로퍼티는 모든 객체의 부모를 나타내는 내부 프로퍼티인 [[Prototype]]과 다르다는 것입니다.

     

     

    헠? 이게대체몬소리져!!!

    여기서 다시 한 번 더 짚고 넘어갑시다.

     

     


     

    JS는 객체지향언어지만, 다른 객체지향언어(Java, Python 등)과는 달리 클래스 개념이 없습니다. (ES6가 돼서야 추가됐죠!)

    클래스가 없으니 상속 기능도 없지만, 대신 '프로토타입'이라는 걸 통해 상속을 흉내내도록 구현해 사용합니다.

    그래서 JS는 프로토타입 기반 언어라고 합니다.

     

    function Person(){
      this.eyes = 2;
      this.noes = 1;
    }
    
    var kim = new Person();
    var lee = new Person();
    
    console.log(kim.eyes); // 2
    console.log(lee.eyes); // 2

     

    kim과 lee는 eyes와 noes 프로퍼티를 공통으로 가지고 있는데, 메모리에는 각각 두 개씩 총 4개가 할당됩니다.

    만약 프로퍼티가 10개인데 new Person();으로 객체를 10개 만들면 100개의 변수가 메모리에 할당됩니다(...)

    이 값을 객체 하나하나가 고유로 갖지 않고, 어딘가에서 참조해서 쓰면 차아암 좋을 텐데요... 

    이러한 문제를 프로토타입으로 해결할 수 있습니다.

     

    function Person(){
    }
    
    Person.prototype.eyes = 2;
    Person.prototype.noes = 1;
    
    var kim = new Person();
    var lee = new Person();
    
    console.log(kim.noes); //1
    console.log(lee.noes); //1

     

    간단히 설명하자면 Person.prototype이라는 빈 Object가 어딘가 존재하고,

    Person 함수로부터 생성된 객체(kim, lee)는 그 어딘가의 Object에 들어있는 값을 가져다 쓸 수 있습니다.

    즉, eyes와 noes를 어딘가에 있는 빈 공간에 넣어놓고 kim과 lee가 그것을 공유하는 거죠.

     

    여기서 주의할 점! Prototype이라고 통틀어 부르는 것은 사실, Prototype Object와 Prototype Link로 나뉩니다. (띠용?)

     

     

    Prototype Object

    모든 객체는 함수로부터 파생됩니다. 위 예제에서 kim과 lee 객체도 Person()이라는 함수에서 생성됐죠.

    우리가 일반적으로 쓰는 객체도 그러합니다.

    흔히 객체를 만들 때 var obj = { }; 라고 썼을 텐데, 이는 var obj = new Object(); 와 동일합니다.

     

    Object는 JS에서 기본으로 제공하는 함수입니다.

    Function이나 Array 등도 편하게 [ ] 와 같이 쓰지만 사실 함수로 정의되어 있는 거고요.

     

    자, 여기서 좀 더 나아가보죠. 함수가 정의될 때는 다음의 2가지 일이 동시에 일어납니다.

     

     

    1. 해당 함수에 Constructor(생성자) 자격 부여

    - Constructor 자격이 부여되면 new를 통해 객체를 만들어낼 수 있습니다.

    - 이것이 함수만 new 키워드를 사용할 수 있는 이유입니다.

     

    2. 해당 함수의 Prototype Object 생성 및 연결

    - 함수를 정의하면 함수만 생성되는 게 아니라 해당 함수의 Prototype Object도 함께 생성됩니다.

    - 생성된 함수는 prototype이라는 프로퍼티를 통해 Prototype Object에 접근할 수 있습니다.

    - Prototype Object는 일반적인 객체와 같으며 기본 프로퍼티로 constructor와 __proto__를 가지고 있습니다.

     

     

    function Person(){}
    Person.prototype.eys = 2;
    var kim = new Person();
    
    console.log(kim.eyes); //2

     

    Prototype Object는 일반적인 객체이므로 속성을 마음대로 추가/삭제할 수 있습니다.

    kim은 Person() 함수를 통해 생성된 객체이므로 Person.prototype을 참조할 수 있습니다.

     

     

     

    Prototype Link

    kim에는 eyes라는 속성이 없음에도 kim.eyes를 실행하면 2라는 값을 참조합니다.

    Prototype Object에 존재하는 eyes 속성을 참조한 거죠.

    이는 kim이 가진 프로퍼티, __proto__ 덕분입니다.

     

    prototype 프로퍼티는 오로지 함수만이 가지지만,

    __proto__ 프로퍼티는 모든 객체가 가집니다.

     

    __proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킵니다.

    kim 객체는 Person 함수로부터 생성되었으니, kim의 __proto__ 프로퍼티는 Person 함수의 Prototype Object를 가리킵니다.

     

    kim.__proto__ === Person.prototype //true

     

    kim 객체가 eyes를 가지고 있지 않기 때문에 eyes 속성을 찾을 때까지 상위 프로토타입을 탐색합니다.

    만약 최상위인 Object의 Prototype Object에서도 해당 속성을 찾지 못하면 undefined를 리턴합니다.

    이처럼 __proto__ 속성을 통해 상위 프로토타입과 연결되어 있는 형태를 프로토타입 체인(Prototype Chain)이라 합니다.

     

     

    위 내용을 정리해보겠습니다.

     

    * 프로토타입 오브젝트

    • 함수 정의 시, 함수와 동시에 생성되는 객체에요.
    • constructor라는 고유의 프로퍼티를 가져요. 동시에 __proto__ 프로퍼티도 기본으로 가져요 (얘도 객체거든요!)
    • 생성된 함수는 prototype 프로퍼티로 이 프로토타입 오브젝트를 참조(접근)해요
    • 그리고 얘는 constructor 프로퍼티로 생성된 함수를 참조해요

     

    * 프로토타입 링크

    • 함수를 포함한 모든 객체가 가져요.
    • 부모 역할을 하는 객체를 __proto__ 프로퍼티를 통해 참조(접근)할 수 있어요.
    • 그래서 부모 역할을 하는 객체의 속성을 쓸 수 있어요.
    • 부모에게도 찾는 프로퍼티가 없으면 __proto__를 통해 조상까지 거슬러 올라가요.

     

     

     


     

    3. 함수의 다양한 형태

     

    1) 콜백함수

     

    JS의 함수 표현식에서 함수 이름은 꼭 붙이지 않아도 됩니다.

    이렇게 이름 없는 함수를 익명 함수라고 하는데, 이러한 익명 함수는 보통 콜백 함수로 쓰입니다.

     

    콜백 함수는 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때, 시스템에서 호출되는 함수를 말합니다.

    또한, 특정 함수의 인자로 넘겨서 코드 내부에서 호출되는 함수 또한 콜백 함수가 될 수 있습니다.

     

    대표적인 예가 이벤트 핸들러 처리입니다. 이벤트가 로드되거나 키보드가 입력되는 등의 DOM 이벤트가 발생했을 때, 브라우저는 정의된 DOM 이벤트에 해당하는 이벤트 핸들러를 실행시킵니다.

     

    window.onload = function(){ //이벤트 발생 시 이벤트 핸들러가 alert 함수 호출
        alert("Hi!");
    };

     

    콜백함수는 함수 내부의 처리 결과값을 함수 외부로 내보낼 때 사용합니다(return문과 비슷하죠!).

    어떤 함수의 처리 결과를 콜백 함수의 매개변수에 담고, 그 콜백 함수를 호출하여 사용하는데 이렇게 하면 '로직 구현 부분'과 '로직 처리 부분'을 나눌 수 있습니다.

     

    const sum = (num1, num2, callback) => {
    	let sum = num1 + num2; //로직 "구현"
    	callback(sum); //로직 "처리"
    }
    
    const result = val => {
    	(val > 100) ? console.log("합계는 "+val+"이며 100을 넘습니다.") : console.log("합계는 "+val+"이며 100을 넘지 않습니다.");
    }
    
    sum(23,147,result);
    //[출력] "합계는 170이며 100을 넘습니다."
    

     

     

    2) 즉시 실행 함수

     

    함수를 정의함과 동시에 바로 실행되는 함수를 즉시 실행 함수라고 합니다.

     

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

     

    즉시 실행 함수는 같은 함수를 다시 호출할 수 없습니다. 즉, 최초 한 번만 실행하면 되는 초기화 코드 부분 등에 사용할 수 있습니다.

     

    그리고 또 하나, jQuery의 소스코드도 즉시 실행 함수로 감싸져 있습니다. 왜냐하면 JS의 변수 유효범위 특성 때문입니다.

    자바스크립트는 Function-scope죠. 따라서, 라이브러리 내의 변수 등이 전역 네임스페이스를 침범하지 않도록(충돌이 일어나지 않도록) 즉시 실행 함수로 감싸고 있는 것입니다.

     

     

     

    3) 내부 함수

     

    function parent(){
       var a = 100;
       var b = 200;
    
       //child() 내부 함수 정의
       function child(){
          var b = 300;
          console.log(a); //[출력] 100
          console.log(b); //[출력] 300
       }
       child();
    
       console.log(a); //[출력] 100
       console.log(b); //[출력] 200
    }
    
    parent();
    child(); //[출력] 에러 뙇!!!

     

    내부 함수는 어떠한 함수 내에서만 사용 가능합니다.

    또, 내부 함수에서는 자신을 둘러싼 부모 함수의 변수에 접근이 가능합니다.

     

    child()에서 먼저 지역변수 내에 a가 있는지 탐색합니다. 없다면 자신을 호출한 부모 함수에게서 a 변수를 찾습니다.

    그래도 없다면 전역에서 찾게 됩니다.

    변수 b의 경우 childe()내 지역변수로 존재하기 때문에 굳이 부모 찾아 떠나지 않은 거고요(...)

    child() 함수를 호출한 뒤 a와 b의 값을 살펴보면 b는 그대로 200임을 알 수 있습니다. 왜냐면 JS에서 scope는 함수 단위이기 때문입니다. 함수 내에서 선언된 var는 지역변수로 함수가 종료되면 뿅 그걸로 끝입니다.

    마지막에 child()를 호출했을 때 에러가 뙇!! 나는 것도 그 이유입니다. 함수 스코프 밖에서는 함수 스코프 안에 선언된 모든 변수나 함수에 접근할 수가 없거든요.

    반대로 함수 내부에서 함수 밖에 선언된 변수에 접근할 수 있던 건 JS의 스코프 체이닝 덕분입니다.

     

    음.. 그런데 사실 함수 외부에서도 특정 함수 스코프 안에 선언된 내부 함수를 호출할 수 있긴 합니다.

    부모 함수에서 내부 함수를 외부로 리턴하면 되거든요.

    부모 함수 스코프의 변수를 참조하는 함수를 클로저라고 하는데, 이건 후에 배워보도록 하겠습니다 :-D

     

     

    4) 함수를 리턴하는 함수

     

    함수도 객체이므로 일반 값처럼 함수 자체를 리턴할 수도 있습니다. 이렇게 하면 함수를 호출함과 동시에 다른 함수로 바꾸거나, 자기 자신을 재정의하는 함수를 구현할 수 있습니다.

     

    var self = function(){
       console.log('a');
       return function(){
         console.log('b');
       }
    };
    
    self = self(); //a
    self(); //b

     

    처음 self() 함수가 호출됐을 때는 a가 출력되고, 다시 self 함수 변수에 self 함수 호출 리턴값으로 내보낸 함수가 저장됩니다. 따라서 두 번째 호출했을 때는 b가 출력됩니다. self 함수 변수가 가리키는 함수가 원래 함수에서 리턴받은 새로운 함수로 변경됐기 때문입니다.

     

     


     

     

    4. 함수 호출과 this

     

    1) arguments 객체

     

    JS는 굉장히 유연합니다. 그래서 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지 않습니다.

     

    function func(arg1, arg2){
       console.log(arg1, arg2);
    }
    
    func(); //undefined undefined
    func(1); //1 undeifned
    func(1, 2); //1 2
    func(1, 2, 3);  //1 2 

     

    정의된 매개변수 수에 맞춰 인자를 넘기지 않더라도 에러 없이 실행된 모습입니다.

    넘겨지지 않은 인자에는 undeinfed 값이 할당됐고, 초과된 인자는 무시됐네요.

     

    JS의 이러한 특성 때문에 함수 코드를 작성할 때, 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해줘야 할 경우가 있습니다. 이때 필요한 게 arguments 객체입니다.

    JS에서 함수를 호출하면 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달됩니다.

     

    arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 말하는데,

    이 객체는 배열처럼 생겼지만 배열은 아니고 유사 배열 객체입니다.

     

    function add(a, b){
      console.dir(arguments);
      return a + b;
    }
    
    console.log(add(1)); //Arguments(1), NaN
    console.log(add(1,2)); //Arguments(2), 3
    console.log(add(1,2,3)); //Arguments(3), 3
    

     

    다음은 위의 add 함수를 콘솔창에서 실행한 결과입니다.

     

     

    arguments 객체가 호출 시 넘겨진 인자를 배열 형태로 저장하고 있음을 알 수 있습니다.

    length 프로퍼티는 넘겨진 인자 개수를 뜻하고,

    callee 프로퍼티는 현재 실행 중인 함수의 참조값입니다.

     

    arguments 객체를 이용하면 매개변수 개수가 정해지지 않은 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 하는 함수를 구현할 때 유용합니다.

     

    //인자 개수에 상관없이 이들 각각의 값을 모두 더해 리턴하는 함수
    function sum(){
       var result = 0;
    
       for (var 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

     

    arguments 객체는 배열처럼 생겼지만 배열이 아니라고 했는데요, 그 말은 즉슨 배열 메서드를 사용할 수 없습니다.

    사용케 하는 방법도 있지만 그건 나중에 배워봅죠!

     

     

    2) 호출 패턴과 this 바인딩

     

    JS에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, 앞서 설명한 arguments 객체 및 this 인자가 함수 내부로 전달됩니다. 특히 this 인자가 중요한데요, 함수 호출 방식에 따라 this가 다른 객체를 참조하기 때문입니다.

     

     

    2-1] 객체의 메서드 호출 시의 this 바인딩

     

    객체의 프로퍼티가 함수면 뭐죠? 메서드입니다.

    메서드 호출 시 메서드 내부 코드에서 사용된 this는, 해당 메서드를 호출한 객체로 바인딩됩니다.

     

    var myWebsite = {
      owner: 'nykim',
      whois: function() {
        console.log(this.owner + "'s website.");
      }
    }
    
    var yourWebsite = {
    	owner: 'yeonme'
    }
    
    yourWebsite.whois = myWebsite.whois;
    		
    myWebsite.whois(); //nykim's website.
    yourWebsite.whois(); //yeonme's website.

     

     

     

     

    2-2] 함수 호출 시의 this 바인딩

     

    JS에서는 함수 호출 시 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩됩니다.

    브라우저에서 자바스크립트를 실행하는 경우에 전역 객체는 window 객체가 됩니다.

    (음, 그럼 브라우저가 아닌 경우... 그러니까 Node.js 같은 경우에는 전역 객체는 global 객체가 되는데 이 부분은 스루할게요!)

     

    결국 자바스크립트의 모든 전역 변수는, 전역 객체의 프로퍼티라고 할 수 있습니다.

     

    var foo = "NY KIM";
    console.log(foo === window.foo); //출력: TRUE

     

    함수를 호출할 때 this 바인딩이 어떻게 되는지 살펴봅시다.

     

    var fusrodah = "FUS... RO DAH!!!!!";
    console.log(window.fusrodah);
    
    var sayFusroDah = function () {
      console.log(this.fusrodah);
    }
    
    sayFusroDah();
    
    /*출력:
     FUS... RO DAH!!!!!
     FUS... RO DAH!!!!!
    */

     

    위 예제에서 우리는 전역 변수인 fusrodah를 선언했습니다. 그래서 window의 프로퍼티로 접근할 수가 있죠.

    한편 그 다음엔 함수를 만들어 호출했습니다. 자, 함수를 호출할 때 this는 어디에 바인딩 된다고 했나요? 전역 객체입니다.

    함수 호출 시에 this는 전역 객체에 바인딩 되고, 여기선 window가 전역 객체이므로 값이 잘 출력된 거죠.

    우왕 자바스크립트님 짱 자비로워

     

    하지만 이러한 바인딩 특성은 내부 함수를 호출했을 때도 그대로 적용됩니다.

     

    //전역 변수 정의
    var value = 100;
    
    //myObject 객체 생성
    var myObject = {
      value: 1,
      func1: function () {
        this.value += 1;
        console.log("함수1 실행되었으며 value 값은 " + this.value);
    
        func2 = function () {
          this.value += 1;
          console.log("함수2 실행되었으며 value 값은 " + this.value);
    
          func3 = function () {
            this.value += 1;
            console.log("함수3 실행되었으며 value 값은 " + this.value);
          }
          func3();
    
        }
        func2();
      }
    };
    
    myObject.func1();

     

    위 코드의 실행 결과는 무엇일까요?

    대충 생각해봤을 때 1+1=2, 2+1=3, 3+1=4니까 [2,3,4]라는 값이 나오지 않을까 싶은데요...

     

     

    실제 출력결과는 이렇습니다.

     

    함수1 실행되었으며 value 값은 2
    함수2 실행되었으며 value 값은 101
    함수3 실행되었으며 value 값은 102

     

    ...도대체 101과 102는 어디서 튀어나온 걸까요?

     

    차근차근 살펴봅시다.

    우선 왜 이따구(?)로 동작했냐 하면, JS에서는 내부 함수 호출 패턴이 정의되어 있지 않습니다.

    달리 말하면 내부 함수 호출을 특별 취급하지 않아서, 일반 함수 호출과 똑같이 취급합니다.

     

    아까 함수 호출 시 this는 어디에 바인딩된다고 했죠? 전역 객체, 그러니까 window겠군요.

    엌... 그러고보니 우리가 이미 window.value로 만들어놓은 애가 있었죠! 그 값은 100이었구요!!

     

    그래서 func2()가 호출될 때부터 this는 myObject.value가 아니라 window.value를 참조하고 있었던 셈입니다.

     

     

     

    럴수럴수가.

    그럼 어떻게 내부 함수가 부모 함수의 this를 참조할 수 있게 하죠?

    간단합니다. this를 딴 데다 저장하면 되죠!

    보통 이렇게 this 값을 저장하는 변수는 이름을 that 또는 self 라고 짓습니다.

     

    그래서 저 위의 코드가 [2,3,4]를 출력하려면 이렇게 바꿔주면 됩니다.

     

    //전역 변수 정의
    var value = 100;
    
    //myObject 객체 생성
    var myObject = {
      value: 1,
      func1: function () {
        var that = this;
        that.value += 1;
        console.log("함수1 실행되었으며 value 값은 " + this.value);
    
        func2 = function () {
          that.value += 1;
          console.log("함수2 실행되었으며 value 값은 " + that.value);
    
          func3 = function () {
            that.value += 1;
            console.log("함수3 실행되었으며 value 값은 " + that.value);
          }
          func3();
    
        }
        func2();
      }
    };
    
    myObject.func1();

     

    this.value에서 that.value로 바꿔버렸습니다.

    이렇게 하면 내부 함수는 부모 함수의 변수에 접근할 수 있죠.

    그래서 func2()도 func3()도 that 변수를 통해 func1()의 this가 바인딩된 객체인 myObject에 접근할 수 있게 됩니다.

     

    이와 같은 this 바인딩의 한계를 극복하고자, this 바인딩을 명시적으로 설정하는 것도 가능합니다.

    바로 call 과 apply 메서드인데요, 이건 나중에 살펴보도록 하죠.

     

     

    2-3] 생성자 함수를 호출할 때 this 바인딩

     

    자바스크립트에서는 생성자 함수에 대한 특별한 형식이 없습니다.

    그냥 아무 함수에다가 new 연산자를 붙이면 그 함수는 그때부터 생성자 함수가 됩니다.

    그런데 이건 원치않는 함수가 생성자 함수로 작동할 수 있다는 문제점도 있죠.

    따라서 생성자 함수는 함수 이름의 첫 글자를 대문자로 작성합니다.

     

    생성자 함수는 아래와 같이 동작합니다.

     

    1. 빈 객체 생성 및 this 바인딩

     - 생성자 함수는 새로운 빈 객체를 생성하는데 이 객체는 this로 바인딩 됩니다. 따라서 이후 생성자 함수 코드 내부의 this는 이 빈 객체를 가리킵니다.

     - 이 새로운 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정합니다.

     

    2. this를 통한 프로퍼티 생성

      - 이후에도 함수 코드 내부에서 this를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메서드를 생성할 수 있습니다.

     

    3. 생성된 객체 리턴

      - 리턴문이 특별히 없을 경우에는 this로 바인딩된 새로 생성된 객체가 리턴됩니다.

        (생성자 함수가 아닌, 일반 함수의 경우 리턴값이 명시되어 있지 않으면 undefined를 리턴하는 것과 다르죠!)

     

    //Person() 생성자 함수
    var Person = function (name) {
      // 함수 코드 실행 전
      this.name = name;
      // 함수 리턴
    }
    
    //객체 생성
    var nykim = new Person("nykim");
    console.log(nykim.name);

     

     

    Person() 함수가 생성자로 호출되면, 빈 객체가 생성됩니다. 이 객체는 Person() 생성자 함수의 prototype 프로퍼티가 가리키는 객체(Person.prototype 객체)를 [[Prototype]]( = __proto__ )으로 연결해서 자신의 프로토타입으로 설정합니다. 그리고 이렇게 생성된 객체는 생성자 함수 코드에서 사용되는 this로 바인딩됩니다.

    위 예제에서 리턴값을 명시하지 않았기 때문에 this로 바인딩한 객체가 생성자 함수릐 리턴값으로 반환되어 nykim 변수에 저장된 거죠.

     

    앗, 그럼 이런 생각이 드네요.

    객체 리터럴 방식과 생성자 함수로 생성된 함수의 차이는 뭘까요?

     

     

    👉객체 리터럴 방식과 생성자 함수로 생성된 함수의 차이

    리터럴 방식은 같은 형태의 객체를 재생성할 수 없습니다.

    하지만 생성자 함수는 다른 인자를 넘겨서 같은 형태의 서로 다른 객체를 생성할 수 있죠.

     

    또, 리터럴 방식으로 생성된 객체의 프로토타입 객체는 Object입니다. (더 정확히는 Object.prototype이죠)

    반면에 생성자 함수로 만들어진 객체, 이를 테면 위 예제 nykim의 프로토타입 객체는 생성자 함수인 Person()입니다. (더 정확히는 Person.prototype이고요)

    프로토타입 객체가 다른 이유는? 생성자 함수가 다르기 때문입니다. 리터럴 방식의 생성자 함수는 Object()니까요.

     

     

    👉생성자 함수를 new 없이 쓴다면?

    그나저나 생성자 함수를 new 없이 호출한다면 무슨 일이 일어날까요?

    뭐... 엄청난 큰일이 발생하는 건 아니지만 대충 이런 상황이 생깁니다.

     

    //Person() 생성자 함수
    var Person = function (name) {
      // 함수 코드 실행 전
      this.name = name;
      // 함수 리턴
    }
    
    //객체 생성
    var anne = Person("anne");
    console.log(anne); //출력: undefined
    console.log(anne.name); //출력: Cannot read property
    console.log(window.name); //출력: anne

     

    new 없이 호출하니 anne 객체를 찾을 수 없다고 나오고, window.name이란 프로퍼티가 생겨버렸네요...!!

     

    자, 우리가 뭔 짓을 했는지 돌이켜봅시다. new가 없으니 우리가 한 건 그냥 일반 함수 호출과 같습니다.

    아까 배운 내용을 떠올려봅니다. 일반적인 함수 호출에서 this는 어디에 바인딩된다고 했죠?

    스크롤을 쬐끔만 올려서 확인해봤더니, 바로 전역 객체였습니다.

    그래서 전역 객체인 window에 name 프로퍼티를 생성해버린 거죠.

     

    그럼 왜 anne 객체는 undefined인 걸까요?

    우리가 만든 Person() 함수에는 리턴값이 없습니다. 생성자 함수의 경우 별도 리턴값이 없다면 새로 생성된 객체가 리턴될 테죠. 하지만 new가 없었으니 일반 함수 호출로 동작했고, 따라서 리턴값이 undefined가 된 것입니다.

     

     

     

    👉강제로 인스턴스 생성하기

    우리가 아무리 주의를 기울인다고 한들, 생성자 함수를 new 없이 호출해버리는 상황이 생길 수 있습니다. 그럼 전역 객체에 this가 바운딩되면서... 어마어마한 일이 발생할 수도 있겠죠. 이런 문제를 해결하고자 만든 패턴이 있습니다.

     

    function A(arg) {
      if (!(this instanceof A)) {
        return new A(arg);
      }
      this.value = arg ? arg : 0;
    }
    
    var a = new A(100);
    var aa = A(10);
    
    console.log(a.value); //출력: 100
    console.log(aa.value); //출력: 10

     

    함수 A에서는 A가 호출될 때 this가 인스턴스인지를 확인하는 분기문을 추가했습니다. 만약 저 조건에 들어맞는다면, 인스턴스가 아니라는 거고, 그 소리는 new 로 호출되지 않았음을 의미합니다. 그래서 new로 A를 호출하여 반환하게 하였죠.

    따라서 A(10); 과 같이 new 없이 호출하더라도 새 인스턴스가 무사히 생성되어 aa에 반환되었습니다.

     

    function B(arg) {
      if (!(this instanceof arguments.callee)) {
        return new B(arg);
      }
      this.value = arg ? arg : 0;
    }
    
    var b = new A();
    var bb = A(5);
    
    console.log(b.value);
    console.log(bb.value);

     

    또는, 함수명을 직접 쓰는 대신 이렇게 사용할 수도 있습니다. arguments.callee는 호출된 함수를 가리키거든요!

     

     

     

     

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

     

    지금까지 내용을 살펴보면, 함수 호출이 발생하는 상황에 따라 this가 정해진 객체에 바인딩된다는 것을 확인했습니다. 그런데 이런 내부적인 바인딩 외에도, this를 특정 객체에 명시적으로 바인딩할 수도 있습니다.

    바로 apply()와 call() 메서드입니다. 이 메서드는 모든 함수의 부모 객체라 할 수 있는 Function.prototype 객체의 메서드이기 때문에, 모든 함수가 호출해서 쓸 수 있습니다.

    아, call()과 apply()는 같은 기능입니다! 넘겨받는 인자의 형식만 다를 뿐이에요.

     

    우선 apply()를 살펴봅시다.

     

    function.apply(thisArg, argArray)

     

    여기서 주의할 점은, apply() 메서드를 호출하는 주체가 함수라는 것입니다. 결국 본질적으로는 함수를 호출하는 셈이죠. 그래서 Person.apply()는 Person()을 호출하는 게 됩니다.

     

    첫 번째 인자인 thisArg는 apply()를 호출한 함수 내부에서 사용한 this에 바인딩할 객체를 가리킵니다. 즉, 첫 번째 인자로 넘긴 객체가 this로 바인딩됩니다. 두 번째 argArray는 함수를 호출할 때 넘길 인자들의 배열을 가리킵니다.

     

    apply()의 기능을 정리하자면, 

     

    • 함수를 호출한다.
    • 1번째 인자(thisArg 객체)를 함수 내부에서 사용된 this로 바인딩한다.
    • 2번째 인자(argArray 배열)를 자신을 호출한 함수의 인자로 사용한다.

    인 것입니다.

     

    //생성자 함수
    function Person(name, age, gender) {
      this.name = name;
      this.age = age;
      this.gender = gender;
    }
    
    //빈 객체 생성
    var nykim = {};
    
    //apply() 메서드 호출
    Person.apply(nykim, ["Na-young", 27, "Female"]);
    console.table(nykim);

     

     

    call() 메서드는 apply()와 기능은 같지만, 두 번째 인자를 배열 형태가 아니라 각각 하나의 인자로 넘깁니다.

     

    Person.call(nykim, "Na-young", 27, "Female");

     

    그럼 이 메서드들을 언제 사용하면 좋을까요?

    저어어어어 위에서 잠깐 언급했던, 유사 배열 객체에서 배열 메서드를 사용할 때 사용합니다.

    arguments 객체 기억나시나요? 

     

    ~회상 중~

    arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 말하는데,
    이 객체는 배열처럼 생겼지만 배열은 아니고 유사 배열 객체입니다.

    ~회상 끝~

     

    arguments 객체는 실제 배열이 아니므로, pop()이나 shift() 같은 표준 배열 메서드를 사용할 수 없습니다.

    하지만 배열 메서드를 사용하고 싶다!! 그럴 때 apply()를 써주면 됩니다.

     

    function myFunction() {
      console.dir(arguments);
    
      // arguments.shift(); //Uncaught TypeError: arguments.shift is not a function
    
      var args = Array.prototype.slice.apply(arguments);
      args.shift(); //[2,3]
      console.dir(args);
    }
    
    myFunction(1, 2, 3);

     

     

    shift() 메서드는 배열의 첫 번째 원소를 삭제하는데, arguments 객체는 유사 배열이라서 이 메서드를 못 씁니다. 그래서 에러가 뙇 뜨죠. 이럴 때 쓴 게 Array.prototype.slice.apply(arguments)입니다. 이 코드를 분석해볼게요!

     

    먼저 Array.prototype은 모든 배열 객체의 부모라 할 수 있는, 자바스크립트 기본 프로토타입 객체입니다. 그래서 배열 표준 메서드를 흥청망청 쓸 수가 있죠. 

    한편, slice(start, end) 메서드는 이 메서드를 호출한 배열의 start 인덱스에서 end-1 인덱스까지 복사한 배열을 리턴합니다. end 인자가 없으면 배열의 length 길이 값이 되고, 인자를 아무것도 넘기지 않으면 배열 전체가 복사됩니다.

     

    var arr = [1,2,3];
    arr.slice(); //[1,2,3]
    arr.slice(1); //[2,3]
    arr.slice(1,2); //[2]
    

     

    Array.prototype.slice.apply(arguments) ← 이 코드에서는 slice() 메서드에 아무 인자도 넘기지 않았습니다. 그러니 이 메서드를 호출한 배열(여기서는 arguments 객체)을 복사한 새로운 배열을 생성하게 됩니다.

     

     

     

    3) 함수 리턴

     

    자바스크립트에서 함수는 항상 리턴값을 반환합니다. 설령 return문이 없더라도, 묻지도 따지지도 않고 다음의 규칙이 적용됩니다.

    I got old rules, I count 'em~ 

     

     

    3-1] One: 일반 함수나 메서드는 지정된 리턴값이 없다면 undefined를 리턴한다

     

    var noReturn = function () {
      console.log("리턴을 명시하지 않았다...")
    };
    
    var result = noReturn();
    console.log(result); //출력: undefined

     

     

    3-2] Two: 생성자 함수는 지정된 리턴값이 없다면 생성된 객체를 리턴한다

     

    생성자 함수에서 별도 리턴값이 없다면 this로 바인딩된 새로 생성된 객체가 리턴됩니다. 이건 아까 살펴본 내용이죠! 따라서 생성자 함수에서는 보통 리턴값을 지정하지 않습니다.

    다만 리턴값을 명시한 경우, 그 리턴값이 객체일 때만 해당 값이 리턴됩니다. 

     

    function Person(name, age) {
      this.name = name;
      this.age = age;
    
      return {
        name: "Anonymous",
        age: 99
      }
    }
    
    var result = new Person("nana", 27);
    console.log(result); //리턴값: "Anonymous" 객체

     

    위 코드는 리턴값을 객체 리터럴 방식의 특정 객체로 지정한 예입니다.

    콘솔을 확인해보면, 리턴값에서 명시적으로 넘긴 객체가 리턴된 것을 볼 수 있습니다.

     

    단, 지정한 리턴값이 불린, 숫자, 문자열일 경우 무시하고 this로 바인딩된 객체가 리턴됩니다.

     

    function Person(name, age) {
      this.name = name;
      this.age = age;
    
      return 100;
    }
    
    var result = new Person("nana", 27);
    console.log(result); //리턴값: "nana" 객체

     


     

     

    5. 프로토타입 체이닝

     

    1) 프로토타입의 두 가지 의미

     

    JS는 C++이나 JAVA와 다르게, 프로토타입 기반의 객체지향 프로그래밍을 지원합니다.

    이게 몬소리냐고 한다면 저도 몰라요 @.@

    그러니 그걸 지금부터 배워봅시다 허허허

     

    자, JAVA는 클래스를 정의한 다음 이를 통해 객체를 생성합니다. 하지만 기본 자바스크립트에는 클래스 개념이 없습니다.

    대신 객체 리터럴이나 생성자 함수를 통해 객체를 생성하죠. 이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체입니다. 자식 객체는 부모 객체가 가진 프로퍼티 접근이나 메서드를 상속받아 호출할 수가 있습니다.

     

    앞에서도 계속 말했지만, 모든 객체는 프로토타입 객체를 가리키는 숨겨진 프로퍼티가 있습니다. 이걸 무슨.. 암묵적 프로토타입 링크? 라고 부른다네요. 뭔가 어둠의 느낌.

    아무튼 이 링크를 지금부터 [[Prototype]] 링크 또는 __proto__라고 부르겠습니다. EMCAScript에서는 [[Prototype]] 프로퍼티로 정하고 내부적으로 사용된다고 명시하는데, 크롬 등에서는 __proto__ 프로퍼티라고 명시적으로 제공하고 있습니다.

     

    여기서 주의할 점! 이 [[Prototype]] 링크를, 함수만이 갖는 프로퍼티인 prototype 프로퍼티와 헷갈리지 않아야 한다는 점입니다.

     

    이제 몇 번을 짚고 넘어가는지 모르겠지만 또 짚고 넘어갑시다.

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

    결국 객체를 생성하는 건 생성자 함수지만, 생성된 객체의 실제 부모 역할을 하는 건 생성자의 prototype 프로퍼티가 가리키는 프로토타입 객체라는 거죠.

     

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    var me = new Person("nana", 27);
    
    console.log(Person.prototype === me.__proto__); //출력: true
    

     

    그림으로 표현하면 아래와 같습니다.

     

     

     

     

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

     

    자식 객체들이 부모 역할을 하는 프로토타입 객체의 프로퍼티를 제껏마냥 가져다 쓸 수 있는 이유는?! 바로 프로토타입 체이닝 덕분입니다.

     

    var myObject = {
      name: "nayoung",
      sayName: function () {
        console.log("My name is " + this.name);
      }
    };
    
    console.dir(myObject.__proto__);
    myObject.sayName(); //출력: My Name is nayoung
    console.log(myObject.hasOwnProperty("name")); //true
    console.log(myObject.hasOwnProperty("nickName")); //false
    myObject.sayNickName(); //Uncaught TypeError
    console.log(myObject.lalala); //undefined

     

     

    위 예제를 보면, myObject가 가진 프로퍼티는 name이랑 sayName 뿐입니다.

    그런데 hasOwnProperty라는 메서드를 잘 호출해서 잘 썼습니다. 어떻게 가능했을까요?

     

     

    숨겨진 프로퍼티인 __proto__를 눌러 확인해보면, 부모로부터 스윽 빌려서 호출했음을 알 수 있습니다.

     

     

    모두가 다 아는 사실이지만, 객체 리터럴로 생성한 객체는 사실 Object() 라는 내장 생성자 함수로 만들어진 것입니다. Object() 생성자도 함수이므로 prototype 프로퍼티가 있겠죠. 따라서 우리가 만든 myObject는 Object()가 prototype 프로퍼티로 연결하고 있는 객체를 [[Prototype]] 링크로 연결하고 있을 겁니다.

    ...써놓고보니까 대략 멍해지는 설명인데, 단순 요약하면 이렇습니다 : myObject는 __proto__를 가지고 Object().prototype에 접근해서 프로퍼티를 빌려썼다!

     

    결국 프로토타입 체이닝이란 건, [[Prototype]] 링크를 따라 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색하는 것을 말합니다.

     

    예제에서 lalala를 콘솔에 찍었을 때 undefined가 출력되고, sayNickName() 메서드를 호출했을 때 에러가 난 이유는 바로 이 때문입니다. 아무리 부모를 거슬러 올라가 검색해봐도 어떤 객체에도 해당 프로퍼티가 없었기 때문이죠.

     

     

     

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

     

    생성자 함수로 생성한 객체는 살짝 다른 방식으로 프로토타입 체이닝이 이뤄집니다.

    하지만 아래의 기본원칙은 동일합니다.

    "모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체(부모 객체)로 취급한다."

     

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    //생성자 함수로 객체 생성
    var nana = new Person("nana", 27);
    
    //프로토타입 체이닝
    console.log(nana.hasOwnProperty("name")); //true
    
    //Person.prototype 호출
    console.dir(Person.prototype); //Object
    
    console.dir(nana.__proto__ === Person.prototype); //true
    console.dir(nana.__proto__.__proto__ === Object.prototype); //true

     

    예제를 봅시다. 우리는 Person()이라는 생성자 함수를 통해 객체를 하나 만들었습니다. 이름은 nana죠. 이 nana는 객체이므로 [[Prototype]] 링크를 반드시 하나 갖고 있습니다. 누구와 연결될까요? 생성자 함수인 Person()의 프로토타입 객체가 됩니다.

    따라서, nana.__proto__ === Person.prototype 은 true가 됩니다.

     

    우리가 만든 nana 객체에는 메서드가 없습니다. 하지만 hasOwnProperty가 에러없이 잘 실행됐죠. 아까 말한 프로토타입 체이닝 덕분입니다. nana 객체가 한 일을 따라가보죠. 먼저, nana 객체는 자기한테서 해당 메서드를 찾습니다. 하지만 안 보이네요. 그럼 부모 객체인 Person.prototype에게 가서 해당 메서드가 있는지 물어봅니다. 하지만 Person.prototype이 가진 거라곤 constructor 프로퍼티밖에 없어요. 그럼 또 거슬러 올라가야죠. 조상을 찾아야 합니다. Person.prototype은 객체이므로 반드시 얘도 [[Prototype]] 링크로 누군가와 연결되어 있을 겁니다. 누굴까요? 바로 Object.prototype입니다.

    따라서, nana.__proto__.__proto__ === Object.prototype은 true입니다.

     

     

     

     

    결국 프로토타입은 언젠가 끝이 날 텐데요, 그 종점은 바로 Object.prototype 객체입니다. 달리 말하면 모든 자바스크립트 객체은 Object.prototype 객체가 가진 프로퍼티에 접근할 수 있다는 뜻이죠.

     

     

     

    4) 기본 데이터 타입 확장

     

    자바스크립트에서 숫자, 문자열, 배열 등에서 사용되는 표준 메서드들의 경우, 이들의 프로토타입인 Number.prototype, String.prototype, Array.prototype 등에 정의되어 있습니다. 뭐, 얘네도 결국 객체니까 최종적으로는 Object.prototype과 연결되겠지만요.

    이러한 표준 빌트인 프로토타입에 우리만의 프로퍼티나 메서드를 추가할 수 있을까요? 넵, 가능합니다. 그리고 당연하게도 체이닝을 따라 접근할 수 있어요.

     

    String.prototype.myMethod = function () {
      console.log("내가 만든 메서드!");
    }
    
    var str = "test";
    str.myMethod();
    
    console.dir(String.prototype);

     

    myMethod라는 메서드를 추가해서 잘 실행했고,

    콘솔에 찍어보면 추가된 모습을 볼 수 있습니다.

     

     

     

    5) 프로토타입도 객체다

     

    자바스크립트는 객체 기반이죠! (아아 자바스크립트 월드 안에 객체가 가득해)

    그러니 프로토타입도 객체라고 생각할 수 있습니다.

     

    기본적으로 얘는 constructor 프로퍼티밖에 없지만, 일반 객체처럼 프로퍼티를 추가/삭제하는 것이 가능합니다. 실시간으로 체이닝에 반영되는 건 덤이죠!

     

    function Person(name, age) {
      this.name = name;
    }
    
    //생성자 함수로 객체 생성
    var nana = new Person("nana");
    
    //프로토타입 객체에 메서드 정의
    Person.prototype.sayHello = function (name) {
      console.log("Hello, I am " + name);
    }
    
    //체이닝을 통한 메서드 호출
    nana.sayHello("nana");

     

     

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

     

    자, 그럼 프로토타입 객체 내의 메서드 안에서 this는 어디에 바인딩 될까요?

    스크롤을 거슬러 올라가 확인해보면, '메서드 호출 시 메서드 내부 코드에서 사용된 this는, 해당 메서드를 호출한 객체로 바인딩됩니다.'라는 내용을 볼 수 있습니다 :>

    예제로 확인해보죠!

     

    function Person(name) {
      this.name = name;
    }
    
    //프로토타입 객체에 메서드 정의
    Person.prototype.getName = function (name) {
      return this.name;
    }
    
    //생성자 함수로 객체 생성
    var nana = new Person("nana");
    console.log(nana.getName()); //출력: nana
    
    //프로토타입 객체에 name 프로퍼티를 동적으로 추가
    Person.prototype.name = "person";
    console.log(Person.prototype.getName()); //출력: person

     

    nana.getName()을 실행했을 때, nana 객체에는 해당 메서드가 없습니다. 따라서 체이닝 발동!! 그럼 Person.prototype.getName이 호출되겠죠. 이때, getName()을 호출한 주체는? nana 객체입니다. 따라서 this는 nana 객체가 되고, 출력값은 nana가 됩니다.

    한편 Person.prototype에 접근해서 getName()을 호출했을 때 this는 Person.prototype으로 바인딩된 걸 볼 수 있습니다.

     

     

     

     

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

     

    디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, prototype 프로퍼티에 연결됩니다. 이러한 디폴트 프로타입 객체를 다른 일반 객체로 변경(!)하는 것도 가능한데요. 이 부분은 다음 파트에서 다뤄보겠습니다.

     

    주의할 점은, 이렇게 프로토타입 객체가 변경되면 그 이후에 생성된 객체들은 그 객체로 [[Prototype]] 링크를 연결합니다. 하지만 변경되기 전에 생성된 객체들은 기존 객체로 [[Prototype]] 링크를 연결하고 있죠.

    아래 예제를 확인해보세요!

     

    //생성자 함수 생성
    function Person(name) {
      this.name = name;
    }
    
    console.log(Person.prototype.constructor); //출력: Person(name) /** ① **/
    
    //생성자 함수를 통한 객체 생성
    var nana = new Person("nana");
    console.log(nana.country); //출력: undefined /** ② **/
    
    
    //프로토타입을 새로운 객체로 변경
    var newObj = {
      country: "korea"
    }
    Person.prototype = newObj;
    
    
    console.log(Person.prototype.constructor); //출력: Object() /** ③ **/
    
    //새로운 객체 생성
    var kkli = new Person("kkli"); /** ④ **/
    
    console.log(kkli.__proto__ === newObj); //출력: true
    console.log(nana.country); //출력: undefined
    console.log(kkli.country); //출력: korea
    console.log(nana.constructor); //출력: Person(name)
    console.log(kkli.constructor); //출력: Object()

     

     

     

    뭔가 화살표가 많은데@.@ 하나하나 살펴봅시다.

    ① Person() 함수가 만들어질 때 디폴트로 생성된 Person.prototype 객체가 있습니다. 얘가 가진 프로퍼티는 constructor 하나 뿐이죠. 따라서 Person.prototype.contructor는 Person()을 가리킵니다.

    ② 그 다음으로 nana 객체를 생성했습니다. 하지만 nana 객체에도, Person.prototype에도, Object.prototype에도 country 프로퍼티는 없기 때문에 undefined가 리턴됐습니다. 

    ③ 그리고 객체 리터럴 방식으로 생성한 newObj를 프로토타입 객체로 변경했습니다. 이 객체가 가진 프로퍼티는 country 하나 뿐이고, constructor 프로퍼티가 없습니다. 하지만 요놈도 __proto__를 통해 체이닝을 할 수가 있죠. 그래서 Object.prototype이 가진 constructor 프로퍼티를 통해 Object() 생성자 함수가 출력됐습니다.

    ④ 변경한 다음 새로운 객체인 kkli를 생성했습니다. 이 시점에서 Person.prototype은 newObj를 가리키고 있죠. 따라서 kkli.__proto__ === newObj가 됩니다. 따라서 nana와 kkli 두 객체는 전혀 다른 객체를 각자의 __proto__로 연결하고 있는 셈입니다.

     

     

     

    8) 체이닝은 프로퍼티를 읽거나 메서드를 실행할 때만 동작한다

     

    객체의 특정 프로퍼티를 읽을 때, 그 프로퍼티가 해당 객체에 없다면 체이닝을 통해 찾아낸다고 했죠. 하지만 객체의 프로퍼티에 값을 쓰려고 할 때는 체이닝이 발생하지 않습니다.

    잠깐 생각해보면 당연한 말인데요. 1편에서도 살펴봤지만, 자바스크립트에서는 프로퍼티에 값을 넣을 때 그 값이 '있으면 재할당', '없으면 프로퍼티를 생성'합니다. 그러니 체이닝이 발생하는 대신 그 객체에 프로퍼티를 새롭게 생성하겠죠.

     

    //생성자 함수 생성
    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.country = "korea";
    
    var nana = new Person("nana");
    var kkli = new Person("kkli");
    
    /** ① **/
    console.log(nana.country); //출력: korea
    console.log(kkli.country); //출력: korea
    
    kkli.country = "USA";
    
    /** ② **/
    console.log(nana.country); //출력: korea
    console.log(kkli.country); //출력: USA

     

    ① 먼저 nana랑 kkli에는 country 프로퍼티가 없습니다. 따라서 country 프로퍼티를 읽을 때, Person.prototype에 체이닝해서 가져왔습니다.

    ② 이후 kkli에 country 프로퍼티를 새롭게 추가했습니다. 따라서 kkli는 country 프로퍼티를 읽을 때 더 이상 체이닝할 필요가 없습니다. 그래서 USA를 출력합니다. 반면 nana는 여전히 해당 프로퍼티가 없으므로 체이닝해서 korea를 출력합니다.

     

     


     

     

    이번 파트는 엄청 길었네요! 이 글을 무려 1년에 걸쳐 쓴 건 안 비밀.

     

    자바스크립트에서는 너도 나도 죽창 한방인 객체라는 사실을,

    그리고 태초의 객체인 Object()가 존재하며,

    각 객체들은 링크로 연결되어 있다는 걸 알게 됐습니다.

     

    이 개념을 이해하는 데 오래 걸렸지만 뿌듯하네요 ᕕ( ᐛ )ᕗ예이

    댓글 0