TIL(함수2)

Seungbin Lee·2024년 5월 17일

스코프와 함수 스택
재귀

함수는 자신을 참조하고 호출할 수 있습니다. 
함수가 자신을 참조하는 방법에는 세 종류가 있습니다.

1. 함수의 이름
2. arguments.callee
3. 함수를 참조하는 스코프 내 변수
예를 들어, 다음 함수의 정의를 고려해보세요.
var foo = function bar() {
  // 여기에 구문이 들어갑니다.
};
함수 본문 내에서 다음은 모두 동일합니다.

bar()
arguments.callee()
foo()
자신을 호출하는 함수를 재귀 함수라고 합니다. 어찌 보면 재귀는 루프와 유사합니다. 
둘 다 동일한 코드를 여러 번 실행하고, 무한 루프를 방지하는 조건 
(여기서는 무한 재귀를 방지하는 조건)이 필요합니다.

예를 들어, 다음 루프는 재귀 함수로 바꿀 수 있고 그 함수를 호출할 수 있습니다.
var x = 0;
while (x < 10) {
  // 루프 조건: "x < 10"
  // 뭔가 합니다.
  x++;
}

function loop(x) {
  if (x >= 10)
    // "x >= 10" 는 탈출 조건 ("!(x < 10)"와 동일)
    return;
  // 뭔가 합니다.
  loop(x + 1); // 재귀 호출
}
loop(0);

그러나 일부 알고리즘은 단순 반복 루프가 될 수 없습니다. 
예를 들어, DOM과 같은 트리 구조의 모든 노드를 가져오는 것은 
재귀를 사용하는 편이 더 간편합니다.

function walkTree(node) {
  if (node == null) {
    return;
  }
  // 노드를 가지고 뭔가 합니다.
  for (var i = 0; i < node.childNodes.length; i++) {
    walkTree(node.childNodes[i]);
  }
}

함수 loop 와 비교하여, 각 재귀 호출 자체는 많은 재귀 호출을 수행합니다.

재귀 알고리즘을 비재귀 알고리즘으로 변환하는 것은 가능하지만 
논리가 더 복잡해지는 경우가 있고 스택을 사용해야 합니다.

사실 재귀도 스택(함수 스택)을 사용합니다. 스택과 같은 동작은 
다음 예제에서 확인할 수 있습니다.

function foo(i) {
  if (i < 0) {
    return;
  }
  console.log(`begin: ${i}`);
  foo(i - 1);
  console.log(`end: ${i}`);
}
foo(3);

// Output:

// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3

.
.

중첩된 함수와 클로저

함수 내에 함수를 끼워 넣을 수 있습니다. 중첩된(내부) 함수는 
그것을 포함하는 (외부) 함수에서만 사용할 수 있습니다.
것은 또한 클로저를 형성합니다. 
클로저는 (표현식을 "닫는")변수를 바인딩하는 환경과 함께 
자유 변수를 가질 수 있는 표현(일반적으로는 함수)입니다.

중첩 함수는 클로저이므로, 중첩된 함수는 그것을 포함하는 
함수의 인수와 변수를 "상속"할 수 있음을 의미합니다. 
즉, 내부 함수는 외부 함수의 스코프를 포함합니다.

요약하자면,

내부 함수는 외부 함수의 명령문에서만 액세스할 수 있습니다.
내부 함수는 클로저를 형성합니다: 
외부 함수는 내부 함수의 인수와 변수를 사용할 수 없는 반면에, 
내부 함수는 외부 함수의 인수와 변수를 사용할 수 있습니다.
다음 예는 중첩된 함수를 보여줍니다.
function addSquares(a, b) {
  function square(x) {
    return x * x;
  }
  return square(a) + square(b);
}
a = addSquares(2, 3); // 13
b = addSquares(3, 4); // 25
c = addSquares(4, 5); // 41

내부 함수는 클로저를 형성하기 때문에 외부 함수를 호출하고, 
외부 및 내부 함수 모두에 인수를 지정할 수 있습니다.

function outside(x) {
  function inside(y) {
    return x + y;
  }
  return inside;
}
// 어떤 값이 들어와도 3을 더하는 함수라 생각해보세요.
fn_inside = outside(3);
result = fn_inside(5); // 8

result1 = outside(3)(5); // 8

변수의 보존

