모듈 다시내보내기

카테고리 JavaScript

📌 모듈 다시 내보내기 기능

import한 모듈을 즉시 다시 내보내기 기능을 구현할 수 있다.

사용하는 이유

진입점인 index.js 파일이 있다고 가정하자. 이 진입점을 내보내서 다른 개발자들이 안의 모듈들을 사용하려고 할 때, 다른 개발자가 모듈의 내부 구조를 건드리게 하면 안된다.

그러기 위해서 공개할 모듈만 index.js 파일에 넣어 내보내기를 하고 나머지는 숨기는 것이 현명하다.

때문에 내보낼 기능들을 분산하여 구현한 뒤 index.js에서 해당 기능들을 가져와서 다시 내보내기 하면 원하는 목적을 이룰 수 있다.

사용방법

1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
// currency, currencyKR 모듈을 다시 내보냅니다.
export * from "./currency/currency.js";

// fetchBooks 모듈을 다시 내보냅니다.
export * from "./fetchBooks.js";
as
import, export 할 때 as 라는 문법을 사용할 수 있다.

as를 사용하면 이름을 바꿔서 모듈을 가져올 수 있다.

// module.js
import { currencyKR as currency } from "../../utils/index.js";

currencyKR 기능을 currency 라는 이름으로 바꿔서 import 해주었다.

🏓 소감

module에 대해서는 쓸 때마다 헷갈렸었는데, 이렇게 모르는 것이 생길 때마다 공부하여 정리하다보면 익숙해지면서 알게 될 것 같다.

댓글 공유

이터레이션 프로토콜

ES6에서 도입된 이터레이션 프로토콜은 순회 가능한 데이터 컬렉션을 만들기 위한 규약이다.

즉, ES6 이후부터는 순회 가능한 데이터 컬렉션을 이터레이션 프로토콜 규약을 준수하는 이터러블로 통일하였다.

이터러블 프로토콜

Symbol.iterator를 프로퍼티 키로 메서드 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 메서드를 호출 시 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.

위의 규약을 준수한 객체를 이터러블이라고 한다. 이터러블은 다음이 가능하다.

  • for…of 문 순회
  • spread 문법
  • 배열 디스트럭처링 할당의 대상

일반객체는 Symbol.iterator 메서드를 구현하거나 상속받지 않으므로 이터러블이 아니다.

이터레이터 프로토콜

이터러블의 Symbol.iterator 메서드 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환한다. 이터레이터는 next 메서드를 소유하고 next 메서드 호출 시 이터러블을 순회하며 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환한다. 이터레이터는 이터러블 요소를 탐색하기 위한 포인터 역할을 한다.

이터레이션 프로토콜의 필요성

이터러블은 for..of문, 스프레드 문법, 배열 디스트럭처링 할당과 같은 데이터 소비자에 의해 사용되므로 데이터 공급자의 역할을 한다고 볼 수 있다.

다양한 데이터 공급자(Array, String, Map 등)가 이터레이션 프로토콜이라는 하나의 규약만 준수하도록 규정하면 데이터 소비자(for..of, 스프레드 문법 등)는 이터레이션 프로토콜만 지원하도록 구현하면 된다.

즉, 이터레이션 프로토콜은 다양한 데이터 공급자가 하나의 순회방식을 갖도록 규정하여 데이터 소비자가 효율적으로 다양한 데이터 공급자를 사용할 수 있도록 데이터 소비자와 데이터 공급자를 연결하는 인터페이스 역할을 한다.

결론

어떤 객체가 Symbol.iterator 메서드를 호출하여 이터레이터 프로토콜을 준수한 이터레이터를 반환한다.

위와 같은 규약을 이터러블 프로토콜이라 하며 이터러블 프로토콜을 준수한 객체를 이터러블이라 한다.

next 메서드를 소유한 이터레이터가 next 메서드 호출하면 이터러블 순회하면서 이터레이터 리절트 객체 반환한다.

위와 같은 규약을 이터레이터 프로토콜이라 하며 이터레이터 프로토콜을 준수한 객체를 이터레이터라고한다.

  • 이터레이터 : next 메서드 소유하여 호출시 이터레이터 리절트 객체 반환
  • 이터러블 : Symbol.iterator 메서드를 호출하여 이터레이터 반환

댓글 공유

ES6 이후 메서드

ES6 이전의 함수는 사용 목적에 따라 일반함수로 호출이 가능했고 생성자 함수로 호출이 가능했다. 즉, ES6 이전의 모든 함수는 callable이면서 constructor다.

1
2
3
[1, 2, 3].map(function (item) {
return item * 2;
}); // [ 2, 4, 6 ]
  • 심지어 콜백함수도 constructor이기 때문에 불필요한 프로토타입 객체를 생성한다.

이처럼 ES6 이전의 모든 함수는 사용 목적에 따라 명확한 구분이 없어 호출방식에 제약이 없고 생성자 함수로 호출되지 않아도 프로토타입 객체를 생성한다. 이는 혼란스러워 실수를 유발하고 성능에도 좋지 않다.

  • ES6 이후부터는 오류에 엄격하도록 만들고 있다. ⇒ 오류를 발생시킬 여지가 있는 것들은 그 부분을 개선해주고 있다.

ES6 사양에서 메서드는 메서드 축약표현으로 정의된 함수만을 의미한다.

인스턴스 생성할 수 없는 non-constructor이다.

  • 인스턴스 생성 불가
  • prototype 프로퍼티 없다
  • 프로토타입 생성 불가

ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 갖는다.

함수 사용 목적에 따라 구분한 ES6에서의 함수

이를 해결하기 위해 ES6에서는 함수를 사용 목적에 따라 3가지로 구분하였다.

ES6 함수의 구분 constructor prototype super arguments
일반함수 O O X O
메서드 X X O O
화살표 함수 X X X X

화살표 함수

function 키워드 함수보다 표현만 간단한 것이 아니라 내부 동작도 간략한다.

주로 콜백 함수 내부에서 this가 전역 객체를 가리키는 문제를 해결하기 위해 사용한다.

화살표 함수 정의

함수 선언문 안되고 함수 표현식으로만 정의 가능하다.

1
2
3
4
5
const multiply = (x, y) => x * y;

const multiply = (x) => x * y; // 매개변수 1개면 소괄호 생략 가능

const multiply = () => x * y; // 매개변수 없으면 소괄호 생략 불가

화살표 함수 몸체 정의

함수 몸체 감싸는 {} 중괄호를 생략한 경우 표현식이 아닌 문일 경우 에러를 발생 시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
// concise body
const power = x => x ** 2;
power(2); // -> 4

