코어 자바스크립트 (마무리2)

문린이·2023년 2월 6일
0

1. 데이터 타입

얕은 복사와 깊은 복사

  • 얕은 복사 : 바로 아래 단계의 값만 복사하는 방법 (중첩된 객체에서 참조형 데이터가 저장된 프로퍼티를 복사할 때 그 주솟값만 복사)
const copyObj = originalObj;

console.log(originalObj === copyObj); // true

위 코드는 동일한 객체를 가리키는 변수를 하나 더 만드는 방법 (하나의 객체를 가르키는 변수가 2개)
ex) 참조 할당(=), Object.assign, Spread Operator

  • 깊은 복사 : 내부의 모든 값들을 하나하나 찾아서 전부 복사하는 방법 (객체 트리의 최말단 노드까지 복제) -> 불변 객체

객체의 프로퍼티 중에서 그 값이 기본형 데이터일 경우에는 그대로 복사, 참조형 데이터는 다시 그 내부의 프로퍼티들을 복사(재귀적으로 수행)
ex) JSON.parse(JSON.stringify) -> 함수는 json 데이터 타입이 아니기 때문에 x, 재귀적으로 직접 수행, 외부 라이브러리(Cloneable, Cloner)

2. 실행 컨텍스트

  • VariableEnvironment : 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보.
    선언 시점의 LexicalEnvironment의 스냅샷으로, 변경 사항은 반영되지 않음
    (environmentRecord(최초 snapshot), outerEnvironmentReference(최초 snapshot)으로 구성)
  • LexicalEnvironment : 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨. (사전적 환경)
    (environmentRecord(컨텍스트 내부의 식별자 정보 (매개변수의 이름, 함수 선언, 변수명), outerEnvironmentReference(외부 Scope의 주소 참조, 바로 직전 실행 컨텍스트의 LexicalEnvironment 정보를 참조))
  • ThisBinding : this 식별자가 바라봐야 할 대상 객체.

호이스팅 개념은 environmentRecord의 관심사에 맞춰 수집 대상을 끌어 올린 개념과 관련이 있다.

environmentRecord는 현재 실행될 컨텍스트의 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있고, 각 식별자에 어떤 값이 할당될 것인지는 관심이 없다.
따라서 변수를 호이스팅할때 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둔다.

함수 선언문과 함수 표현식

console.log(sum(1, 2));
console.log(multiply(3, 4));

function sum (a, b) { // 함수 선언문 sum
	return a + b;
}

var multiply = function(a, b) { // 함수 표현식 multiply
	return a * b;
}

호이스팅 후

var sum = function sum (a, b) { // 함수 선언문은 전체를 호이스팅
	return a + b;
}

var multiply; // 변수는 선언부만 끌어올린다.

console.log(sum(1, 2));
console.log(multiply(3, 4));

multiply = function(a, b) { 
	return a * b;
}

이러한 개념때문에 함수 표현식이 선언문 보다 안전하다.

스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위, 스코프(식별자의 유효범위)를 안에서부터 바깥으로 차례로 검색해나가는 것을 스코프 체인, 이를 가능케 하는것이 outerEnvironmentReference이다.

A 함수 내부에 B 함수를 선언하고 다시 B 함수 내부에 C 함수를 선언한 경우,
함수의 C의 outerEnvironmentReference는 함수 B의 LexicalEnvironment를 참조하고
함수 B의 LexicalEnvironment에 있는 outerEnvironmentReference는 다시 함수 A의 LexicalEnvironment를 참조한다.
이처럼 계속 찾아 올라가면 전역 컨텍스트의 LexicalEnvironment가 있다.
또한, 각 outerEnvironmentReference는 오직 자신이 선언된 시점의 LexicalEnvironment만 참조하고 있으므로 가장 가까운 요소부터 차례대로만 접근할 수 있고 다른 순서로 접근하는 것은 불가능하다.

이런 구조적 특성 덕분에 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다.

스코프 체인

(1)  var a = 1;						
(2)  var outer = function () {		
(3) 	 var inner = function () {	
(4)   	 	 console.log(a);
(5)      	 var a = 3;
(6)    	 };
(7)  	 inner():
(8)    	 console.log(a);
(9)	 };
(10) outer();
(11) console.log(a);
  1. 시작 : 전역 컨텍스트가 활성화
    전역 컨텍스트의 e에 { a, outer } 식별자를 지정. 전역 컨텍스트는 선언 시점이 없으므로 전역 컨텍스트의 o에는 아무것도 담기지 않는다. (this : 전역 객체)

  2. (1), (2) : 전역 스코프에 있는 변수 a에 1, outer에 함수를 할당

  3. (1) : outer 함수를 호출
    전역 컨텍스트의 코드는 10번째 줄에서 임시중단, outer 실행 컨텍스트가 활성화 -> (2)로 이동

  4. (2) : outer 실행 컨텍스트의 e에 { inner } 식별자를 저장
    o에는 outer 함수가 선언될 당시의 LE가 담긴다. outer 함수는 전역 공간에서 선언됐으므로 전역 컨텍스트의 LE를 참조복사한다. 이를 [ GLOBAL, { a, outer } ] 라고 표기. 첫 번째는 실행 컨텍스트의 이름, 두 번째는 e 객체 (this : 전역 객체)

  5. (3) : outer 스코프에 있는 변수 inner에 함수를 할당

  6. (7) : inner 함수를 호출
    outer 실행 컨텍스트의 코드는 7번째 줄에서 임시중단, inner 실행 컨텍스트가 활성화 -> (3)로 이동

  7. (3) : inner 실행 컨텍스트의 e에 { a } 식별자를 저장
    o에는 inner 함수가 선언될 당시의 LE가 담긴다. inner 함수는 outer 함수 내부에서 선언됐으므로 outer 함수의 LE. 즉 [ outer, { inner } ]를 참조복사 (this : 전역 객체)

  8. (4) : 식별자 a에 접근
    현재 활성화 상태인 inner 컨텍스트의 e에서 a를 검색. a가 발견됐는데 여기에는 아직 할당된 값이 없다. (undefined 출력)

  9. (5) : inner 스코프에 있는 변수 a에 3을 할당

  10. (6) : inner 함수 실행 종료
    inner 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 outer 실행 컨텍스트가 다시 활성화되면서, 앞서 중단했던 7번째 줄의 다음으로 이동

  11. (8) : 식별자 a에 접근
    활성화된 실행 컨텍스트의 LE에 접근. 첫 요소의 e에서 a가 있는지 찾아보고, 없으면 o에 있는 e로 넘어가는 식으로 계속해서 검색. 두 번째 (전역 LE)에 a가 있으니 그 a에 저장된 값을 1을 반환 (1 출력)

  12. (9) : outer 함수 실행 종료
    outer 실행 컨텍스트가 콜 스택에서 제거되고, 바로 아래의 전역 컨텍스트가 다시 활성화되면서, 앞서 중단했던 10번째 줄의 다음으로 이동

  13. (11) : 식별자 a에 접근
    현재 활성화 상태인 전역 컨텍스트의 e에서 a를 검색. 바로 a를 찾을 수 있다.(1 출력) 이로써 모든 코드의 실행이 완료된다. 전역 컨텍스트가 콜 스택에서 제거되고 종료

변수 은닉화 : inner 함수 내부에서 a 변수를 선언했기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없다.

3. this

this 우회

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

  2. 생성자 함수 내부에서의 this
    new 명령어와 함께 함수를 호출 -> 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

  3. call 메서드
    call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 한다. 함수를 그냥 실행하면 this는 전역객체를 참조하지만 call 메서드를 이용하면 임의의 객체를 this로 지정할 수 있다.

var obj = {
	a : 1,
  	method: function (x, y) {
    	console.log(this.a, x, y);
    }
};

obj.method(2, 3); // 1 2 3
obj.method.call({ a: 4 }, 5, 6); // 4 5 6 
  1. apply 메서드
    call 메서드와 기능적으로 완전히 동일하다. call 메서드는 첫 번째 인자를 제외한 나머지 모든 인자들을 호출할 함수의 매개변수로 지정하는 반면, apply 메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.

  2. bind 메서드
    bind 메서드는 call과 비슷하지만 즉시 호출하지는 않고 넘겨 받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다. 다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다. 즉 bind 메서드는 함수에 this를 미리 적용하는 것과 부분 적용 함수를 구현하는 두 가지 목적을 모두 지닌다.

(1)  var func = function (a, b, c, d) {
(2)  	console.log(this, a, b, c, d);
(3)  };
(4)  func(1, 2, 3, 4); // Window{ ... } 1 2 3 4
(5)
(6)  var bindFunc1 = func.bind({ x: 1 });
(7)  bindFunc1(5, 6, 7, 8); // { x : 1} 5 6 7 8
(8)
(9)  var bindFunc2 = func.bind({ x: 1 }, 4, 5);
(10) bindFunc2(6, 7); // { x: 1 } 4 5 6 7 

6번째 줄에서 bindFunc1 변수에는 func에 this를 { x: 1 }로 지정한 새로운 함수가 담긴다.
이제 7번째 줄에서 bindFunc1을 호출하면 원하는 결과로 얻을 수 있게 된다. 한편 9번째 줄의 bindFunc2 변수에는 func에 this를 { x: 1 }로 지정하고, 앞에서부터 두 개의 인수를 각각 4, 5로 지정한 새로운 함수를 담았다. 이후 10번째 줄에서 매개변수로 6, 7을 넘기면 this 값이 바뀐 것을 제외하고는 최초 func 함수에 4, 5, 6, 7을 넘긴 것과 같은 동작을 한다.
6번째 줄의 bind는 this만을 지정한 것이고, 9번째 줄의 bind는 this 지정과 함께 부분 적용 함수를 구현한 것이다.

ex) 활용

