Skip to content

Latest commit

 

History

History
195 lines (172 loc) · 14.5 KB

20210317.md

File metadata and controls

195 lines (172 loc) · 14.5 KB

JavaScript

인상 깊은 조언

  • 영화 Touching the void에서, 만신창이가 된 몸을 이끌고 생존지점까지 가서 결국 목숨을 건진 주인공은 멀리 있는 목표지점을 생각하지 않고 바로 앞 큰 바위까지 20m만 더 가자는 작은 목표를 반복하며 결국 생존했다. 우리도 하루를 20m라 생각하고 살아간다면 결국 원하는 목표에 도달해 있을 것이다. 결국은 멘탈 싸움이다.
  • 질문을 할 땐 에러메시지를 잘 읽고나서, 용어를 확실히 하여 문제상황을 전달할 것
  • 자료구조에 대한 책은 쉬운 것부터 어려운 것까지 여러 권을 읽어볼 것.
  • 메서드는 이해가 아닌 코딩스킬 숙달의 영역이기 때문에 백문이 불여일타. 손이 알아서 코딩을 쳐낼 때까지 근육의 기억세포에 입력해줘야 한다.
  • 라이브러리의 메서드의 결과를 보면서 해당 메서드를 구현해보는 것을 연습해볼 것.
  • 여러가지 방식이 존재한다면 그 중 하나의 정답은 없다. 정답이 있었다면 해당 방식만 살아남아 그것만 배웠을 것. 각각의 장단이 있기 때문에 여러가지가 존재하는 것이니, 어떤 장단이 있는지 제대로 알아두자.

수업 전 스코프 퀴즈

  1. 다음 코드에서 함수식별자 foo는 스코프체인에 몇 개 존재할까?
function foo(){
};
foo();
  • 정답은 2개. JS엔진이 함수 이름을 식별자로 하여 전역에 함수이름 foo 식별자 등록, 그리고 함수 몸체 내에서 유효한 식별자인 foo가 함수스코프 안에서 만들어진다.
  • 만약 foo 함수 내에서 자기자신을 호출하는 재귀함수라면 함수스코프 안의 foo 식별자를 불러내는 것
  1. 그렇다면 다음 코드에서는 어떨까?
const foo = function (){
};
  • 정답은 1개. 전역의 foo만 존재하며, 이 때 만약 함수 내에서 재귀함수로 스스로를 호출한다면 그것은 전역변수 foo를 호출하는 것이다.

함수 호출

  • 함수의 호출에는 일반함수로써 호출(foo();)하는 방법과, 생성자 함수로써 호출(const foo = new Foo();)이 있다.
    • 생성자 함수: 객체를 만들어내기 위해 사용되는 함수
function foo(){
  console.log(1);
}
foo(); // 일반함수로써 호출, 리턴 값은 1
new foo(); // 생성자 함수로써 호출, 리턴 값은 foo {}
  • 위 코드에서 두 호출방식은 각각 리턴값이 다르다.

  • 또 다른 방법, 메서드로써 호출하는 것도 있다.

function foo(){};
const o = { foo };
o.foo(); // 메서드로써 호출, undefined 리턴
  • 생성자로써 호출할 때 객체를 리턴하는 건 아직 몰라도 되지만 아래처럼 동작한다.
function foo(){
  this.a = 1;
  return this.a;
}
new foo(); // { a : 1 }
  • {}이 코드블럭을 의미하는 동시에 빈 객체를 의미하는 것처럼, 함수 또한 문맥에 따른 중의적 의미를 갖는다. 호출을 어떻게 하느냐에 따라 생성자 함수 or 일반 함수가 결정된다.

  • function Foo(){ ... }: 파스칼케이스로 식별자 네이밍이 되어있으니 생성자함수라고 유추할 수 있을 뿐이다.

  • 원래대로라면(ES5 이전) 어떻게 호출될 지 모르는 상황이니 생성자 함수와 일반 함수 두 가지의 기능, 즉 각 호출방식에 대한 내부로직을 미리 다 가지고 있어야 하는데 이는 함수를 두껍게 만들고 성능을 저하시킨다.

  • ES6에서 새로 추가된 메서드 축약형 함수는 다음과 같이 이해할 수 있다.

