Primitive(값) vs Object(참조)
Primitive Type
- String : 텍스트를 셋팅하는데 사용하는 타입.
- Number : 숫자를 셋팅하는데 사용하는 타입. 기본적으로 소수점도 가능하다.(infinity, -inifinity, NaN 표현이 가능하다.)
- Null : null타입은 정확히는 1개의 값은 가지고 있지만 비어있다는 뜻이다.
- Undefined : 값이 할당되지 않는 것을 나타내는 타입.
- Boolean : true 또는 false 로 나타내는 타입.
- Symbol : 새로 추가된 타입으로 unique하고 immutable한 원시값 으로 사용된다.(ES6)
Primitive Type의 생성 방법
- Literal
- Literal로 생성한다고 하면 우리가 가장 많이 사용하는 방법
var bol = true;
var str = "hello";
var num = 3.14;
var nullType = null;
var undef = undefined;
var bol2;
var str2;
bo2 = false
str2 = "world"
-
Wrapper Object
- Wrapper Object를 사용해서 만든다고 하면 Constructor를 사용해서 만드는 것
- 즉,
new
를 사용하여 생성
new Boolean(false);
new String("world");
new Number(42);
Symbol("foo"); //Symbol 타입의 생성방법
Literal vs Wrapper
typeof true; //"boolean"
typeof Boolean(true); //"boolean"
typeof new Boolean(true); //"object"
typeof (new Boolean(true)).valueOf(); //"boolean"
typeof "abc"; //"string"
typeof String("abc"); //"string"
typeof new String("abc"); //"object"
typeof (new String("abc")).valueOf(); //"string"
typeof 123; //"number"
typeof Number(123); //"number"
typeof new Number(123); //"object"
typeof (new Number(123)).valueOf(); //"number"
Literal로 생성한 것의 타입은 6가지 중 하나로 나오게 된다. 그런데 new를 사용하여 Wrapper Object로 만들게 되면 Object타입이 나오게 된다. 사용을 하려면 valueOf라는 Function을 사용해야만 입력한 값이 나오게 된다.
값 타입
var a = 13 // assign `13` to `a`
var b = a // copy the value of `a` to `b`
b = 37 // assign `37` to `b`
console.log(a) // => 13
b의 값을 변경을 했지만 a에는 영향이 가지 않았다. 이유는 2개의 값이 저장된 공간이 다르기 때문이다.
Object Type
- Array : 우리가 알고 있는 배열, 리스트의 형태를 가지고 있다.
- Function : Javascript에서는 Function Object가 존재하지만 결국 Function도 Object.
- Object : Map처럼 사용하는 즉, key : value의 형태로 사용하고 있는 Object.
var a = { c: 13 } // assign the reference of a new object to `a`
var b = a // copy the reference of the object inside `a` to new variable `b`
b.c = 37 // modify the contents of the object `b` refers to
console.log(a) // => { c: 37 }
var a = [];
var b = a;
a.push(1);
console.log(a); // [1]
console.log(b); // [1]
console.log(a === b); // true
function changeAgeImpure(person) {
person.age = 25;
return person;
}
var alex = {
name: 'Alex',
age: 30
};
var changedAlex = changeAgeImpure(alex);
console.log(alex); // -> { name: 'Alex', age: 25 }
console.log(changedAlex); // -> { name: 'Alex', age: 25 }
원시타입과는 다르게 복사한 것을 변경을 했더니 기존 객체에도 영향이 간다. 이유는 같은 값의 주소를 복사했기 때문이다.
🙏 Reference
Primitive(값) vs Object(참조)
Primitive Type
- String : 텍스트를 셋팅하는데 사용하는 타입.
- Number : 숫자를 셋팅하는데 사용하는 타입. 기본적으로 소수점도 가능하다.(infinity, -inifinity, NaN 표현이 가능하다.)
- Null : null타입은 정확히는 1개의 값은 가지고 있지만 비어있다는 뜻이다.
- Undefined : 값이 할당되지 않는 것을 나타내는 타입.
- Boolean : true 또는 false 로 나타내는 타입.
- Symbol : 새로 추가된 타입으로 unique하고 immutable한 원시값 으로 사용된다.(ES6)
Primitive Type의 생성 방법
- Literal
- Literal로 생성한다고 하면 우리가 가장 많이 사용하는 방법
var bol = true;
var str = "hello";
var num = 3.14;
var nullType = null;
var undef = undefined;
var bol2;
var str2;
bo2 = false
str2 = "world"
-
Wrapper Object
- Wrapper Object를 사용해서 만든다고 하면 Constructor를 사용해서 만드는 것
- 즉,
new
를 사용하여 생성
new Boolean(false);
new String("world");
new Number(42);
Symbol("foo"); //Symbol 타입의 생성방법
Literal vs Wrapper
typeof true; //"boolean"
typeof Boolean(true); //"boolean"
typeof new Boolean(true); //"object"
typeof (new Boolean(true)).valueOf(); //"boolean"
typeof "abc"; //"string"
typeof String("abc"); //"string"
typeof new String("abc"); //"object"
typeof (new String("abc")).valueOf(); //"string"
typeof 123; //"number"
typeof Number(123); //"number"
typeof new Number(123); //"object"
typeof (new Number(123)).valueOf(); //"number"
Literal로 생성한 것의 타입은 6가지 중 하나로 나오게 된다. 그런데 new를 사용하여 Wrapper Object로 만들게 되면 Object타입이 나오게 된다. 사용을 하려면 valueOf라는 Function을 사용해야만 입력한 값이 나오게 된다.
값 타입
var a = 13 // assign `13` to `a`
var b = a // copy the value of `a` to `b`
b = 37 // assign `37` to `b`
console.log(a) // => 13
b의 값을 변경을 했지만 a에는 영향이 가지 않았다. 이유는 2개의 값이 저장된 공간이 다르기 때문이다.
Object Type
- Array : 우리가 알고 있는 배열, 리스트의 형태를 가지고 있다.
- Function : Javascript에서는 Function Object가 존재하지만 결국 Function도 Object.
- Object : Map처럼 사용하는 즉, key : value의 형태로 사용하고 있는 Object.
var a = { c: 13 } // assign the reference of a new object to `a`
var b = a // copy the reference of the object inside `a` to new variable `b`
b.c = 37 // modify the contents of the object `b` refers to
console.log(a) // => { c: 37 }
var a = [];
var b = a;
a.push(1);
console.log(a); // [1]
console.log(b); // [1]
console.log(a === b); // true
function changeAgeImpure(person) {
person.age = 25;
return person;
}
var alex = {
name: 'Alex',
age: 30
};
var changedAlex = changeAgeImpure(alex);
console.log(alex); // -> { name: 'Alex', age: 25 }
console.log(changedAlex); // -> { name: 'Alex', age: 25 }
원시타입과는 다르게 복사한 것을 변경을 했더니 기존 객체에도 영향이 간다. 이유는 같은 값의 주소를 복사했기 때문이다.
🙏 Reference
명시적 변환 vs 암묵적 변환
Number(value)
와 같은 코드를 작성하여 변환할 의사를 명확하게 표현하는 것을 명시적 변환이라고 한다. JavaScript
는 동적 타입 언어이므로 값을 자동으로 여러 유형간에 변환을 자동으로 한다. 이것을 암묵적 변환 이라고 한다.
암묵적 변환을 하지 않는 연산자는 ===
이며, 완전 항등 연산자 라고 한다. 반면에 느슨한 항등 연산자 ==
는 필요하다면 비교와 타입 강제 변환을 수행한다.
String 변환
String(123) // 명시적
123 + '' // 암시적
String(123) // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
Symbol
변환은 명시적으로만 변환될 수 있고, 암시적 변환은 되지 않는다.
String(Symbol('my symbol')) // 'Symbol(my symbol)'
'' + Symbol('my symbol') // TypeError is thrown
Boolean 변환
Boolean(2) // 명시적
if (2) { ... } // 논리적 문맥 때문에 암시적
!!2 // 논리적 문맥 때문에 암시적
2 || 'hello' // 논리적 문맥 때문에 암시적
논리 연산자(예 : ||
및 &&
)에 따른 Boolean 변환을 내부적으로 수행하지만 Boolean값이 아니더라도 원래 피연산자의 값을 실제로 반환한다.
// true를 반환하는 것이 아닌 123를 반환하고 있다.
// 'hello' and 123 은 표현식을 계산하기 위해서 Boolean으로 강제 변환을 한다.
let x = 'hello' && 123; // 123
Boolean('') // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
object
, function
, Array
, Date
, 사용자 정의 유형등은 true
로 변환한다.
Boolean({}) // true
Boolean([]) // true
Boolean(Symbol()) // true
!!Symbol() // true
Boolean(function() {}) // true
Numeric 변환
Number()
함수를 사용하면 된다. 암시적 변환은 많은 경우에서 작동이 되기 때문에 까다롭다.
Number('123') // 명시적 - 123
+'123' // 암시적 - 123
123 != '456' // 암시적 - true
4 > '5' // 암시적 - false
5 / null // 암시적 - Infinity
true | 0 // 암시적 - 1
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123
- 문자열을 숫자로 변환할 때 엔진은 먼저 앞뒤의 공백,
\ n
,\ t
문자를 제거하고, 문자열이 유효한 숫자를 나타내지 않으면NaN
을 반환한다.string
이 비어 있으면0
을 반환한다. - null와 undefined는 다르게 처리가 되는데 null은 0으로 undefined는 NaN으로 된다.
Symbol
은 명시적 또는 암시적으로 숫자로 변환될 수 없다. 또한 TypeError
는 undefined
로 발생하는 것처럼 NaN
으로 자동 변환하는 대신 throw
된다.
Number(Symbol('my symbol')) // TypeError is thrown
+Symbol('123') // TypeError is thrown
Tips
==
를null
또는undefined
에 적용하면 숫자 변환이 발생하지 않는다.null
은null
,undefined
와 동일하다.
null == 0 // false, null is not converted to 0
null == null // true
undefined == undefined // true
null == undefined // true
null === undefined // false
NaN
은 그 자체가 동등하지 않다.
var value = NaN;
if (value !== value) { console.log("we're dealing with NaN here") }
🙏 Reference
Functional Scope vs Block Scope
Functional Scope
자바스크립트는 함수를 단위로 Scope를 구분한다. 즉 같은 함수 안에서 선언된 변수들은 같은 레벨의 Scope를 가지게 되는 것이다. 각각의 함수는 독립적인 Scope를 가지게 되어 다른 함수의 Scope에 접근을 할 수 없다.
// Global Scope
function someFunction() {
if (true) {
var name = "BKJang";
}
console.log(name); //BKJang
}
위와 같이 Global Scope에 someFunction()
을 선언하고 내부에 if문 괄호 안에 선언한 변수는 someFunction function Scope 에 붙게 된다. 함수를 단위로 스코프가 생기기 때문에 name
을 출력하면 undefined
가 아닌 BKJang
이 출력된다.
Block Scope
Block Statement는 우리가 많이 보는 if문, switch문, for, while문이다. 이러한 문장들은 괄호로 감싸진 부분이 존재하지만 새로운 Scope를 만들지는 않는다. Block Statement 안에서 정의한 변수는 가장 가까운 함수의 Scope에 붙게 된다.
if (true) {
var name = "BKJang";
}
console.log(name); // BKJang
ES6
에서는 let
, const
가 추가 되었다. 이 2개는 var
대용으로 사용된다. 그러나 그보다 더 중요한 개념이 들어간다. 바로 Block Level Scope 라는 것이다. 기존의 자바스크립트는 위에서 본 것처럼 Functional Scope 이다. 그러나 let
, const
를 사용하게 되면 Block Level Scope 지원이 가능하다.
if (true) {
var name = "BKJang";
let likes = "Coding";
const lang = "Javascript";
}
console.log(name); // 'BKJang'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(lang); // Uncaught ReferenceError: lang is not defined
var
와는 다르게 let
, const
는 Block Statement내에서 Local Scope 를 지원한다. 즉 이제 Scope가 가장 가까운 function에 붙는 것이 아닌 해당 Block에 붙게 되는 것이다.
참고로 Global Scope는 응용 프로그램이 살아있을 때까지 유효하며, Local Scope은 함수가 호출되고 실행되는한 유지가 된다.
🙏 Reference
JavaScript의 this
자바스크립트에서 this
의 바인딩은 함수의 호출 방식에 따라 결정된다.
- 객체의 메서드 호출
- 일반 함수 호출
- 생성자 함수의 호출
call
과apply
를 이용한this
바인딩- ES6의 화살표 함수
객체의 메서드 호출
var obj = {
organization: "Im-D",
sayHello: function() {
return "Welcome to " + this.organization;
}
};
console.log(obj.sayHello());
객체의 메서드를 호출할 때 this
는 해당 객체에 바인딩된다.
일반 함수 호출
var organization = "Im-D";
function sayHello() {
var organization = "Kyonggi";
return "Welcome to " + this.organization;
}
console.log(sayHello());
일반 함수를 호출할 때, 자바스크립트의 this
는 전역 객체(window 객체)에 바인딩 된다.
생성자 함수의 호출
function Organization(name, country) {
this.name = name;
this.country = country;
}
var imD = new Organization("Im-D", "South Korea");
var kyonggi = Organization("Kyonggi", "South Korea");
console.log(imD);
console.log(kyonggi);
생성자 함수를 new
키워드를 통해 호출할 경우, 새로 생성되는 빈 객체에 바인딩 된다. 단, new
키워드를 사용하지 않으면 this
는 전역객체에 바인딩된다.
call
, apply
, bind
를 활용한 this
바인딩
call
function Module(name) {
this.name = name;
}
Module.prototype.getName = function() {
const changeName = function() {
console.log(this);
return this.name + "입니다.";
};
// return changeName.call(this, 1,2,3,4);
return changeName.call(this);
};
const module = new Module("BKJang");
console.log(module.getName());
apply
function Module(name) {
this.name = name;
}
Module.prototype.getName = function() {
const changeName = function() {
console.log(this);
return this.name + "입니다.";
};
// return changeName.apply(this, [1,2,3,4]);
return changeName.apply(this);
};
const module = new Module("BKJang");
console.log(module.getName());
또한 call
이나 apply
메서드를 활용하여 유사배열 객체를 일반 배열로 바꿀 수도 있다.
function sayHello() {
console.log(arguments);
var args = Array.prototype.slice.apply(arguments);
console.log(args);
}
sayHello("Im-D", "South Korea");
call
과 apply
는 내부 함수에서 사용할 this를 설정하고 함수 바로 실행까지 해주지만, bind
는 this
만 설정해주고 함수 실행은 하지 않고 함수를 반환한다.
function Module(name) {
this.name = name;
}
Module.prototype.getName = function() {
const changeName = function() {
console.log(this);
return this.name + "입니다.";
};
let bindChangeName = changeName.bind(this);
return bindChangeName();
};
const module = new Module("BKJang");
console.log(module.getName());
화살표 함수
var obj = {
organization: "Im-D",
outerFunc: function() {
var that = this;
console.log(this.organization);
innerFunc = function() {
console.log(that.organization);
console.log(this.organization);
};
innerFunc();
}
};
obj.outerFunc();
var obj = {
organization: "Im-D",
outerFunc: function() {
console.log(this.organization);
innerFunc = () => {
console.log(this.organization);
};
innerFunc();
}
};
obj.outerFunc();
ES5
에서는 원래 내부 함수에서의 this
는 window
객체에 바인딩 되었기 때문에 var that=this;
와 같이 선언하여 that
에 this
를 할당하고 내부 함수에서는 that
을 활용하는 방식을 사용했었다.
하지만, ES6
에서 등장한 화살표 함수에서는 this가 무조건 상위 스코프의 this를 가리킨다.
이에 따라 내부함수에서 var that=this;
와 같은 구문을 사용할 필요가 없다.
이처럼 정적으로 this
가 바인딩되기 때문에 Lexical this라고 한다.
🙏 Reference
Prototype
프로토타입 체인
특정 객체의 메서드나 프로퍼티에 접근하고자할 때, 해당 객체에 접근하려고 하는 프로퍼티나 객체가 없다면 프로토타입 링크([[Prototype]] 프로퍼티)를 따라 자신의 부모 역할을 하는 프로토타입 객체를 차례로 검색한다. 이를 프로토타입 체인이라 한다.
var developer = {
name: "BKJang",
age: 25,
sex: "male"
};
console.log(developer.hasOwnProperty("name")); //true
console.log(developer.__proto__ === Object.prototype); //true
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true
developer
객체에는 hasOwnProperty()
메서드가 존재하지 않지만 에러가 나지 않는다.
이는 developer
객체의 부모 객체인 Object.prototype
해당 메서드를 검색하기 때문이다.
객체 리터럴 방식으로 생성했을 때 프로토타입 체인
var developer = {
name: "BKJang",
age: 25,
sex: "male"
};
console.log(developer.__proto__ === Object.prototype); //1.true
console.log(Object.prototype.constructor === Object); // 2.true
console.log(Object.__proto__ === Function.prototype); //3.true
console.log(Function.prototype.__proto__ === Object.prototype); //4.true
developer
객체와 Function.prototype
객체의 프로토타입 객체는 Object.prototype
객체다.
객체리터럴 방식으로 객체를 생성하면 해당 객체의 프로토타입 객체는
Object.prototype
객체다.
생성자 함수를 생성했을 때 프로토타입 체인
function Developer(name) {
this.name = name;
}
var web = new Developer("BKJang");
console.log(web.__proto__ === Developer.prototype); //1.true
console.log(Developer.prototype.__proto__ === Object.prototype); //2.true
console.log(Developer.prototype.constructor === Developer); //3.true
console.log(Developer.__proto__ === Function.prototype); //4.true
console.log(Function.prototype.__proto__ === Object.prototype); //5.true
Developer.prototype
객체와 Developer() 생성자 함수의 프로토타입 객체인 Function.prototype
객체의 프로토타입 객체는 Object.prototype
객체다.
프로토타입 체인의 종점(End of prototype chain)
객체 리터럴 방식으로 객체를 생성하든 생성자 함수를 이용해 객체를 생성하든 결국 모든 객체의 부모 객체(프로토타입 객체)는
Object.prototype
객체다. 이 때Object.prototype
객체를 프로토타입 체인의 종점이라 한다.
프로토타입 객체의 확장
프로토타입 객체 역시 객체다. 따라서, 객체의 프로퍼티를 동적으로 추가하거나 삭제할 수 있다.
function Developer(name) {
this.name = name;
}
var web = new Developer("BKJang");
web.printAge(25); // Uncaught TypeError: web.printAge is not a function
위의 코드의 결과를 보면 web객체에서 printAge()
라는 메서드가 없기 때문에 에러가 나는 것을 볼 수 있다.
function Developer(name) {
this.name = name;
}
var web = new Developer("BKJang");
Developer.prototype.printAge = function(age) {
console.log("The age of this developer is", age);
};
web.printAge(25); // The age of this developer is 25
web 객체의 프로토타입 객체(부모 객체)인 Developer.prototype
객체에 printAge(age)
라는 메서드를 추가했다.
이에 따라 web 객체에서 printAge(age)
메서드에 접근하면 결과 값을 출력하는 것을 볼 수 있다.
기본 데이터 타입의 확장
자바스크립트에서 숫자, 문자열과 같은 기본 데이터 타입에서 사용되는 표준 메서드의 경우 Number.prototype
과 String.prototype
객체에 정의되어 있다.
var str = "hello world";
str.printStr = function(text) {
console.log(text);
};
str.printStr("This is the test"); //Uncaught TypeError: str.printStr is not a function
원시 데이터 타입인 문자열의 경우에는 객체가 아니기 때문에 프로퍼티를 동적으로 추가할 수 없다.
그렇다면, 위에서 str
변수에 printStr
메서드를 동적으로 추가하는 코드에서는 왜 에러가 발생하지 않을까?
그 이유는 기본 데이터 타입으로 프로퍼티나 메소드를 호출하면 기본 데이터 타입과 연관된 객체로 일시적으로 변환되어 프로토타입 객체를 공유하게 되기 때문이다.
var str = "hello world";
String.prototype.printStr = function(text) {
console.log(text);
};
str.printStr("This is the test"); //This is the test
"this is string".printStr("This is the test"); //This is the test
문자열 타입의 경우, String.prototype
객체에 표준 메서드가 정의 되어있기 때문에 해당 객체에 메서드를 추가해주면 기본 데이터 타입에서도 해당 메서드를 사용할 수 있다.
var str = "hello world";
String.prototype.printStr = function(text) {
console.log(text);
};
console.log(str.__proto__ === String.prototype); //1.true
console.log(String.prototype.__proto__ === Object.prototype); //2.true
console.log(String.prototype.constructor === String); //3.true
console.log(String.__proto__ === Function.prototype); //4.true
console.log(Function.prototype.__proto__ === Object.prototype); //5.true
str.printStr("This is the test"); //This is the test
프로토타입 객체의 변경
자바스크립트에서 특정 객체는 부모 객체인 프로토타입 객체를 임의로 변경할 수 있다.
function Developer(name) {
this.name = name;
}
var web = new Developer("BKJang");
Developer.prototype = { age: 25 };
var android = new Developer("YAKim");
console.log(web.age); //undefined
console.log(android.age); //25
console.log(web.constructor); //Developer(name)
console.log(android.constructor); //Object()
- 변경 전 : 파란색 번호
- 변경 후 : 주황색 번호
프로토타입 객체를 변경하기 전, web객체의 constructor
는 프로토타입 체이닝에 따라 Developer()
생성자 함수를 가리킨다.
프로토타입 객체를 변경한 후, android객체의 constructor
는 Object()
함수를 가리킨다.
프로토타입 객체가 변경되면서 Developer.prototype
객체의 constructor
프로퍼티와 Developer()
생성자 함수의 연결이 깨진다.
이에 따라 프로토타입 체인이 동작하고 android 객체의 constructor
는 Object.prototype
객체의 constructor
프로퍼티가 가리키는 Object()
함수가 되는 것이다.
- 프로토타입 객체를 변경하기 전과 후의 프로토타입 링크([[Prototype]] 프로퍼티)는 각각 다른 프로토타입 객체와 바인딩 된다.
- 프로토타입 객체를 변경한 후에는 프로토타입 객체의 constructor 프로퍼티와 생성자 함수와의 연결이 깨진다.
프로토타입 체인의 동작 조건
프로토타입 체인은 객체의 특정 프로퍼티에 접근할 때, 그 프로퍼티가 해당 객체에 없는 경우 동작한다.
function Developer(name) {
this.name = name;
}
Developer.prototype.age = 25;
Developer.prototype.sex = "male";
var web = new Developer("BKJang");
var android = new Developer("YAKim");
android.sex = "female";
console.log(web.age); //1.25
console.log(web.sex); //2.male
console.log(android.age); //1.25
console.log(android.sex); //3.female
-
web
객체에는age
와sex
프로퍼티가 없기 때문에 프로토타입 체인에 따라Developer.prototype
객체의age
와sex
프로퍼티에 접근하고 있다. -
android
객체에는age
프로퍼티는 없지만sex
프로퍼티는 있기 때문에sex
프로퍼티의 경우엔 프로토타입 체인이 동작하지 않고android
객체의sex
프로퍼티 값을 반환하고 있다.
🙏 Reference
- 인사이드 자바스크립트 (송형주, 고형준)
- Poiemaweb - Prototype
실행 컨텍스트
실행 컨텍스트는 자바스크립트가 동작하는 원리라고 할 수 있다.
쉽게 말하면, 코드가 실행되는 환경이라고 보면 된다.
-
전역 컨텍스트 생성 후, 함수 호출 시마다 함수 컨텍스트가 생긴다.
-
컨텍스트 생성 시 컨텍스트 안에
변수객체(arguments, variable)
,scope chain
,this
가 생성된다. -
컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾는다.
-
함수 실행이 끝나면 해당 컨텍스트는 사라지고, 페이지가 종료되면 전역 컨텍스트는 사라진다.
실행 컨텍스트 스택
코드가 실행 될 때, 실행 컨텍스트 스택(Stack) 이 생성하고 소멸한다.
현재 실행 중인 컨텍스트에서 관련없는 코드(예를 들어, 다른 함수)가 실행되면 새로운 컨텍스트가 생성된다.
var global = "global";
function foo() {
var local1 = "local1";
function bar() {
var local2 = "local2";
console.log(local1, local2, global); //local1 local2 global
}
bar();
}
foo();
변수 객체(Variable Object)
실행 컨텍스트가 생성되면 자바스크립트 엔진은 실행에 필요한 여러 정보들을 담을 객체를 생성한다. 이를 Variable Object(VO / 변수 객체) 라고 한다.
변수 객체는 arguments(인수 정보) 와 variable(스코프의 변수) 을 담고 있고, 전역 컨텍스의 경우와 함수 컨텍스트의 경우에 가리키는 객체가 다르다.
전역 컨텍스트
전역 컨텍스트의 경우, 변수 객체는 arguments
를 가지지 않는다.
그리고 변수 객체는 모든 전역 변수, 전역 함수 등을 포함하는 전역 객체(Global Object / GO)를 가리킨다.
전역 객체는 전역 변수와 전역 함수를 프로퍼티로 가진다.
함수 컨텍스트
함수 컨텍스트의 경우, 변수 객체는 Activation Object(AO / 활성 객체)를 가리킨다.
또한, 전역 컨텍스트와 다르게 매개변수와 인수들의 정보를 배열의 형태로 담고 있는 유사 배열 객체 arguments
도 가진다.
스코프 체인(Scope Chain)
스코프 체인은 현재 컨텍스트의 유효 범위를 나타내는 스코프 정보를 담고 있으며, 연결 리스트의 형태와 유사하게 생성된다.
이 리스트를 이용해 현재 컨텍스트의 변수와 상위 실행 컨텍스트의 변수에도 접근할 수 있다.
이 리스트는 현재 실행 컨텍스트의 활성 객체를 먼저 가리키고 순차적으로 상위 컨텍스트의 활성 객체를 가리키고 마지막으로 전역 객체를 가리킨다.
즉, 스코프 체인은 식별자 중 변수를 검색하는 것을 말하고, 변수가 아닌 객체의 프로퍼티를 검색하는 것을 프로토타입 체인이라고 한다.
🙏 Reference
- 인사이드 자바스크립트 (송형주, 고형준)
- Poiemaweb - 실행 컨텍스트와 자바스크립트의 동작 원리
클로저(Closure)
클로저는 실행 컨텍스트와 밀접한 관련이 있다.
생성된 함수 객체는 [[Scopes]]
프로퍼티를 가지게 된다.
[[Scopes]]
프로퍼티는 함수 객체만이 소유하는 내부 프로퍼티(Internal Property)로서 현재 실행 컨텍스트의 스코프 체인이 참조하고 있는 객체를 값으로 설정한다.
내부 함수의 [[Scopes]]
프로퍼티는 자신의 실행 환경(Lexical Enviroment) 과 자신을 포함하는 외부 함수의 실행 환경과 전역 객체를 가리킨다.
이 때, 자신을 포함하는 외부 함수의 실행 컨텍스트가 소멸하여도 [[Scopes]]
프로퍼티가 가리키는 외부 함수의 실행 환경(Activation Object)은 소멸하지 않고 참조할 수 있다. 이것이 클로저이다.
외부함수에서 내부함수를 반환하는 코드를 보자.
function foo() {
var x = "variable of outerFunc";
function bar() {
console.log(x);
}
return bar;
}
var innerFunc = foo();
innerFunc(); //variable of outerFunc
위의 코드를 보면 외부함수 foo()
에서 bar()
를 반환하고 소멸한다.
외부함수 foo()
는 실행된 이후, 실행 컨텍스트 스택에서 제거되기 때문에 변수 x
도 같이 소멸될 것으로 보인다. 이에 따라 변수 x
에 접근할 방법이 없어 보인다.
하지만 innerFunc()
함수를 호출하면 변수 x의 값이 출력되는 것을 볼 수 있다.
이처럼 클로저는 외부함수(foo()
) 밖에서 내부함수(bar()
)가 호출되더라도 외부함수의 지역 변수(var x
)에 접근할 수 있다.
클로저가 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는 이유를 설명한 그림이다.
외부함수인 foo()
함수가 종료되면 함수 실행 컨텍스트도 소멸하지만 foo()
함수 실행 컨텍스트의 활성 객체는 유효하다.
이 때문에 외부 함수 foo()
가 실행이 종료되어도 내부 함수 bar()
에서 접근이 가능한 것이다.
클로저를 사용하면 클로저에서의 스코프 체인 접근 방식, 메모리의 부담 등의 이유로 성능적인 면과 자원적인 면에서 손해를 볼 수 있다.
그렇기 때문에 좋은 구현을 위해서는 충분한 경험을 쌓을 필요가 있다.
클로저를 활용한 전역 변수의 사용 억제
클로저를 활용한 대표적인 예로 카운터가 있다. 우선, 전역 변수를 사용한 예를 한 번 살펴보자.
var counter = 0;
function calculator() {
return console.log(++counter);
}
calculator(); //1
calculator(); //2
calculator(); //3
위의 결과는 에상대로 잘 나오고 있지만 전역 변수 counter
를 쓰고 있다.
전역 변수는 어디서든 접근이 가능하기 때문에 값이 변할 수 있고 이에 따라 오류를 불러올 수 있다.
var outerFunc = (function() {
var counter = 0;
function calculater() {
return console.log(++counter);
}
return calculater;
})();
outerFunc(); //1
outerFunc(); //2
outerFunc(); //3
위의 코드와 같이 클로저를 이용하면 전역 변수의 사용을 줄일 수 있다.
루프 안에서의 클로저 활용
클로저를 활용하는데 있어 주의할 사항에 대해 설명할 때 가장 많이 등장하는게 이 경우다.
function count(numberOfCount) {
for (var i = 1; i <= numberOfCount; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
}
count(4);
보면 알겠지만, 위 코드의 의도는 1초 간격으로 1,2,3,4를 출력하는 것이다. 하지만 결과는 예상과 다르게 5가 4번 1초 간격으로 출력된다.
그 이유는 변수 i
는 외부함수의 변수가 아닌 전역변수이고 setTimeout()
함수가 실행되는 시점은 count()
함수가 종료된 이후다.
이 때는 이미 i
의 값이 5인 상태이다.
function count(numberOfCount) {
for (var i = 1; i <= numberOfCount; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, i * 1000);
})(i);
}
}
count(4);
즉시 실행 함수를 실행시켜 루프의 i
값을 j
에 복사하고 setTimeout()
함수에서 사용했다.
이 때 j
는 상위스코프의 자유변수이므로 그 값이 유지된다.
이러한 문제는 자바스크립트의 함수형 스코프로 인해 for 루프의 초기문에서 사용된 변수는 전역 스코프를 가지기 때문에 발생한다.
ES6에서는 let
을 이용해 블록 레벨 스코프를 구현할 수 있다.
function count(numberOfCount) {
for (let i = 1; i <= numberOfCount; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
}
count(4);
첫 번째 코드에서 var
를 let
으로만 바꿔주면 위의 코드처럼 깔끔하게 구현할 수 있다.
🙏 Reference
- [인사이드 자바스크립트 (송형주, 고형준)]
- Poiemaweb - Closure
템플릿 리터럴(Template Literal)
ES6(ECMA Script2015)에서는 새로운 문자열 표기법이 도입됐는데 이것이 템플릿 리터럴이다.
템플릿 리터럴을 사용하면 기존 문자열 표기 방법의 불편함을 어느 정도 해결할 수 있다.
-
템플릿 리터럴은 `(백틱)을 사용한다.
-
이스퀘이프 시퀀스를 사용하지 않아도 템플릿 리터럴 내의 white space가 그대로 인식된다.
//ES5
var str = "Hello.\n My Name is BKJang. \n I'm developer.";
//ES6
let templateStr = `Hello.
May Name is BKJang
I'm developer.`;
console.log(templateStr);
/*
Hello.
May Name is BKJang
I'm developer.
*/
- 여러 개의 문자열을 연결할 때는
+
연산자를 사용하지 않고String Interpolation(문자열 인터폴레이션)
을 사용한다.
let str1 = "Hello";
let str2 = "World";
let id = "bkjang";
//ES5
console.log(str1 + " " + str2); //Hello World
//ES6
console.log(`${str1} ${str2}`); //Hello World
let url = `http://localhost:3000/api/user/${id}`;
console.log(url); //http://localhost:3000/api/user/bkjang
console.log("1 더하기 2 는 " + (1 + 2) + " 입니다,");
console.log(`1 더하기 2 는 ${1 + 2} 입니다.`);
🙏 Reference
화살표 함수(Arrow Function)
화살표 함수는 function
대신 =>
를 사용함으로써 좀 더 간결하게 함수를 선언할 수 있다.
또한 화살표 함수는 익명 함수로만 사용할 수 있기 때문에 함수 표현식을 사용한다.
const foo = () => {...} //매개변수가 없을 때
const foo = x => {...} //매개변수가 하나일 때
const foo = (x, y) => {...} //매개변수가 여러 개일 때
const foo = x => { return x; }
const foo = x => x; // 함수의 블록이 한줄이라면 중괄호와 return을 생략
const sum = (x, y) => {
return x + y;
}
console.log(sum(1, 2)); //3
//ES5
var arr = ["JS", "Java", "Go"];
var foo = arr.map(function(element) {
return { Lang: element };
});
console.log(foo); //[{Lang: "JS"}, {Lang: "Java"}, {Lang: "Go"}]
//ES6
const arr = ["JS", "Java", "Go"];
const foo = arr.map(element => {
return { Lang: element };
});
console.log(foo); //[{Lang: "JS"}, {Lang: "Java"}, {Lang: "Go"}]
화살표 함수에서의 this 바인딩
ES5에서는 함수의 호출 방식에 따라 this가 동적으로 바인딩이 이뤄진다.
하지만 화살표 함수에서는 this가 무조건 상위 스코프의 this를 가리킨다.
즉 정적인 방식으로 this가 바인딩이 되는데 이를 Lexical this 라고 한다.
위의 소스를 화살표 함수를 이용해서 바꾸면 var that = this;
라는 구문을 쓸 필요가 없다.
const lang = "Korean";
const obj = {
lang: "English",
outerFunc() {
//ES6의 축약 메서드 표현
console.log("outerFunc : ", this.lang);
innerFunc1 = () => {
console.log("innerFunc1 : ", this.lang);
innerFunc2 = () => {
console.log("innerFunc2 : ", this.lang);
};
innerFunc2();
};
innerFunc1();
}
};
obj.outerFunc();
/*
outerFunc : English
innerFunc1 : English
innerFunc2 : Engilsh
*/
항상 화살표 함수일까?
화살표 함수는 위에서 본 것 처럼 정적으로 this를 바인딩(Lexical this)를 지원하기 때문에 콜백 함수로 쓰기 매우 편하다. 하지만, 주의해서 써야할 경우가 몇 가지 있다.
addEventListener의 콜백 함수 선언
const btn = document.getElementById("submitBtn");
btn.addEventListener("click", () => {
console.log(this); //Window
});
위에서 볼 수 있듯이 addEventListener
의 콜백 함수를 화살표 함수로 선언하면 this
는 window
에 바인딩되므로 그냥 일반함수를 사용하여 선언하여야 한다.
객체의 메서드에 선언
객체의 메서드를 선언할 때, 화살표 함수를 쓰면 그 객체에 this가 바인딩되지 않고 전역(window)에 바인딩된다.
var lang = "Korean";
const obj = {
lang: "English",
foo: () => {
console.log("outerFunc : ", this.lang);
}
};
obj.foo(); //outerFunc : Korean
위에선 English가 나온다고 생각할 수도 있다.
하지만, foo
선언할 때 화살표 함수를 사용했고 이에 따라 상위 컨텍스트인 window
에 this
가 바인딩 되기 때문에 결과는 Korean이 나온다.
따라서 원하는 결과가 English라면 축약 메서드 표현법으로 정의하는 것이 맞다.
var lang = "Korean";
const obj = {
lang: "English",
foo() {
console.log("outerFunc : ", this.lang);
}
};
obj.foo(); //outerFunc : English
프로토타입 객체의 메서드 선언
프로토타입 객체에 메서드를 화살표 함수로 정의하면 객체의 메서드에 화살표 함수로 선언할 때와 같이 this
가 window
에 바인딩된다.
const devleoper = {
name: "BKJang"
};
Object.prototype.renderData = () => console.log(this.name);
devleoper.renderData(); //undefined
생성자 함수 선언
결론부터 말하면, 화살표 함수는 prototype
프로퍼티를 갖고 있지 않다. 따라서 생성자 함수를 선언할 때 화살표 함수를 쓰면 인스턴스를 생성할 수 없다.
const Person = name => {
this.name = name;
};
const jang = new Person("BKJang"); //Uncaught TypeError: Person is not a constructor
Spread연산자 & rest파라미터
rest 파라미터
rest 파라미터는 Spread 연산자(...
)를 사용하여 파라미터를 정의한다.
기존에 ES5에서는 가변 파라미터를 정의할 때 arguments
객체를 사용할 수 있었지만 이를 rest 파라미터
로 대체할 수 있다.
//ES5
function foo() {
console.log(Array.isArray(arguments));
console.log(arguments);
}
foo(1, 2, 3, 4, 5);
/*
false
{'0': 1, '1': 2, '2': 3, '3': 4, '4': 5, length: 5}
*/
//ES6
function foo(...rest) {
console.log(Array.isArray(rest));
console.log(rest);
}
foo(1, 2, 3, 4, 5);
/*
true
[1, 2, 3, 4, 5]
*/
위의 결과를 보면 arguments 객체와 rest 파라미터의 중요한 차이를 알 수 있다.
arguments
객체는 유사배열 객체다. 따라서 Array.isArray(arguments)
의 결과는 false를 반환한다.
여기서 큰 차이가 발생하는데 유사배열객체는 배열의 메서드를 사용할 수 없다. 배열의 메서드를 사용하기 위해서는 이를 배열로 변환하는 과정을 거쳐야하는 불편함이 있다.
하지만 가변 인자 함수를 rest 파라미터로 정의하면 파라미터는 배열의 형태로 넘어온다.
또한 ES6의 화살표 함수(Arrow Function)에서는 arguments
를 사용할 수 없다.
//ES6 화살표 함수
const foo = () => {
console.log(arguments);
};
foo(1, 2, 3, 4, 5); //Uncaught ReferenceError: arguments is not defined
rest
파라미터는arguments
와 달리 배열로 파라미터가 넘어온다.- ES6의 화살표 함수에서는
arguments
를 사용할 수 없다.
Spread 연산자
Spread 연산자는 말그대로 전개 연산자다. 배열 또는 Iterable object(반복 가능한 객체)의 엘리먼트를 하나씩 분리하여 전개한다.
let a = "Hello";
let arr = [...a];
console.log(arr); //["H", "e", "l", "l", "o"]
Spread 연산자의 활용
- concat
ES5에서는 배열을 합칠 때 concat
을 사용했었다. 이를 Spread
연산자로 대체할 수 있다.
//concat
var arr1 = [5, 6];
var arr2 = [1, 2, 3, 4];
console.log(arr2.concat(arr1)); //[1, 2, 3, 4, 5, 6]
console.log(arr1.concat(arr2)); //[5, 6, 1, 2, 3, 4]
//ES6 Spread Operator
let arr1 = [5, 6];
let arr2 = [1, 2, 3, 4, ...arr1];
console.log(arr2); //[1, 2, 3, 4, 5, 6]
console.log([1, 2, ...arr1, 3, 4]); //[1, 2, 5, 6, 3, 4]
단순히 앞, 뒤에 배열의 요소를 붙이는데는 concat
이 성능이 더 좋다. 하지만 중간에 특정 배열의 값을 추가하고 싶다면 Spread
연산자를 사용하는 것도 좋은 방법이다.
- split
문자열을 배열로 변환할 때 많이 쓰이는 함수가 split()
이다. 이 또한 Spread 연산자를 활용하면 좀 더 편하게 변환할 수 있다.
//split
var a = "Hello";
var arr = a.split("");
console.log(arr); //["H", "e", "l", "l", "o"]
//ES6 Spread Operator
let a = "Hello";
let arr = [...a];
console.log(arr); //["H", "e", "l", "l", "o"]
- 함수의 인자로 사용
기존에 ES5에서는 배열의 각 요소를 개별적인 파라미터로 전달하고 싶은 경우, Function.prototype.apply
를 사용하는 것이 일반적이었다.
하지만 ES6의 Spread
연산자를 활용하여 함수의 인자에 들어가는 배열을 개별요소로 전달할 수 있다.
//ES5 apply
var arr = [1, 2, 3];
function sum(a, b, c) {
console.log(a, b, c); //1 2 3
return a + b + c;
}
console.log(sum.apply(null, arr)); //6
//ES6 Spread Operator
let arr = [1, 2, 3];
const sum = (a, b, c) => a + b + c;
console.log(sum(...arr)); //6
- 객체에서 사용
객체는 Iterable Object아니지만 Spread 연산자를 사용하면 객체를 손쉽게 병합 또는 변경할 수 있다.
let obj1 = {
name: "BKJang",
job: "Developer"
};
let obj2 = {
...obj1,
lang: "Korean"
};
console.log(obj2); //{name: "BKJang", job: "Developer", lang: "Korean"}
Spread 연산자를 활용하면 유사배열객체(arguments, HTMLCollection 등)를 배열로 변환하기도 편하다.
function foo() {
let args = arguments;
let arr = [...args];
console.log(Array.isArray(args)); //false
console.log(Array.isArray(arr)); //true
console.log(arr); //
}
foo(1, 2, 3, 4, 5); //[1, 2, 3, 4, 5]
🙏 Reference
Destructuring(비구조화 할당)
디스트럭처링은 구조화된 배열 혹은 객체를 분해하여 변수에 할당하는 방식이다. 이 개념을 몰랐더라도 React
를 사용해봤던 개발자라면 아마 많이 봤을 문법이다.
const { state } = this.props;
오른쪽의 특정 값을 해체하여 왼쪽에 할당하는 표현식을 **Destructuring Assignment
**라고 한다.
배열 디스트럭처링
//ES5
var arr = ["JS", "Java", "Node.js"];
var x = arr[0];
var y = arr[1];
var z = arr[2];
console.log(x, y, z); //JS Java Node.js
//ES6
const arr = ["JS", "Java", "Node.js"];
let [x, y, z] = arr;
console.log(x, y, z); //JS Java Node.js
const numArr = [1, 2, 3, 4];
let [x, y, , z] = numArr;
console.log(x, y, z); //1 2 4
위의 결과를 보면 알 수 있듯이 배열을 디스트럭처링하면 각각의 변수에 배열의 index
를 기준으로 할당된다.
디스트럭처링을 사용했을 때 편한 대표적인 예는 변수의 swap처리를 할 때다.
//ES5(For Swap)
var x = 1;
var y = 2;
var tmp = y;
console.log(x, y); //1 2
y = x;
x = tmp;
console.log(x, y); //2 1
//ES6
let x = 1;
let y = 2;
console.log(x, y); //1 2
[x, y] = [y, x];
console.log(x, y); //2 1
객체 디스트럭처링
객체 또한 디스트럭처링이 가능하며 배열과 크게 다르지 않다.
//ES5
var obj = { name: "BKJang", lang: "Korean", job: "Developer" };
var name = obj.name;
var lang = obj.lang;
var job = obj.job;
console.log(name, lang, job); //BKJang Korean Developer
//ES6
const obj = { name: "BKJang", lang: "Korean", job: "Developer" };
let { name, lang, job } = obj;
console.log(name, lang, job); //BKJang Korean Developer
만약 변수 명을 다르게 하고 싶다면 다음과 같이 처리하면 된다.
var obj = { a: 1, b: "hello" };
var { a: key, b: value } = obj;
console.log(key, value); // 1, 'hello'
중첩 객체의 경우에는 아래와 같이 사용한다.
const developer = {
name: "BKJang",
stack: {
front: "HTML / CSS / JS",
back: "Java / Node.js"
}
};
const {
name,
stack: { front }
} = developer;
console.log(name, front); //BKJang HTML / CSS / JS
디스트럭처링을 사용하면 **기본 값(Default Value)**이나 **기본 파라미터(Default Parameter)**를 세팅할 수 있고, Speread Operator또한 사용할 수도 있다.
Spread Operator 용
const arr = [1, 2, 3, 4];
let [x, y, ...z] = arr;
console.log(x, y, z); //1 2 [3, 4]
const obj = { one: 1, two: 2, three: 3, four: 4 };
let { one, two, ...rest } = obj;
console.log(one, two, rest); //1 2 {three: 3, four: 4}
기본 값(Default Value)
const arr = [1, 2];
let [x, y, z = 3] = arr;
console.log(x, y, z); //1 2 3
const obj = { one: 1, two: 2 };
let { one, two, three = 3 } = obj;
console.log(one, two, three); //1 2 3
기본 파라미터(Default Parameter)
const doSomething = (name, stack = "FrontEnd") => {
stack = stack === null ? "FullStack" : stack;
console.log(`${name}은 ${stack}개발자입니다.`);
};
doSomething("BKJang"); //BKJang은 FrontEnd개발자입니다.
doSomething("BKJang", "BackEnd"); //BKJang은 BackEnd개발자입니다.
doSomething("BKJang", undefined); //BKJang은 FrontEnd개발자입니다.
doSomething("BKJang", null); //BKJang은 FullStack개발자입니다.
//Default Parameter는 함수의 length에 포함되지 않는다.
console.log(doSomething.length); //1
자바스크립트에서 함수는 length
프로퍼티를 가지는데 인자의 갯수를 나타낸다.
Default Parameter
는 이 length
프로퍼티에 포함되지 않는다.
🙏 Reference
- MDN Web Docs - 구조 분해 할당
- [ES6] 5. Destructuring and Default Parameter
- Destructuring 디스트럭처링
- ImD/Dev-Docs - Destructuring_Assignment
클래스(class)
ES6
에서부터는 ES5
까지는 존재하지 않았던 class
가 생겨났다. Java
에서의 Class
와는 똑같은 기능을 한다고 생각해서는 안된다.
자바스크립트는 기본적으로 Prototype
기반의 객체지향 언어다. 즉, ES6
의 Class
또한 프로토타입을 기반으로 동작하며 이는 기존의 자바스크립트에서 객체지향적으로 설계할 때의 방식을 좀 더 편하게 보완한 Syntatic Sugar
다.
클래스의 정의
//ES5
var Person = (function() {
//생성자 함수 정의
function Person(name, job) {
this.name = name;
this.job = job;
}
Person.prototype.sayInfo = function() {
console.log("Name : " + this.name + ", Job : " + this.job);
};
return Person;
})();
var bkJang = new Person("BKJang", "Developer");
bkJang.sayInfo(); //Name : BKJang, Job : Developer
ES6
에서 Class
가 생기기 전 우리는 위와 같은 방식으로 생성자 함수와 프로토타입을 이용해 객체지향 프로그래밍을 진행했었다. 위와 같은 코드를 ES6
의 Class
를 사용하여 구현하면 아래와 같이 좀 더 간결하게 구현할 수 있다.
//ES6
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
sayInfo() {
console.log(`Name : ${this.name}, Job : ${this.job}`);
}
}
const bkJang = new Person("BKJang", "Developer");
bkJang.sayInfo(); //Name : BKJang, Job : Developer
클래스는 기본적으로 위와 같이 Class Person {}l
으로 정의하며, 흔치는 않지만 const Person = class MyClass {};
처럼 함수 표현식으로도 정의 가능하다.
둘의 또 다른 차이점은 생성자 함수를 이용하여 선언하면 window
에 할당되지만, Class
를 이용하여 선언하면 window
에 할당되지 않는다.
또한 Class
안에 있는 코드는 항상 strict mode
로 실행되기 때문에 "use strict" 명령어가 없더라도 동일하게 동작한다.
function Person() {}
class Developer {}
console.log(window.Person); //ƒ Person() {}
console.log(window.Developer); //undefined
인스턴스의 생성과 호이스팅
Class
를 사용하여 인스턴스를 생성할 때는 반드시 new
를 이용해 호출해야하며 new
를 사용하지 않으면 Type Error
가 발생한다.
class Foo {}
const foo = Foo(); //Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'
ES6
의 Class
는 let, const와 마찬가지로 호이스팅이 일어나지만, 선언이 일어나고 할당이 이뤄지기 전 TDZ(Temporary Dead Zone)
에 빠지기 때문에 할당 이전에 호출하면 Reference Error
가 발생한다.
const foo = new Foo(); //Uncaught ReferenceError: Foo is not defined
class Foo {}
constructor
constructor
는 인스턴스를 생성하고 Class
의 property
를 초기화한다. ES5
에서는 생성자 함수를 이용해 property
를 초기화하고 생성자 함수를 반환함으로써 객체지향을 구현했었다.
- class는 constructor를 반환하며 생략할 수 있다.
const foo = new Foo()
와 같이 선언했을 때 Foo
는 class
명이 아닌 constructor
다.
class Foo {}
const foo = new Foo();
console.log(Foo == Foo.prototype.constructor); //true
위의 코드에서 볼 수 있듯이 new
와 함께 호출한 Foo
는 constructor
와 같음을 확인할 수 있다.
또 확인할 수 있는 것은 class Foo
내부에 constructor
를 선언하지 않았음에도 인스턴스의 생성이 잘 이뤄지는 것을 볼 수 있다.
이는 Class
내부에 constructor
는 생략할 수 있으며 생략하면 Class
에 constructor() {}
를 포함한 것과 동일하게 동작하기 때문이다.
즉, 빈 객체를 생성하기 때문에 property
를 선언하려면 인스턴스를 생성한 이후, property
를 동적 할당해야 한다.
console.log(foo); //Foo {}
foo.name = "BKJang";
console.log(foo); //Foo {name: "BKJang"}
- class의 property는 constructor 내부에서 선언과 초기화가 이뤄진다.
class
의 몸체에는 메서드만 선언 가능하며, property
는 constructor
내부에서 선언하여야 한다.
class Foo {
name = ""; //Syntax Error
}
class Bar {
constructor(name = "") {
this.name = name;
}
}
const bar = new Bar("BKJang");
console.log(bar); //Bar {name: "BKJang"}
constructor
내부에서 선언한property
는Class
의 인스턴스를 가리키는this
에 바인딩 된다.
getter, setter
class
의 프로퍼티에 접근하기 위한 인터페이스로서, getter
와 setter
를 정의할 수 있다.
class Person {
constructor(name) {
this.name = name;
}
//getter
get personName() {
return this.name ? this.name : null;
}
//setter
set personName(name) {
this.name = name;
}
}
const person = new Person("BKJang");
console.log(person.personName); //BKJang
person.personName = "SHJo";
console.log(person.personName); //SHJo
Static 메서드
class
에서는 정적 메서드를 정의할 때, static
키워드를 사용하여 정의한다. 정적 메서드는 인스턴스를 생성하지 않아도 호출가능하며, 인스턴스가 아닌 Class
의 이름으로 호출한다. 이와 같은 특징 때문에 애플리케이션을 위한 유틸리티성 함수를 생성하는데 주로 사용한다.
또한 정적 메서드 내부에서는 this
를 사용할 수 없다. 왜냐하면 정적 메서드 내부에서는 this
가 Class
의 인스턴스가 아닌 Class
자기 자신을 가리키기 때문이다.
class Person {
constructor(name) {
this.name = name;
}
//getter
get personName() {
return this.name ? this.name : null;
}
//setter
set personName(name) {
this.name = name;
}
static staticMethod() {
console.log(this);
return "This is static";
}
}
console.log(Person.staticMethod());
/*
class Person { ... }
This is static
*/
const instance = new Person("BKJang");
console.log(instance.staticMethod()); //Uncaught TypeError: instance.staticMethod is not a function
위에서 볼 수 있듯이 인스턴스로는 Class
의 정적 메서드를 호출할 수 없다.
또한 정적 메서드는 prototype
에 추가되지 않는다.
console.log(Person.staticMethod === Person.prototype.staticMethod); //false
console.log(new Person().personName === Person.prototype.personName); //true
클래스의 상속
class
를 이용하여 OOP
의 특징 중 하나인 상속을 구현할 수 있다.
class
의 상속을 위해서는 extends
와 super
키워드에 대해서 알아야 한다.
class Person {
constructor(name, sex) {
this.name = name;
this.sex = sex;
}
getInfo() {
return `Name : ${this.name}, Sex : ${this.sex}`;
}
getName() {
return `Name : ${this.name}`;
}
getSex() {
return `Sex : ${this.sex}`;
}
}
class Developer extends Person {
//extends를 사용하여 Person 클래스 상속
constructor(name, sex, job) {
//super메서드를 사용하여 부모 클래스의 인스턴스를 생성
super(name, sex);
this.job = job;
}
//오버라이딩
getInfo() {
//super 키워드를 사용하여 부모 클래스에 대한 참조
return `${super.getInfo()} , Job: ${this.job}`;
}
getJob() {
return `Job : ${this.job}`;
}
}
const person = new Person("SHJo", "Male");
const developer = new Developer("BKJang", "Male", "Developer");
console.log(person); //Person {name: "SHJo", sex: "Male"}
console.log(developer); //Developer {name: "BKJang", sex: "Male", job: "Developer"}
console.log(person.getInfo()); //Name : SHJo, Sex : Male
console.log(developer.getName()); //Name : BKJang
console.log(developer.getSex()); //Sex : Male
console.log(developer.getJob()); //Job : Developer
console.log(developer.getInfo()); //Name : BKJang, Sex : Male , Job: Developer
console.log(developer instanceof Developer); //true
console.log(developer instanceof Person); //true
위의 소스를 기준으로 중요한 특징을 정리하자면 다음과 같다.(대부분의 객체 지향 언어에서 상속의 특징과 거의 동일하다.)
-
부모 클래스(슈퍼 클래스)의 메서드를 사용할 수 있다.
-
부모 클래스의 메서드를 오버라이딩(Overriding)할 수 있다.
-
super
키워드를 통해 부모 클래스의 메서드에 접근할 수 있다. -
super
메서드(위의 Developer 클래스의 constructor내부에 선언)는 자식 클래스의constructor
내부에서 부모 클래스의 constructor(super-constructor
)를 호출한다.
🙏 Reference
모듈(Module)
자바스크립트(ES5
)에서 기본적으로 모듈 기능이 없다.
기본적으로 자바스크립트에서 변수는 전역(global)에 할당되기 때문에 모듈을 구현하기 위해서는 Namespace Pattern
혹은 Module Pattern과 같은 기법이 필요했다.
이러한 상황에서 클라이언트 사이드에서뿐만이 아니라 범용적인 사용이 일어나면서 모듈화의 필요성이 대두되었다. 이에 따라 CommonJS와 AMD 이렇게 2개의 진영으로 나뉘게 되었다.
우리가 잘 알고 있는 Node.js
는 CommonJS
의 모듈화 방식을 따르고 있다.
ES6
에서는 클라이언트 사이드에서도 모듈화를 제공하기 위해 export
와 import
가 추가되었다.
단, ES6
의 모듈 기능은 대부분의 브라우저에서는 지원하지 않기 때문에 RequireJS
와 같은 모듈 로더나 Webpack
과 같은 모듈 번들러와 함께 babel
과 같은 트랜스파일러를 사용하여야 한다.
export
ES6의 모듈은 보통 파일 단위로 구성되며 독립적인 파일 스코프를 가지기 때문에 외부에서 모듈의 기능을 사용하고 싶다면 위와 같이 export
를 해줘야한다.
//module.js
export const message = "this is variable";
export function sayHello() {
console.log("Hello World");
}
export function sayName(name) {
console.log(`Hi ${name}`);
}
export class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
}
위와 같이 각각의 변수, 함수, 클래스에 export
키워드를 붙여 export할 수 있고 아래와 같이 하나의 객체로 묶어 한 번에 export할 수도 있다.
//module.js
const message = "this is variable";
function sayHello() {
console.log("Hello World");
}
function sayName(name) {
console.log(`Hi ${name}`);
}
class Person {
constructor(name, job) {
this.name = name;
this.job = job;
}
}
export { message, sayHello, sayName, Person };
import
ES6에서 export한 모듈을 사용하기 위해서는 해당 파일에서 import
키워드를 사용하여 가져와 쓰면 된다.
import { message, sayHello, sayName, Person } from "./module";
console.log(message); //this is variable
console.log(sayHello()); //Hello World
console.log(sayName("BKJang")); //Hi BKJang
console.log(new Person("BKJang", "Developer")); //Person { name : BKJang, job: Developer }
위와 같이 각각를 import하지 않고 한꺼번에 import하거나 이름을 변경하여 import 할 수도 있다.
//한꺼번에 묶어서 import
import * as module from "./module";
console.log(module.sayName("BKJang")); //Hi BKJang
//이름을 변경하여 import
import { sayHello as hello } from "./module";
console.log(hello()); //Hello World
default
모듈에서 하나만 export할 경우에는 default
키워드를 사용하면 된다.
//Person.js
export default class Person {
constructor(name) {
this.name = name;
}
}
//Person.js
class Person {
//...
}
export default
이를 import할 때는 {}
없이 해당 모듈을 임의의 이름으로 가져와 사용하면 된다.
import Person from "./Person";
🙏 Reference
Promise와 async-await
Promise
이전에는 비동기 작업을 처리 할 때에는 콜백 함수로 처리를 해야 했다. 콜백 함수로 처리를 하게 된다면 비동기 작업이 많아질 경우 코드의 가독성이 떨어지게 되었다. 이를 콜백 지옥이라고 한다.
Promise는 이러한 콜백 지옥에서 탈출하기 위해 ES6
에서 도입된 문법이다.
const printNumber = number => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const value = number + 1;
if (value === 4) {
const error = new Error("This is Error!");
reject(error);
return;
}
console.log(value);
resolve(value);
}, 1000);
});
};
printNumber(0)
.then(n => {
return printNumber(n);
})
.then(n => {
return printNumber(n);
})
.then(n => {
return printNumber(n);
})
.then(n => {
return printNumber(n);
})
.catch(error => {
console.log(error);
});
Promise
에서는 resolve
와 reject
를 파라미터로 전달하고 성공시에는 resolve
를 실패시에는 reject
를 실행한다. 다만, Promise
를 사용했을 때는 어떤 부분에서 error
를 잡아야할지가 조금 애매할 수 있고 조건 분기를 해줄 때 불편함이 있다. 이러한 점을 보완한 것이 async-await
문법이다.
Promise.all()
Promise.all()
은 여러 개의 비동기적 처리를 한 번에 처리하고 싶을 때 사용한다. 단, 모든 비동기 함수가 처리가 완료되면 결과를 출력한다. 따라서 비동기 함수 중 하나라도 에러가 발생한다면 결과 값은 에러가 된다.
const myPromise = time => {
return new Promise(resolve => setTimeout(resolve, time));
};
const printHello = async () => {
await myPromise(100);
return "Hello";
};
const printName = async () => {
await myPromise(500);
return "BKJang";
};
const printWorld = async () => {
await myPromise(300);
return "world";
};
const printError = async () => {
await myPromise(600);
throw new Error("This is Error!");
};
const makeProcess = async () => {
try {
const result = await Promise.all([printHello(), printName(), printWorld()]);
console.log(result);
} catch (error) {
console.error(error);
}
};
const makeProcessWithError = async () => {
try {
const result = await Promise.all([
printHello(),
printName(),
printWorld(),
printError()
]);
console.log(result);
} catch (error) {
console.error(error);
}
};
makeProcess(); //['Hello', 'BKJang', 'world']
makeProcessWithError(); //Error : This is Error!
Promise.race()
Promise.race()
도 Promise.all()
과 마찬가지로 모든 비동기 함수를 동시에 요청하지만 가장 먼저 처리된 비동기 함수의 결과만을 반환한다.
const myPromise = time => {
return new Promise(resolve => setTimeout(resolve, time));
};
const printHello = async () => {
await myPromise(100);
return "Hello";
};
const printName = async () => {
await myPromise(500);
return "BKJang";
};
const printWorld = async () => {
await myPromise(300);
return "world";
};
const printError = async () => {
await myPromise(600);
throw new Error("This is Error!");
};
const makeProcessWithError = async () => {
try {
const result = await Promise.race([
printHello(),
printName(),
printWorld(),
printError()
]);
console.log(result);
} catch (error) {
console.error(error);
}
};
makeProcessWithError(); //Hello
위의 코드를 보면 에러를 발생시키는 비동기 함수가 printHello
함수보다 늦게 처리되기 때문에 에러가 아닌 Hello
가 출력되는 것을 볼 수 있다.
async-await
async-await
문법은 ES8
에서 새롭게 도입된 문법으로 Promise
를 좀 더 편하게 다룰 수 있게 해준다. 또한 try-catch
구문을 사용할 수 있기 때문에 에러 처리도 좀 더 간편하게 할 수 있다.
const printNumber = number => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const value = number + 1;
if (value === 4) {
const error = new Error("This is Error!");
reject(error);
return;
}
console.log(value);
resolve(value);
}, 1000);
});
};
const makeProcess = async () => {
try {
const num1 = await printNumber(0);
const num2 = await printNumber(num1);
const num3 = await printNumber(num2);
const num4 = await printNumber(num3);
} catch (error) {
console.error(error);
}
};
makeProcess();
🙏 Reference
- MDN Docs - Promise
- Captain Pangyo - 자바스크립트 Promise 쉽게 이해하기
- bono's blog - [javascript] async, await를 사용하여 비동기 javascript를 동기식으로 만들자
- MDN Docs - async function
- Im-D/Dev-Docs - Promise1
- Im-D/Dev-Docs - Promise2
- Im-D/Dev-Docs - Promise Pattern
Iteration Protocol
Iteration Protocol
은 ES6에서 도입되었다. 이는 새로운 구문이 아닌 하나의 프로토콜, 규약이다. 즉, 같은 규칙을 준수하는 객체에 의해 구현될 수 있다. Iteration Protocol
에는 Iterable Protocol 과 Iterator Protocol 이 있다.
Iterable
Iterable
은 Iterable Protocol
을 준수한 객체다. Iterable
은 반복 가능한 객체이며 Symbol.iterator
메서드를 구현하거나 프로토타입 체인에 의해 상속한 객체를 말한다. Iterable
은 for...of
문에서 반복 가능하며 Spread Operator의 대상이 될 수 있다.
Iterable
은 Symbol.iterator
을 가지기 때문에 해당 메서드가 없는 객체는 Iterable 객체가 아니다.
Iterable 객체로는 내장 객체인 Array
, Map
, Set
, String
등이 있다.
const iterator = [1, 2, 3][Symbol.iterator]();
iterator.next().value; // 1
iterator.next().value; // 2
iterator.next().value; // 3
iterator.next().done; // true
반면, 일반 객체는 Symbol.iterator
메서드를 가지고 있지 않다. 따라서 일반 객체는 Iterable 객체가 아니다.
const obj = { a: 1, b: 2 };
console.log(Symbol.iterator in obj); // false
// TypeError: obj is not iterable
for (const item of obj) {
console.log(item);
}
하지만 일반 객체도 Iterable Protocol
을 준수하도록 구현하면 Iterable 객체가 될 수 있다.
const iterableObj = function(max) {
let i = 0;
return {
[Symbol.iterator]() {
return {
next() {
return {
value: ++i,
done: i === max
};
}
};
}
};
};
const iterator = iterableObj(10);
for (let item of iterator) {
console.log(item);
}
Iterator
Iterator Protocol
은 next
메서드를 가진다. next
메소드를 호출하면 Iterable 객체를 순회하며 value
, done
프로퍼티를 갖는 Iterator Reuslt 객체를 반환한다. 이 규약을 준수한 객체가 Iterator 객체다.
위에서 잠깐 봤던 코드의 일부분을 다시 한 번 살펴보자.
[Symbol.iterator]() {
return {
next() {
return {
value: ++i,
done: i === max
};
}
};
}
Symbol.iterator
메서드를 실행하면 next
메서드를 가진 객체를 반환하고 next
메서드를 실행하면 value
, done
프로퍼티를 가진 객체를 반환한다. 그렇다.
Iterable 객체가 가진 Symbol.iterator
메서드를 실행하면 Iterator 객체를 반환한다.
const array = [1, 2, 3];
const iterator = array[Symbol.iterator]();
console.log("next" in iterator); // true
iterator.next().value; // 1
iterator.next().value; // 2
iterator.next().value; // 3
iterator.next(); // { value: undefined, done: true }
Iterator 객체의 next
메서드가 반환하는 Iterator Result 객체의 value
프로퍼티는 Iterable 객체의 값을 반환하고 done
프로퍼티는 Iterable 객체의 반복 완료 여부를 반환한다.
Iteration Protocol이 왜 필요할까?
for...of
, Spread Operator
, Destructuring
, Map/Set constructor
등을 데이터 소비자(Data Consumer) 라고 한다.
반면, 배열
, 문자열
, Map
, Set
, DOM Data Structure
등과 같은 Iterable 객체는 데이터 공급자(Data Provider) 라고 한다.
만약, 위와 같은 Data Provider인 Iterable 객체들이 각각 다른 방식의 순회 방식을 갖는다면 어떨까? 당연히 효율적이지 못하다.
순회 방식에 대한 하나의 규약을 정해놓고 사용한다면 Data Consumer가 여러 구조의 Iterable을 효율적으로 사용할 수 있을 것이다. 즉, Iteration Protocol은 Data Consumer와 Data Provider를 연결하는 인터페이스 역할을 해주기 때문에 필요하다고 볼 수 있다.
🙏 Reference
- Poiemaweb - 이터레이션과 for...of 문
- wonism.github.io - JavaScript iterables와 iterator 이해하기
- MDN Docs - Iteration protocols
Generator
Generator
는 ES6
에서 도입되었으며 Iterable
을 생성하는 함수다. Generator
를 사용하면 Iteration Protocol
을 사용하여 Iterable
을 생성하는 방식보다 간편하다.
Iteration Protocol
에 대한 자세한 내용은 다음을 참고하길 바란다.
Generator 함수의 정의
Generator
함수는 코드 블록을 한 번에 실행하지 않고 함수 코드 블록의 실행을 중지했다가 필요한 시점에 다시 시작할 수 있는 함수다.
Generator
함수는 function *
키워드를 사용하며 하나 이상의 yield
문을 포함한다.
// 함수 선언문
function* decFunc() {
yield 1;
}
let genObj = decFunc();
// 함수 표현식
const expFunc = function*() {
yield 1;
};
genObj = expFunc();
// 메서드
const obj = {
*objectMethod() {
yield 1;
}
};
genObj = obj.objectMethod();
// 클래스 메서드
class GenClass {
*classMethod() {
yield 1;
}
}
const genClass = new GenClass();
genObj = genClass.classMethod();
Generator 객체
Generator
함수를 호출하면 코드 블록이 실행되는 것이 아니라 Generator
객체를 반환한다. Generator
객체는 이터러블이면서 동시에 이터레이터다. 따라서 Symbol.iterator
를 사용하여 이터레이터를 생성할 필요 없다.
function* counter() {
console.log("First");
yield 1;
console.log("Second");
yield 2;
console.log("Third");
yield 3;
console.log("The end");
}
const genObj = counter();
console.log(genObj.next()); //{value: 1, done: false}
console.log(genObj.next()); //{value: 2, done: false}
console.log(genObj.next()); //{value: 3, done: false}
console.log(genObj.next()); //{value: undefined, done: true}
Generator
객체는 이터러블이면서 이터레이터이기 때문에 next()
메서드를 가지고 있다. 따라서 next()
메서드를 호출하면 yield
문까지 실행되고 일시 중지된다. 다시 next()
메서드를 호출하면 다음 yield
문을 만날 때까지 실행된 뒤 일시 중지된다.
Generator 객체를 이용한 이터러블 구현
const genObj = (function*() {
let i = 0;
while (true) {
yield ++i;
}
})();
for (let item of genObj) {
if (item === 10) break;
console.log(item);
}
// Generator 함수에 파라미터 전달
const genObj = function*(max) {
let i = 0;
while (true) {
if (i === max) break;
yield ++i;
}
};
for (let item of genObj(10)) {
console.log(item);
}
// next 메서드에 파라미터 전달
function* genFunc(n) {
let res;
res = yield n;
console.log(res);
res = yield res;
console.log(res);
res = yield res;
console.log(res);
return res;
}
const genObj = genFunc(0);
console.log(genObj.next());
console.log(genObj.next(1));
console.log(genObj.next(2));
console.log(genObj.next(3));
Generator를 이용한 비동기 처리
Generator
의 진면목은 비동기 프로그래밍에서 볼 수 있다. 함수가 실행 도중에 멈춘다니. 언제 응답이 올지 알 수 없기 때문에, callback을 등록하는 비동기 프로그래밍에 응용하면 callback hell을 탈출할 수 있지 않을까?
function getId(phoneNumber) {
// …
iterator.next(result);
}
function getEmail(id) {
// …
iterator.next(result);
}
function getName(email) {
// …
iterator.next(result);
}
function order(name, menu) {
// …
iterator.next(result);
}
function* orderCoffee(phoneNumber) {
const id = yield getId(phoneNumber);
const email = yield getEmail(id);
const name = yield getName(email);
const result = yield order(name, "coffee");
return result;
}
const iterator = orderCoffee("010-1234-1234");
iterator.next();
Generator는 어떻게 구현되어 있을까?
// ES6
function* foo() {
yield bar();
}
// ES5 Compiled
("use strict");
var _marked = /*#__PURE__*/ regeneratorRuntime.mark(foo);
function foo() {
return regeneratorRuntime.wrap(
function foo$(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
case 0:
_context.next = 2;
return bar();
case 2:
case "end":
return _context.stop();
}
}
},
_marked,
this
);
}
Genrator
는 결국 iterable Protocol
를 구현하는 객체이다. 그러나 프로토콜과 관련된 어느것도 보이지 않는다.
대신 regeneratorRuntime
이 보인다.
babel
에서는 regeneratorRuntime
라이브러리를 사용해서 구현을 했다.
코드의 역사를 따라가다 보면 facebook/regenerator repository에 도달하게 된다.
이 라이브러리는 2013년 Node.js v0.11.2에서 generator syntax를 지원하기 위해 만들어 졌으며, Babel에서는 이 라이브러리를 사용하여 generator를 구현하고 있다. 실제 코드를 들여다보면 Symbol과 Iterator를 이용해서 Iterable Protocol을 구현하고 있다.
🙏 Reference
이벤트 루프(Event Loop)
자바스크립트는 싱글쓰레드 기반이다.
자바스크립트를 공부해본 개발자라면 한 번쯤은 자바스크립트는 싱글 쓰레드 기반의 언어다.
라는 말을 들어봤을 것이다. 하지만 우리는 실제 웹 애플리케이션에서 여러 개의 작업이 동시에 처리되는 것처럼(비동기적) 느끼는 일이 더 많다. 싱글 쓰레드 기반의 언어에서 즉, 한 번에 하나의 작업만 처리가능한 환경에서 어떻게 많은 작업이 동시에 처리되는 것처럼 느낄 수 있을까? 그 답은 이벤트 루프에 있다.
브라우저 환경을 간단히 표현하면 다음 이미지와 같다.
우선, 위의 그림에서 보여지는 각각에 대해서 살펴본 후, 전체적으로 이벤트 루프가 동작하는 방식을 살펴보도록 하자.
자바스크립트 엔진
Heap
동적으로 생성된 객체 인스턴스는 Heap에 할당이 된다. Heap은 메모리에서 대부분 구조화되지 않은 영역을 나타낸다.
Call Stack(호출 스택)
호출 스택은 이름 그대로 Stack
이며 LIFO(Last-In-First-Out)구조를 갖는다. 함수를 호출하면(작업을 요청하면) 작업은 순차적으로 호출 스택에 쌓이고 실행된다. 자바스크립트 엔진은 하나의 스택만 가지고 있기 때문에 하나의 작업이 끝나기 전까지 다른 작업을 수행할 수 없다.
Web APIs
흔히 WebAPI라 불리는 API들은 실행환경에 내장되어 있다.
이것은 자바스크립트에 포함되는 것이 아니다. 즉, 우리는 Web API의 내부는 조작할 수 없으며 호출만 가능하다. 또한 자바스크립트 언어를 사용하는데 있어 강력한 성능을 제공한다.
Web API의 종류는 다음을 참조하면 알 수 있다.
콜백함수
자바스크립트의 싱글 쓰레드 구조에서 비동기성의 이벤트 기반 실행(대표적으로 setTimeout
)이나 ajax
요청이 필요하다면, 콜백 함수를 큐로 보내고 큐에서는 호출 스택으로 보내 해결하게 된다.
자바스크립트에서는 쓰레드를 통해 병렬처리가 안되기 때문에 콜백 함수의 사용은 필수불가결하게 되는 것이다.
Event Queue(이벤트 큐)
이벤트 큐는 말 그대로 콜백 함수들이 대기하는 Queue
이며 FIFO(First-In-First-Out)
의 구조를 갖는다. 이벤트 루프는 호출 스택이 비워질 때마다 큐에서 콜백 함수를 호출 스택에 넣어주는 역할을 해준다.
이벤트 루프를 통한 비동기적 처리
이벤트 루프의 역할은 생각보다 단순한다. 호출 스택에 실행 중인 작업이 있는지, 이벤트 큐에 대기 중인 작업이 있는지 반복해서 확인한다. 만약 호출 스택이 비어있다면 이벤트 큐에 있는 작업을 호출 스택으로 옮긴다. 그리고 이 작업을 수행하는 것은 결국 호출 스택이다.
function func1() {
console.log("func1");
func2();
}
function func2() {
setTimeout(function() {
console.log("func2");
}, 0);
func3();
}
function func3() {
console.log("func3");
}
func1();
위 예제는 이벤트 루프를 설명할 때 가장 많이 사용되는 예제다. 만약, 이벤트 루프가 수행하는 과정이 없고 순차적으로 호출 스택에만 쌓이게 된다면 func1
, func2
, func3
의 순서로 출력될 것이다. 하지만 실제로 위 코드를 실행해보면 func1
, func3
, func2
의 순서로 출력되는 것을 볼 수 있을 것이다. 이런 결과가 나오는 이유는 위에서 설명한 것 처럼 이벤트 큐와 이벤트 루프를 통해 비동기 처리를 수행하는 setTimeout
함수가 다른 함수들과 다르게 동작하기 때문이다.
아래 이미지는 위 코드가 실행되는 과정을 보여준다.
이미지 출처: https://poiemaweb.com/js-event
위 과정을 순차적으로 정리하면 다음과 같다.
func1
함수가 호출되고 이는 호출 스택에 올라가고console.log('func1')
이 실행된다.func2
함수가 호출 스택에 올라가고setTimout
함수를 호출한다.- 호출된
setTimeout
함수의 수행은 비동기적 처리를 수행하는 Web API에 넘어간다.func3
함수가 호출 스택에 올라가고console.log('func3')
이 실행된다.- Web API에서
setTimout
함수에서 지정한 시간이 지나면callback
함수를 이벤트 큐로 넘긴다.- 작업이 끝난
func3
,func2
,func1
은 순차적으로 호출 스택에서 제거된다.- 이벤트 루프는 호출 스택에 작업 중인 태스크가 없는 것을 확인하고 이벤트 큐에 있는
callback
함수를 호출 스택으로 올린다.- 호출 스택에 올라간
callback
함수가 실행되면서console.log('func3')
가 실행된다.
위 설명에서 주의 깊게 볼 것은 비동기 함수인 setTimeout
함수에 세팅된 시간이 3초라면 3초 후에 콜백 함수를 실행시켜라
가 아닌 3초 후에 콜백 함수를 이벤트 큐에 넣어라
가 된다는 것이다.
즉, setTimeout
함수는 n초 뒤에 콜백을 단순히 큐에 집어넣는게 끝이다. 코드를 간단히 보자면 아래와 같다.
var eventLoop = [];
var event;
while (true) {
// 틱!
if (eventLoop.length > 0) {
event = eventLoop.shift();
}
try {
event(); // 호출스택으로 밀어넣는다
} catch (err) {
//...
}
}
이 큐에 이미 대기번호가 100개가 있다면 func3
는 101번째 대기표를 받게 될 것이다. 따라서 setTimeout
은 지정한 시간동안은 실행되지 않는 것은 보장할 수 있지만 지정한 시간에 실행되는것은 보장할 수 없다.
while (await messageQueue.nextMessage()) {
let message = messageQueue.shift();
message.run();
}
결론적으로, 이벤트 루프는 메시지 큐에 메시지가 더 있는지 확인하는 루프이다.
메시지 큐에 메시지가 있으면 메시지 큐에서 다음 메시지를 제거하고 그 메시지와 연관된 기능을 호출 스택으로 보낸다. 그렇지 않으면 새 메시지가 메시지 대기열에 추가될 때까지 대기를 한다. 이벤트 루프가 자바스크립트에게 비동기를 허용하는 기본 모델이다.
ES6이후의 변화된 비동기 처리와 이벤트 루프
기본적으로 이벤트 루프는 위에서 설명한 내용이 큰 틀이다. 큐와 스택을 감시하며 스택의 작업이 없으면 큐의 작업을 스택에 올린다. 다만, ES6
이후에는 몇 가지 비동기적 작업을 수행하는 API들이 추가되었고 이에 따라 약간의 추가된 내용이 있다. 하지만, 전체적인 실행 방식은 동일하며 각각의 비동기 처리에 수행 순서에 초점을 두고 살펴보자.
기존에 살펴보았던 이벤트 큐(Event Queue)를 좀 더 자세히 나눠보면 다음과 같다.
Task Queue
: 가장 사람들이 잘 알고 있는 비동기 작업인setTimeout
이 들어가는 큐Micro Task Queue
: ES6에서 추가된Promise
와 ES8의Async Await
(Async Await도 결국 Promise)AnimationFrame
:requestAnimationFrame(rAF)
의 콜백 함수가 들어간다.
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
requestAnimationFrame(function {
console.log("requestAnimationFrame");
})
console.log("script end");
위의 코드를 실행하면 다음과 같은 결과가 출력된다.
script start
script end
promise1
promise2
requestAnimationFrame
setTimeout
즉, 이벤트 큐에서 나눠지는 3가지 영역의 우선 순위는 다음과 같다.
Micro Task Queue => AnimationFrame => Task Queue
기존에 이벤트 루프에 대해서 이해가 된 상태라면 이 내용은 크게 어렵지 않다. 쉽게 보면 비동기 작업을 처리하는 방법이 추가되었고 이에 따라 이벤트 큐에서 내부적으로 처리하는 로직에 약간의 변화가 생겼을 뿐이다. 결국, 정리하면 다음과 같다.
-
비동기 작업으로 등록되는 작업은
Task
와Micro Task
, 그리고AnimationFrame
으로 구분된다. -
Micro Task
는Task
보다 먼저 처리된다. -
Micro Task
가 처리된 이후requestAnimationFrame
이 호출되고 이후 브라우저 랜더링이 발생한다.
🙏 Reference
- Im-D/Dev-Docs
- JavaScript Event Loop Explained
- What is the Event Loop in Javascript
- Understanding JS: The Event Loop
- Event loop in javascript
- The JavaScript Event Loop
- Tasks, microtasks, queues and schedules
- Poiemaweb - 자바스크립트/이벤트
이벤트 루프(Event Loop)
자바스크립트는 싱글쓰레드 기반이다.
자바스크립트를 공부해본 개발자라면 한 번쯤은 자바스크립트는 싱글 쓰레드 기반의 언어다.
라는 말을 들어봤을 것이다. 하지만 우리는 실제 웹 애플리케이션에서 여러 개의 작업이 동시에 처리되는 것처럼(비동기적) 느끼는 일이 더 많다. 싱글 쓰레드 기반의 언어에서 즉, 한 번에 하나의 작업만 처리가능한 환경에서 어떻게 많은 작업이 동시에 처리되는 것처럼 느낄 수 있을까? 그 답은 이벤트 루프에 있다.
브라우저 환경을 간단히 표현하면 다음 이미지와 같다.
우선, 위의 그림에서 보여지는 각각에 대해서 살펴본 후, 전체적으로 이벤트 루프가 동작하는 방식을 살펴보도록 하자.
자바스크립트 엔진
Heap
동적으로 생성된 객체 인스턴스는 Heap에 할당이 된다. Heap은 메모리에서 대부분 구조화되지 않은 영역을 나타낸다.
Call Stack(호출 스택)
호출 스택은 이름 그대로 Stack
이며 LIFO(Last-In-First-Out)구조를 갖는다. 함수를 호출하면(작업을 요청하면) 작업은 순차적으로 호출 스택에 쌓이고 실행된다. 자바스크립트 엔진은 하나의 스택만 가지고 있기 때문에 하나의 작업이 끝나기 전까지 다른 작업을 수행할 수 없다.
Web APIs
흔히 WebAPI라 불리는 API들은 실행환경에 내장되어 있다.
이것은 자바스크립트에 포함되는 것이 아니다. 즉, 우리는 Web API의 내부는 조작할 수 없으며 호출만 가능하다. 또한 자바스크립트 언어를 사용하는데 있어 강력한 성능을 제공한다.
Web API의 종류는 다음을 참조하면 알 수 있다.
콜백함수
자바스크립트의 싱글 쓰레드 구조에서 비동기성의 이벤트 기반 실행(대표적으로 setTimeout
)이나 ajax
요청이 필요하다면, 콜백 함수를 큐로 보내고 큐에서는 호출 스택으로 보내 해결하게 된다.
자바스크립트에서는 쓰레드를 통해 병렬처리가 안되기 때문에 콜백 함수의 사용은 필수불가결하게 되는 것이다.
Event Queue(이벤트 큐)
이벤트 큐는 말 그대로 콜백 함수들이 대기하는 Queue
이며 FIFO(First-In-First-Out)
의 구조를 갖는다. 이벤트 루프는 호출 스택이 비워질 때마다 큐에서 콜백 함수를 호출 스택에 넣어주는 역할을 해준다.
이벤트 루프를 통한 비동기적 처리
이벤트 루프의 역할은 생각보다 단순한다. 호출 스택에 실행 중인 작업이 있는지, 이벤트 큐에 대기 중인 작업이 있는지 반복해서 확인한다. 만약 호출 스택이 비어있다면 이벤트 큐에 있는 작업을 호출 스택으로 옮긴다. 그리고 이 작업을 수행하는 것은 결국 호출 스택이다.
function func1() {
console.log("func1");
func2();
}
function func2() {
setTimeout(function() {
console.log("func2");
}, 0);
func3();
}
function func3() {
console.log("func3");
}
func1();
위 예제는 이벤트 루프를 설명할 때 가장 많이 사용되는 예제다. 만약, 이벤트 루프가 수행하는 과정이 없고 순차적으로 호출 스택에만 쌓이게 된다면 func1
, func2
, func3
의 순서로 출력될 것이다. 하지만 실제로 위 코드를 실행해보면 func1
, func3
, func2
의 순서로 출력되는 것을 볼 수 있을 것이다. 이런 결과가 나오는 이유는 위에서 설명한 것 처럼 이벤트 큐와 이벤트 루프를 통해 비동기 처리를 수행하는 setTimeout
함수가 다른 함수들과 다르게 동작하기 때문이다.
아래 이미지는 위 코드가 실행되는 과정을 보여준다.
이미지 출처: https://poiemaweb.com/js-event
위 과정을 순차적으로 정리하면 다음과 같다.
func1
함수가 호출되고 이는 호출 스택에 올라가고console.log('func1')
이 실행된다.func2
함수가 호출 스택에 올라가고setTimout
함수를 호출한다.- 호출된
setTimeout
함수의 수행은 비동기적 처리를 수행하는 Web API에 넘어간다.func3
함수가 호출 스택에 올라가고console.log('func3')
이 실행된다.- Web API에서
setTimout
함수에서 지정한 시간이 지나면callback
함수를 이벤트 큐로 넘긴다.- 작업이 끝난
func3
,func2
,func1
은 순차적으로 호출 스택에서 제거된다.- 이벤트 루프는 호출 스택에 작업 중인 태스크가 없는 것을 확인하고 이벤트 큐에 있는
callback
함수를 호출 스택으로 올린다.- 호출 스택에 올라간
callback
함수가 실행되면서console.log('func3')
가 실행된다.
위 설명에서 주의 깊게 볼 것은 비동기 함수인 setTimeout
함수에 세팅된 시간이 3초라면 3초 후에 콜백 함수를 실행시켜라
가 아닌 3초 후에 콜백 함수를 이벤트 큐에 넣어라
가 된다는 것이다.
즉, setTimeout
함수는 n초 뒤에 콜백을 단순히 큐에 집어넣는게 끝이다. 코드를 간단히 보자면 아래와 같다.
var eventLoop = [];
var event;
while (true) {
// 틱!
if (eventLoop.length > 0) {
event = eventLoop.shift();
}
try {
event(); // 호출스택으로 밀어넣는다
} catch (err) {
//...
}
}
이 큐에 이미 대기번호가 100개가 있다면 func3
는 101번째 대기표를 받게 될 것이다. 따라서 setTimeout
은 지정한 시간동안은 실행되지 않는 것은 보장할 수 있지만 지정한 시간에 실행되는것은 보장할 수 없다.
while (await messageQueue.nextMessage()) {
let message = messageQueue.shift();
message.run();
}
결론적으로, 이벤트 루프는 메시지 큐에 메시지가 더 있는지 확인하는 루프이다.
메시지 큐에 메시지가 있으면 메시지 큐에서 다음 메시지를 제거하고 그 메시지와 연관된 기능을 호출 스택으로 보낸다. 그렇지 않으면 새 메시지가 메시지 대기열에 추가될 때까지 대기를 한다. 이벤트 루프가 자바스크립트에게 비동기를 허용하는 기본 모델이다.
ES6이후의 변화된 비동기 처리와 이벤트 루프
기본적으로 이벤트 루프는 위에서 설명한 내용이 큰 틀이다. 큐와 스택을 감시하며 스택의 작업이 없으면 큐의 작업을 스택에 올린다. 다만, ES6
이후에는 몇 가지 비동기적 작업을 수행하는 API들이 추가되었고 이에 따라 약간의 추가된 내용이 있다. 하지만, 전체적인 실행 방식은 동일하며 각각의 비동기 처리에 수행 순서에 초점을 두고 살펴보자.
기존에 살펴보았던 이벤트 큐(Event Queue)를 좀 더 자세히 나눠보면 다음과 같다.
Task Queue
: 가장 사람들이 잘 알고 있는 비동기 작업인setTimeout
이 들어가는 큐Micro Task Queue
: ES6에서 추가된Promise
와 ES8의Async Await
(Async Await도 결국 Promise)AnimationFrame
:requestAnimationFrame(rAF)
의 콜백 함수가 들어간다.
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise1");
}).then(function() {
console.log("promise2");
});
requestAnimationFrame(function {
console.log("requestAnimationFrame");
})
console.log("script end");
위의 코드를 실행하면 다음과 같은 결과가 출력된다.
script start
script end
promise1
promise2
requestAnimationFrame
setTimeout
즉, 이벤트 큐에서 나눠지는 3가지 영역의 우선 순위는 다음과 같다.
Micro Task Queue => AnimationFrame => Task Queue
기존에 이벤트 루프에 대해서 이해가 된 상태라면 이 내용은 크게 어렵지 않다. 쉽게 보면 비동기 작업을 처리하는 방법이 추가되었고 이에 따라 이벤트 큐에서 내부적으로 처리하는 로직에 약간의 변화가 생겼을 뿐이다. 결국, 정리하면 다음과 같다.
-
비동기 작업으로 등록되는 작업은
Task
와Micro Task
, 그리고AnimationFrame
으로 구분된다. -
Micro Task
는Task
보다 먼저 처리된다. -
Micro Task
가 처리된 이후requestAnimationFrame
이 호출되고 이후 브라우저 랜더링이 발생한다.
🙏 Reference
- Im-D/Dev-Docs
- JavaScript Event Loop Explained
- What is the Event Loop in Javascript
- Understanding JS: The Event Loop
- Event loop in javascript
- The JavaScript Event Loop
- Tasks, microtasks, queues and schedules
- Poiemaweb - 자바스크립트/이벤트
repaint와 reflow
위의 그림과 같이 브라우저는 화면을 rendering하는 과정에서 배치(flow) 와 그리기(paint) 의 과정을 거친다.
생성된 DOM 노드의 레이아웃이 변경될 떄, 변경 후 영향을 받는 모든 노드를 다시 계산하고 렌더 트리를 재생성 한다.
이러한 과정을 reflow
라 하고 reflow
가 일어난 후, repaint
가 일어난다.
즉, DOM의 노드가 변경될 때 마다 DOM tree라는 자료구조에 접근해야 하기 때문에 DOM의 레이아웃을 변경하는 코드를 작성할 때는 이를 최적화하기 위한 고민이 필요하다.
reflow
function reFlow() {
var container = document.getElementById('container');
container.appendChild(document.createTextNode('hello'));
}
위의 코드를 보면 conatiner
라는 엘리먼트에 hello
라는 TextNode
를 추가했다. 이로 인해 DOM 노드의 레이아웃이 바뀌며 reflow
와 repaint
가 일어날 것이다.
repaint
function repaint() {
var container = document.getElementById('container');
container.style.backgroundColor = 'black';
container.style.color = 'white';
}
위의 코드에서는 이전의 코드와 다르게 엘리먼트의 style
만 변경했다. 이러한 경우 DOM 노드의 레이아웃은 변경되지 않았고 style
속성만 변경되었기 때문에 reflow
는 일어나지 않고 repaint
만 일어나게 된다.
reflow
와 repaint
가 많아질수록 애플리케이션의 렌더링 성능은 느려지게 되기 때문에 이를 줄일 수록 성능을 높일 수 있다.
🙏 Reference
네임스페이스 패턴(Namespace Pattern)과 IIFE
전역 변수를 많이 쓰면 안좋다고 흔히들 말한다. 그 이유를 알 수 있는 간단한 예를 보자.
var x = 100;
function test() {
x = 10;
console.log('10이 나오겠지?', x);
}
test();
console.log('100을 출력해볼까 ?', x);
/*
10이 나오겠지? 10
100을 출력해볼까 ? 10
*/
지역 스코프에서 전역변수를 참조할 수 있으므로 전역변수의 값도 변경할 수 있다. 내부 함수의 경우에는, 전역변수는 물론 상위 함수에서 선언한 변수에 접근/변경이 가능하다.
프로젝트가 클수록, 협업이 이루어질수록 전역 변수가 많아지면 원하는 결과가 아닌 다른 결과가 나타날 수 있다.
네임스페이스 패턴(Namespace Pattern)
네임스페이스 패턴은 말 그대로 이름 공간을 선언하여 다른 공간과 구분하는 패턴이라고 보면 된다.
var APP_GLOBAL = {
name : 'BKJang',
age : '25',
getInfo : function() {
console.log('name :', this.name, 'age :', this.age);
}
}
console.log(APP_GLOBAL); //{name: "BKJang", age: "25", getInfo: ƒ}
console.log(APP_GLOBAL.name, APP_GLOBAL.age); //BKJang 25
APP_GLOBAL.getInfo(); //name : BKJang age : 25
이처럼 전역 변수 사용을 위해 전역 객체 하나를 만들어 사용하는 것이다.
즉시 실행 함수 표현식(IIFE, Immediately-Invoked Function Expression)
즉시 실행 함수를 사용하면 함수가 실행되고 전역에서 사라진다. 이 방법으로 라이브러리를 많이 만들곤 한다.
(function moduleFunction() {
var a = 3;
function helloWorld(){
console.log('Hello');
}
helloWorld(); //Hello
})();
helloWorld(); //Uncaught ReferenceError: helloWorld is not defined
즉시실행함수가 실행되고 전역에서 사라지기 때문에 그 밖에선 출력 값이 에러가 발생하는 것을 볼 수 있다.
var singleton = (function () {
var a = 3;
function helloWorld(){
console.log('Hello');
}
return {
a : a,
sayHello: helloWorld
}
})();
singleton.sayHello(); //Hello
console.log(singleton.a); //3
위와 같이 반환 값을 변수에 담아 전역 변수를 한 번 만 사용하여 전역 변수의 사용을 줄일 수도 있다.
🙏 Reference
모듈 패턴(Module Pattern)
var Developer = function(arg) {
var lang = arg ? arg : '';
return {
getLang : function() {
return lang;
},
setLang : function(arg) {
lang = arg;
}
}
};
var bkjang = new Developer('javascript');
console.log(bkjang.getLang()); //javascript
bkjang.setLang('java');
console.log(bkjang.getLang()); //java
위의 코드를 보면 Developer 생성자 함수에서 this
가 아닌 var lang = arg ? arg : '';
으로 선언하면 자바스크립트는 함수형 스코프를 따르기 때문에 private해진다.
그리고 getLang()
과 setLang()
이라는 함수는 클로저이기 때문에 외부에서는 lang
이라는 변수의 값에 접근할 수 있는 인터페이스가 된다.
위와 같이 getLang()
과 setLang()
과 같은 public 메서드를 인터페이스로 제공하고 lang
과 같은 private한 변수에 인터페이스를 통해서만 접근하도록 하는 것이 모듈 패턴이다.
그렇다면 private 멤버 변수가 객체나 배열일 경우는 어떻게 될까?
var Developer = function (obj) {
var developerInfo = obj;
return {
getDeveloperInfo: function() {
return developerInfo;
}
};
};
var developer = new Developer({ name: 'BKJang', lang: 'javascript' });
var bkJang = developer.getDeveloperInfo();
console.log('bkJang: ', bkJang);
// bkJang: {name: "BKJang", lang: "javascript"}
bkJang.lang = 'java'; //인터페이스가 아닌 직접 변경
bkJang = developer.getDeveloperInfo();
console.log('bkJang: ', bkJang);
// bkJang: {name: "BKJang", lang: "java"}
console.log(Developer.prototype === bkJang.__proto__); //false
일반 변수가 아닌 객체나 배열을 멤버 변수로 가지고 이를 그대로 반환할 경우, 외부에서 이 멤버를 변경할 수 있다.
왜냐하면, 객체나 배열을 반환하는 경우는 얕은 복사(shallow copy)로 private 멤버의 참조값을 반환하게 된다.
따라서, 반환할 객체나 배열의 정보를 담은 새로운 객체를 만들어 깊은 복사(deep copy)를 거친 후 반환해야 한다.
또한, 위처럼 일반 객체를 반환하면 프로토타입 객체는 Object.prototype
객체가 되기 때문에 상속을 구현할 수 없다. 따라서 함수를 반환해야 한다.
var Developer = (function() {
var lang;
//생성자 정의
function Developer(arg) {
lang = arg ? arg : '';
}
Developer.prototype = {
getLang : function() {
return lang;
},
setLang : function(arg) {
lang = arg;
}
}
return Developer;
}());
var bkJang = new Developer('javscript');
console.log(bkJang.getLang()); //javscript
bkJang.lang = 'java'; //인터페이스를 통해서가 아닌 직접 변경
console.log(bkJang.getLang()); //javscript
bkJang.setLang('java');
console.log(bkJang.getLang()); //java
console.log(Developer.prototype === bkJang.__proto__); //true
마지막 출력 값을 보면 인스턴스인 bkJang
의 프로토타입 객체가 Developer.prototype
객체임을 알 수 있고 이는 상속을 구현할 수 있음을 의미한다.
🙏 Reference
- 인사이드 자바스크립트 (송형주, 고형준)
- 자바스크립트 객체지향 프로그래밍
- [JavaScript] 8-3. 객체지향 프로그래밍(캡슐화)
렉시컬 스코프(Lexical Scope)
자바스크립트는 렉시컬 스코프를 지원한다.
우선적으로, 자바스크립트 엔진에서 코드를 컴파일 하는 과정을 보면 다음과 같다.
- 토크나이징/렉싱 - 코드를 잘게 나누어 토큰으로 만든다.
- 파싱 - 나눈 토큰을 의미있게 AST(Abstract Syntax Tree)라는 트리로 만든다.
- 코드생성 - AST를 기계어로 만든다.
렉시컬 스코프란 1단계에서 발생하는 즉, 렉싱 과정에서 정의되는 스코프를 말한다. 프로그래머가 변수와 스코프 블록을 어떻게 구성하는냐에 따라 렉싱 타임에서 정의되는 스코프를 렉시컬 스코프라고 한다.
var x = 'global'
function test1() {
var x = 'local';
test2();
}
function test2() {
console.log(x);
}
test1(); //global
test2(); //global
위의 코드에서 test2()
함수를 어디서 호출하는지가 아닌 어디에 선언되어있냐에 집중할 필요가 있다.
즉, test2()
함수의 상위 스코프는 test1()과 전역이 아닌 전역이다. 이에 따라 test2()
에서 출력한 값은 global
이 나올 것이다.
쉽게 말하면, 렉시컬 스코프는 함수를 어디서 호출하는지가 아닌 어디서 선언했는지에 따라 결정된다. 이러한 특성때문에 정적 스코프(Static Scope) 라고도 한다.
🙏 Reference
Deep Copy
use JSON.parse
, JSON.stringify
function changeAgePure(person) {
var newPersonObj = JSON.parse(JSON.stringify(person));
newPersonObj.age = 25;
return newPersonObj;
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
or use $.extend()
function changeAgePure(person) {
var newPersonObj = $.extend(true, {}, person);
newPersonObj.age = 25;
return newPersonObj
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
or use destructuring
(ES6)
function changeAgePure(person) {
var newPersonObj = { ...person };
newPersonObj.age = 25;
return newPersonObj
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
🙏 Reference
Deep Copy
use JSON.parse
, JSON.stringify
function changeAgePure(person) {
var newPersonObj = JSON.parse(JSON.stringify(person));
newPersonObj.age = 25;
return newPersonObj;
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
or use $.extend()
function changeAgePure(person) {
var newPersonObj = $.extend(true, {}, person);
newPersonObj.age = 25;
return newPersonObj
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
or use destructuring
(ES6)
function changeAgePure(person) {
var newPersonObj = { ...person };
newPersonObj.age = 25;
return newPersonObj
}
var alex = {
name: 'Alex',
age: 30
};
var alexChanged = changeAgePure(alex);
console.log(alex); // -> { name: 'Alex', age: 30 }
console.log(alexChanged); // -> { name: 'Alex', age: 25 }
🙏 Reference
이벤트 위임(Event Delegation)
이벤트 위임의 이해에는 이벤트 버블링과 이벤트 캡쳐에 대한 이해가 수반된다.
우선 그림으로 보면, 이벤트 버블링과 이벤트 캡쳐의 개념은 다음과 같다.
이벤트 버블링(Event Bubbling)
<body>
<div class="el1">
<div class="el2">
<div class="el3">
</div>
</div>
</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
div.addEventListener('click', bubbleEvent);
});
function bubbleEvent(e) {
console.log(e.target.className);
}
/*
el3
el2
el1
*/
위의 코드에서는 class 명이 el3인 element(<div class="el3"></div>
)을 클릭했을 때, 이벤트가 발생하는 요소인 <div class="el3"></div>
에서 상위에 있는 요소까지 이벤트를 전파시키고 있다.
이에 따라 결과 값은 el3만 나오는 것이 아닌 el1까지 콘솔에 출력되고 있다.
이처럼 이벤트가 발생한 요소로부터 상위요소로 전파시키는 이벤트 전파 방식을 이벤트 버블링이라고 한다.
이벤트 캡쳐(Event Capture)
<body>
<div class="el1">
<div class="el2">
<div class="el3">
</div>
</div>
</div>
</body>
var divs = document.querySelectorAll('div');
divs.forEach(function(div) {
div.addEventListener('click', captureEvent, {
capture : true
});
});
function captureEvent(e) {
console.log(e.target.className);
}
/*
el1
el2
el3
*/
이벤트 캡쳐(Event Capture)를 테스트하기 위해 addEventListener()
함수에 capture : true
라는 옵션을 추가했다.
위의 코드에서는 마찬가지로 class 명이 el3인 element(<div class="el3"></div>
)을 클릭했을 때, 이벤트 버블링과 다르게 상위 요소에서 하위 요소로 즉, 이벤트 버블링과 반대 방향으로 이벤트를 전파하고 있다.
이에 따라 결과 값은 el3부터 el1까지 상위 요소부터 콘솔에 출력되고 있다.
이처럼, 이벤트가 발생했을 때, 상위 요소부터 하위요소로 전파시키는 이벤트 전파 방식을 이벤트 캡쳐라고 한다.
이벤트 위임(Event Delegation)
이벤트 위임은 하위 요소 각각에 이벤트를 구현하지 않고 상위 요소에서 하위요소의 이벤트를 제어하는 방식이다.
<body>
<ul class="list">
<li>
<input type="checkbox" id="one"/>
</li>
<li>
<input type="checkbox" id="two"/>
</li>
</ul>
</body>
var items = document.querySelectorAll('input');
items.forEach(function(item) {
item.addEventListener('click', function(e) {
console.log(e.target.id);
});
});
위의 코드를 보면 각각의 input
요소를 클릭했을 때, 해당 요소의 id 값을 콘솔에 출력하도록 코드가 짜여있다.
하지만 이런 방식으로, 이벤트를 줄 요소를 for문을 돌려 이벤트를 구현할 시, 동적으로 추가되는 요소는 이벤트가 적용되지 않는다는 치명적인 단점이 있다.
쉽게 말해, 위의 코드에서 li를 특정 버튼을 눌러 추가한 이후, 그 요소를 클릭하면 위의 js코드가 작동하지 않는다는 것이다. 이러한 문제를 해결할 수 있는 방법이 이벤트 위임(Event Delegation)을 적용하는 것이다.
/*
var items = document.querySelectorAll('input');
items.forEach(function(item) {
item.addEventListener('click', function(e) {
console.log(e.target.id);
});
});
*/
//이벤트 위임 방식으로 코드 변경
var items = document.querySelector('.list');
items.addEventListener('click', function(e) {
console.log(e.target.id);
});
위의 수정된 코드를 보면, 이벤트 줄 요소를 해당 요소가 아닌 그 상위 요소인 <ul class="list"></ul>
을 지정하고 있다.
이벤트를 줄 요소가 아닌 그 요소의 부모 요소를 지정하여 이벤트 리스너를 달고, 하위에서 발생한 클릭 이벤틀를 감지하도록 한다.
이렇게 코드를 짜면, 동적으로 추가된 요소에 대해서도 이벤트가 동작하도록 할 수 있다.
이벤트 위임(Event Delegation)의 장점
-
상위 요소에서 이벤트 리스너를 관리하기 때문에 하위 요소에는 자유롭게 요소를 추가할 수 있다. 즉, 동적인 element를 관리하기에 수월하다.
-
이벤트 핸들러를 한 곳에서 관리하기 때문에 관리하기 수월하다.
-
동적으로 추가되는 요소에 대한 이벤트가 없기 때문에 메모리의 사용이 줄어든다.
-
이벤트 핸들러가 줄어들기 때문에 메모리 누수의 가능성도 줄어든다.
🙏 Reference
DocumentFragment
DocumentFragment
를 활용하는 것은 reflow
를 줄이기 위한 방법 중 하나다.
<body>
<select id="timer">
</select>
</body>
function addElements() {
var target = document.getElementById('timer');
for (var i = 0; i < 24; i++) {
var option = document.createElement('option');
option.innerText = i;
target.appendChild(option);
}
}
위 코드는 0시부터 23시까지의 option
엘리먼트를 셀렉트 박스에 추가하는 예제이다.
timer
셀렉트박스에 0부터 23까지 반복을 돌려 매번 셀렉트 박스에 엘리먼트를 추가하고 있다. 24번의 DOM 레이아웃 변경이 일어나게 되기 때문에 24번의 reflow
와 repaint
가 각각 일어나게 된다.
DocumentFragment
를 활용했을 때의 가장 큰 차이는 DocumentFragment
객체는 활성화된 DOM트리의 일부가 아니기 때문에 DocumentFragment
객체에서 일어나는 변경사항은 DOM에 영향을 주지 않는다. 즉, reflow
를 일으키지 않으며 성능에 큰 영향을 미치지 않게 된다.
function addElements() {
var target = document.getElementById('timer');
var docFrag = document.createDocumentFragment();
for (var i = 0; i < 24; i++) {
var option = document.createElement('option');
option.innerText = i;
docFrag.appendChild(option);
}
target.appendChild(docFrag.cloneNode(true));
}
위의 코드를 보면 DOM레이아웃을 변경시키는 경우는 timer
셀렉트 박스 엘리먼트에 추가할 때 발생한다. 즉, DocumentFragMent
객체를 셀렉트 박스 엘리먼트에 추가할 때 1번만 DOM 레이아웃이 변경된다. 따라서 각각 24번의 reflow
와 repaint
가 일어나던 것을 1번씩만 일어나도록 줄일 수 있게 된다.
최신 브라우저의 경우에는 reflow
가 발생하지 않도록 엔진을 최적화하기 때문에 DocumentFragment
를 통한 성능 향상을 체감할 수 없는 경우가 많다. 그러나 DOM 객체에 대한 다수의 접근을 필요로하는 작업을 수행해야하는 상황에서는 충분한 성능 향상을 체감할 수 있다.
🙏 Reference
repaint와 reflow 최적화
Repaint
와 Reflow
가 많아질수록 애플리케이션의 렌더링 성능은 느려지게 된다.
즉, 이를 줄일수록 성능을 높일 수 있다.
DOM객체의 캐싱
//Before
for(var i=0; i<100; i++) {
document.getElementById('container').style.padding = i + 'px';
}
//After
var container = document.getElementById('container');
for(var i=0; i<100; i++) {
container.style.padding = i + 'px';
}
class명과 cssText사용
//Before
var container = document.getElementById('container');
container.style.padding = "20px";
container.style.border = "10px solid red";
container.style.color = "blue";
//After cssText
container.style.cssText = 'padding:20px;border:10px solid red;color:blue;';
//After class
container.className = 'test';
애니메이션이 들어간 노드는 가급적 position:fixed 또는 position:absolute로 지정
<div id="animation" style="background:blue;position:absolute;"></div>
프레임에 따라 reflow비용이 많은 애니메이션 효과의 경우엔 노드의 position
을 absolute
나 fixed
로 주면 전체 노드에서 분리된다.
이 경우엔, 전체 노드에 걸쳐 Reflow 비용이 들지 않으며 해당 노드의 Repaint 비용만 들어가게 된다.
테이블 레이아웃을 피한다.
테이블로 구성된 페이지 레이아웃의 경우, 점진적 페이지 렌더링이 일어나지 않고 모든 계산이 완료된 후, 화면에 렌더링이 되기 때문에 피하는게 좋다.
Virtual DOM의 사용
Virtual DOM은 React나 Angular와 같은 UI/UX기반의 라이브러리 혹은 프레임워크에서의 컨셈이 되는 개념이다.
기존에 javascript나 jQuery에서 사용되던 DOM 직접접근 방식의 문제는 reflow와 repaint의 연관성도 빼놓을 수 없다.
DOM은 정적이다. DOM 요소에 접근하여 동적으로 이벤트를 주어 layout을 바꾸게 되면 reflow와 repaint가 일어나게 된다.
이 과정에서 규모가 큰 애플리케이션일수록 recalculate할 양이 늘어나고 이는 성능에 큰 영향을 미친다.
- 데이터가 업데이트되면, 전체 UI 를 Virtual DOM 에 리렌더링.
- 이전 Virtual DOM 에 있던 내용과 현재의 내용을 비교.
- 바뀐 부분만 실제 DOM 에 적용.
즉, Virtual DOM을 사용함으로써 바뀐 부분(Component)만 rerendering하기 때문에 컴포넌트가 업데이트 될 때, 레이아웃 계산이 한 번만 일어나게 된다.
🙏Reference
쓰로틀링 vs 디바운싱
함수를 호출할 때 호출이 너무 많이 되어 과부화 됨을 방지하기 위한 기술이다.
함수 호출이 잦은 예로는 브라우저의 이벤트가 있다. onscroll 이나 onchange 와 같은 이벤트의 콜백으로 함수를 호출하는 경우 굉장히 많은 호출이 발생할 수 있다.
infinite scroll 이나 자동완성 기능의 경우 사용자의 특정 이벤트에 따라 비동기 콜백을 호출하는 방식이다. 이 경우 이벤트가 매우 빈번하게 일어나며 많은 호출을 제어하지 않으면 브라우저가 버티지 못할 것이다. 이 때 사용하는 것이 쓰로틀링과 디바운싱이다.
쓰로틀링
Throttle 은 정해진 시간동안 특정 행위를 한 번만 호출하도록 하는 것이다. 예를 들어 스크롤 행위가 1 초에 500 번이 발생한다면 이를 0.2 초에 한 번만 실행하게 만들 수 있다.
Throttle 처리가 되지 않은 경우 콜백이 500 번 발생한다. 하지만 Throttle 처리가 된다면 5 번만 실행되게 만드는 기술이다.
스크롤 이벤트의 경우 작은 움직임에도 엄청나게 많은 이벤트가 발생한다. 따라서 1 초 미만으로 쓰로틀링을 하여 같은 동작의 여러번 호출을 1 번으로 제어하는 것이 좋다.
var timer;
document.querySelector('#input').addEventListener('input', function (e) {
// 1. timer 값이 undefined니까 if문 실행
if (!timer) {
// 2. timer 에 time함수 설정
timer = setTimeout(function () {
// 3. 설정시간에 맞춰 timer 초기화 및 함수 실행
timer = null;
console.log('비동기 요청', e.target.value);
}, 2000);
}
});
디바운싱
Debounce 는 한 행위를 여러 번 반복하는 경우, 마지막 행위 이후 특정 시간이 흐른 뒤에 콜백을 호출하도록 하는 방식이다. 자동완성 즉 autocomplete 를 떠올리면 편하다.
input 의 onChange
가 일어나면 callback 으로 AJAX 를 이용해 관련 데이터를 긁어온다. 그런데 사용자의 모든 입력에 AJAX 호출을 한다면 브라우저가 견디지 못할 것이다.
그래서 일정시간동안 Timer 를 만든다. 이 타이머의 시간동안 입력이 발생해 변경이 일어나면 Timer 를 초기화 한다.
입력이 멈추어 Timer 가 다 되면 AJAX 를 호출한다.
var timer;
document.querySelector('#input').addEventListener('input', function (e) {
// 1. timer 값이 undefined니까 if문 실행
if (timer) {
// 3-1. setTimeout이 끝나기 전에 다시 이벤트 실행 시 time함수 클리어 (이벤트 호출 시 마다 반복)
clearTimeout(timer);
}
// 2. 함수 할당
// 3-2. 함수 할당 (이벤트 호출 시 마다 반복)
timer = setTimeout(function () {
console.log('비동기 요청', e.target.value);
}, 2000);
// 4. 설정 시간 지나면 함수 종료
});
Lodash 와 Underscore에는 해당 기능들이 구현되어 있다.
🙏Reference
- [JS] 쓰로틀링(Throttling)과 디바운싱(Debouncing)
- zerocho blog - 쓰로틀링과 디바운싱
- CSS-Tricks 의 Throttling 과 Debouncing => 예시가 구현되어 있음
스크롤 이벤트 최적화 (rAF)
Throttling(쓰로틀링)
Throttling
은 스크롤 이벤트에서 주로 사용되는 기술로 지나치게 많은 이벤트가 발생하는 것을 몇 초에 한 번, 또는 몇 밀리초에 한 번씩만 실행되게 제한을 두는 것이다.
var timer;
document.querySelector('#input2').addEventListener('input', function (e) {
if (!timer) {
timer = setTimeout(function() {
timer = null;
console.log('ajax 요청', e.target.value);
}, 2000);
}
});
위의 코드에서 보다시피 setTimeout
을 이용하여 2초 동안에 한 번만 ajax요청을 하도록 하고 있다. 하지만 setTimeout
은 지정된 시간 뒤에 무조건 실행되는 것을 보장할 수 없다. 위의 코드는 2초 뒤에 콜백 함수를 실행하는 것이 아니라 2초 뒤에 Task Queue
에 넣는 것을 의미한다. Task Queue
에 들어간 함수는 순차적으로 Call Stack
에 옮겨지고 실행되는데 만약 Call Stack
이 비워져 있지 않다면, 지정한 시간 이후에 실행되는 것을 보장할 수 없다.
requestAnimationFrame(rAF)
rAF
는 브라우저의 최적화 상태를 고려하여 이벤트를 실행한다. 즉, setTimeout
처럼 무조건 지정된 시간에 한 번씩 이벤트를 트리거하지 않아도 된다.
Jbee님 포스팅 중 스크롤 이벤트 최적화를 보면 rAF
를 이용하여 스크롤 이벤트를 구현하는 방식을 살펴 볼 수 있는데 간단히 보면 다음과 같다.
function rAFScroll(callback) {
let tick = false
return function trigger() {
if (tick) {
return;
}
tick = true
return requestAnimationFrame(function task() {
tick = false
return callback();
})
}
}
trigger
함수를 보면 tick
의 값이 false
일 경우에만 requestAnimationFrame
의 콜백이 실행된다.
즉, 콜백 함수(callback
)가 rAF
에 의해 브라우저가 최적화된 상태에서만 tick
의 값이 false
가 되고 실행된다. 순차적으로 정리하면 다음과 같다.
rAF
의 콜백 함수인task
함수가animation frame
에 들어간다.tick
의 값이true
라면trigger
함수가 호출되어도 콜백 함수가 실행되지 않는다.task
함수가 실행되면서tick
이false
가 되면 다시 콜백 함수가 실행될 수 있는 환경이 된다.
이처럼 쓰로틀링을 사용하지 않고 rAF
을 사용하여 이벤트 최적화가 가능하며 rAF
는 애니메이션의 최적화에도 많이 사용되는 API이기 때문에 잘 알아두는게 좋다.
🙏Reference
값 비교하기 (Array, String, Object ...)
react-router-dom
에서 사용 중인value-equal
라이브러리
function valueOf(obj) {
return obj.valueOf ? obj.valueOf() : Object.prototype.valueOf.call(obj);
}
function valueEqual(a, b) {
// Test for strict equality first.
if (a === b) return true;
// Otherwise, if either of them == null they are not equal.
if (a == null || b == null) return false;
if (Array.isArray(a)) {
return (
Array.isArray(b) &&
a.length === b.length &&
a.every(function(item, index) {
return valueEqual(item, b[index]);
})
);
}
if (typeof a === 'object' || typeof b === 'object') {
var aValue = valueOf(a);
var bValue = valueOf(b);
if (aValue !== a || bValue !== b) return valueEqual(aValue, bValue);
return Object.keys(Object.assign({}, a, b)).every(function(key) {
return valueEqual(a[key], b[key]);
});
}
return false;
}
🙏 Reference
배열 내장함수
자바스크립트의 배열에서는 기본적으로 많은 종류의 내장 함수를 제공하고 있고 이를 이용하면 보다 간단한 코드로 다양한 기능을 구현할 수 있다.
forEach
forEach
는 배열을 반복하여 기존 값을 가져오는데 주로 쓰인다.
const arr = [1, 2, 3, 4, 5];
arr.forEach(item => {
console.log(item); //1 2 3 4 5
});
map
map
과 forEach
의 차이는 배열을 반복하면서 각각의 원소에 대하여 특정 로직을 수행한 뒤, 새로운 배열을 반환하고 싶을 때 주로 사용한다.
const arr = [1, 2, 3, 4, 5];
const newArr = arr.map(item => {
return item * 2;
});
console.log(arr); //[1, 2, 3, 4, 5]
console.log(newArr); //[2, 4, 6, 8, 10]
위에서 볼 수 있듯이 map
을 통해 반환한 배열은 깊은 복사를 사용하기 때문에 기존 배열을 영향을 주지 않고 새로운 배열을 반환한다.
filter
filter
함수의 기능은 이름 그대로 배열을 반복하며 각각의 원소들 중 특정 조건에 해당하는 원소들만 뽑아내어 새로운 배열을 반환한다.
filter
또한 map
과 마찬가지로 기존 배열에 영향을 주지 않는다.
const arr = [1, 2, 3, 4, 5];
const newArr = arr.filter(item => {
return item >= 3;
})
console.log(arr); //[1, 2, 3, 4, 5]
console.log(newArr); //[3, 4, 5]
indexOf
indexOf
함수는 배열에서 찾고자 하는 원소가 있다면 그 원소의 index
값을 반환한다.
const arr = ['a', 'b', 'c', 'd'];
console.log(arr.indexOf('c')); //2
findIndex
findIndex
함수도 indexOf
함수와 마찬가지로 찾고자 하는 원소의 index
값을 반환한다. 하지만 원소가 객체로 되어있거나 배열로 되어있을 때 indexOf
함수로는 index
값을 찾을 수 없지만 findIndex
함수는 조건 처리를 통하여 index
값을 찾을 수 있다.
const arr = [
{
id: 1,
name: 'BKJang',
age: 27,
},
{
id: 2,
name: 'JHKim',
age: 25,
}
]
console.log(arr.findIndex(item => item.id === 2)); //1
find
find
함수는 findIndex
함수와 사용법은 동일하지만 반환하는 값이 index
값이 아닌 찾아낸 값 자체를 반환한다.
const arr = [
{
id: 1,
name: 'BKJang',
age: 27,
},
{
id: 2,
name: 'JHKim',
age: 25,
}
]
console.log(arr.find(item => item.id === 2));
// {id: 2, name: "JHKim", age: 25}
splice
splice
함수의 첫번째 파라미터는 지우기 시작할 원소의 index
, 두번째 파라미터는 지울 원소의 갯수를 넘긴다.
const arr = ['a', 'b', 'c', 'd', 'e'];
const arr2 = arr.splice(2, 3);
console.log(arr); //["a", "b"]
console.log(arr2); //["c", "d", "e"]
slice
slice
함수와 splice
함수의 가장 큰 차이점은 slice
함수는 기존 배열에 영향을 주지 않고 새로운 배열을 반환한다는 것이다.
또 다른 차이점은 slice
함수는 두번째 파라미터로 보낸 원소의 index
값 전까지 원소를 제거한다.
const arr = ['a', 'b', 'c', 'd', 'e'];
const arr2 = arr.slice(1, 3);
console.log(arr); //["a", "b", "c", "d", "e"]
console.log(arr2); //["b", "c"]
shift
shift
함수는 기존 배열의 원소를 앞에서부터 하나씩 제거한다.
const arr = [1, 2, 3, 4, 5];
arr.shift();
console.log(arr); //[2, 3, 4, 5]
arr.shift();
console.log(arr); //[3, 4, 5]
unshift
unshift
함수는 기존 배열에 새로운 원소를 앞에 추가한다.
const arr = [1, 2, 3, 4, 5];
arr.unshift(0);
console.log(arr); //[0, 1, 2, 3, 4, 5]
arr.unshift(-1);
console.log(arr); //[-1, 0, 1, 2, 3, 4, 5]
push
push
함수는 기존 배열에 새로운 원소를 뒤에 추가한다.
const arr = [1, 2, 3, 4, 5];
arr.push(6);
console.log(arr); //[1, 2, 3, 4, 5, 6]
arr.push(7);
console.log(arr); //[1, 2, 3, 4, 5, 6, 7]
pop
pop
함수는 기존 배열의 원소를 뒤에서부터 하나씩 제거한다.
const arr = [1, 2, 3, 4, 5];
arr.pop();
console.log(arr); //[1, 2, 3, 4]
arr.pop();
console.log(arr); //[1, 2, 3]
concat
concat
은 여러 개의 배열을 하나로 합쳐주는 함수다. 기존 배열들에는 영향을 끼치지 않고 새로운 배열을 반환한다. ES6
에서는 concat
대신 Spread Operator
를 많이 사용한다.
//concat
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd', 'e'];
const newArr = arr1.concat(arr2);
console.log(arr1); //["a", "b"]
console.log(arr2); //["c", "d", "e"]
console.log(newArr); //["a", "b", "c", "d", "e"]
//ES6 Spread Operator
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd', 'e'];
const newArr = [...arr1, ...arr2];
console.log(arr1); //["a", "b"]
console.log(arr2); //["c", "d", "e"]
console.log(newArr); //["a", "b", "c", "d", "e"]
join
join
함수는 배열의 원소들을 합쳐 문자열 형태로 반환한다.
const arr = ['a', 'b', 'c', 'd', 'e'];
console.log(arr.join(', ')); //a, b, c, d, e
console.log(arr.join('')); //abcde
every
every
함수는 특정 조건에 대하여 배열의 모든 원소가 통과해야만 true
를 반환하며 빈 배열에 대해서는 무조건 true
를 반환한다.
const arr = [1, 2, 3, 4, 5];
const result1 = arr.every(item => item <= 5);
const result2 = arr.every(item => item < 3);
console.log(result1, result2); //true false
some
some
함수는 특정 조건에 대하여 배열의 어떤 한 요소라도 통과하면 true
를 반환하며 빈 배열에 대해서는 무조건 false
를 반환한다.
const arr = [1, 2, 3, 4, 5];
const result1 = arr.some(item => item <= 5);
const result2 = arr.some(item => item < 3);
console.log(result1, result2); //true true
sort
const fruit = ['orange', 'apple', 'banana'];
fruit.sort();
console.log(fruit); //["apple", "banana", "orange"]
const imD = [
{ name : "BKJang", age : 27},
{ name : "JHKim", age : 25},
{ name : "SHJo", age : 28},
{ name : "DHJung", age : 29},
{ name : "JSKang", age : 23}
]
/* 이름순 */
imD.sort((a, b) => { // 오름차순
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
});
/**
0: {name: "BKJang", age: 27}
1: {name: "DHJung", age: 29}
2: {name: "JHKim", age: 25}
3: {name: "JSKang", age: 23}
4: {name: "SHJo", age: 28}
*/
imD.sort((a, b) => { // 내림차순
return a.name > b.name ? -1 : a.name < b.name ? 1 : 0;
});
/**
0: {name: "SHJo", age: 28}
1: {name: "JSKang", age: 23}
2: {name: "JHKim", age: 25}
3: {name: "DHJung", age: 29}
4: {name: "BKJang", age: 27}
*/
/* 나이순 */
const sortingField = 'age';
imD.sort((a, b) => { // 오름차순
return a[sortingField] - b[sortingField];
});
/**
0: {name: "JSKang", age: 23}
1: {name: "JHKim", age: 25}
2: {name: "BKJang", age: 27}
3: {name: "SHJo", age: 28}
4: {name: "DHJung", age: 29}
*/
imD.sort((a, b) => { // 내림차순
return b[sortingField] - a[sortingField];
});
/**
0: {name: "DHJung", age: 29}
1: {name: "SHJo", age: 28}
2: {name: "BKJang", age: 27}
3: {name: "JHKim", age: 25}
4: {name: "JSKang", age: 23}
*/
reduce
reduce
함수는 누적 값과 현재 값을 이용하여 여러 가지 기능을 구현할 수 있는 매우 유용한 함수다. 사실, reduce
만 잘 사용할 줄 알아도 filter
함수의 기능까지도 구현할 수 있다.
var arr = [1, 2, 3, 4, 5];
var sum = arr.reduce((acc, cur) => {
return acc + cur;
});
console.log(sum); // 15
var arr = [1, 2, 3, 4, 5];
var reducer = (acc, cur) => {
return acc + cur;
};
var sum = arr.reduce(reducer);
console.log(sum); // 15
var arr = ['Kim', 'Kim', 'Kang', 'Jang', 'Jo', 'Kang', 'Jo'];
var reducer = (acc, cur, index) => {
if (!acc[cur]) {
acc[cur] = 1;
} else {
acc[cur] = acc[cur] + 1;
}
return acc; // 여기
};
var sorting = arr.reduce(reducer, {});
console.log(sorting); // { Kim: 2, Kang: 2, Jang: 1, Jo: 2 }
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 20];
var reducer = (acc, cur, index) => {
if (cur % 2 === 0) acc.push(cur * 2);
return acc;
};
var double = arr.reduce(reducer, []);
console.log(double);
const input = [
{
title: '슈퍼맨',
year: '2005',
cast: ['장동건', '권상우', '이동욱', '차승원']
},
{
title: '스타워즈',
year: '2013',
cast: ['차승원', '신해균', '장동건', '김수현']
},
{
title: '고질라',
year: '1997',
cast: []
}
];
const key = 'cast';
const flatMapReducer = (acc, cur) => {
cur[key].reduce((acc2, cur2) => {
if (acc2.indexOf(cur2) === -1) acc2.push(cur2);
return acc2;
}, acc);
return acc;
};
const flattenCastArray = input.reduce(flatMapReducer, []);
// ['장동건', '권상우', '이동욱', '차승원', '신해균', '김수현']
console.log(flattenCastArray);
reduceRight
reduceRight
함수는 기존의 reduce
와 원리는 동일하지만 current
에 마지막 index
원소부터 첫번째 원소 순서로 들어간다. 아래는 배열을 nested object
로 만드는 예시다.
const makeNestedObjWithArray = (arr) => {
return arr.reduceRight(
(accumulator, item) => {
const newAccumulator = {};
newAccumulator[item] = Object.assign(
{},
accumulator
);
return newAccumulator;
},
{}
);
};
const list = ["a", "b", "c"];
const obj = makeNestedObjWithArray(list);
console.log(obj);
/**
a : { b: { c: {} } }
*/
🙏 Reference
- MDN Docs - Array.some()
- MDN Docs - Array.every()
- DUDMY HOME - 자바스크립트 정렬 함수, sort()
- 비비로그 - 자바스크립트의 유용한 배열 메소드 사용하기... map(), filter(), find(), reduce()
- Im-D/Dev-Docs - Reduce
- How to create nested child objects in Javascript from array?
Object.keys(), Object.values(), Object.entries() 그리고 for...in
Object.keys(), Object.values(), Object.entries()
const developer = {
name : 'BKJang',
age : 26,
lang: 'Korean'
}
console.log(Object.keys(developer)); // ["name", "age", "lang"]
console.log(Object.values(developer)); // ["BKJang", 26, "Korean"]
console.log(Object.entries(developer));
/**
0: ["name", "BKJang"]
1: ["age", 26]
2: ["lang", "Korean"]
*/
for...in
const developer = {
name : 'BKJang',
age : 26,
lang: 'Korean'
}
for(let key in developer) {
console.log(`${key} : ${developer[key]}`);
}
/**
name : BKJang
age : 26
lang: 'Korean'
*/