This & Binding Example

soom·2021년 1월 4일
1
post-thumbnail

Execution Context

  • 코드를 만들어서 자바스크립트 엔진에 실행을 하면 그 즉시 memoryexecution context를 엔진이 객체로 생성해 만든다.

  • 실행을 하기 위해서는 여러가지 정보가 필요한데 다음과 같은 정보를 구분하고 가지고 있기 위해 execution context를 객체 형태로 관리한다.

  • 함수가 호출되면 scope에 따라 다음과 같은 정보가 execution context가 만들어 져 call stackpush 된다. 함수를 벗어나면 call stack 에서 pop된다.

  • scope 내 변수: 지역변수, 매개변수, 전역변수, 객체의 프로퍼티
  • 호출의 근원 (caller)
  • 전달의 인지 (arguments)
  • this

그러므로 변수가 담긴 함수를 선언하면 local memorylocal execution context가 생성된다. 자바스크립트는 lexical scope이기 때문에 어디서 선언 되었는지가 중요하다.

this란?

this는 일반적으로 메서드를 호출한 객체가 저장되어 있는 속성이다. 하지만 this 속성은 메서드를 호출할 때뿐 아니라 일반 함수를 호출할 때도 만들어지며 이벤트 리스너가 호출될 때에도 this 속성이 만들어진다. 문제는 this 속성에 저장되는 값이 동일한 값이 아니라 각각 다르다는 점이다.

this의 참조 법칙

this 키워드는 모든 함수 scope에서 자동으로 설정되는 특수한 식별자이다. execution context를 이루는 요소 중 하나로 사용할 수 있다.
this가 상황에 따라 무엇인지 식별하는데, this는 5가지 각기 다른 상황에 따라 무엇을 지칭하는지 알 수 있다.

  • Implicit Binding (암묵적 바인딩)
  • Explicit Binding (명시적 바인딩)
  • New Binding
  • window Binding
  • Lexical Binding

Implicit Binding

가장 일반적인 규칙이다. 거의 80% 정도가 this가 무엇을 참조하는지 이 규칙을 통해 알 수 있다.

const user = {
  name: "Tyler",
  age: 27,
  greet() {
    alert(`Hello, my name is ${this.name}`)
  },
  mother: {
    name: "Stacey",
    greet() {
      alert(`Hello, my name is ${this.name}`)
    }
  }
}

user.greet() // Hello, my name is Tyler
user.mother.greet() // Hello, my name is Stacey

look to the left of the dot when the function is invoked

Explicit Binding

아래와 같이 greet 함수가 user object에 속한 메서드가 아니라 단독 함수라면 this 가 user obejct를 참조하도록 하면서 greet함수를 호출하려면 어떻게 해야할까?

  • call 함수, Function.prototype.call()를 통해 가능하다.
    .call은 모든 함수에 있는 메소드로, 함수가 호출될 컨텍스트를 지정하여 함수를 호출할 수 있게 한다. 이것이 바로 명시적 바인딩이다. this 키워드가 무엇을 참조할 지 명시적으로 알려주는 방법이다.
    만약 파라미터가 필요한 함수라면 .call 함수의 첫번째 인자로 컨텍스트를 넣은 다음, 차례대로 인자를 넣어주면 된다.

  • 하지만 이렇게 일일이 인자를 넣어주는 게 귀찮다면, .apply 함수, Function.prototype.apply()를 사용해서 인자를 한번에 배열로 넣어주면 된다.

  • 마지막으로 .bind, Function.prototype.bind()를 살펴보면, .call 함수와 동일한데 즉시 함수를 호출하는 게 아니라 바인딩된 새로운 함수를 리턴하는 것이 다르다.

function greet(l1, l2, l3) {
  alert(`Hello, my name is ${this.name} and I know ${l1}, ${l2}, and ${l3}`)
}

const user = {
  name: "Tyler",
  age: 27
}

const languages = ["JavaScript", "Ruby", "Python"]
// .call 
greet.call(user, languages[0], languages[1], languages[2]) 
// .apply 
greet.apply(user, languages)
// .bind
const newFn = greet.bind(user, languages[0], languages[1], languages[2]) 

명시적인 바인딩은 묵시적 바인딩보다 우위를 갖는다.

new Binding

new 키워드로 함수를 호출하면 자바스크립트 인터프리터는 새로운 object를 리턴하는데 this는 그 새로운 object를 참조한다.

