[JS] this 호출

colki·2021년 2월 27일
2

자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작한다.

하지만 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로 메서드로 고정되는 것이 아니라 객체의 메서드로 호출할 때만 메서드로 동작하고, 그렇지 않으면 그냥 함수일 뿐이다.
그래서 이를 생각하지 않고 로직을 짰을 때 원하지 않는 결과를 받을 수 있다.

💡 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

이렇게 애매한 관계들 속에서 this는 함수 안에서 맥락에 따라 사용되는 가변적인 변수로써 함수와 객체간의 관계를 정립해주는 역할을 한다.

즉, 메서드라고 해서 무조건 객체가 고정되어 있는 것은 아니기 때문에
함수를 호출할 때 this가 가리키는 객체가 무엇인지 명시해주어야 한다.

💡 this 에는 호출한 주체에 대한 정보가 담긴다

함수만 선언해놓은 것만 보고는 this가 가리키는 값을 정확히 알 수 없다.
함수의 선언이나 할당은 this 값과 무관하다.

먼저 this가 들어있는 함수가 어떻게 호출되고 실행되는지찾아야 한다.
실행되는 부분을 찾아서 실행하는 “주체”가 무엇인가를 파악하는 게 중요하다.
실행 방식에 따라 this 함수실행방식을 정리해봤다.


  • Regular Function Call (일반함수)
  • Object Method Call (메서드의 객체)
  • Constructor('new'keyword)
  • apply && call
  • bind



🙋‍ Regular Function Call (일반함수)

전역객체는 자바스크립트 런타임 환경에 따라 각각 다른 이름을 가지고 있는데,
브라우저 환경에서 전역객체는 window이고, Node.js환경에서의 전역객체는 global이다.
그래서 window객체를 global객체라고 부른다.

Window는 브라우저를 가리키는 최상위 객체로, 모든 전역 객체와 전역 함수는 Window 객체의 프로퍼티와 메서드에 해당한다.

함수가 명시된 객체에 소속되어 있지 않을때, 함수 내부의 this는 전역객체인 window 와 같다.
( Non Strict Mode 한정 )

다만 생략될 수 있어서 우리 눈에 보이지는 않지만, 모든 변수와 객체와 함수들은 Window 안에서 선언되고 실행되고 있다.

(예제1)

function func() {
    if(window === this){
        document.write("window === this");
    }
}
func(); 

*평범하게 함수이름만을 이용해서 실행하고 있다.*

사실 window.func이지만, 최상위 객체인 window가 생략되어 보이지 않았을 뿐이다. 함수내에서의 this는 func 이라는 이름의 메서드가 속해있는 객체(window)를 가리킨다.


(예제2)
var name = 'colki';

function foo() {
  var name = 'hany';
  sayName(); // 😁 여기가 중요 😁
}

function sayName() {
  console.log(this.name);
}

foo();

이때 this 는 무엇일까?

foo()는 단순히 sayName함수를 실행시켜주는 다리역할을 할 뿐 this와는 상관이 없다.

this가 들어있는 sayName함수가 실행되는 부분이 제일 중요하다!

this가 들어있는 함수실행문이 어디에 들어있건 말건 상관없다. *

sayName()형태로 그저 평범하게 호출했으니 일반함수실행인 것이다.

그러면? 글로벌객체 window를 가리키게 된다.


🔔 STRICT MODE 에서는 this의 값은 글로벌객체가 아닌 undefined 이다.

'use strict';

let name = 'colki';

function sayName() {
  console.log(this.name);
}

sayName();

'use strict'에서는 this는 window가 아닌 undifined를 가리키기 때문에,
undefined.name 이게 되므로 에러가 난다.

사실상 현업에서는 this로 window를 가리키는 객체로 쓰지 않기 때문에 strict mode에서
this가 undifined로 나오는 에러를 발견한다면, 버그 찾기에 훨씬 수월해져서 실수를 바로잡기에 좋다.



🙋‍ Object Method Call ( Dot Notation)

먼저 일반적으로 객체 안에서의 this는 객체를 가리킨다.

객체를 집이라고 하고 프로퍼티를 토끼라고 했을 때,
집에서 토끼야~ 하면 집토끼인거 다들 알지만
산에서 토끼야~ 하고 부르면,
그게 집토끼를 부르는 건지 산토끼를 부르는 건지 지구상에 존재하는 모든 토끼를 부르는 것인지 알 수가 없다.
그래서 집.토끼야~ 하고 명시해주는 거라 생각하면 된다.

