JavaScript의 함수 선언 방식은 크게 두 가지가 있다.
function print(name){
console.log('Hi ' + name + '~!');
}
print('Jaeung');
C언어에서도 자주 볼 수 있던 익숙한 형태의 함수 선언 방식이다.
다른 언어와는 달리 JavaScript는 느슨한 타입 체크 언어이므로 매개변수의 타입을 명시해주지 않는다는 특징이 보인다.
함수 선언문은 기본적으로 함수명을 반드시 정의해줘야 한다.
var print = function func(name){
console.log('Hi ' + name + '~!');
};
print('jaeung');
이는 JavaScript의 함수가 일급객체이기 때문에 가능한 것이다.
var print = function(name){
console.log('Hi ' + name + '~!');
};
print('jaeung');
그럼 함수 표현식에서 함수명은 의미가 없는 것 아닌가? Nope!
함수 표현식에서 함수명은 재귀 함수를 호출하거나 디버깅 등을 할 때 사용된다.
이 외에도 Object()
와 같이 Function()
생성자 함수로도 함수 생성이 가능하지만, 굉장히 가독성이 떨어지고 잘 사용하지 않으므로 제외한다.
위에서 설명했던 함수명 정의 말고도 두 방식에는 큰 차이점이 존재하는데 그건 바로 호이스팅이다.
JavaScript는 내부적으로 유효 범위(scope) 안에 있는 모든 선언문을 해당 범위의 최상단으로 옮기고, 초기화는 그 뒤에 하게 되어있다.
일단 아래의 코드를 보자.
var func = function(){
console.log(a); //undefined
var a = 5;
console.log(a); // 5
};
func();
java 혹은 c언어의 상식으로는 변수의 선언 및 초기화 이전에 해당 변수를 참조하려고 했으므로 에러가 발생하는 게 정상이다. 하지만 결과는 undefined
가 출력된다....?🤨
내부적으로는 다음과 같이 처리된다.
var func = function(){
var a;
console.log(a); //undefined
a = 5;
console.log(a); // 5
};
func();
이렇듯 내부적으로 변수 선언을 해당 유효범위의 최상단으로 끌어올려 주는 것이 JavaScript의
변수 호이스팅 이다.
물론 함수 호이스팅도 존재한다. 다만 함수 선언문에서만 발생한다. 아래 코드를 참고하자.
var func = function(){
console.log(add(a, b)); //NaN
var a = 4;
var b = 6;
function add(a, b){
return a + b;
}
console.log(add(a, b)); //10
}
func();
console.log(a + b); //not defined error
console.log(add(a, b)); //not defined error
두 번째 줄을 보면 undefiend
끼리 덧셈 연산을 해서 NaN(Not a Number)
이 출력됐지만,
function add(a, b)
가 선언되기 전인데도 해당 함수를 호출에 성공한 것이 보인다.
맨 아래의 두 줄은 일단은 넘어가 보자.
역시나 기존에 알던 순서와는 뭔가 다르다....😐
내부적으로는 다음과 같이 처리된다.
var func = function(){
var a;
var b;
function add(a, b){
return a + b;
}
console.log(add(a, b)); //NaN
a = 4;
b = 6;
console.log(add(a, b)); //10
}
func();
console.log(a + b); //not defined error
console.log(add(a, b)); //not defined error
JavaScript는 기본적으로 함수 단위로 유효범위를 가지게 된다. 따라서 변수 호이스팅이 일어나도 변수는 func가 참조하는 익명 함수 내부의 최상단에 선언되게 되고, 그렇기 때문에 함수 외부에서는 해당 변수를 참조할 수 없다.
위의 예제들처럼 호이스팅은 코드의 순서를 헷갈리게 만들어 디버깅을 어렵게 만드는 요소가 된다.😥
하지만 함수 호이스팅의 경우 함수 선언문에서만 호이스팅이 발생하므로 함수 표현식을 이용하면 된다!
JavaScript는 놀랍게도 함수도 객체다.
이는 함수의 역할을 수행함과 동시에 객체처럼 프로퍼티를 가질 수 있고, 동적으로 추가 및 삭제도 가능하다는 소리다. 다음 코드를 살펴보자.
function func(){
console.log('I\'m Function!!');
}
func(); //I'm Function!!
func.who = 'Jaeung';
func.print = function(){
console.log('I\'m func\'s method!');
};
console.log(func.who); //Jaeung
func.print(); //I'm func's method!
delete func.who;
console.log(func.who); //undefined
console.dir(func); // [Function: func] { method: [Function] }
func
함수를 선언하고 동적으로 who
프로퍼티와 print
메서드를 추가 및 삭제해봤다.
이처럼 JavaScript는 함수가 객체의 성질도 갖는다는 것을 알 수 있다.
JavaScript는 객체 지향 언어가 아니지만 객체 지향 프로그래밍을 구현하기 위해 몇 가지 기능을 넣어놨는데, 바로 생성자 함수와 프로토타입이다.
프로토타입은 생성자 함수와 밀접한 연관이 있지만, 역시 주제와 벗어나므로 간단하게만 설명한다.
일단 코드부터 보도록 하자.
function Car(model, color){
this.model = model;
this.color = color;
}
var audi = new Car('S8L', 'black');
var genesis = new Car('G70', 'red');
console.log(audi); //Car { model: 'S8L', color: 'black' }
console.log(genesis); //Car { model: 'G70', color: 'red' }
일단 함수명이 파스칼 표기법으로 되어있다. 이것은 '이 함수는 생성자 함수입니다.' 라고 표현해주는 일종의 규칙이다.
또한 Car
함수는 분명 함수 리터럴을 이용한 함수인데, new
연산자를 사용해 마치 java의 생성자처럼 호출하고, 해당 함수를 호출한 결과는 객체를 반환하고 있다. 이유는 생성자 함수의 동작 방식에 있다.
아래에서 말하는 this
와 프로토타입
에 대해 궁금하다면
this 그리고 apply, call, bind 와 프로토타입과 프로토타입 체이닝을 참고하자. 아마 this
와 프로토타입
을 모르고서는 이해하기 힘들 수도 있다.
다음은 절대로 함수명이 특별해서가 아닌 new
연산자를 사용했기 때문에 일어나는 일이다.
- 빈 객체의 생성 및 this 바인딩
new
연산자를 이용해 함수를 호출하면 일단 비어있는 객체가 생성되고,this
를 해당 객체에 바인딩시킨다. 그리고 생성된 객체의 프로토타입은 자신을 생성한 생성자 함수의prototype
프로퍼티가 가리키는 객체가 된다.
(자신을 생성한 생성자 함수의prototype
프로퍼티를 가리키는 것은 JavaScript의 모든 객체가 그러하다.)
JavaScript의 함수는 객체로서 기본적으로 prototype
프로퍼티를 가지고 있고, 해당 프로퍼티는 함수를 생성할 때 동시에 생성된 프로토타입 객체를 가리키게 된다.
(JavaScript의 모든 객체가 가지고 있는 [[prototype]]
프로퍼티와는 다르다. 헷갈리지 말자.)
this
를 이용한 프로퍼티 생성
1번에서 바인딩 되었던this
를 이용해 해당 객체에 프로퍼티의 생성이 가능해진다.
- 생성된 객체 반환
빈 객체에this
바인딩도 끝나고this
를 이용한 프로퍼티 생성까지 끝났으면, 별도의return
값을 명시해 주지 않은 경우에는this(생성된 객체)
를 반환한다.
생성자 함수의 return
은 기본적으로는 바인딩 된 this
를 반환하지만, 명시적으로 다른 객체를 반환하는 것도 가능하다.
아래의 코드를 한 번 보자.
function Car(model, color){
this.model = model;
this.color = color;
var obj = {
'name' : 'i\'m new obj'
};
return obj;
}
var audi = new Car('S8L', 'black');
var genesis = new Car('G70', 'red');
console.log(audi); //{ name: "i'm new obj" }
console.log(genesis); //{ name: "i'm new obj" }
이처럼 다른 객체가 반환되는 것을 볼 수 있다. 또한 객체 이외에 다른 기본 자료형들을 return
값에 넣어줄 경우 무시하고 this
로 바인딩 된 객체를 반환한다.
그렇다면 결국 객체 리터럴이나 생성자 함수나 객체를 생성하는 점은 같은데, 무슨 차이가 있을까?
prototype
프로퍼티를 가리키게 된다.[[prototype]]
프로퍼티는 함수명.prototype
을 가리키게 되고,new Object()
생성자 함수를 이용해 생성되므로[[prototype]]
프로퍼티는 Object.prototype
을 가리키게 된다.생성자 함수는 결국 new
연산자를 이용했을 뿐 함수라는 사실은 변하지 않는다. 만약 new
연산자 없이 함수를 호출하게 된다면 어떻게 될까?
function Car(model, color){
this.model = model;
this.color = color;
};
var audi = Car('S8L', 'black');
var genesis = Car('G70', 'red');
console.log(audi); //undefined
console.log(genesis); //undefined
console.log(global.model); //G70
console.log(global.color); //red
위의 코드처럼 일반적인 함수 호출 규칙을 따르게 된다. Car
함수에 return
값이 없으므로 undefined
를 반환, 함수 호출 시 this
는 전역 객체에 바인딩 된다.
따라서 전역 객체(브라우저는 window
, node.js는 global
)에 model
과 color
변수가 선언되고 함수 호출 시 전달된 전달 인자가 대입되게 된 것이다.
이렇게 의도하지 않은 작동을 방지하기 위해 생성자 함수의 네이밍에 규칙을 둔 것이고, 그래도 혹시 실수하는 경우를 막기 위한 패턴이 존재한다.
function Car(model, color){
if(!(this instanceof Car)){
return new Car(model, color);
}
/*or
* if(!(this instanceof arguments.callee)){
* return new Car(model, color);
* }
*/
this.model = model;
this.color = color;
};
var audi = Car('S8L', 'black');
var genesis = new Car('G70', 'red');
console.log(audi); //Car { model: 'S8L', color: 'black' }
console.log(genesis); //Car { model: 'G70', color: 'red' }
console.log(global.model); //undefined
console.log(global.color); //undefined
위에서 이미 설명했지만, new
연산자를 이용하지 않고 함수를 호출한 경우 원하지 않는 동작이 일어날 수 있다. 그래서 일반 함수로 호출했을 경우를 대비해 생성자 함수의 상단에 조건문을 추가해주었다.
A instanceof B
는 A객체의 프로토타입 체인에 B.prototype이 존재하는 판별해주는 연산자인데, 여기서 this(전역 객체)
의 프로토타입 체인에는 Car.prototype
이 존재하지 않으므로 조건문은 !(false) === true
가 된다.
조건에 따라 생성자 함수는 바인딩 된 this(전역 객체)
가 아닌 새로운 객체를 명시적으로 반환해 줌으로서 생성자 함수의 일반 함수 호출을 막을 수 있다.