(8장) 함수 [자바스크립트 완벽 가이드 7판]

iberis2·2023년 1월 29일
0

🗂 목차

  • 객체로써의 함수 : 프로퍼티와 메서드
    • 함수의 프로퍼티
    • 함수의 메서드
  • 함수 생성하기
    • 함수 선언문과 함수 표현식
    • 화살표 함수
  • 함수 호출하기
    • 함수 호출과 this
    • 생성자 함수 호출
  • 함수의 파라미터
    • 파라미터의 기본값
    • 나머지 연산자, 분해 연산자, 구조분해할당
    • arguments 객체
  • 함수의 바디
    • 로컬 네임스페이스
    • 클로저
  • 함수형 프로그래밍

기본 용어정리
파라미터(parameter) : 함수 바디에서 로컬 변수처럼 동작하는 매개변수
아규먼트(argument) : 함수에 전달되는 전달인자
매서드(method) : 객체의 프로퍼티로 할당된 함수

function myFunction (para) { // para 가 파라미터이다
  // { } 중괄호 내부가 함수 바디이다. 
  console.log(para);
}

myFunction('arg') // arg 가 아규먼트 이다.

// 객체의 프로퍼티로 있는 함수가 매서드이다.
const myObj = {
  method: function myFunction(x) {
    console.log(x);
  },
}; 
myObj.method(1);

💻 자바스크립트 함수

자바스크립트의 함수도 객체?!😲

typeof Function 은 문자열 'function'을 반환하지만 사실 함수는 특별한 종류의 자바스크립트 객체입니다.

MDN
JavaScript의 함수는 다른 모든 객체처럼 속성메서드를 가질 수 있으므로 일급(first-class) 객체입니다. 다른 객체와 함수를 구별하는 것은, 함수는 호출할 수 있다는 점입니다. 간단히 말해, 함수는 Function 객체입니다.

JavaScript의 함수를 1급 객체로 표현하기도 하는데, 1급 객체의 특징은 다음과 같습니다.

  • 변수나 데이터 구조안에 담을 수 있다.
  • 파라미터로 전달 할 수 있다.
  • 반환값(return value)으로 사용할 수 있다.
  • 동적으로 프로퍼티 할당이 가능하다.

즉, 함수 자체를 '값'으로 활용할 수 있습니다.

📁 객체로써의 함수 : 프로퍼티와 메서드

📑 함수의 프로퍼티

먼저 객체로써의 특징인 함수의 프로퍼티와, 메서드를 살펴보겠습니다.
함수는 length, name, prototype 등의 기본 프로퍼티를 가지고 있고, 새로운 프로퍼티도 정의할 수 있습니다.

  • length : 함수의 파라미터의 개수이며, ...rest파라미터는 length에 포함되지 않습니다.
  • name : 함수 이름을 프로퍼티 값으로 갖고, 익명함수의 경우 변수명이 name의 프로퍼티 값이 됩니다.
  • prototype : 화살표 함수를 제외하고, 모든 함수는 객체를 참조하는 prototype 프로퍼티를 가집니다.

익명함수(Anonymous Function)와 기명 함수 표현식(Named function Expression)
함수 이름이 있는 함수는 기명함수, 함수 이름이 없는 함수는 익명 함수라고 합니다.

/* 익명 함수 (Anonymous function) */
const greeting = function (){
 console.log('hi');
}

greeting.name; // greeting

/* 기명 함수 표현식 (Named function Expression) */
const greeting2 = function printHi(){
  console.log('hi');
}

함수 선언문에서는 함수 이름을 생략할 수 없으며, 변수에 함수를 할당하는 함수 표현식에서는 함수 이름을 생략하여 익명함수를 만들 수 있습니다.
익명함수의 경우 변수명이 name 프로퍼티 값이 됩니다.
함수 표현식으로 만든 함수에 이름을 붙이는 경우 기명 함수 표현식(Named function Expression) 이라고 합니다.

quiz ⁉️
익명함수의 name 프로퍼티 값은 함수가 할당된 변수명입니다. 그렇다면,

  • 기명 함수 표현식의 name 프로퍼티 값은 어떻게 될까요?
  • 기명 함수 표현식은 변수명함수 이름 둘 다로 호출할 수 있을까요?
  • 기명 함수 표현식에서 함수 이름은 어떻게 활용될까요?
    함수 생성하기 - 함수 표현식 - 재귀함수에서 정답을 확인할 수 있습니다.

함수에 자체적으로 만든 프로퍼티를 추가할 수도 있습니다.

/* 함수 호출 횟수를 세는 counter 프로퍼티 만들기 */
function printHi() {
  console.log("Hi");

  // 함수를 호출할 때마다 프로퍼티 값 증가
  printHi.counter++;
}

printHi.counter = 0; // 초깃값

printHi(); // Hi
printHi(); // Hi

printHi.counter; // 2

📑 함수의 메서드

내장 메서드로는 .call(), .apply(), .bind(), .toString() 등을 가지고 있습니다.

call 과 apply 메서드

const myCat = { name: "mimi", age: 10, };

function introduceMyPet (animal="우리"){
  return `${animal} ${this.name}의 나이는 ${this.age}살 입니다.`;
}

