0%

201202_TIL(let, const, 프로퍼티 어트리뷰트)

오늘 배운 것

let, const와 블록 레벨 스코프

1
2
3
4
5
6
7
// 전역 변수
var x = 1;

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

window는 전역 객체면서 window 자체가 전역 스코프이다.

프로퍼티 어트리뷰트

프로퍼티 어트리뷰트는 프로퍼티의 상태를 나타낸다. 프로퍼티 상태는 프로퍼티의 값(value), 값의 갱신 가능 여부(writable), 열거 가능 여부(enumerable), 재정의 가능 여부(configurable)를 말한다.

프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태가 값은 내부 슬롯이다. 또한 내부 상태 값이 메서드로 되어있는 것은 내부 메서드( [[Environment]], [[Prototype]] )이다.

내부 슬롯(internal slot)

내부 슬롯( [[Value]], [[Writable]], [[Enumerable]], [[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
37
38
39
40
const o = {
x: 1,
y: function () {},

//접근자 프로퍼티
_z: 1,
get z() {
return _x;
},
set z(v) {
this._z = v;
},
};

console.log(Object.getOwnPropertyDescriptor(o, "x"));
// { value: 1, writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptor(o, `y`));
/*
{
value: [Function: y],
writable: true,
enumerable: true,
configurable: true
} */

console.log(Object.getOwnPropertyDescriptor(o, `z`));
/* {
get: [Function: get z],
set: [Function: set z],
enumerable: true,
configurable: true
} */

// getter 실행
console.log(o.x); // 1

//setter 실행
o.x = 2;
console.log(o.x); // 2

접근자 프로퍼티 ([[Get]], [[Set]] )를 잘 이용하는 것이 중요하다. 함수의 경우 호출할 때 인수가 필요할 수도 있기 때문에, 인수가 필요없는 접근자 프로퍼티가 유용하고 간편할 때가 있다.

접근자 프로퍼티는 자체적으로 갖는 값과 내부슬롯이 없다. 접근자 프로퍼티는 다른 데이터 프로퍼티의 값을 조작한다.

1
2
3
4
5
6
7
8
const circle = {
radius: 10,
get getDiameter() {
return 2 * this.radius;
},
};

console.log(circle.getDiameter); // 20

생성자 함수에 의한 객체 생성

객체 리터럴 말고 생성자 함수에 의해서도 객체를 생성할 수 있다.

생성자 함수(Contruct) = 객체를 생성하는 함수

생성자 함수 이름, Class는 파스칼 케이스로 네이밍한다.

객체 리터럴은 하나의 객체를 만들 때는 유용하지만 같은 형식의 객체를 여러개 만들 때는 생성자 함수가 유리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Circle(radius) {
this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}

// 일반 함수 호출
// 여기서의 this는 전역 객체
const circle = Circle(10);
console.log(circle); // undefined

// 생성자 함수
// 여기서의 this는 생성자 함수가 생성할 인스턴스
const circle1 = new Circle(5);
const circle2 = new Circle(20);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 40

객체 Circle의 프로퍼티 radius는 현재 상태를 말한다. 따라서 각각의 Circle은 radius가 각각 다를 수 있으며 중복되는 것이 아니다. 하지만 객체 Circle의 메서드는 모든 Circle 객체에서 동일하며 중복된다. 메서드 중복을 방지하는 것은 프로토타입을 알아야 가능하다.

생성자 함수를 파스칼 케이스 사용하여 구별하는 이유가 new를 붙여서 호출하라는 뜻이기도하다. (방어코드도 생성하는 것이 좋음 new.target 등)

생성사 함수로서 호출되면 맨 위에 this에 빈객체가 바인딩되고 동적으로 프로퍼티가 추가된다.

1
2
3
4
5
6
7
8
9
10
11
12
function Circle(radius) {
console.log(this); // Circle {}
this.radius = radius;
console.log(this); // Circle { radius: 100 }
this.getDiameter = function () {
return 2 * this.radius;
};
console.log(this); // Circle { radius: 100, getDiameter: [Function] }
}

const circle4 = new Circle(100);
console.log(circle4); // Circle { radius: 100, getDiameter: [Function] }

this 바인딩은 함수 호출 방식에 따라 동적으로 결정된다.

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
function foo() {
console.log(this);
}

// 일반 함수로서 호출
foo(); // this는 전역 객체
/*
Object [global] {
global: [Circular],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
},
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Function]
}
}
*/

// 객체 생성
const o = {
a: 1,
b: 2,
foo,
};
// 메서드로서 호출
o.foo; //this는 .앞에 붙은 o를 뜻한다.
console.log(o); // 위 o.foo와 같은 결과가 나온다.
// { a: 1, b: 2, foo: [Function: foo] }

// 생성자 함수로서 호출
new foo(); // 빈 객체 생성자 함수
// foo {}

내부 메서드 [[Call]]과 [[Construct]]

new 연산자와 함께 함수를 호출하면 해당 함수는 생성자 함수로 동작하며 함수 객체의 내부 메서드 [[Call]]이 호출되는 것이 아니라 [[Construct]]가 호출된다.

  • constructor: 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
1
2
3
4
5
6
7
8
9
10
const o = {
foo: function () {}, // ES6에서는 엄밀하게는 메서드가 아니다.
bar() {}, // 메서드이다. ES6의 메서드 축약 표현만을 메서드로 인정한다.
};

console.log(o.foo()); // undefined
console.log(new o.foo()); // foo {}

console.log(o.bar()); // undefined
console.log(new o.bar()); // TypeError: o.bar is not a constructor
1
2
3
4
// 화살표 함수 정의
const arrow = () => {};
console.log(arrow()); // undefined
console.log(new arrow()); //TypeError: arrow is not a constructor

new.target

new 연산자 없이 일반 함수로서 호출된 함수 내부의 new.target은 undefined다.

1
2
3
4
5
6
7
8
9
function foo() {
console.log(new.target);
}

// foo.[[Call]]
foo(); // undefined

// foo.[[Construct]]
new foo(); // [Function: foo]

undefined값이 false인 것을 이용한다.

1
2
3
4
5
6
7
8
9
10
function foo() {
if (!new.target) return new foo();
console.log(new.target);
}

// foo.[[Construct]]
foo(); // [Function: foo]

// foo.[[Construct]]
new foo(); // [Function: foo]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 생성자 함수
function Circle(radius) {
// 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined다.
if (!new.target) {
// new 연산자와 함께 생성자 함수를 재귀 호출하여 생성된 인스턴스를 반환한다.
return new Circle(radius);
}

this.radius = radius;
this.getDiameter = function () {
return 2 * this.radius;
};
}

// new 연산자 없이 생성자 함수를 호출하여도 new.target을 통해 생성자 함수로서 호출된다.
const circle = Circle(5);
console.log(circle.getDiameter());

아래 예를 보면 둘 다 생성자 함수로 동작한다. 즉, Object 생성자 함수는 new.target과 같은 행위를 자동으로 한다.

1
2
3
4
5
let obj = new Object();
console.log(obj); // {}

obj = Object();
console.log(obj); // {}

함수와 일급 객체

일급 객체

함수형 프로그래밍이 가능하려면 함수가 일급 객체여야한다.

  1. 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.

    1
    2
    3
    4
    5
    6
    function foo(f) {
    f();
    }

    // 런타임에 함수 만들어서 인수로 함수를 넘김
    foo(function () {});
  2. 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.

  3. 함수의 매개변수에게 전달할 수 있다.

  4. 함수의 반환값으로 사용할 수 있다.

함수 객체의 프로퍼티

arguments, caller, length, name, prototype 프로퍼티는 모두 함수 객체의 데이터 프로퍼티다. 이들 프로퍼티는 일반 객체에는 없는 함수 객체 고유의 프로퍼티다. 하지만 proto는 접근자 프로퍼티이며, 함수 객체 고유의 프로퍼티가 아니라 Object.prototype 객체의 프로퍼티를 상속받은 것이다. Object.prototype 객체의 프로퍼티는 모든 객체가 상속받아 사용할 수 있다. 즉, Object.prototype 객체의 __**proto__** 접근자 프로퍼티는 모든 객체가 사용할 수 있다.

arguments

arguments는 유사배열 객체이다.

1
2
3
4
5
6
7
8
9
const sum = function () {
let res = 0;
for (let i = 0; i < arguments.length; i++) {
res += arguments[i];
}
return res;
};

console.log(sum(1, 2, 3, 4, 5, 6, 7));

caller 프로퍼티

함수 객체의 caller 프로퍼티는 함수 자신을 호출한 함수를 가리킨다.

에러처리할 때 유용하다.

1
2
3
4
5
6
7
8
9
10
11
function foo(func) {
return func();
}

function bar() {
return "caller : " + bar.caller;
}

// 브라우저에서의 실행한 결과
console.log(foo(bar)); // caller : function foo(func) {...}
console.log(bar()); // caller : null

length 프로퍼티

함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킨다.

arguments 객체의 length 프로퍼티와 함수 객체의 length 프로퍼티의 값은 다를 수 있으므로 주의해야 한다. arguments 객체의 length 프로퍼티는 인자(argument)의 개수를 가리키고, 함수 객체의 length 프로퍼티는 매개변수(parameter)의 개수를 가리킨다.

name 프로퍼티

함수 객체의 name 프로퍼티는 함수 이름을 나타낸다. name 프로퍼티는 ES6 이전까지는 비표준이었다가 ES6에서 정식 표준이 되었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 기명 함수 표현식
var namedFunc = function foo() {};
console.log(namedFunc.name); // foo

// 익명 함수 표현식
var anonymousFunc = function () {};
// ES5: name 프로퍼티는 빈 문자열을 값으로 갖는다.
// ES6: name 프로퍼티는 함수 객체를 가리키는 변수 이름을 값으로 갖는다.
console.log(anonymousFunc.name); // anonymousFunc

// 함수 선언문(Function declaration)
function bar() {}
console.log(bar.name); // bar

prototype 프로퍼티

prototype 프로퍼티는 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor만이 소유하는 프로퍼티다.

생성자 함수가 생성될 때 prototype 프로퍼티를 가지고 생성된다. 처음 prototype에는 constructor 프로퍼티만을 가지며 이 프로퍼티는 함수 객체를 가리킨다. new로 인스턴스를 생성하면 새로운 인스턴스는 부모격인 생성자 함수의 prototype과 연결된다.

모든 객체는 자신의 부모 역할을 하는 prototype객체와 연결된다.

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log(`Hi, I am ${this.name}`);
};
}

const me = new Person("Lee");
const you = new Person("Kim");
me.sayHi(); // Hi, I am Lee
you.sayHi(); // Hi, I am Kim

prototype 프로퍼티는 함수가 객체를 생성하는 생성자 함수로 호출될 때 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.
sayHi 함수는 계속 중복되어 생성된다. 이 함수를 prototype의 프로퍼티로 넣으면 중복 생성하지 않아도 된다.

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

Person.prototype.sayHi = function () {
console.log(`Hi, I am ${this.name}`);
};

const me = new Person("Lee");
const you = new Person("Kim");
me.sayHi(); // Hi, I am Lee
you.sayHi(); // Hi, I am Kim

Person.prototype은 후에 생성할 인스턴스에 필요한 것들을 위한 프로퍼티다.
Person을 가지고 prototype을 찾아갈 수 있다. 인스턴스의 [[Prototype]]을 간접적으로 이용해서 Person.prototype을 찾아갈 수 있다.( __proto__ 접근자 프로퍼티)

prototype chain은 scope chain과 비슷한 방식으로 프로퍼티를 찾는 연결된 순서이다.

Nyong’s GitHub