TIL_220620_코어 자바 스크립트_this

설탕유령·2022년 6월 20일
0

2-4 this

실행 컨텍스트에는 this로 지정된 객체가 저장 됨
실행 컨텍스트 활성화 당시에 this가 지정되지 않은 경우 전역 객체가 저장 됨
그 밖에는 함수를 호출하는 방법에 따라 this에 저장되는 대상이 다름

2-5 정리

실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체

  • 실행 컨텍스트는 전역 공간에서 자동으로 생성되는 전역 컨텍스트와 eval 및 함수 실행에 의한 컨텍스트 등이 있음
  • 실행 컨텍스트 객체는 활성화되는 시점에 VariableEnvironment, LexicalEnviroment, ThisBinding의 세 가지 정보를 수집함

실행 컨텍스트를 생성할 때는 VariableEnvironment와 LexicalEnviroment가 동일한 내용으로 구성 됨

  • LexicalEnviroment는 함수 실행 도중에 변경되는 사항이 즉시 반영 됨
  • VariableEnviroment는 초기 상태를 유지
  • 두 환경은 매개변수명, 변수의 식별자, 선언한 함수의 함수명 등을 수집하는 enviromentRecord와 바로 직전 컨텍스트의 LexicalEnviroment 정보를 참조하는 OuterEnviromentReference로 구성됨

호이스팅은 코드 해석을 좀 더 수월하게 하기 위해 enviromentRecord의 수집 과정을 추상화한 개념으로, 실행 컨텍스트가 관여하는 코드 집단의 최상단으로 이들을 '끌어올린다'고 해석하는 것

  • 변수의 선언과 값 할당이 동시에 이뤄진 문장은 '선언부' 만을 호이스팅함
  • 할당 과정은 원래 자리에 남는데 함수 선언문과 함수 표현식의 차이가 발생함

스코프는 변수의 유효 범위를 말함

  • outerEnviromentReference는 해당 함수가 선언 된 위치의 LexicalEnviroment를 참조
  • 코드 상으로 변수에 접근 시, 현재 컨텍스트의 LexicalEnviroment를 탐색해 발견되면 그 값을 반환
  • 발견되지 못한 경우 다시 outerEnviromentReference에 담긴 LexicalEnviroment를 탐색하는 과정을 거침
  • 전역 컨텍스트의 LexicalEnviroment까지 탐색해도 해당 변수를 찾지 못하면 undefined를 반환

전역 선텍스트의 LexicalEnviroment에 담긴 변수를 전역변수라 함

  • 그 밖에 함수에 의해 생성된 실행 컨텍스트의 변수들은 모두 지역변수
  • 가급적 전역 변수의 사용은 최소화 하는 것이 좋음

this에는 실행 컨텍스트를 활성화하는 당시에 지정된 this가 저장

  • 함수를 호출하는 방법에 따라 그 값이 달라짐
  • 지정되지 않은 경우에는 전역 객체가 저장됨

3 this

대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미
자바스트립트에서의 this는 어디서든 사용할 수 있음

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 둘을 구분 할 수 있는 유일한 기능

3-1 상황에 따라 달라지는 this

자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정됨

  • 실행 컨텍스트는 함수를 호출할 때 생성
    즉 this는 함수를 호출할 때 결정됨
  • 함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지는 것

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

전역 공간에서 this는 전역 객체를 가리침

  • 개념상 전역 컨텍스트를 생성하는 주체가 전역 객체이기 때문
  • 전역 객체는 자바스크립트 런타임 환경에 따라 다른 이름과 정보를 가짐
  • 브라우저 환경에서는 window, node.js 환경에서는 global

자바스크립트 엔진은 전역변수를 선언 시 전역객체의 프로퍼티로도 할당함

  • 자바스크립트의 모든 변수는 특정 객체의 프로퍼티로 동작하기 때문
  • 특정 객체란 실행 컨텍스트의 LexicalEnviroment(이하 L.E)
  • 실행 컨텍스트는 변수를 수집해 L.E의 프로퍼티로 저장
  • 이후 변수를 호출 시 L.E를 조회해 일치하는 프로퍼티가 있을 경우 그 값을 반환
  • 전역 컨텍스트의 경우 L.E는 전역객체를 그대로 참조함

'전역 변수를 선언하면 자동으로 전역객체의 프로퍼티로도 할당한다'는 사실 틀린 말임

  • 정확히는 '전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다'
  • var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 대부분은 결과적으로는 동일하게 동작함
  • 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우도 존재하며 '삭제 명령'이 해당됨

var a와 window.b 형태로 선언된 변수가 있을 때, delete a의 결과는 false, delete b의 결과는 true임

  • 사용자가 의도치 않게 삭제를 방지하는 차원에서 마련한 방어 전략
  • 전연변수 선언 시 자바스크립트 엔진이 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의함
  • var로 선언한 전역변수와 전역객체의 프로퍼티를 호이스팅 여부 및 configurable 여부에서 차이를 보임

