자바스크립트 Deep Dive - week 6

하머·2022년 12월 14일
0

문제

  1. 다음 코드의 실행 결과를 쓰시오.
var a = 10;
var obj = {
  a: 20,
  b: this.a + 10,
  c: function () {
    return this.a + 20;
  },
};

console.log(obj.b); // ?
console.log(obj.c()); // ?

var btn1 = document.querySelector("#btn1");

btn1.addEventListener("click", function () {
  console.log(this); // ?
});

var btn2 = document.querySelector("#btn2");

btn2.addEventListener("click", () => {
  console.log(this); // ?
});
  1. curry 함수에서 12를 만들어 낼 수 있는 방법을 쓰시오
const curry = (fn) => (fn2) => (a) => (b) => fn(a, b) + fn2(a, b);

const add = (a, b) => a + b;
const mul = (a, b) => a * b;

const addMul = curry(add)(mul);
console.log(addMul(3)(2)); // 11

this 키워드

  • this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수 다.
  • this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
  • this가 가리키는 값, 즉 this 바인딩은 함수 호출 방식에 의해 동적으로 결정된다.
    • 바인딩: 식별자와 값을 연결하는 과정, 여기선 this 식별자와 this가 가리킬 객체를 뜻한다.
    • this가 바인딩되는 렉시컬 스코프는 함수 정의가 평가되어 함수 객체가 생성되는 시점에 생기지만, this 바인딩은 함수 호출 시점에 결정된다.
var foo = function () {
  console.dir(this);
};

// 1. 함수 호출
foo(); // window
// window.foo();

// 2. 메소드 호출
var obj = { foo: foo };
obj.foo(); // obj

// 3. 생성자 함수 호출
var instance = new foo(); // instance

// 4. apply/call/bind 호출
var bar = { name: "bar" };
foo.call(bar); // bar
foo.apply(bar); // bar
foo.bind(bar)(); // bar

1. 함수 호출

  • 기본적으로 this에는 전역 객체(global object, window(browser) or global(node.js))가 바인딩 된다.

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

    • 객체를 생성하지않는 일반함수에서 this는 의미가 없으므로 strict mode가 적용된 this에서는 일반함수 내부에서는 undefined가 바인딩된다.
  • 자바스크립트 개발자 중 하나인 더글라스 크락포드는 “이것은 설계 단계의 결함으로 메소드가 내부함수를 사용하여 자신의 작업을 돕게 할 수 없다는 것을 의미한다”라고 말했다.

  • 다만 내부함수의 this가 전역객체를 참조하는 것의 회피방법도 존재한다.

    • 새로운 변수에 현재 실행 문맥의 this를 담아 실행

      var value = 1;
      
      var obj = {
        value: 100,
        foo: function () {
          var that = this; // Workaround : this === obj
      
          console.log("foo's this: ", this); // obj
          console.log("foo's this.value: ", this.value); // 100
          function bar() {
            console.log("bar's this: ", this); // window
            console.log("bar's this.value: ", this.value); // 1
      
            console.log("bar's that: ", that); // obj
            console.log("bar's that.value: ", that.value); // 100
          }
          bar();
        },
      };
      
      obj.foo();
    • apply, call, bind 메소드

      var value = 1;
      
      var obj = {
        value: 100,
        foo: function () {
          console.log("foo's this: ", this); // obj
          console.log("foo's this.value: ", this.value); // 100
          function bar(a, b) {
            console.log("bar's this: ", this); // obj
            console.log("bar's this.value: ", this.value); // 100
            console.log("bar's arguments: ", arguments);
          }
          bar.apply(obj, [1, 2]);
          bar.call(obj, 1, 2);
          bar.bind(obj)(1, 2);
        },
      };
      
      obj.foo();
    • 화살표 함수

      var value = 1;
      
      const obj = {
        value: 100,
        foo() {
          setTimeout(() => console.log(this.value), 100); // 100
        },
      };
      
      obj.foo();

