- 영화 Touching the void에서, 만신창이가 된 몸을 이끌고 생존지점까지 가서 결국 목숨을 건진 주인공은 멀리 있는 목표지점을 생각하지 않고 바로 앞 큰 바위까지 20m만 더 가자는 작은 목표를 반복하며 결국 생존했다. 우리도 하루를 20m라 생각하고 살아간다면 결국 원하는 목표에 도달해 있을 것이다. 결국은 멘탈 싸움이다.
- 질문을 할 땐 에러메시지를 잘 읽고나서, 용어를 확실히 하여 문제상황을 전달할 것
- 자료구조에 대한 책은 쉬운 것부터 어려운 것까지 여러 권을 읽어볼 것.
- 메서드는 이해가 아닌 코딩스킬 숙달의 영역이기 때문에 백문이 불여일타. 손이 알아서 코딩을 쳐낼 때까지 근육의 기억세포에 입력해줘야 한다.
- 라이브러리의 메서드의 결과를 보면서 해당 메서드를 구현해보는 것을 연습해볼 것.
- 여러가지 방식이 존재한다면 그 중 하나의 정답은 없다. 정답이 있었다면 해당 방식만 살아남아 그것만 배웠을 것. 각각의 장단이 있기 때문에 여러가지가 존재하는 것이니, 어떤 장단이 있는지 제대로 알아두자.
- 다음 코드에서 함수식별자
foo
는 스코프체인에 몇 개 존재할까?
function foo(){
};
foo();
- 정답은 2개. JS엔진이 함수 이름을 식별자로 하여 전역에 함수이름 foo 식별자 등록, 그리고 함수 몸체 내에서 유효한 식별자인 foo가 함수스코프 안에서 만들어진다.
- 만약 foo 함수 내에서 자기자신을 호출하는 재귀함수라면 함수스코프 안의 foo 식별자를 불러내는 것
- 그렇다면 다음 코드에서는 어떨까?
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개일 땐 생략 불가.
- 무조건 일반 함수로 호출. 생성자함수로써 호출할 수 없기 때문에 생성자 함수에 필요한 로직이나 기능이 없어서 성능이 향상되었다.
- 화살표 함수가 아닌 함수선언문이나 표현식으로 함수를 정의하면 일을 더 하며 메모리도 더 차지하는 것이니 꼭 생성자 함수로 쓸 의도를 가지고 있어야만 한다. 안 그러면 죄책감을 가져야 한다.
- 함수 호출 시 투입되는 인수는 함수 내부에 생겨나는 arguments라는 객체의 프로퍼티값으로 등록된다.
- 인수가 몇 개 넘어올지 모르는 상황에서 함수를 구현할 때, 매개변수를 아예 받지 않는 가변인자함수로 만든 후 arguments 객체를 통해 인수 정보를 파악할 수 있다.
- arguments는 프로퍼티 key들이 숫자로 되어있고, length라는 프로퍼티를 갖는 유사배열객체로, 순회하며 작업이 가능하다.
- 그러나 여전히 배열이 아니기 때문에 배열에 유용한 메서드를 쓸 수 없다.
- 유사배열객체인 arguments를 배열로 만들기 위해 ES5 이전에는
Array.prototype.slice.call(arguments);
와 같은 각고의 노력을 기울였다. - ES6부터는 나머지 매개변수(rest parameter)를 통해 쉽게 인수들을 담은 배열객체를 얻을 수 있다.
- 유사배열객체인 arguments를 배열로 만들기 위해 ES5 이전에는
const sum = function(... args) {};
sum(1, 2, 3);
- 위의 코드에서
args
는 호출 시 투입된 인수들(여기서는 1, 2, 3)을 요소로 갖는 배열이 할당된 식별자가 된다.args = [1, 2, 3]
- 배열 메서드를 사용할 수 있는 arguments 객체가 생긴 셈.
args.reduce( () => {}, 0);
- rest parameter인 args에는 기본값을 줄 수 없다.
['banana', 'apple', 'mango']
형태의 객체이다.- 배열은 요소(element)로 구성되며 JS에서 값으로 인정되는 모든 것이 올 수 있다.
- JS는 너무 관대한 탓에 배열 안에 서로다른 타입의 요소들이 올 수 있는데, 이는 바람직하지 않다.
- 배열은 결국 for문을 돌리며 반복하여 작업을 수행하기 위해 존재하는 것인데(그래서 length값이 있는건디...) 요소의 타입이 다 다르면 통일된 작업을 못하기 때문이다.
- 타입이 일치하지 않는 요소들을 가진 배열에는 일관성있는 일을 못하니, 배열의 의미가 없다. JS엔진이 문법적으로 허용하는 것관 별개로 요소들의 타입이 일치한 배열을 만들 것.
- JS배열의 또다른 특징은 요소가 올 자리가 비워질 수 있다는 것이다. 이를 희소배열이라고 하는데, 이 또한 문법적으론 허용해도 쓰지 말자. 해당 인덱스를 참조하면 undefined가 나와서 타입 불일치하기 때문이다.
- 일반적인 의미의 배열은 메모리셀 상에서 같은 크기의 공간을 차지하며(같은 타입이기 때문에) 공백 없이 이어져 밀집되어 있다.
- 선두address 주소 + 메모리 공간 크기 * 인덱스 = 해당 인덱스의 값에 접근가능
- 인덱스 값을 통해 고속으로 접근할 수 있지만, insert나 delete할 때 중간에 넣거나 빼면 나머지 모든 요소가 밀집하기 위해 이동해야 해서 효율이 좋지 않다. (그럴 땐 linked list 쓰는 게 좋다)
- JS의 배열은 이름만 배열이고, 동일한 크기의 메모리셀을 차지하지 않는다. 해쉬테이블로 매핑되어 잇다.
- 같은 타입으로 만들면 JS엔진도 진짜 배열의 구조로 구성하는 최적화 기능이 있긴 한데, 타입이 같아도 문자열 길이가 다르면 메모리 크기 달라서 말짱꽝
- 요소의 추가/삭제에도 더 빠르게 동작, 그리고 일반객체보다는 더 빠른 속도를 내도록 내부 최적화에 공을 들여놓기는 했다.
- 요소의 개수를 나타낸다. 가장 큰 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의 배열이 가진 다채로운 매력을 알아가는 건 힘들지만 즐겁다.
- 프로토타입 체인도 신기하고 재밌다.