0%

javascript this 정리

자바스크립트 this 를 정리합니다.

자바스크립트는 함수를 호출할때 파라미터로 전달하는 값 외에 argumentsthis 를 추가로 전달받습니다.

1
2
3
4
5
6
7
8
9
function square(number) {

console.log(arguments);
console.log(this);

return number * number;
}

square(3);

자바의 경우라면 this 는 객체 자신을 참조하겠지만, 자바스크립트의 경우는 조금 다릅니다.

함수 호출 방식과 this

자바스크립트의 경우 함수 호출 방식에 의해 this 에 바인딩할 객체가 동적으로 지정됩니다.

호출 방식은 아래와 같습니다.

  1. function 호출
  2. method 호출
  3. 생성자 함수 호출
  4. apply / call / bind
  5. ES6 arrow function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var foo = function () {
console.dir(this);
};

// 1. 함수 호출
foo(); // window
// window.foo();

// 2. 메소드 호출
var obj = { foo: foo };
obj.foo(); // obj

// 3. 생성자 함수 호출
var instance = new foo(); // instance

// 4. apply/call/bind 호출
var bar = { name: 'bar' };
foo.call(bar); // bar
foo.apply(bar); // bar
foo.bind(bar)(); // bar

function 호출

기본적으로 this 는 전역객체(window) 에 바인딩 됩니다. 전역 함수는 물론이고 내부함수의 경우에도 this 는 외부함수가 아닌 전역객체로 에 바인딩 됩니다.

1
2
3
4
5
6
7
8
function foo() {
console.log("foo's this: ", this); // window
function bar() {
console.log("bar's this: ", this); // window
}
bar();
}
foo();

method 내부의 function 일 경우에도 this 는 전역객체에 바인됭 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var value = 1;

var obj = {
value: 100,
foo: function() {
console.log("foo's this: ", this); // obj
console.log("foo's this.value: ", this.value); // 100
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1
}
bar();
}
};

obj.foo();

callback function 의 경우에도 this 는 전역객체에 바인딩 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var value = 1;

var obj = {
value: 100,
foo: function() {
setTimeout(function() {
console.log("callback's this: ", this); // window
console.log("callback's this.value: ", this.value); // 1
}, 100);
}
};

obj.foo();

결론은,
function 은 어디에 선언되었든 관계없이 this 가 전역객체에 바인됭 됩니다.

그러나, this 가 전역객체 참조를 회피하는 방법이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var value = 1;

var obj = {
value: 100,
foo: function() {
var that = this; // Workaround : this === obj

console.log("foo's this: ", this); // obj
console.log("foo's this.value: ", this.value); // 100
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1

console.log("bar's that: ", that); // obj
console.log("bar's that.value: ", that.value); // 100
}
bar();
}
};

obj.foo();

이 방법 외에도 apply, call, bind 메소드를 통해 참조를 변경할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var value = 1;

var obj = {
value: 100,
foo: function() {
console.log("foo's this: ", this); // obj
console.log("foo's this.value: ", this.value); // 100
function bar(a, b) {
console.log("bar's this: ", this); // obj
console.log("bar's this.value: ", this.value); // 100
console.log("bar's arguments: ", arguments);
}
bar.apply(obj, [1, 2]);
bar.call(obj, 1, 2);
bar.bind(obj)(1, 2);
}
};

obj.foo();

method 호출

함수가 객체의 프로퍼티로서 존재한다면 이는 메소드로 호출되게 됩니다. 이때 메소드 내부의 this 는 해당 메소드를 호출한 객체가 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var obj1 = {
name: 'Lee',
sayName: function() {
console.log(this.name);
}
}

var obj2 = {
name: 'Kim'
}

obj2.sayName = obj1.sayName;

obj1.sayName(); // Lee
obj2.sayName(); // Kim

프로토타입 객체도 메소드를 가질 수 있습니다. 프로토타입 객체 메소드 내부에서 사용된 this 도 일반 메소드 방식과 마찬가지로 해당 메소드를 호출한 객체에 바인딩 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}

Person.prototype.getName = function() {
return this.name;
}

var me = new Person('Lee');
console.log(me.getName()); // Lee

Person.prototype.name = 'Kim';
console.log(Person.prototype.getName()); // Kim

생성자 함수 호출

functionnew 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작하게 됩니다.

1
2
3
4
5
6
7
8
// 함수
function Person(name) {
this.name = name;
}

// 생성자로 사용
var me = new Person('Lee');
console.log(me); // Person {name: "Lee"}

new 연산자와 함께 생성자 함수를 호출하면 this 바인딩이 메소드나 함수 호출때와 다르게 동작합니다.

생성자 함수 동작 방식

new 연산자와 함께 생성한 함수를 호출하면 다음과 같은 순으로 동작하게 됩니다.

  1. 빈 객체 생성 및 this 바인딩
    생성자 함수의 코드가 실행되기 전 빈 객체가 생성된다. 이 빈 객체가 생성자 함수가 새로 생성하는 객체이다. 이후 생성자 함수 내에서 사용되는 this는 이 빈 객체를 가리킨다. 그리고 생성된 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.

  2. this를 통한 프로퍼티 생성
    생성된 빈 객체에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다. this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가된다.

  3. 생성된 객체 반환
    반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환된다. 명시적으로 this를 반환하여도 결과는 같다.
    반환문이 this가 아닌 다른 객체를 명시적으로 반환하는 경우, this가 아닌 해당 객체가 반환된다. 이때 this를 반환하지 않은 함수는 생성자 함수로서의 역할을 수행하지 못한다. 따라서 생성자 함수는 반환문을 명시적으로 사용하지 않는다.

