본문 바로가기
JS

JS 3주차 개념 정리

by 썸맨 2023. 5. 23.
  • 데이터 타입 : 1) 기본형 : symbol 타입(ES6 문법) - 변경 불가능한 원시 타입의 값(충돌 위험이 없는 유일한 객체의 프로퍼티 키를 만들기 위해 사용!)  2) 참조형 : 참조형의 경우는 크기가 커서 기본적으로 객체로 구성(배열, 함수, map(), set() 등등 - 크기가 커서 참조하고 있는 타입)
  • 기본형과 참조형의 구분 방법 : 값의 저장방식 & 불변성 여부
  • 저장방식 1) 기본형 : 값이 담긴 주소 값을 다이렉트로 복제함. (메모리 관점에서 불변성)
  • 저장방식 2) 참조형 : 주소값들로 이루어진 묶음을 가리키는 주소값을 복제(메모리 관점에서 불변x)
  • 비트(0,1) - 메모리 구성을 위한 작은 조각  / byte - 8개 비트로 구성 / 메모리 - 바이트로 구성(모든 데이터는 byte단위의 식별자인 메모리 주소값을 통해 서로 구분)
  • tmi : JAVA는 정수형의 길이에 따라 다르게 지정 -> 메모리가 적었던 시절의 효율적 저장을 위해 / JS는 모두 8바이트 사용함(개발자가 메모리 크기 신경쓰지 않아도 될정도로 발전)
  • 식별자 = 변수명 / 데이터 = 변수  ex)let a = 3 (let a는 식별자 / 3은 데이터(변수))
  • 변수 / 데이터 영역을 나누는 이유 : 데이터를 자유롭게 변환(ex) 숫자타입에서 긴 문자열로 바꾸면 변환이 자유롭지 않다)하고 효율적인 메모리관리(중복 값 여러번 저장 방지 등) //변수 영역의 공간은 데이터영역보다 상대적으로 작음
  • let / const : 변수 영역의 메모리를 변경 할 수 있냐 / 없냐에 따라 구분 (변수는 불변! - 데이터 영역의 값 바꿀 수 없다)
  • 불변하다/불변하지 않다 : 데이터 영역의 메모리를 변경 할 수 없냐 / 있냐에 따라 구분
  • JS garbage collector : 변수영역에서 메모리 영역으로 참조가 되지 않는 데이터(값의 변경으로 인한)를 제거하고 주소를 수거 해서 메모리를 관리함 - 이러한 데이터는 다른 곳에 참조가 되지 않기에 참조카운트는 0이다.
  • 참조형 데이터 : 객체의 각 property 영역을 세팅하기 위해 별도 공간이 필요!(각 객체의 프로퍼티의 주소 값을 가져오는 공간) - 이 세팅된 주소들을 변수 영역의 공간에 전달
  • why 참조형은 가변? : 데이터 변경 시, 데이터의 영역은 불변하지만! 별도 property 공간에서의 데이터를 가리키는 주소가 바뀌었기에 가변이라고 칭함.
  • 중첩객체 관리 : 객체 안에 또 다른 객체(배열 등)의 경우를 말하는데 이 때, 중첩 객체를 위한 별도의 공간을 또 할당함
  • 참조형 변수의 복사 후 값 변경 : 기본형 변수 경우 주소를 복사 후 복사된 변수의 값을 변경 시 원래 변수와는 다른 결과 값이 출력(주소를 복사하기 때문에 - 값이 바뀌면 가져오는 주소 값을 변경) BUT!! 참조형 객체를 복사 후 복사된 객체의 값을 변경할 때 주소를 바로 복사해 버리면 원본 또한 같이 뮤테이트 되버리는 현상이 일어남(원하지 않은 결과 - 같이 가변된다는 건 위험)
var obj1 = { c: 10, d: 'ddd' };
var obj2 = obj1;                    //참조형

obj2.c = 20;
obj1 === obj2;                      //true

=> 객체의 일부 속성 변경 시, 해당 데이터가 가르키는 주소의 값은 변경 - 이것으로 원본도 변경이 됨.

  • 위 방법 해결을 위해 객체 자체를 변경해 새로운 객체를 만들어 return하면 원본 객체가 뮤테이트 되지 않음(별도의 객체 공간이 생성되므로) but) 최선x, 10~20개의 속성이 넘어가면 그만큼의 retrun값도 증가(유지보수 최악)
  • 따라서 해결을 위해 얕은 복사 차용
