Sprint | koans

SURI·2021년 11월 26일

1. expect() 테스트 케이스


expect(테스트하는 값).기대하는 조건

expect() 함수를 이용해서 테스트 케이스를 만들 수 있다. 테스트하는 값(실제값)과 기대하는 값과 기대하는 조건(matcher)을 구성해서 테스트 통과 기준을 만들어낼 수 있다.

  • 기대하는 조건에 해당하는 함수를 matcher라고 한다.
  • 기대하는 값은 표현식이거나 함수의 실제 실행 결과다.
    • 표현식: true || false, 1 + 1, 10 * 3
    • 함수의 실행: isEven(3), sum(1, 2)

Q. 테스트하는 값, 기대하는 값, 실제 값, 특정 값 이런 용어들이 헷갈리긴 한다. 어쨌든 표현식이나 함수의 실행값을 넣어 어떤 특정값과 같도록 기대하는 조건을 사용하는 식으로 문제 테스트 케이스를 만들어낼 수 있다는 것.

실제 테스트 코드이다.
function () {
    let input = ['Mars', 'Wayne', 'Mary'];
    transformFirstAndLast(input);
    expect(input).to.deep.equal(['Mars', 'Wayne', 'Mary']);
  }
  • input 변수에 배열을 할당하고 내가 문제 풀이로 작성한 함수에 인자로 집어 넣는다.
  • 그 후에, 배열의 요소가 바뀌었는지 deep.equal matcher를 사용해서 기준을 만든다.

2. 느슨한/엄격한 동치 연산


==을 통한 느슨한 연산은 실행 중 타입 변환이 일어난다. 느슨함을 보여주는 여러 예시가 있다. 느슨한 동치 연산을 하면 아래 테스트 케이스를 모두 통과하게 된다. 이런 예외 상황을 외우지 말고, 엄격한 동치 연산 ===을 사용하라.


    expect(0 == false).to.be.true;
    expect('' == false).to.be.true;
    expect([] == false).to.be.true;
    expect(![] == false).to.be.true;
    expect([] == ![]).to.be.true;
    expect([] == '').to.be.true;
    expect([] == 0).to.be.true;
    expect([''] == '').to.be.true;
    expect([''] == 0).to.be.true;
    expect([0] == 0).to.be.true;
	// expect('2' == 2).to.be.true;

자바스크립트의 별난 부분들 기괴하다. 이런 상황들을 모두 외우려 하지는 말라. 최대한 같은 타입끼리 연산을 하고, 엄격한 동치 연산을 사용하고, 조건문에 비교 연산을 명시하는 코딩 습관을 길러라.

1 + '1' // '11'
123 - '1' // 122
1 + true // 2
'1' + true // '1true'

3. let과 const


const로 선언된 변수에는 재할당이 금지된다.

const로 선언된 배열의 경우 새로운 요소를 추가/삭제할 수 있다.
const로 선언된 객체의 경우 속성을 추가/삭제할 수 있다.
하지만, 여전히 재할당은 안 된다.

위와 아래가 상충된다고 느끼나? 원래 정말 이런 질문을 했었다. 참조 자료형에서 새로운 요소와 속성을 추가/삭제하는 것은 재할당과 같을까? 그렇지 않다.

새로운 요소나 속성을 추가/삭제하는 것은 새로운 주소를 부여하는 재할당이 아니고, 주소는 그대로인 집에 가구만 뺐다 넣었다 하는 거라고 보면 된다.

const 키워드로 생성한 배열이나 객체에 새롭게 [], {} 값을 재할당하면 이건 결국, 새로운 주소값을 주는 것과 같다.

코딩을 할 때, 변수가 의도적으로 재할당되지 않아야 하는 경우가 많다. 변수의 값이 바뀌지 않기를 원할 때, const 키워드가 추천된다.

4. Scope


스코프는 변수에 담긴 값을 찾을 때 확인하는 곳이다.

5. 함수 호이스팅(hoisting)

