본문 바로가기

프로그래밍/js

lessons 17. this 키워드

2023.02.18

자바스크립트 스터디 6회차

공부 사이트: https://poiemaweb.com/

 

17. this 키워드

자바스크립트에서 함수는 호출될 때 매개변수로 전달되는 인자값 외에 arguments 객체와 this를 암묵적으로 전달받는다.

Java에서 this는 인스턴스 자신을 가리키는 참조변수이다. 그러나 자바스크립트는 함수 호출 방식에 따라 this에 바인딩되는 객체가 달라진다.

 

함수를 선언할 때 this에 바인딩할 객체가 정적으로 결정되는 것이 아니라. 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정된다.

 

[함수 호출 방식]

- 함수 호출

- 메소드 호출

- 생성자 함수 호출

- apply, call, bind 호출

 

✅ 요약

1. 기본적으로 this는 전역 객체에 바인딩 됨. (window, global)
2. 메소드 내부의 this는 해당 메소드를 호출한 객체
3. 생성자 함수의 경우, 새로 생성한 객체(인스턴스) 바인딩

 

1) 함수 호출

기본적으로 this는 전역객체(Global object)에 바인딩된다. 내부함수의 경우에도 this는 외부함수가 아닌 전역객체에 바인딩된다. 

내부함수는 일반 함수, 메소드, 콜백함수 어디에서 선언되었든 관계없이 this가 전역 객체를 바인딩한다.

#전역객체: 전역 스코프를 갖는 전역변수/함수를 프로퍼티로 소유함. 모든 객체의 유일한 최상위 객체. 브라우저에서는 window, node.js에서는 global 객체를 의미함.

function foo() {
  console.log(this === global);
  
  // 내부함수 bar
  function bar() {
    console.log(this === global); 
  }
  bar();
}
foo();

 

 

내부함수의 this가 전역객체를 참조하는 것을 막기위한 방법을 알아보자.

① 변수에 this를 할당해준다.

var value = 1;

var obj = {
  value: 100,
  foo: function() {
    var that = this;  // Workaround : this === obj

    console.log("foo's this: ",  this);  // obj
    console.log("foo's this.value: ",  this.value); // 100
	console.log("that's that.value: " + that.value);  // 100
	
    function bar() {
      console.log("bar's this: ",  this); // window 또는 global
      console.log("bar's this.value: ", this.value); // 1

      console.log("bar's that: ",  that); // obj
      console.log("bar's that.value: ", that.value); // 100
    }
    bar();
  }
};

obj.foo();

[ 코드 설명 ]

- foo 메소드에 that 변수를 생성해서 this를 할당

- foo 메소드 내에서 this 호출 시 전역 객체가 아닌 obj가 출력되며, 그렇기에 this.value와 that.value는 모두 100

- foo 메소드의 내부함수 bar에서 this 호출 시 전역 객체가 출력된다. 그렇기에 this.value의 값은 전역 변수 값인 1

- 그러나 내부함수 bar에서 that 호출 시 obj가 출력. that.value는 당연히 100이다.

 

 

② this를 명시적으로 바인딩하는 apply, call, bind 메소드를 사용한다.

  함수 설명
  call - 함수에서 접근할 this 객체 변경 가능 (일시적)
- EX) 호출함수.call(this로 지정할 대상, 인수1, 인수2...)
  apply - 함수에서 접근할 this 객체 변경 가능 (일시적) 
- 매개변수를 배열 형태로 받는다.
- EX) 호출함수.apply(this로 지정할 대상, [인수1, 인수2, ...])
  bind - 함수에서 접근할 this 객체 변경 가능 (영구적)
-  EX) 호출함수.bind(this로 지정할 대상, 인수1, 인수2...)
- 함수를 호출하는 것이 아니라 새로운 함수를 만들어서 반환해준다.
- 그렇기에 bind 함수는 사용 후 바로 함수호출이 되지 않음.
- this를 원하는 객체로 전달한 후, 재호출을 해 주어야 함. → bind(obj)()

 

 

2) 메소드 호출

메소드 내부의 this는 해당 메소드를 소유한 객체, 즉 해당 메소드를 호출한 객체에 바인딩된다.

 

