JS(Section 12. 스코프와 바인딩)

짜스의 하루 ·2024년 4월 26일

렉시컬과 클로저

I. 렉시컬(정적) 스코프 lexical(static) scope : 함수가 정의된 위치에 따라 변수의 유효 범위가 결정되는 스코프 규칙이다. 이것은 코드가 작성된 위치에 기반하여 스코프가 정적으로 결정되는 것을 의미한다.

--> 렉시컬 스코프의 핵심 아이디어는 함수가 어디에서 호출되는지가 아니라 어디에서 정의되는지에 따라 변수에 대한 접근 권한이 결정된다는 것이다.

여기서 inner 함수는 outer 함수 내에서 정의되었다. 따라서 inner 함수는 outer 함수의 스코프에 접근할 수 있다. 함수 inner가 호출될 때, outerVar 변수에 접근하여 그 값을 출력할 수 있다.

  • 전역 범위에서 x, y, z 변수가 각각 1로 초기화된다.
  • func1 함수 내에서 새로운 y, z 변수가 각각 2로 초기화된다.
  • func2 함수 내에서도 새로운 z 변수가 3으로 초기화된다.
  • console.log 문은 함수 호출과 함께 실행된다. 각각의 출력에는 해당 시점에서의 x, y, z 변수의 값이 포함되게 된다.

II. 렉시컬 환경 lexical environment

  • 전체 문서, 함수, 블록을 실행하기 전 만들어지는 내부 객체
  • 각 스코프의 고유 값들과 외부 스코프에 대한 참조를 포함
    --> 변수가 선언된 위치에 따라 유효 범위와 식별자 결정이 이루어지는 방식을 의미한다.

  • 함수가 호출될 때마다 해당 함수의 렉시컬 환경이 생성되며, 각 함수의 범위에 따라 변수의 값이 달라지는 것을 확인할 수 있다.
  • 전역 범위에서 x, y, z 변수가 각각 1로 초기화된다.
  • func1 함수가 호출될 때, a 매개변수의 값이 1로 전달된다. 또한 func1 함수 내부에서 새로운 y, z 변수가 각각 2로 초기화된다
  • func2 함수가 호출될 때, a + 1 값이 b 매개변수로 전달된다. 또한 func2 함수 내부에서 새로운 z 변수가 3으로 초기화된다.
  • 각 함수에서 console.log 문이 실행되며, 해당 시점에서의 x, y, z, a, b 변수의 값이 출력된다.

위의 그림과 같이 func2()에서 없을 경우, func1()를 찾아보고, func1()에도 없을 경우, 전역 렉시컬 환경에서도 찾아보면서 타고타고 내려가게 된다.

III. 클로저 closure
내부 함수 --> 외부 함수의 값에 접근할 수 있다는 개념(함수 중첩시)

func1 함수 내부에 정의된 func2 함수는 외부 함수인 func1의 변수인 word를 참조하고 있다. 이때 func2 함수가 func1 함수 외부에서 호출되더라도 word 변수에 접근할 수 있다.

  • func1 함수가 호출되면서 func2 함수가 반환된다. 이때 func1 함수 내부의 환경이 클로저로서 유지된다.
  • 반환된 func2 함수는 logHello 변수에 할당된다.
  • logHello 함수가 호출되면, 내부에 정의된 func2 함수가 실행된다.
    --> func2 함수 내에서 외부 변수인 word를 참조하여 그 값을 출력한다.
  • func2와 func2가 선언된 환경(func1의 스코프)의 조합 - 클로저

  • createCounter 함수는 외부에서 시작값(start)을 받아와서 내부에서 정의된 함수를 반환한다. 반환된 함수는 시작값을 기준으로 숫자를 증가시키고 그 값을 출력하는 역할을 한다.

  • createCounter 함수가 호출될 때 시작값 start가 전달된다. 이 시작값은 클로저 내부에서 유지될 변수이다.

  • createCounter 함수 내부에서는 익명 함수가 정의되고 반환된다. 이 익명 함수는 외부에서 count 변수에 할당된다.

  • 반환된 함수가 count 변수에 할당되면서, 이 함수는 클로저를 형성하게 된다.
    --> 클로저는 createCounter 함수가 종료된 이후에도 외부 변수인 start에 접근할 수 있다.
    --> count 함수가 호출될 때마다 시작값 start가 증가하고, 그 값을 출력한다.


