10 분 소요

1. 함수 개요

1. 함수의 개념

함수란 어떤 목적을 가진 작업들을 수행하는 코드들이 모인 블럭이다. (재사용성 증가, 코드 가독성 향상)

즉, input과 output을 가진 기능의 단위⭐를 함수라고 한다!!


아래 코드는 식별자와 함수 이름이 동일해서 함수 이름으로 호출된 것이라고 생각하기 쉽다!

var add = function add(x, y) {
  return x + y;
};
console.log(add(2, 3)); // 5


하지만, 함수는 함수 이름으로 호출하는 것이 아닌 함수 객체를 가르키는 식별자로 호출한다.



2. 함수의 정의

2.1 함수 선언문

function 키워드를 사용하여 함수를 선언하는 방식을 함수 선언문이라고 한다. ( add가 곧 변수명 )

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


2.2 함수 표현식(리터럴)

변수에 함수를 할당하는 방식을 함수 표현식(함수 리터럴)이라고 한다.

함수 표현식의 함수 리터럴은 함수 이름을 생략하는 것이 일반적이다.

const result = function (x, y) {
  return x + y;
};


2.3 함수 선언문 vs 함수 표현식

함수 선언문과 함수 표현식은 호이스팅에 차이가 있다.

자세한 내용은 《 호이스팅 》을 참고하자.


2.4 생성자 함수

생성자 함수란 객체를 생성하는 함수를 말한다. 생성자 함수에 의해 생성된 객체를 인스턴스라고 한다.

① 생성자 함수를 사용하면 비슷한 객체를 여러개 만들 수 있고 상속을 구현할 수 있다.

② 함수 이름의 첫 글자는 대문자로 시작하고, 반드시 ‘new’ 연산자를 붙여 실행해야한다.


비슷한 객체를 여러개 만들고 싶을 때는 생성자 함수를 사용하면 된다.

let user = {
  name: "sieun",
  age: 22,
};

function User(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  };
}

//new 연산자를 이용해 호출
let user1 = new User("sieun", 22);
let user2 = new User("kori", 7);

user2.sayName(); // "kori"


생성자 함수는 아래와 같이 동작한다.

function User(name) {
  // this = {};  (1. 빈 객체가 암시적으로 만들어짐)

  // 2. 새로운 프로퍼티를 this에 추가함
  this.name = name;
  this.isAdmin = false;

  // return this;  (3. this가 암시적으로 반환됨)
}


생성자함수에 메서드를 추가

  • 생성자 함수에 메서드를 추가하려면 prototype이라는 속성 안에 추가해야한다.
  • prototype을 사용하면 생성자 함수와 프로토타입 메서드를 별도로 정의해야해 구문이 복잡해진다.
  • 따라서 class를 사용하여 생성자와 메서드를 하나의 구문으로 묶어 간결하게 코드를 작성할 수 있다.
function User(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  };
}

// 프로토타입 메서드
User.prototype.addAge = function (user) {
  this.age += 1;
  console.log(`${user.name}님 주름이 하나 늘었네요(;´༎ຶД༎ຶ)`);
};
const user1 = new User("sieun", 22);
const user2 = new User("kori", 7);

console.log(user1.age); // 22
user1.addAge(user1); // sieun님 주름이 하나 늘었네요(;´༎ຶД༎ຶ)
console.log(user1.age); // 23


화살표 함수를 통해 내부의 객체를 반환하려고 할 때엔 소괄호로 감싸줘야한다.

소괄호가 없을 때 (block)으로 해석 -> 문제발생

const getObject = () => {
  name: "sieun";
  age: 22;
};


소괄호가 있을 때 -> 정상출력

const getObject = () => ({
  name: "sieun",
  age: 22,
});
const obj = getObject();
console.log(obj.name);



3. 콜백 함수

3.1 개념

콜백 함수란, 다른 코드의 인자로 넘겨주는 함수를 콜백 함수라고 한다. 인자로 넘겨준다는 얘기는 콜백함수를 넘겨받는 코드가 있다는 얘기이다.

  • callback = call(부르다) + back(되돌아오다) = 되돌아와서 호출!

  • 콜백 함수를 넘겨받은 forEach, setTimeout 등은 이 콜백 함수를 필요에 따라 적절한 시점에 실행하게 된다.
  • 제어권은 콜백 함수를 넘겨주는 주체한테 있다.
    • 즉, 제어권을 넘겨줄테니 너가 알고 있는 로직으로 처리해줘! 라는 명령을 내릴 수 있는 것이다.


정리

  • 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.


3.2 제어권

콜백함수를 넘겨받은 코드는 어떠한 제어권을 갖게될까?

  1. 콜백 함수 호출 시점에 대한 제어권을 갖는다.
  • 예시
    • 콜백 함수의 제어권을 넘겨받은 《 setInterval() 》 이 언제 콜백함수를 호출할지에 대한 제어권을 가지게 된다.
    • 매개변수로 받은 콜백함수의 로직을 반복해서 수행한다.
