본문 바로가기
Javascript

javascript function 개념과 종류

by memeseo 2021. 11. 14.

javascript랑 나름 친하다고 생각했는데 이번에 이론적으로 개념을 잡으면서 살짝 멀어졌다. 고차함수에 익숙해지자!

 

1급 객체


javascript function는 1급 객체이다. 1급 객체가 뭔지 알아보자.

 

 1급 객체 (First Class Object)

1. 변수에 저장이 가능하다.
2. 함수의 Parameter로 전달할 수 있다.
3. 함수의 return 값으로 사용할 수 있다.
4. 자료 구조 (Object, Array 등)에 저장할 수 있다.

 

아래는 가장 기본적인 function의 선언 방법이다. ES6 이상을 사용한다면 Arrow function을 사용하여 선언할 수 있다.

function call(params){
	//code
}

call('value');

//Arrow function
const call = params => {
	//code
}

call('value');

function의 이름을 선언하고 실행시킬 시 call(), 혹은 call(인자값)로 실행시킬 수 있다. 

 

🔎 function 특징

1. 함수에 이름을 붙여, 함수명으로 함수 호출
2. 실행문이 아니므로 끝에 세미콜론(;)을 안붙이는 게 일반적
3. 함수 정의 전 호출 코드로 사용 가능 (= Hosting 가능)
4. 자체호출 불가

 

🔎 예시를 통해 1급 객체가 뭔지 자세히 알아보도록 하자.

 

1. 변수 저장이 가능하다

let call = function(){
	//code
}

//Arrow function
let call = () => {
	//code
}

이와 같이 작성할 수 있다. 이 것을 익명함수 (Anonymouse Function)이라고 하며, 함수 선언시 자주 사용된다. 함수 호출 역시 일반 함수와 똑같이 call()을 이용하면 된다.

 

2. 함수의 parameter로 전달할 수 있다.

function call (num1, num2, func){
	retrun func(num1, num2); // 16
}

call(2, 8, function(a,b){
	return a * b;
})

이처럼 함수 호출 시 함수를 인자로 전달하여 연산이 가능하게 할 수 있다.

보통 callback 함수를 전달할 때 사용된다. 대표적인 예로 addEventListener가 있겠다.

 

document.getElemenyById('item').addEventListener('click', function(){
	//code
})

 

3. 함수의 return값으로 사용할 수 있다.

function call(a,b){
	let result = a + b;
    return function(){
    	return result;
    }
}

let getNum = call(1,2);
getNum(); //3

변수에 함수를 할당할 수 있는 특징 덕분에 getNum은 함수 호출의 return 결과로 함수가 할당되어, getNum()으로 실행이 가능해진다. 대부분의 라이브러리들이 이러한 특징을 이용하여 작성된다.

 

4. 자료구조(Object, Array 등)에 저장할 수 있다.

let arr = ['a', 'b', function(){
	return c;
}];

console.log(arr[0]); // 'a'
console.log(arr[1]); // 'b'
console.log(arr[2]()); // 'c' [함수이기 때문에 ()를 붙여 호출해야 한다.]
let obj = {
	call : function(str){
    	return '#' + str;
    }
};

console.log(obj.call('welcom')); // '#welcom'

 

 

함수의 종류


1. 익명 함수
2. 중첩 함수 (nested function)
3. 콜백 함수 (callback function)
4. 클로저 (closure)
5. 로드 함수 
6. $(document).ready()
7. 즉시 실행 함수 (immediate function)

 

1. 익명 함수

- 익명 함수는 이름이 없다.

- 주로 변수에 할당되거나 함수의 인자값 또는 반환값으로 사용된다.

- 콜백함수의 생성에 사용된다.

 

let add = function(x,y){
	return x + y;
}

 

2. 중첩 함수

- 다른 함수 내부에서 정의 되는 함수이다.

- 특정 함수에서만 사용할 기능이라면 전역 스코프에 함수를 구현하지 않고 특정 함수 내부에 구현할 수 있다.

- 함수 스코프로 변수의 스코프가 이루어지므로, 내부 함수에서는 외부 함수에 정의된 변수에 접근할 수 있다.

