[JavaScript] 3-2. JavaScript function in depth

JavaScript function in depth


객체로서의 함수

함수는 Function 생성자로부터 생성되는 객체입니다.

function func() {}

// 이제는 코드를 보면 당연히 이해가 되어야 합니다.
func instanceof Function;
Object.getPrototypeOf(func) === Function.prototype;

다만, 다른 객체들과는 다르게 호출할 수 있다(callable)는 특징이 있습니다.

함수 객체는 두 가지 유용한 특성을 가지고 있습니다.

function add(x, y) {
  return x + y;
}

console.log(add.length); //  2
console.log(add.name); //  add

name 속성의 값은 다양한 조건에 의해 결정됩니다.
function 을 통해 직접 이름을 지정하는 경우는 위와 같습니다.
변수를 선언하고 변수의 값으로 함수가 선언되는 경우에는 변수의 이름이 함수명으로,
객체리터럴 안에 함수가 값으로 정의된 경우에는 함수를 정의하는 속성의 이름을 함수명으로 받습니다.

// 변수의 값으로 함수가 선언된 경우
let f = function() {};

// 객체리터럴 안에 함수가 값으로 정의된 경우
let object = {
  someMethod: function() {}
};

console.log(f.name); //  'f'
console.log(object.someMethod.name); //  'someMethod' (Object는 생략해도 동일하게 작동합니다.)



주인 없는 this

this 생성자는 혹은 메소드에서 객체를 가리킬 때 사용되는 키워드입니다.
하지만 꼭 생성자나 메소드가 아닌 함수에서 this 키워드를 사용한다고 해서 에러가 나지는 않습니다.

function printThis() {
  console.log(this);
}

printThis(); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}

위의 코드에서 this 는 전역 객체를 가리키고 있습니다.
ES5 미만의 예전 버젼 JS 는 여러 좋지 못한 부분이 있는데, this 가 전역 객체를 가리키는 성질은 이들 중 대표적인 것입니다.
이런 이상한 동작 방식 때문에, 프로그래머의 작은 실수로 인해 큰 문제가 생길 수 있습니다.

function Person(name) {
  this.name = name;
}

// 'new'를 빠트린 채 생성자를 호출하면, 'this'는 전역 객체를 가리키게 됩니다.
Person("john");

// 의도치 않게 전역 객체의 속성이 변경되었습니다.
console.log(window.name); //   'john'
function Person(name) {
  this.alert = name;
}

Person("john");

console.log(window.alert); // 'john'

alert("hello"); // TypeError : alert is not a function

// 위와 같이 전역 객체의 함수명과 일치되는 부분에 this 가 바인딩되면 전역 객체의 함수는 기능을 잃어버리게 됩니다.

[주의 - JS 에서 함수가 객체의 속성이 아닌 경우에 함수를 호출하면 this 는 자동으로 전역객체에 바인딩이 됩니다.]



엄격 모드 (strict mode)

JS 에서는 엄격 모드라는 것이 있습니다.
엄격 모드에서는 JS 언어의 동작 방식이 미묘하게 바뀌는데,
예전 버젼 JS 의 특징으로 인해 프로그래머가 실수하기 쉬운 몇 가지 문법에 대해 제약사항을 추가합니다.
예를 들어, 엄격 모드에서는 위와 같이 this 를 사용했을 때, 전역 객체 대신 undefined 를 반환합니다.

function Person(name) {
  // 엄격 모드를 활성화 합니다.
  "use strict";

  // 'undefined'의 속성을 변경하려고 하고 있기 때문에 에러가 발생합니다.
  this.name = name;
}

Person("john"); // Cannot set property 'name' of undefined

이처럼 엄격 모드는 프로그래머의 실수를 미연에 방지해주기 때문에, 항상 사용하는 것이 좋습니다.
엄격 모드를 사용하기 위해서 매번 ‘use strict’를 작성해야 하느냐? 그렇지 않습니다.
ES2015 모듈을 이용해 작성된 코드는 항상 엄격 모드로 동작하기 때문에,
함수 위에 ‘use strict’;를 붙여주지 않아도 엄격 모드로 동작합니다.
요즘 만들어지는 클라이언트 측 JS 코드는 대부분 Babel 과 TypeScript 같은 트랜스파일러를 통해
ES2015 모듈 방식으로 작성되기 때문에, 이런 도구를 사용하고 있다면 본인이 작성하는 코드가 항상 엄격 모드로
동작하고 있다고 생각해도 무방합니다.