this 의 동적 바인딩

I. ⭐️ this - "이곳의~"

  • 기본적으로 자신이 속한 곳을 가리킴 - 문맥 context
  • 💡 함수의 호출 방식에 따라 가리키는 바가 달라짐 --> 자바스크립트 특성

1 . 전역에서의 this

  • 전역 코드 내에서 this는 전역 객체인 window를 참조한다. 브라우저에서는 전역 객체가 window이며, Node.js에서는 global을 나타낸다.

2 . 함수 안에서의 this

  • 첫 번째 코드에서는 "use strict" 모드를 사용하지 않았으므로 기본적으로 비엄격 모드(non-strict mode)로 실행된다.
  • 이 경우 전역 함수 func가 호출될 때 this는 전역 객체를 참조하게 되어 globalThis (브라우저 환경에서는 window, Node.js 환경에서는 global)가 출력된다.
  • 두 번째 코드에서는 "use strict" 모드를 사용하여 엄격 모드(strict mode)로 실행된다.
    --> 엄격 모드에서는 전역 함수가 호출될 때 this가 자동으로 전역 객체를 참조하지 않는다. 따라서 func 함수 내부에서 this는 더 이상 전역 객체를 참조하지 않고 undefined가 된다. 이것이 출력 차이가 발생하는 이유다.

3 . 객체 안에서의 this
a. 객체 리터럴 - 해당 객체를 가리킴

  • getX 메서드 내에서 this.x는 메서드가 호출된 객체를 가리킨다. 따라서 obj.getX()를 호출하면 this는 obj 객체를 참조하게 되고, 따라서 x 속성인 123의 값을 반환한다.

b. 생성자 함수 - 생성될 인스턴스를 가리킴

  • Person 함수는 이름(name)과 나이(age)를 매개변수로 받아 객체를 생성한다. this를 사용하여 name과 age 속성을 설정하고, introduce 메서드를 정의한다.

  • 이후에는 new Person('이서연', 20).introduce()를 호출하여 Person 생성자 함수를 사용하여 객체를 생성하고, 해당 객체의 introduce 메서드를 호출하여 객체를 소개하는 문자열을 반환한다.

c . 클래스 선언 - 생성될 인스턴스를 가리킴

  • PyoChicken 클래스는 생성자 함수와 메서드를 포함하고 있다. 생성자 함수인 constructor에서는 객체의 속성인 name과 no를 초기화한다. introduce 메서드는 객체를 소개하는 문자열을 반환한다.
  • new PyoChicken('서초', 20).introduce()를 호출하여 PyoChicken 클래스를 사용하여 객체를 생성하고, 해당 객체의 introduce 메서드를 호출하여 객체를 소개하는 문자열을 반환한다.

II. ⭐ 동적 바인딩

  • 자바스크립트의 독특한 동작방식
  • this가 가리키는 대상이 함수의 호출 주체 또는 그 방식에 따라 달라짐

이와 같이 korean, italian을 명시해두고, korean을 출력해보자

makeStew이름의 함수를 살펴보면, isHot이 true일 경우, 매운, false일 경우, 순한을 출력하는 코드이다. 이후, korean.makeStew(true,2)를 출력하니, 매운김치찌개, 2냄비가 출력된 것을 확인할 수 있다.

찌개를 만드는 기술을 이탈리안 친구에게 전달하기 위해서
italian.makeStew = korean.makeStew 로 전달해보았다. 이후, italian을 출력해보니

김치찌개가 아닌, 피자찌개가 출력된 것을 확인할 수 있다.