const o = {
  foo : function(){},
  bar(){}
}
  • foo는 프로퍼티 값으로 함수가 온 것이기 때문에 생성자 함수로 호출될 수가 있다. new foo()로 호출이 가능하며 const x = o.foo; new x();로도 같은 참조값을 가진 이상 생성자로써 호출할 수 있다.
  • 반면 bar는 메서드 축약형으로 왔기 때문에 생성자 함수로서 호출될 경우 에러가 난다. ES6 이후 메서드는 꼭 생성자 함수로써 호출할수 없는 메서드 축약형으로 표기할 것.

화살표 함수

function add(a, b){
  return a + b;
}
  • ES5 이전의 함수는 다음과 같이 세 가지로 호출이 가능했다.
    • add(1, 2): 일반 함수로써 호출
    • new add(1, 2): 생성자 함수로써 호출
    • o.add(1, 2): 객체 o 속 프로퍼티 값으로 넣어 메서드로써 호출
  • 이제 화살표 함수로 이 모든 것들의 비효율과 혼란을 잠재우자

화살표 함수의 문법

  • function을 지우고, 매개변수 목록이 담긴 () 후에 => { 코드블럭 }
  • 리턴문이 하나일 때, return과 중괄호 생략 가능, return문이 두개 이상이면 생략 안하고 코드블럭 + 중괄호 쓴다. 따라서 한 줄짜리 코드에 최적화
    • 한줄짜리 코드는 좋은 것인가? YES. 작은 단위는 좋은 것이며 가독성에도 좋다.
    • 특히 배열의 고차함수에서 callback 함수를 인수로 전달할 때 한 줄로 쓰고 싶은 욕망이 생겨야 한다.
  • 매개변수가 두 개 이상일 땐 매개변수 목록을 괄호로 묶어주어야 하지만, 매개변수가 한 개일땐 생략이 가능하다. (일반적으론 생략, but 회사별 컨벤션을 따른다.) 매개변수 0개일 땐 생략 불가.

화살표 함수의 장점

  • 무조건 일반 함수로 호출. 생성자함수로써 호출할 수 없기 때문에 생성자 함수에 필요한 로직이나 기능이 없어서 성능이 향상되었다.
  • 화살표 함수가 아닌 함수선언문이나 표현식으로 함수를 정의하면 일을 더 하며 메모리도 더 차지하는 것이니 꼭 생성자 함수로 쓸 의도를 가지고 있어야만 한다. 안 그러면 죄책감을 가져야 한다.

Rest Parameter (나머지 매개변수)

arguments 객체

  • 함수 호출 시 투입되는 인수는 함수 내부에 생겨나는 arguments라는 객체의 프로퍼티값으로 등록된다.
  • 인수가 몇 개 넘어올지 모르는 상황에서 함수를 구현할 때, 매개변수를 아예 받지 않는 가변인자함수로 만든 후 arguments 객체를 통해 인수 정보를 파악할 수 있다.
  • arguments는 프로퍼티 key들이 숫자로 되어있고, length라는 프로퍼티를 갖는 유사배열객체로, 순회하며 작업이 가능하다.
  • 그러나 여전히 배열이 아니기 때문에 배열에 유용한 메서드를 쓸 수 없다.
    • 유사배열객체인 arguments를 배열로 만들기 위해 ES5 이전에는 Array.prototype.slice.call(arguments);와 같은 각고의 노력을 기울였다.
    • ES6부터는 나머지 매개변수(rest parameter)를 통해 쉽게 인수들을 담은 배열객체를 얻을 수 있다.

args

const sum = function(... args) {};
sum(1, 2, 3);
  • 위의 코드에서 args는 호출 시 투입된 인수들(여기서는 1, 2, 3)을 요소로 갖는 배열이 할당된 식별자가 된다.
    • args = [1, 2, 3]
  • 배열 메서드를 사용할 수 있는 arguments 객체가 생긴 셈.
args.reduce( () => {}, 0);
  • rest parameter인 args에는 기본값을 줄 수 없다.

배열 (Array)

