[JavaScript] 함수형 프로그래밍 (go, pipe, curry)

 

 

 

- 데이터

const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 }
]

 

 

 

# go

const go = (...args) => reduce((f, a) => f(a), args);

go(
   0, 
   a => a + 1, 
   a => a + 10, 
   a => a + 100, 
   console.log
  );

go() 함수는 함수들과 인자를 전달해서 즉시 어떤 값을 평가한다. go() 함수는 5가지의 인자를 함수를 받는다. go의 인자는 rest 파라미터를 이용하여 여러가지 인수를 배열로 받는다. rest 파라미터는 배열이고 이터러블 프로토콜을 만족한다. 차례대로 함수를 실행하여 값을 도출하기 위해 reduce() 함수를 사용하여 축약된 값을 반환하고 마지막은 console.log 함수를 통해 결과를 로그로 출력한다.

 

- go 함수 이용하기

go(
   products,
   products => filter(p => p.price < 20000, products),
   products => map(p => p.price, products),
   prices => reduce(add, prices),
   console.log
  ); // 30000

기존의 map, filter, reduce 함수를 사용했을 때는 아래에서 위, 오른쪽에서 왼쪽으로 읽어야하는 느낌이 강했지만 go 함수를 사용하게되면 차례대로 위에서 아래, 왼쪽에서 오른쪽으로, 연속적으로 평가할 수 있다.

 

 

# pipe

const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (...fs) => (a) => go(a, ...fs);

const f = pipe(
               a => a + 1, 
               a => a + 10,
               a => a + 100
              );
  
console.log(f(0));

pipe 함수는 함수들이 나열된 합성된 함수를 리턴하는 함수이다. pipe 함수는 여러개의 함수들을 인자로 받아 연속적으로 실행하여 축약하는 하나의 함수를 만들어 리턴한다. pipe 함수의 인자로 여러가지 함수(fs)를 받고 함수를 시작하는데 쓰는 인자(a)를 나중에 받고, 받은 인자를 가지고 go()함수를 실행하면서 인자(a)와 함수(fs)를 전달한다.

 

- go, pipe 변형

const go = (...args) => reduce((a, f) => f(a), args);
const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs);

go(
   add(0, 1),
   a => a + 10,
   a => a + 100,
   console.log
  ); // 111

const f = pipe(
               (a, b) => a + b, 
               a => a + 10,
               a => a + 100
              );
  
console.log(f(0, 1)); // 111
// console.log(f(add(0, 1))); // pipe() 함수의 이점이 사라짐

만약 인자를 2가지 이상을 받아서 평가된 값으로 함수를 진행시킨다면 go() 함수의 경우는 2개 이상의 인자를 전달 받고 평가하는 함수를 지정하면 된다. 하지만 pipe() 함수는 f(add(0, 1))의 형식으로 인자를 전달해야 하는데 이 방식은 pipe() 함수로 사용하기에 다소 아쉽다. 그렇기 때문에 pipe() 함수에서 인자를 전달 받는 방식을 변경한다. pipe() 함수의 첫번째 인자로 들어갈 함수(f)는 따로 지정하고 리턴할 함수의 인자(a)를 여러개를 받아야하기 때문에 (...as)로 받는다. go() 함수의 인자로 전달될 부분도 마찬가지로 첫번째 인자와 나머지 전달될 인자를 구분한다.

 

 

# curry

const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);

const mult = curry((a, b) => a * b);
console.log(mult(3)); // (..._) => f(a, ..._);
console.log(mult(3)(2)); // 6

const mult3 = mult(3);
console.log(mult3(10)); // 30
console.log(mult3(5)); // 15
console.log(mult3(3)); // 9

curry는 함수를 값으로 다루면서 받아둔 함수를 원하는 시점에 평가시키는 함수이다. 먼저 함수를 받아서 함수를 리턴하고 인자를 받아서 인자가 원하는 갯수만큼 일때 받아두었던 함수를 평가시킨다. mult() 함수는 두개의 인자를 받고 곱하는 함수이다. 이 함수를 curry() 함수로 호출하면 두번째 인자까지 받았다는 조건(_.length ?)이 만족되면 실행(f(a, ..._)하고 첫번째 인자까지 받았다면 함수((..._) => f(a, ..._))를 반환한다.

 

- go와 curry의 조합

const curry = f => (a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._);

const map = curry((f, iter) => {
  let res = [];
  for (const a of iter) {
    names.push(f(a));
  }
  return res;
});

const filter = curry((f, iter) => {
  let res = [];
  for(const a of iter){
    if (f(a)) res.push(a);
  }
  return res;
});

const reduce = curry((f, acc, iter) => {
  if(!iter){
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for(const a of iter){
    acc = f(acc, a)
  }
  return acc;
});


// go(
//    products,
//   products => filter(p => p.price < 20000)(products),
//    products => map(p => p.price)(products),
//    prices => reduce(add)(prices),
//    console.log
//   ); // 30000  
  
go(
   products,
   filter(p => p.price < 20000),
   map(p => p.price),
   reduce(add),
   console.log
  ); // 30000

기존에 만들었던 map, filter, reduce에도 curry를 적용했다. 하여 이 세 함수가 인자를 하나만 받으면 이후 인자를 더 받기로 기다리는 함수를 리턴하도록 되어있다. go() 함수를 살펴보면 기존에 함수(filter, map, reduce)를 첫번째 인자로 받고 두번째 인자에 데이터(products)를 받았는데, curry() 함수를 통해서 (map or filter or reduce)(products or price) 형식으로 첫번째 인자를 먼저 실행하면서 두번째 인자를 이후에 실행할 수 있다. 

또한, products를 받아서 함수(map or filter or reduce)에 그대로 products를 전달한다는 것은 해당하는 함수가 products를 받는다는 이야기고 products => (map or filter or reduce())(product) 함수가 products를 받는다는 얘기는 products를 제외하고 map or filter or reduce() 만으로 사용할 수 있다.

 

 

# 함수 조합으로 함수 만들기

const total_price = pipe(
  map(p => p.price),
  reduce(add));
)

const base_total_price = predi => pipe(
  filter(predi);
  total_price;
)

go(
   product,
   base_total_price(p => p.price < 20000),
   console.log
  ); // 30000
  
go(
   product,
   base_total_price(p => p.price >= 20000),
   console.log
  ); // 75000

 

total_price라는 pipe 함수를 만들어서 가격의 축약된 값을 도출하는 함수를 만들어 중복을 제거한다. 또, base_total_price라는 predi 함수를 전달 받는 pipe 함수를 만든다. 이 함수의 경우는 predi함수로 전달받은 함수를 filter 함수의 인자로 pipe를 리턴하는 함수이다. 이러한 방식들로 고차함수들을 통해 함수의 조합으로 만들어가면서 함수를 잘게 나누면서 중복을 제거하며 더 많은 곳에서 사용할 수 있다.

 

 

[JavaScript] 함수형 프로그래밍 (map, filter, reduce)

 

 

 

- 데이터

const products = [
  { name: '반팔티', price: 15000 },
  { name: '긴팔티', price: 20000 },
  { name: '핸드폰케이스', price: 15000 },
  { name: '후드티', price: 30000 },
  { name: '바지', price: 25000 }
];

 

 

# map

const map = (f, iter) => {
  let res = [];
  for (const a of iter) {
    names.push(f(a));
  }
  return res;
}

함수의 인자로 받은 iter를 통해 순회를 하고 새로운 배열의 형태를 만든다. 첫번째 인자 f는 이터러블의 어떤 값을 수집할 것인지 추상화한다. 그렇기때문에 받아온 함수(f)를 통해서 어떤 값을 수집할 것인지 f 함수에게 위임한다.

 

let names = [];
for (const a of products){
  names.push(p.name);
}

console.log(names); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]
console.log(map(p => p.name, products)); // ["반팔티", "긴팔티", "핸드폰케이스", "후드티", "바지"]


let prices = [];
for (const a of products){
  prices.push(p.price);
}
console.log(prices); // [15000, 20000, 15000, 30000, 25000];
console.log(map(p => p.price, products)); // [15000, 20000, 15000, 30000, 25000];

함수형 프로그래밍에선 map이라는 함수의 보조함수를 통해서 이터러블 안에 있는 수집하고자 하는 어떠한 값을 수집할 수 있다. map 함수는 함수를 값으로 다루면서 원하는 시점에 인자를 적용하는 고차함수이다.

 

 

# 이터러블 프로토콜을 따른 map의 다형성

console.log([1, 2, 3].map(a => a + 1)); // [2, 3, 4]
console.log(document.querySelectorAll('*')); // undefined