호이스팅 : 함수 안에 있는 변수의 선언들을 모두 끌어올려서 해당 함수의 유효범위(스코프)의 최상단에 선언을 해주는 것을 일컫는다.

함수 선언식은 호이스팅의 대상이다. 스코프 내에서 어떤 위치에서 함수 선언을 하든지 호출할 수 있습니다. 표현식은 호이스팅의 대상이 아니다.

sayName();

function sayName(){
  console.log('surisrui');
}

====비교====
sayName();

var sayName = function(){
  console.log('surisuri');
}

// var 호이스팅에 의해 위의 코드는 아래와 같이 작동한다.

var sayName;
sayName(); // TypeError 발생
sayName = function(){
  console.log('surisuri')
}
  

let 키워드를 사용해 함수 표현식을 작성할 때와 var 키워드를 사용해 함수 표현식을 작성할 때 던지는 에러도 다르다.

객체의 키 값에 함수가 들어 있을 때, 키 값을 조회하는 방식에서 새로운 부분이 있었다.

function funcDeclared() {
      return 'this is a function declaration';
    }

    let funcExpressed = function () {
      return 'this is a function expression';
    };

const funcContainer = { func: funcExpressed };
funcContainer.func(); // 'this is a function expression'

값을 함수로 가지는 키 이름에 () 호출하는 표시를 붙이면 실행값을 가져오는 걸 알 수 있었다.

 function shadowParameter(message) {
      message = 'Do not use parameters like this!';
      return message;
    }

이렇게 매개변수에 실행 코드에서 값을 할당해버린 함수에 대해서 어떻게 생각하는지? 이 안에는 어떤 인자를 전달해도 결국 같은 메세지만 반복할 뿐이다! 그래서 메세지 또한 이런 식으로 사용하지 말라고 나오는 것 같다.

6. 클로저


클로저는 함수와 함수가 선언된 어휘적 환경의 조합을 말한다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.

클로저 유즈 케이스 :

  • function factories
  • namespacing private variables/functions

Q. 함수 안에 있는 변수는.. 그 함수가 실행되고 있다고 가정하고 그대로 따라가면 되는건가? 선언부 부분.. 실행문 부분. 그에 따른 값의 변화들. 조금 헷갈리지만 대충은 알 것 같다. (sprint 4번)

7. 화살표 함수


화살표 함수는 함수 표현식에서 function 키워드를 생략하고 화살표 =>를 붙인다.

const add = function (x, y) {
      return x + y
    }

=====function 키워드 생략하고 =>를 붙인다=====
const add = (x, y) => {
	return x + y
}


=====중괄호와 return이 삭제 가능한 경우=====
const add = (x, y) => x + y

// 여기에서 파라미터가 하나인 경우 소괄호도 생략이 가능하다.

화살표 함수로 클로저 표현하기


  const adder = x => {
      return y => {
        return x + y
      }
    }

  	adder(50)(10); // 60

	======비교======

    const subtractor = x => y => {
      return x - y
    }
    
    const subtractor2 = subtractor(3);
	subtractor2(1); // 2
const htmlMaker = tag => textContent => `<${tag}>${textContent}</${tag}>`

const liMaker = htmlMaker('li') 
liMaker('hi');
// 외부함수에 인자 'li'가 전달된 채로 실행되고 종료가 되었다. 그리고 그 실행값(리턴값)인 내부 함수를 다른 변수에 담는다. 이 내부 함수는 외부 함수의 변수 tag에 접근할 수 있고 그 tag는 'li' 인자를 기억하고 있는 상태가 된다. 이렇게 생각하면 될까? limaker(); 이렇게 할 수 있는 건.. 생각해보면 위에 식은 함수 표현식이라고 볼 수도 있지 않을까?

8. 원시 자료형과 참조 자료형


원시 자료형은 값 자체에 대한 변경이 불가능하다. 배열이나 객체와 달리 원본을 수정할 수 없다는 뜻인 것 같다. 재할당은 가능해도!

변수에 값을 재할당하는 것과 값 자체의 변경은 다른 것이다. 값 자체를 변경할 수는 없어도 새로운 값으로 재할당은 가능하다.