function User(name, age) {
  this.name = name
  this.age = age
}

const me = new User("Tyler", 27)

Hard Binding & Lexical Binding

하드 바인딩은 bind() (ES5)으로 가능하다. bind() 메소드는 우리가 지정한 this 컨텍스트를 가진 기존 함수를 불러오기 위해 하드코딩된 새로운 함수를 반환한다.

var myMethod = function () {
  console.log(this);
};

var obj1 = {
  a: 2
};

var obj2 = {
  a: 3
};

myMethod = myMethod.bind(obj1); // 2
myMethod.call( obj2 ); // 2 명시적 바인딩은 obj2이나, obj1로 하드바인딩 되어있음

하드바인딩은 명시적 바인딩보다 우위를 갖는다

const user = {
  name: "Tyler",
  age: 27,
  languages: ["JavaScript", "Ruby", "Python"],
  greet() {
    const hello = `Hello, my name is ${this.name} and I know`

    const langs = this.languages.reduce(function(str, lang, i) {
      if (i === this.languages.length - 1) {
        return `${str} and ${lang}.`
      }

      return `${str} ${lang},`
    }, "")

    alert(hello + langs)
  }
}

user.greet() // Uncaught TypeError: Cannot read property 'length' of undefined

this.languagesundefined 여서 Cannot read property ‘length’ of undefined 에러가 발생한다.
원래 바라는 바는 thisuser object를 참조하는 것인데, 어디서 잘못됐는지 차근차근 확인해보자.

어디서 함수가 호출되었나? → language 문자열을 합치는 함수는 reduce 함수에 전달되었다. reduce 함수내에서 어떻게 함수가 호출되는지 정확히 알 수 없다.

이 함수가 user 컨텍스트에서 호출될 수 있도록 명시적으로 알려줘야 한다. bind를 이용해서 user 를 참조하는 this를 바인딩해주면 된다.

greet() {
  const hello = `Hello, my name is ${this.name} and I know`

  const langs = this.languages.reduce(function (str, lang, i) {
    if (i === this.languages.length - 1) {
      return `${str} and ${lang}.`
    }

    return `${str} ${lang},`
  }.bind(this), "")

  alert(hello + langs)
}

하지만 좀 보기 복잡한 면이 있다.

ES6부터 도입된 arrow function은 일반 function과 다르게 함수 자체의 this를 가지고 있지 않다. 대신 lexical하게 this를 결정짓는다. (lexical scope : 바깥에서 선언한 변수는 안쪽에서 접근 가능하다)

arrow function을 이용하면 this를 명시적으로 바인딩해주지 않아도 부모 스코프의 this를 참조한다.

Arrow functions don’t have their own this.

const langs = this.languages.reduce((str, lang, i) => {
  if (i === this.languages.length - 1) {
    return `${str} and ${lang}.`
  }
  return `${str} ${lang},`
}, "")

window Binding

자바스크립트는 기본적으로 this 키워드는 window object를 참조하고 있다. (Node.js에서는 global object)

window.age = 27

function sayAge() {
  console.log(`My age is ${this.age}`)
}

sayAge() // My age is 27

참고로 ES5부터 “엄격 모드”를 활성화하면 JavaScript가 올바른 작업을 수행하며 창 객체를 기본값으로 사용하는 대신 “this”를 정의되지 않은 상태로 유지한다

"use strict"

window.age = 27

function sayAge() {
  console.log(`My age is ${this.age}`)
}

sayAge() // TypeError: Cannot read property 'age' of undefined

다시 한번 정리해보자면, this 키워드가 무엇을 참조하는지는 아래의 순서에 따라 판단하면 된다.

  1. 함수가 어디에서 호출됐는지 확인

  2. . 왼쪽에 object가 있나? 그 object가 this 키워드가 참조하는 거다. 만약 그렇지 않다면 #3 규칙으로

  3. 함수가 call, apply, bind를 이용해서 호출됐나? 그렇다면 call, apply, bind의 첫번째 인자가 바로 this 키워드가 참조하고 있는 것이다. 아니면 #4 규칙으로

  4. 함수가 new 키워드로 호출됐나? 그렇다면 this 키워드는 새롭게 생성된 object를 참조하고 있다. 아니면 #5로

  5. 화살표함수 안에 this가 있나? 그렇다면 부모 스코프에서 this 가 무엇을 참조하는지 찾을 수 있을거다. 아니면 #6으로

  6. strict mode인가? 그렇다면 this 는 undefined다. 아니면 #7로

  7. 위의 모든 케이스에 해당되지 않는다면, this는 window object를 참조하고 있다!

