Javascript 핵심개념 - 원시 참조 자료형, 스코프, 클로저

jeongjwon·2023년 3월 2일
0

SEB FE

목록 보기
13/56

📌 원시 자료형 Primitive vs 참조 자료형 Reference

원시 자료형참조자료형
number, string, boolean, undefined, null배열, 객체, 함수 등
변수 할당메모리 공간에 값 자체가 저장메모리 공간에 주소 값이 저장(주소값을 저장하고 힙(heap)에 주소값에 따른 변수를 저장)
변수를 다른 변수에 할당원시 값 자체가 복사되어 전달주소값이 복사되어 전달
변경 불가능한 값 immutable value => 한 번 생성된 원시 자료형은 읽기 전용 Read Only(새로운 값으로 변경하는 것이 아니라 새로운 메모리 공간에 값을 저장하고 이전의 값은 자동으로 메모리에서 삭제 = garbage collector 가비지콜렉터 => 높은 신뢰성)변경 가능한 값 mutable value(저장공간 heap 에 주소값을 저장하고 변수가 참조하고 있는 주소에 저장되어있는 값을 변경가능)
let num = 20;
let copiedNum = num ;//변수에 변수 할당
console.log(copiedNum); // 20
console.log(num === copiedNum); // true

num = 30;//원 변수에 값 변경
console.log(num);//30
console.log(copiedNum);//20
console.log(num === copiedNum); // 30 !== 20 값 자체가 다르므로 false


let arr = [0,1,2,3];
let copiedArr = arr;//변수에 변수할당 
console.log(copiedArr); //[0,1,2,3]
console.log(arr === copiedArr); // true

arr.push(4);//원 변수에 값 추가
console.log(arr);// [0,1,2,3,4]
console.log(copiedArr);// [0,1,2,3,4]
console.log(arr === copiedArr); // [0,1,2,3,4] === [0,1,2,3,4] 값을 변경 추가 삭제를 하더라도 같은 주소값을 참조하기 때문에 true


let str = 'code';
str = 'states'; //원시 자료형인 string 변수 값 변경

str[5] = 'z'; //string 은 문자 하나하나를 인덱스로 배열로 접근가능하지만 원시 자료형이기 떄문에 값을 변경시킬 수는 없다.
console.log(str);//'states'
console.log(str[5]);//'s'



📌 얇은 복사 vs 깊은 복사

원시자료형을 할당한 변수를 다른 변수에 할당하면 둘 중 하나의 값을 변경해도 다른 하나에는 영향을 미치지 않는 값 자체의 복사가 일어난다. 반면, 참조자료형이 저장된 변수를 다른 변수에 할당할 경우, 두 변수는 같은 주소를 참조하고 있을 뿐 값 자체가 복사되었다고 할 수 없다. 주소값을 복사한 변수에 요소를 추가하면 같은 주소를 참조하고 있는 원본에도 영향을 미친다.
(참조자료형은 임이의 저장공간에 값을 저장하고, 그 저장공간을 참조하는 주소를 메모리에 저장하기 떄문에 다른 변수에 할당할 경우 값 자체가 아닌 메모리에 저장된 주소가 복사되어, 둘 중 하나를 변경하면 해당 변수가 참조하고 있는 주소에 있는 값이 변경되기 떄문에 다른 하나에도 영향을 미치게 된다.)


얇은 복사 shallow copy - slice(), Object.assign(), spread syntax


배열 복사

  1. slice()
    원본 배열을 복사하여 새롭게 생성된 배열은 원본 배열과 같은 요소를 갖지만 참조하고 있는 주소는 다름.
    그렇기 때문에 복사한 배열에 요소를 추가, 삭제, 변경을 한다고 해도 원본 배열에는 아무런 영향을 끼치지 않음.
	let arr = [0,1,2,3];
	let copiedArr = arr.slice();
	console.log(copiedArr); // [0,1,2,3]
	console.log(arr === copiedArr); // 참조형에서 비교연산은 값이 아니라 주소이기 때문에 다른 주소를 갖기에 false
	
	coppiedArr.push(4);
	console.log(copiedArr); // [0,1,2,3,4]
	console.log(arr); // [0,1,2,3] 주소가 다르기 떄문에 원본에는 영향을 미치지 않음
	
  1. spread syntax ...
    배열을 펼치면 배열의 각 요소를 확인할 수 있지만, 이를 이용하여 복사하는 경우에는 같은 요소를 갖지만 다른 주소를 참조하므로 같다고 할 수 없으며 복사한 배열에 요소를 추가 삭제 변경하더라도 원본 배열에는 아무런 영향을 미치지 않음.
	let arr = [0,1,2,3];
	console.log(...arr); // 0 1 2 3

	let num = [1,2,3];
    let int = [1,2,3];
	console.log(num === fint); // 같은 요소를 가진 두 배열은 다른 주소를 참조하기 떄문에 false

	let arr = [0,1,2,3];
	let copiedArr = [...arr]; // 각 요소들을 가지고 배열을 만듦
	console.log(copiedArr); // [0,1,2,3]
	console.log(arr === copiedArr);//spread 로 전달하여 우너본 배열과 같은 요소를 가지고 있지만, 각각 다른 주소를 참조하게 되어 false

	copiedArr,push(4);
	console.log(copiedArr);//[0,1,2,3,4]
	console.log(arr); //[0,1,2,3]  따라서 각기 다른 주소를 참조하기 때문에 복사된 배열에 요소를 추가하더라도 원본 배열에는 아무런 영향을 미치지 않는다.

