본문 바로가기
Study Note/Javascript

javascript #함수형 자바스크립트 프로그래밍 - 1~2장 기록

by 시뮝 2021. 11. 29.
728x90

1장 함수형 자바스크립트 소개

함수형 프로그래밍 특징

  • page 6. 불변성
  • page 18. 객체지향 프로그래밍이 약속된 이름이 메서드를 대신 실행해 주는 식으로 외부 객체에게 위임을 한다면, 함수형 프로그래밍은 보조형 함수를 통해 완전히 위임해주는 방식을 취한다. 이는 더 높은 다형성과 안정성을 제공한다.
  • page 20. findIndex 함수 예시
  •  
  • function findIndex(list, predicate) { for (let i = 0, i = list.length; i < len; i++) { if (predicate(list[i])) return i; } return -1; }

함수를 잘 다루기 위해 필수로 알아야 하는 기능들

  • 고차 함수
    • 함수를 인자로 받거나 함수를 리턴하는 함수 보통 고차 함수는 함수를 인자로 받아 필요한 때에 실행하거나 클로저를 만들어 리턴한다.
  • 일급 함수
    • 자바스크립트에서 함수는 일급 객체이자 일급 함수이다. 자바스크립트에서 객체는 일급 객체다. 여기서 '일급'은 값으로 다룰 수 있다는 의미로, 아래와 같은 조건을 만족해야 한다.
      • 변수에 담을 수 있다.
      • 함수나 메서드의 인자로 넘길 수 있다.
      • 함수나 메서드에서 리턴할 수 있다. 자바스크립트에서 모든 값은 일급이다. 자바스크립트에서 모든 객체는 일급 객체이며 함수도 객체이자 일급 객체다. 일급 함수는 아래 조건을 더 만족한다.
      • 아무 때나(런타임에서도) 선언이 가능하다.
      • 익명으로 선언할 수 있다.
      • 익명으로 선언한 함수도 함수나 메서드의 인자로 넘길 수 있다.
      •  
      • // (1) 함수를 값으로 다룰 수 있다. function f1() {} var a = typeof f1 == 'function' ? f1 : function() {}; // (2) 함수를 리턴한다. function f2() { return function() {}; } // (3) a와 b를 더하는 익명 함수를 선언하여 즉시 실행하였다. (function(a, b) { return a + b; }))(10, 5); // (4) callAndAdd 를 실행하면서 익명 함수를 선언했고 바로 인자로 사용된다. callAndAdd는 넘겨받은 함수 둘을 실행하여 결과들을 더한다. function callAndAdd (a, b) { return a() + b(); } callAndAdd(function() { return 10; }, function() { return 5; });
  • 클로저
    • 클로저는 자신이 생성될 때의 환경을 기억하는 함수이다.
    • 클로저는 자신이 생성될 때의 스코프에서 알 수 있엇던 변수를 기억하는 함수이다.
    • 클로저로 만들 함수가 myfn이라면, myfn 내부에서 사용하고 있는 변수 중에 myfn 내부에서 선언되지 않은 변수가 있어야 한다. 그 변수를 a라고 한다면, a라는 이름의 변수가 myfn을 생성하는 스코프에서 선언되었거나 알 수 있어야 한다. 위와 같은 조건을 충족시키지 않는다면 그 함수가 아무리 함수 안에서 선언되었다고 하더라도 일반 함수와 전혀 다를 바가 없다. 클로저가 기억할 환경이라는 것은 외부의 변수들밖에 없기 때문이다.
    • 글로벌 스코프를 제외한 외부 스코프에 있었던 변수 중 클로저 혹은 다른 누군가가 참조하고 있지 않은 모든 변수는 실행 컨텍스트가 끝난 후 가비지 컬렉션 대상이 된다. 어떤 함수가 외부 스코프의 변수를 사용하지 않았고 그래서 외부 스코프의 환경이 가비지 컬렉션 대상이 된다면 그렇게 내버려 두는 함수를 클로저라고 보기는 어렵다.
    • 클로저를 선언하여 메모리 누수가 있다고 볼 수 있을까?
      • 그렇지 않다. 메모리가 해제되지 않는 것과 메모리 누수는 다르다. 메모리 누수는 메모리가 해제되지 않을 때 일어나는 것은 맞지만, 위 상황을 메모리 누수라고 할 수는 없다. a는 한 번 생겨날 뿐, 계속해서 생겨나거나 하지 않는다.
      • 메모리 누수란 개발자가 '의도하지 않았는데' 메모리가 해제되지 않고 계속 남는 것을 말한다.
    • 클로저가 기억하는 변수의 값은 언제든지 자신에 의해 변경될 수 있다.
    • 클로저가 강력하고 실용적인 상황
      • 이전 상황을 나중에 일어날 상황과 이어 나갈 때
      • 함수로 함수를 만들거나 부분 적용을 할 때
  • 콜백 패턴
    • 콜백 패턴은 클로저 등과 함께 사용할 수 있는 매우 강력한 표현이자 비동기 프로그래밍에 있어 없어서는 안 될 매우 중요한 패턴이다.
    • 콜백 패턴은 끝이 나면 컨텍스트를 다시 돌려주는 단순한 협업 로직을 가진다.
  • 부분 적용
    • bind 는 this 와 인자들이 부분적으로 적용된 함수를 리턴한다. bind의 경우 인자보다는 주로 함수 안에서 사용될 this를 적용해 두는데 많이 사용한다.
    • bind 는 첫 번째 인자로 bind가 리턴할 함수에서 사용될 this를 받는다. 두 번째 인자부터 함수에 미리 적용될 인자들이다.
    • bind 는 인자를 왼쪽에서부터 순서대로만 적용할 수 있다는 점과 bind를 한 번 실행한 함수의 this는 바꿀 수 없다.
    • 많은 자바스크립트 개발자들이 bind에서 this가 제외된 버전의 curry를 만들어 좀 더 간결한 코드를 제안했다. Lodash 의 _.curry 가 그 예이다.
    • bind 는 왼쪽에서부터 원하는 만큼의 인자를 지정해 둘 수 있지만 원하는 지점을 비워 두고 적용할 수는 없다. 예를 들어 어떤 함수가 필요로 하는 인자가 3개가 있는데 그중 두 번째 인자만을 적용해 두고 싶다면 bind로는 이것을 할 수 없다. 이러한 점을 개선한 방식이 partial이다.
    • Function.prototype.partial = function() {
        var fn = this, _args = arguments; // 클로저가 기억할 변수에는 원본을 남기기
        return function() {
          var arg = Array.prototype.slice.call(_args); // 리턴될 함수가 실행될 때마다 복사하여 원본 지키기
          for (var i = 0; i < args.length && arg < arguments.length; i++) {
            if (args[i] === undefined) args[i] = arguments[arg++];
          }
          return fn.apply(this, args);
        };
      };
  • arguments 객체 다루기
  • 함수 객체의 메서드 (bind, call, apply)