const italian = {
    favorite : '피자'
    makeStew : function(isHot, pots) {
        return `${isHot? '매운' : '순한'}${this.favorite}찌개, ${pots}냄비`;
    }
};

여기서 this.favorite는 italian의 favvorite인 피자를 가리키고 있기 때문이다.
여기서, 김치찌개를 출력하고 싶을 때는 어떻게 해야할까?
--> call, apply bind 를 사용하면 된다!

call, apply, bind는 모두 함수를 호출할 때 this 값을 명시적으로 지정할 수 있는 메서드이다.
이들은 주로 함수의 컨텍스트를 변경하거나 다른 객체의 메서드를 호출할 때 사용된다.

1 . call를 사용한 함수 호출
call: 함수를 호출하면서 첫 번째 매개변수로 지정한 객체를 함수 내부에서 this로 사용한다.
그리고 추가적인 매개변수를 전달하여 함수를 호출할 수 있다.

2 . apply를 사용한 함수 호출
call과 유사하지만, 함수에 전달할 인수를 배열로 받는다. 이 배열의 각 요소가 함수 내부의 매개변수에 순서대로 매핑된다.

3. ⭐ bind를 사용한 this 대상 고정
함수를 호출하지는 않지만, 함수의 this 값을 영구적으로 바인한다.
원본 함수의 복사본을 만들어 반환하며, 이 복사본은 바인딩된 this 값을 가지게 된다. 추가 인수가 전달되면 해당 인수들이 원본 함수의 매개변수에 바인딩됩니다.

  • makeStew 함수를 호출할 때 this를 korean 객체로 바인딩하는 새로운 함수를 만든다. italian 객체의 makeRightStew 메서드를 만들고, 이 메서드를 호출할 때 this를 korean 객체로 설정한다.

  • 이처럼, true, 1와 같은 매개변수를 넘겨주어, 값을 고정할 수도 있다.

4 . 바인딩된 함수를 내보내는 함수

  • korean 객체의 teachMakingStew 메서드는 makeStew 메서드를 호출할 때 this를 korean 객체로 설정하는 새로운 함수를 반환하므로, this는 호출 시점에 korean 객체를 가리키게 된다.
  • italian 객체의 makeStew 메서드는 korean 객체의 makeStew 메서드와 동일한 동작을 수행하게 된다.
  • 즉, makeStew 메서드를 호출할 때 this는 korean 객체로 설정되며, korean 객체의 favorite 속성과 makeStew 메서드를 사용하여 요리를 설명하는 문자열을 반환한다.

5 . 생성자 함수일 경우 - 함수 자체를 미리 인스턴스에 바인딩하기

  • makeStew 메서드를 호출할 때 this를 현재 객체인 Korean으로 설정하도록 bind 메서드를 사용하여 바인딩을 고정시켰다.
  • italian.makeStew()를 호출해도, bind 메서드를 사용하여 this를 현재 객체인 Korean으로 설정하였기 때문에 this는 호출 시점에 Korean 객체를 가리키게 된다.

6 . call, apply, bind의 다른 활용
어느날 된장찌개가 먹고 싶다면?

  • call 메서드를 사용하면 함수를 호출할 때 this 값을 지정할 수 있다. 첫 번째 매개변수로 지정한 객체가 함수 내부에서 this로 사용되며, 이후에는 함수의 매개변수를 순서대로 전달하여 호출할 수 있다.

  • 따라서 위 코드의 출력은 korean 객체의 makeStew 메서드를 호출하되, this를 {favorite: '된장'} 객체로 설정하여 호출한 결과를 나타낸다. 이는 된장을 사용하여 매운 또는 순한 찌개를 만들고, 2냄비를 사용하는 결과를 반환한다.

  • intro 함수는 this를 이용하여 객체의 name과 age 속성을 참조하고, job 매개변수를 이용하여 해당 객체의 직업을 설명하는 문자열을 반환한다.

  • lee 객체는 name과 age 속성을 가지고 있으며, intro 함수를 lee 객체의 메서드로 추가한 후 호출하고 있다.

  • 이렇게 하면 lee 객체의 intro 메서드를 호출할 때 this는 lee 객체를 가리키게 되어 name과 age 속성을 참조할 수 있다.

  • 위 코드는 intro 함수를 호출하되, this를 명시적으로 lee 객체로 설정하여 호출한다. 또한, call과 apply 메서드를 사용하여 함수를 호출할 때 this 값을 지정하고, 추가적인 인자를 전달한다.

  • call: 함수를 호출할 때 첫 번째 매개변수로 지정한 객체가 함수 내부에서 this로 사용된다.

  • apply: call과 유사하지만, 추가적인 인자를 배열로 받아 함수의 매개변수에 전달한다.

