JS 기초 문법 필기

배코딩·2022년 10월 23일
0

note

목록 보기
53/151

poiemaweb 11번부터 작성


객체의 방어적복사

Object.assign(객체1, 객체2, 객체3);

객체1에 객체2, 객체3의 프로퍼티를 덮어씌움. 객체1이 변경됨

함수의 리턴값도 객체1의 참조값이라서, 만약에

let new_obj = Object.assign(객체1, 객체2, 객체3);

여기서 new_obj의 프로퍼티를 변경하면 객체1의 프로퍼티도 변경된다.

단, 객체 내부의 객체는 얕은 복사가 된다.


불변 객체화

Object.freeze(객체);

이 후 객체의 프로퍼티를 변경할 수 없다.

다만 객체 내부의 객체는 변경할 수 있으므로, 이를 방지하려면 깊은 복사 메소드를 직접 구현하여 사용하면 된다.

function deepFreeze(obj) {
    const props = Object.getOwnPropertyNames(obj);
    
    props.forEach((name) => {
        const prop = obj[name];
        
        if (typeof prop === 'object' && prop !== null) {
            deepFreeze(prop);
        }
    });
    
    return Object.freeze(obj);
}

12번 함수

  • 함수의 호이스팅은 변수의 호이스팅과 달리 내용까지도 호이스팅이 돼서 선언 이전에 함수를 사용할 수 있다. 대체로 이를 지양하므로, 함수 선언문으로 만들어두기보다는 변수의 호이스팅이 일어나도록 변수에 함수를 저장하는 함수 표현식으로 선언하자.

  • 함수는 익명 함수, 기명 함수 둘 다 선언 가능하다.

  • 함수는 일반 객체와 달리 함수만의 프로퍼티를 갖고 있다.

    arguments 프로퍼티는 가변 인자 구현에 유용하다. iterable한 유사 배열 객체인데, 실제로 들어온 매개변수들을 순회할 수 있다.

    length 프로퍼티는 함수 정의 시 지정한 매개변수 갯수이다. 실제로 들어온 매개변수인 arguments의 길이 값과 다를 수 있으니 유의하자.

    caller 프로퍼티는 자신을 호출한 함수를 의미한다.

    name 프로퍼티는 자기 자신(함수)의 이름을 의미한다. 익명 함수라면 빈 문자열이다.

    proto 프로퍼티는 자기 자신(함수)의 프로토타입 객체에 접근하기 위한 접근자 프로퍼티이다. 모든 객체의 프로토타입 객체는 Object.prototype이다. 이 것이 proto 프로퍼티를 가지는데 이를 모든 객체가 상속을 받아서, 접근자 프로퍼티로서 기능하는 것이다.

    함수도 proto로 함수의 프로퍼티에 접근할 수 있다. 이 때 프로토타입 객체는 Function.prototype이다.

    참고로 함수 객체는 prototype 프로퍼티를 갖고 있고, 일반 객체는 없다.


즉시 실행 함수

람다함수같은 느낌이다. 소괄호로 묶어줘서 쓴다

(function(){})(); 또는 (function(){}());

이는 스코프 측면에서 유용하다.

두 개의 스크립트 파일이 있는데 이름이 같은 변수 또는 함수끼리 충돌할 것 같을 때, 한 쪽에 소괄호를 씌워주면 충돌이 일어나지 않게된다.


  • 자바스크립트는 함수 단위 스코프를 가진다.

    그래서 외부 함수에서 내부 함수의 변수에 접근할 수 없고, 내부 함수에서 외부 함수 변수에 접근할 수는 있다.

    또한 부모 함수의 외부에서 자식 함수에 접근할 수 없다.


  • 콜백 함수는 특정 이벤트가 발생했을 때 시스템에 의해 호출되는 함수이다.

    예를 들면 addEventListener 내부에서 클릭 이벤트가 발생하면 그 때 지정해놓은 콜백 함수를 실행하는 이벤트 핸들러 처리가 있다.

    콜백 함수는 주로 비동기식 처리 모델에 사용된다.


13강 타입 체크

  • typeof로 타입을 체크할 수 있지만, 원시 타입은 문제 없는데 객체의 경우 세세한 종류를 구분하지 못하고 다 그냥 object로 리턴을 해버리는 허점이 있다.

    이를 구현하기 위해 Object.prototype.toString.call 메소드를 사용하면 모든 타입을 판단할 수 있다.

    그런데 이 것의 허점은 객체의 상속 관계까지 추려낼 순 없다는 것이다.

    예를 들어 객체는 객첸데 DOM을 HTMLElement를 상속받은 DOM 요소인 객체를 추려내고 싶다면, 이 부분은 instanceof를 활용하면 된다.

    A instanceof B를 하면, A의 프로토타입 체인에 존재하는 모든 constructor를 검사하여 B와 일치하는 constructor가 있다면 true를 반환해준다.


  • 배열인지 확인할 땐 Array.isArray 메소드

