함수매개변수

'use strict';

const list = [];
const testFunc = (current = 2021, born = 1994, age = current - born) => {
  list.push([current, born, age]);
};
testFunc(); 
testFunc(2022);
testFunc(2020, 1999);
// null을 넣으면 기본값이 들어가지 않고 null이 들어간다.
testFunc(2028, null, 20);
testFunc(2028, null);
// 값에 기본값을 넣고 뛰어넘기
testFunc(2028, undefined, 20);
console.log(list);

/*
0: (3) [2021, 1994, 27]
1: (3) [2022, 1994, 28]
2: (3) [2020, 1999, 21]
3: (3) [2028, null, 2028]
4: (3) [2028, 1994, 20]
*/

함수의 매개변수는 위와 같이 기본값을 넣을 수 있고 앞에서 받은 매개변수를 바로 활용할수도 있다. ES5 버전에서는 저런 문법이 불가해서 ||으로 처리했지만, ES6부터는 다르다.

다른 매개변수에 기본값이 없는데도 불구하고 사용하면 당연히 undefined 에러가난다.

중간 매개변수에 기본값을 넣어 그냥 매개변수 대입값을 뛰어넘고 싶다면 undefined를 넣으면 된다. 비슷한 의미의 null 값을 대입하면 기본값이 아니라 해당 매개변수에 null이 들어가게 된다.

❗️ null 값은 연산시 0으로 처리된다?
- 연산자는 Type coerion 때문에 자동으로 숫자로 형변환된다.
null도 그럴 것으로 예상한다. undefined는 아니다. NaN으로 연산된다.
... 자바스크립트의 수많은 버그 같은 것일까?

함수 매개변수 - 객체

const doodream = {
  name: 'doodream',
  age: 21,
  charactor: 'funny',
};
const work = 'None';
const testFunc = (person, job) => {
  person.name = 'dream';
  person.age = 19;
  job = 'Programmer';
};
testFunc(doodream, work);
console.log(doodream, work);
// {name: "dream", age: 19, charactor: "funny"} "None"

위코드에서 매개변수에 객체를 넣으면 해당 객체가 직접전달되는 것이 아니라 자바스크립트 콜스텍에 들어있는 객체가 들어있는 힙주소를 가르키는 주소값이 전달된다.

즉, 참조값이 들어가버리기 때문에 객체안에서 해당값을 수정하면 함수의 블럭을 넘어서 직접 객체를 조작하게 된다.

객체가 아닌 primitive 값은 콜스텍에서 해당 값이 직접 변수로 전달되기 때문에 격리되어 데이터가 처리되는 모습이다.

💡 원본 객체를 손상 시키지 않게하기
이 주제는 객체의 깊은복사와 연관이 크다. 깊은복사가 되어야 하는것이 전제가되어야 위 주제가 성립되기 때문이다. 이와 관련한 주제는 나중에 포스팅하겠습니다.

First Class Functions

  • 자바스크립트에서 함수를 first-class citizens 로 다루고 있다. first-class citizens이 단어의 뜻은 함수는 단순히 하나의 값이라고 취급하는 것입니다.
  • 실제로 함수는 또하나의 객체 타입에 불과하며 하나의 값에 불과합니다.
  • 값이기 때문에 다른 함수의 매개변수로 전달되기도 합니다.

위 예제를 살펴보면 함수는 어떠한 값을 반환하거나 객체의 프로퍼티에 값으로 추가 됨으로서 단순한 값의 형태로 전달되는 것을 볼수 있다.

Higher-Order Functions

함수를 매개변수로 받거나 함수를 반환하는 함수를 Higher-order functions 라고 한다. 이러한 함수 예로서 addEventListner가 있다.

위 종류의 함수는 다른 형태로 취급해야한다.

const testStr = '메롱';

const jumpWord = function (str) {
  return ' ' + str + ' ';
};

const transformer = function (str, fn) {
  console.log(`원래 문자열 : ${str}`);
  console.log(`바꾼 문자열 : ${fn(str)}`);
  console.log(`문자열을 바꾼 함수의 이름 : ${fn.name}`);
  // 함수라는 객체의 기본 프로퍼티로서 함수의 이름을 반환하는 프로퍼티가 있다.
};

transformer(testStr, jumpWord);