2장 함수형 자바스크립트를 위한 문법 다시 보기

난해해 보이는 문법들을 확인하는 목적

  1. 더 짧은 코드를 위해
  2. 추상화의 다양한 기법
  3. if를 없애기 위해
  4. 특별한 로직을 위해
  5. 캐시를 위해
  6. 은닉을 위해
  7. 함수를 선언하고 참조하기 위해
  8. 컨텍스트를 이어주기 위해

함수나 배열에 달기

근소한 차이지만 성능차이가 나는 배열 값 추가 방법들 (크롬 2017기준)

console.time('- 1 -')
var l = 100000;
var list = [];
for (var i = 0; i < l; i++) { list.push(i); }
console.timeEnd('- 1 -')
​
console.time('- 2 -')
var l = 100000;
var list = [];
for (var i = 0; i < l; i++) { list[list.length] = i; }
console.timeEnd('- 2 -')
​
console.time('- 3 -')
var l = 100000;
list.length = l;
for (var i = 0; i < l; i++) { list[i] = i; }
console.timeEnd('- 3 -')
​
console.time('- 4 -')
var l = 100000;
var list = Array(l);
for (var i = 0; i < l; i++) { list[i] = i; }
console.timeEnd('- 4 -')
​
// result
- 1 -: 5.544921875 ms
- 2 -: 4.71630859375 ms
- 3 -: 2.957275390625 ms
- 4 -: 3.496826171875 ms

즉시 실행 함수

괄호 없이 정의가 가능한(즉시 실행 가능한) 상황