/* 전달하는 아규먼트가 없을 때에는 .call(), .apply() 중 아무거나 사용 가능합니다. */
introduceMyPet.call(myCat); // "우리 mimi의 나이는 10살 입니다." 
introduceMyPet.apply(myCat); // "우리 mimi의 나이는 10살 입니다." 

/* .apply() 와 .call()의 차이는 아규먼트를 배열로 전달한다는 것입니다. */
introduceMyPet.call(myCat, '고양이'); // "고양이 mimi의 나이는 10살 입니다." 
introduceMyPet.apply(myCat, ['고양이']); // "고양이 mimi의 나이는 10살 입니다." 

함수.call()함수.apply() 매서드는 첫 번째 파라미터로 객체를 받고, 두 번째 이후 파라미터로 아규먼트를 받습니다.

두 메서드 모두 객체의 임시 메서드로 함수를 간접적으로 호출합니다.
즉, 함수.call(객체, 파라미터1, 파라미터2)객체.함수(파라미터1, 파라미터2) 처럼 객체의 메서드로 함수를 호출한 것과 같습니다.
따라서 함수에 정의되어 있는 this 값은 호출한 객체를 가리키게 됩니다.

quiz ⁉️
call과 apply 메서드로 호출하면서 객체 자리에 아무 것도 전달하지 않는다면 어떻게 될까요?

💡 만약 함수 바디에 this가 있는 함수를 call과 apply 메서드로 호출하면서 객체 자리에 아무것도 전달하지 않거나, null, undefined를 전달할 경우

  • 일반 모드에서는 전역객체를 가리키고,
  • 'use strict' 모드에서는 TypeError가 발생합니다.
    • 단, 함수 바디에 this가 없는 경우 'use strict' 모드에서도 에러가 발생하지 않고, 전역객체를 가리킵니다.
    • 단, 전달하는 아규먼트가 있는 경우 공백은 SyntaxError 가 발생합니다.
// 위 예시에서 이어짐 
/*일반 모드*/
introduceMyPet.call(); //우리 undefined의 나이는 undefined살 입니다.
introduceMyPet.call(undefined); //우리 undefined의 나이는 undefined살 입니다.
introduceMyPet.apply(null); // 우리 undefined의 나이는 undefined살 입니다.

'use strict'
introduceMyPet.call(); // TypeError: Cannot read properties of undefined (reading 'name')
introduceMyPet.call(undefined); // TypeError: Cannot read properties of undefined (reading 'name')
introduceMyPet.apply(null); // TypeError: Cannot read properties of undefined (reading 'name')

두 번째 파라미터로 call 메서드는 아규먼트를 받고, apply 메서드는 배열로 아규먼트를 받습니다.

.apply() 메서드를 활용하면 자바스크립트의 기본 내장 메서드와 함수를 효과적으로 사용할 수도 있습니다.

/* 두 배열의 요소를 합치는 방법*/
const alphabet = ["a", "b"];
const number = [0, 1, 2];

/* concat 메서드는 배열의 요소가 합쳐진 새로운 배열을 반환합니다. 기존 배열에는 아무 영향이 없습니다.*/
const newAlphabet = alphabet.concat(number);
console.log(newAlphabet); // [ 'a', 'b', 0, 1, 2 ]
console.log(alphabet); // [ 'a', 'b' ]

/* push 메서드를 apply로 호출하면, 기존 배열에 다른 배열의 요소를 추가할 수 있습니다. */
alphabet.push.apply(alphabet, number);
console.log(alphabet); // [ 'a', 'b', 0, 1, 2 ]

/* (apply 없이) push 메서드만 사용하면 배열 자체가 요소로 추가됩니다. */
alphabet.push(number);
console.log(alphabet); // [ 'a', 'b', 0, 1, 2, [ 0, 1, 2 ] ]

.apply() 로 내장 함수에 배열의 요소를 바로 전달하여, 루프 없이 탐색하도록 할 수도 있습니다.

const numbers = [5, 6, 2, 3, 7];

/* 배열의 요소 중 최대값, 최솟값을 찾기 위해 forEach 또는 for 반복문을 사용합니다.. */
let max = -Infinity, min = +Infinity;

// forEach 사용
numbers.forEach((number) => {
  number > max && (max = number);
  number < min && (min = number);
});

// for of 사용 
for (let number of numbers) {
  max = number > max ? number : max;
  min = number < min ? number : min;
  }

console.log(max); // 7
console.log(min); // 2

/*  apply 메서드로 Math.max 와 Math.min 를 호출하여 간단하게 최댓값, 최소값을 구할 수도 있습니다. 
이는 Math.max(numbers[0], ...) 또는 Math.max(5, 6, ...)와 거의 같습니다. */
const maxNum = Math.max.apply(null, numbers); 
const minNum = Math.min.apply(null, numbers);
// null 대신 전역 객체 Math, 전역 객체를 가리키는 undefined, this 를 전달해도 됩니다. 

/* 단, 아규먼트가 있는 경우 공백은 SyntaxError 가 발생합니다. */
const maxNum2 = Math.max.apply( , numbers); // SyntaxError: Unexpected token ','

console.log(maxNum); // 7
console.log(minNum); // 2