따라서 위 코드에서는 intro 함수를 호출할 때 this를 lee 객체로 설정하여 호출하고 있다. 또한, call과 apply 메서드를 사용하여 job 매개변수를 전달하고 있다.

⭐ 배열 메서드의 thisArg

  • 콜백으로 주어진 함수 내에서 this가 가리킬 대상
  • 보통 콜백함수 다음 인자로 넣음

  • forEach 메서드로 배열 numbers를 순회하고 있으며, 각 요소에 대해 콜백 함수를 호출하고 있다. 이때 콜백 함수는 배열의 각 요소인 value를 받아들이고 있다.
  • 콜백 함수 내에서는 this.multiply(value) 를 호출하고 있다. 여기서 this는 forEach 메서드의 두 번째 매개변수로 전달된 obj 객체를 가리킨다. 즉, 콜백 함수 내에서 this.multiply(value)는 obj.multiply(value)와 동일한 역할을 수행한다.
  • forEach 메서드로 배열을 순회하면서 각 요소를 obj.multiply 함수에 전달하여 계산한 결과를 출력한다.
const car = {
  brand: 'Toyota',
  model: 'Corolla',
  getInfo: function () {
    return `${this.brand} ${this.model}`;
  }
};

const cars = [
  { brand: 'Ford', model: 'Focus' },
  { brand: 'Honda', model: 'Civic' },
  { brand: 'Chevrolet', model: 'Malibu' }
];

cars.forEach(function (carInfo) {
  console.log(this.getInfo.call(carInfo)); // 각 객체의 정보를 호출하여 출력
}, car);
  • forEach 메서드를 사용하여 cars 배열을 순회하면서 각 객체의 정보를 출력한다. 이때 thisArg로 car 객체를 전달하여, 콜백 함수 내에서 this를 car 객체로 설정한다.
  • 그리고 각 객체의 정보를 출력하기 위해 this.getInfo.call(carInfo)를 호출한다.
    --> 여기서 carInfo는 cars 배열의 각 요소이며, 콜백 함수의 첫 번째 매개변수로 전달된다.

다른 예시를 살펴보자

  • products 배열의 각 요소에 대해 새로운 객체를 생성하고, 각 객체에 idx라는 새로운 속성을 추가한다. 이를 위해 map 메서드를 사용한다.
  • map()으로 새로운 배열을 생성한 후, 해당 배열에서 사용자 성별과 사이즈에 맞는 상품들만을 필터링하여 새로운 배열을 생성한다. 이를 위해 filter 메서드를 사용한다.
  • 필터링 조건은 주어진 사용자의 성별과 사이즈에 맞는 상품들이다.
  • 필터링된 상품들에 대해 각각의 상품 번호를 출력한다. 이를 위해 forEach 메서드를 사용한다.
    위 코드에서 주목해야 할 점은 각 메서드에서 thisArg를 사용하여 메서드를 호출한 객체를 각각의 콜백 함수 내에서 this로 설정하고 있다는 점이다. 따라서 me 객체를 각각의 콜백 함수 내에서 this로 사용할 수 있게 된다.
  • 이러한 사용자 정보를 기반으로 recommendForYou 함수를 호출하면 해당 사용자에게 추천할 상품이 출력된다. 이때 사용자의 이름이 this.name으로 출력되며, 추천 상품의 번호가 순차적으로 출력된다.

