new 연선자와 생성자 함수

new연산자와 생성자 함수를 사용하여 유사한 객체 여러개를 쉽게 만들 수 있다.

생성자 함수

생성자 함수는 아래 두 관례를 따른다.

  1. 함수 이름의 첫 글자는 대문자로 시작한다.
  2. 반드시 new연산자를 붙여 실행한다.

생성자 함수는 다음과 같은 방식으로 동작한다.

1
2
3
4
5
6
7
8
function User(name) {
// this={}; (빈 객체가 암시적으로 만들어짐)
this.name = name;
this.isAdmin = false;
// return this;
}

let user = new User("sunghyeon");

js 조건문과 비교연산자

배열의 sort함수를 사용하면서 condition함수를 작성하던 중 실수가 있었다.

사소한 실수지만 실수를 통해 공부한 것들을 정리해두려고 한다.

자바스크립트의 비교 연산자는 두 가지가 존재한다.

==

==는 값을 비교하여서 같으면 true, 다르면 false이다.

아래의 코드를 실행하였을 때 0과 -0은 false, 양수인 1은 true, 음수는 true, false 아무 값도 가지지 않았다.

1
2
3
4
5
6
7
8
9
console.log("0==:", 0 == true, 0 == false);
console.log("-0==:", -0 == true, -0 == false);
console.log("1==:", 1 == true, 1 == false);
console.log("-1==:", -1 == true, -1 == false);

// 0==: false true
// -0==: false true
// 1==: true false
// -1==: false false

===

=== 연산자는 값과 데이터의 타입이 모두 같으면 true, 다르면 false이다.

아래의 코드를 실행하여 보았다.
숫자는 데이터타입이 Number이므로 콘솔에는 다음과 같은 결과가 찍혔다.

1
2
3
4
5
6
7
8
9
console.log("0===:", 0 === true, 0 === false);
console.log("-0===:", -0 === true, -0 === false);
console.log("1===:", 1 === true, 1 === false);
console.log("-1===:", -1 === true, -1 === false);

// 0===: false false
// -0===: false false
// 1===: false false
// -1===: false false

if문

자바스크립트의 조건문인 if문에서는 falsy값(false,0,-0,null,undefined,NaN,"",0n)을 제외한 모든 값은 true로 간주된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (0) {
console.log("0 is true");
}
if (-0) {
console.log("-0 is true");
}
if (1) {
console.log("1 is true");
}
if (-1) {
console.log("-1 is true");
}

// 1 is true
// -1 is true

결론

오름차순 정렬을 위해서는 다음과 같이 적어야한다..!
적고보니 당연한 거지만 코드를 적을때는 미처 찾지 못하였다.

1
2
3
arr.sort((a, b) => {
if (a - b < 0) return -1;
});

js Array zip함수

파이썬에는 배열에 적용할 수 있는 zip함수가 있다.

이 함수는 배열을 다음과 같이 만든다.

1
2
3
4
5
arr1 = [1, 2, 3];
arr2 = [4, 5, 6];
arr3 = [7, 8, 9];

zip(arr1, arr2, arr3); //[[1,4,7],[2,5,8],[3,6,9]];

js로는 다음과 같이 zip함수를 만들 수 있다.

1
zip = (rows) => rows[0].map((_, c) => rows.map((row) => row[c]));

실제 실행결과는 다음과 같다..!

1
2
3
4
5
6
7
8
9
10
11
arr1 = [1, 2, 3];
arr2 = [4, 5, 6];
arr3 = [7, 8, 9];

zip[(arr1, arr2, arr3)];

/* result
0: (3) [1, 4, 7]
1: (3) [2, 5, 8]
2: (3) [3, 6, 9]
*/

js 이벤트 루프

전 게시글에선 js의 타이머 함수에 대해서 알아보았다.

이번엔 이런 실행의 기반이 되는 자바스크립트의 동시성 모델과 이벤트 루프에 대해서 정리하려 한다.

런타임 개념

  1. 함수 호출은 자바스크립트의 콜스택 위에 하나의 프레임을 만든다.
  2. 자바스크립트는 콜스택의 맨 위부터 하나씩 실행하며 실행 후에는 제거한다.
  3. 자바스크립트는 메시지 큐( 처리할 메시지의 대기열)을 사용한다.
    큐에서 가장 오래된 메시지와 연결된 함수를 호출하여 실행 후 제거한다.
    마찬가지로 새로운 스택 프레임도 생성된다.