[1, 2, 3]은 배열(Array)이고 이터러블이다. 하지만 querySelectorAll(NodeList)은 이터러블이긴 하지만 배열은 아니므로 Array.prototype을 상속하고 있지 않다. 따라서 map함수를 사용할 수 없다.

 

console.log(map(el => el.nodeName, document.querySelectorAll('*')));

const it = document.querySelectorAll('*')[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

querySelectorAll은 이터러블이므로 [Symbol.iterator]() 메서드를 가지며 next()메서드로 값을 순회할 수 있다.

 

function* gen(){
  yield 2;
  if(false) yield 3;
  yield 4;
}

console.log(map(a => a * a, gen())); // [4, 16]

querySelectorAll 말고도 이터러블을 만족하는 것이라면 위에서 만들어놓은 map함수를 사용할 수 있다. gen()이라는 제너레이터 함수 또한 map으로 순회 가능하다. 이터러블 프로토콜을 따르는 이미 만들어져있는 이터러블에 map을 사용할 수 있지만 gen() 제너레이터 함수의 결과(문장)에 대해서도 map을 사용하여 순회할 수 있다. 그렇다는 것은 사실상 모든것들에 대해 map을 할 수 있다.

 

앞으로 만들어지는 WEB API 자바스크립트를 포함한 브라우저에서 사용되는 값들(querySelectorAll) 또한 ECMAScript의 이터러블 프로토콜을 따르고 있기때문에 계속해서 이러한 형태로 만들어질 것이다. 이터러블 프로토콜을 따르는 함수를 사용하는 것은 앞으로의 많은 다른 함수와의 조합성이 좋아진다. 훨씬 더 유연하고 다형성이 있는 기법을 사용할 수 있을것이다.

 

- Map

let m = new Map();
m.set('a', 10);
m.set('b', 20);

console.log(map(([k, a]) => [k, a * 2], m)); // [ ["a", 20], ["b", 40] ]
console.log(new Map(map(([k, a]) => [k, a * 2], m))); // { "a" => 20, "b" => 40 }

처음에 만든 Map객체와 동일하게 내부적으로 로직처리를 하고 난 후(*2)의 새로운 Map 객체를 map함수를 통해서 만들 수 있다.

 

 

# filter

const filter = (f, iter) => {
  let res = [];
  for(const a of iter){
    if (f(a)) res.push(a);
  }
  return res;
}

map과 형태는 동일하다. 하지만 첫번째 인자로 받는 함수(f)를 가지고 어떤 조건을 가지고 로직을 처리할 것인지에 대해 체크하는 부분을 받아온 함수에 위임한다.

 

let under20000 = [];
for (const a of products){
  if(p.price < 20000) under20000.push(p);
}
console.log(...under20000); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

console.log(...filter(p => p.price < 20000, products)); // { name: "반팔티", price: 15000 } { name: "핸드폰케이스", price: 15000 }

let over20000 = [];
for(const a of products){
 if(p.price >= 20000) over20000.push(p);
}
console.log(...over20000); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

console.log(...filter(p => p.price >= 20000, products); // { name: "긴팔티", price: 20000 } { name: "후드티", price: 20000 } { name: "바지", price: 25000 }

내부에 있는 값에대한 다형성은 첫번째 인자로 받은 보조함수를 통해서 지원을 해주고 외부의 경우는 두번 째 인자로 받은 이터러블 프로토콜을 따르는 인자를 통해서 다형성을 지원해줄 수 있다.

 

console.log(filter(n => n % 2, [1, 2, 3, 4])); // [1, 3]
console.log(fitler(n => n % 2, function* () {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}())); // [1, 3, 5]

filter함수 역시 이터러블 프로토콜을 따르고 함수형적인 코딩을 통해서 중복을 제거하고 구현할 수 있다.

 

 

# reduce

const reduce = (f, acc, iter) => {  
  for(const a of iter){
    acc = f(acc, a)
  }
  return acc;
}

reduce는 값을 축약하는 함수이다. 특정한 값들을 순회하면서 하나의 값으로 누적할 때 사용하는 함수이다. 

 

const add = (a, b) => a + b;

console.log(reduce(add, 0, [1, 2, 3, 4, 5]); // 15

// console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5)); 형태로 실행된다.

reduce 함수는 첫번째 인자로 로직을 처리할 함수, 두번째 인자로 초기값, 세번째 인자로 이터러블을 받는다.

 

console.log(reduce(acc, [1, 2, 3, 4, 5]); // 15

초기 값을 전달하지 않아도 reduce 함수는 똑같은 값을 도출하는데 그 이유는

 

const reduce = (f, acc, iter) => {
  if(!iter){
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for(const a of iter){
    acc = f(acc, a)
  }
  return acc;
}

위의 if문의 로직과 같이 reduce 함수의 파라미터의 초기값을 누락하고 호출하게되면 reduce 내부의 로직에서 acc에 들어온 이터러블의 [Symbol.iterator] 메서드를 실행하고 초기 값에 iter 이터러블의 next()를 실행한 값을 넘겨주기때문에 초기값이 없더라도 같은 값으로 실행된다.

 

console.log(reduce(
  (total_price, product) => total_price + product.price, 
  0, 
  products)); // 105000

reduce는 보조함수를 통해서 축약하는 것을 완전히 위임하기 때문에 숫자나 숫자 배열 뿐만아니라 객체 등의 복잡한 값들을 특정한 값으로 축약할 수 있다. reduce도 역시 보조함수를 통해서 안쪽에 있는 값의 다형성을 지원해주며 이터러블을 통해서 외부값의 다형성도 지원한다.

 

 

# map, filter, reduce

const add = (a, b) => a + b;

console.log(
  reduce(
    add, 
    map(p => p.price, 
      filter(p => p.price < 20000, products))));
      
console.log(
  reduce(
    add, 
    filter(n => n >= 20000, 
      map(p => p.price, products))));

map과 filter, reduce 함수가 중첩되어있는 함수이다. 이렇게 작성된 함수들은 뒤부터 읽으면 쉽게 읽을 수 있다. 함수를 작성할 때 내부에서 사용하려는 함수와 외부에서 평가될 이터러블의 값을 유추하면서 작성한다.

 

 

[JavaScript] 함수형 프로그래밍 (제너레이터, 이터레이터)

 

 

 

# 제너레이터 / 이터레이터

 - 제너레이터: 이터레이터이자 이터러블을 생성하는 함수

function* gen(){
  yield 1;
  if(false) yield 2;
  yield 3;
}

let iter = gen();
// console.log(iter[Symbol.iterator]() == iter); // true
console.log(iter.next()); // { value: 1, done: false } 
console.log(iter.next()); // { value: 3, done: false } 
console.log(iter.next()); // { value: undefined, done: true } 
console.log(iter.next()); // { value: undefined, done: true } 

for(const a of iter) console.log(a); // 1 3

제너레이터는 이터러블과 이터레이터의 조건을 만족한다. 따라서 제너레이터 함수의 객체를 이터러블을 만족하는 작업 없이도 for...of 문으로 순회 할 수 있다. 또한 제너레이터는 순회할 값을 문장(yield)으로 표현할 수 있다. 어떠한 값이든 이터러블이면 순회할 수 있고, 제너레이터는 문장을 값으로 만들수 있고 문장을 통해 순회할 수 있는 값을 만들 수 있기때문에 어떠한 상태나 어떠한 값이든 순회할 수 있도록 만들 수 있다.

 

이 점은 함수형 프로그래밍의 관점에서 중요하다. 자바스크립트는 다형성이 높다고 할 수있다. 제너레이터라는 문장을 통해 순회할 수 있는 값을 만들 수 있다는 것은 어떠한 값도 순회할 수 있는 형태로 동작할 수 있고 다양한 값을 순회하는 이터러블을 만들 수 있다.

 

 

# odds

function* infinity(i = 0){
  while(true) yield i++;
}

function* limit(l, iter){
  for (const a of iter){
    yield a;
    if(l == a) return;
  }
}

function* odds(l) {
  for (const a of limit(l, infinity(1))){
    if (a % 2) yield a;
  }
}

let iter = odds(10);
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 3, done: false }
iter.next(); // { value: 5, done: false }
iter.next(); // { value: 7, done: false }
iter.next(); // { value: 9, done: false }
iter.next(); // { value: undefined, done: true }
iter.next(); // { value: undefined, done: true }

for(const a of odds(40)) console.log(a); // 1, 3, 5 ... 39

제너레이터를 이용해서 홀수만을 출력하는 함수를 만들었다. infinity() 제너레이터함수의 경우는 무한하는 while 문에서 i의 값을 1씩 증가시킨다. 겉으로 보기엔 함수를 실행하면 브라우저가 다운되겠지만 제너레이터 함수의 yield를 통해서 하나씩 값을 실행하고 실행을 중지 시키기때문에 그렇게 되지는 않는다.

 

limit() 제너레이터함수는 파라미터로 숫자와 이터러블을 만족하는 값을 받아온다. 받아온 이터러블을 순회하면서 인자로 받은 숫자와 이터러블의 값이 같아질 때 리턴하여 done: true를 반환한다.

 

odds() 제너레이터 함수는 limit() 제너레이터함수와 infinity() 제너레이터함수를 사용하여 홀수 값을 가져온다. limit과 infinity는 이터러블 프로토콜을 만족하므로 for...of를 순회하는 이터러블 값으로 사용될 수 있다.

 

 

# for of, 전개 연산자, 구조 분해 할당, 나머지 연산자

console.log(...odds(10)); // 1 3 5 7 9
console.log([...odds(10), ...odds(20)]); // [1, 3, 5, 7, 9, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

const [head, ...tail] = odds(5);
console.log(head); // 1
console.log(tail); // [3, 5]

const [a, b, ...rest] = odds(10);
console.log(a); // 1
console.log(b); // 3
console.log(rest); // [5, 7, 9]

자바스크립트에서는 이터러블 / 이터레이터 프로토콜을 가지고 활용할 수 있는 문법들과 기능들이 많다. 또한 많은 라이브러리나 함수들도 이터러블 프로토콜을 따르도록 구성이 되어있다면 제너레이터와 이터레이터를 활용하여 좀 더 조합성이 높은 프로그래밍을 할 수 있다.

[JavaScript] 함수형 프로그래밍 (이터러블, 이터레이터)

 

 

 

ES6부터 리스트를 순회하는 방법의 차이가 생겼다.

console.log('Array');
const arr = [1, 2, 3];
for(const a of arr) console.log(a); // 1 2 3

console.log('Set');
const set = new Set([1, 2, 3]);
for(const a of set) console.log(a); // 1 2 3

console.log('Map');
const map = ([['a', 1], ['b', 2], ['c', 3]]);
for(const a of map) console.log(a); // ["a", 1] ["b", 2] ["c", 3]
for(const a of map.keys()) console.log(a); // a b c
for(const a of map.values()) console.log(a); // 1 2 3
for(const a of map.entries()) console.log(a); // ["a", 1] ["b", 2] ["c", 3]

 

 

# 이터러블/이터레이터 프로토콜

 - 이터러블: 이터레이터를 리턴하는 [Symbol.iterator]() 를 가진 값

 - 이터레이터: { value, done } 객체를 리턴하는 next() 를 가진 값 (리절트 객체)

 - 이터러블 / 이터레이터 프로토콜: 이터러블을 for...of, 전개 연산자, 구조 분해 할당 등과 함께 동작하도록 만든 규약

arr[Symbol.iterator] // f values() { [native Code] }

let iterator = arr[Symbol.iterator]();
iterator.next() // { value: 1, done: false} 
iterator.next() // { value: 2, done: false} 
iterator.next() // { value: 3, done: false} 
iterator.next() // { value: undefined, done: true}

 

 

# 사용자 정의 이터러블을 통해 알아보기

const iterable = {
  [Symbol.iterator](){
    let i = 3;
    return {
      next(){
        return (i === 0) ? {done: true} : { value: i--, done: false };
      }
    }
  }
}

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // TypeError: iterator is not iterable

이터러블 / 이터레이터 프로토콜을 사용하기위해 사용자가 정의할때는 위의 [Symbol.iterator]() 메서드를 정의하고 [Symbol.iterator]() 메서드는 next() 메서드를 반환해야하며 next() 메서드의 내용으로는 { value, done } 을 가지는 객체를 반환해야 한다.

 

하지만 위의 이터러블 / 이터레이터는 완벽하지 않다. Array나 Set, Map 등 다른 이터러블 프로토콜을 만족하는 것들의 내부를 보면 이터러블 프로토콜을 만족하면서 이터레이터 프로토콜을 만족한다. 그러한 이터러블 프로토콜을 well-formed 이터러블 프로토콜 이라고 한다.

 

const iterable = {
  [Symbol.iterator](){
    let i = 3;
    return {
      next(){
        return (i === 0) ? {done: true} : { value: i--, done: false };
      },
      [Symbol.iterator]() { return this; }
    }
  }
}

let iterator = iterable[Symbol.iterator]();
iterator.next();
for(const a of iterable) console.log(a); // 2 1
for(const a of iterator) console.log(a); // 2 1

well-formed 이터러블 프로토콜은 이터러블을 for...of 문으로 순회를 해도 순회가되고 이터레이터로 만들어서 for...of 문으로 순회해도 순회가 된다. 또한 일정부분 next() 메서드를 통해 진행을 하고 순회를 해도 마찬가지로 순회가 된다.

 

console.log(document.querySelectorAll('*')) // NodeList(...) [...]
for(const a of document.querySelectorAll('*')) console.log(a); // <html>...</html> <head>...</head> ...
const all = document.querySelectorAll('*');
let iter = all[Symbol.iterator]();
iter.next(); // { value: html, done: false }
iter.next(); // { value: head, done: false }
iter.next(); // { value: script, done: false }

해당하는 이터러블 프로토콜은 오픈소스 라이브러리 뿐만 아니라 브라우저의 DOM 또한 이터러블 프로토콜을 만족한다.

 

 

# 전개연산자

const a = [1, 2];
// a[Symbol.iterator] = null; // null이라면 아래 코드 TypeError
console.log([...a, ...arr, ...set, ...map.keys()]); // [1, 2, 1, 2, 3, 1, 2, 3, "a", "b", "c"]

자바스크립트에서 ES6의 이터러블 / 이터레이터 프로토콜은 중요하다. 해당하는 프로토콜을 만족하면 기존에 ES5에서 사용하던 자료구조보다 더 다양하고 쉬운 방식으로 데이터들을 핸들링 할 수 있다.

[JavaScript] 함수형 프로그래밍

 

 

 

# 평가

 - 코드가 계산되어 값으로 만들어지는 것

 

# 일급

 - 값으로 다룰 수 있다.

 - 변수에 담을 수 있다.

 - 함수의 인자로 사용될 수 있다.

 - 함수의 결과로 반환될 수 있다.

 

# 일급 함수

 - 함수를 값으로 다룰 수 있다.

 - 조합과 추상화의 도구

 

자바스크립트에서 함수는 일급이다. 변수에 함수를 값으로 담을 수 있고, 함수의 인자로 함수를 전달 할 수 있다. 또 함수를 평가해서 값으로 다른 함수에도 전달 할 수 있다.

const add5 = a => a + 5;
console.log(add5); // a => a + 5
console.log(add5(5)); // 10

const f1 = () => () => 1;
console.log(f1()); // () => 1

const f2 = f1();
console.log(f2); // () => 1
console.log(f2()); // 1

 

 

# 고차 함수

 - 함수를 값으로 다루는 함수

 

고차함수는 두가지로 나눠진다.

 

 

## 함수를 인자로 받아서 실행하는 함수

const apply1 = f => f(1);
const add2 = a => a + 2;
console.log(apply1(add2)); // 3 -> (a => a + 2)(1);
console.log(apply1(a => a - 1)); // 0

const times = (f, n) => {
  let i = -1;
  while(++i) f(i);
}

times(console.log, 3); // 0 1 2
times(a => console.log(a + 10), 3); // 10 11 12

함수를 인자로 받고 받은 인자를 가지고 내부적인 동작을 실행하며 원하는 인자를 적용하는 함수이다.

 

 

## 함수를 만들어 리턴하는 함수 (클로저를 만들어 리턴하는 함수)

const addMaker = a => b => a + b;
const add10 = addMaker(10);
console.log(add10); // b => a + b;
console.log(add10(5)); // 15
console.log(add10(10)); // 20

addMaker 함수는 함수를 리턴하는 함수이며, 클로저를 만들어 리턴한다. b => a + b 함수는 함수이자 a를 기억하는 클로저이다. 그러므로 addMaker는 클로저를 리턴하는 함수이고 함수가 함수를 만들어서 리턴할 때는 결국엔 클로저를 만들어서 리턴하는 방식으로 사용한다.

 

[JavaScript] ES6 chap.3

 

 

 

Destructuring

* Array

// Array
var array = [2,3,4];
var a = array[0]; 
var b = array[1];
var c = array[2];

// ==
var [a,b,c] = [2,3,4];

// default 값
var [a,b,c = 5] = [2,3];

 

 

* Object

var { name : a, age : b } = { name : 'Kim', age : 30 }; // { a: 'Kim', b: 30 }

// key 생략
var { name, age } = { name : 'Kim', age : 30 }; // { name: 'Kim', age: 30 }

 

- 변수를 Object에 넣기

var name = 'Kim';
var age = 30;

var obj = { name : name, age : age } // { name: 'Kim', age: 30 }
// == //
var obj = { name, age } // { name: 'Kim', age: 30 }

 

 

* 함수

- Object

function 함수(name, age){
  console.log(name);
  console.log(age);
}

var obj = { name : 'Kim', age : 20 }
함수(obj.name, obj.age);


// destructuring
function 함수( { name, age }){
  console.log(name);
  console.log(age);
}

var obj = { name : 'Kim', age : 20 };
함수(obj);

 

 

- Array

function 함수( name, age ){
  console.log(name);
  console.log(age);
}

var array = [ 'Kim', 30 ];
함수(array[0], array[1]);


// destructuring
function 함수( [name, age] ){
  console.log(name);
  console.log(age);
}

var array = [ 'Kim', 30 ];
함수( ['Kim', 30] );

 

 

 

import & export

* default export

// import할 곳
<script type="module">
  import a from 'library.js';
  console.log(a);
</script>


// export할 곳
var a = 10;
export default a;

- export default는 한번만 사용이 가능하다.

- import 시에 변수명을 새롭게 작명이 가능하다.

 

 

* export

// import할 곳
<script type="module">
  import {a,b} from 'library.js';
  console.log(a);
</script>

// export할 곳
var a = 10;
var b = 20;
export {a, b};

- export는 {변수명1, 변수명2} 이렇게 사용해야하고 import 할때도 정확한 변수명을 써줘야한다.

 

 

* default export & export 동시사용

// import할 곳
<script type="module">
  import c, {a,b} from 'library.js'; // 순서는 default가 먼저 온다.
  console.log(c);
</script>

// export할 곳
var a = 10;
var b = 20;
var c = 30;
export {a, b};
export default c;

** default export로 설정한 변수가 export보다 먼저 와야한다.

 

 

* 알리아스(as)

// import할 곳
<script type="module">
  import c as 폭풍, {a as 간지, b as 폭발} from 'library.js'; // 순서는 default가 먼저 온다.
  console.log(폭풍); // 30
  console.log(간지); // 10
  console.log(폭발); // 20
</script>

// export할 곳
var a = 10;
var b = 20;
var c = 30;
export {a, b};
export default c;

as라는 키워드를 통해 새로운 변수명을 지정할 수 있다.

 

 

* 아스테리스크(*)

// import할 곳
<script type="module">
  import c, {* as 변수모음} from 'library.js'; // 순서는 default가 먼저 온다.
  console.log(a); // 10
  console.log(c); // 30
</script>

// export할 곳
var a = 10;
var b = 20;
var c = 30;
export {a, b};
export default c;

export 했던 모든 변수들을 지정할 수 있다.

** 그냥쓰면 안되고 꼭 as를 통해 변수명을 새로 지정해주어야 한다.

 

 

 

Stack & Queue

* Stack과 Queue의 동작

브라우저는 C++ 기반으로 짜여져 자바스크립트 코드를 발견하면 stack에 넣어 돌린다. 동기식 처리로 맨 윗줄부터 하나하나 실행시키는 공간이다. 하지만 ajax 요청이나 이벤트리스너, setTimeout 등의 코드를 만나게되면 처리하기까지의 시간이 오래 걸리기 때문에 Stack에서 제외하여 잠시 보류시킨다. 보류시키는 동안에 Stack에 있는 다른 처리들을 먼저 처리하고 보류되어있던 코드들은 실행 시점이 다 되었을 때 Queue에 집어 넣고 Queue에 있는 코드를 Stack에 옮겨서 처리하는데 Stack이 비어있을 때만 차례로 집어넣어 처리한다.

 

때문에 자바스크립트의 스택에 너무 무겁거나 많은 데이터가 쌓여있으면 Queue를 통해서 오는 ajax 요청이나 이벤트리스너 setTimeout 등의 코드를 실행 할 수 없다. (Stack이 10초간 실행중이면 해당 코드들은 실행 시점이 와도 실행이 되지 않음)

 

프로그램을 무겁게 돌려야 할 시

1. setTimeout을 통해 분할해서 실행한다.

2. Web worker를 이용한다.

 

 

 

 

 

Synchronous / Asynchronous

* Synchronous

- 동기식 처리: 한번에 코드 한줄씩 차례차례 실행

<script>
  console.log(1); 
  console.log(2); 
  console.log(3);
  // 1 2 3
</script>

 

 

- 비동기식 처리: 먼저 시작된 코드와 상관없이 새로운 코드를 실행

// python
print(1)
print(2)
time.sleep(1)
print(3)
// 1 2 1초쉬고 3

<script>

  console.log(1);
  setTimeout(function(){console.log(2);}, 1000);
  console.log(3);
  // 1 3 1초쉬고 2

</script>

 

 

브라우저는 비동기처리를 하는 코드들을 제쳐두고 원래 코드부터 실행한다. (ex. ajax, setTimeout, addEventListener)

 

코드를 실행하다가 ex에 해당하는 코드를 보게되면 Web API 대기실로 옮겨서 대기 시키다가 실행시점이 도달했을 때 코드를 꺼내서 실행시킨다. 때문에 시간이 오래 걸리는 코드들을 비동기식으로 처리할 수 있다.

 

 

* 콜백함수

- 함수 안에 들어가는 함수. 비동기 상황 등에서 순차적으로 실행하고 싶을 때 사용한다. 어떤 이벤트가 발생했거나 특정시점에 도달했을 때 시스템에서 호출.

function 첫째함수(){
  console.log(1)
}

function 둘째함수(){
  console.log(2)
}

function 셋째함수(){
  console.log(3);
}

첫째함수(); // 비동기 시 실행순서 보장 못함
둘째함수(); 


// 콜백함수
function 첫째함수(콜백){
  console.log(1);  
  콜백();
}

function 둘째함수(콜백){
  console.log(2);
  콜백();
}

function 셋째함수(){
  console.log(3);
}

첫째함수(function(){
  둘째함수(function(){
    셋째함수(function(){
      ...
    });
  });
}); // 1 2 3

다른 프로그래밍 언어라면 순차적으로 출력되겠지만 자바스크립트의 경우는 비동기식 처리 함수가 있을 시 Web API 대기실로 보내져서 나중에 실행된다.

 

 

- callback hell

첫째함수(function(){
  둘째함수(function(){
    셋째함수(function(){
      어쩌구..
    });
  });
}):

 

 

 

Promise

- 콜백함수의 한 디자인 패턴 (성공&실패 판정)

var 프로미스 = new Promise(function(성공, 실패){
  var 연산 = 1 + 1;
  성공(연산);
});

프로미스.then(function(결과){
  // Promise 성공 시 실행할 코드
  // 성공 파라미터를 통해 결과 값을 받아올 수 있음
}).catch(function(){
  // Promise 실패 시 실행할 코드
}).finally(function(){
  // Promise 결과와 상관없이 실행할 코드
});

성공하면 then(), 실패하면 catch()를 실행하는 코드를 짤 수 있다.

 

: 장점

 - 콜백함수 사용시 코드가 옆으로 길어지지 않아 보기 직관적이다.

 - 성공뿐만 아니라 실패 또는 결과와 상관없이 실행할 코드를 지정할 수 있다. (catch, finally)

 

 

* Promise 특징

1. Promise를 선언한 변수를 출력하면 대기<pending>, 성공<resolved>, 실패<rejected>의 3단계로 나뉜다. (한번 정해진 상태를 되돌릴 수는 없다.)

 

2. 동기를 비동기로 만드는 것이 아니라 콜백 함수를 지정하는 하나의 디자인 패턴이다. (Promise 안에 어려운 연산을 시키면 브라우저가 멈춤)

 

 

 

async / await

- Promise와 then을 쉽게 쓸수 있게 만들어주는 ES8 문법

 

* async

async function 연산 (){ // 이 함수 자체가 Promise
  return 1 + 1;
}

연산().then(function(결과){ // then을 사용할 수 있음
  console.log(결과); // 2
})

Promise를 알아서 생성해주기 때문에 함수를 실행할 때 then을 붙일 수 있다. 또한 async 함수 안에서 나온 결과를 then 안에서 파라미터로 받아서 사용할 수 있다.

 

 

* await

async function 더하기(){
  var 어려운연산 = new Promise((성공, 실패)=>{
    var 결과 = 1 + 1;
    성공();
  });
  어려운연산.then(function(result){
    console.log(result);
  });
}
더하기();

/////////// 같은 결과 /////////////

async function 더하기(){
  var 어려운연산 = new Promise((성공, 실패)=>{
    var 결과 = 1 + 1;
    성공();
  });
  var 결과 = await 어려운연산;
  console.log(결과);
}
더하기();

////////// async만 사용 했을 때 //////////

async function 더하기(){
  async function 어려운연산(){                
    return 1 + 1;
  }
  var result = await 어려운연산();
  console.log(result);
}
더하기();

단순히 then 대신에 await을 사용했다. await을 사용하면 문법이 훨씬 더 간단하다. 어려운연산 Promise를 기다린 다음에 완료되면 결과를 변수에 담는다. ( ** 비동기식 처리되는 코드를 담는다면 await을 기다리는 동안 브라우저가 잠깐 멈출수 있다.)

 

 

async function 더하기(){
  var 어려운연산 = new Promise((성공, 실패)=>{
    실패();
  });
  try {  
    var 결과 = await 어려운연산 // 성공했을 시 실행할 코드
  }
  catch { 
    // 어려운연산 Promise가 실패할 경우 실행할 코드 
  }
}

await은 Promise가 실패하면 에러가 나고 코드가 멈추기때문에 하단의 코드들은 더 이상 실행되지 않지만 그것을 방지하기 위해 try{} catch{}를 실행하면 된다. try 문에 성공했을 시 수행할 코드, catch 문엔 실패했을 시 수행할 코드를 넣으면 된다.

 

 

 

반복문
  • for 반복문 (공통)
  • forEach() 반복문 (Array 전용)
  • for in 반복문 (Object 전용)
  • for of 반복문 (iterable 전용)

 

* for in 반복문

- Object 자료형에 저장된 자료들을 꺼내고 싶을 때 사용

var 오브젝트 = { name : 'Kim', age : 30 };

for (var key in 오브젝트) { // 오브젝트의 자료 2개 = 2번 반복
  console.log(오브젝트[key]); // key는 오브젝트의 key
}

: 특징

 1. enumerable한 것만 출력한다.

var 오브젝트 = { name : 'Kim', age : 30 };

console.log( Object.getOwnPropertyDescriptor(오브젝트, 'name') ); // 오브젝트 속성
// {value: "Kim", writable: true, enumerable: true, configurable: true}
// enumberable은 셀수 있는지의 여부인데 object는 생성 시 true로 되어있음.

enumerable이 true로 체크 되어있어야 for in 문에서 거르지 않는다.

 

 

2. 부모의 prototype에 저장된 것도 출력한다.

class 부모 {

}
부모.prototype.name = 'Park';

var 오브젝트 = new 부모();

for (var key in 오브젝트) {
  console.log(오브젝트[key]); // Park
}

부모가 가지고 있는 prototype도 출력한다.

 

class 부모 {
 
}
부모.prototype.name = 'Park';

var 오브젝트 = new 부모();

for (var key in 오브젝트) {
  if (오브젝트.hasOwnProperty(key)) {
    console.log(오브젝트[key]);
  }
}

해당 오브젝트에 hasOwnProperty() 함수를 사용하면 오브젝트가 직접 가지고 있는지 true or false로 체크 하여 사용할 수 있다.

 

 

* for of 반복문

- 여러가지 자료형에 사용할 수 있는 반복문 (Array, 문자, arguments, NodeList, Map, Set 등)

var 어레이 = [2,3,4,5];
for (var 자료 of 어레이) {
  console.log(자료);
}

: 특징

1. iterable한 자료형들에만 사용 가능하다.

var 어레이 = [2,3,4,5];
console.log( 어레이[Symbol.iterator]() ); // ~~~ iterator {} 속성이 있음
// 위의 방법으로 콘솔 출력시 iterator속성이 있으면 iterable하다고 한다.

 

 

 

 

 

Symbol
var 심볼 = Symbol('설명아무거나적기');

- Object 자료형에 비밀스러운 key값을 부여하고싶을 때 사용한다.

 

 

var person = { name : 'Kim' };
person.weight = 100;

var weight = Symbol('내 진짜 몸무게');
person[weight] = 200;

console.log(person);

: 특징

- for문에 등장하지 않는다.

- 특이한 이름을 가진 자료를 Object안에 만들고 싶을 때 사용한다.

 

// 직접 입력
var height = Symbol('내 키임');
var person = { name : 'Kim', [height] : 160 };


// 심볼은 값은 같아도 서로 다른 Symbol
var a = Symbol('설명1');
var b = Symbol('설명1');
console.log(a === b); // false


// 전역 심볼
var a = Symbol.for('설명1');
var b = Symbol.for('설명1');
console.log(a === b); // true


// 기본 내장 Symbol
ar array = [2,3,4];
console.log(array[Symbol.iterator]); // Array iterator {}...

 

 

 

 

Map & Set

* Map

- 자료의 연관성을 위해 사용한다.

var person = new Map();
person.set('name', 'Kim');
person.set([1,2,3], 'Kim'); // key 값에 어떤 자료도 가능하다
person.set('age', 20);
// Map {"name" => "Kim", "age" => 20}

 

 

- 사용법

var person = new Map();
person.set('age', 20);

person.get('age'); //자료 꺼내는 법
person.delete('age'); //자료 삭제하는 법
person.size; //자료 몇갠지 알려줌

//Map자료 반복문 돌리기
for (var key of person.keys() ){
  console.log(key)
}

//자료를 직접 집어넣고 싶으면

var person = new Map([
  ['age', 20],
  ['name', 'Kim']
]);

Array 같은 자료형에 대량의 데이터가 있을시 Hash Map, Hash Table 등을 사용한다. Array 같은경우 필요한 데이터를 사용 시에 반복문을 돌려 일일이 출력해봐야 아는데, Hash Table은 미리 abc순으로 정렬되어 있어서 빠르게 찾을 수 있다. Hash Table은 자료에 key 값을 부여해놓고 정렬하는 식으로 구성되어있다. Object 자료형이랑 비슷하다. Object는 hasOwnProperty, toString 키들도 집어넣을수 있어서 너무 유연하고, key 값이 문자형태로 제한되어 들어올 수 밖에 없는 차이가 있다.

 

 

* Set

- 자료를 Array처럼 일렬로 저장할 수 있다. (***중복 제거)

var 출석부2 = new Set([ 'john' , 'tom', 'andy', 'tom' ]);

console.log(출석부2); // Set { "jhon", "tom", "andy"}

Set은 중복된 자료를 허용하지 않는다.

 

 

var 출석부2 = new Set([ 'john' , 'tom', 'andy', 'tom' ]);

출석부2.add('sally'); //자료더하기 
출석부2.has('tom'); //자료있는지 확인
출석부2.size;  //자료 몇갠지 세기

var 출석부 = [ 'john' , 'tom', 'andy', 'tom' ];

var 출석부2 = new Set(출석부); //Array를 Set으로 바꾸기

출석부 = [...출석부2]  //Set을 Array로 바꾸기

 

 

 

Web Components

- 여러개의 태그들을 하나의 단어로 축약해서 사용할 수 있다

<custom-input name="이름"></custom-input>
<custom-input name="비번"></custom-input>

<script>
  class 클래스 extends HTMLElement {

      connectedCallback() { // html 태그 사용 할 부분
        let name = this.getAttribute('name'); // attribute 추가 해서 사용 가능
        this.innerHTML = '<label>${name}을 입력하쇼</label><input>'
      }
      
      static get observedAttributes() {
        return ['name'] // 여러개면 [ 속성, 속성...] 이런식으로 사용
      }
      
      attributeChangedCallback() {
        // attribute 변경시 실행할 코드
      }
  }

  customElements.define("custom-input", 클래스); // 컴포넌트 등록
</script>

<custom-input> 같은 커스텀 태그를 컴포넌트라고 한다. 해당 기능은 React나 Vue에서 제공하는 자동 html 재렌더링 기능처럼 구현한 것이다. 

 

 

* Shadow DOM과 template 모듈화

- Shadow DOM

<div class="mordor"></div>
<script>
  document.querySelector('mordor').attachShadow({mode : 'open'}); // shadow 공간 만들기
  document.querySelector('mordor').shadowRoot.innerHTML = '<p>심연에서왔도다</p>' // html 사용
</script>

해당 방법으로 shadow DOM을 만들 수 있고 shadow DOM은 web component와 사용시 유용하다.

 

 

: web component 문제점

<custom-input></custom-input>
<label>왜 나까지 빨개짐?</label>

<script>

  class 클래스 extends HTMLElement {
    connectedCallback() {
      this.innerHTML = `<label>이름을 입력하쇼</label><input>
        <style> label { color : red } </style>`
    }
  }

  customElements.define("custom-input", 클래스);
  
</script>

컴포넌트화할 때 스타일까지 포함하는 경우가 많은데 해당 컴포넌트를 제외한 다른 요소들까지 같이 적용될 수가 있다. 이럴때 스타일을 shadow DOM을 열어서 집어넣게 되면 shadow DOM안에 있는 스타일은 밖에까지 영향을 끼치지 않기때문에 html의 모듈화 개발이 가능하다.

 

 

- template 태그

해당 태그에 적힌 html은 렌더링 되지 않는다.

 <custom-input></custom-input>

  <template id="template1">
    <label>이메일을 입력하쇼</label><input>
    <style>label { color : red }</style>
  </template>

  <script>
    class 클래스 extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode : 'open'});
        this.shadowRoot.append(template1.content.cloneNode(true));
        let el = this.shadowRoot.querySelector('label');
        el.addEventListener('click', function(){ // 이벤트리스너 사용
          console.log('클릭함')
        })
      }
    }
    customElements.define("custom-input", 클래스);
  </script>