this 의 정적 바인딩

: 정적 바인딩(static binding)은 실행 컨텍스트에서 this 키워드가 함수가 호출될 때가 아니라, 함수가 정의될 때 바인딩되는 것을 의미

일반적으로 자바스크립트에서 함수를 호출할 때 this의 값은 호출 방법에 따라 동적으로 결정된다. 하지만 화살표 함수의 경우 this는 정적으로 바인딩되어 함수가 정의될 때 외부의 컨텍스트에 의해 결정된다. 이것이 정적 바인딩이다.

객체의 메서드 종류별 비교

const obj = {
  // function 선언 함수
  func1: function () { return true; },

  // 메서드
  func2 () { return true; },

  // 화살표 함수
  func3: () => true
}

console.log(
  obj.func1(),
  obj.func2(),
  obj.func3()
);
  • func1: 함수 선언 방식으로 정의된 함수, 이 함수는 function 키워드를 사용하여 선언되었다.
  • func2: 메서드 축약 표현을 사용하여 정의된 함수, 이는 ES6에서 추가된 문법으로, 함수 이름을 생략하고 함수를 직접 정의할 수 있다.
  • func3: 화살표 함수로 정의된 함수, 화살표 함수는 function 키워드 대신 => 기호를 사용하여 정의

  • 함수를 각각 출력했을 때, 출력 결과가 조금씩 다른 것을 확인할 수 있다.

⭐ 화살표 함수와 this

  • function 함수나 메서드의 동적 바인딩과 다르게 동작
  • 함수가 어디서 선언되었는가에 따름 - ⭐️ 가장 근접한 상위 스코프에 바인딩 고정
    --> 즉 this를 정적으로 바인딩함

  • func1: 이 함수는 일반 함수 선언 방식을 사용하여 정의되었다 --> 따라서 this는 함수가 호출될 때 해당 함수를 호출한 객체를 가리킨다. 따라서 obj.func1()을 호출할 때 this는 obj 객체를 가리키게 된다.
  • func2: 이 함수는 메서드 축약 표현을 사용하여 정의되었다 --> 메서드 축약 표현은 함수의 이름을 생략하고 함수를 직접 정의하는 단축 문법이다. func1과 마찬가지로 이 함수도 호출될 때 this는 해당 함수를 호출한 객체를 가리킨다. 따라서 obj.func2()을 호출할 때 this는 obj 객체를 가리키게 된다.
  • func3: 이 함수는 화살표 함수로 정의되었다. 화살표 함수는 함수가 정의될 때의 외부 스코프의 this 값을 가져와서 사용하므로, 외부 스코프가 전역 객체인 경우에는 전역 객체를 가리키게 된다.
    --> 즉, func3는 obj 객체의 메서드가 아니라 전역 스코프에서 정의되었기 때문에 this는 전역 객체를 가리키게 된다.

  • regularFunction은 일반 함수로 정의되어 있으므로 호출 시 this는 obj 객체를 가리킨다.
  • 반면에 arrowFunction은 화살표 함수로 정의되어 있으므로 this는 정적으로 바인딩되어 외부 스코프의 this 값을 가져와서 전역 객체를 가리킨다.
    --> 따라서 obj.arrowFunction()을 호출할 때 this.name은 undefined가 된다.

  • makeStew 메서드는 일반적인 함수 선언 방식으로 정의되었으며, fryRice 메서드는 화살표 함수로 정의되어 있다.
  • 따라서 makeStew 메서드 내에서의 this는 호출 시 해당 메서드를 호출한 객체를 가리키고, fryRice 메서드 내에서의 this는 해당 함수가 정의된 시점의 외부 스코프의 this 값을 가져와서 사용한다.

  • makeStew 메서드는 일반적인 함수 선언 방식으로 정의되었으므로, 호출 시 해당 메서드를 호출한 객체를 가리키는 this를 가진다. 따라서 italian.makeStew(false)를 호출할 때 this는 italian 객체를 가리키게 된다.
  • fryRice 메서드는 화살표 함수로 정의되었으므로, 함수가 정의된 시점의 외부 스코프의 this 값을 가져와서 사용한다. 따라서 fryRice 메서드 내에서의 this는 korean을 가리키게 된다.

  • Korean 클래스의 makeStew 메서드는 일반적인 메서드 정의 방식으로 정의되었으며, 이 메서드가 호출될 때의 this는 해당 메서드를 호출한 객체인 korean을 가리킨다. 따라서 italian.makeStew(false)를 호출할 때 makeStew 메서드 내에서의 this는 korean 객체를 가리키므로, 피자찌개가 출력된다.

  • 반면에 fryRice 메서드는 화살표 함수로 정의되었으며, 화살표 함수는 정의된 시점의 외부 스코프의 this 값을 가져와서 사용하므로, fryRice 메서드 내에서의 this는 Italian 클래스를 생성한 객체인 korean을 가리키게 된다. 따라서 italian.fryRice(false)를 호출할 때 fryRice 메서드 내에서의 this는 korean 객체를 가리키므로, 김치볶음밥이 출력된다.

