자바스크립트 딥다이브 - ES6 함수의 추가 기능

ChoiYongHyeun·2023년 12월 18일
0

자주 사람들 코드를 보다보면 발견되는 화살표 함수를 이해하지 못했는데 이번 시간 덕에 이해 할 수 있게 됐다.

사실 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 키워드 대신 화살표 (=>) 를 사용하여 기존 함수 정의 방식 보다 간략하게 함수를 정의 할 수 있다.

화살표 함수에 대한 내용을 자세히 살펴보기 전에 화살표 함수가 필요한 이유에 대해서 살펴보자

화살표 함수가 필요한 이유

1. 렉시컬 스코프 바인딩

화살표 함수는 자신을 둘러싼 스코프에서 가져오기 때문에 함수를 정의한 시점에서 컨텍스트를 기억한다.

이를 통해 일반 함수의 this 는 호출되는 시점에 따라 정의 되었다면 화살표 함수의 this 는 선언된 당시에 이미 정의된다.

2. 간결한 문법

간략하게 설명 가능하다. 이는 화살표 함수가 주로 사용되는 곳이 콜백 함수로서 사용 될 때 가독성을 향상 시킨다

콜백 함수

다른 함수의 인수로 전달 되어 호출한 함수의 실행이 완료 된 후 호출되는 함수

3. 단축 메소드 구문

메소드를 정의 할 때 더 간결한 구문을 제공한다.

4. 생성자 함수로서 작동 불가능

화살표 함수는 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~!

이처럼 화살표 함수는 고차 함수 내에서 가독성 있는 코드를 유지시켜 준다.

화살표 함수와 일반 함수의 차이점

1. 화살표 함수는 생성자로 사용 할 수 없다.

const foo = () => {};

console.log(foo.hasOwnProperty('prototype')); // false

화살표 함수는 prototytpe 프로퍼티를 가지지 않기 때문에 생성자 함수로 사용 할 수 없다.

2. 화살표 함수는 중복된 매개 변수를 사용 할 수 없다.

일반 함수는 에러 없이 시행된다.

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

3. 화살표 함수의 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')

엥 에러가 발생한다.

분명 메소드 내부에서 정의된 함수는 생성되는 인스턴스를 가리키니까 thisperfixer를 가리키고 있을거라고 기대하던 것과 완전 딴판이다.

그 이유는 다음과 같다.

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

위 코드에서 Personfoo 값에서 즉시 실행 함수로 일반 함수 호출 방식을 이용해 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 를 바인딩 시켜주는 방법이 존재했다.

화살표 함수를 이용하게 된다면 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
profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글