Before Start
함수 y = f(x)의 의미를 살펴보면, input x가 처리 과정(process) f를 거쳐서 y라는 output을 반환한다는 것이다. 이 때 f로 정의된 process는 우리가 코드를 통해 작성할 수 있는 부분이며, 같은 기능을 사용하는 어느 곳에서나 재사용할 수 있다.
+) y=f(x)에서 x를 매개변수(parameter)라고 부르며, x에 3을 대입했다면 3을 인자(argument)라고 부른다.
pure function
+) 순수 함수 관련 article | what is pure function? what is side effects?
순수 함수는 몇가지 특징을 가지고 있는 함수를 일컫는 보편적인 용어이다. 순수 함수의 가장 중요한 특징 중 하나는, 함수의 결과 값(return value, output)을 결정하는 것은, 그 함수에 전달된 인자의 값(input value, argument) 뿐 이어야 한다는 것이다. 즉, 같은 값의 인자를 함수에 전달했다면, 같은 결과 값을 반환해야 한다.
Input
input이 없는 함수 ❌
어떤 인자도 받지 않는 함수는 순수 함수가 아니다. 아래의 예제 코드를 살펴 보자.
function print() {
console.log('print');
}
print('apple') // print
print('hello') // print
어떤 인자를 전달하든지 print가 하는 일은 콘솔에 print를 작성하는 일이다. print('apple'), print('hello')의 결과가 모두 같다. 이런 함수를 작성하는 것은 좋지 않다.
+) IIFE(Immediately Invoked Function Expression) | 함수를 정의함과 동시에 실행
- 함수를 정의함과 동시에 실행하고자 할 때 사용할 수 있다.
- 함수의 정의를 소괄호로 묶은 뒤 ()를 통해 호출한다. | ( function( ){ ... } )() ;
input을 { 블럭 } 내에서 변경하는 함수 ❌
함수의 인자와 관련해서 중요한 사항이 하나 더 있는데 상태 불변성(state immutability)에 관한 것이다. 즉, 함수를 실행했을 때 어떤 값도 변경되어서는 안된다는 것이다. 이 값은 함수의 인자가 될 수도 있지만, 함수 밖에 선언된 변수의 값이 될 수도 있다.
let myDepartment='front-end';
function change(department) {
department = 'back-end';
return department;
}
change(myDepartment); // 'back-end'
myDepartment; // 'front-end'
// --------------------------------
function change() {
myDepartment = 'back-end';
return myDepartment;
}
change(); // 'back-end'
myDepartment; // 'back-end', forever change!
두 change 함수는 매개변수가 있냐, 없느냐에 차이가 있다.
매개변수를 전달받는 첫번째 change 함수는, 코드 내부에서 department를 마음대로 바꾸는 과정을 거치는데, 이 과정은 사용자가 무엇을 input으로 전달하든지 간에, 그 값을 다른 값('back-end')으로 바꾼다. 즉 인자를 아무 값도 전달하지 않은 것과 다름 없다. 하지만 함수를 호출하더라도, 함수 바깥에 선언된 myDepartment의 값을 변경하지는 않는다.
반면 두번째 change 함수 코드는 아무런 input을 받고 있지 않기 때문에 pure function의 관점에서 나쁜 코드일 뿐만 아니라, 외부에 선언되어있는 변수(myDepartment)의 값을 직접 수정함으로써 그 값('front-end' => 'back-end')을 영원히 바꾸기 까지 한다.
+) 왜 함수의 실행이 끝나면 외부의 변수가 원래 값으로 돌아오는 걸까? | ✍️ scope chapter
블럭 안에서 변수를 설정하고 값을 할당하는 것은 블럭 안에서만 유효하다. 예를 들어 위의 첫번째 change 함수에서 생성되는 매개 변수는 department는 그 함수 안에서만 유효하다. 따라서 global 변수인 myDepartment를 인자로 받고 change 함수 안에서 그 인자의 값을 변경하더라도, 함수 블럭에서 빠져나오면, 즉 change(myDepartment)를 실행하고 다음 코드로 실행 순서가 넘어가면, 우리는 다시 원래 변수의 값을 얻을 수 있다.
반면 두번째 예제 코드는 외부에 선언되어 있는 변수를 직접 변경하는 함수이기 때문에, change의 실행 결과 변수에 할당된 값이 다른 값으로 계속 바뀌어있는 것을 확인할 수 있다.
change 함수에서 변경한 데이터는 문자열 타입이었다. 만약 객체 타입의 데이터를 전달하는 경우에는 어떻게 될까?
const example = { first: 'original value' };
function bad(obj) {
obj['first'] = "don't do this!";
console.log(example === obj);
return obj;
}
bad(example); // true; { first : "don't do this!" };
example; // { first : "don't do this!" } forever change!
함수 bad는 블럭 바깥에 선언되어있는 example을 직접 수정하지 않는다. 대신, bad 함수 내에서 object 라는 매개 변수를 생성한 뒤에 키를 이용해서 값을 수정한다. 위의 예시에서 첫번째 change 함수와 같은 상황이다.
그런데 change와 달리, bad의 실행이 종료된 이후에도 example의 값이 원래 값으로 돌아오는 것이 아니라, 함수의 실행 결과가 그대로 남아있는 것을 확인할 수 있다. 이것은 non primitive data가 메모리에 저장되는 방식 때문이다.
변수 명 | 변수가 가리키는 주소 | 주소 1에 저장된 값 | 주소 2에 저장된 값 |
example | 주소 1 | 주소 2 | 'original value' |
매개변수 obj | 주소 1 (example === obj => true) |
주소 2 | 'don't do this!' |
??? obj['first']는 메모리 주소 2를 가리키고 있고, 그 자체(메모리 주소)가 변경 되지는 않지만, 주소 2에 저장되어있는 value('original value')는 영영 바뀌어버린다.
function test(obj) {
obj = { first: 'changed?' };
return obj;
}
test(example); // { first: 'changed?' }
example; // { first: 'dont' do this!' }
변수 명 | 변수가 가리키는 주소 | 주소 1에 저장된 값 | 주소 n에 저장된 값 |
example | 주소 1 | 주소 2 | 'original value' |
매개변수 obj | 주소 1 | 주소 3 | 'changed?' |
??? 여기에서 주소 3에 할당되는 값은 함수 블럭이 실행되는 동안만 유효하다.
Rest Parameter
함수의 인자가 받을 수 있는 parameter의 개수는 정해져 있지 않다. rest parameter라는 문법은 function add(a, b, ...numbers) { return numbers } 라고 작성했을 때 우리가 전달하는 세번째 인자부터 마지막 인자까지를 하나의 배열로 return한다.
add(1, 2, 3, 4, 5); // [3, 4, 5]
Output: Return
함수를 실행할 때 내부 코드에서 return을 만나면, 함수는 return 에 작성된 값을 반환한 뒤 종료된다(return 다음에 작성된 코드는 실행하지 않는다). 아래 예제와 같이 add 함수 내부의 조건문을 만났을 때, 조건에 해당하면 NaN을 반환하는 return에 의해 함수의 실행이 종료된다. 반면 조건에 해당하지 않으면 조건문 내부의 return을 실행하지 않고, x+y를 반환하는 return을 실행한다.
function add(x, y) {
if(typeof x !== "number" || typeof y !== "number"){
return NaN;
} return x + y;
}
functional programming은 pure function을 이용한 프로그래밍을 지향한다고 언급 했다. 인자를 받지 않는 함수와 마찬가지로, 어떤 값도 return하지 않는 함수 또한 순수함수가 아니다. 함수의 핵심 개념은 전달받은 인자를 어떤 값으로 '반환'해서 그 값을 필요에 따라 언제든 재사용하는 것이기 때문이다.
+) return의 default value는 undefined이고, 그렇기 때문에 return이 생략되어 있는 함수(특정 값을 반환하지 않고, 함수 내부의 코드를 '실행' 하기만 하는 함수)는 undefined을 반환한다.
Declaration vs. Expression
Declaration | hoisting ⭕
declare(); // no error! because declare is hoisted.
function declare() {
/* code... */
}
- 함수의 선언은 function name() { ... } 형태로 작성한다.
- JS 엔진이 코드를 한줄씩 분석하기 전에, 전체 코드 중에 선언된 함수가 없는지 살펴본다.
- 선언된 함수가 있다면, 이 함수들은 모두 코드의 최상단으로 끌어올려지는데(hoisting), 이 때문에 함수가 선언되기 이전에도 함수를 호출하는 것이 가능하다. 자세한 내용은 hoisting chapter에서 다루어 볼 것이다.
- hoisting은 실제로 코드가 끌어올려진다기 보다는, 최상단에 작성된 것처럼 작동한다는 의미이다.
Expression | hoisting ❌
함수 표현식의 핵심은 함수가 값으로 취급되기 때문에 변수에 할당할 수 있다는 것이다. 즉, 함수를 숫자, 문자열, 배열, 객체... 등과 같은 데이터처럼 취급한다. 이렇게 일반 데이터처럼 연산(operate)이 가능한 함수를 first class-function이라고 하며, 최신 프로그래밍 언어에서 대부분 지원하고 있다. '데이터처럼 연산이 가능하다'는 것의 의미는, 함수가 변수에 할당되거나, 동등비교 연산의 대상이 되거나, 객체 property의 value가 될 수 있다(method)는 뜻이다.
함수 선언문과 표현식의 가장 큰 차이점은 값이냐 아니냐이고, 선언문은 값이 아니기 때문에 변수에 저장하지 않고 독립적으로 사용할 수 있지만, 표현식은 값이기 때문에 변수와 함께 사용되어야 한다는 것이다.
+) method | 객체에 종속되어있는 함수, ex. String.toUpperCase
이 때 함수를 변수에 할당하는 방법은 세 가지로 나누어 볼 수 있다. 첫번째 방법은 무명 함수(anonymous function)를 이용하는 방법이다.
const add = function(a, b) {
return a+b;
}
- add에 할당된 함수 표현식은, 함수 선언에서와 달리 이름을 갖지 않기 때문에 무명 함수라고 부른다.
- add는 내부적으로 객체 데이터(js에서 함수는 객체이다)를 할당받았기 때문에, 'add'가 가리키는 주소의 메모리에는 function이 저장되어 있는 메모리의 주소가 저장되어있다.
두번째 방법은 화살표 함수(arrow function)을 이용하는 방법이다.
const add = (a,b) => { return a+b };
const add2 = (a,b) => ( a+b ); // implicit return, but should have 1 line code
const add3 = (a,b) => a+b ;
const print = x => console.log(x);
- function 키워드 대신 화살표(=>)를 사용한다.
- 화살표 함수는 ( ) 안에 함수의 인자를 나타내고, { } 안에 함수의 코드를 작성한다.
- add2처럼 { }을 작성하지 않았다면, return 키워드도 생략할 수 있다.
- 언제 화살표 함수를 사용할까?
- 다른 함수의 인자로 전달되어서 딱 한번만 사용되고, 재사용될 여지가 없을 때 (function expression without storing in variable)
- let sum = nums.reduce( (accumulator, num) => accumulator + num;); | 아래보다 더 간결함
- let sum = nums.reduce( function (accumulator, num) { return accumulator + num; } );
- static this binding이 필요할 때 | scope, closure chapter 참고
- 하지만 한계점도 분명 있다.
- 다른 함수의 인자로 전달되어서 딱 한번만 사용되고, 재사용될 여지가 없을 때 (function expression without storing in variable)
constructor function
function Dog(name) {
this.name = name;
}
const choco = new Dog('초코');
- 생성자 함수(constructor function)는 실행 결과로 객체를 return 하는 함수이다.
- 일반 함수와 달리 이름의 첫 글자는 대문자로 한다.
- 예제 코드에서는 new Dog()으로 함수를 실행한 결과를 choco라는 변수에 할당하고 있다. 자세한 내용은 객체에서 다룬다.
Higher Order Function
higer order function(고차함수)은 다음과 같은 일을 수행할 수 있다.
- 인자로 다른 함수를 받는다.
- 실행 결과로 다른 함수를 return한다.
// passed function arguments
function caller(func) {
func();
}
function greet() {
console.log('hello!');
}
greet(); // hello!
caller(greet); // also hello!
이 때 caller는 "higher order function"이고, argument로 전달된 greet 함수를 "callback function"이라고 한다. f(g(x))라고 생각해 보면, g가 콜백함수, f가 고차함수가 된다.
+) greet 함수는 마치 greet이라는 변수에 저장된 객체 데이터처럼 함수의 인자로 전달된다. => first class function
// returns other function
function makeBetweenFunc(min, max) {
return function(num) {
return num >= min && num <= max ;
};
}
const isChild = makeBetweenFunc(0, 18);
isChild(40); // false
isChild(3); // true
위의 makeBetweenfunc 함수는 다른 함수를 반환하는 고차함수이다.
function calculator(a, b, action) {
return action(a, b)
}
위의 예제 코드에서 action은 콜백함수, calculator는 고차함수가 된다. 인자로 전달되는 함수에 callback이라는 이름이 붙은 이유는, 이 함수가 고차함수 안에 인자로 전달될 때 실행되는 것이 아니라, 고차함수를 나중에(back) 호출(call) 해야 이 함수가 실행되기 때문이다.
사실 calculator는 action이라는 함수를 전달받지만, 함수를 정의하는 시점에서는 action이라는 함수가 무엇인지 모른다(더하기를 할지, 빼기를 할지, 곱하기를 할지는 calculator를 호출할 때 정해지는 것이다).
function calculator(a, b, action) {
return action(a, b)
}
function add(a, b) {
return a+b;
}
calculator(2, 3, add); // 5
Functional programming
JS Functional Programming // 함수형 프로그래밍에 대한 설명 영상
함수형 프로그래밍에 대해 더 명확하게 알아보자. 먼저 함수형 프로그래밍은 코드를 작성해 나가는 패러다임, 방법 중 하나이다. 함수형 프로그래밍에서는 서로 연관된 일을 하는 함수들을 하나로 엮어나가는 방식으로 코드를 작성한다. 이 때 이 함수들은 'pure function' 이어야 한다. 어떤 함수가 순수한 함수일까?
가장 기본은 input에 어떤 처리를 한 뒤 output을 반환해야 한다는 것이다. 이것이 인풋과 아웃풋에서 살펴본 내용이다. 이 때 함수는 처리 과정에서 input 또는 global variable data를 변경하지 않아야 한다(Immutability). 예를 들어 [2, 3, 6, 7, 8] 이라는 배열을 input으로 받아 홀수 item만을 filtering하는 함수를 작성한다면, [2, 3, 6, 7, 8] 배열에서 2와 6, 8 item을 삭제하는 것이 아니라, [3, 7]로 이루어진 새로운 배열을 반환해야 한다.
이 외에도 procedual(imperative) programming과 달리, 순수 함수는 코드 내부에서 variable이나 loop 또는 conditional statement를 사용하지 않는다(이러한 것들은 변수에 저장된 데이터를 변경하기 때문에, 예를 들어 i++, pure하지 않다).
'Study > JavaScript' 카테고리의 다른 글
Array (0) | 2022.03.29 |
---|---|
Object (0) | 2022.03.28 |
Control flow statement (0) | 2022.03.25 |
Operator (0) | 2022.03.25 |
Data Type (0) | 2022.03.24 |