객체 복사

  1. Object.assing()
	let obj = { firstName: 'coding', lastName : 'kim' };
	let copiedObj = Object.assign({}, obj);

	console.log(copiedObj) // {firstName: 'coding', lastName: 'kim' }
	console.log(obj === copiedObj); // false
  1. spread syntax ...
	let obj = { firstName: 'coding', lastName : 'kim' };
	let copiedObj = {...obj};
	
	console.log(copiedObj) // {firstName: 'coding', lastName: 'kim' }
	console.log(obj === copiedObj); // false

이 두 가지 객체 복사 또한 값 요소는 복사가능하지만 다른 주소를 갖기 때문에 같다고 할 수 없음.

  • 참조 자료형 내부에 참조 자료형이 중첩되어 있는 경우, slice(), Object.assign(), spread syntax 를 사용해도 참조 자료형 내부에 참조 자료형이 중첩된 구조는 복사할 수 없음.
	let users = [//주소값 32
		{
			name: "kimcoding",
			age: 26,
			job: "student"
		}, // 주소 값이 65
		{
			name: "parkhacker",
			age: 29,
			job: "web designer"
		}, // 주소 값이 71
	];

	let copiedUsers = users.slice(); //주소값 54
	console.log(users === copiedUsers); // false
	console.log(users[0] === copiedUsers[0]);// true
	//위의 설명처럼 중첩된 구조에서는 내부에 중첩되어 있는 {주소값 65}, {주소값 71} 참조 자료형은 같은 주소를 참조하지만, 가장 외부에 있는 참조 자료형 users 는 복사가 되어 주소값이 다르다.	

깊은 복사 deep copy - JSON.stringfy() , JSON.parse()

참조 자료형 내부에 중첩되어 있는 모든 참조 자료형을 복사. JavaScript 내부적으로 중첩된 구조 전체를 복사하는 것을 구현할 수 없어서 다른 문법을 응용함.

JSON.stringfy() , JSON.parse()

JSON.stringfy() : 참조자료형을 문자열 형태로 변환하여 반환
JSON.parse() : 문자열의 형태를 객체로 변환하여 반환
따라서 참조자료형 -> 문자열 -> 객체 ( JSON.stringfy() -> JSON.parse() )

	const arr = [1,2,[3,4]]; //주소 85 , 내부 배열 [3,4] 주소 85
	const copiedArr = JSON.parse(JSON.stringfy(arr)); //주소 92 , 내부 배열 [33]
	//참조자료형인 arr를 JSON.stringfy()를 이용하여 문자열로 반환하고 JSON.parse 를 이용하여 객체로 반환
	console.log(arr); // [1,2,[3,4]]
	console.log(copiedArr); // [1,2,[3,4]] 
	console.log(arr === copiedArr); //false
	console.log(arr[2] === copiedArr); //flase
	//중첩된 내부와 외부 참조 자료형을 복사하긴 하지만 복사한 모든 참조 자료형의 주소는 다름.	
  • 예외, 중첩된 참조 자료형 중에 함수가 포함되어 있는 경우, 함수는 null 로 바뀌게 됨. 따라서 이 또한 완전한 깊은 복사 방법이라고 보기 어려움.
	const arr = [1, 2, [3, function(){ console.log('hello world')}]]; //함수가 포함된 arr 배열
	const copiedArr = JSON.parse(JSON.stringify(arr));
	
	console.log(arr); //[1, 2, [3, function(){ console.log('hello world')}]]
	console.log(copiedArr); //[1, 2, [3, null]]
	
	console.log(arr == copiedArr); //false
	console.log(arr[2] === copiedArr[2]); //false
	
	console.log(arr[0] === copiedArr[0]); // true
	console.log(arr[1] === copiedArr[1]); // true
	console.log(arr[2][0] === copiedArr[2][0]); // true
	//각각의 배열의 주소값은 다르지만 배열 안의 요소값들은 같다. 배열의 요소값은 원시자료형!

