본문 바로가기

03. this와 객체

본 내용은 책 코어 자바스크립트(2019, 정재남)를 세 번 정독 후, 글쓴이가 이해한 내용과 실제 업무에서 겪은 경험을 기술하였습니다. 다소 형식적이지 못합니다.

객체는 코어 자바 스크립트에서 소개되어있지 않은 내용이나, 2019년 6월 NHN OOP 교육을 복기하기 위해 추가하였습니다.
프로토타입과 this를 이해하는 데 배경 지식이 됩니다.

 

1. this Binding 요약


3장은 미리 결론을 짓고 뒷부분에서 좀 더 깊이 뛰어드는 게 개인적으로 이해하기 편했다.

명시적인 thisBinding이 없을 때

  • 전역공간: 전역 객체(window, global)
  • 함수: 자기 자신을 호출한 주체 (===전역객체)
  • 메소드: 닷노테이션(.) 혹은 []가 붙으면 메소드의 주체가 되는 객체
  • 콜백함수: setTimeout, forEach 처럼 콜백함수 내에서 this를 정의하지 않는 함수는 전역객체,반면 addEventListener는 이벤트 및 엘리먼트 객체
  • 생성자(new): 생성자로 생성된 구체적인 인스턴스

명시적으로 thisBinding하는 방법

  • call, apply: target this가 되는 객체와 인자들을 넘겨 임의로 this를 설정하여 실행
  • bind: target this를 넘겨 바인딩된 새 함수를 리턴 (this를 고정)
  • 화살표 함수(=>): ES6의 화살표 함수는 this를 바인딩 하지 않고 스코프 체인처럼 this를 탐색해나가면서 가장 가까운 스코프에 있는 객체를 this로 바인딩한다.
  • thisArg: 배열 관련한 반복 메소드들에 인자로 target this를 넘길 수 있음


사실 위의 이것들만 기억하면 왠만한 문제는 다 풀고 넘어가는 듯 하다.

아래의 문제의 정답을 맞추시는 분은 사실 이 장은 그냥 노룩패스하셔도 될 정도!

<button onclick="console.log(this)">1.</button>
<button onclick="(function(){console.log(this)})()">2.</button>


Q. 1번과 2번을 클릭했을 때 콘솔에 무엇이 출력될지 1분만 고민하고 가보자!


드래그 하면 정답이 보인다 :)


정답: 1. button 엘리먼트, 2. Window {...} 전역 객체

2. 전역 공간에서의 this

기본적으로 this는 2장에서 배웠던 실행 컨텍스트가 생성될 때 결정된다.

실행 컨텍스트가 생성된 순간은 바로 함수가 호출될 때를 의미하며, 즉 함수가 실행되기 전에 결정된다.

실행 컨텍스트는 VariableEnvironment, LexicalEnvironment, ThisBinding으로 이루어져 있는 객체라고 했었는데 바로 이 ThisBinding에 this가 가리키는 객체가 저장된다.

기본적으로 전역 공간에서 전역 컨텍스트가 전역 객체를 참조하므로 this 또한 전역 객체를 가리킨다.

var a = 1; window.a = 1; this.a = 1;

우리는 보통 var, const, let으로 a를 선언했는데 사실 전역 변수는 전역 객체의 프로퍼티로 할당되는 것이다. 따라서 우리가 window.a, this.a라고 해도 모두 같은 의미가 된다.

자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작합니다.


그렇다면 var와 window.~의 차이는 뭘까?

var와 window는 객체의 프로퍼티 속성에서 차이를 보인다.

var a = 1; 
delete a; // false 

window.b = 2; 
delete b; // true 

console.log(a); // 1 
console.log(b); // Uncaught Reference Error: b is not defined

var로 할당한 경우, 객체 프로퍼티 속성 configurable이 자동으로 false로 잡혀 삭제할 수 없지만, window 프로퍼티로 할당한 경우, configurable가 true로 설정되어 삭제가 가능하다.

3. 함수와 메소드에서의 this

함수와 메소드를 우선 구분짓고 가는 것이 좋다. 함수와 메소드의 가장 큰 차이점은 독립성이다.

함수는 자기자신 스스로 독립적으로 기능을 수행하지만, 메소드는 자신을 호출한 객체에 대한 기능을 수행한다.

둘 다 같은 함수인데 어떻게 이 구분이 가능할까? 바로 this를 다르게 정의함으로써 이를 가능케 했다.