1
2
3
4
5
6
7
8
function Person(name) {
// 생성자 함수 코드 실행 전 -------- 1
this.name = name; // --------- 2
// 생성된 함수 반환 -------------- 3
}

var me = new Person('Lee');
console.log(me.name);

객체 리터럴 방식과 생성자 함수 방식의 차이

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 객체 리터럴 방식
var foo = {
name: 'foo',
gender: 'male'
}

console.dir(foo);

// 생성자 함수 방식
function Person(name, gender) {
this.name = name;
this.gender = gender;
}

var me = new Person('Lee', 'male');
console.dir(me);

var you = new Person('Kim', 'female');
console.dir(you);
  • 리터럴 방식의 경우, 생성된 객체의 프로토타입 객체는 Object.prototype 입니다.
  • 생성자 함수 방식의 경우, 생성된 객체의 프로토타입 객체는 Person.prototype 입니다.

ES6 의 class

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
constructor(name) {
this.name = name;
}

hi() {
console.log(this);
}
}

const me = new Person('Lee');
me.hi(); // Person {name: "Lee"}

동일한 new 연산자를 사용하였이며 class 를 사용한다고 하여 크게 다르지 않습니다.

apply / call / bind

this 에 바인딩될 객체는 함수 호출 방식에 의해 결정됩니다. 이는 자바스크립트 엔진이 암묵적으로 수행하는것이지만, 개발자가 직접 지정하는 방법을 제공하기도 합니다.

  • Function.prototype.apply
  • Function.prototype.call

이 메소드들은 모두 함수 객체의 프로토타입 객체의 메소드 입니다.

apply

1
2
3
4
func.apply(thisArg, [argsArray])

// thisArg: 함수 내부의 this에 바인딩할 객체
// argsArray: 함수에 전달할 argument의 배열

apply 메소드를 호출하는 주체는 함수입니다.

1
2
3
4
5
6
7
8
9
10
 var Person = function (name) {
this.name = name;
};

var foo = {};

// apply 메소드는 생성자함수 Person을 호출한다. 이때 this에 객체 foo를 바인딩한다.
Person.apply(foo, ['name']);

console.log(foo); // { name: 'name' }

빈 객체 fooapply() 메소드의 첫번째 매개변수에, argument 의 배열을 두번째 매개변수에 전달하여 Person 함수를 호출했습니다.

Person 함수의 thisfoo 객체에 바인딩 되었습니다.
this.name 프로퍼티에 매개변수 name 을 대입하려 하지만, foo 객체에는 name 프로퍼티가 없으므로 name 프로퍼티가 동적으로 추가되고 값이 할당됩니다.

call

1
2
3
Person.apply(foo, [1, 2, 3]);

Person.call(foo, 1, 2, 3);

applycall 메소드는 기능은 같지만 두번쨰 인자를 배열형태와 각각 넘기는것에 차이가 있을뿐입니다.

bind

ES5에 추가된 Function.prototype.bind 를 이용할 수 도 있습니다.

bind 는 함수에 인자로 전달한 this 가 바인딩되는 새로운 함수를 리턴합니다.

bindapply, call 과 달리 함수를 바로 실행하지 않기때문에 명시적으로 함수를 호출할 필요가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name) {
this.name = name;
}

Person.prototype.doSomething = function (callback) {
if (typeof callback == 'function') {
// callback.call(this);
// this가 바인딩된 새로운 함수를 호출
callback.bind(this)();
}
};

function foo() {
console.log('#', this.name);
}

var p = new Person('Lee');
p.doSomething(foo); // 'Lee'

ES6 Arrow function

1
2
3
4
5
6
7
8
9
function Foo(bars) {
this.bars = bars;

bars.map(function(bar) {
console.log(this); // window
});
}

var foo = new Foo([1,2,3,4]);

callback function 의 경우에도 this 는 전역객체에 바인딩 되는것을 위에서도 언급했습니다.

es6 에 추가된 문법인 arrow 를 사용하게 될 경우 thisnew 키워드로 생성한 객체에 바인딩 됩니다.

1
2
3
4
5
6
7
8
9
function Foo(bars) {
this.bars = bars;

bars.map(bar => {
console.log(this); // Foo {bars: Array(4)}
});
}

var foo = new Foo([1,2,3,4]);

ES6 문법이므로 babel 을 사용하여 트랜스해보면 아래와 같은 결과를 볼 수 있습니다.
https://babeljs.io/

1
2
3
4
5
6
7
8
9
10
function Foo(bars) {
var that = this;
this.bars = bars;

bars.map(function (bar) {
console.log(that); // Foo {bars: Array(4)}
});
}

var foo = new Foo([1,2,3,4]);