[책요약] 코어 자바스크립트 3장 this

2021-02-03

3장 this

  • 대부분의 객체지향 언어에서 this
    • 클래스로 생성한 인스턴스 객체
    • 혼란의 여지가 없음
  • 자바스크립트의 this
    • 어디서든 사용할 수 있음
    • 정확한 작동 방식을 이해하지 못하면 문제 해결에 어려움

3.1 상황에 따라 달라지는 this

  • 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됨
    • 실행 컨텍스트는 함수를 호출할 때 생성
    • 함수를 호출할 때 결정

3.1.1 전역 공간에서의 this

  • 전역 공간에서의 this는 전역 객체
  • 브라우저에서는 window, node.js에서는 global
console.log(this);
console.log(global);
console.log(this === global);
  • 전역 공간에서만 발생하는 특이한 성질 하나가 있다
    • 전역변수를 선언하면 자바스크립트 엔진은 이를 전역 객체의 프로퍼티로도 할당
var a = 1;
console.log(a);
console.log(global.a);
console.log(this.a);
  • 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작
  • a를 직접 호출할 때에 1이 나오는 까닭은?
    • 스코프 체인에서 a를 검색하다가 가장 마지막에 도달하는 전역 스코프(전역 객체)에서 해당 프로퍼티 a를 발견
    • 단순히 (global. or window.)이 생략된 것이라고 여겨도 무방
  • 전역 공간에서는 var로 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 똑같이 동작할 것일까??
    • 대부분의 경우에는 이 말이 맞다
var a = 1;
global.b = 2;
console.log(a, global.a, this.a);
console.log(b, global.b, this.b);

global.a = 3;
b = 4;
console.log(a, global.a, this.a);
console.log(b, global.b, this.b);
  • 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우도 있다.
    • ‘삭제’ 명령
var a = 1;
delete global.a;
console.log(a, global.a, this.a);
var b = 2;
delete b;
console.log(b, global.b, this.b);
global.c = 3;
delete global.c;
console.log(c, global.c, this.c);
global.d = 4;
delete d;
console.log(d, global.d, this.d);
  • 처음부터 전역객체의 프로퍼티로 할당한 경우에는 삭제가 되지만 전역변수로 선언한 경우에는 삭제가 되지 않는다.
  • 전역변수를 선언하면 자바스크립트 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의

3.1.2 메서드로서 호출할 때 그 메서드 내부에서의 this

함수 vs 메서드

  • 어떤 함수를 실행하는 방법
    • 함수로서 호출
    • 메서드로서 호출
  • 둘을 구분하는 유일한 차이
    • 독립성
    • 함수는 그 자체로 독립적인 기능을 수행
    • 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행
  • 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 구현
  • 자바스크립트 초심자분들은 흔히 메서드를 ‘~객체의 프로퍼티에 할당된 함수~’로 이해 (▵)
    • 그 자체로서 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출한 경우에만 메서드로 동작
var func = function (x) {
  console.log(this, x);
};
func(1);
var obj = {
  method: func,
};

obj.method(2);
  • ‘함수로서 호출’, ‘메서드로서 호출’ 구분
    • 함수 앞에 점(.)이 있으면 메서드로서 호출
    • (대괄호 표기법에 따른 경우에도 메서드로서 호출)
var obj = {
  method: function (x) {
    console.log(this, x);
  },
};

obj.method(1);
obj["method"](2);

메서드 내부에서의 this

  • this에는 호출한 주체에 대한 정보가 담김
  • 어떤 함수를 메서드로서 호출하는 경우
    • 호출 주체는 함수명(프로퍼티명) 앞의 객체
    • 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this
var obj = {
  methodA: function () {
    console.log(this);
  },
  inner: {
    methodB: function () {
      console.log(this);
    },
  },
};

obj.methodA();
obj["methodA"]();
obj.inner.methodB();
obj.inner["methodB"]();
obj["inner"].methodB();
obj["inner"]["methodB"]();

