내부 슬롯과 내부 메서드

  • 내부 슬롯 internal slot과 내부 메서드 internal method는 자바스크립트 엔지의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에 사용하는 의사 프로퍼티 pseudo property와 의사 메서드 pseudo method
  • ECMASciprt 사양에 등장하는 이중 대괄호([[...]])로 감싼 이름에 해당
  • 자바스크립트의 내부 로직이므로 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지는 않음 (일부에 한하여 간접적으로 접근 가능)
const o = {};
// 내부 슬롯은 직접 접근 불가능
o.[[Prototype]] // SyntaxError
// 일부에 한해 간접적으로 접근 가능
o.__proto__; // Object.prototype

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

  • 자바스크립트 엔진은 프로퍼티를 생성할 때, 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의
  • 프로퍼티 어트리뷰트는 직접 접근할 수 는 없지만 Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인 가능
  • 내부 상태 값 meta-property: 프로퍼티 상태
    • [[Value]]: 프로퍼티 값
    • [[Writable]]: 값의 갱신 여부
    • [[Enumerable]]: 열거 가능 여부
    • [[Configurable]]: 재정의 가능 여부
const person = {
  name: "Jin",
};

// Object.getOwnPropertyDescriptor 메서드는 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체 반환
console.log(Object.getOwnPropertyDescriptor(person, "name")); 
// { value: 'Jin', writable: true, enumerable: true, configurable: true }
const person = {
  name: "Jin",
};
person.location = "Seoul";

// ES8에 도입된 Object.getOwnPropertyDescriptors 메서드는 모든 프로퍼티 어트리뷰트 정보 제공
console.log(Object.getOwnPropertyDescriptors(person));
// {
//   name: {
//     value: 'Jin',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   location: {
//     value: 'Seoul',
//     writable: true,
//     enumerable: true,
//     configurable: true
//   }
// }

데이터 프로퍼티와 접근자 프로퍼티

  • 데이터 프로퍼티 data property: 키와 값으로 구성된 일반적인 프로퍼티
  • 접근자 프로퍼티 accessor property: 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수 accessor function으로 구성된 프로퍼티

데이터 프로퍼티

프로퍼티
어트리뷰트
프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Value]] value - 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값
- 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]]에 값을 재할당. 이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고 생성된 프로퍼티의 [[Value]]에 값저장
[[Writable]] writable - 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값
- [[Writable]] 값이 false인 경우 해당 프로퍼티의  [[Value]] 값을 변경할 수 없는 읽기 전용 프로퍼티가 됨
[[Enumerable]] enumerable - 프로퍼티의 열거 가능 여부를 나타내며 불리언 값
- [[Enumerable]] 값이 false인 경우 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거 불가능
[[Configurable]] configurable - 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값
- [[Configurable]] 값이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지 됨. 단, [[Writable]] 값이 true인 경우 [[Value]]의 변경과 [[Writable]]을 false로 변경하는 것은 허용

접근자 프로퍼티

  • 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티
  • 접근자 함수는 getter / setter 함수라고도 부름
프로퍼티
어트리뷰트
프로퍼티 디스크립터 객체의 프로퍼티 설명
[[Get]] get 접근자 프로퍼티를 통해 데이터 프로퍼티 값을 읽을 때 호출되는 접근자 함수. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Set]] set - 접근자 프로퍼티를 통해 데이터 프로퍼티 값을 저장할 때 호출되는 접근자 함수. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환
[[Enumerable]] enumerable - 프로퍼티의 열거 가능 여부를 나타내며 불리언 값
- [[Enumerable]] 값이 false인 경우 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거 불가능
[[Configurable]] configurable - 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값
- [[Configurable]] 값이 false인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지 됨
const person = {
  // 데이터 프로퍼티
  firstName: "HongYeop",
  lastName: "Jin",

  // fullName은 접근자 함수로 구성된 접근자 프로퍼티
  // getter 함수
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set fullName(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조 (setter함수 호출)
console.log(person.firstName + " " + person.lastName); // HongYeop Jin

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
person.fullName = "Ungmo Lee";
console.log(person); // { firstName: 'Ungmo', lastName: 'Lee', fullName: [Getter/Setter] }

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조 (getter함수 호출)
console.log(person.fullName); // Ungmo Lee

// 데이터 프로퍼티 어트리뷰트 확인
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value: 'Ungmo', writable: true, enumerable: true, configurable: true}

// 접근자 프로퍼티 어트리뷰트 확인
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor);
// {get: [Function: get fullName], set: [Function: set fullName], enumerable: true, configurable: true}
// 일반 객체의 __proto__는 접근자 프로퍼티
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
// {enumerable: false, configurable: true, get: ƒ, set: ƒ}

// 함수 객체의 prototype은 데이터 프로퍼티
Object.getOwnPropertyDescriptor(function () {}, "prototype");
// {value: {…}, writable: true, enumerable: false, configurable: false}

프로퍼티 정의

  • Object.defineProperty 메서드를 사용하여 프로퍼티의 어트리뷰트를 정의 가능
  • Object.defineProperties 메서드를 사용하여 여러 개의 프로퍼티 한 번에 정의 가능
const person = {};

Object.defineProperty(person, "firstName", {
  value: "HongYeop",
  writable: true,
  enumerable: true,
  configurable: true,
});

Object.defineProperty(person, "lastName", {
  value: "Jin",
});

let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value: 'HongYeop', writable: true, enumerable: true, configurable: true}

// 디스크립터 객체의 프로퍼티를 누락시키면 undefined, false가 기본 값
descriptor = Object.getOwnPropertyDescriptor(person, "lastName");
console.log(descriptor); // {value: 'Jin', writable: false, enumerable: false, configurable: false}

// [[Enumerable]] 값이 false인 경우
// 해당 프로퍼티는 for ... in 문이나 Object.keys 등으로 열거 불가능
// lastName 프로퍼티는 [[Enumerable]] 값이 false이므로 열거되지 않음
console.log(Object.keys(person)); // ['firstName']

// [[Writable]] 값이 false인 경우 해당 프로퍼티의 [[Value]] 값을 변경할 수 없음
// lastName 프로퍼티는 [[Writable]] 값이 false이므로 값을 변경할 수 없음
// 이때 값을 변경하면 에러는 발생하지 않고 무시
person.lastName = "Park";
console.log(person); // {firstName: 'HongYeop', lastName: 'Jin'} lastName이 변경되지 않음

// [[Configurable]] 값이 false인 경우 해당 프로퍼티를 삭제할 수 없음
// lastName 프로퍼티는 [[Configurable]] 값이 false이므로 값을 삭제할 수 없음
// 이떄 프로퍼티를 삭제하면 에러는 발생하지 않고 무시
delete person.lastName;
console.log(person); // {firstName: 'HongYeop', lastName: 'Jin'} lastName이 삭제되지 않음

// [[Configurable]] 값이 false인 경우 해당 프로퍼티를 재정의할 수 없음
// Object.defineProperty(person, "lastName", { enumerable: true });
// Uncaught TypeError: Cannot redefine property: lastName

// 접근자 프로퍼티 정의
Object.defineProperty(person, "fullName", {
  // getter 함수
  get() {
    return `${this.firstName} ${this.lastName}`;
  },
  // setter 함수
  set(name) {
    [this.firstName, this.lastName] = name.split(" ");
  },
  enumerable: true,
  configurable: true,
});

descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log("fullName", descriptor); // fullName {enumerable: true, configurable: true, get: ƒ, set: ƒ}

person.fullName = "Ungmo Lee";
console.log(person); // {firstName: 'Ungmo', lastName: 'Jin'}
const person = {};