14강 Prototype

  • 모든 객체는 Prototype과 연결되어있고, 프로토타입의 프로퍼티와 메소드를 사용할 수 있다.

  • 객체의 프로토타입 접근자는 .proto 이다.

  • 객체는 자신의 프로토타입을 가리키는 [[Prototype]]이라는 인터널 슬롯을 가지고 있다. 객체의 프로토타입은 다른 임의의 객체로 바꿀 수 있다. 즉, 객체의 상속을 구현할 수 있다.

  • 함수는 다른 객체와 달리 [[Prototype]]과 더불어 prototype 프로퍼티도 가지고 있다.

함수의 [[Prototype]] : Function.prototype 을 가리킴.

함수의 prototype 프로퍼티 : 함수 객체가 생성자로 사용될 때, 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다.


  • 프로토타입 객체는 constructor 프로퍼티를 갖는다. 이는 객체의 입장에서 자신을 생성한 객체를 가리킨다.
function Person(name) {
  this.name = name;
}

var foo = new Person('Lee');

foo.constructor === Person
Person.constructor === Function
Person.prototype.constructor === Person


  • Prototype chain : 객체에 특정 메소드나 프로퍼티가 접근했을 때 그게 없다면, 프로토타입 객체로 거슬러 올라가 찾는다. 이걸 프로토타입 체인이라고 한다.

객체에 hasOwnProperty라는 메소드가 없어도, 프로토타입에는 그 메소드가 있으므로 정상 출력된다.


  • 객체 생성 방법 3가지

1) 객체 리터럴

2) 생성자 함수

3) Object() 생성자 함수

객체 리터럴 방식으로 생성된 객체의 프로토타입 체인 그림


함수를 정의하는 방식 3가지

1) 함수선언식

2) 함수표현식

3) Function() 생성자 함수

함수도 객체도 리터럴 방식으로 생성할 때, 내부적으로는 Object()나 Function() 생성자 함수로 생성한다.

따라서 어떤 방식으로 함수 객체를 생성하든 그 것의 prototype 객체는 Function.prototype이다.

함수를 생성하는 생성자 함수도 결국 함수 객체이므로 이 것의 prototype 객체도 Function.prototype이다.

생성자 함수로 생성된 객체의 프로토타입 체인 그림

그림에 의하면 foo 객체의 프로토타입인 Person.prototype 객체, Person() 생성자 함수의 프로토타입인 Function.prototype의 프로토타입은 모두 Object.prototype이다. 객체 리터럴 방식이나 생성자 함수 방식이나 결국 모든 객체의 부모 객체인 Object.prototype에서 체인이 끝나기에, Object.prototype 객체를 프로토타입 체인의 종점이라 한다.


Person 생성자 함수로 foo 객체를 만들었다고 치자.

이 때 Person.prototype 객체에 sayHello라는 메소드를 추가하면,

이 것이 프로토타입 체인에 바로 반영되어서, Person에 의해 생성된 모든 객체는

부모 객체인 Person.prototype의 sayHello 메소드를 사용할 수 있다.

Person.prototype 객체는 Person으로 생성되는 모든 객체의 부모 객체라고 생각하면 될 듯

참고 그림


var str = 'test';
console.log(typeof str);                 // string
console.log(str.constructor === String); // true
console.dir(str);                        // test

var strObj = new String('test');
console.log(typeof strObj);                 // object
console.log(strObj.constructor === String); // true
console.dir(strObj);
// {0: "t", 1: "e", 2: "s", 3: "t", length: 4, __proto__: String, [[PrimitiveValue]]: "test" }

console.log(str.toUpperCase());    // TEST
console.log(strObj.toUpperCase()); // TEST

위 코드에서 볼 수 있듯이, 문자열은 원시 타입인데도 toUpperCase같은 메소드도 쓰고 있고 constructor가 String 객체를 가리키고 있다.

당연히 원시 타입인 문자열은 메소드를 가질 수 없고, 메소드를 호출하게 되면 원시 타입과 연관된 객체, 즉 String 객체로 일시 변환되어 프로토타입 객체를 공유하여 그 안의 메소드를 쓸 수 있게 되는 것이다.