this를 영구적으로 변경

const kPeople = {
  nat: "korea" 
};

function func(name) {
  console.log(this.nat, name)
};

const print = func.bind(kPeople);

print('JJ'); // korea JJ

함수를 짧게

const print = console.log.bind(document);

print('aaa'); // aaa
print('bbb'); // bbb

4. 콜백 함수

비동기 제어

  1. 기명함수로 변환
var coffeeList = '';

var addEspresso = function (name) {
	coffeeList = name;
  	console.log(coffeeList);
  	setTimeout(addAmericano, 500, '아메리카노');
};

var addAmericano = function (name) {
	coffeeList += ', ' + name;
  	console.log(coffeeList);
}

setTimeout(addEspresso, 500, '에스프레소');
  1. Promise
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);
    });   
});

new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)으로 넘어가지 않는다.
따라서 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.

Promise API

  • Promise.all
    요소 전체가 프라미스인 배열을 받고 새로운 프라미스를 반환한다.
    Promise.all에 전달되는 프라미스 중 하나라도 거부되면, Promise.all이 반환하는 프라미스는 에러와 함께 바로 거부된다.

  • Promise.allSettled
    Promise.all과 다르게 모든 프라미스가 처리될 때까지 기다린다.
    Promise.allSettled를 사용하면 각 프라미스와 상태와 값 또는 에러를 받을 수 있다.

  • Promise.race
    Promise.all과 비슷하지만 가장 먼저 처리되는 프라미스의 결과(혹은 에러)를 반환한다.

  • Promise.resolve와 Promise.reject
    Promise.resolve은 결과값이 value인 이행 상태 프라미스를 생성,
    Promise.reject는 결과값이 error인 거부 상태 프라미스를 생성한다.

  1. Generator