// 위 표현은 다음과 동일하다.
// block body
const power = x => { return x ** 2; };

const arrow = () => const x = 1; // SyntaxError: Unexpected token 'const'

// 위 표현은 다음과 같이 해석된다.
const arrow = () => { return const x = 1; };

객체 리터럴을 반환하는 경우 소괄호로 감싸줘야한다.

1
2
3
4
5
6
7
const create = (id, content) => ({ id, content });
create(1, "JavaScript"); // -> {id: 1, content: "JavaScript"}

// 위 표현은 다음과 동일하다.
const create = (id, content) => {
return { id, content };
};

화살표 함수와 일반함수 차이

  1. 화살표 함수는 non-constructor로, 인스턴스 생성할 수 없다.
  • 인스턴스 생성 불가
  • prototype 프로퍼티 없다
  • 프로토타입 생성 불가
  1. 화살표 함수는 중복된 매개변수 이름 선언시 에러 발생
1
2
const arrow = (a, a) => a + a;
// SyntaxError: Duplicate parameter name not allowed in this context
  1. 화살표 함수는 this, arguments, super, new.target 바인딩 갖지 않는다.

따라서 화살표 함수 내부에서 위의 것들을 참조할 때 스코프 체인상 가장 가까운 상위 함수 중 화살표 함수가 아닌 함수의 this, arguements, super, new.target을 참조한다.

주의사항 : 메서드를 화살표 함수로 정의하지 않기

단, 메서드를 화살표 함수로 정의하면 안된다.

1
2
3
4
5
6
7
8
9
10
// Bad
const person = {
name: "Lee",
sayHi: () => console.log(`Hi ${this.name}`),
};

// sayHi 프로퍼티에 할당된 화살표 함수 내부의 this는 상위 스코프인 전역의 this가 가리키는
// 전역 객체를 가리키므로 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는
// window.name과 같다. 전역 객체 window에는 빌트인 프로퍼티 name이 존재한다.
person.sayHi(); // Hi

메서드 정의할 때는 메서드 축약 표현을 사용하는 것이 좋다.

1
2
3
4
5
6
7
8
9
// Good
const person = {
name: "Lee",
sayHi() {
console.log(`Hi ${this.name}`);
},
};

person.sayHi(); // Hi Lee
  • 메서드 내부에서 this는 메서드를 호출한 인스턴스에 바인딩된다.
1
2
3
4
5
6
7
8
9
10
// Bad
function Person(name) {
this.name = name;
}

Person.prototype.sayHi = () => console.log(`Hi ${this.name}`);

const person = new Person("Lee");
// 이 예제를 브라우저에서 실행하면 this.name은 빈 문자열을 갖는 window.name과 같다.
person.sayHi(); // Hi
  • 프로토타입 객체의 프로퍼티에 화살표 함수를 사용하는 것도 문제가 된다.
  • 프로퍼티 동작 추가 시에는 ES6 메서드 정의 사용할 수 없으므로 일반함수로 정의해야한다.

댓글 공유

상속

카테고리 JavaScript

자바스크립트에서 상속을 구현하기 위해 프로토타입을 기반으로 구현한다고 이전시간에 배워보았다.

그럼 상속을 구현하는 방법이 과연 이 방법뿐일까? 한번 알아보자.

직접상속

Object.create에 의한 직접 상속 (정적메서드)

Object.create 메서드는 명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다.

1
2
Object.create(생성할 객체의 프로토타입, 생성할 객체의 프로퍼티키와 프로퍼티 디스크립터 객체로 이뤄진 객체)
Object.create(prototype[, propertiesObject])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 프로토타입이 null인 객체를 생성한다. 생성된 객체는 프로토타입 체인의 종점에 위치한다.
// obj → null
let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj) === null); // true
// Object.prototype을 상속받지 못한다.
console.log(obj.toString()); // TypeError: obj.toString is not a function

// obj → Object.prototype → null
// obj = { x: 1 };와 동일하다.
obj = Object.create(Object.prototype, {
x: { value: 1, writable: true, enumerable: true, configurable: true },
});
// 위 코드는 다음과 동일하다.
// obj = Object.create(Object.prototype);
// obj.x = 1;
console.log(obj.x); // 1
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// 빈객체만 생성하는 것이 아니다.
const obj1 = Object.create(obj, {
x: { value: 10 },
y: { value: 20, writable: true },
});
console.log(obj1); // {x: 10, y: 20}

const myProto = { x: 10 };
// 객체리터럴로 생성된 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
obj = Object.create(myProto);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true
console.log(Object.getPrototypeOf(obj) === Object.prototype); // false

// 생성자 함수
function Person(name) {
this.name = name;
}

// obj → Person.prototype → Object.prototype → null
// obj = new Person('Lee')와 동일하다.
obj = Object.create(Person.prototype);
obj.name = "Lee";
console.log(obj.name); // Lee
console.log(Object.getPrototypeOf(obj) === Person.prototype); // true

Object.create 메서드의 첫번째 인수로 전달한 객체를 프로토타입으로 하는 프로토타입 체인에 속하는 객체를 생성한다. 즉, 객체를 생성하면서 직접적인 상속을 구현하는 것이다.

  • new 연산자 없이 객체 생성 가능
  • 프로토타입을 지정하면서 객체 생성 가능
  • 객체 리터럴로 생성된 객체도 상속받을 수 있다.

Object.prototype의 빌트인 메서드를 직접 호출하는 것은 위험하다. 그 이유는 Object.create 메서드를 사용하여 프로토타입 체인 종점에 위치하는 객체를 생성할 수도 있기 때문이다.

1
2
3
4
5
6
7
8
// 프로토타입이 null인 객체, 즉 프로토타입 체인의 종점에 위치하는 객체를 생성한다.
const obj = Object.create(null);
obj.a = 1;

console.log(Object.getPrototypeOf(obj) === null); // true

// obj는 Object.prototype의 빌트인 메서드를 사용할 수 없다.
console.log(obj.hasOwnProperty("a")); // TypeError: obj.hasOwnProperty is not a function

그러므로 Object.prototype의 빌트인 메서드는 call,apply 등을 통해 간접적으로 호출하는 것을 권장한다.

하지만 위 방법은 너무 길다…

그래서 새로나온 문법을 사용하자

1
Object.hasOwn(obj, property);

위 방법을 사용하여 같은 기능을 구현할 수 있다.

정적 프로퍼티/메소드

생성자 함수로 인스턴스를 생성하지 않아도 참조,호출할 수 있는 프로퍼티, 메소드를 말한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 생성자 함수
function Person(name) {
this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};

