자바스크립트의 함수

Kim Jin Hyeok·2021년 2월 9일
0
post-thumbnail

자바스크립트에서 가장 중요한 개념 1순위는 함수다. 자바스크립트에서 함수는 다른 언어처럼 기능을 제공한다. 하지만 기능 외에도 모듈화 처리나 클로저, 객체 생성 등의 많은 기능도 제공한다.

함수 정의

자바스크립트에서 함수를 생성하는 방법은 3가지가 있다. 각각의 방식에 따라 함수 동작이 미묘하게 차이가 있다.

  • 함수 선언문 function statement
  • 함수 표현식 function expression
  • Function() 생성자 함수

함수 리터럴

함수 선언문, 함수 표현식 둘 다 이 방식으로 함수를 생성

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

function - 함수 리터럴은 function 키워드로 시작한다.
함수명(add) - 함수를 구별하는 식별자이다. 자바스크립트에서는 함수명이 선택사항이다. 함수명이 없으면 익명함수라 한다.
매개변수(x, y) - 데이터 타입을 기술하지 않는다.
함수 내부 - 함수가 호출됬을 때 실행하는 부분이다.

함수 선언문 방식으로 함수 생성하기

반드시 함수명이 정의되어 있어야 한다.

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

함수 표현식 방식으로 함수 생성하기

자바스크립트에서 함수도 하나의 값이기 때문에 변수에 할당하는 것이 가능하며 그렇게 생성하는 방법이 함수 표현식이다. 또 끝에 세미콜론(;)을 붙이는 것을 권장한다.

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

기명 함수 표현식(함수 이름이 포함된 함수 표현식)

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

console.log(add(1,2)); // 3
console.log(sum(1,2)); // Uncaught ReferenceError: sum is not defined 발생

함수 표현식에 사용된 함수 이름은 정의된 함수 내부에서 해당 함수를 재귀적으로 호출하거나, 디버거 등에서 함수를 구분할 때 사용된다.

Function() 생성자 함수로 함수 생성하기

자바스크립트의 함수도 Function()이라는 기본 내장 함수로부터 생성된 객체라고 볼 수 있다.

new Function (arg1, arg2, ... argN, functionBody)

var add = new Function(x, y, 'return x+y');

함수 호이스팅 function hoisting

함수 생성 방식에 따른 차이 중 하나이며, 함수가 정의된 위치에 상관 없이 끌이올려지는 것이다. 이것의 원인은 자바스크립트의 변수 생성 instantiation과 초기화 initialization의 작업이 분리되어 진행되기 때문이다.

함수 선언문 방식과 함수 호이스팅

add(1,2); // 3

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

add(2,3); // 5

함수 호이스팅이 발생한다.

함수 표현식 방식과 함수 호이스팅

add(1,2); // uncaught type error

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

add(2,3); // 5

함수 호이스팅이 발생하지 않는다.

함수 객체

자바스크립트에서는 함수는 객체이자 값

자바스크립트에서는 함수도 객체다. 즉, 프로퍼티를 가질 수 있다. 또한 다음과 같은 동작까지 가능한 일급 first class 객체다.

  • 리터럴에 의해 생성
  • 변수나 배열의 요소, 객체의 프로퍼티 등에 할당 가능
  • 함수의 인자로 전달 가능
  • 함수의 리턴값으로 리턴 가능
  • 동적으로 프로퍼티를 생성 및 할당 가능

변수나 프로퍼터의 값으로 할당

변수에 함수 할당(함수 표현식)
var add = function(x, y) { return x+y; };
console.log(add(2,3)); // 5
프로퍼티에 함수 할당
var obj = {};
obj.add = function (x,y) { return x+y; };
console.log(obj.add(4,5)); // 9

함수 인자로 전달

인자로 받은 함수를 함수 호출 연산자()를 붙여 호출할 수 있다.

var printFunc = function(func) {
    func(); // 인자로 받은 func 호출
};

printFunc(function() { // 익명함수를 인자로 넘겼다.
    console.log('Function can be used as the arguments.');
});

리턴값으로 활용

var obj1 = function() {
    return function() {
        console.log('This function is the return value.');
    };
};
var obj2 = obj1();
obj2(); // This function is the return value.

함수 객체의 기본 프로퍼티

함수 객체만의 표준 프로퍼티를 가지고 있다.