실행환경과 자바스크립트

자바스크립트와 자바스크립트 실행환경(nodejs,브라우저)는 다른 구조를 갖고있다.

실행환경은 싱글스레드가 아니다

자바스크립트는 싱글스레드를 사용하지만 실제 실행환경은 멀티스레드로 실행된다.

브라우저 환경을 간략하게 그림으로 표현하면 다음과 같다.

browser

위의 그림과 같이 비동기 호출을 위한 함수들은 webAPI 영역에 정의되어있다.

또한 이벤트 루프와 태스크 큐 등도 자바스크립트 엔진 외부에 구현되어있다.

nodejs또한 마찬가지이다.

자바스크립트 엔진

하지만 자바스크립트 엔진은 단일 호출 스택과 동시에 여러 함수를 실행할 수 없다.

자바스크립트 엔진에서의 함수 호출은 스택 프레임을 형성한다.

또한 스택의 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 어떤 함수도 실행될 수 없다.

태스크 큐와 이벤트 루프

앞서 설명했듯이 실행환경은 멀티스레드로 동작하고 자바스크립트 엔진은 싱글스레드로 동작한다.

이벤트루프와 태스크 큐는 다른 방식으로 동작하는 실행환경과 자바스크립트 엔진이 상호작용하는 방법이다.

태스크 큐

코드가 실행되면 이 코드는 현재 실행중인 태스크가 된다.

코드를 실행하는 중에 호출된 함수들은 태스크 큐에 추가된다.

마이크로 태스크 큐

마이크로 태스크 큐는 일반 태스크보다 더 높은 우선순위를 갖는 태스크이다.

대표적으로 promisethen()과 같은 메서드들은 태스크 큐가 아닌 마이크로 태스크 큐에 추가된다.

이벤트 루프

이벤트루프를 간략하게 나타내면 다음과 같다.

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

waitForMessage는 현재 실행중인 태스크가 없을 때 다음 태스크가 큐에 추가될 때 까지 대기한다.

현재 실행중인 태스크가 없는지태스크 큐에 태스크가 있는지를 반복적으로 확인한다.

현재 실행중인 함수의 실행이 다 끝나 호출스택이 비워지면 이벤트 루프는 다음과 같이 실행된다.

  1. 마이크로 태스크 큐가 비었는지 먼저 확인한다. 비어있지 않을 경우 마이크로 태스크 큐에 있는 콜백을 실행한다.( 호출스택에 추가한다.)
  2. 비어있을 경우 (일반)태스크 큐에 대기중인 가장 오래된( 맨 앞의 ) 태스크를 실행한다(호출스택에 추가한다.)

정리하자면 이벤트루프는 태스크 큐와 자바스크립트의 단일 호출 스택을 상호작용하게 해준다.

ref

Event Loop mdn
자바스크립트와 이벤트 루프
코드종 영상
Tasks, microtasks, queues and schedules

js의 타이머는 실행시간을 보장하지 못한다

자바스크립트에서 타이머가 동작하는 방식을 정리해보았다.
자바스크립트의 싱글스레드 이해해도 많은 도움이 되는 것 같다.

타이머 함수

자바스크립트 타이머 관련 함수는 3 가지가 있다.
하지만 자바스크립트는 해당 delay의 정확한 시간을 보장하지 않는다.

또한 타이머는 외부의 웹 api로 브라우저에서 제공한다.

1
2
3
4
5
6
let id = setTimeout(fn, delay);
// delay 만큼의 지연 후 fn을 호출한다. 나중에 취소할 수 있는 고유 ID를 반환한다.
let id = setInteral(fn, delay);
// setTimeout과 유사하지만 delay 마다 fn을 계속 호출한다.
clearInterval(id);
// id에 해당하는 타이머의 콜백을 발생하지 않도록 한다.

싱글스레드로 인한 실행예시

이는 자바스크립트가 싱글스레드로 동작하기 때문이다.
이는 한 번에 하나의 코드블럭만 실행할 수 있다는 것을 의미한다.

이는 실행 가능할 때(빈 자리가 있을 때)만 실행된다.

아래의 그림은 이를 설명하는 그림이다.

call stack

위에서부터 실행된다.
첫 번째 자바스크립트 블록은 18ms동안 실행되며 마우스 클릭 블록은 11ms동안 실행된다.