💡 call, apply, bind의 this 인자 무시됨
--> 화살표 함수를 사용하면 위와 같은 call, apply, bind의 사용이 무시된다.


퀴즈

1 . 클로저를 활용해서, 함수 생성시 인자로 주어진 수를 함수 실행시 인자로 주어진 수와 곱한 결과를 반환하는 코드를 작성해보자.

  • 위 코드에서 multiplyByTwo(5)은 multiplyBy(2)를 호출하여 반환된 함수를 5을 인자로 호출하는 것과 같다. 따라서 내부적으로는 5 * 2를 계산하여 결과값 10을 반환하게 된다.
  • 마찬가지로 multiplyByFive(12)은 multiplyBy(5)를 호출하여 반환된 함수를 12을 인자로 호출하는 것과 같다. 따라서 내부적으로는 12 * 5를 계산하여 결과값 60을 반환하게 된다.

2 . width 와 height 프로퍼티를 갖고 있는 객체들이 각자의 넓이를 출력할 때 사용할 수 있는 외부 함수를 만들고, 이를 사용하는 코드를 작성해보자

  • printArea 함수를 call 메서드를 사용하여 rectangle1 객체와 rectangle2 객체에 대해 각각 호출해 보았다.
    이렇게 하면 this가 각 객체를 가리키게 되어 해당 객체의 너비와 높이를 사용하여 넓이를 계산하고 출력할 수 있다.

3 . 어떤 값이 출력되는지 맞춰보자!

class Obj1 {
    constructor (name) {
        this.name = name;
        this.arrowFunc = () => this.name;
    }
    normFunc () { return this.name }
}

class Obj2 {
    constructor (name) {
        this.name = name;
    }
}

const obj1 = new Obj1('Apple'); 
const obj2 = new Obj2('Banana');

obj2.arrowFunc = obj1.arrowFunc;
obj2.normFunc = obj1.normFunc;

console.log(
    obj2.arrowFunc(),
    obj2.normFunc()
);

정답 : Apple, Banana

  • obj1 = Apple, obj2 = Banana가 저장되어 있다.
obj2.arrowFunc = obj1.arrowFunc;
obj2.normFunc = obj1.normFunc;
  • obj1 객체의 arrowFunc 메서드와 normFunc 메서드를 각각 obj2 객체의 arrowFunc 속성과 normFunc 속성에 할당하는 것이다.
  • obj2.arrowFunc()는 obj1 객체의 arrowFunc 메서드가 반환하는 값, 즉 'Apple'이 되고, obj2.normFunc()는 obj1 객체의 normFunc 메서드가 반환하는 값, 즉 'Banana'가 된다.
profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글