- 일반적으로 특정 함수의 외부에서는 그 안에 구현된 내장 함수에 접근할 수 없다.

 

function example(){
	let a = 1;
    function sum(){
    	retunr a + 2;
    }
    
    return sum();
}

 

3.  콜백 함수

- 이름 그대로 나중에 호출되는 함수

- 코드를 통해 명시적으로 호출하는 함수가 아니라 개발자는 단지 함수를 등록하기만 하고, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수를 말한다.

- 대표적인 예로는 자바스크립트에서 이벤트 핸들러 처리와 비동기 통신이 있다.

 

function getData(callbackFunc){
	axios.get('/getData', function(res){
    	callbackFunc(res); // 서버에 받은 데이터를 callbackFunc()에 넘긴다.
    });
}

getData(
	function(value){
    	console.log(value); 
    }
)

 

4. 클로저

- 내부 함수의 scopes 프로퍼티는 자신의 실행 환경(Lexical Enviroment)과 자신을 포함하는 외부 함수의 실행 환경과 전역 객체를 가르킨다. 이 때, 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 scopes 프로퍼티가 가리키는 외부 함수의 실행 환경 (Activation Object)은 소멸하지 않고 참조할 수 있다. 예시를 보면 이해가 갈 것.

 

function foo(){
	let x = 'variable of outerFunc';
    
    function bar(){
    	console.log(x);
    }
    
    return bar;
}

let innerFunc = foo();
innerFunc(); // result : variable of outFunc;

위 예제를 보면 외부 함수 foo()에서 bar()를 반환하고 소멸한다.

 

외부함수 foo()는 실행된 이후, 실행 컨텍스트 스택에서 제거되기 때문에 변수 x도 같이 소멸될 것으로 예상한다. 하지만 innerFunc()함수를 호출하면 변수 x값이 출력되는 걸 볼 수 있다. 이 처럼 클로저는 외부 함수 foo() 밖에서 내부 함수 bar()가 호출되더라도 외부 함수의 지역 변수 'let x'에 접근할 수 있다.

 

외부 함수의 지역 변수에 접근할 수 있는 이유는 , 외부 함수인 foo()함수가 종료되면서 '실행 컨텍스트'도 소멸하지만 foo()함수 '실행 컨텍스트 활성 객체'는 유효하다.  이 때문에 외부 함수 foo()가 실행이 종료되어도 내부 함수 bar()에서 접근이 가능한 것이다.

 

- 클로저로 전역 변수 사용 억제 하기

let counter = 0;

function calculator(){
	return console.log(++counter);
}

calulator(); //1
calulator(); //2
calulator(); //3

전역 변수는 어디서든 접근이 가능하기 때문에 값이 변할 수도 있고 이에 따라 오류를 불러올 수 있다.

 

let outerFunc = (function(){
	let counter = 0;
    
    function calulator(){
    	return console.log(++counter);
    }
    
    return calculator;
}());

outerFunc(); //1
outerFunc(); //2
outerFunc(); //3

 

- 루프 안에서 클로저 활용

function count(number){
	for(var i = 1; i <= number; i ++){
    	setTimeout(function(){
        	console.log(i);
        }, i*1000)
    }
}

count(4);

위에 의도는 1 초 간격으로 1,2,3,4를 출력하는 것이다. 하지만 결과는 예상과 다르게 5가 4번 1초 간격으로 출력된다. 그 이유는 변수 i는 외부 함수의 변수가 아닌 전역변수이고 setTimeout() 함수가 실행되는 시점은 count()함수가 종료된 이후다.  이 때 이미 i 값이 5인 상태이다.

 

function count(number){
	for(var i=1; i <= number; i++){
    	(function(j){
        	setTimeout(function(){
            	console.log(i);
            }, i*1000)
        }(i))
    }
}

count(4);

즉시 실행 함수를 실행시켜 루프의 i값을 j에 복사하고 setTimeout()함수에서 사용했다. 이 때 j는 상위스코프의 자유변수이므로 그 값이 유지된다. ES6에서는 let을 이용해 깔끔하게 구현할 수 있다.

 