첫 번째 자바스크립트 블럭의 실행순서는 다음과 같다.

  1. setTimeout 타이머 설정
  2. 마우스 클릭 이벤트 발생
  3. setInterval 타이머 설정
  4. setTimeout 실행

하지만 1,2는 비동기 이벤트이고 현재 자바스크립트 블럭의 실행이 끝나지 않았으므로 즉시 실행되지 않고 큐에 넣어서 대기한다.

자바스크립트 블록의 실행이 끝났을 경우 내부에선 대기중인 큐를 확인한다.
이 과정이 what's waiting이다.
확인에도 시간이 소요된다.

그 다음 큐에서 클릭 핸들러를 가져와 실행하여 Mouse Click Callback 블럭이 생긴다.

Mouse Click Callback 실행중에
setInterval이 발생하여 대기중인 큐에 넣는다.

Mouse Click Callback의 실행이 끝나게 되면,
내부에서 다시 큐를 확인하여 setTimeout을 실행한다.

setTimeout의 실행 중에 setInterval이 발생하지만 이미 큐에는 setInterval이 대기중이므로 이는 무시된다.

그 후 setTimeout의 실행이 끝난 뒤에 발생한 setInterval은 what's Waiting? 없이 즉시 실행된다.(실행중인 블록과 대기중인 큐에 아무 것도 없기 때문이다.)

정리

  1. 자바스크립트 엔진은 싱글스레드로 동작하여 비동기 이벤트를 큐에 넣는다.
  2. 타이머 함수는 웹 api로 브라우저에서 제공하여 외부에서 delay마다 큐에 넣는다.
  3. setInterval은 마지막 콜백에 상관없이 delay마다 웹API에서 실행을 시도한다. 이미 큐에 있을 경우 무시된다.
  4. setTimeout은 웹 API에서 delay마다 실행을 시도하지만 실행되고 있는 콜백이 있을 경우 해당 콜백의 실행이 끝날 때까지 큐에서 대기한 후 실행을 시도한다. 때문에 delay만큼의 시간을 보장할 수 없다.

ref

How JavaScript Timers Work
자바스크립트 타이머는 지각쟁이? 그 이유는 싱글 콜 스택?

js Error

과거 자바스크립트가 아닌 다른 언어로 프로그래밍 할 때도 에러를 주로 caller에게 넘겨서 처리하곤 했었다.

자바스크립트로 프로그래밍 하다보면 에러를 발생시킨 caller를 차례대로 콘솔에 찍어준다.
이에 대한 정확한 로직이 궁금하여 공부 해보았다.

Error

Error객체는 런타임 오류가 발생했을 떄 던져진다.
Error 객체를 사용자 지정 예외의 기반 객체로 사용할 수 있다.

Error() 생성자를 통해 Error 객체를 생성할 수 있다.

Error 생성하기

throw 문을 통해 예외를 발생시킬 수 있다.

1
2
3
4
5
6
try {
throw "new Error";
} catch (e) {
console.error(e);
}
//new Error

다음과 같이 catch문을 작성하지 않아도 예외는 발생한다.

1
2
3
4
5
6
7
8
9
function sum(x, y) {
if (typeof x !== "number" || typeof y !== "number") {
throw "숫자가 아닙니다.";
}
return x + y;
}

console.log(sum(1, 2)); //3
console.log(sum("a", 2)); // Uncaught 숫자가 아닙니다.

콜스택과 에러

throw로 예외를 발생 시켰을 때는 현재 함수의 실행이 중지된다.
그 후 콜스택의 첫번째 catch 블록으로 전달된다.
만약 catch 블록이 존재하지 않을 경우 프로그램이 종료된다.

예외처리를 해주지 않았을 경우

아래의 코드는 예외처리를 해주지 않은 코드이다.
이 경우엔 콜스택의 첫 번째 요소부터 catch 블록을 탐색하고 처리될 때 까지 콜 스택을 차례대로 비우게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f2() {
console.log("f2 start");
throw "에러발생";
console.log("f2 end");
}

function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}

console.log("will:f1");
f1();
console.log("did: f1");

실행 순서는 다음과 같다.

  1. 콜스택이 비어있기 때문에 익명함수 호출
  2. will:f1
  3. 익명함수가 f1호출
  4. f1 start
  5. f1이 f2 호출
  6. f2 start
  7. Uncaught 에러발생
  8. 에러가 발생하며 처리될 떄 까지 호출스택을 비움