Object.defineProperties(person, {
  // 데이터 프로퍼티 정의
  firstName: {
    value: "HongYeop",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  lastName: {
    value: "Jin",
    writable: true,
    enumerable: true,
    configurable: true,
  },
  // 접근자 프로퍼티 정의
  fullName: {
    // getter 함수
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    // setter 함수
    set(name) {
      [this.firstName, this.lastName] = name.split(" ");
    },
    enumerable: true,
    configurable: true,
  },
});

person.fullName = "UngMo Lee";
console.log(person); // {firstName: 'UngMo', lastName: 'Lee'}

객체 변경 방지

  • 객체는 변경 가능한 값이므로 재할당 없이 직접 변경할 수 있음
  • 자바스크립트는 객체의 변경을 방지하는 다양한 메서드 제공
구분 메서드 프로퍼티
추가
프로퍼티
삭제
프로퍼티
값 읽기
프로퍼티
값 쓰기
프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X
객체 동결 Object.freeze X X O X X

객체 확장 금지

  • Object.preventExtensions 메서드는 객체의 확장 금지
  • 확장이 금지된 객체는 프로퍼티 추가가 금지
  • 프로퍼티의 추가는 동적추가와 Object.defineProperty 두가지 방법 모두 금지
  • 확장 가능 여부는 Object.isExtensible 메서드로 확인 가능
const person = { name: "Jin" };

// person 객체는 확장 가능 객체
console.log(Object.isExtensible(person)); // true

// person 객체 확장 금지
Object.preventExtensions(person);

// person 객체는 확장 불가능 객체
console.log(Object.isExtensible(person)); // false

// 프로퍼티 추가 금지됨
person.age = 29; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 추가 금지되지만 삭제 가능
delete person.name;
console.log(person); // {}

// 프로퍼티 정의에 의한 프로퍼티 추가도 금지
Object.defineProperty(person, "age", { value: 29 });
// TypeError: Cannot define property age, object is not extensible

객체 밀봉

  • Object.seal 메서드는 객체를 밀봉
  • 객체 밀봉이란 프로퍼티 추가 및  삭제와 프로퍼티 어트리뷰트 재정의 금지
  • 즉, 밀봉된 개체는 읽기와 쓰기만 가능
  • 밀봉 여부는 Object.isSealed 메서드로 확인 가능
const person = { name: "Jin" };

// person 객체는 밀봉안된 객체
console.log(Object.isSealed(person)); // true

// person 객체 밀봉
Object.seal(person);

// person 객체는 밀봉된 객체
console.log(Object.isSealed(person)); // false

// 밀봉된 객체는 configurable이 false
console.log(Object.getOwnPropertyDescriptors(person));
// {name: {value: 'Jin', writable: true, enumerable: true, configurable: false}}

// 프로퍼티 추가 금지됨
person.age = 29; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 삭제 금지
delete person.name; // 무시 strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 값 갱신 가능
person.name = "Park";
console.log(person); // { name: 'Park' }

// 프로퍼티 재정의 금지
Object.defineProperty(person, "name", { configurable: true });
// TypeError: Cannot redefine property: name

객체 동결

  • Object.freeze 메서드는 객체를 동결
  • 객체 동결은 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트의 재정의 금지, 프로퍼티 값 갱신 금지 의미
  • 즉, 동결된 개체는 읽기만 가능
  • 동결 여부는 Object.isFrozen 메서드로 확인 가능
const person = {
  name: "Jin",
};

// person 객체는 동결된 객체 아님
console.log(Object.isFrozen(person)); // false

// person 객체 동결. 프로퍼티 추가, 삭제, 재정의, 쓰기 금지
Object.freeze(person);

// person 객체는 동결된 객체
console.log(Object.isFrozen(person)); // true

// 동결된 객체는 writable과 configurable이 false
console.log(Object.getOwnPropertyDescriptors(person));
// {name: {value: 'Jin', writable: false, enumerable: true, configurable: false}}

// 프로퍼티 추가 금지
person.age = 20; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 삭제 금지
delete person.name; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 값 갱신 금지
person.name = "Park"; // 무시. strict mode에서는 에러
console.log(person); // { name: 'Jin' }

// 프로퍼티 어트리뷰트 재정의 금지
Object.defineProperty(person, "name", { configurable: true });
// TypeError: Cannot redefine property: name

불변 객체

  • 지금까지 변경 방지 메서드들은 얕은 변경 방지 shallow only로 직속 프로퍼티만 변경 방지
  • 객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해서 재귀적으로 Object.freeze 메서드를 호출해야 함
const person = {
  name: "Jin",
  address: { city: "Seoul" },
};

// 얕은 객체 동결
Object.freeze(person);

// 직속 프로퍼티만 동결
console.log(Object.isFrozen(person)); // true
// 중첩 객체는 동결하지 못함
console.log(Object.isFrozen(person.address)); // false
// 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Oject.freeze 메서드를 호출하는 함수
function deepFreeze(target) {
  // 객체가 아니거나 동결된 객체는 무시하고 동결되지 않은 객체만 동결
  if (target && typeof target === "object" && !Object.isFrozen(target)) {
    Object.freeze(target);
    // Oject.keys 메서드로 열거 가능한 프로퍼티 키를 배열로 반환,
    // forEach 메서드로 해당 배열을 순회하여 배열의 각 요소에 대해 콜백함수 실행
    Object.keys(target).forEach((key) => deepFreeze(target[key]));
  }
  return target;
}

const person = {
  name: "Jin",
  address: { city: "Seoul" },
};

deepFreeze(person);

console.log(Object.isFrozen(person)); // true
// 중첩 객체는 동결하지 못함
console.log(Object.isFrozen(person.address)); // true

var 키워드로 선언한 변수의 문제점

변수 중복 선언 허용

  • var 키워드로 선언한 변수는 중복 선언 가능
  • 선언한 변수를 중복 선언하면
    • 초기화문이 있는 변수 선언문은 var 키워드가 없는 것처럼 동작
    • 초기화문이 없는 변수 선언문은 무시되고 에러 발생되지 않음
var x = 1;
var y = 1;

// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언 허용
// 초기화문이 있는 경우: var 키워드가 없는 것처럼 동작
var x = 100;
console.log(x); // 100

// 초기화문이 없는 경우: 변수 선언문 무시
var y;
console.log(y); // 1

함수 레벨 스코프

  • var 키워드로 선언한 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정
var x = 1;

// if문의 변수 선언문에서 var 키워드로 선언한 변수도 전역변수가 됨
if (true) {
  var x = 10;
}

console.log(x); // 10

// for문의 변수 선언문에서 var 키워드로 선언한 변수도 전역변수가 됨
for (var x = 0; x < 5; x++) {}

console.log(x); // 5

변수 호이스팅

  • var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 올려진 것처럼 동작
  • 할당문 이전에 변수를 참조할 경우 undefined 반환
// 이 시점에
// 1. 호이스팅에 의해 변수 foo는 이미 선언
// 2. foo는 undefined로 초기화
console.log(foo); // undefined

foo = 123;

console.log(foo); //123

// 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 실행
var foo;

let 키워드

변수 중복 선언 금지

  • let 키워드로 이름이 같은 변수를 중복 선언하면 문법 에러 발생 SyntaxError
var foo = 123;
// var 키워드 중복 선언 허용
// 아래 변수 선언문은 var 키워드가 없는 것처럼 동작
var foo = 456;

let bar = 123;
// let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 변수 중복 선언 허용 x
let bar = 456; // SyntaxError: Identifier 'bar' has already been declared

블록 레벨 스코프

  • let 키워드로 선언한 변수는 모든 코드 블록 (함수, if 문, for문, while문, try/catch 문 등)을 지역 스코프로 인정하는 블록 레벨 스코프 block-level scope를 따름

블록 레벨 스코프의 중첩

변수 호이스팅

  • var 키워드로 선언한 변수와 달리 let 키워드로 선언한 변수는 변수 호이스팅이 발생하지 않는 것처럼 동작
  • let 키워드로 선언한 변수는 "선언 단계"와 "초기화 단계"가 분리되어 진행
  • 초기화 단계가 실행되기 이전에 변수를 접근하려고 하면 참조 에러 ReferenceError 발생
  • 일시적 사각지대  TDZ; Temporal Dead Zone: 스코프의 시작 시점부터 초기화 시작 시점까지 변수를 참조할 수 없는 구간
  • 호이스팅이 발생하지 않는 것처럼 동작하는 것이지 실제로 호이스팅은 발생
console.log(foo); 
// 브라우저: ReferenceError: foo is not defined
// NodeJs: ReferenceError: Cannot access 'foo' before initialization
let foo;
// var
// 선언한 변수는 런타임 이전에 선언 단계와 초기화 단계가 실행
// 따라서 변수 선언 이전에 변수를 참조 가능
console.log(foo); // undefined

var foo;
console.log(foo); // undefined

foo = 1;
console.log(foo); // 1

// let
// 런타임 이전에 선언 단계가 실행 / 초기화는 진행 x
// 초기화 이전의 일시적 사각 지대에서는 변수 참조 불가능
console.log(bar); // ReferenceError: Cannot access 'bar' before initialization

let bar; // 변수 선언문에서 초기화 실행
console.log(bar); // undefined

bar = 2; // 할당문에서 할당 실행
console.log(bar); // 2

var vs let 변수의 생명주기

// let의 호이스팅
let foo = 1; // 전역 변수

{
  console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
  let foo = 2; // 지역 변수
}

전역 객체와 let

  • var 키워드로 선언한 전역 변수와 전역 함수, 선안하지 않은 변수에 값을 할당한 암묵적 전역은 전역 객체 window의 프로퍼티가 됨
  • 참고: 전역 객체의 프로퍼티를 참조할 때 window를 생략할 수 있음
  • let 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아님
  • let 전역 변수는 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재
// 전역 변수
var x = 1;
// 암묵적 전역
y = 2;
// 전역 함수
function foo() {}

// var키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티
console.log(window.x); // 1
// 전역 객체 window의 프로퍼티는 전역 변수처럼 사용 가능
console.log(x); // 1

// 암묵적 전역은 전역 객체 window의 프로퍼티
console.log(window.y); // 2
console.log(y); // 2

// 함수 선언문으로 정의한 전역 함수는 전역 객체 window의 프로퍼티
console.log(window.foo); // ƒ foo() {}
console.log(foo); // ƒ foo() {}
let x = 1;

// let, const키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티가 아님
console.log(window.x); // undefined
console.log(x); // 1

const 키워드

  • const 키워드는 상수 const를 선언하기 위해 사용
  • 반드시 상수만을 위해 사용하지는 않음

선언과 초기화

  • const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 함
  • let 키워드와 마찬가지로 블록 레벨 스코프, 변수 호이스팅이 발생하지 않는 것처럼 동작
// const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화
const foo = 1;

const bar // SyntaxError: Missing initializer in const declaration

재할당 금지

  • const 키워드로 선언한 변수는 재할당이 금지
  • var 또는 let 키워드로 선언한 변수는 재할당이 자유로움
const foo = 1;
foo = 2; // TypeError: Assignment to constant variable.

상수

  • const 키워드로 선언한 변수에 원시 값을 할당한 경우 변수 값을 변경할 수 없음. 원시 값은 변경 불가능한 값이므로 해당 변수에는 재할당 없이 값을 변경 불가능. 이 특징을 이용해 const 키워드를 상수를 표현하는데 사용
  • 상수: 재할당이 금지된 변수
  • 일반적으로 상수의 이름은 대문자로 선언해 상수임을 명확히 나타냄. 여러 단어로 이뤄진 경우에는 언더스코어(_)로 구분해서 스네이크 케이스로 표현하는 것이 일반적
const TAX_RATE = 0.1;

let preTaxPrice = 100;
let afterTaxPrice = preTaxPrice + preTaxPrice * TAX_RATE;

console.log(afterTaxPrice); // 110

const 키워드와 객체

  • const 키워드로 선언된 변수에 객체를 할당한 경우 값을 변경할 수 있음
  • const 키워드는 재할당을 금지할 뿐, "불변"을 의미하지 않음
const person = {
  name: "Jin",
};

// 객체는 변경 가능한 값. 따라서 재할당 없이 변경 가능
person.name = "Park";

console.log(person); // { name: 'Park' }

var vs. let vs. const

  • ES6를 사용한다면 var 키워드는 사용하지 않음
  • 재할당이 필요한 경우에 한정해 let 키워드 사용. 이때 변수의 스코프는 최대한 좁게
  • 변경이 발생하지 않고 읽기 전용으로 사용하는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 사용
  • ES6 기준: 기본적으로 const 사용하되 재할당이 필요한 경우에만 let 사용
  • 전역 변수의 무분별한 사용은 위험
  • 전역 변수를 반드시 사용해야할 이유를 찾기 못한다면 지역 변수를 사용해야 함

변수의 생명 주기

지역 변수의 생명 주기

  • 일반적으로 지역 변수의 생명 주기는 함수의 생명 주기와 일치
  • 변수의 생명 주기는 메모리 공간이 확보 allocate된 시점부터 메모리 공간이 해체 release되어 가용 메모리 풀 memory pool에 반환되는 시점
  • 지역 변수의 스코프가 함수 종료 이후에도 참조되고 있다면 스코프는 해체되지 않고 생존, 지역 변수는 함수가 생성한 스코프에 등록되므로 해당 경우에는 함수가 종료되더라도 지역 변수가 생존할 수 있음
  • 호이스팅은 스코프 단위로 동작. 변수 선언이 스코프의 선두로 올려진 것처럼 동작하게 됨
function foo() {
  var x = "local";
  console.log(x); // local
  return x;
}

foo();
console.log(x); // ReferenceError: x is not defined

var x = "global";

function foo() {
  console.log(x); // undefined 해당 시점에 지역 변수 x는 선언되었고 undefined로 초기화
  var x = "local";
}

foo();
console.log(x); // global

전역 변수의 생명 주기

  • var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치
  • 전역 객체: 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체

전역 변수의 문제점

  • 암묵적 결합 implicit coupling: 변수 유효 범위가 클수록 코드의 가독성은 나빠지고 의도치 않게 상태가 변경 가능
  • 긴 생명 주기
  • 스코프 체인 상에서 종점에 존재, 전역 변수의 검색 속도가 가장 느림
  • 네임스페이스 오염

전역 변수의 사용을 억제하는 방법

즉시 실행 함수

  • 즉시 실행 함수로 감싸면 해당 변수는 즉시 실행 함수의 지역 변수가 됨
(function () {
  var foo = 10;
})();

console.log(foo); // ReferenceError: foo is not defined

네임스페이스 객체

  • 전역에 네임스페이스 namespace 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법
  • 네임스페이스 객체에 또 다른 네임 스페이스 객체를 추가하는 방법으로 계층적으로 구성 가능
  • 네임스페이스 자체가 전역 변수에 할당되므로 그다지 유용하지 않음
var MYAPP = {}; // 전역 네임스페이스 객체

MYAPP.person = {
  name: "Jin",
  address: "Seoul",
};

console.log(MYAPP.person.name); // Jin

모듈 패턴

  • 모듈 패턴은 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감사 하나의 모듈을 만듦
  • 모듈 패턴은 자바스크립트의 강력한 기능인 클로저를 기반으로 동작
  • 전역 변수의 억제와 캡슐화까지 구현 가능
  • 캡슐화  encapsulation는 객체의 상태 state를 나타내는 프로퍼티와 프로퍼티를 참조하고 도작할 수 있는 동작 behavior인 메서드를 하나로 묶는 것, 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용되기도 하는데 이를 정보 은닉 information hiding 이라고 함
  • 자바스크립트는 public, private, protected 등의 접근 제한자를 제공하지 않지만, 타입스크립트는 제공함
var Counter = (function () {
  // private 변수
  var num = 0;
  // 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환
  return {
    increase() {
      return ++num;
    },
    decrease() {
      return --num;
    },
  };
})();

// private 변수는 외부로 노출되지 않음
console.log(Counter.num); // undefined
console.log(Counter.increase()); // 1
console.log(Counter.decrease()); // 0

ES6 모듈

  • ES6 모듈을 사용하면 더는 전역 변수를 사용할 수 없음
  • ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공
  • 모던 브라우저에서는 ES6 모듈을 사용할 수 있음. sript 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립 파일은 모듈로 동작. 모듈의 파일 확장자는 mjs 권장
  • IE 같은 구형 브라우저에서는 Webpack 등의 모듈 번들러를 사용하는 것이 일반적
<script type="module" src="lib.mjs"></script>
<script type="module" src="app.mjs"></script>

스코프란?

  • 스코프 scope (유효범위): 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정,
    • 식별자가 유효한 범위
    • 식별자를 검색할 때 사용하는 규칙
    • 네임스페이스
  • 식별자 결정 identifier resolution: 자바스크립트 엔진이 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야할 것인지 결정
  • 식별자는 어떤 값을 구별할 수 있으므로 유일 unique 해야함, 즉 하나의 값은 유일한 식별자에 연결 name binding 되어야 함
var var1 = 1; // 코드의 가장 바깥에서 선언한 변수

if (true) {
  var var2 = 2; // 코드 블록 내에서 선언한 변수
  if (true) {
    var var3 = 3; // 중첩된 코드 블록 내에서 선언한 변수
  }
}

function foo() {
  var var4 = 4; // 함수 내에서 선언한 변수

  function bar() {
    var var5 = 5; // 중첩된 함수 내에서 선언한 변수
  }
}

console.log(var1); // 1
console.log(var2); // 2
console.log(var3); // 3
console.log(var4); // ReferenceError: var4 is not defined
console.log(var5); // ReferenceError: var5 is not defined
var x = "global";

function foo() {
  var x = "local";
  console.log(x);
}

foo();

console.log(x);

스코프는 네임스페이스

스코프의 종류

  • 코드는 전역 global과 지역 local으로 구분
  • 변수는 자신이 선언된 위치(전역 또는 지역)에 의해 자신이 유효한 범위인 스코프가 결정
구분 설명 스코프 변수
전역 코드의 가장 바깥 영역 전역 스코프 전역 변수
지역 함수 몸체 내부 지역 스코프 지역 변수

전역과 전역 스코프

  • 전역이란 코드의 가장 바깥 영역을 의미
  • 전역은 전역 스코프 global scope를 만듦
  • 전역에 변수를 선언하면 전역 스코프를 갖는 전역 변수 global variable이 됨
  • 전역 변수는 어디서든지 참조할 수 있음
var x = "global x";
var y = "global y";

function outer() {
  var z = "outer's local z";

  console.log(x); // global x
  console.log(y); // global y
  console.log(z); // outer's local z

  function inner() {
    var x = "inner's local x";

    console.log(x); // inner's local x
    console.log(y); // global y
    console.log(z); // outer's local z
  }

  inner();
}

outer();

console.log(x); // global x
console.log(y); // global y
console.log(z); // ReferenceError: z is not defined

전역 스코프와 지역 스코프

지역과 지역 스코프

  • 지역이란 함수 몸체 내부
  • 지역은 지역 스코프 local scope를 만듦
  • 지역에 변수를 선언하면 지역 스코프를 갖는 지역 변수 local variable이 됨
  • 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효

스코프 체인

  • 스코프가 함수의 중첩에 의해 계층적 구조를 갖음
  • 스코프 체인 scope chain: 모든 스코프가 계층적 구조로 연결
  • 변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색 identifier resolution
  • 스코프 체인은 물리적인 실체로 존재하며, 렉시컬 환경 Lexical Environment를 실제로 생성
  • 변수 선언이 실행되면 변수 식별자가 렉시컬 환경에 키 key로 등록되고, 변수 할당이 일어나면 변수 식별자에 해당하는 값을 변경

스코프 체인

스코프 체인에 의한 변수 검색

  • 자바스크립트 엔진은 스코프 체인을 따라 변수를 참조하는 코드의 스코프에서 시작해서 사위 스코프 방향으로 이동하며 선언된 변수를 검색
  • 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조 가능
  • 하위 스코프에서 유효한 변수를 상위 스코프에서는 참조할 수 없음

스코프 체인에 의한 함수 검색

  • 함수도 식별자에 해당되기 때문에 스코프를 갖음
  • 함수는 식별자에 함수 객체가 할당된 것 외에는 일반 변수와 다를 바 없음

함수 레벨 스코프

  • 블록 레벨 스코프 block level scope: 모든 코드 블록 (if, for, while, try/catch emd)이 지역 스코프를 만듦 (대부분의 프로그래밍 언어; C, 자바)
  • 함수 레벨 스코프 function level scope: 오로지 함수 코드 블록 (함수 몸체)만을 지역 스코프로 인정 ( var 키워드로 선언된 변수)
  • ES6에서 도입된 let, const 키워드는 블록 레벨 스코프를 지원

렉시컬 스코프

  • 동적 스코프 dynamic scope: 함수를 어디서 호출했는지에 따라 함수의 상위 스코프 결정
  • 렉시컬 스코프 lexical scope 혹은 정적 스코프 static scope: 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정
  • 자바스크립트는 렉시컬 스코프를 따름
  • 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정되며, 생성된 객체는 이렇게 결정된 상위 스코프를 기억함
var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 1
bar(); // 1

// foo 내부에서 호출한 bar는 전역 스코프에서 정의되었기 때문에
// bar가 호출될 때 전역 스코프의 x를 참조함
  • 수학의 함수: 입력을 받아 출력을 내보내는 일련의 과정
  • 프로그래밍의 함수: 일련의 과정을 문 statement로 구현하고, 코드 블록으로 감싸서 하나의 실행 단위로 정의한 것
  • 함수의 구성 요소
    • 매개변수 parameter: 함수 내부로 입력을 전달받는 변수
    • 인수 argument: 입력
    • 출력 return value: 출력
  • 함수는 함수 정의 function definition을 통해 생성
  • 함수 호출 function call/invoke: 인수를 매개 변수를 통해 함수에 전달하면서 함수의 실행을 명시적으로 지시

// 함수 정의
function add(x, y) {
  return x + y;
}
// 함수 호출
add(2, 5);

함수를 사용하는 이유

  • 코드의 재사용
  • 유지보수의 편의성
  • 코드의 신뢰성
  • 코드의 가독성

함수 리터럴

  • 함수는 객체 타입의 값
  • 일반 객체는 호출할 수 없지만 함수는 호출할 수 있음. 일반 객체에는 없는 함수 객체 고유의 프로퍼티 갖음
  • 함수 리터럴은 function 키워드, 함수 이름, 매개 변수 목록, 함수 몸체로 구성
구성 요소 설명
함수 이름 함수 이름은 식별자. 따라서 식별자 네이밍 규칙을 준수
함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자
함수 이름은 생략 가능. 이름이 있는 함수 기명함수 named function, 이름이 없는 함수를 무명/익명 함수 anonymous function이라 함
매개변수 목록 0개 이상의 매개 변수를 소괄호로 감싸로 쉼표로 구분
각 매개변수에는 함수를 호출할 때 지정한 인수가 순서대로 할당. 즉, 매개변수 목록은 순서에 의미가 있음
매개변수는 함수 몸체 내에서 변수와 동일하게 취급. 따라서 식별자 네이밍 준수
함수 몸체 함수가 호출되었을 때 일괄적으로 실행될 문들은 하나의 실행 단위로 정의한 코드 블록
함수 몸체는 함수 호출에 의해 실행

 

// 변수에 함수 리터럴을 할당
var f = function add(x, y) {
  return x + y;
};

함수 정의

  • 함수를 정의하는 4가지 방법
    • 함수 선언문 
    • 함수 표현식
    • Function 생성자 함수
    • 화살표 함수(ES6)
  • 각 정의 방식에는 차이가 있음
  • 변수는 선언  declaration / 함수는 정의 definition
  • 함수 선언문이 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당됨
// 함수 선언문
function add(x, y) {
  return x + y;
}
// 함수 표현식
var add = function (x, y) {
  return x + y;
};
// Function 생성자 함수
var add = new Function("x", "y", "return x + y");
// 화살표 함수(ES6)
var add = (x, y) => x + y;

함수 선언문

  • 함수 선언문은 함수 리터럴과 형태가 동일하나, 함수 선언문은 함수 이름을 생략할 수 없음
  • 함수 선언문은 표현식이 아닌 문 (원래 표현식이 아닌 문은 변수에 할당할 수 없음)
  • 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거이에 함수 객체를 할당
  • 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 식별자로 호출
// 함수 선언문
function add(x, y) {
  return x + y;
}
// 실제 동작하는 느낌
// 좌 식별자, 우 함수 이름
var add = function add(x, y) {
  return x + y;
};
// 식별자로 호출
console.log(add(2, 5)); // 7

함수 표현식

  • 자바스크립트의 함수는 일급 객체
  • 함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있음
  • 함수 리터럴의 함수 이름은 생략할 수 있으며 표현식에서 일반적으로 함수 이름 생략
  • 함수 선언문은 표현식이 아닌 문 / 함수 표현식은 표현식인 문
// 기명 함수 표현식
var add = function foo(x, y) {
  return x + y;
};

// 함수 객체를 가리키는 식별자로 호출
console.log(add(2, 5)); // 7

// 함수 이름으로 호출하면 ReferenceError 발생
// 함수 이름은 함수 몸체 내부에서만 유효한 식별자
console.log(foo(2, 5)); // ReferenceError: foo is not defined

함수 생성 시점과 함수 호이스팅

  • 함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성 시점이 다름
  • 함수 선언문으로 함수를 정의하면 런타임 이전에 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당
  • 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트의 고유 특징을 함수 호이스팅 function hoisting 이라고 함
  • 변수 할당문의 값은 할당문이 실행되는 시점, 즉 런타임에 평가되므로 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 됨
  • 함수 표현식으로 함수를 정의하면 함수 호이스팅이 아닌 변수 호이스팅이 발생
  • 위 특성 때문에 JSON 창시자 더글라스는 함수 선언문 대신 함수 표현식 사용 권장
// 함수 참조
console.dir(add); // f add(x, y)
console.dir(sub); // undefined

// 함수 호출
console.log(add(2, 5)); // 7;
console.log(sub(2, 5)); // TypeError: sub is not a function

// 함수 선언문
function add(x, y) {
  return x + y; 
}
// 함수 표현식
var sub = function (x, y) {
  return x - y;
};

Function 생성자 함수

  • 빌트인 함수 Function 생성자 함수에 매개 변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환 (new 연산자 없이 호출해도 결과는 동일)
  • Function 생성자 함수로 함수를 생성하는 방식은 일반적이지 않으며 바람직하지 않음
  • 클로저 closure를 생성하지 않는 등 함수 선언문이나 함수 표현식으로 생성한 함수와 다르게 동작함
var add1 = (function () {
  var a = 10;
  return function (x, y) {
    return x + y + a;
  };
})();

console.log(add1(1, 2)); // 13

var add2 = (function () {
  var a = 10;
  return new Function("x", "y", "return x + y + a");
})();

console.log(add2(1, 2)); // ReferenceError: a is not defined

화살표 함수

  • ES6에서 도입된 화살표 함수 arrow function는 function 키워드 대신 화살표 fat arrow =>를 사용해 좀 더 간략한 방법으로 함수 선언 가능
  • 화살표 함수는 항상 익명 함수로 정의
  • 내부 동작 또한 간략화 되어 있음
    • 생성자 함수로 사용 불가능
    • 기존 함수와 this 바인딩이 다름
    • prototype 프로퍼티가 없음
    • arguments 객체를 생성하지 않음
// 화살표 함수
const add = (x, y) => x + y;
console.log(add(2, 5)); //7

함수 호출

  • 함수를 가리키는 식별자와 한쌍의 소괄호인 함수 호출 연산자로 호출
  • 함수 호출 연산자 내에는 0개 이상의 인수를 쉼표로 구분해서 나열
  • 함수를 호출하면 현재의 실행 흐름을 중단하고 호출된 함수로 실행 흐름을 옮김
  • 이 때 매개 변수가 순서대로 할당되고 함수 몸체의 문들이 실행

매개변수와 인수

  • 함수를 실행하기 위해 필요한 값을 함수 외부에서 함수 내부로 전달할 필요가 있는 경우, 매개변수 parameter(인자)를 통해 인수 argument를 전달
  • 인수는 값으로 표현될 수 있는 표현식, 개수와 타입에 제한 없음
  • 매개변수는 함수 몸체 내부에서만 참조할 수 있고 함수 몸체 외부에서는 참조 불가능
  • 함수는 매개변수의 개수와 인수의 개수 일치 여부 판단 x
    • 인수가 부족할 경우: 할당되지 않은 매개변수의 값은 undefined
    • 인수가 더 많을 경우: 초과된 인수는 무시됨, 암묵적으로 arguments  객체의 프로퍼티로 보관
function add(x, y) {
  console.log(arguments);
  return x + y;
}

console.log(add(2)); 
// Arguments [2, callee: ƒ, Symbol(Symbol.iterator): ƒ] 
// NaN
console.log(add(1, 2, 4));
// Arguments(3) [1, 2, 4, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 3

인수 확인

  • 자바스크립트 엔진 인수 확인 문제점
    • 매개변수와 인수의 개수가 일치하는지 확인 안함
    • 자바스크립트는 동적 타입 언어, 매개변수 타입을 사전에 지정 불가능
  • 함수를 정의할 때 적절한 인수가 전달되었는지 확인 필요
  • arguments 객체를 통해 인수 개수 확인 가능
  •  ES6에 도입된 매개변수 기본 값을 사용하면 함수 내에서 수행하던 인수 체크 및 초기화를 간소화 가능. 매개변수 기본값은 매개변수에 인수를 전달하지 않을 경우와 undefined를 전달하였을 경우에만 유효
  • 타입스크립트 같은 정적 타입을 선언할 수 있는 자바스크립트의 상위 확장을 도입하여 컴파일 시점에 부적절한 호출 을 방직하는 것도 하나의 방법
// 적절한 인수 전달 확인
function add(x, y) {
  if (typeof x !== "number" || typeof y !== "number") {
    throw new TypeError("인수는 모두 숫자 값이어야 합니다");
  }
  return x + y;
}

console.log(add(2)); // TypeError: 인수는 모두 숫자 값이어야 합니다
console.log(add(1, "a")); // TypeError: 인수는 모두 숫자 값이어야 합니다
// 기본 값 할당
function add1(a, b, c) {
  a = a || 0;
  b = b || 0;
  c = c || 0;
  return a + b + c;
}

// 기본 값 할당 ES6
function add2(a = 0, b = 0, c = 0) {
  return a + b + c;
}

매개변수의 최대 개수

  • 매개 변수가 많아지면 유지보수성이 떨어지고 사용이 힘들어짐
  • 이상적인 함수는 한가지 일만 해야 하며 가급적 작게 만들어야 함
  • 매개 변수는 최대한 0개에 가깝게 유지해야하며 3개를 넘기지 않도록, 그 이상의 매개변수를 전달할 경우에는 하나의 매개변수를 선언하고 객체를 인수로 전달
  • 다만 함수 외부에서 내부로 전달한 객체를 함수 내부에서 변경할 경우 함수 외부의 객체가 변경되는 부수 효과 side effect 발생 유의
// jQuery의 ajax 메서드에 객체를 인수로 전달 예시
$.ajax({
  method: "POST",
  url: "/user",
  data: { id: 1, name: "Jin" },
  cache: false,
});

반환문

  • 함수는 return 키워드와 표현식(반환값)으로 이뤄진 반환문을 사용해 함수 외부로 반환 return 할 수 있음
  • 반환문은 생략 가능
  • 반환문의 역할
    • 반환문은 함수의 실행을 중단하고 함수 몸체를 빠져나감 (반환문 이후의 다른 문 무시)
    • return 키워드 뒤에 오는 표현식을 평가해 반환, 표현식을 명시적으로 지정안할 경우 undefined 반환
function add(x, y) {
  return x + y;
  console.log("실행되지 않음"); // 반환문 이후의 다른 문은 실행되지 않고 무시
}

var result = add(1, 2); // 함수 호출은 반환 값으로 평가

참조에 의한 전달과 외부 상태의 변경

  • 원시 타입 인수는 값 자체가 복사되어 매개변수에 전달되기 때문에 함수 몸체에서 그 값을 변경해도 원본은 훼손되지 않음
  • 객체 타입 인수는 참조 값이 복사되어 매개변수에 전달되기 때문에 함수 몸체에서 그 값을 변경했을 경우 원본도 훼손 (부수효과)
  • 대응 방법
    • 옵저버 패턴 등을 통해 객체를 참조를 공유하는 모든 이들이 객체 변경을 추적할 수 있도록 함
    • 객체를 불변 객체 immutable object로 만들어 사용
  • 순수함수: 외부 상태를 변경하지 않고, 외부 상태에 의존하지 않는 함수
  • 순수함수를 통해 부수효과를 최대한 억제하여 오류를 피하고 프로그래밍의 안정성을 높이는 프로그래밍 패러다임을 함수형 프로그래밍이라고 함
function changeVal(primitive, obj) {
  primitive += 100;
  obj.name = "Jin";
}

// 외부 상태
var num = 100;
var person = { name: "Park" };

console.log(num); // 100
console.log(person); // { name: 'Park' }

// 원시 값은 값 자체가 복사되어 전달되고 객체는 참조 값이 복사되어 전달
changeVal(num, person);
// 원시 값은 값 원본이 훼손되지 않음
console.log(num); // 100
// 객체는 원본이 훼손
console.log(person); // { name: 'Jin' }

다양한 함수 형태

즉시 실행 함수

  • 함수의 정의와 동시 즉시 호출되는 함수를 함수를 즉시 실행 함수 IIFE, Immediately Invoked Function Expression
  • 즉시 실행 함수는 단 한 번만 호출되며, 다시 호출 불가
  • 즉시 실행 함수는 익명 함수를 사용하는 것이 일반적, 기명 실행 함수도 실행 가능하나, 기명 함수는 함수 선언문이 아니라 함수 리터럴로 평가되기 때문에 함수 이름은 함수 몸체에서만 참조 가능한 식별자이므로 다시 호출할 수 없음 (의미 없음)
  • 즉시 실행함수는 그룹 연산자 (...)로 감싼 뒤 사용
  • 즉시 실행 함수는 변수나 함수 이름의 충돌을 방지할 수 있음
// 즉시 실행 함수도 일반 함수처럼 값을 반환 가능
var res = (function () {
  var a = 3;
  var b = 5;
  return a + b;
})();
console.log(res); // 8

// 즉시 실행 함수도 일반 함수처럼 인수 전달 가능
res = (function (a, b) {
  return a + b;
})(3, 5);
console.log(res); // 8

재귀 함수

  • 재귀 호출 recursive call: 함수가 자기 자신을 호출
  • 재귀 함수 recursive function: 재귀 호출을 수행하는 함수
  • 함수 내부에서 자기 자신을 호출할 때 사용한 식별자인 함수 이름은 함수 몸체 내부에서만 유효
  • 재귀 함수 내에서는 재귀 호출을 멈출 수 있는 탈출 조건이 반드시 있어야 함. 탈출 조건이 없을 경우 스택 오버플로 stack overflow 에러가 발생
// 팩토리얼
function factorial(n) {
  // 탈출 조건: n이 1 이하일 떄 재귀 호출 중단
  if (n <= 1) return 1;
  // 재귀 호출
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

 

중첩 함수

  • 함수 내부에 정의된 함수를 중첩 함수 nested function 또는 내부 함수 inner function이라고 함
  • 중첩 함수를 포함하고 있는 함수는 외부 함수 outer function이라고 함
  • 중첩 함수는 외부 함수 내에서만 호출 가능
  • 일반적으로 중첩함수는 자신을 포함하는 외부 함수를 돕는 헬퍼 함수 helper function의 역할을 함
  • ES6부터 함수 정의는 문이 위치할 수 있는 문맥이라면 어디든지 가능 (if문이나 for문 등의 코드 블록 내 등)
  • ES6 이전에는 코드의 최상위 또는 다른 함수 내부에서만 정의 가능
  • 호이스팅으로 인해 혼란이 발생할 수 있으므로 if 문이나 for 문 등의 코드 블록에서 함수 선언문을 통해 함수를 정의하는 것은 지양
  • 중첩 함수는 스코프와 클로저에 깊은 관련이 있음
function outer() {
  var x = 1;
  // 중첩 함수
  function inner() {
    var y = 2;
    // 외부 함수의 변수 참조 가능
    console.log(x + y); // 3
  }
  inner();
}

outer();

콜백 함수

  • 콜백 함수 callback function: 함수의 매개변수를 통해 다른 함수의 내부로 전달되는 함수
  • 고차 함수 HOF Higher-Order Function: 매개변수를 통해 함수의 외부에서 콜백함수를 전달받은 함수
  • 고차 함수는 매개변수로 받은 콜백 함수를 자신의 일부분으로 합성하고, 호출 시점을 결정해서 호출하며, 필요에 따라서 콜백 함수에 인수를 전달 할 수 있음
  • 함수형 프로그래밍 패러다임뿐만 아니라 비동기 처리(이벤트 처리, Ajax 통신, 타이머 함수) 등에 사용되는 중요 패턴
  • 배열 고차 함수에도 사용
// 외부에서 전달받은 f를 n만큼 반복 호출
function repeat(n, f) {
  for (var i = 0; i < n; i++) {
    f(i); // i를 전달하면서 f를 호출
  }
}

var logAll = function (i) {
  console.log(i);
};

repeat(5, logAll); // 0 1 2 3 4

var logOdds = function (i) {
  if (i % 2) console.log(i);
};

repeat(5, logOdds); // 1 3
// 콜백 함수를 사용한 이벤트 처리
document.getElementById("myButton").addEventListener("click", function () {
  console.log("button clicked");
});

// 콜백 함수를 사용한 비동기 처리
setTimeout(function () {
  console.log("1초 경과");
});

// 콜백 함수를 사용하는 고차 함수 map
var res = [1, 2, 3].map(function (item) {
  return item * 2;
});
console.log(res); // [ 2, 4, 6 ]

// 콜백 함수를 사용하는 고차 함수 filter
res = [1, 2, 3].filter(function (item) {
  return item % 2;
});
console.log(res); // [ 1, 3 ]

// 콜백 함수를 사용하는 고차 함수 reduce
res = [1, 2, 3].reduce(function (acc, cur) {
  return acc + cur;
}, 0);
console.log(res); // 6

순수 함수와 비순수 함수

  • 순수 함수 pure function: 어떤 외부 상태에 의존하지도 않고, 변경하지 않는, 즉 부수 효과가 없는 함수
  • 비순수 함수 impure function: 외부 상태에 의존하거나 외부 상태를 변경하는, 즉 부수 효과가 있는 함수
  • 함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 외부 상태를 변경하는 부수 효과를 최소화해서 불변성을 지향하는 프로그래밍 패러다임
var count = 0;

// 순수 함수 increase는 동일한 인수가 전달되면 언제나 동일한 값을 반환
function increase(n) {
  return n++;
}

// 순수 함수가 반환한 결과값을 변수에 재할당해서 상태를 변경
count = increase(count);
console.log(count); // 1
var count = 0;

// 비순수 함수
function increase() {
  return ++count; // 외부 상태에 의존하며 외부 상태를 변경
}

// 비순수 함수는 외부 상태(count)를 변경하므로 상태 변화를 추적하기 어려워짐
increase();
console.log(count); // 1
  • 원시 타입의 값, 즉 원시 값은 변경 불가능한 값 / 객체(참조) 타입의 값은 변경 가능한 값
  • 원시 값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값 저장 / 객체를 변수에 할당하면 변수(확보된 메모리 공간)에는 참조 값이 저장
  • 값에 의한 전달: 원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 원시 값이 복사되어 전달
  • 참조에 의한 전달: 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달

원시 값

변경 불가능한 값

  • 원시 타입 primitive type의 값, 즉 원시 값은 변경 불가능한 값 immutable value
  • 한번 생성된 원시 값은 읽기 전용 read only 값으로서 변경할 수 없음. 이는 데이터의 신뢰성을 보장
  • 변경 불가능하다는 것은 변수가 아니라 값에 대한 진술
  • 원시 값을 할당한 변수에 새로운 원시 값을 재할당하면 메모리 공간에 저장되어 있는 재할당 이전의 원시 값을 변경하는 것이 아니라 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장한 후, 새롭게 재할당한 원시 값을 가리킴
  • 불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법 없음
// const 키워드를 사용해 선언한 변수는 재할당이 금지. 상수는 재할당이 금지된 변수일 뿐이다
const o = {};

// const 키워드를 사용해 선언한 변수에 할당한 원시 값(상수)는 변경할 수 없다
// 하지만 const 키워드를 사용해 선언한 변수에 할당한 객체는 변경할 수 있다
o.a = 1;
console.log(o); // { a: 1 }

원시 값은 변경 불가능한 값

문자열과 불변성

  • C에서는 문자를 문자의 배열로 처리하고 자바에서는 문자열을 String 객체로 처리
  • 자바스크립트는 원시 타입인 문자열 타입 제공
  • 문자열은 유사 배열 객체이면서 이터러블이므로 배열과 유사하게 각 문제에 접근 가능
  • 한번 생성된 문자열은 읽기 전용 값으로서 변경할 수 없음 -> 원시 값 특성
// 유사 배열 객체
// 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고 length 프로퍼티를 가지는 객체
var str = "string";

// 문자열은 유사 배열이므로 인덱스를 사용해 각 문자에 접근 가능
console.log(str[0]); // s

// 원시 값인 문자열이 객체처럼 동작
console.log(str.length); // 6
console.log(str.toUpperCase()); // STRING

값에 의한 전달

  • 값에 의한 전달: 변수에 원시 값을 갖는 변수를 할당하면 할당 받는 변수에는 할당되는 변수의 원시 값이 복사되어 전달
  • 이때 같은 값을 갖는다는 점은 동일하지만 다른 메모리 공간에 저장된 별개의 값임
  • 변수와 같은 식별자는 실제로 메모리 주소를 기억하고 있고 실제로는 값을 전달하는 것이 아닌 메모리 주소를 전달함
var score = 80;

// copy 변수에는 score의 변수 값 80이 복사되어 할당
var copy = score;

console.log(score, copy); // 80 80
console.log(score === copy); // true

// score 변수와 copy 변수의 값은 다른 메모리 공간에 저장된 별개의 값
// 따라서 score 변수의 값을 변경해도 copy 변수의 값에는 어떠한 영향도 주지 않음

score = 100;
console.log(score, copy); // 100 80
console.log(score === copy); // false

값에 의해 전달된 값은 다른 메모리 공간에 저장된 별개의 값

객체

  • 객체는 복합적인 자료구조이므로 생성하고 프로퍼티에 접근하는 것도 원시 값과 비교하였을 때 비용이 많이 듦
  • 위 이유 때문에 원시 값과는 다르게 동작하도록 설계됨

변경 가능한 값

  • 객체(참조) 타입의 값, 즉 객체는 변경 가능한 값 mutable value
  • 객체를 할당한 변수에는 생성된 객체가 실제로 저장된 메모리 공간의 주소가 저장됨
  • 객체는 변경 가능한 값이므로, 재할당 없이 객체를 직접 변경 가능.즉, 재할당 없이 프로퍼티를 동적으로 추가, 갱신, 삭제 가능
  • 객체에 할당한 변수의 참조 값은 변경되지 않음
  • 단점: 여러 개의 식별자가 하나의 객체를 공유할 수 있게 됨
var person = {
  name: "Jin",
};

// 프로퍼티 값 갱신
person.name = "Kim";

// 프로퍼티 동적 생성
person.address = "Seoul";

console.log(person); // { name: 'Kim', address: 'Seoul' }

객체는 변경 가능한 값

참조에 의한 전달

  • 얕은 복사의 경우 두 개의 식별자가 저장된 메모리는 다르지만 결국 동일한 참조 값을 갖으므로 하나의 객체를 공유하게 됨
  • 따라서 원본 사본 중 어느 한쪽에서 객체를 변경하면 서로 영향을 받음
var person = {
  name: "Jin",
};

// 참조 값을 복사 (얕은 복사)
var copy = person;

참조에 의한 전달

var person = {
  name: "Jin",
};

// 참조 값을 복사, person과 copy는 동일한 참조 값을 갖음
var copy = person;

// copy와 person은 동일한 객체를 참조
console.log(copy === person); // true

// copy를 통해 객체를 변경
copy.name = "Park";

// person을 통해 객체를 변경
person.address = "Seoul";

// person과 copy는 동일한 객체를 가리킴
// 원본 사본 중 어느 한쪽에서 객체를 변경하면 서로 영향 받음
console.log(person); // { name: 'Park', address: 'Seoul' }
console.log(copy); // { name: 'Park', address: 'Seoul' }
var person1 = {
  name: "Jin",
};

var person2 = {
  name: "Jin",
};

// === 일치 비교 연산자로 객체를 할당한 변수를 비교하면 참조 값 비교
console.log(person1 === person2); // false
// === 일치 비교 연산자로 원시 값을 할당한 변수를 비교하면 원시 값 비교
console.log(person1.name === person2.name); // true
// 얕은 복사와 깊은 복사
// 객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 의미,
// 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 의미

const o = { x: { y: 1 } };

// 얕은 복사
const c1 = { ...o };
console.log(c1 === i); // false
console.log(c1.x === o.x); // true

// 깊은 복사 lodash의 cloneDeep을 사용
// npm install lodash

const _ = require("lodash");

const c2 = _.cloneDeep(o);
console.log(c2 === i); // false
console.log(c2.x === o.x); // false

// 원본과 복사본은 참조값이 다른 별개의 객체
// 하지만 얕은 복사의 경우 객체에 중첩되어 있는 객체의 경우 참조 값을 복사

 

객체란?

  • 자바스크립트는 객체 object기반의 프로그래밍 언어
  • 원시 값을 제외한 나머지 값(함수, 배열 , 정규 표현식 등)은 모두 객체
  • 객체 타입 object/reference type은 다양한 타입의 값(원시 값 또는 다른 객체)을 하나의 단위로 구성한 복합적인 자료구조 date structure
  • 객체 타입의 값은 변경 가능한 값 mutable value / 원시 값은 변경 불가능한 값 immutable value
  • 객체는 0개 이상의 프로퍼티로 구성된 집합이며 프로퍼티는 키 key와 값 value로 구성
  • 자바스크립트에서 사용하는 모든 값은 프로퍼티가 될 수 있음
  • 자바스크립트의 함수는 일급 객체이므로 값으로 취급할 수 있음
  • 프로퍼티 값이 함수일 경우, 일반 함수와 구분하기 위해 메서드 method라고 부름
  • 객체는 프로퍼티와 메서드로 구성된 집합체
    • 프로퍼티: 객체의 상태를 나타내는 값 (data)
    • 메서드: 프로퍼티(상태 데이터)를 참조하고 조작할 수 있는 동작 (behavior)
  • 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임을 객체지향 프로그래밍이라 함

객체는 프로퍼티의 집합
객체의 프로퍼티와 메서드

객체 리터럴에 의한 객체 생성

  • c++이나 자바 같은 클래스 기반 객체지향 언어는 클래스를 사전에 정의하고 필요한 시점에 new 연산자와 함께 생성자 constructor를 호출하여 인스턴스를 생성하는 방법으로 객체 생성
  • 자바스크립트는 프로토타입 기반 객체지향 언어로서 클래스 기반 객체지향 언어와는 달리 다양한 객체 생성 방법을 지원
    • 객체 리터럴
    • Object 생성자 함수
    • 생성자 함수
    •  Object.create 메서드
    • 클래스(ES6)
  • 객체 리터럴은 중괄호 ({...}) 내에 0개 이상의 프로퍼티를 정의하여 객체 생성
  • 변수에 할당되는 시점에 자바스크립트 엔진은 객체 리터럴을 해석해 객체를 생성
  • 객체 리터럴의 중괄호는 코드 블록을 의미 하는 것이 아닌 값으로 평가되는 표현식이므로 객체 리터럴의 중괄호 뒤에는 세미콜론이 붙음
var person = {
  name: "Jin",
  sayHello: function () {
    console.log(`Hello! My name is ${this.name}`);
  },
};

console.log(typeof person); // object
console.log(person); // {name: 'Jin', sayHello: ƒ}

프로퍼티

  • 객체는 프로퍼티의 집합이며, 프로퍼티는 키와 값으로 구성
  • 프로퍼티를 나열할 때는 쉼표 (,)로 구분, 마지막 프로퍼티 뒤에는 쉼표를 일반적으로 사용하지 않으나 사용해도 됨
  • 프로퍼티 키와 프로퍼티 값으로 사용할 수 있는 값
    • 프로퍼티 키: 빈 문자열을 포함하는 모든 문자열 또는 심벌 값
    • 프로퍼티 값: 자바스크립트에서 사용할 수 있는 모든 값
  • 프로퍼티 키는 일반적으로 문자열을 사용하며 따옴표로 묶어야 하지만, 식별자 네이밍을 준수하는 이름일 경우 따옴표 생략 가능. 즉, 식별자 네이밍 규칙을 따르지 않을 경우 반드시 따옴표 사용
  • 문자열 또는 문자열로 평가할 수 있는 표현식을 사용해 프로퍼티 키를 동적으로 생성할 수 있음. 이 경우에는 프로퍼티 키로 사용할 표현식을 대괄호([...])로 묶어야 함
  • 빈 문자열도 프로퍼티 키로 사용가능하나 키로서 의미를 갖지 않으므로 권장하지 않음
  • 프로퍼티 키에 문자열이나 심벌 외의 값을 사용하면 암묵적 타입 변환을 통해 문자열이 됨
  • var, function 같은 예약어를 프로퍼티로 사용해도 에러가 발생하지 않으나 권장하지 않음
  • 이미 존재하는 프로퍼티 키를 중복 선언하면 나중에 선언한 프로퍼티가 기존 프로퍼티를 덮어씀. 이때 에러가 발생하지 않으므로 주의
// 식별자 네이밍 규칙을 따르지 않을 경우 반드시 따옴표 사용
var person = {
  firstName: "HongYeop",
  "last-name": "Jin",
};

console.log(person); // {firstName: 'HongYeop', last-name: 'Jin'}
var obj = {};
var key = "hello";

// ES5: 프로퍼티 키 동적 생성
obj[key] = "world";
// ES6 계산된 프로퍼티 이름
// var obj = { [key]: "world"};

console.log(obj); // {hello: 'world'}
// 이미 존재하는 프로퍼티 키를 중복 선언할 경우, 나중에 선언한 프로퍼티가 기존 프로퍼티를 덮어씀
var foo = {
  name: "Lee",
  name: "Kim",
};

console.log(foo); // {name: 'Kim'}

메서드

  • 자바스크립트의 함수는 일급객체라서 값으로 취급할 수 있기 때문에 프로퍼티 값으로 사용 가능
  • 프로퍼티 값이 함수일 경우 일반 함수와 구분하기 위해서 메서드 method라고 부름
  • 메서드 내부에서 사용한 this 키워드는 객체 자신을 가리키는 참조변수
var square = {
  side: 5, // 프로퍼티
  getPerimeter: function () {
    return 4 * this.side;
  },
};

console.log(square.getPerimeter()); // 20

프로퍼티 접근

  • 프로퍼티에 접근하는 방법은 2 가지
    • 마침표 표기법 dot notation: 마침표 프로퍼티 접근 연산자(.) 사용
    • 대괄호 표기법 bracket notation: 대괄호 프로퍼티 접근 연산자([...]) 사용
  • 프로퍼티 키가 식별자 네이밍에 준수하는 이름이면 두 가지 방법 모두 사용 가능
  • 대괄호 프로퍼티 접근 연산자 내부에 지정하는 프로퍼티 키는 반드시 따옴표로 감싼 문자열. 따옴표로 감싸지 않을 경우 자바스크립트 엔진은 식별자로 해석
  • 객체에 존재하지 않은 프로퍼티에 접근하면 undefined를 반환. 이때 ReferenceError가 발생하지 않는 것에 주의
var person = {
  name: "Jin",
};

// 마침표 표기법
console.log(person.name); // Jin

// 대괄호 표기법
console.log(person["name"]); // Jin
// 객체에 존재하지 않는 프로퍼티에 접근할 경우
var person = {
  name: "Jin",
};

console.log(person.age); // undefined

프로퍼티 값 갱신

  • 이미 존재하는 프로퍼티에 값을 할당하면 프로퍼티 값이 갱신
var person = {
  name: "Jin",
};

// person 객체에 name프로퍼티가 존재하므로 name 프로퍼티 값이 갱신
person.name = "Lee";

console.log(person.name); // Lee

프로퍼티 동적 생성

  • 존재하지 않는 프로퍼티에 값을 할당하면 프로퍼티가 동적으로 생성되어 추가되고 프로퍼티 값이 할당
var person = {
  name: "Jin",
};

// person 객체에 age 프로퍼티가 존재 x, age 프로퍼티가 동적 생성 및 할당
person.age = "29";

console.log(person); // {name: 'Jin', age: '29'}

프로퍼티 삭제

  • delete 연산자는 객체의 프로퍼티를 삭제
  • 존재하지 않는 프로퍼티를 삭제하면 아무런 에러 없이 무시
var person = {
  name: "Jin",
};

// 프로퍼티 동적 생성
person.age = "29";

// 프로퍼티 삭제
delete person.age;

// 존재하지 않는 프로퍼티 삭제 - 이 경우 에러 없음...
delete person.address;

console.log(person); // {name: 'Jin' }

ES6에서 추가된 객체 리터럴의 확장 기능

프로퍼티 축약 표현

  • ES6에서는 프로퍼티 값으로 변수를 사용하는 경우, 변수 이름과 프로퍼티 키가 동일한 이름일 때, 프로퍼티 키를 생략 property shorthand할 수 있음. 이때 프로퍼티 키는 변수 이름으로 자동 할당
// ES5
var x = 1,
  y = 2;

var obj = {
  x: x,
  y: y,
};

console.log(obj); // { x: 1, y: 2 }
// ES 6
let x = 1,
  y = 2;

// 프로퍼티 축약 표현
const obj = { x, y };

console.log(obj); // { x: 1, y: 2 }

계산된 프로퍼티 이름

  • 문자열 또는 문자열로 타입 변환할 수 있는 값으로 평가되는 표현식을 사용해 프로퍼티 키를 동적으로 생성할 수 있음
  • 단 프로퍼티 키로 사용할 표현식을 대괄호([...])로 묶어야 함. 이를  계산된 프로퍼티 이름 computed property name이라고 함
  • ES5에서는 계산된 프로퍼티 이름으로 프로퍼티 키를 동적 생성하려면 객체 리터럴 외부에서 대괄호 표기법 사용
  • ES6에서는 객체 리터럴 내부에서도 계산된 프로퍼티 이름으로 프로퍼티 키를 동적 생성 가능
// ES5
var prefix = "prop";
var i = 0;

var obj = {};

// 계산된 프로퍼티 이름으로 프로퍼티 키 동적 생성
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;
obj[prefix + "-" + ++i] = i;

console.log(obj); // { 'prop-1': 1, 'prop-2': 2, 'prop-3': 3 }
// ES6
const prefix = "prop";
let i = 0;

// 객체 리터럴 내부에서 계산된 프로퍼티 이름으로 프로퍼티 키를 생성
const obj = {
  [`${prefix}-${++i}`]: i,
  [`${prefix}-${++i}`]: i,
  [`${prefix}-${++i}`]: i,
};

console.log(obj); // { 'prop-1': 1, 'prop-2': 2, 'prop-3': 3 }

메서드 축약 표현

  • ES5에서 메서드를 정의하면 프로퍼티 값으로 함수를 할당
  • ES6에서 메서드를 정의할 때 function 키워드를 생략한 축약 표현 사용할 수 있음
// ES5
var obj = {
  name: "Jin",
  sayHi: function () {
    console.log(`Hi! ` + this.name);
  },
};

obj.sayHi(); // Hi! Jin
// ES6
const obj = {
  name: "Jin",
  // 메서드 축약 표현
  sayHi() {
    console.log(`Hi! ` + this.name);
  },
};

obj.sayHi(); // Hi! Jin

타입 변환이란?

  • 자바스크립트의 모든 값은 타입이 존재
  • 값의 타입은 개발자의 의도에 따라서 다른 타입으로 변경될 수 있음
  • 개발자가 의도적으로 값의 타입을 변경하는 것을 명시적 타입 변환 explicit coercion 또는 타입 캐스팅 type casting이라고 함
  • 개발자의 의도와 상관 없이 표현식을 평가하는 도중 자바스크립트 엔진에 의해 암묵적으로 타입이 자동으로 변경되는 것을 암묵적 타입 변환 implicit coercion 또는 타입 강제 변환 type coercion이라고 함
  • 중요한 것은 예측 가능한 코드
// 명시적 타입 변환
var x = 10;
var str = x.toString();
console.log(typeof str, str); // string 10

// 변수 x 값이 변경된 것은 아님
console.log(typeof x, x); // number 10
// 암묵적 타입 변환
var x = 10;
var str = x + "";
console.log(typeof str, str); // string 10

// 변수 x 값이 변경된 것은 아님
console.log(typeof x, x); // number 10

암묵적 타입 변환

  • 자바스크립트 엔진은 표현식을 평가할 때, 개발자의 의도와는 상관 없이 코드의 문맥을 고려해 암묵적으로 데이터 타입을 강제 변환할 때가 있음
  • 문자열 타입, 숫자 타입, 불리언 타입으로 변환시킴
  • 불리언 타입
    • 자바스크립트 엔진은 불리언 타입이 아닌 값을 Truthy (참으로 평가되는 값) 또는 Falsy (거짓으로 평가되는 값)으로 구분
    • Falsy로 평가되는 값 (나머지 값은 모두 Truthy)
      • false
      • undefined
      • null
      • 0, -0
      • NaN
      • '' (빈 문자열)
// 피연산자 모두 문자열 타입이어야 하는 문맥
'10' + 2 // '102'
// 피연산자가 모두 숫자 타입이어야 하는 문맥
'5' * 10 // 50
// 피연산자 또는 표현식이 불리언 타입이어야 하는 문맥
!0 // true
if (1) {}
function isFalsy(v) {
  return !v;
}

isFalsy(false); // true
isFalsy(undefined); // true
isFalsy(null); // true
isFalsy(0); // true
isFalsy(NaN); // true
isFalsy(''); // true

명시적 타입 변환

  • 개발자의 의도에 따라 명시적으로 타입을 변경하는 방법
    • 표준 빌트인 생성자 함수(String, Number, Boolean)를 연산자 없이 호출
    • 빌트인 메서드 사용
    • 암묵적 타입 변환

단축 평가

논리 연산자를 사용한 단축 평가

  • 논리합(||) 또는 논리곱(&&) 연산자의 표현식 평가 결과가 불리언이 아닌 점을 이용한 방법
  • 단축 평가 short-circuit evalutation는 표현식을 평가하는 도중에 평가 결과가 확정된 경우 나머지 평가 과정을 생략하는 것을 의미
  • 단축 평가를 활용하면 if 문을 대체할 수 있음
  • 객체가 가리키기를 기대하는 변수가 null 또는 undefined가 아닌지 확인하고 프로퍼티를 참조할 때
  • 함수 매개변수에 기본 값을 설정할 때
// 논리합(||) 연산자는 첫 번째 피연산자가 연산자 표현식의 평가 결과를 결정할 때, 첫 번째 피연산자를 그대로 반환
"Cat" || "Dog"; // 'Cat'
// 논리곱(&&) 연산자는 두 번째 피연산자가 연산자 표현식의 평가 결과를 결정할 때, 두 번째 피연산자를 그대로 반환
"Cat" && "Dog"; // 'Dog'


// 논리 연산의 결과를 결정하는 피연산자를 타입 변환하지 않고 그대로 반환
// 논리합(||) 연산자
'Cat' || 'Dog' // 'Cat'
false || 'Dog' // 'Dog'
'Cat' || false // 'Cat'
// 논리곱(&&) 연산자
'Cat' && 'Dog' // 'Dog'
false && 'Dog' // false
'Cat' && false // false
// 어떤 조건이 Truthy일 떄, &&로 if문 대체 가능
var done = true;
var message = "";

if (done) message = "완료";

// 단축 평가
message = done && "완료";
// 어떤 조건이 Falsy일 떄, ||로 if문 대체 가능
var done = false;
var message = "";

if (done) message = "미완료";

// 단축 평가
message = done || "미완료";
// 참고: 삼항 조건 연산자는 if ... else 대체 가능
var done = true;
var message = "";

if (done) message = "완료";
else message = "미완료";

message = done ? "완료" : "미완료";
// 객체는 key와 value로 구성된 property의 집합
// 객체를 가르키기를 기대하는 변수의 값이 객체가 아닌 null이나 undefined라면 타입 에러 발생
var elem = null;
// var value = elem.value; // TypeError: Cannot read properties of null (reading 'value')

// 단축 평가를 사용하면 에러를 발생시키지 않음
var value = elem && elem.value; // null
function getStringLength(str) {
  str = str || "";
  return str.length;
}

getStringLength("abc"); // 3
getStringLength(""); // 0

// 참고 ES6의 매개변수 기본값 설정
function getStringLengthES6(str = "") {
  return str.length;
}

옵셔널 체이닝 연산자

  • ES11(ECMAScript2020)에서 도입된  옵셔널 체이닝 optional chaing 연산자 ?.는 좌항의 피연산자가 null 또는 undefined인 경우 undefined를 반환하고, 그렇지 않으면 우항의 프로퍼티 참조를 이어감
var elem = null;

// elem이 null 또는 undefined이면 undefined를 반환하고, 그렇지 않으면 우항의 프로퍼티 참조를 이어감
var value1 = elem?.value;
console.log(value1); // undefined

// 옵셔널 체이닝 연산자 ?.가 도입되기 이전에는 논리연산자 &&를 사용한 단축 평가를 통해 null 또는 undefined를 확인
var value2 = elem && elem.value;
console.log(value2); // null

// 차이점

var str = "";

// 옵셔널 체이닝 연산자 ?.는 좌항 피연산자가 Falsy로 평가되는 값이라도 null undefined가 아니면 우항의 프로퍼티 참조 이어감
var value1 = str?.length;
console.log(value1); // 0

// 논리연산자 &&는 좌항 피연산자가 Falsy로 평가되는 값이면 좌항 피연산자를 그대로 반환
var value2 = str && str.length;
console.log(value2); // ''

null 병합 연산자

  • ES11(ECMAScript2020)에서 도입된 null 병합 nullish coalescing 연산자 ?? 는 좌항의 피연산자가 null 또는 undefined인 경우 우항의 피연산자를 반환하고, 그렇지 않으면 좌항의 피연산자를 반환
  • null  병합 연산자는 ?? 변수에 기본값을 설정할 때 유용
var foo = null ?? "default value";
console.log(foo); // 'defalut value'

// null 병합 연산자 ?? 는 좌항이 Falsy 값이라도 null 또는 undefined 값이 아니면 좌항의 피연산자 값을 반환
var value1 = "" ?? "default string";
console.log(value1); // ''

//  null 병합 연산자 ??가 도입되기 이전에는 논리연산자 ||를 사용한 단축 평가를 통해 변수에 기본값을 설정
// 좌항의 피연산자가 Falsy 값이면 우항 피연산자 값을 반환
var value2 = "" ?? "default string";
console.log(value2); // 'default string

+ Recent posts