//얕은 복사
var copyObject = function (target) {
    var result = {};

    // for ~ in 구문을 이용하여, 객체의 모든 프로퍼티에 접근
    // 이 copyObject로 복사를 한 다음, 복사를 완료한 객체의 프로퍼티를 변경하면
    // 뮤테이트가 일어나지 않는다!
    for (var prop in target) {                 //1depth 만 copy하기 때문에 중첩객체는 효과 없음
        result[prop] = target[prop];
    }
    return result;
}
var user = { name: 'wonjang', gender: 'male' };
var user2 = copyObject(user);
user2.name = 'twojang';

if (user !== user2) {
    console.log('유저 정보가 변경되었습니다.');         //console출력
}

console.log(user.name, user2.name);                 //wonjang twojang
console.log(user === user2);                              //false

=> 하지만 이것에도 문제가 있음! 중첩된 객체(객체 안의 객체)는 여전히 그 주소를 복사해 올 수 밖에 없기 때문

  • 깊은 복사(내부의 모든 값들을 하나하나 다 찾아서 복사하는 방법)를 하면 중첩된 객체의 문제 해결 : 재귀적 수행으로 해결(recursive, 자기 자신을 호출)
//깊은 복사
var copyObjectDeep = function(target) {
    var result = {};
    if (typeof target === 'object' && target !== null) {
        for (var prop in target) {
            result[prop] = copyObjectDeep(target[prop]);    //재귀 호출
        }
    } else {
        result = target;
    }
    return result;
}
  • others : JSON.stringfy()함수로 복사 시, 코드 간결& 이해가 쉽지만 모든 정보 복사 되지 않음(함수, undefined) &순환참조 지원하지 않음(객체안의 객체 중첩 복사x) => 따라서 객체 구조가 간단하고 함수/undefined속성이 없을 때 적합
  • undifined/null : undifined는 변수값 지정안하거나(데이터 영역의 메모리주소가 없다) .&[]로 접근 시 해당 데이터가 없을 때 그리고 return이 없거나 호출되지 않는 함수의 실행결과로 나오고(의도적x) / null은 개발자끼리 명시적으로 없다는 것을 표시 할때 사용 (typeof null - object나오는 건  JS의 버그), undifined == null(동등 연산자 사용하면 true) / 일치 연산자 === 사용시 false(일치연산자 사용하기!)
  • 실행context : 객체!!로 구성이 되어있고 실행할 코드(개발자 작성)가 수행할 수 있도록 제공하는 환경 정보를 모아놓은 객체로 작성된 코드를 수행 시킬 수 있는 여러 환경 정보를 모아놓은 context를 call stack으로 쌓아놓음(코드들은 동일한 환경으로 구성된 객체 끼리 묶이고 콜스택에 쌓임) -> 쌓아 둔 후 가장 위에 쌓인 context와 관련 코드를 실행하는 방법으로 코드의 환경 및 순서 보장(실행컨텍스트를 콜스텍으로 쌓아 놓는 방식으로 관리)
  • 실행context 구성 : 강의자료를 토대로... 코드 실행이 되면 전역컨텍스트가 in(활성화됨) -> 선언부를 지나 outer() 실행함수를 만날 때! outer()는 활성화(in)되고 전역 컨텍스트는 중단이 된다 -> outer() 내부함수를 실행하다 inner()를 만나면 inner()가 활성화(in)되면서 outer()는 중단이 된다 -> inner() 함수가 실행을 마치면 콜스택에서 popUp(out)되면서 outer()가 재개가 된다 -> outer()도 실행을 마치면 콜스택에서 popUp(out)이되며 전역컨텍스트가 재개 된다 -> 마지막으로 전역 컨텍스트를 실행하고 나면 전역컨텍스트도 popUp(out)이 되면서 코드가 종료된다.
  • 실행context 활성화 시점에 하는 일 1. 호이스팅 2. 외부 환경 정보(outer)를 구성 3. this 값 설정
  • 실행 context객체 안에 들어있는 요소 : VariableEnvironment / LexicalEnvironment / ThisBinding
  • VE/LE : 현재 context내의 record(EnvironmentRecord로 현재 context와 관련된 식별자 정보 = 변수/함수)&outer(외부 환경 정보, outerEnvionmentReference)를 가짐. 두 개는 생성시 완전 동일하지만 VE경우 스냅샷형태(생성될 때 그 모습 그대로)로 유지되고 LE는 변경사항을 실시간으로 반영하며 업데이트 하는 차이가 있음.
  • =>정리 : 실행 컨텍스트를 생성하면 VE에 정보를 먼저 담은 후 이를 그대로 복사해 LE를 만들고 LE를 주로 활용함.
  • VE/LE 구성요소 1) Record : 수집 과정을 호이스팅이라고 하며 호이스팅(가상개념)은 식별자 정보를 맨위로 끌어오며 수집한다(여기서 함수 자체도 수집이 됨), context 내부를 처음부터 끝까지 순서대로 수집(실행은 안함)
  • * 호이스팅 규칙 : 매개변수/변수는 선언부를 호이스팅(let a), 함수 전체를 호이스팅(모든 함수가 아닌 조건에 따라) - 함수 선언문 경우 함수 전체가 위로 끌어올려주지만 함수 표현식은 변수 부분만 위로 끌어올려 줌