3-1-2 메서드로서 호출할 때 그 메서드 내부에서의 this

함수를 실행하는 방법중 가장 일반적인 두 가지는 함수로서 호출하는 경우와 메서드로서 호출하는 경우
함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치로, 이 둘을 구분하는 유일한 차이는 독립성에 있음

  • 함수는 그 자체로 독립적인 기능을 수행함
  • 메서드는 자신이 호출한 대상 객체에 관한 동작을 수행함
  • 자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현함

자바스크립트를 처음 접하는 경우 흔히 메서드를 '객체의 프로퍼티로 할당 된 함수'로 이해함

  • 반은 맞고 반은 틀림
  • 어떤 함수를 객체의 프로퍼티로 할당한다고 그 자체로 메서드가 되는게 아님
  • 객체의 메서드로서 호출한 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작

'함수로서의 호출'과 '메서드로서 호출'은 함수 앞에 점(.)의 여부로 구분 가능
대괄호 표기법도 동일하며, 함수 호출시 함수 이름앞에 객체가 명시된 경우 메서드로 호출 한 것임

[메서드 내부에서의 this]

this에는 호출한 주체에 대한 정보가 담김
어떤 함수를 메서드로서 호출하는 경우 호출 주체는 바로 함수명(프로퍼티명) 앞의 객체임

3-1-3 함수로서 호출할 때 그 함수 내부에서의 this

[함수 내부에서의 this]

함수를 함수로서 호출할 경우에는 this가 지정되지 않음

  • this에는 호출한 주체에 대한 정보가 담김
  • 함수로서 호출하는 것은 호출 주체(객체)를 명시하지 않고 개발자가 코드에 직접 관여한 것이기 때문
  • 함수로서 호출하는 것은 호출 주체의 정보를 알 수 없음
  • this가 지정되지 않은 경우 this는 전역 객체를 바라봄
  • 즉 함수에서의 this는 전역 객체를 가리킴
  • 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적함

[메서드 내부함수에서의 this]

this 바인딩에 관해서 함수를 실행하는 당시의 주변 환경(메서드 내부, 함수 내부인지 등)은 중요하지 않고, 오직 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 관건

[메서드 내부 함수에서의 this를 우회하는 방법]

ES5 까지는 자체적으로 내부 함수에 this를 상속할 방법이 없지만, 우회는 가능

  • 대표적인 방법은 바로 변수를 활용하는 것
  • 객체 내부에 function을 선언 한 뒤, var self = this의 형태로 속성을 변수에 저장 후 호출 시 self에는 객체가 출력 됨

[this를 바인딩하지 않는 함수]
ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자 this를 바인딩 당하지 않는 화살표 함수를 도입함

  • 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용 할 수 있음

  • 그 밖에서 call, apply 등의 메서드를 활용해 함수를 호출 할 때 명시적으로 this를 지정하는 방법이 있음

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

함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 함

  • 이때 함수 A는 함수 B의 내부 로직에 따라 실행 됨
  • this 역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정 됨
  • 콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조함
  • 다만 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조함

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

생성자 함수는 어떤 공통된 성질을 지나는 객체들을 생성하는데 사용하는 함수

  • 객체지향 언어에서는 생성자를 클래스(class), 클래스를 통해 만든 객체를 인스턴스(instance)라고 함

자바스크립트는 함수에 생성자로서의 역할을 함께 부여함

  • new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작함
  • 어떤 함수가 생성자 함수로서 호출 된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 됨

생성자 함수를 호출(new 명령어와 함께 함수를 호출) 하면 생성자의 prototype 프로퍼티를 참조하는 proto라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여

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

상황별로 this에 어떤 값이 바인딩 되는지 살펴봤지만, 이러한 규칙을 깨고 this에 별도의 대상을 바인딩 하는 방법도 있음

3-2-1 call 메서드

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령

  • call 메서드의 첫 번째 인자를 this로 바인딩
  • 이후의 인자들을 호출할 함수의 매개변수로 함
  • 함수를 그냥 실행하면 this는 전역개체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정 할 수 있음
  • 메서드에 대해서도 마찬가지로 객체의 메서드를 그냥 호출하면 객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정 할 수 있음

3-2-2 apply 메서드

apply 메서드는 call 메서드와 기능적으로 동일

  • call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정
  • apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정

3-2-3 call / apply 메서드의 활용

객체에는 배열 메서드를 직접 적용 할 수 없음

  • 그러나 키가 0 또는 양의 정수인 프로퍼티가 존재하고, length 프로퍼티의 값이 0 또는 양의 정수인 객체 즉 배열의 구조와 유사한 객체의 경우(유사배열객체) call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있음
  • 함수 내부에서 접근할 수 있는 arguments 객체도 유사배열객체이므로 배열로 전환해 활용 가능
  • querySelectoraAll, getElementsByClassName 등의 Node 선택자로 선택한 결과인 NodeList도 마찬가지

