본문으로 건너뛰기

JavaScript 심화 요약 (내가 보려고 만든)

Dev Javascript Js
목차
thumbnail

띄엄띄엄 알고있던 JavaScript 문법을 제대로 공부하며 정리하였습니다.

앞으로 개발할 때 자주 찾아보며 익숙해지려고 합니다.

내용이 길어 기본과 심화로 나누어 포스팅합니다.


Prototype ✨
#

JavaScript의 모든 객체는 내부에 숨겨진 [[Prototype]]을 가지고 있습니다.

prototype

외부에서 직접 접근은 불가하며 아래 속성 통해서 접근이 가능합니다.

  • __proto__
  • Object.getPrototypeOf()
  • Object.setPrototypeOf()
  • 생성자 함수에서는 prototype으로 접근 가능

모두 같은 prototype을 상속받습니다.

Property descriptor
#

각 요소에 대해 디스크립터가 존재하며 각 설정 값은 아래와 같습니다.

  • value: 요소의 실제 값
  • writable: 수정 가능 여부
  • enumerable: 열거 가능 여부
  • configurable: 속성 삭제/변경 가능 여부
const cbum = { name: 'Chris bumstead', division: 'Classic physique' };

// 키 존재 확인
console.log('division' in cbum);
console.log(cbum .hasOwnProperty('name'));

// 요소 존재 확인
console.log(Object.entries(cbum));
console.log(Object.keys(cbum));
console.log(Object.values(cbum));

// 모든 디스크립터 확인
const descriptors = Object.getOwnPropertyDescriptors(cbum);
console.log(descriptors);

// 특정 프로퍼티의 디스크립터 확인
const desc = Object.getOwnPropertyDescriptor(cbum, 'name');
console.log(desc);

Property descriptor 활용 예시
#

// e.g. 프로퍼티 디스크립터 설정 예시
const ruff = {};
Object.defineProperties(ruff, {
  firstName: {
    value: 'Ruffin',
    writable: true,
    enumerable: true,
    configurable: false,
  },
  lastName: {
    value: 'Terrence',
    writable: true,
    enumerable: true,
    configurable: true,
  },
  fullName: {
    get() {
      return `${this.lastName} ${this.firstName}`;
    },
    set(name) {
      [this.lastName, this.firstName] = name.split(' ');
    },
    configurable: true,
  },
});

// 1. 일반 속성 접근
console.log(ruff.firstName);  // 'Ruffin'
console.log(ruff.lastName);   // 'Terrence'

// 2. fullName getter 사용
console.log(ruff.fullName);   // 'Terrence Ruffin'

// 3. fullName setter 사용
ruff.fullName = "Smith John";  // 이름 변경
console.log(ruff.lastName);    // 'Smith'
console.log(ruff.firstName);   // 'John'

// 4. 속성 열거
console.log(Object.keys(ruff));  // ['firstName', 'lastName', 'fullName']

// 5. 속성 수정 (writable: true이므로 가능)
ruff.firstName = "Mike";
console.log(ruff.firstName);  // 'Mike'

Freeze, seal, preventExtensions
#

객체에 아래 속성을 적용할 경우, 아래와 같은 상태가 됩니다.

단, 얕게 (Shallow) 적용이 됩니다. 객체의 요소가 객체일 경우, 해당 요소 객체는 속성에 적김용받지 않습니다.

속성추가삭제수정재정의 (Define property)
Object.preventExtensionsXOOO
Object.sealXXOX
Object.freezeXXXX
const nick = { name: 'Nick Walker', division: 'Open bodybuilding' };
const cbum = { name: 'Chris Bumstead', division: 'Classic physique' };
const ryan = { name: 'Ryan Terry', division: "Men's physique" };

// Object.preventExtensions
Object.preventExtensions(nick);
nick.name = '닉 워커';
console.log(nick);
delete nick.name;
console.log(nick);
nick.birth = 1994;
console.log(nick);

// Object.seal
Object.seal(ryan);
ryan.name = '라이언 테리';
console.log(ryan);
delete ryan.category;
console.log(ryan);

// Object.freeze 
Object.freeze(cbum);
cbum.name = '크리스 범스테드';
console.log(cbum);
cbum.age = 30;
console.log(cbum);
delete cbum.name;
console.log(cbum);