var count = 0;
var cbFunc = function () {
  console.log(count);
  if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);

// 실행 결과
// 0 (0.3sec)
// 1 (0.6sec)
// 2 (0.9sec)
// 3 (1.2sec)
// 4 (1.5sec)


  • 원래 cbFunc()를 수행한다면 그 호출주체제어권은 모두 사용자가 된다.
  • setInterval로 넘겨주게 되면 호출주체제어권은 모두 setInterval이 된다.
code 호출 주체 제어권
cbFunc(); 사용자 사용자
setInterval(cbFunc, 300); setInterval setInterval


  1. 인자에 대한 제어권을 갖는다.
  • 예시
    • map 》 함수는 각 배열 요소를 변환하여 새로운 배열을 반환한다.
    • 즉, 기존 배열을 변경하지 않고, 새로운 배열을 생성한다.
// map 함수에 의해 새로운 배열을 생성해서 newArr에 담고 있다.
var newArr = [10, 20, 30].map(function (currentValue, index) {
  console.log(currentValue, index);
  return currentValue + 5;
});
console.log(newArr);

// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [ 15, 25, 35 ]


  • 콜백함수에서 currentValue, index 이 변수의 순서를 바꿔보자
// map 함수에 의해 새로운 배열을 생성해서 newArr에 담고 있다.
var newArr2 = [10, 20, 30].map(function (index, currentValue) {
  console.log(index, currentValue);
  return currentValue + 5;
});
console.log(newArr2);

// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [ 5, 6, 7 ]

➡️ 컴퓨터는 사람이 아니기 때문에, index - currentValue의 의미를 사람처럼 이해할 수 없다. 따라서 의도하지 않은 값이 나오는 것이다.

➡️ 이처럼, map 메서드를 호출해서 원하는 배열을 얻고자 한다면 정의된 규칙대로 작성해야 한다.

➡️ 이 모든것은 전적으로 map 메서드 즉, 콜백 함수를 넘겨받은 코드에게 그 제어권이 있는 것이다.


  1. this에 대한 제어권을 갖는다.
  • 앞전 포스팅에서 콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조한다. 라고 하였다.
  • 하지만, ⭐ 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조한다.라는 예외 사항이 존재하였다.
// addEventListener는 부모를 상속한다.
document.body.innerHTML += '<button id="a">클릭</button';
document.body.querySelector("#a").addEventListener("click", function (e) {
  console.log(this, e);
});


  • 어떻게 상속이 가능할까?
    • 별도의 this를 지정하는 방식을 이해하기 위해map 함수를 직접 구현해 보자.
    • 핵심은 call, apply에 있다.
// 어떤 배열이든지 [].mapaaa로 호출 가능
Array.prototype.mapaaa = function (callback, thisArg) {
  var mappedArr = [];

  for (var i = 0; i < this.length; i++) {
    // call의 첫 번째 인자는 thisArg가 존재하는 경우는 그 객체, 없으면 전역객체
    // call의 두 번째 인자는 this가 배열일 것(호출의 주체가 배열)이므로,
    // i번째 요소를 넣어서 인자로 전달
    var mappedValue = callback.call(thisArg || global, this[i]);
    mappedArr[i] = mappedValue;
  }
  return mappedArr;
};

const a = [1, 2, 3].mapaaa((item) => {
  return item * 2;
});

console.log(a);

➡️ 즉, 바로 제어권을 넘겨받을 코드에서 call, apply 메서드의 첫 번째 인자에서 콜백 함수 내부에서 사용될 this를 명시적으로 binding 하기 때문에 this에 다른 값이 담길 수 있다는 것을 확인할 수 있다.


3.3 콜백 함수는 함수다

콜백 함수로 어떤 객체의 메서드를 전달하더라도, 그 메서드는 메서드가 아닌 함수로 호출한다.

var obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(this, v, i);
  },
};

//method로써 호출
obj.logValues(1, 2); // [1, 2, 3]

//callback => obj를 this로 하는 메서드를 그대로 전달한게 아니다.
//단지, obj.logValues가 가리키는 함수만 전달한 것이다.(obj 객체와는 연관이 없다!!! 함수 자체를 넣은 것)
[4, 5, 6].forEach(obj.logValues); // global


3.4 콜백 함수 내부의 this에 다른 값 바인딩하기

콜백 함수 내부에서 this가 문맥에 맞는 객체를 바라보게 할 수는 없을까? 한 번 알아보자!

전통적 방식

  • closure 》사용
    • closure? 현재 함수가 끝났음에도 영향력을 끼치는 것
var obj1 = {
  name: "obj1",
  func: function () {
    var self = this; //이 부분!
    return function () {
      console.log(self.name);
    };
  },
};

