반응형

 내용은 이웅모 님의 모던 "자바스크립트 DeepDive" 책을 보고 정리한 내용입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.
 

렉시컬 스코프

함수가 선언된 위치에 따라 함수가 접근 가능한 변수들의 유효범위를 결정하는 개념으로, 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값에 의해 스코프안의 값이 결정된다.
 
예시)

const a = "x";

function second() {
  const x = "y";
  first();
}

function first(){
  console.log(a);
}

second(); // x
first(); // x

 
 

클로저(Closure)란?

  • 클로저는 함수와 해당 함수가 선언된 렉시컬 환경의 조합이다.
  • 이미 생명주기가 종료한 외부 함수의 변수를 참조하는 중첩 함수를 클로저라고 한다.
  • 클로저는 함수가 선언될 당시의 스코프를 기억하고, 이후에도 그 스코프에 접근할 수 있다.
  • 클로저에 의해 참조되는 상위 스코프의 변수자유 변수라고 한다.

예시) 외부 렉시컬 환경인 outerFunction의 실행 컨텍스트 스택이 제거되어도 전역 변수 closureFunction가 참조하고 있는 innerFunction이 outerFunction의 렉시컬 환경을 참조하고 있기 때문에 outerFunction의 변수를 사용할 수 있다.

const name = "jjmoon";

function outerFunction() {
  const name = "sjmoon";
  
  function innerFunction() {
    console.log("my name is " + name); // innerFunction은 outerFunction의 스코프에 접근할 수 있음
  }
  
  return innerFunction;
}
  
const closureFunction = outerFunction();
closureFunction(); // 출력 결과: "my name is sjmoon"

 
예시) 중첩함수내에 변수가 상위 스코프의 어떤 식별자도 참조하지 않는 경우 상위 스코프를 기억하지 않는다. 따라서 이와 같은 경우 innerFunction 함수는 클로저라고 할 수 없다.

function outerFunction() {
  const city = "seoul";
  function innerFunction() {
    const name = "sjmoon";
    console.log("my name is " + name); // innerFunction은 outerFunction의 스코프에 접근할 수 있음
  }
  
    return innerFunction;
}
  
const closureVal = outerFunction();
closureVal(); // 출력 결과: "my name is sjmoon"

 
 

클로저의 활용

클로저는 변수와 함수의 상태를 은닉하여 안전하게 변경 또는 유지를 위해 사용한다. 이를 통해 해당 변수나 함수의 변경이 불가능하며 특정함수에게만 상태 변경을 허용한다.
 
예시) raise와 lower 를통해 total 변수를 변경할 수 있다.

const totalPrice = (function() {
  // 총금액
  let total = 0;

  // 클로저와 메서드를 갖는 객체 반환한다.
  // 객체 리터럴은 스코프를 만들지 않으므로 즉시 실행 함수의 렉시컬 환경이다.
  return{
    // 총금액 증가
    raise(price){
      total += price
      return total;
    },

    // 총금액 감소
    lower(price){
      total-=price    
      return total >= 0 ? total : 0;;
    }
  };
}());

console.log(totalPrice.raise(10000)) // 10000
console.log(totalPrice.raise(20000)) // 20000

console.log(totalPrice.lower(25000)) // 5000
console.log(totalPrice.lower(10000)) // 0

 
예시) 생성자 함수로 클로저를 구현한 예시로 raise와 lower 메서드는 함수 정의가 평가되어 함수 객체가 될 때 실행 중인 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경을 기억하고 있다. 

const TotalPrice = (function() {
  // 총금액
  let total = 0;
  function TotalPrice(){

  }
  // 총금액 증가
  TotalPrice.prototype.raise = function(price){
    total += price;
    return total;
  }

  // 총금액 증가
  TotalPrice.prototype.lower = function(price){
    total -= price;
    total = total >= 0 ? total : 0;
    return total;
  }

  return TotalPrice;
}());

const totalPrice = new TotalPrice();

console.log(totalPrice.raise(10000)) // 10000
console.log(totalPrice.raise(20000)) // 20000

console.log(totalPrice.lower(25000)) // 5000
console.log(totalPrice.lower(10000)) //

 
예시) getTotalPrice 함수는 보조 함수를 인자로 전달받고 다시 함수를 반환하는 고차 함수이다. 이는 자신이 생성됐을 때의 렉시컬 환경 함수의 스코프에 속한 total 변수를 기억하는 클로저이다. 또한 getToTalPrice 함수를 호출할 때마다 새로운 렉시컬 환경이 생성되기에 반환된 함수는 자신만의 독립된 렉시컬 환경을 가진다.

function getTotalPrice(fc, price){
    // 총금액
    let total = 20000;
    return function(){
      total = fc(total, price);
      return total;
    };
}

function raise(total, price){
  total += price;
  return total;
}