// 확인
console.log(Object.isExtensible(nick));
console.log(Object.isSealed(ryan));
console.log(Object.isFrozen(cbum));

Prototype function Overriding
#

Instance Level Function
#

  • 각 인스턴스에 개별적으로 정의되는 함수입니다.
  • 메모리 사용량이 많아질 수 있습니다.
function Olympian(name, division, cheers) {
  this.name = name;
  this.division = division;
  this.cheers = cheers;
  //Instance level function
  this.workoutInstanceLv = () => {
    console.log(`${this.name}: ${this.cheers}`);
  };
}

Prototype Level Function
#

  • 모든 인스턴스가 공유하는 함수입니다.
  • 메모리 사용이 효율적입니다.
Olympian.prototype.workout = function () {
  console.log(`${this.name}: ${this.cheers}`);
};

호출

// 인스턴스 생성 및 프로토타입 함수 호출
const allright = new Olympian('박재훈', 'Classic physique', 'Allright 24 죽는 느낌으로 🔥🔥🔥');
const thanos = new Olympian('김민수', "Men's physique", '💪💦💦💦');

allright.workout(); // 박재훈: Allright 24 죽는 느낌으로 🔥🔥🔥
thanos.workout(); // 김민수: 💪💦💦💦

Prototype function Overriding
#

  • 특정 인스턴스에서만 프로토타입 레벨의 함수를 덮어씌울 수 있습니다.
  • 이때 오버라이딩된 함수는 해당 인스턴스에만 적용됩니다.

allright.workout = function () {
  console.log('예 마 샤딕입니다');
};

allright.workout(); // 예 마 샤딕입니다
thanos.workout(); // 김민수: 💪💦💦💦

Static Function & Property
#

  • 정적 함수는 인스턴스가 아닌 클래스(생성자 함수)에 직접 정의됩니다.
  • 인스턴스에서 호출할 수 없습니다.
// **정적 함수**
Olympian.overtraining = () => {
  console.log('yeah buddy!');
};

Olympian.overtraining(); // yeah buddy!

// 정적 속성
Olympian.MAX_COUNT = 12;
console.log(Olympian.MAX_COUNT); // 12

Prototype 상속
#

부모 클래스 Olympian

function Olympian(name, division) {
  this.name = name;
  this.division = division;
}

Olympian.prototype.introduce = function () {
  console.log(`${this.name}: ${this.division}`);
};

자식 클래스 Jaehun

function Jaehun(name, division, cheers) {
  Olympian.call(this, name, division); // 부모 생성자 호출
  this.cheers = cheers; // 추가 속성
}

Jaehun.prototype = Object.create(Olympian.prototype);

Jaehun.prototype.allright = function () {
  console.log(`${this.name}: ${this.cheers} 🔥🔥🔥`);
};

자식 클래스 Ronnie

function Ronnie(name, division, cheers) {
  Olympian.call(this, name, division); // 부모 생성자 호출
  this.cheers = cheers; // 추가 속성
}

Ronnie.prototype = Object.create(Olympian.prototype);

Ronnie.prototype.yeah = function () {
  console.log(`${this.name}: ${this.cheers} 💦💦💦`);
};

인스턴스 생성

const jaehun = new Jaehun('박재훈', 'Classic physique', 'Allright 24 죽는 느낌으로');
const ronnie = new Ronnie('Ronnie Coleman', "Open bodybuilding", 'Yeah Buddy!');

메서드 호출

jaehun.introduce(); // 박재훈: Classic physique
jaehun.allright(); // 박재훈: Allright 24 죽는 느낌으로 🔥🔥🔥

ronnie.introduce(); // Ronnie Coleman: Open bodybuilding
ronnie.yeah(); // Ronnie Coleman: Yeah Buddy! 💦💦💦

instanceof 확인

객체가 특정 생성자 함수의 인스턴스인지 확인합니다.

console.log(jaehun instanceof Jaehun); // true
console.log(jaehun instanceof Olympian); // true
console.log(jaehun instanceof Ronnie); // false

console.log(ronnie instanceof Ronnie); // true
console.log(ronnie instanceof Olympian); // true
console.log(ronnie instanceof Jaehun); // false

