입력(input)을 받아 출력(output)을 내보내는 일련의 과정을 정의한 것
수학에서의 함수는 위와 같다. 예를 들어,
위와 같은 함수와 입력 값이 존재 한다면,
위와 같은 식이 성립한다.
즉 함수는 마치 재료를 투입받아 제품을 생산하는 기계와 같은 일을 한다.
함수를 실행하기 위해 필요한 입력인 2와 5는 함수 외부에서 함수 내부의 x, y 변수에 전달된다. 그리고 함수의 실행 결과인 출력은 다시 함수 외부로 반환된다. 이를 프로그래밍 언어의 관점에서 재해석 하면,
일련의 과정을 문(statement)으로 구현하고, 코드 블록으로 감싸, 하나의 실행 단위로 정의(definition) 한 것.
이라 할 수 있다.
// 함수 정의(function definition)
function add(/*매개변수(parameter)*/x, y){
//반환 값(return type)
return x + y;
}
// 함수 호출(function invoke)
add(/*인수(argument)*/2, 5);
이 때, 함수 내부로 입력을 전달받는 변수(x, y)를 매개변수(parameter, 파라미터)라 하고, 입력을 전달하는 입력 값을 인수(argument, 아규먼트)라 하며, 함수의 출력 타입을 반환 타입(return type) , 출력 값을 반환 값(return value)이라 한다.
또한 자바스크립트에서 함수는 값(value)이며, 여러 개의 함수가 존재할 수 있으므로, 다른 함수와 구별하기 위해 식별자 함수 이름을 사용할 수 있다.
자바스크립트에서의 함수는 객체 타입의 값(value)이다.
// i --> 숫자 타입의 값을 담을 공간(변수)
// 10 --> 숫자 리터럴
var i = 10;
숫자 값을 숫자 리터럴로 생성하고,
// student --> 객체 타입의 값을 담을 공간(변수)
// {name = `jong`, age = 23} --> 객체 리터럴
var student = {
name : `jong`;
age : 23;
};
객체 값을 객체 리터럴로 생성 하듯
// add --> 함수 타입의 값을 담을 공간(변수)
// (x, y) -> { return x + y} -->함수 리터럴
function add(x, y){
return x + y;
}
위 예제 코드에선 함수 리터럴을 add라는 변수에 할당하고 있다. 리터럴은 값을 생성하기 위한 표기법이므로, 함수 리터럴 또한 평가되어 값을 생성한다.
이 때 생성되는 값은 객체이므로 자바스크립트에서 함수는 객체다.
그러나 함수는 일반 객체와는 다른데, 일반 객체는 호출(invoke) 할 수 없지만, 함수는 호출할 수 있다.
함수 또한 함수 리터럴로 생성할 수 있다. 함수 리터럴은 function 키워드, 함수 이름, 매개변수 목록, 함수 몸체로 구성된다.
function add(x, y){
return x + y;
}
console.log(add(2, 5)); // --> 7
함수 선언문(function declarations)은 함수 리터럴(function literal)과 형태가 동일하다. 그러나 함수 리터럴은 함수 이름을 생력할 수 있지만, 함수 선언문은 함수 이름을 생략할 수 없다.
이유는 함수 선언문은 표현식(expression)이 아닌 문(statement) 이기 때문이다.
개발자 도구에서 함수 선언문을 실행하면 완료 값 undefined가 출력된다. 만약 함수 선언문이 표현식인 문이라면, 완료 값 undefined 대신 표현식이 평가되어 생성된 함수가 출력되어야 한다.
표현식이 아닌 문은 변수에 할당할 수 없고, 함수 선언문도 표현식이 아닌 문이므로 변수에 할당할 수 없다.
그러나 함수 선언문은 마치 변수에 할당되는 것 처럼 보인다.
var /*식별자*/myFunction = function /*함수 이름*/add(x, y){
return x + y;
}
console.log(myFunction(2, 5)); // --> 7
이는 자바스크립트 엔진이 코드의 문맥에 따라 두 가지 중 하나의 해석을 하기 때문이다.
동일한 함수 리터럴을 표현식이 아닌 문인 함수 선언문으로 해석
표현식인 문인 함수 리터럴 표현식으로 해석
자바스크립트 세상에서 중괄호는 블록문일 수도 있고, 객체 리터럴 일수도 있는 중의적인 표현이다. 중괄호가 단독으로 존재하면 자바스크립트 엔진은 중괄호를 블록문으로 해석한다. 그러나 할당 연산자의 우변(rvalue)처럼 중괄호가 값으로 평가되어야 할 문맥에선 중괄호를 객체 리터럴로 해석한다.
// SyntaxError: Function statements require a function name
// at Object.compileFunction (node:vm:360:18)
// at wrapSafe (node:internal/modules/cjs/loader:1055:15)
// at Module._compile (node:internal/modules/cjs/loader:1090:27)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
// at Module.load (node:internal/modules/cjs/loader:1004:32)
// at Function.Module._load (node:internal/modules/cjs/loader:839:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
// at node:internal/main/run_main_module:17:47
function (){
console.log(`myFunction`);
}
(function () {
console.log('myFunction');
})
위 예제의 첫 번째 함수는 단독으로 사용된 함수 리터럴이다. 자바스크립트 엔진은 이를 함수 선언문으로 해석하는데, 함수 선언문은 이름을 생략할 수 없으므로, 위와 같은 에러가 발생한다.
그룹 연산자인 소괄호 내에 있는 두 번째 함수는 함수 선언문으로 해석되지 않고 함수 리터럴 표현식으로 해석된다. 그룹 연산자의 피연산자는 값으로 평가될 수 있는 표현식이어야 하기 때문에, 자바스크립트 엔진은 이를 함수 리터럴 표현식으로 해석한다.
function myFunction1(){
console.log(`myFunction1`);
}
(function myFunction2() {
console.log('myFunction2');
})
myFunction1(); // -> myFunction1
myFunction2(); // -> ReferenceError: myFunction2 is not defined
함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출한다.
함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다. 따라서 함수 몸체 외부에서는 함수 이름으로 함수를 참조할 수 없으므로, myFunction2 함수는 참조 에러가 발생한다. 만약 함수 몸체 외부에서 함수를 호출하기 위해선 함수를 가리키는 식별자를 정의해야 한다.
그러나 myFunction1 함수는 함수 외부에서도 함수 이름으로 호출이 가능하다. 그 이유는 자바스크립트 엔진이 함수 선언문으로 정의된 함수는 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당하기 때문이다.
자바스크립트의 함수는 일급 객체(first-class object)다. 따라서 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있다. 이러한 함수 정의 방식을 함수 표현식(function expression)이라 한다.
// 함수 표현식(function expression)
var myFunction = function add(x, y){
return x + y;
}
console.log(myFunction(2, 5)); // --> 7
함수 리터럴의 함수 이름은 생략할 수 있다. 이런 함수를 익명 함수(anonymous function)라 한다. 통상적으로 리터럴 값은 이름을 생략하는 것이 관례이다.
console.log(myFunction1()); // -> 0
console.log(myFunction2()); // ->
// TypeError: myFunction2 is not a function
// at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:2:13)
// at Module._compile (node:internal/modules/cjs/loader:1126:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
// at Module.load (node:internal/modules/cjs/loader:1004:32)
// at Function.Module._load (node:internal/modules/cjs/loader:839:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
// at node:internal/main/run_main_module:17:47
function myFunction1(){
return 0;
}
var myFunction2 = function (){
return 10;
}
함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있다. 그러나 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다.
모든 선언문은 코드가 한 줄씩 순차적으로 실행되는 런타임(runtime) 이전에 자바스크립트 엔진에 의해 먼저 실행된다.
즉, 함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성된다.
따라서 함수 선언문으로 정의한 함수는 선언문 이전에 해당 함수를 참조 및 호출할 수 있으며 이를 함수 호이스팅(function hoisting)이라 한다.
함수 호이스팅과 변수 호이스팅은 미묘한 차이가 있는데, var 키워드를 사용한 변수 선언문과 함수 선언문은 런타임 이전에 식별자를 생성한다는 점에서 동일하다.
그러나 var 키워드로 선언된 변수는 undefined로 초기화 되고, 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화된다.
따라서 변수 호이스팅을 통해 참조한 변수는 undefined로 평가되지만, 함수 호이스팅을 통해 호출한 함수는 정상적으로 호출이 가능하다.
함수 표현식은 변수에 함수 리터럴 값을 할당한다. 따라서 함수 표현식으로 함수를 정의하면 함수 호이스팅이 발생하는 것이 아니라 변수 호이스팅이 발생한다.
함수 호이스팅은 함수를 호출하기 전에 반드시 함수를 선언해야 한다는 프로그래밍 언어의 자명한 규칙을 무시한다. 따라서 유명한 자바스크립트 개발자들은 함수 선언문 대신 함수 표현식을 사용할 것을 권장한다.
new 연산자와 함께 Object 생성자 함수를 호출하면 기본적으로 빈 객체를 생성하여 반환한다. 빈 객체에 프로퍼티 또는 메서드를 추가하여 완전한 객체를 완성할 수 있다.
var student = new Object(); // var student = {};
student.name = 'jong';
student.no = 81;
student.age = 23;
console.log(typeof student); // -> object
console.log(student); // -> { name: 'jong', no: 81, age: 23, getAge: [Function: getAge] }
또한 Object와 Function 생성자 함수는 new 연산자 없이 호출해도, new 연산자와 함께 호출한 것과 동일하게 동작한다.
// 생성자 함수는 파스칼 표기법(Pascal Case)으로 작성하는 것이 관례이다.
function Student(name, no, age){
this.name = name;
this.no = no;
this.age = age;
}
var student = new Student('jong', 81, 23);
console.log(typeof student); // -> object
console.log(student); // -> { name: 'jong', no: 81, age: 23, getAge: [Function: getAge] }
생성자 함수를 이용하면 같은 속성(Attribute)를 가진 여러 객체를 손 쉽게 만들 수 있다. 이는 템플릿(클래스)을 이용해 객체를 만드는 것과 유사하다.
C++, Java와 같은 클래스 기반 객체지향 언어의 생성자와의 차이점으로, 생성자 함수를 일반 함수와 동일한 방법으로 정의한다. 그리고 new 연산자와 함께 호출된 함수는 생성자 함수로 동작한다.
자바스크립트는 Object 생성자 함수 외에도 다양한 빌트인(built-in) 생성자 함수를 제공한다. 대표적인 빌트인 생성자 함수는 다음과 같다.
여기서 주의할 점은 String, Number, Boolean 생성자 함수는 new 연산자와 함께 호출했을 때, 각 데이터 타입을 표현한 값 객체를 생성하여 반환한다.
그러나 new 연산자 없이 호출하면 문자열, 숫자, 불리언 값 등 리터럴(literal) 값을 그대로 반환한다.
생성자 함수의 역할은 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 규격을 정의 하고, 그 규격에 따라 인스턴스를 생성 및 생성된 인스턴스를 초기화(initialization) 하는 것이다.
이 때 인스턴스를 생성하는 것은 필수이지만, 생성된 인스턴스를 초기화하는 것은 부가 옵션이다.
function Student(name, age){
this.name = name;
this.age = age;
}
let student1 = new Student('jong', 23);
console.log(student1); // student { name: 'jong', age: 23 }
그러나 위 예제에선 인스턴스를 생성하고 반환하는 코드는 보이지 않는다. 이는 new 연산자와 함께 생성자 함수를 호출하면 자바스크립트 엔진이 암묵적으로 인스턴스를 생성 및 초기화하고, 인스턴스를 반환하기 때문이다.
이 과정은 다음과 같다.
암묵적으로 생성된 빈 객체가 this에 바인딩 된다. 이는 함수의 선두에서 바로 이루어진다.
생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩된 인스턴스를 초기화 한다.
생성자 함수 내부의 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
주의할 점은 this가 아닌 다른 객체를 명시적으로 반환(return)하면, return 문에 명시된 객체가 반환된다.
프로퍼티 어트리뷰트(property attribute) - 생성자 함수(constructor function)
생성자 함수가 new 연산자 없이 호출되는 것을 방지하기 위해 개발자들은 생성자 함수의 이름에 통상적으로 파스칼 케이스(pascal case) 컨벤션을 사용한다.
그러나 이 사실을 자바스크립트 엔진이 알리가 없고, 개발자의 실수로 언제든 문제가 발생할 수 있다. 따라서 이런 위험한 상황을 방지하기 위해 ES6부터 메타 프로퍼티(meta property)의 일종인 new.target을 지원한다.
function Student(name, age){
if(!new.target){
return new Student(name, age);
}
this.name = name;
this.age = age;
}
let student1 = new Student('jong', 23);
console.log(student1); // student { name: 'jong', age: 23 }
new 연산자와 함께 생성자 함수로서 Student 함수가 호출되면, Student 함수 내부의 new.target은 함수 자신(this)을 가리킨다.
new 연산자 없이 일반 함수로서 Student 함수를 호출했다면, 호출된 Student 함수 내부의 new.target은 undefined가 된다.
따라서 위 코드의 if문은 일종의 Assersion 코드 역할을 한다.
단 주의할 점은 인터넷 익스플로러(IE; internet explorer)에선 new.target을 지원하지 않으므로, 이 경우 크로스 브라우징(cross browsing)을 고려해야 한다.
function Circle(radius) {
if (!(this instanceof Circle)) {
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}
위 Circle 함수는 인터넷 익스플로러와의 크로스 브라우징을 고려하여, 스코프 세이프 생성자 패턴(scope-safe constructor pattern)이 적용되었다.
ES6에서 도입된 화살표 함수(arrow function)은 function 키워드 대신 화살표를 사용해 좀 더 간략한 방법으로 함수를 선언할 수 있다.
자바의 람다식(lambda expression)과 비슷하며, 자바스크립트의 화살표 함수 또한 항상 익명 함수로 정의한다.
var add = (x, y) => {return x + y};
console.log(add(2, 5)); // 7
함수가 결과값 이외에 다른 상태를 변경시킬 때 부작용(Side Effect)이 있다고 말한다.
자바스크립트에서 원시 값(primitive value)은 값에 의한 전달(pass by value), 객체(object)는 참조에 의한 전달(pass by reference) 방식으로 동작한다.
함수를 호출하면서 매개변수(parameter)에 값을 전달하는 방식을 구분할 때는 값에 의한 호출(call by value)과 참조에 의한 호출(call by reference)로 구분한다.
그러나 동작 방식은 값에 의한 전달, 참조에 의한 전달과 동일하다.
function setZero(x){
x = 0;
}
function setKim(x){
x.name = `kim`;
}
var age = 23;
var student = {
name : `jong`
}
setZero(age);
setKim(student);
console.log(age); // 23 -> 23
console.log(student); // jong -> kim
원시 타입 인수는 변경 불가능한 값(immutable value)이므로, 직접 변경 할 수 없다. 따라서 재할당을 통해 기존의 원시 값을 할당된 새로운 원시값으로 교체해 사용했다.
그러나 객체는 변경 가능한 값(mutable value)이므로, 직접 변경 할 수 있어 재할당 없이 기존의 할당된 객체를 변경 했다.
따라서 객체를 아규먼트로 받는 파라미터 변수는 원본 객체의 데이터가 변경되는 부작용(side effect)을 고려해야 한다.
부작용을 해결하는 방법은 다음과 같다.
Java에서의 불변 클래스(Immutable Class)와 불변 객체(Immutable Object)
방어적 복사(defensive copy)를 이용한 불변 클래스(Immutable Class) 설계
함수 정의와 동시에 즉시 호출되는 함수를 즉시 실행 함수(IIFE, immediately invoked function expression)이라 한다.
console.log((function (){ // -> 100
return 10 * 10;
}()));
console.log((function (x, y){ // -> 100
return x * y;
}(10, 10)));
즉시 실행 함수는 함수 정의와 동시에 즉시 호출된다. 즉시 실행 함수는 단 한번만 호출되며, 다시 재호출 할 수 없다.
따라서 즉시 실행 함수는 함수 이름이 없는 익명 함수를 사용하는 것이 일반적이다.
함수가 자기 자신을 호출하는 것을 재귀 호출(recursive call)이라 하고, 재귀 함수(recursive function)는 재귀 호출을 수행하는 함수를 말한다.
function factorial(n){
if(n <= 1){
return 1;
}
return n * factorial(n - 1);
}
팩토리얼(factorial)은 재귀 함수로 구현 가능한 대표적인 예다.
재귀 함수의 주의점으로 재귀 함수 내에 있는 재귀 호출을 멈출 수 있는 탈출 조건이 없다면, 스택 오버플로우(stack overflow)가 발생한다.
함수(function) 내부에 정의된 함수를 중첩 함수(nested function) 또는 내부 함수(inner function)이라 한다.
중첩 함수를 포함한 함수를 외부 함수(outer function)이라 한다.
function outer(){
var i = 10;
function inner(){
var x = 10;
console.log(i * x);
}
inner();
}
outer(); // -> 100
inner(); // ->
// ReferenceError: inner is not defined
// at Object.<anonymous> (c:\Users\bitcamp\git\mjs\test.js:14:1)
// at Module._compile (node:internal/modules/cjs/loader:1126:14)
// at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
// at Module.load (node:internal/modules/cjs/loader:1004:32)
// at Function.Module._load (node:internal/modules/cjs/loader:839:12)
// at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
// at node:internal/main/run_main_module:17:47
중첩 함수는 외부 함수 내부에서만 호출할 수 있다.
일반적으로 중첩 함수는 자신을 포함한 외부 함수를 돕는 도우미 함수(helper function)의 역할을 수행한다.
함수(function)의 매개변수(parameter)를 통해 다른 함수의 내부로 전달되는 함수를 콜백 함수(callback function)라고 한다.
매개변수를 통해 함수의 외부에서 콜백 함수를 전달받은 함수를 고차 함수(HOP, higher-order function)라 한다.
function render(i){
for(; i > 0; i--){
console.log(i);
}
}
function renderIfEvenNum(i){
for(; i > 0; i--){
if(!(i % 2)){
console.log(i);
}
}
}
render(10); // -> 10 9 8 7 6 ...
renderIfEvenNum(10); // -> 10 8 6 4 ...
render 함수와 renderIfEvenNum 함수는 공통적으로 수행하는 로직이 있지만, 반복하면서 하는 로직의 내용은 다르다.
따라서 함수의 일부분만이 다르기 때문에 함수를 매번 새롭게 정의해야 한다.
이 때 함수의 변하지 않는 공통 로직은 미리 정의하고, 경우에 따라 변경되는 로직은 추상화(abstraction)해서 함수 외부에서 함수 내부로 전달하는 문법이 존재한다.
function render(i, func){
for(; i > 0; i--){
func(i);
}
}
var renderAll = function (i){
console.log(i);
}
var renderIfEvenNum = function(i){
if(!(i % 2)){
console.log(i);
}
}
render(10, renderAll); // -> 10 9 8 7 6 ...
render(10, renderIfEvenNum); // -> 10 8 6 4 ...
여기서 outer 함수가 고차 함수(higher-order function)이 되고, inner 함수가 콜백 함수(callback function)이 된다.
외부 상태에 의존하지 않거나 외부 상태를 변경할 수 없는, 즉 부작용(Side Effect, 부수 효과)이 없는 함수를 순수 함수(pure function)이라 한다.
외부 상태에 의존하거나 외부 상태를 변경할 수 있는, 즉 부작용이 있는 함수를 비순수 함수(impure function) 이라 한다.
var cnt = 0;
function increase(i){
return ++i;
}
for(var j = 0; j < 10; j++){
console.log(increase(cnt)); // -> 1 1 1 1 1 ...
}
외부 상태에 의존하지 않고, 자신의 매개변수로 받은 인수가 같으면 언제나 동일한 값을 반환한다.
따라서 위 increase 함수는 순수 함수(pure function)이다.
var cnt = 0;
function increase(){
return ++cnt;
}
for(var j = 0; j < 10; j++){
console.log(increase(cnt)); // -> 1 2 3 4 5 ...
}
외부 상태에 의존하며, 함수의 외부 상태를 변경하는 부작용(side effect)이 있다.
따라서 위 increase 함수는 비순수 함수(impure function)이다.
해시넷
http://wiki.hash.kr/index.php/%ED%95%A8%EC%88%98_(%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)
모던 자바스크립트 Deep Dive
이웅모 저 | 위키북스 | 2020년 09월 25일