[javascript] this

7 minute read

그동안 회사에 적응하고 너무 편해져서 공부를 소홀히 하다가 자바스크립트가 낯설어지는 현상이 생겨서 다시 주말에 공부하기로 했다. 아직도 코드를 짜다보면 현재 this가 어디에 바인딩 되어있는지를 구분 못할 때가 종종 있다. 이 부분을 확실히 정리하고 싶어서 책을 다시 펼쳤다.

코어 자바스크립트 책에 있는 내용과 예제를 요약한 포스트입니다.




1. 상황에 따라 달라지는 this

1-1. 전역 공간에서의 this

  • 브라우저 환경에서 전역객체 : window
  • Node.js 환경에서 전역객체 : global

1-2. 메서드 호출 시 메서드 내부에서 this

함수와 메서드

독립성의 차이, 함수는 자체로 독립적 기능 수행, 메서드는 호출한 객체에 관련된 동작을 수행. 자바스크립트에서는 객체의 메서드로서 호출할 경우에 메서드로 동작하고, 아니면 함수로 동작

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

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


var obj = {
    methodA: function() { console.log(this); },
    inner: {
        methodB: function() { console.log(this); }
    }
};

obj.methodA(); // obj
obj.inner.methodB(); // obj.inner

1-3. 함수로서 호출할 때 함수 내부에서의 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(); // obj1 -> window -> obj2

호출 구문 앞에 . 또는 []가 있는지가 관건.


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

직전 컨텍스트의 this를 바라보게 하고 싶을 때 우회적인 방법으로 변수를 활용

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

        var self = this; // _this 등으로 할당하기도.
        var innerFunc2 = function(){
            console.log(self);
        };
        innerFunc2();
    }
};
obj.outer(); // obj -> window -> obj

상위 스코프의 this를 저장해서 내부 함수에서 활용하려는 수단이다.

화살표 함수는 this를 바인딩 하지 않는다! 따라서 바인딩 과정이 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.

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

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

기본적으로 this가 전역객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우 그 대상을 참조.

setTimeout(function(){ console.log(this); }, 300); // window

[1, 2, 3, 4, 5].forEach(function(x){ // window (1~5)
    console.log(this, x);
});

document.body.innerHtml += '<button id="a">click</button>';
document.body.querySelector('#a').addEventListener('click', function(e){
    console.log(this, e);   // button element
});

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

new 명령어와 함께 함수를 호출하면 함수가 생성자로서 동작하게 됨. 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 새로만들 인스턴스 자신이 된다. 생성자를 호출하면 생성자의 prototype 프로퍼티를 참조하는 proto 가 있는 인스턴스를 만들고 공통 속성 및 개성을 해당객체 (this)에 부여한다.

var Cat = function(name, age){
    this.bark = 'nya~~';
    this.name = name;
    this.age = age;
};
var choco = new Cat('초코', 7); // this : choco
var nabi = new Cat('나비', 5); // this : nabi
console.log(choco, nabi);

/*
Cat {bark: "nya~~", name: "초코", age: 7}
Cat {bark: "nya~~", name: "나비", age: 5}
*/

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

2-1. call 메서드

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

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 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-2. apply 메서드

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

apply 메서드는 call 메서드와 기능적으로 완전 동일함. apply는 두번째 인자를 배열로 받아 매개변수르 지정한다는 차이만 있음.

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

2-3. call, apply 활용

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

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

var obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};
Array.prototype.push.call(obj, 'd');
// { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }

var arr = Array.prototype.slice.call(obj);
// ['a', 'b', 'c', 'd']

참고 : slice는 매개변수를 아무 것도 넘기지 않을 경우 원본 배열의 얕은 복사를 한다.


arguments, NodeList 에 배열 메서드를 적용

function a(){
    var argv = Array.prototype.slice.call(arguments);
    argv.forEach(function(arg){
        console.log(arg);
    });
}
a(1, 2, 3);  // 1 // 2 // 3

document.body.innerHTML = '<div>a</div><div>b</div><div>c</div><div>d</div>';
var nodeList = document.querySelectorAll('div');
var nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function(node){
    console.log(node);
}); // div element // div element ... 