예외처리를 해주었을 경우

아래의 코드는 try catch 구문을 통해 예외처리를 해주었다.
때문에 아래의 부분은 파괴되지 않고 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f2() {
console.log("f2 start");
throw "에러발생";
console.log("f2 end");
}

function f1() {
console.log("f1 start");
try {
f2();
} catch (e) {
console.log(e);
}
console.log("f1 end");
}

console.log("will:f1");
f1();
console.log("did: f1");
  1. 익명함수 호출
  2. will:f1
  3. 익명함수가 f1호출
  4. f1 start
  5. f1이 f2 호출
  6. f2 start
  7. 에러발생
  8. f1 end
  9. did: f1

Error 객체

아래 코드는 Error 객체를 이용하여 예외를 발생시켰을 경우이다.
이럴 경우 에러가 발생했을 당시의 콜스택 상황을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f2() {
console.log("f2 start");
throw new Error("에러");
console.log("f2 end");
}

function f1() {
console.log("f1 start");
f2();
console.log("f1 end");
}

console.log("will:f1");
try {
f1();
} catch (e) {
console.log(e);
}
console.log("did: f1");
  1. 익명함수 호출
  2. 익명함수가 f1호출
  3. f1 start
  4. f1이 f2 호출
  5. f2 start
  6. 에러 + 콜스택 정보( f2에서의 콜스택 상황을 반환함)
  7. did: f1

Promise의 에러처리

Promise 내부에선 두 번쨰 인자인 reject를 이용하여 에러를 발생시킬 수 있다.

하지만 예외를 발생시킨 타이밍이 try catch 블럭이 아니다.
비동기 호출이기 때문에 콜스택이 비었을 때 타이머가 큐에 넣고 해당 큐의 내용이 콜스택이 쌓인 것이라 try 구문에서 catch가 안 된다.

따라서 실행 결과는 Uncaught (in Promise) error!이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}

try {
wait(3);
} catch (e) {
console.error(e);
}

Promise의 catch 함수

Promise의 호출뒤에 .catch(callback)을 통해 에러가 발생했을 떄 실행시킬 함수를 작성할 수 있다.

callback의 인자는 Promise내부에서 reject된 값이다.

아래 코드의 실행결과는 1st catch error!이다.

1
2
3
4
5
6
7
8
9
10
11
function wait(sec) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("error!");
}, sec * 1000);
});
}

wait(3).catch((e) => {
console.log("1st catch", e);
});

async/ await

async 함수의 리턴은 Promise이다.
따라서 Promise와 같은 방법으로 예외처리를 할 수 있다.

차이점은 await 키워드이다.

awaitasync함수 안에서만 사용할 수 있다.
Promisefullfill되거나 reject될 때 까지 async함수의 실행을 일시 정지한다.

await을 쓰지 않을 경우

아래의 코드는 await을 사용하지 않은 코드로 Promise와 같이 예외처리 해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function myAsyncFunc() {
throw "myAsyncError!";
}

function myPromiseFun() {
return new Promise((resolve, reject) => {
reject("myError");
});
}

const result = await myAsyncFunc().catch((e) => {
console.log(e);
});
// myAsyncError!
const result2 = myPromiseFun().catch((e) => {
console.log(e);
});
// MyError!

await을 쓸 경우

아래의 코드는 await을 사용한 코드로 myAsyncFun 함수는 wait 함수의 Promisefullfill 될 때까지 함수의 실행을 일시정지한다.
때문에 try catch 구문을 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function wait(sec) {
return new Promise((resolve) => {
setTimeout(() => {
reject("wait Error");
}, sec * 1000);
});
}
async function myAsyncFun() {
console.log(new Date());
try {
await wait(3);
} catch (e) {
console.error(e);
}
console.log(new Date());
}
const result = myAsyncFunc();
//현재시간, 3초뒤 에러

느낀점

비동기 함수의 예외처리는 Promise Chaining을 고려하면 된다.

ref

throw
코드종(설명이 직관적이고 친절하시다..!)
await

js prototype!

자바스크립트는 프로토타입 기반 언어라고 불린다.
프로토타입에 대해 개념을 확실하게 하기 위해 알아보았다.

프로토타입

