this 공부하면서 제일 정의되어있지 않아서 공부하는데 나로써 이해하는지 굉장히 힘들었다.
이해는 했다고 생각했지만, 막상 설명하려고 하면 어떻게 설명을 해야하는 지가 어려움을 느꼈다.
내가 이해한 this를 토대로 정리해서 글로 남겨보려고 한다.
👉🏻 객체지향 언어에서는 클래스로 생성한 인스턴스 객체를 의미하고, 클래스에서만 사용할 수 있기 때문에 혼란스럽지 않지만, 자바스크립트에서의 this는 상황에 따라 this가 바라보는 대상이 달라진다.
this를 공부하기전에는전역공간에서 전역변수를 선언하면 당연히 알아서 자동으로 전역객체를 바라보는거 아니야? 라고 생각했는데
여기서 깨달은 점은 전역변수를 선언하면 전역공간에서 호출을 했기때문에 전역객체의 프로퍼티로 할당한다라는 것이다.
여기서 조금 더 말을 정리 해보면
❗️자바스크립트의 this는 선언이 아니라 어디에서 호출하느냐에 따라this
가 결정된다는 것을 알게 되었다.
조금 더 나아가서는 ❗️어디서 어떻게 호출을 했느냐에 따라this
가 결정❗️된다.
객체의 프로퍼티: 자바스크립트의 모든 변수는 특정객체의 프로퍼티로서 동작하기 때문이다.
특정객체는 실행컨텍스트의 렉시컬환경을 말한다.
❗️렉시컬환경은 JavaScript 코드에서 변수나 함수 등의 식별자를 정의하는데 사용하는 객체이다.
예시를 보며 이해해보자.
var a = 1;
console.log(a);
console.log(window.a);
console.log(this.a);
이렇게 작성하게 된다면, console.log에서 무엇을 보여줄건가 ?
전역공간에서 a로 변수를 선언을 해주었을 뿐인데 모두 '1'을 보여준다.
앞에서 this를 설명할때 선언이 아니라 호출시점이다.
전역공간에서 this를 console.log를 호출 시켜줬기 때문에 a라는 객체를 프로퍼티가 할당되었을 것이다.
여기를 보면 이해가 갈거다.
전역공간인 window에 a라는 객체프로퍼티가 생성이 되었다.
var afunc = function(x){
console.log(this, x);
}
func(1);
function afunc2() {
console.log(this);
}
func2();
function afunc3() {
console.log(this);
function afunc3_1() {
console.log(this);
}
afunc3_1();
}
afunc3();
console.log를 보면 this는 전역함수든, 내부함수든 전역객체에 바인딩 되는걸 볼 수 있다.
어? 그럼 메소드 내에서 함수나 콜백함수는 다르지 않을까 ? 한번 해보자.
var obj = {
amothod: function () {
console.log('amothod', this);
function func4 () {
console.log('func4', this);
}
func4();
}
}
obj.amothod();
var obj2 = {
amothod: function () {
setTimeout(function() {
console.log('obj2', this);
}, 1000)
}
}
obj2.amothod();
정말 함수에서 this
는 어디서든 관계없이 전역객체를 바인딩 하는 걸 볼 수 있다.
this
의 값을 지정해주는 방법이다.
한번 해 보자.
var obj = {
amothod: function () {
console.log('amothod', this);
var self = this;
function func4 () {
console.log('func4', self);
}
func4();
}
}
obj.amothod();
var obj2 = {
amothod: function () {
var self = this;
setTimeout(function() {
console.log('obj2', this);
}, 1000)
}
}
obj2.amothod();
엇 ? 메소드에서의 내부함수에서는 전역객체를 바라보진 않지만 콜백함수에서는 this
가 전역객체를 바라본다.
여기서 콜백함수에서는 this
값을 지정 해주는 것이 무시 되는것을 알 수 있다.
콜백함수의 전역객체를 바인딩 되는 것을 어떻게 막을 수 있을 까 ?
ES6에서부터 함수 내부에서
this
가 전역객체를 바라보는 문제를 보완한다고this
를 바인딩하지 않는 화살표 함수를 새로 도입했다고 한다.
ES5 환경에서는 사용하지 못한다고 한다.
( ES5환경에서는 막을 수 있는 방법인 뒤에 나올 call, apply, bind 메소드를 사용하지 않고 ES6에서는 간결하고 편리한 화살표함수를 사용한다고 한다. )
👉🏻 화살표 함수는 스코프체인상 제일 가까운 this
에 접근한다.❗️
그럼 화살표 함수를 사용해 보자 !
var obj = {
amothod: function () {
console.log('amothod', this);
var func4 = () => {
console.log('func4', this);
}
func4();
}
}
obj.amothod();
var obj2 = {
amothod: function () {
setTimeout(() => {
console.log('obj2', this);
}, 1000)
}
}
obj2.amothod();
this
가 전역객체를 바라보지 않는 것을 볼 수 있다.
일반함수는 호출위치에 따라
this
정의
화살표함수는 자신이 선언된 함수 범위에서this
정의 ( 즉, 상위 스코프의this
를 가르킨다. )
const user = {
name: 'yang',
normal: function() {
console.log(this.name); // yang
},
arrow: () => {
console.log(this.name); // undefind
}
}
user.normal();
user.arrow();
첫번째 콘솔은 yang, 두번째 콘솔에는 undefind가 나온다.
❓ 왜 why
👉🏻 익명함수인 normal라는 메소드 안에 함수를 선언했고 그 안에서 호출을 했기 때문에 user라는 객체 안에서 name을 찾게 되지만,
arrow는 자신이 선언되어있는 함수범위에서 this를 찾기 때문에 undefind가 뜬것이다.
( 즉, 화살표함수는 스코프와도 연관이 있다. 결론은 화살표함수에서의 this는 상위스코프를 가르킨다. )
또 다른 예제를 보자.
이 경우에는 어떻게 출력이 될까 ?
const timer = {
name: 'yang',
timeout: function () {
setTimeout(function() {
console.log(this.name);
}, 2000);
}
}
timer.timeout()
const timer2 = {
name: 'yang',
timeout: function () {
setTimeout(() => {
console.log(this.name);
}, 4000)
}
}
timer2.timeout()
첫번째 timer는 undefind가 뜨고, 두번째 thimer2는 상위스코프인 yang이라는 것이 뜬다.
앞서, 전역공간에서의 this
에서 설명 했듯이 자바스크립트에선 객체의 프로퍼티에 함수를 할당한다고 했다.
❗️객체 지향 프로그래밍
객체를 사용하여 개체를 표현하는 방식을 객체 지향 프로그래밍(object-oriented programming, OOP) 이라 부릅니다.
user = {
name: yang,
age: 30,
pr: function() {
console.log('안녕하세요');
}
}
user.name();
user라는 객체를 만들었고 그 안에 name, age를 메서드로 사용할 수 있고, 함수를 만들고 그 함수를 메서드로도 사용 할 수 있다.
user(객체:능력).name(메소드:행동)();
메서드(행동)는 this로 객체(능력)를 참조한다.
동일하지만 단축한 코드이다. function은 생략이 된다.
user = {
greet: function() {
alert("Hello");
}
};
// 단축 구문
user = {
greet() {
alert("Hello");
}
};
function을 생략해도 메서드를 정의할 수 있다.
자 그럼 객체에 접근하기 위해 메서드를 사용해보자.
var user = {
name: yang,
age: 30,
pr: function() {
console.log(`안녕하세요', ${user.name} 입니다.`);
}
}
newuser.name();
그렇다면 호출되는 값은 안녕하세요, yang 입니다.
가 잘 나올 것이다.
흠, 그럼 이런경우는 값이 과연 어떻게 나올까 ?
var user = {
name: yang,
age: 30,
pr: function() {
console.log(`안녕하세요', ${user.name} 입니다.`);
}
}
var newuser = user;
user = null;
newuser.name();
과연 console.log
는 무엇을 보여줄까 ?
user
를 newuser
로 다른 하나의 변수를 이용해 객체를 생성 해주었고, user
를 null
로 덮어 씌었고, 호출했다.
그랬더니 에러가 발생한다. 😭
( 이 부분이 이해가 되지 않는다면 데이터할당, 불변값, 가변값 등을 공부하고 와야한다. 그럼 그 뒤의 생성자 호출도 이해가 될 것이다. 비슷한 부분이 엮여있는 맥락이기 때문에 ... )
해결방법은 콘솔의 user.name
을 this.name
로 바꿔주면 된다.
여기서 객체자리에 this를 사용하는 이유를 알 수 있다.
this를 사용하지 않고 외부 변수를 참조해 객체에 접근하는 것도 가능하지만 ! 이러한 경우때문에,user.name
대신this.name
을 사용하는 이유이다.
메서드 내부에 this를 이용해 객체에 접근할 수 있다.
또 다른 예시.
이벤트 핸들러안에서의 this는 무엇인가 ?
var btn = document.querySelector('#btn')
btn.addEventListener('click', function () {
console.log(this);
});
출력되는 값은 btn
이 된다.
👉🏻 메서드명의 점(.) 앞부분이 곧 this가된다.
앞 예제들을 이해하고 여기까지 내려왔다면, 함수로서, 메서드로서의 호출방법이 다르다는 것을 눈치를 챘을 것이다.
/* 함수로서 호출 */
var func = function(x) {
console.log(this, x);
};
func(1); // Window
/* 메서드로서 호출 */
var obj = {
method: func
};
obj.method(2); // {method: f} 2
함수 앞에 점(.) 이 있는지 여부만으로 간단하게 구분할 수 있다.
메서드로서 호출 : 점 표기법, 대괄호 표기법
var obj = {
method: function(x) {
console.log(this, x);
}
inner: {
methodAA: function() {
console.log(this);
}
}
};
obj.method(1); // {method: f, inner{...} } 1
obj['method'](2); // {method: f, inner{...} } 2
obj.inner.methodAA(); // {methodAA: f}
obj.inner['methodAA'](); // {methodAA: f}
obj['inner'].methodAA(); // {methodAA: f}
obj['inner']['methodAA'](); // {methodAA: f}
이 예시를 보면 차이점을 한 번에 알 수 있다.
대괄호 표기법이든 점 표기법이든 결국, 어떤 함수를 호출할 때 그 함수이름(프로퍼티) 앞에 객체가 명시 되어있는 경우에는 메서드로 호출한 것을...
( 그 객체가 this가 된다는 점 )
자바스크립트에서의 생성자 함수 는 무엇인가 ?
생성한다. 객체를 생성한다. 새로운 객체를 생성한다.
예를 들어 설명해 보자면, 기본샘플종이서류가(생성자 객체) 있다. 그런데 새로운 이름을 판 도장(새로운 객체안의 내용 new)으로 서류종이에 찍는다. 그럼 새로 판 도장으로 찍은 서류종이(new 생성자함수)라고 설명을 할 것 같다.
사용하는 방법은
기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다.
변수를 만들어 new 연산자를 사용해 생성자 함수를 만드는데, 변수의 이름을 인스턴스라고 한다.
코드의 예시를 들어보자.
var Dog = function(name, age) {
this.breed = 'Shih Tzu';
this.name = name;
this.age = age;
};
var choco = new Dog('초코', 5);
var nabi = new Dog('나비', 9);
console.log(choco, nabi); //choco, nabi를 인스턴스라 부른다.
출력값은
하핳.. ! 보이는 것 처럼 새로운 객체를 생성했다.
그렇다면 new를 붙이지 않는다면 어떤 결과를 보여줄까 ?
var Dog = function(name, age) {
this.breed = 'Shih Tzu';
this.name = name;
this.age = age;
};
var choco = Dog('초코', 5);
var nabi = new Dog('나비', 9);
console.log(choco, nabi);
결국 '초코'는 undefind의 값을 출력해 버렸다.
❓ 왜 why
일반함수와, 생성자 함수는 함수 호출 시 this
를 바인딩하는 방식이 다르기 때문이다.
일반함수는 호출을 할 시 this
는 전역객체에 바인딩 되지만, new 연산자를 사용한 생성자 함수를 호출하면은 this
는 생성자 함수가 암묵적으로 생성한 빈 객체에 바인딩된다.
❗️주의할 점은
객체 생성 목적으로 작성한 생성자 함수를 new 없이 호출하거나 일반함수에 new를 붙여 호출하면 오류가 발생한다. 이 점을 주의하자 !
❗️개발자들의 서로 암묵적으로 생성자함수를 알아볼 수 있도록 규칙이 있는데 첫번째 알파벳을 대문자를 사용한다고 한다.
// 객체 리터럴 방식
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);
이 둘의 차이점은
프로토타입 객체( Prototype )에 있다.
이 부분은 나중에 정리가 되면 올려보겠다.
객체 리터럴 방식의 경우, 생성된 객체의 프로토타입 객체는 Object.prototype이다.
생성자 함수 방식의 경우, 생성된 객체의 프로토타입 객체는 Person.prototype이다.
객체 리터럴은 Object.prototype 이지만, 생성자 함수 방식은 생성자 함수의 이름이 지정되어있는 Person.prototype이라는 것이므로 어느 점이 다른지 대충 눈치를 챘다.
함수를 즉시실행 하도록 하기 위해 메서드를 이용해서
this
를 명시해주고 바인딩을 해줄 수 있는 방법이다.
즉,인자(arg)
를this
로 만들어 주는 기능이다.
/* call */
Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
/* apply */
Function.prototype.apply(thisArg, [argsArray])
call
과 apply
는 기능적으로는 완전 동일한 역할을 한다.
첫 번째 인자를 this
를 넣어주어 바인딩한다.
둘의 차이점은
call
메서드는 두번째 인자를 매개변수로 지정한다.
반면에, apply
메서드는 두 번째 인자를 배열로 받는다는 점이다.
apply
는 두 번째 인자를 배열로 받고 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다.
/* call */
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,
mothod: 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
/* apply */
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,
mothod: function(x, y) {
console.log(this.a, x, y);
}
}
obj.method.apply({ a: 4}, [5, 6]}; // 4 5 6
call
은 매개변수로, apply
는 배열로 받는 걸 볼 수 가 있다.
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', 4: 'd', length: 4 }
var arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]
여기서 잠깐❗️
객체에는 배열 메서드를 직접 사용할 수 없다.
하지만, 키가 0 또는 양의 정수인 프로퍼티가 존재하고 length
프로퍼티의 값이 0 또는 양의 정수인 객체, 즉 배열의 구조와 유사한 객체의 경우를 유사배열객체라 한다. 그러므로 call, apply메서드를 사용할 수 있다.
👉🏻
slice
메서드
위에 예제에서 원래는 시작인덱스값과 마지막 인덱스값을 받아 시작값부터 마지막값의 앞부분까지의 배열 요소를 추출하는 메서드이다. ( 오직 배열 형태로복사
하기 위해 사용 )
하지만, 매개변수를 아무것도 넘기지 않을 경우에는 그냥 원본 배열의 얕은 복사본을 반환한다.
결국은call
메서드를 이용해obj
를 얕은 복사를 한것인데slice
메서드가 배열 메서드이기 때문에 복사본은 배열로 반환하게 된것이다.
문자열에서의 배열 메서드는 결론을 먼저 이야기하자면,
결국 에러가 뜨거나 모두 제대로된 출력값을 얻지 못할 것이다.
문자열의 경우는 프로퍼티가 읽기전용이기에 원본 문자열에 변경을 가 하는 메서드는 에러를 던지며, concat
처럼 대상이 반드시 배열이여야 하는 경우는 에러가 나지 않지만 제대로 된 결과를 얻을 수없다.
( 메서드는 push
, pop
, shift
, unshift
, splice
등.. 이 있다. )
이 코드를 하나씩 콘솔에 찍어보면 확인이 된다.
var start = 'hello,';
Array.prototype.push.call(start, ', javascript');
Array.prototype.concat.call(start, ', javascript');
Array.prototype.every.call(start, function(char) {
return char !== '';
});
Array.prototype.some.call(start, function(char) {
return char !== '';
});
var mapmethod = Array.prototype.map.call(start, function(char) {
return char + '!';
});
console.log(mapmethod);
var reducemapmethod = Array.prototype.reduce.apply(start, [
function(string, char, i) {
return string + char + i;
}, ''
])
console.log(reducemapmethod);
ES6에 추가 된 from
유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 메서드이다.
var name = 'hello, yang';
var arr = Array.from(name);
console.log(arr);
생성자 내부에 공통된 내용이 있을 경우
call
,apply
메서드를 이용하면 간결한 코드로 반복을 줄일 수 가 있다.
function AnimalType(name, age) {
this.name = name;
this.age = age;
};
function Dog(name, age, type) {
AnimalType.call(this, name, age);
this.type = type;
}
function Cat(name, age, color) {
AnimalType.apply(this, [name, age]);
this.color = color;
}
var dogtype = new Dog('초코', 5, 'bulldog');
var cattype = new Cat('나비', 9, 'white');
console.log(dogtype, cattype);
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);
var bindFunc1 = func.bind({ x:1 });
bindFunc1(5, 6, 7, 8);
var bindFunc2 = func.bind({ x:1 }, 4, 5);
bindFUnc2(6, 7);
bindFUnc2(8, 9);
첫 번째 func
는 전역에 콘솔이 찍힐 것이다.
bindFunc1
을 호출 했을 때는 func
에 this
를 { x:1 }
로 새로운 함수가 담기며, 호출한 5, 6, 7, 8
이 호출될 것이다.
그럼 bindFunc2
로
첫번째 호출했을 때는 { x:1 }, 4, 5
로 새로운 함수가 담길 것이고, { x:1 }, 4, 5, 6, 7
두번째 호출 값은 { x:1 }, 4, 5, 8, 9
가 호출 되는 것을 볼 수 가 있다.
/* call */
var objCall = {
outer: function() {
console.log(this);
var innerFunc = function() {
console.log(this);
}
innerFunc.call(this);
}
}
objCall.outer();
/* bind */
var objBind = {
outer: function() {
console.log(this);
var innerFunc = function() {
console.log(this);
}.bind(this);
innerFunc()
}
}
objBind.outer();
/* 내부함수에 this 전달 */
var objThis = {
logThis: functionn() {
console.log(this);
},
logThisLater1: function() {
setTimeout(this.logThis, 500);
},
logThisLater2: function() {
setTimeout(this.logThis.bind(this), 1000);
}
}
objThis.logThisLater1(); // window
objThis.logThisLater2(); // objThis { logThis: f, ... }
잠시 위쪽에 우회법으로 화살표함수를 소개했었다.
ES5에서는 call
, apply
, bind
를 적용하였지만, ES6에서 부턴 이런경우 화살표함수를 사용하므로 적용할 필요가 없어 더욱 간결해졌다.
/* call, bind */
var objCallback = {
outer: function() {
console.log(this);
var innerFunc = () => {
console.log(this);
}
innerFunc();
}
}
objCallback.outer();
/* 내부함수에 this 전달*/
var objThis = {
logThis: function() {
console.log(this);
},
logThisLater: function() {
setTimeout(() => {
console.log(this);
}, 1500);
}
}
objThis();
Array.prototype.메서드(callback[, thisArg]);
Array.prototype.from(arrayLike[, callback[, thisArg]]);
Set.prototype.forEach(callback[, thisArg]);
Map.prototype.forEach(callback[, thisArg]);
forEach
, map
, filter
, some
, every
, find
, findIndex
, flatMap
, from
, Set
, Map
이 부분을 이해한다면 잠깐의 번외 편에서 일반함수 vs 화살표함수 예시를 이해했다는 것😘
const cat = {
name: 'hodu',
sayHi: () => console.log(`Hi ${this.name}`)
};
cat.sayHi(); // Hi undefind
메소드를 호출한 객체를 가르키지 않고 상위 컨택스트인 전역객체 window
를 가르킨다.
❗️ 즉, 화살표함수로 메소드를 정의하는 것은 좋지 않다.
const cat = {
name: 'hodu',
};
Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);
cat.sayHi() // Hi undefind
1번과 동일한 이유이다.
❗️ 일반함수로 정의하여 매핑하는것이 일반적이다.
const Fun = () => {};
/* 화살표함수는 prototype 프로퍼티가 없다. */
console.log(Fun.hassOwnProperty('prototype')); // false
const fun = new Fun(); // TypeError: Fun is not a constructor
화살표함수는 생성자 함수로 사용할 수 없다.
❓이유는
1. 생성자 함수는 prototype
프로퍼티를 가진다.
2. prototype
프로퍼티가 가르키는 프로토타입 객체의 constructor
를 사용한다.
3. ㅠㅠ그런데 화살표함수는 prototype
프로퍼티를 가지고 있지 않기 때문에 생성자 함수로 화살표함수를 이용할 수 없다.
const button = document.getElementById('button');
button.addEventListener('click', () => {
console.log(this === window); // true
the.innerHTML = 'Clicked button';
});
addEventListener
함수의 콜백 함수를 화살표 함수로 정의하면this
가 상위 컨택스트인 전역 객체window
를 가리킵니다.
const button = document.getElementById('button');
button.addEventListener('click', function() {
console.log(this === button); // true
the.innerHTML = 'Clicked button';
});
그러므로 addEventListener
함수의 콜백함수 내에서 this
를 사용하는 경우,
function
키워드로 정의한 일반함수를 사용해야한다.
일반 함수로 정의된
addEventListener
함수의 콜백 함수 내부의this
는 이벤트 리스너에 바인딩된 요소(currentTarget)를 가리킵니다.
( 즉, 저button
이this
라는 것 )
이 정도면 this
를 어느정도 이해 했다고 생각한다.
물론, 공부하다 보면 알고 있어도 잠시 잊어버릴 수도 있지만, 기본적인 기초는 머리에 넣고 이해 했으니 되었다.
그리고 화살표함수를 왜 사용하는지를 알게되었다.
또 !
this
를 공부하면서 class
와 prototype
과도 연관이 이어져있다는 것을 알게 되었다.
class는 .. 객체지향이다.
공부할 게 아직 많고 멀고 멀었다..