let str = 'hello'
str.toUpperCase(); // 'HELLO'
console.log(str) // 'hello'

원시 자료형을 변수에 할당할 경우, 값 자체의 복사가 일어난다.
원시 자료형 또는 원시 자료형의 데이터를 함수의 인자로 전달할 경우, 값 자체의 복사가 일어난다. 이 말은 복사가 일어나므로 그 변수의 값 자체는 변경이 되지 않는다는 뜻...

let currentYear = 2020;
    function afterTenYears(year) {
      year = year + 10;
    }
    
	afterTenYears(currentYear); 
  

    function afterTenYears2(currentYear) {
      currentYear = currentYear + 10;
      return currentYear;
    }
    let after10 = afterTenYears2(currentYear);
 
  • 위의 개념을 바탕으로 여기에서 최종적으로 after10의 값과 전역변수 currentYear의 값이 어떻게 될 지 찬찬히 생각해봐라!
  • 원시자료형 또는 원시 자료형 데이터를 함수의 인자로 전달할 때, 값 자체의 복사가 일어나는 것. 그 말은.. 그 값을 가지고 어떤 실행을 해도 원본 값의 변경은 일어나지 않는다는 거다.

자바스크립트에서 원시 자료형이 아닌 모든 것은 참조 자료형 입니다. 배열([])과 객체({}), 함수(function(){})가 대표적이다.

  • 참조 자료형은 데이터가 동적으로 변한다.
  • 참조 자료형의 데이터는 heap이라는 특별한 공간에 저장되고 그 주소값만 변수에 할당된다.(참조자료형을 변수에 할당하면, 데이터의 주소가 저장된다)
  • 참조 자료형 안에 또 참조 자료형이 들어 있는 경우, 그 또한 주소값을 저장하고 있다는 사실..?
 let number = 123;
 const word = "hello";
 let arr = [1, 2, 3];
 const isEven = true;
	[1, 2, 3]; // 이 데이터가 heap에 저장되지만 변수 할당이 되지 않아 주소는 어디에도 저장되지 않는다.

  원시 자료형의 데이터가 저장되는 공간 (stack)
     1 | number |   123
     2 | word | "hello"
     3 | arr | heap의 12번부터 3// (실제 데이터가 저장되어 있는 주소)
     4 |isEven|   true
  =====================================
  Object 자료형의 데이터가 저장되는 공간 (heap)
     10 ||   
     11 || 
     12 || 1
     13 || 2  
     14 || 3  
  
 실제 자바스크립트는 변수를 위와 같이 저장한다. 

9. 배열


 const multiTypeArr = [
      0,
      1,
      function () {
        return 10
      },
      { value1: 1, value2: 2},
      [6, 7],
    ];

console.log(multiTypeArr[2]()) // 낯선 표기! 하지만 10 
const arr = [1, 2, 3]
const poppedValue = arr.pop(); // 이 의미는 arr.pop(); 메소드를 실행하고 그 실행값을 변수에 담는다는 뜻이다.

console.log(poppedValue) // 3
console.log(arr) // [1, 2]

arr.slice()
slice() 메서드는 배열의 값을 복사해서 새로운 배열을 리턴한다.

const copiedArr = arr.slice(); // arr 배열을 복사해서 새로운 배열을 리턴한다. 여기에서도 데이터는 heap에 저장되고, heap의 주소를 copiedArr에 할당하는 거겠지. 

const shiftedValue = arr.shift(); // shift() 메서드는 원본 배열을 수정하는 것이고, 실행값은 제거된 값이다. 

const newArrLen = arr.unshift(3); // unshift() 메서드는 원본 배열을 수정하는 것이고, 실행값은 변경된 배열의 길이다. 따라서 arr.length + 1 값이 나올 것이다. 
  • start와 end 인덱스가 같은 경우, 빈 배열을 반환한다.
  • start가 end 인덱스보다 큰 경우, 빈 배열을 반환한다.

배열을 함수의 인자로 전달한 경우, 참조 주소가 전달된다.

