
Quiz-1부터Quiz-12까지는 이전 글에서 풀어 보실 수 있습니다.
키워드 : Primitive, Builtin Object , Wrapper Object
숫자 또는 문자열과 같은 원시타입이 마치 객체처럼 접근 연산자 .을 사용해서 프로퍼티에 접근하거나 메소드를 사용할 수 있는 이유는 무엇일까?
"asdfg".toUpperCase(); // "ASDFG"
(10.45).toFixed(1); // "10.4"
정답보기📝
원시타입에 접근 연산자 또는 대괄호로 접근하면 자바스크립트 엔진이 원시값에 해당 하는 빌트인객체 (String, Number, Symbol ...)를 이용하여 암묵적으로 객체를 생성하는데 이를 래퍼 객체라고 한다. 즉, 래퍼객체를 통해 원시값이 메소드나 프로퍼티에 접근할 수 있도록 언어 차원에서 허용 하고 있는 것
이때 만들어진 래퍼객체는 다시 원래상태로 돌아갈 수 있도록 이전의 원시값을 기억하고 있으며, 임시로 만들어진 래퍼객체는 GC의 대상이 되어 삭제된다.
아래의 코드처럼 래퍼객체에 속성을 추가하는 것도 가능 하긴한데 어차피 바로 원시값으로 돌아오기 때문에 의미는 없다 (단 Strict 모드에선 에러발생)
const user = "jiheon";
user.type = "student"; // 래퍼객체 생성 -> 제거
console.log(user.type); // undefinded
키워드 : Object, Iterable, Spread
스프레드 연산자 (...) 는 기본적으로 문자열이나 배열과 같은 이터러블 객체를 순회하면서 이터러블을 확장할때 사용한다.
자바스크립트의 일반객체는 이터러블 객체가 아니라서 이터레이터로 인한 순회가 불가능함에도 불구하고 어째서 일반객체에 스프레드 연산자 사용이 가능할까?
const user = { name: "plain", age: 12 }
const coppied = { ...user } // { name: 'plain', age: 12 }
정답보기📝
원래 라면 user객체가 이터러블 객체가 아니니까 스프레드 연산자 사용이 불가능 해야할 것 같지만 ES2018부터 객체 리터럴 내에서 spread 프로퍼티를 사용할 수 있게 확장되어 일반객체 에서도 스프레드 연산자를 통한 확장이 가능하다. 즉, 원래라면 안되는데 언어차원에서 객체 리터럴에서 사용 가능하도록 추가된 것
일반객체의 스프레드 연산자 사용의 경우 이터러블 프로토콜을 사용하지 않고
key-value쌍을 복사하여 확장한다고 한다
키워드 : Object, Property, hasOwnProperty
Object.prototype에는 특정 프로퍼티가 해당 객체에 존재하는지의 여부를 확인하는 hasOwnProperty이라는 메소드가 존재한다
const user = { name: "A" }
user.hasOwnProperty("name") // true
하지만 아래처럼 객체가 이미 hasOwnProperty라는 속성을 갖고 있다고 가정하면 섀도잉으로 인해 Object.prototype.hasOwnProperty는 사용할 수 없다.
const user = { name: "A", hasOwnProperty: (v) => v };
user.hasOwnProperty("name") // "name"
이 경우 hasOwnProperty를 정상적으로 사용하려면 어떻게 해야할까?
Function.prototype에서 제공하는 call 메소드를 통해서 this를 user로 지정하고 함수를 호출하면 된다.
Object.prototype.hasOwnProperty.call(user,"hasOwnProperty") // true
또는 hasOwnProperty 대신에 ES2022에서 추가된 Object.hasOwn() 메소드를 사용할 수 있는데 섀도잉으로 인한 이름 충돌을 피하기 위해서 객체 자체의 속성 여부를 훨씬 직관적이고 안전하게 할 수 있게 해주는 메소드이다
const user = { name: "A", hasOwnProperty: (v) => v };
Object.hasOwn(user, "name") // true
이처럼 Object.prototype의 메소드들을 직접 참조해서 사용 하는것은 그리 좋은 방법은 아니다. 만약 프로토타입 체이닝이 끊겨버린 오염된 객체라던지 위의 문제와 같이 속성명이 섀도잉 된 경우 메소드가 정상적으로 동작하지 않기때문, 그래서 Object.prototype의 메소드들을 사용할 때는 call 메소드를 통해 호출하는 것이 더 안전하다.
키워드 : Class, Constructor, new
아래의 코드에서 함수 A는 생성자 함수로 new키워드로 호출해서 인스턴스를 생성하고있고, 함수 B는 일반함수로 인자를 받아서 단순히 객체를 반환한다. 결과만 놓고 보면 같은 객체를 생성하고있는 것 같은데 차이점이 뭘까?
function A(name) {
this.name = name;
}
function B(name){
return { name: name }
}
const a = new A("jiheon") // { name: "jiheon" }
const b = B("jiheon") // { name: "jiheon" }
정답보기📝
사실 생성자 함수와 일반함수가 기술적인 차이가 존재하는건 아니고 똑같은 함수라서 모든 함수가 생성자 함수가 될 수 있지만, new 키워드를 사용해서 함수를 호출하면 함수의 일반 호출과 다른 알고리즘으로 동작한다.
- 내부적으로 객체를 생성해서
this할당과 속성 추가후 자동 반환- 프로토타입 체이닝을 통해
A.prototype의 속성에 접근이 가능
따라서 프로토타입에 메소드를 추가했을때 일반 호출로 생성된 객체는 new 를 사용하여 호출된 객체와 다르게 접근이 불가능하다
B.prototype.myName = function(){ return this.name }
const b = B("jiheon")
b.myName() // TypeError
a -> A.prototype -> Object.prototype
b -> Object.prototype
키워드 : Class, extends, Dynamic
아래와 같은 클래스에서 extends 키워드 뒤에 동적으로 클래스 확장이 가능할까?
function generateClass(){
return class { /* ... */ }
}
class A extends generateClass() {
// ...
}
정답보기📝
extends 키워드 뒤에는 표현식이 올 수 있기 때문에 가능하다. 따라서 이를 이용하면 동적으로 extends 를 통한 클래스 확장이 가능하다
키워드 : Class, Class Field, Constructor
아래의 코드에서 Parent 클래스의 인스턴스가 생성될때 클래스 멤버가 먼저 초기화 될까? 아니면 생성자 함수가 먼저 실행될까?
const initalize = ()=>{
console.log("initalize");
return 10
}
class Parent {
count = initalize()
constructor(){
console.log("constructor");
}
}
const parent = new Parent()
정답보기📝
1) initalize
2) constructor
클래스 필드의 초기화가 생성자함수의 실행보다 먼저 발생한다
키워드 : Asynchronous, Exception , Try Catch
func 함수 내부에서 예외가 발생 한다면 catch문에서 예외처리가 가능할까?
function func(){ throw new Error() }
try {
setTimeout(func);
} catch (error) {
// ...
}
정답보기📝
try...catch는 동기적으로 동작한다. 그래서 try문 안에서 스케쥴링된 코드는 이후에 try...catch문이 이미 종료된 시점에 try문을 벗어나서 실행되기 때문에 func함수 내부에서 발생한 예외는 try...catch문 에서 잡아낼 수 없다
에러 핸드링을 위해서는 아래 처럼 setTimeout함수 내부에서 추가 해야한다.
setTimeout(() => {
try {
func();
} catch {}
});
키워드 : Promise, Promise-Chaining, Exception
보통 then() 메소드의 2번째 인자는 잘 사용하지 않지만, 선택적으로 프로미스가 거부되었 을때 실행할 콜백함수를 전달 받을 수 있다. [MDN] 스펙
그렇다면 then 메소드에 2번째 인자를 전달하는 것과 catch() 메소드를 사용하여 예외처리를 하는 것과 무슨차이가 있는 걸까?
// promise : 임의의 프로미스 객체
// f1, f2 : 임의의 콜백함수
promise.then(f1).catch(f2); // 1번
promise.then(f1, f2); // 2번
정답보기📝
프로미스에서 발생한 에러는 프로미스 체인상의 가장 가까운 catch로 전달되는데 f1에서 발생한 에러를 f2에서 잡을 수 있냐 없냐의 차이가 있다.
따라서 1번은 f1에서 발생한 에러를 catch문에서 잡을 수 있지만, 2번은 프로미스가 거부되면 f2가 실행될 뿐, 이어지는 체인이 없기 때문에 f1에서 발생한 에러를 잡을 수 없다
키워드 : Promise, Promise-Chaining, Finally
Quiz-20에서 then()메소드의 선택적 2번째 인자를 알아보았는데 그렇다면 f라는 임의의 함수를 프로미스가 이행, 거부시 각각 같은 함수가 실행되도록 한다고 가정하면 finally()에서 한번만 실행하는 것과 동일할까?
// promise : 임의의 프로미스 객체
// f : 임의의 콜백함수
promise.then(f,f) // 1번
primise.finally(f) // 2번
정답보기📝
1번, 2번 모두 프로미스가 이행되거나 거부되거나 항상 f함수가 실행된다는 점에서 유사한데 finally는 then, catch와는 다르게 아무런 인자도 받지않고 프로미스 결과를 자동으로 다음 체이닝으로 이어간다. 따라서 1번은 promise의 결과에 따라서 value또는 error값이 f 함수의 인자로 들어오지만 2번은 결과에 상관없이 실행되며 인자가 들어오지 않는 차이가 있다.
키워드 : Class, Super, Constructor
자바스크립트의 super 키워드는 항상 클래스에서만 유효할까?
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
정답보기📝
const parent = {
name: "Parent name",
}
const child = {
run(){
console.log(super.name);
},
}
Object.setPrototypeOf(child, parent)
child.run() // "Parent name"
꼭 그렇지만은 않다 명세서상의 super의 실제 내부동작은 [[HomeObject]] 라는 내부슬롯이 가리키는 객체의 __proto__를 가리킨다.
즉, super는 클래스 내부에서 사용하지 않더라도 일반 객체에서 메소드 축약 표현으로 정의한 메소드라면 사용할 수 는 있다
[[HomeObject]]는 자신이 바인딩 하고있는 객체를 가리키는 내부슬롯인데 해당 내부슬롯은ES6의 메소드 축약표현으로 정의한 메소드만 갖는다
키워드 : Object, Primitive, Conversion
아래의 코드에서 콘솔에 로그가 출력되도록 obj객체를 구현해보자.
(단, Strict Equal 연산자 ===가 아닌 Equal 연산자 == 가 사용되었다)
const obj = { /*...*/ }
if( obj == 1 && obj == 2 && obj == 3 ){
console.log("true"); // true
}
정답보기📝
조건문에서 객체를 원시타입과 비교 하고있다. 이때 자바스크립트는 obj객체를 원시타입으로 변환하는 메소드를 실행시키는데 Object.prototype에 존재하는 valueOf 또는 toString 메소드가 실행되어 원시값으로 변환된다. 따라서 이러한 원시타입으로 변환하는 메소드를 obj객체 내에서 재정의함에 따라 비교연산시 임의로 동작을 변경시킬 수 있다.
또한 Symbol.toPrimitive 심볼을 추가하는 방법도 존재한다.
// 3가지 방법 모두 가능
const obj = {
value: 1,
[Symbol.toPrimitive]() { return this.value++ },
// valueOf() { return this.value++ },
// toString() { return this.value++ },
}
키워드 : Object, Property, Flag, enumerable
Object.prototype.defineProperty을 사용하면 객체의 프로퍼티 플래그를 새로 추가하거나 변경할 수 있는데 enumerable: false를 적용해서 객체(배열)의 새로운 속성을 추가하고 아래와 같이 3가지의 방법으로 순회를 했을때 결과는?
const arr = [1,2,3]
Object.defineProperty(arr, '3', {
value: 4,
enumerable: false
});
for( const x of arr){ console.log(x) } // 1번
for( const x in arr){ console.log(arr[x]) } // 2번
Object.values(arr) // 3번
정답보기📝
for( const x of arr){ console.log(x) } // 1,2,3,4
for( const x in arr){ console.log(arr[x]) } // 1,2,3
Object.values(arr) // [1, 2, 3]
for...of 루프는 이터레이터에 기반해서 동작하기 때문에 배열의 모든 요소를 순회하고 따라서 객체 속성의 enumerable은 고려되지 않고, 결과적으로 새로 추가된 요소까지 콘솔에 찍힌 것을 확인할 수 있다.
하지만 for...in 루프와 Object.values() 와 같은 메소드들은 심볼을 제외하고 객체의 열거 가능한 속성만을 순회하기 때문에 플래그가 enumerable: false로 설정된 속성은 순회하지 않는다.
예전에 공부하던 자료들, 코드들을 참고하면서 내용을 추리고 수정하고를 반복하면서 총 24문제로 마무리를 하게 되었다. 😅 문제들의 출처는 대부분 기억나지 않지만 많은 문제들은 코어 자바스크립트 에서 영향을 받았고 예문이나 연습문제를 가져오거나 나만의 스타일로 다듬고 각색한 부분이 많다. 나 또한 많은 공부가 되었고 가볍게 문제풀이에 도전해보는 것도 좋을 것 같다.