// 단순히 함수만 전달한 것이기 때문에, obj1 객체와는 상관이 없다.
// 메서드가 아닌 함수로서 호출한 것과 동일하다.
var callback = obj1.func();
setTimeout(callback, 1000);

➡️ 위 방식은 실제로는 this를 사용하는게 아니기도 하고, 번거롭다. ➡️ 그렇다면 콜백 함수 내부에서 아예 this를 사용하지 않는다면 어떨까?


  • this 사용x
var obj1 = {
  name: "obj1",
  func: function () {
    console.log(obj1.name);
  },
};
setTimeout(obj1.func, 1000);

➡️첫 번째 예시보다는 훨씬 간결하지만, this를 사용하지 않으면서 결과만을 위한 코딩이 되어버렸다. ➡️ 즉, this를 이용해서 다양한 것을 할 수 있는 장점을 놓치게 된 것이다.


  • this의 활용도를 높이기 위해 첫 번째 예시를 재활용해보자
var obj1 = {
  name: "obj1",
  func: function () {
    var self = this; //이 부분!
    return function () {
      console.log(self.name);
    };
  },
};

// ---------------------------------

// obj1의 func를 직접 아래에 대입해보자
var obj2 = {
  name: "obj2",
  func: obj1.func,
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);

// obj1의 func를 직접 아래에 대입해보자
var obj3 = { name: "obj3" };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

➡️ 위 방법은 조금 번거롭긴 해도 this를 우회적으로나마 활용하여 원하는 객체를 바라보게 할 수 있다.


즉시 실행 하지 않는 bind 메서드를 이용하는 방법 (가장 좋은 방법)

var obj1 = {
  name: "obj1",
  func: function () {
    console.log(this.name);
  },
};
//함수 자체를 obj1에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj1로 고정
setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = { name: "obj2" };
//함수 자체를 obj2에 바인딩
//obj1.func를 실행할 때 무조건 this는 obj2로 고정
setTimeout(obj1.func.bind(obj2), 1500);


3.5 콜백 지옥과 비동기 제어

콜백지옥이란

  • 콜백 함수는 콜백은 비동기 자바스크립트 코드를 작성할 수 있도록 해주지만, 콜백함수를 너무 남용하면 코드의 가독성이 떨어지는데 이를 콜백 지옥이라고 부른다.
  • 주로 이벤트 처리서버 통신과 같은 비동기적 작업을 수행할 때 발생한다.


  • 값 전달 순서 : 아래 → 위
setTimeout(
  function (name) {
    var coffeeList = name;
    console.log(coffeeList);

    setTimeout(
      function (name) {
        coffeeList += ", " + name;
        console.log(coffeeList);

        setTimeout(
          function (name) {
            coffeeList += ", " + name;
            console.log(coffeeList);

            setTimeout(
              function (name) {
                coffeeList += ", " + name;
                console.log(coffeeList);
              },
              500,
              "카페라떼"
            );
          },
          500,
          "카페모카"
        );
      },
      500,
      "아메리카노"
    );
  },
  500,
  "에스프레소"
);


동기 vs 비동기

  • 동기 (synchronous)
    • 현재 실행중인 코드가 끝나야 다음 코드를 실행하는 방식을 말한다.
    • CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적 코드이다.
  • 비동기(asynchronous)
    • 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식이다. (순서 보장x)
    • setTimeout, addEventListner 등이 존재한다.
    • 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 모두 비동기적 코드이다.
// setTimeout 함수의 동작원리
setTimeout(function () {
  // 1000ms이 지나야 아래 로직이 실행이 된다.
  console.log("hi");
}, 1000);


콜백지옥의 해결방안

  • 기명함수로 변환하는 방법
    • 기명함수란? 이름이 있는 함수를 말한다.
var coffeeList = "";

var addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, "아메리카노");
};

var addAmericano = function (name) {
  coffeeList += ", " + name;
  console.log(coffeeList);
  setTimeout(addMocha, 500, "카페모카");
};

var addMocha = function (name) {
  coffeeList += ", " + name;
  console.log(coffeeList);
  setTimeout(addLatte, 500, "카페라떼");
};

var addLatte = function (name) {
  coffeeList += ", " + name;
  console.log(coffeeList);
};

setTimeout(addEspresso, 500, "에스프레소");

➡️ 가독성 좋지만, 한 번만 쓰고 말텐데 이렇게 이름을 다 붙여야 하는건 비효율적이다. ➡️ 따라서 자바스크립트에서는 비동기적인 작업을 동기적으로(동기적인 것 처럼 보이도록) 처리해주는 Promise, Generator(ES6), async/await(ES7)를 제공한다.

⭐ 즉, 비동기 작업의 동기적 표현이 필요한 것이다!