10. 객체


let obj = {
  greeting: function () {
    return 'hello'
  }
}

obj.name = 'suri' // 객체에 속성 추가
'name' in obj // true
obj.name = 'masuri' // 객체에 속성 변경
delete obj.name //객체에 속성 제거
  • obj.greeting() 이렇게 객체의 키를 불러올 수 있다. obj.greeting과의 차이는? 전자는 실행값을 불러오고, 후자는 함수 그 자체를 불러온다.
  • 배열이나 객체에서 없는 요소나 속성을 조회하면 undefined를 리턴한다. 객체를 .length 프로퍼티를 사용해서 길이를 조회해도 undefined가 나온다.
  • 참조 자료형 안에 또 참조 자료형이 들어 있는 경우, 컴퓨터 메모리 안에서 어떤 일이 일어나는지 궁금하긴 하다.

메서드는 어떤 객체의 속성으로 정의된 함수를 말한다. 위의 예에서 greeting은 obj 객체의 속성으로 정의된 함수인 메서드라고 할 수 있다. obj.greeting() 같은 형태로 사용할 수 있다.

  • 전역 변수에 선언한 함수도 window 객체의 속성으로 정의된 함수다.
  • 메서드는 항상 어떤 객체의 메서드이다. this는 이 어떤 객체를 가리키는 키워드다.
  • 화살표 함수의 특이점은 자신의 this가 없다.
  • this는 메서드를 호출하는 시점에 결정된다.

    obj.greeting() 실행값은 this이고, 자신을 정의한 객체를 뜻한다는 걸 확인할 수 있다.

    화살표 함수의 경우 자신의 this가 없고, 자신을 감싼 정적범위가 this다. 전역에서는 전역 객체를 가리킨다.

    객체의 메서드를 화살표 함수로 정의하고 실행값을 this로 줬는데, window객체를 반환하는 걸 알 수 있다.

객체의 메서드를 정의하는 두 가지 형식

const members = {
     manager : 'suri',
     programmer: 'masuri',
     showName: function () {
        return this.manager + this.programmer;
      },
     greeting(number) {
        return `There are ${this.manager} and` + ` ${this.programmer}`.repeat(number);
      },
    };
  • greeting 메서드 정의하는 방식이 낯설다. 이렇게도 할 수 있구나.
  • showName과 greeting 메서드를 브라켓 노테이션으로만 조회하려고 했더니 안 된다.
  • repeat() 메서드 간단한 건데 왜 헤맸을까?

객체를 함수의 인자로 전달할 경우, 주소값(reference)이 전달된다. 따라서 함수 안에서 객체의 속성값을 변경할 경우, 원본 객체도 수정이 된다.

Object.assign(target, source)

function test() {
  'use strict';

  let obj1 = { a: 0 , b: { c: 0}};
  let obj2 = Object.assign({}, obj1);
  console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}

  obj1.a = 1;
  console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
  console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}

  obj2.a = 2;
  console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
  console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}

  obj2.b.c = 3;
  console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
  console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}

  // 깊은 복사
  obj1 = { a: 0 , b: { c: 0}};
  let obj3 = JSON.parse(JSON.stringify(obj1));
  obj1.a = 4;
  obj1.b.c = 4;
  console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}
}

test();
  • 객체의 키 값이 원시 자료형일 경우, 한 쪽에서 그 값을 변경해도 서로 영향을 미치지 않았다.
  • 그런데 객체의 키 값이 참조 자료형일 경우, 한 쪽에서 그 값을 변경하자 다른 쪽의 값도 똑같이 변경되었다. (흠?) 이 말은 복사된 새로운 객체 안에 참조자료형은 같은 걸 바라 보고 있다. 주소값만 복사된 얕은 복사가 일어났다. 이렇게 생각하면 될 것 같다.

    문제에서는 객체로만 보여줬는데 배열도 역시 마찬가지다. Object.assign() 메서드로는 깊은 복사가 일어나지는 않는구나. 참조자료형인 키 값은 주소값만 복사하는 것 같다.
    => 'Object.assign'을 통한 복사는 reference variable은 주소만 복사한다.