컴포넌트를 만들 때 html이 너무 길어지면 가독성이 떨어지니 template태그에 내용을 보관해두고 shodowRoot를 통해 집어 넣을 수 있다.

 

 

 

[JavaScript] ES6 chap.2

 

 

 

default parameter & arguments

- default parameter

function 더하기 (a, b = 10){ // b에 default 값을 부여한다. 연산이나 함수 사용 가능
  console.log(a + b)
}

더하기(1); // 11
더하기(1, 3); // 4

 

- arguments

function 함수(a,b,c){
  console.log(arguments);
}

함수(2,3,4); // [2, 3, 4]....arguments
function 함수(a,b,c){
  for(var i=0; i<arguments.length; i++){
  	console.log(arguments[i]); // 반복문으로 함수의 파라미터 조회
  }
}

함수(2,3,4);

 

 

 

Rest Parameter

ES6에 추가된 arguments랑 비슷한 용도의 파라미터 (나머지 파라미터)

function 함수2(...파라미터들){
  console.log(파라미터들)
}

함수2(1,2,3,4,5,6,7); // [1, 2, 3, 4, 5, 6, 7]

arguments와의 차이점은 arguments는 함수를 사용할 때의 파라미터의 갯수가 변경되면 기존 함수의 파라미터 갯수도 변경 해야하지만 rest parameter는 유동적으로 변경 가능하다.

 