var addCoffee = function (prevName, name) {
	setTimeout(function () {
    	coffeeMaker.next(prevName ? prevName + ', ' + name : name);
    }, 500);
};

var coffeeGenerator = function* () {
	var espresso = yield addCoffee('', '에스프레소');
  	console.log(espresso);
  	var americano = yield addCoffee(espresso, '아메리카노');
  	console.log(americano);
}

var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

'*'이 붙은 함수가 Generator 함수이다.
Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메서드를 가지고 있다.
이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다.
이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그다음에 등장하는 yield에서 함수의 실행을 멈춘다.
즉, 비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.

  1. Async/await
var addCoffee = function (name) {
	return new Promise(function (resolve) {
    	setTimeout(function () {
        	resolve(name);
        }, 500);
    });
};

var coffeeMaker = async function () {
	var coffeeList = '';
  	var _addCoffee = async function (name) {
    	coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
    };
  	await _addCoffee('에스프레소');
  	console.log(coffeeList);
    await _addCoffee('아메리카노');
  	console.log(coffeeList);
};
coffeeMaker();

비동기 작업을 수행하자고 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행된다.
즉, Promise의 then과 흡사한 효과를 얻을 수 있다.

5. 클로저

클로저란

  • 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는 함수

  • 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수

  • 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3

현재 상태를 기억하고 변경된 최신 상태를 유지하고 싶을 때 활용

var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');

var toggle = (function () {
	var isShow = false;
    // ① 클로저를 반환
    return function () {
    	box.style.display = isShow ? 'block' : 'none';
        // ③ 상태 변경
        isShow = !isShow;
    };
})();
// ② 이벤트 프로퍼티에 클로저를 할당
toggleBtn.onclick = toggle;
  • 쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 하는 것
  • 디바운싱: 연이어 호출되는 함수들 중 마지막 함수(또는 제일 처음)만 호출하도록 하는 것

커링 함수 활용

var endPoint = baseUrl => path => id => fetch(baseUrl + path + '/' + id);

var imageUrl = 'image.com/'

var getImage = endPoint(imageUrl); // image.com/
var getEmoticon = getImage('emoticon'); // image.com/emoticon
var getIcon = getImage('icon'); // image.com/emoticon/icon