유사 배열객체에는 call/apply 메서드를 이용해 모든 배열 메서드를 적용할 수 있음

  • 배열처럼 인덱스와 length 프로퍼티를 지니는 문자열에 대해서도 마찬가지
  • 단, 문자열의 경우 length 프로퍼티가 읽기 전용이기 때문에 원본 문자열에 변경을 가하는 메서드는 에러를 던짐
  • concat처럼 대상이 반드시 배열이야하는 경우에는 에러는 나지 않지만, 제대로된 결과를 얻을 수 없음

[여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용]

여러 개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을 때 apply 메서드를 사용하면 좋음

  • var numbers = [10, 20, 3, 16, 45]
    Math.max.apply(null, numbers)

ES6에서는 펼치기 연산자를 이용하면 apply를 적용하는 것보다 더욱 간단하게 작성 가능

  • Math.max.(...numbers)

call/apply 메서드는 명시적으로 별도의 this를 바인딩 하면서 함수 또는 메서드를 실행하는 좋은 방법이지만 오히려 이로 인해 this를 예측하기 어렵게 만들어 코드 해석을 방해함
그럼에도 ES5 이하의 환경에서는 마땅한 대안이 없기 때문에 실무에서 광범위 하게 사용됨

3-2-4 bind 메서드

ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드

  • 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록됨
  • 즉, bind 메서드는 함수에 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지님

[name 프로퍼티]

bind 메서드를 적용해 새로 만든 함수는 name 프로퍼티에 동사 bind의 수동태인 'bound'라는 접두어가 붙는 성질이 있음

  • 어떤 함수의 name 프로퍼티가 'bound xxx'라면 이는 곧 함수명이 xxx인 원본 함수에 bind 메서드를 적용한 새로운 함수라는 의미
  • 이는 기존에 call이나 apply 보다 코드를 추적하기에 더 수월함

[상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기]

메서드의 내부 함수에서 메서드의 this를 그대로 바라보게 하귀 위한 방법으로 self 등의 변수를 활용한 우회법(3-1-3 참조)을 소개했는데, call, apply 또는 bind 메서드를 이용하면 더 깔끔하게 처리 가능

또한 콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this에 관여하는 함수 또는 메서드에 대해서도 bind 메서드를 이용하면 this 값을 바꿀 수 있음

3-2-5 화살표 함수의 예외사항

ES6에 새롭게 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩 하는 과정에 제외됨

  • 즉 이 함수 내부에는 this가 아예 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근

3-2-6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정할 수 있는 경우가 있음

  • 이러한 메서드의 thisArg 값을 지정하면 콜백 함수 내부에서 this 값을 원하는 대로 변경 가능
  • 이런 형태는 여러 내부 요소에 대해 같은 동작을 반복 수행해야하는 배열 메서드에 많이 포진되 있음
  • 같은 이유로 ES6에 새로 등장한 Set, Map 등의 메서드에도 일부 존재함
  • 대표적으로 forEach에 예가 존재함

3-3 정리

다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립함

  • 전역공간에서의 this는 전역객체를 참조
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명의 앞에 객체)를 참조
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조(메서드의 내부함수에서도 같음)
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역객체를 참조
  • 생성자 함수에서의 this는 생성될 인스턴스를 참조

명시적 this 바인딩의 경우, 위 규칙의 부합하지 않는 경우에는 다음 내용을 바탕으로 this를 예측 가능

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듬
  • 요소를 순화하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자를 this로 받기도 함

질문

2개의 별도의 callback 함수를 정의하고
1번째 호출 안에서 2번째 callback 함수를 호출 시
2번쨰 callback 호출 이후에 this로 표현되는건 1번째 호출의 callback일까 2번째 호출의 callback일까?

정답은 1번째 호출에 this가 2번째와도 연동되어 결과적으로 1번째 호출의 this임
[소스코드]
function introduce (lastName, firstName, callback) {
var fullName = lastName + firstName;
console.log('introduce : ',this.name)
this.name = "introduce"
console.log('introduce after set: ',this.name)
callback(fullName);
console.log('')
}
function introduceTwo (lastName, firstName, callback) {
var fullName = lastName + firstName;
console.log('introduce Two: ',this.name)
this.name = "introduce Two"
console.log('introduce Two after set: ',this.name)
callback(fullName);
console.log('')
}

introduce("홍", "길동", function(name) {
console.log(name);
console.log('introduce callback: ',this.name)
this.name = "introduce callback"
console.log('introduce callback after set: ',this.name)
console.log('')
introduceTwo("길동", "홍", function(name) {
console.log(name);
console.log('introduce callback two: ',this.name)
this.name = "introduce callback two"
console.log('introduce callback two after set: ',this.name)
console.log('')
});
});

[결과]
introduce : undefined
introduce after set: introduce
홍길동
introduce callback: introduce
introduce callback after set: introduce callback

introduce Two: introduce callback
introduce Two after set: introduce Two
길동홍
introduce callback two: introduce Two
introduce callback two after set: introduce callback two

Process finished with exit code 0

profile
달콤살벌

0개의 댓글