[코어 자바스크립트] 07. 클래스(1)

1. 클래스와 인스턴스 1) 클래스 ex) 음식 - 고기 - 채소 - 과일 - 배 - 사과 - 바나나 → 음식이라는 범주 안에 고기, 채소, 과일 / 과일이라는 범주 안에 배, 사과, 바나나 → 음식과 과일은 어떤 사물

data-science-study.tistory.com

 

 2) 클래스가 구체적인 데이터를 지니지 않게 하는 방법

    - 클래스가 값을 가지면 그 하위 클래스에서 delete 등의 변화가 발생했을 때, 원하지 않는 값이 참조되는 등 상위 클래스가 하위 클래스에 관여하는 안전성의 문제가 계속 발생

    - 따라서, 클래스가 구체적인 값을 지니지 않도록 해야 함

    - 가장 쉬운 방법은 일단 만들고 나서, 프로퍼티를 일일히 지운 뒤, 새로운 프로퍼티를 추가할 수 없도록 freeze

delete Square.prototype.width;
delete Square.prototype.height;
Object.freeze(Square.prototype);

    - 프로퍼티가 많은 경우, 반복 작업을 없애고 범용적인 함수로 만들어 쓰기

var extendClass1 = function (SuperClass, SubClass, subMethods) {

  // SubClass의 prototype 내의 프로퍼티 삭제
  SubClass.prototype = new SuperClass();
  for (var prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete Subclass.prototype[prop];
    }
  }
  
  // 메서드가 있다면, SubClass에 넘겨주기
  if (subMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = submethod[method];
    }
  }
  
  // 그 후 더이상 SubClass의 prototype에 새로운 프로퍼티를 추가할 수 없도록 freeze
  Object.freeze(SubClass.prototype);
  return SubClass;
};

var Square = extendClass1(Rectangle, function (width) {
  Rectangle.call(this, width, width);
});

      → extnedClass1에 상위 클래스인 Rectangle과 Rectangle을 상속받는 SubClass인 Square를 익명함수로 전달

      → SubClass에 있는 SuperClass에서 상속받은 프로퍼티를 삭제하여 애초에 상위 클래스의 프로퍼티를 사용하도록 설정(width를 두 번 전달할 때, Square의 width, width가 아닌 Rectangle의 width, height로 전달되어, Square의 width가 삭제되어도 Rectangle의 width, height로써 저장되어 있는 값이 변하지 않아 결과도 변하지 않게 됨)

      → 메서드는 인스턴스가 사용가능하도록 SubClass에 넘겨주기

 

    - 두 번째 방법으로, 아무 프로퍼티를 생성하지 않는 빈 생성자 함수(Bridge)를 만들어서 그 prototype이 SuperClass의 prototype을 바라보게 하고, SubClass의 인스턴스에는 Bridge의 인스턴스 할당

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
var Square = function (width) {
  Rectangle.call(this, width, width);
};
var Bridge = function () {};
Bridge.prototype = Rectangle.prototype;
Square.prototype = new.Bridge();
Object.freeze(Square.prototype);

      → Rectangle 자리를 Bridge가 대체하며 인스턴스를 제외한 프로토타입 경로에 구체적인 데이터 x

    - 범용성을 고려하여 함수로 작성하면 다음과 같이 작성할 수 있음

var extendClass2 = (function () {
  var Bridge = function () {};
  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    SubClass.prototype = new Bridge();
    if (subMethods) {
      for (var method in sybMethods) {
        SubClass.prototype[method] = SubMethods[method];
      }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
  };
})();

 

    - 세 번째 방법, Object.create 사용

...

Square.prototype = Object.create(Rectangle.prototype);
Object.freeze(Square.prototype);

...

      → 이 방법으로 생선한 SubClass의 prototype의 __proto__는 SuperClass의 Prototype을 바라보지만, SuperClass의 인스턴스가 되지는 않음

 

  - 위 세가지 방법 모두 SubClass.prototype의 __proto__가 SuperClass.prototype을 참조하고, SubClass.prototype에는 불필요한 인스턴스 프로퍼티가 남아있지 않아야 함

 

 

 3) constructor 복구하기

    - SubClass의 인스턴스의 constructor는 여전히 SuperClass를 가리킴

    - 따라서, SubClass.prototype.constructor가 원래의 SubClass를 바라보도록 해주어야 함

var extendClass1 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = new Superclass();
  for (var prop in SubClass.prototype) {
    if (SubClass.prototype.hasOwnProperty(prop)) {
      delete SubClass.prototype[prop];
    }
  }
  
  SubClass.prototype.constructor = SubClass;
  
  if (SubMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
}
var extendClass2 = (function () {
  var Bridge = function () {};
  return function (SuperClass, SubClass, subMethods) {
    Bridge.prototype = SuperClass.prototype;
    SubClass.prototype = new Bridge();
    
    SubClass.prototype.constructor = SubClass;
    
    if (subMethods) {
      for (var method in sybMethods) {
        SubClass.prototype[method] = SubMethods[method];
      }
    }
    Object.freeze(SubClass.prototype);
    return SubClass;
  };
})();
var extendClass3 = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = Object.create(Superclass.prototype);
  
  SubClass.prototype.constructor = SubClass;
  
  if (SubMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
}

    - 세가지의 각 방법에서 SubClass의 prototype을 SuperClass의 prototype으로 지정한 다음에 SubClass.prototype.constructor를 SubClass로 다시 선언해주는 코드를 통해 SubClass의 constructor가 다시 자시능ㄹ 바라보도록 함

    - 상위 클래스는 이제 완전히 상속만을 위해 추상화 됨

 

 

 4) 상위 클래스에의 접근 수단

    - 하위 클래스에서 상위 클래스의 메서드 실행 결과를 바탕으로 추가 작업 수행하려고 할 때, 하위 클래스에서 상위 클래스의 prototype의 메서드에 접근할 수 있는 수단

var extendClass = function (SuperClass, SubClass, subMethods) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.constructor = SubClass;
  
  // 새롭게 super 메서드의 동작 추가
  SubClass.prototype.super = function (propName) {
    var self = this;
    if (!propName) return function () {         // 인자가 없다면 SuperClass 생성자 함수에 클로저를 통해 접근
      SuperClass.apply(self, arguments);
    }
    var prop = SuperClass.prototype[propName];
    if (typeof prop !== 'function') return prop;  // 사용하려는 SuperClass의 프로퍼티의 타입이 함수가 아니면 사용하려는 값을 그대로 반환
    return function () {                               // 함수라면, 클로저를 통해 메서드에 접근
      return prop.apply(self, arguments);
    }
  };
  // 추가된 내용 끝
  
  if (subMethods) {
    for (var method in subMethods) {
      SubClass.prototype[method] = subMethods[method];
    }
  }
  Object.freeze(SubClass.prototype);
  return SubClass;
};

    - super 메서드: SuperClass의 생성자 함수에 접근하고자 할 때는 this.super(), SuperClass의 prototype에 접근하고자 할 때는 this.super(propName)

    - 기존 this에 바인딩하는 문법을 썼다면 이제 this.super()(width, width)라고 함으로써 가독성 증가

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

var Square = extendClass(
  Rectangle,
  function (width) {
    this.super()(width, width);
  }, {
      getArea: function () {
        console.log('size is :', this.super('getArea')());
      }
  }
);

var sq = new Square(10);
sq.getArea();
console.log(sq.super('getArea')());

 

 

4. ES6의 클래스 및 클래스 상속

    - ES6의 클래스

// ES5
var ES5 = function (name) {
  this.name = name;
};

ES5.staticMethod = function () {
  return this.name + ' staticMethod';
};

ES5.prototype.method = function () {
  return this.name + ' method';
};

var es5Instance = new ES5('es5');
console.log(ES5.staticMethod());        // ES5 staticMethod
console.log(es5Instance.method());    // es5 method
// ES6
var ES6 = class {

  // 생성자 함수
  constructor (name) {
    this.name = name;
  }
  
  // static 메서드로, 생성자 함수(클래스) 자신만 호출 가능)
  static staticMethod () {
    return this.name + ' staticMethod';
  }
  
  // prototype 객체 내부에 할당되는 메서드, 인스턴스가 프로토타입 체이닝을 통해 호출 가능
  method () {
    return this.name + ' method';
  }
};