유사배열객체 외에도 문자열에도 call/apply 메서드를 이용하여 배열 메서드를 적용할 수 있다. 단, 문자열의 경우는 length가 읽기 전용이라 문자열을 변경하는 메서드에 에러를 던진다.

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

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

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

ES6 에서는 순회가능한 데이터타입을 배열로 전환하는 Array.from 메서드를 도입했다.


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

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', '구골');

여러 인수를 묶어 하나로 전달하고 싶을 때

여러개의 인수를 받는 메서드에게 하나의 인수를 전달하고 싶을 때 apply를 사용. ex) 배열에서 최대/최솟값을 구해야 하는 경우.

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

단, ES6 환경에서는 펼침연산자를 사용할 수 있어 펼침연산자를 사용하는 것이 훨씬 효율적이다. (주석 참조)


2-4. bind 메서드

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

call과 유사하나 즉시 호출하지 않고 넘겨받은 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 });    // this만을 지정
bindFunc1(5, 6, 7, 8);  // { x: 1 } 5 6 7 8

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

name 프로퍼티

bind 메서드를 적용해서 새로 만든 함수는 name 프로퍼티에 bound라는 접두어가 붙는다.

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 를 내부함수나 콜백에 전달하는 방법은 위에 변수를 활용한 우회법이 있는데, call, apply, bind를 사용하면 더 깔끔하게 처리할 수 있다.

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

콜백 함수를 인자로 받는 함수나 메서드 중 this에 관여하는 것도 bind를 사용하면 this의 값을 바꿀 수 있다.

// 내부 함수에 this 전달
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, ... }

2-5. 화살표 함수의 예외

화살표 함수는 실행 컨텍스트 생성시 this를 바인딩 하는 과정이 제외되었기 때문에 내부에 this가 아예 없으며, 접근 시 스코프체인 상 가장 가까운 this에 접근한다.

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

따라서 화살표 함수를 사용하면 this를 우회하거나, call/apply/bind를 적용할 필요가 없어 간결하고 편리하다.


2-6. 콜백 함수 내에서의 this

별도의 인자로 this를 받는 경우. 콜백함수를 인자로 받는 메서드 중 일부는 this를 지정할 객체 thisArg를 인자로 지정할 수 있는 경우가 있다. 이런 형태는 동작을 반복수행하는 배열 메서드에 많이 있으며 예제에서는 forEach의 예를 살펴보도록 한다.

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);   // report 바인딩
    },
    average: function(){
        return this.sum / this.count;
    }
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average);  // 240 3 80

콜백 함수와 함께 thisArg를 인자로 받는 메서드

  • Array.prototype.forEach(callback[, thisArg])
  • Array.prototype.map(callback[, thisArg])
  • Array.prototype.filter(callback[, thisArg])
  • Array.prototype.some(callback[, thisArg])
  • Array.prototype.every(callback[, thisArg])
  • Array.prototype.find(callback[, thisArg])
  • Array.prototype.findIndex(callback[, thisArg])
  • Array.prototype.flatMap(callback[, thisArg])
  • Array.prototype.from(callback[, thisArg])
  • Set.prototype.forEach(callback[, thisArg])
  • Map.prototype.forEach(callback[, thisArg])

정리

  • 전역공간에서 this는 전역객체 (브라우저 : Window)를 참조한다.
  • this 는 함수를 호출할 때 결정 된다.
    • 메서드로 호출한 경우 메서드 호출주체(.앞 객체)를 참조
    • 함수로 호출한 경우 전역객체를 참조
  • 콜백함수 내부의 this는 제어권을 넘겨받는 함수가 정의한 것을 따르며, 되지 않았을때는 전역객체를 참조.
  • 생성자 함수에서 this는 생성될 인스턴스를 참조
  • call, apply는 this를 명시적으로 지정하면서 함수/메서드를 호출한다.
  • bind는 this에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하며 콜백을 반복 호출하는 일부 메서드들은 thisArg를 받는다.

Comments