// 그러나 더 간단히 분해 연산자를 사용할 수도 있습니다.😅
const simpleMax = Math.max(...numbers);
const simpleMin = Math.max(...numbers);

하지만 이러한 방식으로 apply 를 사용하는 경우 주의해야 합니다.
JavaScript 엔진의 인수 길이 제한을 초과하는 위험성에 대해 이해할 필요가 있습니다. 함수에 너무 많은(대략 몇 만개 이상) 인수를 줄 때의 상황은 엔진마다 다른데(예를 들어 JavaScriptCore의 경우 인수의 개수 제한은 65536), 상한이 특별히 정해져 있지 않기 때문입니다. 어떤 엔진은 예외를 던집니다. 더 심한 경우는 실제 함수에 인수를 전달했음에도 불구하고 참조할 수 있는 인수의 수를 제한하고 있는 경우도 있습니다(이러한 엔진에 대해 더 자세히 설명하면, 그 엔진이 arguments의 상한을 4개로 했다고 하면(실제 상한은 물론 더 위일 것입니다), 위 예제 코드의 전체 배열 [5, 6, 2, 3, 7] 이 아니라 5, 6, 2, 3 만 apply 에 전달되어 온 것처럼 작동합니다).
이에 대한 더 자세한 설명은 MDN을 참고해주세요

bind 메서드

function add(num){ return this.x + num; }
const firstObj = { x: 1 };

/* add 함수와 firstObj 객체 결합하여, add 함수의 this는 firstObj를 가리키게 됩니다. */
const addFirstObj = add.bind(firstObj); 
addFirstObj(2); // 3

/* secondObj의 메서드가 되어도 add 함수의 this가 가리키는 건 결합된 firsObj 객체입니다. */
const secondObj = { x: 10, addFirstObj };
secondObj.addFirstObj(2); // 3 

/* 두 번째 아규먼트는 파라미터 값으로 고정됩니다. */
const fixedAdd = add.bind(firstObj, 3); // this.x 는 firstObj.x 로, num은 3으로 결합되었습니다.
fixedAdd(); // 4
fixedAdd(10); // 4 
// 결합된 파라미터(num) 다음 파라미터가 있다면 10이 아규먼트로 전달되지만, 없다면 무시됩니다.

함수.bind(객체, 파라미터1,파라미터2)
bind 메서드는 원본 함수 바디의 this를 첫 번째 파라미터인 객체에 결합시킨 새로운 함수를 반환합니다.

/* 위 예시에서 add.bind()도 새로운 함수를 리턴했습니다.
bind된 함수는 "bound 원본함수 이름" 을 name 프로퍼티 값으로 가집니다.*/
addFirstObj.name; // bound add
fixedAdd.name; // bound add

두 번째 이후 파라미터는 전달받는 아규먼트를 원본 파라미터의 값으로 결합합니다.
이러한 부분 적용은 화살표 함수에서도 동작하며, 함수형 프로그래밍에서 널리 쓰이는 커링(currying) 기법으로 불립니다.

const sum = (x, y) => x + y;
const addTwo = sum.bind(null, 2);
addTwo(5); // 7 
// x는 2이고, y는 아규먼트 5를 전달받았습니다.

setTimeout 과 bind

/* this 없이 setTimeout의 콜백함수로 전달했을 때 호출되는 전역 함수 */
function showMessage() {
  console.log(`${this.id} global scope!`);
}

/* class 객체 생성 */
class User {
  constructor(id) {
    this.id = id;
  }

  showMessage() {
    console.log(`${this.id} 로그인 되었습니다!`);
  }
  
/* 1초 뒤 showMessage 함수를 호출하는 메서드 
브라우저 환경에서는 window.setTimeout 으로 할 수도 있습니다. */
  login() {
    setTimeout(this.showMessage.bind(this), 1000); // (1초 뒤) iberis 로그인 되었습니다!
    // setTimeout(this.showMessage, 1000); // (1초 뒤) undefined 로그인 되었습니다!
    // setTimeout(showMessage, 1000); // (1초 뒤) undefined global scope!
    // setTimeout(showMessage.bind(this), 1000); // (1초 뒤) iberis global scope!
  }
}

const user1 = new User("iberis");
user1.login(); // showMessage 를 어떻게 콜백함수로 전달하느냐에 따라 달라집니다.

window.setTimeout() 내에서 this 키워드는 기본적으로 window (또는 global) 객체로 설정됩니다.
그리고 메서드 내부에 중첩된 함수의 this도 기본적으로 전역 객체를 가리키킵니다.

따라서 전역 스코프에 있는 함수가 아닌 객체의 메서드인 showMessage를 호출하기 위해서는 this.showMessage로 지정해주어야 합니다.
또한 setTimeout의 콜백 함수 바디에 this가 있는 경우, this가 widow 객체가 아닌 인스턴스 객체를 가리키도록 하기 위해, bind 메서드로 호출한 인스턴스를 가리키는 this를 결합해줄 수 있습니다.

toString 메서드

MDN 에서 함수의 toString 메서드를 검색하면 toSource 메서드가 나오는데, 사용을 권장하지 않고 있습니다.

/* 내장 함수는 '[native code] ' 같은 문자열을 함수 바디로 반환합니다. */
Function.toString(); // function Function() { [native code] }