var es6Instance = new ES6('es6');
console.log(ES6.staticMethod());        // ES6 staticMethod
console.log(es6Instance.method());    // es6 method

 

    - ES6의 클래스 상속

var Rectangle = class {
  constructor (width, height) {
    this.width = width;
    this.height = height;
  }
  getArea () {
    return this.width * this.height;
  }
};

var Square = class extends Rectangle {  // Square가 Rectangle의 상속을 받게 하기 위하여 extends 추가
  constructor (width) {
    super(width, height);              // constructor 내부의 super는 SuperClass의 constructor 실행
  }
  
  getArea () {
    console.log('size is :', super.getArea());   // constructor 외부의 super는 객체처럼 사용 가능, 이때 객체는 SuperClass.prototype을 바라보고, this는 super가 아닌 원래의 this
  }
};

1. 클래스와 인스턴스

 1) 클래스

    ex)   음식 - 고기

                        - 채소

                        - 과일 - 배

                                    - 사과

                                    - 바나나

          → 음식이라는 범주 안에 고기, 채소, 과일 / 과일이라는 범주 안에 배, 사과, 바나나

          → 음식과 과일은 어떤 사물의 공통 속성을 모아 정의한 것이고, 배, 사과, 바나나는 실존하여 만질 수 있는 것

          → 음식과 과일은 '클래스'이고 클래스 중 음식이 상위 개념, 과일이 하위 개념이므로 음식이 superClass, 과일이 subClass

          → 클래스 내부에서 실존하는 객체는 '인스턴스', 인스턴스는 인스턴스를 포함하고 있는 클래스의 속성을 모두 가짐

 

2. 자바스크립트의 클래스

  - 자바 스크립트는 프로토타입 기반 언어로 클래스의 개념이 존재하지 않지만, 프로토타입 체이닝 등으로 상속을 구현 가능

     (생성자 함수 Array를 new와 함께 사용하여 새로운 인스턴스를 만들면, 인스턴스는 Array의 prototype 내부 요소들을 사용 가능)

  - 이때, 인스턴스에 상속되는 프로퍼티가 있고(prototype 내부의 프로퍼티), 인스턴스에 상속되지 않는 프로퍼티가 있음

    → 인스턴스에 상속이 되지 않는 프로퍼티: 스태틱 멤버(static member(static methods + static properties))

    → 인스턴스에 상속되는 프로퍼티: 프로토타입 메서드

  - 아래 사진에서 prototype 내부의 메서드(push(), pop() 등)만 [1, 2]라는 인스턴스가 사용가능

  - 그 외, from(), isArray(), arguments, length 등은 [1, 2]에서 사용이 불가능

var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
}

// 생성자 함수 Rectangle의 prototype 내에 getArea 메서드 생성
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};

// 생성자 함수 Rectangle의 prototype 내부에가 아닌 static method로 isRectangle 함수 생성
Rectangle.isRectangle = function (instance) {
  return instance instanceof Rectangle && instance.width > 0 && instance.height > 0;
};

// 생성자 함수 Rectangle로 생성한 instance rect1
var rect1 = new Rectangle(3, 4)

// 인스턴스 rect1에서 Rectangle의 prototype에 있던 getArea함수가 정상 작동함
console.log(rect1.getArea()); // 12

// 인스턴스 rect1에서 Rectangle의 static methos인 isRectangle은 작동x
console.log(rect1.isRectangle(rect1)); // Error

// 생성자 함수 Rectangle에서는 당연히 Rectangle의 static method인 isRectangle이 작동함
console.log(Rectangle.isRectangle(rect1)); // True

  - 클래스는 static method를 통해 호출할 때는, 자신만 사용할 수 있는 것을 사용하는 것으로, 하나의 개체로 취급

  - 앞서 배웠던 것처럼 하위 개념에 자신의 특성을 부여하는 경우에 클래스는 틀의 역할을 하는 추상적인 개념

 

 

3. 클래스 상속

 1) 기본 구현

    - 프로토타입 체인을 활용해 클래스 상속을 구현

var Grade = function () {
  var args = Array.prototype.slice.call(arguments);
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};
Grade.prototype = [];
var g = new Grade(100, 80);

    - Grade의 prototype에 []을 참조시켜 임의로 배열로 선언

    - 따라서, Grade는 Array에 프로토타입 체이닝되어 Array의 prototype을 참조할 수 있게됨

Grade가 Array와 Object의 prototype을 참조할 수 있게 되는 과정

  - 하지만 세부적으로 완벽한 superclass와 subclass의 구현이 이루어지지 않음

      → length 프로퍼티가 삭제 가능한 점

      → Grade.prototype에 빈 배열을 참조시킨 점

// 90을 유사 배열 객체인 g에 Array의 메서드 push를 이용하여 잘 적용됨
g.push(90);
console.log(g);  // Grade{ 0: 100, 1: 80, 2: 90, length: 3 }

// 하지만, g의 생성자 함수 Grade를 통해 선언되었던 length는 삭제가 가능
delete g.length;
g.push(70);
console.log(g);  // Grade{ 0: 70, 1: 80, 2: 90, length: 1 }

      → 70을 push했을 때 0번째 인덱스에 70이 들어가고 length가 1이 된 이유

      → g.__proto__, 즉 Grade.prototype이 빈 배열 []을 가리키고, g.length가 없어서 참조한 g.__proto__.length는 Array의 length 메서드가 적용됨

      → 따라서, 빈 배열에 값(70)을 할당하고, 이 배열의 길이를 읽어들인 것

      → Grade.prototype을 빈 배열 []이 아닌 요소가 있는 배열로 정의했다면

Grade.prototype = [ 'a', 'b', 'c', 'd' ];
var g = new Grade(100, 80);

// push는 그대로 정상 작동
g.push(90);
console.log(g);  // Grade{ 0: 100, 1: 80, 2: 90, length: 3 }

// g.length가 delete 되는 것도 그대로
// 하지만, g.lenght가 없어져서 참조하게 되는 g.__proto__가 요소가 이미 4개가 있는 배열이므로
// 70을 push하면 70은 인덱스가 4인 곳에 들어가고, length도 기존 4에서 1이 추가된 5로 출력됨
delete g.length;
g.push(70);
console.log(g);  // Grade{ 0: 100, 1: 80, 2: 90, ___ 4: 70, length: 5 }

  - 이처럼 클래스의 값(메서드 등)이 인스턴스의 동작에 영향을 미치면 클래스의 추상성이 없어짐

  - 클래스는 인스턴스가 사용할 메서드(속성)만 지닌 추상적인 틀로 작용하게끔 작성해야 함

 

  - 다른 예제

// 직사각형을 만드는 생성자 함수, 가로와 세로 길이가 인자로 필요함
var Rectangle = function (width, height) {
  this.width = width;
  this.height = height;
};
Rectangle.prototype.getArea = function () {
  return this.width * this.height;
};
var rect = new Rectangle(3, 4);
console.log(rect.getArea());  // 12


// 정사각형을 만드는 생성자 함수, 가로 길이 하나만 있으면 세로 길이는 똑같으므로 필요 x
var Square = function (width) {
  this.width = width;
};
Square.prototype.getArea = function () {
  return this.width * this.width;
};
var sq = new Square(5);
console.log(sq.getArea());  // 25

      → Rectangle과 Square의 공통적인 요소(인자 width, 함수 getArea()의 비슷한 패턴)를 사용하여 상속관계 형성

// Square 함수를 Rectangle 함수와 똑같은 형태로 변형
...

var Square = function (width) {
  this.width = width;
  this.height = width;
};

Square.prototype.getArea = function () {
  return this.width * this.height;
};

...


// 변형한 Square 함수를 새로 쓰지 않고 아예 Rectangle 함수를 끌어와서
// call을 통해 Rectangle 함수에 Square 함수의 인자를 전달(width 자리에 width, height 자리에 width 전달)
// prototype 또한 Rectangle 생성자 함수를 사용하여 똑같이 생성
...

