[코어 자바스크립트] this

김진서·2025년 5월 9일

우아한테크코스 7기

목록 보기
40/56
post-thumbnail

👉 ‘코어 자바스크립트’의 ‘03 this’을 읽으며 기억할 내용들과 새로 알게 된 내용을 정리하였습니다.

  • 일반적인 this: 클래스로 생성한 인스턴스 객체
  • JS의 this: 어디서든 사용 가능하다. 상황에 따라 대상이 다름. 이유를 파악하기 힘든 경우, 예상과 다른 경우 등.. 어려움.
  • 함수와 객체(메서드)의 구분이 느슨한 JS의 this는 실질적으로 구분하는 거의 유일한 기능.

🔽 3-1 상황에 따라 달라지는 this


  • 기본적으로 this는 실행 컨텍스트가 생성될 때 함께 결정: this는 함수를 호출할 때 결정.

  • 어떤 방식으로 호출하냐에 따라 값이 달라짐.

  • 3-1-1 전역 공간에서의 this
    : 전역 객체를 가리킴. 개념상 전역 컨텍스트를 생성하는 주체가 전역 객체.

    • 브라우저 환경의 전역 객체: window
    • Node 환경의 전역 객체: global

    💡 전역변수 선언 시, 전역 객체의 property로도 할당: JS의 모든 변수는 사실 특정 객체(실행 컨텍스트의 LexicalEnvironment)의 property로 동작

    var a = 1;
    delete window.a;  // false
    console.log(a, window.a, this.a);  // 1 1 1
    
    var b = 2;
    delete b;  // false
    console.log(b, window.b, this.b);  // 2 2 2
    
    window.c = 3;
    delete window.c;  // true
    console.log(c, window.c, this.c);  // Uncaught ReferenceError: c is not defined
    • 처음부터 전역 객체의 property로 할당한 경우 delete 되는 반면, 전역변수로 선언한 경우 delete 되지 않음.
    • 사용자가 의도치 않게 삭제하는 것을 방지하는 방어 전략으로 해석됨.
  • 3-1-2 메서드로서 호출할 때 그 메서드 내부에서의 this

    함수 vs 메서드

    • 독립성의 차이: 함수는 그 자체로 독립적, 메서드는 호출한 대상 객체에 대한 동작.
    • 객체의 프로퍼티로 함수를 할당한다고 해서 무조건 메서드가 아니라, 객체의 메서드로 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작.
    var func = function (x) {
    	console.log(this, x);
    }
    func(1);  // Window { ... } 1
    
    var obj = {
    	method: func
    };
    obj.method(2);  // { method: f } 2
    
    var obj = {
    	method: function (x) { console.log(this, x); }
    };
    obj.method(1);  // { method: f } 1
    obj['method'](2);  // { method: f } 2

    메서드 내부에서의 this
    : 호출한 주체에 대한 정보, 마지막 점 앞에 명시된 객체.

  • 3-1-3 함수로서 호출할 때 그 함수 내부에서의 this

    함수 내부에서의 this
    : this가 지정되지 않음. 호출 주체를 알 수 없기 때문. this가 지정되지 않으면 this는 전역 객체를 가리키게 됨.

    메서드의 내부함수에서의 this

    • this라는 단어 자체가 주는 느낌적 느낌 그대로 생각하면 예상과 다른 결과가 나온다.
    var obj1 = {
    	outer: function () {
    		  console.log(this);  // (1)
        var innerFunc = function () {
      	console.log(this);  // (2) innnerFunc (3) obj2.innerMethod
        }
        innerFunc();
      
        var obj2 = {
      	innerMethod: innerFunc
        };
        obj2.innerMethod();
      }
    };
    obj1.outer();
    • (1): obj1, (2): 전역 객체(Window), (3): obj2
    • 같은 함수임에도 바인딩되는 this의 대상이 서로 다르다.
    • this의 바인딩은 함수를 실행하는 당시의 주변 환경(메서드 내부인지, 함수 내부인지 등)은 중요하지 않고, 호출하는 구문에 . 혹은 [] 표기가 있는지 없는지가 중요.

    메서드 내부 함수에서의 this를 우회하는 방법

    • 호출 주체가 없을 때 자동으로 전역 객체를 바인딩하지 않고, 호출 당시 주변 환경의 this를 그대로 상속받아 사용할 수 있는 방법이 없는가?
    • 변수를 활용하여 우회할 수 있다!
    var obj = {
      outer: function () {
        console.log(this);  // (1) { outer: f }
        var innerFunc1 = function () {
      	console.log(this);  // (2) Window { ... }
        }
        innerFunc1();
        var self = this;
        var innerFunc2 = function () {
          console.log(self);  // (3) { outer: f }
        };
        innerFunc2();
      }
    };
    obj1.outer();

    this를 바인딩하지 않는 함수

    • 함수 내부에서 this가 전역 객체를 바라보는 문제를 보완하고자, this를 바인딩하지 않는 화살표 함수(arrow function)을 새로 도입.
    • 화살표 함수는 실행 컨텍스트 생성 시 this 바인딩 과정 자체가 없어, 상위 스코프의 this를 그대로 활용 가능.
    • 화살표 함수를 사용하면, 우회법 불필요.
    var obj = {
      outer: function () {
        console.log(this);  // (1) { outer: f }
        var innerFunc = () => {
      	console.log(this);  // (2) { outer: f }
        }
        innerFunc();
      }
    };
    obj.outer();
  • 3-1-4 콜백 함수 호출 시 그 함수 내부에서의 this

    • 콜백 함수: 함수의 제어권을 다른 함수 or 메서드에 넘겨주는 함수.
      • 콜백 함수도 함수이므로 기본적으로 this가 전역 객체 참조.
      • 제어권을 받은 함수에서 콜백 함수에 별도로 this의 대상을 지정한 경우 그 대상을 참조.
    setTimeout(function () { console.log(this); }, 300);  // (1) 전역 객체(Window)
      
    [1, 2, 3, 4, 5].forEach(function (x) {  // (2) 전역 객체(Window)
      console.log(this, x);
    });
      
    document.body.innerHTML += '<button id="a">클릭</button>';
    document.body.querySelector('#a')
      .addEventListener('click', function (e) {  // (3) <button id="a">클릭</button>
      console.log(this, e);
    });
  • 3-1-5 생성자 함수 내부에서의 this

    • 생성자(constructor): 구체적인 인스턴스를 만들기 위한 일종의 틀.

      • 함수에 생성자로서의 역할을 함께 부여. new 키워드와 함께 함수 호출 시 생상자로서 동작.
        • 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신.
        • 생성자 함수 호출 시 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체(인스턴스)를 만들고, 미리 준비된 공통 속성 및 개성을 해당 객체(this)에 부여.
      var Cat = function (name, age) {
      	this.bark = '야옹';
      	this.name = name;
      	this.age = age;
      };
      var choco = new Cat('초코', 7);
      var nabi = new Cat('나비', 5);
      console.log(choco, nabi);
      
      /* 결과
      Cat { bark: '야옹', name: '초코', age: 7 }
      Cat { bark: '야옹', name: '나비', age: 5 }
      */