깊은 복사 vs 얕은 복사

const arr = [1, 2, [1, 2]];
const copied = arr.slice();
arr[2].push(3);
console.log(copied[2][2]) // 3

=======비교=======
arr[2] = [1, 2, 3, 4];
// 이렇게 한 경우에는 copied의 값이 바뀔까? 
  
  • slice를 통한 복사는 중첩된 구조 복사를 제대로 수행할 수 없다. 원본 arr의 참조 자료형 값을 변경했더니 복사된 배열의 값도 변경되었다.
  • 질문에 대한 답은 '아니다'였다. 이 부분을 잠시 이해하지 못했었다. 저런 경우에는 새로운 주소 값을 가진 배열이 재할당 된 것이기 때문에 복사된 배열의 값에는 영향을 주지 않는다. 이제 서로 다른 주소값을 가진 배열을 독립적인 요소로 갖게 되는 것이다.

* reference site : 깊은 복사와 얕은 복사에 대하여

10. 전개문법(Spread sytnax)


전개문법을 통해 배열과 객체를 풀어서 각각의 요소로 넣거나 병합할 수 있다.

const arr1 = [0, 1, 2]
const arr2 = [3, 4, 5]
const concat = [...arr1, ...arr2] // [0, 1, 2, 3, 4, 5]
  • 빈 배열에 전개문법을 사용하면 아무것도 전달되지 않는다.
  • 여러 개의 배열을 이어 붙일 수 있다.
  • 여러 개의 객체를 병합할 수 있다.
    • reference variable에 대해서는 깊은 복사가 일어나지 않는다. 즉, 주소 값이 할당된다.
  const lover = {
      name: 'teo',
      age: 30,
     };

    const me = {
      name: 'suri loves teo'
      status: 'sleepy',
      todos: ['study', 'sleep'],
    };

    const merged = { ...lover, ...me };

// {name: 'suri loves teo', age: 30, status: 'sleepy', todos: Array(2)}

me.todos[0] = 'hang out with friends';
me.todos[0] === merged.todos[0] // true
  • merged에 할당된 것은 객체1과 객체2의 복사된 값이다. 그렇다면 deep copy일까? 아니다. 중첩 구조에 대한 복사는 일어나지 않았다.

Rest Parameter는 함수의 인자를 배열로 다룰 수 있게 한다.

  • 자바스크립트는 함수 호출 시 인자의 순서가 중요하다.
    • 매개변수가 하나인 함수에 복수의 인자를 전달해도 나머지는 무시된다.
    • 매개 변수가 여러 개여도 전달된 인자가 부족하면 제대로 실행되지 않는다.
  • rest parameter는 전개 문법을 통해 간단하게 구현된다.
  • arguments를 통해 비슷하게 함수의 인자들을 다룰 수 있다. (전개문법 도입 이전)
    • arguments는 모든 함수의 실행 시 자동으로 생성되는 객체이다.
function getAllParamsByRestParameter(...args) {
  return args;
}

function getAllParamsByArgumentsObj() {
  return arguments;
}

========위의 두 함수를 비교========
  
const restParams = getAllParamsByRestParameter('first', 'second', 'third');
const argumentsObj = getAllParamsByArgumentsObj('first', 'second', 'third');

// 두 함수에 똑같은 인자를 전달해주었다. 실행값이 어떻게 달라지는지 비교해보자.

restParams // ['first', 'second', 'third']
Array.isArray(restParams) // true
  
======================
argumentsObj 
// Arguments(3) ['first', 'second', 'third', callee: ƒ, Symbol(Symbol.iterator): ƒ] 
typeof argumentsObj // 'object' (배열은 아님)
Object.keys(argumentsObj)	// ['0', '1', '2']
// 키의 이름이 인덱싱으로 되어있다. 스트링 처리 안해준 것 주의!
Object.values(argumentsObj) // ['first', 'second', 'third']

======================
restParams === argumentsObj // false