2. 메소드 호출

  • 메소드 내부의 this에는 메소드를 호출한 객체, 즉 메소드를 호출할 때 메소드 이름 앞의 마침표 연산자 앞에 기술한 객체가 바인딩된다.

    • 메소드 내부의 this는 메소드를 소유한 객체가 아닌 호출한 객체에 바인딩된다.
    var obj1 = {
      name: "Lee",
      sayName: function () {
        console.log(this.name);
      },
    };
    
    var obj2 = {
      name: "Kim",
    };
    
    obj2.sayName = obj1.sayName;
    
    obj1.sayName(); // Lee
    obj2.sayName(); // Kim
    • 프로토타입 객체도 메소드를 가질 수 있는데, 이 때 프로토타입 객체 메소드 내부에서의 this 또한 해당 메소드를 호출한 객체에 바인딩 된다.
    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());

3. 생성자 함수 호출

  • 생성자 함수 내부의 this에는 생성자 함수가 생성할 인스턴스가 바인딩된다.

    • 생성자 함수 호출 시 동작
      1. 빈 객체 생성 및 this 바인딩 : 생성자 함수의 코드가 실행되기 전 빈 객체가 생성되고, 이 빈 객체가 생성자 함수가 새로 생성하는 객체이며 this가 된다. 그리고 이 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.
      2. this를 통한 프로퍼티 생성 : 생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다. this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티 및 메소드는 새로 생성한 객체에 추가된다.
      3. 생성된 객체 반환
        1. 반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환된다. 명시적으로 this를 반환해도 결과는 같다.
        2. 반환문이 this가 아닌 다른 객체를 반환하는경우, this가 아닌 해당 객체가 반환되며 이때 this를 반환하지 않은 함수는 생성자 함수로서의 역할을 수행하지 못한다.
  • 하지만 생성자 함수 역시 일반 함수로 호출 될 수 있으므로(new 키워드 없이 호출 되었을 경우) 이 경우에도 전역 객체가 this 바인딩 된다.

    function Person(name) {
      // new없이 호출하는 경우, 전역객체에 name 프로퍼티를 추가
      this.name = name;
    }
    
    // 일반 함수로서 호출되었기 때문에 객체를 암묵적으로 생성하여 반환하지 않는다.
    // 일반 함수의 this는 전역객체를 가리킨다.
    var me = Person("Lee");
    
    console.log(me); // undefined
    console.log(window.name); // Lee
    • 이러한 위험을 피하기 위해 Scope-Safe Constructor 패턴을 사용한다고 한다.
    // 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;
    }
    
    var a = new A(100);
    var b = A(10);
    
    console.log(a.value);
    console.log(b.value);

4. apply/call/bind 호출

  • apply() 메소드

    Function.prototype.apply(thisArg, [argsArray]);
    
    // thisArg: 함수 내부의 this에 바인딩할 객체
    // argsArray: 함수에 전달할 argument의 배열
    
    // -----
    var Person = function (name) {
      this.name = name;
    };
    
    var foo = {};
    
    /* apply 메소드는 생성자함수 Person을 호출한다. 이때 this에 객체 foo를 바인딩한다.
    Person 함수는 this의 name 프로퍼티에 매개변수 name에 할당된 인수(argsArray)를 할당하는데
    this에 바인딩된 foo 객체에는 name프로퍼티가 없으므로 동적 추가되고 값이 할당된다.
    */
    Person.apply(foo, ["name"]);
    
    console.log(foo); // { name: 'name' }
    
    // apply 메소드의 대표적인 용도
    function convertArgsToArray() {
      console.log(arguments);
      // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    
      // arguments 객체를 배열로 변환
      // slice: 배열의 특정 부분에 대한 복사본을 생성한다.
      var arr = Array.prototype.slice.apply(arguments); // arguments.slice
      // var arr = [].slice.apply(arguments);
    
      console.log(arr);
      // [1, 2, 3]
      return arr;
    }
    
    convertArgsToArray(1, 2, 3);
  • call() 메소드 : apply와 기능은 같지만 apply의 두번째 인자인 argsArray가 아닌 각각의 인자로 바뀌었다.

    Person.apply(foo, [1, 2, 3]);
    
    Person.call(foo, 1, 2, 3);
    
    // call메소드의 사용법
    
    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.doSomething = function (callback) {
      if (typeof callback == "function") {
        // callback()
        callback.call(this);
      }
    };
    
    function foo() {
      console.log(this.name);
    }
    
    var p = new Person("Lee");
    p.doSomething(foo); // 'Lee', prototype에서 그대로 callback을 호출한다면 undefined
  • bind() 메소드: bind함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴한다. 함수를 실행하지않고 반환하기 때문에 명시적으로 호출할 필요가 있다.

    function Person(name) {
      this.name = name;
    }
    
    Person.prototype.doSomething = function (callback) {
      if (typeof callback == "function") {
        // callback.call(this);
        // this가 바인딩된 새로운 함수를 호출
        callback.bind(this)();
      }
    };
    
    function foo() {
      console.log("#", this.name);
    }
    
    var p = new Person("Lee");
    p.doSomething(foo); // 'Lee'