나도 이 책을 보기 전까지는 메소드는 그저 객체의 프로퍼티로 함수를 할당하면 메소드다! 라고만 알고 넘어갔었다.

하지만 단순히 함수를 프로퍼티로 설정한다고 메소드가 되는 것이 아니라 객체를 주체로 하여 호출할 때만 메소드가 되는 것이다.

function foo () { 
  console.log(this) 
}

foo(); // Window {...} 

var obj = { func: foo } 

obj.func(); // { func: foo }

foo 함수를 전역공간에서 호출하냐 객체의 프로퍼티에 담아 호출하느냐에 따라 this가 달라진다.

(단, strict mode에서는 this는 undefined이다.)

이를 구분짓는 가장 큰 방법은 우리가 흔히 객체의 프로퍼티를 가져올 때처럼 함수 앞에 .이 붙거나 []로 감싸져서 호출되면 메소드로서 호출된 함수이다.

여기서 중요한 것은 this는 자신을 호출한 주체가 된다는 점을 기억해야 한다.

function foo() {
  console.log(this); 
} 

var obj = { 
  func: foo, 
  inner: { 
    func: foo 
  }
} 

obj.func(); // { func: foo(), inner: { func: foo() } } 
obj.inner.func(); // { func: foo() }
더보기

[참고] 어 아까 실수하나 했었는데 obj.func과 obj.inner.func에 foo()를 넣어주면 어떻게 될까?

 

var obj = { 
  func: foo(), 
  inner: { 
    func: foo() 
  }
}

이때의 foo는 실행된 결과값이므로 this의 주체는 전역 객체가 되어 전역 객체가 func에 할당된다.



이제 다음 문제를 풀어보면 확실하게 this에 대한 개념을 어느정도 정립한 것이다.

var obj = { 
  outer: fuction () { 
    console.log(this); // -- (1) 
    
    var inner = function () { 
      console.log(this); // -- (2) 
    } 
    
    inner(); 
    
    var obj2 = {
      func: function () { 
        console.log(this); // -- (3) 
      } 
    } 
    
    obj2.func(); 
  }
} 

obj.outer();


이 문제로 출력되는 결과를 생각해보자!

정답은 아래 더보기에 있다.

더보기

정답 및 풀이

(1) obj 객체

 

(2) Window 객체

 

(3) obj2 객체

 

힌트: this는 실행 컨텍스트가 생성될 때(== 함수가 호출될 때) 결정된다.

 

4. 콜백함수에서의 this

콜백함수에서의 this는 콜백함수가 this를 별도로 지정하지 않은 이상 함수이기 때문에 역시 this가 전역 객체로 잡힌다.

책에서는 setTimeout, forEach는 this를 별도로 정의하지 않아 전역 객체이고, addEventListener 는 this를 이벤트를 호출한 targetEvent 엘리먼트로 잡힌다.

5. 생성자 함수에서의 this

생성자는 어떤 공통의 성질을 가지는 객체를 생성하는 틀이다.

프로그래밍적으로 '생성자'는 구체적인 인스턴스를 만들기 위한 일종의 틀입니다.


JS에서는 함수에 이 생성자의 역할을 함께 포함시켜 함수 앞에 new 키워드를 붙이면 생성자 함수로서 동작하여 새 인스턴스 객체를 만들 수 있다.

정확히는 new 키워드로 생성자 함수를 호출하면, 생성자의 프로토타입을 참조하는 __proto__라는 프로퍼티 객체를 만들고, 공통 속성들을 this에 매핑하여 구체적인 인스턴스를 만든다.

var Cat = function (name, kind, age) { 
  this.name = name; 
  this.kind = kind; 
  this.age = age; 
  this.bark ="meow"; 
} 

var Chovy = new Cat("청이", "믹스", 2); 
var Danny = new Cat("단이", "믹스", 2); 

console.log(Chovy); 
console.log(Danny);

예제에서 Chovy와 Danny의 this는 각각의 인스턴스를 가리킨다는 것을 확인할 수 있다.

6. 명시적으로 this를 바인딩하기

위의 규칙을 깨고 this를 원하는 대로 설정하는 방법들이다.

call, apply

var Todo = { 
  complete: false, 
  title: "todo_1" 
} 

function completeTodo (complete) { 
  this.complete = complete; 
} 

completeTodo(true); 

console.log(Todo.complete); // false 
console.log(window.complete); // true

