
타입 변환
자바스크립트의 타입 변환은 명시적 강제 변환
, 암시적 강제 변환
두 가지가 있다. 명시적 강제 변환
은 의도적인 타입 변환을 나타내는 것이고, 암시적 강제 변환
은 표현식의 평가 중 타입이 변환되는 것이다.
명시적 강제 변환
명시적 강제 변환은 명확하게 의도를 갖고 타입을 변환하는 것이다.
문자열로 변환
문자열로 변환하는 가장 간단한 방법은 String()
함수를 호출하는 것이다. new 키워드를 붙이지 않고 String()
함수를 호출하면 랩퍼 객체를 생성하는 것이 아니라 타입 변환 함수로 동작한다.
console.log(String(3)); // '3'
console.log(String(false)); // 'false'
console.log(String(null)); // 'null'
다른 타입에서 문자열 타입으로의 변환은 타입별로 방법이 정해져있다. ECMAScript에서는 이 규칙을 ToString 추상 연산
이라고 명시하고 있으며, 명시적이든 암시적이든 문자열로 변환될 때 수행된다.
정확히 말하면
String()
함수는 심볼 이외의 타입에 대해서 ToString 추상 연산을 따른다. 심볼 타입의 경우SymbolDescriptiveString
연산을 통해 변환된다.
undefined
=>'undefined'
null
=>'null'
boolean
=>true
->'true'
,false
->'false'
number
=>Number.toString()
의 결과 반환 (3 -> ‘3’)Symbol
=> 문자열로 변환을 시도하는 경우 TypeError 발생BigInt
=> `BigInt.toString()의 결과 반환 (3n -> ‘3n’)object
=> 객체를 원시 타입으로 변환한 후 다시 한 번 ToString 추상 연산을 수행한 값을 반환한다. 객체를 원시 타입으로 변환하는 과정을 정확히는ToPrimitive 추상 연산
이라고 한다.
const boolVal = true;
const numVal = 2;
console.log(String(boolVal)); // 'true'
console.log(String(numVal)); // '2'
심볼은 연산을 통해 문자열 또는 숫자로 변환될 경우 TypeError가 발생한다. 이는 심볼이 변환되어 객체의 다른 프로퍼티에 접근하는 것을 방지하기 위해서이다.
myObject["__" + Symbol("key")]; // TypeError
또 다른 문자열 변환 방법은 toString()
메서드를 사용하는 것이다.
자바스크립트의 모든 객체는
toString()
메서드를 갖고 있다. 문자열, 숫자, 불리언 원시 타입은 랩퍼 객체를 통해 사용이 가능하며, 심볼과 BigInt는 자체적으로 구현된toString()
메서드를 가지고 있다.
const num = 4;
console.log(num.toString()); // '4'
String()
과 toString()
의 차이점
console.log(String(null)); // 'null'
console.log(String(undefined)); // 'undefined'
undefined.toString(); // TypeError 발생
null.toString(); // TypeError 발생
null과 undefined 타입은 값이 비어 있음, 할당되지 않은 상태를 나타내는 원시 타입이므로 toString()
메서드를 호출할 수 있다면 논리상 말이 되지 않는다. 또한 이 타입들은 객체가 아니기 때문에 객체에서 사용할 수 있는 프로퍼티들을 사용할 수 없는 것이 옳다.
반면, String()
함수의 경우 심볼 이외의 모든 타입이 규칙을 정확히 따르기 때문에 문자열로 명시적 강제 변환을 하고 싶은 경우엔 toString()
메서드보단 String()
함수를 사용하는 것이 더 적합하다.
숫자로 변환
숫자로의 변환도 문자열과 유사하게 Number()
함수를 호출하여 변환하는 방법이 있으며, BigInt 타입을 제외한 데이터는 ECMAScript의 ToNumber
추상 연산 명세를 기준으로 변환된다.
- undefined => NaN
- null => +0
- string => 숫자로 변경이 불가능한 값 -> NaN, 숫자형 문자열 -> 숫자
- boolean => true -> 1, false -> 0
- Symbol => 숫자로 변환을 시도하는 경우 TypeError 발생
- BigInt => 숫자로 변환을 시도하는 경우 TypeError 발생
- object => 객체를 원시 타입으로 변환한 후 다시 한 번 ToNumber 추상 연산을 수행한 값을 반환한다.
console.log(Number("3")); // 3
console.log(Number(true)); // 1
console.log(Number(null)); // 0
BigInt 타입은 연산 중 숫자로 변환되는 것을 방지하기 위해 TypeError를 발생시킨다. BigInt 타입은 숫자 타입과 다르게 큰 정수(안전한 숫자 타입의 범위를 벗어나는 정수)의 연산을 위해 나온 타입이기 때문에 숫자와는 타입이 구분되어야 한다.
parseInt()
함수
parseInt()
함수는 문자열만 대상으로 변환한다. 값이 문자열이 아닌 경우 해당 값을 문자열로 변환한 후 사용한다. 문자열의 변환 과정은 ToString 추상 연산 과정을 따른다.
console.log(parseInt("10", 10)); // 10
console.log(parseInt("-1", 10)); // -1
parseInt()
함수의 두 번째 인자는 기수를 의미한다. 예제 코드는 기수를 10으로 지정하였기 때문에 10진수를 기준으로 문자열을 숫자로 변환한다. 기수를 생략하면 첫 번째 인자를 기준으로 추정하여 변환하므로 의도하지 않은 결과가 나올 수 있다.
parseInt()
함수는 Number()
함수와 달리 인내심을 가지고 끝까지 변환을 수행한다.
console.log(Number("10A", 10)); // NaN
console.log(parseInt("10A", 10)); // 10
Number()
함수의 경우 숫자로 변경 불가능한 문자가 있으면 곹바로 NaN을 반환하지만, parseInt()
함수는 변경 불가능한 문자가 나타날 때까지 최대한 숫자로 변환해서 결과를 반환한다.
불리언으로 변환
Boolean도 ECMAScript의 ToBoolean 추상 연산에 따라 타입을 변환한다.
- undefined => false
- null => false
- string => 빈 문자열 -> false, 그 외 문자열 -> true
- number => +0, -0, NaN -> false, 그 외 숫자 -> true
- Symbol => true
- BigInt => 0n -> false, 그 외 BigInt 정수 -> true
- object => true
불리언 타입으로의 변환은 Boolean()
함수를 호출하는 방법과 이중 부정 연산자를 사용하는 방법이 있으며, 두 연산은 모두 동일한 결과를 반환한다.
const a = null;
const b = 0;
const c = "";
const d = {};
const e = [];
console.log(Boolean(a)); // false
console.log(Boolean(b)); // false
console.log(Boolean(c)); // false
console.log(Boolean(d)); // true
console.log(Boolean(e)); // true
console.log(!!a); // false
console.log(!!b); // false
console.log(!!c); // false
console.log(!!d); // true
console.log(!!e); // true
객체의 원시 타입 변환
객체의 원시 타입 변환은 문자열로 변환, 숫자로 변환 두 가지로 나눌 수 있다. 이 과정에서 valeOf()
과 toString()
메서드가 중요한 역할을 한다.
문자열로 변환
- 객체에 정의된
toString()
메서드를 호출한다. 별도로 정의한toString()
메서드가 없다면 기본적으로Object
,prototype.toString()
메서드는 결과 값으로'[Object object]'
문자열을 반환한다. - 1단계의 결과가 원시 타입이라면 그 결과를 문자열로 변환하여 반환하고, 그렇지 않다면
valueOf()
메서드를 호출한다.valueOf()
메서드 역시 객체에 별도로 정의한valueOf()
메서드가 없다면 기본적으로Object.prototype.valueOf()
메서드를 실행한다. valueOf()
메서드의 결과 값이 원시 타입이라면 그 결과를 문자열로 변환하여 반환하고, 그렇지 않다면 TypeError가 발생한다.
console.log(String({})); // '[Object object]'
빈 객체가 '[Object object]'
문자열로 변환되었다. '[Object object]'
가 어떻게 나온 결과 값인지 의아할 수 있지만, 이 결과는 위의 단계를 정확하게 따른 결과이다.
- 빈 객체를 문자열로 변환하기 위해 객체의
toString()
메서드를 호출한다. 직접 정의한toString()
메서드가 없기 때문에Object.prototype.toString()
메서드를 실행한다. - 1단계의 결과값은
'[Object object]'
문자열이며 원시 타입이기 때문에 이 결과를 반환한다.
빈 객체의 toString()
메서드의 결과값이 '[Object object]'
문자열 원시 타입이기 때문에 이 결과를 반환하는 것이다. 또한 toString()
메서드의 결과값이 원시 타입이기 때문에 valueOf()
메서드는 호출되지 않는다.
숫자로 변환
- 객체에 정의된
valueOf()
메서드를 호출한다. 별도로 정의한valueOf()
메서드가 없다면 기본적으로Object.prototype.valueOf()
메서드를 실행한다.Object.prototype.valueOf()
메서드는 결과 값으로 객체를 그대로 반환한다. - 1단계의 결과가 원시 타입이라면 그 결과를 숫자로 변환하여 반환하고, 그렇지 않다면
toString
메서드를 호출한다. 별도로 정의한toString()
메서드가 없다면 기본적으로Object.prototype.toString()
메서드를 실행한다. toString()
메서드의 결과 값이 원시 타입이라면 그 결과를 숫자로 변환하여 반환하고, 그렇지 않다면 TypeError가 발생한다.
valueOf()
메서드를 먼저 호출한 후 toString()
메서드를 호출하는 것을 제외하면 객체를 문자열로 변환하는 과과 유사하다.
console.log(Number({})); // NaN
- 빈 객체를 숫자로 변환하기 위해 객체의
valueOf()
메서드를 호출한다. 직접 정의한valueOf()
메서드가 없기 때문에Object.prototype.valueOf()
메서드를 실행한다. - 1단계의 결과값은 원시 타입이 아닌 빈 객체를 그대로 반환하기 때문에
toString()
메서드를 호출한다. toString()
메서드의 결과값은'[Object object]'
문자열이며 원시 타입이기 때문에 이 결과를 숫자로 변환한다.'[Object object]'
문자열은 숫자로 변환할 수 없는 값이기 때문에 최종적으로 NaN을 반환한다.
객체의 valueOf()
와 toString()
만약 valueOf()
와 toString()
메서드를 직접 정의한 경우 어떤 결과 값을 반환할까?
const obj = {
valueOf() {
return 1;
},
toString() {
return "toString";
},
};
console.log(String(obj)); // 'toString'
console.log(Number(obj)); // 1
객체의 valueOf()
와 toString()
메서드를 직접 정의하면 타입 변환의 결과도 달라진다. 따라서 valueOf()
와 toString()
메서드를 재정의할 때는 주의해야 한다.
배열, Date, 정규식과 같은 특수한 객체들은 자체적인 toString()
또는 valueOf()
메서드를 가지고 있다. 예를 들어 배열을 문자열로 변경하면 배열의 원소를 콤마(,)로 구분하여 문자열로 병합한다.
console.log([1]); // '1'
console.log([1, 2]); // '1,2'
객체의 원시 타입 변환 과정을 ECMAScript에서는 ToPrimitive 추상 연산으로 정의하고 있다.
암시적 강제 변환
암시적 강제 변환은 연산 중에 내부적으로 타입을 변환하는 것이다. 명시적 강제 변환과 달리 코드에서 명확하게 타입을 변환하는 것인지 알기 어렵다. 많은 개발자가 자바스크립트를 좋아하지 않는 이유 중 하나이며, 초보 개발자들이 가장 헷갈려 하는 부분이다. 하지만 암시적 강제 변환 역시 ECMAScript 명세의 기준대로 정확하게 동작하고 있다.
덧셈 연산자
덧셈 연산자는 숫자 연산이나 문자열을 병합할 때 사용하는 것이라고 생각할 수 있지만, 덧셈 연산자는 몇 가지 특징이 있다.
- 피연산자 중 하나가 문자열 타입인 경우 나머지 타입도 문자열로 변환하여 병합한다.
// 문자열과 숫자의 덧셈 연산은 숫자를 문자로 변환하여 병합한다.
console.log(1 + ""); // '1'
String()
과a + ''
의 문자열 변환은 차이가 있다. 피연산자의 타입이 객체인 경우String()
함수는 객체의toString()
,valueOf()
의 순서로 메서드를 호출하여 결과를 반환하며,a + ''
는valueOf()
,toString()
의 순서로 호출한 결과를 반환한다.
- 피연산자 중 하나가 객체이며 문자열로 변환 가능한 경우 문자열로 변환하여 연산한다. 객체의 문자열 변환 과정은 ‘문자열로 변환’에서 설명한 과정과 동일하다.
// 빈 객체는 '[object Object]' 문자열로 변환이 가능하므로 숫자 1을 문자열로 변환하여 두 문자열을 병합한다.
console.log(1 + {}); // '1[object Object]'
- 피연산자가 모두 문자열과 객체가 아닌 경우 숫자로 변환하여 연산한다. 만약 변환 결과의 타입이 각각 다른 경우 TypeError가 발생한다.
// 피연산자 중 객체나 문자열이 없기 때문에 true를 숫자로 변환하여 연산한다.
console.log(1 + true); // 2
동등 연산자
동등 연산자의 가장 큰 특징은 암시적 강제 변환을 허용하는 것이다. 물론 피연산자의 타입이 서로 같은 경우에는 변환하지 않는다.
- 피연산자 중 하나는 문자열, 하나는 숫자인 경우 문자열을 숫자로 변환하여 동등함을 비교한다.
// 문자열 '1'을 숫자로 변환하여 동등함을 판단한다.
console.log(1 == "1"); // true
- 피연산자 중 하나는 문자열, 다른 하나는 BigInt인 경우 문자열을 BigInt로 변환하여 동등함을 비교한다.
// 문자열 '1'을 BigInt로 변환하여 동등함을 판단한다.
console.log(1n == "1"); // true
- 피연산자 중 하나는 null, 다른 하나는 undefined인 경우 동등하게 판단한다.
console.log(null == undefined); // true
console.log(undefined == null); // true
- 피연산자 중 하나가 불리언일 경우 불리언을 숫자로 변환하여 동등함을 비교한다.
// 불리언 true를 숫자로 변환하여 비교한다.
console.log(true == 1); // true
- 피연산자 중 하나는 객체, 다른 하나가 문자열, 숫자, BigInt, 심볼 중 하나일 경우 객체를 원시 타입으로 변환하여 동등함을 비교한다.
// 빈 객체를 원시 타입으로 변환한 후 비교한다.
console.log("[object Object]" == {}); // true
- 피연산자 중 하나는 숫자, 다른 하나는 BigInt인 경우 내부적인 숫자 비교 알고리즘에 따라 비교한 결과를 반환한다.
console.log(1 == 1n); // true
true와 문자열 ‘1’은 전혀 다른 타입으로 동등 비교 시 false가 나올 것 같지만 그렇지 않다. 불리언 true는 숫자 1로 변환되어 문자열 ‘1’과 비교하게 되고, 이 과정에서 문자열 ‘1’은 숫자 1로 변환되어 최종적으로 숫자 1과 1을 비교하게 된다.
console.log(true == "1"); // true
위에서 설명한 변환 규칙을 충실히 따른 결과이지만 확실히 가독성이 떨어진다. 이런 경우 명확하게 엄격한 동등 연산자를 사용하는 것이 가독성에 훨씬 좋다. 하지만 동등 연산자도 무조건 나쁜 것만은 아니다.
function isEmpty(a) {
if (a == null) {
// ...
}
}
특정 값의 비어 있음을 판단하기 위해 동등 연산자를 사용하였다. 동등 연산자는 null과 undefined를 동등하게 보기 때문에 두 경우의 수를 모두 편리하게 찾아낼 수 있다.
비교 연산자
비교 연산자는 숫자 데이터의 대소 비교에서 사용하는 경우가 대부분일 것이다. 하지만 비교 연산자 역시 동등 연산자처럼 피연산자가 서로 다른 타입일 경우 암시적 강제 변환이 발생한다. ECMAScript 명세는 a < b 연산 기준으로 설명하고 있기 때문에 이 기준을 따라 설명하도록 한다.
비교 연산자는 크게 문자열 데이터의 비교, 그 이외의 경우 두 가지로 나누어져 있다. 다만, 피연산자가 객체인 경우 먼저 객체를 원시타입으로 변환한 후 비교한다. 변환된 결과가 모두 문자열이라면 문자열 비교를 하고 이외에는 문자열 외의 비교 규칙에 따라 비교한다.
문자열 비교
문자열 비교는 각 문자를 알파벳 순서로 비교한다.
console.log("a" < "b"); // true
// 왼쪽에서부터 문자 단위로 비교한다는 것에 주의하자! 왼쪽부터 '1'과 '0'을 먼저 비교하고, 그 다음 문자를 비교한다.
console.log("1" < "04"); // false
배열은 각각 ‘a’, ‘b’ 문자열로 변환되기 때문에 문자열끼리의 비교를 수행한다.
console.log(["a"] < ["b"]); // true
문자열 외의 비교
문자열끼리의 비교가 아닌 그 외의 경우는 아래와 같은 규칙으로 동작한다.
- 피연산자 중 하나는 문자열, 다른 하나는 BigInt인 경우 문자열을 BigInt로 변환하여 비교한다.
// 문자열 '1'을 BigInt로 변환하여 비교한다.
console.log("1" < 2n); // true
- 피연산자를 모두 숫자로 변환하여 비교한다. 만약 피연산자 중 하나는 숫자, 다른 하나는 BigInt인 경우 내부적인 숫자 비교 알고리즘에 의해 비교를 수행한다.
// 불리언 true를 숫자로 변환한다.
console.log(1 < true); // false
// 숫자와 BigInt는 내부 숫자 비교 알고리즘에 의해 비교된다.
console.log(1n < 2); // true
비교 연산과 타입 변환
비교 연산은 동등 연산과 달리 엄격한 비교 연산자 같은 표현식은 존재하지 않는다. 즉 다른 타입 간의 비교 연산에서 암시적인 강제 변환을 막을 수 없다. 하지만 동등 연산과 달리 비교 연산은 서로 다른 타입에 대해 사용할 일이 드믈고, 객체나 배열을 원시 타입과 비교하는 일도 거의 없다.
// 아래와 같은 비교 연산 코드는 실제로 거의 사용하지 않는다.
const a = " 1";
const b = ["02"];
if (a < b) {
// ...
}
만약 서로 다른 타입을 대상으로 비교 연산자를 사용한다면, 명시적 강제 변환을 통해 변환한 후 사용하는 것이 안전하다.
const a = "1";
const b = ["02"];
if (Number(a) < Number(b)) {
// ...
}
Number()
함수를 사용하여 모두 숫자로 변환한 후 비교한다. 이처럼 다른 타입의 비교 연산은 명시적 타입 변환을 통해 동일한 타입으로 변환한 후 실행하는 것이 안전하다.
조건 표현식과 논리 연산자
조건 표현식에서 암시적 강제 변환은 아주 흔하게 사용된다. 모든 값은 불리언으로 변환되어 조건 표현식에서 평가된다. 빈 문자열, null, undefined 등 falsy한 값을 필터링하기 위해 많이 사용하며 명시적 강제 변환보다 더 많이 사용된다.
const a = 0;
const b = "javascript";
const c = nul;
if (a) {
console.log("호출되지 않음");
}
while (b) {
console.log("truthy");
break;
}
console.log(c ? "truthy" : "falsy"); // 'falsy'
조건 표현식에서의 암시적 강제 변환은 Boolean()
함수나 이중 부정 연산자를 사용하여 명시적 강제 변환을 수행하는 것보다 훨씬 간결하게 조건식을 표현할 수 있다.
-
논리 연산자(
&&
,||
) : 자바스크립트의 논리 연산자는 단락 평가 방식을 따른다. 하지만 자바스크립트의 논리 연산자는 특이한 점이 있다. 논리 연산자의 결과값이 불리언 타입이 아닐 수 있다는 것이다.&&
논리 연산자는 첫 번째 피연산자의 값이 true로 평가되는 경우 두 번째 피연산자의 값을 반환하고, false로 평가되면 첫 번째 피연산자의 값을 반환한다.&&
논리 연산자는 첫 번째 피연산자 a부터 평가한다.- 첫 번째 피연산자 a는 불리언값이 아니므로 암시적 타입 변환을 통해 불리언값으로 변환된다. null은 falsy 값이기 때문에 false로 변환된다.
- a의 평가 결과가 false이기 때문에 단락 평가 방식에 따라 다음 피연산자인 b는 평가하지 않는다.
- 최종적으로 피연산자 a의 값을 반환한다.
-
||
논리 연산자는 첫 번째 피연산자의 값이 true로 평가되는 경우 첫 번째 피연산자의 값을 반환하고, false로 평가되면 두 번째 피연산자의 값을 반환한다.||
논리 연산자는 첫 번째 피연산자 b부터 평가한다.- 첫 번째 피연산자 b는 불리언값이 아니므로 암시적 타입 변환을 통해 불리언 값으로 변환된다. ‘javascript’ 문자열은 truthy 값이기 때문에 true로 변환된다.
- b의 평가 결과가 true이기 때문에 단락 평가 방식에 따라 다음 피연산자인 c는 평가하지 않는다.
- 최종적으로 피연산자 b의 값을 반환한다.
const a = null; const b = "javascript"; const c = 1; console.log(a && b); // null console.log(b || c); // 'javascript'
-
논리 연산자의 활용 : 논리 연산자의 특징은 디폴트 값을 설정하거나 조건에 따라 함수를 실행할 때 유용하다.
setDefault()
함수는 a의 값이 falsy 값인 경우 ‘default string’이라는 문자열을 디폴트 값으로 설정한다.
function setDefault(a) { return a || "default string"; }
- 피연산자 a가 truthy값인 경우에만
doSomething()
함수를 실행한다. React를 사용할 때 컴포넌트를 조건에 따라 렌더링하기 위해 자주 사용하는 표현식이다.
const a = "javascript"; function doSomething() { // ... } a && doSomething();
- falsy값이 아닌 null, undefined처럼 값이 비어있는 경우에만 디폴트 값을 설정하고 싶은 경우
||
연산자가 아닌 ES2020에서 등장한 null 병합(nullish coalescing) 연산자를 사용할 수 있다.
const a = ""; // a가 null, undefined인 경우에만 'default' 문자열이 b의 값으로 할당된다. const b = a ?? "default";
함수
함수란 무엇인가?
함수는 객체의 특별한 형태이며, 문(statement)으로 구성된 몸체를 가진 하나의 실행 단위이다.
function doSomething() {
console.log("hello javascript");
}
또한 자바스크립트의 함수는 일급 함수(first-class function)로서 다른 함수의 매개변수나 반환값으로도 사용할 수 있다.
function finishSomething(callback) {
callback();
console.log("finish");
}
finishSomething(doSomething);
doSomething()
함수를 finishSomething()
함수의 인자로 넘긴 것을 볼 수 있다. 다른 함수의 인자로 넘어가는 함수를 콜백함수라고 부르며, 이 패턴은 자바스크립트에서 아주 많이 쓰인다. 가장 대표적인 예로 DOM에 이벤트를 추가하는 addEventListener()
함수가 있다.
button.addEventListener("click", callback);
일급 함수
는 다음과 같은 조건을 만족해야 한다. 자바스크립트의 함수는 이 조건을 모두 만족하기 때문에 일급 함수라고 할 수 있다.
- 변수에 함수를 할당할 수 있다.
- 함수를 인자로 전달할 수 있다.
- 함수를 반환 값으로 사용할 수 있다.
함수의 정의 방법
자바스크립트 함수는 function 키워드로 정의되며 다음과 같은 구성 요소를 가지고 있다.
- 함수의 이름(식별자) : 함수를 할당한 변수의 이름이다.
- 함수의 매개변수 : 매개변수는 괄호 안에서 쉼표로 분리하며, 함수의 몸체에서 지역 변수처럼 사용된다.
- 함수의 몸체 : 중괄호({}) 안에 정의하며, 함수가 호출될 때마다 실행되는 문의 집합니다. 함수를 생성하는 방법은 세 가지가 있다.
함수 선언문
,함수 표현식
,Function 생성자 함수
를 사용하는 방법이다. 여기서는 함수 선언문과 함수 표현식만을 다룰 것이며, Function 생성자 함수는 보안 및 성능 문제가 있어 권장되지 않으므로 다루지 않을 것이다.
Function 생성자 함수
Function 생성자 함수는 인수와 함수 본체를 문자열로 정의한다. Function 생성자 함수는 문자열 삽입 공격에 노출될 경우 인자, 함수 본체를 조작할 수 있기 때문에 보안면에서 굉장히 취약하다.const addNumber = new Function( "num1, num2", 'console.log("더하기"); return num1+num2' ); console.log("결과: " + addNumber(1, 2));
함수 선언문
함수 선언문은 함수의 이름, 매개변수, 몸체로 구성되며, 함수의 이름이 반드시 정의되어야 한다. 함수 선언문이 실행되면 함수의 이름과 동일한 변수를 정의하여 함수 객체를 변수에 할당한다.
function multiply(a, b) {
return a * b;
}
함수 선언문으로 multiply()
함수를 정의했다. 정확히 말하면 함수의 이름과 동일한 multiply라는 변수를 선언하여 함수 객체를 할당하는 것이다.
함수 선언문은 호이스팅(hoisting)으로 인해 함수가 선언된 위치에서 코드의 최상단으로 끌어올려진다. 따라서 이 함수는 선언된 위치보다 상단에서 호출될 수 있다.
console.log(multiply(2, 3)); // 6
function multiply(a, b) {
return a * b;
}
이처럼 함수 선언문으로 정의한 함수는 호이스팅되기 때문에 정의된 위치보다 앞서 호출될 수 있다.
함수 표현식
익명 함수 표현식
함수 표현식은 함수 선언문과 달리 함수의 이름이 선택 사항이며, 변수에 함수를 직접 할당한다.
const multiply = function (a, b) {
return a * b;
};
함수 선언문과 달리 함수의 이름을 생략하였고, multiply라는 변수를 선언하여 함수 객체를 할당했다. 여기서 multiply가 함수의 이름이 아닌 변수라는 것을 명심해야 한다. 이렇게 이름이 없는 함수를 익명 함수(anonymous function) 표현식이라고 부른다. 익명 함수의 호출은 함수를 할당한 변수를 사용하여 호출한다.
const multiply = function (a, b) {
return a * b;
};
console.log(multiply(2, 3)); // 6
기명 함수 표현식
기명 함수 표현식에서 정의한 함수의 이름은 함수의 몸체에서만 사용할 수 있다.
const multiply = function doSomething(a, b) {
return a * b;
};
console.log(multiply(2.3)); // 6
console.log(doSomething(2, 3)); // Uncaught ReferenceError: doSomething is not defined
기명 함수 표현식에 사용된 함수의 이름은 외부에서 접근할 수 없기 때문에 함수의 이름으로 호출할 경우 ReferenceError가 발생한다. 외부에서 함수 표현식을 호출할 경우 반드시 함수를 할당한 변수를 사용해야 한다는 것을 명심해야 한다.
기명 함수 표현식은 주로 함수 표현식을 재귀적으로 호출할 때 사용한다.
const factorial = function doSomething(n) {
return n <= 1 ? 1 : n * doSomething(n - 1);
};
console.log(factorial(5)); // 120
또한 함수 표현식은 함수 선언문과 달리 호이스팅이 도지 않기 때문에 변수에 함수를 할당하기 전에 호출할 수 없다.
console.log(multiply(2, 3)); // Uncaught TypeError: multiply is not a function
var multiply = function (a, b) {
return a * b;
};
함수의 호출
함수 선언문 또는 함수 표현식으로 함수를 정의하였다면 함수를 호출하여 실행할 수 있다. 함수 호출은 표현식이기 때문에 값으로 평가되며, 함수 몸체 안에 return 문을 사용하여 결과값을 반환할 수 있다. 만약 return 문을 명시적으로 호출하지 않는다면 함수 호출의 결과값은 undefined가 된다.
function greeting() {
return "Hello javascript";
}
console.log(greeting()); // 'Hello javascript'
매개변수(parameter)
함수에 필요한 정보들을 전달하기 위해서는 함수를 정의할 때 매개변수를 명시하여 정보들을 전달할 수 있다. 매개변수들은 함수 몸체에서 지역 변수처럼 사용된다.
// name이라는 매개변수를 명시하여 필요한 정보를 전달받을 수 있다.
function greeting(name) {
return `Hello ${name}`;
}
자바스크립트는 느슨한 타입을 가진 언어이기 때문에 매개변수 역시 타입을 명시하지 않는다. 또한 함수 호출 시 전달하는 인자의 값도 검사하지 않으며, 심지어 인자의 개수도 검사하지 않는다. 함수 호출 시 본래 정의된 매개변수보다 적은 수로 인자를 전달하면 나머지 매개변수는 undefined값으로 설정된다. 반대로 정의된 매개변수보다 많은 인자를 전달하면 나머지 인자들은 무시된다.
인자
와매개변수
인자와 매개변수는 동일한 의미 같지만 구분되어야 한다.인자
는 함수 호출 시 전달되는 값을 의미하며,매개변수
는 함수에서 전달된 인자를 받아들이는 변수를 의미한다. 영문으로도 인자는 argument, 매개변수는 parameter로 구분해서 사용한다.
function greeting(name) {
return `Hello ${name}`;
}
console.log(greeting()); // 'Hello undefined'
console.log(greeting("Lee", "Han")); // 'Hello Lee'
해체 할당과 매개변수 기본값
함수의 매개변수는 ES2015의 해체 할당과 매개변수 기본값을 사용하여 좀 더 편리하에 사용할 수 있다. 매개변수 해체 할당은 객체와 배열 모두 가능하며 프로퍼티가 없는 변수는 undefined를 할당받는다.
function getUserInfo({ name, age, country }) {
return `name: ${name}, age: ${age}, country: ${country}`;
}
const userInfo = { name: "Lee", age: 20 };
console.log(getUserInfo(userInfo)); // 'name: Lee, age: 20, country: undefined'
매개변수에 해당하는 인자를 넘기지 않으면 매개변수의 값은 undefined로 설정된다. 만약 undefined가 아닌 기본값을 할당하고 싶은 경우 매개변수 기본값을 사용하여 지정할 수 있다.
function greeting(name = "Lee") {
return `Hello ${name}`;
}
console.log(greeting()); // Hello Lee
arguments
화살표 함수를 제외한 모든 함수에서는 arguments라는 객체를 사용할 수 있다. arguments 객체를 사용하여 함수에 실제로 전달된 인자들을 참조할 수 있다. 또한 arguments 객체는 유사배열 객체이기 때문에 인덱스로 프로퍼티에 접근할 수 있으며, length 프로퍼티를 가지고 있다.
function sum(x, y, z) {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments[2]); // undefined
console.log(arguments.length); // 2
return x + y + z;
}
sum(1, 2);
arguments 객체는 ES2015에서 등장한 나머지 매개변수로 대체할 수 있다. 나머지 매개변수는 유사 배열 객체가 아닌 진짜 배열이기 때문에 인자들을 배열로 다루고 싶은 경우 유용하게 사용할 수 있다.
function sum(...args) {
// args는 배열이기 때문에 배열 내장 메서드인 forEach()를 사용할 수 있다.
args.forEach(function (arg) {
// ...
});
}
sum(1, 2);
나머지 매개변수는 일반 매개변수와도 함께 사용할 수 있다. 하지만 이 경우 반드시 마지막 매개변수만 나머지 매개변수가 될 수 있다.
function sum(type, ...args) {
args.forEach(function (arg) {
// ...
});
}
sum("money", 1, 2); // money
화살표 함수
화살표 함수는 ES2015에서 등장한 문법으로 기존의 함수 표현식에 비해 간결하게 함수를 정의할 수 있다. 화살표 함수는 항상 익명 함수이며, 다음과 같은 문법 특징이 있다.
- function 키워드를 생략한다.
- 매개변수가 하나인 경우 괄호를 생략할 수 있다.
- 함수 몸체에서 문이 하나인 경우 중괄호({})나 return 키워드를 생략할 수 있다.
// function 키워드 생략
const greeting1 = () => {
return "hello";
};
// 매개변수가 name 하나이기 때문에 괄호 생략
const greeting2 = (name) => {
return `hello ${name}`;
};
// 함수 몸체 내용이 return 문 하나이기 때문에 중괄호와 return 키워드를 생략
const greeting3 = (name) => `hello ${name}`;
화살표 함수의 간결함은 배열의 map()
과 filter()
메서드처럼 함수를 인자로 받는 경우 매우 유용하다.
arr.map(function (element, index) {
return `${index}: ${element}`;
});
// 화살표 함수를 사용하면 익명 함수를 훨씬 간결하게 전달할 수 있다.
arr.map((element, index) => `${index}: ${element}`);
화살표 함수의 특징
화살표 함수는 일반 함수와 다른 점 두 가지가 더 존재한다. 화살표 함수는 arguments 객체와 this를 바인딩하지 않기 때문에 기존 함수와는 다르게 동작한다.
const func = () => {
arguments[0];
};
func(1); // Uncaught ReferenceError: arguments is not defined
화살표 함수에서는 나머지 매개변수를 사용하여 arguments 객체를 대체할 수 있다.
일반적으로 변수와 변수에 관련된 프로퍼티를 연관시키고 값을 설정하는 것을 바인딩이라고 한다. 자바스크립트에서는 함수 호출 시 값이 결정되는 arguments 객체나 this에 바인딩된다는 표현을 자주 사용한다.
const func = (...args) => args[0];
func(1); // 1
this
자바스크립트의 함수에서는 this라는 특별한 키워드를 사용할 수 있다. this는 읽기 전용 값으로 런타임 시 설정할 수 없으며 함수를 호출한 방법에 의해 값이 달라진다. 자바스크립트의 this 바인딩은 특히 헷갈리는 개념이므로 어떻게 동작하는지 명확하게 이해해야 한다.
일반 함수
전역 실행 컨텍스트에서의 this는 항상 전역 객체를 참조한다. 전역 실행 컨텍스트는 자바스크립트 엔진이 코드를 실행할 때 처음으로 생성되는 컨텍스트이다. 자바스크립트 코드가 실행되는 최상위 환경이라고 할 수 있다.
전역 객체는 자바스크립트를 실행하는 환경마다 다르다. 브라우저 환경에서 자바스크립트를 실행한다면 window 객체가 전역 객체가 되며, Node.js 환경에서 실행하면 global 객체가 전역 객체가 된다.
function func() {
console.log(this === window); // true
}
func();
func()
함수 호출시에도 마찬가지로 this의 값이 전역 객체를 참조한다. 하지만 this는 window 전역 객체를 참조하는 것이 아니라 undefined로 남아 있어야 한다. window.func()
처럼 메서드 호출이 아닌 func()
함수를 직접 호출하여 함수의 컨텍스트가 어디에 속하는지 알 수 없기 때문이다.
하지만 this가 바인딩되지 않은 경우 기본 값으로 전역 객체를 참조하기 때문에 예제 코드와 같은 결과가 나온다. 이 문제는 자바스크립트의 엄격 모드를 사용하여 해결할 수 있다.
자바스크립트의 엄격 모드는 ES5에서 등장하였으며, 언어의 중요한 문제점을 수정하고 엄격하게 오류를 검사하여 안전한 자바스크립트 작성을 도와준다. ‘use strict’ 지시문을 함수 본문 최상단에 넣어 엄격 모드를 활성화할 수 있다.
function func() {
"use strict";
console.log(this === window); // false
console.log(this === undefined); // true
}
func();
엄격 모드에서는 this의 값이 window 전역 객체를 참조하지 않고 undefined로 올바르게 남아있는 것을 알 수 있다.
엄격 모드
: 엄격모드에서는 예시에서 설명한 것 이외에도 잠재적인 오류 방지를 위한 다양한 규칙들이 있다."use strict"; a = 1; // Uncaught ReferenceError: a is not defined
위 코드는 엄격 모드에서 ReferenceError가 발생한다. 엄격 모드가 아닌 경우 자바스크립트는 아무런 에러 없이
var a = 1
을 실행한 것처럼 전역 변수를 선언하여 값을 할당한다. 이러한 잠재적인 오류를 유발할 수 있는 코드는 엄격 모드에서 허용되지 않는다.
생성자 함수
new 키워드를 사용하여 함수를 호출하면 생성자 함수로 동작한다. 생성자 함수의 this 바인딩은 일반 함수 호출과는 다르게 동작한다.
function Vehicle(type) {
this.type = type;
}
const car = new Vehicle("Car");
Vehicle()
생성자 함수를 호출하여 객체를 생성했다. 생성자 함수를 호출하여 객체가 생성될 때 아래와 같은 단계로 동작한다.
- 객체를 생성하여 this에 바인딩
생성자 함수 내의 코드를 실행하기 전에 객체를 만들어 this에 바인딩한다. 생성된 객체는 생성자 함수의 prototype 프로퍼티에 해당하는 객체를 프로토타입으로 설정한다. 또한 이 객체는 이후 단계에서 this를 통해 계속 참조된다. - 프로퍼티 생성
this에 바인딩한 객체에 프로퍼티를 생성한다.
function Vehicle(type) {
this.type = type; // 프로퍼티 생성
}
- 객체 반환 생성된 객체, this에 바인딩한 객체를 반환한다. 또한 반환 값을 따로 명시하지 않아도 this에 바인딩한 객체가 반환된다. 다만, this가 아닌 다른 반환값을 명시적으로 지정하였다면 this가 아닌 해당 값이 반환된다.
function Vehicle(type) {
this.type = type;
return this; // 이 부분을 생략해도 this에 바인딩한 객체가 반환된다.
}
메서드
자바스크립트에서는 객체의 프로퍼티인 함수를 일반 함수와 구분하여 메서드라고 부르며, this 바인딩도 일반 함수와는 다르게 동작한다. 메서드를 호출하면 this는 해당 메서드를 소유하는 객체로 바인딩된다.
const obj = {
lang: "javascript",
greeting() {
// this가 obj 객체로 바인딩된다.
console.log(this);
return `hello ${this.lang}`;
},
};
console.log(obj.greeting()); // 'hello javascript'
여기서 중요한 것은 메서드를 어떻게 호출했느냐에 따라 this 바인딩이 달라진다는 것이다.
const obj = {
lang: "javascript",
greeting() {
return `hello ${this.lang}`;
},
};
const greeting = obj.greeting;
console.log(greeting()); // 'hello undefined'
위와 같이 메서드를 호출한다면 일반 함수를 호출했을 때와 동일하게 함수의 컨텍스트가 어디에 속하는지 알 수 없다. 이 경우 this는 전역 객체를 참조하거나 엄격 모드인 경우 undefined로 남아 있게 된다. 메서드를 의도한대로 사용하기 위해서는 반드시 해당 객체의 컨텍스트(obj)로 명확하게 지정하여 호출해야 한다.
call()
, apply()
, bind()
함수의 호출 방법과 상관없이 this를 특정한 객체로 바인딩할 수 없을까? 자바스크립트에서는 함수의 내장 메서드인 call()
, apply()
그리고 bind()
메서드를 이용하여 this로 바인딩될 객체를 변경할 수 있다. 이러한 방법을 명시적 바인딩이라고 부른다.
call()
, apply()
call()
, apply()
메서드는 어떤 함수를 다른 객체의 메서드처럼 호출할 수 있게 한다. 두 메서드는 넘겨받는 인자의 형식만 다를 뿐, this를 특정 객체에 바인딩하여 함수를 호출하는 역할을 한다.
const obj = { name: "javascript" };
function greeting() {
return `Hello ${this.name}`;
}
console.log(greeting.call(obj)); // 'Hello javascript'
call()
메서드는 첫 번째 인자로 this로 바인딩할 객체를 지정한다. obj 객체를 call()
메서드의 첫 번째 인자로 넘겨 this로 바인딩했다. greeting()
함수는 obj의 메서드처럼 사용할 수 있기 때문에 this를 통해 obj의 프로퍼티인 name에 접근할 수 있게 된다.
call()
메서드를 통해 호출하는 함수로 인자를 전달할 수도 있다. call()
메서드의 첫 번째 인자 이후의 인자들은 모두 호출하는 함수로 전달된다.
const obj = { name: "Lee" };
function getUserInfo(age, country) {
return `name: ${this.name}, age: ${age}, country: ${country}`;
}
console.log(getUserInfo.call(obj, 20, "Korea")); // 'name: Lee, age: 20, country: Korea'
apply()
메서드는 call()
메서드와 동일하지만 호출하는 함수에 전달할 인자들을 배열 형태로 전달해야 한다.
const obj = { name: "Lee" };
function getUserInfo(age, country) {
return `name: ${this.name}, age: ${age}, country: ${country}`;
}
console.log(getUserInfo.apply(obj, [20, "Korea"])); // 'name: Lee, age: 20, country: Korea'
bind()
bind()
메서드는 함수의 this 바인딩을 영구적으로 변경한다. (생성자 함수로 사용되는 경우는 예외이다.)bind()
메서드로 this가 변경된 함수는call()
,apply()
또는 다른bind()
메서드를 사용해도 this 바인딩을 변경할 수 없다.- this를 바인딩하여 함수를 호출하는 것이 아니라 새로운 함수를 반환한다.
bind()
메서드는 함수가 어디서 어떻게 호출되는지에 상관없이 this의 값을 고정하고 싶을 때 사용한다.
const obj1 = { name: "Lee" };
const obj2 = { name: "Han" };
function getUserInfo(age, country) {
return `name: ${this.name}, age: ${age}, country: ${country}`;
}
const bound = getUserInfo.bind(obj1);
console.log(bound(20, "Korea")); // 'name: Lee, age: 20, country: Korea'
console.log(bound.apply(obj2, [20, "Korea"])); // 'name: Lee, age: 20, country: Korea'
bound()
함수는 this 값이 영구적으로 고정되었기 때문에 전역 컨텍스트에서 호출하든 apply()
메서드를 사용하여 호출하든 this의 값이 obj1로 고정된다.
bind()
메서드는 this 뿐만 아니라 함수에 전달할 인자도 고정시킬 수 있다. 만약 getUserInfo()
함수로 전달되는 age 인자를 항상 20으로 고정하고 싶다면 아래처럼 변경할 수 있다.
const obj1 = { name: "Lee" };
function getUserInfo(age, country) {
return `name: ${this.name}, age: ${age}, country: ${country}`;
}
// getUserInfo 함수에 전달할 age 인자값을 20으로 고정시켰다.
const bound = getUserInfo.bind(obj1, 20);
console.log(bound("Korea")); // 'name: Lee, age: 20, country: Korea'
bind()
메서드는 함수의 동작을 영구적으로 변경하므로 함수의 this가 어떻게 바인딩되는지 잘 파악하고 사용해야 한다. 특히 call()
, apply()
메서드를 사용하여 this를 변경해야 하는 경우 bind()
메서드로 this를 고정한다면 문제가 될 수 있으니 주의해야 한다.
화살표 함수와 렉시컬 this
이 전에 언급한 바와 같이 화살표 함수는 this를 바인딩하지 않는다. 화살표 함수는 this를 바인딩하지 않기 때문에 지금까지 설명한 this 바인딩의 규칙과는 완전히 다르게 동작한다.
렉시컬 this
함수 호출에 따라 동적으로 this를 바인딩하는 것이 아니라 함수를 어디에 선언하는지에 따라 this의 값이 결정된다. 화살표 함수의 this는 화살표 함수를 둘러싸고 있는 렉시컬 스코프에서 this의 값을 받아 사용한다. 이러한 this를 렉시컬 this라고 하며 이 값은 변경되지 않는다.
렉시컬 스코프
?
렉시컬 스코프는 자바스크립트 엔진이 변수를 찾는 검색 방식에 대한 규칙이며, 함수를 어디에 선언했는지에 따라 결정된다.
const obj = {
lang: "javascript",
greeting: () => {
return `hello ${this.lang}`;
},
};
console.log(obj.greeting()); // 'hello undefined'
greeting()
메서드를 화살표 함수로 정의했다. 객체의 메서드 형태인 obj.greeting()
으로 컨텍스트를 지정하여 호출하였음에도 불구하고 this를 통해 lang 프로퍼티에 접근하지 못한다. 이는 화살표 함수의 렉시컬 this가 obj가 아닌 obj를 둘러싸고 있는 전역 컨텍스트에서 값을 받아오기 때문이다.
화살표 함수의 특징
화살표 함수는 this를 따로 바인딩하지 않고 변경되지 않는 렉시컬 this를 갖는다. 이러한 특징 때문에 화살표 함수의 this는 call()
, apply()
, bind()
함수를 사용하여 변경할 수 없다.
const obj = { lang: "javascript" };
const greeting = () => {
return `hello ${this.lang}`;
};
// call() 메서드를 사용해도 화살표 함수의 this는 변경되지 않고 전역 객체를 가리킨다.
console.log(greeting.call(obj)); // 'hello undefined'
또한 화살표 함수는 생성자 함수로도 사용할 수 없다. 생성자 함수는 객체를 생성하여 this에 바인딩하지만, 화살표 함수는 렉시컬 this의 특징 때문에 이러한 동작을 할 수 없기 때문이다.
화살표 함수는 정적인 렉시컬 this를 사용하기 때문에 기존의 동적인 this 바인딩의 혼잡함에서 벗어나 단순하게 사용할 수 있다. 특히 setTimeout()
과 같은 전역 객체의 함수를 메서드에서 호출할 경우 화살표 함수를 사용하면 더욱 명확하고 간결하게 표현할 수 있다.
const obj = {
name: "javascript",
greeting() {
setTimeout(
function timer() {
console.log(this.name);
}.bind(this),
1000
);
},
};
setTimeout()
함수 내에서 실행되는 콜백 함수의 this는 전역 객체로 바인딩된다. 하지만 예제 코드에서는 bind()
메서드를 사용하여 this를 obj 객체로 변경한 새로운 함수를 사용했다.
const obj = {
name: "javascript",
greeting() {
setTimeout(() => {
console.log(this.name);
}, 1000);
},
};
화살표 함수는 자신을 둘러싸고 있는 렉시컬 스코프인 obj를 렉시컬 this로 참조하기 때문에 bind()
메서드를 사용하지 않아도 this를 obj 객체로 바인딩할 수 있다.
DOM에 이벤트를 추가하는 addEventListener()
함수에서는 화살표 함수를 주의해서 사용해야 한다.
block.addEventListener("click", function () {
// this는 block 객체에 바인딩된다.
console.log(this.id);
});
block.addEventListener("click", () => {
// this는 전역 객체에 바인딩된다.
console.log(this.id);
});
일반 함수를 addEventListener()
함수의 콜백으로 넘긴 경우 this는 event.currentTarget 프로퍼티와 동일한 값을 가진다. 하지만 화살표 함수를 사용하는 경우 렉시컬 this로 동작이 완전히 달라지기 때문에 this를 통해 요소에 접근할 수 없다.
<참고 : https://velog.io/@bami/Javascript-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EC%9D%98-%ED%95%A8%EC%88%98-2-Function-%EC%83%9D%EC%84%B1%EC%9E%90 >