자주 사람들 코드를 보다보면 발견되는 화살표 함수를 이해하지 못했는데 이번 시간 덕에 이해 할 수 있게 됐다.
사실 50% 이해했음
ES6 이전까지 함수는 별 다른 구분 없이 다양한 목적으로 사용되었다.
생각해보면 callable 한 함수로도 사용되고 constructor 함수로도 사용되고, 메소드로도 사용되었다.
명확한 구분이 없었기 때문에 객체의 프로퍼티를 동작 시키는 메소드로도 새로운 객체를 만드는게 가능했다.
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log('sayhi~!');
};
}
let tom = new Person('tom');
let jerry = new tom.sayHi();
console.log(jerry); // {}
댕박 어이없음
물론 이렇게 사용하는 일은 없겠지만, 이런 것이 문법적으로 가능하단 것이 문제가 있다.
이를 해결하기 위해 ES6
에서는 함수를 사용 목적에 다라 세 가지 종류로 명확하게 구분했다.
ES6함수의 구분 | constructor | prototype | super | arguments |
---|---|---|---|---|
일반함수(normal) | O | O | X | O |
메소드(method) | X | X | O | O |
화살표함수(Arrow) | X | X | X | X |
추후 깊게 파보겠지만 일반함수의 경우에는 상위 객체의 것을 가져올 필요가 없기 때문에 super
를 사용 할 수 없고
메소드는 생성자로 사용 할 수 없게 하기 위해 constructor , prototytpe
객체를 가지지 않는다.
화살표 함수는 .. 흠 .. 이따 살펴보자
ES6 사양에서 메소드는 메소드 축약 표현으로 정의된 함수만을 의미한다.
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`hi i am ${this.name}`);
}
}
클래스 단원에서 메소드 축약 표현으로 사용해야 한다고 했던 이유는 내부 슬롯으로 [[Homeobject]]
를 가져야 부모 클래스의 메소드나 프로퍼티를 참조 할 수 있기 때문이라고 했었다.
만약 메소드 축약 표현이 아닌 함수 표현식으로 사용하면
class Person {
address = 'korea';
constructor(name) {
this.name = name;
}
sayHi() {
return `hi i am ${this.name}`;
}
}
class Korean extends Person {
introduce = function () {
console.log(`${super.sayHi()} and i lived in ${this.address}`);
};
}
let leedongdong = new Korean('leedongdong');
leedongdong.introduce(); // SyntaxError: 'super' keyword unexpected here
에러가 발생한다. 그 이유는 함수 표현식은 [[HomeObject]]
를 가지지 않아 super
를 사용해도 상위 객체를 참조하지 못한다.
class Person {
address = 'korea';
constructor(name) {
this.name = name;
}
sayHi() {
return `hi i am ${this.name}`;
}
}
class Korean extends Person {
introduce() {
console.log(`${super.sayHi()} and i lived in ${this.address}`);
}
}
let leedongdong = new Korean('leedongdong');
leedongdong.introduce(); // hi i am leedongdong and i lived in korea
Person
을 상속 받은 Korean
의 메소드인 introduce
는 [[HomeObject]]
로 Korean
을 가져 Korean
의 프로토타입 체인을 따라 Person
에 접근하여 Person
의 메소드인 sayHi
를 사용 할 수 있었다.
또한 ES6
에서의 메소드는 constructor , prototytpe
을 갖지 않아 생성자 함수로 사용 할 수 없다.
let kimdongdong = new leedongdong.introduce();
// TypeError: leedongdong.introduce is not a constructor
이처럼 ES6
에서 정의한 메소드를 이용하면 더욱 메소드의 정의에 맞게 적절하게 함수를 사용 할 수 있다.
메소드 정리
ES6
에서 정의한 메소드는 메소드 축약 표현으로 생성 된 것만 의미한다.
메소드는 새로운 인스턴스를 생성 할 수 없으며 상위 객체의 상속이 가능하다.
아 이거 왤캐 많고 어렵냐
화살표 함수는 function
키워드 대신 화살표 (=>
) 를 사용하여 기존 함수 정의 방식 보다 간략하게 함수를 정의 할 수 있다.
화살표 함수에 대한 내용을 자세히 살펴보기 전에 화살표 함수가 필요한 이유에 대해서 살펴보자
화살표 함수는 자신을 둘러싼 스코프에서 가져오기 때문에 함수를 정의한 시점에서 컨텍스트를 기억한다.
이를 통해 일반 함수의 this
는 호출되는 시점에 따라 정의 되었다면 화살표 함수의 this
는 선언된 당시에 이미 정의된다.
간략하게 설명 가능하다. 이는 화살표 함수가 주로 사용되는 곳이 콜백 함수로서 사용 될 때 가독성을 향상 시킨다
콜백 함수
다른 함수의 인수로 전달 되어 호출한 함수의 실행이 완료 된 후 호출되는 함수
메소드를 정의 할 때 더 간결한 구문을 제공한다.
화살표 함수는 prototytpe
을 가지지 않아 인스턴스를 생성하지 않는다. 이를 통해 일반 함수와 명확한 차이점을 갖는 특징을 갖는다.
화살표 함수는 다음처럼 사용한다.
const add = (a, b) => {
return a + b;
};
console.log(add(1, 2)); // 3
화살표 함수는 함수 표현식 형태로 사용해야 한다.
()
안에는 매개 변수가 들어가고 {}
안에는 함수 몸통에 들어갈 로직이 들어간다.
매개변수가 하나 일 경우엔 ()
를 생략 할 수 있다.
매개 변수가 존재하지 않을 경우엔 ()
는 생략 할 수 없다.
{}
안에서 문이 한 줄 이내로 끝난다면 {}
를 생략 할 수 있으며, 반환하고자 하는 값이 값으로 평가되는 표현식일 경우 return
을 생략 할 수 있다.
위 함수 표현식을 간략하게 사용하면
const add = (a, b) => a + b;
console.log(add(1, 2)); // 3
와우 굿 ㅋㅋ
만약 문이 두 줄 이상이라면 {}
를 사용해야 한다.
반환하고자 하는 값이 객체라면 반환 값을 {}
로 감싸줘야 한다.
const makeIdAndFullName = (id, fullname) => {
// 여러 줄의 코드가 들어갈 수 있음
const formattedId = id.toUpperCase();
const formattedFullName = fullname.charAt(0).toUpperCase() + fullname.slice(1);
// 객체를 반환할 때는 소괄호로 감싸준다
return {
id: formattedId,
fullname: formattedFullName,
};
};
const user = makeIdAndFullName('john123', 'john doe');
console.log(user);
// { id: 'JOHN123', fullname: 'John doe' }
만약 감싸지 않으면 함수 몸체를 감싸는 중괄호 {} 로 잘못 해석한다.
화살표 함수도 즉시 실행 함수로 사용 할 수 있다.
const tom = ((name) => ({
sayHi() {
return `hi i am ${name}`;
},
}))('tom');
console.log(tom.sayHi()); // hi i am tom
가장 유용한건 고차 함수에 인수로 전달 할 수 있다는 것이다.
console.log(
arr.map(function (item) {
return item * 2;
}),
); // [2,4,6]
고차 함수에 인수로 함수 선언문을 사용하면 짱못생겼는데
const arr = [1, 2, 3];
console.log(arr.map((item) => item * 2)); // [2,4,6]
와우 굿 ㅋㅋ
말 나온김에 고차함수에 대해서 공부해보자
하나 이상의 함수를 매개 변수로 받을 수 있거나 하나 이상의 함수를 반환 할 수 있는 함수를 고차함수라고 한다.
function multiplier(factor) {
const calcul = (num) => num * factor; // 함수를 반환하는 고차 함수
return calcul;
}
const doubler = multiplier(2);
console.log(doubler(3));
function repeater(iternum) { // 함수를 반환하는 고차 함수
return function (action) { // 함수로 인자로 받는 고차 함수
for (let i = 0; i < iternum; i += 1) {
action();
}
};
}
const sayHello = () => console.log('hello~!'); // 인수가 될 콜백 함수
const tripleIterator = repeater(3);
tripleIterator(sayHello); // hello~! hello~! hello~!
이처럼 화살표 함수는 고차 함수 내에서 가독성 있는 코드를 유지시켜 준다.
const foo = () => {};
console.log(foo.hasOwnProperty('prototype')); // false
화살표 함수는 prototytpe
프로퍼티를 가지지 않기 때문에 생성자 함수로 사용 할 수 없다.
일반 함수는 에러 없이 시행된다.
function add(a, a) {
return a + a;
}
console.log(add(1, 2)); // 4
// 두 번째로 설정된 매개변수 a 가 첫번째로 선언된 a 의 값을 덮어버려 2 + 2 가 되엇다.
하지만 화살표 함수는 중복된 매개 변수 명을 사용하면 에러가 발생한다.
const add = (a,a) => return a+a
// SyntaxError: Duplicate parameter name not allowed in this context
this
는 선언될 때 결정된다.일반적으로 함수나 메소드에서의 this
는 호출 될 때 결정된다.
globalThis.x = 1; // 전역 객체
function foo() {
console.log(this.x); // 전역 객체를 참조
}
const obj = {
x: 2,
foo: foo, // 객체의 x 값 참조
};
foo(); // 1
obj.foo(); // 2
동일한 함수라고 할지라도 일반 함수로 호출 될 때의 this
는 전역 객체를 , 객체의 메소드로 바인딩 된 함수의 this
는 객체를 참조했다.
이처럼 this
는 동적으로 바인딩 되는 값이였지만 화살표 함수의 this
는 선언 될 때 결정된다.
이 때 화살표 함수는 주로 고차 함수의 인수로 전달되는 경우가 많은데 이 때 발생하는 오류들을 방지하기 위해 화살표 함수의 this
는 선언 될 때 결정된다.
this
바인딩이 가져오는 문제는 뭘까 ?내가 만약 김씨 가문 행사에 이름을 적는 아르바이트를 했는데 실수로 사람들의 성을 모두 안적었다고 생각해보자
그래서 사람들의 이름만 적힌 배열이 있을 때 김 이라는 성함을 붙이고자 했다고 해보자
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
return arr.map(function (item) {
return this.prefix + item;
});
}
}
const prefixer = new Prefixer('김');
이렇게 하였을 때 prefixer
는 {prefix : 김}
이라는 인스턴스이며 add(arr)
는 prefixer
의 프로토타입 메소드로 들어오는 배열에 모두 this.prefix
를 더해주기 때문에 모두 김씨가 붙은 배열이 나오기를 기대한다.
console.log(prefixer.add(['길동', '동구']));
// TypeError: Cannot read properties of undefined (reading 'prefix')
엥 에러가 발생한다.
분명 메소드 내부에서 정의된 함수는 생성되는 인스턴스를 가리키니까 this
는 perfixer
를 가리키고 있을거라고 기대하던 것과 완전 딴판이다.
그 이유는 다음과 같다.
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
/**
이 안에서의 this 는 add 안에서 불러졌기 때문에 메소드의 this 바인딩 방식을 따른다.
그렇기 때문에 this 는 생성될 인스턴스를 가리킨다.
*/
return arr.map(function (item) {
/**
이구역에서는 Array.prototytpe.map 에 의해서 호출되었기 때문에
일반 함수의 성격을 갖는다.
일반 함수의 this 는 전역 객체를 가리킨다.
(이 안은 클래스이기 때문에 stric mode 로 인해 undefined 를 가리킨다.)
/
return this.prefix + item;
});
}
}
const prefixer = new Prefixer('김');
고차 함수는 일반 함수를 인수로 받는 것이기 때문에 arr.map(function(item){...})
에서 선언된 함수는 일반 함수이다.
일반 함수로 호출 된 function(item){...}
은 일반 함수이기 때문에 전역 객체를 가리킨다.
클래스 내부에서 호출된 일반 함수는 stricmode 로 인해 undefined 를 가리킨다
globalThis.x = 1; function Person() { this.x = 2; this.foo = (function () { return this.x; })(); } const tom = new Person(); console.log(tom.foo); // 1
위 코드에서
Person
의foo
값에서 즉시 실행 함수로 일반 함수 호출 방식을 이용해this.x
값에 접근했다.일반적으로 메소드로 정의된
function
은 인스턴스를 가리키겠지만 일반 함수로서 호출된 경우의this
는 전역 객체를 가리킨다.그렇기 때문에 전역 객체의 프로퍼티인 1이 나타나는 모습을 볼 수 있다.
이러한 문제가 발생 하는 이유는 일반적인 함수들의 this
의 동적 바인딩 방식에 의해서다.
이런 문제를 화살표 함수가 나타나기 전에는 매우 못생긴 방식으로 해결했다.
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
const that = this; // 인스턴스와 바인딩된 this 를 that 이란 변수에 할당
return arr.map(function (item) {
return that.prefix + item;
});
}
}
const prefixer = new Prefixer('김');
console.log(prefixer.add(['길동', '동구'])); // [ '김길동', '김동구' ]
미리 바인딩 된 this
를 다른 변수에 할당 시켜 회피해버리거나
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
return arr.map(
function (item) {
return this.prefix + item;
}.bind(this), // function(item) 에 사용될 this 를 add(arr) 의 this 로 바인딩
);
}
}
const prefixer = new Prefixer('김');
console.log(prefixer.add(['길동', '동구']));
이런식으로 선언된 함수의 this 를 바인딩 시켜주는 방법이 존재했다.
class Prefixer {
constructor(prefix) {
this.prefix = prefix;
}
add(arr) {
return arr.map((item) => this.prefix + item);
}
}
const prefixer = new Prefixer('김');
console.log(prefixer.add(['길동', '동구'])); // [ '김길동', '김동구' ]
this
바인딩이 인스턴스
와 제대로 바인딩 된 모습을 볼 수 있다.
와우
그 이유는 화살표 함수가 본인만의 this
를 가지지 않고, 선언된 당시의 렉시컬 스코프를 가리키기 때문이다.
일반 함수인 function()
은 본인만의 this
를 가지고 있었고 호출될 때 마다 상황에 맞춰 this
를 바인딩 했다.
하지만 화살표 함수는 본인만의 this
가 존재하지 않고, 자신을 선언한 렉시컬 환경의 this
를 따른다.
해당 코드에서 add(arr)
안에서 arr.map(화살표 함수)
로 선언된 화살표 함수는 고차 함수의 인수로 선언되엇지만 add(arr)
의 함수 몸체에서 선언되었기 때문에 add(arr)
의 문맥을 따른다.
add(arr)
는 메소드이기 때문에 메소드의 this
바인딩 방식을 따라 결정된다.
정리
결국 화살표 함수의
this
는 선언되는 당시 문맥에 따라 결정된다.
위 예시에서는 메소드 내부에서 선언되었기 때문에 메소드 문맥에 따라 인스턴스와 바인딩 된다.
문맥에 따라 this
가 바인딩 된다고 하였으니 전역에서 선언된 화살표 함수는 전역 객체인 window or global
과 바인딩 된다.
진짜 머리가 너무 아프다.
한 번만 더 정리해보자
해당 객체 내에 사용된 화살표 함수는 선언된 환경이 전역 렉시컬 환경이기 때문에 this
는 전역 렉시컬 환경의 객체인 전역 객체를 가리킨다.
그로 인해 전역 객체내의 프로퍼티인 gender 를 참조했다.
이러한 이유로 화살표 함수는 메소드 내에서 사용되기 보다 콜백 함수에서 사용 될 때 this
를 정적으로 바인딩 시켜놓기 좋다.
argument
는 사용 불가능해ES6함수의 구분 | constructor | prototype | super | arguments |
---|---|---|---|---|
일반함수(normal) | O | O | X | O |
메소드(method) | X | X | O | O |
화살표함수(Arrow) | X | X | X | X |
위 테이블에서 살펴 보았듯이 화살표 함수는 argument
프로퍼티를 가지지 않는다.
argument
는 들어올 인수의 수를 확정할 수 없을 때 사용하는 함수의 내부 프로퍼티이다.
하지만 사용 불가능하기 때문에 만약 인수의 수를 확정 할 수 없다면 Rest
파라미터를 이용해야 한다.
Rest
파라미터Rest
파라미터는 맥개변수 이름 앞에 세개의 점 ... 을 붙여 사용하며 함수에 전달된 인수들의 목록을 배열
로 전달 받는다.
function add(...rest) {
return rest.reduce((acc, currentValue) => acc + currentValue, 0);
}
console.log(add(1, 2, 3, 4, 5)); // 15
일반 맥개 변수와 Rest
파라미터는 함께 사용 할 수 있다.
이 때 함수에 전달된 인수들은 매개 변수와 Rest 파라미터에 순차적으로 할당된다.
function add(start, ...rest) {
return start + rest.reduce((acc, currentValue) => acc + currentValue, 0);
} // start 에는 100 , ...rest 에는 [1,2,3] 할당
console.log(add(100, 1, 2, 3)); // 106
그로 인해 Rest 파라미터
는 매개변수의 마지막에 사용해야 한다.
또한 배열로 받아오기 때문에 Array.prototype
에 존재하는 메소드를 사용하는 것이 가능하다.
예전 argument
로 가져오던 방법도 있었지만, 이는 유사 배열 객체일 뿐이지 실제 배열 객체가 아니라 Array.prototype
을 사용하는 것은 불가능했다.
하지만 Rest 파라미터
가 나온 이후로
ES6
이전에는 매개변수보다 많은 수의 인자가 들어오면 인자가 무시되었지만
더 적은 인수가 들어올 경우에는 undefined
가 되어서 불편함이 많았다.
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
console.log(add(1, 2)); // NaN
이를 해결 하기 위해서 단축 평가를 이용하는 방법이 있었다.
function add(a, b, c) {
a = a || 0;
b = b || 0;
c = c || 0;
return a + b + c;
}
console.log(add(1, 2)); // 3
이것은 인수를 전달받지 못한 매개 변수가 undefined
가 되었을 때 falsy
한 값으로 평가되기 때문에 단축 평가를 이용했다.
하지만 ES6
이후 부터는 매개 변수에 기본 값을 전달 할 수 있기 때문에 이런 불필요한 행동을 안해도 된다.
function add(a = 0, b = 0, c = 0) {
return a + b + c;
}
console.log(add(1, 2)); // 3