스터디 5회차 주간 공부 내용 - JS this, 실행 컨텍스트

잔잔바리디자이너·2022년 4월 2일
0

Study

목록 보기
5/19

!잠깐.. 다시 prototype 개념으로 넘어 가면

왜 literal로 생성된 string은 instanceof String 에서 false를 뱉을까...?

instanceof 의 문제점?

const literalString = '123'
undefined
const builtInString = new String(123)
undefined
typeof literalString
'string'
typeof builtInString
'object'
(builtInString instanceof String)
true
(literalString instanceof String)
false
Object.getPrototypeOf(literalString)
// String {'', constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …}
literalString.__proto__
// String {'', constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …}
literalString.constructor
// ƒ String() { [native code] }

instanceof는 객체일때 정확한 평가를 내린다? 따라서 instanceof에 값의 타입 판단을 의존하는것은 문제를 일으킬 수 있다.

As you can see both typeof and instanceof are insufficient to test whether a value is a boolean, a number or a string - typeof only works for primitive booleans, numbers and strings; and instanceof doesn't work for primitive booleans, numbers and strings.

객체가 아닌 값 = 즉 원시 값 = 다시말해 생성자 함수로 생성되지 않은 값들에 대해서는 false를 뱉을것이다.
간단한 해결법으로는, primitive 값의 타입을 구분할때는 typeof를 사용하고, reference type의 값의 타입을 구분할 때는 instanceof 연산자를 사용한다.

constructor와 non-constructor의 구분

모든 함수는 내부 메서드 [[call]] 또는 [[construct]]를 갖는다.
[[construct]] 를 갖는 함수를 constructor 함수, 즉 생성자 함수로서 호출할 수 있는 함수를 의미한다.

constructor: 함수 선언문, 함수 표현식, 클래스
non-constructor: 메서드, 화살표 함수
Fortunately there's a simple solution to this problem. The default implementation of toString (i.e. as it's natively defined on Object.prototype.toString) returns the internal [[Class]] property of both primitive values and objects:

function classOf(value) {
    return Object.prototype.toString.call(value);
}

call 함수 알아보기

암묵적 전역도 같이 살펴보기


This

왜필요해?

객체 내부의 메서드는 자신이 속한 객체의 상태를 참조하고 변경할 수 있다. 이때 자신이 속한 객체의 프로퍼티를 참조하려면 그 이전에 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 한다. 혹은 인스턴스 객체에 프로퍼티, 메서드를 할당하기 위해서 객체를 참조할 수 있어야 하기 때문에 this 키워드를 사용한다.

객체 리터럴 방식으로 생성한 객체

객체 리터럴 방식으로 생성한 객체의 경우 메서드 내부에서 자신이 속한 객체를 가리키는 식별자를 재귀적으로 참조할 수 있다.

//객체 리터럴은 circle 변수에 할당되기 직전에 평가된다.
const circle = {
  radius: 5,
  getDiameter(){
    return circle.radius * 2
  }
}
//즉 getDiameter 메서드 호출 시점에 
//이미 객체 리터럴의 평가가 완료 되어 객체가 생성되고 circle 식별자에 할당되었기 때문.
console.log(circle.getDiameter());

결국 코드의 위치에 따라 영향을 미칠것이다. 따라서 재귀적 참조 방식은 바람직하지 않다.

//ReferenceError: circle is not defined
console.log(circle.getDiameter());
const circle = {
  radius: 5,
  getDiameter(){
    return circle.radius * 2
  }
}

생성자 함수로 생성하는 객체의 경우에는?

리터럴 방식 생성과는 다르게, 생성자 함수 내부에서 메서드, 프로퍼티 등을 추가하기 위해서는 자신이 생성할 인스턴스를 참조할 수 있어야 한다. 생성자 함수로 생성하는 객체는 먼저 함수를 정의하고 new 연산자와 함께 생성자 함수를 호출하는 단계까지 필요하다. 즉 함수를 정의하는 시점에는 아직 미래에 생성할 인스턴스를 가리키는 식별자를 모른다.

이를 위해서 자바스크립티는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 특수한 식별자 this를 제공한다. this는 코드 어디서든 참조할 수 있지만, this가 가리키는 값 즉 ❗️this바인딩은 함수 호출 방식에 따라 동적으로 결정된다.

아까 객체 리터럴로 객체를 생성한 코드를 수정해본다면
=>