3.1.3 함수로서 호출할 때 그 함수 내부에서의 this

함수 내부에서의 this

  • 어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않음
  • 실행 컨텍스트를 활성화할 당시에 this가 지정되지 않은 경우 this는 전역 객체를 바라봄
  • 함수에서의 this는 전역 객체

매서드의 내부함수에서의 this

  • 메서드 내부에서 정의하고 실행한 함수에서의 this는 초심자들이 가장 혼란을 느끼는 지점 중 하나
    • 실제 동작과 다르게 예측
var obj1 = {
  outer: function () {
    console.log(this);
    var innerFunc = function () {
      console.log(this);
    };
    innerFunc();

    var obj2 = {
      innerMethod: innerFunc,
    };
    obj2.innerMethod();
  },
};

obj1.outer();
  • this 바인딩에 관해서는 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고
  • 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건

메서드의 내부 함수에서의 this를 우회하는 방법

  • 호출 주체가 없을 때는 자동으로 전역객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있다면 좋겠다
    • 훨씬 자연스럽고 스코프체인과의 일관성을 지키는 설득력 있는 방식
  • ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없었다.
  • 변수를 활용해서 우회
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc1 = function () {
      console.log(this);
    };
    innerFunc1();

    var self = this;
    var innerFunc2 = function () {
      console.log(self);
    };
    innerFunc2();
  },
};

obj.outer();

this를 바인딩하지 않는 함수

  • this를 바인딩하지 않는 화살표 함수arrow function를 새로 도입
  • 상위 스코프의 this를 그대로 활용
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    };
    innerFunc();
  },
};

obj.outer();
  • 그 밖에 call, apply 등 메서드 활용해 함수를 호출할 때 명시적으로 this를 지정하는 방법(뒤에서 설명)

3.1.4 콜백 함수 호출 시 그 함수 내부에서의 this

  • 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다.
  • 함수 A는 함수 B의 내부 로직에 따라 실행되며,
  • this 역시 함수 B 내부로직에서 정한 규칙에 따라 값이 결정
  • 콜백 함수도 함수이기 때문에 기본적으로 this는 전역객체 참조
  • 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 그 대상을 참조
setTimeout(function () {
  console.log(this);
}, 300);
[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this, x);
});
document.body.innerHTML += '<button id="a">클릭<button>';
document.body.querySelector("#a").addEventListener("click", function (e) {
  console.log(this, e);
});

3.1.5 생성자 함수 내부에서의 this

  • 생성자 함수
    • 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수
  • new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작
  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신
var Cat = function (name, age) {
  this.bark = "야옹";
  this.name = name;
  this.age = age;
};

var choco = new Cat("초코", 7);
var nabi = new Cat("나비", 5);
console.log(choco, nabi);

3.2 명시적으로 this를 바인딩하는 방법

3.2.1 call 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
  • 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령
  • 첫 번째 인자를 this로 바인딩
  • 그 이후의 인자들, 호출할 함수의 매개변수
var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3);
func.call({ x: 1 }, 4, 5, 6);
var obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

obj.method(2, 3);
obj.method.call({ a: 4 }, 5, 6);

3.2.2 apply 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
  • call 메서드와 기능적으로 완전히 동일
  • 두 번째 인자를 배열로 받아 매개변수로 지정
  • call -> comma, apply -> array
var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func.apply({ x: 1 }, [4, 5, 6]);
var obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

obj.method.apply({ a: 4 }, [5, 6]);

3.2.3 call / apply 메서드의 활용

유사배열객체array-like obejct에 배열 메서드를 적용

var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
};

Array.prototype.push.call(obj, "d");
console.log(obj);
var arr = Array.prototype.slice.call(obj);
console.log(arr);
  • 객체에는 배열 메서드를 직접 적용할 수 없다.
  • 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체
    • 배열의 구조와 유사한 객채의 경우(유사배열객체)
  • call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.