외부 라이브러리 사용

완전한 깊은 복사를 위해서는 node.js환경에서는 lodash 혹은 ramda 를 설치해야 함

	const lodash = require('lodash');

	const arr = [1,2,[3,4]];
	const copiedArr = lodash.cloneDeep(arr);

	console.log(arr); // [1,2,[3,4]]
	console.log(copiedArr); // [1,2,[3,4]]
	console.log(arr === copiedArr); // false
	console.log(arr[2] === copiedArr[2]); // false



📌 스코프

스코프는 곧 범위, 범위는 중괄호(블록) 혹은 함수에 의해 나누어져 중첩이 가능하다.

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

바깥쪽 스코프에서 선언한 변수는 안쪽 스코프에서 사용 가능하지만, 안쪽 스코프에서 선언한 변수는 바깥쪽 스코프에서 사용 불가능하다. 여기서 가장 바깥쪽 스코프를 전역 스코프 Global Scope, 그 외의 스코프를 지역 스코프 Local Scope 라고 칭한다. 지역변수는 전역변수보다 더 높은 우선순위를 가진다.

	let name = '김코딩'; //전역변수
	function showName(){
      // 지역변수
      let name = '박해커'; //1. let 선언
      name = '박해커'; //2. let 선언 x
      console.log(name);
    }
						// 1     // 2
	console.log(name); //'김코딩' // '김코딩'
	showName(); 	   //'박해커' // '박해커'
	console.log(name); //'김코딩' // '박해커'
	//1 : 전역변수로 선언된 name 김코딩을 출력 , 
	//	  함수 안쪽은 지역변수로 선언된 name 은 전역변수와 이름이 똑같지만 지역변수가 전역변수보다 우선순위가 높기 때문에 박해커출력 ( 이와 같은 동일한 변수 이름으로 인해 바깥쪽 변수가 안쪽 변수에 의해 가려지는 현상을 쉐도잉 variable shadowing 이라 함) , 
	//	  첫번째 출력과 마찬가지로 전역변수 name 김코딩 출력
	//2 : 함수 내에서 let 키워드를 사용하지 않았기 때문에 지역변수로 사용하고 있는 것이 아니라 전역변수를 그대로 사용하고 있다. 따라서 1번과 동일하게 첫번째 두번째 출력은 같으나, 
	//	  두번째 출력에서 전역변수 name이 박해커로 값이 바뀌었기 때문에 세번째 출력에서도 또한 박해커로 값을 유지하게 됨.

스코프의 종류

  1. 블록 스코프 block scope
    중괄호로 둘러싼 범위 , block 범위를 벗어나는 즉시 변수 사용불가. 만약 벗어나서 사용한다면 Reference Error.

  2. 함수 스코프 function scope
    함수로 둘러싸인 범위 (함수 선언식 및 함수 표현식)

*화살표 함수로 둘러싼 범위 => 블록 스코프 block scope

	let getAge = user => {
      return user.age;
    } // => 화살표 함수 이므로 블록 스코프
    let getAge = function (user) {
      return user.age;
    } // function 키워드를 사용했기 때문에 함수 스코프

키워드 var let const

letconstvar
유효범위블록 + 함수 스코프블록 + 함수 스코프함수 스코프
값 재할당OXO
재선언X->SyntaxErrorX -> TypeErrorO

let : 블록 단위로 스코프를 구분했을 때, 훨씬 더 예측 가능한 코드를 작성할 수 있으므로 let 키워드 사용 권장. 재선언을 방지함. var 키워드를 사용하지 않는다 해도, 함수 스코프는 let으로 선언된 변수의 접근 범위를 제안함.
const : let 키워드와 동일하게 블록 스코프를 따름. 값의 변경을 최소화하고 새롭게 할당할 일이 없다면 사용 권장. 만약 값을 재할당할 경우 TypeError.
var : 블록 스코프를 무시하고 함수 스코프를 따른다. 화살표 함수의 블록 스코프는 무시하지 않는다. 함수 스코프는 함수의 실행부터 종료까지, var 선언은 함수 스코프의 최상단에 선언. 선언 키워드 없는 선언은 최고 스코프에 선언. 함수 내에서 선언 키워드 없는 선언은 함수 실행전까지 선언되지 않은 것으로 취급