/* 사용자 정의 함수는 소스코드 전체를 반환합니다. */
function hello() {
    console.log("Hello, World!");
}

hello.toSource(); // 'function hello() {\n console.log("Hello, World!");\n}'

이 메소드는 보통 JavaScript 에 의해 내부적으로 호출되며 코드에서 명시적으로 사용되지 않습니다. 디버깅할 때 객체의 컨텐츠를 검사하기 위해 toSource 를 호출해보실 수 있습니다.

📁 함수 생성하기

함수를 정의하기 위한 여러가지 방법이 있습니다.

/* 함수 선언문 */
function add(a, b){
  let result = a + b;
  return result;
}

/* 함수 표현식 */
const add1 = function (a, b){
  let result = a + b;
  return result;
};

/* Function 생성자 */
const add2 = new Function("a", "b", "let result = a + b; return result;");
// Function 생성자로 정의한 함수는 호출할 때마다 매번 함수 본문 문자열을 새로 파싱(구조 분석)해야 한다는 문제점이 있습니다. 
// 따라서 가능하다면 Function 생성자를 피해야 합니다.

/* 화살표 함수 */
const add3 = (a, b)=> a + b; 

그렇다면 각각의 생성 방법들은 어떤 차이점을 가지고 있을까요? 🤔

📑 함수 선언문과 함수 표현식

/* 함수 선언문 : 함수가 선언되기 이전에도 호출이 가능합니다.*/
greeting(); // hi 

function greeting(){
  console.log('hi');
}

/* 함수 표현식 : 선언하기 전에 호출할 수 없습니다. */
const printHi = function greeting2 () { // piintHi는 변수명이고, greeting2 는 함수이름입니다. 
  console.log("hi");
};

printHi(); // hi
greeting2(); // ReferenceError: greeting2 is not defined
// 함수 표현식으로 정의된 함수는 함수 이름으로 호출할 수 없습니다.
// 단, 함수 이름은 재귀 함수를 만들 때 활용할 수 있습니다.

함수 선언문의 가장 큰 특징은 호이스팅이 발생한다는 것입니다.

반면 함수 표현식을 변수에 할당하여 호출하는 경우,
호이스팅은 발생하지만(함수를 정의하지 않고 호출했을 때와 에러 메세지가 다름)
함수를 정의하기 이전에는 호출할 수 없습니다.(var로 선언해도!)

/* 함수 표현식 - 함수를 하나의 값으로써 파라미터, 변수에 할당, 리턴값 등으로 활용할 수 있습니다. */

// sort 메서드의 "파라미터"로 함수를 활용했습니다.(콜백함수)
[3, 2, 1].sort(function (a, b) { return a - b; }); // [1, 2, 3]  

// "변수"에 함수를 할당했습니다.
const add = function (a, b) { return a - b; }; 

// 함수 표현식을 정의하는 즉시 아규먼트로 10을 전달해 호출했습니다. (즉시 실행 함수)
const tensquared = (function(x){ return x * x }(10)); // tensquared 에는 함수의 결과값 100이 할당됩니다.

// 함수를 "리턴값"으로 활용했습니다. (고차 함수)
function sumNumber(x) {
  return function (y) {
    return x + y;
  };
}

표현식(expressions)이란 결과적으로 하나의 값이 되는 코드를 말합니다.
즉, 함수 표현식은 함수를 하나의 값으로 활용하도록 만든 것을 말합니다. 앞서 살펴본 것처럼 함수는 1급 객체로써 하나의 값으로 활용할 수 있습니다.

/* 변수명으로 재귀함수를 만드는 경우 */
let result = function factorial(x) {
  return x <= 1 ? 1 : x * result(x - 1);
};

// 재귀 함수를 복사하고, 변수 result를 재할당하면 TypeError가 발생합니다.
let copyResult = result; 
result = null; 
copyResult(5); // TypeError: countdown is not a function 
//자기 자신을 호출하는 부분에서 result가 더이상 함수가 아닌 null이기 때문에 error가 발생합니다.
/* 함수 이름으로 재귀함수를 만드는 경우 */
let result = function factorial(x) {
  return x <= 1 ? 1 : x * factorial(x - 1);
};

// 재귀 함수를 복사하고, 변수 result를 재할당해도 복사한 함수가 정상적으로 작동합니다.
let copyResult = result; 
result = null; 
copyResult(5); // 120

// 기명 함수 표현식의 함수이름으로 함수를 호출 할 수는 없습니다.(함수 바디 안에서만 사용 가능)
factorial(5); // ReferenceError: factorial is not defined

// 기명 함수 표현식의 name 프로퍼티 값은 함수 이름입니다.
copyResult.name; // factorial

기명 함수 표현식에서 함수 이름은 함수 바디에서만 사용할 수 있으며, 자기 자신을 다시 호출하는 재귀함수를 만들 때 활용될 수 있는데,

이처럼 함수 표현식에 이름을 붙여주는 것의 장점으로

  • 오류가 발생했을 때 스택 추적에 함수의 이름이 나타나므로 원인을 찾기 쉽고,
  • 재귀 함수를 복사할 때, 변수명으로 재귀함수를 만들어 사용할 경우 변수가 바뀌면 TypeError가 발생하는데, 변수 이름 대신 함수 이름을 사용하면 이러한 error을 방지할 수 있습니다.