const sayHo = function () {
  console.log('sayHo!');
};
document.querySelector('body').addEventListener('click', sayHo);

[1, 2, 3].forEach(sayHo);// 3 sayHo!


transformer 함수는 fn으로 다른 함수를 매개변수로 받아 기능하게 한다.
addEventListener 함수는 callBack 함수로서 함수를 매개변수로 받아 기능하게 한다.
foreach 함수는 함수를 콜백함수로 받아 주어진 배열요소의 원소마다 실행시킨다.

이렇게 콜백함수로 함수를 분할해서 코드하는 이유는

  • 코드의 분리가 코드의 재사용률을 높이고
  • 추상적인 코드를 만들수 있어 응용률을 엄청나게 높인다.

❗️ 추상화의 수준이 중요한 이유

  • 일부코드의 세부 정보를 숨겨준다.
  • 모든 세부사항에 대한 정보를 신경쓰지 않고 코드를 작성해도 된다.

이렇게 낮은 수준의 함수와 높은 수준의 함수를 구분해서 높은 추상화를 추구하면서 코드를 짜기시작하면 코드의 응용률과 범용성을 크게 높일수 있다. 그러한 코드일수록 재사용률이 높아지기 때문에 많이 사용되는 함수일수록 좋은 코드가 된다.

중첩함수

const greet = greeting => {
  return name => {
    console.log(`${greeting}, ${name}`);
  };
};

const greeter = greet('안녕!');
greeter('두드림');
greeter('노두현');
greet('메롱')('용용 죽겠지');
/*
안녕!, 두드림
안녕!, 노두현
메롱, 용용 죽겠지
*/

위 코드에서 보면 함수에서 매개변수를 받고 반환하는 함수에서도 매개변수를 받는다면 함수를 불러올 때 greeter라는 변수 자체가 greet('안녕')이 되어버린다. 그럼 greet('안녕')은 그대로 반환하려는 함수를 불러온다. 즉, 매개변수가 필요하다. 왜냐면 greet('안녕') 이코드 자체가 하나의 함수 이름이 되어버린것이기 때문에 이 함수를 실행하려면 () 코드가 필요하고 이코드에는 매개변수가 필요해서 name 이라는 매개변수를 넣는 것이다.

커링함수

greeter('노두현') === greet('안녕')('노두현')
그중에서도 위와같이 체인형태로 매개변수를 등록하게 만든 함수를 커링 함수라고 합니다.

Wrapper 함수

함수를 매개변수로 받아 본래 기능을 침범하지 않고 어떠한 함수에 추가기능을 덧씌워주는 함수를 Wrapper함수라고 합니다.

const worker = {
  someMethod() {
    return 1;
  },
  slow(x) {
    alert(`${x} calling`);
    return this.someMethod() * x;
  },
};

/* 
함수에 캐시를 지정해놓고 캐시에 이미 값이 있다면 반환 
없다면 캐시에 추가하고 해당값을 입력함수로 실행시킨다음
실행시킨 값을 반환.
*/
function cachingDecorator(func) {
  const cache = new Map();
  return function (x) {
    if (cache.has(x)) return cache.get(x);
    // ❗️ result는 func(x)를 호춣하면서 func(x)의 this를 받지못해서 worker 객체로 접근을 할수가 없다.
    const result = func(x);
    cache.set(x, result);
    return result;
  };
}
// 함수는 값일 뿐이므로 함수 전체를 매개변수로 해서 감싼 형태의 Wrapper 함수를 덧 씌운다.
worker.slow = cachingDecorator(worker.slow);

console.log(work.slow(2));

❗️ 표시를 한부분에서 에러가 난다. 와퍼함수를 씌워 불러올때 와퍼함수 안에서의 this는 undefined 하기 때문이다. 와퍼함수의 정의부분에서의 this는 undefined이다.

더 자세히 설명하면
cachingDecorator(worker.slow) 이부분에서 work.slow라는 것은 함수를 실행한 것이 아닌 함수의 정의에 불과하다. 즉, work.slow 대신 아래 코드를 집어넣어도 성립같은 기능을 한다. 한데 해당 코드 안에서 this는 undefined이다. 따라서 와퍼 함수의 코드에서
const result = func(x); 이부분에서 아래 코드로 넘어가게되고 this.someMethod()에서 this는 undefined 이기 때문에 에러가 나는 것이다.