var Square = function (width) {
  Rectangle.call(this, width, width);
};
Square.prototype = new Rectangle();

...

      → new Square로 생성한 인스턴스인 sq의 디렉터리 구조에서 Rectangle의 prototype을 참조하는 것을 확인할 수 있음

Square과 Rectangle의 관계 도식화

      → 앞선 예제의 문제가 아직 존재

      → Square의 prototype에 값이 존재하여 sq.width나 sq.height를 delete 한다면 Square.prototype.width의 값이 대신 계산되어 엉뚱한 결과 출력

      → 또한, sq.constructor는 Rectangle을 가리키므로 정사각형이 아닌 직사각형도 적용이 가능해지는 등 구조적 안전성 감소

2. 프로토타입 체인

 1) 메서드 오버라이드

  - __proto__는 생략 가능한 프로퍼티이므로 __proto__를 생략하면 인스턴스는 생성자 함수의 prototype에 정의된 프로퍼티나 메서드를 자기 것처럼 사용 가능

  - 이때, 인스턴스가 통일한 이름의 프로퍼티나 메서드를 가진다면?

// 생성자 함수 Person과 Person의 prototype으로 getName을 생성
var Person = function(name) {
  this.name = name;
};
Person.prototype.getName = function() {
  return this.name;
};

// 생성자 함수 Person에 의해 만들어진 인스턴스인 iu와 iu의 메서드로 getName 생성
var iu = new Person('지금');
iu.getName = function() {
  return '바로' + this.name;
};

// iu.getName을 했을 때
// iu 객체의 생성자인 Person의 prototype을 참조하는 iu.__proto__.getName과 iu의 자체 메서드인 iu.getName중
// iu의 자체 메서드인 iu.getName이 출력됨
console.log(iu.getName());   // 바로 지금

    → 자바스크립트 엔진에서 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 먼저 탐색

    → 자신의 프로퍼티 중 getName이 없으면 그 다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행

    → iu객체에는 자신의 프로퍼티 중 getName이라는 메서드가 있으므로 바로 출력, 이 현상이 메서드 오버라이드

    → 메서드 오버라이드는 iu의 자체 메서드가 있을 때, __proto__의 메서드가 없어지고 교체되는 것이 아닌 덮어씌워지는 것

    → 따라서, __proto__의 getName 메서드도 호출할 수 있음

// __proto__를 명시해주기
// iu.__proto__가 this 객체가 되었는데, iu.__proto__에 name 객체가 없어 undefined 되었지만
// getName이 iu의 자체 메서드가 아닌 iu.__proto__의 메서드로 인식되었음을 확인할 수 있음
console.log(iu.__proto__.getName())  // undefined

// Person.prototype에 name 프로퍼티를 지정해주면
// iu.__proto__가 이를 참조하여 iu.__proto__에도 같은 값을 갖는 name프로퍼티가 생성됨
// 따라서, 이번에는 getName의 this 객체가 iu.__proto__가 되고 this.name은 iu.__proto__의 name 프로퍼티로 잘 인식됨
Person.prototype.name = '이지금';
console.log(iu.__proto__.getName());   // 이지금

// call 메서드를 사용하면 this가 바라보는 것을 Person의 prototype에서 iu 인스턴스 자체로 바꿔줄 수 있음
// iu 인스턴스를 생성할 때, 생성자 함수 Person에 '지금'을 넣으며 생성했고 따라서 iu 인스턴스의 자체 name은 '지금'
console.log(iu.__proto__.getName.call(iu));  // 지금

 

 

 2) 프로토타입 체인

  - 배열 [1, 2]의 디렉터리 구조를 보면 [[prototype]] 내에 또 하나의 [[prototype]]이 존재

  - prototype 객체가 '객체'이기 때문에 '객체(Object)'의 prototype을 참조하기 때문

Object의 prototype을 참조하는 배열의 prototype 도식화

  - 따라서, 배열 [1, 2]는 배열(Array)의 prototype내의 메서드도 사용 가능하고, 객체(Object)의 prototype 내의 메서드도 사용 가능

  - 이처럼 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__프로퍼티가 연쇄적으로 이어진 것이 프로토타입 체인

  - 프로토타입 체인을 따라가며 검색하는 것이 프로토타입 체이닝

  - 프로토타입 체이닝은 메서드 오버라이드와 비슷한 과정으로 검색함

    → 메서드를 호출했을 때, 자신의 __proto__에 해당 메서드가 없다면 그 위의 __proto__에서 메서드 검색

var arr = [1, 2];

// 1)
// arr을 Array의 prototype 내의 메서드 toString으로 불러왔을 때
Array.prototype.toString.call(arr);   // 1 2
// arr을 Object의 prototype 내의 메서드 toString으로 불러왔을 때
Object.prototype.toString.call(arr);  // [object Array]
// 2)
// arr 자체 메서드가 없을 때 toString 검색과정
// arr 자체 메서드에서 toString을 검색 → 없음
// arr 자체 메서드 위에 있는 Array의 prototype 내에서 toString 메서드 검색 → 있음 → 사용
arr.toString();   // 1 2

// arr 자체 메서드로서 toString 생성
arr.toString = function () {
  return this.join('_');
};

// arr 자체 메서드가 있을 때 toString 검색과정
// arr 자체 메서드에서 toString을 검색 → 있음 → 사용
arr.toString();   // 1_2

  - 위의 경우처럼 Array 뿐아니라, Number, String, Boolean 등 자바스크립트 데이터는 모두 그 위에 Object의 prototype을 참조함

 

 

 3) 객체 전용 메서드의 예외사항

  - 객체(Object) 내부에 메서드를 정의하면 다른 모든 자바스크립트 데이터들은 이 메서드를 사용할 수 있음

    → 따라서 객체에서만 사용할 메서드는 객체(Object)의 prototype 내에 정의할 수 없음

Object.prototype.getEntries = function() {
  var res = [];
  for (var prop in this) {
    if (this.hasOwnProperty(prop)) {
      res.push([prop, this[prop]]);
    }
  }
  return res;
};

var data = [
  ['object', {a: 1, b: 2, c: 3}],   // [["a", 1], ["b", 2], ["c", 3]]
  ['number', 345],                    // []
  ['string', 'abc'],                     // [["0", "a"], ["1", "b"], ["2", "c"]]
  ['boolean', false],                  // []
  ['func', function() {}],           // []
  ['array', [1, 2, 3]]                 // [["0", 1], ["1", 2], ["2", 3]]
];

data.forEach(function (datum) {
  console.log(datum[1].getEntries());
});

    → 객체(Object)의 메서드 getEntries는 hasOwnProperty를 사용하여 객체가 특정 프로퍼티(prop)를 자신만 소유하는 지 확인

    → hasOwnProperty를 this에 대해 실행하여 객체인지 아닌지 판단하고, 객체인 것에 대해서만 출력하려고 함

    → 모든 데이터가 객체를 참조하여, getEntries를 자신의 것처럼 사용하여, getentries의 this가 Object로 고정되지 않고, 매 데이터마다 해당 데이터로 바뀜

    → 따라서, 객체에서만 동작하는 객체 전용 메서드는 Object.prototype이 아닌, 스태틱 메서드로 부여해야함

    → 또한, Object와 인스턴스 사이의 this 연결이 안되므로 this를 그냥 사용하면 메서드명 앞의 객체가 this가 되는 것 대신에 this 사용을 포기하고 인스턴스 자체를 인자로 사용해야함

    → ex) Object.frees(instance) 대신, instance.freeze() / Object.getPrototypeOf(instance) 대신, instance.getPrototype()

 

  - 프로토타입의 가장 위에는 항상 Object.prototype이 있어 참조할 수 있지만, 예외적으로 Object.prototype에 접근할 수 없도록 할 수 있음

