[JS] 함수

박시은·2023년 7월 1일
0

JavaScript

목록 보기
28/58
post-thumbnail

▶ 함수

  • 함수란 어떤 목적을 가진 작업들을 수행하는 코드들이 모인 블럭이다. (재사용성 증가, 코드 가독성 향상)
  • 즉, input과 output을 가진 기능의 단위⭐를 함수라고 한다!!

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

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

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




▶ 함수의 정의

▷ 함수 선언문

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

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

▷ 함수 표현식

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

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

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

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

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

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


▷ 생성자 함수

  • 생성자 함수란 객체를 생성하는 함수를 말한다. 생성자 함수에 의해 생성된 객체를 인스턴스라고 한다.
  • 생성자 함수를 사용하면 비슷한 객체를 여러개 만들 수 있고 상속을 구현할 수 있다.
  • 함수 이름의 첫 글자는 대문자로 시작하고, 반드시 'new' 연산자를 붙여 실행해야한다.

비슷한 객체를 여러개 만들고 싶을 때 어떻게 하면 좋을까?

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

바로 생성자 함수를 사용하면 된다. (현재는 생성자 함수보다 Class를 주로 사용하는 추세)

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);



▶ 함수의 호출

함수가 호출되면 함수 몸채 내에서 매개변수가 생성되고 undefined로 초기화된 후 인수가 순서대로 할당된다.

// 함수 선언문
function add(x, y) {
  return x + y;
}

// 함수 호출
console.log(add(2, 3)); // 5

매개변수보다 인수가 더 많은 경우 초과된 인수는 무시된다.

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

console.log(add(2, 3, 10)); // 5

이때, 초가된 인수는 버려지는 것이 아닌 암묵적으로 arguments 객체의 프로퍼티로 보관된다.

function add(x, y) {
  console.log(arguments);
  return x + y;
}

console.log(add(2, 3, 10)); // 5

초과된 인수 10은 매개변수 x와 y에 각각 할당되고, arguments 객체에도 저장된다.
따라서, console.log(arguments)를 실행하면 [2, 3, 10]과 같은 유사 배열 객체가 출력이 되는 것이다.




▶ 익명함수

함수 이름이 없는 함수를 익명함수라고 한다.

▷ 즉시 실행 함수

함수 정의와 동시에 즉시 호출되는 함수를 즉시 실행 함수라고 한다.
즉시 실행 함수는 단 한번만 호출되며 다시 호출할 수 없으며, 익명 함수를 이용하는 것이 일반적이다.

// 익명 즉시 실행 함수
(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

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

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

▷ 화살표 함수

  • 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" 출력

▷ 콜백 함수

① 개념

  • 콜백 함수란, 다른 코드의 인자로 넘겨주는 함수를 콜백 함수라고 한다. 인자로 넘겨준다는 얘기는 콜백함수를 넘겨받는 코드가 있다는 얘기이다.
  • callback = call(부르다) + back(되돌아오다) = 되돌아와서 호출!
  • 콜백 함수를 넘겨받은 forEach, setTimeout 등은 이 콜백 함수를 필요에 따라 적절한 시점에 실행하게 된다.
  • 제어권은 콜백 함수를 넘겨주는 주체한테 있다.
    • 즉, 제어권을 넘겨줄테니 너가 알고 있는 로직으로 처리해줘! 라는 명령을 내릴 수 있는 것이다.

정리

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

② 제어권

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

  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);setIntervalsetInterval

  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에 다른 값이 담길 수 있다는 것을 확인할 수 있다.


③ 콜백 함수는 함수다

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

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

④ 콜백 함수 내부의 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);

⑤ 콜백 지옥과 비동기 제어

콜백지옥이란

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

  • 값 전달 순서 : 아래 → 위
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



▶ 다양한 함수

▷ 재귀 함수

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

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_팩토리얼)


▷ 중첩 함수

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

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

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



📎참조

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

profile
블로그 이전했습니다!

0개의 댓글