function 함수2(a, b, ...파라미터들){
  console.log(파라미터들)
}

함수2(1,2,3,4,5,6,7); // [3, 4, 5, 6, 7]



// 주의
function 함수2(a, ...파라미터들, b){
  console.log(파라미터들)
} // ...파라미터 뒤에 또 다른 파라미터가 오면 에러 발생

function 함수2(a, ...파라미터들, ...파라미터들2){
  console.log(파라미터들)
} // rest 파라미터를 중복으로 써도 에러 발생

 

 

 

Primitive data type / Reference data type

* primitive data type

- 자료 자체가 변수에 저장되는 자료 (문자, 숫자, 문자열 등)

var 이름1 = '김';
var 이름2 = 이름1;
이름1 = '박';
console.log(이름1); // 박
console.log(이름2); // 김

 

 

* reference data type

- 자료가 변수가 아닌 컴퓨터 메모리에 저장되고 참조하여 사용하는 자료 (Array, Object 등)

var 이름1 = { name: '김'};
var 이름2 = 이름1;
이름1.name = '박';
console.log(이름1.name); // 박
console.log(이름2.name); // 박

primitive 타입과 달리 reference 타입은 데이터가 저장된 것이 아니라 참조한다는 값을 가리키는 값이 저장된 것이기 때문에 참조되는 데이터를 변경하게 되면 두 변수가 가리키는 값은 같다.

 

 