!function(a) {
  console.log(a);
  // 1
}(1);
​
true && function(a) {
  console.log(a);
  // 1
}(1);
​
1 && function(a) {
  console.log(a);
  // 1
}(1) : 5;
​
0, function(a) {
  console.log(a);
  // 1
}(1);
​
var b = function(a) {
  console.log(a);
  // 1
}(1);
​
function f2() {}
f2(function(a) {
  console.log(a);
  // 1
}(1));
​
var f3 = function c(a) {
  console.log(a);
  // 1
}(1);
​
new function() {
  console.log(1);
  // 1
};

모두 연산자와 함께 있고, 함수가 값으로 다뤄졌다. 그리고 익명 함수 선언에 대한 오류가 나지 않는다.

즉시 실행하여 this 할당

var a = function(a) {
  console.log(this, a);
  // [1], 1
}.call([1], 1);

함수의 메서드인 call을 바로 .으로 접근할 수도 있으며, 익명 함수를 즉시 실행하면서 this를 할당할 수도 있다.

성능 차이

console.time('4');
var arr = Array(10000);
_.map(arr, function(v, i) { // 안에서 익명 함수를 한 번 더 만들어 즉시 실행
    return function(v, i) {
        return i * 2;
    }(v, i);
});
console.timeEnd('4');
// 4: 0.8ms ~ 1.8ms
​
console.time('5');
var arr = Array(100000);
_.map(arr, function(v, i) {
  return L('v, i => i * 2')(v, i); // 안에서 화살표 함수로 함수를 만들어 즉시 실행
})
console.timeEnd('5');
// 5: 362ms ~ 480ms

메모이제이션(memoization) 기법

// 원래의 L
function L(str) {
  var splitted = str.split('=>');
  return new Function(splitted[0], 'return (' + splitted[1] + ');');
}
​
// 메모이제이션 기법
function L2(str) {
  if (L2[str]) return L2[str]; // 혹시 이미 같은 `str`로 만든 함수가 있다면 즉시 리턴
  var splitted = str.split('=>');
  return L2[str] = new Function(splitted[0], 'return (' + splitted[1] + ');');
  // 함수를 만든 후 L2[str]에 캐시하면서 리턴
}
​
// 코드 구조는 그대로지만 성능은 다시 좋아졌다.
console.time('6');
var arr = Array(100000);
_.map(arr, function(v, i) {
  return L('v, i => i * 2')(v, i); // 안에서 화살표 함수로 함수를 만들어 즉시 실행
})
console.timeEnd('6');
// 6: 0.5ms ~ 1.2ms

유명(named) 함수

유명 함수 표현식

var f1 = function f() {
  console.log(f);
}
  • 함수를 값으로 다루면서 익명이 아닌 f()처럼 이름을 지은 함수를 유명(named) 함수라고 한다.
  • 재귀 등을 이용할 때 편하다.
  • 함수 자신을 가리키기 편하다.

익명 함수에서 함수가 자신을 참조하는 법

var hi = 1;
var hello = function hi() {
  console.log(hi);
}
​
hello();
// function hi() {
//   console.log(hi);
// }
​
console.log(hi);
// 1
​
console.log(++hi);
// 2
​
hello();
// function hi() {
//   console.log(hi);
// }
​
console.log(hello.name == 'hi');
// true
​
var z1 = function z() {
  console.log(z, 1);
}
​
var z2 = function z() {
  console.log(z, 2);
}
​
z1();
// function z() {
//   console.log(z, 1);
// }
z2();
// function z() {
//   console.log(z, 2);
// }
consol.log(z1.name == z2.name);
// true
​
z;
// Uncaught ReferenceError: z is defined

z처럼 이름이 중복되어도 상관없다. 동일한 이름의 유명 함수가 많아도 상관없으며 그 이름을 로직에서 활용할 수도 있다. 위와 같은 특성 덕분에 유명 함수는 이름을 짓는 데 오래 고민할 필요도 없고 안전하게 편하게 사용할 수 있다.

유명 함수를 이용한 재귀

즉시 실행과 유명 함수를 이용한 재귀 함수 flatten

