[JS] #8 논리적 정리

재귀함수

프로퍼티 연동방지

var origin = { member: 100 };
var dup = origin;
dup.member = 200;
console.log(origin.member);
// 200
  • Object에 Object를 할당하면 프로퍼티 값이 연동됩니다.
    ⇒ origin 오브젝트를 dup변수에 할당 후 dup.member에 200을 할당하면, origin.member의 값이 연동되어 바뀝니다. 오브젝트를 할당하면 값을 공유하기 때문입니다.
var origin = [1, 2, 3];
var dup = origin;
dup[1] = 200;
console.log(origin);
// [1, 200, 3]
  • 배열도 마찬가지로 연동됩니다.
var origin = {member: 100};
var dup = {};
for (var name in origin) {
  dup[name] = origini[name];
};
dup.member = 200;
console.log(origin.member);
console.log(dup.member);
// 100
// 200
  • 연동 방지 : 프로퍼티 단위로 할당해야 합니다.

재귀함수 형태

Recursive Function: 함수 안에서 자신 함수를 호출하는 형태

  • 사용 사례
    • {name: {name: {name: value}}}
    • [ [ 1, 2 ], [3, 4] , [5, 6] ]
var book = {
  member: {name: 100},
  point: {value: 200}
};
function show(param){
  for(var type in param){
    typeof param[type] === "object"
      ? show(param[type])
      : console.log(type + ":", param[type]);
  }
};
show(book);
// name: 100
// value: 200
  1. show(book);
    • 마지막 줄에서 show(book)를 호출하며 book 오브젝트를 파라미터 값으로 넘겨줍니다.
  2. for(var type in param) {...}
    • for-in으로 파라미터로 받은 오브젝트를 전개합니다.
  3. typeof param[type] === "object" ? show(param[type]) : console.log(type + ":", param[type]);
    • param[type] 타입이 "object"이면 자기 자신을 호출하면서 show()에 param[type]을 넘겨줍니다.
    • book["member"]이므로 {name: 100}이 넘어갑니다.
  4. param[type]타입이 "object"가 아니면 member: {name: 100}에서 {name: 100}을 읽은 것이기에 값을 출력합니다.

예제

데이터 형태

var member = {
Jan: {item: {title: "JS북", amount: 100}, point: [10, 20, 30]},
Feb: {item: {title: "JS북", amount: 200}, point: [40, 50, 60]}
}

  • 재귀 함수로 데이터를 출력하세요.
    • 오브젝트이면 프로퍼티 이름(title, amount)과 값을 출력하고
    • 배열이면 값([10, 20, 30])을 출력하고 값을 누적하세요.
  • 재귀 호출이 끝나면 누적한 값을 출력하세요.
var member = {
  Jan: {item: {title: "JS북", amount: 100},
    point: [10, 20, 30]},
  Feb: {item: {title: "JS북", amount: 200},
    point: [40, 50, 60]}
};
var total = 0;
// 배열의 값을 출력하고 누적하세요.
function reduce(arr) {
  var result = arr.reduce((a, b) => a + b);
  total += result;
  console.log(result);
};
// 프로퍼티 이름과 값을 출력하세요.
function show(param) {
  for(var type in param){
    if(param[type] instanceof Array) {
      console.log(param[type]);
      reduce(param[type]);
    } else if(typeof param[type] === "object") {
      show(param[type])
    } else {
    console.log(type + ":", param[type]);
    }
  }
};
show(member);
console.log(total);
// title: JS북
// amount: 100
// [10, 20, 30]
// 60
// title: JS북
// amount: 200
// [40, 50, 60]
// 150
// 210

즉시 실행 함수

(function() {
  console.log("JS북");
}());
// JS북
  • 함수 즉시 실행이란?
    엔진이 함수를 만났을 때 자동으로 함수를 실행하는데, 즉시 실행하기에 즉시 실행함수라 합니다.
  • IIFE: Immediately Invoked Function Expression
  • (function(){...}())형태
    함수 이름이 없으므로 함수 선언문, 함수 표현식도 아닙니다.
    문법 에러가 발생하지 않습니다.
    무명 함수, 익명함수라고도 부릅니다.

함수 즉시 실행 과정