completeTodo함수와 Todo 객체와는 아무 연관이 없기 때문의 위의 규칙에 따라 completeTodo의 this는 전역 객체가 된다. (이젠 넘나 당연..)

어떻게 이 함수를 Todo 객체의 complete를 완료칠 수 있도록 변경할 수 있을까?

먼저 call, apply를 이용하여 completeTodo의 this를 Todo 객체로 설정해서 실행하는 방법이다.

var Todo = { 
  complete: false, 
  title: "todo_1" 
} 

function completeTodo (complete) { 
  this.complete = complete; 
} 

completeTodo.call(Todo, true); 

console.log(Todo.complete); // true 
console.log(window.complete); // undefined

apply는 call과 같은 기능을 수행하지만 뒤의 인자를 배열로 넘겨준다.

bind

this가 고정된 채로 새 함수를 리턴하는 bind함수가 있다.

작년에 NHN 교육에서 원래 이 함수는 없었지만 사람들이 call, apply로 이 bind를 구현해서 쓰다보니 정식으로 ES5부터 도입된 함수였다!

따라서 사실 bind는 call, apply로도 구현할 수 있다.

function myBind (f, target) { 
  return function () { 
    f.call(target); 
  } 
}

 

var Todo = { 
  title: "todo_1", 
  complete: false 
} 

function printTodo () { 
  return this.complete ? `${this.title} 완료` : `${this.title} 미완료` 
} 

var printTodoFunc = printTodo.bind(Todo); 

console.log(printTodoFunc());



이전 예제에서 inner 함수의 this를 obj 객체에 연결하고 싶을 때 bind 혹은 call, apply를 적용할 수 있다.

var obj = { 
  outer: fuction () { 
    console.log(this); 
    var inner = function () { 
      console.log(this); 
    }.bind(this); // this === obj 
    
    inner(); 
    
    var obj2 = { 
      func: function () { 
        console.log(this); 
      } 
    } 
    
    obj2.func(); 
  } 
} 

obj.outer();

 

화살표 함수

ES6 부터 도입된 화살표 함수는 this를 아예 명시하지 않아 this가 존재하지 않는다. 따라서 스코프 체인에 의해 가장 가까운 this에 접근한다.

var obj = { 
  outer: function () { 
    console.log(this); 
    
    var inner = () => { 
      console.log(this); // this === obj 
    } 
    
    inner(); 
    
    var obj2 = { 
      func: function () { 
        console.log(this); 
      } 
    } 
    
    obj2.func(); 
  } 
} 

obj.outer();

 

thisArg

배열 관련 콜백함수를 인자로 받는 메소드 중 this를 별도의 인자로 받아 설정하는 메소드들이 있다.

주로 반복 메소드들이 해당한다.

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.protoype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])

 

7. 객체

먼저 기록할까 하다가 혼란스러울까봐 뒤에서 적기로 하였다!

자바스크립트는 클래스 없이도 객체를 생성할 수 있으며, ECMAscript를 따르는 언어 중 하나이다.

일종의 key-value 쌍으로 이루어진 해쉬테이블이다.

해쉬 테이블에 대한 설명은 여길 참조!


자바 스크립트에서 객체는 모두 Object 타입의 인스턴스이다.

모두 Object 객체로부터 프로토타입에 의한 상속을 받아 빈 객체라도 Object 관련 메소드를 사용할 수 있다.

 

객체를 생성하는 방법

 

1. 리터럴 함수

var obj = { 
  name: "newjeong", 
  age: 28, 
  job: "frontend developer", 
  rank: "junior" 
}

 

2. 팩토리 함수


위의 리터럴로는 객체를 여러 개 찍어낼 때는 부적합하다.

따라서 공장(factory) 역할을 하는 함수가 필요해졌다.

var burger ={ 
  title: "불고기와퍼", 
  price: "3000원", 
  topping: ["피클", "치즈", "양파"] 
} 

const createBurger = (title, price, topping = []) => { 
  var burger = {}; 
  burger.title = title; 
  burger.price = `${price}원`; 
  burger.topping = topping; 
  
  return burger; 
} 

var cheeseBurger = createBurger("치즈버거", "4000", ["치즈", "토마토"]); 
var chickenBurger = createBurger("치킨버거", "4500");

 

3. 생성자 함수


위에서 소개했던 new 키워드를 통해 생성자 함수로 인스턴스를 찍어내는 방법이다.

function Burger (title, price, topping = []) { 
  this.title = title; 
  this.price = `${price}원`; 
  this.topping = topping; 
} 