function (x) {
    alert(`${x} calling`);
    return this.someMethod() * x;
  }

이를 고치기위한 함수의 기본 프로퍼티가 있다.

function.call()

모든 함수에는 기본 프로퍼티가 있는데 그중에서 .call().length()가 대표적이다. 후자는 함수가 매개변수로 받은 변수들로 배열을 만든 것의 길이를 반환한다. 즉, 매개변수의 갯수를 반환한다.

다시 function.call(context, arg1, arg2, ...)함수는 첫번째 매개변수로 받은 것을 해당함수의 this로 고정 시켜버린다. 이후 매개변수는 해당함수의 매개변수로 넘어간다. 즉, 이개념을 이용해 위 코드를 수정하면

function cachingDecorator(func) {
  const cache = new Map();
  return function (x) {
    if (cache.has(x)) return cache.get(x);
    // 💡 func의 this는 worker 객체로 고정되었다.
    const result = func.call(worker, x);
    cache.set(x, result);
    return result;
  };
}

func의 this는 worker로 고정되었으므로 const result는 다음코드와 같다.

const result = function (x) {
    alert(`${x} calling`);
    return worker.someMethod() * x;
  }

따라서 에러가 나지 않게 된다.

function.apply()

apply도 call과 거의 같은 역할을 수행합니다만 두번째 매개변수를 전달할때 아애 배열을 전달해버립니다.

function cachingDecorator(func) {
  const cache = new Map();
  return function (x) {
    if (cache.has(x)) return cache.get(x);
    // 💡 func의 this는 worker 객체로 고정되었다.
    // function의 정의에서 arguments를 (매개변수가 들어있는 배열)을 전달해버린다.
    const result = func.apply(worker, arguments);
    cache.set(x, result);
    return result;
  };
}

이렇게하면 함수의 매개변수 전달과정에서 함수의 매개변수가 여러개일지라도 arguments를 전달해버리면 문제가 없다. 하지만 이경우 ... 문법으로 인해 arguments 를 잘 사용하지 않게 되었다.

❗️ arguments는 화살표함수에서는 정의되지 않는 변수이다.
또한 유사배열객체일 뿐이지 엄밀하게 배열은 아니기 때문에 배열의 프로퍼티로 사용가능한 함수들을 사용할수도 없다. 따라서 spread함수로 arguments를 대신해 매개변수를 전달한다.

따라서 최근에는 call에 spread 문법을 같이 사용하서 다음과 같이 사용합니다.

function cachingDecorator(func) {
  const cache = new Map();
  return function (x) {
    if (cache.has(x)) return cache.get(x);
    // 💡 func의 this는 worker 객체로 고정되었다.
    // 만약 x가 배열이나 이터러블한 객체라면 spread로 매개변수를 전달한다.
    // result에는 worker 객체로 들어가 call함수를 호출하고난 결과가 저장된다.
    const result = func.call(worker, ...x);
    cache.set(x, result);
    return result;
  };
}

function.bind()

const testObj = {
  name: 'doodream',
  age: 17,
  print() {
    console.log(this.name, this.age);
  },
};

const printObj = testObj.print.bind(testObj);
printObj();
// doodream 17

위 코드에서와 같이 bind도 call과 apply같이 this를 강제 바인딩 시켜버립니다. 자세히 설명하자면 testObj.print 코드인

() {
	this = testObj;
    console.log(this.name, this.age);
}

이렇게 고정으로 설정되어버린 함수객체를 반환합니다. 원 함수에 this를 고정시켜버린 새로운 함수객체를 반환합니다. 즉, bindapply, call과는 다르게 해당 함수가 속해있는 객체로 접근해 해당 함수를 호출 하지 않습니다. 그냥 새로운 함수객체를 반환해버립니다. 다른 함수로 만들어 버린 것이죠.

const testObj = {
  count: 1,
  print() {
    this.count++;
    console.log(this.count);
  },
};

document
  .querySelector('.buy')
  .addEventListener('click', testObj.print.bind(testObj));

bind 매개변수

const testObj = {
  print(name, age, ...rest) {
    console.log(name, age, rest);
  },
};

const printObj = testObj.print.bind(testObj, ...['doodream', 23]);
printObj(); // doodream 23
printObj('노두현', 28);// doodream 23 ['노두현', 28]