const argsArr = Array.from(argumentsObj); // Array.from 메서드는 새로운 Array 객체를 만든다.

Array.isArray(argsArr) // true
argsArr // ['first', 'second', 'third']
argsArr === restParams // false

  • 전개 문법으로 함수에 인자를 전달해주면 배열의 형태로 다룰 수 있게 한다는 건 이해가 된다.

  • 그런데 return arguments는 배열이 아닌 객체로 읽히는데, 키의 이름이 인덱스로 되어 있고 배열 형태로 보여준다. 이게 유사 배열 객체인 것 같다.

  • Array.from() 메서드는 유사 배열 객체(array-like object)나 반복 가능한 객체(iterable object)를 얕게 복사해 새로운 Array 객체를 만든다.

  • true랑 false에 대한 deep equal은 의미가 없는 거 같다, 맨 뒤에서 이상하게 해석해서 문제를 틀린 것 같다.

'Rest Parameter는 인자의 수가 정해져 있지 않은 경우에도 유용하다.

function sum(...num) {
let sum = 0;
  for (let i = 0; i < num.length; i++) {
  sum += num[i];
  }
  return sum;
  
  //return num;
  // sum(1, 2, 3); => [1, 2, 3] 배열로 출력이 된다.
}
  • num.length 표현과 num[i]가 생소하게 느껴졌다가 num은 배열의 형태로 다룰 수 있다는 사실을 기억해냈다.
  • rest parameter는 항상 배열이다.

'Rest Parameter는 인자의 일부에만 적용 가능하다.

function getAllParams(arg1, arg2, ...args) {
      return [arg1, arg2, args];
    }
getAllParams(123); // [123, undefined, []];   
  • 빈 배열을 리턴하는 부분 기억하자. undefined라고 생각을 했다.

11. 구조 분해 할당


rest/spread 문법을 이용해 배열을 분해한다.

const arr = ['hello', 'beautiful', 'world']
const [el1, el2] = arr;
console.log(el1) // 'hello'
console.log(el2) // 'beautiful'

====함수의 인자로 받을 때도 분해해서 받을 수 있다====
 
const newArr = [];
function sayHello([arg1, arg2]) {
	newArr.push(arg1);
  newArr.push(arg2);
  return newArr;
}

sayHello(arr); // ['hello', 'beautiful']
  • 배열의 요소를 분해해서 얻어낼 수 있다. 아직 문법이 낯설긴 하지만 받아들이면 될 것 같다.
  • rest 문법을 사용해서 배열의 형태로 분해할 수도 있다.
    • 할당하기 전 왼쪽에서, rest 문법 이후에 쉼표가 올 수 없다.
    • const [arr1, ...rest, arr2] 이걸 의미하는 것 같다.

객체의 단축 문법을 사용해 이미 할당된 변수를 그대로 가져와 값을 생략하고 쓸 수도 있다.

rest/spread 문법을 이용해 객체를 분해할 수 있다.

const student = { name: 'suri', major: 'education' }
const { name } = student // 배열을 분해하는 것과 같은 패턴임을 알 수 있다.
console.log(name) // suri

const me = { age : 30, address : 'mola'}
const {age, ...rest} = me
console.log(rest) // {address: 'mola'}
  • 객체 분해에서 rest 문법은 배열 형태로 가져오지 않고 객체로 가져온다.
  • 키의 이름이 동일해야 한다. 동일하지 않으니, 가져오지 못했다.
    • 함수의 인자에서 객체를 분해해서 가져올 때, 키의 이름을 바꿔서 가져오는 예시도 있었다.
  • 객체를 풀어서 가져올 때, 키의 이름이 동일한 경우 덮어쓰기가 되는 부분이 있었다. 순서가 중요하다. 나중에 쓰여진 것으로 덮어쓰여진다.

질문


  • 객체에서 키의 이름에 따옴표를 붙이는 것과 붙이지 않는 건 무슨 차이인가?
profile
Every step to become a better version of me 🚶‍♂️ 블로그 이사중

0개의 댓글