[F-Lab 모각코 챌린지 4일차] This 바인딩(2)

Nami·2023년 6월 4일
0

66일 포스팅 챌린지

목록 보기
4/66

실행 컨텍스트에 대한 정리를 하다 this 바인딩 부분도 공부할 것이 많아 따로 정리하고싶었다.
(1)에서는 this의 정의와 쓰이게 된 이유, 일반 함수 호출, 메서드 호출, 생성자 함수 호출 등 상황별로 어떤 값이 this에 바인딩 되는지를 알아보았다.
(2)에서는 규칙을 깨고 별도의 대상을 명시적으로 this에 바인딩하는 방법에 대해 알아볼 것이다.


this

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

1. call 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
  • call메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.
  • 첫번째 인자를 this로 바인딩하고 이후의 인자들을 호출할 함수의 매개변수로 사용한다.
  • 함수를 그냥 실행하면 this는 전역객체를 참조하지만 call메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.
var func = function (a, b, c) {
  console.log(this, a, b, c);
};

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

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

2. apply 메서드

Function.prototype.apply(thisArg[, argsArray])

apply 메서드는 call메서드와 기능적으로 완전히 동일하다.
apply메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.

var func = function (a, b, c) {
  console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); 			// { 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]); 		// 4 5 6

call / apply 메서드의 활용

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

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj); 	// { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }
// 프로퍼티를 추가함.
var arr = Array.prototype.slice.call(obj);
console.log(arr);	// [ 'a', 'b', 'c', 'd']
// 매개변수를 넘기지 않아 얕은 복사 수행, 배열 메서드라 배열로 반환

객체에는 배열 메서드를 직접 적용할 수 없으나 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의 정수인 객체.
즉, 배열의 구조와 유사한 객체. 유사배열객체의 경우 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.

함수 내부에서 접근할 수 있는 arguments 객체도 유사배열객체이므로 배열로 전환해서 활용할 수 있다. querySelectorAll, getElementsByClassName 등의 Node선택자로 선택한 결과인 NodeList 또한 적용 가능하다.

Array.prototype.slice() - MDN
Function.prototype.call() - MDN
NodeList - MDN

유사배열객체에는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있다. 배열처럼 인덱스나 length프로퍼티를 지니는 문자열에 대해서도 마찬가지.
하지만 문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드(push, pop, shift, unshift, splice 등)는 에러를 던지며 concat처럼 대상이 반드시 배열이어야 하는 경우에는 제대로 된 결과를 얻을 수 없음

call/apply를 이용해 형변환하는 것은 본래의 메서드 의도와는 동떨어진 활용법이다. 이에 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from메서드를 새로 도입했다.

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
var arr = Array.from(obj);
console.log(arr); 			// ['a', 'b', 'c']

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

생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 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('윤보현', 'male', '단국대');
var jn = new Employee('임재령', 'female', 'Google');

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

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 좋다.

예를 들어 배열에서 최대/최솟값을 구해야한다.

(1) 직접 구현할 경우

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); 			// 45 3

(2) 여러 인수를 받는 메서드 Math.max/Math.minapply를 적용할 경우

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);			// 45 3

(3) ES6의 펼치기 연산자 spread operator 활용할 경우

const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max, min);			// 45 3

call/apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메서드를 실행하는 훌륭한 방법이지만 오히려 this를 예측하기 어렵게 만들어 코드 해석을 방해한다는 단점이 있다. 하지만 ES5 이하의 환경의 실무에서 광범위하게 활용되고 있음.

3. bind 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

ES5에서 추가된 기능, 즉시 호출하지 않고 넘겨받은 this및 인수들을 바탕으로 새로운 함수를 반환하는 메서드이다.
함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 지님.

var func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
}
func(1, 2, 3, 4); 				// Window{ ... } 1 2 3 4

var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8);			// { x: 1 } 5 6 7 8
								// this 만을 지정한 것

var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7);				// { x: 1 } 4 5 6 7
bindFunc2(8, 9);				// { x: 1 } 4 5 8 9
								// 	this 지정과 함께 부분 적용 함수를 구현

name 프로퍼티

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); 		// func
console.log(bindFunc.name);		// bound func

name프로퍼티에 동사 bind의 수동태인 bound라는 접두어가 붙는다.
name프로퍼티가 'bound abc'라면 이는 곧 함수명이 abc인 원본 함수에 bind 메서드를 적용한 새로운 함수라는 의미이다.
기존의 call이나 apply보다 코드를 추적하기에 수월해짐.

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

메서드의 내부함수에서 메서드의 this를 그대로 바라보게 하기 위한 방법.
변수를 활용한 우회법보다 더 깔끔하게 처리 가능.

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();	// Window { ... }
obj.logThisLater2();	// obj { logThis: f, ... }

화살표 함수의 예외사항

ES6에 새로 도입된 화살표 함수는 실행 컨텍스트 생성시 this를 바인딩하는 과정이 제외됨. 함수 내부에는 this가 없으며 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됨.

var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    };
    innerFunc();
  }
};
obj.outer();

별도의 변수로 this를 우회하거나 call/apply/bind를 적용할 필요가 없어 더욱 간결하고 편리함.

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

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체thisArg를 인자로 지정할 수 있는 경우가 있다. 이런 메서드의 thisArg값을 지정하면 콜백 함수 내부에서 this값을 원하는 대로 변경할 수 있음. 보통 여러 내부 요소에 대해 같은 동작을 반복 수행해야 하는 배열 메서드에 많이 포진되어있다.

정리

아래 규칙은 명시적 this 바인딩이 없는 한 늘 성립한다.

  • 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조한다.
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조한다. (.[])
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조한다.
    • 메서드의 내부 함수에서도 같다.
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조한다.
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조한다.

위 규칙에 부합하지 않는 경우엔 아래 내용을 바탕으로 this를 예측한다

  • call/apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출한다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 한다. (추후에 콜백함수를 배우며 짚어볼 예정)

참조 ✅

  • 📚 『코어 자바스크립트』
    • 3-2. 명시적으로 this를 바인딩하는 방법
  • 📚 『모던 자바스크립트 Deep Dive』
    • 22.2.4 Function.prototype.apply/call/bind 메서드에 의한 간접 호출

0개의 댓글