즉 String.prototype에 프로퍼티나 메소드를 정의하면 원시 타입인 문자열에 대해서도 호출해서 쓸 수 있다.

String.prototype.myMethod = function(){return 'myMethod';};
console.log("sentence".myMethod); -> 출력: myMethod


var str = 'test';

String.prototype.myMethod = function() {
  return 'myMethod';
}

console.log(str.myMethod());
console.dir(String.prototype);

console.log(str.__proto__ === String.prototype);                 // ① true
console.log(String.prototype.__proto__  === Object.prototype);   // ② true
console.log(String.prototype.constructor === String);            // ③ true
console.log(String.__proto__ === Function.prototype);            // ④ true
console.log(Function.prototype.__proto__  === Object.prototype); // ⑤ true

문자열 프로토타입 체인 그림


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

var foo = new Person('Lee');

// 프로토타입 객체의 변경
Person.prototype = { gender: 'male' };

var bar = new Person('Kim');

console.log(foo.gender); // undefined
console.log(bar.gender); // 'male'

console.log(foo.constructor); // ① Person(name)
console.log(bar.constructor); // ② Object()

  • 객체의 프로토타입은 다른 임의의 객체로 변경할 수 있다.

이 때 프로토타입 변경 이전에 생성된 객체는, 기존의 프로토타입 객체를 그대로 쓰고 있다. 위 코드를 참고하자.

프로토타입 객체를 변경하고나면 Person.prototype이 가리키는 객체가 변경된 것이므로 Person.prototype의 constructor 프로퍼티가 삭제되고, 새로 바꾼 객체로 바뀐다. 즉 bar.constructor의 값은 체이닝에 의해 Object.prototype.constructor 즉 Object()가 된다.

프로토타입 객체 변경에 따른 프로토타입 체인 변화 그림


어떤 객체의 프로퍼티를 조회하는데 없다면 프로토타입 체인이 작동하여 거슬러올라가 찾는다.

그러나 객체의 프로퍼티에 값을 할당할 때는 체인이 작동하지 않는다.

이미 존재한다면 값을 변경하는거고 없다면 그 프로퍼티를 동적으로 추가해준다.

프로토타입 체인의 발생 조건 그림


15강 스코프


var x = 10;

function foo(){
  var x = 100;
  console.log(x);

  function bar(){   // 내부함수
    x = 1000;
    console.log(x); // ?
  }

  bar();
}
foo();
console.log(x); // ?

전역 변수 x, 함수 foo 안의 지역 변수 x, foo 안의 함수 bar 안에서 x에 값을 할당하면, 중첩된 스코프에서 가장 인접한 스코프의 변수에 할당하므로 foo 안의 지역 변수 값이 바뀐다.


함수의 상위 스코프를 결정하는 방식에는 두 가지가 있다.

1) 동적 스코프 : 함수가 호출된 곳을 상위 스코프로 결정

2) 렉시컬 스코프 : 함수가 선언된 곳을 상위 스코프로 결정

JS를 포함한 대부분의 프로그래밍 언어는 렉시컬 스코프를 따른다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // ?
bar(); // ?

위 예제에서 렉시컬 스코프를 따른다면 foo()안의 bar()는 1, 전역에서의 bar()도 1을 리턴한다.

만약 동적 스코프를 따른다면 foo안의 bar는 10, 전역에서의 bar는 1을 리턴한다.


전역 변수는 선언될 때 전역 객체 window의 프로퍼티에도 추가가 된다.


var x = 10; // 전역 변수

function foo () {
  // 선언하지 않은 식별자
  y = 20;
  console.log(x + y);
}

foo(); // 30

y는 선언하지 않은 식별자인데, 여기서는 참조 에러가 발생하지 않고, y를 전역 객체 window의 프로퍼티로 추가하여 이 것을 후에 참조하여 값을 쓰는 것이다. 이를 암묵적 전역이라고 한다.

다만 변수를 선언한 것이 아닌 window의 프로퍼티일 뿐이므로 변수의 호이스팅은 일어나지 않는다는 특징이 있다.

그리고 y는 window의 프로퍼티일 뿐이므로 delete 연산자로 삭제할 수 있다. 물론 전역 변수 x는 삭제되지 않는다.


var MYAPP = {};

MYAPP.student = {
  name: 'Lee',
  gender: 'male'
};

console.log(MYAPP.student.name);

전역변수 남용을 최소화하기 위한 테크닉으로, 전역 객체를 임의로 하나 만들어 여기에 저장해서 쓰는 방법이 있다.


