[JS] JS의 심화문법 정리
Updated:
JavaScript 심화문법 정리
1. 변수
호이스팅
-> scope 내부 어디서든 변수의 선언은 최상위에서 선언된 것처럼 작동하는 방식을 의미한다
-> 호이스팅은 var, const, let 모두에게 적용되는 방식이다
console.log(name); // undefined
var name = "saemyung";
console.log(name); // 오류
const name = "saemyung"; // let으로 지정해도 마찬가지
-> 위의 2가지에서 볼 수 있듯이, var와 달리 const와 let의 경우 Temporal Dead Zone 즉, 변수할당 전에는 사용할 수 없다
cf>Temporal Dead Zone
-> 위의 코드처럼 변수선언 및 초기화 이전의 상태
-> var는 const, let과 달리 Temporal Dead Zone의 영향을 받지 않아 변수할당 전에도 사용할 수 있다
-> 이는 오류발생의 원인이기 때문에 var사용을 피한다
Scope
-> Scope는 Lexical Scope와 Dynamic Scope 2가지로 나눠진다
-> Lexical Scope는 선언된 위치가 상위 Scope를 정하고, Dynamic Scope는 실행한 위치가 상위 Scope를 정한다
-> JS는 Lexical Scope의 방식으로 작동한다
var num = 10;
function func1(){
var num = 100;
func2();
}
function func2(){
console.log(num);
}
func1(); // 10
-> 위처럼 func1의 내부에서 func2를 실행해도 선언된 위치상 func2의 상위 Scope는 전역이므로, 전역변수 num의 값인 10이 출력됨
+α> var, const, let의 Scope 비교
-> block-scoped와 function-scoped 2가지로 나눠진다
-> block-scoped는 function, if문, for문, switch문 등 각 block안에서 선언된 변수는 해당 block내부에서 밖에 사용할 수 없는 방식으로 const, let이 이에 포함된다
-> function-scoped는 blocked-scoped와 달리 function을 제외한 if문, for문, switch문 등의 block안에서 선언된 변수들은 외부에서도 사용가능한 방식으로 var가 이에 해당한다
-> 사실상, var는 오류발생의 여지가 있어 const, let을 주로 사용한다
변수지정어에 따른 변수의 생성과정
-
var
-> ‘선언 및 초기화’ 단계와 ‘할당’ 단계 2가지로 구분(초기화 단계는 undefined를 할당하는 단계)
-> 그렇기 때문에 값을 할당하기 전에 호출해도 undefined라는 값이 출력된다 -
let
-> ‘선언’ 단계, ‘초기화’ 단계, ‘할당’ 단계 3가지로 구분
-> 호이스팅되며 선언 단계가 일어나지만, 초기화 단계는 해당 코드에 도달했을때 발생하기 때문에 Temporal Dead Zone에서 해당 변수 접근 시 reference오류 발생 -
const
-> ‘선언, 초기화 및 할당’ 단계가 한꺼번에 발생
-> var, let과 달리 선언만 하고 할당을 나중에 하는 것이 불가능
2. JS 내장 메서드
map
-> 배열.map((요소, 인덱스, 배열) => { return 요소; }); 의 형태로 사용한다
-> map의 결과로 나오는 배열은 기존의 배열을 수정하지 않고 새로 만들어진 배열이다
let userList = [
{name: 'mike', age:30},
{name: 'tom', age:10},
{name: 'jane', age:27}
];
let userValidList = userList.map((elem, idx) => {
return Object.assign({}, elem, {id: idx+1 ,ageValid: elem.age>20});
});
console.log(userList === userValidList); // false
console.log(userValidList);
// { name: 'mike', age: 30, id: 1, ageValid: true },
{ name: 'tom', age: 10, id: 2, ageValid: false },
{ name: 'jane', age: 27, id: 3, ageValid: true }
-> Object.assign() 메서드를 이용하여 기존 객체에 다른 property값 추가
-> map() 메서드를 통해 새로만든 userValidList는 userList와는 다른 배열임
reduce
-> 배열.reduce((누적값, 현재값, 인덱스, 요소) => { return 결과; }, 초기값); 의 형태로 사용한다. 초기값을 적지 않을 경우 해당배열의 0번째 인덱스 요소값이 자동으로 초기값으로 설정된다
const arr = [1,3,5];
const result = arr.reduce((prev, curr) => {
return prev+curr;
});
console.log(result); // 9
-> 위는 덧셈의 결과를 저장하는 함수의 예시이다
let userList = [
{name: 'mike', age:30},
{name: 'tom', age:10},
{name: 'jane', age:27}
];
let userValidList = userList.reduce((prev, curr, idx, arr) => {
prev.push(Object.assign({}, curr, {id: idx+1, ageValid: curr.age>20}));
return prev;
}, []);
console.log(userList === userValidList); // false
console.log(userValidList);
-> reduce() 메서드를 이용하여 위의 map() 메서드와 같은 결과를 도출함
-> 이처럼 reduce() 메서드를 이용하면 sort, filter, every, some, find, findIndex, includes 메서드를 모두 구현가능함
3. 객체
객체 생성 방법
-> 객체를 만들기 위해서는 하위 3가지 방법이 존재한다
-
{} 이용
const lim = { name: 'saemyung', age: 23, }; console.log(lim); // { name: 'saemyung', age: 23 } -
Class 이용
class person { name; age; constructor(name, age){ this.name = name; this.age = age; } } const lim = new person('seamyung', 23); console.log(lim); // person { name: 'seamyung', age: 23 }-> constructor는 생성자로 Class(틀)을 이용하여 객체를 만들 수 있도록 도와줌
-> 객체를 생성하기 위해 반드시 new 사용 -
생성자 함수 이용
function Person(name, age){ this.name = name; this.age = age; } const lim = new Person('saemyung', 23); console.log(lim); // Person { name: 'saemyung', age: 23 }-> 위의 Person을 생성자함수라고 하며, 첫글자를 대문자로 적어주는 것이 관례
-> 생성자 함수를 사용하기 위해 반드시 new 사용
주의) 화살표 함수에서는 생성자 함수와 new를 이용한 객체생성이 불가능하다!!!
Computed property
function getObj(key, val){
return {
[key]: val
}
};
console.log(test('name', 'lim')); // { name: 'saemyung' } 출력
-> 변수에 저장된 값을 객체의 property로 이용할 수 있는 방식
-> 위의 코드를 통해 name property값이 lim인 객체를 반환받음
객체 메서드
- Object.assign(): 객체 복사
const lim = {
name: 'saemyung',
age: 23,
};
const lim2 = {
weight: 70,
};
const lim3 = Object.assign({height: 180}, lim, lim2);
-> lim3은 lim, lim2의 property에 height를 추가한 객체를 property로 가지는 객체로 생성됨
- Object.keys(): 객체 property의 key값들을 배열로 반환
- Object.values(): 객체 property의 value값들을 배열로 반환
- Object.entries(): 객체 property의 key,value값들을 묶어 배열로 반환
Symbol
-> 객체의 property key값을 고유하게 설정함으로써 key값의 유일성을 보장해준다
-> Symbol()을 통해 생성하고, 이때 인자로 값을 넘길 수 있는데, 이는 디버깅 시에 어떤 값인지 확인하기 용이하도록 돕는다
-> Symbol()을 통해 생성된 값은 항상 다르므로 유일성이 보장된다
-> Symbol.for()을 이용하여 생성한 전역Symbol은 하나의 Symbol만을 생성하여 공유한다
const id = Symbol('id');
const user = {
name: 'lim',
age: 23,
[id]: 'myId',
};
console.log(user[id]); // myId 출력
-> 위의 코드는 객체의 key로써 Symbol을 사용한 예시이다
-> Object.keys(), entries() 등의 객체 메서드를 사용해도 Symbol은 드러나지 않는다
-> Object.getOwnPropertySymbols(객체명)을 이용하여 Symbol을 확인할 수 있다
const user = {
name: 'lim',
age: 23,
};
const showName = Symbol('show name');
user[showName] = function(){
console.log(this.name);
}
user[showName]();
for (let key in user){
console.log(`His ${key} is ${user[key]}`);
}
-> 위처럼 겉으로는 드러나지 않으면서 객체의 key값으로 존재할 수 있다
Property Attribute
-> 객체의 각 property에는 value말고도 writable, enumerable, configurable이라는 attribute를 가진다
-
value: 실제 property의 값
-
writable: 해당 property값을 수정할 수 있는지의 여부
-
enumerable: 열거가 가능한지의 여부(반복문, consol.log등을 통해 해당 property가 나열되는지)
-
configurable
-> 상위 3가지 property attribute값들을 변경할 수 있는지의 여부
-> 단, writable을 true에서 false로 변경하는 경우와 writable이 true일 경우 value를 변경하는 것은 가능하다
const lim = {
name: 'saemyung',
age: 23,
};
console.log(Object.getOwnPropertyDescriptors(lim));
Object.defineProperty(lim, 'name', {
value: 'lim',
writable: false,
enumerable: false,
configurable: false
});
console.log(Object.getOwnPropertyDescriptor(lim, 'name'));
lim.name = "saemyung"; // 수정불가(오류는 발생하지 않음)
console.log(lim); // { age: 23 }
-> Object.getOwnPropertyDescriptor(인스턴스명, 프로퍼티명)을 통해 하나의 property attribute값을 확인할 수 있고, Object.getOwnPropertyDescriptors(인스턴스명)을 통해 모든 property attribute값을 확인가능하다
-> Object.defineProperty(인스턴스명, 프로퍼티명, 수정사항객체)를 통해 property attribute를 설정
-> 위의 예시에서는 name property의 writable이 false이기 때문에 value값이 수정불가하고, configurable이 false이기 때문에 마지막줄 console.log에서도 name property가 출력되지 않는다
불변객체
-
extensible
const lim = { name: 'saemyung', age: 23, }; console.log(Object.isExtensible(lim)); // true Object.preventExtensions(lim); console.log(Object.isExtensible(lim)); // false lim.height = 170; console.log(lim); { name: 'saemyung', age: 23 } delete lim.age; console.log(lim); { name: 'saemyung' }-> Object.isExtensible(인스턴스명)은 default값이 true(기본적으로 property 추가가 가능)
-> Object.preventExtensions(인스턴스명)로 해당객체에 property 추가하는 것을 막을 수 있음
-> 추가하는 것이 안될뿐 delete를 통한 삭제는 가능함 -
Seal
-> property attribute 중 configurable 값이 false이고, 별도의 property 추가 및 삭제가 불가능하도록 만든 것과 같음const lim = { name: 'saemyung', age: 23, }; console.log(Object.isSealed(lim)); // false Object.seal(lim); console.log(Object.isSealed(lim)); // true lim.height = 170; console.log(lim); // { name: 'saemyung', age: 23 } delete lim.age; console.log(lim); // { name: 'saemyung', age: 23 }-> Object.seal(인스턴스명)을 통해 해당 객체를 seal 할 수 있다
-> 원래 있던 property의 value 값 변경은 가능하지만, 별도의 property 추가 및 삭제가 불가능하다
-> configurable값이 false인 것과 같기 때문에 예외(property attribute 설명쪽에 나와있는 2가지 예외)를 제외하면 Object.defineProperty()를 통해 property attribute를 변경하는 것이 불가능하다 -
Freezed
-> read 외의 모든 기능을 막는것
-> Seal에서 property attribute 중 writable 까지 false된 것이라고 생각하면 됨const lim = { name: 'saemyung', age: 23, }; console.log(Object.isFrozen(lim)); // false Object.freeze(lim); console.log(Object.isFrozen(lim)); // true lim.height = 170; console.log(lim); // { name: 'saemyung', age: 23 } delete lim.age; console.log(lim); // { name: 'saemyung', age: 23 }-> Object.freeze(인스턴스명)을 통해 해당 객체를 freeze할 수 있다
-> 별도의 property 추가 및 삭제가 불가능할 뿐만 아니라, 원래 있던 property의 value값 변경 역시 불가능하다
-> property attribute 중 writable과 configurable이 false이기 때문에 모든 property attribute 값을 변경하는 것이 불가능하다
+α) 객체 안의 객체의 경우
const lim = {
name: 'saemyung',
age: 23,
lim2: {
name: 'sammyung',
age: 32
},
};
Object.freeze(lim);
console.log(Object.isFrozen(lim)); // true
console.log(Object.isFrozen(lim.lim2)); // false
-> 어떤 객체를 freeze 시켜도 그 객체 내부의 객체는 freeze되지 않음을 알 수 있다
-> 이는 freeze 뿐만 아니라 extensible, seal에서도 동일하다
this
-> JS는 Lexical Scope 방식이기 때문에 함수의 선언위치에 따라 상위 Scope가 결정되지만, **this 키워드는 객체 생성시점에 binding이 결정**된다
-> 다른 OOP언어와 다르게 상황에 따라 this가 달라지는 경우가 발생한다
[상황에 따른 this값]
- 전역공간에서
-> this가 전역객체를 가르킨다
-> 브라우저일 경우 window 이고, Node.js일 경우 global 이다 - 함수 호출시
-> 마찬가지로 전역객체를 가르킨다 - 메서드 호출시
-> (인스턴스명).메서드명() 으로 실행시에 해당 인스턴스명이 this가 된다
-> eg) a.b.func(); 으로 실행시 a.b가 this가 된다 - new로 생성자함수 호출시
-> new 키워드에 의해 생성된 인스턴스명이 this가 된다 - Callback 호출시
-> Callback 함수를 인자로 넘겨받은 함수가 어떻게 처리하는지에 따라 this가 변한다
=> call(), apply(), bind() 3개의 메서드를 이용하여 this를 내가 원하는대로 변경하는 것이 가능하다!!!
-
call
-> 첫번째 인자값으로 this로 binding하고자 하는 인스턴스, 두번째 인자값부터는 해당 함수에서 사용할 인자값을 받는다function avg(height, weight){ return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`; } const lim = { name: 'saemyung', height: 171, weight: 70, }; console.log(avg.call(lim, lim.height, lim.weight)); // saemyung님의 평균은 120.5 입니다 -
apply
-> call과 같지만, 2번째 인자값으로 해당 함수에서 사용할 모든 인자값을 리스트로 받는다는 차이가 있다function avg(height, weight){ return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`; } const lim = { name: 'saemyung', height: 171, weight: 70, }; console.log(avg.apply(lim, [lim.height, lim.weight])); // saemyung님의 평균은 120.5 입니다 -
bind
-> call과 동일하지만, 바로 실행을 하지않고 나중에 실행시킬 수 있다는 차이가 있다function avg(height, weight){ return `${this.name}님의 평균은 ${(height+weight) / 2} 입니다`; } const lim = { name: 'saemyung', height: 171, weight: 70, }; const laterExecute = avg.bind(lim, lim.height, lim.weight); console.log(laterExecute()); // saemyung님의 평균은 120.5 입니다
4. 프로토타입
-> 프로토타입 객체는 원형을 의미한다
-> 같은 생성자로부터 만들어진 객체들은 같은 프로토타입 객체를 공유한다
-> 메서드를 만들때, ‘인스턴스.메서드’의 방식보다 프로토타입을 이용하는 방식이 더 효율적이다(프로토타입을 통해 만들어놓으면, 굳이 각각의 인스턴스에서 만들 필요가 없기 때문)
__proto__
-> __proto__는 모든 객체에 존재하는 property라고 생각하면됨
-> (객체명).__proto__ 값은 해당 객체의 부모 클래스에 해당됨
class person{
name;
age;
constructor(name, age){
this.name = name;
this.age = age;
}
};
class people extends person{
sayHello(){
console.log(`hi ${this.name}`);
}
}
const lim = new people('saemyung', 23);
console.log(lim.__proto__); // person {}
-> people 클래스를 통해 생성한 lim객체의 __proto__ 값이 people의 부모클래스인 person임
prototype chain
function Person(name, age){
this.name = name;
this.age = age;
}
const lim = new Person('saemyung', 23);
console.log(lim.__proto__ === Person.prototype); // true
console.log(Person.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
-> 모든 것들의 최상위 __proto__ 값은 Object.prototype 임을 알 수 있음
-> 이런 식으로 여러 prototype끼리 연결되어 있는 것을 prototype chain이라고 부름
-> toString() 같은 메서드들도 Object로 부터 상속받아 사용하는 것임
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
return `hi ${this.name}`;
}
const lim = new Person('saemyung', 23);
const lim2 = new Person('sammyung', 25);
console.log(lim.sayHello === lim2.sayHello); // true
-> Person이라는 생성자함수에 sayHello() 메서드를 넣으면 lim과 lim2 인스턴스를 각자 생성했을때, 각각의 sayHello() 메서드가 다른 메모리 공간에 존재하기 때문에 메모리 공간의 낭비가 발생함
-> 위의 방식에서는 Person의 prototype에 sayHello() 메서드를 넣어서 new Person()을 통해 생성된 인스턴스들에서는 모두 같은 메모리 공간의 sayHello()를 사용하도록함
-> 이런 식으로 prototype chain을 잘 활용하면, 효율적인 코딩이 가능함
인스턴스의 prototype변경 vs. 함수의 prototype변경
-> JS에서는 특이하게 함수를 통해 생성된 인스턴스의 prototype을 변경할 수도 있고, 함수 자체의 prototype을 변경할 수도 있다
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function(){
return `hi ${this.name}`;
}
// Person 생성자함수 -> prototype에 sayHello() 추가
function Person2(name, age){
this.name = name;
this.age = age;
this.coding = function(){
return `${this.name}이 코딩중~`;
}
}
// Person2 생성자함수 -> 해당객체의 property에 coding() 추가
const lim = new Person('saemyung', 23);
const lim2 = new Person2('sammyung', 25);
console.log(lim2.sayHello()); // 오류
// 인스턴스의 prototype 변경
Object.setPrototypeOf(lim2, Person.prototype);
console.log(lim2.sayHello()); // hi sammyung
console.log(lim2.constructor === Person); // true
console.log(Person.prototype === Person2.prototype); // false
// 함수의 prototype 변경
Person2.prototype = Person.prototype
const lim3 = new Person2('myungsae', 27);
console.log(Object.getPrototypeOf(lim3) === Person2.prototype); // true
console.log(Object.getPrototypeOf(lim3) === Person.prototype); // true
console.log(Person.prototype === Person2.prototype); // true
-> Object.setPrototypeOf(인스턴스명, 변경하려는 prototype) 을 통해 인스턴스의 prototype변경이 가능하다. 인스턴스의 prototype만을 변경하는 것이므로 해당 인스턴스를 생성한 함수의 prototype은 변하지 않는다 **
-> 함수1.prototype = 함수2.prototype 의 방식으로 함수의 prototype을 변경할 수 있다. 이 경우, 함수 자체의 prototype을 변경하는 것이므로 **인스턴스의 prototype뿐만 아니라 함수의 prototype까지 변경된다
정리
function Person(name, age){
this.name = name;
this.age = age;
}
const lim = new Person('saemyung', 23);
const p1 = new Person.prototype.constructor('proto1', 10);
const p2 = new lim.constructor('proto2', 20);
const p3 = new lim.__proto__.constructor('proto3', 30);
const p4 = new Object.getPrototypeOf(lim).constructor('proto4', 40);
console.log(Person.prototype.constructor === Person); // true
-> 위의 코드처럼 4가지 방식을 통해 동일한 프로토타입 설계가 가능하다
-> 함수명.prototype.constructor을 적용하면 원래의 생성자와 같아진다
5. 실행 컨텍스트
-> JS 코드 실행에 필요한 모든 데이터를 가지고 있는 환경
-> 크게 Global Context 와 Function Context로 나눠진다. Global Context는 최상위 context로, 브라우저의 window 객체 or Nods.js 의 global 객체를 가진다. Function Context는 함수 실행시 함수별로 실행되는 context로 함수 실행에 대한 정보를 가진다
cf) 싱글스레드 기반의 JS -> Memory Heap과 Call Stack(Execution Context Stack)으로 구분됨
Execution Context Stack
-> Execution Context Stack은 Creation Phase와 Execution Phase 2단계로 구성된다
- Creation Phase
-> Global Object를 생성
-> this를 window 또는 global에 binding
-> 변수와 함수를 Memory Heap에 올려놓음(호이스팅) - Execution Phase
-> 코드 실행후 필요시 새로운 Execution Context를 생성함
6. Closure
-> 실행컨텍스트A의 내부에서 함수B가 실행된다고 가정할때, ‘A의 Lexical환경과 내부함수B의 조합’에서 발생하는 특별한 현상을 의미함
-> 요약하면, 컨텍스트A에서 선언한 변수를 내부함수B에서 참조할 경우 발생하는 현상
-> 상위함수보다 하위함수가 더 오래 살아남아있는 현상
function getNum(){ // 외부함수
var num = 10;
function innerFunc() { // 내부함수
return num;
}
return innerFunc;
}
var inner = getNum();
console.log(inner());
-> getNum()을 통해 innerFunc이라는 내부함수를 반환받음
-> 외부함수인 getNum()이 사라진 후에도 내부함수인 innerFunc()이 남아있는 상황
Closure를 사용하는 경우
-
data caching
-> 시간이 오래걸리는 코드를 여러번 반복하지 않고 한번만 실행하도록함function cacheFunc(){ var num = 100 * 100; // 시간이 오래걸리는 코드라고 가정 function innerCacheFunc(newNum){ return num * newNum; } return innerCacheFunc; } var inner = cacheFunc(); console.log(inner(10)); console.log(inner(20));-> cacheFunc()이 종료되어도 inner변수에서 innerCacheFunc()을 참조하고 있고, innerCacheFunc()에서는 cacheFunc함수의 num변수를 참조하고 있기 때문에, num값은 계속 남아있는 상황임
-> innerCacheFunc()만 반복해서 실행하여 시간이 오래걸리는 코드인 100 * 100을 한번만 실행하도록 만듬 -
정보 은닉
function Person(name, age){ this.name = name; var _age = age; this.sayNameandYear = function(){ return `hi ${this.name},${_age}`; } } const lim = new Person('saemyung', 23); console.log(lim.sayNameandYear()); // hi saemyung, 23-> this.age = age; 를 이용하지 않고 _age 라는 변수를 만들어 sayNameandYear 함수에서만 접근할 수 있도록 함으로써 정보를 은닉할 수 있다
-> 추가적으로 property값에 #을 붙임으로써 최근 JS의 기능인 private property를 사용할 수도 있다
7. Async Programming(비동기 프로그래밍)
-> Sync(동기): 현재 실행 중인 task가 종료하기 전까지 다음 task가 실행하지 못하고 대기하는 방식
-> Async(비동기): 현재 실행 중인 task가 종료되지 않았더라도 다음 task를 실행시키는 방식
=> JS는 하나의 실행 컨텍스트를 가지는 싱글스레드로 작동하지만, 이벤트루프의 도움을 받아 비동기처리가 가능하다
Event Loop
-> JS는 동기식 처리를 위해 Memory Heap과 Call Stack을 가지며 당연히 이 둘로는 비동기 처리를 하지 못한다
-> JS는 Event Loop의 도움을 받아 비동기식 처리가 가능하다. 처리 방식은 아래와 같다
- Call Stack에 동기함수가 들어오면 Call Stack에서 처리한다
- Call Stack에 비동기함수가 들어오면 Event Loop에 의해 백그라운드로 옮겨지고, 여기서 비동기 처리를 진행한다 eg) setTimeout() 함수에서 정해진 시간만큼 백그라운드에서 타이머 작동
- 백그라운드에서 타이머에 의해 비동기 처리가 완료되면 Task Queue로 콜백함수를 옮긴다
- Call Stack이 비어있을 경우에만 해당 콜백함수를 Task Queue에서 Call Stack으로 옮긴다. 단, Task Queue에 Promise의 콜백함수와 setTimeout()의 콜백함수가 함께 있을 경우, Promise가 우선순위를 가진다
비동기 프로그래밍 예시
function longWork(){
const now = new Date();
const millisec = now.getTime();
const afterTwoSec = millisec + 2 * 1000;
while(new Date().getTime() < afterTwoSec){ // 2초동안 반복
}
console.log('완료');
}
console.log('hello');
longWork();
console.log('world');
-> 위의 코드는 동기식으로 작동하기 때문에, ‘hello’가 출력되고 2초간 longWork() 작업 후 ‘완료’를 출력하고 나서야 ‘world’가 출력된다
function longWork(){
setTimeout(() => {
console.log('완료');
}, 2000);
}
console.log('hello');
longWork();
console.log('world');
-> 이 코드는 setTimeout()이라는 비동기함수의 도움을 받아 비동기식으로 작동한다
-> ‘hello’가 출력되고 longWork() 작업을 하지만, 비동기식으로 작동하기 때문에, ‘world’가 먼저 출력되고 2초 작업 후 ‘완료’가 출력된다
Promise
과거, JS는 비동기 처리를 하기 위해 콜백함수를 사용하였는데, 아래처럼 복잡한 형태의 콜백지옥의 문제가 발생
function callBack(){
setTimeout(() => {
console.log('1번 callback 끝');
setTimeout(() => {
console.log('2번 callback 끝');
setTimeout(() => {
console.log('3번 callback 끝');
}, 2000);
}, 2000);
}, 2000);
}
callBack();
-> 에러처리가 힘들고, 여러 개의 비동기 처리를 한번에 하는데 한계가 있음
=> 이를 해결하기 위해 생겨난 것이 프로미스임
const promise = new Promise((resolve, reject)=>{
const num = 1 + 1;
setTimeout(() => {
if(num==2) resolve('일치');
else reject('불일치');
}, 2000);
});
promise.then((res)=>{
console.log(res);
}).catch((res)=>{
console.log(res);
})
// 일치
-> Promise객체는 resolve와 reject를 인자로 받는 콜백함수를 인자로 받음
-> 비동기 처리가 성공일 경우 resolve함수를, 실패일 경우 reject함수를 호출
-> new를 통해 생성한 인스턴스를 통해 후속처리가 가능한데, then()은 주로 비동기 처리가 성공시에 사용하고, catch()는 비동기 처리가 실패시에만 예외처리를 위해 사용함
-> Promise는 resolve, reject까지는 동기식으로 처리되고, .then 또는 .catch 부터 비동기식으로 처리됨
-> 이와 별개로 console.log()를 실행 시에 비동기식으로 처리하기 때문에 해당 console.log()가 먼저 출력됨
const promise = (msg)=>{
return new Promise((resolve, reject)=>{
if(msg == 'success') resolve('success');
else reject('fail');
})
}
promise('success')
.then(res => promise(res))
.then(res => console.log(`task is ${res}`))
.catch(err => console.log(`task is ${err}`));
// task is success
-> 이런 식으로 복잡했던 콜백지옥을 promise chaining으로 여러 비동기 함수를 손쉽게 처리가 가능
const getPromise = (sec)=> {
return new Promise((resolve, reject)=>{
setTimeout(() => {
resolve(sec);
}, sec * 1000);
})
}
Promise.all([
getPromise(1),
getPromise(2),
getPromise(3)
]).then((res)=>{
console.log(res); // [1, 2, 3]
})
-> 서로 의존하고 있지 않은 Promise들끼리는 Promise.all()을 이용하여 동시에 실행하여 빠른 처리가 가능
-> 이 경우, 가장 늦은 Promise를 기준으로 처리된다. 위 코드의 경우 제일 늦은 3초 후에 처리결과가 출력됨
async & await
-> Async Programming을 위해 최근에 주로 사용하 는 것이 async와 await 키워드이다
const promise = (sec)=>new Promise((resolve, reject)=>{
setTimeout(() => {
resolve('완료');
}, sec * 1000);
});
async function runner(){
try{
const result1 = await promise(1);
console.log(result1);
const result2 = await promise(2);
console.log(result2);
const result3 = await promise(3);
console.log(result3);
} catch(e){
console.log(e);
}
}
runner();
-> 비동기적으로 처리할 함수 앞에 async 키워드를 붙이고, 처리할 Promise앞에 await를 붙이면 비동기식 처리가 가능하다. 단, await 키워드는 오직 Promise함수에만 붙일 수 있다
-> 위의 비동기 처리방식과 다르게 resolve 처리시에 여기서는 await 키워드를 붙이고 변수로 받아 console.log()로 출력하면 되기 때문에 훨씬 간단해졌다. 그리고 reject 처리시에는 try-catch 구문에서 e로 해당 인자를 받아 처리한다
8. 그외
구조분해
-> 배열 및 객체 구조분해를 이용하면 값변경이 용이하다. 실제로 두 변수의 값을 바꾸려면 또다른 하나의 변수를 선언해야하지만 구조분해를 이용하면 이런 과정없이 해결가능하다
let n1=1, n2=2;
[n1,n2] = [n2,n1];
-> 위의 방식으로 n1과 n2의 값을 바꿀 수 있다
let user = {
name: 'lim',
age:23,
};
let {name, age} = user;
console.log(name); // lim
console.log(age); // 23
-> name과 age 각각의 변수값에 user.name과 user.age의 값이 들어간다
나머지 매개변수
-> 함수로 인자값이 여러개 들어올 경우 일일이 적는 것을 대체하기 위해 ES6에서는 나머지 매개변수라는 방법을 도입했다
-> 나머지 매개변수에는 배열의 형태로 들어간다
function User(name, age, ...skills){
this.name = name;
this.age = age;
this.skills = skills;
}
const user1 = new User('lim', 23, 'js', 'ts', 'node');
const user2 = new User('kim', 33, 'java', 'spring');
const user3 = new User('park', 43, 'python', 'django', 'flask');
console.log(user1);
console.log(user2);
console.log(user3);
-> 생성자로 많은 인수를 받아야할때 나머지 매개변수를 이용하면 간단히 표현이 가능하다
전개구문
-> 전개구문을 사용한 손쉬운 배열, 객체 복사가 가능하다
-> 여기서 복사라고 표현한 이유는 전개구문을 통해 생성된 배열 or 객체는 기존의 것과 별개로 새로 만들어진 배열 or 객체이다
const arr = [1,2,3];
const arr2 = [...arr];
console.log(arr2); // [1,2,3]
console.log(arr===arr2); // false
const obj = {name:'lim', age:23};
const obj2 = {...obj};
console.log(obj2); // { name: 'lim', age: 23 }
console.log(obj===obj2); // false
-> 전개구문을 통해 새로 만든 배열 or 객체는 기존의 배열 or 객체와 다르다
let user = {name: 'lim'};
let info = {age: 23};
let fe = ['js', 'node'];
let lang = ['kor', 'eng'];
let obj = Object.assign({},
user, info,
{skills: []}
);
fe.forEach((elem) => {
obj.skills.push(elem);
});
lang.forEach((elem) => {
obj.skills.push(elem);
});
console.log(obj);
// { name: 'lim', age: 23, skills: [ 'js', 'node', 'kor', 'eng' ] }
let user = {name: 'lim'};
let info = {age: 23};
let fe = ['js', 'node'];
let lang = ['kor', 'eng'];
let obj = {...user, ...info, skills: [...fe, ...lang]};
console.log(obj);
// { name: 'lim', age: 23, skills: [ 'js', 'node', 'kor', 'eng' ] }
-> 2가지 코드 중 위의 것이 전개구문을 사용하지 않고 복사한 경우이고, 아래가 전개구문을 사용하여 복사한 경우인데, 훨씬 효율적인 코드로 바뀌었음을 알 수 있다
Leave a comment