var total = (1 + 2);
console.log(total);
  • 표현식을 평가
    소괄호 () 는 그룹핑 연산자입니다.
  • (1+2) 형태에서 소괄호는 그룹핑 연산자이며 1 + 2는 표현식입니다.
  • 그룹핑 연산자는 소괄호 안의 표현식을 평가하고 결과를 반환합니다.
  • 소괄호와 표현식 평가가 포인트입니다.
var value = function() {
  return 100;
};
console.log(value());
  • 함수 이름 필요
    함수에 이름이 없으면 문법에러가 발생합니다.
  • 함수 표현식으로 엔진이 function 키워드를 만나면 function오브젝트를 생성하여 value 변수에 할당합니다.
  • value 변수를 선언하지 않으면 함수 이름이 없기에 문법에러가 발생합니다. 함수 표현식도, 함수 선언문도 아니기 때문입니다.
  • value()처럼 function 끝에 소괄호를 첨부하면 함수로 호출합니다. 이때, 소괄호()는 그룹핑 연산자가 아닌 함수 호출의 역할을 합니다.
var value = function() {
  return 100;
}();
console.log(value);
  • 함수 표현식 끝에 소괄호 작성
  • function 키워드를 만나 function 오브젝트를 생성합니다.
  • 소괄호가 있으므로 즉시 함수를 실행합니다.
  • 함수에서 반환한 100을 value 변수에 할당합니다.
var value = (function() {
  return 100;
}());
console.log(value);
  • 소괄호( )에 함수 작성
  • 소괄호는 그룹핑 연산자이기에 소괄호 안의 표현식을 평가합니다.
  • 표현식이 함수이므로 function 오브젝트를 생성합니다.
  • function 끝에 소괄호가 있으므로 함수를 실행합니다.
  • 함수 실행결과 값 100을 value에 할당합니다.
(function() {
  console.log(100);
}());

그룹핑 연산자에서 반환된 값이 할당되는 변수를 작성하지 않은 형태

  1. 그룹핑 연산자를 작성하지 않으면 함수 이름이 없어 문법에러가 납니다.
  2. 하지만, 그룹핑 연산자를 작성하면 표현식에 function을 작성한 것이기에 문법에러 발생하지 않습니다. 즉, (1 + 2)에서 1+2 대신 함수를 작성한 것입니다.
  3. 표현식과 표현식 평가 결과는 평가 결과가 반환할 때까지 메모리에 저장하고 평가 결과를 반환하면 지워집니다.
  4. (1+2)의 결과가 메모리에 저장된다면 메모리 낭비입니다.
  5. function(){}();
    코드로 만든 오브젝트도 메모리에 저장되지 않으며(key가 없음) 실행결과도 메모리에 저장되지 않습니다.
  6. 그렇기에 저장해야 한다면 표현식 밖에 변수, 프로퍼티에 저장해야 합니다.
  7. 저장할 필요가 없는 1회성 코드이며 엔진이 function 키워드를 만나는 시점에 즉시 실행해야 한다면 그룹핑 연산자 안에 표현식으로 작성합니다.
  8. 무명 함수는 그룹핑 연산자 안의 코드를 한 번만 사용할 때 사용합니다. 주로 초기값을 설정할 때 사용합니다.

클로저(Closure)

  • function 오브젝트를 생성할 때 함수가 속한 스코프를 [[Scope]]에 설정하고, 함수가 호출되었을 때 [[Scope]]의 프로퍼티를 사용하는 메커니즘입니다.
  • [[Scope]]의 설정과 사용방법을 이해하면 클로저는 단지 논리적인 설명입니다.

클로저 논리

실행 콘텍스트: {
  렉시컬 환경 컴포넌트: {
    환경 레코드: {
      선언적 환경 레코드: {},
      오브젝트 환경 레코드: {}
    },
    외부 렉시컬 환경 참조: {}
  }
}

외부 렉시컬 환경 참조에 선언된 변수, 함수를 내 것처럼 사용하는 것

  • 실행중인 function 오브젝트에 작성한 변수, 함수를 선언적 환경 레코드에 설정합니다.
  • [[Scope]]의 변수, 함수를 외부 렉시컬 환경 참조에 바인딩합니다.
  • 변수 이름으로 접근하여 값을 사용하거나 변경할 수 있고
    함수를 호출할 수 있습니다.

논리 전개