function a() {
  var argv = Array.prototype.slice.call(arguments);
  argv.forEach(function (arg) {
    console.log(arg);
  });
}
a(1, 2, 3);
document.body.innerHTML = "<div>a</div><div>b</div><div>c</div>";
var nodeList = document.querySelectorAll("div");
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function (node) {
  console.log(node);
});
var str = "abc def";

Array.prototype.push.call(str, ", pushed string");
Array.prototype.concat.call(str, "sring");
Array.prototype.every.call(str, function (char) {
  return char !== " ";
});
Array.prototype.some.call(str, function (char) {
  return char === " ";
});
var newArr = Array.prototype.map.call(str, function (char) {
  return char + "!";
});
console.log(newArr);

var newStr = Array.prototype.reduce.apply(str, [
  function (string, char, i) {
    return string + char + i;
  },
  "",
]);
console.log(newStr);
  • call/apply를 이용해 형변환하는 것은 본래 메서드 의도와는 다소 동떨어진 활용법
  • ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입
var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
};

var arr = Array.from(obj);
console.log(arr);

생성자 내부에서 다른 생성자를 호출

  • 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면 간단하게 반복을 줄일 수 있다.
function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}

function Student(name, gender, school) {
  Person.call(this, name, gender);
  this.school = school;
}

function Employee(name, gender, company) {
  Person.apply(this, [name, gender]);
  this.company = company;
}

var by = new Student("보영", "female", "단국대");
var jn = new Employee("재난", "male", "구글");
console.log(by);

여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용

var numbers = [10, 20, 3, 16, 45];
var max = (min = numbers[0]);
numbers.forEach(function (number) {
  if (number > max) {
    max = number;
  }
  if (number < min) {
    min = number;
  }
});

console.log(max, min);
var numbers = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);
var numbers = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min);

3.2.4 bind 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
  • ES5에 추가된 기능
  • call과 비슷하지만 즉시 호출하지는 않고, 넘겨받은 this및 인수들을 바탕으로 새로운 함수를 반환
var func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};
func(1, 2, 3, 4);
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8);
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7);
bindFunc2(8, 9);

name 프로퍼티

  • bind 메서드를 적용해서 새로 만든 함수는 name프로퍼티에 동사 bind의 수동태인 ‘bound’라는 접두어 붙는다.
  • 어떤 함수의 name 프로퍼티가 ‘bound xxx’라면 함수명이 xxx인 원본 함수에 bind를 적용한 새로운 함수라는 의미
var func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};

var bindFunc = func.bind({ x: 1 }, 4, 5);
console.log(func.name);
console.log(bindFunc.name);

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

  • 3.1.3에서 self 등의 변수를 활용한 우회법을 소개
  • call, apply 또는 bind 메서드를 이용해서 깔끔하게 처리
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = function () {
      console.log(this);
    };
    innerFunc.call(this);
  },
};
obj.outer();
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = function () {
      console.log(this);
    }.bind(this);
    innerFunc();
  },
};
obj.outer();
var obj = {
  logThis: function () {
    console.log(this);
  },
  logThisLater1: function () {
    setTimeout(this.logThis, 500);
  },
  logThisLater2: function () {
    setTimeout(this.logThis.bind(this), 1000);
  },
};
obj.logThisLater1();
obj.logThisLater2();

3.2.5 화살표 함수의 예외사항

  • 화살표 함수는 실행 컨텍스트 생성시 this를 바인딩하는 과정이 제외
  • 스코프체인상 가장 가까운 this에 접근
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    };
    innerFunc();
  },
};
obj.outer();

3.2.6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

  • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.
  • forEach, Set, Map 등
var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function () {
    return this.sum / this.count;
  },
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average());

3.3 정리

  • 다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립한다
    • 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조합니다.
    • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조합니다.
    • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조합니다. 메서드의 내부함수에서도 같습니다.
    • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조합니다.
    • 생성자 함수에서의 this는 생성될 인스턴스를 참조합니다.
  • 명시적 this 바인딩
    • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출합니다.
    • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듭니다.
    • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 합니다.

Comments