[자바스크립트] 벨로퍼트와 함께하는 모던 자바스크립트 2

2020. 7. 19. 17:13TIL/자바스크립트

1. 배열

자바스크립트에서 배열을 볼 때 가장 신기했던 것은 역시, 타입에 관계없이 한 배열 안에 많은 항목들이 들어간다는 것이었다. 애초에 타입을 정하지 않으니 이제는 자연스럽게 받아들여야겠다. 

const list = [1,'1',{name : 1},true];

 

이렇게 숫자, 문자열, 객체, boolean값을 한 배열 안에 모두 넣은 것이다... 아직 자바에서 벗어나지 못해서 그런지 참 기괴해 보인다...

배열과 관련된 수많은 메서드가 있는데, 하나하나 잘 사용할 줄 알아야 좋을 것 같다. 

 

배열에 관한 반복문을 돌리기 위한 구문중 for... of 가있었는데, 잘 사용되지는 않지만 알아두면 해당 코드를 마주했을 때 이해할 수 있을 것이다. 

for(let target of list){
console.log(target);
}

이렇게 하면 list에 있는 모든 index에 접근하는 것을 볼 수 있다. 

 

2. 객체를 위한 반복문

객체에 들어있는 정보들에 접근하기 위해 두 가지 방법을 사용할 수 있다. 

 

첫 번째는, Object의 함수를 이용하는 것이다.

Object.entries만 대표적인 예로 사용해보면 아래와 같은 결과를 확인할 수 있다.

 

두 번째는, for... in 구문을 사용하는 것이다.

 


3. 배열의 내장 함수

배열에는 수많은 내장 함수가 있지만 대표적으로 알아두면 유용한 내장 함수들 몇 개에 대해 정리해보았다.

 

1) forEach

const superheroes = ['아이언맨', '캡틴 아메리카', '토르', '닥터 스트레인지'];

superheroes.forEach(hero => {
  console.log(hero);
});

forEach는 기존의 for문을 대체해준다. 파라미터로 함수를 줌으로써 (이렇게 함수 형태의 파라미터를 주는 것을 콜백 함수라고 한다.) 각 인덱스에 해당하는 정보들에 행하고자 하는 행위를 정의할 수 있다. 

 

2) map

map은 배열 안의 각 원소를 변환할 때 사용되며, 이 과정에서 새로운 배열이 만들어진다. 즉, map함수를 사용한 결과물을 담을 새로운 배열을 만들어 주어야 한다. (또는 기존의 배열에 재할당해주어도 된다.) map함수의 파라미터로는 함수가 들어가야 하는데 , 외부에 정의된 함수를 넣어주어도 되고, 새로 정의해서 넘겨주어도 된다. 

const array = [1, 2, 3, 4, 5, 6, 7, 8];

const square = n => n * n;
const squared = array.map(square);
console.log(squared);

이와 같이 square라는 함수를 넣어주면, array의 각 인덱스에 대하여 모두 square함수를 적용한 결괏값을 생성하여 새로운 배열에 넣어주는 식이다. 

 

3) indexOf

indexOf는 특정 정보가 어느 index에 들어있는지를 찾아준다. 

const index = arr.indexOf(정보);

이러한 형식으로 index값을 찾아준다.

 

4) findIndex

indexOf를 통해 정보의 위치를 찾을 수 있는 경우는, 정보의 자료형이 숫자, 문자열, boolean값일 때이다.

만약 찾고자 하는 정보가 객체 거나 배열인 경우에는 findIndex함수를 사용하여 위치를 찾을 수 있다. 단, 파라미터로 조건을 넘겨주어야 해당 조건에 맞는 정보의 위치를 찾아준다. 

역시, 잘 만들어 두신 예시 코드를 통해 쉽게 이해할 수 있었다.

const todos = [
  {
    id: 1,
    text: '자바스크립트 입문',
    done: true
  },
  {
    id: 2,
    text: '함수 배우기',
    done: true
  },
  {
    id: 3,
    text: '객체와 배열 배우기',
    done: true
  },
  {
    id: 4,
    text: '배열 내장함수 배우기',
    done: false
  }
];

const index = todos.findIndex(todo => todo.id === 3);
console.log(index);

findIndex의 파라미터로 id가 3인 조건을 넘겨줌으로써, 해당 정보의 위치를 찾아낼 수 있었다.

 

5) find

findIndex가 위치를 반환해 주었다면, find는 해당 조건에 맞는 정보 자체를 반환해준다. 

위의 경우 todos.find(todo => todo.id===3); 을 해준다면 아래의 객체가 반환될 것이다.