📑 화살표 함수

/* 화살표 함수의 간결한 문법 */
const sum = (a, b) => {return a + b};

// return 문이 하나라면 "return" 과 "{ }" 중괄호를 생략할 수 있습니다.
const sum2 = (a, b) => a + b;

//단, 객체를 리턴할 때에는 객체의 중괄호와 혼동하지 않게 명시해야합니다.
const add = (a, b) =>{return {sum: a + b}; };

// 파라미터가 1개라면 "( )" 소괄호를 생략할 수 있습니다.
const square = a => a * a;

// 단, 파라미터가 0개일 때에는 반드시 "()" 빈 괄호를 써야 합니다.
const print = () => console.log("hi");

ES6 이후 화살표 함수라는 간결한 문법으로 함수를 정의할 수 있습니다.

화살표 함수의 가장 큰 특징은

  • 화살표 함수는 this를 가지고 있지 않고
  • prototype 프로퍼티가 없어 새로운 클래스의 생성자 함수로 사용할 수 없다는 것입니다.

💡 매서드와 화살표 함수
일반 함수에서 this 는 함수를 호출한 객체를 가리킵니다.
하지만 화살표함수에는 this가 없어 함수 밖에서 this가 가리키는 값을 찾게 됩니다.
따라서 객체의 매소드를 만들 때에는 화살표 함수가 아닌, 일반함수를 사용할 것이 권장됩니다.

📁 함수 호출하기

함수 선언과 마찬가지로 함수를 호출할 수 있는 다양한 방법이 있습니다.
return 값이 없는 경우 undefined를 반환하게 됩니다.

function greeting(){
  return hi
}

/* 함수로 호출 */
greeting(); // hi 

/* 조건부 호출 : 호출하는 함수가 null이나 undefined가 아닌 경우에만 호출합니다. */
const printHi = null;
printHi?.(); // undefined

greeting?.(); // hi

/* 메서드로 호출 */
const myObj = { greeting };
myObj.greeting(); // hi 
myObj['greeting'](); // hi 

/* 생성자 함수 호출 */
const obj = new Object;
obj; // {}

/* call(), apply() 메서드를 통해 간접적으로 호출 */
greeting.call(obj); // hi
greegint.apply(obj); // hi

이 중 함수의 호출과 관련된 특이한 2가지 this생성자 함수에 대해서 좀 더 깊이 살펴볼까요?! 🤓

📑 함수 호출과 this

/* name 프로퍼티와 thisMethod 메서드를 가지고 있는 객체 obj */
const obj = {
  name: "OBJ",
  thisMethod() {
    let self = this; // ②  this값을 변수에 할당
    console.log(this === obj); // true ③ 메서드의 this는 호출된 객체한 가리킵니다.
    return innerFunc(); // ④ 함수 안의 함수 호출

    function innerFunc() {
      console.log(this === obj); // false ⑤ 내부 함수의 this는 외부 함수를 호출한 객체를 가리키지 않습니다. 전역 객체이거나 strict 모드라면 undefined 입니다.
      console.log(self === obj); // true  ⑥ 내부 함수에서 호출한 외부 함수를 호출한 객체를 활용해야할 때에는 this 대신 this 값을 할당받은 변수를 참조할 수 있습니다.

      return self.name; // OBJ ⑦ obj 객체의 name 프로퍼티값 리턴
    }
  },
};

// ① 객체의 메서드 호출
obj.thisMethod(); // true false true OBJ

this는 호출된 시점에 평가됩니다.
객체의 메서드를 호출하면 메서드 내부의 this는 호출한 객체를 가리킵니다.
하지만 메서드 내부에 중첩된 함수가 있다면, 그 중첩된 함수 내부의 this는 호출한 객체를 가리키지 않고, 일반 모드라면 전역 객체를, use strict 모드라면 undefined를 가리킵니다.

const obj = {
  name: "OBJ",
  thisMethod() {
    let arrowFunc = () => {
      console.log(this === obj); // true ③ 화살표 함수 밖 thisMethod 의 this를 가져와 obj 객체를 가리킵니다.
      return this.name; // OBJ ④ obj의 name 프로퍼티 값 리턴
    };

    return arrowFunc(); // ② 함수 안의 화살표 함수 호출
  },
};

// ① 객체의 메서드 호출
obj.thisMethod(); // true OBJ

메서드에 중첩된 내부 함수를 화살표 함수로 만들면,
화살표 함수 안에는 this가 없기 때문에,
(함수의 내부에서는 밖의 변수를 볼 수 있으므로) 화살표 함수 바로 바깥의 메서드의 this 값을 가져오게 됩니다.
즉 this는 메서드를 호출한 객체를 가리키게 됩니다.

const obj = {
  name: "OBJ",
  thisMethod() {
    console.log(this === obj); // true ② 메서드의 this는 호출한 객체를 가리킵니다.

    function innerFunc() { 
      console.log(this === obj); // true ⑤ bind로 obj 객체와 결합되어 내부 this는 obj를 가리킵니다.
      return this.name; // OBJ ⑥ obj의 name 프로퍼티 값을 리턴합니다.
    }

    let bindInnerFunc = innerFunc.bind(obj); // ③ bind 메서드로 innerFunc 함수 내부의 this가 obj 객체를 가리키는 새로운 함수를 만듭니다. 
    return bindInnerFunc(); // ④ bindInnerFunc 함수 호출
  },
};

