221111.til

Universe·2022년 11월 11일
0

study

목록 보기
43/49

챕터 5 함수

함수

함수는 다른게 아니라
긴 코드를 한 단어로 축약 하고 싶을 때 쓰는 프로그래밍 문법이다.
어떤 특정한 로직이나 알고리즘, 기능을 다음에도 재사용 할 수 있도록 모듈화 해놓는 문법이라고 보면 된다.
모듈화를 해놓으면 재사용도 쉬울 뿐 아니라 특정 기능을 선언된 함수별로 수정할 수 있으므로
코드를 길게 나열했을 때 보다 유지보수도 쉬워진다.

함수선언문

function 작명(파라미터1, 파라미터2 ...){
	실행할코드
}

자바스크립트의 함수를 선언하는 방법 첫번째.
이와같은 방법으로 함수를 선언하는 방법을 함수선언문 이라고 한다.
function 키워드를 사용하고 작명한 후 파라미터를 입력하고
중괄호 안에 함수가 호출됐을 때 실행할 코드를 입력한다.

파라미터는 일종의 구멍을 뚫어놓는다고 생각하면 쉽다.
수학시간에 배운

이러한 모양을 생각하면 쉬운데
함수의 소괄호에 input 의 규칙을 지정하고
함수 내부의 코드로 input 을 가공한 뒤,
output 을 리턴하는 개념이다.

이처럼 선언된 함수에 파라미터를 넣고 함수 내부에서 파라미터를 호출하면 해당 값을 호출할 수 있다.

예를들어,

function add(a,b){
	return a+b;
}

add(1,2)
//3

함수의 소괄호 란에 파라미터 구멍 2개를 뚫고
함수를 호출할 때 파라미터 1,2를 입력하면 그 값이 그대로 호출되는 식이다.

함수표현식

함수를 선언하는 두번째 방법으로는

const func = function(파라미터1, 파라미터2){
	실행할코드
}

마치 변수를 선언하듯 함수를 선언할 수 있다.
이러한 방법을 함수표현식 이라고 한다.
함수표현식에서 함수를 선언할 때는 함수 자체에는 따로 작명을 하지 않는데,
그 자리에 익명함수가 들어간다.

익명함수란 단순하게 이름없는 함수라고 생각하면 쉬운데,
변수에 함수의 코드를 저장하는 대신 함수명을 사용하지 않는 함수이다.
아례의 예시를 보자.

let func = () => {console.log('hello world')}
func()

//hello world

이렇게 선언된 변수는 심지어 변수명 변경도 가능하며,
함수선언문 처럼 파라미터도 사용할 수 있다.

let a = (a,b) => {return a+b}
let b = a;

console.log(b(1,2))

//3

a 변수를 b 변수에 할당하면 b변수를 호출해도 똑같은 기능을 수행할 수 있다.
b 변수는 a 변수를 참조 하기 때문이다.

추가로 조금 다른 경우이지만 이러한 경우에는 어떨까 ?

let func = function func2(a,b){
	return a+b;
}

console.log(func(1,2))
console.log(func2(1,2))

// 3
// RefernceError: func2 is not defined

func2 라는 함수도 분명 선언되었는데 왜 호출할 수 없을까 ?
그 이유는 사실 자바스크립트에서 함수 선언문은 함수 표현식으로 동작하기 때문이다.

예를들어,

function func(a,b){
	return a+b;
}

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

이 두 함수는 완벽하게 같은 의미를 가진다.
자바스크립트 내부 엔진에서는 함수 선언문을 함수 표현식으로 변환한 후 호이스팅을 진행한다.
함수 표현식에서 사용된 함수이름은 외부에서 접근할 수 없다.

또한 함수표현식의 경우 자유롭게 파라미터를 할당할 수 있는데,
아래의 경우를 보자

let func = (a, b, c) => {
	return a+b+c;
}

let result1 = func(1,2,3,4,5,6)
let result2 = func(1,2)
console.log(result1,result2)
// 6 NaN

func 는 3개의 파라미터가 필요한데
result1은 무려 6개를, result2는 겨우 2개를 할당했다.
그러나 함수표현식에서는
6개의 파라미터가 있어도 필요한 만큼만,
2개의 파라미터가 있으면 마지막 파라미터로 자동으로 Undefined 를 할당해준다.
개발자의 관점에서 자유롭다는 단어는 항상 좋은 것만은 아니니 주의하여 사용할 것.

함수표현식과 함수선언문은 어떤 차이가 있을까?