🚨 주의사항
1. var 로 선언된 전역변수 및 전역함수는 window 객체에 속하게 된다.
* window 객체란 ? only 브라우저 창을 대표하는 객체, but 브로우저 창과 관계없이 전역항목도 담고 있음

	var myName = '김코딩';
	console.log(window.myName); // 김코딩

	function foo(){
      console.log('bar');
    }
	console.log(foo === window.foo); // true
  1. 전역변수를 많이 쓰면 편리하지만, 로직이나 다른 함수에 의해 의도되지 않은 변경 등 부수 효과 side effect 발생하므로 전역변수를 최소화!
  2. let 과 const 를 주로 사용! var 는 블록스코프를 무시하고 재선언을 해도 에러가 발생하지 않아 버그를 유발할 수도 있고, window 기능을 덮어씌어 내장 기능을 사용할 수 없게 된다.
  3. 선언키워드와 변수를 함께 할당! 선언없이 변수를 할당하면 var와 같이 전역변수로 취급된다. 실수 방지를 위해 'use Strict'라는 Strict Mode 를 적용하여 문법적으로 실수 할 수 있는 부분들을 에러로 진단할 수 있다.



📌 클로저

함수와 함수가 선언된 어휘적 Lexical 환경/주소/변수(함수가 호출되는 환경과 별개로 기존에 선언되어 있던 환경)의 조합, 이 환경은 클로저가 생성된 시점의 유효범위 내에 있는 모든 지역변수로 구성됨.

	const globalVar = '전역 변수';
	
	function outerFn(){
      const outerFnVar = 'outer 함수 내의 변수';
      const innerFn = function(){
        return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.';
      }
      return innerFn;
    }
	
	const innerFnOnGlobal = outerFn();
	//함수 outerFn() 호출 -> return innerFn -> function(){ return 'innerFn은 ' + outerFnVar + '와 ' + globalVar + '에 접근할 수 있습니다.'; } = 주소값 반환
	const message = innerFnOnGlobal();
	//변수 message에 innerFnGlobal() 호출 
	console.log(message);
	//return 값인 'innerFn은 outer 함수 내의 변수와 전역변수에 접근할 수 있습니다.' 반환

외부함수의 변수에 접근할 수 있는 내부함수를 클로저 함수

  1. 함수 outerFn 에서 globalVar 에 접근 가능 => 함수 outerFn과 outerFn에서 접근할 수 있는 globalVar
  2. 함수 innerFn 에서 globalVar 과 함수 outerFn 내부의 outerFnVar 에 접근 가능 => 함수 innerFn 과 innerFn 에서 접근할 수 있는 globalVar, outerFnVar
  3. innerFnOnGlobal 은 outerFn 내부의 innerFn 의 주소값을 가짐. innerFn 은 클로저로서 outerFnVar에 접근할 수 있다.
    innerFnOnGlobal 은 내부스코프인 outerFnVar에 접근할 수 없다고 생각하지만, innerFn 함수가 최초 선언된 환경에서는 OuterFnVar 에 접근 가능하다.
    => 어휘적 환경
    outerFn, innerFn 처럼 함수가 함수를 리턴하는 패턴를 자주 사용하는데
    이 때, outerFn는 외부함수, innerFn 을 (외부함수의 변수에 접근할 수 있는) 내부함수 라고 칭할 수 있다.

데이터를 보존하는 함수

	function getFoodRecipe(foodName){
      let ingredient1, ingredient2;
      return `${ingredient1} + ${ingredient2} = ${foodName}!`;
    }
	console.log(ingredient1); //ReferenceError : 함수 내부에서 선언한 변수에 접근 불가
	console.log(foodName); //ReferenceError : 매개변수에 접근 불가

일반적으로는 위와 같이 함수 내부에 선언한 변수와 매개변수에는 접근할 수 없다.

	function createFoodRecipe(foodName){
      let ingredient1 = '탄산수';
      let ingredient2 = '위스키';
      const getFoodRecipe = function() {
        return `${ingredient1} + ${ingredient2} = ${foodName}`;
      }
      return getFoodRecipe;
    }
	const recipe = createFoodRecipe('하이볼');
	console.log(recipe); // getFoodRecipe 함수 주소값 저장
	recipe(); // getFoodRecipe 함수 반환값 '탄산수 + 위스키 = 하이볼'
	

