Primitive & Reference / Scope / Closusre

holang-i·2021년 3월 10일
0

JavaScript

목록 보기
8/25

Primitive & Reference

Primitive Type

number, string, boolean과 같은 고정된 저장공간을 차지하는 데이터를 말한다.

자바스크립트에서의 원시 타입의 데이터는 객체가 아니면서 메소드를 가지지 않는 6가지의 타입을 말한다.

string, number, bigint, boolean, undefined, symbol이 있다.

null의 경우 원시타입과 거의 같게 사용되지만, 엄밀하게 따지면 원시타입이라고 볼 수 없기 때문에 이에 대한 부분은 추가로 더 공부해 봐야 된다.

원시 자료형은 모두 1개의 정보를 담고 있다.


Reference Type

저장공간이 고정되어있지 않고, 값을 유동적으로 줄이고 늘릴 수 있다.
어떻게 배열과 객체는 저장공간이 계속해서 늘어날 수 있을까?

배열과 객체는 특별한 저장 공간을 사용한다.

// 함수 표현식으로 숫자 2개의 값을 더하는 함수 선언
let sumFunction = function(num1, num2) {
  let result = num1 + num2;
  return result;
}

// 함수 호출, 사용
let sumResult = sumFunction(3024, 38); // 3062

위의 함수를 선언하고, 함수를 사용할 때 Reference Type의 값이 어떻게 되는지 알고있어야 된다.

원시 자료형과 참조 자료형에 대해 알아야 될 것

  • 원시 자료형(primitive type)과 참조 자료형(reference type)의 구분과 차이를 설명 할 수 있어야 된다.
  • 원시 자료형이 할당될 때에는 변수에 값(value) 자체가 담긴다.
  • 참조 자료형이 할당될 때는 특별한 저장공간에 주소(reference)가 담긴다.
  • 참조 자료형은 기존에 고정된 크기의 저장공간(보관함)이 아니라,
    동적으로 크기가 변하는 특별한 저장공간(보관함)을 사용한다.

primitive data VS reference data

primitive data

string, number, boolean, null, undefined의 원시타입의 데이터를 다룰 때를 먼저 살펴볼 것이다.

변수 num1을 선언하면, 메모리에서 일어나는 작업을 살펴볼 것이다.

stack이라는 메모리에 한켠에 num1이라는 자리를 차지하게된다.
그리고 num1에 7이라는 값을 할당하면, 메모리에서 num1이라는 값을 찾아서 num1에 7을 할당해 주게 된다.

그리고 num1을 찾을 때도 메모리에서 값을 찾아서 num1의 값을 반환하게 된다.

let num1;
num1 = 7;

메모리에 여러가지 값들을 넣는다.

let str1 = 'Hello JavaScript!';
let bool1 = true;
let bool2 = false;
let num1 = 100;
let value1 = null;
let value2 = undefined;

reference data

배열, 객체, 함수와 같이 하나의 변수안에 많은 데이터가 있을 때, 각각의 데이터를 찾을 때 많은 시간이 걸릴 수 있다.
그렇기 때문에 참조타입은 조금 다른 방식으로 값을 저장하게 된다.

메모리에는 stack영역 말고 또 다른 영역인 heap이라는 빈 공간이 있다.

stack영역에는 값 대신 변수의 이름을 넣고, 값 대신에 주소를 넣는다.
그 주소는 heap에 연결되어 있다.

값을 넣을 때는 stack에 있는 변수의 주소를 찾아서 그 주소를 찾아가서 heap안에 값들을 넣어준다.

값을 찾을 때는 해당 변수의 주소를 heap에서 찾은다음 반환하게 된다.

이렇게 되면 값을 빼거나 넣을 때 주소를 찾아서 처리하게 된다.


원시타입 데이터의 복사, 값 변경

원시타입 데이터는 각 변수간에 원시타입 데이터를 복사할 경우 데이터의 값이 복사된다.

let num1 = 10;
let num2 = num1;
console.log(num1);
console.log(num2);
num2 = 30;
console.log(num1);
console.log(num2);

위의 코드를 실행해본 결과를 살펴보면 기존 데이터에 영향이 가지 않는다.


변수에는 1개의 데이터만이 담기게 된다.

그리고, 원시 자료형은 값 자체에 대한 변경이 불가능(immutable)하지만, 변수에 다른 데이터를 할당할 수는 있다.

let num1 = 12345;
let num2 = 300;

// 변수에 다른 데이터를 다시 할당
num1 = 369;
num2 = num1;

참조타입 데이터의 복사, 값 변경

