코어자바스크립트 스터디

3,4강 발표자료

려낭 2024. 7. 13. 15:40
3장 this

 

자바스크립트에서 this는 기본적으로 함수가 호출될 때 함께 결정됩니다.

(함수가 호출될 때 생성되는 실행 컨텍스트는 생성과 동시에 this 바인딩 과정을 거치기 때문입니다)

 

>바인딩 : 프로그램의 어떤 기본 단위가 가질 수 있는 구성요소의 구체적인 값, 성격을 확정하는 것

(예: int num 123; > num은 변수의 이름, int는 변수의 자료형, 123은 변수의 자료값이라는 구체적인 값을 할당하는 과정)


전역 공간에서의 this

 

전역 공간에서는 전역 컨텍스트를 생성하는 주체가 전역객체이기 때문에 this는 이 전역객체를 가리키게 됩니다.

 

전역 변수를 선언하면 JS 엔진은 해당 변수를 전역 객체의 프로퍼티로도 할당하는데 예시를 통해 살펴봅시다.

 

//전역 공간(브라우저)
var a = 1;

 

이렇게 전역 공간에서 a라는 식별자를 가진 변수를 선언하게 되면,

// 전역 공간(브라우저)
console.log(a);	// 출력: 1
console.log(window.a); // 출력: 1
console.log(this.a); // 출력: 1

 

위처럼, 변수 a, window의 프로퍼티 a, 그리고 this가 가리키고 있는 객체의 a는 모두 같은 값을 가리키고 있습니다.

이는 전역변수 a는 window의 프로퍼티로도 할당이 되고, this가 가리키고 있는 객체는 전역객체(window)라는 것을 보여줍니다.

 

대부분의 경우 해당 변수 a는 window의 프로퍼티로써의 역할과 거의 동일하게 움직이지만, delete키워드를 이용한 '삭제' 명령에서는 다른 모습을 보여줍니다.

 

a = 1;
window.b = 2;
console.log(a, window.a, this.a) // 출력: 1 1 1
console.log(b, window.b, this.b) // 출력: 2 2 2

delete a; 	// false
		   // (=== delete window.a;)
console.log(a, window.a, this.a) // 출력: 1 1 1

delete b;  // true
		   // (=== delete window.b;)
console.log(b, window.b, this.b) // Uncaught ReferenceError : b is not defined

 

위처럼, 전역변수로 선언을 하든, 전역객체의 프로퍼티로 선언을 하든 동작은 같지만

delete명령 시 전역 객체의 프로퍼티로 선언을 한 경우에만 해당 변수를 삭제할 수 있는 것을 알 수 있습니다.

 

이는 JS엔진이 변수의 의도치 않은 삭제에 대하 마련해 둔 방어 전략이다.

 


함수 vs 메서드

 

this를 이해하기 위해서는 함수를 호출하는 두 가지 방식인 함수호출과 메서드 호출을 먼저 이해해야 합니다.

이 둘을 구분하는 유일한 차이는 독립성에 있습니다.

 

function a() {console.log(this)}

var obj = {
  method: a
}

a();	// 함수로서 호출
obj.method();	// 메서드로서 호출

 

객체에 ' . '를 붙여서 호출하는 함수는 메서드로서의 호출이고 그 외에는 함수로서의 호출입니다.

(obj ['method']()처럼 호출해도 '. ' 의 의미와 같습니다.)

 

함수로서 호출될 때와 메서드로서 호출될 때 해당 함수가 가리키고 있는 this의 값은 어떻게 다를까?

 

함수로서 호출될 때의 this는 전역 객체를 가리키고 메서드로서 호출될 때는 호출을 실행한 주체,

즉 ' . ' 앞에 있는 객체를 가리킵니다.

 

obj.method(); //(메서드) 출력: obj { method: f }

 


메서드 내부 함수에서의 this

 

메서드 내부 함수에서의 this가 전역 객체를 가리키는 것에 대해 더글라스 크락포트는 명백한 설계상의 오류라고도 표현하였습니다.

 

그렇다면 우리는 이때의 this에 대해 혼동할 필요가 없도록 상위 스코프를 가리키도록 해줍시다.

 

