본문 바로가기
FRONTEND/Javascript

Javascript Function - 함수 표현식

by 또야또야 2020. 9. 23.
반응형

재귀

재귀함수는 일반적으로 함수가 자기자신을 이름으로 호출하는 형태입니다.

function factorial (num) {
  if (num <= 1) return 1
  else return num * factorial(num - 1)
}

console.log(factorial(4))   // 24

클로저

클로저(closuer)란 다른 함수의 스코프에 있는 변수에 접근 가능한 함수 (다른 함수 내부에 정의된 함수) 입니다. 따라서 외부 함수의 변수에 접근 가능합니다. 

function comparison (prop) {             // A scope
  return function (obj1, obj2) {         // B scope
    const val1 = obj1[prop]
    const val2 = obj2[prop]
    
    if (val1 < val2) return -1
    else if (val1 > val2) return 1
    else return 0
  }
}

이 예제는 내부함수 (B scope)이면서 외부 함수(A scope)의 변수(prop) 에 접근합니다. 내부 함수의 scope 체인에 comparison 함수의 스코프가 포함되기 때문에 내부 함수(b)가 반환되어 다른 컨텍스트에서 실행되는 동안에도 prop 에 접근할 수 있습니다.
스코프 체인에 대해서 여기에서 설명했습니다 >>

다른 함수의 내부에 존재하는 함수는, 외부 함수의 활성화 객체 (내부에서 사용하는 매개변수 - arguments 또는 이름 붙은 매개변수) 를 자신의 스코프 체인에 추가하여 자기 자신의 매개변수뿐 아니라 외부 함수의 활성화 객체를 가리키는 포인터가 존재하므로 외부 함수의 변수에 접근할 수 있습니다..

내부에서 동작하는 방식은,

  1. 함수를 호출하면 실행 컨텍스트가 생성되며, 외부 함수의 [[ Scope ]] 프로퍼티에 들어있는 객체를 복사하여 스코프 체인이 생성됩니다.

  2. 다음에는 활성화 객체가 생성되어 컨텍스트의 스코프 체인 맨 앞에 위치합니다.

  3. 이 과정이 내부에 존재하는 함수에서 계속 발생하여 스코프 체인이 전역 실행 컨텍스트에서 종료될 때 까지 이어집니다.

함수를 실행하면 사용할 변수를 스코프 체인에서 검색합니다. 전역 컨텍스트의 변수 객체는 항상 존재하지만, 개발자에 의해 정의된 로컬 컨텍스트 변수는 함수를 실행하는 동안에만 존재합니다. 그러므로 함수가 클로저를 반환했다면 해당 함수의 스코프는 클로저가 존재하는 동안에는 메모리에 계속 존재합니다.

일반적으로 함수가 실행을 마치면 함수의 스코프 및 그에 담긴 변수 전체가 파괴됩니다.

클로저와 변수

클로저 내부  this 객체

일반적으로 this 는 런타임에서 함수가 실행중인 컨텍스트에 묶입니다. 즉 전역 함수에서 this  는 strict모드가 아닐 때는 window , strict 모드에서는 undefined 이며 함수가 객체 메서드로 호출되었을 때 this 는 해당 객체입니다.
이 컨텍스트에서 익명함수는 특정 객체에 묶여있지 않으므로 스트릭트 모드가 아니라면 this  객체는 window  이며 스트릭트 모드에서는 undfined 입니다.

그러나 클로저 내부의 this 객체는 꽤 복잡하게 동작하며, 클로저를 작성하는 방식 때문에 이를 분명히 알기는 어렵습니다.

const object = {
  name: 'My Obj',
  getName () {
    return this.name
  },
  getWindow () {
    return function () {
      return this
    }
  }
}

console.log(object.getName())        // My Obj
console.log(object.getWindow()())    // [object Window]

내부 함수는 결코 외부함수의  this  arguments 에 직접적으로 접근할 수 없습니다.
다음과 같이 외부함수의 this 객체를 다른 변수에 저장해서 클로저가 이 변수에 접근하도록 하는일은 가능합니다.

const name = 'the window'

const object = {
  name: 'My Object',
  
  getNAmeFunc: function () {
    const that = this;
    return function () {
      return that.name
    }
  }
}

console.log(object.getNameFunc()())     // My Object

메모리 누수

클로저는 IE9 이전 버전에선 메모리 문제를 일으키는데, 이는 자바스크립트 객체와 COM 객체에 사용하는 가비지 컬렉션 방법이 다르기 때문입니다.

function assignHandler () {
  const element = document.getElementById('someElement')
  const id = element.id
  
  element.onClick = function () {
    console.log(id)
  }
  
  element = null
}

예제에서, 익명함수는 assignHandler() 함수의 활성화 객체에 대한 참조를 계속 유지하는데, 이 때문에 element 에 대한 참조 카운트가 줄어들지 않습니다. 익명함수가 존재하는 한 참조 카운트는 최소 1이고 따라서 메모리를 회수할 수 없습니다.

그러나 마지막에  element  id 사본을 변수에 저장하여 순환참조를 막았습니다. 하지만 이것만으로 메모리 문제를 해결할 수는 없으므로, 필요하다면  element  변수에  null 을 할당하여 COM 객체에 대한 참조를 제거하고, 참조 카운트도 감소하므로 메모리를 회수할 수 있습니다.

블록스코프 흉내내기

자바스크립트에는 블록 레벨 스코프라는 개념이 없으므로 블록 문장에서 정의한 변수는 해당 문장이 아니라 외부 함수에 묶입니다.
익명함수를 블록 스코프 처럼 쓰는 문법은 고유 스코프 라고 부르기도 하며 다음과 같은 형태입니다.