JS의 배열 리터럴

  • ['banana', 'apple', 'mango'] 형태의 객체이다.
  • 배열은 요소(element)로 구성되며 JS에서 값으로 인정되는 모든 것이 올 수 있다.
  • JS는 너무 관대한 탓에 배열 안에 서로다른 타입의 요소들이 올 수 있는데, 이는 바람직하지 않다.
    • 배열은 결국 for문을 돌리며 반복하여 작업을 수행하기 위해 존재하는 것인데(그래서 length값이 있는건디...) 요소의 타입이 다 다르면 통일된 작업을 못하기 때문이다.
    • 타입이 일치하지 않는 요소들을 가진 배열에는 일관성있는 일을 못하니, 배열의 의미가 없다. JS엔진이 문법적으로 허용하는 것관 별개로 요소들의 타입이 일치한 배열을 만들 것.
  • JS배열의 또다른 특징은 요소가 올 자리가 비워질 수 있다는 것이다. 이를 희소배열이라고 하는데, 이 또한 문법적으론 허용해도 쓰지 말자. 해당 인덱스를 참조하면 undefined가 나와서 타입 불일치하기 때문이다.
  • 일반적인 의미의 배열은 메모리셀 상에서 같은 크기의 공간을 차지하며(같은 타입이기 때문에) 공백 없이 이어져 밀집되어 있다.
    • 선두address 주소 + 메모리 공간 크기 * 인덱스 = 해당 인덱스의 값에 접근가능
    • 인덱스 값을 통해 고속으로 접근할 수 있지만, insert나 delete할 때 중간에 넣거나 빼면 나머지 모든 요소가 밀집하기 위해 이동해야 해서 효율이 좋지 않다. (그럴 땐 linked list 쓰는 게 좋다)
  • JS의 배열은 이름만 배열이고, 동일한 크기의 메모리셀을 차지하지 않는다. 해쉬테이블로 매핑되어 잇다.
    • 같은 타입으로 만들면 JS엔진도 진짜 배열의 구조로 구성하는 최적화 기능이 있긴 한데, 타입이 같아도 문자열 길이가 다르면 메모리 크기 달라서 말짱꽝
    • 요소의 추가/삭제에도 더 빠르게 동작, 그리고 일반객체보다는 더 빠른 속도를 내도록 내부 최적화에 공을 들여놓기는 했다.

length 프로퍼티

  • 요소의 개수를 나타낸다. 가장 큰 index 값 + 1이다.

배열의 생성

  • 배열 리터럴을 통해 생성하는 게 가장 일반적

배열 요소의 추가와 삭제

  • 추가: property의 동적 추가와 내부 동작이 동일. 따라서 이미 있는 인덱스에 값을 할당하면 갱신하는 것.
    • JS배열은 객체이기 때문에 property key에 숫자(인덱스)가 아닌 것을 넣어도 문법적으론 허용되지만 하지마라.
  • 삭제: delete 쓰면 희소배열이 되니 쓰지마라. pop()이나 shift() 등 method로만 삭제할 것.

프로토타입

function Person(name){
  this.name = name;
  this.sayHi = function(){
    console.log(`Hi! My name is ${this.name}.`);
  }
};
const me = new Person('Lee');
const you = new Person('Kim');
  • 위 코드에서, this는 생성자 함수로 만들어진 인스턴스 자신을 가리킨다.
  • 객체 리터럴로 왜 안 만들까? 이름만 다르고 나머지 property는 같은 여러개의 객체를 만들 때 유용하다.
  • 인스턴스마다 각각의 이름은 달리하지만, sayHi라는 메서드는 하는 일이 정확히 같은데 굳이 각각 가질 필요가 없다. 그래서 Person 생성자함수의 프로토타입 체인에 동적 추가해주고 상속을 받게 함으로써 메모리를 아낀다.
  • 프로토타입은 런타임 이전에 함수객체 Person이 태어날 때 동시에 Person.prototype로 태어나 상속시킬 function을 등록한다.
    • 런타임에 실행되는 me = new Person('Lee')에서 new 연산자는 뒤에 오는 인스턴스를 만들고, 이를 Person.prototype과 연결시켜준다.
  • 스코프체인이 식별자가 등록되는 곳인 것과 같이 프로토타입 체인도 프로퍼티가 등록되는 곳으로, 프로퍼티를 찾는 방향을 알려준다. (각 인스턴스에서 먼저 찾고 프로토타입으로 가서 찾는다)
    • 스코프체인과 프로토타입 체인이 협력하여 참조를 수행하는 것.