this가 만들어지는 경우 (암묵적 바인딩)

  • 일반 함수에서의 this (window 객체)
  • 일반 중첩 함수에서의 this (window 객체)
  • 이벤트 리스너에서의 this (이벤트를 발생시킨 객체)
  • 메서드에서의 this (메서드를 호출한 객체)
  • 메서드 내부의 중첩 함수에서 this (window 객체)

일반 함수에서의 this

일반 함수 내부에서 this는 전역 객체인 window가 저장된다.

var data = 10;

function thisFn() {
  this.data = 20;

  console.log("this.data = 20; 실행한 후 ===== ===== =====");
  console.log("1.data : " + data);
  console.log("2.this.data : " + this.data);
  console.log("3.window.data : " + window.data);

  data = 30;

  console.log("data = 30; 실행한 후 ===== ===== =====");
  console.log("1.data : " + data);
  console.log("2.this.data : " + this.data);
  console.log("3.window.data : " + window.data);
}

thisFn();

출력결과

this.data = 20; 실행한 후 ===== ===== =====
1.data : 20
2.this.data : 20
3.window.data : 20
data = 30; 실행한 후 ===== ===== =====
1.data : 30
2.this.data : 30
3.window.data : 30

미리 선언해준 전역 변수 data에 새값을 할당해도 this.data는 오염되지 않고 유지된다.

일반 중첩 함수에서 this

thisInnerFn() 함수는 thisOuterFn() 함수에 만들어져 있기 때문에 thisOuterFn() 내부에서만 사용할 수 있는 전형적인 중첩 함수다. 따라서 일반 중첩 함수에서 this 역시 window가 된다.

var data = 10;

function thisOuterFn() {
  function thisInnerFn() {
    this.data = 20;

    data = 30;

    console.log("1.data : " + data);
    console.log("2.this.data : " + this.data);
    console.log("3.window.data : " + window.data);
  }

  thisInnerFn();
}

thisOuterFn();

출력결과

1.data : 30
2.this.data : 30
3.window.data : 30

Event listener에서의 this

이벤트 리스너에서 this는 이벤트를 발생시킨 객체가 된다.

var data = 10;

$(document).ready(function () {
  $("#thisButton").click(function () {
    this.data = 20;

    console.log("this.data = 20; 실행한 후 ===== ===== =====");
    console.log("1.data : " + data);
    console.log("2.this.data : " + this.data);
    console.log("3.window.data : " + window.data);

    data = 30;

    console.log("data = 30; 실행한 후 ===== ===== =====");
    console.log("1.data : " + data);
    console.log("2.this.data : " + this.data);
    console.log("3.window.data : " + window.data);
  });
});

출력결과

this.data = 20; 실행한 후 ===== ===== =====
1.data : 10
2.this.data : 20
3.window.data : 10
data = 30; 실행한 후 ===== ===== =====
1.data : 30
2.this.data : 20
3.window.data : 30

Method 에서의 this

메서드에서 this는 객체 자신이 저장된다.

var data = 10;

function thisFn() {
  this.data = 0;
}

thisFn.prototype.thisMethod = function () {
  this.data = 20;

  console.log("this.data = 20; 실행한 후 ===== ===== =====");
  console.log("1.data : " + data);
  console.log("2.this.data : " + this.data);
  console.log("3.window.data : " + window.data);

  data = 30;

  console.log("data = 30; 실행한 후 ===== ===== =====");
  console.log("1.data : " + data);
  console.log("2.this.data : " + this.data);
  console.log("3.window.data : " + window.data);
};

var thisFn = new thisFn();

thisFn.thisMethod();

출력결과

this.data = 20; 실행한 후 ===== ===== =====
1.data : 10
2.this.data : 20
3.window.data : 10
data = 30; 실행한 후 ===== ===== =====
1.data : 30
2.this.data : 20
3.window.data : 30

Method 내부의 중첩 함수에서의 this

객체의 메서드 내부에 만들어지는 중첩 함수에서 this는 객체가 아닌 window가 된다.

var data = 10;

function thisOuterFn() {
  this.data = 0;
}