this 바꿔치기

앞에서 봤던 것처럼 this 는 때에 따라 다른 값을 가리킵니다.
심지어는 우리가 원하는 값을 가리키게 만들 수도 있는데, 함수 객체의 bind, call, apply 메소드를 사용하면 됩니다.

함수 객체의 bind 메소드를 호출하면, 메소드의 인수로 넘겨준 값이 this 가 되는 새로운 함수를 반환합니다.

function favoriteFood() {
  console.log(`${this.name}님이 좋아하시는 음식은 ${this.menu}입니다.`); // 지금은 this는 window를 가리킵니다.
}

const info = { name: "이근환", menu: "초밥" };
const favoriteFoodForMoong2 = favoriteFood.bind(info); // 하지만 favoritefood 함수에 info 객체를 바인딩 하였으므로

favoriteFoodForMoong2(); //  이근환님이 좋아하시는 음식은 초밥입니다.

call 혹은 apply 메소드를 사용하면, 새로운 함수를 만들지 않고도 임시적으로 this 를 바꿔버릴 수 있습니다.
call 과 apply 는 인수를 넘겨주는 형식에 차이가 있을 뿐, 나머지 기능은 같습니다.

function favoriteFood(menu) {
  console.log(`${this.name}님이 좋아하시는 음식은 ${menu}입니다.`);
}

const info = { name: "이근환" };

// favoritefoood함수에 call 메소드를 사용하여 info 변수와 인수를 불러와서 임시적으로 this의 값을 바꿔줍니다.
favoriteFood.call(info, "초밥");

// favoritefoood함수에 apply 메소드를 사용하여 info 변수와 배열 형태의 인수를 불러와서 임시적으로 this의 값을 바꿔줍니다.
// apply 메소드는 배열을 인수로 넘겨주고 싶을 때 사용하면 좋습니다.
favoriteFood.apply(info, ["떡볶이"]);
// 오즘에는 apply를 대체하여 이렇게 사용하기도 합니다. (...연산자의 사용 - 스프레드 연산자)
function doStuff(x, y, z) {}
let args = [0, 1, 2];
doStuff(...args);



arguments 와 나머지 매개변수 (Rest Parameters)

function 구문을 통해 생성된 함수가 호출될 때는, arguments 라는 변수가 함수 내부에 자동으로 생성됩니다.
arguments 는 유사 배열 객체(array-like object)이자 반복 가능한 객체(iterable object)로, 함수에 주어진 인수가
순서대로 저장되기 때문에 인덱스를 가지고 인수를 읽어오거나 for...of 를 통해 순회할 수 있습니다.

function add(x, y) {
  //  'arguments[0]'에는 'x'와 같은 값이, 'arguments[1]'에는 'y'와 같은 값이 저장됩니다.
  console.log(arguments[0], arguments[1]);
  return x + y;
}

add(1, 2); // 1 2

arguments 는 ES2015 이전까지 인수의 개수에 제한이 없는 함수를 정의하는 데에 사용되고는 했습니다.

function sum() {
  let result = 0;
  for (let item of arguments) {
    result += item;
  }
  return result;
}

sum(1, 2, 3, 4); //  10

하지만, ES2015 에서 도입된 나머지 매개변수(rest parameters) 문법을 통해 똑같은 기능을 더 깔끔한 문법으로 구현할 수 있기 때문에
arguments 는 더 이상 사용되지 않는 기능이 되었습니다.

function sum(...ns) {
  let result = 0;
  for (let item of ns) {
    result += ns;
  }
  return result;
}

sum(1, 2, 3, 4); //  10

위의 예제와 같이, 매개변수 앞에 …을 붙여주면, 헤당 매개변수에 모든 인수가 저장됩니다.
arguments 와는 달리 나머지 매개변수는 실제 배열이기 때문에, 배열의 메소드를 활용할 수 있습니다.

function sum(...ns) {
  // 'for...of' 루프 대신에 'reduce' 메소드를 사용해서 합계를 구할 수 있습니다.
  return ns.reduce((acc, item) => acc + item, 0);
}

sum(1, 2, 3, 4); //  10

단, … 문법은 마지막 매개변수에만 사용할 수 있습니다.

function printGrades(name, ...greades) {
  console.log(`${name} 학생의 점수는 ${grades.join("")} 입니다.`);
}

printGrades("moong2", 99, 93, 92); //  moong2 학생의 점수는 99, 93, 92 입니다.