var _proto = Object.create(null)
_proto.getValue = function(key) {
    return this[key];
};
var obj = Object.create(_proto);
obj.a = 1;
console.log(obj.getValue('a'));
console.dir(obj);

    → Object.create(null)은 __proto__가 없는 객체를 생성하여, Object.prototype을 참조하지 않음

    → 따라서, 디렉터리 구조를 확인한 결과, Object.create(null)로 생성된 _proto내부에는, 직접 생성한 getValue 메서드 이외에 다른 메서드는 없음(Object.prototype을 참조하지 않아서 Object의 메서드를 사용할 수 없음)

    → 기본 기능에 제약이 생기지만, 객체의 무게가 가벼워짐

 

 

 4) 다중 프로토타입 체인

  - 프로토타입 체인은 보통 1단계(객체) - 2단계(나머지)로 끝나지만, __proto__를 연결해나가기만 하면 무한대로 체인관계를 이어갈 수 있음

  - 이를 이용해 다른 언어의 클래스롸 비슷하게 동작하는 구조 생성 가능

  - __proto__를 연결하는 방법은 __proto__가 가리키는 대상(생성자 함수의 prototype)이 연결하고자하는 상위 생성자 함수의 인스턴스를 바라보게끔 하면 됨

// Grade는 여러 인자를 받아서, 순서대로 인덱싱하여 저장하고, length 프로퍼티도 부여
// 배열의 형태로 저장하지만 배열(Array)의 메서드를 사용하지는 못하는 유사 배열 객체
var Grade = function () {
  var args = Array.prototype.slice.call(arguments);
  for (var i = 0; i < args.length; i++) {
    this[i] = args[i];
  }
  this.length = args.length;
};

var g = new Grade(100, 80);

    → g는 Grade의 인스턴스

    → Grade는 배열의 형태로 저장하지만 배열(Array)의 메서드를 사용할 수는 없는 상태

    → Grade가 배열의 메서드를 사용할 수 있도록, Grade.prototype이 Array를 참조할 수 있도록 해야함

Grade.prototype = [];

    → 위의 코드를 작성하면 Grade.prototype이 (대괄호로 감싸진)배열의 형태를 갖게되며, Array의 프로토타입 삼각형에 연결됨

Array를 참조할 수 있게 된 Grade 도식화

    → 이에 따라서, Grade로 생성된 g 또한 Array의 메서드를 사용할 수 있게 됨

console.log(g);  // Grade(2) [100, 80]

g.pop();            // 배열의 끝 값을 없애며 다른 데 저장하는 배열 메서드 pop() 적용 가능
console.log(g);  // Grade(1) [100]

g.push(90);       // 배열의 새로운 값을 저장하는 배열 메서드 push() 적용 가능
console.log(g);  // Grade(2) [100, 90]

·자바스크립트는 프로토타입 기반 언어

  - 클래스 기반 언어는 상속을 사용(프로그래밍 언어 대부분이 클래스 기반)

  - 프로토타입 기반 언어는 어떤 객체를 원형으로 삼고, 이를 복제(참조)하여 상속과 비숫한 효과를 얻음

 

1. 프로토타입의 개념 이해

 1) constructor, prototype, instance의 관계

  - 코드와 코드를 도식화한 그림

var instance = new Constructor();

  - 생성자 함수 Constructor()를 new 연산자와 함께 호출

  - Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(var instance)가 생성됨

  - instance에는 __proto__라는 프로퍼티가 자동으로 부여되며, 이 프로퍼티는 Constructor의 prototype 프로퍼티를 참조

  - prototype은 객체이며, 이를 참조하는 __proto__ 또한 객체

  - prototype 내부에는 instance가 사용할 메서드 저장, instance에서도 __proto__를 통해 이 메서드들에 접근 가능

  - __proto__를 사용한 접근 대신 Object.getPrototypeOf(instance) 또는 Refelect.getPrototypeOf(instance)를 통해 접근 권장

 

  - 예시

// Person이라는 생성자 함수
var Person = function (name) {
  this._name = name;
};

// Person의 prototype에 getname이라는 메서드 지정
Person.prototype.getName = function() {
  return this._name;
};

// 생성자 함수 Person을 통해 새로운 인스턴스 name을 생성
var name = new Person('name');

// Person의 인스턴스 name은 __proto__프로퍼티를 통해 getName 메서드 호출 가능
name.__proto__.getName();  // undefined

// 인스턴스의 __proto__는 Person의 prototype을 참조하므로, 결국 둘은 같은 객체를 바라봄
Person.prototype = name.__proto__  // true

  - name.__proto__.getName()이 undefined로 뜬 것은 getName()함수가 this._name을 반환하는데 this._name이 없기 때문

  - 메서드의 this는 메서드 바로 앞의 객체이므로 이때 this는 name.__proto__

  - 따라서, name.__proto__에 _name 프로퍼티를 지정해주면 정상적으로 getName()함수가 실행될 것

var name = new Person('name');
name.__proto__._name = 'NAME__proto__';
name.__proto__.getName();  // NAME__proto__

 

  - __proto__는 생략 가능한 프로퍼티

var name1 = new Person('name1', 25);
name1.getName();   // name1
var name2 = new Person('name2', 25);
name2.getName();   // name2

  - __proto__를 생략하고 메서드를 호출하면 메서드(getName())의 this는 name.__proto__가 아닌 name이 되어 바로 name._name을 return할 수 있음

  - 도식화하면 다음과 같이 됨

 

  - prototype과 __proto__

var Constructor = function (name) {
  this.name = name;
};
Constructor.prototype.method1 = function() {};
Constructor.prototype.property1 = 'Constructor Prototype Property';

var instance = new Constructor('Instance');
console.dir(Constructor);
console.dir(instance);

  - Constructor의 디렉터리 구조

    → f Constructor(name): Constructor라는 이름의 함수, name을 인자로 가진다는 의미

    → 옅은 색의 arguments, caller, length, name, prototype은 열거할 수 없는(접근할 수 없는) 프로퍼티

    → 짙은 색의 method1, property1은 직접 지정하였고, 열거할 수 있는 프로퍼티

  - instance의 디렉터리 구조

    →Constructor가 가장 먼저 출력되어 생성자 함수 Consturctor에 의해 생성된 것임을 명시

    → [[Prototype]]은 __proto__와 같은 것으로, 이는 Constructor.prototype을 참조하므로 같은 내용을 출력

 

  - 내장 생성자 함수 Array

var arr = [1, 2];
console.dir(arr);
console.dir(Array);

arr의 디렉터리 구조
Array의 디렉터리 구조

  - arr의 디렉터리 구조

    → Array(2): length가 2인 Array

    → 인덱스 0과 1이 짙은 색으로, 접근 가능하다는 것을 의미

    → length는 옅은 색

    → [[Prototype]](=__proto__): 옅은 색상의 다양한 메서드(원래 배열에 사용하는 다양한 메서드들이 출력되어 있음)

  - Array의 디렉터리 구조

    → 함수라는 의미의 f

    → 함수의 기본적인 프로퍼티인 concat 등이 출력됨

    → prototype을 열면 arr의 __proto__와 같은 메서드들이 출력됨

Array와 arr의 디렉터리 구조 도식화

  - Array의 prototype 내부에 있는 메서드들에는 Array 생성자 함수를 통해 생성된 arr 같은 인스턴스도 얼마든지 접근 가능

  - Array의 prototype 외부에 있는 from, isArray 등의 메서드에는 인스턴스(arr)가 직접 호출할 수 없고 Array 생성자 함수에서 직접 접근해야 실행 가능

var arr = [1, 2];
arr.forEach(function (){});  // (0), forEach는 Array의 prototype 내부에 존재하므로 인스턴스인 arr도 사용가능
Array.isArray(arr);             // (0) true, isArray는 Array의 메서드이므로 Array가 사용가능
arr.isArray();                    // (x) TypeError: arr.isArray is not a function, isArray는 Array의 prototype 외부에 있으므로 인스턴스인 arr은 사용 불가능

 

 

 2) constructor 프로퍼티

  - 생성자 함수의 prototype 내부에는 constructor라는 프로퍼티가 항상 존재

  - 생성자 함수의 prototype을 참조하는 인스턴스의 __proto__에도 동일하게 constructor 프로퍼티가 항상 존재

  - constructor 프로퍼티는 원래의 생성자 함수를 참조

  - 인스턴스로부터 그 원형이 무엇인지 알기 위해 사용