// ① 객체의 메서드 호출
obj.thisMethod(); // true true OBJ

함수의 내장 메서드인 bind()로 호출한 객체와 결합한 새로운 함수를 정의해서 활용할 수도 있습니다.

📑 생성자 함수 호출

const x = new Object();
console.log(x); // {}

// 파라미터가 없는 경우 빈 소괄호 ()를 생략할 수도 있습니다.
const y = new Object;
console.log(y); // {}

함수나 메서드를 호출할 때 앞에 new 키워드를 붙이면 생성자로 호출됩니다.
생성자 함수는 생성자의 prototype 프로퍼티에서 지정된 객체를 상속하는 빈 객체를 새로 생성합니다.

function Cat(name) {
  this.name = name;
}

Cat.prototype.play = function () {
  return `${this.name}가 즐거워합니다.`;
};

const myCat = new Cat("mimi");
myCat; // Cat { name: 'mimi' }
myCat.play(); // mimi가 즐거워합니다.

⁉️quiz
위 예제에서 이어지는 아래 코드는 어떤 값을 반환할까요?

new myCat.play();

new 키워드를 붙여서 함수를 호출하게 되면, 생성자 함수로써 호출이 되기 때문에, prototype에 지정된 새로운 객체를 생성합니다.

생성자 함수는 일반적으로 return을 사용하지 않고, 생성된 새 객체가 생성자 호출 표현식의 값입니다.
명시적으로 return 문을 사용해서 객체를 반환하는 경우에는 그 객체가 호출 표현식의 값이 됩니다. 하지만 return 키워드 뒤 반환 값이 없거나, 객체가 아닌 기본 값을 반환한다면 해당 반환 값은 무시하고 새 객체를 호출 표현식의 값으로 사용합니다.

// return 값이 문자열이므로, 무시하고 새 객체를 리턴하게 됩니다.
let x = new myCat.play();
console.log(x); // {}

📂 함수의 파라미터

자바스크립트 함수는 파라미터와 아규먼트로 어떤 타입을 받을지 정의하지 않고 파라미터와 아규먼트의 개수의 일치 여부 등을 체크하지 않습니다. 타입의 경우에는 자바스크립트에서 내부적으로 필요에 따라 타입 변환이 이루어지기도 하는데요,

그렇다면 정의한 파라미터와 전달받은 아규먼트의 개수가 서로 다른 경우에는 어떻게 될 지 살펴보겠습니다. 🧐

📑 파라미터의 기본값

/* 파라미터의 개수 < 아규먼트의 개수인 경우 */
function printOne (a){
  return `${a}`
}

printOne('apple', 'banana'); // 'apple'


/* 파라미터의 개수 > 아규먼트의 개수인 경우 */
function printTwo (a, b, c=`${a}!`){
  return `${a} ${b} ${c}`
}

printTwo('apple'); // 'apple undefined apple!'

파라미터의 개수보다 더 많이 전달된 아규먼트는 무시됩니다.
아규먼트가 전달되지 않은 파라미터는 undefined를 반환합니다.
=기본값으로 아규먼트가 전달되지 않을 경우 사용할 기본값을 파라미터에 지정해줄 수도 있는데, 앞서 전달된 아규먼트를 이용해 기본값을 만들 수도 있습니다.

📑 나머지 연산자, 분해 연산자, 구조분해할당

/* ...rest 파라미터 앞에 점 세개를 붙이면, 
나머지 아규먼트들이 요소로 담긴 하나의 배열로 전달됩니다. */
function printArgument(a, ...para) {
  console.log(a, para);
}

printArgument("apple", "banana", "cake", "dessert");
// apple [ 'banana', 'cake', 'dessert' ]


/* 함수를 호출할 때 아규먼트에 분해 연산자를 사용할 수 있습니다. */
let numbers = [4, 7, 0, -1];
Math.min(...numbers); // -1


/* 파라미터와 아규먼트에 구조 분해 할당도 사용할 수 있습니다. */
function chekElement([x, y]){
  console.log(x, y) 
}

checkElement([1, 2]); // 1 2

arguments 객체와 ...rest 파라미터
분해 연산자 sprad

📑 arguments 객체

{ } 함수 바디 안에서 arguments는 해당 함수를 호출할 때 전달하는 아규먼트들을 요소로 갖는 유사배열입니다.

function printArgument(a, b, c) {
  console.table(arguments);
  console.log(arguments.length); // argument들의 개수 확인
  console.log(arguments[0]); //index 번호로 argument에 접근
}

printArgument("a", "b", "c", "d", "e");

/*
┌─────────┬────────┐
│ (index) │ Values │
├─────────┼────────┤
│    0    │  'a'   │
│    1    │  'b'   │
│    2    │  'c'   │
│    3    │  'd'   │
│    4    │  'e'   │
└─────────┴────────┘
5
a */