모든 객체들이 메소드와 속성을 상속받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미이다.

상속되는 속성과 메소드들은 각 객체의 생성자의 prototype라는 속성에 정의되어 있다.

프로토타입 속성

객체의 prototype 속성은 생성자가 존재하는 함수객체에만 존재한다.

인스턴스를 생성하게 되면 해당 객체의 prototype속성 내부의 멤버들을 상속받는다.

prototype속성도 하나의 객체이며 프로토타입 체인을 통해 상속하고자 하는 속성과 메소드를 담아두는 버킷이다.

생성자가 존재하는 함수객체의 인스턴스를 생성하게 되면 객체의 프로토타입은 해당 인스턴스의 __proto__로 참조하게 된다.

아래의 코드는 문자열의 프로토 타입을 확인하는 코드이다.

1
2
3
4
5
6
let str = "hihi";
//인스턴스 생성
str.__proto__;
// 인스턴스의 __proto__ 확인(String의 prototype)
str.constructor.prototype;
// 생성자의 prototype 확인(String의 prototype)

프로토타입 체인

프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속받을 수 있다. 이는 null을 프로토타입으로 가지는 오브젝트에서 끝난다. null은 프로토타입 체인의 종점 역할을 한다.

아래의 코드는 String()에 존재하는 메소드인 toUpperCase()를 호출하는 코드이다.

1
2
3
4
let str = "hihi";
str.toUpperCase();
//"HIHI"
// 프로토타입에 정의된 함수를 호출할 수 있다.

실행 순서는 다음과 같다.

  1. 브라우저가 str 객체가 toUpperCase 메소드를 가지고 있는지 체크한다.
  2. 없으므로 str.__proto__를 통해 객체의 프로토타입 객체(String() 생성자의 프로토타입)를 참조하여 toUpperCase()메소드가 있는지 확인 후 존재하므로 실행한다.

String() 생성자의 프로토타입인 Object에만 존재하는 valueOf() 메소드를 호출했을 경우의 코드는 다음과 같다.

1
2
3
let str = "hihi";
str.valueOf();
//hihi

실행 순서는 다음과 같다.

  1. 브라우저가 str 객체가 toUpperCase 메소드를 가지고 있는지 체크한다.
  2. 없으므로 str.__proto__를 탐색하여 str의 프로토타입 객체에 toUpperCase() 메소드가 있는지 확인한다.
  3. 존재하지 않으므로 String.prototype.__proto__Object()에서 해당 메소드를 찾는다. 존재하므로 실행한다.

아래의 코드는 존재하지 않는 메서드를 실행했을 때의 코드이다.

1
2
3
let str = "hihi";
hi.asdf();
//error

실행 순서는 다음과 같다.

  1. 브라우저가 str 객체가 toUpperCase 메소드를 가지고 있는지 체크한다.
  2. 없으므로 str.__proto__를 탐색하여 str의 프로토타입 객체에 toUpperCase() 메소드가 있는지 확인한다.
  3. 존재하지 않으므로 String.prototype.__proto__(str.__proto__.__proto) 즉 Object() 생성자에서 해당 메소드를 찾는다.
  4. 존재하지 않으므로 Object() 생성자의 프로토 타입도 확인해야 하지만 Object.protoType은 항상 체인의 끝으로 null이므로 해당 함수는 실행되지 않는다.

아래 코드를 통해 객체간의 프로토타입 체인을 확인해 볼 수 있다.

1
2
3
4
String.prototype.__proto__;
// Object
Object.protoType.__proto__;
// null

create()

Object.create() 메소드는 새 인스턴스를 생성한다.
정확히는 create() 메소드는 주어진 객체를 프로토타입 객체로 삼아 새로운 객체를 생성하는 함수이다.

1
2
3
let person2 = Object.create(person1);
person2.__proto__;
//person1

정리

자바스크립트는 프로토타입 기반의 언어이고
선형리스트 탐색과 유사하게 __proto__를 통해 상위 프로토타입(편의상 정의한 단어이다.)의 메소드를 탐색한다.

주의사항

위의 코드는 예시일 뿐 실제 개발환경에서 __proto__를 확인해야 할 경우 Object.getProtoTypeOf()함수를 이용해야 한다..!

ref

Object prototypes
자바스크립트 생각보다 간단했던 프로토타입
Inheritance_and_the_prototype_chain