bind의 매개변수를 넣어보내면 원 함수의 매개변수의 순서대로 고정되어 넣어져버린다. 따라서 함수를 호출할때 매개변수를 넣어도 bind당시 넣었던 매개변수 이후에 넣어진것 처럼 취급된다. 이것을 이용하여 bind는 어떠한 함수이든 기본값을 지정시켜버릴수 있다. this에 바인딩 할것을 null로 넘겨버리고 뒤의 이자들을 testFunc의 매개변수에 지정시켜 새로운 함수객체를 반환시킨다.

const testFunc = (a, b) => {
  return console.log(a + b);
}
testFunc(); // NaN
const testFunc2 = testFunc.bind(null, 1, 2);
testFunc2(); // 3

Closure

closure는 개발자가 알고 있어야할 언어 입니다. 뜻은 외부변수를 기억하고 이러한 외부변수에 접근이 가능한 함수라는 뜻입니다. 다른 언어에서는 특수한 방식을 사용하거나 구조적으로 클로저의 구현이 불가능 합니다만, 자바스크립트에서는 모든함수가 클로저입니다.

코어 자바스크립트 02. 실행 컨텍스트

여기서 게시했듯이 모든 함수들은 실행컨텍스트가 생성되고 Lexical Environment라는 내부 숨김 객체를 갖게 됩니다. 이 객체는 Environment Record 라는 환경에 모든 지역변수를 저장하고 Outer Lexical Environmnet에 외부 코드와 연관된 해당 함수가 선언되었을 당시의 해당 실행컨텍스트의 Lexical Environment를 참조합니다. 즉, 외부 코드의 지역변수를 접근할 수 있게 합니다.

따라서 모든 함수들은 자신보다 상위 부모에 놓여있는 실행컨텍스트의 외부 변수들에 스코프 체인을 통해 모두 접근이 가능합니다. 따라서 클로저라고 할 수 있습니다.

예시 : 순환참조의 오류를 막기위해 사용하기도 합니다.

const UserType = new GraphQLObjectType({
  name: "User",
  fields: () => ({
    id: {
      type: GraphQLString,
    },
    firstName: { type: GraphQLString },
    age: { type: GraphQLInt },
    company: {
      type: CompanyType,
      resolve(parentValue, args) {
        const data = axios
          .get(`http://localhost:3000/companies/${parentValue.companyId}`)
          .then((res) => res.data);
        return data;
      },
    },
  }),
});
const CompanyType = new GraphQLObjectType({
  name: "Company",
  fields: () => ({
    id: { type: GraphQLString },
    name: { type: GraphQLString },
    description: { type: GraphQLString },
    user: {
      type: new GraphQLList(UserType),
      resolve(parentValue, args) {
        const data = axios
          .get(`http://localhost:3000/companies/${parentValue.id}/users`)
          .then((res) => res.data);
        return data;
      },
    },
  }),
});

위 코드를 보면 일단 순환참조 에러가 날수 있는 환경입니다. 하지만 field 속성은 함수를 반환합니다. 따라서 호이스팅당시 실핼할 때 바로 실행되지 않습니다. field속성이 읽힐때 함수가 실행되기 때문에 field 속성안의 Company타입은 Company 속성이 정의되고 난 다음에 실행되게 됩니다. 따라서 순환참조의 에러에서 벗어날 수 있습니다. 이러한 방법은 아주 유용하게 사용됩니다.

가비지 컬렉션

이렇게 함수의 호출이 끝나면 콜스택에서 실행컨텍스트가 제거되는 과정에서 Lexical Environment도 제거 됩니다. 따라서 함수가 살아있는 상태에서는 해당 컨텍스트에 Lexical Environment 와 연관된 모든 실행컨텍스트의 Lexical Environment를 유지합니다. 하지만 그렇지 않은 실행컨텍스트는 종료되면 모든 데이터가 제거됩니다. 하지만 실제로는 자바스크립트 엔진이 이를 계속해서 최적화합니다. 자바스크립트 엔진에서 외부변수가 참조되지 않는다고 생각하면 제거해버립니다. 따라서 디버깅시 이론상 접근이 가능해야하지만 접근이 안되는 변수들이 생깁니다.

profile
일상을 기록하는 삶을 사는 개발자 ✒️ #front_end 💻

0개의 댓글