- cf

var 이름1 = { name : '김' };
var 이름2 = { name : '김' };

console.log(이름1 == 이름2); // false
console.log(이름1 === 이름2); // false
console.log(이름1.name == 이름2.name); // true

위 처럼 새로운 {}를 할당할 때, 새로운 object가 생성되기 때문에 {} 안에 있는 오브젝트 데이터는 같지만 서로 다른 object이며 다른 object를 각각 가리키고 있는 것이다.

 

var 이름1 = { name : '김' };

function 변경(obj){
  obj = { name : 'park' }; // obj라는 변수에 새로운 {} object 재할당
  obj.name = 'park' // 파라미터로 들어온 object의 name의 value 변경
}

변경(이름1);

 

 

 

constructor

기존의 object 생성 방식

var 학생1 = { name : 'Kim', age : 15 };
var 학생2 = { name : 'Park', age : 15 };
...

여러번 선언하고 정의하거나 복사를 해야하는데 복사하는데 있어서 데이터가 같은 값을 참조 할 수 있기 때문에 constructor 방식으로 object를 생성할 수 있다.

 

 

* constructor

function 기계(이름){ // 기계: constructor
  this.name = 이름; // instance
  this.age = 15; // instance
  this.sayHi = function(){ // instance
    console.log('안녕하세요' + this.name + ' 입니다');
  }
}
var 학생1 = new 기계('Park');
var 학생2 = new 기계('Kim');

