
문제 출처는 시간이 많이 지나서 불분명 하지만 (사실 대부분 까먹었다) 예전에 공부했던 코어 자바스크립트 에서 영감을 많이 받았고, 책이나 유튜브 등 다른 곳에서 흥미로운 코드를 봤을 때 메모 해두었던 코드들을 바탕으로 이를 내 스타일대로 각색한 케이스가 많다.
재미와 흥미를 목적으로 쓴 글이라서 가볍게 풀어보면 좋을 것 같고 모든 문제들은 답을 접어두어 문제 풀이에 집중할 수 있도록 했으며, 정답에 대한 해설을 직접 추가 했지만 부족한 부분이 많기 때문에 수정이 필요한 부분은 댓글로 달아주시면 감사하겠습니다. 😊
(글을 전부 작성하고 확인해보니 벨로그는 접어두기가 안되는 문제가... 😭)
쉬운 문제도 있지만 자바스크립트에 대한 깊은 이해를 필요로 하는 부분도 있으며 난해한 부분도 있어서 사실 난이도 자체는 조금 어려울 수 도 있다
키워드 : Function, Prototype, Method, Static
생성자 함수에서 메소드를 다양한 방법으로 추가하고 있는데 차이점은?
function Animal() {
this.run = function(){ return "run" } // 1번
}
Animal.prototype.move = function(){ return "move" } // 2번
Animal.fly = function(){ return "fly" } // 3번
정답보기📝
1번은 생성자 함수 내부에서 정의한 메소드로 인스턴스를 생성할때 마다 독립적으로 메소드가 만들어지기 때문에 메모리 사용측면에서 비효율적이다.
2번은 프로토타입 객체에 메소드를 추가하는 방법으로 생성자 함수로 생성한 모든 인스턴스에 의해 프로토 타입으로 상속되기 때문에 모든 인스턴스에서 접근 가능하며 1번과 다르게 한번 만들어진 메소드를 다 같이 공유한다.
3번은 정적 메소드를 추가하는 방법으로 클래스 내부에서 static 키워드로 추가한 메소드와 동일하다. 정적 메서드는 클래스의 인스턴스에 의해 상속되지 않아서 인스턴스에서 접근할 수 없다.
Animal.move() // "move"
const animal = new Animal();
animal.run() // "run"
animal.move() // TypeError
키워드 : Function, Prototype, Method, Closure
Quiz-1에서 생성자 함수 내부에서 메소드를 정의하는 방법은 매번 새로운 함수를 생성하기 때문에 프로토타입에 메소드를 정의하는 방법에 비해서 비효율적 이라고 했는데 그럼에도 불구하고 더 적합한 경우는 언제일까?
function Animal() {
this.run = function(){ return "run" } // 1번
}
Animal.prototype.move = function(){ return "move" } // 2번
정답보기📝
물론 대부분의 경우 프로토타입에 메서드를 정의하는 것이 좋겠지만 외부에서 직접 접근할 수 없는 변수를 만들고 싶을 때 생성자함수 내부의 메소드를 정의하여 클로저를 통한 private한 변수를 만들 때 사용될 수 있다.
function Counter() {
let count = 0;
this.increment = function() { return ++count }
this.decrement = function() { return --count }
}
위의 코드에서 count는 외부에서는 접근 불가능한 함수 내부의 로컬 변수로 클로저를 만들고 생성자 함수 내부에서 정의한 2개의 메소드로만 count변수를 조작 가능하다.
키워드 : Promise, Flow, Asynchronous
아래의 코드에서 콘솔에 출력되는 숫자의 순서는 어떻게 될까?
async function sub() {
const data = await new Promise((res) => {
console.log("1");
setTimeout(() => res("2"));
});
console.log("3");
return data;
}
function main() {
sub().then((result) => console.log(result));
console.log("4");
}
main();
정답보기📝
프로미스 자체는 비동기 작업을 처리하기 위한 도구이지만 프로미스를 생성할 때 실행되는 Executor함수 자체는 동기적으로 동작한다는 것에 주의해서 천천히 따라가 보면 어렵지 않게 해결할 수 있다.
main 함수는 동기함수이고 sub함수는 비동기 함수이며, 비동기 함수도 함수 내부의 코드는 동기적으로 실행된다 라는게 포인트
출력은 1 -> 4 -> 3 -> 2 순으로 출력된다.
키워드 : Function, Exception, Finally
try 문에서 함수가 바로 종료되는데 finally문은 실행되어 로그가 콘솔에 찍힐까?
function func() {
try {
return
} finally { console.log("finally") }
}
func(); // ?
정답보기📝
finally 절은 return, throw와 같이 try..catch를 빠져나가는 경우를 포함하여 try..catch가 종료되는 모든 상황에서 실행된다. 즉, finally는 try..catch를 벗어나는 직전에 실행되기 때문에 항상 함수의 종료보다 우선적으로 실행되어 clean up을 보장받고 따라서 콘솔에 출력이 된다.
키워드 : Import, Dynamic import
ES6 모듈 시스템에서 아래와 같은 방식으로 import 하는것이 가능할까?
import user from "./user" + ".js"
import user from ("./user" + ".js")
import user from (["./user", ".js"].join(""))
정답보기📝
from 뒤에 오는 값은 반드시 정적인 문자열이어야 한다 따라서 전부 불가능하다
만약 동적으로 import를 하고 싶다면 ES2020에 추가된 import()함수를 사용하여 모듈을 비동기적으로 로드할 수 있는 Dynamic import를 지원한다
import("./user"+".js") // 프로미스를 반환
.then((module) => console.log(module))
.catch(error => console.error(error))
import() 함수는 조건부로 모듈을 로드를 하거나 코드 스플리팅을 통해 필요한 모듈만 먼저 로드하여 초기 로드속도를 높이는 최적화 기법에 사용될 수 있다
키워드 : Object, This, Bind, Context
아래의 askPassword 함수를 실행하면 콘솔에 undefined 가 찍히게 되는데
this 를 살려서 정상적으로 콘솔에 name이 찍히게 하려면 어떻게 해야 할까?
const user = {
name: "Jiheon",
loginOk() { console.log(this.name) },
loginFail() { console.log(this.name) },
};
function askPassword(ok, fail) {
ok(); // undefined
fail(); // undefined
}
askPassword(user.loginOk, user.loginFail);
정답보기📝
function askPassword(ok, fail) {
ok.call(user)
fail.call(user)
}
// 또는
askPassword(
user.loginOk.bind(user),
user.loginFail.bind(user)
);
Function.prototype에서 제공하는 call, apply, bind 함수를 이용해서 직접 적으로 this에 해당하는 user 객체를 바인딩하여 해결 할 수 있으며
askPassword(()=> user.loginOk(), ()=> user.loginFail());
또는 askPassword를 호출할 때 콜백함수로 한번 감싸서 참조하는 객체인 user 객체를 살려둘 수 있다. 즉, 쉽게말해 콜백함수로 한번 감싸서 함수가 실행될 때 user 객체를 잃어버리지 않고, 기억할 수 있게 한다
키워드 : Object, This, ArrowFunction, Asynchronous
아래의 user 객체는 메소드 축약형태의 normal, 화살표 함수 형태의 arrow, 그리고 setTimeout 함수가 실행되는 timmer 메소드를 갖고 있다. 각각 어떻게 출력될까?
const user = {
name: "Jiheon",
normal() { console.log(this.name) },
arrow: () => { console.log(this.name) },
timmer() {
setTimeout(function () { console.log(this.name) });
},
};
user.normal() // ?
user.arrow() // ?
user.timmer() // ?
정답보기📝
user.normal() // Jiheon
user.arrow() // undefined
user.timmer() // undefined
메소드 축약 형태의 normal 메소드는 정의될 때 this가 의도한 대로 자신이 속한 user객체를 잘 가리키지만, 화살표 함수로 정의한 arrow 메소드는 자신만의 this를 갖지 않고, 자신이 정의된 시점의 상위 스코프에서 this를 가져오기 때문에 arrow 메소드가 정의된 시점인 user객체의 상위 즉, 전역 스코프에서 name을 찾기 때문에 undefined가 출력되게 된다.
반면 timmer 메소드는 normal과 마찬가지로 메소드 축약 형태라서 this값을 제대로 가리키지만 setTimeout의 콜백함수는 기본적으로 전역 컨텍스트에서 실행 되기 때문에 따로 this바인딩을 하지 않으면 this는 전역 객체를 가리키게 된다
이런 문제는 함수의 호출방식, this 바인딩 동작 차이 때문에 발생하는 문제로 this 얘기를 할 때 쉽게 볼 수 있는데 이런게 자바스크립트를 공부하는 사람을 쉽게 혼란스럽게 한다
근본적으로 this라는 키워드의 존재의의를 생각해 보면 객체지향 언어의 객체 내부에서 자기 자신을 참조하기 위해서 사용한다고 생각하는데 자바스크립트의 함수는 사실 객체이기 때문에 결국 함수 내부에서 this 를 사용할 수 있기에 생기는 혼란 이라고 생각한다.
이런 동적이고 유연한 동작이 자바스크립트의 특징이지만 this의 동작은 항상 쉽게 예상되지만은 않는 것 같다
키워드 : Function, Reference , Garbage Collection
1) 함수 내부에서 같은 이름의 함수를 정의할 수 있을까?
function func(value){
return function func() {
return value
}
}
2) 또한 자기 자신을 반환할 수 있을까?
function self(){ return self }
정답보기📝
func 함수 내부에서 정의된 func 함수는 내부에서만 접근할 수 있기 때문에 바깥쪽 func 함수와 이름이 중독되어도 문법적으로 문제가 되지 않는다.
마찬가지로 자기 자신을 반환하는 것도 문제 되지 않는다
self === self()()()()() // true
키워드 : Class, Static, Prototype
정적 프로퍼티 또는 메소드는 상속될까? 또한 그러한 이유는?
class Parent {
static title = "Parent";
static run() {
console.log("hello");
}
}
class Child extends Parent {}
Child.title; // ?
Child.run(); // ?
정답보기📝
사실 Child extends Parent은 동시에 2개의 참조가 일어난다
Child.prototype -> [[prototype]] -> Parent.prototype
Child -> [[prototype]] -> Parent
따라서 부모의 프로토타입과 참조가 되는 동시에 Child 클래스 자체도 Parent 클래스와 참조가 되어 정적 프로퍼티나 메소드에 접근할 수 있다
키워드 : Class, Prototype, Extends
클래스 A와 클래스 B는 무슨 차이가 있고, extends Object 에서 명시적으로 Object를 상속 받는다는게 무슨 의미일까?
class A {}
class B extends Object {}
정답보기📝
클래스 A는 기본적으로 함수이기 때문에 프로토타입은 Function.prototype을 가리키는 반면, 클래스 B는 명시적으로 Object을 상속받기 때문에 프로토타입은 Object 클래스가 된다
# 화살표는 프로토타입 관계를 나타냄
A -> Function.prototype -> Object.prototype
B -> Object -> Function.prototype -> Object.prototype
비표준 속성 __proto__를 통해서 프로토타입이 가리키는 대상을 확인 해보면 쉽게 확인 가능하다. (물론 표준인 Object.getPrototypeOf을 사용해도 됨)
A.__proto__ === Function.prototype // true
B.__proto__ === Object // true
B.__proto__.__proto__ === Function.prototype // true
클래스 B는 A와 다르게 프로토타입 체인상에 Object가 있고 따라서 Object의 정적 메소드에 접근이 가능하다 그래서 Object.assign Object.create와 같은 메소드를 클래스 A로는 접근할 수 없지만 클래스 B로는 접근할 수 있다.
키워드 : Object, This, Operator, reference type
아래의 코드는 어떻게 동작할까?
const user = {
name: "Jiheon",
hi(){ console.log(this.name) },
bye(){ console.log("bye") }
};
(user.name === "Jiheon" ? user.hi : user.bye)() // ?
정답보기📝
(user.name === "Jiheon" ? user.hi : user.bye)() // undefined
이 문제는 코어 자바스크립트의 참조타입 이라는 내용에서 재미있게 봤던 주제여서 가져온 예시인데 해당 글에서도 얘기 하지만 크게 중요한 내용은 아니고 이런 내용도 있구나 하고 한번 읽어보면 좋을 것 같다.
결론은 user.hi()와 같이 객체의 접근연산자 .을 통해서 객체의 메소드에 접근하면 내부적으로 참조타입 이라는 특별한 형태로 반환되어 this 정보를 전달할 수 있게되고 hi() 메소드를 실행할 때 this정보가 전달될 수 있는 것
user.hi() // "Jiheon"
하지만 . 연산자 혹은 대괄호가 아닌 다른 연산에서는 이러한 this정보를 전달해 주는 참조타입으로 반환되지 않기 때문에 위의 문제에서는 결국 삼항연산자로 인한 연산으로 this값을 잃어버려서 undefined가 출력된다.
키워드 : Object, Reference , Garbage Collection, WeakMap
아래의 코드에서 user 변수에 객체를 할당한 후, 다시 null을 할당해서 참조를 해제 했을때, 여전히 userList변수에서 객체가 참조되는 이유는 무엇일까?
또한 user 변수의 참조가 해제 되었을때 userList역시 참조가 해제 되도록 할 수 있는 방법은 뭐가 있을까?
let user = { name: "Jiheon" }
const userList = [ user ]
user = null
console.log(userList); // [{ name: "Jiheon" }]
정답보기📝
만약 아래처럼 user 변수에 객체를 할당하고 그대로 null로 재할당을 했다면 생성된 객체는 더 이상 참조 될 수 있는 방법이 없는 상태가 되어버리기 때문에 자바스크립트 엔진내 GC에 의해서 이러한 객체들을 삭제한다
let user = { name: "Jiheon" }
user = null
하지만 문제에선 메모리에서 해제되기 전에 const userList = [ user ] 코드가 중간에 있기 때문에 해당 객체는 userList 변수를 통해서 참조 가능 하고, 결국 GC의 대상이 되지 않고 메모리에서 지워지지 않기 때문에 그대로 참조되는 것
만약 userList라는 변수가 정말 필요에 의해서 참조되고 있을 수도 있지만 의도치 않게 불필요한 변수 때문에 정작 GC의 대상이 되지 않아 메모리의 낭비가 발생하고 결과적으로는 브라우저 성능에 영향을 미칠 수 있게된다
이처럼 GC에 의한 불필요한 메모리 누수를 방지하기 위해 만들어진 객체가 WeakMap과 WeakSet이다. (weak는 약한 참조를 유지한다 라는 의미)
let user = { name: "Jiheon" }
const userList = new WeakMap();
userList.set(user, "value") // user 객체가 위크맵의 키로 참조중
user = null // user 변수 참조 해제
userList.get(user) // undefined
// user객체가 위크맵의 키로 참조 중이었지만 자동으로 메모리에서 제거됨
위크맵과 위크셋에 저장된 객체는 GC의 대상이 될 수 있어서 저장된 객체가 더 이상 참조되지 않는다면, 메모리에서 자동으로 제거된다
내용이 너무 많아져서 2편에 이어서 문제가 이어집니다 :) 👍