var arr = [1, 2];
Array.prototype.constructor === Array
arr.__proto__.constructor === Array
arr.constructor === Array  // __proto__는 생략 가능한 프로퍼티

var arr2 = new arr.constructor(3, 4);
console.log(arr2);   // [3, 4]

  - arr 인스턴스는 Array라는 생성자 함수로부터 생성되었으며, constructor 프로퍼티를 통해 이를 확인 가능

  - arr.constructor는 생성자 함수 Array를 가리키므로 이를 활용해 새로운 인스턴스를 생성할 수도 있음

 

  - constructor는 읽기 전용 속성이 부여된 Number, String, Boolean을 제외하고는 값을 바꿀 수 있음

var NewConstructor = function() {
  console.log('this is new constructor!');
};

var dataTypes = [
  1,                              // Number & false
  'test',                         // String & false
  true,                           // Boolean & false
  {},                             // NewConstructor & false
  [],                             // NewConstructor & false
  function(){},             // NewConstructor & false
  /test/,                       // NewConstructor & false
  new Number(),          // NewConstructor & false
  new String(),             // NewConstructor & false
  new Boolean(),          // NewConstructor & false
  new Object(),            // NewConstructor & false
  new Array(),             // NewConstructor & false
  new Function(),        // NewConstructor & false
  new RegExp(),         // NewConstructor & false
  new Date(),             // NewConstructor & false
  new Error()             // NewConstructor & false
];

dataTypes.forEach(function(d) {
  d.constructor = NewConstructor;
  console.log(d.constructor.name, '&', d instanceof NewConstructor);
});

    → Number인 1, String인 'test', Boolean인 true는 d.constructor = NewConstructor에 의해 값이 바뀌지 않음

    → 다른 데이터타입들은 NewConstructor로 constructor가 변경됨

    → d instanceof NewConstructor는 d가 NewConstructor라는 생성자 함수에 의해 생성된 것인지 여부를 출력

    → d instanceof NewConstructor가 전부 false로 나왔으므로, constructor가 변경되어도 데이터 타입이나 인스턴스의 원형(인스턴스를 생성한 생성자 함수)이 바뀌는 것은 아님

 

  - 예시

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

var p1 = new Person('사람1');
var p1Proto = Object.getPrototypeOf(p1);
var p2 = new Person.prototype.constructor('사람2');
var p3 = new p1Proto.constructor('사람3');
var p4 = new p1.__proto__.constructor('사람4');
var p5 = new p1.constructor('사람5');

[ p1, p2, p3, p4, p5 ].forEach(function (p) {
  console.log(p, p instanceof Person);
});

// { name: "사람1"} true
// { name: "사람2"} true
// { name: "사람3"} true
// { name: "사람4"} true
// { name: "사람5"} true

    → p1은 생성자 함수([Constructor])인 Person에 의해 직접 생성

    → p2를 생성한 Person.prototype.constructor는 생성자 함수 자기 자신의 constructor, 즉 생성자 함수([Constructor])를 가리킴

    → p3를 생성한 p1Proto는 Object.getPrototypeOf(p1)은 p1의 __proto__와 같고 p1.__proto__.constructor는 p1을 생성한 생성자 함수([Constructor])를 가리킴

    → p4를 생성한 p1.__proto__.constructor는 위와 같은 의미

    → p5를 생성한 p1.constructor는 위에서 생략 가능한 프로퍼티인 __proto__를 생략한 것

 

  - 정리하면, 다음 각 줄은 모두 같은 대상을 가리킴

[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

  - 또한, 다음 각 줄은 모두 같은 객체(prototype)에 접근 가능

[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

 3) 부분 적용 함수

  -인자를 n개 받는 함수에 m개의 인자만 넘겨 기억시켰다가 나중에 나머지 (n-m)개의 인자를 넘기면 비로소 함수의 실행 결과를 얻을 수 있게끔 하는 함수

  -예시(bind 메서드)

var add = function () {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10));  //55

   →addPartial 함수는 인자 5개를 미리 적용(첫번째 인자 null은 this에 대한 것)

   →추후 나머지 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행

   →add 함수는 this를 사용하지 않아 bind 메서드만으로 구현되었지만 this 값을 변경할 수 밖에 없어, 메서드에 사용하려면 this에 관여하지 않도록 만들어야 함

 

  -예시(부분 적용 함수 구현 - 1)

var partial = function () {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];
  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function () {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

var add = function () {
  var result = 0;
  for (var i = 0; i <arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};

var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55

var dog = {
  name: '강아지',
  greet: partial(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, ')
};
dog.greet('입니다!');  // 왈왈, 강아지입니다!

   →addPartial에 partial 함수에 적용할 원본 함수와 미리 적용할 인자들을 전달

   →이후 addPartial에 나머지 인자들을 partial 함수가 반환하는 함수(부분 적용 함수)에 넘겨주어 이들을 한데 모아(concat) 원본 함수(add) 호출(apply)

   →실행 시점의 this를 그대로 반영하여 this에는 아무런 영향을 주지 않음

 

  -예시(부분 적용 함수 구현 - 2): 추후에 넘길 인자의 위치를 정하기

Object.defineProperty(window, '_', {
  value: 'EMPTY_SPACE',
  writable: false,
  configurable: false,
  enumerable: false
});

var partial2 = function () {
  var originalPartialArgs = arguments;
  var func = originalPartialArgs[0];
  if (typeof func !== 'function') {
    throw new Error('첫 번째 인자가 함수가 아닙니다.');
  }
  return function () {
    var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
    var restArgs = Array.prototype.slice.call(arguments);
    for (var i = 0; i < partialArgs.length; i++) {
      if(partialArgs[i] === _) {
        partialArgs[i] = restArgs.shift();
      }
    }
    return func.apply(this, partialArgs.concat(restArgs));
  };
};

var add = function () {
  var result = 0;
  for (var i = 0; i < arguments.length; i++) {
    result += arguments[i];
  }
  return result;
};

var addPartial = partial2(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10)); // 55

var dog = {
  name: '강아지',
  greet: partial2(function(prefix, suffix) {
    return prefix + this.name + suffix;
  }, '왈왈, ')
};
dog.greet(' 배고파요!');  // 왈왈, 강아지 배고파요!

   →전역객체에서 Object.defineProperty를 이용해 '_'를 비워놓음의 의미로 준비(삭제, 변경 등 접근에 대한 방어 차원에서 여러 프로퍼티 설정 해둠)

   →또, partial2 함수 내에서 if 문을 통해 인자가 _인 부분을 골라내어 restArgs의 자리인 것을 명시적으로 나타냄

※ '_'문자를 '비워놓음'의 의미로 바꿀 때, Object.defineProperty 외에 Symbol.for을 사용해도 됨

(function () {
  var EmptySpace = Symbol.for('EMPTY_SPACE'); // 기존 전역 심볼공간에 'EMPTY_SPACE'라는 심볼을 새로 생성
  console.log(EmptySpace);
})();

(function () {
  console.log(Symbol.for(EmptySpace)); // 기존 전역 심볼공간에 'EMPTY_SPACE'라는 심볼이 있으므로 해당 값을 참조
})();

return function () {
  // ...
  for (var i = 0; i < partialArgs.length; i++) 
    if(partialArgs[i] === Symbol.for('EMPTH_SPACE')) { // 바뀐 부분
      partialArgs[i] = restArgs.shift();
    }
  }
  // ...
};

// ...

var _ = Symbol.for('EMPTY_SPACE'); // 추가된 부분, '_'의 심볼을 'EMPTY_SPACE'로 설정
var addPartial = partial(add, 1, 2, _, 4, 5, _, _, 8, 9);
console.log(addPartial(3, 6, 7, 10));

 

  -디바운스: 짧은 시간동안 동일한 이벤트가 많이 발생할 경우 모두 처리하지 않고 가장 처음 또는 마지막 이벤트에 대해 한 번만 처리

   →scroll, wheel, mousemove, resize 등에 적용

   →실무에서 부분 적용 함수 사용에 적합

  -예시(디바운스)

var debounce = function (eventName, func, wait) {
  var timeoutId = null;
  return function (event) {
    var self = this;
    console.log(eventName, 'event 발생');
    clearTimeout(timeoutId);
    timeoutId = setTimeout(func.bind(self, event), wait);
  };
};

var moveHandler = function (e) {
  console.log('move event 처리');
};
var wheelHandler = function (e) {
  console.log('wheel event 처리');
};
document.body.addEventListener('mousemove', debounce('move', moveHandler, 500));
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler, 700));

   →디바운스 함수는 어떤 이벤트가 발생했는지 출력하기 위한 eventName, 이벤트에 따라 실행할 함수인 func, 마지막으로 발생한 이벤트인지 판단하기 위한 대기시간인 wait(ms)를 인자로 받음

   →timeoutID의 초기값은 null로 주며 선언

   →EventListener에 의해 호출될 함수를 return

   →self에 this 객체를 미리 저장해둠

   →함수 내부에서는 어떤 이벤트가 발생했는지 eventName을 출력

   →clearTimeout()을 사용하여 대기큐를 무조건 초기화해주는데, 처음 이벤트 발생 후 함수를 실행시킬 대기 시간 이전에 반복해서 이벤크가 발생한다면, 여기서 대기큐를 초기화시켜 다시 대기 시간을 계산할 수 있게 해줌

   →마지막으로 setTimeout()을 사용하여 대기시간(wait)만큼 지연시킨 후 func 호출, 대기시간 이전에 동일한 이벤트 발생 시 반환할 함수가 다시 실행되며 윗 줄에서 clearTimeout되고 다시 대기시간 계산

   →디바운스 함수에서 클로저로 처리되는 변수는 eventName, func, wait, timeoutId

 

 

 4) 커링 함수

  -커링 함수: 여러 인자를 받는 함수를 하나의 인자를 받는 함수로 나눠 순차적으로 호출될 수 있도록 체인 형태로 구성한 것

  -예시