참조타입 데이터는 주소를 복사한다.

그렇기 때문에 복사한 데이터에서 원소를 변경하면 주소 안에 있는 데이터가 변경된다.

그렇기 때문에 기존의 데이터가 영향을 받게된다.

let arr1 = [1, 3, 5, 7, 9];
let arr2 = arr1;
console.log(arr1);
console.log(arr2);
arr2[1] = 'arr2의 2번째 요소의 값을 변경했어요!';
arr2[3] = 100;
console.log('arr2의 2번째, 4번째의 값 변경');
console.log(arr1);
console.log(arr2);

위의 코드를 실행해본 결과를 살펴보면, 기존 데이터의 값도 같이 변경되는 것을 확인할 수 있다.


Scope: 변수 접근 규칙에 따른 유효 범위

스코프의 뜻을 찾아보면 범위, 영역이라는 뜻이 나온다.

변수는 어떤 환경 안에서만 사용이 가능하고,
변수와 해당 값이 어디부터 어디까지가 유효한지를 판단하는 범위가 스코프이다.

자바스크립트에서는 기본적으로 함수(함수 단위로 스코프를 가지는 것이 기본)가 선언되면서(lexical) 자신만의 scope를 가지게 된다.

스코프의 영어 뜻만 놓고 본다면 무언가 제한된 범위에 대한 내용을 다룰 것 같다.

  • Scope의 의미와 적용범위를 알아야 된다.
  • 중첩 규칙
  • block scope(block level scope) VS function scope(function level scope)의 차이
  • let, const, var의 차이
  • 전역 변수, 전역 객체의 의미

Local Scope VS Global Scope

안쪽에 있는 scope에서 바깥쪽에 있는 함수와 변수에 접근하는 것이 가능하지만, 바깥에 있는 scope에서 안쪽의 함수와 변수에 접근하는 것은 되지 않는다.

  • scope는 중첩이 가능하기 때문에 함수안에 함수를 넣을 수 있다.

  • Global Scope는 최상단 scope로 전역변수는 어디서든 접근이 가능하다.

  • 지역 변수는 함수 내에서 전역 변수보다 더 높은 우선순위를 가진다.


Function Scope VS Block Scope

Block: 중괄호로 시작하고, 끝나는 단위를 말한다.

if(true) {
  console.log('I love JavaScript!');
}

for(let i=0; i<10; i++) {
  console.log(i);
}

if(!false) {
  console.log(`I'm not false!`);
}


var / let

변수를 정의하는 키워드에는 let 이외에도 var라는 키워드가 존재한다.

var: 자바스크립트는 기본적으로 함수 단위로 자신만의 Scope를 구분한다.

let: Block 단위로 Scope를 구분하게되면 예측하기 쉬운 코드를 작성할 수 있다.


scope 퀴즈

Q1) welcome( )과 name 실행의 결과는?

let str = 'Hello';

let welcome = function() {
  let myName = 'Judy';
  return `${str} ${myName}!`;
}

welcome(); // "Hello Judy!"
myName; // ReferenceError 발생!

위의 코드에서 name의 값을 접근하려고 할 때, undefined가 아니라
Uncaught ReferenceError: myName is not defined 라는 에러가 발생하는 것을 확인할 수 있다.

myName이라는 변수가 이전에 정의된 적이 없기 때문에
myName이라는 변수를 참조할 수 없다는 에러이다.

welcome 함수 안에 myName이라는 변수가 선언되어는 있지만,
외부에서는 함수 안의 변수에 접근할 수가 없다!


변수 myName에 접근할 수 있는 범위가 존재하고,
Local Scrope 안쪽에서 선언된 변수는 밖에서 사용할 수 없다.


Q2) 순서대로 콘솔에 출력되는 결과는?

let name = "tiger";

function showName() {
  let name = "lion"; // 지역변수의 name
  //showName 함수 안에서만 접근이 가능하다.
  console.log(name); // "lion"
}

console.log(name); // "tiger"
showName(); // "lion"
console.log(name); // "tiger"

위에서 전역변수의 name과 showName 함수 안에서의 name은 서로 다른 변수이다.


Q3) 순서대로 콘솔에 출력되는 결과는?

let name = "dotori";

function showName() {
  name = "daramgi"; // 지역변수의 name
  //showName 함수 안에서만 접근이 가능하다.
  console.log(name); // "daramgi"
}

console.log(name); // "dotori"
showName(); // "daramgi"
console.log(name); // "daramgi"

