describe('expect에 대해서 학습합니다.', function () {
/*
---
연습문제(coplit)를 풀다보면 여러분이 제출한 코드를 테스트 케이스 별로 검사하여 전체 결과를 보여줬던 것을 기억하시나요?
여러분이 작성한 함수가 주어진 입력값에 대해서 리턴하는 값이 기대하는 값과 같은지를 비교하는 것입니다.
이때 테스트하는 값과 기대값을 비교하기 위해 expect 함수를 사용합니다.
expect의 사용법은 아래와 같습니다.
expect(테스트하는값).기대하는조건
expect(isEven(3)).to.be.true => 'isEven(3)'의 결과값은 참(true)이어야 한다'
expect(1 + 2).to.equal(3) => 'sum(1, 2)의 결과값은 3과 같아야(equal) 한다'
'기대하는조건'에 해당하는 함수를 matcher라고 합니다.
'참인 것이어야 한다' => to.be.true
'3과 같아야 한다' => to.equal(3)
mocha, chai framework에는 다양한 matcher가 있지만, 이번 과제에서는 그 일부가 사용됩니다.
궁금하시면 아래 링크를 통해 확인하시기 바랍니다. (어떤 것들이 있는지만 확인하는 수준이면 충분합니다.)
https://www.chaijs.com/api/bdd/
여러분들은 expect를 활용하여 모든 Koans 과제를 완성하시면 됩니다.
즉, 각 테스트 케이스가 테스트를 통과하도록 테스트 코드를 적절하게 변형하여야 합니다.
단순히 테스트를 통과하는 것을 넘어, '왜 통과하는지'를 고민하는 시간이 되었으면 합니다.
*/
it('테스트하는 값(expect의 전달인자)이 true인지의 여부를 검사합니다.', function () {
/*
첫 문제는 가볍게 풀어봅시다.
expect 함수의 전달인자를 아래와 같이 false 대신 true로 변경하여 테스트를 통과하세요.
expect(true).to.be.true;
*/
// TODO: 테스트가 통과될 수 있도록(테스트하는 값이 true가 되도록) expect의 첫 번째 전달인자를 수정합니다.
expect(true).to.be.true;
});
it('테스트하는 값(expect의 전달인자)이 falsy 여부를 검사합니다.', function () {
// 반대의 경우에는 어떻게 해야할까요?
// TODO: 테스트가 통과될 수 있도록(테스트하는 값이 false가 되도록) expect의 첫 번째 전달인자를 수정합니다.
expect(false).to.be.false;
});
/*
2가지 matcher(.true, .false)를 통해 '기대하는 값'이 true인지 false인지 확인하는 방법을 학습했습니다.
이 때, '기대하는 값'은 표현식(expression)이거나 함수의 실제 실행 결과입니다.
1) 표현식: true || false, 1 + 1, 10 * 3
2) 함수의 실행: isEven(3), sum(1, 2)
보시면 알 수 있듯이, '기대하는 값'은 true, false 뿐만 아니라 어떤 구체적인 값인 경우가 있습니다.
이 경우 '기대하는 값'이 '특정 값'과 같은지를 비교해서 테스트를 진행하면 됩니다.
가장 간단한 방법은 비교 연산자 === 을 사용하는 방법이 있습니다.
'기대하는 값과 특정 값을 비교한 결과'가 true인지 확인하면 됩니다.
expect('기대하는 값과 특정 값을 비교한 결과').to.be.true;
*/
it("'테스트하는 값'을 '기대하는 값'과 비교한 결과가 참 인지 확인합니다.", function () {
// '테스트하는 값'은 우리가 작성한 어떤 코드의 실제 실행 결과 값이므로 '실제 값'이라고 불러도 됩니다.
let actualValue = 1 + 1;
let expectedValue = 1+1; // TODO: 'FILL_ME_IN'을 변경하여 테스트 케이스를 완성합니다.
expect(actualValue === expectedValue).to.be.true;
});
/*
이처럼 to.be.true, to.be.false 만을 가지고도 많은 테스트 케이스를 작성할 수 있습니다.
하지만 이는 직관적이지 않고 다소 불편합니다.
두 값 A와 B를 '비교한 결과'가 참인지를 확인하는 대신에 직접 A가 B와 같은지 확인하는 matcher가 없을까요?
.equal이 바로 그런 역할을 합니다. 아래 테스트 코드는 '테스트하는값'이 '기대하는값'과 같은지 직접 확인합니다.
expect('테스트하는값').to.equal('기대하는값');
이제 'FILL_ME_IN'을 적절한 값으로 변경하여 테스트 케이스를 완성하시면 됩니다.
이후에도 같은 방식으로 'FILL_ME_IN' 변경하면 됩니다.
*/
it('Matcher .equal 의 사용법을 학습합니다.', function () {
let expectedValue = 1+1; // TODO
// .equal은 두 값이 타입까지 엄격하게 같은지 검사(strict equality, ===)합니다.
expect(1 + 1).to.equal(expectedValue);
});
it('Matcher .equal의 사용법을 학습합니다.', function () {
let actualValue = (1 + 1).toString();
expect(actualValue).to.equal((1+1).toString()); // TODO
});
});
describe('type에 대해서 학습합니다.', function () {
it("비교연산자 '=='는 두 값의 일치 여부를 느슨하게 검사(loose equality)합니다.", function () {
let actualValue = 1 + 1;
let expectedValue = 1+1;
expect(actualValue == expectedValue).to.be.true;
/*
혹시 'FILL_ME_IN'을 '2'(문자열 '2')로 변경해도 테스트가 통과된다는 사실을 알고 계십니까?
'=='의 실행 중 타입 변환(type coercion)이 일어나기 때문입니다.
이는 다소 복잡하고, 명확한 규칙이 존재하지 않습니다.
느슨한 동치 연산자 '=='의 느슨함을 보여주는 예시가 아래에 있습니다.
아래 테스트 코드들의 주석을 제거해도 이 테스트는 통과합니다.
*/
// 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;
});
/*
사실 느슨한 동치 연산(loose equality)는 프로그램의 작동을 예측하기 다소 어렵게 만듭니다.
'=='의 특성을 정확하게 외워서 모든 상황에 대응하겠다는 자세는 접어두시기 바랍니다.
매우 비효율적일뿐더러 일반적인 컴퓨터 프로그래밍 언어의 관습(convention)과도 거리가 먼 자세입니다.
지금부터는 엄격한 동치 연산(strict equality) '==='을 사용하시기 바랍니다.
*/
it("비교연산자 '==='는 두 값의 일치 여부를 엄격하게 검사(strict equality)합니다.", function () {
let actualValue = 1 + 1;
let expectedValue = 1+1;
expect(actualValue === expectedValue).to.be.true;
// 이제 'FILL_ME_IN'을 대신할 수 있는 건 number 타입의 2뿐입니다.
// 문자열 '2'는 테스트를 통과하지 못합니다.
});
it('expect의 전달인자로 들어간 표현식의 평가(evaluation) 결과를 예측해 봅니다.', function () {
expect(1 + '1').to.equal(1+'1');
});
it('expect의 전달인자로 들어간 표현식의 평가(evaluation) 결과를 예측해 봅니다.', function () {
expect(123 - '1').to.equal(123-'1');
});
it('expect의 전달인자로 들어간 표현식의 평가(evaluation) 결과를 예측해 봅니다.', function () {
expect(1 + true).to.equal(1+true);
});
it('expect의 전달인자로 들어간 표현식의 평가(evaluation) 결과를 예측해 봅니다.', function () {
expect('1' + true).to.equal('1'+true);
});
/*
지금까지 본 것처럼 자바스크립트에는 다소 이해하기 힘든 부분들이 존재합니다.
아래처럼 자바스크립트의 별난(quirky) 부분들을 따로 모아둔 저장소도 있을 정도입니다.
https://github.com/denysdovhan/wtfjs
여기서도 동치 연산을 학습하면서 당부한 자세가 요구됩니다.
이런 별난 특성들을 전부 외워서 모든 상황에 대응하려고 하지 말고, 올바른 코딩 습관을 기르시기 바랍니다.
대표적으로 최대한 같은 타입끼리 연산을 하고, 즉 엄격한 동치 연산('===')을 사용하고, 조건문에 비교 연산을 명시하는 것이 훨씬 좋습니다.
*/
});
describe("'const'에 대해서 학습합니다.", function () {
it("'const'로 선언된 변수에는 재할당(reassignment)이 금지됩니다.", function () {
// 아래 코드에서 문제가 되는 부분을 삭제합니다.
const constNum = 0;
expect(constNum).to.equal(0);
const constString = 'I am a const';
expect(constString).to.equal('I am a const');
});
it("'const'로 선언된 배열의 경우 새로운 요소를 추가하거나 삭제할 수 있습니다.", function () {
const arr = [];
const toBePushed =42;
arr.push(toBePushed);
expect(arr[0]).to.equal(42);
// 여전히 재할당은 금지됩니다.
// arr = [1, 2, 3];
});
it("'const'로 선언된 객체의 경우, 속성을 추가하거나 삭제할 수 있습니다.", function () {
const obj = { x: 1 };
expect(obj.x).to.equal(1);
delete obj.x;
expect(obj.x).to.equal(0);
// 여전히 재할당은 금지됩니다.
// obj = { x: 123 };
obj.occupation ='SW Engineer';
expect(obj['occupation']).to.equal('SW Engineer');
});
/*
재할당도 안되는 'const' 키워드를 굳이 써야하는지 이해가 안 될수도 있습니다.
'let' 키워드는 재할당이 가능하기 때문에 여러모로 편하고, 큰 문제도 없어 보이기 때문입니다.
이에 대해서 잠시 고민하신 후, 'const'가 추천되는 이유에 대해 직접 찾아보시기 바랍니다.
동기 부여를 위해 구글 자바스크립트 코딩 스타일 가이드를 소개해 드립니다.
세계의 탑코더들이 있는 구글 스타일 가이드는 선언 키워드에 대해서 어떻게 안내하고 있을까요?
https://google.github.io/styleguide/jsguide.html#features-use-const-and-let
*/
});
describe("'const'에 대해서 학습합니다.", function () {
it("'const'로 선언된 변수에는 재할당(reassignment)이 금지됩니다.", function () {
// 아래 코드에서 문제가 되는 부분을 삭제합니다.
const constNum = 0;
expect(constNum).to.equal(0);
const constString = 'I am a const';
expect(constString).to.equal('I am a const');
});
it("'const'로 선언된 배열의 경우 새로운 요소를 추가하거나 삭제할 수 있습니다.", function () {
const arr = [];
const toBePushed =42;
arr.push(toBePushed);
expect(arr[0]).to.equal(42);
// 여전히 재할당은 금지됩니다.
// arr = [1, 2, 3];
});
it("'const'로 선언된 객체의 경우, 속성을 추가하거나 삭제할 수 있습니다.", function () {
const obj = { x: 1 };
expect(obj.x).to.equal(1);
delete obj.x;
expect(obj.x).to.equal(0);
// 여전히 재할당은 금지됩니다.
// obj = { x: 123 };
obj.occupation ='SW Engineer';
expect(obj['occupation']).to.equal('SW Engineer');
});
/*
재할당도 안되는 'const' 키워드를 굳이 써야하는지 이해가 안 될수도 있습니다.
'let' 키워드는 재할당이 가능하기 때문에 여러모로 편하고, 큰 문제도 없어 보이기 때문입니다.
이에 대해서 잠시 고민하신 후, 'const'가 추천되는 이유에 대해 직접 찾아보시기 바랍니다.
동기 부여를 위해 구글 자바스크립트 코딩 스타일 가이드를 소개해 드립니다.
세계의 탑코더들이 있는 구글 스타일 가이드는 선언 키워드에 대해서 어떻게 안내하고 있을까요?
https://google.github.io/styleguide/jsguide.html#features-use-const-and-let
*/
});
describe('화살표 함수에 관해서', function () {
it('함수 표현식 사용법을 복습합니다', function () {
const add = function (x, y) {
return x + y
}
expect(add(5, 8)).to.eql(13)
})
it('화살표 함수 사용법을 익힙니다', function () {
// function 키워드를 생략하고 화살표 => 를 붙입니다
const add = (x, y) => {
return x + y
}
expect(add(10, 20)).to.eql(30)
// 리턴을 생략할 수 있습니다
const subtract = (x, y) => x - y
expect(subtract(10, 20)).to.eql(-10)
// 필요에 따라 소괄호를 붙일 수도 있습니다
const multiply = (x, y) => (x * y)
expect(multiply(10, 20)).to.eql(200)
// 파라미터가 하나일 경우 소괄호 생략이 가능합니다
const divideBy10 = x => x / 10
expect(divideBy10(100)).to.eql(10)
})
it('화살표 함수를 이용해 클로저를 표현합니다', function () {
const adder = x => {
return y => {
return x + y
}
}
expect(adder(50)(10)).to.eql(60)
const subtractor = x => y => {
return x - y
}
expect(subtractor(50)(10)).to.eql(40)
const htmlMaker = tag => textContent => `<${tag}>${textContent}</${tag}>`
expect(htmlMaker('div')('code states')).to.eql('<div>code states</div>')
const liMaker = htmlMaker('li')
expect(liMaker('1st item')).to.eql('<li>1st item</li>')
expect(liMaker('2nd item')).to.eql('<li>2nd item</li>')
})
})
describe('primitive data type과 reference data type에 대해서 학습합니다.', function () {
/*
* 아래 주석이 이해하기 어렵다면, 유어클래스 Lesson - Primitive & Reference를 복습하세요 :)
자바스크립트에서 원시 자료형(primitive data type 또는 원시값)은 객체가 아니면서 method를 가지지 않는 아래 6가지의 데이터를 말합니다.
string, number, bigint, boolean, undefined, symbol, (null)
*/
it('원시 자료형은 값 자체에 대한 변경이 불가능(immutable)합니다.', function () {
let name = 'codestates';
expect(name).to.equal('codestates');
expect(name.toUpperCase()).to.equal('CODESTATES');
expect(name).to.equal('codestates');
// 새로운 값으로 재할당은 가능합니다.
name = name.toUpperCase();
expect(name).to.equal('CODESTATES');
/*
원시 자료형은 값 자체에 대한 변경이 불가능하다고 하는데, 한 변수에 다른 값을 할당하는 것은 변경이 된 것이 아닌가요?
let num1 = 123;
num2 = 123456;
원시 자료형 그 자체('hello', 123, 456n, true 등)와 원시 자료형이 할당된 변수는 구분되어야 합니다.
사과 박스에 귤을 담았다고 해서, 귤이 갑자기 사과가 되지는 않는 것과 같이 123이 갑자기 123456이 되지 않습니다.
*/
});
it('원시 자료형을 변수에 할당할 경우, 값 자체의 복사가 일어납니다.', function () {
let overTwenty = true;
let allowedToDrink = overTwenty;
overTwenty = false;
expect(overTwenty).to.equal(false);
expect(allowedToDrink).to.equal(true);
let variable = 'variable';
let variableCopy = 'variableCopy';
variableCopy = variable;
variable = variableCopy;
expect(variable).to.equal('variable');
});
it('원시 자료형 또는 원시 자료형의 데이터를 함수의 전달인자로 전달할 경우, 값 자체의 복사가 일어납니다.', function () {
let currentYear = 2020;
function afterTenYears(year) {
year = year + 10;
}
afterTenYears(currentYear);
expect(currentYear).to.equal(2020);
function afterTenYears2(currentYear) {
currentYear = currentYear + 10;
return currentYear;
}
let after10 = afterTenYears2(currentYear);
expect(currentYear).to.equal(2020);
expect(after10).to.equal(2030);
// 사실 함수의 전달인자도 변수에 자료(data)를 할당하는 것입니다.
// 함수를 호출하면서 넘긴 전달인자가 호출된 함수의 지역변수로 (매 호출 시마다) 새롭게 선언됩니다.
});
/*
자바스크립트에서 원시 자료형이 아닌 모든 것은 참조 자료형 입니다. 배열([])과 객체({}), 함수(function(){})가 대표적입니다.
const pi = 3.14
const arr = ["hello", "world", "code", "states"];
위 두 가지 코드에서 어떤 차이를 찾으실 수 있나요? 아쉽게도 보기에는 큰 차이가 없습니다.
하지만 자바스크립트는 보기와는 다르게 작동되는 부분이 있습니다. (under the hood)
여기서 변수 pi에는 3.14라는 원시 자료형 '값'이 할당되고, arr에는 참조 자료형의 '주소'가 할당됩니다.
영어 단어 reference 의미와 연결시켜보면 실제 데이터가 저장된 주소를 가리킨다(refer), 즉, 참조(reference)한다로 이해하면 쉽습니다.
왜 참조 자료형에서는 '주소'를 할당할 수 밖에 없을까요?
원시 자료형은 immutable 하다고 말씀 드렸습니다. 참조 자료형은, 그렇지 않습니다.
우리가 배열에 요소를 추가 및 삭제하고, 객체에 속성을 추가 및 삭제할 수 있었습니다.
이것 자체가, 참조 자료형은 이미 immutable하지 않다는 것을 보여주고 있습니다.
언제든 데이터가 늘어나고 줄어들 수 있죠 (동적으로 변한다.), 그렇기 때문에 특별한 저장공간의 주소를 변수에 할당함으로써 더 잘 관리하고자 합니다.
이런 저장 공간을 heap이라고 부릅니다.
아래와 같이 코드가 작성되어 있다면...
let num = 123;
const msg = "hello";
let arr = [1, 2, 3];
const isOdd = true;
원시 자료형의 데이터가 저장되는 공간 (stack)
1 | num | 123
2 | msg | "hello"
3 | arr | heap의 12번부터 3개 // (실제 데이터가 저장되어 있는 주소)
4 |isOdd| true
=====================================
Object 자료형의 데이터가 저장되는 공간 (heap)
10 ||
11 ||
12 || 1
13 || 2
14 || 3
실제 자바스크립트는 변수를 위와 같이 저장할 것입니다.
*
* 위의 원리가 잘 이해 되셨나요? heap과 stack이라는 용어가 어색하더라도, 위 원리가 잘 이해되었다면 괜찮습니다.
* 이해가 잘 안되시면, 원시 자료형이 할당되는 경우는 값 자체가 할당되고, 참조 자료형은 주소가 할당된다고 암기하셔도 좋습니다.
*
* const hello = "world"; // "world" 그 자체
* const arr = [1, 2, 3]; // [1, 2, 3] 의 메모리 주소 xxxxxx
*
*/
it('참조 자료형의 데이터는 동적(dynamic)으로 변합니다.', function () {
const arr = [1, 2, 3];
expect(arr.length).to.equal(3);
arr.push(4, 5, 6);
expect(arr.length).to.equal(6);
arr.pop();
expect(arr.length).to.equal(5);
const obj = {};
expect(Object.keys(obj).length).to.equal(0);
obj['name'] = 'codestates';
obj.quality = 'best';
obj.product = ['sw engineering', 'product manager', 'growth marketing', 'data science'];
expect(Object.keys(obj).length).to.equal(3);
delete obj.name;
expect(Object.keys(obj).length).to.equal(2);
});
it('참조 자료형을 변수에 할당할 경우, 데이터의 주소가 저장됩니다.', function () {
/*
참조 자료형의 경우, 값 자체의 복사가 일어나지 않는 이유는 어느 정도 납득할만한 이유가 있습니다.
배열이 얼마나 많은 데이터를 가지고 있는지가 프로그램의 실행 중 수시로 변경될 수 있기 때문입니다.
쉽게 생각해서 number 타입 데이터 100만개를 요소로 갖는 배열을 생각해 봅시다.
따로 명시하지 않는 이상 100만개의 데이터를 일일히 복사하는 것은 상당히 비효율적입니다.
따라서 일단은 주소만 복사해서 동일한 데이터를 바라보는 게 만드는 것이 효율적입니다.
배열과 객체의 데이터를 복사하는 방법은 06_Array.js, 07_Object.js에서 다룹니다.
*/
const overTwenty = ['hongsik', 'minchul', 'hoyong'];
let allowedToDrink = overTwenty;
overTwenty.push('san');
expect(allowedToDrink).to.deep.equal([
'hongsik', 'minchul', 'hoyong','san'
]);
overTwenty[1] = 'chanyoung';
expect(allowedToDrink[1]).to.deep.equal('chanyoung');
// .deep.equal은 배열의 요소나 객체의 속성이 서로 같은지 확인하는 matcher입니다.
// .equal아닌 .deep.equal을 사용하는 이유는 아래 테스트 코드를 통해 고민하시기 바랍니다.
const ages = [22, 23, 27];
allowedToDrink = ages;
expect(allowedToDrink === ages).to.equal(true);
expect(allowedToDrink === [22, 23, 27]).to.equal(false);
const nums1 = [1, 2, 3];
const nums2 = [1, 2, 3];
expect(nums1 === nums2).to.equal(false);
const person = {
son: {
age: 9,
},
};
const boy = person.son;
boy.age = 20;
expect(person.son.age).to.equal(20);
expect(person.son === boy).to.equal(true);
expect(person.son === { age: 9 }).to.equal(false);
expect(person.son === { age: 20 }).to.equal(false);
/*
아래의 테스트 코드들은 선뜻 받아들이기 힘들 수 있습니다.
const nums1 = [1, 2, 3];
const nums2 = [1, 2, 3];
expect(nums1 === nums2).to.equal(FILL_ME_IN);
배열 nums1과 배열 num2에는 동일한 데이터 [1, 2, 3]이 들어있는 게 분명해 보이는데, 이 둘은 같지가 않습니다.
사실 변수 num1와 num2는 배열이 아닙니다.
참조 타입의 변수에는 (데이터에 대한) 주소만이 저장된다는 것을 떠올려 봅시다.
정확히 말해서 변수 num1은 데이터 [1, 2, 3]이 저장되어 있는 메모리 공간(heap)을 가리키는 주소를 담고 있습니다.
따라서 위의 코드는 각각 다음의 의미를 가지고 있습니다.
const nums1 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num1에 저장된다.
const nums2 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num2에 저장된다.
이제 heap에는 두 개의 [1, 2, 3]이 저장되어 있고, 각각에 대한 주소가 변수 num1, num2에 저장되어 있습니다.
이게 비효율적으로 보일수도 있습니다. 굳이 같은 데이터를 왜 한번 더 저장하는 지 이해하기란 쉽지 않습니다.
하지만 [1, 2, 3]이 아니라 상당히 큰 데이터(예. length가 100,000인 배열)를 가지고 다시 생각해 봅시다.
const nums1 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열
const nums2 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열
이 두 배열이 서로 같아서 두 번 저장할 필요가 없다고 말하려면, 일단 두 배열이 같은지 확인해야 합니다.
이런 작업을 Object 자료형을 쓸 때마다 한다고 가정해보면, 이것이 얼마나 비효율적인지를 금방 알 수 있습니다.
이제 아래와 같이 정리할 수 있습니다. 반드시 기억하시기 바랍니다.
Object 자료형은 데이터는 heap에 저장되고, 변수에 할당을 할 경우 변수에는 주소가 저장된다.
1) [1, 2, 3]; // [1, 2, 3]이라는 데이터가 heap에 저장되지만 변수 할당이 되지 않아 주소는 어디에도 저장되지 않는다.
2) const num1 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num1에 저장된다.
3) const num2 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num2에 저장된다.
1), 2), 3)에서 말하는 주소는 전부 다른 주소입니다.
아래의 객체 간 비교도 동일한 논리로 이해하시면 됩니다.
expect(person.son === { age: 20 }).to.equal(FILL_ME_IN);
다음 문제를 해결해 보시기 바랍니다.
const num1 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 그 주소가 변수 num1에 저장된다.
const num2 = num1; // 변수 num1에 저장된 주소가 변수 num2에 저장된다.
// 두 변수 num1, num2는 같은 주소를 저장하고 있습니다. 아래 결과는 어떻게 될까요?
expect(num1 === num2).to.equal(FILL_ME_IN);
*/
});
});
describe('primitive data type과 reference data type에 대해서 학습합니다.', function () {
/*
* 아래 주석이 이해하기 어렵다면, 유어클래스 Lesson - Primitive & Reference를 복습하세요 :)
자바스크립트에서 원시 자료형(primitive data type 또는 원시값)은 객체가 아니면서 method를 가지지 않는 아래 6가지의 데이터를 말합니다.
string, number, bigint, boolean, undefined, symbol, (null)
*/
it('원시 자료형은 값 자체에 대한 변경이 불가능(immutable)합니다.', function () {
let name = 'codestates';
expect(name).to.equal('codestates');
expect(name.toUpperCase()).to.equal('CODESTATES');
expect(name).to.equal('codestates');
// 새로운 값으로 재할당은 가능합니다.
name = name.toUpperCase();
expect(name).to.equal('CODESTATES');
/*
원시 자료형은 값 자체에 대한 변경이 불가능하다고 하는데, 한 변수에 다른 값을 할당하는 것은 변경이 된 것이 아닌가요?
let num1 = 123;
num2 = 123456;
원시 자료형 그 자체('hello', 123, 456n, true 등)와 원시 자료형이 할당된 변수는 구분되어야 합니다.
사과 박스에 귤을 담았다고 해서, 귤이 갑자기 사과가 되지는 않는 것과 같이 123이 갑자기 123456이 되지 않습니다.
*/
});
it('원시 자료형을 변수에 할당할 경우, 값 자체의 복사가 일어납니다.', function () {
let overTwenty = true;
let allowedToDrink = overTwenty;
overTwenty = false;
expect(overTwenty).to.equal(false);
expect(allowedToDrink).to.equal(true);
let variable = 'variable';
let variableCopy = 'variableCopy';
variableCopy = variable;
variable = variableCopy;
expect(variable).to.equal('variable');
});
it('원시 자료형 또는 원시 자료형의 데이터를 함수의 전달인자로 전달할 경우, 값 자체의 복사가 일어납니다.', function () {
let currentYear = 2020;
function afterTenYears(year) {
year = year + 10;
}
afterTenYears(currentYear);
expect(currentYear).to.equal(2020);
function afterTenYears2(currentYear) {
currentYear = currentYear + 10;
return currentYear;
}
let after10 = afterTenYears2(currentYear);
expect(currentYear).to.equal(2020);
expect(after10).to.equal(2030);
// 사실 함수의 전달인자도 변수에 자료(data)를 할당하는 것입니다.
// 함수를 호출하면서 넘긴 전달인자가 호출된 함수의 지역변수로 (매 호출 시마다) 새롭게 선언됩니다.
});
/*
자바스크립트에서 원시 자료형이 아닌 모든 것은 참조 자료형 입니다. 배열([])과 객체({}), 함수(function(){})가 대표적입니다.
const pi = 3.14
const arr = ["hello", "world", "code", "states"];
위 두 가지 코드에서 어떤 차이를 찾으실 수 있나요? 아쉽게도 보기에는 큰 차이가 없습니다.
하지만 자바스크립트는 보기와는 다르게 작동되는 부분이 있습니다. (under the hood)
여기서 변수 pi에는 3.14라는 원시 자료형 '값'이 할당되고, arr에는 참조 자료형의 '주소'가 할당됩니다.
영어 단어 reference 의미와 연결시켜보면 실제 데이터가 저장된 주소를 가리킨다(refer), 즉, 참조(reference)한다로 이해하면 쉽습니다.
왜 참조 자료형에서는 '주소'를 할당할 수 밖에 없을까요?
원시 자료형은 immutable 하다고 말씀 드렸습니다. 참조 자료형은, 그렇지 않습니다.
우리가 배열에 요소를 추가 및 삭제하고, 객체에 속성을 추가 및 삭제할 수 있었습니다.
이것 자체가, 참조 자료형은 이미 immutable하지 않다는 것을 보여주고 있습니다.
언제든 데이터가 늘어나고 줄어들 수 있죠 (동적으로 변한다.), 그렇기 때문에 특별한 저장공간의 주소를 변수에 할당함으로써 더 잘 관리하고자 합니다.
이런 저장 공간을 heap이라고 부릅니다.
아래와 같이 코드가 작성되어 있다면...
let num = 123;
const msg = "hello";
let arr = [1, 2, 3];
const isOdd = true;
원시 자료형의 데이터가 저장되는 공간 (stack)
1 | num | 123
2 | msg | "hello"
3 | arr | heap의 12번부터 3개 // (실제 데이터가 저장되어 있는 주소)
4 |isOdd| true
=====================================
Object 자료형의 데이터가 저장되는 공간 (heap)
10 ||
11 ||
12 || 1
13 || 2
14 || 3
실제 자바스크립트는 변수를 위와 같이 저장할 것입니다.
*
* 위의 원리가 잘 이해 되셨나요? heap과 stack이라는 용어가 어색하더라도, 위 원리가 잘 이해되었다면 괜찮습니다.
* 이해가 잘 안되시면, 원시 자료형이 할당되는 경우는 값 자체가 할당되고, 참조 자료형은 주소가 할당된다고 암기하셔도 좋습니다.
*
* const hello = "world"; // "world" 그 자체
* const arr = [1, 2, 3]; // [1, 2, 3] 의 메모리 주소 xxxxxx
*
*/
it('참조 자료형의 데이터는 동적(dynamic)으로 변합니다.', function () {
const arr = [1, 2, 3];
expect(arr.length).to.equal(3);
arr.push(4, 5, 6);
expect(arr.length).to.equal(6);
arr.pop();
expect(arr.length).to.equal(5);
const obj = {};
expect(Object.keys(obj).length).to.equal(0);
obj['name'] = 'codestates';
obj.quality = 'best';
obj.product = ['sw engineering', 'product manager', 'growth marketing', 'data science'];
expect(Object.keys(obj).length).to.equal(3);
delete obj.name;
expect(Object.keys(obj).length).to.equal(2);
});
it('참조 자료형을 변수에 할당할 경우, 데이터의 주소가 저장됩니다.', function () {
/*
참조 자료형의 경우, 값 자체의 복사가 일어나지 않는 이유는 어느 정도 납득할만한 이유가 있습니다.
배열이 얼마나 많은 데이터를 가지고 있는지가 프로그램의 실행 중 수시로 변경될 수 있기 때문입니다.
쉽게 생각해서 number 타입 데이터 100만개를 요소로 갖는 배열을 생각해 봅시다.
따로 명시하지 않는 이상 100만개의 데이터를 일일히 복사하는 것은 상당히 비효율적입니다.
따라서 일단은 주소만 복사해서 동일한 데이터를 바라보는 게 만드는 것이 효율적입니다.
배열과 객체의 데이터를 복사하는 방법은 06_Array.js, 07_Object.js에서 다룹니다.
*/
const overTwenty = ['hongsik', 'minchul', 'hoyong'];
let allowedToDrink = overTwenty;
overTwenty.push('san');
expect(allowedToDrink).to.deep.equal([
'hongsik', 'minchul', 'hoyong','san'
]);
overTwenty[1] = 'chanyoung';
expect(allowedToDrink[1]).to.deep.equal('chanyoung');
// .deep.equal은 배열의 요소나 객체의 속성이 서로 같은지 확인하는 matcher입니다.
// .equal아닌 .deep.equal을 사용하는 이유는 아래 테스트 코드를 통해 고민하시기 바랍니다.
const ages = [22, 23, 27];
allowedToDrink = ages;
expect(allowedToDrink === ages).to.equal(true);
expect(allowedToDrink === [22, 23, 27]).to.equal(false);
const nums1 = [1, 2, 3];
const nums2 = [1, 2, 3];
expect(nums1 === nums2).to.equal(false);
const person = {
son: {
age: 9,
},
};
const boy = person.son;
boy.age = 20;
expect(person.son.age).to.equal(20);
expect(person.son === boy).to.equal(true);
expect(person.son === { age: 9 }).to.equal(false);
expect(person.son === { age: 20 }).to.equal(false);
/*
아래의 테스트 코드들은 선뜻 받아들이기 힘들 수 있습니다.
const nums1 = [1, 2, 3];
const nums2 = [1, 2, 3];
expect(nums1 === nums2).to.equal(FILL_ME_IN);
배열 nums1과 배열 num2에는 동일한 데이터 [1, 2, 3]이 들어있는 게 분명해 보이는데, 이 둘은 같지가 않습니다.
사실 변수 num1와 num2는 배열이 아닙니다.
참조 타입의 변수에는 (데이터에 대한) 주소만이 저장된다는 것을 떠올려 봅시다.
정확히 말해서 변수 num1은 데이터 [1, 2, 3]이 저장되어 있는 메모리 공간(heap)을 가리키는 주소를 담고 있습니다.
따라서 위의 코드는 각각 다음의 의미를 가지고 있습니다.
const nums1 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num1에 저장된다.
const nums2 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 이 위치의 주소가 변수 num2에 저장된다.
이제 heap에는 두 개의 [1, 2, 3]이 저장되어 있고, 각각에 대한 주소가 변수 num1, num2에 저장되어 있습니다.
이게 비효율적으로 보일수도 있습니다. 굳이 같은 데이터를 왜 한번 더 저장하는 지 이해하기란 쉽지 않습니다.
하지만 [1, 2, 3]이 아니라 상당히 큰 데이터(예. length가 100,000인 배열)를 가지고 다시 생각해 봅시다.
const nums1 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열
const nums2 = [10, 2, 71, ..., 987]; // 길이 100,000개인 배열
이 두 배열이 서로 같아서 두 번 저장할 필요가 없다고 말하려면, 일단 두 배열이 같은지 확인해야 합니다.
이런 작업을 Object 자료형을 쓸 때마다 한다고 가정해보면, 이것이 얼마나 비효율적인지를 금방 알 수 있습니다.
이제 아래와 같이 정리할 수 있습니다. 반드시 기억하시기 바랍니다.
Object 자료형은 데이터는 heap에 저장되고, 변수에 할당을 할 경우 변수에는 주소가 저장된다.
1) [1, 2, 3]; // [1, 2, 3]이라는 데이터가 heap에 저장되지만 변수 할당이 되지 않아 주소는 어디에도 저장되지 않는다.
2) const num1 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num1에 저장된다.
3) const num2 = [1, 2, 3]; // // [1, 2, 3]이라는 데이터가 heap에 저장되고, 그 주소가 변수 num2에 저장된다.
1), 2), 3)에서 말하는 주소는 전부 다른 주소입니다.
아래의 객체 간 비교도 동일한 논리로 이해하시면 됩니다.
expect(person.son === { age: 20 }).to.equal(FILL_ME_IN);
다음 문제를 해결해 보시기 바랍니다.
const num1 = [1, 2, 3]; // [1, 2, 3]이 heap에 저장되고, 그 주소가 변수 num1에 저장된다.
const num2 = num1; // 변수 num1에 저장된 주소가 변수 num2에 저장된다.
// 두 변수 num1, num2는 같은 주소를 저장하고 있습니다. 아래 결과는 어떻게 될까요?
expect(num1 === num2).to.equal(FILL_ME_IN);
*/
});
});
describe('Object에 대해서 학습합니다.', function () {
/*
이번 과제에서는 객체의 기본적인 내용을 재확인합니다.
이머시브 과정에서 객체를 보다 자세하게 학습하게 됩니다. (예. prototype)
*/
it('Object의 기본을 확인합니다.', function () {
const emptyObj = {};
expect(typeof emptyObj === 'object').to.equal(true);
expect(emptyObj.length).to.equal(undefined);
const megalomaniac = {
mastermind: 'Joker',
henchwoman: 'Harley',
getMembers: function () {
return [this.mastermind, this.henchwoman];
},
relations: ['Anarky', 'Duela Dent', 'Lucy'],
twins: {
'Jared Leto': 'Suicide Squad',
'Joaquin Phoenix': 'Joker',
'Heath Ledger': 'The Dark Knight',
'Jack Nicholson': 'Tim Burton Batman',
},
};
expect(megalomaniac.length).to.equal(undefined);
expect(megalomaniac.mastermind).to.equal('Joker');
expect(megalomaniac.henchwoman).to.equal('Harley');
expect(megalomaniac.henchWoman).to.equal(undefined);
expect(megalomaniac.getMembers()).to.deep.equal(['joker','Harley']);
expect(megalomaniac.relations[2]).to.equal('Lucy');
expect(megalomaniac.twins['Heath Ledger']).to.deep.equal("The Dark Knight");
});
it('Object의 속성(property)를 다루는 방법을 확인합니다.', function () {
const megalomaniac = { mastermind: 'Agent Smith', henchman: 'Agent Smith' };
expect('mastermind' in megalomaniac).to.equal(true);
megalomaniac.mastermind = 'Neo';
expect(megalomaniac['mastermind']).to.equal('Neo');
expect('secretary' in megalomaniac).to.equal(false);
megalomaniac.secretary = 'Agent Smith';
expect('secretary' in megalomaniac).to.equal(true);
delete megalomaniac.henchman;
expect('henchman' in megalomaniac).to.equal(false);
});
it("'this'는 method를 호출하는 시점에 결정됩니다.", function () {
const currentYear = new Date().getFullYear();
const megalomaniac = {
mastermind: 'James Wood',
henchman: 'Adam West',
birthYear: 1970,
calculateAge: function (currentYear) {
return currentYear - this.birthYear;
},
changeBirthYear: function (newYear) {
this.birthYear = newYear;
},
};
expect(currentYear).to.equal(2022);
expect(megalomaniac.calculateAge(currentYear)).to.equal(52);
megalomaniac.birthYear = 2000;
expect(megalomaniac.calculateAge(currentYear)).to.equal(22);
megalomaniac.changeBirthYear(2010);
expect(megalomaniac.calculateAge(currentYear)).to.equal(12);
/**
* !!Advanced [this.mastermind]? this.birthYear? this가 무엇일까요?
*
* method는 '어떤 객체의 속성으로 정의된 함수'를 말합니다. 위의 megalomaniac 객체를 예로 든다면,
* getMembers는 megalomaniac 객체의 속성으로 정의된 함수인 '메소드'라고 할 수 있습니다. megalomaniac.getMembers()와 같은 형태로 사용(호출)할 수 있죠.
* 사실은, 전역 변수에 선언한 함수도 웹페이지에서 window 객체의 속성으로 정의된 함수라고 할 수 있습니다.
* window. 접두사 없이도 참조가 가능하기 때문에(window.foo()라고 사용해도 됩니다.), 생략하고 쓰는 것뿐입니다. 이렇듯, method는 항상 '어떤 객체'의 method입니다.
* 따라서 호출될 때마다 어떠한 객체의 method일 텐데, 그 '어떠한 객체'를 묻는 것이 this입니다.
* 예시로, obj이라는 객체 안에 foo라는 메서드를 선언하고, this를 반환한다고 했을 때 ( 예: let obj = {foo: function() {return this}}; )
* obj.foo() === obj 이라는 코드에 true라고 반환할 것입니다.
* this는 함수의 호출에 따라서 값이 달라지기도 합니다. (apply나 call, bind에 대해서는 하단의 학습자료를 통해 더 공부해 보세요.)
*
* 그러나 화살표 함수는 다릅니다. 자신의 this가 없습니다.
* 화살표 함수에서의 this는 자신을 감싼 정적 범위(lexical context)입니다. (전역에서는 전역 객체를 가리킵니다.)
* 일반 변수 조회 규칙(normal variable lookup rules)을 따르기 때문에, 현재 범위에서 존재하지 않는 this를 찾을 때, 화살표 함수 바로 바깥 범위에서 this를 찾습니다.
* 그렇기에 화살표 함수를 사용할 때, 이러한 특이점을 생각하고 사용해야 합니다.
*
* 이와 관련하여, this에 대해서 더 깊이 학습하셔도 좋습니다.
* 가이드가 될 만한 학습자료를 첨부합니다.
* https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this
*/
});
it('객체의 method를 정의하는 방법을 확인합니다.', function () {
const megalomaniac = {
mastermind: 'Brain',
henchman: 'Pinky',
getFusion: function () {
return this.henchman + this.mastermind;
},
battleCry(numOfBrains) {
return `They are ${this.henchman} and the` + ` ${this.mastermind}`.repeat(numOfBrains);
},
};
expect(megalomaniac.getFusion()).to.deep.equal('PinkyBrain');
expect(megalomaniac.battleCry(3)).to.deep.equal('They are Pinky and the Brain Brain Brain');
});
it('Object를 함수의 전달인자로 전달할 경우, reference가 전달됩니다.', function () {
const obj = {
mastermind: 'Joker',
henchwoman: 'Harley',
relations: ['Anarky', 'Duela Dent', 'Lucy'],
twins: {
'Jared Leto': 'Suicide Squad',
'Joaquin Phoenix': 'Joker',
'Heath Ledger': 'The Dark Knight',
'Jack Nicholson': 'Tim Burton Batman',
},
};
function passedByReference(refObj) {
refObj.henchwoman = 'Adam West';
}
passedByReference(obj);
expect(obj.henchwoman).to.equal('Adam West');
const assignedObj = obj;
assignedObj['relations'] = [1, 2, 3];
expect(obj['relations']).to.deep.equal([1,2,3]);
const copiedObj = Object.assign({}, obj);
copiedObj.mastermind = 'James Wood';
expect(obj.mastermind).to.equal('Joker');
obj.henchwoman = 'Harley';
expect(copiedObj.henchwoman).to.equal('Adam West');
delete obj.twins['Jared Leto'];
expect('Jared Leto' in copiedObj.twins).to.equal(false);
/*
마지막 테스트 코드의 결과가 예상과는 달랐을 수도 있습니다.
'Object.assign'을 통한 복사는 reference variable은 주소만 복사하기 때문입니다.
이와 관련하여 얕은 복사(shallow copy)와 깊은 복사(deep copy)에 대해서 학습하시기 바랍니다.
가이드가 될 만한 학습자료를 첨부합니다.
https://scotch.io/bar-talk/copying-objects-in-javascript
https://medium.com/watcha/깊은-복사와-얕은-복사에-대한-심도있는-이야기-2f7d797e008a
*/
});
});