var add = function (x, y) {
  return x+y;
}
console.dir(add);

  • length - 정상 실행 시 기대되는 인자의 개수
  • prototype
    [[Prototype]]과 혼동하면 안된다. 이건 이 함수가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다. 다음 그림과 같이 constructor 프로퍼티 하나만 있는 객체를 가리킨다. 그리고 그 객체의 constructor 프로퍼티는 자신과 연결된 함수를 가리킨다. 즉, 자바스크립트에서 함수를 생성할 때 연결된 프로토타입 객체를 동시에 생성한다.
  • name - 함수의 이름이며 익명함수라면 null
  • arguments - 호출할 때 전달된 인자
  • caller - 자신을 호출한 함수
  • proto - [[Prototype]], Function.prototype 객체

함수의 다양한 형태

콜백 함수 callback function

코드를 통해 명시적으로 호출되는 함수가 아니라, 개발자는 단지 함수를 등록하기만 하고, 어떤 이벤트가 발생하거나 특정 시점에 시스템에서 호출되는 함수를 말한다. 또한, 특정 함수의 인자를 넘겨서, 코드 내부에서 호출되는 함수 또한 콜백 함수가 될 수 있다.

대표적인 콜백함수의 예는 이벤트 핸들러 처리이다. 키보드가 입력되는 등의 DOM 이벤트가 발생한 경우 브라우저는 이벤트에 해당하는 핸들러를 실행시킨다.

<!DOCTYPE html>
<html>
<body> 
  <script>
    // 페이지 로드 시 호출될 콜백 함수
    window.onload = function() {
      alert('This is the callback function.');
    };
  </script>
</body>
</html>

웹페이지 로딩이 끝난 시점에 load 이벤트가 발생하면 window.onload 이벤트 핸들러에 연결된 콜백 함수가 호출된다.

즉시 실행 함수 immediate function

함수를 정의와 동시에 실행하는 함수를 말한다. 다시 호출할 수 없기에 최초 한 번의 실행만을 요구하는 초기화 부분 등에 사용할 수 있다.

(function (name) {console.log('my name is '+name)})('kim');

내부 함수 inner function

함수 내부에 정의된 함수를 말한다. 클로저나 독립적인 헬퍼 함수를 구현하는 용도로 사용한다.

function parent() { // 외부 함수 정의
    var a = 100;
    var b = 200;
    
    function child() { // 내부 함수 정의
    	var b = 300;
        
        console.log(a); // 100, 내부함수는 외부 함수의 변수에 접근이 가능하다.
        console.log(b); // 300;
    }
}
parent(); // 100, 300
child(); // Uncaught ReferenceError: child is not defined

내부 함수는 자신이 정의된 외부 함수 내에서만 호출이 가능하다. 하지만 예외적인 방법도 있다.

function parent() {
    var a = 100;
    var child = function () {
    console.log(a);
    };
    return child;
}
var inner = parent();
inner();

이와 같이 실행이 끝난 parent()와 같은 부모 함수의 변수를 참조하는 inner()와 같은 함수를 클로저 closure 라고 한다.

함수를 리턴하는 함수

자바스크립트에서 함수도 일급 객체이므로 함수 자체를 리턴할 수도 있다.

var self = function() {
    console.log('a');
    return function() {
    	console.log('b');
    }
}
self = self(); // a
self(); // b

함수 호출과 this

자바스크립트에선 타 언어들과는 달리 문법이 느슨한 편이라 함수 호출이 자유로운 점이 있다.

arguments 객체

함수 호출 시 인자를 맞춰 넘기지 않아도 에러가 발생하지 않는다.

function func(arg1, arg2) {
    console.log(arg1, arg2);
}
func(); // undefined undefined
func(1); // 1 undefined
func(1,2); // 1 2
func(1,2,3); // 1 2

위와 같이 넘기지 않을 경우 undefined가 할당, 반대로 초과된 인자는 무시된다.

이런 특성을 이용해 인자의 개수에 따라 동작을 다르게 해줄 수 있는데 이를 가능케 하는 것이 arguments 객체다.
arguments 객체는 함수 호출시 넘긴 인자들이 저장된 유사 배열 객체다.

function func(arg1, arg2) {
    console.log(arg1, arg2);
    console.dir(arguments); // arguments 객체 출력
}
func(); // undefined undefined
func(1); // 1 undefined
func(1,2); // 1 2
func(1,2,3); // 1 2

위 코드를 크롬 브라우저에서 실행하면 다음과 같은 결과가 나온다.

여기서 주목 할 부분은 세 부분이다.

  • 함수를 호출할 때 넘겨진 인자 (배열 형태)
  • length : 인자의 갯수
  • callee : 현재 실행 중인 함수의 참조값

이를 응용하면 다음과 같은 인자의 개수에 상관 없는 합계 함수를 구할 수 있다.