inside가 반환될 때 외부 함수의 인수 x가 보존된다는 점을 알 수 있습니다. 
클로저는 그것을 참조하는 모든 스코프에서 인수와 변수를 보존해야 합니다. 
각 호출은 잠재적으로 다른 인수를 제공할 수 있기 때문에, 
클로저는 outside에 대하여 매번 새로 생성됩니다. 
메모리는 그 무엇도 inside에 접근하지 않을 때만 해제됩니다.

변수의 보존은 일반 객체에서 참조를 저장하는 것과 별 차이점은 없지만, 
사용자가 직접 참조를 설정하고 검사하는게 아니여서 명확하지 않은 경우가 많습니다.

.
.
다중 중첩 함수

함수는 다중 중첩될 수 있습니다. 예시입니다.

a. 함수 A에는 함수 B가 포함되며, B에는 함수 C가 포함됩니다.
b. 함수 B, C는 모두 클로저를 형성합니다. 
   그래서 B는 A에, C는 B를 접근 할 수 있습니다.
c. 또한 C는 A에 접근 할 수 있는 B에도 접근할 수 있기 때문에 
   A에도 접근 할 수 있습니다.

따라서 클로저는 여러 스코프가 포함될 수 있으며, 
이 스코프를 포함하는 함수의 스코프를 재귀적으로 포함합니다. 
이것을 스코프 체이닝이라 합니다. 
(그것을 "체이닝"이라 하는 이유는 추후에 설명할 것입니다.)

다음 예를 살펴 보겠습니다. 
function A(x) {
  function B(y) {
    function C(z) {
      console.log(x + y + z);
    }
    C(3);
  }
  B(2);
}
A(1); // logs 6 (1 + 2 + 3)

이 예에서, C는 B의 y와 A의 x에 접근할 수 있고, 다음과 같이 됩니다.

B는 A를 포함하는 클로저를 형성합니다. 
즉, B는 A의 인수와 변수를 엑세스할 수 있습니다.
C는 B를 포함하는 클로저를 형성합니다.
C의 클로저는 B를 포함하고 B의 클로저는 A를 포함하기 때문에, 
C의 클로저도 A를 포함합니다. 
따라서 C는 A의 인수와 변수에 접근할 수 있다는 것을 의미합니다. 
즉, C는 A의 스코프를 체이닝합니다.
하지만 A는 C의 변수인 B의 인수나 변수에 접근 할 수 없기 때문에 
그 반대로는 불가능합니다. 
따라서 C는 B에게만 비공개로 유지됩니다.

이름 충돌

클로저의 스코프에서 두 개의 인수 또는 변수의 이름이 같은 경우, 
이름 충돌이 발생합니다. 
이 경우 더 안쪽 스코프가 우선순위를 갖습니다. 
가장 바깥 스코프는 우선순위가 가장 낮은 반면에, 
가장 안쪽 스코프는 가장 높은 우선순위를 갖습니다. 
이것이 스코프 체인(scope chain)입니다. 
체인의 첫번째는 가장 안쪽 스코프이고, 마지막은 가장 바깥 쪽 스코프입니다.

다음을 참고하세요.
function outside() {
  var x = 10;
  function inside(x) {
    return x * 2;
  }
  return inside;
}
result = outside()(20); // 10 대신 20을 반환합니다

이름 충돌은 x * 2를 반환하는 부분에서 발생하며, 
inside의 매개 변수 x와 outside의 매개 변수 x 사이에서 발생합니다. 
여기에서 스코프 체이닝은 {inside, outside, 전역 객체}로 이루어집니다. 
따라서 inside의 x는 outside의 x보다 높은 우선순위를 갖게 되고, 
20(inside의 x)이 10(outside의 x) 대신에 반환됩니다.

클로저

클로저는 JavaScript의 강력한 기능 중 하나입니다. 
JavaScript는 함수의 중첩(함수 안에 함수를 정의하는 것)을 허용하고, 
내부 함수가 외부 함수 안에서 정의된 모든 변수와 
함수(및 외부함수가 접근할 수 있는 다른 모든 변수와 함수들까지)에 대해 전체 
접근 권한을 부여합니다.

하지만 외부 함수는 내부 함수에 정의된 변수와 함수에 접근 할 수 없습니다. 
이는 내부 함수의 변수에 대한 일종의 캡슐화를 제공합니다.