아쉽게도 ES5까지에서는 이를 해결할 명확한 방법은 없고 우회하는 방법만 존재합니다.

 self라는 변수를 만들어서 상위스코프를 가리키는 this를 self에 담아두고 이를 this 대신 이용하는 것입니다.

 

var obj = {
  outer: function() {
    console.log(this); //  출력: obj1
    var self = this;	// 상위스코프를 가리키는 임의의 self 생성
    var innerFunction = function() {
		console.log(self);	//  출력: obj1 (this 대신 사용)
    };
    innerFunction();
  }
};
obj.outer();

 

변수에 상위스코프를 담아서 this 대신에 사용합니다. 

하지만 직관적으로 변수에 담아서 this 대신에 사용한다는 것이 마음에 들지 않을 수도 있습니다.

 

그래서 ES6이 우리에게 화살표 함수를 제공해주었습니다.

ES6에서는 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자

this를 바인딩하지 않는 화살표 함수를 새로 도입했습니다.

 

화살표 함수는 실행 컨텍스트 생성 시, this 바인딩 과정 자체를 제외하여 상위 스코프의 this를 그대로 활용할 수 있도록 하였습니다.

 

var obj = {
  outer: function() {
    console.log(this);	//  출력: obj1
    var innerFunction = () => {
    	console.log(this); // 출력: obj1
    }
    innerFunction(); // 실행컨텍스트 생성 시 화살표 함수를 만나면 this 바인딩 제외	
  }
};
obj.outer();

 


명시적 바인딩

 

this가 바인딩되는 규칙을 깨고 명시적으로 this 바인딩을 해줄 수 있는 방법이 있는데

바로 call, apply, bind 메서드를 이용하는 방법입니다.

 

1) call 메서드

 

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

(호출 주체는 해당 메서드를 부른 '. ' 앞에 있는 대상입니다.

 

첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 이용합니다.

var func = function (a, b, c) {
  console.log(this, a, b, c);
}

func(1, 2, 3);		// 출력: Window {...} 1 2 3 
func.call({ x: 1 }, 4, 5, 6);	// 출력: { x: 1 } 4 5 6

 

call 메서드의 호출 주체인 func를 즉시 호출하고, 첫 번째 인자인 { x:1 }로 해당 함수의 this에 바인딩하였고, 이후 인자들을 매개변수로 이용하였습니다. 객체의 메서드에서도 동일하게 작동합니다.

 

2) apply 메서드

 

apply메서드는 call 메서드의 기능적인 동작방식이 완전히 동일합니다.

하지만 다른 점은 첫 번째 인자 이후 두 번째 인자를 배열로 받아 배열의 요소들을 매개변수로 지정한다는 것입니다.

 

var obj = {
  x: 1,
  method: function(a, b, c) {
	console.log(this, a, b, c)
  }
}
obj.method(1, 2, 3)		// 출력: obj { x: 1, method: f } 1 2 3
obj.method.apply({ y: 2 }, [4, 5, 6]);	// 출력: { y: 2 } 4 5 6

 

 

3) bind 메서드

 

bind 메서드는 call 메서드와 비슷하지만 즉시 호출하지는 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환해 주는 메서드입니다.

첫 번째 인자로 this바인딩을 하고 두 번째로부터 인자로 넘겨받은 값들은 순서대로 매개변수에 등록하게 됩니다.

bind를 통해 생성된 함수에 인자를 넘기면 bind 메서드에서 넘겨받은 값들에 뒤이어 등록됩니다.

 

함수에는 name이라는 식별자를 담는 프로퍼티를 가지고 있는데, bind 메서드를 통해 새로 만들어진 함수에는 이 name프로퍼티의 식별자 앞에 bound라는 접두어가 붙습니다.

즉, aa라는 식별자를 가진 어떤 함수의 name 프로퍼티가 bound aa라면 이 함수는 bind 메서드를 통해 새로 생성된 함수라는 것을 알 수 있습니다.

 

var func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); 	// 출력: Window {...} 1 2 3 4
console.log(func.name);		// 출력: func

var bindFunc = func.bind({ x: 1 }, 4, 5);
bindFunc(6, 7);		// 출력: { x: 1} 4 5 6 7
console.log(bindFunc.name);		// 출력:bound func

 

 


콜백 함수 내부에서의 this

 

함수 A의 제어권을 다른 함수 B에게 넘겨주는 경우에 함수 A를 콜백함수라고 합니다.