arguments 객체는 유사 배열 이므로 배열의 내장 매서드는 사용할 수 없습니다.
arguments를 파라미터의 이름으로 사용하거나, 함수 내부에서 arguments라는 이름의 변수나 함수를 만드는 것은 피해야합니다.
화살표 함수에서는 사용할 수 없습니다.

본 책에서는 비효율적이며 초기화가 어려워 사용을 지양해야한다고 합니다.

여기까지 특별한 객체인 함수의 프로퍼티메서드를 살펴보고,
함수를 생성하고, 호출하며, 함수 호출에 사용되는 파라미터에 대해서 알아보았습니다.
이제 함수의 바디 와 관련된 특별한 경우들에 대해 살펴 볼건데요!
특히 함수 바디 안에 또 다른 함수가 들어있는 경우는 어떨까요? 🧐

📁 함수의 바디

📑 로컬 네임스페이스

먼저, 함수 안에서는 밖에서 선언한 변수를 가지고 활용할 수 있지만,
함수 바깥에서는 함수 안의 변수를 볼 수 없다는 특징이 있습니다.

const favoritDrink = {name: 'green tea latte', cost: 5000}

/* 이미 선언된 변수 favoritDrink 를 함수 안에서 다른 값으로 활용했습니다. */
function chunkNamespace(){
  const favoritDrink = 'Americano';
  const cost = 3000;
  return {name: favoritDrink, cost}
}

// 단, 코드를 활용하기 위해 함수를 호출하는 것을 잊지 말아야 합니다.
chunkNamespace(); // {name: 'Americano', cost: 3000}

이미 선언된 변수를 다른 값으로 사용해야하거나,
사용하려는 변수가 이미 선언된 변수여서 의도치 않게 덮어쓸까봐 걱정된다면,
새로운 함수를 만들고 함수 안에 코드를 작성하는 방법으로 해결할 수도 있습니다.

함수 안에 코드를 작성하면,
함수 밖에서 같은 이름의 변수가 이미 선언되었는지 여부와 상관하지 않아도 되어, 전역 네임스페이스를 어지럽힐 걱정 없이 코드를 작성할 수 있습니다.

단, 함수를 생성하기만 하면 함수 바디의 코드가 작동되지 않으므로,
함수 내부에 작성한 코드를 활용하기 위해서는 반드시 함수 호출을 잊지 말아야 합니다.

📑 클로저

자바스크립트는 어휘적 스코프(lexical scope) 를 사용하는데,
이는 함수가 호출 시점의 스코프가 아닌 자신이 정의된 시점의 변수 스코프를 사용하여 실행된다는 뜻입니다.

클로저 란 함수와 함수가 선언된 어휘적 환경의 조합을 말합니다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성됩니다.

const scope = 'global scope';

function outer(){
  const scope = 'local scope';
/*inner 함수가 정의된 시점의 스코프는 outer함수의 내부 스코프입니다.*/
  function inner(){ return scope; }
  return inner;
}

/* inner 함수가 호출된 시점의 스코프는 전역 스코프 입니다.*/
outer()(); // local scope

위 예제에서 inner 함수는 outer 함수 내부에서 선언되고, outer함수 바깥에서 호출되었습니다.
만약 호출 받았을 때 변수를 찾았다면, outer 함수 바깥 스코프에서만 변수를 찾았을 것입니다. (함수 밖에서는 함수 안의 변수를 볼 수 없으니까요!)

하지만 자바스크립트의 함수는 어휘적 스코프를 사용하기 때문에,
inner 함수가 outer 함수 내부에서 정의되었을 때의 스코프에서 변수를 찾을 수 있게 됩니다.
즉, 함수 내부에서 변수를 찾기 때문에, 내부 먼저 → 없으면 함수 외부에 까지도 변수를 탐색할 수 있습니다.

클로저를 이용해서 프라이빗 메소드 (private method) 흉내내기

함수를 리턴하는 함수를 고차함수라고 합니다.
(함수를 파라미터로 받는 함수도 고차함수라고 부르기도 합니다.)
클로저는 주로 고차함수에서 활용됩니다.

어휘적 스코프(lexical scope)와 클로저를 활용하여 외부(전역 스코프)에서는 접근할 수 없는 프라이빗한 변수나 메서드를 만들 수 있습니다.

 /* 함수를 정의하며 즉시 호출했습니다. counter 상수에는 리턴값인 내부 함수가 할당됩니다.*/
const counter = (function () {
/* 외부 함수가 종료된 후에는 내부 함수 외에는 상수 privateCounter에 접근할 수 없습니다.*/
  let privateCounter = 0;
 
  return function () {
    return ++privateCounter;
  };
})();

counter(); // 1
counter(); // 2
function counter(value) {
  return {
// 메서드 이름을 getter와 setter 함수의 이름으로 지정합니다.
    get count() {      // getter 는 파라미터를 갖지 않습니다
      return ++value;  // 메서드를 호출하면 getter의  return 값이 반환됩니다.
    },

    set count(setValue) {   // 메서드에 값을 할당하면 setter 의 파라미터로 전달되어 함수가 실행됩니다.
      if (setValue > value) value = setValue;  // setter는 리턴값이 없습니다.
      else throw Error("카운트는 더 큰 값만 가능합니다.");
    },
  };
}

const c = counter(10);
c.count; // 11
c.count = 12;
c.count = 11; // Error: 카운트는 더 큰 값만 가능합니다.

