JavaScript

Regular function vs arrow function

song 2022. 8. 1. 20:33
// Function declaration
function greet(who) {
  return `Hello, ${who}!`;
}

// Function expression
const greet = function(who) {
  return `Hello, ${who}`;
}

// Arrow function
const greet = (who) => {
  return `Hello, ${who}!`;
}

Function declaration과 function expression을 regular function이라고 칭하고 이들과 arrow function의 차이를 정리한다.

 

1. this value

regular function 내부에서 this의 값은 dynamic하게 결정된다. 즉, 어떻게 함수가 호출되었는지에 따라 함수 내부에서 참조하는 this의 값이 변경된다. this는 4가지 방법에 의해 binding된다.

 

Regular function의 this

기본 바인딩

this는 global object(browser에서는 window객체, node에서는 global 객체)에 바인딩 된다. (단, strict mode에서 돌아가는 function은 global object에 binding되지 않고 this는 undefined가 된다.)

function myFunction() {
  console.log(this);
}
// Simple invocation
myFunction(); // logs global object (window)​

암시적 바인딩

객체의 property로 함수를 호출하면 해당 함수의 this는 그 호출부의 객체가 된다.

const myObject = {
  method() {
    console.log(this);
  }
};
// Method invocation
myObject.method(); // logs myObject

다음과 같은 상황에서는 어떨지 헷갈릴 수 있다. 객체의 프로퍼티를 할당했지만 새로운 변수에 reference를 저장하는 순간 이 함수는 일반 함수가 된다. 따라서 암시적 바인딩이 아닌 기본 바인딩이 이루어져 global object가 this가 된다.

const myObject = {
  method() {
    console.log(this);
  }
};

const objMethod = myObject.method;
// Simple invocation O, Method invocation X
objMethod(); // logs global object

명시적 바인딩

명시적 바인딩은, call, apply, bind를 사용하여 명시적으로 binding할 객체를 지정하는 경우이다.

function myFunction() {
  console.log(this);
}
const myContext = { value: 'A' };
myFunction.call(myContext);   // logs { value: 'A' }
myFunction.apply(myContext);  // logs { value: 'A' }
myFunction.bind(myContext)(); // logs { value: 'A' }

New 바인딩

new keyword를 사용해서 함수를 호출하면 this가 생성된 객체가 된다.

function MyFunction() {
  console.log(this);
}
const myFuncObj = new MyFunction(); // logs an instance of MyFunction
// myFuncObj === this

바인딩 우선순위

new > 명시적 > 암시적 > 기본

Arrow function의 this

Arrow function은 regular function과 다르게 execution context를 생성하지 않으며 따라서 this도 새로 binding하지 않는다. 즉 arrow function 내부의 this는 arrow function 외부의 this와 완전히 같다.

 

function method() {
  setTimeout(function callback() {
    console.log(this);
  });
}

const myObject = {
  method: method,
};

method(); // logs global object (window)​
myObject.method(); // logs global object (window)​
method.call({}); // logs global object (window)​​

method안의 setTimeout의 인자로 들어간 callback function은 실행될 때 기본 바인딩이 일어나기 때문에 세 가지 경우 모두 global object가 찍히는 것을 확인할 수 있다. (method의 this는 모두 다르지만 setTimeout의 callback은 method와 상관없이 실행된다)

function method() {
  setTimeout(() => {
    console.log(this);
  });
}

const myObject = {
  method: method,
};

method(); // logs global object (window)​
myObject.method(); // logs myObject
method.call({}); // logs {}

반면에 callback으로 arrow function을 넣게되면 arrow function 내부의 this가 method의 this가 되어 상황에 따라 다른 this가 출력된다.

 

Arrow function을 사용하면 안되는 경우

this binding과 관련하여, 함수 내부에서 this를 사용하는 경우에는 regular function을 사용할지 arrow function을 사용할지 잘 선택해야 한다.

method

this를 사용하는 method를 arrow function으로 선언하면 this가 method를 호출한 객체가 아닌 상위 객체를 가리키게 된다.

// Bad
const person = {
  name: 'Song',
  sayHi: () => console.log(`Hi ${this.name}`)
};
person.sayHi(); // Hi undefined

// Good
const person = {
  name: 'Song',
  sayHi() {
    console.log(`Hi ${this.name}`);
  }
};
person.sayHi(); // Hi Song

반면 위와 같은 암시적 바인딩이 아닌 new 바인딩의 경우에, method를 callback으로 사용하게 된다면 의도하지 않은 결과를 얻을 수도 있다.

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  logName() {
    console.log(this.heroName);
  }
}
const batman = new Hero('Batman');

setTimeout(batman.logName, 1000);
// after 1 second logs "undefined"

setTimeout(batman.logName.bind(batman), 1000);
// after 1 second logs "Batman"

이럴땐 arrow function을 사용해서 객체의 this를 사용하는 것이 깔끔하게 구현할 수 있다.

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  logName = () => {
    console.log(this.heroName);
  }
}
const batman = new Hero('Batman');

setTimeout(batman.logName, 1000);
// after 1 second logs "Batman"

Prototype

// Bad
const person = {
  name: 'Song',
};

Object.prototype.sayHi = () => console.log(`Hi ${this.name}`);
person.sayHi(); // Hi undefined

// Good
const person = {
  name: 'Song',
};

Object.prototype.sayHi = function() {
  console.log(`Hi ${this.name}`);
};
person.sayHi(); // Hi Song

addEventListener

const button = document.getElementById('myButton');

// Bad
button.addEventListener('click', () => {
  console.log(this === window); // => true
  this.innerHTML = 'Clicked button'; // window의 html이 변경된다.
});

// Good
button.addEventListener('click', function() {
  console.log(this === button); // => true
  this.innerHTML = 'Clicked button';
});

 

2. constructor

regular function은 object의 생성자가 될 수 있고, arrow function은 그렇지 않다.

function Car(color) {
  this.color = color;
}
const redCar = new Car('red');
redCar instanceof Car; // => true
const Car = (color) => {
  this.color = color;
};
const redCar = new Car('red'); // TypeError: Car is not a constructor

 

3. arguments object

Regular function의 body에서는 arguments 라는 array같이 생긴 object를 사용할 수 있다. 이름과 같이 function의 argument로 넘어온 값들을 갖는다.

function myFunction() {
  console.log(arguments);
}
myFunction('a', 'b'); // logs { 0: 'a', 1: 'b', length: 2 }

arrwo function에서 argument를 사용하기 위해서는 rest parameter를 사용해서 참조할 수 있다.

 const myArrowFunction = () => {
    console.log(arguments);
  }
  myArrowFunction('a', 'b'); // Uncaught ReferenceError: arguments is not defined
  
  const myArrowFunction = (...args) => {
    console.log(args);
  }
  myArrowFunction('a', 'b'); // logs ['a', 'b']