[Javascript] 함수(Function)

silverj-kim·2020년 8월 23일
1

OREILLY의 러닝 자바스크립트 를 공부하며 정리한 CHAPTERR 6 함수 내용입니다.

함수

함수는 하나의 단위로 실행되는 문의 집합으로 일종의 부속 프로그램으로 생각해도 좋다.
모든 함수에는 바디가 있으며 함수 바디는 함수를 구성하는 문의 모음.
아래는 함수 선언(function declaration) 의 한 예이다.

function sayHello() {
    //함수 바디는 여는 중괄호로 시작
    console.log("Hello world!");
    // 닫는 중괄호로 종료
}

함수 선언만 해서는 바디가 실행되지 않는다. 함수를 호출(call) 해줘야 한다.

sayHello();

호출한다(call, invoke)와 실행한다(execute, run)는 섞어서 사용해도 된다. 컨텍스트와 언어에 따라 용어 사이에 미묘한 차이가 있을 수 있지만 일반적으로느 같다고 봐도 된다.


반환값

함수 바디 안에 return 키워드를 사용하면 함수를 즉시 종료하고 값을 반환한다.
그 값이 바로 함수 호출의 값이다.

function getGreeting() {
	return "Hello world!";
}

console.log(getGreeting()); //Hello world!

return을 명시적으로 호출하지 않으면 반환 값이 undefined가 된다.


호출과 참조

자바스크립트에서는 함수도 객체다. (first class object)
따라서 다른 객체와 마찬가지로 넘기거나 할당할 수 있다. 함수 호출과 참조의 차이를 정확히 이해하는 것이 중요하다.

getGreeting(); //"Hello world!"
getGreeting; //function getGreeting()

getGreeting() 처럼 함수 식별자 뒤에 괄호를 쓰면 함수를 호출 하는 것이고.
getGretting 처럼 쓰면 다른 값과 마찬가지로 함수를 참조하는 것이며 함수가 실행되지 않는다.

const f = getGeeting;
f(); //"Hello world!"

const o = {};
o.f = getGreeting;
o.f(); //"Hello world!"

const a = [];
a[1] = getGreeting;
a[1](); //"Hello world!"

함수를 호출하지 않고 다른 값과 마찬가지로 참조하기만 할 수 있다는 특징은 자바스크립트를 매우 유연한 언어로 만든다.
값 뒤에 괄호를 붙이면 자바스크립트는 그 값을 함수로 간주하고 호출한다. 따라서 함수가 아닌 값에 괄호를 붙이면 에러가 발생한다.

"abc" is not a function


함수와 매개변수

함수를 호출하면서 정보를 전달할 때는 함수 매개변수(argument, parameter) 를 이용한다. 매개변수는 함수가 호출되기 전에는 존재하지 않는다는 점을 제외하면 일반적인 변수나 마찬가지이다.

function avg(a,b) {
    return (a+b)/2;
}

avg(5, 10); //2

위 예제에서 a,b를 정해진 매개변수 (formal argument)라고 하며 함수가 호출되면 정해진 매개변수는 실제 매개변수 (actual argument)가 된다.
실제 매개변수는 변수와 비슷하지만 함수 안 에서만 존재 한다

const a = 5, b = 10;
avg(a,b);

첫 행의 변수 a, b는 함수 avg의 매개변수인 a,b와 같은 이름 이지만, 엄연히 다른 변수이다.
함수를 호출하면 함수 매개변수는 변수 자체가 아니라 그 값을 전달받는다. a, b를 전달 받는 것이 아니라 5, 10을 전달 받는다.

아래는 같은 이름의 변수가 함수 밖에 존재해도 매개변수가 함수 안에서만 존재하는 예제이다.

function f(X) {
    console.log(`f 내부 : x=${x}`);
    x = 5;
    console.log(`f 내부 : x=${x} (할당 후)`);
}

let x = 3;
console.log(`f를 호출하기 전: x=${x}`);
f(x);
console.log(`f를 호출한 다음: x=${x}`);

//실행결과
f를 호출하기 전: x=3
f 내부: x=3
f 내부: x=5 (할당 후)
f를 호출 한 다음: x=3

함수 안에서 x에 값을 할당 하더라도 함수 바깥의 x에는 아무 여향도 없다는 것이 중요 뽀인트!
하지만 함수 안에서 객체 자체를 변경하면 그 객체는 함수 바깥에서도 바뀐다.

function f(o) {
    o.message = `f 안에서 수정함 (이전 값: ${o.message})`
}
let o = {
	message: "초기값"
}

console.log(`f를 호출하기 전: o.message = ${o.message}`);
f(o);
console.log(`f를 호출한 다음: o.message = ${o.message}`);

//실행결과
f를 호출하기 전: o.message = 초기값
f를 호출한 다음: o.message = f 안에서 수정함 (이전 값: 초기 값)

함수 f 안에서 객체 o를 수정했고 그게 반영되어 있음을 알 수 있다.
이것이 원시값과 객체의 핵심적인 차이 이다.
원시값은 불변이므로 수정할 수 없지만 객체는 바뀔 수 있다.