function b () {           // 호이스팅 시
    console.log(b);     // var b;                
    var b = 'bbb';        // function b() {}   
    console.log(b);     // console.log(b) //function b
    function b() { }      // b= 'bbb;
    console.log(b);     // console.log(b)  // bbb
}                               // console.log(b)  // bbb
b();

=> 같은 변수/함수 이름 작성 시 둘다 호이스팅 되는 문제 -> 함수표현식은 선언부분만 올라가기에 문제가 없음(선언부 사용 하는 것을 권장)

  • Scope : 식별자에 대한 유효범위(변수의 영향 범위) / Scope chain : 스코프가 어떻게 연결되었는지 보여주는 것
  • 구성요소 2) outer(outer 환경참조 정보) : 현재 호출된 함수가 선언될 당시!의 외부 환경정보를 가지고 있음(즉, 실행context가 만들어 질때의 환경정보(바로 직전의 콜스택 환경 정보(LE)) why? 변수 정보를 참조하기 위함(scope chaining이 가능하게 끔) - ex) outer()함수에는 a라는 변수가 선언되어 있지 않다 이 때, a를 출력하려고 하면 내부함수인 inner()함수는 이미 콜스택에서 popUp된 상태라 a의 변수를 참조할 수 없으므로 outer()함수를 호출한 실행컨텍스트인 전역컨텍스트의 LE(외부환경)을 참조해서 전역컨텍스트에 있는 a의 값을 가져오게 된다 - 요것이 스코프 체이닝
  • 정리 : 각각의 실행context는 LE안에 record와 outer를 가지고 있고 outer안에는 그 실행context가 선언될 당시의 LE정보가 들어 있어 scope chain에 의해 상위 context의 record를 읽어올 수 있다.
  • 함수정의의 방법 3가지 : 함수 선언문(정의부만 존재하는 것, 명령은 x) / 함수 표현식(익명 함수로 함수를 별도의 변수에 할당하는 방식) / 기명함수 표현식(변수에 할당하면서 함수의 이름을 적는 거, 이건 기억x) 앞 두 개만 알아 둘 것
  • Runtime : 코드가 돌아가는 환경(시간)
  • 함수와 메소드 차이 : 둘 다 함수 형태로 되어 있으나 제일 큰 차이는 독립성이다! 함수는 그 자체로 수행이 가능함(스스로 수행가능 -> 호출 주체가 없다는 의미) but 메소드는 다른 객체에 의해 늘 수행되는 종속성의 특징을 가지고 있음(즉, 자신을 호출한 대상 객체에 대한 동작을 수행함)
  • this : JS에서의 상황에 따라 다른 의미를 가짐(상황에 따라 달라짐)
  • thisBinding : this 식별자가 바라봐야 할 객체, 실행context가 생성될 때 결정됨
  • 1. 전역공간의 this - node.js의 this는 global 객체 / 브라우저의 this는 window객체를 의미 
  • 2. 함수 / 메소드 호출 시의 this - 함수로서 '독립적'으로 호출 시 this는 항상 전역 객체를 가리킴(호출의 주체를 명시할 수 없기에) / 메소드로서 호출 시 this는 호출의 주체(객체)가 된다.(호출의 주체를 명시 할 수 있기 때문!) but. 매소드 내부여도 어떤 함수를 함수로서 호출할 경우 this는 지정x(호출 주체를 알수 없기 때문) 아래가 예시
  • 헷갈리지 말기! : 메소드 내부에 있는 함수도 예외는 없다(메소드 내부에 있는 함수를 함수로써 호출하면 이 함수의 this는 전역객체를 가리킨다!!) 아래 예시)
