안녕하세요 YB 김고은입니다. 이번 주차는
콜백함수 (12강 _ 12.7.4) / this (342 ~ 354) / 클로저 (389 ~ 413)를 다뤄보고자 합니다.
이번 주차도 마찬가지로 내용 정리를 중점으로, 약간의 호기심을 해결해나가는 형식으로 아티클 작성해보겠습니다!
콜백 함수
콜백 함수(callback function)란 함수 매개변수를 통해 함수의 내부로 전달되는 함수를 말한다.
한편, 함수의 외부에서 콜백함수를 전달받은 함수를 고차함수(Higher-Order Function) 라고 한다.
콜백함수는 고차함수에 전달되어 핼퍼 함수의 역할을 한다. 이때의 콜백함수는 함수외부에서 고차함수 내부로 주입되기 때문에 자유롭게 교체될 수 있다는 장점이 있다. 이때, 고차 함수는 콜백함수를 자신의 일부분을 합성한다.
이때 고차함수가, 매개변수인 콜백 함수의 호출 시점을 결정해서 호출하는데,
이때 매개변수로 콜백함수를 전달할때에는 콜백 함수를 호출하지 않고, ❌ 함수 자체를 전달해야 한다. ⭕️
이때
1️⃣ 익명 함수 리터럴을 전달하는 방식과,
2️⃣ 함수 외부에서 함수를 정의한 후 함수 참조를 전달하는 방식
이렇게 2가지로 전달해줄 수 있는데, 이둘은 생성 횟수에서 차이가 존재할 수 있다
// 익명 힘수 리터렬을 콜백 함수로 전달하는 경우
// 익명 함수 리티럴은 repeat 합수를 호출할 때마다, 평기되어 함수 객체를 생성 (적어도 1번 이상)
repeat(5, Function (i) {
if(i%2)console.1og(i);
}); // 1 3
// 함수 선언 후, 참조 전달하는 경우
// 몇번을 호출하던, 함수 자체는 단 한번 생성된다. (딱 1번)
var logOdds = function (i) {
if(i % 2) console.log(i);
};
repeat(5, logOdds);
특히, 콜백함수의 쓰임새는 비동기처리에서 굉장히 중요한데!!
마침 다음주에 비동기처리에 대해서 다루니 그때 더 자세히 적어보겠다 ㅎㅎ
뿐만 아니라 배열 고차함수 에서도 사용되는데,
말이 어려워서 그렇지 우리 흔히 아는 `map` `filter` `reduce`, `sort`가 여기에 사용된다.
그 외에도 some 과 every 도 있는데 코테에서 정말 드물게 .. 사용했던 기억이 있던 것 같아 살짝 설명해보려고 한다,
some:
조건을 만족하는 요소가 하나라도 있으면 true 반환
every :
모든 요소가 조건을 만족하면 true, 그렇지 않음녀 false 반환
const nums = [1, 2, 3];
const hasEven = nums.some(num => num % 2 === 0);
console.log(hasEven); // true (짝수가 하나라도 있을 경우
const numbers = [1, 2, 3];
const allEven = numbers.every(num => num % 2 === 0);
console.log(allEven); // false (모두 짝수일 경우)
this
객체는 상태를 나타내는 프로퍼티와 동작을 나타내는 메서드를 논리적 단위로 묶은 복합적 구조
이때 메서드는, 프로퍼티를 참조하고 변경할 수 있어야 하는데 자신이 속한 객체의 프로퍼티를 참조하기 위해서는
=> 자신이 속한 객체를 가리키는 식별자를 참조할 수 있어야 한다
재귀적 참조
사실 this 사용하여 참조하는 방법 이외에도, 재귀적으로 식별자를 참조하는 방법이 있다. (처음 알았다 )
const circle = {
radius:5, //프로퍼티
getDIameter() { // 메서드
return 2 * circle.radius; // 자신이 속한 객체인 circle을 참조할 수 있어야 함
}
};
console.1og(circle.getDiameter()); // 10
근데 재귀적으로 식별자를 참조하는 방식은 일반적이지도 않으면서, 바람직하지 않다고 한다
궁금증 : 왜 일반적이지 않고 바람직하지도 않을까? )
1) 객체 이름 재명명시, 내부 코드 수정 필요
const newCircle = circle; // circle -> newCircle
console.log(newCircle.getDiameter()); // 내부 참조는 circle.radius로 남아있어 오류 발생!
위와 같이 객체 이름을 바꿔주게 된다면, 객체 내부에 식별자를 참조하는 모든 부분들을 모조리 다 수정줘야 한다
2) 객체의 유연성 저하
객체를 복사하거나 다른 곳에서 사용하고 싶을때에도, 메소드는 원래의 식별자인 circle을 참조하여
기대한 결과를 도출해낼 수 없음
const anotherCircle = { ...circle, radius: 10 };
console.log(anotherCircle.getDiameter()); // 여전히 원래의 circle.radius를 참조하고 있음
따라서, this 키워드를 통해서, 자신이 속한 객체를 가리키는 식별자를 알아내는 것이 중요하다!
this 참조
this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수
this를 통해 객체 혹은 생성할 인스턴스의 프로퍼티 및 메서드를 참조 가능
이때, this 가 가리키는 값(this 바인딩)은 함수 호출 방식에 의해 동적으로 결정
이때 바인딩이란, 식별자와 값을 연결하는 과정
(ex. 변수 선언에서 바인딩은, 변수 이름 <--> 메모리 공간의 주소를 묶어주는것)
여기에서 this 바인딩은, this <--> this가 가르킬 객체를 묶어주는 것
클래스 기반 언어에서 this 는 클래스가 생성하는 인스턴스이지만,
자바스크립트에서는 호출되는 방식에 따라 this에 바인딩되는 값이 바뀐다
렉시컬 스코프 vs this 바인딩 / 결정 시기의 차이
렉시컬 스코프 : 함수의 상위 스코프를 결정하는 방식으러, 함수 객체 생성 시점에 결정됨
this 바인딩 : 함수 객체 생성시가 아닌, 함수 호출 시점에 결정됨
상황에 따라 가리키는 대상이 달라지는 this
객체 리터럴 메서드 내부에서 this -> 메서드를 호출한 객체 (를 가리킴)
전역에서 this -> 전역 객체 window
생성자 함수 내부에서 this -> 함수가 생성할 인스턴스
일반 함수 내부에서 this -> 전역 객체 window
함수 호출 방식에 따라 정리해본 this
물론 ⬆️ 위에 있는 친구들을 호출한 것과 같음 + a (bind / apply / call )
1️⃣ 일반함수 호출
-> 기본적으로 전역 객체(window)가 바인딩 ..물론 객체를 생성하지 않는 일반 함수에서는 의미가 없긴하다
(this 알아봤자 쓸 일이 없으니 .. ) 그래서 엄격 모드일때는 undefined와 묶이게 된다
물론 중첩 함수의 경우에도, 일반 함수로 호출되면 중첩 함수 내부에 this 도 전역 객체가 바인딩된다.
(상위 함수가 객체여도, 일반 함수로 중첩 함수가 호출되면 전역 객체가 바인딩된다 )
어떤 방식(콜백이든, 중첩 함수든 )으로든 일반 함수로 호출되면 this 에는 전역 객체가 바인딩
function Foo() {
'use strict';
console.1og("foo's this: ", this); //undefined
function bar() {
console.1og("bar's this: ", this); // undefined
}
bar(); // 일반 함수 호출로 인해, 중첩 함수에 전역객체 바인딩
}
foo( );
물론 전역 객체만을 바인딩하면, 중첩 함수 / 콜백함수 사용시에 헬퍼함수로 동작하기에 크게 도움이 되지 않는다
(외부 함수와 this 불일치 되는 경우 )
그래서, 메소드 내부의 중첩 함수나 콜백 함수의 this 바인딩을 🔄 메서드의 this 바인드와 일치시키기 위해서
this 바인딩을 변수에 해주고, 그 변수를 참조 해주면 된다!
(혹은 bind 와 같은 간접 참조 메소드 사용도 가능 / 아니면 화살표 함수 사용도 가능 )
var value = 1;
const obj = {
value: 100,
Foo() {
// this바인딩을 변수에 넣어줌!!
const that = this;
setTimeout(functlon(){
// 콜백힘수내부에서 => that을 참조
console.1og(that.value); // 100
},100);
}
};
obj.foo();
2️⃣ 메소드 호출
-> 호출시, 메서드 이름 앞에 마침표 연산자 앞에 기술한 객체가 바인딩 (객체.메소드 )
💡 메소드가 가리키는 객체는, 이때 메소를 담고 있는 전체 객체에 포함된것이 아니라! 독립적으로 존재하는 객체라고 한다!
그래서 메서드 내부의 this는 프로퍼티!!! 메소드를 가리키고 있는 객체와는 관계 없음 (근본이 되는 선언 객체)
3️⃣ 생성자 함수 호출
-> 생성자 함수가 생성할 인스턴스에 바인딩
new 로 만들어서 할당해줄 인스턴스가 this가 된다는 의미!
4️⃣ apply / bind / call 메서드에 의한 간접 호출
-> apply / call 메소드 모두 함수를 호출하면서, 첫번째 인수에 전달한 특정 객체를 호출한 함수의 this 에 바인딩
단, apply 는 호출할 함수 인수를 배열로 묶어서 전달 하지만,
call 메소드는 함수의 인수를 쉼표로 구분한 리스트 형식으로 전달한다.
(단지, 인수 전달 방식만 다를뿐 동작은 같음 )
요 두 메소드는 특히, 객체와 같은 유사 배열 객체에 배열 메서드를 사용하는 경우에 사용한다.
반면, bind는 함수 호출 없이, 첫번째 인수로 전달한 값으로 this 바인딩이 교체된 함수를 새롭게 생성해서 반환
bind는 특히, 메서드 내부의 중첩 함수 또는 콜백 함수의 this 가 불일치하는 문제를 해결할때 사용
https://wave-web.tistory.com/69
[Week 01] this 와 bind 를 알아보자
안녕하세요. YB 김고은입니다. js 스터디 1주차 아티클로 세미나 시간에 다루어보았던 js의 전반적인 내용 중일부를 선정하여 더 알아보기로 했었는데요! 저는 this 키워드와 bind 메소드에 대해서
wave-web.tistory.com
클로저
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합
여기에서 저번주에 공부했던 렉시컬 스코프란!
렉시컬 스코프란(정적 스코프),
함수를 어디에서 호출했는지가 아니라 🙅🏻♀️ 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하는것
상위 스코프를 결정 === 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조값을 결정
함수가 정의된 환경과 / 호출되는 환경은 다를 수 있기 때문에, 렉시컬 스코프를 따르기 위해서
자신이 정의된 환경(상위 스코프)를 기억해야 하는데, 이때 함수는 내부 슬롯[[Environment]]에 자신 상위 스코프의 참조를 저장한다.
이전에 실행 컨텍스트에서, 실행 컨텍스트 스택과 / 렉시컬 환경을 배웠을때 익히 봤던 그래프이다.
여기에서 우리가 주목해야 할 점은 바로, 렉시컬 환경의 외부 렉시컬 환경 참조값이 할당되는 방법인데,
바로, 내부 슬롯[[Environment]]에 저장된 렉시컬 환경 참조가 할당된다는 점이다.
즉, 내부 슬롯에 저장된 참조는 상위 스코프를 의미하고, 렉시컬 스코프의 실체라고 할 수 있다.
클로저란
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 ➡️ 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있는데
이때, 중첩함수를 클로저라고 한다.
이때, 외부 함수의 생존 여부(실행 컨텍스트 스택에 있는지 여부)에 관계없이 정의된 위치인 상위 스코프를 기억한다.
(이때, 외부 함수보다 중첩 함수의 생명 주기가 더 긴 경우 -> 외부 함수에 대한 반환값으로 중첩 함수를 리턴하는 경우 )
클로저가 되기 위한 조건
1️⃣ 중첩 함수가 일단 상위 스코프 보다 더 오래 유지되면서 &&
2️⃣ 중첩함수가 상위 스코프의 식별자를 참조하고 있는 경우
그래서 아무튼 클로저를 통해서, 참조되는 상위 스코프의 변수에 접근할 수 있는 것인데
이때 이 변수를 "자유 변수" 라고 하고, 클로저는 자유 변수에 묶여있는 함수라고 말할 수 있다 .
클로저 그래서 어떻게 활용하는데?
클로저는 상태를 안전하게 변경하고 유지하기 위해 사용됨.
상태가 의도치 않게 변경되지 않도록, 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용
const increase = (function () {
let num = 0; // 카운트 상태 변수
return function () { // 클로저
return ++num;
}
} ());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
해당 코드는
1) 카운트 상태는 increase 함수 호출 전까지 변경되지 않고 유지되고 있으며 (지역변수에 담아둠으로써)
2) 카운트 상태는 increase 함수만이 변경하게 한다.
이처럼 상태를 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하게 하기 위해 사용된다.
자바스크립트는 public, private, protected 와 같은 접근 제한자를 제공하지 않기 때문에,
모든 객체의 메서드와 프로퍼티는 기본적으로 public 하다.
const Person = (function () {
let _age = 0; // private (클로저로 보호됨)
// 생성자 함수
function Person(name, age) {
this.name = name; // public
_age = age; // 외부에서 접근 불가
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
// 생성자 함수를 반환
return Person;
}());
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
console.log(me._age); // undefined (private 변수이므로 외부에서 접근 불가)
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined (private 변수이므로 외부에서 접근 불가)
이때 프로퍼티인 name은 수정과 참조가 자유로운 (public) 반면,
생성자 함수 내부에 선언된 변수인 _age 는 생성된 인스턴스 외부에서 직접 접근할 수 없다 (private)
즉, Person 함수와 sayHi 함수 메소드는 클로저로 _age를 은닉하고 보호한다.
근데 사실 이 방법도 빈틈이 없지 않다는 사실,.!
-> person 생성자 함수가 여러개의 인스턴스를 생성할 경우 _age 변수의 상태가 유지 되지 않을 수 있기 때문
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
// 은닉 실패! _age 변수 값이 변하게 된다
me.sayHi(); // Hi! My name is Lee. I am 30
(이유)
요 현상은 Person.prototype.sayHi 메서드가 단 한 번 생성되는 클로저이기 때문에 발생하는 현상인데
person.prototype.sayHi 가 렉시컬 환경 참조를 [[Enviroment]]에 저장하여 기억한다.
어떤 인스턴스를 생성하든 하나의 동일한 상위 스코프를 쓰기 때문에, 여러개의 인스턴스를 생성하면 _age가 유지되지 못하고 바뀐다
자바스크립트는 정보 은닉을 완전히 지원하지 못하기 때문에.. 자유 변수를 통해 private를 흉내낼 수는
prototype 메서드 사용하면 정보 보호가 깨진다.
(오호 그래서 타스가 최고라는건가 )
'자스핑' 카테고리의 다른 글
[week 03] this is me (0) | 2024.11.14 |
---|---|
[week3] this is .. (0) | 2024.11.13 |
[week 02] 실행 컨텍스트와 스코프, 호이스팅 (1) | 2024.11.07 |
[Week02] 스코프, 호이스팅, 실행 컨텍스트 (1) | 2024.11.07 |
[week 02] 실행 컨텍스트와 스코프, 호이스팅 (1) | 2024.11.07 |