var curry3 = function (func) {
  return function (a) {
    return function (b) {
      return func(a, b);
    };
  };
};

var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8));      // 10
console.log(getMaxWith10(25));    // 25

var getMaxWith10 = curry3(Math.min)(10);
console.log(getMaxWith10(8));      // 8
console.log(getMaxWith10(25));    // 10

   →인자 하나를 더 받아 10과 비교하여 더 큰 값을 찾는 함수

   →curry3에 인자로 Math.max함수를 주고, curry3 함수 내의 첫번째 함수의 인자로 10을 주어 저장

   →이후, 10과 비교할 값을 두번째 함수의 인자로 줌과 동시에 원본 함수(Math.max) 실행

   →마지막에 반환되는 원본 함수에서 curry3 함수 내의 다른 함수 두 개에서 a와 b 변수를 참조하여 사용하므로 변수 a, b는 클로저로 처리됨

 

  -커링 함수에서 마지막 인자가 전달되기 전까지 원본 함수는 실행되지 않음

  -커링 함수의 단점으로 가독성이 떨어짐

var curry5 = function (func) {
  return function (a) {
    return function (b) {
      return function (c) {
        return function (d) {
          return function (e) {
            return func(a, b, c, d, e);
          };
        };
      };
    };
  };
};

var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));

   →다행히 화살표 함수로 같은 내용을 한 줄에 표시 가능

var curry5 = func => a => b => c => d => e => func(a, b, c, d, e);

 

  -커링 함수는 당장 필요한 정보만 받아서 전달하고, 또 필요한 정보가 들어오면 전달하는 식으로 원하는 시점까지 지연시켰다가 실행하고 싶을 때 유용

var getInformation = baseUrl => path => id => fetch(baseUrl + path + '/' + id);

   →HTML5의 fetch 함수는 url을 받아 해당 url에 HTTP 요청을 함

   →이때 baseUrl은 몇 개로 고정되지만 path와 id는 많은 값을 가지므로 baseUrl만 기억시킨 후 특정 path와 id만으로 서버 요청을 수행하도록 하면 효육적이고 가독성 높아짐

 

  -url 요청에서 커링 함수 활용 예시

var imageUrl = 'http://imageAddress.com/';
var productUrl = 'http://productAddress.com/';

// 이미지 path 설정
var getImage = getInformation(imageUrl);         // http://imageAddress.com/
var getEmoticon = getImage('emoticon');          // http://imageAddress.com/emoticon
var getIcon = getImage('icon');                        // http://imageAddress.com/icon

// 제품 path 설정
var getProduct = getInformation(productUrl);   // http://productAddress.com/
var getFruit = getProduct('fruit');                     // http://productAddress.com/fruit
var getVegetable = getProduct('vegetable');     // http://productAddress.com/vegetable

// id까지 설정하여 실제 url 불러오기
var emoticon1 = getEmoticon(100);                // http://imageAddress.com/emoticon/100
var emoticon2 = getEmoticon(102);                // http://imageAddress.com/emoticon/102
var icon1 = getIcon(205);                              // http://imageAddress.com/icon/205
var icon2 = getIcon(234);                              // http://imageAddress.com/icon/234
var fruit1 = getFruit(300);                             // http://productAddress.com/fruit/300
var fruit2 = getFruit(400);                             // http://productAddress.com/fruit/400
var vegetable1 = getVegetable(456);             // http://productAddress.com/vegetable/456
var vegetable2 = getVegetable(789);             // http://productAddress.com/vegetable789

3. 클로저 활용 사례

 1) 콜백 함수 내부에서 외부 데이터 사용

  -예시 1)

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

fruits.forEach(function (fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', function () {
    alert('your choice is' + fruit);
  });
  $ul.appendChild($li);
});
document.body.appendChild($ul);

  -ul 생성, ul 내부에 fruits 목록에 있는 과일을 하나씩 li로 추가

   →li에 fruits 목록의 값을 하나씩 추가하는 forEach 함수는 외부 변수를 사용하지 않으므로 클로저가 없음

  -li를 클릭하면 "'your choice is' + fruit" 출력, 이때, fruit는 addEventListener의 콜백 함수 function 내부에서 외부의 변수를 참조하여 사용

  →따라서, addEventListener의 콜백 함수에는 클로저가 있음

 

  -예시 2)

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

var alertFruit = function(fruit) {
      alert('your choice is' + fruit);
};
fruits.forEach(function(fruit) {
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit);
    $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

  -공통적으로 사용되는 함수인 alertFruit를 콜백 함수 외부로 꺼냄

  -마지막 코드인 alertFruit(fruits[1])에서 fruits[1]은 정상적으로 'banana'를 불러와 수행

  -하지만 클릭 시 수행되는 alertFruit는 대상 과일명이 아닌 [object PointerEvent]가 출력됨

   →콜백 함수의 인자에 대한 제어권이 addEventListener한테 있는 상태

   →addEventListener는 콜백 함수 호출 시 첫번째 인자에 이벤트 객체를 넣기 때문에 alertFruit에 이벤트 객체 [Object PointerEvent]가 들어감

   →bind 메서드로 해결 가능

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

var alertFruit = function(fruit) {
      alert('your choice is' + fruit);
};
fruits.forEach(function(fruit) {
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit.bind(null,fruit));
    $ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);

   →bind 메서드를 사용하면 첫번째 인자는 바인딩할 this인데 이 값은 생략할 수 없으므로 null로 정의해야하고, 이 때문에 원래의 this를 유지할 수 없는 경우가 많음

   →두번째 인자에는 이벤트 객체가 넘어옴

...

var alertFruit = function(fruit, s) {
      alert('your choice is' + fruit + ' ' + s);
};

...

   →다음과 같이 임의의 변수 s를 지정해주면 bind의 인자로 null로 정의한 this와 fruit 외에 암시적으로 이벤트 객체를 넘겨줬음을 알 수 있음

   →해결을 위해 고차함수를 활용하면 됨

...

var alertFruitBuilder = function (fruit) {
  return function () {
    alert('your choice is ' + fruit);
  };
};
fruits.forEach(function (fruit) {
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', alertFruitBuilder(fruit));
  $ul.appendChild($li);
});

...

   →alertFruitBuilder로 정의한 함수에서 익명 함수를 반환하는데 이 익명함수가 기존의 alertFruit 함수임

   →addEventListener의 콜백 함수로 alertFruitBuilder를 지정하면 alertFruitBuilder는 fruit값을 받아 alert를 띄우는 함수를 반환함

   →결국, fruit를 받아 alert를 띄우는 함수가 addEventListener의 콜백 함수가 되고, 클릭이 발생했을 때, alert('your choice is ' + fruit)가 바로 fruit를 참조함

   →alertFruitBuilder가 반환하는 함수에는 클로저가 존재함

 

 

 2) 접근 권한 제어(정보 은닉)

  -정보 은닉: 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화

   →모듈 간의 결합도 낮추고 유연성 높임

  -접근 권한의 종류

   →public: 외부에서 접근이 가능

   →private: 내부에서만 사용하며 외부에 노출되지 않는 것

   →protected: private이지만 동일 패키지의 클래스나 하위 클래스 관계에서 접근 가능, 거의 사용 안함

  -함수 내에서 return을 사용하여 선택적으로 일부 변수에만 접근 권한을 부여할 수 있음

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());

   →outer라는 변수를 통해 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 개입 불가

   →외부에서는 오직 outer함수가 return한 정보(inner)에만 접근 가능

   →return한 변수들은 공개 멤버, return하지 않은 정보는 비공개 멤버

 

  -예시(자동차 경주 게임)

   →규칙 1) 각 턴마다 주사위를 굴려 나온 숫자(km)만큼 이동

   →규칙 2) 차량별 연료(fuel), 연비(power)는 무작위

   →규칙 3) 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동 못함

   →규칙 4) 모든 유저가 이동할 수 없는 턴에 게임 종료

   →규칙 5) 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리