function lower(total, price){
  total -= price;
  total = total >= 0 ? total : 0;
  return total;
}

const raised = getTotalPrice(raise, 10000)
console.log(raised()) // 30000
console.log(raised()) // 40000

const lowered = getTotalPrice(lower, 10000)
console.log(lowered()) // 10000
console.log(lowered()) // 0

 
예시) 총금액을 유지하기 위한 자유 변수 total을 기억하는 클로저를 반환한다. 이는 렉시컬 환경을 서로 공유한다.

// 함수 반환하는 고차 합수
const getTotalPrice = (function(){
    // 총금액
    let total = 20000;
    // 함수를 인수로 전달받는 클로저를 반환
    return function(fc, price){
      total = fc(total, price);
      return total;
    };
}());

function raise(total, price){
  total += price;
  return total;
}

function lower(total, price){
  total -= price;
  total = total >= 0 ? total : 0;
  return total;
}

// 보조함수 전달 후 호출
console.log(getTotalPrice(raise,10000)) // 30000
console.log(getTotalPrice(raise,10000)) // 40000

console.log(getTotalPrice(lower,10000)) // 30000
console.log(getTotalPrice(lower,10000)) // 20000

 
 

캡슐화와 정보은닉

  • 캡슐화는 객체의 상태(데이터)와 행위(메서드)를 하나로 묶는 것을 말하는 객체 내부의 데이터와 메서드들은 서로 관련성이 높고 응집력이 있어야 한다.
  • 캡슐화를 통해 외부에서 객체의 내부 데이터에 직접 접근하거나 수정하는 것을 제한함으로써 안정성과 유지보수성을 높일 수 있다.
  • 정보 은닉은 캡슐화의 결과물 객체의 상태를 외부에서 직접 접근하지 못하도록 숨기는 것을 의미한다.
  • 객체의 내부 데이터에 직접 접근할 수 없도록 private하게 보호하고, 외부에서는 공개된 인터페이스를 통해서만 객체와 상호작용할 수 있도록 한다.

예시) 아래 패턴을 사용하면 접근 제한자 없이 정보 은닉이 가능하지만 Member 생성자 함수가 여러개 의 인스턴스를 생성할 경우 _age 변수 상태가 유지되지 않는다. 이는 Member.prototype.hi 메서드가 단 한 번 생성되는 클로저 이기에 발생하는 현상이다.

const Member = (function(){
  let _age = 0; // private

  // 생성자 함수
  function Member(name, age){
    this.name = name; // public
    _age = age;
  }

  // 프로토타입 메서드
  Member.prototype.hi = function(){
    console.log(`Hi My nema is ${this.name}. I am ${_age}`)
  }

  return Member;
}());

const member = new Member('sjmoon', 28)
member.hi(); // Hi My nema is sjmoon. I am 28
console.log(member.name); // sjmoon
console.log(member._age); // undefined

const member2 = new Member('jjmoon', 23)
member2.hi(); // Hi My nema is jjmoon. I am 23
console.log(member2.name); // jjmoon
console.log(member2._age); // undefined

 
 

클로저 사용시 주의점

예시1 ) var 키워드로 선언된 변수인 'i'가 전역 변수로 처리되어, 반복문에서 생성된 함수들이 모두 같은 'i'를 참조하여 마지막으로 할당된 값인 3이 출력되는 문제가 발생한다.

var arr = [];

// 1.
for(var i = 0; i < 3; i++){
  arr[i] = function(){ return i; };
}

// 1.
for(var j = 0; j < arr.length; j++){
  console.log("arr: " + arr[j]());
}
// arr: 3
// arr: 3
// arr: 3

 
예시2 ) 즉시 실행 함수의 매개변수 id 반환값의 상위 스코프이기에 반환한 중첩 함수는 해당 렉시컬 환경을 기억하는 클로저이고 매개변수 id는 즉시 실행 함수가 반환한 중첩함수에 묶여있는 자유변수가 되어 값이 유지된다.

var arr2 = [];

// 입력
for(var i = 0; i < 3; i++){
  arr2[i] = (function(id) {
    return function(){
      return id
    };
  }(i));
}

// 출력
for(var j = 0; j < arr.length; j++){
  console.log("arr2: " + arr2[j]());
}
// arr2: 0
// arr2: 1
// arr2: 2

 
예시3 ) let 키워드로 선언한 변수 사용 시 for문의 코드 블록이 실행 될때마다 렉시컬 환경이 새로 생성되기에 해당 값이 유지된다.

var arr3 = [];

// 3.
for(let i = 0; i < 3; i++){
  arr3[i] = function(){ return i; };
}

// 3.
for(var j = 0; j < arr.length; j++){
  console.log("arr3: " + arr3[j]());
}
// arr3: 0
// arr3: 1
// arr3: 2

 

반응형

+ Recent posts