커어어어억
내용이 슬슬 어려워지기 시작한다.
우리는 여태 객체를 생성 할 때
객체 리터럴
방법을 이용해 객체를 생성했다.
var a = 'string';
var b = 123;
var c = true;
var d = function () {};
var e = {};
다음처럼 변수 명에 문자 리터럴, 숫자 리터럴, 불리언 리터럴 , 함수 리터럴 , 객체 리터럴 ...
등
리터럴 표기법을 이용해서 변수에 값을 할당하거나 객체를 설정했다.
console.log(a.constructor);
console.log(b.constructor);
console.log(c.constructor);
console.log(d.constructor);
console.log(e.constructor);
[Function: String]
[Function: Number]
[Function: Boolean]
[Function: Function]
[Function: Object]
이 때 각 변수들의 내부 메소드인 constructor
를 사용하면 뜬금없게도 [Function
: 함수명
] 이 나타난다.
여기서 constructor
는 우리 말로 하면 생성자
로 해당 변수의 생성자는 Function
이면서, 함수명
이란 함수를 이용해 생성 되었다는 뜻이다.
결국 객체를 생성한다는 것은 생성자 함수
를 통해 만드는 것임을 알 수 있다.
let person1 = new Object({
name: 'lee',
age: 16,
});
let person2 = { name: 'lee', age: 16 };
console.log(person1);
console.log(person2);
console.log(person1.constructor);
console.log(person2.constructor);
{ name: 'lee', age: 16 }
{ name: 'lee', age: 16 }
[Function: Object]
[Function: Object]
동일한 프로퍼티를 가진 두 객체를 생성 할 때 하나는 객체 생성자 함수를 이용하여 생성하고, 하나는 객체 리터럴 방법을 이용해서 생성하였다.
두 객체의 생성자를 확인해보니 Object
로 같음을 알 수 있다.
이렇게 보면 객체 생성자를 사용하기 보다 객체 리터럴 방법으로 사용 하는 것이 훨씬 간편해보이며
이는 사실이다.
하지만 객체 리터럴 방법으로 객체를 생성할 때 발생하는 단점이 있는데, 그 때는 객체 생성자 방법을 이용하는 것이 간편하다.
정리
객체를 생성하는 것은
객체 생성자 함수
와객체 선언
인new
키워드를 이용해 객체를 생성한다.
만약 내가 어떤 숫자가 주어졌을 때 실제 숫자의 값과, 홀수인지를 물어보는 함수를 가진 객체를 생성하고 싶다고 해보자
let a = {
number: 2,
isOdds() {
if (this.number % 2) {
return true;
}
return false;
},
};
let b = {
number: 3,
isOdds() {
if (this.number % 2) {
return true;
}
return false;
},
};
console.log(a); // { number: 2, isOdds: [Function: isOdds] }
console.log(a.isOdds()); // false
console.log(b); // { number: 3, isOdds: [Function: isOdds] }
console.log(a.isOdds()); // true
변수 a,b
모두 동일한 프로퍼티를 갖는 객체임에도 불구하고 객체 리터럴 방법을 이용 할 때에는 코드를 반복해서 작성해야 한다는 단점이 존재한다.
해당 객체 리터럴에서 객체에 함수도 들어간 모습을 볼 수 있는데
함수 또한 객체이기 때문에 객체에 함수가 존재 할 수 있다.
여기서 익숙치 않은 문법이 나오는데 그건 this
문법이다.
this
this
는 객체 자신의 프로퍼티나 메소드를 참조하기 위한 자기 참조 변수 (self-referencing variable)
이다. this
가 가리키는 값, 즉 this 바인딩
은 호출 방식에 따라 동적으로 결정된다.
호출 방식 | this 바인딩 |
---|---|
전역 함수에서 호출 | 전역 객체 (브라우저에서는 `window`) |
메소드로서 호출 | 메소드가 속한 객체 |
함수로서 호출 | 전역 객체 또는 `undefined` (strict mode) |
생성자 함수로서 호출 | 새로 생성된 객체 |
call 또는 apply | 수동으로 지정한 객체 |
여기서 우리는 전역 함수에서 호출 , 메소드로서 호출 , 생성자 함수로서 호출
에 대해서만 알아보자
function foo() {
console.log(this);
}
foo();
<ref *1> Object [global] {
global: [Circular *1],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
clearInterval: [Function: clearInterval],
....
전역에서 생성된 영역에서 전역 함수의this
를 로그하면 전역 환경에 대한 내용이 로그 된다.
전역 함수에서 this
가 가리키는 것은 전역 객체
이다.
메소드로서의 호출은 함수를 객체의 메소드로서 사용될 때를 의미한다.
function foo() {
console.log(this);
}
let obj = { name: 'lee', foo };
obj.foo(); // { name: 'lee', foo: [Function: foo] }
다음 같은 경우는 foo
라는 함수가 obj
라는 객체의 프로퍼티로 들어갔다. 메소드로서 말이다.
메소드로서 호출되면 foo
는 전역 객체를 가리키는 것이 아닌 메소드가 선언된 객체를 가리킨다.
이부분은 추후 밑에서 설명하도록 한다.
function foo() {
console.log(this);
}
let obj = new foo();
obj; // foo {}
new
키워드와 함께 사용하여 생성자 함수로서 호출하게 되면 생성자 함수가 (미래에) 생성할 인스턴스를 반환한다.
인스턴스
생성자 함수로 인해 생성된 객체를 인스턴스라고 한다.
function foo() {
console.log('foo!');
}
console.log(foo); // [Function: foo]
foo.age = 16;
foo.address = 'korea';
console.log(foo); // [Function: foo] { age: 16, address: 'korea' }
그렇기 때문에 함수에 프로퍼티를 추가하면 프로퍼티에 객체가 추가되는 모습을 볼 수 있다.
이것은 예시를 위해 사용했을 뿐 실제로 함수에 객체를 추가하는 행위는 권장되지 않는다.
그럼 위에서 표현했던
let a = {
number: 2,
isOdds() {
if (this.number % 2) {
return true;
}
return false;
},
};
let b = {
number: 3,
isOdds() {
if (this.number % 2) {
return true;
}
return false;
},
};
를 생성자 함수를 이용하여 표현해보자
function Naming(num) {
this.num = num;
this.isOdds = function () {
if (this.num % 2) return true;
else return false;
};
}
위의 로직을 표현하는 함수 Naming
을 만들어주자
이 때 함수 몸체 안에 this
를 이용하여 Naming
함수의 객체 this.num , this.isOdds
를 설정해주었다.
이것이 의미하는 것은 Naming
함수에 매개변수 값 num
이 들어오면 Naming
함수가 새로운 객체 {}
를 만들고 this
는 방금 만든 새로운 객체 {}
를 가리킨다.
this.num = num , this.isOdds = function(){ ... }
를 통해
방금 만든 새로운 객체 {}
의 프로퍼티와 값으로 설정해주겠다는 뜻이다.
let number2 = new Naming(2);
let number3 = new Naming(3);
console.log(number2);
console.log(number2.isOdds());
console.log(number3);
console.log(number3.isOdds());
Naming { num: 2, isOdds: [Function: isOdds] }
false
Naming { num: 3, isOdds: [Function: isOdds] }
true
이후 생성자 함수를 통해 반복되는 로직을 가진 객체를 생성할 수 있다.
스텝 바이 스텝으로 찾아보자
생성자 함수의 역할은 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 템플릿(클래스)으로서 동작하여 인스턴스를 생성하는 것과 생성된 인스턴스를 초기화 하는 것이다.
this
바인딩function Naming(num) {
// 인스턴스 생성 및 바인딩
this.num = num;
this.isOdds = function () {
if (this.num % 2) return true;
else return false;
};
}
new Naming
을 실행하는 순간 빈 객체은 {}
가 생성된다.
이 때 생성된 객체를 인스턴스
라고 하며 , Naming
내부에 존재하는 this
는 방금 생성된 인스턴스
를 가리킨다.
이러한 행위를 바인딩
이라고 한다.
바인딩
식별자와 값을 연결하는 행위
위에서는this
라는 식별자가 인스턴스를 가리킨다.
function Naming(num) {
// 인스턴스 생성 및 바인딩
this.num = num;
this.isOdds = function () {
if (this.num % 2) return true;
else return false;
};
// 인스턴스 초기화 및 할당
}
이후 Naming
내부에 존재하는 로직을 통해 인스턴스{} 이자 식별자는 this
의 프로퍼티들을 설정한다.
생성자 함수 내부에서 모든 처리가 끝나면 완성된 this
를 암묵적으로 반환한다.
this
를 반환한다는 것은 프로퍼티가 설정된인스턴스
를 반환한다는 것과 같다.
만약 함수 내에 반환문(return)
이 존재하며 객체를 반환한다면 this
가반환되지 못하고 명시한 객체가 반환된다.
function Naming(num) {
this.num = num;
this.isOdds = function isOdds() {
if (this.num % 2) return true;
else return false;
};
return { name: 'lee' }; // 객체를 반환
}
let number2 = new Naming(2);
console.log(number2); // { name: 'lee' }
하지만 만약 객체가 아닌 원시값을 반환한다면 무시한다.
그러니 생성자 함수는 객체를 반환하지 않는 한 암묵적으로 생성한 인스턴스를 반환한다.
new
빼면 어떻게 되는데 ?function Naming(num) {
this.num = num;
this.isOdds = function isOdds() {
if (this.num % 2) return true;
else return false;
};
}
let number2 = Naming(2);
console.log(number2); // undefined
생성자 선언문인 new
를 제거하면 함수는 생성자 함수가 아닌 일반 함수로서 호출되기 때문에
블록문 내의 결과 값을 return
한다.
이 때는 return
될 값이 없기에 undefined
가 반환되었다.
new
를 통해 함수가 일반 함수인지, 생성자 함수인지를 구분하는구나 !
[[call]]
과 [[Construct]]
일반적으로 함수는 호출되어 기능을 구현하는 역할을 한다.
하지만 new
를 사용하면 생성자 함수
로서의 역할을 한다.
함수는 상황에 따라 호출되어 발생하는 함수, 생성자 함수로서의 역할을 해야 하는데
그건 자바스크립트 엔진이 어떻게 평가할까 ?
이전 챕터에서 객체는 내부 어트리뷰트와 내부 메소드를 갖는다고 하였는데 함수 또한 객체이기 때문에 내부 어트리뷰트와 내부 메소드를 갖는다.
내부 어트리뷰트 | 설명 |
---|---|
[[Call]] | 함수가 호출될 때 수행되는 내부 메소드 |
[[Construct]] | 생성자로서 호출될 때 수행되는 내부 메소드 |
내부 메소드 | 설명 |
---|---|
[[GetPrototypeOf]] | 객체의 프로토타입을 반환하는 내부 메소드 |
[[SetPrototypeOf]] | 객체의 프로토타입을 설정하는 내부 메소드 |
[[GetOwnProperty]] | 객체의 속성을 반환하는 내부 메소드 |
[[HasProperty]] | 객체가 특정 속성을 가지고 있는지 확인하는 내부 메소드 |
보다 많은 메소드들이 있으나 우리가 주목해야 할 부분은 내부 어트리뷰트
이다.
함수가 호출 될 때 생성자 없이 호출되면 내부 어트리뷰트가 call
인 상태로 호출되어
함수 블록문 내부에 존재하는 로직을 평가한다.
하지만 생성자와 함께 호출되면 내부 어트리뷰트가 Construct
인 상태로 호출되어
생성자 함수로서 작동한다.
중요한 포인트는 모든 함수들은 호출 할 수 있기 때문에 call
이 가능하다. 이러한 함수의 특징을 callable
(호출가능한) 한 특성을 갖는다고 한다.
하지만 함수별로 생성자 함수로서 이용가능 할수도 있고, 없을 수도 있다.
생성자 함수로서 이용 가능한 함수를 constructor
이라 하고 , 불가능한 함수를 non-constructor
이라고 한다.
constructor
와 non-constructor
함수 | 설명 |
---|---|
function Example() {} | 기본 생성자 함수 |
class MyClass {} | ES6+ 클래스 문법은 또한 생성자 함수를 만듭니다 |
함수 | 설명 |
---|---|
const arrowFunction = () => {}; | 화살표 함수는 생성자를 갖지 않습니다 |
function regularFunction() {} | `class` 또는 `function` 키워드 없이 작성된 일반 함수는 생성자가 아닙니다 |
new.target
위에서 생성자 함수로 사용하기 위해선 생성자 선언문인 new
를 사용해야 한다고 하였다.
근데 만약 내가 생성자 함수를 만들어두고 생성자 선언문을 사용하지 않고 코드를 작성하면
예기치 못한 오류가 발생 할 수 있다.
이를 막기 위해 ES6
에서는 new.target
을 지원한다.
new.target
은 함수의 내부 어트리뷰트에 따라서 값이 변하낟.
만약 함수의 내부 어트리뷰트가 call
이라면 (일반 함수로서 호출된다면) new.target
은 undefined
값을 가리키게 된다.
하지만 내부 어트리뷰트가 constructor
라면 (생성자 함수로서 호출된다면) new.target
은 함수 자체를 가리키게 된다.
function Example() {
if (new.target) { // undefined 값은 데이터 타입 변환으로 falsy 한 값이다.
console.log('이 함수는 new 키워드로 호출되었습니다.');
} else {
console.log('이 함수는 new 키워드로 호출되지 않았습니다.');
}
}
new Example(); // "이 함수는 new 키워드로 호출되었습니다."
Example(); // "이 함수는 new 키워드로 호출되지 않았습니다."
new.target
을 이용해 생성자 함수로 사용되지 않은 함수를 찾아낼 수 있다.
이를 이용하여 재귀적으로 일반 호출로서 표현된 함수도 생성자 함수로 사용 할 수 있다.
function Naming(num) {
if (!new.target) {
console.log('new 를 까먹었구나?');
return new Naming(num);
}
this.num = num;
this.isOdds = function isOdds(num) {
if (num % 2) return true;
else return false;
};
}
let number2 = Naming(2);
console.log(number2);
// new 를 까먹었구나?
// Naming { num: 2, isOdds: [Function: isOdds] }