함수

김수정·2020년 4월 7일
0

함수의 용도

  1. 중복 피하기
  2. 기능을 분리하기

함수 생성

기명함수 표현식

이름이 있는 함수 표현식을 나타내는 용어입니다.

let sayHi = function func(who) {
  alert(`Hello, ${who}`);
};

함수에 이름을 넣었지만 이는 여전히 함수 표현식으로 표현된 함수 입니다. 함수 선언문으로 바뀌지 않으며 외부에서 func로 호출할 수 없습니다.
오직 함수 표현식 내부에서 자신을 참조하는 용도로 사용가능합니다. 바깥에서 함수 표현식으로 할당한 변수의 값을 바꾸어 버려도 func는 그대로이므로 func로 호출한 내용은 에러가 나지 않습니다. 재귀 함수 등에서 좋을 것 같네요.

let sayHi = function(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    sayHi("Guest"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // sayHi는 더 이상 호출할 수 없습니다!

함수 표현식

함수 선언식

new Function

잘 사용하는 방법은 아니고 이 방법 외에 대안이 없을 때 사용합니다.
이 방법은 다른 방법들과 달리 런타임에 받은 문자열을 사용해 함수를 만들 수 있다는 점입니다. 서버에서 전달받은 문자열을 이용해 새로운 함수를 만들고 이를 실행하는 것도 가능합니다.

let func = new Function ([arg1, arg2, ...argN], functionBody)

let sum = new Function('a', 'b', 'return a + b');
alert( sum(1, 2) ); // 3

let sayHi = new Function('alert("Hello")');
sayHi(); // Hello

한 가지 특이한 점은, 클로저의 동작방식입니다. new Function을 통해 만든 함수는 [[Environment]]프로퍼티가 현재 렉시컬 환경이 아닌 전역 렉시컬 환경을 참조합니다. 따라서 이 함수는 오직 전역 변수에만 접근할 수 있습니다.

function getFunc() {
  let value = "test";
  let func = new Function('alert(value)');

  return func;
}

getFunc()(); // ReferenceError: value is not defined

함수의 실행 원리

call stack

자바스크립트의 함수는 스택 구조입니다.
구체적으로 실행될 때 스택 구조를 확인하려면 chrome devTools>sources>call stack에서 확인할 수 있습니다.
스택은 Last-in-First-Out 원리로 되어 있습니다.

실행 컨텍스트

자바스크립트가 실행되는 전역 공간부터 실행 컨텍스트(execution context)가 실행됩니다.
새로운 함수가 호출되면 기존의 실행 컨텍스트를 중단하고 실행 컨텍스트 스택에 임시 저장합니다.
새로운 함수의 새로운 실행컨텍스트를 열면서 콜스택에 쌓이게 됩니다.
새로운 함수의 실행이 끝난 후 실행 컨텍스트 스택에서 중단했던 실행 컨텍스트를 다시 꺼내와서 진행합니다.

실행 컨텍스트는 크게 2부분으로 나뉘고 아래 순서로 진행됩니다.
(1) Creation - lexical Environment에 입각하여 환경 값들을 정의하는 단계
(2) Execution - 코드를 실행하면서 값을 할당하고 변화시켜 가는 단계

Lexical Environment

자바스크립트는 실행 중인 함수, 코드 블록({...}), 스크립트 전체가 렉시컬 환경을 갖고 있습니다. 렉시컬 환경이라는 것은 내부에 숨겨진 상태로 연관되어 있는 객체라는 뜻인데 명세에 이론상으로만 존재하기에 실제 접근 및 변경은 불가능하며, 각각 자바스크립트 호스팅 환경에서 자체적으로 구현해놓은 것입니다.

렉시컬 환경은 아래와 같이 구성됩니다.
(1) 환경 레코드 (Environment Record) - 모든 식별자와 참조 혹은 값(변수, 함수...)을 프로퍼티로 저장하고 있는 객체. 식별자와 값을 매핑합니다.
(2) 외부 렉시컬 환경(Outer Lexical Environment) - 외부 환경을 참조하는 포인터. 스코프 체인을 위해 존재합니다.
(3) this binding

* 지금부터 렉시컬 환경에 대한 설명을 할 때 사용한 사진은 전부 https://ko.javascript.info/closure 에서 가져왔습니다.
변수는 환경레코드의 프로퍼티일 뿐이며 동작하는 원리는 아래 그림과 같습니다.
선언되기 전에는 TDZ 상태이며 현재는 전역공간에서 실행되므로 참조하는 외부 환경이 없습니다.

함수를 호출하여 실행하면 새로운 렉시컬 환경이 자동으로 만들어집니다.
내부 렉시컬 환경 - 매개변수와 함수의 지역변수가 저장됩니다.
외부 렉시컬 환경 - 아래 그림의 say함수가 참조하고 있는 외부 변수와 실행된 곳을 가리키고 있습니다.

코드에서 변수에 접근할 때, 먼저 내부 렉시컬 환경에서 찾고 없다면 외부 렉시컬 환경으로 점차 검색 영역을 확대하여 최대 전역영역까지 찾습니다.

closure

클로저가 만들어지는 과정을 렉시컬 환경으로 설명해 봅시다.

(1) makeCounter를 실행하면 새로운 렉시컬 환경이 만들어집니다. 이 때도 역시 내부, 외부 두 개가 생기겠죠?

(2) makeCounter 함수는 중첩익명함수를 가지고 있고, 거기서 count 변수를 참조하고 있습니다. 모든 함수는 함수가 생성된 곳의 렉시컬 환경을 [[Environment]]라는 숨겨진 프로퍼티에 렉시컬 환경에 대한 참조를 저장하고 있습니다. Environment는 함수가 생성될 때 딱 한 번 그 값이 세팅되고 영원히 변하지 않습니다.

(3) 이제 counter를 실행시키면 해당 함수의 내부에서 count변수를 찾고, 없으니 외부 렉시컬 환경([[Environment]])에서 찾습니다. 이를 클로저라고 합니다. 자바스크립트는 한가지 예외를 제외하고 이렇게 쉽고 자연스럽게 클로저를 만들 수 있습니다.

(4) 함수 호출이 끝나면 그 함수에 대응하는 렉시컬 환경이 모두 메모리에서 제거되지만 렉시컬 환경 객체를 참조하는 클로저는 제거되지 않습니다. 또, 클로저를 여러번 호출하면 다 각자의 렉시컬 환경이 따로 만들어지고 이들은 메모리에 그대로 있습니다. 그 이유는 원래 함수의 반환값인 이 클로저 함수를 받은 변수가 어딘가에 살아있기 때문입니다. 그들도 제거한다면 클로저도 제거됩니다.

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // g가 살아있는 동안엔 연관 렉시컬 환경도 메모리에 살아있습니다.
g = null; // 도달할 수 없는 상태가 되었으므로 메모리에서 삭제됩니다.

함수 선언문의 scope

함수 선언문은 변수와 다르게 바로 초기화가 되어 선언되기 전에도 사용이 가능합니다.
sloppy mode에서는 브라우저마다 동작 방법이 다르고 안전성을 보장하지 못합니다.
strict mode에서는 함수선언문도 블록스코프에 갇힙니다.

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

parameter

default parameter

기본 값을 설정할 수 있는 방법이 생겼습니다.

old

const f = function (x, y, z) {
  x = x !== undefined ? x : 3
  y = typeof x !== "undefined" ? y : 4
  console.log(x, y)
}
f(0, null)

new

const f = function (a = 1, b = 2, c = 3, d = 4, e = 5, f = 6) {
  console.log(a, b, c, d, e, f)
}
f(7, 0, "", false, null)

default parameter로 값, 식이 올 수 있습니다. 단, 뒤에 있는 변수를 참조하는 건 안됩니다. TDZ에 걸리기 때문이죠.

correct

const multiply = function (x, y = x * 2) {
  console.log(x * y)
}
multiply(2, 3)
multiply(2)

wrong

const multiply = function (x = y * 3, y) {
  console.log(x, y)
}
multiply(2, 3)
multiply(undefined, 2)

인수가 너무 많을 경우, 차라리 객체로 인자값을 넘기는 게 낫습니다. 그런데 이 때는 객체가 인자로 안들어오면 문제가 될 수 있으므로 기본값에 빈객체 설정을 넣어줍니다.

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert( `${title} ${width} ${height}` );
}

showMenu(); // Menu 100 200

rest parameter

함수는 정의 방법과 상관없이 인수의 개수에 제약이 없습니다. 따라서 인수를 정하지 않고 받고 싶을 때 나머지 매개변수가 유용합니다.
rest parameter는 꼭 맨 마지막에 있어야만 동작하며 함수 안에서 나머지 매개 변수들을 완벽한 배열 로 받습니다. 유사배열객체여서 배열 메소드를 쓰려면 call등을 써야 했던 arguments와 다릅니다. 이전부터 존재하던 arguments는 이제 rest parameter로 대체되고 있어 화살표 함수에는 없습니다.

function showName(firstName, lastName, ...titles) {
  alert( firstName + ' ' + lastName ); // Julius Caesar

  // 나머지 인수들은 배열 titles의 요소가 됩니다.
  // titles = ["Consul", "Imperator"]
  alert( titles[0] ); // Consul
  alert( titles[1] ); // Imperator
  alert( titles.length ); // 2
}

showName("Julius", "Caesar", "Consul", "Imperator");

spread syntax

...이터러블 객체를 사용하면 이터러블 객체 안에 있는 요소들을 하나씩 꺼내어 전개할 수있습니다. 요소 하나 하나가 객체라는 껍데기를 벗고 나오는 것이죠. 이는 rest parameter와 매우 유사하게 생겼지만 사용법은 전혀 다릅니다. 전개 문법은 여러 개를 사용해도 상과 없고, 중간에 와도 상관 없습니다.

let arr = [3, 5, 1];

alert( Math.max(...arr) ); // 5 (전개 문법이 배열을 인수 목록으로 바꿔주었습니다.)

this

함수의 실행 주체는 this입니다.
자바스크립트에선 모든 함수에 this를 사용할 수 있지만 this값은 실행 될 때 할당되므로 실행 컨텍스트에 따라 다르게 정해집니다.

  • 객체 안에서 호출된 this -> 해당 객체가 this
let user = { name: "John" };
function sayHi() {
  alert( this.name );
}
user.f = sayHi;
user.f(); // John  (this == user)
  • 전역함수, 익명 함수에서 호출된 this -> window객체
function sayHi() {
  alert(this);
}

sayHi(); // window (this === window)

call forwarding & method borrowing

this의 유동성 때문에 예상치 못한 결과를 얻는 경우가 종종 있죠. 우리가 원하는 this로 자동전환이 되지 않을 때 우리가 this를 고정하는 방법을 알아봅시다.
이 방식을 이용해서 해당 자료형에만 속해있는 내장 메서드 등을 다른 자료형에도 사용할 수 있게 됩니다.

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

(1) func.call(context, arg1, arg2, ...) [자주사용. 좀 더 최적화]
func의 인수를 하나씩 따로 따로 받음.

(2) func.apply(context, args)
func의 인수를 2번째 인자로 유사배열객체 형식으로 받음.

함수 바인딩

this 정보가 사라질 때, 연결하는 방법들에 대해 알아봅시다.

(1) wrapper함수
간결한 방법이지만 외부 환경에 의존하므로 외부 변수가 바뀌어버릴 때 문제가 있습니다.

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000); // user의 정보가 중간에 바뀌어 버린다면 의도한 John이 나오지 않습니다.