(function () {
  var MYAPP = {};

  MYAPP.student = {
    name: 'Lee',
    gender: 'male'
  };

  console.log(MYAPP.student.name);
}());

console.log(MYAPP.student.name);

전역변수 남용을 최소화하기 위한 또 하나의 테크닉으로 즉시실행함수를 활용하는 방법이 있다.

라이브러리에서 자주 활용된다.


16강 strict mode


function foo() {
  x = 10;
}

console.log(x); // ?

위와 같은 암묵적 전역 변수 등을 통과시키지 않고 에러를 발생시켜 디버깅하기 쉽게 하기위해 strict mode 또는 ESLint를 쓸 수 있다. ESLint를 권장한다.

strict mode는 script, 전역, 함수 단위로 쓰면 여러 에로사항이 있으므로, 웬만하면 즉시 실행 함수 단위로 사용하자.

strict mode는 암묵적 전역 변수, 변수/함수/매개변수의 삭제, 매개변수명 중복, with문의 사용 시에 에러를 발생시키고, 일반 함수를 호출할 때 this에는 undefined가 바인딩된다. 물론 생성자 함수로 쓰일 때는 함수 내 코드 상 this에는 함수 객체가 바인딩된다.


17강 this


this는 함수의 호출된 위치에 따라 동적으로 바인딩된다.

일반적으로 내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언되었든 관계없이 this를 전역객체에 바인딩한다.

이 것은 설계상 결함이다.

만약 내부함수 안에서 this를 본인을 호출한 상위 함수로 바인딩하게 하려면,

  1. 상위 함수 내에서 this를 변수에 저장해놓고, 그걸 내부 함수에서 this처럼 활용하면 된다.

  2. apply, call, bind 메소드를 활용한다.


프로토 타입의 메소드와 프로퍼티

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

Person.prototype.getName = function() {
  return this.name;
}

var me = new Person('Lee');
console.log(me.getName());

Person.prototype.name = 'Kim';
console.log(Person.prototype.getName());

프로토타입도 메소드를 가질 수 있고, 호출 시 this에는 프로토타입 객체가 바인딩된다.

요약 그림


생성자 함수 동작 방식


new 연산자와 함께 생성자 함수 호출 > 빈 객체 생성(이 후 생성자 함수 내에서 사용되는 this는 이 빈 객체를 가리킴) > 이 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 삼음

this를 통해 설정한 프로퍼티와 메소드는 새로 생성된 빈 객체에 추가됨

생성자 함수 내에 리턴문이 없는 경우, this에 바인딩 된 새로 생성된 객체가 반환됨. 명시적으로 this를 반환하는 것과 결과는 같다.

만약 명시적으로 다른 객체를 리턴하는 경우, 이 함수는 더 이상 생성자 함수로서 기능하지 못한다.

따라서 생성자 함수는 리턴문을 명시적으로 사용하지 않는다.


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


프로로타입 객체에 차이가 있다.

객체 리터럴 방식의 프로토타입 객체는 Object.prototype 이고,

생성자 함수에 의해 생성된 객체의 프로토타입 객체는 생성자 함수의 프로토타입 객체와 같다고 했으므로 (생성자함수명).prototype 임


생성자 함수를 new 연산자 없이 쓰는걸 방지하기 위한 패턴


// Scope-Safe Constructor Pattern
function A(arg) {
  // 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈객체를 생성하고 this에 바인딩한다.

  /*
  this가 호출된 함수(arguments.callee, 본 예제의 경우 A)의 인스턴스가 아니면 new 연산자를 사용하지 않은 것이므로 이 경우 new와 함께 생성자 함수를 호출하여 인스턴스를 반환한다.
  arguments.callee는 호출된 함수의 이름을 나타낸다. 이 예제의 경우 A로 표기하여도 문제없이 동작하지만 특정함수의 이름과 의존성을 없애기 위해서 arguments.callee를 사용하는 것이 좋다.
  */
  if (!(this instanceof arguments.callee)) {
    return new arguments.callee(arg);
  }

  // 프로퍼티 생성과 값의 할당
  this.value = arg ? arg : 0;
}
  • object instanceof constructor : object의 프로토타입 체인에 생성자의 프로토타입이 존재하는지 판별함

  • arguments.callee : 현재 함수의 함수명


apply/bind/call 메소드


apply, bind, all 함수는 함수의 프로토타입의 메소드, 즉 Function.prototype의 메소드이므로 모든 함수는 해당 메소드를 사용할 수 있다.