아래처럼 마지막 매개변수가 아닌 매개변수에 … 문법을 사용하려고 하면 에러가 납니다.

function printGrades(...grades, name) {
    console.log(`${name} 학생의 점수는 ${grades.join(',')} 입니다.`);
}

// Syntax Error : Rest Parameter must be last formal parameter

arguments 객체는 더 많은 기능을 가지고 있지만, 여기에서 소개하지 않은 기능(arguments.slice)은
주인 없는 this와 함께 예전 버전의 JS 의 좋지 않은 부분 중 하나이므로 사용하지 않는 것이 좋습니다.
(arguments 에 slice, 변수의 값으로의 전달 및 참조등.. 을 사용하게 되면 일부 JS 엔진에서는 최적화를 막습니다.)



화살표 함수(arrow function)

함수 정의를 위한 새로운 표기법인 화살표 함수는 ES2015 에서 도입되었습니다.
화살표 함수는 익명함수로만 사용가능합니다.

// 바로 반환시키지 않고 function 키워드를 통한 함수 정의처럼 여러 구문을 사용하려면 curly braces(중괄호) ({...})으로 둘러싸줘야 합니다.
// '=>' 다음 부분을 중괄호로 둘러싸면, 명시적으로 'return' 하지 않는 한 아무것도 반환되지 않습니다.
const add = (x, y) => {
  const result = x + y;
  return result;
};

const add = (x, y) => {
  return x + y;
};

const negate = x => {
  return !x;
};

// const 변수명 = 파라미터 =>(이 화살표가 return임) 함수작성할 때처럼 식을 적어주면되는데 한줄이면 curly braces 없어도 되고 두줄이면 있어야한다.

다만, 특정 조건을 만족하는 화살표 함수는 조금 더 간결한 문법으로 정의할 수도 있습니다.

이 성질을 이용해 위 코드를 더 짧게 작성할 수 있습니다.

const add = (x, y) => x + y;
const negate = x => !x;

function 구문으로 정의되는 함수와 비교했을 때, 화살표 함수는 문법적인 측면에서만 다른 것이 아니라
특별한 성질을 갖고 있습니다.

여기서 스스로의 this 를 가지지 않는다는 것은 함수 내부에서 this 를 사용할 수 없다는 말이 아닙니다.
화살표 함수 내부에서 this 를 쓰면, 그 this 는 함수가 정의된 스코프에 존재하는 this 를 가리킵니다.
이는 new.target, arguments, super 모두 마찬가지입니다.

function Person(name) {
  this.name = name;
  this.getName = () => {
    // 여기에서 사용된 'this'는 '함수가 정의된 스코프', 즉 'Person 함수 스코프'에 존재하는 'this'를 가리키게 됩니다.
    return this.name;
  };
}

const moong2 = new Person("moong2");
moong2.getName(); //  'moong2'

// 화살표 함수와 function의 차이점
// 화살표 함수는 선언됨과 동시에 this가 결정되지만 function은 호출됨과 동시에 this가 결정됩니다.

이런 성질 때문에, 화살표 함수 내부에 있는 this 는 엄격 모드의 영향을 받지 않습니다.

// 주의! 화살표 함수는 생성자로 사용될 수 없습니다.
const Person = name => {
  "use strict";
  this.name = name;
};

Person("moong2");
console.log(window.name); //  moong2

화살표 함수는 스스로의 this 를 갖지 않는다고 했습니다.
이 때문에, 화살표 함수에 대해 bind, call, apply 메소드를 호출해도 아무런 효과가 없습니다.

function Person(name) {
  this.name = name;
  this.getName = () => {
    // 여기에서 사용된 this는 함수가 정의된 스코프, 즉 Person 함수 내부의 this를 가리킵니다.
    return this.name;
  };
}

const moong2 = new Person("moong2");

// this를 바꿔보려 해도, 아무런 효과가 없습니다.
moong2.getName.call({ name: "min_zzz" }); //  'moong2'

그리고 화살표 함수 내부에서 this 를 사용하면 함수가 정의된 스코프에 있는 this 를 가리킨다고 했습니다.
즉, 화살표 함수 내부의 this 는 화살표 함수는 정의된 문맥에 의해 결정됩니다.
function 구문으로 함수에서 쓰이는 this 가 어떻게 호출되는지에 의해 결정되는 것과는 다른 방식을 보입니다.

const mary = {
  name: "mary",
  getName: () => {
    return this.name;
  }
};