(2) bind
모든 함수는 this를 수정하게 해주는 내장 메서드 bind를 제공합니다.

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// this를 user로 바인딩합니다.
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (인수 "Hello"가 넘겨지고 this는 user로 고정됩니다.)

(2-1) 인수 바인딩
this 바인딩보다 잘 쓰이진 않지만 유용한 문법입니다.

function mul(a, b) {
  return a * b;
}

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

위와 같이 함수의 첫 번째 인수를 2로 고정했습니다. 바인드는 컨텍스트를 항상 넘겨줘야 하기 때문에 컨텍스트 부분은 null로 넘겨주었습니다.
인수 일부는 고정하고 컨텍스트 this는 고정하고 싶지 않다면 헬퍼 함수를 구현합니다.

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

// 사용법:
let user = {
  firstName: "John",
  say(time, phrase) {
    alert(`[${time}] ${this.firstName}: ${phrase}!`);
  }
};

// 시간을 고정한 부분 메서드를 추가함
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// 출력값 예시:
// [10:00] John: Hello!

함수의 종류

arrow function

화살표 함수는 다른 곳에서 실행되는 작은 함수로 써야할 때(보통 익명함수 형태로) 좋습니다.
화살표 함수들은 자신의 "컨텍스트"가 없고, 오히려 현재 컨텍스트에서 작동하는 짧은 코드를 위해 만들어졌습니다.