이때 함수 A의 this는 함수 B의 내부 로직에서 정한 규칙에 따라 값이 결정됩니다.

 

예를 들어, 인자로 콜백 함수를 ㅂ다으며 이벤트 리스너를 등록해 주는 역할을 하는 addEventListener 메서드는 

내부 로직에 따라 this 값을 addEventListener가 가리키는 this로 바인딩하게 됩니다.

 

이때, 앞서 보았던 call, apply, bind 같은 메서드를 사용하여 명시적으로 thisf를 바인딩하여 콜백함수를 넘겨줄 수 있습니다.

 


생성자 함수 내부에서의 this

 

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

자바스크립트는 함수에 생성자로서의 역할을 함께 부여하는데, new명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 됩니다.

 

어떤 함수가 생성자 함수로서 호출된 경우, 내부에서의 this에는 곧 새로 만들 구체적인 인스턴스 자신이 바인딩됩니다.

 

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

var Cat = function (name, age) {
	this.bark = '야옹';
  	this.name = name;
  	this.age = age;
  	console.log(this);
};
var choco = new Cat('초코', 7);	 // 출력: Cat {bark: '야옹', name: '초코', age: 7}
var nabi = new Cat('나비', 5);	// 출력: Cat {bark: '야옹', name: '나비', age: 5}

 

생성자 함수를 호출하면 생성되는 객체가 this에 바인딩됩니다.

 

 

 

 


4장 콜백함수

 

 

콜백함수란?

 

콜백함수는 다른 코드의 인자로 넘겨주는 함수입니다. 

콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행할 것입니다.

 

callback은 '부르다' '호출(실행)하다' 라는 의미인 call과 '뒤돌아오다', '뒤돌다' 는 의미인 back의 합성어로 되돌아 호출해달라는 의미입니다.

 

어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것입니다.

이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출합니다.

 


제어권

 

콜백함수 예제 -setInterval 메서드

var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);

 

setInterval의 매개변수로는 func, delay 값을 반드시 전달해야 하고, 세번째 매개변수부터는 선택적입니다.

func에 넘겨준 함수는 매 delay마다 실행되며, 그 어떠한 값도 리턴하지 않습니다.

 

>>setInterval을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값이 반환됩니다

 

<호출시점>

var count = 0;

var cbFunc = function(){
    console.log(count);
    if(++count > 4) clearInterval(timer);
};

var timer = setInterval(cbFunc, 300); // 0 1 2 3 4

 timer 변수에는 setInterval의 ID값이 담깁니다.

setInterval에 전달한 첫번째 인자인 sbFunc함수(=콜백함수)는 0.3초 마다 자동으로 실행됩니다.

 

첫번째 인자로서 sbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로 판단에 따라 0.3초마다 익명함수를 실행합니다.

 

이처럼 콜백함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 가집니다.

 

 


this

 

별도의 this를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 map메서드 예제를 통해 알아봅시다.

 

Array.prototype.map = function (callback, thisArg) {
	var mappedArr = [];
    for (var i = 0; i < (this.length; i++ ) {
    	var mappedValue = callback.call(thisArg || window, this[i], i, this);
        mappedArr[i] = mappedValue;
    }
   return mappedArr;
 };

 

메서드 구현의 핵심은 call/apply에 있습니다.

this에는 thisArg값이 있을 경우에는 그 값을, 없을 경우에는 전역 객체를 지정하고, 

첫 번째 인자에는 메서드의 this 가 배열을 가리킬 것이므로 배열의 i번째 요소 값을, 두 번째 인자에는 i 값을, 세번째 인자에는 배열 자체를 지정해 호출합니다.

그 결과가 변수 mappedValue에 담겨 mappedArr의 i 번째 인자에 할당됩니다.

 

이제 this에 다른 값이 담기는 이유를 알 수 있다.

바로 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문입니다.

 


콜백함수는 함수다

 

 

콜백 함수는 함수입니다.

콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출됩니다.

 

var obj = {
    vals: [1, 2, 3],
    logValues: function(v, i){
        console.log(this, v, i)
    }
};

(1) obj.logValues(1, 2); // {vals: Array(3), logValues: ƒ} 1 2

(2) [4, 5, 6].forEach(obj.logValues);
// window {...} 4 0 
// window {...} 5 1
// window {...} 6 2

 

obj 객체의 logValues는 메서드로 호출되어 this는 obj을 가르킵니다.

logValues 메서드를 forEach의 콜백함수로 전달합니다.

obj.logValues는 this가 별도로 지정되지 않았으므로 this는 전역객체를 바라보게 됩니다.

 

 


콜백 지옥과 비동기 제어

 

콜백지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로,

자바스크립트에서 흔히 발생하는 문제입니다.

 

주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하지만,

가독성도 떨어지고 코드를 수정하기도 어렵습니다.

 

동기적인 코드는 현재 실행중인 코드가 완료된 후에야 다음 코드를 실행하는 방식입니다.

 

반대로 비동기적 코드는 현재 실행중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어갑니다.

 

CPU에 의해 즉시 처리가 가능한 대부분의 코드는 동기적인 코드입니다.

 

  • 반면 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeout), 
  • 사용자의 직접적인 개입이 있을 때 어떤 함수를 실행하도록 대기한다거나(addEventListener),
  • 웹 브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고, 그에 대한 응답이 왔을 때 어떤 함수를 실행하도록 대기하는 등 (XMLHttpRequest), 