function sum() {
    var result = 0;
    for(var i=0; i<arguments.length; i++) {
    	result += arguments[i];
    }
    return result;
}
console.log(sum(1,2,3)); // 6
console.log(sum(1,2,3,4,5,6,7,8,9,10)); // 55

호출 패턴과 this 바인딩

자바스크립트에선 함수 호출시 인자는 물론 arguments 객체와 this 인자가 암묵적으로 전달된다. 여기서 어렵지만 this를 확실히 이해해야 한다.
this가 이해하기 어려운 이유는 여러 함수가 호출되는 방식(호출 패턴)에 따라 this가 다른 객체를 참조(this 바인딩)하기 때문이다.

메서드 호출 시 this 바인딩

객체의 프로퍼티인 함수, 즉, 메서드를 호출할 때, 메서드 내부에 사용된 this는 해당 메서드를 호출한 객체로 바인딩 된다.

var myObject = {
  name: 'kim',
  sayName: function() {
    console.log(this.name);
  }
};

var otherObject = {
  name: 'jin'
};

otherObject.sayName = myObject.sayName;

myObject.sayName(); // kim
otherObject.sayName(); // jin

함수 호출 시 this 바인딩

함수 호출 시 함수 내부에서 사용된 this는 전역 객체에 바인딩된다. 브라우저에서 실행하는 경우 전역 객체는 window 객체가 된다. Node.js 와 같은 자바스크립트 런타임 환경에서는 전역 객체는 global 객체가 된다.

var test = 'this is test';
console.log(window.test); // this is test

var func = function() {
	console.log(this.test);
}

func(); // this is test

이러한 특성은 내부 함수의 경우에도 동일하게 적용되므로 주의해야한다.

// 전역 변수 정의
var value = 100;

// 객체 생성
var myObject = {
  value: 1,
  func1: function() { // 메서드
    this.value++;
    console.log('func1 called', this.value);

    func2 = function() { // 내부함수
      this.value++;
      console.log('func2 called', this.value);

      func3 = function() { // 내부함수
        this.value++;
        console.log('func3 called', this.value);
      }

      func3(); 
    }
    
    func2(); 
  }
};

myObject.func1(); 

/*
func1 called 2
func2 called 101
func3 called 102
*/

이렇게가 아니라 내부함수에서 부모함수(위에선 메서드 func1)의 this 접근하기 위해선 부모의 this를 다른 변수에 저장하는 방법을 사용한다. 관례상 that을 사용한다.

// 전역 변수 정의
var value = 100;

// 객체 생성
var myObject = {
  value: 1,
  func1: function() { // 메서드
    var that = this;
    this.value++;
    console.log('func1 called', this.value);

    func2 = function() { // 내부함수
      that.value++;
      console.log('func2 called', that.value);

      func3 = function() { // 내부함수
        that.value++;
        console.log('func3 called', that.value);
      }

      func3(); 
    }
    
    func2(); 
  }
};

myObject.func1();

/*
func1 called 2
func2 called 3 
func3 called 4 
*/ 

자바스크립트에선 이와 같은 this 바인딩의 한계를 극복하기 위해, this 바인딩을 명시적으로 할 수 있도록 call과 apply 메서드를 제공한다.

생성자 함수를 호출할 때 this 바인딩

자바스크립트에서 객체를 생성하는 방법은 크게 객체 리터럴 방식(중괄호 이용)과 생성자 함수를 이용하는 방법이 있다. 이번에 생성자 함수를 이용한 방식을 알아보자.

자바스크립트의 생성자 함수는 말그대로 각체를 생성하는 역할을 한다. 하지만 C++이나 자바와 다르게 형식이 정해진 것이 아닌 기존 함수에 new 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작한다. 관례상 이럴 경우 함수 이름의 첫 문자를 대문자로 사용한다.

이러한 생성자 함수를 생성할 때, 생성자 함수 코드 내부에서는 앞서 본 메서드나 함수와 다르게 this 바인딩이 작동한다.

// 생성자 함수 정의 
var Person = function(name) {
  // 생성자 함수 코드 실행 전, 빈 객체가 생성되며 생성자 내부의 this는 이 객체를 가리킨다. 
  this.name = name; // this를 이용해 앞서 생성한 빈 객체에 동적 프로퍼티나 메서드를 생성한다.
  // 함수가 종료될 때, 리턴값이 명시되있지 않아도 this 바인딩 된 새로 생성된 객체가 리턴된다.
}

// 생성자 함수 호출
var kim = new Person('kim');
console.log(kim.name); // kim
객체 리터럴 방식과 생성자 함수를 통한 객체 생성 방식의 차이

