카카오 클라우드 스쿨 1기 개발자 과정 4주차 - 4

EUNLEE·2022년 7월 7일
0

2022.07.07 목요일

🐠 생성자 함수와 prototype

__ proto__ property와 prototype property

__proto__

  • 접근자 property
  • 상속되었기 때문에 사용가능!
  • Object 생성자 함수에게 상속받는다.
  • Object.prototype.__proto__

💡 상속을 표현할 때는 화살표방향을 아래에서 위를 가리키게 그린다.

function Person(name){
    this.name = name; // instatnce의 property
}

const person = new Person('홍길동');

console.dir(Person); // 브라우저에서 확인

1️⃣ f Person(name) → prototype: {constructor: f} [[Prototype]]: Object → proto: Object → proto: null

2️⃣ f Person(name) → [[Prototype]]: f () → [[Prototype]]: Object → proto: f ()→ [[Prototype]]: Object → proto: Object → proto: null
1️⃣, 2️⃣ 내용은 브라우저에서 확인할 수 있다.

💡 그런데 __proto__라는 표현이 code에 직접 나오는 것은 권장되지 않는다.
혹시라도 상속을 못받는 경우 undefined가 뜨기 때문이다.
__proto__ 보다는 Object.getPrototypeOf()를 사용하는 것이 좋다.

// 객체 생성하는 방법
// 객체를 생성할 때 객체의 상위 prototype 객체를 직접 지정할 수 있다.
const obj = Object.create(null);

console.log(obj.__proto__); //undefined

// 그럼 이런 경우에는 어떻게 하면 좋을까요?
// Object()가 가지고 있는 method를 이용하는게 좋다.
// Object.getPrototypeOf(obj): obj의 상위 프로토타입 객체를 들고와라!
console.log(Object.getPrototypeOf(obj)); //null -> 상위 프로토타입 객체가 없다.

prototype property

  • constructor(함수 선언문, 함수 표현식, class를 생성자 함수로 사용했을 때)만 가질 수 있는 property
  • 따라서 일반객체(객체 literal같은거), non-constructor(arrow-function, ES6 method)는 prototype property를 가지지 않는다.
property소유의미
proto모든 객체prototype 객체의 참조
prototypeconstructorprototype 객체의 참조


객체 literal로 생성된 Object의 상위 prototype 객체

💡 그림을 code로 확인해보자.

var foo = function(){}; // 함수표현식
console.log(foo.__proto__ === Function.prototype); //true
console.log(foo.prototype.__proto__ === Object.prototype); //true
console.log(Object.prototype.__proto__); //null
// 함수 foo의 생성자는 Function 생성자 함수
console.log(foo.constructor === Function); //true

literal에 따른 생성자 함수

객체 literal → Object.prototype → Object
함수 literal → Function.prototype → Function
배열 literal → Array.prototype → Array

Prototype 객체는 언제 생성되나요?
→ 생성자 함수와 같이 만들어져요.

overriding과 property shadowing

/* instance 메소드와 prototype 메소드 */
function Person(name){
    this.name = name;
    // instance 메소드
    // this.getName = function(){

    // }
}

// prototype 메소드 선언
Person.prototype.sayHello = function(){
    console.log(`안녕하세요. ${this.name}`);
}

// instance를 생성
const me = new Person('홍길동');

/* overriding과 property shadowing */
// overriding: 상속되는 method를 재정의
me.sayHello = function(){
    console.log(`Hello ${this.name}`);
}

// 만약 overriding이 발생하면 이 발생된 overriging에 의해서
// 숨겨진 prototype 메소드를 property shadowing 되었다고 한다.
me.sayHello(); //Hello 홍길동

Prototype 객체 변경하기

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

// 자동으로 만들어진 Person의 prototype을 객체literal을 사용해서 변경하기
Person.prototype = {
    sayHello(){
        console.log(('안녕하세요!'));
    }
}

const me = new Person('홍길동');

console.log(me.constructor === Object); //true
// me의 생성자는 Person이 아닌 Object임을 주의할 것!

그런데 이렇게 변경을 하게되면 객체 literal로 만든 객체는 non-constructor여서 me 객체는 constructor property가 없기 때문에 상위 객체인 Person과의 연결이 끊기게 된다. 그렇기 때문에 me의 constructor를 더 상위 객체에서 찾게된다. 따라서 me의 constructor는 Person이 아닌 Object가 된다.

만약 이런 문제가 발생하는 것이 싫다면 직접 변경한 객체에 constructor property를 추가해서 Person과 연결시켜주면 된다.

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

// 자동으로 만들어진 Person의 prototype을 객체literal을 사용해서 변경하기
Person.prototype = {
    **constructor: Person,**
    sayHello(){
        console.log(('안녕하세요!'));
    }
}

const me = new Person('홍길동');

console.log(me.constructor === Person); //true


Prototype 객체의 상위 객체 변경하기

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

