Node 환경에서 코딩을 하면서 this를 사용해야 하는 경우가 있었습니다. 그런데 경우에 따라서 이 this의 동작이 달랐습니다.
다른 언어에서와는 달리 JavaScript는 this의 의미가 다르게 동작하는 경우가 있어서 제대로 이해하고 사용하고자 정리해 두었습니다.
1. this 란?
컴퓨터 프로그래밍 언어에서 this는 현재 실행 중인 코드가 속한 객체, 클래스 또는 기타 엔티티를 참조하는 데 사용하는 키워드입니다. this가 참조하는 엔티티는 실행 콘텍스트(예: 해당 메서드가 호출되는 객체)에 따라 달라집니다.
문제는 프로그래밍 언어마다 이 키워드를 약간 다른 방식으로 사용합니다.
많은 프로그래밍 언어에서 this는 한 가지 의미로 사용하는 경우가 많습니다만, JavaScript의 경우에는 사용되는 위치에 따라 다르게 동작합니다.
2. JavaScript에서 this의 사용방법
JavaScript에서 함수의 this 키워드는 다른 언어와 조금 다르게 동작합니다.
또한 엄격 모드(strict mode)와 비엄격 모드(non-strict mode)에서도 일부 차이가 있습니다.
JavaScript에서 this는 대부분의 경우 어떻게 함수가 호출되는지(runtime binding)에 따라 this값이 결정됩니다. 실행 중에는 할당으로 this를 설정할 수 없고, 함수를 호출할 때마다 다를 수 있습니다.
ES5는 함수를 어떻게 호출했는지 상관하지 않고 this값을 설정할 수 있는 bind 메서드를 도입했고, ES2015는 스스로의 this 바인딩을 제공하지 않는 화살표 함수(arrow function)를 추가했습니다.
예시
const Person = {
name: "john",
getName: function () {
return this.name;
},
};
console.log(Person.getName()); // "john"
2.1 Syntax (문법)
this;
value(값)
non-strict mode에서는 항상 객체에 대한 참조입니다. strict mode에서는 임의의 값이 될 수 있습니다. 이 값은 함수(function), 클래스(glass), 전역(global)등 어떤 컨텍스트에 나타나는지에 따라 달라집니다.
컨텍스트(context)란 작업이 중단되고 나중에 동일한 지점에서 계속 실행될 수 있도록 저장해야 하는 작업(프로세스, 스레드 등)에서 사용하는 최소 데이터 집합입니다. 만약 인터럽트 등으로 실행 중인 작업이 중단되어야 한다면 프로세서는 컨텍스트를 저장하고 다른 작업을 수행하게 됩니다.
2.2 Global Context (전역 컨텍스트)
global context에서 this는 strict mode에 상관없이 전역 객체를 참조합니다.
예시 : 웹 브라우저
// 웹 브라우저에서는 window 객체가 전역 객체
console.log(this === window); // true
a = 37;
console.log(window.a); // 37
this.b = "MDN";
console.log(window.b); // "MDN"
console.log(b); // "MDN"
예시 : Node
node에서는 global context를 위해 global이라는 별도의 객체를 제공하고 있습니다.
node는 프로그램 실행 중에 global 컨텍스트에서 this는 빈 객체를 반환합니다.
console.log(this, typeof this); // {} object
this.a = 1234; // this에 a 속성 추가
console.log(this.a); // 1234
console.log(global);
global.b = "hello world"; // global 영역에 b 속성값 추가.
console.log(this.b); // undefined
console.log(global.b); // "hello world"
2.3 Function Context (함수 컨텍스트) - 단순 호출
함수 내부에서 this의 값은 함수를 호출한 방법에 의해 결정됩니다.
2.3.1 단순 호출
non-strict mode
this의 값이 호출에 의해 설정되지 않으므로, 기본값으로 전역 객체를 참조합니다.
function f1() {
return this;
}
// 브라우저
f1() === window; // true
// Node.js
f1() === global; // true
strict mode
this값은 실행 문맥에 진입하며 설정되는 값을 유지하므로 undefined가 됩니다.
function f2() {
"use strict";
return this;
}
f2() === undefined; // true
this값을 한 문맥에서 다른 문맥으로 넘기려면 아래 예시와 같이 call()이나 apply()를 사용합니다.
예시 1
// call 또는 apply의 첫 번째 인자로 객체가 전달될 수 있으며 this가 그 객체에 묶임
var obj = { a: "Custom" };
// 변수를 선언하고 변수에 프로퍼티로 전역 window를 할당
var a = "Global";
function whatsThis() {
return this.a; // 함수 호출 방식에 따라 값이 달라짐
}
whatsThis(); // this는 'Global'. 함수 내에서 설정되지 않았으므로 global/window 객체로 초기값을 설정한다.
whatsThis.call(obj); // this는 'Custom'. 함수 내에서 obj로 설정한다.
whatsThis.apply(obj); // this는 'Custom'. 함수 내에서 obj로 설정한다.
예시 2
function add(c, d) {
return this.a + this.b + c + d;
}
var o = { a: 1, b: 3 };
// 첫 번째 인자는 'this'로 사용할 객체이고,
// 이어지는 인자들은 함수 호출에서 인수로 전달된다.
add.call(o, 5, 7); // 16
// 첫 번째 인자는 'this'로 사용할 객체이고,
// 두 번째 인자는 함수 호출에서 인수로 사용될 멤버들이 위치한 배열이다.
add.apply(o, [10, 20]); // 34
non-strict mode에서 this로 전달된 값이 객체가 아닌 경우, call과 apply는 이를 객체로 변환하기 위한 시도를 합니다. null과 undefined 값은 전역 객체가 됩니다. 7이나 'foo'와 같은 primitive 값은 관련된 생성자를 사용해 객체로 변환됩니다. 따라서 숫자 7은 new Number(7)에 의해 객체로 변환되고 문자열 'foo'는 new String('foo')에 의해 객체로 변환됩니다.
function bar() {
console.log(Object.prototype.toString.call(this));
}
bar.call(7); // [object Number]
bar.call("foo"); // [object String]
bar.call(undefined); // [object global] 브라우저: window, Node: global
2.3.2 Bind 메서드
ES5에서 Function.prototype.bind를 도입했습니다. f.bind(someObject)를 호출하면 f와 같은 코드와 범위를 가졌지만 this값은 bind 된 새로운 함수를 생성합니다. 새로운 함수의 this값은 호출 방식과 상관없이 영구적으로 bind()의 첫 번째 매개변수(처음 호출한 매개변수)로 고정됩니다.
function f() {
return this.value;
}
let newFunction_1 = f.bind({ value: "hello" });
console.log(newFunction_1()); // hello
let newFunction_2 = newFunction_1.bind({ value: "world" });
// bind는 한 번만 동작함!
console.log(newFunction_2()); // hello
let o = { value: 37, f: f, nf1: newFunction_1, nf2: newFunction_2 };
console.log(o.value, o.f(), o.nf1(), o.nf2()); // 37, 37, hello, hello
2.3.3 화살표 함수 (Arrow Function)
화살표 함수에서 this값은 자신을 감싼 정적 범위입니다. 전역 코드에서는 전역 객체를 가리킵니다.
아래 코드에서 f()는 브라우저에서는 window객체를, node에서는 빈 객체{ }를 반환합니다.
화살표 함수는 call(), bind(), apply()를 사용해 호출할 때 this값을 설정해도 무시합니다. 사용할 매개변수를 정해주는 건 문제없지만, 첫 번째 매개변수(thisArg)는 null을 지정해야 합니다.
let globalObject = this;
let f = () => {
return this;
};
console.log(f() === globalObject, globalObject);
// 객체로서 메서드 호출
var obj = { func: f };
console.log(obj.func() === globalObject); // true
// call을 사용한 this 설정 시도
console.log(f.call(obj) === globalObject); // true
// bind를 사용한 this 설정 시도
foo = f.bind(obj);
console.log(f() === globalObject); // true
어떤 방법을 사용하든 f의 this값은 생성 시점(위 예시에서는 global 객체)의 것으로 설정됩니다. 다른 함수 내에서 생성된 화살표 함수에도 동일하게 적용됩니다. this는 lexical context의 것으로 유지됩니다.
lexical context (렉시컬 컨텍스트)
- static context (정적 컨텍스트)으로 부르기도 함.
- 변수 혹은 함수가 정의(defined)된(생성되어 메모리에 할당된) context 기준으로 동작
- 컴파일 타임에 결정됨
dynamic context (동적 컨텍스트)
- runtime context (실행 컨텍스트), (calling context) 호출 컨텍스트로 부르기도 함
- 변수 혹은 함수가 호출(called)된 context를 기준으로 동작
- 런 타임에 결정됨
var obj = {
bar: function () {
var x = () => this;
return x;
},
};
var fn = obj.bar();
console.log(fn() === obj); // true
var fn2 = obj.bar;
console.log(fn2()() == window); // true
strict mode에서 실행되는 경우에는 global 객체 대신 undefined를 반환합니다.
2.4 Object Method (객체 메서드)
함수를 어떤 객체의 메서드로 호출하면 this값은 그 객체를 사용합니다.
var obj = {
name: "thanos",
getName: function () {
return this.name;
},
};
console.log(obj.getName()); // "thanos"
함수가 객체의 메서드로 동작하는 경우, 함수가 정의된 방법이나 위치에 전혀 영향을 받지 않습니다. 아래와 같은 방법으로도 동일하게 동작합니다.
var obj1 = { name: "thanos" };
function fn() {
return this.name;
}
obj1.getName = fn;
console.log(obj1.getName()); // "thanos"
이는 함수가 객체의 메서드로 호출된 것만이 중요하다는 것을 보여줍니다.
아래의 예시는 같은 함수를 객체와 객체의 내부 객체에도 적용하고 이 메서드(함수)를 호출한 결과입니다.
함수 fn은 obj의 getValue 메서드와 obj의 inner object의 getValue 메서드로 동일한 함수를 사용하였습니다. 객체의 메서드를 호출하면 메서드(함수 fn)가 참조하는 this 값은 메서드를 할당한 객체를 가리키고 있습니다. 따라서 메서드를 호출한 객체의 value 값을 각각 반환합니다.
2.4.1 객체의 프로토타입 체인에서의 this
같은 개념으로 객체의 프로토타입 체인 어딘가에 정의한 메서드도 마찬가지입니다. 메서드가 어떤 객체의 프로토타입 체인 위에 존재하면, this값은 그 객체가 메서드를 가진 것 마냥 설정됩니다.
var obj = {
add: function () {
return this.a + this.b;
},
};
var newObj = Object.create(obj);
newObj.a = 1;
newObj.b = 4;
console.log(newObj.add()); // 5
obj를 상속받아 생성한 객체인 newObj는 obj에서 정의한 함수 add를 메서드를 가진 것처럼 동작합니다. 객체를 조회해 보면 add 메서드는 Prototype에 존재합니다. 이때 newObj의 메서드로 동작하는 add 메서드에서 참조하는 this값은 새로 생성한 newObj가 됩니다. 따라서 add 메서드는 newObj에 추가한 속성인 a, b를 참조하여 더한 결과인 5를 반환합니다.
2.4.2 getter(접근자)와 setter(설정자)의 this
같은 개념으로, 함수를 getter와 setter에서 호출하더라도 동일합니다. getter 및 setter의 this값은 속성이 정의된(defined) 객체가 아니라 속성이 접근(access)된 객체를 기반으로 합니다. getter 또는 setter로 사용되는 함수에는 속성을 설정하거나 가져오는 객체에 대한 this 바인딩이 있습니다.
function sum() {
return this.a + this.b + this.c;
}
var obj = {
a: 1,
b: 2,
c: 3,
get average() { // get 키워드를 붙여 average 메서드를 getter로 정의
return (this.a + this.b + this.c) / 3;
},
};
Object.defineProperty(obj, "sum", { // obj에 getter로 sum 함수를 추가
get: sum,
enumerable: true,
configurable: true,
});
console.log(obj.average, obj.sum); // 2, 6
2.5 Constructor(생성자)
함수가 생성자로 사용되면(new 키워드 사용) 생성자 함수에 접근하는 객체에 관계없이 생성되는 새 객체에 this가 바인딩됩니다. 생성자의 기본값은 this에서 참조하는 객체를 반환하지만 대신 다른 객체를 반환할 수도 있습니다. 반환 값이 객체가 아닌 경우 this 객체가 반환됩니다.
function Thanos() {
this.name = "thanos";
}
var obj1 = new Thanos();
console.log(obj1.name); // "thanos"
function Captain() {
console.log(this) // { name: "avengers" }
this.name = "avengers"; // dead code
return { name: "captain", team: "avengers" }; // 다른 객체를 반환
}
var obj2 = new Captain();
console.log(obj2.name); // "captain"
console.log(obj2);
두 번째 obj2에서는 생성 중에 새로운 객체{ name: "captain"}가 반환되었으므로 this값에 바인딩된 새 객체{this.name = "avengers"}가 삭제됩니다. 즉 this.name = "avengers"; 이 코드는 데드 코드가 됩니다. 데드 코드는 실행은 되지만 외부에 아무런 영향을 미치지 않습니다.
3. 요약
3.1 this
* this는 현재 실행 중인 코드가 속한 객체, 클래스 또는 기타 엔티티를 참조하는 데 사용하는 키워드입니다
* JavaScript의 경우에는 사용되는 위치에 따라 다르게 동작합니다.
* 대부분의 경우 어떻게 함수가 호출되는지(runtime binding)에 따라 this값이 결정됩니다.
* this값은 non-strict mode에서는 항상 객체에 대한 참조입니다. strict mode에서는 임의의 값이 될 수 있습니다. 이 값은 함수(function), 클래스(glass), 전역(global)등 어떤 컨텍스트에 나타나는지에 따라 달라집니다.
3.2 Global Context (전역 컨텍스트)에서 사용
* strict mode에 상관없이 전역 객체를 참조합니다.
* 브라우저에서는 window, Node.js에서는 global을 반환합니다.
3.3 Function (함수: 일반함수, 익명함수)
* 함수 내부에서 this의 값은 함수를 호출한 방법에 의해 결정됩니다.
* non-strict mode에서는 this의 값이 호출에 의해 설정되지 않으므로, 기본값으로 전역 객체(브라우저: window, Node.js: global)를 참조합니다.
* strict mode에서는 this값은 실행 문맥에 진입하며 설정되는 값을 유지하므로 undefined가 됩니다.
3.3.1 Bind 메서드
* ES5에서 Function.prototype.bind를 도입했습니다.
* f.bind(someObject)를 호출하면 f와 같은 코드와 범위를 가졌지만 this값은 bind 된 새로운 함수를 생성합니다. 새로운 함수의 this값은 호출 방식과 상관없이 영구적으로 bind()의 첫 번째 매개변수(처음 호출한 매개변수)로 고정됩니다.
3.3.2 Arrow Function (화살표 함수)
* 화살표 함수에서 this값은 자신을 감싼 정적 범위입니다.
* 전역 코드에서는 전역 객체를 가리킵니다.
* 어떤 방법을 사용하든 화살표 함수의 this값은 생성 시점의 것으로 설정됩니다.
* 화살표 함수는 call(), bind(), apply()를 사용해 호출할 때 this값을 설정해도 무시합니다.
* this는 lexical context의 것(함수가 정의(defined)된 context 기준)으로 유지됩니다.
3.3 Object Method (객체의 메서드)
* 함수를 어떤 객체의 메서드로 호출하면 this값은 그 객체를 사용합니다.
* 함수가 객체의 메서드로 동작하는 경우, 함수가 정의된 방법이나 위치에 전혀 영향을 받지 않습니다.
3.3.1 객체의 프로토타입 체인에서의 this
* 같은 개념으로 객체의 프로토타입 체인 어딘가에 정의한 메서드도 마찬가지입니다. this값은 그 객체를 사용합니다.
3.3.2 getter(접근자)와 setter(설정자)의 this
* 같은 개념으로, 함수를 getter와 setter에서 호출하더라도 동일합니다.
* getter 및 setter의 this값은 속성이 정의된(defined) 객체가 아니라 속성이 접근(access)된(getter, setter를 호출한) 객체를 기반으로 합니다.
3.4 Object Constructor (객체의 생성자)
* 함수가 생성자로 사용되면(new 키워드 사용) 생성자 함수에 접근하는 객체에 관계없이 생성되는 새 객체에 this가 바인딩됩니다.
* 생성자의 기본값은 this에서 참조하는 객체를 반환하지만 대신 다른 객체를 반환할 수도 있습니다. 이경우 반환되는 객체가 this가 됩니다.
참고문헌
https://en.wikipedia.org/wiki/This_(computer_programming)
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/this
https://en.wikipedia.org/wiki/Context_(computing)
'Dev. Cookbook > Javascript' 카테고리의 다른 글
[Node, NPM] SQL Bricks, JavaScript로 SQL 구문을 생성하는 패키지 (0) | 2023.11.26 |
---|---|
[JavaScript] JS에서 객체 이름 확인하는 방법 (0) | 2023.11.20 |
[JSON] JSON 이해하기 - 2. JSON 기본 : JSON에서 사용하는 데이터 타입 (0) | 2023.11.06 |
[JavaScript] 모니터, 듀얼 모니터 및 브라우저 기준으로 팝업창 가운데 띄우기 (2) | 2022.05.30 |
[JavaScript] window.open(), window.opener() 부모창 자식창 간의 제어 방법 (0) | 2022.05.26 |
댓글