const circle = {
  radius: 5,
  getDiameter(){
    return this.radius * 2
  }
}
console.log(circle.getDiameter())
const circle = {
  radius: 5
}

const circle2 = {
  radius: 10
}

const getDiameter = function getDiameter(){
    return this.radius * 2
}
circle.getDiameter = getDiameter;
circle2.getDiameter = getDiameter;

// 이처럼 this는 호출 방식에 따라 바인딩 되는 값이 다르다.
// 메서드로 호출되었기 때문에 .노테이션 앞의 객체(메서드를 호출한 객체)에 this값이 바인딩 됨.
console.log(circle.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

생성자 함수에서의 this 바인딩 예시를 본다면 생성자 함수 내부의 this는 생성자 함수가 생성할 (미래의) 인스턴스를 가리킨다.

const Circle = function(radius){
  this.radius = radius,
  Circle.prototype.getDiameter = function(){
    return this.radius * 2;
  }
}

const circle = new Circle(5);
circle.getDiameter();

함수 호출 방식과 this 바인딩

함수 호출 방식에 따른 this 값의 동적 결정 4가지 경우를 본다면

// 1. 전역에서 this 참조
console.log(this); 
// window

// 2. 일반 함수 내부의 this
function foo(){
  console.log(this);
}
foo();
// window
// 일반함수 내부에서 this는 사용할 필요가 없다. 
// strict mode에서는 undefined가 바인딩된다.

// 3. 메서드 내부의 this
const person = {
	name: 'kim',
  getName(){
    console.log(this)
    // 메서드를 호출한 객체 person
    return this.name
  }
}
console.log(person.getName())

// 4. 생성자 함수 내부의 this
function Person(name){
  this.name = name;
  console.log(this); // 생성자 함수가 생성한 instance
  this.getName = function(){
    return this.name
  }
}
const person1 = new Person('lee')
console.log(person1.getName())

const person2 = new Person('park')
console.log(person2.getName())
/*
Person {
  name: 'lee',
  __proto__: { constructor: ƒ Person() }
}
Person {
  name: 'park',
  __proto__: { constructor: ƒ Person() }
}
'lee'
'park'*/

생성자 함수로 호출되길 기대한 함수를 일반 함수로 호출 한다면?

const Circle = function(radius){
  this.radius = radius,
  this.getDiameter = function(){
    return this.radius * 2;
  }
}

const circle = new Circle(5);
circle.getDiameter()

// 일반 함수로 호출함
const circle2 = Circle(10)
console.log(circle2) // undefined
radius // 10
getDiameter() // 20

🤔this를 명시적으로 바인딩 하기. 왜??

어떤 함수든 일반 함수로 호출하면 내부 this에는 결국 전역객체가 바인딩 된다.
즉, 메서드 안의 중첩 함수나 콜백 함수 등이 일반 함수로 호출 되었을 때 전역 객체가 바인딩 된다는 것이고, 결국 this 값이 일치하지 않아 동작을 어렵게 만든다.

var value = 100;
const obj ={
  value: 20,
  foo(){
    console.log(this); // obj
    console.log(this.value); // 20
    function bar(){
      console.log(this); // window
      console.log(this.value); // 100
    }
    bar();
  }
}

obj.foo(); 

해결 방안?

  • this 바인딩을 변수에 할당하여 중첩 함수나 콜백 함수 내부에서 그 변수를 참조하게 한다.
  • Function.prototype.apply/call/bind 등의 메서드를 사용한다.
  • 화살표 함수를 사용하여 this 바인딩을 일치시킨다.

apply/call/bind

bind 메서드는 메서드의 this와 메서드 내부의 중첩 함수 또는 콜백함수의 this가 불일치하는 문제를 해결하기 위해 유용하게 사용된다.

const person = {
  name: 'kim',
  foo(callback){
    callback();
  }
}
const callback = function(){
  console.log(`Hi, I'm ${this.name}.`)
}
person.foo(callback); // "Hi, I'm ."

이렇게 아래처럼. 근데 나는 처음에 callback.bind(this); 로 작성했다가 에엥; 왜 안되지? 왜 안돼??? 이러고 있었음; bind는 함수를 호출하지 않는다. 명시적으로 호출해야한다.

const person = {
  name: 'kim',
  foo(callback){
    callback.bind(this)();
  }
}
const callback = function(){
  console.log(`Hi, I'm ${this.name}.`)
}
person.foo(callback); // "Hi, I'm kim."

다른 예시

const person = {
	name: 'kim',
  getName: function(){
    return this.name
  }
}
const unBound = person.getName;
console.log(unBound()); // ''
const bounded = person.getName.bind(person)
console.log(bounded()); // 'kim'

실행 컨텍스트

소스코드의 타입

소스코드는 4가지 타입으로 구분된다. 자바스크립트 엔진은 소스코드를 평가하여 "실행 컨텍스트"를 생성하는데, 소스코드의 타입에 따라 실행 컨텍스트를 생성하는 과정과 관리 내용이 다르다.

  • 전역 코드
  • 함수 코드
  • eval 코드
  • 모듈 코드

소스코드의 평가와 실행

실행 컨텍스트의 역할

코드가 실행되려면 스코프, 식별자, 코드 실행 순서 등의 관리가 필요한데 이 모든것을 관리하는 것이 실행 컨텍스트이다. 모든 코드는 실행 컨텍스트로 실행되고 관리된다. 두가지로 쪼개 보자면,
식별자, 스코프: 실행 컨텍스트의 렉시컬 환경으로 관리.
코드 실행 순서: 실행 컨텍스트의 스택으로 관리.

실행 컨텍스트 스택

생성된 실행 컨텍스트는 스택 자료구조로 관리된다. 이를 실행 컨텍스트 스택이라고 부름. 코드가 실행되는 시간의 흐름에 따라 실행 컨텍스트 스택에는 실행 컨텍스트가 추가되고 제거된다.

렉시컬 환경

식별자, 스코프: 실행 컨텍스트의 렉시컬 환경으로 관리.
렉시컬 환경은 실행 컨텍스트를 구성하는 컴포넌트인데 식별자와 식별자에 바인딩된 값을 관리하고 스코프 체인을 통해 상위 스코프에 대한 참조를 가능하게 하는 자료 구조다.

실행 컨텍스트의 생성과 식별자 검색 과정

var x = 1;
const y = 2;
function foo(a){
	var x = 3;
	const y = 4;

	function bar(b){
    const z = 5;
    console.log(a+b+x+y+z);
    }
    bar(10);
  }
foo(20);

1. 전역 객체 생성

  • 전역 객체도 프로토타입 체인의 일원. Object.prototype을 상속 받음.

2. 전역 코드 평가

  1. 전역 실행 컨텍스트 생성

    • 전역 실행 컨텍스트를 생성하여 실행 컨텍스트 스택에 푸시한다.
  2. 전역 렉시컬 환경 생성

    • 전역 렉시컬 환경을 생성하고 전역 실행 컨텍스트에 바인딩한다.
      1. 전역 환경 레코드 생성
      2. this바인딩
      3. 외부 렉시컬 환경에 대한 참조 결정

3. 전역 코드 실행

  • 전역 코드가 순차적으로 실행된다. 변수 할당문이 실행된다. 함수가 호출된다.

    변수 할당문 또는 함수 호출문을 실행하려면 식별자를 먼저 확인 해야하는데 이때, 어느 스코프의 식별자를 참조할지 식별자 결정을 해야하는데, 이 과정을 위해 실행 중인 실행 컨텍스트에서 식별자를 검색하기 시작한다.

4. 함수 코드 평가

  1. 함수 실행 컨텍스트 생성
    • 함수 실행 컨텍스트는 함수 렉시컬 환경이 완성된 다음 실행 컨텍스트 스택에 푸시된다. 즉 최상위 실행 컨텍스트가 된다.
  2. 함수 렉시컬 환경 생성
    1. 함수 환경 레코드 생성
    2. this 바인딩
    3. 외부 렉시컬 환경에 대한 참조 결정: 해당 함수의 정의가 평가된 시점에 실행 중이던 실행 컨텍스트의 렉시컬 환경 참조가 할당된다. 함수 정의를 평가할때 이 정보는 함수 객체 내부 슬롯 [[Environment]]에 저장해둔다. 이는 클로저 개념과 밀접한 관계가 있음.

5. 함수 코드 실행

6. 중첩 함수 코드 평가

7. 중첩 함수 코드 실행

  • console 식별자 검색
    스코프 체인에서 식별자를 검색할 때는 항상 현재 실행 중인 실행 컨텍스트의 렉시컬 환경에서 시작하여 외부 렉시컬 환경에 대한 참조, 검색으로 진행된다.

8. 중첩 함수 코드 실행 종료

9. 외부 함수 코드 실행 종료

10. 전역 코드 실행 종료

클로저

모듈패턴

0개의 댓글