프로토타입 체인

Jaehun  Olympian  Object.prototype  null
Ronnie  Olympian  Object.prototype  null
  • JaehunRonnie는 모두 Olympian을 통해 Object.prototype에 접근합니다.
  • 각 클래스는 고유 메서드(allright, yeah)를 가지며, 공통 메서드(introduce)는 Olympian.prototype에서 상속받습니다.

mixin: 여러 함수 상속받기
#

Object.assign 을 이용하여 여러 함수를 상속 받을 수 있습니다.

공통 함수
#

const allright = {
  allright: function () {
    console.log(`${this.name}: Allright 24 죽는 느낌으로 🔥🔥🔥`);
  },
};

const yeah = {
  yeah: function () {
    console.log(`${this.name}: Yeah Buddy! 💦💦💦`);
  },
};

Prototype에서 mixin
#

function Ruff(name) {
  this.name = name;
}

Object.assign(Ruff.prototype, allright, yeah);
const ruff = new Ruff('Ruff');
console.log(ruff);
ruff.allright();
ruff.yeah();

Class에서 mixin
#

class Bodybuilder {}
class Ronnie extends Bodybuilder {
  constructor(name) {
    super();
    this.name = name;
  }
}

Object.assign(Ronnie.prototype, allright, yeah);
const ronnie = new Ronnie('Ronnie');
ronnie.allright();
ronnie.yeah();

Closures (클로저) ✨
#

함수가 자신이 생성될 때의 렉시컬 환경(Lexical Environment)을 기억하고, 그 환경에 접근할 수 있는 기능을 의미합니다. 간단히 말해, 클로저는 함수와 함수가 선언된 어휘적 환경(Lexical Scope)의 조합입니다.

기능적으로 보자면, 이너 함수에서 외부 함수의 스코프에 접근하는 것을 뜻합니다.

예시
#

기본적인 클로저 ✅

function outerFunction() {
  let outerVariable = '나는 외부 변수입니다';

  function innerFunction() {
    console.log(outerVariable); // 외부 변수에 접근 가능
  }

  return innerFunction;
}

const closureFunction = outerFunction();
closureFunction(); // "나는 외부 변수입니다!"
  1. outerFunctioninnerFunction을 반환합니다.
  2. closureFunctioninnerFunction을 참조합니다.
  3. innerFunctionouterFunction의 스코프에 접근할 수 있습니다.
  4. closureFunction이 호출될 때 outerVariable에 접근할 수 있는 이유는 클로저 때문입니다.

클로저로 데이터 은닉하기 ✅

