[JavaScript 문법] this(바인딩, call, apply, bind)

조아영·2024년 7월 26일

📌

ThisBindings는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체(실행 컨텍스트) 중 하나입니다.

◼ 상황에 따라 달라지는 this

this는 함수를 호출할 때 결정됩니다.

1. 전역 공간에서의 this

  • 전역 공간에서 this는 전역 객체를 가리킵니다.
  • 런타임 환경에 따라 this는 window(브라우저 환경) 또는 global(node 환경)를 각각 가리킵니다.

런타임 환경
프로그램이 구동중인 환경을 말합니다. node 파일이름.js로 vscode상에서 구동한다면 node 환경, html 파일 안에 숨겨놓아서 크롬브라우저 등에서 연다고 한다면 브라우저 환경이라고 할 수 있습니다.

브라우저 환경 this

console.log(this);
console.log(window);
console.log(this === window); // true

node 환경 this

console.log(this);
console.log(global);
console.log(this === global); // true

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

메서드, 함수 차이점

메서드화 함수는 상당히 비슷해 보이지만 엄연한 차이가 존재합니다. 기준은 독립성입니다.
메서드는 자신을 호출한 대상 객체에 대한 동작을 수행합니다.
함수는 그 자체로 독립적인 기능을 수행합니다.

// 메서드
객체.메서드명();

// 함수
함수명();

메서드

호출 주체를 명시할 수 있기 때문에 this는 해당 객체(obj)를 의미합니다.
obj는 곧 { method: f }를 의미하죠?

var obj = {
    method: func,
};
obj.method(2); // { method: f } 2

함수

호출 주체를 명시할 수 없기 때문에 this는 전역 객체를 의미합니다.

var func = function (x) {
    console.log(this, x);
};
func(1); // Window { ... } 1

호출 구분 기준

메서드로서의 호출 구분 : . []

var obj = {
    method: function (x) {
        console.log(this, x);
    },
};
obj.method(1); // { method: f } 1
obj["method"](2); // { method: f } 2
var obj = {
    methodA: function () {
        console.log(this);
    },
    inner: {
        methodB: function () {
            console.log(this);
        },
    },
};

obj.methodA(); // this === obj
obj["methodA"](); // this === obj

obj.inner.methodB(); // this === obj.inner
obj.inner["methodB"](); // this === obj.inner
obj["inner"].methodB(); // this === obj.inner

obj["inner"]["methodB"](); // this === obj.inner

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

함수 내부에서의 this

  • 어떤 함수를 함수로서 호출할 경우, 호출 주체를 알 수 없어 this는 지정되지 않습니다.
  • 실행컨텍스트를 활성화할 당시 this가 지정되지 않은 경우, this는 전역 객체를 의미합니다.
  • 함수로서 독립적으로 호출할 때this는 항상 전역객체를 가리킨다는 것을 주의해야합니다.

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

메서드의 내부라고 해도, 함수로서 호출한다면 this는 전역 객체를 의미합니다.
함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지가 관건이라는 것을 알 수 있습니다.

var obj1 = {
    outer: function () {
        console.log(this);
        var innerFunc = function () {
            console.log(this);
        };
        innerFunc(); // 함수로서의 호출 - 전역객체(global)

        var obj2 = {
            innerMethod: innerFunc,
        };
        obj2.innerMethod(); // 메서드로서의 호출 - obj2
    },
};
obj1.outer(); // 메서드로서의 호출 - obj1

메서드의 내부함수에서의 this 우회

변수를 활용하는 방법

내부 스코프에 이미 존재하는 this를 별도의 변수(ex : self)에 할당하는 방법이 있습니다.

var obj1 = {
    outer: function () {
        console.log(this); // outer

        // AS-IS(기존)
        var innerFunc1 = function () {
            console.log(this); // 전역객체
        };
        innerFunc1();

        // TO-BE(이후)
        var self = this;
        var innerFunc2 = function () {
            console.log(self); // outer
        };
        innerFunc2();
    },
};

obj1.outer(); // 메서드 호출 부분

화살표 함수(=this를 바인딩하지 않는 함수)

ES6에서 처음 도입된 화살표 함수는, 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 없습니다. 따라서, this는 이전의 값(스코프 체인상 가장 가까운 this)이 유지됩니다. ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표함수를 도입했습니다.
일반 함수와 화살표 함수의 가장 큰 차이점은 this binding 여부입니다.

바인딩(Binding)
데이터를 특정 위치에 동적으로 연결하는 것을 의미합니다.

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

obj.outer();

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

함수, 메서드의 인자(매개변수)로 넘겨주는 함수를 콜백함수라고 합니다.
이 때, 콜백함수 내부의 this는 해당 콜백함수를 넘겨받은 함수(메서드)가 정한 규칙에 따라 값이 결정됩니다.
콜백 함수도 함수기 때문에 this는 전역 객체를 참조하지만, 콜백함수를 넘겨받은 함수에서 콜백 함수에 별도로 this를 지정한 경우는 예외적으로 그 대상을 참조하게 되어있습니다.

// 별도 지정 없음 : 전역객체
setTimeout(function () { console.log(this) }, 300);

// 별도 지정 없음 : 전역객체
[1, 2, 3, 4, 5].forEach(function(x) {
    console.log(this, x);
});

// addListener 안에서의 this는 항상 호출한 주체의 element를 return하도록 설계되었음
// 따라서 this는 button을 의미함
document.body.innerHTML += "<button id="a">클릭</button>";
document.body.querySelector("#a").addEventListener("click", function(e) {
    console.log(this, e);
});
  1. setTimeout 함수, forEach 메서드는 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않으므로, this는 곧 전역객체입니다.
  2. addEventListner 메서드는 콜백 함수 호출 시, 자신의 this를 상속하므로, this는 addEventListner의 앞부분(button 태그)입니다.