객체 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성 할 수 없다. 이에 반해 생성자 함수를 사용하면 생성자 함수 호출 시에 다른 인자를 전달함으로써 다른 객체를 찍이낼 수 있다.

또한 프로토타입 객체(__proto__ 프로퍼티)도 차이가 있다.

// 객체 리터럴 방식
var kim = {
  name: 'kim',
  sex: 'male'
}
console.dir(kim);

// 생성자 함수 방식
function Person(name, sex) {
  this.name = name,
  this.sex = sex
}

var lee = new Person('lee', 'female');
console.dir(lee);

자바스크립트 각체는 자신이 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다.

생성자 함수를 new를 붙이지 않고 호출한 경우

this 바인딩이 다르기 때문에 다른 결과가 나타난다. 앞서 설명한 일반 함수처럼 this가 전역 객체에 바인딩 되기 때문이다.

var park = Person('park', 'male');
console.log(park.name); // undefined

console.log(window.name); // park
console.log(window.sex); // male

call과 apply 메서드를 이용한 명시적인 this 바인딩

각각의 상황에 따라 달라지는 자동적인 this 바인딩 외에도 명시적으로 this 바인딩을 하는 방법이 제공된다. apply()와 call() 메서드를 이용하는데, 이 메서드들은 모든 함수의 부모 객체인 Function .prototype 객체의 메서드이기에 모든 함수는 다음처럼 apply() 메서드를 호출하는 것이 가능하다.

function.apply(thisArg, argArray)

call() 메서드는 apply() 메서드와 기능이 같지만 인자의 형식이 다르다.

기억해야 할 것은 apply() 메서드도 호출하는 주체가 함수고, apply() 메서드도 thi를 특정 객체에 바인딩할 뿐 본질적인 기능은 함수 호출이라는 점이다.

첫 번째 인자 thisArg는 apply() 메서드를 호출한 함수 내부에서 사용한 this에 바인딩할 객체를 가리킨다. 두 번째 인자 argArray는 함수를 호출 할 때 넘길 인자들의 배열을 가리킨다. 즉, apply() 메서드도 함수를 호출하는 것이기에 함수 호출에 필요한 인자들을 넘기는 것이다.

function Person(name, sex) {
  this.name = name,
  this.sex = sex
}

var kim = {};

Person.apply(kim, ['kim', 'male']);
// Person.call(kim, 'kim', 'male'); 
console.log(kim);

이러한 apply()나 call() 메서드는 this를 원하는 값으로 명시적으로 매핑해서 특정 함수나 메서드를 호출할 수 있다는 장점이 있다. 대표적인 것이 arguments 객체 같은 유사 배열 객체에서 표준 배열 메서드를 사용하는 것이다.

function myFunction() {
  console.dir(arguments);
  // arguments.shift(); 에러 발생

  var argsArr = Array.prototype.slice.apply(arguments);
  console.dir(argsArr);
}

myFunction(1,2,3);

Array.prototype.slice.apply(arguments);
이 구문을 apply() 메서드의 동작 방식에 따라 해석하면 다음과 같다.
Array.prototype.slice() 메서드를 호출하되 내부의 this는 arguments 객체로 바인딩하라.
다시 말해, arguments 객체가 Array.prototype.slice() 메서드를 자신의 메서드 인 양 arguments.slice()와 같은 형태로 호출하라는 것이다.

함수 리턴

자바스크립트 함수는 항상 리턴값을 반환한다. return 문을 사용하지 않을 경우 다음과 같은 두 가지 규칙을 따른다.

일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴된다.

상식적으로 생각하자.

생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.

위에서 살펴봤듯이 this로 바인딩 된 새로 생성된 객체가 리턴된다.

하지만 명시적으로 리턴했을 때 예외상황이 있다.

function Person(name, sex) {
  this.name = name,
  this.sex = sex

  return {name: 'lee', sex: 'female'};
}

var kim = new Person('kim', 'male');
console.log(kim); // {name: 'lee', sex: 'female'}

명시적으로 객체를 지정한 경우, 생성자 함수 호출을 통해 새로운 객체를 생성하더라도, 명시적으로 넘긴 객체나 배열이 리턴된다.

function Person(name, sex) {
  this.name = name,
  this.sex = sex

  return 100;
}

var kim = new Person('kim', 'male');
console.log(kim); // {name: 'kim', sex: 'male'}

기본 타입을 지정한 경우 무시되고, this로 바인딩 된 객체가 리턴된다.

참고: 송형주, 고현준, 인사이드 자바스크립트(2014)

0개의 댓글