즉, 객체의 프로퍼티에 함수를 할당해서(메서드) 호출한 경우에 this는 객체를 가리킨다. 다시 말해 객체.메서드의 형태일 때 this는 마지막.앞의 객체를 가리킨다.

너 거기 집.토끼! 라고 부르는 거다.

🔅 메서드 호출 표기법

var obj = {
   foo: function (a) { console.log(a);
 	inner: {
     bar: function () {}
   }
 };

① 점 표기법
obj.foo();

obj.inner.bar()

② 대괄호 표기법
메서드가 string 타입일 때 유용하다!

obj['foo'];,
obj['inner'].bar(),
obj.inner.['bar'](),
obj['inner']['bar']()


🔅 함수 작성법

내가 헷갈렸던 부분이 있는데 객체 안에 함수(메서드로 존재할 때)가 표기되는 방식이다. 바로 함수선언문, 함수표현식 또는 함수기명식이다.
이미 알고 있던 개념이었지만 그놈이 이놈인지 기억해내지 못했다..빈약했던 기본기때문에 흔들렸던 것.......😳

함수 정의방식에는 3가지 방법이 있다.

  • 함수선언문 foo() { }
  • 함수표현식(=익명함수표현식) const foo = function () { }
  • 기명 함수표현식 const bar = function foo() { }

여기서 함수선언문과 함수표현식은 실행방식에 차이가 있을 뿐 똑같은 아이다.
단, 메서드 안에서는 =가 :로 대체될 뿐이다.

🔹 write

const obj = {name:'colki', age: 20, name:'colki'};

console.log(obj); //  {name: "colki", age: 20}
// 같은 하늘 아래 colki가 두 명일 수 없듯이..(인덱스가 달라도 다 앎)



const myName = {
  foo: function () {
  console.log('colki');
  },

  foo() {
  console.log('colki');
  },
  
  bar: function foo() {
  console.log('colki');
  }
}

console.log(myName); // {foo: ƒ, bar: ƒ}
// 함수선언문과 함수표현식은 같은 놈, 기명함수표현식은 딴 놈

🔹 execute

const food = {
  name: 'hotdog',
  drinkName: 'juice',
  eatFood() {
    console.log(`eat ${this.name}`);
  },
  drinkJuice: function () {
    console.log(`drink ${this.drinkName}`);
  },
  together: function haveTogether() {
    console.log(`have ${this.name}, ${this.drinkName}`);
  }
};

food.eatFood(); // eat hotdog
food.drinkJuice(); // drink juice
food.together(); // have hotdog, juice
// 셋 다 똑같이 '.프로퍼티()' 로 호출한다...


함수 이름 앞에 객체가 명시되어 있으면 메서드로 호출한 것이고 (Object Method Call), 아니라면 모두 일반함수로 호출한 것이다. (Regular Function Call)

(예제1)
let context = {
    func : function () {
        if (o === this) {
            document.write("o === this");
        }
    }
}
  
context.func();   

//context === this

1번 예제의 객체가 window였다면, 여기서는 context가 상위 객체인 것이다.
여기서의 func메서드의 객체는 context이므로 context.func 으로 표기할 수 있고
this는 객체인 context를 가리킨다.

1, 2 의 함수실행방법에 따른 this가 의미하는 객체에 대해 이해했다면
아래 예제를 풀 수 있을 것이다.


예제2. (1)(2)를 실행했을 때 콘솔에 출력될 각각의 결과는?

var age = 100;

const person = {
  age: 35,
  foo: function () {
    console.log(this.age);
  }
};

const func = person.foo;

// (1) Dot Notation
person.foo();

// (2) Regular Function Call
func(); 


🙋‍ Constructor ('new' keyword)

생성자란, 여러개의 동일한 프로퍼티를 가지는 객체를 생성할 때 사용되는 함수다.
객체지향 언어에서는 생성자를 Class라고 하며, Class를 통해 생성한 객체를 Instance 라고 한다. 프로그래밍적으로 생성자는 구체적인 인스턴스를 만들기 위한 틀이라고 생각하면 된다.

자바스크립트의 내장 명령어인 키워드 new를 붙여 함수를 호출하면 해당 함수가 생성자로 동작하게 되고, 내부에서의 this는 새로 만들어지는 인스턴스 자신이 된다.

쉽게 생각해서 생성자는 일종의 사용자가 만드는 함수라고 보면 된다.
객체 리터럴 문법으로 구현되며, 하나 하나 객체를 만드는 것보다 가볍고 가독성있게 객체를 구성하는 방법이다. (생성자함수명은 일반함수와 구분될 수 있게 대문자로 시작하고 명사형이다.)

예제1)
  