좀 더 명확히 말하면 함수 안의 o와 함수 바깥의 o는 서로 다른 개체이다. 하지만 그 둘은 같은 객체를 가리키고 있다.

function f(o) {
    o.message = "f에서 수정함";
    o = {
        message: "새로운 객체"
    }
    console.log(`f 내부: o.message = ${o.message} (할당 후)`);
}
let o = {
    message: 초기값
}
console.log(`f를 호출하기 전 o.message = ${o.message}`);
f(o);
console.log(`f를 호출한 다음: o.message = ${o.message}`);

//실행결과
f를 호출하기 전: o.message = 초기값
f 내부: o.message = 새로운 객체 (할당 후)
f를 호출한 다음 o.message = f에서 수정함

위 예제에서 보면 명백히 함수 내부의 매개변수 o와 함수 바깥의 변수 o는 다르다. f를 호출하면 둘은 같은 객체를 가르키지만 f 내부에서 o에 새로 할당한 객체는 바깥의 o와 상관없는 새로운 객체이다. 함수 바깥의 o는 여전히 원래 객체를 가리키고 있다.

자바스크립트의 원시값을 값 타입(value type) 이라고 말한다. 원시값을 전달할 때 값이 복사되기 때문이다. 객체느 참조 타임(reference type) 이달고 말한다. 객체를 전달할 때 두 변수는 같은 객체를 가리키기 때문이다.

매개변수가 함수를 결정하는가?

여러 언어에는 함수의 시그니처에는 매개변수가 포함 된다.
예를 들어 c언어에서 매개변수 없는 함수 f()는 f(x), f(x,y)와 다르다. 하지만 자바스크립트에서는 그런 차이가 없다. 함수 f가 있다면 매개변수를 몇 개 전달하든 같은 함수를 호출하는 것이다.
정해진 매개변수에 값을 제공하지 않으면 암시적으로 undefined가 할당된다.

매개변수 해체

//객체
function getSentence({ subject, verb, object }) {
    return `${subject} ${verb} ${object}`;
}
const o = {
    subject: "I",
    verb: "love",
    object: "Javascript"
}
getSentence(o); //I love Javascript

//배열
function getSentence([subject, verb, object]) {
    return `${subject} ${verb} ${object}`;
}
const arr = ["I", "love", "Javascript"];
getSentence(arr); //I love Javascript

//확장 연산자 사용
function addPrefix(prefix, ...words) {
    const prefixedWords = [];
    for(let i = 0; i < words.length; i++) {
        prefixedWords[i] = prefix + words[i];
    }
    return prefixedWords;
}
addPrefix("con", "verse", "vex"); //["converse", "convex"]

매개변수 기본 값

ES6에서는 매개변수 기본값(default value)를 지정하는 기능이 추가됐다.
매개변수에 값을 제공하지 않으면 undefined가 할당되지만 기본 값을 설정해주면 그 값이 할당된다.

function f(a, b=3) {
    return `a - b`;
}
f(5,2); // 5-2
f(5); // 5-3

객체의 프로퍼티인 함수

객체의 프로퍼티인 함수를 메서드(method)라고 부르며 일반적인 함수와 구별한다.
아래와 같이 메서드를 추가한다.

const o = {
    name: "Wallace",
    bark: function() { return "Woof!"; }
}

const o2 = {
    name: "Wallace",
    bark() { return "Woof!"; }
}

this 키워드

함수 바디 안에는 특별한 읽기 전용 값인 this가 있다. this는 일반적으로 객체지향 프로그래밍(Object Oriented Programming) 개념에 밀접한 연관이 있다.
일반적으로 this는 객체의 프로퍼티인 함수에서 의미가 있다. 메서드를 호출하면 this는 호출한 메서드를 소유하는 객체가 된다.

const o = {
    name: "Wallace",
    speak() { return `My name is ${this.name}`; }
}

o.speak()를 호출하면 this는 o에 묶인다.
this는 함수를 어떻게 선언했으냐가 아니라 어떻게 호출했느냐에 따라 달라진다는 것을 이해해야 한다.
즉, this가 o에 묶인 이유는 speak가 o의 프로퍼티여서가 아니라 o에서 speak를 호출했기 때문이다.

const speak = o.speak;
speak === o.speak //true
speak(); //My name is undefined!

speak 와 o.speak는 같은 함수를 가리키지만 speak()로 호출하면 자바스크립트는 이 함수가 어디에 속하는지 알 수 없으므로 thisundefined에 묶인다.

중첩된 함수 안에서 this를 사용한 예제를 보자.

const o = {
    name: "Gil dong Hong",
    greeting: function() {
        function getFirstName() {
            const words = this.name.split(' ');
            const firstName = words[2];
            return firstName;
        }
        return `Hello ${getFirstName()}`;
    }
}
o.greeting(); //Hello undefined