또한, 내부 함수는 외부 함수의 스코프에 접근할 수 있기 때문에, 
내부 함수가 외부 함수의 수명 보다 오래 생존하는 경우 
외부 함수에서 선언된 변수나 함수는 외부 함수의 실행 기간보다 오래 유지됩니다. 
내부 함수가 외부 함수 밖의 모든 스코프에서 사용 가능할 때 클로저가 생성됩니다.
const pet = function (name) {
  // 외부 함수는 'name'이라 불리는 변수를 정의합니다.
  const getName = function () {
    return name; // 내부 함수는 외부 함수의 'name' 변수에 접근합니다.
  };
  return getName; // 내부 함수를 반환함으로써, 외부 스코프에 노출됩니다.
};

const myPet = pet("Vivie");

myPet(); // "Vivie"로 반환합니다

클로저는 위 코드보다 더 복잡해 질 수도 있습니다. 
외부 함수의 내부 변수를 다루는 메서드를 포함한 객체도 반환될 수도 있습니다.

const createPet = function (name) {
  let sex;

  const pet = {
    // `setName(newName)`은 이 문맥에서
    // `setName: function (newName)`과 동일합니다.
    setName(newName) {
      name = newName;
    },

    getName() {
      return name;
    },

    getSex() {
      return sex;
    },

    setSex(newSex) {
      if (
        typeof newSex === "string" &&
        (newSex.toLowerCase() === "male" || newSex.toLowerCase() === "female")
      ) {
        sex = newSex;
      }
    },
  };

  return pet;
};

const pet = createPet("Vivie");
pet.getName(); // Vivie

pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver

위의 코드에서 외부 함수의 name 변수는 내부 함수에 접근할 수 있으며, 
내부 함수를 통해서만 내부 변수에 접근할 수 있습니다. 
내부 함수의 내부 변수는 외부 인수와 변수를 안전하게 저장하는 역할을 합니다. 
이들은 내부 기능이 작동할 수 있게 '지속적'이고 '캡슐화된' 데이터를 보유합니다. 
함수를 변수에 할당하거나 이름을 가질 필요가 없습니다.

const getCode = (function () {
  const secureCode = "0]Eal(eh&2"; // 외부에서 접근할 수 없게 하고 싶은 변수.

  return function () {
    return secureCode;
  };
})();

getCode(); // `secureCode`를 반환합니다.

참고: 클로저를 사용할 때 주의해야 할 점이 있습니다!

.

둘러싸인 함수가 외부 스코프의 변수와 동일한 이름을 가진 변수를 정의하면, 
외부 스코프의 변수에 다시 참조할 방법이 없습니다. 
(내부 스코프 변수는 프로그램이 내부 스코프를 종료할 때까지 
외부 스코프 변수를 덮어씁니다. 이건 이름 충돌로 생각할 수 있습니다.)

// 외부 함수는 변수 `name`을 정의합니다.
const createPet = function (name) {
  return {
    // 내부의 함수 또한 변수 `name`을 정의합니다.
    setName(name) {
      // 그럼 외부 함수에서 정의된 `name`에는 어떻게 접근해야 할까요?
      name = name;
    },
  };
};

인수(arguments) 객체 사용하기

함수의 인수는 배열과 비슷한 객체로 처리가 됩니다. 
함수 내에서는, 전달 된 인수를 다음과 같이 다룰 수 있습니다.
arguments[i];
i 는 0으로 시작하는 순서 번호입니다. 
따라서 함수에 전달된 첫 번째 인수는 arguments[0] 입니다. 
총 인수의 개수는 arguments.length 에서 얻을 수 있습니다.

인수(arguments) 객체를 이용하면, 
보통 함수에 정의된 개수보다 많은 인수를 넘겨주면서 함수를 호출할 수 있습니다. 
이것은 얼마나 많은 인수가 함수로 넘겨질지 모르는 상황에서 유용한 경우가 많습니다.
arguments.length를 함수에 실제로 넘겨받은 인수의 수를 알아낼 때 사용할 수 있고, 
각각의 인수에 인수(arguments) 객체를 이용하여 접근 할 수 있습니다.

예를 들어, 여러 문자열을 연결하는 함수를 생각해 봅시다. 
이 함수의 유일한 형식 인수는 각 문자열을 구분해주는 문자를 나타내는 문자열입니다.
이 함수는 다음과 같이 정의됩니다.