{
    id: 3,
    text: '객체와 배열 배우기',
    done: true
  }

 

6) filter

filter 함수는 배열에서 특정 조건을 만족하는 값들만 따로 추출하여 새로운 배열을 만든다. 즉, 문맥에서도 알 수 있듯이 filter함수 또한 파라미터로 특정 조건을 넣어주어야 한다. 또한, 새로운 배열을 만들어 주므로 기존의 배열이든 새로운 배열이든 할당을 해주어야 한다. (할당 없이는 원본자료가 바뀌지 않음) 

const tasksNotDone = todos.filter(todo => todo.done === false);

이와 같이 하면 tasksNotDone은  done값이 false인 객체들만 들어간 배열이 될 것이다.

 

7) splice

splice는 배열에서 특정 항목을 제거할 때 사용한다.

두 개의 파라미터를 받는데, 첫 번째 파라미터는 제거를 시작할 index이고 , 두 번째 파라미터는 그 index로부터 몇 개를 지울지를 결정한다.

 

+ splice는 배열의 특정 인덱스에 값을 넣을때도 사용된다.

splice(넣을위치, 0 , 새로운값);

 

+ splice는 해당 배열에서 호출하여 사용할때랑, 호출한 값을 배열에 할당해줄때랑 결과가 다르다.

그냥 호출하여 사용할때 위에 정리한것처럼 작동하고, 다른 배열에 할당해주면 slice와 같은 동작을 하게된다.

 

8) slice

slice는 splice와 마찬가지로 두개의 파라미터를 넣게 되는데 첫 번째 파라미터는 어디서부터 자를지, 그리고 두 번째 파라미터는 어디까지 자를지 를 의미한다. 차이점은, splice함수는 기존 배열 자체를 변화시킨다면, slice는 위에서 여러 함수들과 마찬가지로 새로운 배열을 만들어 리턴한다는 것이다. 한마디로 원본은 유지되고, slice 기능이 적용된 배열을 새로 리턴해주는 것이므로 할당할 배열을 지정해주어야 한다.

(자바의 substring과 매우 유사)

const numbers = [10, 20, 30, 40];
const sliced = numbers.slice(0, 2); // 0부터 시작해서 2전까지

console.log(sliced); // [10, 20]
console.log(numbers); // [10, 20, 30, 40

 

9) shift와 unshift , push와 pop

shift와 unshift는 큐를 생각하면 되고, push와 pop은 스택을 생각하면 된다.

단, shift가 맨 앞의 원소를 제거하는 것이고 unshift가 맨 앞에 원소를 삽입하는 것이다.

 

10) concat

concat은 두 배열을 합친 새로운 배열을 반환해준다. ( 마찬가지로 원본 배열들에는 변화를 주지 않는다.)

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const concated = arr1.concat(arr2);

console.log(concated); //[1,2,3,4,5,6]

 

11) join

배열의 각 원소 들을 문자열 형태로 합쳐준다. (파라미터로 준 문자열 값들을 사이에 끼게 된다.)

const array = [1, 2, 3, 4, 5];
console.log(array.join()); // 1,2,3,4,5
console.log(array.join(' ')); // 1 2 3 4 5
console.log(array.join(', ')); // 1, 2, 3, 4, 5

 

 

----------------------------------------------------------------------------------------------------------------------------

그 외에 잘 사용하면 아주 유용하다는 reduce가 있는데, 조금 복잡해 보여서 따로 정리를 할 예정이다.

배열의 내장 함수들을 사용해보며 한 가지 알게 된 점은, 자바스크립트에서 배열은 객체라는 것이다. (typeof를 통해 알 수 있었음.)

또한, console.log는 값을 참조하는 형식이기 때문에, 실시간으로 값이 업데이트된다는 점이었다.

5를 push 하기 전의 numbers까지도 1,2,3,4,5가 찍힌다는 것이다. 상당히 헷갈린다... 좀 더 깊은 공부를 통해 이해해야 할 것 같다. 

 

4. 프로토타입과 클래스

프로토타입과 클래스를 알기 전에, 객체 생성자에 대해 알아야 한다. 객체 생성자는 함수를 통해서 새로운 객체를 만들고 그 안에 넣고 싶은 값 또는 함수를 구현할 수 있게 해 준다. 다음의 예를 통해서 이해해 보자.

function Animal(type, name, sound) {
  this.type = type;
  this.name = name;
  this.sound = sound;
  this.say = function() {
    console.log(this.sound);
  };
}