greeting() 을 호출한 시점에서 자바스크립트는 this를 의도한대로 o에 묶지만, getFirstName()을 호출하면 this는 더이상 o에 묶이지 않는다.
이런 문제를 해결하기 위해 널리 사용하는 방법은 다른 변수에 this를 할당하는 것이다.

const o = {
    name: "Gil dong Hong",
    greeting: function() {
        const self = this;
        function getFirstName() {
            const words = self.name.split(' ');
            const firstName = words[2];
            return firstName;
        }
        return `Hello ${getFirstName()}`;
    }
}
o.greeting(); //Hello Hong

함수표현식과 익명 함수

지금까지는 함수 선언만 봤지만 자바스크립트에서는 익명함수(anonymous function)도 지원한다. 익명함수에는 식별자가 없다.
익명함수는 함수표현식(function expression) 으로 선언되어 식별자에 할당 할 수도 즉시 호출(IIFE(Immediatelyn invoked function expression)) 할 수도 있다.

const f = function() {
	...
}

복잡하게 생각할 건 없고 호출할 생각으로 함수를 만든다면 함수 선언을 사용하고, 다른 곳에 할당하거나 다른 함수에 넘길 목적으로 함수를 만든다면 함수 표현식을 사용하면 된다.


화살표 표기법 (arrow function)

ES6에서 새로 만든 화살표 표기법(arrow function)은 간단히 말해 function이라는 단어와 중괄호 숫자를 줄이려고 고안된 단축 문법이다.
아마 요즘에는 많이 사용하는 추세여서 자바스크립트를 공부하는 사람은 다들 접해봤을 거라고 생각한다.

const f1 = function(name) { return `hello ${name}`; };
// 또는
const f1 = (name) => `hello ${name}`;

arrow function은 익명함수를 만들어 다른 곳에 전달하려 할 때 가장 유용하다.
일반 함수와 arrow function의 가장 큰 차이점은 this가 다른 변수와 마찬가지로 정적으로(lexically) 묶인다는 점이다.
아까 위에서 작성한 예제를 그대로 사용해보면 아래와 같다.

const o = {
    name: "Gil dong Hong",
    greeting: function() {
        const getFirstName = () => {
            const words = this.name.split(' ');
            const firstName = words[2];
            return firstName;
        }
        return `Hello ${getFirstName()}`;
    }
}
o.greeting(); //Hello Hong

arrow function을 쓰면 this를 내부 함수 안에서 그대로 사용할 수 있다.


call과 apply, bind

this를 사용하는 일반적인 방법은 위에서 살펴봤다. 다른 객체지향 언어에서도 this를 이런식으로 사용한다. 자바스크립트에서는 이 외에도 함수를 어디서, 어떻게 호출했느냐와 상관없이 this가 무엇인지 지정할 수 있다.

call메서드는 모든 함수에서 사용할 수 있으며 this를 특정 값으로 지정할 수 있다.

const bruce = { name: "Bruce" };
//이 함수는 어떤 객체와도 연결되어 있지 않지만 this를 사용한다.
function greet() {
    return `Hello ${this.name}!`;
}

greet();		// Hello undefined!
greet.call(bruce);	// Hello Bruce!

function update(birthYear, occupation) {
    this.birthYear = birthYear;
    this.occupation = occupation;
}

update.call(bruce, 1949, "singer");
update.apply(bruce, [1949, "singer"]);
// bruce = { name: "Bruce", birthYear: 1949, occupation: "singer" }

함수를 호출하면서 call을 사용하고 this로 사용할 객체를 넘기면 해당 함수가 주어진 객체의 메서드인 것 처럼 사용할 수 있다. call의 첫번째 매개변수는 this로 사용할 것이고, 매개변수가 더 있으면 그 매개변수는 호출하는 함수로 전달된다.

apply는 함수 매개변수 처리 방법을 제외하면 call과 완전히 같다. call은 일반적인 함수와 마찬가지로 매개변수를 직접 받지만 apply 는 매개변수를 배열로 받는다.

bind를 사용하면 함수의 this값을 영구히 바꿀 수 있다.
update 메서드를 이리저리 옮기면서도 호출할 때 this값은 항상 bruce가 되게끔 call이나 apply, 다른 bind와 함께 호출하더라도 this값이 항상 bruce가 되도록 하려면 bind를 사용해야 한다.

const madeline = { name : "Madeline" };
const updateBruce = update.bind(bruce);
updateBruce(1904, "actor");
// bruce = { name: "Bruce", birthYear: 1904, occupation: "actor" }
updateBruce.call(madeline, 1274, "king");
// bruce = { name: "Bruce", birthYear: 1274, occupation: "king" }
// madeline은 변하지 않는다.

bind는 함수의 동작을 영구히 바꾸므로 어려운 버그의 원인이 될 수 있다.
bind는 매우 유용하지만 함수의 this가 어디에 묶이는 지 정확히 파악하고 사용해야 한다.

나는 주로 실제 프로젝트에서 bind만 사용하고 대부분의 경우에서는 arrow function만 사용하는데 callapply도 종종 사용해봐야겠다.

profile
Front-end developer

0개의 댓글