showName 함수내에 있는 변수 name은 let이라는 키워드가 없기 때문에 새로 선언된 값이 아닌 전역변수에 있는 name을 그대로 가져온다.

그리고 showName 함수 내에서 새로운 값을 할당해주면 전역변수의 값을 변경하게 된다.


Q4) 다음 아래의 코드를 보고, 콘솔에 출력되는 결과는?

for(let i=0; i<5; i++) {
  console.log(i); // 총 몇번의 반복을 돌까? 
}

console.log('여기에서 i에 접근할 수 있을까?', i); // ReferenceError!!!!

위의 코드 실행 결과를 보면, 마지막에 Uncaught ReferenceError: i is not defined 라는 에러가 출력되는것을 확인할 수 있다.

i는 특정한 블럭 내에 들어있는 값이기 때문에 block 범위를 벗어나면 해당 변수에 접근할 수 없고, 값을 사용할 수도 없다.


Q5) 다음 아래의 코드를 보고, 콘솔에 출력되는 결과는?

for(var j=0; j<7; j++) {
  console.log(j); 
}

console.log('j의 값은?', j);


var라는 키워드를 사용해서 변수를 사용하면, block의 범위를 벗어나도 사용이 가능하다.
var는 같은 function scope 안에서는 사용이 가능하기 때문이다.


콘솔창에서 var와 let 비교하기

var의 작동원리 살펴보기: function scope

function hello(myName) {
  var time = 'morning';
  if(time === 'morning') {
    var greeting = 'Good Morning';
  } 
  return `${greeting} ${myName}`;
}

hello('dotori'); // "Good Morning dotori"  


let의 작동원리 살펴보기: block scope

function hello(myName) {
  let time = 'morning';
  if(time === 'morning') {
    let greeting = 'Good Morning';
  } 
  return `${greeting} ${myName}`;
}

hello('daramgi'); // Uncaught ReferenceError: greeting is not defined


const

const는 값이 변하지 않는 값을 담을 때 사용하는 키워드이다.

상수를 사용할 때 const 키워드를 사용해서 변수를 만든다.

  • let 키워드처럼 Block Scope이다.
  • 값을 재할당, 재정의하려고 하면, TypeError가 발생하게 된다.
const pi = 3.14159;
pi = 777; // Uncaught TypeError: Assignment to constant variable.

위의 코드를 실행해보면, Uncaught TypeError: Assignment to constant variable.이라는 에러가 출력된다.


let / const / var 의 비교

종류letconstvar
유효 범위Block ScopeBlock ScopeFunction Scope
값 재정의, 재할당OXO
값 재선언XXO

재선언

변수를 만들 때, 재선언을 하는 경우를 살펴볼 것이다.

let 키워드를 사용하면 재선언을 막을 수 있는데 아래의 코드로 살펴볼 것이다.

let courseName = 'JavaScript';
let courseName = 'React Course';

var language = 'Java';
var language = 'NodeJS'; // Uncaught SyntaxError: Identifier 'courseName' has already been declared


window 객체

window 객체는 전역 범위를 나타낸다.

Global Scope에서 선언된 함수이고, var 키워드를 사용해서 만든 변수들은 window 객체와 연결된다.

아래의 코드를 통해 var와 윈도우 객체가 연결되는 것을 확인해 볼 것이다.

var pizzaName = 'Chicago-style Pizza';
console.log(window.pizzaName); // 'Chicago-style Pizza'

function frenchFries() {
  console.log('감자튀김 먹고싶다!!');
}

console.log(frenchFries === window.frenchFries); // true


아래는 윈도우 객체에서 위에서 만든 frenchFries 함수를 찾아본 것이다.


선언이 없이 초기화된 전역 변수

let, const 등과 같은 선언 키워드가 없이 변수를 초기화하면 안 된다.

위에서 변수의 스코프 범위를 알아보기 위해 예제 코드를 작성했었는데,
변수의 이름을 name으로 놓고 코드를 실행했을 때 생각지 못했던 오류가 났었다.

let str = 'Hello';

let welcome = function() {
  let name = 'Judy';
  return `${str} ${name}!`;
}

welcome(); // "Hello Judy!"
name; // "" <-- ""이란 결과가 나올 줄 몰랐다.

아래의 코드도 한 번 더 살펴보도록 할 것이다.

function myAge() {
  age = 100; // 여기서 age는 선언 키워드가 없다. 즉, 전역 변수로 취급이 된다.
  console.log(age);
}

myAge(); // 100
console.log(age); // 100

여기서 위의 코드의 age는 윈도우 객체의 age와 같다.

age === window.age;