프라이빗한 비공개 메서드를 getter와 setter를 활용해서도 만들 수 있습니다.
함수 내부의 로컬 변수 대신 파라미터 value 를 활용하면 비공개 변수의 초깃값을 정할 수 있습니다.

// ④ 내부의 화살표 함수는 리턴값 value를 찾기 위해 파라미터 value 에 전달 된 아규먼트 i = 5에 접근합니다. 
function constfunc(value){return () => value};

// ① 배열의 요소로 constfunc 함수의 리턴값 화살표 함수를 추가합니다.
let funcs = [];
for(let i = 0; i < 10; i++) funcs[i] = constfunc(i); 
/* 💡 ② constfunc 함수가 호출되어 종료되어도, 
내부의 화살표 함수가 실행될 때 외부 함수 constfunc의 파라미터에 전달된 아규먼트 i의 값에 다시 접근할 수 있습니다. */

//③ 배열의 요소인 화살표 함수를 호출합니다.
funcs[5](); // 5 
// 루프를 함수 안에 넣을 수도 있습니다.
function constfuncs(){
  let funcs = [];
  for(let i = 0; i < 10; i++){ funcs[i] = () => i; }
/* for(var i = 0; i < 10; i++){ funcs[i] = () => i; } */
/* 단, 루프의 변수를 var 로 선언하면, 
 마지막 var i = 10 은 루프의 스코프 밖, 즉 함수 스코프에 남아있어, 
함수 안의 funcs 배열의 요소인 모든 화살표 함수의 리턴값 i가 10이 되므로 주의해야 합니다. */ 

  return funcs;
}

let funcs = constfuncs();
funcs[5]() // 5;

함수 내부의 함수는 외부 함수가 종료된 후에도 자신을 감싸고 있었던 외부 함수의 어휘적 스코프에 있는 변수 값들을 사용할 수 있습니다.

💡 Function 생성자로 만든 함수는 클로저를 형성하지 않습니다.

함수 표현식과 함수 선언, 화살표 함수로 정의하는 함수는 어휘적 스코프(lexical scope) 를 사용합니다. 즉, 클로저를 형성합니다. 반면 Function 생성자로 정의한 함수는 (다른 모든 함수가 상속하는) 전역 스코프를 제외하면 어떠한 스코프도 상속하지 않습니다.

📁 함수형 프로그래밍

마지막으로 살펴볼 것은, 자바스크립트의 함수로 (완벽한 함수형 프로그래밍은 아니지만) 함수형 프로그래밍 기법을 사용할 수 있다는 것입니다.

함수형 프로그래밍(functional programming) - 위키피디아
함수형 프로그래밍(functional programming)은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
(⋯중략⋯)
명령형 프로그래밍에서는 상태를 바꾸는 것을 강조하는 것과는 달리, 함수형 프로그래밍은 함수의 응용을 강조한다. 프로그래밍이 문이 아닌 식이나 선언으로 수행되는 선언형 프로그래밍 패러다임을 따르고 있다.
(⋯중략⋯)
함수형 코드에서는 함수의 출력값은 그 함수에 입력된 인수에만 의존하므로 인수 x에 같은 값을 넣고 함수 f를 호출하면 항상 f(x)라는 결과가 나온다. 부작용을 제거하면 프로그램의 동작을 이해하고 예측하기가 훨씬 쉽게 된다. 이것이 함수형 프로그래밍으로 개발하려는 핵심 동기중 하나이다.

함수형 프로그래밍의 특징

  • 함수와 데이터를 중점으로 생각한다.
  • 입출력이 순수해야한다. (순수함수)
    • 반드시 하나 이상의 인자를 받고, 받은 인자를 처리하여 반드시 결과물을 돌려주어야한다.
    • 받은 인자만으로 결과물을 내야 한다. 즉, 인자를 제외한 다른 변수는 사용하면 안된다.
  • 부작용(side-effect)이 없어야한다.
    • 프로그래머가 바꾸고자하는 변수 외에는 바뀌어서는 안 된다.
    • 원본 데이터는 변하지 않아야 한다.
    • 프로그래머가 모든 것을 예측하고 통제할 수 있어야한다.

함수로 배열 처리

고차함수(higher-order function)

함수의 부분 적용

메모이제이션

동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 것을 말합니다.
(값을 기록해 놓는다는 점에서 캐싱(Caching)이라고도 합니다.)

위 그림은 6번째 피보나치 수를 구하는 재귀 함수를 표현한 것입니다.
메모이제이션을 활용하여 한 번 계산된 값을 기록해 놓는 경우, 점선의 함수는 반복해서 계산하지 않고 저장한 캐시 값만 불러오면 되므로 실행 시간을 단축시킬 수 있습니다.

참고
https://www.zerocho.com/category/JavaScript/post/576cafb45eb04d4c1aa35078
https://ko.wikipedia.org/wiki/함수형_프로그래밍
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Functions
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
https://ko.wikipedia.org/wiki/메모이제이션
https://velog.io/@kimdukbae/다이나믹-프로그래밍-Dynamic-Programming

profile
React, Next.js, TypeScript 로 개발 중인 프론트엔드 개발자

0개의 댓글