이벤트 핸들러 내부의 this

<button id="btn">Click</button>

<script>
  var btn = document.getElementById("btn");

  btn.onclick = function () {
    console.log(this); // <button id="btn">Click</button>
  };
</script>

실행 컨텍스트

실행 컨텍스트란?

  • 실행 컨텍스트는 자바스크립트 코드가 실행되는 환경이다.
  • 실행 컨텍스트는 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념이다.
  • 실행 가능한 코드란 전역 코드와 함수 코드를 말한다.
    • 이 외에도 eval 코드, 모듈 코드가 있다.
    • 전역 코드는 전역에 존재하는 코드를 말하고, 함수 코드는 함수 내에 존재하는 코드를 말한다.
  • 실행 컨텍스트는 실행 가능한 코드가 실행되기 위해 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다.
  • 실행 가능한 코드가 실행되기 위해 필요한 환경이란 식별자와 식별자에 바인딩된 값, 스코프, 변수 선언 등을 말한다.
  • 실행 컨텍스트는 런타임 환경이 제공하는 여러 기능을 사용할 수 있도록 객체의 형태로 제공한다.
  • 런타임 환경이란 실행 컨텍스트가 실행되는 환경을 말한다.
  • 렉시컬 환경, 변수 환경, this 바인딩 등이 있다.
  • 실행 컨텍스트는 코드가 실행되는 환경이므로 코드가 실행되는 시점에 생성된다.
    • 전역 코드는 전역 실행 컨텍스트를 생성하고, 함수 코드는 함수 실행 컨텍스트를 생성한다.
    • 함수가 호출되면 함수 실행 컨텍스트가 생성되고, 함수의 실행이 종료되면 함수 실행 컨텍스트는 소멸한다.
    • 함수 실행 컨텍스트는 함수가 호출될 때마다 생성된다.
    • 함수 평가와 실행은 분리되어 있다.
      • 함수 평가는 함수 선언문이나 함수 표현식을 평가하여 함수 객체를 생성한다.
      • 함수 실행은 함수 객체를 호출하여 함수 코드를 실행한다.
      • 이 때 분리되어 있는 이유 때문에 호이스팅이 발생한다.
  • 실행 컨텍스트는 스택 형태로 관리된다.
    • 실행 컨텍스트 스택은 코드가 실행되는 순서대로 컨텍스트를 저장하는 스택이다.
    • 실행 컨텍스트 스택은 LIFO(Last In First Out) 구조를 갖는다.
    • 실행 컨텍스트 스택의 가장 위에 존재하는 컨텍스트는 현재 실행 중인 컨텍스트이다.
    • 이벤트 루프의

