해당 글의 내용은 poiemaweb 을 읽고 요약하여 재정리한 내용입니다.
이번에는 JavaScript lesson 14~17에 해당되는 프로토타입, 스코프, Strict mode, this에 대해 알아보겠습니다.
14. 프로토타입(Prototype)
프로토타입 객체
JavaScript의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있습니다.
이러한 부모 객체를 Prototype 객체 줄여서 Prototype(프로토타입)
이라고 합니다.
해당 객체는 생성자 함수에 의해 생성된 각각의 객체에 공유 프로퍼티를 제공하기 위해 사용합니다.
객체를 생성할 때 프로토타입은 결정되며, 결정된 프로토타입 객체는 다른 임의의 객체로 변경할 수 있습니다. (동적으로 변경할 수 있음을 의미)
이러한 특징을 활용하여 객체의 상속을 구현할 수 있습니다.
[[Prototype]] vs prototype 프로퍼티
모든 객체는 자신의 프로토타입 객체를 가리키는 [[Prototype]]
인터널 슬롯을 가지며 상속을 위해 사용됩니다.
[[Prototype]]
은 객체의 입장에서 자신의 부모 역할을 하는 프로토타입 객체를 가리키며 함수 객체의 경우 Function.prototype
를 가리킵니다.
함수 객체는 일반 객체와 달리 prototype 프로퍼티
도 가지게 되는데, 이는 인터널 슬롯과는 차이가 있으니 주의해야 합니다.
함수 객체만 prototype 프로퍼티
를 갖고 있으며, 함수 객체가 생성자로 사용될 때 해당 함수를 통해 생성될 객체의 부모 역할을 하는 객체(프로토타입 객체)를 가리킵니다.
constructor 프로퍼티
프로토타입 객체는 constructor
프로퍼티를 갖는데, 해당 프로퍼티는 객체의 입장에서 자신을 생성한 객체를 가리킵니다.
function Person(name) {
this.name = name;
}
var foo = new Person('Lee');
// Person() 생성자 함수에 의해 생성된 객체를 생성한 객체는 Person() 생성자 함수임
console.log(Person.prototype.constructor === Person);
// foo 객체를 생성한 객체는 Person() 생성자 함수임
console.log(foo.constructor === Person);
// Person() 생성자 함수를 생성한 객체는 Function() 생성자 함수임
console.log(Person.constructor === Function);
Prototype chain
JavaScript는 특정 객체의 프로퍼티나 메소드에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티 또는 메소드가 없다면 [[Prototype]]
이 가리키는 링크를 따라갑니다.
자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드를 차례대로 검색하여 찾습니다.
객체 리터럴 방식으로 생성된 객체의 프로토타입 체인
객체 리터럴 방식({}
로 생성)으로 생성된 객체는 내장 함수인 Object()
생성자 함수로 객체를 생성하는 것을 단순화시킨 것 입니다.
JavaScript 엔진은 객체 리터럴 방식의 코드를 만나면 내부적으로 Object()
생성자 함수를 사용하여 객체를 생성합니다.
객체 리터럴을 사용하여 객체를 생성한 경우, 해당 객체의 프로토타입 객체는 Object.prototype
입니다.
자세한 설명은 원본 글을 참고하시길 바랍니다.
생성자 함수로 생성된 객체의 프로토타입 체인
생성자 함수를 정의할 때 3가지의 함수 정의 방식(함수선언식, 함수표현식, Function()생성자 함수)을 사용하게 됩니다.
어떠한 방식으로 함수 객체를 생성해도 Function()
생성자 함수를 통해 함수 객체를 생성하기 때문에, 모든 함수 객체의 prototype
객체는 Function.prototype
입니다.
생성자 함수도 함수 객체이기에 생성자 함수의 prototype
객체도Function.prototype
입니다.
이 Function.prototype
의 프로토타입 객체는 Object.prototype
객체 입니다.
따라서 프로토타입 체인의 종점은 모든 객체의 부모 객체인 Object.prototype
객체로 해당 객체에서 프로토타입 체인이 끝나게 됩니다.
프로토타입 객체의 확장
프로토타입 객체도 일반 객체와 같이 프로퍼티를 추가, 삭제 할 수 있습니다.
그리고 이렇게 추가/삭제된 프로퍼티는 즉시 프로토타입 체인에 반영됩니다.
function Person(name) {
this.name = name;
}
var foo = new Person('Lee');
Person.prototype.sayHello = function(){
console.log('Hi! my name is ' + this.name);
};
foo.sayHello();
위 코드에서 Person.prototype
객체에 메소드 sayHello
를 추가했습니다.
이 때 sayHello
메소드는 프로토타입 체인에 반영됩니다.
그러므로 생성자 함수 Person
에 의해 생성된 모든 객체는 프로토타입 체인에 의해 부모 객체인 Person.prototype
의 메소드를 사용할 수 있게 됩니다.
원시 타입(Primitive data type)의 확장
원시 타입은 객체가 아니므로 프로퍼티나 메소드를 가질수 없고, 직접 추가할 수 없습니다.
하지만 원시 타입으로 프로퍼티나 메소드를 호출할 때 원시 타입과 연관된 객체로 일시적으로 변환되어 프로토타입 객체를 공유하게 됩니다.
예를 들어 String
객체의 프로토타입 객체 String.prototype
에 메소드를 추가하면 원시 타입, 객체 모두 메소드를 사용할 수 있습니다.
내장 객체의 Global objects인 String
, Number
, Array
객체 등이 가지고 있는 표준 메소드는 프로토타입 객체인 String.prototype
, Number.prototype
, Array.prototype
등에 정의 되어 있습니다.
이것들의 프로토타입 객체 또한 Object.prototype
를 프로토타입 체인에 의해 자신의 프로토타입 객체로 연결 합니다.
프로토타입 객체의 변경
앞에서 부모 객체인 프로토타입을 동적으로 변경할 수 있으며, 상속을 구현할 수 있다고 했습니다.
프로토타입 객체를 변경할 때 주의할 것이 있습니다.
프로토타입 객체 변경 시점 이전에 생성된 객체
→ 기존 프로토타입 객체를
[[Prototype]]
에 바인딩함프로토타입 객체 변경 시점 이후에 생성된 객체
→ 변경된 프로토타입 객체를
[[Prototype]]
에 바인딩함
function Person(name) {
this.name = name;
}
var foo = new Person('Lee');
// 프로토타입 객체 변경
Person.prototype = { gender: 'male' };
var bar = new Person('Kim');
console.log(foo.gender); // undefined
console.log(bar.gender); // 'male'
동작 원리는 다음과 같습니다.
constructor
프로퍼티는Person()
생성자 함수를 가리킵니다.- 프로토타입 객체 변경 후,
Person()
생성자 함수의Prototype
프로퍼티가 가리키는 프로토타입 객체를 일반 객체로 변경 하면서Person.prototype.constructor
프로퍼티도 삭제 됩니다. - 프로토타입 체인에 의해
bar.constructor
의 값은 프로토타입 체이닝에 의해Object.prototype.constructor
즉Object()
생성자 함수가 됩니다.
프로토타입 체인 동작 조건
객체의 프로퍼티를 참조하는 경우, 해당 객체에 프로퍼티가 없는 경우, 프로토타입 체인이 동작 합니다.
객체의 프로퍼티에 값을 할당하는 경우에는 프로토타입 체인이 동작하지 않습니다.
객체에 해당 프로퍼티가 있는 경우, 값을 재할당하고 해당 프로퍼티가 없는 경우는 해당 객체에 프로퍼티를 동적으로 추가하기 때문입니다.
15. 스코프(Scope)
스코프란?
참조 대상 식별자를 찾아내기 위한 규칙 입니다.
변수는 전역 또는 코드 블록이나 함수 내에 선언하며 코드 블록이나 함수는 중첩될 수 있습니다.
식별자는 자신이 어디에서 선언 됐는지에 의해 자신이 유효한(다른 코드가 자신을 참조할 수 있는) 범위를 갖습니다.
var x = 'global';
function foo () {
var x = 'function scope';
console.log(x);
}
foo(); // ?
console.log(x); // ?
위 예시에서 함수 foo
내에서 선언된 변수 x
는 함수 foo
내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없는데, 이러한 규칙을 스코프라고 합니다.
스코프의 구분
JavaScript에서 구분을 해보면 2가지로 나눌 수 있습니다.
전역 스코프 (Global scope)
코드 어디에서든지 참조할 수 있음
지역 스코프 (Local scope or Function-level scope)
함수 코드 블록이 만든 스코프, 함수 자신과 하위 함수에서만 참조할 수 있음
변수는 선언 위치(전역 또는 지역)에 의해 스코프를 가지게 됩니다.
전역에서 선언된 변수는 전역 변수, 지역(함수 내부)에서 선언된 변수는 지역 변수가 됩니다.
자바스크립트 스코프의 특징
다른 언어(C언어 등)들은 코드 블록({…}
)내에서 유효한 스코프인 블록 레벨 스코프(block-level scope)를 따릅니다.
JavaScript는 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않는 함수 레벨 스코프(function-level scope)를 따릅니다.
단, ES6에서 도입된 let
키워드를 사용하면 블록 레벨 스코프를 사용할 수 있습니다.
전역 스코프(Global scope)
var
키워드로 선언한 전역 변수는 전역 객체(Global Object) window
의 프로퍼티 입니다.
JavaScript는 다른 언어(C언어 등)와는 달리 특별한 시작점이 없으며 코드가 나타나는 즉시 해석되고 실행됩니다.
이는 전역에 변수를 선언하기 쉬움을 의미하며, 전역 변수를 남발하게 하는 문제를 일으킬 수 있습니다.
따라서 전역 변수의 사용은 변수 이름이 중복될 수 있고, 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만들기 때문에 사용을 억제해야 합니다.
함수 레벨 스코프(Function-level scope)
var x = 'global';
function foo() {
var x = 'local';
console.log(x); // local
function bar() { // 내부함수
console.log(x); // local
}
bar();
}
foo();
console.log(x); // global
함수(지역) 영역에서 전역변수를 참조할 수 있기 때문에 전역변수의 값도 변경할 수 있습니다.
내부 함수의 경우, 전역변수는 물론 상위 함수에서 선언한 변수에 접근/변경이 가능합니다.
var foo = function ( ) {
var a = 3, b = 5;
var bar = function ( ) {
var b = 7, c = 11;
// 이 시점에서 a는 3, b는 7, c는 11
a += b + c;
// 이 시점에서 a는 21, b는 7, c는 11
};
// 이 시점에서 a는 3, b는 5, c는 not defined
bar( );
// 이 시점에서 a는 21, b는 5
};
중첩 스코프의 경우 가장 인접한 지역을 우선하여 참조하게 됩니다.
렉시컬 스코프
JavaScript를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프(또는 정적 스코프)를 따릅니다.
렉시컬 스코프는 함수를 어디에서 호출하는지가 아니라 어디에 선언했는지에 따라 결정됩니다.
따라서 JavaScript는 함수를 선언한 시점에서 상위 스코프가 결정되고, 어디에서 호출했는지는 아무런 의미를 주지 않습니다.
암묵적 전역
var a = 15; // 전역 변수
function foo () {
b = 25; // 선언하지 않은 식별자
console.log(a + b);
}
foo(); // 40
위 코드에서 선언하지 않은 식별자인 b는 선언된 변수처럼 동작하고 있습니다.
이는 선언하지 않은 식별자에 값을 할당하면 전역 객체의 프로퍼티가 되기 때문입니다.
b는 전역 객체의 프로퍼티가 되어 마지 전역 변수(window.b=25
)처럼 동작하게 되며, 이를 암묵적 전역(implicit global)이라고 합니다.
하지만 b는 객체의 프로퍼티로 추가되었을 뿐 변수는 아니기 때문에 변수 호이스팅이 발생하지 않습니다. (console.log
로 확인 가능)
또, 단순 프로퍼티이기 때문에 delete
연산자로 삭제할 수 있습니다. (전역 변수는 delete
로 삭제 X)
최소한의 전역 변수 사용
var MYAPP = {};
MYAPP.student = {
name: 'JM',
gender: 'female'
};
console.log(MYAPP.student.name); // JM
전역 변수 사용을 최소화하기 위해서는 위와 같이 전역 변수 객체 하나를 만들어 사용하는 방법이 있습니다.
즉시 실행 함수를 이용한 전역 변수 사용 억제
(function () {
var MYAPP = {};
MYAPP.student = {
name: 'JM',
gender: 'female'
};
console.log(MYAPP.student.name); // JM
}());
console.log(MYAPP.student.name); // ReferenceError: MYAPP is not defined
전역 변수 사용을 억제하기 위해 즉시 실행 함수(IIFE)를 사용할 수 있습니다.
16. Strict mode(안정적인 개발 환경을 위한)
strict mode란?
개발자의 의도와는 상관없이 JavaScript 엔진이 생성한 암묵적 전역 변수는 오류를 발생시키는 원인이 될 가능성이 큽니다.
따라서 반드시 var
, let
, const
키워드를 사용하여 변수를 선언한 다음 변수를 사용해야 합니다.
잠재적인 오류를 발생시키기 어려운 개발 환경을 지원하기 위해 ES5부터 strict mode
가 추가 되었습니다.
문법을 엄격히 적용하여 기존에 무시 되던 오류를 발생시킬 가능성이 높거나 작업에 문제 일으킬 수 있는 코드에 대해 명시적인 에러를 발생 시킵니다.
ESLint
와 같은 정적 분석 도구를 사용해도 strict mode
와 유사한 효과를 얻을 수 있습니다.
- 정적 분석? 소스 코드의 실행 없이 정적으로 프로그램의 문제를 찾는 과정
(원본 글의 필자는 개인적으로 strict mode 보다 린트 도구의 사용을 선호한다고 하네요 - ESLint 사용법)
strict mode의 적용
전역의 선두 or 함수 몸체의 선두에 'use strict';
를 추가하면 적용되며, 코드의 선두에 추가해야 적용됩니다.
전역의 선두에 추가하게 되면 스크립트 전체에 strict mode
가 적용됩니다.
전역에 strict mode
를 사용하는 것은 권장되지 않는데, 이는 non-strict mode
와 혼용하게 될 가능성도 있으며, 이로 인해 오류를 발생시킬 수 있기 때문입니다.
외부 서드 파티 라이브러리를 사용하는 경우 라이브러리가 non-strict mode
일 경우도 있기에 전역에 strict mode
를 적용하는 건 바람직하지 않습니다.
함수 단위로 적용하는 것도 권장되지 않는데, 이 또한 혼용될 가능성이 있으며 일일이 적용하는 것이 번거롭기 때문입니다.
이를 해결하기 위해서는 즉시실행함수로 감싼 스크립트 단위로 적용하는 것이 바람직합니다.
strict mode가 발생시키는 에러
적용했을 때 에러가 발생하는 대표적인 사례는 다음과 같습니다.
암묵적 전역 변수 : 선언하지 않은 변수 참조 시
Reference Error
변수, 함수, 매개변수의 삭제 :
delete
사용 시Syntax Error
매개변수 이름 중복 : 중복된 함수 파라미터 이름 사용 시
Syntax Error
with
문의 사용 :Syntax Error
발생일반 함수의
this
: 일반 함수 내부에서this
를 사용할 필요가 없기 때문에undefined
가 바인딩 됨 (에러 발생 X)
17. 함수 호출 방식에 의해 결정되는 this
함수는 호출될 때, 매개변수로 전달되는 인자값 이외에 arguments
객체와 this
를 암묵적으로 전달 받습니다.
JavaScript의 경우 함수 호출 방식에 따라 this
에 바인딩 되는 객채가 동적으로 결정 됩니다.
이는 함수가 어떻게 호출했는지에 따라 바인딩할 객체가 결정되는 것을 의미합니다.
- 바인딩? 프로그램의 어떤 기본 단위가 가질 수 있는 구성요소의 구체적인 값, 성격을 확정하는 것
함수 호출
전역 객체(Global Object)는 모든 객체의 유일한 최상위 객체를 의미하며, 일반적으로 Browser-side
에서는 window
객체를 의미합니다. (Server-side
(Node.js)에서는 global
객체를 의미)
전역 객체는 전역 스코프를 갖는 전역 변수를 프로퍼티로 소유합니다.
var value = 1;
var obj = {
value: 100,
foo: function() {
console.log("foo's this: ", this); // obj
console.log("foo's this.value: ", this.value); // 100
function bar() {
console.log("bar's this: ", this); // window
console.log("bar's this.value: ", this.value); // 1
}
bar();
}
};
obj.foo();
기본적으로 this
는 전역 객체에 바인딩되며, 전역 함수는 물론이고 내부 함수의 경우도 전역객체에 바인딩 됩니다.
또, 메소드의 내부 함수일 경우와 콜백함수의 경우에도 this
는 전역 객체에 바인딩 됩니다.
내부 함수는 일반 함수, 메소드, 콜백함수 어디에서 선언 되었는지 관계 없이 this
는 전역 객체를 바인딩 합니다.
이를 회피하는 방법으로는 위 코드에서 foo 함수 코드 선두에 var that = this;
를 추가하면 됩니다.
이외에도 this
를 명시적으로 바인딩할 수 있는 apply
, call
, bind
메소드가 있습니다.
메소드 호출
var obj1 = {
name: 'Lee',
sayName: function() {
console.log(this.name);
}
}
var obj2 = {
name: 'Kim'
}
obj2.sayName = obj1.sayName;
obj1.sayName(); // Lee
obj2.sayName(); // Kim
함수가 객체의 프로퍼티 값이면 메소드로서 호출되는데, 이때 메소드 내부의 this
는 해당 메소드를 호출한 객체에 바인딩 됩니다.
프로토타입 객체도 메소드를 가질 수 있으며, 이 또한 해당 메소드를 호출한 객체에 바인딩 됩니다.
생성자 함수 호출
생성자 함수는 객체를 생성하는 역할을 하며, 기존 함수에 new
연산자를 붙여서 호출하면 생성자 함수로 동작하게 됩니다.
생성자 함수 동작 방식
- 빈 객체 생성 및
this
바인딩 - → 코드가 실행되기 전 빈 객체가 생성되며,
this
는 빈 객체를 가리키게 됩니다. this
를 통한 프로퍼티 생성- →
this
를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가됩니다. - 생성된 객체 반환(
this
를 반환하지 않는 함수는 생성자 함수로서의 역할을 수행하지 못하기 때문에 생성자 함수는 반환문을 사용하지 않음) - → 반환문이 없는 경우 새로 생성한 객체가 반환되며, 다른 객체를 명시적으로 반환하는 경우에는
this
가 아닌 해당 객체가 반환 됩니다.
생성자 함수에 new 연산자를 붙이지 않고 호출할 경우
객체 생성 목적으로 작성한 생성자 함수를 new
없이 호출 or 일반 함수에 new
를 붙여 호출하면 this
바인딩 방식이 다르기 때문에 오류가 발생할 수 있습니다.
일반 함수를 호출 → this는 전역객체에 바인딩됨
new
연산자와 함께 생성자 함수를 호출 →this
는 생성자 함수가 암묵적으로 생성한 빈 객체에 바인딩됨
function Person(name) {
// new없이 호출하는 경우, 전역객체에 name 프로퍼티를 추가
this.name = name;
};
// 일반 함수로서 호출되었기 때문에 객체를 암묵적으로 생성하여 반환하지 않는다.
// 일반 함수의 this는 전역객체를 가리킨다.
var me = Person('Lee');
console.log(me); // undefined
console.log(window.name); // Lee
생성자 함수를 new
없이 호출한 경우, 함수 Person 내부의 this
는 전역객체를 가리키므로 name은 전역 변수(window)에 바인딩 됩니다.
또한 new
와 함께 생성자 함수를 호출하는 경우에 암묵적으로 반환하던 this
도 반환하지 않으며, 반환문이 없으므로 undefined
를 반환하게 됩니다.
이러한 방법을 해결하기 위해서는 예전에는 원본 글처럼 arguments.callee
를 사용했습니다.
하지만 ES5부터 strict mode
에서 제거 되었으며, ES6부터 도입된 class
문법을 사용 합니다.
// ES6 class 문법
class MyClass {
constructor() {
// 생성자 함수 내용
}
}
var obj = new MyClass(); // 올바르게 생성자 함수를 호출
apply/call/bind 호출
JavaScript 엔진의 암묵적 this
바인딩 이외에 this
를 특정 객체에 명시적으로 바인딩하는 방법 입니다.
var Person = function (name) {
this.name = name;
};
var foo = {};
// apply 메소드는 생성자함수 Person을 호출. 이때 this에 객체 foo를 바인딩함
Person.apply(foo, ['name']);
console.log(foo); // { name: 'name' }
위의 코드에서 Person함수의 this
는 foo 객체가 되며, this
에 바인딩된 foo객체에 name
프로퍼티가 없기 때문에 name
프로퍼티가 동적 추가되고 값이 할당됩니다.
apply()
메소드의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에 배열 메소드를 사용하는 경우 입니다.
자세한 설명은 원본 글을 참고하시길 바랍니다.
call()
메소드의 경우, apply()
와 기능은 같지만 apply()
의 두번째 인자에서 배열 형태로 넘긴 것을 각각 하나의 인자로 넘깁니다.
// apply와 call의 차이
Person.apply(foo, [1, 2, 3]);
Person.call(foo, 1, 2, 3);
bind()
의 경우에는 함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴합니다.
여기까지 JavaScript lesson 14~17에 해당되는 프로토타입, 스코프, Strict mode, this에 대해 알아보았습니다.