getFoodRecipe 가 클로저로서 foodName, ingredient1, ingredient2 에 접근할 수 있다. 이 때 매개변수로 전달된 foodName은 createFoodRecipe 함수가 보존하고 있기 때문에 recipe 함수 호출시 계속 재사용할 수 있다.

	function createFoodRecipe(foodName){
      const getFoodRecipe = function(ingredient1, ingredient2) {
        return `${ingredient1} + ${ingredient2} = ${foodName}`;
      }
      return getFoodRecipe;
    }
	const highballRecipe = createFoodRecipe('하이볼'); // highballRecipe = getFoodRecipe
	highballRecipe('콜라','위스키');//'콜라 + 위스키 = 하이볼'
	highballRecipe('탄산수','위스키');//'탄산수 + 위스키 = 하이볼'
	highballRecipe('토닉워터','연태고량주');//'토닉워터 + 연태고량주 = 하이볼'

기존의 highballRecipe 는 foodName '하이볼'을 보존한 채로 매개변수를 추가하여 다양한 값들이 출력될 수 있다.


커링

여러 전달인자를 가진 함수를 함수를 연속적으로 리턴하는 함수로 변경하는 행위

	function sum(a,b){//매개변수로 첫 번째 인자, 두 번째 인자
      return a + b;
    }
	function currySum(a){//첫 번째 인자
      return function(b){//리턴 함수 두 번째 인자
        return a + b;
      }
    }
	console.log(sum(10,20) === currySum(10)(20)); //true

currySum 처럼 첫번째 전달인자를 리턴한 함수에 두번째 전달인자와 함께 호출하여 sum 처럼 같은 값을 반환할 수 있다. 이때 커링을 활용한 currySum 과 같은 함수를 커링 함수라 한다.


	function makePancake(powder){
      return function(sugar){
        return function(pan){
          return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
        }
      }
    }
	
	const addSugar = makePancake('팬케이크가루');
	const cookPancake = addSugar('백설탕');
	const morningPancake = cookPancake('후라이팬'); //팬케이크 완성! 재료: 팬케이크가루, 백설탕 조리도구: 후라이팬

	const lunchPancake = cookPancake('후라이팬'); //팬케이크 완성! 재료: 팬케이크가루, 백설탕 조리도구: 후라이팬

위는 전체 프로세스의 일정부분까지만 실행할 수 있다.

	function makePancakeAtOnce(power, sugar, pan){
      return `팬케이크 완성! 재료: ${powder}, ${sugar} 조리도구: ${pan}`;
    }
	const morningPancake = makePancakeAtOnce('팬케이크가루', '백설탕', '후라이팬'); //팬케이크 완성! 재료: 팬케이크가루, 백설탕 조리도구: 후라이팬

반면, 위는 커링이 적용되지 않은 채 무조건 전체 프로세스만을 진행한다.

따라서 커링은 결과값은 같을 수 있으나 함수의 일부만 호출하거나, 일부 프로세스가 완료된 상태를 저장하기에 용이하다.


모듈 패턴

모듈이란 하나의 기능을 온전히 수행하기 위한 코든 코드를 가지고 있는 코드 모음으로 하나의 단위로서 역할을 한다. 이는 다른 모듈에 의존적이지 않고 독립적이어야 한다. 그러기 위해서는 기능 수행을 위한 모든 기능을 갖추고 있어야 하고, 외부 코드 실행을 통해서 모듈의 속성이 훼손 받지 않아야 한다. 모듈의 속성을 꼭 변경해야 할 필요가 있는 경우에는 제한적으로 노출된 인터페이스에 의해 변경되어야 한다.

	function makeCalculator(){
      let displayValue = 0;
      
      return{
        add: function(num){
          displayValue = displayValue + num;
        }
        subtract: function(num){
          displayValue = displayValue - num;
        }
     	multiply: function(num){
          displayValue = displayValue * num;
        }
        divide: function(num){
          displayValue = displayValue / num;
        }
      	reset: function(num){
          displayValue = 0;
        }
      	display: function(num){
          return displayValue
        }
      }
 	}
	const cal = makeCalculator(); //return {}
	cal.display(); // 0 return displayValue 
	cal.add(1); // return 은 하지않지만 더하기 연산을 처리
	cal.display(); //1
	console.log(displayValue); //ReferenceError: 내부 변수이기때문에 접근할 수 없다. => 보호

따라서 위와 같이 클로저는 특정 데이터를 다른 코드의 실행으로부터 보호해야 할 때 용이하다.

0개의 댓글