5. 생성자 함수 내부에서의 this

생성자

구체적인 인스턴스(객체)를 만들기 위한 일종의 틀

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

var choco = new Cat("초코", 7); // this : choco
var nabi = new Cat("나비", 5); // this : nabi

◼ 명시적 this 바인딩

자동으로 부여되는 상황별 this의 규칙을 깨고 this에 별도의 값을 저장하는 방법입니다.

1. call 메서드

호출 주체인 함수를 즉시 실행하는 명령어입니다.
call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 binding할 수 있습니다.

전역객체를 바라보는 상황에서 명시적 바인딩

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

// no binding
func(1, 2, 3); // Window{ ... } 1 2 3

// 명시적 binding
// func 안에 this에는 {x: 1}이 binding
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 - method 함수 안의 this는 obj
obj.method.call({ a: 4 }, 5, 6); // 4 5 6

2. apply 메서드

call 메서드와 완전히 동일합니다. 다만, this에 binding할 객체는 똑같이 넣어주고 나머지 부분만 배열 형태로 넘겨줍니다.

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

3. call / apply 메서드 활용

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

유사배열객체
배열과 유사한 형태의 객체입니다. 반드시 length가 필요하고 index번호가 0부터 시작해서 1씩 증가해야합니다.

객체에는 배열 메서드를 직접 적용할 수 없습니다. 예시) obj.push(X), obj.slice(X)
유사배열객체에는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있습니다.
this의 자리에 유사배열객체를 넣어줌으로 기능을 수행하도록 합니다.

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" ]

Array.from 메서드(ES6)

call, apply는 this binding만 하는 것이 아니라 객체 → 배열로 형 변환을 위해서도 쓸 수 있습니다. 그러나 원래 의도와는 거리가 먼 방법입니다.
따라서, ES6에서는 Array.from이라는 방법을 제시했습니다.

// 유사배열객체
var obj = {
    0: "a",
    1: "b",
    2: "c",
    length: 3
};

// 객체 → 배열
var arr = Array.from(obj);

console.log(arr); // ["a", "b", "c"]

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

공통된 내용 반복을 제거하는 방법

비효율

function Student(name, gender, school) {
    this.name = name;
    this.gender = gender;
    this.school = school;
}
function Employee(name, gender, company) {
    this.name = name;
    this.gender = gender;
    this.company = company;
}
var kd = new Student("길동", "male", "서울대");
var ks = new Employee("길순", "female", "삼성");

효율

function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}
function Student(name, gender, school) {
    Person.call(this, name, gender); // this는 student 인스턴스
    this.school = school;
}
function Employee(name, gender, company) {
    Person.apply(this, [name, gender]); // this는 employee 인스턴스
    this.company = company;
}
var kd = new Student("길동", "male", "서울대");
var ks = new Employee("길순", "female", "삼성");

Student, Employeenamegender 속성 모두 필요합니다.
그러니 StudentEmployee 인스턴스를 만들 때 마다 세 가지 속성을 모두 각 생성자 함수에 넣기 보다는
Person이라는 생성자 함수를 별도로 빼는게 구조화에 도움이 됩니다.

여러 인수를 묶어 하나의 배열로 전달할 때 apply 사용

비효율

var numbers = [10, 20, 3, 16, 45];
var max = min = numbers[0]; // 10
numbers.forEach(function (number) {
    // 현재 돌아가는 숫자가 max값 보다 큰 경우
    if (number > max) {
        // max 값을 교체
        max = number;
    }
    // 현재 돌아가는 숫자가 min값 보다 작은 경우
    if (number < min) {
        // 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);

// 펼치기 연산자(Spread Operation)를 통하면 더 간편하게 해결 가능
const numbers = [10, 20, 3, 16, 45];
const max = Math.max(...numbers);
const min = Math.min(...numbers);
console.log(max min);

4. bind 메서드

call, apply와는 다르게 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드입니다.

목적

  1. 함수에 this를 미리 적용합니다.
  2. 부분 적용 함수 구현할 때 용이합니다.
var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // window객체

// 함수에 this 미리 적용
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); // 4와 5를 미리 적용
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(8, 9); // { x: 1 } 4 5 8 9

name 프로퍼티

bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 bound(bind의 수동태. 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를 사용하면 깔끔하게 처리 가능합니다.

call 사용

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

        // call을 이용해서 즉시실행하면서 this를 넘겨줌
        innerFunc.call(this); // obj
    },
};
obj.outer();

bind 사용

var obj = {
    outer: function () {
        console.log(this);
        var innerFunc = function () {
            console.log(this);
        }.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
        innerFunc();
    },
};
obj.outer();

콜백함수

콜백함수도 함수이기 때문에, 함수가 인자로 전달될 때는 함수 자체로 전달합니다.(this가 유실)

var obj = {
    logThis: function () {
        console.log(this);
    },
    logThisLater1: function () {
        // 0.5초를 기다렸다가 출력. 정상동작하지 않음.
        // 콜백함수도 함수이기 때문에 this를 bind해주지 않아서 유실
        setTimeout(this.logThis, 500);
    },
    logThisLater2: function () {
        // 1초를 기다렸다가 출력. 정상동작.
        // 콜백함수에 this를 bind 해주었기 때문에 정상작동.
        setTimeout(this.logThis.bind(this), 1000);
    },
};

obj.logThisLater1();
obj.logThisLater2();

5. 화살표 함수의 예외사항

화살표 함수는 실행 컨텍스트 생성 시, this를 바인딩하는 과정이 제외합니다.
이 함수 내부에는 this의 할당과정(바인딩 과정)이 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근하게 됩니다.
this우회, call, apply, bind보다 편리한 방법입니다.

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

0개의 댓글