var cheeseBurger = new Burger("치즈버거", "4000", ["치즈", "토마토"]); 
var chickenBurger = new Burger("치킨버거", "4500");
참고: 화살표 함수는 this를 명시하지 않기 때문에 생성자로 사용할 수 없다.

Arrow functions can never be used as constructor functions. Hence, they can never be invoked with the new keyword. As such, a prototype property does not exist for an arrow function.


나중에 프로토타입을 복기할 때 나오겠지만 생성자 함수로 만든 인스턴스의 공통 함수는 모두 다른 주소값을 참조하고 있기 때문에 결국 새 함수를 생성하는 것이 된다. 따라서 프로토타입을 이용하는 것이 비용 소모가 적다.

Object.defineProperty()


만든 객체의 프로퍼티에 접근 권한 등의 속성을 지정하거나, getter, setter를 만들 수 있는 메소드이다.

var obj = {}; 

Object.defineProperty(obj, "name", { 
  value: 'yujeongJeon', // 값 
  configurable: true, // 삭제, 속성 변경 
  writable: true, // 값 변경 
  enumarable: true // for-in 루프에서 해당 프로퍼티를 반환 
}); 

// 기본 문법의 프로퍼티 추가는 configurable, writable, enumerable 모두 true
obj.name = 'juj' 

getter, setter의 경우에는 calculatable한 프로퍼티를 만들때 사용하거나, 객체 내 데이터를 외부에 간접적으로 나타낼 때 쓴다.

모두 함수 형태로 작성하기 때문에 setter에 같은 프로퍼티에 값을 할당하면 무한 루프에 빠질 수 있으니 주의한다.

var obj = {}; 
var age = 0; 

Object.defineProperty(obj, 'age', { 
  set: function(value) { // obj.age에 값 설정할 때 
    age = value; 
  }, 
  get: function() { // obj.age의 값을 사용할 때 
    return age > 40
    ? age + '세'
    : age + '살'
  } 
}); 

obj.age = 27; 
console.log(obj.age); // 27살
var todo = { 
  title: "todo_1", 
  complete: false, 
  setComplete: function (complete) { 
    this.complete = complete 
  }, 
  setTitle: function (value) { 
    this.title = value; 
  }, 
  getTitle: function () { 
    return this.title; 
  } 
} 

var check = ""; 

Object.defineProperty(todo, "state", { 
  get: function () { 
    return this.complete ? this.title + " 완료" : this.title + " 미완료" 
  } 
}); 

Object.defineProperty(todo, "checked", { 
  set: function (value) { 
    check = value; 
  } 
});

위의 예제에서 setComplete를 달리 하면 state또한 다르게 변경되는 것을 볼 수 있다.

다음과 같이 볼 수 있다. (getter, setter에서 this는 당연히 메소드를 호출한 주체인 객체가 된다는 거!)

※ 기억하기 쉽게 문제 풀기

드래그 하면 정답이 보입니다 :)

문제 1. 각 (1)과 (2)에는 어떤 값이 들어갈까요?

var obj = { 
  foo : () => console.log(this) 
}; 

obj.foo(); // -- (1) 

function Obj () { 
  this.foo = () => console.log(this); 
} 

var obj2 = new Obj(); 
obj2.foo(); // -- (2)


정답: (1) - Window {…}, (2) - Obj {foo: ƒ}


이유: obj리터럴은 새 스코프를 생성하지않으므로 화살표 함수가 찾은 바깥의 this는 전역객체이다


문제 2. p.age를 출력하면 어떤 결과가 나올까요?

function Person() { 
  this.age = 0; 
  setInterval(function growUp() {
    this.age++; 
  }, 1000); 
} 

var p = new Person();



정답: 계속 0




문제 3. 어떻게 p.age를 1초마다 1씩 증가시킬까요?

정답: (접어두었음)

더보기

1. growUp 함수에 this를 인스턴스로 바인딩한다.

2. growUp를 화살표 함수로 바꾼다.

 

Tip

화살표 함수로 호출했을 때 this가 무엇인지 햇갈린다면 화살표 함수 바로 바깥의 this가 무엇인지 고민해본다.

'Books > 코어 자바스크립트' 카테고리의 다른 글

06. 프로토타입  (0) 2020.04.16
05. 클로저  (0) 2020.04.12
04. 콜백 함수  (0) 2020.04.04
02. 실행 컨텍스트  (0) 2019.12.31
01. 데이터 타입  (0) 2019.12.31