new
키워드를 붙이지 않고 Stirng()
함수를 호출한다.
ToString 추상 연산 규칙에 따라 변환된다.
ToString 추상 연산 규칙은 다음과 같다.
undefined : 'undefined'
null : 'null'
boolean : true이면 ➡ 'true', false이면 ➡ 'false'
number : Number.toString()
의 결과 반환 ( 3 ➡ '3' )
Symbol : ❌ 타입 에러 발생
심볼 연산을 통해 문자열 또는 숫자로 변환될 경우 타입 에러가 발생한다.
연산을 통한 타입 변환은 암묵적 타입 변환으로써 ToString 추상 연산 규칙에 따르기 때문이다.
(예시)myObj['abc' + Symbol('key')] // TypeError
이는 심볼이 변환되어 객체의 다른 프로퍼티에 접근하는 것을 방지하기 위해서이다.
하지만 심볼 타입은 자체적으로 `toString()` 메서드가 정의되어있다. 이는 ToString 추상 연산을 따르지 않으니 심볼을 문자열로 타입 변환하고자 할때는 `String()`이나 `toString()` 메서드를 직접 호출해서 명시적으로 타입 변환을 해야 한다.myObj.abckey = 1; // abckey 라는 프로퍼티가 있다고 할때 myObj['abc' + Symbol('key')] = 2; // 만약 에러가 나지 않는다면 abckey값이 변경된다.
BigInt : BigInt.toString()
의 결과 반환 ( 3n ➡ '3n' )
object : ToString 추상 연산을 수행한 값을 반환
객체를 원시 타입으로 변환하는 과정을 정확히는 ToPrimitive 추상 연산이라고 한다.
또 다른 문자열 변환 방법은 toString()
메서드를 사용하는 것이다.
String()
은 함수이고 toString()
은 메서드이기 때문에
String()
은 언제든 호출 가능하지만 toString()
은 메서드로서만 호출 가능하다.
즉, 이 말은 null
타입과 undefined
타입은 원시 타입이라 toString()
메소드를 가질 수 없다.
때문에 타입 변환을 할 땐 toString()
메서드 보단 String()
함수를 사용하는 것이 더 적합하다.
원래 원시 타입은 메서드를 가질 수 없다. 문자열, 숫자, 불리언 원시 타입은 랩퍼 객체를 통해 메서드사용이 가능하고, 심볼과 BigInt는 자체적으로 구현된
toString()
메서드를 가지고 있다. 하지만null
과undefined
는 그렇지가 않다.
Number()
함수를 호출하여 변환한다.
BigInt 타입을 제외한 나머지 타입들은 ToNumber 추상 연산 명세를 기준으로 변환된다.
undefined : NaN
null : 0
string : 숫자로 변경이 불가능한 값 ➡ NaN, 숫자형 문자열 ➡ 숫자
boolean : true ➡ 1, false ➡ 0
Symbol : ❌ 타입 에러 발생
BigInt : ❌ 타입 에러 발생
BigInt 타입은 숫자 타입과는 다르게 큰 정수의 연산을 위해 나온 타입이기 때문에 숫자와는 타입이 구분되어야 한다.
object : 객체를 원시 타입으로 변환한 후 다시 한번 ToNumber 추상 연산을 수행한 값을 반환
parseInt()
함수를 이용해서 숫자로 타입 변환할 수도 있다.
하지만 parseInt()
함수는 문자열만을 숫자로 변환할 수 있으며 첫 번째 인자로 문자열이 아닌 다른 타입의 값을 전달받으면 ToString 추상 연산 과정을 거친 후 문자열로 만든 뒤에 숫자로 변환한다.
두 번째 인자로는 기수를 전달 받는다.
기수를 생략하면 첫 번째 인자를 기준으로 자동 추정하여 변환하므로 의도치 않은 결과가 나올 수 있다.
버그를 만들고 싶지않다면 기수를 생략하지 않도록 한다.
Boolean()
함수를 이용해 변환한다.
Boolean도 ToBoolean 추상 연산에 따라 타입을 변환한다.
truthy와 falsy값도 ToBoolean 추상 연산의 규칙을 그대로 따른 것이다.
!
연산자를 이용해 변환하기도 한다.
객체의 원시 타입 변환은 문자열로 변환, 숫자로 변환 두 가지로 나눌 수 있다.
이 과정에서 valueOf()
와 toString()
메서드가 중요한 역할을 한다.
객체가 문자열로 변환되는 과정은 아래와 같은 단계로 진행된다.
객체에 정의된 toString()
메서드를 호출한다.
메서드가 없다면 Object.prototype.toString()
메서드를 실행한다.
1단계의 결과가 원시 타입이라면 그 결과를 문자열로 변환하여 반환한다.
그렇지 않다면 valueOf()
메서드를 호출한다.
valueOf()
메서드 역시 없다면 Object.prototype.valueOf()
메서드를 실행한다.
valueOf()
메서드의 결과 값이 원시 타입이라면 그 결과를 문자열로 변환하여 반환하고, 그렇지 않다면 타입에러가 발생한다.
객체가 숫자로 변환되는 과정은 아래와 같은 단계로 진행된다.
객체에 정의된 valueOf()
메서드를 호출한다.
메서드가 없다면 Object.prototype.valueOf()
메서드를 실행한다.
1단계의 결과가 원시 타입이라면 그 결과를 숫자로 변환하여 반환한다.
그렇지 않다면 toString()
메서드를 호출한다.
toString()
메서드 역시 없다면 Object.prototype.toString()
메서드를 실행한다.
toString()
메서드의 결과 값이 원시 타입이라면 그 결과를 숫자로 변환하여 반환하고, 그렇지 않다면 타입에러가 발생한다.
const obj = {
valueOf() {
return 1;
},
toString(){
return "문자열";
}
};
console.log(Number(obj)); // 1
console.log(String(obj)); // 문자열
암시적 강제 변환은 연산 중에 내부적으로 타입을 변환하는 것이다.
숫자 + 문자열 ➡ 문자열
1 + ""
➡ "1"
숫자 + 객체 ➡ 문자열
1 + {}
➡ "1\[object Object\]"
숫자 + 불리언 ➡ 숫자
1 + true
➡ 2
문자열 == 숫자 ➡ 숫자
1 == "1"
➡ 1 == 1
➡ true
문자열 == BigInt ➡ BigInt
1n == "1"
➡ 1n == 1n
➡ true
null == undefined ➡ 동등하게 판단
null == undefined
➡ true
null
과undefined
가 동등하게 판단되기 때문에 아래와 같은 코드도 가능하다.function isEmpty(value){ if (value == null) return true; // value가 null일때 뿐만 아니라 undefined일때도 잡는다. return false; }
불리언 == 숫자 ➡ 숫자
true == 1
➡ 1 == 1
➡ true
객체 == 문자열 | 숫자 | BigInt | 심볼 ➡ 원시 타입
{} == "[object Object]"
➡ "[object Object]" == "[object Object]"
➡ true
숫자 == BigInt ➡ 내부적인 숫자 비교 알고리즘에 따라 결과를 반환
1 == 1n
➡ true
위에서 설명한 변환 규칙을 충실히 따라도 확실히 가독성이 떨어진다.
이런 경우에는 엄격한 동등 연산자 ( ===
) 를 사용하는것이 훨씬 좋다.
비교 연산자는 크게 문자열 데이터의 비교, 그 외의 경우 두 가지로 나누어져 있다.
다만, 피연산자가 객체인 경우 먼저 객체를 원시 타입으로 변환한 후 비교한다.
문자열 비교는 각 문자를 알파벳 순서로 비교한다.
왼쪽에서부터 문자 단위로 비교한다.
'1' < '04' // false, "1"과 "0"을 먼저 비교하고 그 다음 문자를 비교한다.
['a'] < ['b'] // true, 각각 "a" 와 "b"로 타입변환 후에 비교한다.
문자열 < BigInt ➡ BigInt
"1" < 2n
➡ 1n < 2n
➡ true
피연산자를 모두 숫자로 변환하여 비교
1 < true
➡ 1 < 1
➡ false
BigInt와 숫자의 비교는 내부적인 숫자 비교 알고리즘에 의해 비교를 수행한다.
1n < 2
➡ true
비교 연산은 동등 연산과 달리 엄격한 비교 연산자 같은 연산자는 존재하지 않는다.
즉, 다른 타입 간의 비교 연산에서 암시적인 강제 변환을 막을 수 없다.
만약 서로 다른 타입을 대상으로 비교 연산을 사용해야 한다면, 명시적 강제 변환을 통해 변환한 후 사용하는 것이 안전하다.
조건 표현식에서 암시적 강제 변환은 아주 흔하게 사용된다.
모든 값은 불리언 타입으로 변환되어 조건 표현식에서 평가된다.
자바스크립트의 논리 연산자는 특이한 점이 있다.
논리연산자의 결과 값이 불리언 타입이 아닐 수도 있다.
보통 단축 평가로 사용된다.
function setDefault(value){
return value || "Default Value"; // 빈 값이면 기본 값을 반환
}
const obj = { title: "타이틀" };
console.log(obj && obj.title) // 객체가 있다면 객체의 속성을 반환
falsy 값이 아닌 null, undefined 처럼 값이 비어 있는 경우에만 디폴트 값을 설정하고 싶을때는 ||
대신 nullish coalescing을 사용한다.
const str = undefined ?? "Default Value"; // "Default Value"
const num = 0 ?? 1 // 0
자바스크립트의 함수는 일급 함수 ( first-class function ) 로서 다른 함수의 매개변수나 반환 값으로도 사용할 수 있다.
다른 함수의 인자로 넘어가는 함수를 콜백 함수라고 부르며 이 패턴은 아주 많이 쓰인다.
일급 함수는 아래와 같은 조건을 만족한다.
- 변수에 함수를 할당할 수 있다.
- 함수의 인자로 전달할 수 있다.
- 함수를 반환 값으로 사용할 수 있다.
함수를 생성하는 방법은 3가지가 있다.
함수 선언문, 함수 표현식, Function 생성자 함수.
이 중 Function 생성자 함수는 보안 및 성능 문제가 있어 권장되지 않는다.
function func(a, b){
reutn a+b;
}
함수 선언문은 호이스팅 ( hoisting ) 으로 인해 함수가 선언된 위치에서 코드의 최상단으로 끌어올려 진다.
따라서 이 함수는 선언된 위치보다 상단에서 호출될 수 있다.
함수 표현식은 함수 선언문과 달리 호이스팅이 되지 않고, 함수의 이름이 선택사항이다.
const func1 = function(a, b){ return a+b } // 함수 이름이 없다.
const func2 = function doSum(a, b){ return a+b } // 함수 이름이 있다.
함수 이름은 외부에서 접근이 불가능하고 함수 몸체에서만 접근이 가능하므로 주로 함수 표현식을 재귀적으로 호출할때 사용한다.
함수의 호출은 표현식이기 때문에 값으로 평가되며, 만약 함수 몸체안에 return 문을 명시적으로 호출하지 않았다면 그 함수의 호출 결과 값은 undefined
가 된다.
함수 호출 시 본래 정의된 매개변수보다 적은 수로 인자를 전달하면 나머지 매개변수는 undefined
로 설정되고 반대로 정의된 매개변수보다 많은 인자를 전달하면 넘치는 인자들은 무시된다.
인자는 함수 호출시 전달되는 값을 의미하며, 매개변수는 함수에서 전달된 인자를 받아들이는 변수를 의미한다.
영문으로 인자는 argument, 매개변수는 parameter로 구분해서 사용한다.
해체 할당과 매개변수 기본값 세팅은 다음과 같다.
const user = { name: "홍길동", age: 20 };
function getUserInfo({ name, age, country = "서울" }) { // 해체 할당과 기본 값 세팅
return `name: ${name}, age: ${age}, country: ${country}`;
}
console.log(getUserInfo(user)); // name: 홍길동, age: 20, country: 서울
만약 객체가 2중으로 되어있다면 해체 할당도 같은 구조로 하면 된다.
const users = {
user1: {name: '홍길동', age: 20},
user2: {name: '김철수', age: 21},
user3: {name: '김영희', age: 22},
}
function getUser2Info({user2:{name, age, country = "서울"}}){
return `name: ${name}, age: ${age}, country: ${country}`;
}
console.log(getUser2Info(users)) // name: 김철수, age: 21, country: 서울
그렇다면 배열은 어떨까
function getUser3Info({ user: [, , { name, age, country = "서울" }] }) {
return `name: ${name}, age: ${age}, country: ${country}`;
}
const users = {
user: [
{ name: "홍길동", age: 20 },
{ name: "김철수", age: 21 },
{ name: "김영희", age: 22 }
]
};
console.log(getUser3Info(users)); // name: 김영희, age: 22, country: 서울
화살표 함수를 제외한 모든 함수에서는 arguments라는 객체를 사용할 수 있다.
유사 배열 객체이기 때문에 인덱스로 프로퍼티에 접근이 가능하고 length프로퍼티를 가지고 있다.
arguments 객체는 나머지 매개변수로 대체할 수 있다.
function myFun(a, b, ...manyMoreArgs) {
console.log("a", a);
console.log("b", b);
console.log("manyMoreArgs", manyMoreArgs);
}
myFun("one", "two", "three", "four", "five", "six");
// Console Output:
// a, one
// b, two
// manyMoreArgs, ["three", "four", "five", "six"]
나머지 매개변수는 유사 배열 객체가 아닌 찐 배열이다.
화살표 함수는 다음과 같은 특징이 있다.
const func = () => { return "hello"; };
const func = name => { return "hello " + name; };
const func = name => "hello" + name;
화살표 함수는 arguments
객체와 this
를 바인딩하지 않기 때문에 기존 함수와는 다르게 동작한다.
arguments 객체는 나머지 매개변수로 대체할 수 있고
this는 call()
이나 apply()
로 대체 가능하다.
this
는 읽기 전용 값으로 런타임 시 설정할 수 없으며 함수를 호출한 방법에 의해 값이 정해진다.
전역 실행 컨텍스트에서의 this
는 항상 전역 객체를 참조한다.
전역 객체는 자바스크립트를 실행하는 환경마다 다르다.
브라우저 환경에서는window
객체, Node.js 환경에서는global
객체가 전역 객체가 된다.
함수 호출 시에도 this
의 값이 전역 객체를 참조한다.
하지만 window.func()
처럼 메서드 호출이 아닌 func()
함수를 직접 호출하면 함수의 컨텍스트가 어디에 속하는지 알 수 없기 때문에 this
는 window
객체가 아닌 undefined
가 되어야 한다.
'use strict' 지시문을 함수 본문 최상단에 넣어서 엄격 모드를 활성화한다면 해결할 수 있다.
function func1(){
console.log(this === window);
console.log(this === undefined)
}
function func2(){
'use strict';
console.log(this === window);
console.log(this === undefined)
}
func1(); // true false
func2(); // false true
new
키워드를 사용하여 함수를 호출하면 생성자 함수로 동작한다.
생성자 함수의 this 바인딩은 일반 함수 호출과는 다르게 동작한다.
생성자 함수 내의 코드를 실행하기 전에 객체를 만들어 this
에 바인딩한다.
생성된 객체는 생성자 함수의 prototype 프로퍼티에 해당하는 객체를 프로토타입( [[Prototype]] )으로 설정한다.
this
에 바인딩한 객체에 프로퍼티를 생성한다.
생성된 객체, this
에 바인딩한 객체를 반환한다.
또한 return 문을 따로 명시하지 않아도 this
에 바인딩한 객체가 반환된다.
다만, 다른 반환 값을 명시적으로 지정하였다면 this
가 아닌 해당 값이 반환된다.
메서드를 호출하면 this
는 해당 메서드를 소유하는 객체로 바인딩 된다.
const obj = {
lang: 'js',
greeting(){
return `hello ${this.lang}`;
}
}
console.log(obj.greeting()); // hello js
중요한 점은 메서드를 어떻게 호출했느냐에 따라 this 바인딩이 달라진다는 점이다.
const obj = {
lang: 'js',
greeting(){
return `hello ${this.lang}`;
}
}
const greeting = obj.greeting;
console.log(greeting()); // hello undefined
greetng
는 메서드를 참조하기 때문에 호출하면 메서드가 호출된다.
하지만 모양새는 일반 함수를 호출하는 것과 같아서 함수의 컨텍스트가 어디에 속하는지 알 수 없다.
메서드를 의도한 대로 사용하기 위해서는 반드시 해당 객체의 컨텍스트( obj ) 로 명확하게 지정하여 호출해야 한다.
함수 호출 방법과 무관하게 this
를 특정한 객체로 바인딩할 수 있다.
자바스크립트 함수의 내장 메서드인 call()
, apply()
, bind()
를 이용하면 된다.
이러한 방법을 명시적 바인딩 이라 한다.
const obj = {
lang: 'js',
greeting(p1, p2){
return `hello ${this.lang} ${p1} ${p2}`;
}
}
const greeting = obj.greeting;
console.log(greeting.call(obj, "python", "java"));
console.log(greeting.apply(obj, ["python", "java"]));
앞서 살펴본 call()
과 apply()
와는 두 가지 차이점이 있다.
this
바인딩을 영구적으로 변경한다. ( 생성자 함수로 사용되는 경우는 예외 )
bind()
메서드로 this
가 고정된 함수는 call()
, apply()
와 또다른 bind()
메서드로도 변경이 불가능하다.
this
를 바인딩하여 함수를 호출하는 것이 아니라 새로운 함수를 반환한다.
const obj1 = {
name: "홍길동",
age: 20
};
const obj2 = {
name: "김철수",
age: 21
};
function userInfo(country = "서울") {
return `${this.name} ${this.age} ${country}`;
}
// obj1를 this에 바인딩
const bindedUserInfo = userInfo.bind(obj1);
console.log(bindedUserInfo()); // 홍길동 20 서울
// 후에 obj2를 바인딩해도 변경안됨
console.log(bindedUserInfo.call(obj2)); // 홍길동 20 서울
// obj1를 this에 바인딩하고 인자도 "부산" 으로 고정.
// country 매개변수에 자동으로 "부산" 인자가 전달됨
const bindedUserInfo2 = userInfo.bind(obj1, "부산");
console.log(bindedUserInfo2()); // 홍길동 20 부산
// "대전"이라는 인자를 넘겨주면 인자가 "부산", "대전" 2개가 되므로
// "부산"은 country에 바인딩 되고 "대전"은 무시된다.
console.log(bindedUserInfo2("대전")); // 홍길동 20 부산
함수 호출에 따라 동적으로 this
를 바인딩하는 것이 아니라 함수를 어디에 선언하는지에 따라 this
의 값이 결정된다.
화살표 함수의 this
는 화살표 함수를 둘러싸고 있는 렉시컬 스코프에서 this
를 받아 사용한다.
이러한 this
를 렉시컬 this라고 하며 이 값은 변경되지 않는다.
( 심지어 call(), apply(), bind() 메서드를 사용해도 변경되지 않는다. )