function이라는 함수를 정의하고 해당 함수를 new 함수() 형태로 사용한다. 어떤 변수에 기계라는 함수를 new 기계()로 할당하게되면 기계는 constructor가 되며 기계() 안에 있는 this는 instance가 되어 여러가지의 object를 생성할 수 있다.

 

 

 

prototype

prototype = 유전자의 역할

function 기계(){
  this.name = 'Kim';
  this.age = 15;
}
var 학생1 = new 기계(); // 상속의 개념 => 기계의 name과 age를 사용할 수 있음

console.log(기계.prototype); // 내부에 prototype이라는 항목이 생성

학생1이 기계의 constructor가 가진 name과 age의 instance를 사용할 수 있게되는 것을 상속(inheritance)했다고 한다. 하지만 constructor말고도 상속기능을 구현할 수 있는데 Java와 같은 다른 언어의 상속(extend)의 개념과 달리 Javascript는 prototype의 개념을 사용한다.

 

new 기계()를 생성하게되면 표면적으로 보이지 않는 내부의 공간에 prototype이라는 공간이 생성되는데 이것은 부모의 유전자 역할을 한다.

 

 

function 기계(){
  this.name = 'Kim';
  this.age = 15;
}

기계.prototype.gender = '남';
var 학생1 = new 기계();
var 학생2 = new 기계();

console.log(학생1.gender); // 남
console.log(학생2.gender); // 남

기계라는 부모의 prototype(유전자)에 {gender: '남'} 이라는 속성을 추가하게되면 기계를 상속한 학생1, 학생2 자식은 {gender: '남'}이라는 속성을 사용할 수 있다.

 

 

* 작동원리

- 자바스크립트는 오브젝트에서 값을 출력할 때

  • 학생1에 gender라는 값이 있는가 ?
  • 기계라는 부모 유전자에 gender라는 값이 있는가 ?
  • 그럼 그 부모의 유전자에 gender라는 값이 있는가 ?
  • ...

 

// Array 생성 시
var arr = [1,2,3];
// == var arr = new Array(1,2,3);

// Object 생성 시
var obj = {name: 'kim'}
// == var obj = new Object({name: 'kim'});

Array나 Object 등의 생성 방식의 차이일 뿐 결국엔 내부에선 constructor 방식으로 생성되기 때문에 해당 object에 prototype으로 저장 되어있는 내장 함수 또한 사용할 수 있다.

 

 

** prototype과 constructor의 차이

  • constructor: 자식들이 직접 값을 소유하게 하기
  • prototype: 부모만 가지고 있고 그걸 참조해서 쓰게 하기

 

 

 

* 특징

1. prototype은 constructor 함수에만 몰래 생성된다.

 - 일반 object, array엔 prototype 없음 (constructor 함수, Object.create(), class 써야 함)

 

 

2. 부모 유전자 찾기

function 기계(){
  this.name = 'Kim';
  this.age = 15;
}
var 학생1 = new 기계();
console.log(학생1.__proto__); // 기계
console.log(기계.prototype);

 - 부모의 의해 생성된 자식엔 __proto__라는 속성이 있는데 해당 속성은 자식의 부모 object를 찾아준다.

 

 