function count(number){
	for(let i=1; i <= number; i++){
    	setTimeout(function(){
        	console.log(i);
        }, i*1000)
    }
}

count(4);

 

5. 로드 함수

- 페이지의 모든 요소들이 로드된 이후에 호출된다.

-html 로딩이 끝난 후 실행된다.

-화면에 필요한 모든 요소 (css, js, image 등)이 웹브라우저 메모리에 모두 올려진 다음에 실행된다.

 

window.onload = function(){
	alert('this is the callback function');
}

 

6.  $(document).ready()

-DOM(document object model) 트리들이 모두 로드된 시점에 호출 된다.

-window.onload 함수의 문제점을 해결하기 위해 사용되었고 window.onload보다 빠르다.

-$(function())과 쓰임이 같다. *하지만 $.ready() 함수를 사용하는 것을 권장.

 

$.ready(function(){
	// code
})

 

7. 즉시 실행 함수

- 함수를 정의함과 동시에 실행하는 함수

- 함수를 정의하자마자 실행하기 때문에 같은 함수를 다시 호출할 수 없다. 이러한 특성을 이용해 최초에 한 번만 실행되는 초기화 코드에서 사용할 수 있다.

 

(function(returnValue){
	console.log(returnValue);
})('Hello World');

즉시 실행 함수의 경우, 변수를 전역으로 선언하는 것을 피할 수 있기 때문에 라이브러리를 만들 때 많이 사용한다.

 

let moduleFunc = (function(){

	let a = 3;
    
    function helloworld(){
    	console.log('Hello');
    }
    
    return {
    	a : a,
        sayHello : helloworld
    }
})();

function doSometing(x){
	console.log('moduleFunc - a : ' + x);
}

moduleFunc.sayHello(); // Hello
doSomething(moduleFunc.a); // moduleFunc - a : 3

 

🔎  우리는 왜 javascript 함수가 1급 객체인 걸 알아야 할까?

: 함수를 활용할 수 있는 범위가 넓어지고 코드 재사용 및 간결화에 도움을 준다. 같은 맥락으로 고차함수(High Order Function)사용이 가능하다는 걸 알 수 있다. 고차함수란 함수를 인자로 받거나 함수를 결과로 반환하는 함수이다. javascript의 내장 함수 중에는 Array.prototype.map, Array.prototype.filter, Array.prototype.reduce가 있다.

 

 

고차 함수 (Higher-Order Function)


고차 함수의 동작

 

1. Array.prototype.map

map() 메소드는 입력으로 들어온 배열 내 모든 엘리먼트를 인자로 제공받는 콜백 함수를 호출함으로써 새로운 배열을 만들어 낸다. map 메소드로 전해진 콜백 함수는 'element', 'index', 'array' 이 세 가지 인자를 받는다. 

 

1-1. Example

숫자가 들어있는 배열의 각각 숫자 값이 두배가 된 배열을 만들고자 한다. 고차 함수가 있을때와 없을때를 비교해보자.

 

-고차 함수가 아닌 함수로 작성했을 때

const arr1 = [1, 2, 3];
const arr2 = [];

for(let i=0; i < arr1.length; i++){
	arr2.push(arr[i] * 2);
}

console.log(arr2); // result : 2, 4, 6

 

-고차 함수로 작성했을 때

const arr1 = [1, 2, 3];

const arr2 = arr1.map(function(items){
	return items * 2;
})

console.log(arr2); // result : 2, 4, 6

 

-Arrow function을 사용하면 훨씬 더 짧게 작성 가능

const arr1 = [1, 2, 3];
const arr2 = arr1.map(items => items * 2);

console.log(arr2); // result : 2, 4, 6

 

2. Array.prototype.filter

filter() 메소드는 콜백 함수에 의해 제공된 테스트를 통과한 모든 엘리먼트를 가진 새로운 배열을 만들어 낸다. filter() 메소드 역시 element, index, array를 받는다.

 

2-1. Example