① 일반 객체의 메소드

var obj1 = {
  name: '홍길동',
  sayName: function() {
    console.log(this.name);
  }
}

var obj2 = {
  name: '고길동'
}

console.log(obj1);
console.log(obj2);

obj2.sayName = obj1.sayName;

console.log(obj1);
console.log(obj2);

obj1.sayName();
obj2.sayName();

[ 코드 설명 ]

- obj2 객체에 없는 sayName 메소드를 할당해줌. 

- obj1의 메소드 내 this는 obj1을 가리키고, obj2의 메소드 내 this는 ob2를 가리킴.

 

 

② 프로토타입 객체의 메소드

프로토타입 객체도 메소드를 가질 수 있다. 프로토타입 객체 메소드 내부에서 사용된 this도 해당 메소드를 호출한 객체에 바인딩된다.

function Person(name){
	this.name = name;
}

Person.prototype.getName = function(){
	return this.name;
}

var me = new Person('홍길동');
console.log(me.getName);

Person.prototype.name = '고길동';
console.log(Person.prototype.getName());

 

 

 

3) 생성자 함수 호출

형식이 정해져있는 JAVA와 다르게, 자바스크립트의 생성자 함수는 기존 함수에 new 연산자를 붙여서 호출하면 생성자 함수로 동작한다.

생성자 함수가 아닌 일반 함수에 new 연산자를 붙여 호출하면 생성자 함수처럼 동작한다는 의미로, 생성자 함수를 구분하기 위해 함수명 첫 문자를 대문자로 기술하자.

 

[ 동작 방식 ]

- 빈 객체 생성 및 this 바인딩

- this를 통한 프로퍼티 생성

- 생성된 객체 반환

- 반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환됨.

- 반환문이 this가 아닌 다른 객체를 명시적 반환하는 경우, this가 아닌 해당 객체가 반환

※ this를 반환하지 않는 함수는 생성자 함수로서의 역할 수행하지 않음. 생성자 함수는 반환문을 명시적으로 사용하지 않는다.

 

 

① 객체 리터럴 방식과 생성자 함수 방식의 차이

객체 리터럴 방식과 생성자 함수 방식의 차이는 프로토타입 객체([[Prototype]])에 있다.

- 객체 리터럴 방식: 생성된 객체의 프로토타입 객체는 Object.prototype

- 생성자 함수 방식: 생성된 객체의 프로토타입 객체는 생성자함수.prototype

 

 

② 생성자 함수를 new 없이 호출

생성자 함수를 new 없이 호출하면, 함수 내 this는 전역객체를 가리킨다.

function Person(name){
	this.name = name;
}

var me = Person('홍길동');

console.log(me);   //undefined
console.log(window.name); //혹은 global.name

일반함수와 생성자 함수의 형식상 차이가 크게 없기 때문에, 생성자 함수의 첫 문자를 대문자로 기술하지만.. 실수는 언제든지 발생할 수 있기에. 방지하기 위한 패턴이 있다. ▶ Scope-Safe Constructor

// Scope-Safe Constructor Pattern
function Func(arg){
	// arguments: 전달받은 매개변수를 저장하는 객체
	// arguments.collee: 함수 바디 내에서 현재 실행 중인 함수의 이름을 반환
	if(!(this instanceof arguments.callee)){
		return new arguments.callee(arg);
	}
	
	this.value = arg ? arg : 0;
}

var a = new Func(100);
var b = Func(10);

console.log(a.value);
console.log(b.value);

[ 코드 설명 ] 

- 만약 arguments.callee가 this의 인스턴스가 아니라면, new arguments.callee(arg) 호출하여 인스턴스 반환.

- A instanceof B: A는 B의 인스턴스인가?

  var a = new Func() var b = Func()
  this a this 전역객체(window, global)
  arguments.callee Func arguments.callee Func

- new 연산자를 사용한 경우, this인 a는 arguments.callee인 Func의 인스턴스가 맞다.

- 그러나 new 연산자를 사용하지 않은 경우, this에 바인딩된 전역객체는 Func의 인스턴스가 아니다.

- 그렇기에 new 키워드와 함께 arguments.callee(arg) 하여 생성자 함수를 호출함으로서 인스턴스를 반환해준다.

