this
는 전역 객체를 가리킨다.그러나, 런타임 환경에 따라 this
는 브라우저 환경에서는 window
객체를
node js 환경에서는 global
객체를 가리키게 된다.
<브라우저 환경>
console.log(this === window);
// true
<Nodejs 환경>
console.log(this === global);
// true
먼저 함수와 메서드의 차이를 알아보자.
함수는 말 그대로function
을 선언하여 함수명()
에 형식으로 사용하는
독립적인 형태를 이야기 한다.
메소드는 객체 내부의 위치하여 객체.메서드명
에 형식으로 사용되는 것을 이야기 한다.
이제 아래 코드를 살펴보면 this의 차이점을 알 수 있다.
// 함수의 this
// 호출 주체를 명시할 수 없기 때문에 this는 전역 객체를 의미
var func = function (x) {
console.log(this, x);
};
func(1); // Window { ... } 1
// 브라우저 환경일 경우 window가 출력 된다.
// 메서드의 this
// 호출 주체를 명시할 수 있기 때문에 this는 해당 객체(obj)를 의미해요.
// obj는 곧 { method: f }를 의미하죠?
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(); // 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
// 객체 내부에 또 다른 객체 안에 메서드를 생성했을 때에도 그 메서드가 속한 곳을 가리킨다.
중요한 점은 객체내부에서 함수로써 호출된 this는 전역객체를 가리키게 된다. 즉, 객체.메서드명
의 형태로 호출되어야 this는 자기가 속한 곳을 가리킨다.
아래 코드 참고.
var obj1 = {
outer: function() {
console.log(this); // (1) 출력 : obj1
var innerFunc = function() {
console.log(this); // (2) 출력 : 전역객체(window or global)
}
innerFunc();
var obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod(); // (3) 출력 : obj2
}
};
obj1.outer();
그렇다면 함수에서 this를 전역객체가 아닌, 다른 객체를 가리키게 할 수 없는 것인가에 의문이 생긴다.
그 방법에 대해 알아보자.
먼저, 변수를 활용하는 방법과 화살표 함수를 사용하는 방법이 있다.
1) 변수를 활용하는 방법
내부 스코프에 이미 존재하는 this를 별도의 변수(ex : self)에 할당하는 방법
var obj1 = {
outer: function() {
console.log(this); // outer
// 변수에 할당하는 방법
var self = this;
var innerFunc2 = function() {
console.log(self); // outer
};
innerFunc2();
}
};
// 메서드 호출 부분
obj1.outer();
위 코드처럼 변수를 활용하면 같은 결과를 볼 수 있다.
2) 화살표 함수를 활용하는 방법
ES6에서 처음 도입된 화살표 함수는, 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 없다. 따라서, 이전에 this에 적용된 값이 그대로 유지된다.
(ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표함수를 도입)
따라서 일반 함수와 화살표 함수에 가장 큰 차이는 this binding 여부이다.
var obj = {
outer: function() {
console.log(this);
var innerFunc = () => {
console.log(this); // this가 binding되지 않아 이전 this 유지
};
innerFunc();
}
}
obj.outer();
// 결과 값
// {outer: ƒ}
// {outer: ƒ}
추가적으로 콜백 함수(매개변수로 들어가는 함수)에서 this도 기본적으로 전역객체이다. 그러나 예외가 있다.
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
console.log(this, e);
});
addEventListener
사용할 때 콜백함수에 this
는 함수 내에서 호출한 주체의 element를 return하도록 설계되었다.
위 코드에 경우 button
을 의미한다.
생성자(constructor)
메서드는 클래스의 인스턴스 객체를 생성하고 초기화하는 특별한 메서드다.
class 내부에 위치하며 this를 활용하여 인스턴스를 만든다.
class Person {
constructor() {
this.name = 'Jay';
}
}
const newPerson = new Person();
console.log(newPerson.name);
// 출력값 : "Jay"
자동으로 부여되는 상황별 this의 규칙을 깨고 this에 별도의 값을 저장하는 방법
크게, call / apply / bind
메서드가 있다.
호출 주체인 함수를 즉시 실행하는 명령어.
call명령어를 사용하여, 첫 번째 매개변수에 this로 binding할 객체를 넣어주면 명시적으로 binding 할 수 있다.
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
위 코드와 같이 사용법은 객체명.method.call(this객체, ...arg)
call
의 첫번째 인자로 this를 명시적으로 binding
할 객체를 넣어주면 된다.
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
잠시 call, apply
에 관련된 추가적인 내용을 짚고 넘어가자.
유사배열객체(array-like-object)에 배열 메서드를 적용할 수 있다.(객체지만 배열의 메서드를 사용)
먼저, 유사배열객체의 조건은
1. length
가 필요하다. (이 조건은 필수임)
2. key값이 index
와 같이 0부터 1씩 증가해야한다. (이 조건은 필수는 아니지만 사용하는데 혼선을 줄 수 있기 때문에 극히 드문 상황을 제외하고는 필수)
// 기본적으로 객체에는 배열 메서드를 직접 적용할 수 없다.
// 유사배열객체에는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3 // length 값을 만들어준다.
};
Array.prototype.push.call(obj, 'd'); // 배열 메서드 push 사용
console.log(obj); // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }
var arr = Array.prototype.slice.call(obj); // 배열 메서드 slice 사용
console.log(arr); // [ 'a', 'b', 'c', 'd' ]
직전의 내용은 사실 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']
유사배열객체가 바로 배열로 형변환이 된다.
function Person(name, gender) {
this.name = name;
this.gender = gender; // name과 gender 중복
}
function Student(name, gender, school) {
this.name = name;
this.gender = gender; // name과 gender 중복
this.school = school;
}
function Employee(name, gender, company) {
this.name = name;
this.gender = gender; // name과 gender 중복
this.company = company;
}
var kd = new Student('길동', 'male', '서울대');
var ks = new Employee('길순', 'female', '삼성');
위 코드를 살펴보면 반복되는 구문이 많아 비효율적으로 보인다. 이때 this Binding을 사용할 수 있다.
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', '삼성');
call
이 this
와 매개변수
를 넘겨주면서 Person안에서 Student나 Employee에 대한 정보를 대신 만들어준다.
이렇게 사용하면 훨신 간결한 코드가 된다.
여러 인수를 묶어 하나의 배열로 전달할 때 apply 사용할 수도 있다.
// 효율적인 코드작성
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);
bind()
메소드가 호출되면 새로운 함수를 생성한다. 받게되는 첫 인자의 value로는 this 키워드를 설정하고, 이어지는 인자들은 바인드된 함수의 인수에 제공된다.
call과 apply랑 다른 점은 함수에 this를 미리 적용한다는 것과 부분 적용 함수 구현할 때 용이하다는 것이다.
아래 예제를 보면 이해할 수 있다.
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // 출력: 전역객체 1 2 3 4
// 함수에 this 미리 적용
var bindFunc1 = func.bind({ x: 1 }); // func를 base로 한 bindFunc1라는 새로운 함수에 this가 미리 적용된다.
bindFunc1(5, 6, 7, 8); //따라서 미리 적용한 { x: 1 }뒤에 인자를 이어서 넣어준다.
// 출력 : { 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
// 다른 예제
const module = {
x: 10,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // this는 전역객체이므로 x값을 찾을 수 없다.
// 출력 : undefined
const boundGetX = unboundGetX.bind(module); // this를 module객체로 바인딩.
console.log(boundGetX()); // this 즉, module객체에서 x를 찾을 수 있다.
// 출력 : 10
모든 함수에는 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);
// func와 bindFunc의 name 프로퍼티의 차이
console.log(func.name); // func
console.log(bindFunc.name); // bound func
위와 같은 이유로 이 함수가 바인딩된 함수인지 파악하고 추적하기 쉽다.
따라서, 전에 설명한 변수를 활용해 우회하는 방법보단 call
bind
와 같은 메서드를 사용하는 것이 좋다.
다시 사용법을 정리하자면,
// call 사용하기
var 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); // 출력 : obj
var innerFunc = function () {
console.log(this);
}.bind(this); // innerFunc에 this를 결합한 새로운 함수를 할당
innerFunc(); // 출력 : obj
}
};
obj.outer();
다음은 콜백함수에 binding 하는 방법이다.
var obj = {
logThis: function () {
console.log(this);
},
logThisLater1: function () {
// 0.5초를 기다렸다가 this를 출력하는 함수지만 동작하지 않는다.
setTimeout(this.logThis, 500); // 콜백함수로써 this가 들어왔기 때문에 유효하지 않다.
},
logThisLater2: function () {
// 1초 뒤에 obj를 출력한다.
setTimeout(this.logThis.bind(this), 1000); // 콜백함수에 this를 bind하기
}
};
obj.logThisLater1();
obj.logThisLater2();
결론적으로,
var obj = {
outer: function () {
console.log(this);
var innerFunc = () => {
console.log(this);
};
innerFunc();
};
};
obj.outer();
이렇게 화살표 함수로 작성하면 this우회, call, apply, bind을 애초에 할 필요가 없다.
그럼에도 우리가 알아야하는 이유는 JS가 발달하기 전에 만들어진 코드를 해석해야하기 때문이다.