한줄로 요약하자면 함수선언식은 호이스팅에 영향을 받지만,
함수표현식은 호이스팅에 영향을 받지 않는다.
호이스팅이란
인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당 하는 개념이다.
쉽게 얘기해서
변수의 선언과 초기화를 분리한 후, 선언만 코드의 최상단으로 옮기는 방식이다.
따라서 함수선언문은 선언된 코드이기 때문에 최상단으로 코드가 끌어올려지고
함수표현식은 할당의 초기화가 일어나는 부분이기 때문에
'변수' 자체에 대한 호이스팅은 일어나 중복될 수는 없지만,
해당 변수를 호출하기 전까지 함수의 기능은 호이스팅되지 않는 것이다.

호이스팅은 반드시 좋다고 볼 수 없다.
코드가 길어지면 길어질수록 실행 흐름을 예측하기 어려워지고,
협업을 하다보면 함수의 이름이 겹칠 수 있다.
예를들어,

func()

function func() {
  console.log('첫번째 함수')
}


function func() {
  console.log('두번째 함수')
}

호이스팅이 일어나 선언 위치에 상관없이 사용할 수 있지만,
똑같은 이름의 함수임에도 오류메세지를 출력하지 않고
두번째 함수가 첫번째 함수를 덮어쓰기까지 한다.


const func = () => {console.log('첫번째함수')}
const func = () => {console.log('두번째함수')}

func()

//Uncaught SyntaxError: Identifier 'func' has already been declared

이렇게 함수 표현식으로 작성하면 호이스팅이 일어나지 않아
실행의 위치를 고려해주어야 하지만
적어도 중복은 확실히 막을 수 있게된다.

반드시 함수선언문을 사용해야 하는건 아니지만
일관성있는 함수를 설계하게 된다면 함수표현식 쪽을 사용하는 것이 바람직하다.

arguments

함수 내부에서 arguments 키워드를 이용하면 모든 파라미터를 다룰 수 있다
아래의 경우를 보면

let func = function(a,b,c){
	let result =0;
	for(let i=0; i<arguments.length; i++){
		result = result + arguments[i]

	}
  return result;
}

console.log(func(1, 2, 3, 4, 5, 6, 7));

arguments 라는 키워드를 사용하면 뒤에 기입한 파라미터 다음에 올 값도 호출할 수 있다.
arguments 는 배열과 비슷한 자료형을 갖는다.
배열과 유사하다 뿐이지 배열이 아니기때문에 forEach, map 같은 메소드는 사용할 수 없다.
그러나 iterable 한, 반복가능한 객체이기 때문에

let func = function(a,b,c){
  for( arg of arguments){
    console.log(arg)
  }
}

func(1, 2, 3, 4, 5, 6, 7)

// 1,2,3,4,5,6,7

for of 반복문은 사용할 수 있다.

그런데

let func = (a,b,c) => {
	console.log(arguments)
}

func(1,2,3,4,5,6,7)

// Uncaught ReferenceError: arguments is not defined

화살표 함수에서는 arguments 키워드를 사용할 수 없다.
arguments 키워드는 사실 스코프 속 이름에 대한 참조를 의미하기 때문이다.
화살표함수는 prototype 조차 없기 때문에 arguments 객체를 바인딩하지 않는다.

Rest parameter

이러한 문제를 해결하기 위해서 ES6 이상부터는 Rest파라미터 를 사용한다.
arguments 키워드와는 정반대로 파라미터에 …파라미터 를 입력하면
모든 파라미터를 인식한다.
위의 함수를 rest 파라미터를 이용해 바꿔본다면

let func = function(...a){
	let result = 0
	a.forEach((e)=>{ result = result + e })
  return result;
}

func(1,2,3,4,5,6,7,8,9,10)
// 55

rest 파라미터는 arguments 키워드와는 다르게
앞으로 올 파라미터들을 배열의 형태로 반환해주기때문에
forEach, map 같은 배열 전용 메소드들도 사용할 수 있는 것이 장점.
rest 파라미터는 ‘앞으로 이 파라미터 뒤로 올 파라미터를 배열로 묶어줘’ 같은 의미 이기 때문에
두번 사용할 수 없고 rest 파라미터 뒤로 파라미터를 추가로 넣을 수도 없다.

spread operator

전개연산자 라고 부른다.
아주 쉽게 설명하자면 괄호 하나를 벗겨주는 연산자 라고 생각하면 쉽다.

let arr = ['hello', 'world']

console.log(...arr)
// hello world

이런식으로
조금 더 재미있는 기능이 있는데,