function flatten(arr) {
  return function f(arr, new_arr) {
    arr.forEach(function(v) {
      Array.isArray(v) ? f(v, new_arr) : new_arr.push(v);
    });
    return new_arr;
  }(arr, []);
}
​
flatten([1, [2], [3,4]]);
// [1, 2, 3, 4]
flatten([1, [2], [[3], 4]]);
// [1, 2, 3, 4]
flatten([1, [[2], [[3], [[4], 5]]]]);
// [1, 2, 3, 4, 5]

즉시 실행 + 유명 함수 기법이 아닌 경우

function flatten2(arr, new_arr) {
  arr.forEach(function(v) {
    Array.isArray(v) ? flatten2(v, new_arr) : new_arr.push(v);
  });
  return new_arr;
}
flatten2([1, [2], [3, 4]], []); // 항상 빈 Array를 추가로 넘겨야 하는 복잡도 증가
​
function flatten3(arr, new_arr) {
  if (!new_arr) reurn flatten3(arr, []); // if문 존재
  arr.forEach(function(v) {
    Array.isArray(v) ? flatten3(v, new_arr) : new_arr.push(v);
  });
  return new_arr;
}
flatten3([1, [2], [3, 4]]);

자바스크립트에서의 재귀

재귀 횟수의 한계

  • 대략 15,000번 이상 재귀가 일어나면 'Maximum call stack size exceeded' 라는 에러가 발생하고 소프트웨어가 죽는다.
  • ES6 스펙상에는 꼬리 재귀 최적화(tail recursion optimization)가 명시되어 있지만 브라우저별로 지원하지 않는 경우가 있다.

arguments 다시보기

function test(a, b) { 
    console.log(arguments); // [1, 2] (length: 2)
    b = 10;
    console.log(arguments); // [1, 10] (length: 2)
    arguments[1] = 20;
    console.log(arguments); // [1, 20] (length: 2)
    arguments[2] = 30;
    console.log(arguments); // [1, 20, 2: 30] (length: 2)
}
test(1, 2)

인자를 변경하는 코드를 작성할 경우 값이 변경될 수 있으니 유의하여 작성해야한다.

this 다시보기

  • 자바스크립트에서의 함수는 '어떻게 선언했느냐'와 '어떻게 실행했느냐' 모두 중요하다.
  • '어떻게 정의했느냐'는 클로저와 스코프 관련 부분을 결정한다.
  • '어떻게 실행했느냐'는 this와 arguments를 결정한다.

call, apply 다시보기

  • call은 Function.prototype.call이다. call은 함수 자신을 실행하면서 첫 번째 인자로 받은 값을 this로 사용한다.
  • apply은 call과 동일하게 동작하지만 인자 전달 방식이 다르다. 인자들을 배열이나 배열과 비슷한 객체를 통해 전달한다.

네이티브 코드 활용하기

var slice = Array.prototype.slice;
​
function toArray(data) {
  return slice.call(data);
}
function rest(data, n) {
  return slice.call(data, n || 1);
}
​
var arr1 = toArry({ 0: 1, 1: 2, length: 2});
// [1, 2]
arr1.push(3);
console.log(arr1);
// [1, 2, 3]
​
rest([1, 2, 3]);
// [2, 3]
​
rest([1, 2, 3], 2);
// [3]

Array.prototype.slice의 경우, 키를 숫자로 갖고 length를 갖는 객체이기만 하면 Array가 아닌 값이어도 call을 통해 Array.prototype.slice를 동작시킬 수 있다. toArray와 rest 함수는 구현을 Native Helper에게 위임하여 짧은 코드로 성능이 좋은 유틸 함수를 만들었다.

 

if else || && 삼항 연산자 다시보기

if (expression) { statements }

if 괄호에서 할 수 없는 일

  • 지역 변수, 지역 함수 선언
  • 비동기 프로그래밍
    • async / await 를 사용할 경우에는 if 의 ()와 {}에서도 비동기 코드를 동기적으로 동작시킬 수 있다.

이미 선언되어 있는 변수의 값 재할당

var a;
if (a = 5) console.log(a); // 5
​
if (a = 0) console.log(1); 
else console.log(a) // 0
​
if (!(a = false)) console.log(a); // false
​
if (a = 5 - 5); 
else console.log(a); // 0

미리 선언된 변수에 값을 할당하는 것은 가능하다.

var obj = {};
if (obj.a = 5) console.log(obj.a); // 5
​
if (obj.b = false) console.log(obj.b); 
else console.log('hi'); // hi
​
var c;
if (c = obj.c = true) console.log(c); // true