const me = new Person('홍길동');

const parent = {
    sayHello(){
        console.log('안녕하세요!');
    }
}

// me의 prototype 객체의 상위 객체를 parent로 변경한다.
Object.setPrototypeOf(me, parent);

console.log(me.__proto__ === Person.prototype); //false

instance 메소드, prototype 메소드, static 메소드

function Person(name){
    this.name = name
    // instance 메소드: 각각의 instance가 독자적으로 갖는 메소드
    this.callme = function(){}
}

// prototype 메소드: 만들어진 하위 객체에 상속되는 메소드
Person.prototype.sayHello = function(){
    console.log('안녕!');
}

// static 메소드: instance가 사용할 수 없는 메소드
Person.staticMethod = function(){ // person의 property에 붙는다.
    console.log('Hi!');
}

🐠 Strict mode

Implicit global

function foo(){
    x = 10; 
    // ReferenceError가 아닌 implicit global(묵시적 전역)이 발생
    // 전역변수화! (window객체의 property로 붙는다.)
}

foo();

console.log(x); //10
// x가 window라는 전역 객체의 property로 붙어서 전역 변수처럼 사용할 수 있게된다.

🧐 이런 현상이 좋은건가요?
당연히 좋지 않다. 어디선가 side effect가 생길 확률이 높다. 이런 식의 code는 지양해야 한다.
이런 상황을 언어적인 차원에서 사용하지 못하게끔 처리할 수 있다. 어떻게요??
Strict mode
'use strict'; 를 적어준다.

'use strict';
function foo(){
    x = 10; 
}

foo();
console.log(x); //ReferenceError: x is not defined
  • Strict mode는 일반적으로 전역에 잡지 않는다.
    • JavaScript Engine이 기동되면서 여러 라이브러리들이 전역으로 함께 올라온다. 만약 전역에 strcit mode를 잡으면 다른 일반 라이브러리들이 동작을 안할 수 있다.
    • 따라서 함수 레벨에 적어주는 것이 좋다.
  • 즉시 실행 함수(IIFE)를 만들어서 사용하는 것이 일반적이다.
    • strict mode를 함수별로 사용할 수 있다.

      (function(){
          // non-strict mode
          var let = 10; // 예약어인 let을 식별자를 써도 error가 발생하지 않는다.
          // inner function, nested function
          function foo(){
              'use strict'; // strict mode
              let = 20; //SyntaxError: Unexpected strict mode reserved word
          }
      }())

Strict mode 사용시 주의점

  1. Implicit global(묵시적 전역)을 사용할 수 없다.
  2. 변수, 함수, 매개변수를 delete로 삭제할 수 없다.
  3. this의 의미가 달라진다.
    • 일반 함수에서 this는 최상위 객체인 window를 가리키는데 strict모드에서는 this가 undefined가 된다.

🐠 Built-in Object

Built-in Object, Built-in 함수(function), Built-in 전역함수

모두 같은 용어이다.

built-in (전역)함수

JavaScript Engine이 기동하면 생성된다.

  • Object
  • Function
  • Array
  • Math
  • JSON
  • Number
  • String
  • 등등등.. 대략 40개

💡 Math와 JSON은 생성자 함수가 아니라서 객체를 만들 수 없습니다! 대신 자체적인 정적 method를 제공합니다.

/* 자바스크립트의 최상위 객체는 window */
console.log(Object === window.Object); //true (브라우저에서 확인하기)

유사 배열 객체(wrapper 객체)

var obj = new Object();

var str = 'Hello'; // primitive value
                   // data type: string
'Hello'.toUpperCase(); // 내부적으로 refer 객체를 만든다.
str.toUpperCase(); // wrapper 객체 생성, 소멸(객체화시킨 후 대문자로 바꾸고 소멸된다.)
str.tolowerCase(); // wrapper 객체 생성, 소멸(객체 생성 소멸이 반복되므로 좋지않다.)

var strObj = new String('홍길동');
//console.log(typeof strObj); //object
console.dir(strObj); // 유사 배열 객체(브라우저에서 확인)

전역 객체(사용하는 platform에 따라 다르다.)

  • window, globalglobalThis(ES11 표준)
  • 개발자가 의도적으로 생성할 수 없다.
  • browser 환경에서는 window
    • DOM(Document Object Model)
    • BOM(Browser Object Model)
    • XMLHttpRequest
    • var로 선언된 전역변수(let, const 전역변수는 붙지않는다.)
    • 묵시적 전역
    • NaN(property key) → NaN(primitive value인 property value)
    • undefined(property key) → undefined(primitive value인 property value)

🐠 Closure(클로저)

closure는 JavaScript 고유의 개념이 아니다. 함수형 프로그래밍 언어에서 제공되는 기능이다.

First-class citizen(object)(일급객체)에 대한 이해 선행