별도의 요청, 실행대기, 보류 등과 관련된 코드는 비동기적인 코드입니다.

 

 

익명의 콜백함수를 모두 기명함수로 전환한다면 가독성 문제와 어색함을 동시에 해결할 수 있습니다.

new Promise (function (resolve){
    setTimeout(function(){
        var name = '에스프레소';
        console.log(name);
        resolve(name);
    },500);
}).then(function (prevName){
    return new Promise(function(resolve){
        setTimeout(function(){
            var name = prevName + ', 아메리카노';
            console.log(name);
            resolve(name);
        },500);
    });
}).then(function (prevName){
    return new Promise(function(resolve){
        setTimeout(function(){
            var name = prevName + ', 카페모카';
            console.log(name);
            resolve(name);
        }, 500);
    });
}).then(function (prevName){
    return new Promise(function(resolve){
        setTimeout(function(){
            var name = prevName + ', 카페라떼';
            console.log(name);
            resolve(name);
        },500);
    });
});

// 에스프레소
// 에스프레소, 아메리카노
// 에스프레소, 아메리카노, 카페모카
// 에스프레소, 아메리카노, 카페모카, 카페라떼

 

new연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백함수는 바로 실행되지만

그 내부의 resolve 또는 reject 함수를 호출하는 구문이 있을 경우

둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)으로 넘어가지 않습니다.

 

비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 동기적 표현이 가능합니다.

 

또한 함수 선언과 함수 호출만 구분할 수 있다면 위에서 아래로 순서대로 읽어 내려가는 데 어려움이 없습니다.

 

 

var addCoffee2 = function(name){
    return new Promise (function(resolve){
        setTimeout(function(){
            resolve(name);
        }, 500);
    });
};

var coffeeMaker2 = async function(){
    var coffeeList = '';
    var _addCoffee = async function(name){
        coffeeList += (coffeeList ? ',' : '') + await addCoffee2(name);
    }
    
    await _addCoffee('에스프레소');
    console.log(coffeeList);
    await _addCoffee('아메리카노');
    console.log(coffeeList);
    await _addCoffee('카페모카');
    console.log(coffeeList);
    await _addCoffee('카페라떼');
    console.log(coffeeList);
};

coffeeMaker2();

// 에스프레소
// 에스프레소, 아메리카노
// 에스프레소, 아메리카노, 카페모카
// 에스프레소, 아메리카노, 카페모카, 카페라떼

 

ES2017에서는 가독성이 뛰어나면서도 작성법도 간단한 새로운 기능이 추가됐는데, 

바로 async/await입니다.

 

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 

함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행합니다.

 

즉 Promise의 then과 흡사한 효과를 얻을 수 있습니다.

 

 

'코어자바스크립트 스터디' 카테고리의 다른 글

6장 프로토타입  (1) 2024.07.22
5장 클로저  (0) 2024.07.15
4장 콜백함수  (0) 2024.07.10
3장 this  (0) 2024.07.08
1장 데이터 타입, 2장 실행 컨텍스트 발표자료  (0) 2024.07.05