function createCounter() {
  let count = 0; // 은닉된 변수

  return {
    increment: function () {
      count++;
      console.log(count);
    },
    decrement: function () {
      count--;
      console.log(count);
    }
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
  1. count 변수는 createCounter 내부에 선언되어 외부에서 직접 접근할 수 없습니다.
  2. incrementdecrement 함수는 count에 접근할 수 있습니다.
  3. count는 클로저를 통해 유지되고, 은닉화된 상태로 안전하게 관리됩니다.

활용 사례
#

  • 데이터 은닉 및 캡슐화

    • 클로저를 사용하면 외부에서 직접 접근할 수 없는 변수를 유지하면서 특정 함수만 그 변수를 수정하거나 읽을 수 있도록 할 수 있습니다.
  • 콜백 함수

    • 이벤트 리스너, 비동기 작업 등에서 클로저가 자주 사용됩니다.
    function makeMultiplier(x) {
      return function(y) {
        return x * y;
      };
    }
    
    const double = makeMultiplier(2);
    console.log(double(5)); // 10
    

메모리 관리
#

  • 클로저는 함수가 종료된 후에도 변수를 참조하기 때문에 메모리 누수(memory leak)가 발생할 가능성이 있습니다.

  • 불필요한 클로저는 해제해야 합니다.

  • e.g. 메모리 누수 방지

    function create() {
      let data = new Array(1000000).fill("메모리 사용");
      return function () {
        console.log(data[0]);
      };
    }
    
    const heavyClosure = create();
    // 클로저가 필요 없어지면 참조를 해제합니다.
    heavyClosure = null;
    

장단점
#

  • 장점
    1. 데이터 은닉화: 외부 접근을 제한해 안전하게 변수를 관리할 수 있습니다.
    2. 상태 유지: 함수 호출 후에도 변수를 유지할 수 있습니다.
    3. 모듈 패턴: JavaScript 모듈 패턴을 구현할 때 유용합니다.
  • 단점
    1. 메모리 누수: 잘못 사용하면 메모리 누수가 발생할 수 있습니다.
    2. 디버깅 어려움: 스코프 체인이 복잡해지면 디버깅이 어려울 수 있습니다.

클로저를 사용할 때 주의할 점
#

  1. 불필요한 변수 유지 방지: 필요하지 않은 변수는 null로 초기화하거나 참조를 제거합니다.
  2. 스코프 체인 확인: 클로저는 상위 스코프를 계속 참조하므로 성능에 영향을 줄 수 있습니다.

클래스를 이용하여 다시 작성
#

class Counter {
  #count; // private 필드 (은닉된 변수)

  constructor() {
    this.#count = 0; // 초기값 설정
  }

  increment() {
    this.#count++;
    console.log(this.#count);
  }

  decrement() {
    this.#count--;
    console.log(this.#count);
  }
}

// 사용 예시
const counter = new Counter();
counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1

This ✨
#

컨텍스트 별 This
#

  • 글로벌 컨텍스트의 this
    • 브라우저: window
    • 노드: module
  • 함수 내부의 this
    • Strict mode에서는 undefined
      • 함수 내부에서는 this 정보가 없기 때문
    • 느슨한 mode에서는 globalThis
  • 생성자 함수 또는 클래스에서의 this
    • 생성될 인스턴스 자체

동적 바인딩
#

JavaScript에서는 호출하는 시점에 따라 동적으로 this가 결정됩니다. 인자에 this가 사용된 함수를 전달하는 경우, 함수 안의 this가 의도치 않은 동작/출력을 할 수 있습니다. 이 경우, 정적 바인딩이 필요합니다.

function BodyBuilding(name) {
  this.name = name;
  this.sayMyName = function () {
    console.log(`MR.Olympia: ${this.name}`);
  };
}

function ClassicPhysique(name) {
  this.name = name;
  this.sayMyName = function () {
    console.log(`Olympia Classic Physique Champion: ${this.name}`);
  };
}

const samson = new BodyBuilding('Samson Dauda');
const cbum = new ClassicPhysique('Chris Bumstead');
samson.sayMyName();
cbum.sayMyName();

cbum.sayMyName = samson.sayMyName;
cbum.sayMyName();
samson.sayMyName();

function prize(callback) {
  console.log('콜백 실행');
  callback();
}

prize(cbum.sayMyName);

출력

콜백 함수 내에 name이 정의되어 있지 않으므로 undefined로 출력됩니다.

MR.Olympia: Samson Dauda
Olympia Classic Physique Champion: Chris Bumstead
Olympia Classic Physique Champion: Samson Dauda
Olympia Classic Physique Champion: Chris Bumstead
콜백 실행
Olympia Classic Physique Champion: undefined

정적 바인딩
#

메서드나 속성이 호출될 대상이 런타임이 아닌 컴파일 타임(정의 시점)에 결정되는 방식입니다. JavaScript에서는 this가 어떤 객체를 가리킬지 호출 시점에 동적으로 결정됩니다. 하지만 정적 메서드는 항상 클래스 자체에 바인딩됩니다.

JavaScript에서 정적 바인딩을 사용하기 위해 아래와 같은 방법이 사용됩니다.

  1. bind 함수를 사용합니다.
  2. Arrorw 함수를 사용합니다.
    1. 생성자로 사용 불가
    2. 근접한 상위 Scope의 this에 정적으로 바인딩
function BodyBuilding(name) {
  this.name = name;

  // 1. bind 함수 이용
  // this.sayMyName = this.sayMyName.bind(this);

  // 2. arrow 함수를 사용
  this.sayMyName = () =>{
    console.log(`MR.Olympia: ${this.name}`);
  };
}

function ClassicPhysique(name) {
  this.name = name;
  this.sayMyName = function () {
    console.log(`Olympia Classic Physique Champion: ${this.name}`);
  };
}

const samson = new BodyBuilding('Samson Dauda');
const cbum = new ClassicPhysique('Chris Bumstead');
samson.sayMyName();
cbum.sayMyName();

samson.sayMyName= cbum.sayMyName;
samson.sayMyName();
cbum.sayMyName();

function prize(callback) {
  console.log('콜백 실행');
  callback();
}

prize(cbum.sayMyName);

출력

컴파일 시점에 this에 정적으로 바인딩되어, 다른 변수에 할당되어도 값을 그대로 가지고 있습니다.

MR.Olympia: Samson Dauda
Olympia Classic Physique Champion: Chris Bumstead
MR.Olympia: Samson Dauda
MR.Olympia: Samson Dauda
콜백 실행
MR.Olympia: Samson Dauda

Strict mode ✨
#

strict mode는 JavaScript 코드의 오류를 더 엄격하게 검사하여 잠재적인 버그를 사전에 발견할 수 있도록 돕는 기능입니다. ES5(ECMAScript 5)에서 도입되었으며, 코드를 더 안전하고 예측 가능하게 만듭니다.

strict mode 활성화 방법
#

전체 스크립트에서 사용하기
#

"use strict";를 스크립트 파일의 맨 위에 추가합니다.

"use strict";

function test() {
  x = 3.14; // ReferenceError: x is not defined
}
test();
  • "use strict";스크립트 전체에 엄격 모드를 적용합니다.
  • 위 코드에서는 변수를 선언 없이 할당했기 때문에 오류가 발생합니다.

함수 단위로 사용하기
#

strict mode를 특정 함수에서만 적용하려면 함수의 첫 번째 줄에 "use strict";를 추가합니다.

function test() {
  "use strict";
  x = 3.14; // ReferenceError: x is not defined
}

test();

function noStrict() {
  y = 3.14; // 정상 동작 (strict mode 미적용)
}

noStrict();

strict mode로 방지할 수 있는 오류
#

  • 변수 선언 없이 사용 금지

    "use strict";
    x = 3.14; // ReferenceError: x is not defined
    
  • 읽기 전용 속성에 값 할당 금지

    "use strict";
    const obj = Object.freeze({ name: "Strict" });
    obj.name = "Non-Strict"; // TypeError: Cannot assign to read only property 'name'
    
  • delete로 변수 삭제 금지

    "use strict";
    let x = 10;
    delete x; // SyntaxError: Delete of an unqualified identifier in strict mode
    
  • 중복 매개변수 이름 금지

    "use strict";
    function duplicateParams(a, a) {
      return a + a; // SyntaxError: Duplicate parameter name not allowed in strict mode
    }
    
  • this의 값 제한

    • strict mode에서는 thisundefined가 됩니다. (기본적으로 글로벌 객체에 바인딩되지 않음)
    "use strict";
    function showThis() {
      console.log(this); // undefined
    }
    showThis();
    
  • 예약어 사용 금지

    • interface, protected, private 등 미래에 예약될 가능성이 있는 식별자를 사용할 수 없습니다.
    "use strict";
    let public = 1; // SyntaxError: Unexpected strict mode reserved word
    

strict mode와 ES6 모듈
#

  • ES6 모듈에서는 기본적으로 strict mode가 적용됩니다. 따로 "use strict";를 선언할 필요가 없습니다.
// ES6 모듈 파일
export function test() {
  x = 10; // ReferenceError: x is not defined
}

주의사항
#

  • 하위 호환성 문제
    • 오래된 브라우저에서는 strict mode가 지원되지 않을 수 있습니다.
  • 문자열로 선언해야 함
    • "use strict";는 문자열이어야 하며, 작은 따옴표나 큰 따옴표로 감싸야 합니다.
  • 스크립트 최상단에 선언 필요
    • strict mode는 반드시 최상단에 선언되어야 합니다.

언제 사용해야 할까?
#

  • 새로운 프로젝트에서는 기본적으로 "use strict";를 사용하는 것이 좋습니다.
  • ES6 모듈을 사용할 경우 별도의 선언은 필요 없습니다.
  • 안전하고 오류가 적은 JavaScript 코드를 작성하고 싶다면 strict mode를 사용하는 것이 필수적입니다.