function myConcat(separator) {
  // 리스트를 초기화한다
  let result = "";
  // arguments를 이용하여 반복한다
  for (let i = 1; i < arguments.length; i++) {
    result += arguments[i] + separator;
  }
  return result;
}

어떤 개수의 인수도 이 함수로 넘겨줄 수 있고, 
이 함수는 각각의 인수를 하나의 문자열 "리스트" 로 연결합니다.

// "red, orange, blue, "를 반환합니다.
myConcat(", ", "red", "orange", "blue");

// "elephant; giraffe; lion; cheetah; "를 반환합니다.
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");

// "sage. basil. oregano. pepper. parsley. "를 반환합니다.
myConcat(". ", "sage", "basil", "oregano", "pepper", "parsley");

참고: 인수(arguments) 객체는 배열과 닮은 것이지 배열이 아닙니다. 
인수 객체는 번호가 붙여진 인덱스와 길이 속성을 가지고 있다는 점에서 
배열과 닮은 것입니다. 
인수 객체는 배열을 다루는 모든 메서드를 가지고 있지 않습니다.

함수의 매개변수

ECMAScript 2015에서 추가된 매개변수 구문(parameter syntax)에는 
기본 매개변수와 나머지 매개변수라는 두 가지 특수한 유형이 있습니다.

기본 매개변수

JavaScript에서 함수의 매개변수는 undefined 가 기본으로 설정됩니다. 
하지만 경우에 따라 다른 기본값을 설정하는 것이 유용할 수 있습니다. 
이것이 바로 기본 매개변수가 하는 일입니다.

이전에는 기본값 설정을 위해 함수 본문에서 매개변수 값을 테스트하고, 
undefined인 경우에 값을 할당하는 것이 보편적인 방식이었습니다.

다음 예제에서 b 매개변수에 아무 값도 주지 않으면, 
a * b를 계산 할 때 b 매개변수의 값은 undefined가 되므로 
multiply 함수 호출은 NaN을 반환할 것입니다. 
그러나 두번째 줄에서 이 문제가 방지됩니다.
// ECMAScript 2015 이전
function multiply(a, b) {
  b = typeof b !== "undefined" ? b : 1;
  return a * b;
}

multiply(5); // 5

디폴트 매개변수를 사용한다면 함수 본문에서 일일히 확인할 필요가 없습니다. 
간단히 b 의 기본값으로 1을 넣어주면 됩니다.

// ECMAScript 2015 이후
function multiply(a, b = 1) {
  return a * b;
}

multiply(5); // 5

나머지 매개변수

나머지 매개변수 구문을 사용하면 배열로 임의 개수의 인수를 나타낼 수 있습니다.

이 예제에서, 
우리는 나머지 매개변수를 2번째 인수부터 마지막 인수까지 얻기 위하여 사용하였습니다. 
그리고 우리는 첫번째 값으로 나머지 매개변수에 곱하였습니다. 
이 예제는 다음 섹션에서 소개할 화살표(arrow) 함수 입니다.

function multiply(multiplier, ...theArgs) {
  return theArgs.map((x) => multiplier * x);
}

var arr = multiply(2, 1, 2, 3);
console.log(arr); // [2, 4, 6]

화살표 함수

화살표 함수 표현은 함수 표현식에 비해 짧은 문법을 가지고 있고 
사전적으로 this 값을 묶습니다. 화살표 함수는 언제나 익명입니다.
화살표 함수 도입에는 더 짧은 함수와 바인딩 되지않은 
this라는 두 가지 요인이 영향을 미쳤습니다

더 짧은 함수

몇몇 함수 패턴에서는 더 짧은 함수를 사용하는 것이 더 유용합니다. 
한번 비교해보세요.
var a = ["Hydrogen", "Helium", "Lithium", "Beryllium"];

var a2 = a.map(function (s) {
  return s.length;
});
console.log(a2); // logs [8, 6, 7, 9]

// 화살표 함수 사용
var a3 = a.map((s) => s.length);
console.log(a3); // logs [8, 6, 7, 9]

사전적 this

화살표 함수 이전 까지 모든 new 함수는 고유한 this 값을 정의했습니다.
(생성자의 경우 new 객체, 엄격 모드에서는 호출 되지 않음, 
함수가 "객체 메서드"로 호출 되는 경우의 기본 객체 등)
이런 방식은 객체 지향 프로그래밍 스타일과 잘 맞지 않았습니다.