이들의 본질적인 기능은 해당 메소드를 호출한 함수를 호출하는 것과, apply를 호출한 생성자 함수 A에 대해 A의 this에 특정 객체를 바인딩하는 것이다.

apply() 메소드의 대표적인 용례는, 배열이 아닌 유사 배열 객체 arguments 를 대상으로 slice 메소드를 사용하는 경우이다.

Array.prototype.slice.apply(arguments)

이 구문은 Array.prototype.slice를 호출하되, this에 arguments 객체를 바인딩하라는 구문이다.

call() 메소드의 경우 apply()와 기능은 같으나 두 번째 매개변수가 apply와 달리 배열이 아니고, 각각의 원소를 다 보낸다.

ex)
Person.apply(foo, [1, 2, 3]);
Person.call(foo, 1, 2, 3);

또한 apply와 call은 콜백 함수의 this를 콜백 함수를 호출한 함수의 this와 똑같이 맞추기 위해 사용되기도 한다.

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

Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    // --------- 1
    callback();
  }
};

function foo() {
  console.log(this.name); // --------- 2
}

var p = new Person('Lee');
p.doSomething(foo);  // undefined

위 코드에서 1에서의 this는 Person 객체이고, 2에서의 this는 전역 객체이다.

따라서 callback()을 callback().call(this) 로 바꿔주면

콜백 함수 내부의 this로 Person 객체에 바인딩된다.


Function.prototype.bind를 활용할 수도 있다.

callback().bind(this)();

다만 bind 메소드는 콜백 함수 callback()이면서 this에 Person이 바인딩 된, 새로운 객체를 리턴하므로 명시적 함수 호출을 실행해주면 된다.


18강 : 실행 컨텍스트와 JS 동작 원리


사진과 같이, 먼저 전역 코드로 컨트롤이 진입할 때 전역 실행 컨텍스트가 실행 컨텍스트 스택에 쌓인다. 만약 다른 함수 등이 호출되면 해당 함수의 컨텍스트가 따로 만들어져서 스택에 쌓이게 된다. 함수 실행이 끝나면 그 함수의 컨텍스트를 파기하고 직전의 컨텍스트에 컨트롤을 반환한다.


Variable Object

실행 컨텍스트는 추상적인 개념이지만 물리적으로 객체의 형태를 가지고 있으며 3가지 프로퍼티를 소유한다.

  1. Variable Object

  2. Scope chain

  3. thisValue



자바스크립트 엔진은 실행에 필요한 여러 정보를 담을 객체를 생성한다.(VO)

전역 실행 컨텍스트에서는 최상위에 유일하게 위치하는 전역 객체(GO)를 가리킨다.

GO는 전역 프로퍼티와 전역 함수를 프로퍼티로 소유하고 있다.

함수 실행 컨텍스트에서는 활성 객체(AO)를 가리킨다.

AO는 함수의 arguments 객체와 내부 함수, 지역 변수를 가리키고 있다.


Scope Chain

스코프 체인은 리스트로서, 중첩된 함수의 AO를 가지고 마지막에는 GO를 가리킨다.

현재 실행 컨텍스트의 AO를 선두로 하여 순차적으로 상위 컨텍스트의 활성 객체를 가리키며 마지막에는 전역 객체(GO)를 가리킨다.

스코프 체인은 식별자 중에서 객체의 프로퍼티가 아닌 식별자, 즉 변수를 검색하는 메커니즘이다. 이와 대조적으로 프로토타입 체인은 변수가 아닌 객체의 프로퍼티(메소드도 포함)를 검색하는 메커니즘이다.

엔진은 스코프 체인을 통해 렉시컬 스코프(선언 위치 기준 스코프)를 파악한다.

예를 들어 함수 내의 코드에서 변수를 참조하면, 현재 실행 컨텍스트의 AO에 접근하여 변수를 검색하고, 없으면 다음 리스트 원소가 가리키는 AO를 순차적으로 검색하고, 끝까지 갔는데 없으면 Reference Error를 발생시킨다. 스코프 체인은 프로퍼티 [[Scope]]로 참조할 수 있다.


this value

this 프로퍼티에는 this 값이 할당된다. 물론 this에 바인딩되는 값은 함수의 호출 패턴에 의해 결정된다.