let arr = 'string'
console.log(...arr)
// s t r i n g

문자열을 각각 쪼개어 나열할 수도 있다.
string 은 인덱싱을 할 수 있으니 그 인덱스를 하나하나 쪼개어 나누어 줬다고 생각하면 쉽다.

spread operator 는 배열의 복사와 결합에 주로 쓰이는데,

const arr = [1,2,3,4,5]
const _arr = [...arr]

배열을 spread operator로 깊은복사 하여 가공하여 사용한다.
배열과 오브젝트는 reference data 타입이므로

const arr = [1,2,3,4,5]
const arr2 = arr;

arr2.push(6)

console.log(arr)
// [1,2,3,4,5,6]

이런식으로 배열 자체의 값을 참조하는 것이 아닌
해당 배열의 ‘주소값’ 을 참조하게 된다.
따라서 원본의 데이터를 보존하는 식으로 코딩을 하기 위해서
해당 값을 깊은복사 하여 가공하는 것이 관례이다.

단, 깊이가 1일 경우에만.
예를들어,

const obj1 = { a: { b:1, c:1 }, d: 2};
const obj2 = { ...obj1, a:{ ...obj1.a } };
obj1.a.b = 100;
console.log(obj1 === obj2) // false
console.log(obj2.a.b) // 1

깊이가 2 이상인 배열은 spread operator 로 깊은복사가 불가능하다.
깊은 복사를 하는 간단한 방법으로는

JSON.parse(JSON.stringify(a))

JSON.stringify 메소드는 값이나 객체를 JSON 문자열로 반환하고
JSON.parse 메소드는 JSON 문자열의 구문을 분석하여 값이나 객체로 변환해준다.

깊이가 아무리 깊은 오브젝트도 그 자체로 깊은복사 할 수 있다.

재귀함수

재귀(recursion)은 어떠한 것을 정의할 때 자기 자신을 참조하는 것을 뜻한다.
재귀함수는 정의단계에서 자신을 재참조 하는 함수를 의미한다.
재귀함수를 가장 잘 설명할 수 있는 예시중 하나는 팩토리얼이 있다.
팩토리얼은 자연수의 계승 이라고도 하는데 그 수보다 작거나 같은 모든 양의 정수의 곱을 의미한다.
예를들어 10! 의 경우 1부터 10까지의 곱셈을 의미한다.

아래의 예시를 보자.

function factorial(e){
	let count = 1;
	if(e >1){
		for(let i=0; i<e; i++){
			count = count + (count * i)
		}
		return count;
	}
	return 1;
}

일반적으로 팩토리얼을 구현해보면
이런식으로 구현할 수 있는데

function factorial(e){
	if(e >1){
		return e * factorial(e-1)
	}
	return 1;
}

console.log(factorial(10))
// 3628800

재귀함수를 사용하면 그 자신을 불러 계속 곱하는 방식으로 팩토리얼을 구현할 수 있다.
이처럼 수학적인 재귀적으로 정의된 알고리즘을 보다 쉽게 구현할 수 있다.

콜백함수

콜백함수는 쉽게 생각해서 함수를 매개변수로 하는 함수 라고 생각하면 되겠다.
자바스크립트에서는 콜백함수를 활용한 메소드들이 정말 많은데
대표적으로는 setTimeout 이나 addEventListener,
배열의 array.forEach, map 등이 있다.

let arr = [1,2,3,4,5]
//map() 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환합니다.
const mapResult = arr.map(e=>e*2)
// mapResult = [2,4,6,8,10]

//filter() 메서드는 주어진 함수의 테스트를 통과하는 모든 요소를 모아 새로운 배열로 반환합니다.
const filterResult = arr.filter( e => e>3 )
// filterResult = [4,5]

//forEach() 메서드는 주어진 함수를 배열 요소 각각에 대해 실행합니다.
arr.forEach((e)=>{
  console.log(e * e)
})
// console : 1 4 9 16 25

//find() 메서드는 주어진 판별 함수를 만족하는 첫 번째 요소의 값을 반환합니다. 그런 요소가 없다면 undefined를 반환합니다.
const findResult = arr.find(e=>e>3)
//findResult = 4

//findIndex() 메서드는 주어진 판별 함수를 만족하는 배열의 첫 번째 요소에 대한 인덱스를 반환합니다. 만족하는 요소가 없으면 -1을 반환합니다.
const findIndexResult = arr.findIndex(e=>e>3)
//findIndexResult = 3