객체의 key에 값을 할당하는 것도 가능하다.

function add(a, b) {
  return a + b;
}
​
if (add(1, 2)) console.log('hi1');
​
var a;
if (a = add(1, 2)) console.log(a); // 3
​
if (function() { return true; }()) console.log('hi'); // hi

함수를 실행할 수도 있고 실행한 결과를 변수에 담으면서 참과 거짓을 판단할 수도 있다. 익명 함수나 유명 함수를 정의하면서 즉시 실행할 수도 있다.

|| &&

// boolean || &&
var a = true;
var b = false;
​
var v1 = a || b;
console.log(v1); // true
​
var v2 = b || a;
console.log(v2); // true
​
var v3 = a && b;
console.log(v3); // false
​
var v4 = b && a;
console.log(v4); // false

boolen 의 경우 예상한 결과가 나온다.

// string || &&
var a = "hi";
var b = "";
​
var v1 = a || b; // a가 긍정적인 값이면 || 이후를 확인하지 않고 a를 담는다.
console.log(v1); // "hi"
​
var v2 = b || a; // b가 부정적인 값이어서 a를 확인하고 담는다.
console.log(v2); // "hi"
​
var v3 = a && b; // a가 긍정적인 값이어서 && 이후를 확인하게 되고 b 값이 담긴다.
console.log(v3); // ""
​
var v4 = a && b; // b가 부정적인 값이어서 && 이후를 확인하지 않고 b 값이 담긴다.
console.log(v4); // ""

문자에 || && 를 사용한 경우 예상과 다른 값이 출력된다. 아래는 ||와 &&의 다양한 활용법이다.

console.log(0 && 1); // 0
console.log(1 && 0); // 0
console.log([] || {}); // []
console.log([] && {}); // {}
console.log([] && {} || 0); // {}
console.log(0 || 0 || 0 || 1 || null); // 1
console.log(add(10, -10) || add(10, -10)); // 0
console.log(add(10, -10) || add(10, 10)); // 20
​
var v;
console.log((v = add(10, -10)) || v++ && 20); // 0
​
var v;
console.log((v = add(10, -10)) || ++v && 20); // 20

if else 대체하기

오른쪽으로 더 갈 것인가 말 것인가를 한 줄로 만들어 if else를 대체할 수도 있다.

function addFriend(u1, u2) {
  if (u1.friends.indexOf(u2) == -1) {
    if (confirm('친구로 추가할까요?')) {
      u1.friends.push(u2);
      alert('추가되었습니다.');
    }
  } else {
    alert('이미 친구입니다.');
  }
}
var pj = { name: "PJ", friends: [] };
var ha = { name: "HA", friends: [] };
​
addFriends(pj, ha); // 친구로 추가할까요? -> 확인 -> 추가되었습니다.
addFriends(pj, ha); // 이미 친구입니다.
​
function addFriends2(u1, u2) {
  (u1.friends.indexOf(u2) == -1 || alert('이미 친구입니다.')) && confirm('친구로 추가하시겠습니까?') && u1.friends.push(u2) && alert('추가되었습니다.');
}

addFriends2의 코드는 addFriend의 if else를 대체한다.

함수 실행의 괄호

var add5 = function(a) { // 새로운 공간
  return a + 5;
}
var call = function(f) { // 새로운 공간
  return f();
}
​
/* 함수를 실행하는 괄호 */
add5(5); // 10
call(function() { return 10; }); // 10

함수를 실행하는 괄호는 일반 괄호와 특성이 모두 같지만 한 가지 특성을 더 가지고 있다. 이 괄호를 통해 새로운 실행 컨텍스트가 열린다. 함수를 실행하는 괄호에서는 코드가 실행되었을 때 해당 지점에 값을 만든 후 끝나지 않고, 이 값이 실행된 함수의 공간으로 넘어간다. 새롭게 열린 공간이 끝나기 전까지는 이전 공간의 상황들도 끝나지 않으며 이 공간들을 실행 컨텍스트라고 한다.

 

// 작성중

 


 

이 문서는 아래 도서를 읽은 뒤 남긴 기록입니다.

함수형 자바스크립트 프로그래밍
유인동 저 | 인사이트(insight) | 2017년 11월 22일
728x90

댓글