// 규칙에 따라 생성한 자동차 객체

var car = {
  fuel: Math.ceil(Math.random() * 10 + 10),
  power: Math.ceil(Math.random() * 3 + 2),
  moved: 0,
  run: function () {
    var km = Math.ceil(Math.random() * 6);
    var wasteFuel = km / this.power;
    if (this.fuel < wasteFuel) {
      console.log('이동불가');
      return;
    }
    this.fuel -= wasteFuel;
    this.moved += km;
    console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
  }
};

   →랜덤으로 설정되는 fuel, power와 이동거리인 moved에 접근이 가능하여 다음과 같이 사용자가 조작 가능

car.fuel = 10000;
car.power = 100;
car.moved = 1000;

   →문제 해결을 위해 자동차를 객체가 아닌 함수로 만들고, 필요한 멤버만 return하면 됨

// 규칙에 따라 생성한 자동차 함수

var createCar = function () {
  var fuel = Math.ceil(Math.random() * 10 + 10);
  var power = Math.ceil(Math.random() * 3 + 2);
  var moved = 0;
  return {
    get moved () {
      return moved;
    },
    run = function () {
      var km = Math.ceil(Math.random() * 6);
      var wasteFuel = km / power;
      if (fuel < wasteFuel) {
        console.log('이동불가');
        return;
      }
      fuel -= wasteFuel;
      moved += km;
      console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
    }
  };
};
var car = createCar();

   →fuel과 power는 비공개 멤버로 함수 외부에서 접근할 수 없고, moved는 getter만 부여하여 읽기 전용

   →외부에서는 moved를 읽는 것과 run 메서드를 실행하는 것만 가능

console.log(car.moved) // 지금까지 총 이동거리 출력
consoel.log(car.fuel) // undefined
consoel.log(car.power) // undefined

car.fuel = 1000;          // car.fuel에 1000을 대입할 수 있고
console.log(car.fuel);  // 출력하면 대입한 값인 1000이 나오지만
car.run();                  // run함수에까지 영향을 미치지는 못함, power와 moved도 마찬가지

   →여전히 run 메서드를 다른 내용으로 덮어씌우는 어뷰징은 가능한 상태이고 이것까지 막으려면 freeze 등의 메서드 사용

1. 클로저의 의미 및 원리

 -A closure is the combination of a function and the lexical environment within that function was declared. -MDN-

 -클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상

 -여기서 함수가 선언될 당시의 lexical environment는 lexical environment의 outerEnvironmentReference에 해당

 

[코어 자바스크립트] 02. 실행 컨텍스트

1. 실행 컨텍스트 1) 실행 컨텍스트: 실행할 코드에 제공할 환경 정보들을 모아놓은 객체 -선언된 변수, 함수 등을 실행 컨텍스트에 저장해두었다가 호출하면 나옴 2) 스택(stack): 우물 같은 구조로

data-science-study.tistory.com

 -윗글을 참조하면  outerEnvironmentReference는 현재 컨텍스트 바깥의 컨텍스트에 접근할 수 있도록 바깥의 컨텍스트를 저장

     ex) 컨텍스트 A → 내부에 존재하는 내부함수 B

          ▶내부함수 B의 실행 컨텍스트가 활성화된 시점에서 B의 outerEnvironmentReference가 A의 lexical environment를 참조하므로 A의 변수에 접근 가능

          ▶A는 B에서 선언한 변수에 접근 불가능

 -예시에서 B가 A의 변수에 접근가능하여 사용하면 B와 A가 상호작용을 한 것이지만 A의 변수를 무조건 쓰지는 않으므로 상호작용 하지 않을 수도 있음

 -즉, 내부함수에서 외부 변수를 참조하는 경우에 한해서만 combination이 발생

 

 -결론적으로, 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상

     (컨텍스트 A에서 선언한 변수를 내부함수 B에서 참조하는 경우에 클로저 발생)

 

 -예시 1)

var outer = function() {
  var a = 1;
  var inner = function() {
    console.log(++a);
  };
  inner();
};
outer();

  →outer 함수에서 변수 a를 선언

  →outer 함수 내에서 inner 함수 선언

  →inner 함수 내에서 a의 값을 1 증가시킨 후 출력

  →inner 함수 내에 변수 a가 선언되지 않았으므로 environmentRecord에서 값을 찾지 못하고 outerenvironmentReference에 지정된 outer 함수의 environmentRecord에 접근하여 outer 함수에서 지정된 변수 a에 접근

  →따라서, '2' 출력

 

 -예시 2)

var outer = function() {
  var a = 1;
  var inner = function() {
    return ++a;
  };
  return inner();
};
var outer2 = outer();
console.log(outer2);

  →이번에는 outer 함수 내에서 inner() 함수의 실행 결과(2)를 반환

  →outer 함수의 실행 컨텍스트가 종료된 시점 이후 a 변수와 inner 함수는 사라질 것

  →outer2에는 inner 함수의 실행결과인 '2'가 저장되어, '2'가 출력되며 outer2를 이후에 반복해서 출력해도 '2'만 출력

 

 -예시 3)

var outer = function() {
  var a = 1;
  var inner = function() {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());

  →inner 함수 자체를 반환하여 outer2에는 inner(a에 1씩 더한 값을 반환하는) 함수가 저장됨

  →예시 2)에서 outer2 = 2였다면 예시 3)에서 outer2 = function() { return ++a }

  →따라서, outer2()를 실행할 때마다 ++a, 즉 a에 1씩 더해져 반환되고 반복해서 실행하면 계속 1씩 더해진 값을 가질 수 있음

  →outer 함수의 실행 종료 시점에서 inner 함수를 반환하고 inner 함수를 반환하고 나면 outer 함수는 종료된 상태

  →이 때, inner 함수는 outer2에 저장되어 outer2를 실행하면 언제든 호출될 가능성이 열린 것이므로 inner 함수가 참조해야하는 outer 함수의 LexicalEnvironment는 가비지 콜렉터의 수집대상에서 제외됨

 

 -예제 3)을 통해 알 수 있는 클로저의 정의

  →어떤 함수 A(outer)에서 선언한 변수(a)를 참조하는 내부함수 B(inner)를 외부로 전달(outer2에 inner 함수 자체를 반환)할 경우, A의 실행 컨텍스트 종료 이후에도 변수가 사라지지 않는 현상

  →이때, 외부로 전달이 return만 의미하는 것은 아님

 

 -예시 4)