// 위의 화살표 함수는 전역 스코프에서 정의되었기 때문에, 'this'는 전역 객체를 가리킵니다.
// `mary`의 메소드로 사용된다고 해도, 이 사실이 변하지 않습니다.
// 브라우저 환경의 전역 객체인 `window`는 `name`이라는 속성에 빈 문자열을 갖고 있기 때문에, 이 값이 대신 반환됩니다.
mary.getName(); // ''

복잡해 보이지만, 사실 코드의 문맥 상으로 화살표 함수에서의 동작 방식이 더 자연스럽습니다.
this 가 항상 눈에 보이는 대로 이미 결정되어 있기 때문에, 혹여나 this 때문에 문제가 생기지 않을까 걱정하지 않아도 됩니다.

정리해보면 function 구문으로 생성되는 함수가 단순한 함수 이외에 생성자나 제네레이터 등의 여러 기능까지 떠맡고 있는 반면에
화살표 함수는 오직 함수 혹은 메소드로 사용되도록 만들어졌습니다.
그리고 this 가 변하지 않기 때문에 화살표 함수를 코드의 이곳저곳으로 가지고 다녀야 하는 상황에서도 안심하고 호출할 수 있습니다.
게다가 문법까지 간결하니, 함수를 값으로 다루어야 하는 경우에는 화살표 함수가 일반 함수보다 편리한 경우가 많습니다.



매개변수의 기본값

const arr = [1, 2, 3, 4.5];
arr.slice(); //  [1,2,3,4,5]
arr.slice(2); //  [3,4,5]
arr.slice(2, 3); //  [3]

Array.prototype.slice 메소드는 인수를 주었을 때나 주지 않았을 때나 모두 잘 동작합니다.
이런 함수를 어떻게 만들수 있을까요?
다음은 매개변수가 있는 함수에 아무런 인수도 주지 않았을 때 어떻게 되는가에 대한 코드입니다.

// 인수를 그대로 변환하는 함수(identify function)입니다.
const ident = x => x;
ident(); //  undefined

이처럼 함수 호출 시에 인수를 주지 않으면 매개변수에는 undefined 가 대입됩니다.
이 사실을 이용해, 주어지지 않았을 때는 대신 미리 설정된 값을 사용하는 함수를 작성할 수 있습니다.

function hello(name) {
  // 매개변수는 'var' 변수와 같은 성질을 갖기 때문에, 재대입을 할 수 있습니다.
  if (name === undefined) {
    name = "Mary";
  }
  console.log(`Hello, ${name}!`);
}

hello("John"); //  Hello, John!
hello(); //  Hello, Mary!
hello(undefined); //  Hello, Mary!

JS 에서는 위와 같은 기법이 너무 많이 사용되었던 관계로, 이를 쉽게 만들어주는 문법이 ES2015 에서 도입되었습니다.
이 문법을 매개변수의 기본값(default parameter)이라고 부릅니다.
이를 이용하면 위의 코드를 더 깔끔하게 작성할 수 있습니다.

// 'Mary'가 `name` 매개변수의 기본값이 됩니다.
function hello(name = "Mary") {
  // 코드가 훨씬 깔끔해졌습니다.
  console.log(`Hello, ${name}!`);
}

hello("John"); //  Hello, John!
hello(); //  Hello, Mary!
hello(undefined); //  Hello, Mary!



2. Today I Found Out

다소 오늘 공부한 내용이 개인적으로는 이번주에 공부한 것중 제일 이해가 어려웠지만
과거의 JS에서 오늘날 우리가 사용하는 ES2015이상의 JS까지 많은 발전이 있었고,
다양한 이슈를 해결하기 위해서 새로운 장치와 문법, 그리고 메소드들이 탄생한 것을 보면서
지금 배우는 것들이 과거에는 더욱 까다로웠는데 지금은 많이 편리해진 것이구나라고
생각을 할 수 있었습니다. this의 전역객체 바인딩, bind, apply, call 메소드까지
어떻게 실무에서는 이런 기능들을 사용할지 모르지만 그래도 예제를 바꿔가면서 공부하면서
어떤 용도로 사용하면 될지 감을 잡을 수 있었습니다.
앞으로도 더 열심히 공부하도록 하겠습니다.



3. refer

https://helloworldjavascript.net/pages/230-function-in-depth.html

http://programmer-seva.tistory.com/28

http://anster.tistory.com/165

https://seongbeom.github.io/2017/02/08/uses-of-spread-operator.html