var obj1 = {
    outer: function() {
        console.log('TEST =>', this); //맨처음으로 찍힌 this 호출의 주체인 obj1이 찍힘
        var innerFunc = function() {
            console.log('TEST =>', this); // 여기는 함수로서의 호출(innerFunc는 앞에 객체를 가리키고 있지 않기에)이므로 global 객체에 해당!
        }
        innerFunc(); // (2) 호출(함수로써의 호출) - 함수 내부에 있더라도 예외가 없다!!!!!(여기의 this는 전역객체)

        var obj2 = {
            innerMethod: innerFunc
        };
        obj2.innerMethod(); // (3) 호출(객체의 메서드로서의 호출) - obj2를 가리킴
    }
};
obj1.outer(); // (1) 호출(객체의 메서드로써)
  • 호출 주체를 인지 할 수 있는 기준 : .(dot) & [](대괄호) - 두 개 다 같은 용도 임
  • this 우회 방법 1) 변수 활용 : 내부 스코프에 이미 존재하는 this를 별도의 변수에 할당(self를 사용해서)
  • 2) arrow function : ES6문법에서 함수 내부에서 this가 전역 객체를 바라보는 문제(유실) 때문에 도입함.
  • ※함수와 arrow함수의 가장 큰 차이점 : thisBinding여부!!(화살표 함수는 thisBinding 과정 자체를 생략함(없앰))
//this 우회하기!
var obj1 = {
    outer: function() {
        console.log(this); // (1) obj1이라는 주체로 인해 메서드 실행되었으므로 this는 outer { outer: [Function: outer] }

        // AS-IS (기존 것) - 개발에서 많이 사용하는 용어 (수정 전)
        var innerFunc1 = function() {
            console.log(this); // (2) 이것의 this는 전역객체(global)
        }
        innerFunc1();
        // TO-BE (이후 것) - 개발에서 많이 사용하는 용어 (수정 후)
        var self = this;          //스코프 내의 변수 self에 this값을 저장
        var innerFunc2 = function() {
            console.log(self); // (3) self를 출력 { outer: [Function: outer] }
        };
        innerFunc2(); //this는 global객체여야 하지만 내부 스코프의 this를 변수에 할당했기에 this는 obj1를 가리킴
    }
};
obj1.outer();

var obj = {
    outer: function() {
        console.log(this); // (1) obj(outer) { outer: [Function: outer] }
        var innerFunc = () => {          //화살표 함수로 호출 해 thisBinding을 거치지않아 상위 객체인 obj를 바라봄{ outer: [Function: outer] }
            console.log(this); // (2) obj(outer) { outer: [Function: outer] }
        };
        innerFunc();      //여기에서의 this는 당연히 전역변수여야 하지만 화살표 함수를 사용해 상위의 객체인 obj를 바라봄(화살표 함수가 아니였다면 global)
    }
}
obj.outer();
  • 3) Callback함수 호출 시의 내부 this : 콜백함수도 함수이다!!로 기억! -> thisBinding을 하면 내부의 this는 유실됨(기본적으로 전역 객체를 무조건 바라봄) 예외로는 .addEventListener있는데 얘는 만든 개발자가 콜백함수를 호출할때 this를 상속하게끔 별도로 this 지정하게 정한 것
  • 4) 생성자 함수 내부에서의 this : 새롭게 생성되는 인스턴스를 만들 때마다 this는 만들어 지는 해당 인스턴스를 지칭함
  • 명시적인 thisBinding(명시해서 this를 binding하는 것) : call, apply, bind
  • 1) call : 기존의 함수나 메소드 뒤에 붙어 즉시 실행해 새로운 인스턴스를 즉시 생성, 앞에 this로 묶을 객체를 중괄호로 묶고 뒤는 묶지않은 채로 나머지 매개변수를 넣어줌 - 메소드로서의 호출로 this가 해당 객체를 binding 하고 있더라도 call을 붙이면 call 뒤에 명시적으로 binding할 {}부분으로 this가 binding된다.
  • 2) apply : call과 완전 동일한 기능, 차이점은 call은 뒤의 매개변수를 묶지 않지만 apply는 뒤의 매개변수를 대괄호(배열형태)로 묶어줌(뒤에 받는 매개변수를 배열로 받음!)