( function () {
    var a = 0;
    var intervalId = null;
    var inner = function () {
      if (++a >=10 ) {
        clearInterval(intervalId);
      }
      console.log(a);
    };
    intervalId = setInterval(inner, 1000);
})();

  →setInterval에 전달할 콜백 함수 inner는 inner 외부에서 선언된 변수 a를 참조

 

 -예시 5)

( function () {
    var count = 0;
    var button = document.createElement('button');
    button.innerText = 'click';
    button.addEventListener('click', function () {
      console.log(++count, 'times clicked');
    });
    document.body.appendChild(button);
})();

  →'click'이라고 쓰인 버튼을 생성하고, 버튼을 클릭할 때마다 count가 1씩 더해진 값과 'times clicked' 문자가 함께 출력

  →이때, addEventListener에서 실행할 함수 내부에서 외부에 있는 변수 count를 참조함

 

 -예제 5), 6) 모두 지역변수(a, count)를 참조하는 내부함수(inner, function)를 외부(setInterval, button)으로 전달했기 때문에 클로저

 

 

2. 클로저와 메모리 관리

 -클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수가 메모리를 소모하도록(a가 사라지지 않고 계속 참조되도록 하는 등) 함으로써 발생

 -필요성이 사라진 시점에서 메모리를 더 소모하지 않도록 해주면 됨

 -참조 카운트를 0으로 만들어 더 이상 참조되지도 않고 쓰이지도 않게 된다면 메모리가 회수될 것

 -이 때, 참조 카운트를 0으로 만드는 방법으로 null이나 undefined를 할당하면 됨

 

 -예시 1)

var outer = (function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
})();
console.log(outer());
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음

  →outer 함수를 null로 정의하며 outer 함수 내부에서 변수 a를 참조하던 inner 함수를 없애며 a를 참조하는 컨텍스트가 아무것도 없도록 만듦

 -예시 2)

( function () {
    var a = 0;
    var intervalId = null;
    var inner = function () {
      if (++a >=10) {
        clearInterval(intervalId);
        inner = null;  // inner 식별자의 함수 참조를 끊음
      }
      console.log(a);
    };
    intervalId = setInterval(inner, 1000);
})();

  →함수 내부에서 변수를 참조하던 내부함수 inner 자체를 null로 정의하면 함수 내부변수 a룰 참조하는 것이 없도록 만듦

 

 -예시 3)

( function () {
    var count = 0;
    var button = dovument.createElement('button');
    button.innerText = 'click';
    
    var clickHandler = function () {
      concole.log(++count, 'times clicked');
      if (count >= 10) {
        button.removeEventListener('click', clickHandler);
        clickHandler = null;  // clickHandler 식별자의 함수 참주 끊음
      }
    };
    button.addEventListener('click', clickHandler);
    document.body.appendChild(button);
})();

  →count 변수를 계속 참조하며 button에 eventListener로 주는 함수 clickHandler를 null로 정의하며 count를 참조할 컨텍스트를 없앰

5. 콜백 지옥과 비동기 제어

 1) 콜백 지옥: 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 너무 깊어지는 현상

  -비동기적 작업에서 콜백 지옥이 자주 등장

 

 2) 비동기: 동기의 반대말로, 현재 코드 실행 완료 여부와 무관하게 다음 코드 실행

  -동기적인 코드는 현재 실행 중인 코드가 완료된 후 다음 코드 실행

  -CPU 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적인 코드

  -setTimeout, addEventListener, XMLHttpRequest 등 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적 코드

  -웹의 복잡도에 따라 비동기적 코드의 비중이 높아졌고 콜백 지옥에 빠지기 쉬워짐

 

 3) 콜백 지옥 예시

setTimeout(function (name) {
  var coffeeList = name;
  console.log(coffeeList);
  
  setTimeout(function (name) {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    
    setTimeout(function (name) {
      coffeeList += ', ' + name;
      console.log(coffeeList);
      
      setTimeout(function (name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);
      }, 500, '카페라떼');
    }, 500, '카페모카');
  }, 500, '아메리카노');
}, 500, '에스프레소');

// 결과
// 에스프레소
// 에스프레소, 아메리카노
// 에스프레소, 아메리카노, 카페모카
// 에스프레소, 아메리카노, 카페모카, 카페라떼

  -0.5초마다 커피 이름을 하나씩 추가하여 출력

  -들여쓰기가 깊고, 값이 전달되는 순서가 아래에서 위로 어색하게 향함

 

 4) 해결방법

  (1) 기명함수로 변환

var coffeeList = '';

var addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, '아메리카노');
};
var addAmericano = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addMocha, 500, '카페모카');
};
var addMocha = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addLatte, 500, '카페라떼');
};
var addLatte = function (name) {
  coffeeList += ', ' + name;
  console.log(coffeeList);
};

setTimeout(addEspresso, 500, '에스프레소');

  -가독성이 높아지고, 위에서 아래로 읽어내려가는대로 출력되어 어색함도 사라짐

  -일회성 함수를 매번 변수에 할당하는 것이 복잡해 보일 수 있음

 

  (2) promise

   -방법 1

new Promise(function (resolve) {
  setTimeout(function () {
    var name = '에스프레소';
    console.log(name);
    resolve(name);  // promise가 fufilled(성공)일 때, name 반환
  }, 500);
}).then(function (prevName) {  // promise.then에서 위의 resolve 결과를 받아 인자로 사용
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + ', 아메리카노';  // 인자로 받은 초기 name에 '아메리카노' 추가
      console.log(name);
      resolve(name);  // 아메리카노가 추가된 name을 다시 반환
    }, 500);
  });
}).then(function (prevName) {  // 위에서 resolve로 넘겨준 아메리카노가 추가된 name을 인자로 사용
  return new Promise(function (resolve) {  // 이후 반복
    setTimeout(function () {
      var name = prevName + ', 카페모카';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + ', 카페라떼';
      console.log(name);
      resolve(name);
    }, 500);
  });
});

   -resolve 또는 reject가 있으면 둘 중 하나가 실행되기 전까지 다음 then 또는 catch로 넘어가지 않으므로 비동기 작업의 동기적 표현이 가능

 

   -방법 2

var addCoffee = function (name) {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        var newName = prevName ? (prevName + ', ' + name) : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  };
};

addCoffee('에스프레소')()
  .then(addCoffee('아메리카노'))
  .then(addCoffee('카페모카'))
  .then(addCoffee('카페라떼'));

 

   -방법 1에서 반복적이던 내용을 함수화해서 짧게 표현

 

  (3) Generator

var addCoffee = function(prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ', ' +name : name);
  }, 500);
};
var coffeeGenerator = function* () {  // Generator 함수
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  var mocha = yield addCoffee(americano, '카페모카');
  console.log(mocha);
  var latte = yield addCoffee(mocha, '카페라떼');
  console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

   -Generator 함수를 실행하면 Iterator가 반환되며

   -Iterator는 next라는 메서드를 가짐

   -next를 실행하면 Generator 함수 내 가장 첫번째 yield에서 함수의 실행을 멈춤

   -이후 다시 next 메서드 호출하면 멈췄던 부분부터 시작해서 다음 yield에서 함수의 실행을 멈춤

   -비동기 작업이 완료되는 시점(console에 name 출력)마다 next를 호출하면 Generator 함수가 위에서 아래로 순차적으로 진행됨

 

  (4) promise + async/await

var addCoffee = function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
};

var coffeeMaker = async function () {
  var coffeeList = '';
  var _addCoffee = async function (name) {
    coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
  };
  await _addCoffee('에스프레소');
  console.log(coffeeList);
  await _addCoffee('아메리카노');
  console.log(coffeeList);
  await _addCoffee('카페모카');
  console.log(coffeeList);
  await _addCoffee('카페라떼');
  console.log(coffeeList);
};
coffeeMaker();

   -비동기 작업을 수행하고자 하는 함수 앞에 async 표기

   -함수 내부에 실질적으로 비동기 작업이 필요한 위치마다 await 표기

   -await를 쓰려면 함수 앞에 반드시 async가 표기되어 있어야 함

+ Recent posts