(1) this가 없습니다.
함수 스코프지만 실행 컨텍스트 생성 시 this 바인딩을 하지 않습니다. 따라서 메서드로 쓰는 건 안좋고 메서드 안에서 내부 함수로 쓰일 때 쓰는 게 좋습니다. 화살표 함수에서 this를 접근하면 외부 렉시컬 환경을 가리킵니다.
this 바인딩을 하지 않기 때문에 call() 등으로 명시적 this 바인딩도 할 수 없습니다.

(2) 생성자 함수로 사용 못합니다.
함수 안에 prototype property가 없으므로 생성자 함수로도 사용할 수 없습니다.

(3) 인수가 없습니다.
arguments, new.target 등을 바인딩하지 않습니다.

(4) super가 없습니다.
super를 바인딩하지 않습니다.

재귀함수

재귀란

작업을 단순화하고 자기 자신을 다시 호출하는 것입니다.

재귀적 사고

문제를 해결할 때, 반복적인 사고로 반복문을 이용하여 해결할 수도 있고, 자기 자신을 다시 호출하여 해결하는 재귀적 사고가 있습니다.

(1) 재귀의 베이스
명확한 결괏값을 즉시 도출하는 부분

(2) 재귀 단계
목표 작업을 간단한 동작과 목표 작업을 변형한 작업으로 분할합니다.