3. __proto__를 직접 등록하면 object끼리 상속기능 구현가능

var 부모 = { name : 'Kim' };
var 자식 = {};

자식.__proto__ = 부모;
console.log(자식.name); // Kim

 

 

4. 자바스크립트는 모든게 다 Object이다.

- __proto__로 계속 올라가다보면 모든 부모는 Object라는 constructor로 통한다.

 

 

 

상속

* ES5

- Object.create()

var 부모 = { name : 'Kim', age : 50 };
var 자식 = Object.create(부모);
자식.age  = 20;

var 손자 = Object.create(자식);

console.log(손자.age); // 20 ==> 자식의 age

손자에서 age를 호출하면 자식의 속성에 age가 있는지 확인하고 있으면 그 것을 사용하지만 없다면 부모로 올라가서 age가 있는지 확인하는 순서이기 때문에 자식의 age 값은 20으로 되어있어 20이 출력된다.

 

 

* ES6

- class

class 부모 {
  constructor(){
    this.name = 'Kim';
    this.sayHi = function(){ console.log('hello') } // constructor안에 추가하기
  }
  sayHi = function(){ console.log('hello') } // prototype에 추가하기
  // == sayHi(){ console.log('hello') } // prototype에 추가하기
}

var 자식 = new 부모();

function말고도 constructor를 만드는 ES6의 방식. constructor 안에 있는 인스턴스를 만들어 사용할 수 있으며 그 외부에 만들면 prototype으로 사용할 수 있다.

 

 

* Object.getPrototypeOf()

함수 안에 object를 넣으면 부모 prototype을 출력할 수 있다. (__proto__랑 유사함)

 

 

 

extends & super
class 할아버지{
  constructor(name){
    this.성 = 'Kim';
    this.이름 = name;
  }
  sayHi(){
    console.log('안녕 나는 할아버지')
  }  
}

class 아버지 extends 할아버지{ // 할아버지라는 객체의 특성을 복사할 때 사용
  constructor(name){
    super(name); // 할아버지 객체의 속성들을 사용하기위해 선언
    this.나이 = 50;
  }
  sayHi2(){
    console.log('안녕 나는 아버지');
    super.sayHi();
  }
}

var a = new 아버지('만수');
a.sayHi2(); // 안녕 나는 아버지 안녕 나는 할아버지

 

 

할아버지 객체의 속성들을 아버지 객체에 복사하기 위해 extends를 사용한다. extends를 사용하게 되면 따라오는 super라는 예약어가 있는데 super는 부모 객체의 속성들에 접근하는 것이다.

 

super는 constructor 안에서 사용하면 부모(할아버지) class의 constructor이고 prototype 함수 안에서 쓰면 부모(할아버지) class의 prototype이다.

 

 

 

getter & setter

자바스크립트의 getter, setter는 오브젝트 내의 데이터의 무결성을 보존하기 위해 쓰는 키워드이다.

var 사람 = {
  name : 'Kim',
  age : 30,
  nextAge(){
    return this.age + 1  
  }
  setAge(나이){
    this.age = parseInt(나이);
  }
}

console.log(사람.nextAge()); // 31
console.log(사람.setAge(20)); // 20

: 함수로 데이터 꺼내오기

- object 안의 데이터가 복잡할수록 함수로 만들어 데이터를 꺼내기 쉽다.

- 내부에 있는 인스턴스 변수를 건드리지 않아 수정 시 실수를 방지할 수 있다.

 

: 함수로 데이터 수정하기

- 원본 데이터를 덮어쓰지 않고 카피 데이터로 관리할 수 있다.

- 내부에 있는 인스턴스 변수를 건드리지 않아 수정 시 실수를 방지할 수 있다.

 

 

* get, set 사용

var 사람 = {
  name : 'Kim',
  age : 30,
  get nextAge(){
    return this.age + 1  
  }
  set setAge(나이){
    this.age = parseInt(나이);
  }
}

console.log( 사람.nextAge );
console.log( 사람.setAge = 20 );

get, set을 이용하면 오브젝트 내의 함수들을 괄호 없이 사용할 수 있다.

 

 

* 사용하는 기준

- 데이터를 뽑거나 가져올 때는 get, 데이터를 입력하거나 수정할 때는 set을 쓰면 된다.

get: 함수에 파라미터가 있으면 안되며 함수 내에 return이 있어야 한다.

set: 함수는 입력을 받아 수정을 해줘야 하기 때문에 파라미터가 한개 이상 꼭 존재해야 한다.

 

class 사람 {
  constructor(){
    this.name = 'Kim';
    this.age = 30;
  }
  get nextAge(){
    return this.age + 1  
  }
  set setAge(나이){
    this.age = parseInt(나이);
  }
}

var 사람1 = new 사람();

console.log( 사람.nextAge );
console.log( 사람.setAge = 20 );

마찬가지로 class에서도 사용할 수 있다.

 

 

 

 

'JavaScript' 카테고리의 다른 글

[JavaScript] 함수형 프로그래밍 (일급함수, 고차함수)  (0) 2022.03.29
[JavaScript] ES6 chap.3  (0) 2021.12.02
[JavaScript] ES6 chap.1  (0) 2021.11.29
[JavaScript] Chap.2  (0) 2021.11.25
[JavaScript] Chap.1  (0) 2021.11.19

[JavaScript] ES6 chap.1

 

 

 

this

1. 아무데서 쓰거나 일반 함수에서의 this는 window

<script>

    console.log(this); // Window
    
    function 함수(){
    	console.log(this); 
    }
    
    함수(); // Window
    
</script>

 

** use strict **

<script>
    'use strict';

    console.log(this); // Window
    
    function 함수(){
    	console.log(this); 
    }
    
    함수(); // undefined
    
</script>

 

 

2. object 자료형 안의 함수에서 this값은 오브젝트 그 자체

<script>
    
    var 오브젝트 = {
        data: "kim",
        함수: function(){
            console.log(this);
        }
    }
    
    오브젝트.함수(); // { data : 'Kim', 함수 : f }
    
</script>

this값: 오브젝트 객체

 

<script>
    
    var 오브젝트2 = {
        data: {
          함수: function(){
              console.log(this);
          }
        }
    }
    
    오브젝트2.data.함수(); // { 함수 : f }
    
</script>

this값: data 객체

 

 

* window 객체

<script>

  (1)
  function 함수(){
    console.log(this)
  }

  (2)
  window.함수 = function(){
      console.log(this);
  }; <!-- window라는 오브젝트에 함수 자료를 추가 -->
  
  <!-- (2)와 같은 의미
  var window = {
    함수: function(){
      console.log(this);
    }
  }
  실질적으로 window로 객체 변수를 선언하면 에러 남-->
  
</script>

- 2번의 뜻은 1번의 뜻과 상통한다. script라는 태그 자체안에 window라는 큰 객체(object)가 있는것이다.

 

 

3. constructor(생성자) 안에서 쓰면 constructor로 새로 생성되는 오브젝트

<script>
       
    function 생성자(){
        console.log(this);
    }        

    new 생성자(); // 생성자 {}
        
</script>

this 값: 생성자 객체

 

 

<script>

    function 생성자or함수(){
        this.테스트 = "test";
        console.log(this);
    }        

    // 1번의 경우
    new 생성자or함수(); // 생성자or함수 {테스트: kim}
    
    // 3번의 경우
    생성자or함수(); // Window
    
</script>

- 함수로 사용 했을시엔 window 객체를 가리키지만 new를 통해 생성자(constructor)로 만들게 되면 해당 function의 이름으로 새로운 object 객체를 만든다.

 

 

4. eventListener 안에서 쓰는 this는 e.currentTarget

<script>

	document.getElementById('ID').addEventListener('click', function(e){
            // == e.currentTarget
            console.log(this); // document.getElementById('ID')            
	});
    
</script>

- 이벤트리스너 안에서의 this는 지금 이벤트가 동작하는 곳을 의미하며, e.currentTarget과 document.getElementById('ID')와 같은 의미이다.

 

 

** 가장 가까이 있는 함수에 따라 바뀌는 this

<script>

    document.getElementById('버튼').addEventListener('click', function(e){
        console.log(this); // document.getElementById('버튼') (4)
        var 어레이 = [1,2,3];
        어레이.forEach(function(){
            console.log(this); // window, window, window (1)
        });
    });

    var 오브젝트 = {
        이름들 : ['김', '이', '박'],
        함수 : function(){
            console.log(this); // 오브젝트 { 이름들: ['김', '이', '박'] } (2)
            오브젝트.이름들.forEach(function(){
                console.log(this); // window, window, window (1)
            });
        }
    }
    
    console.log(오브젝트.함수());