💡 일급 객체
1. 객체가 익명으로 생성이 가능(runtime에 생성이 가능)
2. 값으로 변수나 자료구조에 저장이 가능
3. 함수의 매개변수로 전달 가능
4. 함수의 return값으로 사용 가능

“function” 함수 → 객체로 취급 → 일급객체 취급 → “first-class function”

MSN(MicroSoft Network)에서 closure를 간단하게 정의

closure는 함수와 그 함수가 선언된 lexical 환경(environment)의 조합이다.

💡 lexical environment는 뭔가요?
execution context(실행 컨텍스트)
실행 컨텍스트 = 정보를 저장하기 위한 메모리 공간(lexical environment)으로 인식 + 코드 실행 순서 관리
메모리 공간
⇒ lexical Environment
코드 실행 순서 관리 ⇒ Execution context Stack(stack으로 관리)

ECMAScript 명세

→ 4가지 종류의 코드

  1. 전역 코드
    • 전역에 존재하는 소스 코드
    • 전역 함수의 내부코드는 포함되지 않는다.
    • 전역 변수를 관리하기 위해 전역 scope를 생성, 전역에 var keyword로 선언된 식별자 찾고 window객체 만들고 식별자를 window 객체에 binding한다.
    ⇒ 이런 작업을 하기위해 전역 실행 컨텍스트를 생성한다!
  2. 함수 코드
    • 함수 내부에 존재하는 소스 코드
    • 중첩 함수의 내부 코드는 포함하지 않는다.
    • 함수 내부에서 사용하는 지역변수, 매개변수, arguments가 관리하기 위해 지역 scope를 생성하고 이 지역 scope를 전역 scope와 연결해서 scope chain을 생성한다.
    ⇒ 이런 작업을 하기위해 함수 실행 컨텍스트를 생성한다!
  3. eval 코드
  4. 모듈 코드

⇒ code가 어디에 있느냐에 따라 scope가 달라진다. scope에 따라 code를 관리해야 한다. 실행 컨텍스트가 별도로 생성되고 관리된다.

const x = 1;

function foo(){
    const y = 2;
    function bar() {
        const z = 3;
        console.log(x+y+z);
    }
    bar();
}
foo()

closure가 되기위한 조건

💡 closure 조건

const x = 1;
function outer(){
    const x = 10;
    const inner = function(){
        console.log(x);
    }
    return inner;
}
const innerFunc = outer();
innerFunc();
  1. closure는 중첩함수
  2. 이 중첩함수가 외부함수의 결과값으로 return
  3. 리턴되는 중첩함수가 외부함수의 식별자를 참조
  4. 리턴되는 중첩함수의 life cycle(생명주기)가 외부함수보다 길어야 한다.
  5. 이 때 중첩함수에서 외부함수에 대한 참조가 남아있기 때문에 외부함수의 실행은 execution context stack에서 제거되지만 외부함수의 lexical environment는 메모리에 남아있어서 중첩함수에 의해 사용될 수 있는 현상

    이 조건에 맞는 것들만 closure이다.
    아래는 closure가 아닌 예시이다.
function foo(){
    const x = 1;
    const y = 2;
    function bar(){
        const z = 3;
        console.log(z);
    }
    return bar;
}
const bar = foo();
bar();
function foo(){
    const x = 1;
    const y = 2;
    function bar(){
        const z = 3;
        console.log(x);
    }
    bar();
}
const bar = foo();
bar();

closure를 어디에 쓸까?

Information Hiding 구현에 사용한다!

let num = 0; // 변수 num은 전역이기 때문에 보호할 수 없다.
const increase = function(){
    return ++num;
}

console.log(increase()); //1
console.log(increase()); //2
num = 100; // num 데이터 손상
console.log(increase()); //101

변수를 보호하고(숨기고) 싶다.. → 전역 변수 num을 지역 변수로 만들자.

const increase = function(){
    let num = 0;
    return ++num;
}

console.log(increase()); //1
console.log(increase()); //1
console.log(increase()); //1

함수를 호출할 때마다 초기화가 되는 문제가 발생한다..😧

💡 함수 본연의 기능을 살리면서 변수를 보호하는 방법은 없을까??
closure를 응용하자!

const increase = (function(){ // 즉시 실행 함수
    let num = 0;
    return function(){
        return ++num;
    }
}());
console.log(increase()); //1
console.log(increase()); //2
console.log(increase()); //3

변수 num에 접근할 수도 없고, 함수 본연의 기능도 잘 유지된다.

const counter = (function(){
    let num = 0;
    return {
        increase(){
            return ++num;
        },
        decrease(){
            return --num;
        }
    }
}());
console.log(counter.increase()); //1
console.log(counter.increase()); //2
console.log(counter.increase()); //3
console.log(counter.decrease()); //2
console.log(counter.decrease()); //1
console.log(counter.decrease()); //0

information hiding을 closure를 통해 구현한 예시

0개의 댓글