const arr = [1, 2, 3];
arr.push(1);
  • 위의 코드에서 arr에 push()라는 프로퍼티가 있는지 인스턴스에서 찾아보고, 없으면 Array.prototype에서 찾는다.

배열 생성

  • 배열 리터럴은 누가 만드는가?
    • 객체를 생성자함수로 만들면 만드는 주체는 생성자함수이며, 그 때 프로토타입이 만들어진다.
    • 배열리터럴로 만들면 실제적으론 생성자 함수가 만든게 아니라 JS엔진이 만든 것이지만 규정상 이를 만든 건 Array라는 생성자 함수이며 Array.prototype과 연결된다.
  • 그래서 배열리터럴로 생성했을지라도 배열의 메서드를 사용할 수 있다.

배열 메서드

  • Array.isArray(): 프로토타입이나 인스턴스 없이 그대로 호출하는 정적 메소드
    • 인수로 넣은 값이 배열인지 여부를 판단하여 boolean 값을 리턴한다.
  • Array.prototype.indexOf(): 인수로 넣은 값을 검색하여 인덱스 값을 리턴하는데, 없으면 -1을 리턴
  • Array.prototype.push(): 인수로 넣은 값을 배열의 맨 뒤에 추가하며 원본 배열을 바꾸는 mutator 메서드
  • Array.prototype.pop(): 배열의 stack 맨 위의 값을 리턴하며 원본 배열에서 삭제하는 mutator 메서드
  • Array.prototype.unshift(): 인수로 넣은 값을 배열의 맨 앞에 추가하며 원본 배열을 바꾸는 mutator 메서드
  • Array.prototype.concat(): 인수로 넣은 값(배열일 경우엔 풀어서)을 원본배열의 맨 뒤에 추가한 새로운 배열을 리턴하는 accessor 메서드
    • 요새는 아예 메서드가 아닌 [...arr1, ...arr2] 로 스프레드 문법을 이용한 표현식으로 배열을 결합. 인수나 매개변수 등 신경쓸게 없어서 훨씬 좋다.
    • arr1.push(...arr2) 등으로 동일한 결과의 바뀐 원본배열을 얻을 수도 있다.
  • Array.prototype.splice(): 배열의 처음이나 끝이 아닌 중간 요소를 추가하거나 삭제할 때 쓰는 mutator 메서드
    • 첫번째 인수에 인덱스, 두 번째 인수에 대체/삭제할 요소 개수, 세 번째부터의 인수에 갱신해넣을 요소 값을 넣는다.
    • 두번째 인수가 0이면 추가만, 세번째 인수를 넣지 않으면 삭제만 한다.
  • Array.prototype.slice(): 배열의 특정 부분을 복사하여 리턴하는 accessor 메서드
    • 첫번째 인수는 시작 인덱스, 두 번째 인수는 복사가 이뤄질 인덱스보다 1이 큰 종료 인덱스
    • 인수를 하나만 주면 해당 인덱스값부터 끝까지 복사한 배열을 리턴하며, 인수를 음수로 주면 뒤에서부터 잘라낸다.
    • 인수를 안 주면 모든 배열을 복사하지만 얕은 복사가 이루어진다.
    • 배열 복사에는 const copy = [... arr]로도 가능하며 이 또한 얕은 복사이다.
    • 얕은 복사를 할 경우 요소값으로 객체가 오면 참조값만 복사하기 때문에 객체 실체는 공유된다. 객체 속 객체까지 모두 복사하는 깊은 복사를 위해서는 library(Lodash의 cloneDeep메서드)를 활용하는 편이 좋다.

느낀 점

  • JS의 배열이 가진 다채로운 매력을 알아가는 건 힘들지만 즐겁다.
  • 프로토타입 체인도 신기하고 재밌다.