// 정적 프로퍼티
Person.staticProp = "static prop";

// 정적 메서드
Person.staticMethod = function () {
console.log("staticMethod");
};

const me = new Person("Lee");

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function

생성자 함수로 생성한 인스턴스로 정적 프로퍼티와 메소드를 참조, 호출할 수 없다.

생성자 함수가 생성한 인스턴스는 프로퍼티, 메소드를 참조할 때 프로토타입 체인 내에서만 할 수 있다.

정적 프로퍼티,메소드는 인스턴스의 프로토타입 체인 내에 없기 때문에 참조, 호출할 수 없다.

댓글 공유

자바스크립트는 public,private,protected 등의 키워드가 없어서 객체지향 프로그래밍이 아니라는 오해를 받지만 자바스크립트는 클래스 기반 객체지향 프로그래밍 언어보다 효율적이며 더 강력한 프로토타입 기반의 객체지향 프로그래밍 언어이다.

객체지향 프로그래밍

객체지향 프로그래밍이란, 특성을 나타내는 속성들의 집합을 통해 실체를 나타내어 구별하거나 인식하는 개념을 프로그래밍에 접목시킨 프로그래밍 패러다임이다. 여기서 프로그램에 필요한 속성만을 간추려 추상화 한것이다.

1
2
3
4
5
6
7
8
9
10
11
고양이 = {
동물
귀여움
털이있음
다리가 4개임...
}

const person = {
name:'Lee',
address:'Seoul'
};

프로토타입과 상속

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
// 불필요한 메서드 중복 소유
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * this.radius ** 2;
};
}

const circle1 = new Circle(1); // 얘도 getArea 가지고
const circle2 = new Circle(2); // 쟤도 getArea 가지네?

console.log(circle1.getArea === circle2.getArea); // false

위 예시는 Circle 생성자 함수가 인스턴스를 생성할 때마다 동일한 동작을 하는 getArea 메서드를 중복 생성하는데, 이 경우 모든 인스턴스가 메서드를 중복 소유하여 메모리 낭비를 야기시킬 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 프로토타입 상속을 기반으로 불필요한 중복 제거
function Circle(radius) {
this.radius = radius;
}

// 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를 공유하여 사용할 수 있도록 프로토타입에 추가
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};

const circle1 = new Circle(1);
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea); // true

프로토타입 객체

프로토타입 객체(줄여서 프로토타입)은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체의 공유 프로퍼티(메서드 포함)를 제공한다.

  • 객체간 상속을 구현하기 위해 사용
  • 프로토타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자유롭게 사용

앞으로 나올 개념은 그림이 없으면 이해하기 어렵다. 이 블로그는 나의 생각을 정리해 둔 것이므로 이해를 돕기 위해서 모던 자바스크립트 Deep Dive 책을 참고하도록하자. 혹은 Poiema Web 사이트를 참고하자.

객체 생성 방식에 따라 객체가 생성될 때, 프로토타입이 결정되고 [[Prototype]] 내부 슬롯의 값으로 저장된다.

함수객체의 prototype 프로퍼티

함수 객체만이 갖는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.

함수 정의 방식에 따라 일반함수 정의할 경우 constructor이고 ES6에서 정한 메서드 축약표현, 화살표 함수로 함수 정의할 경우 non-constructor이다.

non-constructor인 화살표 함수, 메서드 축약표현으로 정의한 메서드는 prototype 프로퍼티를 소유하지 않는다. ⇒ 프로토타입도 생성하지 않는다.

프로토타입 객체(프로토타입), 생성자 함수의 prototype 프로퍼티, 프로토타입의 constructor 프로퍼티, 생성자 함수가 생성한 객체(인스턴스)의 proto 등 헷갈리는 용어들이 많이 나오니 혼동하지 않도록 한다.

프로토타입의 constructor 프로퍼티와 생성자 함수

모든 프로토타입은 constructor 프로퍼티를 갖는다. constructor 프로퍼티는 prototype 프로퍼티로 자신을 참조하고 있는 생성자 함수를 가리킨다.

프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재하며 생성자 함수가 생성되는 시점과 더불어 프로토타입도 생성된다.


프로토타입의 생성 시점

생성자 함수가 생성되는 시점과 더불어 프로토타입도 생성된다.

사용자 정의 생성자 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다.

1
2
3
4
5
6
7
8
9
10
11
12
// 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성
console.log(Person.prototype);

// 생성자 함수
function Person(name) {
this.name = name;
}

// 만약 함수 표현식이라면 에러(변수호이스팅)
const foo = function Person(name) {
this.name = name;
};
  1. 함수 선언문은 함수 호이스팅이 일어나 런타임 이전에 자바스크립트 엔진에 의해 실행된다.

  2. 이 때 함수 객체가 생성되고 더불어 생성된 프로토타입은 Person 생성자 함수의 prototype의 프로퍼티에 바인딩된다.

이 때 생성된 프로토타입은 오직 constructor 프로퍼티만 갖는 객체이다.

생성된 프로토타입의 프로토타입은 항상 Object.prototype이다.

반면, 빌트인 생성자 함수의 경우 전역 객체가 생성되는 시점에 생성된다.

즉, 객체가 생성되기 이전에 생성자 함수와 프로토타입은 이미 객체화 되어 존재한다. 이 후 객체를 생성하면 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당된다. 그 결과 생성된 객체는 프로토타입의 상속을 받는다.


객체 생성 방식과 프로토타입의 결정

객체는 다양한 방식으로 생성되어 세부적인 객체 생성 방식의 차이는 있지만 모두 추상연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있다.

추상연산 OrdinaryObjectCreate는 필수적으로 자신이 생성할 객체의 프로토타입을 인수로 전달받는다.

즉, 프로토타입은 추상연산 OrdinaryObjectCreate에 전달되는 인수에 의해 결정된다. 인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정된다.

  1. 객체 리터럴로 생성된 객체의 프로토타입
1
Object.prototype;
  1. Object 생성자 함수로 생성된 객체의 프로토타입
1
Object.prototype;
  1. 사용자 정의 생성자 함수로 생성된 객체의 프로토타입
1
생성자함수.prototype;

생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체이다. 이 프로토타입은 constructor 프로퍼티만 가지고 있다.


프로토타입 체인