window에서 age값을 100으로 할당해버린 것을 확인할 수 있다.


'use strict'

Strict Mode를 통해서 문법적으로 실수, 오류가 날 수 있는 부분을 에러로 판단하고 알려준다.

자바스크립트 파일의 가장 맨 윗줄에 작성을 해서 사용해야 된다.

function myAge() {
  age = 100; // 여기서 age는 선언 키워드가 없다. 즉, 전역 변수로 취급이 된다.
  console.log(age);
}

myAge(); // 100
console.log(age); // 100

변수의 선언을 키워드 없이 했고, 값을 넣고 있는데
use strict 를 명시함으로써 값이 선언되지 않음을 캐치하고 있다.


Scope 핵심

  • var를 사용하는 것보다 let 키워드를 사용해야되는 이유 중 가장 큰 이유는 재선언을 방지해주기 때문이다.
  • scope는 변수 접근 규칙이다.
  • ReferenceError는 변수를 접근할 수 없을 때 나는 에러이다.
  • var로 정의된 전역변수는 window 객체에 담겨버린다. 그렇기 때문에 var를 사용해서 변수를 너무 많이 만드는것은 좋지 않다.
  • 만약 변수를 선언하지 않으면, scope가 전역변수가 되기때문에 위험하다.(non strict mode일 때, 그렇기 때문에 use strict를 js파일의 가장 상단에 써주는 것이 좋다)

Closure

클로져는 '함수와 함수가 선언된 어휘적(lexical)환경'을 의미한다.

자바스크립트에서는 함수가 호출되는 환경과는 별개로 기존에 선언이 되어 있던 환경 -어휘적 환경-을 기준으로 변수를 조회하려고 하기 때문에, "외부 함수의 변수에 접근할 수 있는 내부함수"를 클로저 함수라고 부른다.

  • Closure의 정의와 특징을 알아야 된다.
  • Closure를 활용하여 외부함수의 변수에 접근
  • Closure의 코딩 패턴

아래의 코드를 살펴볼 것이다.
innerFunc 함수에서 접근할 수 있는 Scope는 총 몇개일까?

function outerFunc() {
  let outVar = 'outer';
  console.log(outVar);

  function innerFunc() {
    let innerVar = 'inner';
    console.log(innerVar);
  }
}

let globalVar = 'global';
outerFunc();

총 3개의 scope에 접근이 가능하다.


함수의 리턴

변수에 할당할 수 있는 값만 리턴할 수 있을까?
아니다! 함수도 리턴을 할 수 있다.

function outerFunc() {
  let outVar = 'outer';
  console.log(outVar);

  function innerFunc() {
    let innerVar = 'inner';
    console.log(innerVar);
  }
  return innerFunc;
}

outerFunc();

위의 코드의 실행순서를 살펴보면

  1. outerFunc 함수 안에있는 console.log(outVar); 문장이 실행되어 콘솔에 outer라는 문자가 찍힌다.

  2. 그 다음에는 return innerFunc 부분에서 아직 실행되지 않은
    함수(innerFunc)가 리턴된다.


아래의 코드를 실행했을 때 콘솔에 찍히는 값은?

function outerFunc() {
  let outVar = 'outer';
  console.log(outVar);

  function innerFunc() {
    let innerVar = 'inner';
    console.log(innerVar);
  }
  return innerFunc;
}

outerFunc()(); // (1)
let innerFunc = outerFunc(); // (2)
innerFunc(); // (3)

(1)의 흐름
outerFunc( )( )을 먼저 살펴보면, outerFunc 함수 안의 console.log(outVar); 콘솔이 가장먼저 실행돼서 1)outer라는 값이 찍힌다.

그 다음에 return innerFunc라는 함수를 리턴하는데

리턴받은 함수를 바로 ( ) 함수 호출을 통해 실행하게 된다.

그러면 outerFunc 함수 안에는 innerFunc를 받고 바로 innerFunc를 호출하기 때문에 innerFunc 함수 안에있는 console.log(innerVar);가 실행되어 2)inner라는 값이 찍히게 된다.


(2)의 흐름
let innerFunc = outerFunc( );을 살펴보면 outerFunc 함수의 실행 결과가 innerFunc라는 변수에 리턴된 값이 담긴다.

outerFunc 함수가 실행되면, console.log(outVar); 가 실행되어서 콘솔에 3)outer라는 값이 찍히고,

return innerFunc 함수가 변수 innerFunc에 담긴다.


(3)의 흐름
innerFunc( );의 innerFunc 함수를 실행하면,

