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장 함수형 자바스크립트를 위한 문법 다시 보기
난해해 보이는 문법들을 확인하는 목적
- 더 짧은 코드를 위해
- 추상화의 다양한 기법
- if를 없애기 위해
- 특별한 로직을 위해
- 캐시를 위해
- 은닉을 위해
- 함수를 선언하고 참조하기 위해
- 컨텍스트를 이어주기 위해
함수나 배열에 달기
근소한 차이지만 성능차이가 나는 배열 값 추가 방법들 (크롬 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일
'Study Note > Javascript' 카테고리의 다른 글
javascript #자바스크립트 객체와 SOLID 원칙 (0) | 2022.05.29 |
---|---|
javascript #DOM reflow 시 자원 소모 최소화 방법 (0) | 2021.02.14 |
javascript #디자인 패턴 - 커링 패턴(currying pattern) (0) | 2021.02.13 |
javascript #디자인패턴 - 콜백 패턴(callback pattern) (0) | 2021.02.13 |
javascript #디자인패턴 - Self-invoking constructor 패턴 (0) | 2021.02.13 |
댓글