function book() {
  var point = 100;
  var getPoint = function(param) {
    point = point + param;
    return point;
  };
  return getPoint;
};
var obj = book();
console.log(obj(200));
  1. var obj = book();
    • book()을 호출하면 엔진은 아래 방법으로 처리합니다.
    • getPoint()의 클로저가 만들어집니다.

<실행 준비 단계>

  1. 실행 콘텍스트(EC) 생성
  2. 3개의 컴포넌트 생성
    • 렉시컬/변수 환경 컴포넌트, this 바인딩 컴포넌트
  3. function 오브젝트의 [[Scope]]를 외부 렉시컬 환경 참조에 바인딩
    실행 콘텍스트: {
      렉시컬 환경 컴포넌트: {
        환경 레코드: {
          선언적 환경 레코드: {},
          오브젝트 환경 레코드: {}
        },
        외부 렉시컬 환경 참조: {[[scope]]}  // 글로벌 스코프
      },
      변수 환경 컴포넌트 = {Same}
      this 바인딩 컴포넌트: {}
    }

<초기화 및 실행 단계>

  1. var point; var getPoint;
    • 변수 이름을 선언적 환경 레코드에 설정합니다(undefined).
  2. var point = 100;
    • 선언적 환경 레코드의 point에 100을 할당합니다.
  3. var getPoint = function(param){코드};
    • function 오브젝트 생성
    • 스코프를 [[Scope]]에 바인딩
    • point:100이 [[Scope]]에 바인딩 됩니다.
    • getPoint 오브젝트의 모습
      렉시컬 환경 컴포넌트 = {
        환경 레코드: {
          선언적 환경 레코드: {},
          },
        외부 렉시컬 환경 참조: {
          point: 100
        }
      },
  4. return getPoint;
    • getPoint function 오브젝트를 반환합니다.
  5. var obj = book();
    • return getPoint에서 반환한 getPoint function오브젝트를 obj에 할당합니다.
  6. console.log(obj(200));
    • obj()를 호출하면 getPoint(200) 함수가 호출됩니다.

<클로저 관련 부분>

  1. 실행 콘텍스트(EC) 생성
    • getPoint 오브젝트의 [[Scope]]를 외부 렉시컬 환경 참조에 바인딩합니다.
    • 파라미터 이름에 값을 매핑 후 결과를 선언적 환경 레코드에 설정합니다.
    • getPoint 오브젝트의 모습
      렉시컬 환경 컴포넌트 = {
      환경 레코드: {
          선언적 환경 레코드: {
            param: 200
          },
        },
        외부 렉시컬 환경 참조: {
          point: 100
        }
      },
  2. 함수 안의 코드 실행
  3. point = point + param:
    • point를 선언적 환경 레코드에서 식별자 해결
      • point가 없기에 외부 렉시컬 환경 참조에서 식별자 해결
      • point가 있으며 값은 100입니다.
    • param을 선언적 환경 레코드에서 식별자 해결
      • param이 있으며 값이 200입니다.
    • 100과 200을 더해 외부 렉시컬 환경 참조의 point에 할당
  4. 변수가 선언적 환경 레코드에 없으면 외부 렉시컬 환경 참조에서 식별자 해결

결론: 내부에서 외부 렉시컬 환경참조 [[Scope]]에 있는 변수, 함수를 자기 것처럼 사용하는 것

예시

function book(bookParam) { 
  var point = 100;
  function getpoint(pointParam) {
    point = point + bookParam + pointParam;
    return point;
  };
  return getPoint;
};
var obj = book(200);
console.log(obj(400));
  1. var obj = book();
    • book()을 호출하면 엔진은 아래 방법으로 처리합니다.
    • getPoint()의 클로저가 만들어집니다.

<실행 준비 단계>

  1. 함수 선언문의 function 키워드를 만나 book 오브젝트가 생성
  2. 실행 콘텍스트(EC) 생성
  3. 3개의 컴포넌트 생성
    • 렉시컬/변수 환경 컴포넌트, this 바인딩 컴포넌트
  4. function 오브젝트의 [[Scope]]를 외부 렉시컬 환경 참조에 바인딩
  5. book(200) 호출
  6. 호출 파라미터 bookParam는 book 오브젝트의 선언적 환경 레코드에 설정 { bookParam: 200 }
  7. getPoint 오브젝트가 생성 → [[scope]] 는 book 오브젝트를 바인딩