function Candy(name,color) {
  this.name = name;
  this.color = color;
}

const candyObj = new Candy('plum','violet')


console.log(candyObj); // Candy {name: "plum", color: "violet"} 
console.log(candyObj.name) // plum
console.log(candyObj.color) // violet

생성자함수에서의 this.속성(또는 메서드) 이렇게 써있는 부분이 처음에 잘 와닿지 않았었다. 이해하기 쉽게 직관적으로 풀이하자면, 생성자함수가 실행될 때
this에게 속성(메서드라면 함수)을 넣어준다.
생성자함수 입장에서는 본인이지만, 나중에 자식들이 엄마껄 지들 것처럼 편히 사용하라고 this를 넣어주는 거다.
this. == 인스턴스.


다음은 위 예제에서 빈객체가 만들어지고 객체가 할당되는 과정이다.

🔅 new를 붙인 생성자함수를 이용해서 속이 빈 this 객체가 만들어지고,
그 this 객체에 프로퍼티이름과 값을 할당할 수 있다.

위 예제의 프로세스를 간단히 정리하면

  • new키워드+ 함수 => 생성자 함수 생성
  • this는 {}(빈객체)로 먼저 만들어진다.
  • 여기에 this.name/ this.colcor로 값을 할당받아서
  • {name: "plum", color: "violet"} 하는 새로운 객체가 만들어지는 것이다.

예제를 하나 더 들어서 클래스, 인스턴스 용어로 생성자함수에 대해 설명해보려고 한다. 과일이라는 큰 범주에 속하는 클래스 안에 하나하나의 과일을 인스턴스로 만들려고 한다.
예제2)
  
var Fruit = function (name, color) {
  this.name = name;
  this.color = color;
};

var banana = new Fruit('바나나', 'yellow'); //banana 인스턴스 가리킴
var strawberry = new Fruit('딸기', 'red'); //strawberry 인스턴스 가리킴

console.log(banana, strawberry);  
//Fruit {name: "바나나", color: "yellow"}, Fruit {name: "딸기", color: "red"}
  • Fruit 이라는 변수에 익명함수를 할당.
  • 내부에서 this를 사용해서 name, color 프로퍼티에 접근.
  • new키워드 + Fruit 함수 호출해서 생성자함수를 변수 banana, strawberry에 할당.
  • 콘솔에서 Fruit{ } =>클래스 Fruit과 인스턴스 { } 객체가 출력된다.

🔅 또한 return문이 없이도 출력할 수있는 특성을 가지고 있다. 다음을 보자.

function Friend() {
  this.name = 'haru';
  console.log(this);
}

위의 일반함수에서는 return문이 없기때문에 undefined가 출력된다.
하지만 아래처럼 키워드 new 를 사용하여 생성자함수를 추가한다면


function Friend() {
  this.name = 'haru';
  console.log(this);
        
  //return this가 생략된 것이나 마찬가지.
}
  
const haru = new Friend();
console.log(haru); // {name: "haru"}

return문이 없어도 생성자함수의 특성상 this를 리턴해낼 수 있으므로
haru라는 변수에 this가 가리키는 객체값{name: "haru"}을 담을 수가 있다.
생성자함수로 만드는 경우 return은 안쓴다고 생각하면 된다.


🔅 만약 return문이 존재하는데, this값이 아닌 다른 객체가 주어진다면?


function Friend() {
  this.name = 'haru';
  console.log(this);
  return {name: 'luho'};
}
  
const haru = new Friend();
console.log(haru); // 

return문이 생략된거나 마찬가지니까 우선순위가 더 높은거겠지?
그러니까 답은 똑같이 {name: "haru"} !



땡!
정답은 {name: "luho"} 이다.

생성자함수 내에서 return문에 대한 규칙은 다음과 같다.

return + 객체 or 원시형

  • Reference(객체) => this 대신 객체가 반환
  • Primitive(원시형) => return문 무시됨.


apply, call, bind는 부모가 되는 Function.prototype 객체의 메서드로써
모-든 함수는 apply, call, bind 라는 프로퍼티를 호출할 수 있다.
메서드는 객체라는 주인(master)에게 종속된 일종의 노예(slave)라고 할 수 있다.

일반적으로 함수를 호출하는 방식으로는 함수이름(); 방법이 있지만
함수이름.메서드() 이름뒤에 메서드를 붙여서 함수를 호출할 수 있다.