console.log(innerVar); 가 실행되면서 콘솔에 4)inner라는 값이 찍히게 된다.


아래는 위의 코드를 실행시켰을 때 콘솔에 값이 찍히는 것을 확인한 결과이다.


Closure 함수란

클로저란 외부 함수의 변수에 접근할 수 있는 내부 함수를 말한다.
또는, 이러한 작동 원리를 일컫는다.

위의 함수에서 innerFunc 함수를 클로저 함수라고 부른다.

클로저 함수 안에서는 3가지 스코프에 접근이 가능하다.

  • 지역 변수 (innerVar)
  • 외부 함수의 변수 (outVar)
  • 전역 변수(globalVar)의 접근이 전부 가능하다.

많이 사용되는 클로저 예제

커링:
함수 1개가 n개의 인자를 받는 대신에 n개의 함수를 만들어서 각각 인자를 받게하는 방법을 말한다.

function addFunc(num1) {
  return function(num2) {
    return num1 + num2; 
  }
}

addFunc(7)(9); // 

앞에 있는 7이라는 인자는 num1의 매개변수로 들어가고,
뒤에 있는 9라는 인자는 num2의 매개변수로 들어간다.

위의 addFunc(7)(9) 함수를 호출하는 과정을 생각해 볼 것이다.
addFunc(9)가 먼저 실행되면, 먼저 addFunc(7)이 들어가서 안의 return function(num2) { return num1 + num2 }에서 num2의 값에 9가 들어가게 된다.

그러면 addFunc(7)을 실행했을 때, 안의 return function(num2) { return 7 + 9 }이 된다.


위와 같이 사용하는 이유를 살펴보면,

특정 값을 고정해 놓고, 재사용 할 수 있다.

let add300 = addFunc(300); // num1의 값을 고정해놓고 재사용할 수 있다.
add300(7); // 307
add300(500); // 800

let add7 = addFunc(7);
add7(1000); // 1007


외부 함수의 변수가 저장되어 템플릿 함수처럼 사용이 가능하다.

function tagMake(tag) {
  let startTag = `<${tag}>`;
  let endTag = `<${tag}>`;

  return function(content) {
    return `${startTag}${content}${endTag}`;
  }
}

let divMake = tagMake('div');
divMake('Java'); // <div>Java</div>
divMake('Script'); // <div>Script</div>

let spanMake = tagMake('span');
spanMake(`I'm span!`); // <span>I'm span!</span>


클로저 모듈 패턴:
변수를 스코프 안쪽에 가두어 함수 밖으로 노출시키지 않는 방법이다.

function makeCount() {
  let privateCount = 0;

  return {
    // 외부 함수의 privateCount라는 변수를 내부 함수에서 가져와서 사용하고 있다.
    increment: function() {
      privateCount++; 
    },
    decrement: function() {
      privateCount--; 
    },
    getValue: function() {
      return privateCount; 
    }
  }
  /*
  //위의 return 하는 객체를 변수에 담아서 아래처럼 변경할 수도 있다.
  let obj = {
    increment: function() {
      privateCount++; 
    },
    decrement: function() {
      privateCount--; 
    },
    getValue: function() {
      return privateCount; 
    }
  }
  return obj;
  */
}

let count1 = makeCount();
count1.increment();
count1.increment();
count1.getValue();

let count2 = makeCount();
count2.increment();
count2.decrement();
count2.increment();
count2.getValue();


스코프 법칙에 의해 함수의 밖에서 privateCount 변수에 직접 접근해서 값을 변경할 수 없다.

그렇기 때문에 increment(), decrement()와 같은 함수로 값을 간접적으로 변경할 수 있다.

위의 count1과 count2는 각기 다른 privateCount를 다루면서 privateCount의 값을 외부로 노출시키지 않고 있다.


또 다른 클로저의 예제를 하나 살펴볼 것이다.

돈을 지불하는 함수를 만들어 볼 것이다.
type이 항상 "cash", "card" 일 때만 실행되도록 조건을 줄 것이다.

function paymentMoney() {
  let type = "cash"; // 반드시 'cash' or 'card'여야만 된다.

  return {
    payWithCash: function(amount) {
      type = 'cash';
      console.log(`${type}(으)로 ${amount}원 만큼 지불합니다!`);
    },
    payWithCard: function(amount) {
      type = 'card';
      console.log(`${type}(으)로 ${amount}원 만큼 지불합니다!`);
    },
  }
}

profile
🌿 주니어 프론트엔드 개발자입니다! 부족하거나 잘못된 정보가 있다면 알려주세요:)

0개의 댓글