실행 컨텍스트 생성 과정

  1. 전역 코드로의 진입 : 컨트롤이 실행 컨텍스트에 진입하기 이전에 유일한 전역 객체 GO가 생성되고, 이 객체의 프로퍼티는 전역이므로 어디서든 접근 가능하다. 초기 상태에는 빌트인 객체, BOM, DOM이 설정되어 있다.

    GO 생성 이후 전역 실행 컨텍스트가 생성되어 스택에 쌓인다.

    이후 이 전역 실행 컨텍스트를 바탕으로 3가지가 실행된다.

    1) 스코프 체인의 생성 및 초기화
    2) Variable Instantiation(변수 객체화) 실행
    3) this value 결정


  1. 스코프 체인을 생성한다. 이 때의 SC는 전역 객체의 참조값을 포함하는 리스트이다.

  1. 변수 객체화를 실행한다. 전역 객체(VO)에 프로퍼티와 값을 추가한다. 다음 순서로 실행된다.

    1) (함수 코드인 경우) 매개변수가 VO의 프로퍼티로, 인수가 값으로 설정된다.

    2) 함수 선언(표현식 제외)을 대상으로 함수명이 VO의 프로퍼티로, 생성된 함수 객체가 값으로 설정된다. (함수 호이스팅)

    3) 변수 선언을 대상으로 변수명이 VO의 프로퍼티로, undefined가 값으로 설정된다. (변수 호이스팅)


  1. 생성된 함수 객체는 [[Scopes]] 프로퍼티를 가진다. 이는 함수 객체가 실행되는 환경, 즉 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 가진다.

    자신의 실행 환경, 자신을 포함하는 외부 함수의 실행 환경, 전역 객체를 가리키는데, 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸되어도, [[Scopes]]가 가리키는 외부 함수의 실행 환경은 소멸하지 않고 참조할 수 있다. 이 것이 클로저이다.

    함수 선언식의 경우 선언문 이전에 함수를 호출할 수 있다고 했는데, 이 것의 원리가 바로 이 것이다.

    지금까지 살펴본 실행 컨텍스트는 코드가 실행되기 이전인데, 이 때 이미 스코프 체인이 가리키는 VO에 이미 함수가 등록되어 있으므로 참조가능하다. 이 것이 함수 호이스팅의 원리이다.

    다만 함수표현식은 일반 변수의 방식을 따르므로 호이스팅이 안된다.


  1. var 키워드로 선언된 변수는 코드 실행 이전에 VO에 등록되어 있고 값은 undefined로 초기화되어있다. 그래서 코드 실행 이전에 변수를 참조할 수 있는데 이 것이 변수 호이스팅이다.

  1. this value가 결정되기 이전에는 this에 전역 객체(GO)가 바인딩되어있고, 이후 함수 호출 패턴에 의해 바인딩되는 값이 변경될 수 있다. 단, 전역 컨텍스트(전역 코드)의 경우에는 VO, SC, this 값은 항상 전역 객체이다.

코드 실행

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}

foo();
  • x는 코드 실행 이전에 GO에 undefined로 초기화되어있다.

    할당문을 실행하면, GO의 선두(SC가 가리키는 리스트)부터 검색하여 발견하면 값을 할당해준다.

  • 함수 foo 선언문을 실행하면 새로 함수 실행 컨텍스트가 생성되어 스택에 쌓인다.

    이 후 컨트롤이 해당 컨텍스트로 이동하여 전역 코드와 마찬가지로 SC 생성과 초기화, 변수 객체화, this value 결정을 순차적으로 실행한다.

    이 때 전역 코드에서와 다른 점은, 위 과정을 실행할 때 함수 코드의 룰이 적용된다는 것이다.

    먼저 SC의 선두에 AO의 참조값을 저장한다. 이 때 AO는 우선 arguments 프로퍼티 초기화를 실행한다.

    이 후, caller(전역 컨텍스트)의 SC가 참조하고 있는 객체([[Scope]], GO)가 함수 컨텍스트의 SC에 push된다. 이에 의해 함수 foo를 실행한 직후 SC는 AO와 GO를 순차적으로 참조하게 된다. (AO가 선두임)

    이제 변수 객체화를 실행한다. AO를 VO로서 변수 객체화를 실행한다. 나머지는 전역 코드와 같은 처리가 실행된다.

    함수 객체를 AO(VO로서)에 바인딩한다. 프로퍼티는 bar, 값은 함수 객체. 함수 객체의 [[Scope]] 프로퍼티의 값은 AO와 GO의 참조값이 들어있는 SC 리스트이다.

  • 이제 this value를 결정한다. 내부 함수의 경우, this value는 전역 객체이므로 foo()의 this에는 전역 객체(GO)가 바인딩된다.

  • 이제 foo를 호출하여 내부 코드가 실행된다. 현재 실행 컨텍스트의 SC가 참조하고 있는 VO를 선두(SC 리스트)부터 검색하여 발견하면 할당해준다.

    이 후 bar가 호출되는데, 이 때 새로운 실행 컨텍스트가 생성되어 스택에 쌓인다.

    1) SC를 생성하고 그 리스트에는 caller의 SC 리스트에 들어있는 객체 참조값이 모두 push된다.

    2) 변수 객체화를 실행한다. 지역 변수 z를 AO의 프로퍼티와 값으로 설정해준다.

    3) this에 전역 객체(GO)를 바인딩한다. (내부 함수이므로)

  • 이제 console.log(x + y + z)를 실행한다. x와 y를 SC의 리스트 선두 값부터 순차적으로 검색한다. bar 실행 컨텍스트의 AO부터 foo의 AO, 전역 코드의 GO까지 쭉 검색한다. 결과적으로 출력은 xxxyyyzzz가 된다.