thisOuterFn.prototype.thisMethod = function () {
  function thisInnerFn() {
    this.data = 20;

    console.log("this.data = 20; 실행한 후 ===== ===== =====");
    console.log("1.data : " + data);
    console.log("2.this.data : " + this.data);
    console.log("3.window.data : " + window.data);

    data = 30;

    console.log("data = 30; 실행한 후 ===== ===== =====");
    console.log("1.data : " + data);
    console.log("2.this.data : " + this.data);
    console.log("3.window.data : " + window.data);
  }

  thisInnerFn();
};

var thisOuterFn = new thisOuterFn();

thisOuterFn.thisMethod();

출력결과

this.data = 20; 실행한 후 ===== ===== =====
1.data : 20
2.this.data : 20
3.window.data : 20
data = 30; 실행한 후 ===== ===== =====
1.data : 30
2.this.data : 30
3.window.data : 30

This in Arrow Function

Arrow function은 자신을 포함하는 외부 scope에서 this를 계승받는다.
즉, Arrow function은 자신만의 this를 생성하지 않는다. (Lexical this)

React Class 형 컴포넌트를 예로 들어보자.

import React, { Component } from 'react';  
  
class App extends Component {  
  constructor(props){  
    super(props);  
    this.state = {  
         data: 'www.javatpoint.com'  
      }  
    this.handleEvent = this.handleEvent.bind(this);  
  }  
  
  handleEvent(){  
    console.log(this.props);  
  }
  
  render() {  
    return (  
      <div className="App">  
       <h2>React Constructor Example</h2>  
       <input type ="text" value={this.state.data} />  
        <button onClick={this.handleEvent}>Please Click</button>  
      </div>  
    );  
  }  
}  
export default App;  

일반적으로 함수는 호출한 자신을 this 로 암묵적으로 지정한다.
하지만 여기서 handleEvent()에서 this는 컴포넌트 classApp 을 가르킨다.
이렇게 되기 위해서 constructor 부분에 this.handleEvent = this.handleEvent.bind(this) 구문을 통해 호출한 handleEvent() 가 아닌 classApp 을 명시적으로 잡아주었다.

만약 이를 Arrow function으로 변환해보자.

import React, { Component } from 'react';  
  
class App extends Component {  
  constructor(props){  
    super(props);  
    this.state = {  
         data: 'www.javatpoint.com'  
    }  
  }  
  
  handleEvent = () => {  
    console.log(this.props);  
  }
  
  render() {  
    return (  
      <div className="App">  
       <h2>React Constructor Example</h2>  
       <input type ="text" value={this.state.data} />  
        <button onClick={this.handleEvent}>Please Click</button>  
      </div>  
    );  
  }  
}  
export default App;  

위와 같이 변환할 수 있다.
따로 thisconstructor 에서 .bind() 해주지 않아도 arrow function의 경우 호출한 함수자체에 대한 this 를 만들지 않고 호출한 함수자체를 포함한 상위의 외부 this를 계승받아 암묵적으로 상위 classAppthis로 인식한다.

부록: Call API

때때로, 우리는 라이브러리나 헬퍼오브젝트를 사용한다. (Ajax, event handling, etc.) 그리고 전달된 콜백을 호출한다. 이러한 경우에는, this의 동작을 주의해야 한다.

myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool',
      this.onSomethingCoolDone);
    },

    onSomethingCoolDone: function () {
      /// Only god knows what is "this" here
    }
};

우리가 "this.onSomethingCoolDone"을 콜백으로 넘겼기 때문에, 스코프가 그 메소드를 참조하고 있다고 생각할 수도 있다.

이 부분을 고치기 위해, 몇가지 방법이 있습니다

  • 주로 라이브러리들은 우리를 위해 또 다른 파라미터를 제공한다. 우리는 그곳에 우리가 다시 가져오길 원하는 스코프를 전달할 수 있다.
myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool', this.onSomethingCoolDone, this);
  },

  onSomethingCoolDone: function () {
    /// Now everybody know that "this" === myObject
  }
};
  • 원하는 스코프를 하드 바인드 할 수도 있다.
myObject = {
  myMethod: function () {
    helperObject.doSomethingCool('superCool', this.onSomethingCoolDone.bind(this));
  },

  onSomethingCoolDone: function () {
    /// Now everybody know that "this" === myObject
  }
};

다음의 글을 참고하였습니다.

profile
yeeaasss rules!!!!

0개의 댓글