(function () {
  // code block
})()

여기서는 익명함수를 정의하는 즉시 호출하므로 즉시호출 함수라고 부르기도 합니다. 함수 선언처럼 보이는 부분을 괄호로 감싸, 이 구문이 함수 표현식임을 나타냅니다. 

고유 변수

엄밀히 말해 자바스크립트에는 고유 객체 프로퍼티 라는 개념이 없으며 객체 프로퍼티는 모두 공용(public) 입니다.
하지만 '고유 변수' 라는 개념은 있습니다. 함수 안에서 정의한 변수는 함수 밖에서 접근할 수 없으므로 모두 고유변수 라고 간주합니다. 여기에는 함수 매개변수, 지역변수, 내부 함수 등이 속합니다.

function add (num1, num2) {
  const sum = num1 + num2
  return sum
}

이 함수에는 num1 , num2 ,  sum 세 가지 고유 변수가 있습니다. 함수 내부에서는 이들 변수에 접근할 수 있지만 함수 밖에서는 불가능합니다. 특정 스코프에 클로저를 만들어 사용하면 외부 스코프에 정의된 고유 변수에 접근 가능한 공용 메서드를 만들 수 있습니다.

고유 변수/함수에 접근 가능한 공용 메서드를 특권(privileged) 메서드 라고 부릅니다.
특권 메서드를 구현하려면 (1) 생성자나 프로토타입 패턴을 이용하고, 싱글톤에서 구현하려면 (2) 모듈이나 모듈 확장 패턴을 이용합니다.

첫 번째 방법은 다음과 같이 1. 생성자 안에서 만드는 방법입니다.

function MyObject () {
  // private var
  const num = 10
  
  // private func
  function foo () { return false }
  
  // privilaged methods
  this.boo = function () {
    num++
    return foo()
  }
}

이 패턴은 생성자 함수 내부에서 모든 고유 변수와 함수를 정의하여 앞에서 이미 정의한 고유 멤버에 접근 가능한 특권 메서드를 만들 수 있습니다. 이 패턴이 가능한 것은 생성자 안에서 정의한 특권 메서드가 클로저가 되어서 생성자의 스코프 안에서 정의된 모든 변수와 함수에 접근할 수 있기 때문입니다. 이 예제의 변수 num 과 함수  foo()  는 boo() 에서만 접근 가능합니다. 일단  MyObject 인스턴스를 생성하면 num  foo() 에 직접적으로 접근할 방법은 없으며 반드시 boo() 를 통해야합니다.

다음과 같이 고유 및 특권 멤버를 정의해서 데이터를 직접적으로 수정할수 없게 보호할 수도 있습니다.

function Person (name) {
  this.getName = function () {
    return name
  }
  
  this.setName = function (value) {
    name = value
  }
}

const p1 = new Person('JJD')
console.log(p1.getName())  // JJD

p1.setName('GGB')
console.log(p1.getName())  // 'GGB'

이 코드의 생성자는 getName setName 두 메서드를 정의합니다.  Person 생성자 외부에서  name 변수를 직접 변경할 방법은 없습니다. 두 메서드는 모두 생성자 안에서 정의된 클로저이므로 스코프 체인을 따라  name 에 접근가능합니다.
한가지 단점은 오직 생성자 패턴을 통해서만 이런 결과가 가능하다는 것입니다. 이전에 설명했듯 생성자 패턴이는 메서드가 인스턴스마다 생성된다는 결점이 있는데, 정적 고유 변수를 사용해 특권 메서드를 만들면 이 문제는 사라집니다.

모듈 패턴

모듈패턴싱글톤에서(인스턴스를 단 하나만 갖게 의도한 객체) 고유 변수와 메서드를 활용해만든 것입니다. 전통적으로 자바스크립트에서 싱글톤을 만들 때는 다음 예제와 같이 리터럴 표기법을 썼습니다.

const singleTon = {
  name: 'JJD',
  method () {
    // method code..
  }
}

이런 방식으로 생성한 싱글톤은 객체 리터럴을 이용했으므로 모두 Object 의 인스턴스입니다.
모듈 패턴은 다음 형식에 따라 기본 싱글톤을 확장해서 고유 변수와 특권 메서드를 쓸 수있습니다.

const singleTon = function () {
  // private var & func
  let num = 10
  
  function foo () { return false }
  
  // privilaged / shared methods & props
  return {
    boo: true,
    coo () {
      num++
      return foo()
    }
  }
}

모듈 패턴은 객체를 반환하는 익명함수를 사용합니다.(반드시 그래야 하는 것은 아닙니다만 규칙임?) 반환된 객체는 익명함수 내에 정의되어 있으므로 특권 메서드 내부의 공용 메서드는 모두 고유 변수와 함수에 접근가능합니다. 모듈패턴은 (1) 하나의 객체를 반드시 생성 - 싱글톤 초기화 하고 (2) 몇 가지 데이터를 가지며 또한 (3) 고유 변수에 접근 해야할 때 유용합니다. 

모듈 확장 패턴

모듈 패턴에서 한 가지 더 살펴볼 것은 객체를 반환하기 전에 확장하는 패턴입니다. 이 패턴은 싱글톤 객체가 특정 타입의 인스턴스지만 프로퍼티나 메서드를 추가하여 확장해야할 때 유용합니다.

반응형

'FRONTEND > Javascript' 카테고리의 다른 글

ECMAScript 2020 - ES11  (0) 2021.01.08
P_브라우저 객체 모델  (0) 2020.10.06
Javascript의 프로토타입  (0) 2020.09.03
Javascript의 this  (0) 2020.08.19
Javascript Function - Function 타입  (0) 2020.08.03

댓글