🙋‍ .apply( ) && .call( )

두 메서드는 첫번째 인자를 this값으로 설정하며, .앞의 함수를 실행시킨다 (함수를 호출한다).

예제로 switch문을 가져왔다. switch문은 ( )안의 표현식을 평가하여 그 값과 일치하는 case문을 수행하고, break문을 만나면 코드블럭에서 탈출한다.

let o = {}
let p = {}

function func() {
  switch (this) {
    case o:
    console.log('my');
    break;
  
    case p:
    console.log('colki');
    break;
  
    case window:
    console.log('window');
    break;          
  }
}
  
func(); // window       ---1
func.call(o); // my      --2
func.apply(p); // colki  --3

1. func() 객체 = window
func() 는 일반함수의 형태를 가지고 있기 때문에 (this)는 this 최상위객체인 window를 가리키고 (window) 표현식과 일치하는 case window:가 수행된다.
그리고 break문을 통해 코드블럭을 탈출하면서 콘솔창에는 window가 출력된다.

2. func.call(o) 객체 = o
func함수는 객체이므로 프로퍼티 call 메서드를 노예처럼 부릴 수 있기에
func.call()로 함수를 호출한다. 이때 첫번째 인자인 o를 객체로 인식하여 fucn함수를
실행하게 되면서 ( )안의 this의 값은 인자 o라는 객체가 되고, 이와 일치하는
case o:가 수행되어 콘솔창에는 my 가 출력된다.
즉, call 메서드는 첫 번째 인자로 받은 값을 this로 설정하여 함수를 실행합니다.

function foo(a, b, c) {
  console.log(this.name); // hany
  console.log(a*b*c); //24
}

let colki = {
  name: 'hany'
};

foo.call(colki, 2, 3, 4);

위와 같은 경우에는 첫번째 인자 colki를 this로 설정하고 2, 3, 4를
foo함수의 매개변수로 받는다.

3. func.apply(p) 객체 = p
2번과 마찬가지로 this에 첫번째 인자인 p값이 객체로 들어가므로, 이와 조건이 일치하는
case p:가 수행되고 콘솔창에는 colki가 출력된다.

=> 이를 통해서 같은 함수라 하더라도 호출 방법에 따라 func이라는
함수는 window, o, p등 각 객체 소속의 메소드가 된 걸 알 수 있다.

이렇게 this로 설정하고 싶은 값을 call, aplly의 전달인자로 넣어서 호출하면 된다.

노예였던 메서드가 객체지향 프로그래밍 언어인 자바스크립트에서는
어깨를 활짝 펴고 주인노릇을 할 수 있게 되었지만(메서드인 함수가 객체가 되기도 하니까) 또 자신을 누가 부르냐에 따라서 다시 노예가 되어 호출되기도 한다는 것이다.
말하고 있는 나도 매우 헷갈리지만 JS를 배울수록 조금씩 알아가고 있는 걸 느낀다.


🔅 call과 apply 차이점

call과 apply는 첫번째 인자값을 this로 설정하고 함수를 호출하는 방식 등 기능적으로 완전히 같지만,
this 값 다음에 오는 두번째 인자를 추가할 경우 인자를 입력하는 방식만 차이 있다.

상황에 따라 원하는 메서드를 사용하면 되는데, 가령 객체 안의 값을 정확히 모르는 동적인 경우에는 call보다는 apply을 이용해서 배열형태로 넣는 것이 더 유용할 것이다.

call ( this로 설정할 값, a, b, c, d, e ...)
apply ( this로 설정할 값, [ a, b, c, d, e ... ] )

a 값은 객체 곧 this가 되고, 두번째 b 값들은 이 객체의 인자가 된다.

  • call 메서드는 b자리에 인수들을 개수 제한없이 넣어줄 수 있다.
    다만 첫번째 인자 a는 this값으로 설정되며, 나머지는 모두 인자로 들어간다.
    => call( a, 1,2,3,4 )


  • apply 메서드의 경우 b자리에는 한 개의 배열 형태로 넣어줘야 한다.
    하지만 함수안에서 배열통째로 쓰이는게 아니라, 안에 있는 요소들만 쓰인다.

    => apply(a, [1, 2, 3, 4])


첫번째 인자값으로 null 또는 undefined를 주는 경우도 있다.
this는 메소드에 의해 보이는 실제값이 아닐 수 있음을 주의:
메소드가 비엄격 모드 코드 내 함수인 경우, null 및 undefined는 전역 객체로 대체되고
원시값은 객체로 변환된다.