*유사배열 객체 : 배열과 유사한 객체로 반드시 length가 필수이고 index번호는 0부터 시작해서 1씩 증가해야 함 but) 배열처럼 .push() / .slice()등의 실제 배열 메소드는 사용 할 수 없다 but. call / apply는 즉시 실행 함수이기에 this binding하는 자리에 해당 배열의 유사객체를 넣어줌으로 사용이 가능하긴 함(call은 this binding목적임, 원래의 목적에서 벗어난 것 - ES6에서 Array.from 완전 편리한 기능 제공으로 실제로 사용하진 않음)

//유사 배열 객체를 배열로 만들기
var obj = { 0: 'a', 1: 'b', 2: 'c', length: 3 };     //유사배열 객체
var arr = Array.from(obj); // 객체 -> 배열로 바꿔줌 (ES6의 짱 좋은 기능!)
console.log(arr); // 찍어보면 배열이 출력 ['a', 'b', 'c']
#효율적인 this binding을 통한 리팩토링
#여러 개 파라미터 사용
var numbers = [10, 20, 3, 16, 45];
numbers.forEach(function(number, idx){                 //요렇게 파라미터를 여러 개 넣을수도 있음!
    console.log(idx + '번쨰의 값은 '+ number+'입니다.')
})

var max = min = numbers[0]; //10

numbers.forEach(function(number) {     //대표적인 max, min을 찾아가는 과정(비효율적임!!)
    if(number > max) {                //현재 돌아가는 숫자가 max보다 큰 경우
        max = number;
    }

    if(number < min) {
        min = number;
    }
})
//위에 것을 간단하게 만들기 apply이용
var max = Math.max.apply(null, numbers)
var min = Math.min.apply(null, numbers)       //간단 call로 하려면 var min = Math.min.call(null, ...numbers)로 spread사용
console.log(max, min)

//spread operator 이용 (Math.max는 풀어 헤치는게 중요 - 콤마 형태로 구분된 숫자들의 나열로서 표현되어야 함)
var max = Math.max(...numbers)
var min = Math.min(...numbers)          // 더 간단해.. 실제로 제일 많이 사용(요게 제일 중요!)
console.log(max, min)

=> 여기서 spread operator로 풀어 헤쳐놓아도 배열이 아니게 되는 게 아님(console.log에만 콤마가 없이 보이게 출력이 되는데 이건 console에서 보여주는 특징이지 배열이 완전 아니게 되는 의미는 아니다!!)

  • 3) bind!! : this를 바인딩하는 메소드로 call/apply와의 차이점으로는 bind경우에는 즉시 호출하지 않는다!(call/apply는 즉시 호출성격), 함수를 바로 호출 하는게 아닌 해당하는 함수를 thisBinding해서 새로운 함수를 return함 -> 이 return을 받는 변수가 필요
  • bind목적 : 함수의 this를 미리 적용하기  /  ★★★★부분 적용 함수(중요!)    =>bind를 제일 많이 사용