const dog = new Animal('개', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');

dog.say();
cat.say();

이와 같이 실행한다면 결과는 멍멍, 야옹이 나올 것이다. dog, cat 모두 같은 say를 실행함에도 결과가 달라지는 것은 this키워드를 통해 각각의 객체의 정보에 접근하기 때문이다. 이와 같이 함수 자체가 생성자의 역할을 하고 new키워드를 통해 객체를 생성할 수 있다. 이때 프로토타입이라는 개념의 필요성에 대해 살짝 생각해 볼 수 있는데, 서로 다른 두 객체가 같은 정보를 필요로 할 때 (여기서는 say함수), 하나의 정보를 두고 그것을 참조하는 식으로 사용하는 것이 프로토타입이다. 이렇게 함으로써, 같은 정보임에도 계속해서 새로운 메모리에 할당되는 것을 막을 수 있다. 

 

function Animal(type, name, sound) {
  this.type = type;
  this.name = name;
  this.sound = sound;
}

Animal.prototype.say = function() {
  console.log(this.sound);
};
Animal.prototype.sharedValue = 1;

const dog = new Animal('개', '멍멍이', '멍멍');
const cat = new Animal('고양이', '야옹이', '야옹');

dog.say();
cat.say();

console.log(dog.sharedValue);
console.log(cat.sharedValue);

프로토 타입은 객체 생성자 함수 아래에 '. prototype. [원하는 키] ='의 형식으로 선언할 수 있다. 위의 결과를 실행하면 멍멍, 야옹, 1,1 이 출력되는 것을 볼 수 있다. 

 

한편 Dog, Cat이라는 새로운 객체 생성자를 만들고 싶고 그것들이 Animal의 기능들을 이용할 수 있도록 하려면 어떻게 해야 할까? 자바로 치면 어떻게 상속을 시킬 수 있는가에 대한 고민이다. ES6부터 도입된 class를 사용하면 좀 더 명확하게 상속 관계를 구현할 수 있지만, 그 이전에 어떻게 상속과 같은 구조를 하였는지 알아보자.

function Animal(type, name, sound) {
  this.type = type;
  this.name = name;
  this.sound = sound;
}

Animal.prototype.say = function() {
  console.log(this.sound);
};
Animal.prototype.sharedValue = 1;

function Dog(name, sound) {
  Animal.call(this, '개', name, sound);
}
Dog.prototype = Animal.prototype;

function Cat(name, sound) {
  Animal.call(this, '고양이', name, sound);
}
Cat.prototype = Animal.prototype;

const dog = new Dog('멍멍이', '멍멍');
const cat = new Cat('야옹이', '야옹');

dog.say();
cat.say();

이와 같이 call함수의 첫 번째 인자로 this를 주고, 상속받고자 하는 생성자 함수에서 필요로 하는 인자들을 넣어주면 된다. 이후에 prototype을 공유해야 하기 때문에 Animal.prototype으로 할당해준다. 

프로토 타입에 대한 이해를 돕는 좋은 페이지를 찾았지만, 여기에 정리하기에는 너무 길어질 것 같아서 따로 정리를 하려고 한다. 

 

다음은 ES6부터 추가된 클래스라는 개념이다. 이는 자바에서 사용하던 class와 거의 비슷해서 어렵지 않게 이해할 수 있었다. 도무지 알쏭달쏭한 내용들 중 만난 한줄기 빛 같은 개념이었다. 

class Animal {
  constructor(type, name, sound) {
    this.type = type;
    this.name = name;
    this.sound = sound;
  }
  say() {
    console.log(this.sound);
  }
}

class Dog extends Animal {
  constructor(name, sound) {
    super('개', name, sound);
  }
}

class Cat extends Animal {
  constructor(name, sound) {
    super('고양이', name, sound);
  }
}

const dog = new Dog('멍멍이', '멍멍');
const cat = new Cat('야옹이', '야옹');

dog.say();
cat.say();

객체 생성자 함수를 만들고, 프로토타입을 선언해주었던 것과 달리, class안에 함수를 위치시키면 해당 내용은 자동적으로 prototype으로 등록이 된다고 한다! 이때 주의할점은, function키워드나 화살표 키워드를 사용하지 않고 함수를 생성한다는 것이다. 또한, 자바와 마찬가지로 상속은 extends 키워드를 통해 이루어진다. 이때 super() 함수는 상속받고자 하는 클래스의 생성자 함수를 가리킨다. 

 

다양한 클래스를 만드는 연습을 통해 익숙해지면 같은 형태를 지닌 객체들을 만들때 객체들이 지닌 값과 함수를 보기 좋은 형태로 쉽게 관리 할 수 있을 것이다.