foo.call(null, 1, 2, 3);
foo.apply(undefined, [1, 2, 3]);

MDN apply, call


🔅 응용1: push.apply()

apply의 특성을 이용해서 다음 예제와 같은 기능을 수행할 수도 있다.
push를 사용하면 배열안에 배열형태로 [... ,[ ], ...]넣게 되는 결과가 생기지만, apply를 사용하면 배열안에 복수의 인자들만 넣을 수 있다.

var array = ['a', 'b'];
var elements = [0, 1, 2];
  
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

🔅 응용2: 유사배열객체에 배열 메서드 사용하는 방법

유사배열객체는 형태만 배열과 비슷할 뿐, 배열이 아니기 때문에 배열매서드를사용할 수 없다.
하지만 call과 apply 메서드는 유사배열객체를 받을 수 있기 때문에, 배열메서드와 함께 사용하면 유사배열객체를 배열로 바꾸는 일종의 꼼수를 부릴 수 있다.
(단, 문자열의 경우에는 length프로퍼니가 읽기 전용이기 때문에 에러가 나올 수 있다.)

let obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};

const array = Array.prototype.slice.call(obj);  
console.log(array); // ["a", "b", "c"]

obj 라는 유사배열객체에 배열메서드를 바로 갖다 쓰는게 아니라 call을 바인딩해서 사용하면
배열메서드를 사용할 수 있다. slice가 배열 매서드이기 때문에 리턴값 역시 배열이 나온다.


사실 this는 이렇게 사용하라고 만들어진 기능은 아니기 때문에 핵심적이 사용법은 아니다.
다이렉트로 Array.from()메서드를 사용해서 쉽고 직관적인 방법으로 바꿀 수 있다.

let obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};

const array = Array.from(obj);
console.log(array);

🔅 응용2-1: arguments에 배열 메서드 사용하는 방법

함수내에서 접근할 수 있는 전달인자 arguments도 유사배열객체이므로
call과 apply를 바인딩해서 배열매서드와 함께 쓸 수 있다. 다음 예제를 보자.


function foo() {
  var argArray = Array.prototype.slice.call(arguments);
  console.log(argArray); 
 // [1, 2, 3]
 //slice에 시작인덱스와 끝인덱스를 넣지 않으면 전체배열을 그대로 복사한배열을 리턴한다.


  argArray.forEach(function (element) {
    console.log(element); // 1, 2, 3
  });
}

foo(1, 2, 3);


🙋‍ bind( )

bind() 메서드는 특이하게 함수를 바로 실행시키는 것이 아니라,
원본함수를 복제한 새로운 함수를 리턴만 하는 메서드이다.
리턴된 함수를 실행해야 비로소 원본 함수가 실행된다!

그래서 보통 메서드뒤에 ()실행문을 붙이거나, 변수에 할당하고 그 변수를 실행시켜야
원본 함수가 실행된다.
이렇듯 bind는 리턴하는 함수에 this값을 미리 바인딩해서 함수를 복제해놓는 것과
부분적으로 함수를 적용하는 기능들을 가진 메서드이다.

bind는 call메서드와 인자값 설정하는 부분이 동일한데,
첫 번째 인자로는 this로 설정할 값, 두 번째 인자는 갯수제한 없이 값을 받을 수 있다.


function foo(a, b, c) {
  console.log(this.age);
  console.log(a + b + c);
}

const colki = {
  age: 20
};

const bar = foo.bind(colki, 1); // 부분적용함수 구현

bar(2, 3); // 35 , 6 

bar에 할당한 함수를 그저 bar() 방식으로 실행하는 것 뿐만 아니라
인자를 추가로 전달해서도 실행할 수 있는데,
foo.bind(colki,1) 에서 this값은 colki로, 1은 a로 지정해두었다.
여기까지는 부분적용함수를 구현한 것으로 볼 수 있다.

추가로 bar(2, 3)에서 2는 b, 3은 c에 매칭된다.

😗 또한, bar(2,3);구문만 보면 일반함수실행 처럼 보이지만
이전에 foo.bind(colki,1)에서 this값을 바인딩을 먼저 했으므로, 일반함수 실행으로 window를 가리키는 것이 아닌 바인딩해둔 this값을 객체로 가리키게 된다.

그래서 일반함수로 호출했는데 window 도 아니고 undefined도 가리키지 않는다면 거의
bind된 내역이 있을 확률이 높다.




Reference: 생활코딩, 코어자바스크립트, javascript.info

profile
매일 성장하는 프론트엔드 개발자

0개의 댓글