비동기 작업의 동기적 표현으로 아래와 같은 것들이 있다. (중요한 개념이므로 따로 포스팅)

  1. Promise
  2. Generator
  3. async와 await



4. 다양한 함수

4.1 즉시 실행 함수

함수 정의와 동시에 즉시 호출되는 함수를 즉시 실행 함수라고 한다.

  • 즉시 실행 함수는 단 한번만 호출되며 다시 호출할 수 없으며, 익명 함수를 이용하는 것이 일반적이다.
  • 함수 이름이 없는 함수를 익명함수라고 한다.
// 익명 즉시 실행 함수
(function () {
  var a = 3;
  var b = 2;
  return a + b;
})();

// 기명 즉시 실행 함수
(function foo() {
  var a = 3;
  var b = 2;
  return a + b;
})();

foo(); // ReferenceError

기명 즉시 실행 함수의 경우 함수 이름은 함수 몸체에서만 유효한 식별자 이므로 함수를 다시 호출할 수 없다.


즉시 실행 함수를 이용하여 두 수를 더해보자

(function (a, b) {
  let sum = a + b;
  console.log(sum);
})(3, 4); // 7


즉시실행함수를 사용하는 이유는 전역 변수 사용을 억제하기 위해서 사용한다.

  • 전역 변수는 모든 코드가 전역변수를 참조하고 변경할 수 있기때문에 상태가 변경 될 수 있는 위험성이 높다.
  • 생명주기가 어플리케이션의 생명주기와 동일하기때문에 메모리 리소스도 오랜 기간 소비한다.
  • 스코프 체인의 종점에 위치하기 때문에 변수 검색 속도가 느리다.
  • 자바스크립트는 파일이 분리 되어있다해도 하나의 전역 스코프를 공유하기때문에 같은 스코프 내에 전역변수나 전역함수가 예상치 못한 결과를 가져올 수 있다.


4.2 화살표 함수 📌

function 키워드 대신 화살표 (=>)를 사용해 좀 더 간략한 방법으로 함수를 선언할 수 있다.

  • 화살표 함수는 항상 익명 함수로 정의한다. ( 익명함수란 이름이 없는 함수를 말한다.)
  • ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제 때문에 화살표함수를 도입했다.
function add1(x, y) {
  return x + y;
}
// add2: add1을 화살표 함수로 나타낼 수 있음
const add2 = (x, y) => {
  return x + y;
};

// add3: {}(중괄호) 다음 return이 나오면 retrun과 중괄호 생략 가능
const add3 = (x, y) => x + y;

// add4: return이 생략된 함수의 본문을 소괄호로 감싸줄 수 있음 (헷갈림 방지)
const add4 = (x, y) => x + y;

// add5: 매개변수 1개면 소괄호 생략 가능 (1개일 때만 가능)
const add5 = (x) => x;

// add6: 매개변수가 없는 함수를 만들 수 있음
const add6 = () => x;


⭐ 일반 함수와 화살표 함수의 가장 큰 차이점은《 this binding 》여부이다!!! ➡️ “ 화살표 함수는 this를 바인딩 하지 않는다.“는 것을 꼭 기억하자!!!

const person = {
  name: "sieun",
  sayHello: () => {
    console.log(`Hello, my name is ${this.name}`);
  },
};

person.sayHello(); // "Hello, my name is undefined" 출력


아래와 같이 괄호를 쓴다면 retrun문이 존재해야한다. 주의하자!

const num = (x, y) => {
  x + y;
};
console.log(num(1, 2)); // undefined -> return이 없기 때문


4.3 재귀 함수

자기 자신을 호출하는 함수를 재귀 함수라고 말한다. 즉, 함수 내에서 자기 자신을 호출하여 반복적인 작업 처리를 위해 사용한다.

function countdown(n) {
  for (var i = n; i > 0; i--) console.log(i);
}

countdown(3); // 3 2 1

위 코드를 재귀 함수를 사용하여 반복문 없이 구현해보자

function countdown(n) {
  if (n < 1) return;
  console.log(n);
  countdown(n - 1);
}

countdown(3); // 3 2 1


→ 재귀 함수는 반복문을 사용하는 것보다 재귀 함수를 사용할 때 직관적으로 이해하기 쉬울 때만 한정적으로 사용한다.(ex_팩토리얼)


4.4 중첩 함수

함수 내부에 정의 된 함수를 중첩 함수라고 하고, 중첩 함수를 포함하는 함수를 외부 함수라고 부른다.

function outer() {
  // 외부 함수
  var x = 1;

  function inner() {
    // 중첩 함수(내부함수)
    var y = 2;
    console.log(x + y); // 3
  }
  inner();
}
outer();



5. 참조

  1. https://www.youtube.com/watch?v=-iZlNnTGotk
  2. https://ko.javascript.info/constructor-new

댓글남기기