자바스크립트는 객체의 프로퍼티에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다.

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}`);
};

const me = new Person("Kim");
console.log(me.hasOwnProperty("name")); // true

Person 생성자 함수에 의해 생성된 me 인스턴스는 Object.prototype의 메서드인 hasOwnProperty 메서드를 호출할 수 있다. 이것은 Person.prototype뿐만 아니라 Object.prototype도 상속 받았다는 것을 알 수 있다.

단, me 객체의 프로토타입은 Person.prototype이다.

그리고 Person.prototype의 프로토타입은 Object.prototype이다.

프로토타입 최상위에 있는 객체는 언제나 Object.prototype(프로토타입 체인의 종점)이다. 그러므로 Object.prototype의 [[Prototype]] 내부 슬롯은 null 이다.

식별자와 프로퍼티 차이

식별자는 함수의 중첩관계로 이루어진 스코프의 계층적 구조로 이뤄진 스코프 체인에서 검색하여 찾는다.

반면에 객체의 프로퍼티는 해당 객체의 프로퍼티가 없다면 프로토타입 체인을 따라 [[Prototype]] 내부 슬롯에 바인딩된 프로토타입으로 이동하면서 프로퍼티를 검색한다.

스코프 체인과 프로토타입 체인은 별도로 동작하지 않고 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다.

소감

프로토타입에 대해 설명하기가 무척 어렵고 난해했는데 그림을 보며 공부하니 이해가 수월하였다. TIL을 정리하면서 저작권때문에 그림을 첨부할 순 없었지만 머릿속으로 다시 그려보면서 정리하니 기억에 오래 남을 것 같다.

이제 러버덕을 하면서 말로 설명해보는 시간을 가져야겠다.

댓글 공유

프로퍼티 어트리뷰트

오늘은 프로퍼티 어트리뷰트에 대해서 알아보자. 한국말로 번역하면 속성 속성이다. 하지만 자바스크립트에서 속성은 두가지로 구분하여 사용하여야 한다. 이번 시간에는 속성 중 하나인 프로퍼티에 대해 알아보자

내부슬롯 internal slot

내부슬롯과 내부메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 프로퍼티와 메서드이다.

내부슬롯은 개발자가 직접 접근할 수는 없다.

하지만, [[Prototype]] 내부슬롯은 proto를 통해 간접적으로 접근할 수 있다.

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

자바스크립트 엔진은 프로퍼티 생성 시 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

1
2
3
4
let a = { name: "kim", age: 28 };
Object.getOwnPropertyDescriptors(a).age.value = 3000;

console.log(a); // {name: 'kim', age: 28}

프로퍼티 어트리뷰트란 내부슬롯이다. 따라서 직접 접근할 순 없지만, Object.getOwnPropertyDescriptor 메서드로 간접적으로 접근할 수 있다.

다만 간접적으로 접근만 가능한 것이므로 위 메서드로 값을 변경하거나 할 수는 없다.

⇒ 프로퍼티 생성될 때 [[value]]는 프로퍼티 값으로 초기화 되고 나머지는 true로 초기화된다.

1
2
3
4
5
const person = {
name : 'Lee'
}

console.log(Object.getOwnPropertyDescriptor(person, 'name'); // {value: 'Lee', writable:true,... configurable: true}
  • 프로퍼티 디스크립터 객체를 반환한다. 존재하지 않거나 상속받은 프로퍼티라면 undefined 반환

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

1. 데이터 프로퍼티

키와 값으로 구성된 일반적인 프로퍼티

  • 프로퍼티의 값 [[value]]
  • 값의 갱신 가능여부 [[writable]] : false이면 프로퍼티의 [[value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티
  • 열거 가능여부 [[enumerable]] : for…in 혹은 Object.keys 메서드로 열거 가능 여부
  • 재정의 가능여부 [[configurable]] : false이면 해당 프로퍼티 삭제, 프로퍼티 어트리뷰트 값의 변경 금지

2. 접근자 프로퍼티

자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티

  • [[get]] : 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[get]]의 값 즉, getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다.
  • [[set]] : 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[set]]의 값 즉, setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다.
  • [[enumerate]]
  • [[configurable]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const person = {
// 데이터 프로퍼티
firstName: "Donald",
lastName: "Duck",

// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
},

// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
},
};

// 데이터 프로퍼티를 통한 프로퍼티 값의 참조
console.log(person.firstName + " " + person.lastName); // Donald Duck

// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName값을 저장하면 setter 함수가 호출
person.fullName = "Daisy Duck";
console.log(person); // {firstName: "Daisy", lastName: "Duck"}

// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출
console.log(person.fullName); // Daisy Duck

// firstName은 데이터 프로퍼티
let descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor);
// {value:"Daisy", writable: true, enumerable: true, configurable: true}

// fullName은 접근자 프로퍼티
descriptor = Object.getOwnPropertyDescriptor(person, "fullName");
console.log(descriptor); // {get: f, set: f, enumerable: true, configurable: true}

접근자 프로퍼티와 데이터 프로퍼티 구분 방법

1
2
3
4
5
6
7
// 일반 객체의 __proto__는 접근자 프로퍼티
Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");
// {get: f, set: f, enumerable: true, configurable: true}

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

위 예시는 접근자 프로퍼티의 경우 다음과 같은 프로퍼티 어트리뷰트가 나오고 데이터 프로퍼티의 경우 출력되는 프로퍼티 어트리뷰트가 다른 것을 확인할 수 있다.

프로퍼티 정의

프로퍼티를 정의한다. 라는 것은 새로운 프로퍼티 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나 기존 프로퍼티 어트리뷰트를 재정의하는 것을 말한다.

데이터 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
const person = {};

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

Object.defineProperty(person, "lastName", {
value: "Kim",
});
  • Object.defineProperty 메서드로 프로퍼티 어트리뷰트 정의할 수 있다. 인수로는 객체의 참조, 데이터 프로퍼티의 키(문자열), 프로퍼티 디스크립터 객체를 전달한다.
  • 디스크립터 객체 누락시키면 false가 기본값

궁금증

왜 프로퍼티 동적으로 생성해줄 때, 프로퍼티 어트리뷰트 생략하는데 프로퍼티 어트리뷰트값이 true인데, define 메서드를 사용할 때 생략하면 기본값 false?

1
2
3
4
5
6
7
const obj = {};
obj['name'] = 'Kim';
Object.defineProperty(obj,'age',{
value:19,
});

왜 다른가?

동적으로 생성시에 편의를 위해서 true로 설정된다. 그렇지 않은 경우 Object.defineProperty() 메서드를 사용할 때, 생략하는 어트리뷰트는 false로 설정된다.

writable : false

1
2
3
// [[writable]] 값이 false 인 경우 해당 프로퍼티의 [[value]]의 값 변경할 수 없다.
// 에러 발생하지 않고 무시
person.lastName = "soondae";

enumerable : false

1
2
// [[enumerable]] 값이 false 인 경우 해당 프로퍼티는 for...in 문이나 Object.keys 등으로 열거할 수 없다.
console.log(Object.keys(person)); // ['firstName']

configurable : false

1
2
3
4
5
6
7
// [[configurable]] 값이 false 인 경우 해당 프로퍼티를 삭제할 수 없다.
// 삭제해도 에러없이 무시된다.
delete person.lastName; // false

// 또한, 해당 프로퍼티를 재정의 할 수도 없다.
// Object.defineProperty(person, 'lastName', { enumerable : true });
// Uncaught TypeError: Cannot redefine property: firstName

접근자 프로퍼티 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.defineProperty(person, "fullName", {
// getter 함수
get() {
return `${this.firstName} ${this.lastName}`;
},
// setter 함수
set(name) {
[this.firstName, this.lastName] = name.split(" ");
},
enumerable: true,
configurable: true,
});

person.fullName = "yoonju hong";
console.log(person); // {firstName: 'yoonju', lastName: 'Kim'}
  • lastName은 writable이 false여서 값이 안바뀌는 것을 볼 수 있다.

객체 변경 방지

객체 변경을 방지하는 메서드를 제공한다. 종류에 따라 금지하는 강도가 다르다.

직속 프로퍼티만 방지(얕은 방지)

구분 메서드 프로퍼티 추가 삭제 값 읽기 값 쓰기 프로퍼티 어트리뷰트 재정의
객체 확장 금지 Object.preventExtensions X O O O O
객체 밀봉 Object.seal X X O O X (configurable : false)
객체 동결 Object.freeze X X O X (writable : false) X (configurable : false)

객체 확장 금지 확인 메서드 - Object.isExtensible( ) false면 확장 금지된 객체

객체 밀봉 확인 메서드 - Object.isSealed( ) true면 밀봉된 객체

객체 동결 메서드 - Object.isFrozen( ) true면 동결된 객체

깊은 방지를 구현하려면 재귀적으로 객체 값을 갖는 모든 프로퍼티에 대해 Object.freeze 메서드를 호출해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepFreeze(target) {
// 객체가 아니거나 동결된 객체는 무시하고 객체이고 동결되지 않는 객체만 동결
if (target && typeof target === "object" && !Object.isFrozen(target)) {
Object.freeze(target);
Object.keys(target).forEach((key) => deepFreeze(target[key]));
}
return target;
}

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

// 깊은 객체 동결
deepFreeze(person);

console.log(Object.isFrozen(person));
console.log(Object.isFrozen(person.address));

person.address.city = "Busan";
console.log(person);

댓글 공유

함수

카테고리 JavaScript

Function 함수

오늘은 자바스크립트의 함수에 대해 공부해보자.

자바스크립트에서 함수란, 코드블록으로 일련의 문(statement)을 감싸서 하나의 실행 단위로 정의한 것이다.

함수를 목적에 맞게 사용하기 위해서는 함수이름, 매개변수, 인자 등을 알맞게 설정해줘야한다. 그렇지 않게 사용하는 것은 지양한다.

목적

함수를 사용하는 목적은 필요할 때마다 호출하여 일련의 코드들을 재사용하기 위해 사용한다.

1
2
3
4
5
6
7
8
9
10
function returnRank(name, tall) {
// 이름과 키라는 매개변수를 통해 순위를 반환해주는 함수
return rank;
}

// 100번째 줄
returnRank("yiju", 188);

// 300번째 줄
returnRank("kim", 160);

위와 같이 하나의 로직을 여러 곳에서 재사용하고 싶을 때 함수를 사용하면 함수 이름으로 가독성도 높아지고 100번째 줄이나 300번째 줄에서 코드가 문제가 발생했을 경우 returnRank 함수가 선언된 부분만 유지보수를 해주면 되기 때문에 유지보수가 간편해진다.

단, 함수는 목적에 맞게 가급적 작게 만들고 매개변수도 3개를 넘지 않도록 만들 것을 지향한다.


정의

함수를 정의하는 방식은 4가지 방식이 있다.

  • 함수 선언문
  • 함수 표현식
  • Function 생성자 함수
  • 화살표 함수(ES6)

기본적인 함수 선언문과 함수 표현식에 대해 알아보자

자바스크립트에서 변수를 선언하면 암묵적으로 정의가 이뤄지기 때문에 선언과 정의가 모호하다.(MDN에서도 모호..) 위에서 함수 선언문이 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당된다. 그렇기 때문에 “함수는 정의된다”로 표현한다. C언어에서 정의는 변수에 값을 할당하여 변수의 실체를 명확히 하는 것이다. 즉, 메모리 주소가 연결되면 정의라고 판단)

함수 리터럴

리터럴은 문자나 약속된 기호를 사용하여 값을 생성하는 표기법이다. 함수로 함수 리터럴을 사용하여 값을 생성할 수 있다. 함수 리터럴은 다음으로 구성되어 있다.

1
2
3
function add(x, y) {
return x + y;
}
  • function 키워드
  • 함수 이름 (add) 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자
  • 매개변수 목록 (x,y)
  • 함수 몸체({})

함수 선언문

1
2
3
4
5
6
7
// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 호출 (함수이름과 똑같은 식별자를 사용)
add(2, 5);
  • 함수 선언문은 표현식이 아닌 문이다.
  • 함수 선언문은 함수 이름을 생략할 수 없다.

앞서 언급했듯이 함수 이름은 함수 몸체 내에서만 참조 가능하다. 그렇다면 함수를 호출하려면 어떻게 할 수 있을까?

함수 선언문으로 사용되면 자바스크립트 엔진이 암묵적으로 함수 이름과 똑같은 식별자를 생성하고 함수 객체를 할당한다.

함수 표현식

1
2
3
4
5
6
7
// 함수 표현식
var add = function (x, y) {
return x + y;
};

// 함수 호출 (함수 이름으로 호출)
add(2, 10);

자바스크립트 함수는 객체타입의 값이기 때문에 변수에 할당하거나 프로퍼티의 값으로 될 수 있고 배열과 같은 자료구조의 요소가 될 수 있다.

이러한 성질 때문에 자바스크립트 함수는 일급객체다.

  • 함수 표현식은 함수 이름 생략하는 것이 일반적이다.
  • 함수 표현식은 표현식인 문이다. 즉, 값처럼 사용할 수 있다. ex)변수할당

중의적 코드 : 기명 함수 리터럴

함수 선언문은 함수 이름을 생략할 수 없으며 표현식이 아닌 문이므로 변수에 할당할 수 없다. 그러면 아래의 코드는 어떻게 동작할지 예상해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 기명 함수 리터럴 단독 사용 문맥 => 함수 선언문으로 해석
function foo() {
console.log("foo");
}

foo(); // foo

// 변수에 할당하는 문맥 => 함수 리터럴 표현식으로 해석
var add = function add(x, y) {
return x + y;
};

console.log(add(2, 3)); // 5

// 피연산자로 사용되는 문맥 => 함수 리터럴 표현식으로 해석
(function bar() {
console.log("bar");
});

bar(); // ReferenceError : bar is not defined

위 코드에서는 함수 선언문이 변수에 할당된 것처럼 보인다. 이게 어떻게 가능할까?

우리는 블록문 {}에서 앞서 중의적 표현과 문맥에 대해 다룬 적이 있다.

자바스크립트 엔진이 {}를 객체 리터럴로 인지할 것인지, 아니면 코드 블록문으로 인지할 것인지는 문맥에 따라 다르게 결정된다.

이와 같이 기명 함수 리터럴도 중의적인 코드이므로 문맥에 따라 해석이 달라질 수 있다. 다음은 자바스크립트 엔진이 기명 함수 리터럴을 해석하는 방식이다.

  1. 기명 함수 리터럴을 단독으로 사용하면 함수 선언문으로 해석

  2. 함수 리터럴이 값으로 평가되어야 하는 문맥, 예시처럼 변수에 할당하거나 피연산자로 사용하면 함수 리터럴 표현식으로 해석한다.

() 그룹 연산자 안에서 기명 함수 리터럴은 함수 리터럴 표현식으로 해석된다.

함수 리터럴에서 함수 이름은 함수 몸체 내부에서만 참조가능하기 때문에 외부에서 함수이름으로 호출시 에러 발생

함수 선언문의 경우 함수이름으로 암묵적으로 식별자를 생성하여 객체를 할당해주기 때문에 함수 이름으로 호출 가능


함수 선언문 vs 함수 표현식

함수 선언문과 함수 표현식은 생성 시점이 다르다.

1
2
3
4
5
6
7
8
9
10
11
12
console.log(add); // ƒ add(x, y) {return x + y;}
console.log(sub); // undefined

// 함수 선언문
function add(x, y) {
return x + y;
}

// 함수 표현식
var sub = function (x, y) {
return x - y;
};
  • 함수 선언문 : 모든 선언문이 런타임 이전에 JS엔진에 먼저 실행된다. 즉, 암묵적으로 함수 이름과 동일한 식별자를 생성하여 함수 객체를 할당한다. 함수 호이스팅

  • 함수 표현식 : 변수 선언 부분은 변수 호이스팅이 발생하여 undefined로 초기화되고 변수 할당문은 런타임에서 평가되어 함수 객체로 할당된다.


함수호출

일반 객체와 함수가 다른점은 함수는 호출을 할 수 있다는 것이다. 호출과 참조는 다르다. 호출은 실행흐름을 바꾸기도 하며 코드문들의 결과값을 반환해준다.

() 함수 호출 연산자를 사용하여 함수를 호출할 수 있다. 그러면 실행흐름이 함수로 옮겨지고 return 키워드를 만나게되면 그 즉시 함수 실행을 종료하고 return 키워드 우측 표현식에 대한 값을 반환한다.

return 키워드 다음 행의 문들은 무시된다. return 키워드 다음에 개행하여 코드를 작성하면 자동 세미콜론 삽입 기능에 의해 큰 오류를 발생시킬 수 있다.

인수

함수를 호출할 때 매개변수에 들어갈 인수를 전달해줘야한다. 하지만 인수가 매개변수보다 적거나 많더라도 오류를 발생시키지 않는다.

  • 매개변수보다 인수를 적게 전달할 경우 부족한 매개변수는 undefined 처리된다.
  • 매개변수보다 인수가 많아도 오류 발생 시키지 않는다. 단, 모든 인수는 arguments 객체의 프로퍼티로 보관된다.
1
2
3
4
5
function add(x,y = 0,z){
return x+y+z;
}

add(2, ,5); // X

중간에 y값의 매개변수를 생략하고 싶어서 공백으로 둬서 호출할 수 없다.

만약 인자를 순서를 신경쓰지 않고 전달해주고 싶다면 객체를 인자로 전달해주면 된다.

1
2
3
4
5
6
$.ajax({
method: "POST",
url: "/user",
data: { id: 1, name: "Lee" },
cache: false,
});

단, 함수 내부에서 객체를 변경하게 되면 참조값으로 복사되었기 때문에 함수 외부의 객체가 변경되는 부수효과가 발생할 수 있으니 주의해야한다.


순수함수와 비순수함수

외부 상태에 의존하거나 참조하거나 즉, 부수효과가 없는 함수를 순수함수라고 하고 그렇지 않은 함수를 비순수함수라고 한다.

1
2
3
4
5
var x = 10;

function add(a, b) {
return a + b + x;
}

위 함수는 a,b 매개변수가 함수 내부로 전달되어도 외부 변수 x의 값이 변경되면 결과값이 달라지기 때문에 순수함수가 아니다.

순수함수는 오직 매개변수만을 통해 함수 내부로 전달된 인수에게만 의존하여 값을 생성해 반환한다. 전달받은 인수는 변경하지 않는다.

하지만 우리가 순수함수만으로 프로그래밍을 하는 것은 불가능하다. 함수형 프로그래밍은 반복문, 조건문을 제거하여 복잡성을 해결하고 전역 변수 사용을 억제 및 생명주기 최소화하여 상태변경을 최소화하는 것을 목표로 하기 때문에 순수함수만을 사용하는 것은 옳지 않다.

콜백함수

함수의 매개변수를 통해 함수 내부로 전달되는 함수를 콜백함수라고 한다. 또한 콜백함수를 매개변수를 통해 받은 함수는 고차함수라고 한다.

콜백함수는 함수 외부에서 고차함수 내부로 주입하기 때문에 자유롭게 교체가 가능하고 경우에 따라 변경되는 로직을 가진 외부 함수를 내부로 전달할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 외부에서 전달받은 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
  • 콜백함수를 전달할 때에는 콜백함수를 호출하지 않고 함수 전달만 해야한다.

콜백함수는 비순수함수인가?

“앞서 함수 내부에서 외부 상태를 직접 참조하지 않더라도 매개변수를 통해 객체를 전달받으면 비순수함수가 된다.”

=> 맞다 콜백함수도 비순수 함수이다. 나중에 addEventListener 함수도 콜백함수를 매개변수로 받아 DOM을 조작하는 것도 배울 텐데 DOM을 조작한다는 것 자체가 비순수함수가 되는 것이다. 다시말해 순수함수만으로 코드를 짜는것은 불가능하다.


소감

모던 자바스크립트 12장 함수 파트를 읽으면서 함수 정의와 목적에 대해 배웠고 그동안 무의식적으로 사용했던 함수 이름을 통한 호출에 대해 이유를 알게되어 재밌었다.

코드 맥락에 맞게 자바스크립트 엔진이 해석하는게 달라질 수 있다는 점과 콜백함수와 순수, 비순수 함수에 대해 제대로 알고 넘어갈 수 있어 좋았다.

댓글 공유

원시타입과 객체타입에 대해서 제대로 알고 넘어가기 위해 오늘은 원시타입과 객체타입, 그리고 더 나아가 얕은복사, 깊은복사에 대해서도 알아보자.

원시타입과 객체타입


원시타입

1. 원시타입의 값

원시타입의 값은 변경 불가능한 값이다. 읽기전용이다.

1
2
3
4
5
var str = "hello";

str[0] = "H";

console.log(str); // hello

변수 str의 첫번째 문자열을 인덱스로 접근하여 직접 바꾸려했지만 변경불가능하여 바꿀 수 없다.

2. 원시타입의 메모리 공간

원시타입은 변수 할당 시 메모리 공간에 실제 값을 저장한다.

변수와 메모리에 대해 자세히 언급하면 사실 변수라는 식별자가 값을 저장하는 게 아닌 값이 저장된 메모리 공간에 대한 메모리 주소를 저장하고 있다.

즉, 메모리 공간에는 값이 저장되어 있고 변수라는 식별자는 메모리 공간의 메모리 주소를 저장한다.

3. 원시타입 값의 전달

원시값을 갖는 변수를 다른 변수에 할당하면 원시값이 복사되어 전달된다.

1
2
3
4
5
6
7
8
var str = "hello";

var greeting = str;

greeting = "hi";

console.log(str); // hello
console.log(greeting); // hi

사실 값이 복사되어 전달되는 것이 아니고 동일한 값을 새로운 메모리 저장공간에 저장한 뒤 해당 메모리 공간의 메모리 주소를 copy라는 변수에 할당하는 것이다.


객체타입

1. 객체타입의 값

객체타입의 값은 변경가능한 값이다.

1
2
3
4
5
6
7
8
9
10
11
var obj = {
name: "yiju",
age: 28,
smoke() {
console.log("no-smoking");
},
};

obj.name = "jangoon";

console.log(obj); // {name: 'jangoon', age: 28, smoke: ƒ}

2. 객체타입의 메모리 공간

객체타입은 변수 할당 시 메모리 공간에 참조값(주소값)이 저장된다.

원시타입에서는 메모리 공간에 값이 저장되고 변수 식별자에 해당 메모리 공간을 가리키는 주소가 저장되어 있다.

하지만, 객체타입에서는 메모리 공간에 다른 메모리 공간을 가리키는 메모리 주소가 담겨있다. 다시말해서, 객체타입의 메모리는 원시타입의 메모리보다 과정이 한번 더 일어나게 되는 것이다.

3. 객체타입 값의 전달

객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조값(주소값)이 복사되어 전달된다.

1
2
3
4
5
6
7
var person = {
name: "Lee",
};

var copy = person;

console.log(person === copy); // true

변수 person과 copy는 {name:’Lee’} 객체 프로퍼티들의 주소가 담겨있는 메모리 공간의 메모리 주소를 저장한다. 그러므로 같은 참조값(주소값)을 비교하고 있기 때문에 일치 비교 연산자가 true가 나왔다.


객체타입의 얕은복사

객체를 spread 문법을 사용하여 얕은복사를 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let person = {
name: "yiju",
fruit: {
1: "peach",
2: "strawberry",
},
};

let someone = { ...person };

// 두 객체가 같은지 비교
console.log(person === someone); // false

// 객체의 프로퍼티 비교
console.log(person.name === someone.name); // true
console.log(person.fruit === someone.fruit); // true

// 1. 원시타입의 값을 변경했을 경우
person.name = "minseok";

console.log(person); // {name: 'minseok', fruit: {…}}
console.log(someone); // {name: 'yiju', fruit: {…}}

// 2. 객체타입의 값을 변경했을 경우
person.fruit["1"] = "watermelon";

console.log(person); // fruit: {1: 'watermelon', 2: 'strawberry'}
console.log(someone); // fruit: {1: 'watermelon', 2: 'strawberry'}

변수 person의 원시타입의 값을 수정하였을 때, 서로의 원시타입의 값은 공유하지 않는다. 하지만 객체타입의 값을 바꾼 경우 서로의 객체타입의 값을 공유하는 모습을 볼 수 있다.

왜 그럴까? 앞서 언급한 내용으로 깊게 파헤쳐보자.

  1. person, someone은 일치하지 않으니 서로 다른 주소값을 갖는다. 각각의 주소는 javascript {name:'yiju',fruit:{1:'peach',2:'strawberry'}} 객체 프로퍼티들의 주소를 담고있는 메모리 공간을 가리킨다.

예를 들면 name 식별자가 가리키는 주소, fruit 식별자가 가리키는 주소 이런식으로 프로퍼티의 주소들을 담고 있다.

여기서 name 식별자는 원시타입의 값을 가리키는 메모리 주소를 저장한다. javascript person.name = 'minseok' 을 실행하면 person.name과 someone.name이 서로 다른 주소값을 갖게된다.

  1. fruit의 식별자 주소는 javascript {1:'peach',2:'strawberry'} 프로퍼티들의 주소를 담고있는 메모리 공간을 가리킨다. person.fruit와 someone.fruit가 서로 같다(메모리 주소가 같다). 즉, fruit 프로퍼티들의 주소가 담긴 메모리 공간을 가리키는 메모리 주소가 같다.

그리하여 javascript fruit['1'] = 'watermelon' 로 fruit 프로퍼티인 1을 수정하면 식별자인 1이 가리키는 메모리 주소가 바뀌게 되고 person과 someone 식별자가 식별자 1을 포함하는 프로퍼티들의 주소를 담은 메모리 공간을 가리키는 메모리 주소는 변하지 않으므로 서로 공유하게 된다.


객체타입의 깊은복사

객체타입의 깊은복사는 JSON.parse, JSON.stringify 메서드를 사용하여 구현할 수도 있고 lodash 라는 라이브러리를 사용한다.

하지만 JSON.stringify 방식으로는 함수(메서드)를 복사할 수 없기 때문에 재귀를 사용하거나 실무에선 라이브러리를 사용한다.

lodash 방법

1
2
3
4
5
6
7
8
const o = { x: { y: 1 } };

const _ = require("lodash");
const c2 = _.cloneDeep(o);

// 객체비교
console.log(c2 === o); // false
console.log(c2.x === o.x); // false

JSON 방법

1
2
3
4
5
6
7
8
9
10
const a = {
b: 1,
c: {
d: 2,
},
};
let b = JSON.parse(JSON.stringify(a));
b.c.d = 4;
console.log(a); // { b: 1, c: { d: 2 } }
console.log(b); // { b: 1, c: { d: 4 } }

소감

자바스크립트 DeepDive 책을 읽으면서 팀원들과 각자 공부한 내용을 말로 풀어서 설명하다보니 내가 무엇이 부족한지 알 수 있었고 내가 모르거나 제대로 알지 못하던 부분을 확실히 알고 이렇게 TIL까지 작성하면서 정리하니 기억에 오래 남을 것 같다.

댓글 공유

데이터 타입

카테고리 JavaScript

Javascript Data type

자바스크립트는 크게 2가지의 데이터 타입으로 나뉜다.

  1. 원시타입 (Primitive Data Type)

  2. 객체타입 (Object Type, Reference Type)

이 중에서도 원시타입에는 6가지의 타입이 존재하고 그 외의 것들은 모두 객체타입이다. 그래서 자바스크립트를 객체 지향 프로그래밍 언어라고 부르나 보다.(6가지 빼곤 모두 객체니깐…)

오늘은 데이터 타입에 대해 공부해보도록 하자.


Primitive Data Type (원시타입)

원시타입의 종류는 6가지가 있다.

  • number (숫자형)
  • string (문자형)
  • undefined
  • null
  • boolean (불리언형)
  • symbol

원시 타입의 값은 변경 불가능한 값이며, pass-by-value(값에 의한 전달)이다. 이에 대한 자세한 내용은 다음 TIL에서 더 알아보고 각 자료형에 대해 알아보자.


number

Javascript에서는 숫자형을 모두 실수로 처리한다. 추가로 아래와 같은 자료도 숫자형이다.

  • Infinity : 양의 무한대
  • -Infinity : 음의 무한대
  • NaN : 산술 연산 불가(Not A Number)

NaN은 자기 자신과 일치하지 않는 유일한 값이다. 그러므로 어떤 값이 NaN인지 판단하기 위해서는 Number.isNaN() 빌트인 함수를 사용하거나 Object.is() 메서드를 사용하여야 한다.

1
2
3
4
5
NaN === NaN; // false

Number.isNaN(NaN); // true

Object.is(NaN, NaN); // true

string

C언어와 다르게 Javascript의 문자열은 원시타입으로 변경 불가능하다.

1
2
var str = "Hello";
str = "World";

위 코드에서 str 변수에 ‘World’라는 문자열로 데이터를 수정하는 것이 아니다.
임의의 메모리 주소를 갖는 메모리 공간에 ‘World’라는 문자열을 저장하고 변수의 식별자인 str이 새로운 메모리 주소를 가리켜 변수를 재할당하는 것이다.

boolean

참, 거짓을 나타내는 자료형이다. 암묵적 불리언 형변환이 발생하면 해당값들을 false로 바꿔주는 falsy 값들이 있다.

falsy

  • “” (빈문자열)
  • undefined
  • null
  • 0, -0
  • NaN

undefined

변수의 선언 단계는 사실 선언단계와 초기화 단계 두 단계로 나눠서 볼 수 있는데, 초기화 단계에서 컴퓨터가 변수가 할당되기 이전에 undefined값으로 변수의 값을 초기화해준다.

때문에 이는 개발자의 의도가 담긴 것이 아니다.

null

변수의 값이 없다는 것에 개발자의 의도를 담고 싶다면 null을 사용한다. 자바스크립트는 대소문자를 구별하기 때문에 null, Null, NULL 모두 다른 값이다.

함수가 유효한 값을 반환할 수 없는 경우 명시적을 null을 반환하기도 한다.

null 값의 type을 확인하기 위해서는 typeof 연산자를 사용하는 것 대신 일치 연산자(===)를 사용한다.

1
2
3
var foo = null;
typeof null; // 'object'
foo === null; // true

symbol

심볼은 이름이 충돌한 위험이 없는 유일무이한 원시타입의 값이다. 주로 객체의 유일한 프로퍼티 키를 만들기 위해 사용한다.

1
2
3
4
5
6
var key = Symbol("key");
typeof key; // 'symbol'

var obj = {};

obj[key] = "value"; // 이름이 충돌할 위험이 없는 유일무이한 값인 심볼을 프로퍼티 키로 사용

Object Type, Reference Type (객체타입)

객체란, 데이터와 그 데이터에 관련된 동작(절차, 방법, 기능 등)을 모두 포함할 수 있는 개념적 존재이다.

다시말해 데이터를 의미하는 프로퍼티와 동작을 의미하는 메소드를 포함할 수 있는 독립적 주체이다.

원시타입을 제외한 나머지 값(배열, 함수, 정규표현식 등)이 모두 객체이다.

객체는 pass-by-reference (참조에 의한 전달)이다. 이 부분에 대해서는 후에 객체에 대해서 TIL 작성시 다뤄보자


소감

모던 자바스크립트 Deep Dive 책을 읽으면서 그전에 포이마 웹을 통해 공부했을 때 놓쳤던 부분까지 꼼꼼하게 이해하고 넘어갈 수 있어서 재밌는 시간이었다.

매일 매일 조금씩 읽은 것들에 대해 하나의 주제를 잡고 정리해보는 습관을 가져봐야겠다.

댓글 공유

opacity

요소의 투명도를 설정하는 속성이다.

0 ~ 1.0 사이의 값을 가질 수 있으며 기본값은 1이다.

1
2
3
img {
opacity: 0.5;
}

exam

특징

내부의 자식 요소들에게도 상속이 된다. 그러므로 내부의 텍스트가 있다면 가독성이 떨어질 수 있으므로 조심해야한다.

또한, RGBA를 사용하여 투명도를 설정할 수 있다.

exam2

1
2
3
div {
background: rgba(76, 175, 80, 0.3); /* Green background with 30% opacity */
}

댓글 공유

loco9939

author.bio


author.job