19강 : 클로저

클로저의 정의는 함수와 그 함수가 선언됐을 때의 렉시컬 환경과의 조합이다

코드로 알아보자.

function outerFunc() {
  var x = 10;
  var innerFunc = function () { console.log(x); };
  return innerFunc;
}

var inner = outerFunc();
inner(); // 10

함수 outerFunc를 호출하면 inner에 innerFunc이 저장되고 outerFunc의 실행 컨텍스트는 소멸한다. 그런데 inner()를 실행하면 x = 10이 정상 출력된다. x는 outerFunc의 변수인데도 소멸 후 참조가 가능하다. 이 때 innerFunc 같은 것을 클로저라고 한다.

outerFunc의 실행 컨텍스트가 소멸되더라도 외부 함수 내의 변수를 필요로 하는 내부 함수가 하나 이상 존재하는 경우, outerFunc의 AO는 계속 남아있게 되어 내부 함수가 변수를 참조할 수 있게 된다. 이 때 외부 함수의 변수를 자유 변수라고 칭한다.

클로저를 한 마디로 표현하자면 자신이 생성될 때의 환경을 기억하는 함수 이다.



클로저의 활용

자신이 생성될 때의 렉시컬 환경을 기억하므로 메모리 차원에서 더 많은 자원을 필요로 하지만 그만큼 강력한 기능이므로 적극 사용하자. 클로저가 유용하게 사용되는 상황을 알아보자.

  1. 상태 유지

    <!DOCTYPE html>
    <html>
    <body>
    <button class="toggle">toggle</button>
    <div class="box" style="width: 100px; height: 100px; background: red;"></div>
    
    <script>
      var box = document.querySelector('.box');
      var toggleBtn = document.querySelector('.toggle');
    
      var toggle = (function () {
        var isShow = false;
    
        // ① 클로저를 반환
        return function () {
          box.style.display = isShow ? 'block' : 'none';
          // ③ 상태 변경
          isShow = !isShow;
        };
      })();
    
      // ② 이벤트 프로퍼티에 클로저를 할당
      toggleBtn.onclick = toggle;
    </script>
    </body>
    </html>

    여기서 toggleBtn의 onclick 이벤트 프로퍼티에는 이벤트 핸들러로서 toggle 즉시 실행 함수의 반환 값인 내부 함수가 저장된다.

    이 내부 함수는 실행 환경이었던 toggle의 환경을 기억하고 있는 클로저이다. 따라서 isShow를 기억하고 있고 이를 활용할 수 있다.

    전역 변수를 사용하지 않고도 isShow라는 변수를 변경 및 최신 상태 유지를 할 수 있다. 다른 곳에서 참조하여 여러 문제를 발생시킬 수 있는 전역 변수를 사용하지 않는다는 점에서 이미 충분히 유용하다고 할 수 있다.


  1. 전역 변수 사용 억제
  <!DOCTYPE html>