- arg가 존재한다면 this.value에 arg 할당, 존재하지 않는다면 0 할당

└> 패턴을 사용하지 않았다면, new 연산자 사용하지 않은 b는 value 프로퍼티가 존재하지도 않는다. 전역객체이 value 프로퍼티가 생성되어 있었을 것. 

 

 

 

4) apply / call / bind 호출

자바스크립트 엔진의 암묵적 this 바인딩 외에, 명시적으로 this를 바인딩하는 방법이 있다. 

Function.prototype.apply와 Function.prptotype.call 메소드를 사용하는 것.

 

① apply()

func.apply(thisArg, [argsArray])

// thisArg: 함수 내부의 this에 바인딩할 객체
// argsArray: 함수에 전달할 argument의 배열

※ apply() 메소드는 this를 특정 객체에 바인딩할 뿐, 본질적인 기능은 함수 호출이다.

 

var Person = function (name) {
  this.name = name;
};

var person = {};  // ①빈 객체 생성

Person.apply(person, ['홍길동']);  // ② apply() 메소드 통한 this 바인딩 및 파라미터 전달 후 함수 호출

console.log(person);

apply() 메소드의 대표적인 용도는 arguments 객체와 같은 유사 배열 객체에서 배열 메소드를 사용하는 경우이다. arguments 객체는 본래 배열이 아니라 배열의 메소드를 사용할 수 없으나, apply() 메소드를 이용하면 가능하다.

 

function convertArgsToArray(){
	console.log(arguments);
	
	// slcie: 특정 부분에 대한 복사본 생성
	var arr = Array.prototype.slice.apply(arguments);
	
	console.log(arr);
	return arr;
}

convertArgsToArray(1,2,3);

Array.prototype.slice.apply(arguments)

- Array.prototype.slice 메소드를 호출. 그리고 this를 arguments 객체로 바인딩해라.

- Array.prototype.slice 메소드를 arguments 객체의 메소드인 것처럼 호출하라는 의미 (arguments.slice() 마냥)

 

 

② call()

appy()와 같은 기능을 한다.

func.apply(thisArg, arg1, arg2, ...)

 

apply()와 call()은 콜백 함수의 this를 위해 사용되기도 한다. 콜백함수는 파라미터로 함수로 전달하는 함수를 의미한다.

 

// 문제가 발생하는 코드
function Person(name) {
  this.name = name;
}

Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    callback();  //이 시점에서 실행될 함수의 this는 Person
  }
};

function func() {
  console.log(this.name);  //일반함수의 this는 전역객체
}

var p = new Person('홍길동');
p.doSomething(func);   //undefined

외부 함수(func) 내부의 this와 콜백 함수(doSomething) 내부의 this가 다르기에 문제가 발생한다.

콜백함수 내부의 this 콜백함수를 호출하는 함수 내부의 this일치시켜주어야 한다.

 

// call() 메소드를 사용하여 문제 해결
function Person(name) {
  this.name = name;
}

Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    callback.call(this);  
  }
};

function func() {
  console.log(this.name);  //일반함수의 this는 전역객체
}

var p = new Person('홍길동');
p.doSomething(func);   //홍길동

 

 

③ bind()

함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴한다.

함수를 실행하지 않기 때문에 명시적으로 함수를 호출해야 한다.

func.bind(thisArg, arg1, arg2, ...)()
// bind() 함수 사용하여 문제 해결
function Person(name) {
  this.name = name;
}

Person.prototype.doSomething = function(callback) {
  if(typeof callback == 'function') {
    callback.bind(this)();   //명시적 호출
  }
};

function func() {
  console.log(this.name);  //일반함수의 this는 전역객체
}

var p = new Person('홍길동');
p.doSomething(func);   //홍길동

'프로그래밍 > js' 카테고리의 다른 글

lessons 19. 객체지향 프로그래밍  (0) 2023.02.14
lessons 18. 클로저  (0) 2023.02.13
lessons 16. Strict mode  (0) 2023.02.12
lessons 15. 스코프  (0) 2023.02.12
lessons 14. 프로토타입  (0) 2023.02.09