</script>

 

 

** function()과 arrow함수의 차이

<script>

    var 오브젝트 = {
        이름들 : ['김', '이', '박'],
        함수 : function(){
            console.log(this); // 오브젝트 { 이름들: ['김', '이', '박'] } (2)
            오브젝트.이름들.forEach(function(){
                console.log(this); // window, window, window (1)
            });
        }
    }

    console.log(오브젝트.함수());
    
    var 오브젝트2 = {
        이름들 : ['김', '이', '박'],
        함수 : function(){
            console.log(this); // 오브젝트 { 이름들: ['김', '이', '박'] } (2)
            오브젝트2.이름들.forEach( () => {
                console.log(this); // 오브젝트 { 이름들: ['김', '이', '박'] } (2)
            });
        }
    }
    
    console.log(오브젝트2.함수());

</script>

ES6에선 function(){} 대신 () => {} 라는 arrow function 문법이 있는데, 둘의 차이는 함수 내부의 this 값을 새로 바꿔주지 않기 때문에(=바깥에 있던 this값을 내부에서 그대로 사용) this를 사용하기 유용하다.

 

 

 

Arrow function
<script>

    var x = function(){} // 기존 함수
    
    var y = () => {} // arrow 함수

</script>

 

 

* 사용법

<script>

    var 두배만들기 = (x) => { return x * 2 }
    console.log( 두배만들기(4) );
    
    
    var 두배만들기 = x => x * 2 ;
    console.log( 두배만들기(8) );

</script>

- 파라미터가 하나면 중괄호 생략이 가능하고 return 값도 한 줄이면 중괄호와 return도 생략 가능하다.

 

 

* arrow function에서의 this

<script>

    var 오브젝트1 = {
        함수 : function(){ 
            console.log(this)
        }
    }

    오브젝트1.함수(); // { 오브젝트1: 함수f }
    
    
    var 오브젝트1 = {
        함수 : () => { console.log(this) }
    }

    오브젝트1.함수(); // window

</script>

arrow function은 외부에 있던 this를 그대로 내부로 가져와서 사용한다.

 

 

 

 

 

var, let, const

* var

  • 재선언 (o)
  • 재할당 (o)
  • 범위: function
var 이름 = 'kim';

이름 = 'lee' // (o)
var 이름 = 'kim'; // (o)

function(){
    var 테스트 = 'test';
    테스트 // (o)
}

테스트 // (x)

 

 

* let

  • 재선언 (x)
  • 재할당 (o)
  • 범위: {}
let 이름 = 'kim';

이름 = 'lee' // (o)
let 이름 = 'kim'; // (x)

if(1 == 1){
    let 테스트 = 'test';
    테스트 // (o)
}

테스트 // (x)

 

 

* const

  • 재선언 (x)
  • 재할당 (x)
  • 범위: {}
const 이름 = 'kim';

이름 = 'lee' // (x)
const 이름 = 'kim'; // (x)

if(1 == 1){
    const 테스트 = 'test';
    테스트 // (o)
}

테스트 // (x)

 

const 오브젝트 = { 이름 : 'Kim' }
오브젝트.이름 = 'Park'; //가능

const 변수를 쓴다고 하더라도 const로 선언된 변수의 자료 형이 object 형이라면 안에 있는 값을 변경 할 수 있다. 변경 불가능한 값을 만드려면 Object.freeze()라는 기본 함수를 사용하면 된다.

 

 

 

호이스팅, 클로저

- 호이스팅

<script>

    // var 이름;

    console.log(이름); // undefined

    var 이름 = 'Kim'; // 이름 = 'kim;
    
    console.log(이름); // kim
    
</script>

var 변수를 사용하게되면 (var 변수) 선언 부분은 맨 위로 올리고 (변수 = 값)만 그 자리에 남게되는데 이런것을 호이스팅이라고 한다. (함수 또한 호이스팅 가능하다.)

 

 

- 클로저

var 나이 = 20;

function 함수(){
  console.log(나이)
}

함수();

클로저는 바깥에 있는 변수는 안쪽에서 자유롭게 사용할 수 있다는 것을 의미한다. 이것을 참조가능하다라고도 한다. 함수 안쪽에 나이라는 변수가 있다면 그것을 사용하겠지만 없으면 바깥에 있는 변수를 사용한다.

 

 

 

Template literals

따옴표 대신 backquote, backtick 이라는 '`' 기호를 사용하여 문자를 만드는 법이다.

: 장점

  • 문자 중간 엔터키 입력가능
  • 문자 중간 변수를 집어넣을 때 편리
var 문자 = `안녕

하세요`;
var 이름 = '손흥민';
var 문자 = `안녕하세요 ${이름} 입니다`;

 

 

* Tagged Literals

 - 문자 해체분석

var 변수 = '손흥민';

function 해체분석기(문자들, 변수들){
  console.log(문자들);
  console.log(변수들);
}

해체분석기`안녕하세요 ${변수} 입니다`; // () 대신에 이런식으로 사용 가능하다.
// 문자열 값: [안녕하세요, 입니다]
// 변수 값: 손흥민

 

 

 

spread operator

- 괄호를 제거해주는 연산자 (함수/소괄호, 오브젝트/중괄호, 어레이/대괄호에서 사용)

var 어레이 = ['hello', 'world'];
console.log(어레이); // ['hello', 'world']
console.log(...어레이); // hello world

 

- 문자의 경우

var 문자 = 'hello';
console.log(문자[0]); // h
console.log(문자[1]); // e
console.log(문자); // hello
console.log(...문자); // h e l l o

 

 

* 합치기 / 복사

// Array
var a = [1,2,3];
var b = [4,5];
var c = [...a, ...b]; // [1, 2, 3, 4, 5]

// Object
var o1 = { a : 1, b : 2 };
var o2 = { c : 3, ...o1 }; // { a: 1, b: 2, c: 3}

: 합치기

 

var a = [1,2,3];
var b = a;

console.log(a);
console.log(b);

var object1 = {a: 1, b: 2};
var object2 = object1;

console.log(object1);
console.log(object2);

: 복사

등호(=)를 이용하여 쉽게 Array와 Object의 값을 복사 할 수는 있지만 참조 값 타입의 변수를 등호(=) 방식으로 복사 하게되면 값 공유가 일어난다. 때문에 복사되어진 값과 복사한 값의 값 변경이 같이 일어나기 때문에 올바른 복사 방식은 아니다.

 

var a = [1,2,3];
var b = [...a];

console.log(a);
console.log(b);

var object1 = {a: 1, b: 2};
var object2 = {...object1};

console.log(object1);
console.log(object2);

: spread를 통한 복사 방식

값 변경이 일어나도 서로의 값에 영향을 주지 않는다.

 

 

* 만약 spread를 통한 복사를 했을 때, 중복되는 값이 있다면 ??

var o1 = { a : 1, b : 2};
var o2 = { a : 3, ...o1 };
console.log(o2); // {a: 1, b: 2}

중복된 값이 있다면 기존의 값에 새롭게 들어온 값으로 저장된다.

 

 

 

apply, call

array를 파라미터 형태로 집어넣고 싶을 때 사용한다.

function 더하기(a,b,c){
   console.log(a + b + c)
}

더하기(1,2,3); // 6

var 어레이 = [10, 20, 30];

더하기(어레이[0], 어레이[1], 어레이[2]); // 60 주먹구구
더하기.apply(undefined, 어레이); // 60 옛날방식
더하기(...어레이); // 60 요즘방식

 

var person = {
    인사 : function(){
      console.log(this.name + '안녕')
    }
}
  
var person2 = {
    name : '손흥민'
}

person.인사.apply(person2);
person.인사.call(person2);

어떤 함수(person)를 실행하는데 다른 오브젝트(person2)에다가 적용해서 실행할 때 사용한다.

 

person.인사.apply(person2, [1,2,3]); // array 형태
person.인사.call(person2, 1,2,3); // 일반 형태

apply와 call의 사용법과 결과는 똑같지만 함수에 파라미터를 넣어서 실행할 때, apply는 array[] 형태로, call은 일반 함수처럼 파라미터를 전달한다.

 

 

 

 

 

'JavaScript' 카테고리의 다른 글

[JavaScript] 함수형 프로그래밍 (일급함수, 고차함수)  (0) 2022.03.29
[JavaScript] ES6 chap.3  (0) 2021.12.02
[JavaScript] ES6 chap.2  (0) 2021.11.30
[JavaScript] Chap.2  (0) 2021.11.25
[JavaScript] Chap.1  (0) 2021.11.19

+ Recent posts