<html>
<body>
  <p>전역 변수를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    // 카운트 상태를 유지하기 위한 전역 변수
    var counter = 0;

    function increase() {
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

위 코드에서는 전역 변수로 카운팅을 하고 있다.

이 경우 다른 곳에서 counter의 값을 조작 및 변경할 경우 문제가 생긴다.

그걸 막기 위해 지역 변수로 한번 리팩토링해보자.

  <!DOCTYPE html>
<html>
<body>
  <p>지역 변수를 사용한 Counting</p>
  <button id="inclease">+</button>
  <p id="count">0</p>
  <script>
    var incleaseBtn = document.getElementById('inclease');
    var count = document.getElementById('count');

    function increase() {
      // 카운트 상태를 유지하기 위한 지역 변수
      var counter = 0;
      return ++counter;
    }

    incleaseBtn.onclick = function () {
      count.innerHTML = increase();
    };
  </script>
</body>
</html>

이제 외부에서의 counter 조작은 막을 수 있지만 이벤트 핸들러를 실행할 때마다 함수 내에서 counter가 0으로 초기화되어 값의 기록이 불가능하다.

그럼 이제 클로저를 활용해보자.

<!DOCTYPE html>
<html>
<body>
<p>클로저를 사용한 Counting</p>
<button id="inclease">+</button>
<p id="count">0</p>
<script>
  var incleaseBtn = document.getElementById('inclease');
  var count = document.getElementById('count');

  var increase = (function () {
    // 카운트 상태를 유지하기 위한 자유 변수
    var counter = 0;
    // 클로저를 반환
    return function () {
      return ++counter;
    };
  }());

  incleaseBtn.onclick = function () {
    count.innerHTML = increase();
  };
</script>
</body>
</html>

이제 자유 변수 counter를 private하게 만들었고, 값도 이벤트 핸들러 함수(클로저)가 계속 참고하고 있는 한 소멸하지 않고 기록된다.

이렇듯 부수 효과(side effect)를 억제하면서 값을 계속 기억할 수 있기 때문에 클로저는 많이 사용되고 있다.

아래는 클로저 간단 활용 예제이다. (고차 함수 개념 적용)

// 함수를 인자로 전달받고 함수를 반환하는 고차 함수
// 이 함수가 반환하는 함수는 클로저로서 카운트 상태를 유지하기 위한 자유 변수 counter을 기억한다.
function makeCounter(predicate) {
// 카운트 상태를 유지하기 위한 자유 변수
var counter = 0;
// 클로저를 반환
return function () {
  counter = predicate(counter);
  return counter;
};
}

// 보조 함수
function increase(n) {
return ++n;
}

// 보조 함수
function decrease(n) {
return --n;
}

// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인자로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

  1. 정보 은닉

    function Counter() {
      // 카운트를 유지하기 위한 자유 변수
      var counter = 0;
    
      // 클로저
      this.increase = function () {
        return ++counter;
      };
    
      // 클로저
      this.decrease = function () {
        return --counter;
      };
    }
    
    const counter = new Counter();
    
    console.log(counter.increase()); // 1
    console.log(counter.decrease()); // 0

    위 코드에서 Counter 생성자 함수로 객체를 생성하고 나면, Counter의 실행 컨텍스트는 소멸되고 내부의 메소드인 increase, decrease는 클로저이다.

    이 때 counter는 Counter의 프로퍼티가 아닌 변수이다. 두 메소드(클로저)는 이 자유 변수에 접근할 수 있다.

    counter는 생성된 객체의 프로퍼티가 아니라 변수 그 자체이므로, 외부에서 접근할 수 없고 내부 메소드(클로저)로는 접근할 수 있다. 이처럼 다른 클래스 기반 언어의 private를 흉내낼 수 있다.


  1. 자주 발생하는 실수

    var arr = [];
    
    for (var i = 0; i < 5; i++) {
      arr[i] = function () {
        return i;
      };
    }
    
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }

    위 코드는 0~4를 순차적으로 반환할 것 같지만 아니다. i가 전역 변수이기 때문이다.

    이를 의도한대로 작동하도록 클로저를 활용해보자.

    var arr = [];
    
    for (var i = 0; i < 5; i++){
      arr[i] = (function (id) { // ②
        return function () {
          return id; // ③
        };
      }(i)); // ①
    }
    
    for (var j = 0; j < arr.length; j++) {
      console.log(arr[j]());
    }

    또는 let을 쓸 수도 있다.

    const arr = [];
    
    for (let i = 0; i < 5; i++) {
      arr[i] = function () {
        return i;
      };
    }
    
    for (let i = 0; i < arr.length; i++) {
      console.log(arr[i]());
    }

    아니면 함수형 프로그래밍의 기법인 고차 함수를 사용하는 방법도 있다. 변수와 반복문의 사용을 억제하기에 앱의 오류를 줄이고 가독성을 좋게 만든다.

    const arr = new Array(5).fill();
    
    arr.forEach((v, i, array) => array[i] = () => i);
    
    arr.forEach(f => console.log(f()));
profile
PS, 풀스택, 앱 개발, 각종 프로젝트 내용 정리 (https://github.com/minsu-cnu)

0개의 댓글