function Person() {
  // `Person()` 생성자는 `this`를 자신으로 정의합니다.
  this.age = 0;

  setInterval(function growUp() {
    // 느슨한 모드에서 `growUp()` 함수는 `this`를 전역 객체로 정의합니다.
    // 이는 `Person()` 생성자에 의해 정의된 `this`와 다릅니다.
    this.age++;
  }, 1000);
}

var p = new Person();

ECMAScript 3/5 에서 이 문제는 this 안의 값을 뒤덮을 수 있는 변수에 
할당하면서 해결했습니다.

function Person() {
  var self = this; // `self` 대신 `that`을 선택하는 사람도 있습니다.
  // 어느 한 쪽만 선택하고 그것만 사용하도록 하죠.
  self.age = 0;

  setInterval(function growUp() {
    // 콜백은 기대하는 객체의 `self` 변수를 참조합니다.
    self.age++;
  }, 1000);
}
또는 적절한 this 값이 growUp() 함수에 전달되도록, 
바인딩된 함수를 만들 수도 있습니다.

화살표 함수에는 this가 없습니다. 화살표 함수를 포함하는 객체 값이 사용됩니다. 
따라서 다음 코드에서 setInterval에 전달 된 함수 내의 this 값은 
화살표 함수를 둘러싼 함수의 this와 같은 값을 갖습니다.
function Person() {
  this.age = 0;

  setInterval(() => {
    // `this`는 person 객체를 가리킵니다.
    this.age++;
  }, 1000);
}

var p = new Person();

미리 정의된 함수들

JavaScript에는 몇 가지 최상위 레벨의 내장 함수가 있습니다:

eval()
: eval() 메소드는 문자열로 표현된 JavaScript 코드를 실행합니다.

uneval() 
: uneval() 메소드는 Object의 소스코드를 표현하는 문자열을 만듭니다.

isFinite()
: 전역 isFinite() 함수는 전달받은 값이 유한한지 결정합니다. 
  만약 필요하다면, 매개변수는 첫번째로 숫자로 변환됩니다.

isNaN()
: isNaN() 함수는 NaN인지 아닌지 결정합니다.

참고: isNaN 함수 안의 강제 변환은 흥미로운 규칙을 가지고 있습니다.
     Number.isNaN()을 이용해 값이 NaN인지 확인할 수 있습니다.
     ECMAScript6에서 정의된 값이 숫자값이 아닌 경우에는 typeof를 
     사용할 수도 있습니다.
     
parseFloat()
: parseFloat() 함수는 문자열 인수 값을 해석하여 부동소숫점 수를 반환합니다.

parseInt()
: parseInt() 함수는 문자열 인수 값을 수학적인 수 체계에 따라 해석하여 
  특정한 진법의 정수를 반환합니다.
  
decodeURI()
: decodeURI() 함수는 사전에 encodeURI을 통해 만들어지거나 
  비슷한 과정을 통해 만들어진 URI(Uniform Resource Identifier) 를 해독합니다.

decodeURIComponent()
: decodeURIComponent() 함수는 인코딩으로 이전에 생성된
  URI(encodeURIComponent)를 디코딩합니다. 
  URI(Uniform Resource Identifier) 또는 유사한 루틴을 사용합니다.

encodeURI()
: encodeURI() 메소드는 URI(Uniform Resource Identifier)를 
  각 인스턴스의 특정한 문자를 한 개, 두 개, 세 개 또는 네 개의 UTF-8인코딩으로 
  나타내어지는 연속된 확장문자들과 바꾸는 방법으로 부호화 합니다. 
  (두 "surrogate"문자로 구성된 문자들은 오직 네 개의 연속된 확장문자 입니다)

encodeURIComponent()
: encodeURIComponent() 메소드는 
  URI(Uniform Resource Identifier) 컴포넌트를 
  각 인스턴스의 특정한 문자를 한개, 두 개, 세 개 또는 네 개의 UTF-8 인코딩으로 
  나타내어지는 연속된 확장문자들과 바꾸는 방법으로 부호화 합니다.
  (두 "surrogate"문자로 구성된 문자들은 오직 네개의 연속된 확장문자 입니다.)

0개의 댓글