렉시컬 환경

  • 렉시컬 환경은 실행 컨텍스트의 런타임 환경이다.
    • 런타임 환경: 실행 컨텍스트가 실행되는 환경
  • 렉시컬 환경은 실행 컨텍스트가 생성될 때 같이 생성된다.
  • 렉시컬 환경은 식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참조를 기록하는 자료구조이다.
    • 식별자는 변수, 함수, 매개변수, 클래스 등의 이름을 말한다.
    • 식별자에 바인딩된 값은 식별자가 가리키는 값이다.
    • 상위 스코프에 대한 참조는 상위 스코프의 렉시컬 환경(Outer Lexical Environment)을 가리킨다.
      • 상위 스코프는 함수가 정의된 위치에 의해 결정된다. (정적 스코프, 13장 - 5절 참고)
    • var, let, const에 따라서 식별자와 바인딩된 값이 기록되는 위치가 다르다.
      • var: 렉시컬 환경의 Object Environment Record에 기록된다.
      • let, const: 렉시컬 환경의 Declarative Environment Record에 기록된다. => TDZ(Temporal Dead Zone)이 존재한다.
  • 함수 실행 컨텍스트가 사라지더라도 렉시컬 환경은 사라지지 않는다.
    • 함수 실행 컨텍스트가 사라지면 함수 실행 컨텍스트의 렉시컬 환경은 GC의 대상이 된다.
    • 함수 실행 컨텍스트가 사라지더라도 함수가 참조하는 상위 스코프의 렉시컬 환경은 사라지지 않는다. => 클로저
    • 함수가 참조하는 상위 스코프의 렉시컬 환경은 함수가 속한 실행 컨텍스트가 사라질 때까지 유지된다. => 누군가가 참조하고 있으면 GC의 대상이 되지 않는다.
    • 함수가 참조하는 상위 스코프의 렉시컬 환경은 함수가 속한 실행 컨텍스트가 사라지면 GC의 대상이 된다.

클로저

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. - MDN

클로저의 생성

  • 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
    • 함수: 클로저를 생성하는 함수
    • 함수가 선언된 어휘적 환경: 클로저를 생성하는 함수가 선언된 어휘적 환경
function outer() {
  let x = 1;
  function inner() {
    x++;
  }
  return inner;
}

const innerFunc = outer();
innerFunc(); // 1
innerFunc(); // 2
  • 함수 outer는 함수 inner를 반환한다.
  • 함수 inner는 함수 outer의 지역 변수 x를 참조한다.
  • 함수 inner는 함수 outer의 지역 변수 x를 참조하므로 함수 outer의 실행 컨텍스트가 사라지더라도 함수 inner의 실행 컨텍스트가 참조할 수 있다.

클로저의 특성

  • 클로저는 함수가 선언된 어휘적 환경을 기억하므로 함수가 호출되는 시점에 상위 스코프의 식별자를 참조할 수 있다.
  • 클로저는 외부 함수의 지역 변수를 참조하므로 외부 함수의 지역 변수는 GC의 대상이 되지 않는다.
    • GC에 의해 사라지지 않으므로 상태를 유지할 수 있다.
  • 또한 클로저를 생성한 외부 함수의 실행 컨텍스트가 사라지더라도 클로저는 외부 함수의 지역 변수를 참조할 수 있다.

  • 클로저는 외부 함수의 지역 변수를 참조하므로 외부 함수의 지역 변수를 변경할 수 있다.

[[Environment]] 내부 슬롯

  • 함수가 선언된 어휘적 환경은 함수 객체의 [[Environment]] 내부 슬롯에 저장된다.
  • 함수 객체의 [[Environment]] 내부 슬롯은 함수가 생성될 때(정적 스코프) 함수가 선언된 어휘적 환경을 참조하므로 함수가 생성된 시점의 상위 스코프를 기억한다.
  • [[Environment]] 내부 슬롯을 통해 클로저가 외부 함수의 지역 변수를 참조함으로써 외부 함수의 지역 변수는 GC의 대상이 되지 않는다.

커링 (Currying)

  • 커링은 함수를 인자의 개수에 따라 여러 개의 함수(클로저)로 나누어서 처리하는 기법이다.
function add(a, b) {
  return a + b;
}

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

add(1)(2); // 3
  • 함수의 인자를 미리 채워서 함수를 호출하는 방식으로 함수의 인자를 부분적으로 적용할 수 있다.
function multiply(x) {
  return function (y) {
    return function (z) {
      return x * y * z;
    };
  };
}

let multiply3 = multiply(3); // 3이 고정됨
let multiply3And5 = multiply3(5); // 3과 5가 고정됨
let multiply3And2 = multiply3(2); // 3과 2가 고정됨
console.log(multiply3And5(7));
console.log(multiply3And5(8));
console.log(multiply3And2(1));

0개의 댓글