위에서 익명함수에서도 살펴봤던 것 처럼, 콜백함수는 익명함수를 주로 사용한다.
익명함수를 좀 더 간단하게 쓸 수 있는문법이 있는데

ES6 의 Arrow function 이다.

Arrow function (화살표함수)

기본적인 선언은 이렇게 할 수 있다.

const func = () => {}

함수표현식의 익명함수로 집어넣는 방식.
화살표함수의 장점은 선언할 때 파라미터에 따라 괄호를 생략할 수 있다는 점에 있는데,

const func1 = ()=> { ... }
const func2 = e => { ... }
const func3 = (e,n) => { ... }

파라미터가 단 한개 있을 경우 소괄호를 생략할 수 있다.
또한

const func2 = e => e * 2
// === const func2 = e => { return e * 2}
func2(2)
//4

함수의 라인이 한 블럭일 경우에 return과 중괄호를 생략할 수 있다.
화살표함수는 익명 함수로만 사용할 수 있기때문에 반드시 함수 표현식을 사용해야 한다.

function func(e) => e*2
Uncaught SyntaxError: Unexpected token '=>'

이런식으로는 사용할 수 없다.

일반적인 함수선언과 arrow function 의 가장 큰 차이점은 this 키워드의 사용에 있다.
this 를 이해하려면 자바스크립트의 정적 스코프, Lexical scope 를 이해해야 하는데
아래의 예시를 보자.

let x = '전역변수'

function func1() {
	let x = '지역변수'
	func2();
}

function func2() {
	console.log(x);
}

func1();
func2();

이 두 함수는 모두 ‘전역변수’ 를 콘솔에 찍는다.
프로그래밍의 스코프, 적용범위는 두가지의 방법중 하나를 따른다.
첫번째는 함수를 어디서 호출했는지에 따라 상위 스코프를 결정하는 것,
두번째는 함수를 어디서 선언했는지에 따라 상위 스코프를 결정하는 것이다.
전자를 동적 스코프(dynamic scope) 개념이라고 하고
후자를 정적 스코프(Lexical scope) 개념이라고 한다.

대부분의 프로그래밍 언어(자바스크립트도 마찬가지로)는 정적스코프 방식을 따른다.
그러므로 함수의 호출의 위치가 아닌, 선언의 위치를 기준으로 상위 스코프가 결정되므로
func1 내부의 let x 는 외부의 선언에 아무런 영향을 줄 수 없다.
this 는 사용하는 위치에 따라서 4개의 다른 뜻을 가지고 있는데,

  1. 일반함수 혹은 window 에서 실행되는 this
console.log(this)

//Window {0: global, 1: global, 2: global, window: Window, self: Window, document: document, name: '', location: Location, …}

(단, use stirct 엄격모드에서는 undefined 를 출력함)

  1. 오브젝트 자료형 내부에서 실행되는 this
let obj = {
	'name':'soo',
	'age':19
	isThis : function(){
		console.log(this)
	}
}

obj.isThis()
// {name: 'soo', age: 19, isThis: ƒ}

오브젝트 자료형 내부에서의 this 키워드는 오브젝트 그 자신을 의미한다.

  1. constructor 내부의 this
function ThisCon(name, age){
	this.name = name,
	this.age = age
}

let soo = new ThisCon('soo', 19)
// ThisCon {name: 'soo', age: 19}

constructor 내부의 this 는 앞으로 만들어질 오브젝트의 key 값으로 바인딩 해줄 수 있다.

  1. addEventlistner 내부의 this
document.querySelector('button').addEventListener('click',function(){
	console.log(this)
})

// 해당 버튼 == e.CurrentTarget

해당 버튼요소를 가리킨다.
그런데,

document.querySelector('button').addEventListener('click',()=>{
	console.log(this)
})

////Window {0: global, 1: global, 2: global, window: Window, self: Window, document: document, name: '', location: Location, …}

화살표 함수에서 eventListener 를 사용하면 window 가 출력된다.
왜냐하면,
기본적인 함수선언은 이처럼 this를 동적으로 바인딩 하는데 반해
화살표함수는 this를 정적으로 바인딩 한다.

혼동하지 않기 위해 다시한번 상기하자면,
자바스크립트는 정적스코프 규칙을 따르지만,
기본적인 함수선언은 동적바인딩 규칙을 따르고
arrow function은 정적바인딩 규칙을 따른다는 점이다.
따라서 eventlistener 를 사용할 때에는 arrow function 사용에 주의해야한다.

profile
Always, we are friend 🧡

0개의 댓글