var func = function (a, b, c, d) {
    console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // glbal객체에 1 2 3 4찍힘

// 함수에 this를!!! 미리 적용(즉시 실행이 아닌)
var bindFunc1 = func.bind({ x: 1 }); // 바로 호출되지는 않아요! 그 외에는 같아요.
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8 출력

// 부분 적용 함수 구현
var bindFunc2 = func.bind({ x: 1 }, 4, 5); // 4와 5를 미리 적용(선점)했음 - 원래는 4개의 인자가 필요한데 두개를 미리 적용 했기에 두 개의 인자만 필요하다!(2개의 인자를 부분 적용한 것!)
bindFunc2(6, 7); // { x: 1 } 4 5 6 7 출력
bindFunc2(10, 11); // { x: 1 } 4 5 10 11 출력

//name 프로퍼티! (bind가 의미하는 한가지 추가기능!)
//bind적용한 함수는 name 프로퍼티에 의미!! 있는 값을 가지고 있음(bound!라는 접두어가 붙은 함수- 'bind의 수동태') => 추적하기가 매우 쉽다!!!!
console.log(func.name);      //func 출력
console.log(bindFunc1.name); //bound func 출력
console.log(bindFunc2.name); //bound func 출력

※ 상위context의 this를 내부함수나 callback 함수에 전달 하는 방법

  • 1. self 붙이기 (하드코딩 방법) - 내부함수에 전달
  • 2. call , apply, bind 사용 - this가 명확하게 binding됨(bind로 미리 적용하는게 제일 많이 사용!)
  • 3. arrow function : thisBinding이 없기에 무조건 상위의 객체를 바라보게 됨! (상위가 전역 객체가 아닌 이상 전역객체로 새롭게 세팅되는 케이스가 없음!) - 제일 편리

 

* 숙제 1) getAged는 숫자만큼 나이를 먹은 객체를 새로 생성하는 문제(객체를 복사하지만 원래의 user객체와 달라야 함! - 객체는 참조형을 쓰고 있기에 순회하며 아예 새로운 객체 만들기)

var getAged = function (user, passedTime) {
        var agedUser = user.name
        var agedAge = user.age + passedTime
        return agedUser, agedAge
}        

=> 내가 적은 답 : 결과는 성공적 but 여러 속성이 들어오게 되면 비효율적

//답안지 정답 요게 더 정확쓰
var getAged = function (user, passedTime) {
    var newUser = {};
    for (var prop in user) {
        newUser[prop] = user[prop];
    }

    newUser.age += passedTime;
    return newUser
}    

=> 순회하면서 새 객체에 기존 객체를 복사함(새로운 객체 만들기)

 

*숙제 2) window의 console에서 나오는 값 예상하기(이유 설명)

//요거는 console에서 찍어야 오류가 안남(vs code에서의 전역 this는 global을 가리키기 때문에 문제 의도에 맞게 브라우저에서 실행 = 얘는 window를 가리킴)
var fullname = 'Ciryl Gane'

var fighter = {
    fullname: 'John Jones',
    opponent: {
        fullname: 'Francis Ngannou',
        getFullname: function () {
            // 1. 객체 this 바인딩 : 프란시스 은가누
            return this.fullname;
        }
    },

    getName: function() {
        // 2. 객체 this 바인딩 : 존 존스
        return this.fullname;
    },

    getFirstName: () => {
        // 3. 함수 this 바인딩 : 시릴
        return this.fullname.split(' ')[0];
    },

    getLastName: (function() {
        // 4. 함수 this 바인딩 : 시릴
        return this.fullname.split(' ')[1];
    })()

}

console.log('Not', fighter.opponent.getFullname(), 'VS', fighter.getName());
console.log('It is', fighter.getName(), 'VS', fighter.getFirstName(), fighter.getLastName);

=> 각 this가 가리키는 값 찾기 우선 첫번째의 fighter.opponent.getFullname()은 객체가 호출 한 것으로 즉 메소드에 의한 호출 따라서 해당 객체this의 fullname인 Francis Ngannou이다. 두 번째, 세번째의 fighter.getName()도 객체가 호출 한 것, 메소드에 의한 호출로 해당 객체this의 fullname인 John Jones이다. 마지막은 fighter.getLastName으로 호출부가 없음(()), 따라서 스스로 선언 후 호출이 되었기에 호출의 주체가 없어 thisBinding을 유실해 전역 객체를 바라보게 된다. 그러므로 Ciryl Ganme이다.

'JS' 카테고리의 다른 글

JS 메소드  (4) 2023.05.30
JS 5주차 개념 정리  (0) 2023.05.25
JS 4주차 개념 정리  (0) 2023.05.25
개발일지(2023.05.22)  (0) 2023.05.22
개발일지(2023.05.22)  (0) 2023.05.22