<초기화 및 실행 단계>

  1. var point;
    • 변수 이름을 선언적 환경 레코드에 설정합니다.
  2. var point = 100;
    • 할당 및 실행 으로 book 오브젝트의 선언적 환경 레코드는 { bookParam : 200, point: 100 }로 설정됩니다.
    • getPoint 오브젝트의 외부 렉시컬 환경 참조도 { bookParam : 200, point : 100 } 형태가 됩니다.
실행 콘텍스트: {
  렉시컬 환경 컴포넌트: {
    환경 레코드: {
      선언적 환경 레코드: {bookParam : 200, point: 100},
      오브젝트 환경 레코드: {}
    },
    외부 렉시컬 환경 참조: {[[scope]]}  // 글로벌 스코프
  },
  변수 환경 컴포넌트 = {Same}
  this 바인딩 컴포넌트: {}
}
  1. return getPoint; / var obj = book(200);
    • getPoint 오브젝트를 반환 obj 에 할당
  2. console.log(obj(400));
    • obj()를 호출하면 book.getPoint(400) 함수가 호출됩니다

<클로저 관련 부분>

  1. 실행 콘텍스트(EC) 생성
    • 파라미터 이름에 값을 매핑 후 결과를 선언적 환경 레코드에 설정합니다.
// obj(400) 호출 된 이후의
// getPoint 오브젝트 형태
렉시컬 환경 컴포넌트 = {
    환경 레코드 : {
        선언적 환경 레코드 : { pointParam: 400 }
    }
    외부 렉시컬 환경 : {
        bookParam: 200,
        point: 100
    }
}
  1. 함수 안의 코드 실행
  2. point = point + bookParam + pointParam
    • point를 선언적 환경 레코드에서 식별자 해결
      • point가 없기에 외부 렉시컬 환경 참조에서 식별자 해결
      • point가 있으며 값은 100입니다.
    • bookParam을 선언적 환경 레코드에서 식별자 해결
      • bookParam가 없기에 외부 렉시컬 환경 참조에서 식별자 해결
      • bookParam가 있으며 값이 200입니다.
    • 100과 200, 그리고 선언적 환경 레코드의 400을 더해 외부 렉시컬 환경 참조의 point에 할당
  3. 변수가 선언적 환경 레코드에 없으면 외부 렉시컬 환경 참조에서 식별자 해결

클로저와 무명 함수

  • 무명 함수 안에 작성한 값, 함수는 무명 함수가 끝나면 지워집니다. 그렇기에 다시 사용하려면 저장이 필요합니다.
  • 한편, 무명 함수는 저장하지 않으려는 의도로 사용합니다.

클로저 활용

  • 클로저는 함수 밖 스코프의 변수와 함수를 사용할 수 있습니다.
  • 변수는 외부에서 직접 접근할 수 없으므로 정보 보호
  • 무명 함수 안에서 클로저의 변수를 가진 함수를 반환하면 함수의 재사용과 정보 보호를 할 수 있습니다.

▶ 클로저를 통한 캡슐화와 은닉화

분석

var book = (function() {
  var point = 100;
  function getPoint(param){
    return point + param;
  };
  return getPoint;
}());
console.log(book(200));
  1. function getPoint(param){...}
    • [[Scope]]에 function 오브젝트를 스코프 설정
  2. return getPoint;
    • 즉시 실행 함수에서 getPoint 함수를 반환합니다.
    • book 변수에 할당합니다.
    • point 변수 값을 사용할 수 있습니다.
  3. console.log(book(200));
    • 반환된 함수를 호출하며 200을 파라미터 값으로 넘겨줍니다.
  4. function getPoint(param){ return point + param; };
    • getPoint function 오브젝트의 [[Scope]]에 point 사용

함수 안에 있는 내용들이 날아가지 않고 반환하게 되므로서 살아있게 됩니다. 내부 변수인 point 또한 바인딩되어 있기에 날아가지 않습니다.

JS에서 클로저

함수에서 함수 밖의 변수 사용은 JS의 기본 메커니즘입니다. 이는 외부 렉시컬 환경 참조에 함수가 속한 스코프가 설정되기 때문인데 클로저는 이를 나타내는 용어로 용어 자체보다는 논리적 구조를 이해하는 것이 중요합니다.

좋은 웹페이지 즐겨찾기