var emoticon1 = getEmoticon(100); // image.com/emoticon/100
var icon1 = getIcon(5); // image.com/icon/5

6. 프로토타입

자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있다. 그리고 이것은 마치 객체 지향의 상속 개념과 같이 부모 객체의 프로퍼티 또는 메소드를 상속받아 사용할 수 있게 한다. 이러한 부모 객체를 Prototype(프로토타입) 객체 또는 줄여서 Prototype(프로토타입)이라 한다.

Prototype 객체는 생성자 함수에 의해 생성된 각각의 객체에 공유 프로퍼티를 제공하기 위해 사용한다.

  • 어떤 생성자 함수(Constructor)를 new 연산자와 함께 호출하면
  • Constructor에서 정의된 내용을 바탕으로 새로운 인스턴스(instance)가 생성된다.
  • 이때 instance에는 __proto__라는 프로퍼티가 자동으로 부여되는데,
  • 이 프로퍼티는 Constructor의 prototype이라는 프로퍼티를 참조한다.

__proto__를 대체

  • Object.create(proto, [descriptors]) – [[Prototype]]이 proto를 참조하는 빈 객체를 만듭니다. 이때 프로퍼티 설명자를 추가로 넘길 수 있다.

  • Object.getPrototypeOf(obj) – obj의 [[Prototype]]을 반환한다.

  • Object.setPrototypeOf(obj, proto) – obj의 [[Prototype]]이 proto가 되도록 설정한다.

let animal = {
  eats: true
};

// 프로토타입이 animal인 새로운 객체를 생성
let rabbit = Object.create(animal);

console.log(rabbit.eats); // true

console.log(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // rabbit의 프로토타입을 {}으로 변경

Object.create와 new 비교

function Dog(){
    this.pupper = 'Pupper';
};

Dog.prototype.pupperino = 'Pups.';
var maddie = new Dog();
var buddy = Object.create(Dog.prototype);

// Using Object.create()
console.log(buddy.pupper); // Output is undefined
console.log(buddy.pupperino); // Output is Pups.

// Using New Keyword
console.log(maddie.pupper); // Output is Pupper
console.log(maddie.pupperino); // Output is Pups.

new Dog는 실제로 생성자 코드를 실행하는 반면, Object.create는 생성자 코드를 실행하지 않기 때문이다.
new는 생성자의 프로퍼티도 가져오고 Object.create 생성자의 프로토타입만 가져온다.

7. 클래스

class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    console.log(this.name);
  }
}

let user = new User("John");
user.sayHi();

new User("John")를 호출하면 새로운 객체가 생성되고 넘겨받은 인수와 함께 constructor가 자동으로 실행된다.
이때 인수 "John"이 this.name에 할당된다. 이런 과정을 거친 후에 user.sayHi() 같은 객체 메서드를 호출할 수 있다.

class User {...} 가 하는 일

  • User라는 이름을 가진 함수를 만든다. 함수 본문은 생성자 메서드 constructor에서 가져온다. 생성자 메서드가 없으면 본문이 비워진 채로 함수가 만들어진다.
  • sayHi같은 클래스 내에서 정의한 메서드를 User.prototype에 저장한다.

extends

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed = speed;
    console.log(`${this.name} 은/는 속도 ${this.speed}로 달립니다.`);
  }
  stop() {
    this.speed = 0;
    console.log(`${this.name} 이/가 멈췄습니다.`);
  }
}

class Rabbit extends Animal {
  hide() {
    console.log(`${this.name} 이/가 숨었습니다!`);
  }
}

let rabbit = new Rabbit("흰 토끼");

rabbit.run(5); // 흰 토끼 은/는 속도 5로 달립니다.
rabbit.hide(); // 흰 토끼 이/가 숨었습니다!

클래스 Rabbit을 사용해 만든 객체는 rabbit.hide() 같은 Rabbit에 정의된 메서드에도 접근할 수 있고, rabbit.run() 같은 Animal에 정의된 메서드에도 접근할 수 있다.

키워드 extends는 프로토타입을 기반으로 동작한다. extends는 Rabbit.prototype.[[Prototype]]을 Animal.prototype으로 설정한다. 그렇기 때문에 Rabbit.prototype에서 메서드를 찾지 못하면 Animal.prototype에서 메서드를 가져온다.

super

  • super.method(...)는 부모 클래스에 정의된 메서드, method를 호출한다.

  • super(...)는 부모 생성자를 호출하는데, 자식 생성자 내부에서만 사용 할 수 있다.

profile
Software Developer

0개의 댓글