[JS] this - 명시적으로 this를 바인딩하는 방법

이승혜·2021년 9월 11일
0

JS/JQUERY

목록 보기
9/10
post-thumbnail

이 글은 📕코어 자바스크립트 책을 바탕으로 정리한 글입니다.

이전 게시물 [JS] this - 상황에 따라 달라지는 this
에서는 상황별로 this에 어떤 값이 바인딩되는지 알아보았다.
이러한 규칙을 깨고 this에 별도의 대상을 바인딩하는 방법도 있다.

💡 명시적으로 this 바인딩하기

call 메서드

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

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.
이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 하는 것이다.
함수를 그냥 실행하면 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.

예제

var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3);
func.call({ x: 1 }, 1, 2, 3);

다음 예제를 실행시켜보면 결과는 다음과 같다.

메서드를 그냥 호출하면 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.

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);

apply 메서드

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

apply 메서드는 call 메서드와 기능적으로 완전히 동일하다.
call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수르 지정하는 반면, apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 매개변수로 지정한다

예제

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]);

call/apply 메서드의 활용

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

유사배열객체란?
key가 0 또는 양의 정수인 프로퍼티가 존재하고, length 프로퍼티 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체를 말한다.

<div class="box">box1</div>
<div class="box">box2</div>
<div class="box">box3</div>
<div class="box">box4</div>
var list = document.querySelectorAll('.box');
console.log(list);


배열같이 생겼지만 배열이 아닌, 배열의 구조와 유사한 유사배열객체이다.

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

Array.prototype.push.call(obj, 'd'); // (1)
console.log(obj);

var arr = Array.prototype.slice.call(obj); // (2)
console.log(arr);

유사배열객체의 경우 call또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.
(1) 라인을 보면 배열 메서드인 push를 객체 obj에 적용해 프로퍼티 3에 'd'를 추가했다.
(2) 라인을 보면 slice 메서드를 적용해 객체를 배열로 전환했다.

slice 메서드의 경우 매개변수를 아무것도 넘기지 않을 경우에는 그냥 원본 배열의 얕은 복사본을 반환한다.
즉, call 메서드를 이용해 원본인 유사배열객체의 얕은 복사를 수행한 것인데, slice 메서드가 배열 메서드기 때문에 복사본은 배열로 반환하게 된 것이다.

Array.prototype 메소드 ?
= 원본 배열을 직접 변경하는 메소드
push(),pop(),shift(),unshift(),reverse(),sort(),splice() 등이 있다.

위에서 잠깐 언급했듯 querySelectorAll, getElementsByClassName등의 Node 선택자로 선택한 결과인 NodeList도 마찬가지로 적용된다.

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);
});


그 밖에도 유사배열객체에는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있다.
배열처럼 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지다.

단, 문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에
원본 문자열에 변경을 가하는 메서드(push, pop, shift, unshift, splice 등)는 에러를 던지며,
concat처럼 대상이 반드시 배열이어야 하는 경우 에러는 나지 않지만 제대로 된 결과를 얻을 수 없다.

var str = 'abc def';

Array.prototype.push.call(str, ', push string'); // // TypeError: Cannot assign to read only property 'length' of object '[object String]'

Array.prototype.concat.call(str, 'string'); // [String {"abc def"}, "string"]

Array.prototype.every.call(str, function (char) {
  return char !== ' ';
}); // false

Array.prototype.some.call(str, function (char) {
  return char === ' '; // true
});

var newArr = Array.prototype.map.call(str, function (char) {
  return char + '!';
});
console.log(newArr); // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!'];


var newStr = Array.prototype.reduce.apply(str, [
  function (string, char, i) {
    return string + char + i;
  },
  '',
]);

console.log(newStr); // "a0b1c2 3d4e5f6"

Array.from

사실 call/apply를 이용해 형변환하는 것은 'this를 원하는 값으로 지정해서 호출한다'라는 본래의 메서드의 의도와는 다소 동떨어진 활용법이라 할 수 있다.

slice 메서드는 오직 배열 형태로 '복사'하기 위해 차용됐을 뿐 숨은 뜻을 알고 있는 사람이 아닌 한 코드만 봐서는 의도를 파악하기 어렵다.
이에 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, age) {
  Person.call(this, name, gender);
  this.age = age;
}

function Employee(name, gender, company) {
  Person.apply(this, [name, gender]);
  this.company = company;
}
var sh = new Student('승혜', 'female', '26');
var cr = new Employee('채린', 'female', 'COP');

console.log(sh);
console.log(cr);

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

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 좋다.
예를 들어 배열에서 최대/최솟값을 구해야 하는 경우 Math.max/Math.min 메서드에 apply를 적용할 수 있다.

var numbers = [10, 30, 54, 2, 40];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);

console.log(max); // 54
console.log(min); // 2

참고로 ES6에서는 펼치기 연산자(spread operator)를 이용하면 apply를 적용하는 것보다 더욱 간편하게 작성할 수 있다.

var numbers = [10, 30, 54, 2, 40];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max); // 54
console.log(min); // 2

bind 메서드

bind 메서드는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드다.
bind 메서드는 함수에 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

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

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

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

이전 포스터에서는 메서드의 내부함수에서 메서드의 this를 그대로 바라보게 하기 위한 방법으로 self 등의 변수를 활용한 우회법을 알아보았는데, call, apply 또는 bind 메서드를 이용하면 더 깔끔하게 처리할 수 있다.

내부함수에 this 전달 - call

var obj = {
  outer: function () {
    console.log(this); // { outer: [Function: outer] }
    var innerFunc = function () {
      console.log(this); // { outer: [Function: outer] }
    };
    innerFunc.call(this);
  },
};

obj.outer();

내부함수에 this 전달 - bind

var obj = {
  outer: function () {
    console.log(this); // { outer: [Function: outer] }
    var innerFunc = function () {
      console.log(this); // { outer: [Function: outer] }
    }.bind(this);
    innerFunc.call();
  },
};

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();

화살표 함수의 예외사항

화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외됐다.
즉 이 함수에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 된다.

var obj = {
  outer: function () {
    console.log(this); // { outer: [Function: outer] }
    var innerFunc = () => {
      console.log(this); // { outer: [Function: outer] }
    };
    innerFunc();
  },
};
obj.outer();

콜백 함수 내에서의 this는 콜백 함수를 다루면서 알아보도록 하자.

profile
더 높이

0개의 댓글