🔽 3-2 명시적으로 this를 바인딩하는 방법


  • this에 별도의 대상을 바인딩하는 방법.

  • 3-2-1 call 메서드

    Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])
    • call 메서드: 호출 주체인 함수를 즉시 실행하도록 하는 명령.

      • 첫 번째 인자는 this로 바인딩, 이후의 인자들은 호출할 함수의 매개변수.
      • 함수를 그냥 실행하면 this는 전역 객체를 참조하지만, call 메서드 사용 시 임의의 객체를 this로 지정 가능.
      var func = function (a, b, c) {
      	console.log(this, a, b, c);
      };
      func(1, 2, 3);  // Window { ... } 1 2 3
      func.call({ x: 1 }, 4, 5, 6);  // { x: 1 } 4 5 6
      
      var obj = {
      	a: 1,
      	method: function (x, y) {
      		console.log(this.a, x, y);
      	}
      };
      obj.method(2, 3);  // 1 2 3
      obj.method.call({ a: 4 }, 5, 6);  // 4 5 6
  • 3-2-2 apply 메서드

    Function.prototype.apply(thisArg[, argsArray])
    • 기능적으로 call과 완전히 동일.

      • 두 번째 인자를 배열로 받아 배열의 요소를 호출할 함수의 매개변수로 지정.
      var func = function(a, b, c) {
      	console.log(this, a, b, c);
      };
      func.apply({ x: 1 }, [4, 5, 6]);  // { x: 1 } 4 5 6
      
      var obj = {
      	a: 1,
      	method: function (x, y) {
      		console.log(this.a, x, y);
      	}
      };
      obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6
  • 3-2-3 call / apply 메서드의 활용

    유사배열객체(Array-like object)에 배열 메서드 적용

    var obj = {
    	0: 'a',
    	1: 'b',
    	2: 'c',
    	length: 3
    };
    Array.prototype.push.call(obj, 'd');
    console.log(obj);  // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }
    
    var arr = Array.prototype.slice.call(obj);
    console.log(arr);  // [ 'a', 'b', 'c', 'd' ]
    • 객체에는 배열 메서드 직접 적용 X.

    • 키가 0 or 양의 정수 프로퍼티 and length 프로퍼티 값이 0 or 양의 정수인 경우(유사배열객체) call or apply를 통해 Array의 프로토타입을 이용해서 배열 메서드를 차용 가능.

    • call 메서드를 이용해 원본인 유사배열객체의 얕은 복사 수행, slice가 배열 메서드이므로 배열로 반환.

      • 함수 내부에서 접근하는 arguments 객체도 유사배열객체.
      • querySelectorAll, getElementsByClassName 등의 Node 선택자로 선택한 결과인 NodeList도 마찬가지.
      function a () {
      	var argv = Array.prototype.slice.call(arguments);
      	argv.forEach(function (arg) {
      		console.log(arg);
      	});
      }
      a(1, 2, 3);
      
      document.body.innerHTML = '<div>a</div><div>b</div><div>c</div>';
      var nodeList = document.querySelectorAll('div');
      var nodeArr = Array.prototype.slice.call(nodeList);
      nodeArr.forEach(function (node) {
      	console.log(node);
      });
    • 모든 배열 메서드 적용 가능. 문자열도 마찬가지(문자열도 인덱스와 length가 존재하기 때문).

      • 문자열은 length 프로퍼티가 읽기 전용이므로, 원본 문자열에 변경을 가하는(push, pop, shift, unshift, splice) 메서드는 에러 throw. concat처럼 대상이 반드시 배열인 경우는 에러는 아니지만 의도대로 동작하지 않음.
      var str = 'abc def';
      
      Array.prototype.push.call(str, ', pushed string');
      // Error: Cannot assign to read only property 'length' of object [object String]
      
      Array.prototype.concat.call(str, 'string');  // [String {"abc def"}, "string"]
      
      Array.prototype.every.call(str, function(char) { return char !== ' '; });  // false
      
      Array.prototype.some.call(str, function(char) { return char === ' '; });  // true
      
      var newArr = Array.prototype.map.call(str, function(char) { return char + '!'; });
      console.log(newArr);  // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']
      
      var newStr = Array.prototype.reduce.apply(str, [
      	function(string, char, i) { return string + char + i; },
      	''
      ]);
      console.log(newStr);  // "a0b1c2 3d4e5f6"
    • call / apply를 이용해 형변환은 ‘this를 원하는 값으로 지정해 호출한다’는 본래의 의도와 다른 활용법. ⇒ 유사배열객체 or 순회 가능 모든 종류 데이터 타입 배열 전환: Array.from 도입.

      var obj = {
      	0: 'a',
      	1: 'b',
      	2: 'c',
      	length: 3
      };
      var arr = Array.from(obj);
      console.log(arr);  // ['a', 'b', 'c']

    생성자 내부에서 다른 생성자 호출

    • 생성자 내부에 다른 생성자와 공통된 내용이 있는 경우 call / apply를 이용해 다른 생성자를 호출하여 간단히 반복 간소화 가능.

      function Person(name, gender) {
      	this.name = name;
      	this.gender = gender;
      }
      function Student(name, gender, school) {
      	Person.call(this, name, gender);
      	this.school = school;
      }
      function Employee(name, gender, company) {
      	Person.apply(this, [name, gender]);
      	this.company = company;
      }
      var by = new Student('보영', 'female', '단국대');
      var jn = new Employee('재난', 'male', '구골');

    여러 인수를 묶어 하나의 배열로 전달하고 싶을 때 - apply 활용

    • 메서드의 인수의 수가 가변적인 경우 apply를 사용하면 좋다.

      • 배열에서 최대/최소값을 구해야 하는 경우. apply 미사용 시.
      var numbers = [10, 20, 3, 16, 45];
      var max = min = numbers[0];
      numbers.forEach(function(number) {
      	if (number > max) {
      		max = number;
      	}
      	if (number < min) {
      		min = number;
      	}
      });
      console.log(max, min);  // 45 3
    • Math.min / Math.max에 apply 적용 시.

      var numbers = [10, 20, 3, 16, 45];
      var max = Math.max.apply(null, numbers);
      var min = Math.min.apply(null, numbers);
      console.log(max, min);  // 45 3
    • 펼치기 연산자(스프레드 연산자(spread operator) 이용.

      var numbers = [10, 20, 3, 16, 45];
      var max = Math.max(...numbers);
      var min = Math.min(...numbers);
      console.log(max, min);  // 45 3
    • call / apply 메서드는 명시적으로 별도의 this를 바인딩하면서 함수 또는 메서드를 실행하는 훌륭한 방법이지만, 오히려 이로 인해 this를 에측하기 어렵게 만들어 코드 해석을 방해한다는 단점.

    • 그럼에도 불구하고 ES5 이하 환경에서는 마땅한 대안이 없어 실무에서도 광범위하게 활용.

  • 3-2-4 bind 메서드

    Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
    • bind: ES5에서 추가된 기능. call 비슷하지만, 즉시 호출하지 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드.

      • 다시 새로 함수를 호출할 때 인수를 넘기면 기존 인수들 뒤에 이어서 등록.
      • bind는 함수에 this를 미리 적용하는 것, 부분 적용 함수를 구현하는 2가지 목적.
      var func = function (a, b, c, d) {
      	console.log(this, a, b, c, d);
      };
      func(1, 2, 3, 4);  // Window{ ... } 1 2 3 4
      
      var bindFunc1 = func.bind({ x: 1 });
      bindFunc1(5, 6, 7, 8);  // { x: 1 } 5 6 7 8
      
      var bindFunc2 = func.bind({ x: 1 }, 4, 5);
      bindFunc2(6, 7);  // { x: 1 } 4 5 6 7
      bindFunc2(8, 9);  // { x: 1 } 4 5 8 9

    name 프로퍼티

    • bind로 생성한 함수는 name 프로퍼티에 bind의 수동태인 bound라는 접두어가 붙음.

      • name이 bound xxx이면, xxx의 bind 함수임을 알 수 있으므로, call / apply보다 코드 추적이 수월함.
      var func = function (a, b, c, d) {
      	console.log(this, a, b, c, d);
      };
      var bindFunc = func.bind({ x: 1 }, 4, 5);
      console.log(func.name);  // func
      console.log(bindFunc.name);  // bound func

    상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

    • self를 활용한 우회법 대신 call / apply 또는 bind를 이용하면 더 깔끔.

      var obj1 = {
      	outer: function () {
      		console.log(this);
      		var innerFunc = function () {
      			console.log(this);
      		};
      		innerFunc.call(this);
      	}
      };
      obj1.outer();
      
      var obj2 = {
      	outer: function () {
      		console.log(this);
      		var innerFunc = function () {
      			console.log(this);
      		}.bind(this);
      		innerFunc();
      	}
      };
      obj2.outer();
    • 콜백을 인자로 받거나 기본적으로 콜백 내의 this에 관여하는 함수 or 메서드에 대해서도 bind를 이용하여 this 변경 가능.

      var obj = {
      	logThis: function () {
      		console.log(this);
      	},
      	logThisLater1: function () {
      		setTimeout(this.logThis, 500);
      	},
      	logthisLater2: function () {
      		setTimeout(this.logThis.bind(this), 1000);
      	}
      };
      obj.logThisLater1();  // Window { ... }
      obj.logThisLater2();  // obj { logThis: f, ... }
  • 3-2-5 화살표 함수의 예외사항

    • 화살표 함수: 실행 컨텍스트 생성 시 this 바인딩 X. 스코프 체인 상 가장 가까운 this 참조.

      var obj = {
      	outer: function () {
      		console.log(this);
      		var innerFunc = () => {
      			console.log(this);
      		};
      		innerFunc();
      	}
      };
      obj.outer();
    • 더욱 간결 and 편리.

  • 3-2-6 별도의 인자로 this를 받는 경우(콜백 함수 내에서의 this)

    • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체(thisArg)를 인자로 지정 가능.

      • thisArg 값을 지정하면 콜백 함수 내에서 this 값을 변경 가능.
      • 배열 메서드에 많음. Set, Map 메서드에도 있음.
      var report = {
      	sum: 0,
      	count: 0,
      	add: function () {
      		var args = Array.prototype.slice.call(arguments);
      		args.forEach(function (entry) {
      			this.sum += entry;
      			++this.count;
      		}, this);
      	},
      	average: function () {
      		return this.sum / this.count;
      	}
      };
      report.add(60, 85, 95);
      console.log(report.sum, report.count,report.average());  // 240 3 80
    • add 메서드가 arguments를 배열로 변환해서 args 변수에 담고, 배열을 순회하며 콜백 실행.

    • 이 콜백 함수 내부에서의 this는 forEach 함수의 2번째 인자로 전달해 준 this가 바인딩.

    • 60, 85, 95를 인자로 add를 호출하면, 이를 배열로 만들어 forEach 메서드 실행.

    • 콜백 함수 내부에서의 this는 add 메서드에서의 this가 전달된 상태이므로 add 메서드의 this(report)를 그대로 참조.

    • 순회하면서 report.sum, report.count 값이 차례로 바뀌고, 순회가 끝나면 240, 3이 담김.

    • 기타 thisArg 인자 메서드

      Array.prototype.forEach(callback[, thisArg])
      Array.prototype.map(callback[, thisArg])
      Array.prototype.filter(callback[, thisArg])
      Array.prototype.some(callback[, thisArg])
      Array.prototype.every(callback[, thisArg])
      Array.prototype.find(callback[, thisArg])
      Array.prototype.findIndex(callback[, thisArg])
      Array.prototype.flatMap(callback[, thisArg])
      Array.prototype.from(callback[, thisArg])
      Set.prototype.forEach(callback[, thisArg])
      Map.prototype.forEach(callback[, thisArg])

🔽 3-3 정리


  • 명시적 this 바인딩이 없는 한 늘 성립하는 규칙.

    • ✅ 전역공간에서의 this는 전역객체(브라우저에서는 window, Node.js에서는 global)를 참조.
    • ✅ 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체(메서드명 앞의 객체)를 참조.
    • ✅ 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조. 메서드의 내부함수에서도 동일.
    • ✅ 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않는 경우에는 전역객체 참조.
    • ✅ 생성자 함수에서의 this는 생성될 인스턴스를 참조.
  • 명시적 this 바인딩 규칙.

    • ✅ call, apply 메서드는 this를 명시적으로 지정하면서 함수 or 메서드를 호출.
    • ✅ bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 생성.
    • ✅ 요소를 순회하면서 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 수용.
profile
PAy IT forwaRD를 실천하는 프론트엔드 개발자.

0개의 댓글