(3) 재귀 깊이
중첩 호출의 최대 개수. 자바스크립트에서 만 개까지 확실히 허용. 그 이상은 엔진에 따라 다릅니다.

데코레이터

인수로 받은 함수의 행동을 변경시켜주는 함수.

함수 객체

자바스크립트에서 함수는 값이고, 자료형은 객체입니다.
함수는 객체 중에서도 호출 가능한 행동 객체이며, 객체처럼 프로퍼티 추가/제거 및 참조를 통한 전달이 가능합니다.

name 프로퍼티

함수의 이름을 가져옵니다. 이름이 없는 함수도 꽤 똑똑하게 유추하여 이름을 지어주지만, 그도 여의치 않다면 undefined를 반환합니다.

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

length 프로퍼티

함수의 매개변수의 개수를 반환합니다. 그러나 rest parameter는 인지하지 못합니다.
인수의 종류에 따라 인수를 다르게 처리하는 방식을 다형성이라고 부릅니다. 다형성 코드를 만들 때 length 프로퍼티를 사용할 수 있습니다.

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// 사용자가 OK를 클릭한 경우, 핸들러 두 개를 모두 호출함
// 사용자가 Cancel을 클릭한 경우, 두 번째 핸들러만 호출함
ask("질문 있으신가요?", () => alert('OK를 선택하셨습니다.'), result => alert(result));

사용자 지정 프로퍼티

함수도 객체이므로 개발자가 직접 프로퍼티를 추가할 수도 있습니다.
그러나 프로퍼티는 변수가 아닙니다. 객체에 프로퍼티를 저장할 수 있지만 이는 실행에 아무 영향을 주지 않습니다. 또, 클로저 대신 사용자 지정 프로퍼티를 사용한다면 외부에서 수정이 가능하다는 점을 고려해야 합니다.

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

호출 스케줄링

일정 시간이 지난 후에 원하는 함수를 예약 실행하는 것입니다.
자바스크립트 명세서엔 이 것들이 없지만 모든 브라우저 및 자바스크립트 호스트 환경 대부분이 이와 유사한 메서드와 내부 스케줄러를 지원합니다.

setTimeout

일정 시간 후, 한 번 실행합니다.
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

  • func|code: 실행하려는 코드. 하위호환성을 위해 문자열도 가능하지만 함수로 작성할 것을 권합니다.
  • delay: 실행 전 대기 시간. 단위는 밀리초이며 기본 값은 0입니다.
  • argN: 함수에 전달할 인수들. IE9까지는 지원하지 않습니다.
function sayHi(phrase, who) {
  alert( phrase + ', ' + who );
}

let timerId = setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
clearTimeout(timerId); // timer cancel

delay 인수가 0이더라도, 바로 시작한다는 의미가 아니라 현재 스크립트의 실행이 종료된 직후에 이 함수가 실행된다는 의미입니다.
왜냐하면 스케줄러는 현재 스크립트의 실행이 종료되고 나서야 스케줄러에 있는 일을 확인하기 때문입니다.

setInterval

일정 시간 마다 실행합니다.
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

// 2초 간격으로 메시지를 보여줌
let timerId = setInterval(() => alert('tick'), 2000);

// 5초 후에 정지
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

setInterval은 중첩 setTimeout으로도 구현가능한데 작용 방식이 조금 다릅니다.
중첩 setTimeout은 지연 간격을 보장하지만 setInterval은 이를 보장하지 않습니다.
100초마다 실행하는 코드라면, 첫 번째가 setInterval, 두 번째가 setTimeout입니다.

가비지 컬렉션
setInterval, setTimeout에 넘긴 함수들은 가비지 컬렉션의 대상이 되지 않습니다. 혹시 함수가 외부 렉시컬 환경을 참조하고 있다면 훨씬 큰 공간을 메모리에서 차지하고 있기 때문에 아무리 작은 함수라도 취소를 꼭 해주어야 합니다.

profile
정리하는 개발자

0개의 댓글