이름과 나이 property를 가진 object를 가지고 있다고 해보자. 여기서 18살 이상의 사람만 필터링 된 새로운 배열을 만들어 보자.

 

- 고차 함수가 아닌 함수로 작성

const persons = [
	{name : 'Peter', age : 16},
    {name : 'Mark', age : 18},
    {name : 'John', age : 27},
    {name : 'Jane', age : 14},
    {name : 'Tony', age : 24},
];

const fullAge = [];

for(let i=0; i < persons.length; i++){
	if(persons[i].age >= 18){
    	fullage.push(persons[i]);
    }
}

console.log(fullAge);

 

- 고차 함수로 작성

const persons = [
	{name : 'Peter', age : 16},
    {name : 'Mark', age : 18},
    {name : 'John', age : 27},
    {name : 'Jane', age : 14},
    {name : 'Tony', age : 24},
];

const fullAge = persons.filter(person => person.age >= 18);

console.log(fullAge);

 

3. Array.prototype.reduce

reduce 메소드는 호출하는 배열의 각각의 멤버에 대해서 콜백 함수를 실행하고 하나의 결과 값만 내보낸다. reduce 메소드는 'reduce 함수(콜백)', '초기값(initialValue)' 옵션 이 두 가지 파라미터를 받는다. 이때 reduce 함수는 'accumulator', 'currentValue', 'crrentIndex', 'sourceArray' 네 가지 파라미터를 받는다.

 

만일 initialValue가 제공되었다면, 그 후에 accumulator는 initialValue와 같아지고 crrentValue는 배열의 첫 번째 값과 동일 하다.

반대로 제공되지 않았다면, accumulator는 배열의 처음 요소와 동일해지고 currentValue는 배열의 두번째 요소와 같아진다. 

 

3-1. Example

숫자 배열의 합을 구하는 예제를 만들어보자.

 

- 고차 함수가 아니 함수로 작성

const arr = [5, 7, 1, 8, 4];

let sum = 0;

for(let i=0; i < arr.length; i++){
	sum = sum + arr[i];
}

console.log(sum); // result : 20

 

- 고차 함수로 작성

const arr = [5, 7, 1, 8, 4];

const sum = arr.reduce(function(accumlator, currentValue){
	return accumulator + currentValue;
})

console.log(sum); // result : 25

 

배열 내부의 각 값에 대해 리듀서 함수가 호출되는 순간에 accumultor는 리듀서 함수로부터 반환된 이전 연산의 결과를 갖고 있다. 그리고 currentValue는 배열의 현재값으로 세팅된다. 결과값은 마지막에 sum 변수에 저장된다. 초기값을 제공한 예시를 봐보자.

 

- 초기값 제공

const arr = [5, 7, 1, 8, 4];

const sum = arr.reduce(function(accumultor, currentValue){
	return accumultor + currentValue;
}, 10);

console.log(sum); // result : 35

 

4. 고차 함수 만들어서 사용하기

javascript에 map 메소드가 없다고 할 때 우리는 스스로 만들어 낼 수 있다. 우리가 문자열의 배열을 가지고 있다고 할때 이 배열을 정수(=문자열의 길이)의 배열로 바꿔보자.

 

const strArray = ['javascript', 'python', 'php', 'java', 'c'];

function mapForEach(arr, func){
	let newArray = [];
    for(let i=0; i < arr.length; i++){
    	newArray.push(func(arr[i]));
    }
    return newArray;
}

const lenArray = mapForEach(strArray, function(item){
	retrun item.length;
});

console.log(lenArray); // result : [10, 6, 3, 4, 1]

위에 예제에서 배열과 콜백 함수를 받는 고차 함수 mapForEach를 만들어 보았다. 이 함수는 제공 받은 배열을 반복하고 newArray.push 함수 내부에서 각각의 반복마다 콜백 함수 func를 호출한다. 콜백 함수 func는 배열의 현재 요소를 받고 newArray의 내부에 저장된 요소의 길이를 반환하다. for 루프가 끝난 이후에 newArray가 반환되고, lenArray에 할당된다.

 

 

 

Reference

-인사이드 자바스크립트 (송형주, 고형준)