[코어 자바스크립트] 3장 this

woo·2023년 3월 7일
0
post-thumbnail

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

자바스크립트에서 this는 실행 컨텍스트가 생성될 때 결정된다. 실행 컨텍스트는 함수를 호출할 때 생성되므로, this는 함수가 호출될때 결정된다.
함수를 어떤 방식으로 호출하느냐에 따라 값이 달라지게 된다.

3-1-1 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다.

// 전역 공간에서 this(브라우저 환경) - window
console.log(this); // { alert: f(), atob: f(), blur: f(), btoa: f(), ... }
console.log(window); // { alert: f(), atob: f(), blur: f(), btoa: f(), ... }
console.log(this === window); // true

// 전역 공간에서 this(Node.js 환경) - global
console.log(this); // { process: { title: 'node', version: 'v10.13.0',... } }
console.log(global); // { process: { title: 'node', version: 'v10.13.0',... } }
console.log(this === global); // true

전역 변수를 선언하면 자바스크립트 엔진은 이를 전역 객체의 프로퍼티로 할당한다.

var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1

console.log(a)가 1이 나오는 이유는, 스코프 체인에서 a를 찾아나갈때 가장 마지막에 도달하는 전역 스코프의 LexicalEnvironment인 전역 객체에서 a값을 출력했기 때문이다.

이렇게 자동적으로 전역 변수를 전역 객체의 프로퍼티로 할당하지만 전역 변수로 선언한 변수를 전역 객체의 프로퍼티로 찾아 삭제할 수는 없다.

let a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1

let 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

window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // Uncaught ReferenceError: d is not defined

3-1-2 메서드로 호출할 때 그 메서드 내부에서의 this

함수는 그 자체로 독립적인 기능을 수행하며 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

this는 함수로 호출하면 window, 메서드로 호출하면 메서드가 담겨있는 객체이다.

const func = function(x) {
  console.log(`this는 ${this}, x는 ${x}`);
};
// ✨ 함수로서 호출
func(1); // this는 Window { ... }, x는 1

var obj = {
  method: func,
};
// ✨ 메서드로서 호출
obj.method(2); // this는 { method: f }, x는 2
obj['method'](2); // this는 { method: f }, x는 2

메서드 내부에서의 this

this에는 호출한 주체인 함수명(프로퍼티명) 앞에 쓰여진 객체이다.

const obj = {
  methodA: function() {
    console.log(this);
  },
  inner: {
    methodB: function() {
      console.log(this);
    },
  },
};

// ✨ 메서드들에 대한 this
obj.methodA(); // { methodA: f, inner: {...} }    ( === obj)
obj['methodA'](); // { methodA: f, inner: {...} } ( === obj)

obj.inner.methodB(); // { methodB: f }            ( === obj.inner)
obj.inner['methodB'](); // { methodB: f }         ( === obj.inner)
obj['inner'].methodB(); // { methodB: f }         ( === obj.inner)
obj['inner']['methodB'](); // { methodB: f }      ( === obj.inner)

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

함수 내부에서의 this

어떤 함수를 함수로서 호출할 경우에는 this가 지정되지 않는다. this가 지정되지 않은 경우 전역 객체를 가리킨다.

메서드의 내부 함수의 this

메서드로서 호출할 경우 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기에 따라 this가 달라진다.

const obj1 = {
  outer: function() { // obj1.outer()의 메서드로서 호출되므로 this는 obj1를 가리킨다. 
    console.log(this); 
    const innerFunc = function() {
      console.log(this); 
    };
    innerFunc(); // 함수로서 호출되어 this는 자동으로 전역객체가 바인딩 된다.

    const obj2 = {
      innerMethod: innerFunc, // obj2.innerMethod()의 메서드로서 호출되므로 this는 obj2를 가리킨다.
    };
    obj2.innerMethod();
  },
};
obj1.outer();

this 바인딩은 메서드 내부인지, 함수 내부인지가 중요한 것이 아니다. 해당 함수를 함수로서 / 메서드로서 호출하는지 구별해야한다.

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

호출 주체가 없을 때 자동으로 전역객체를 바인딩하지 않고 주변 환경의 this를 그대로 상속받게 하기 위해서는 변수를 활용한다.

주로 상위 스코프의 self라는 변수에 this를 저장해서 사용한다.

const obj = {
  outer: function() {
    console.log(this); // (1) { outer: f }
    const innerFunc1 = function() {
      console.log(this); // (2) Window { ... }
    };
    innerFunc1();

    const self = this;
    const innerFunc2 = function() {
      console.log(self); // (3) { outer: f }
    };
    innerFunc2();
  },
};
obj.outer();

this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 보완하고자 this를 바인딩하지 않는 화살표 함수를 새로 도입하였다. 화살표 함수는 실행 컨텍스트를 생성할때 this 바인딩 과정 자체가 빠지게 되어 상위 스코프의 this를 그대로 활용할 수 있다.

그 밖에도 call, apply등의 메서드를 활용해 함수를 호출할 때 명시적으로 this를 지정할 수 있다.

3-1-4 콜백 함수 호출 시 그 함수 내부에서의 this

합수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수고라 한다. 이때 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정된다. 콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조하지만, 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.

setTimeout(function() {
  console.log(this); // 전역 객체 
}, 300); 

[1, 2, 3, 4, 5].forEach(function(x) {
  console.log(this, x); // 전역 객체 
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
  console.log(this, e);  // addEventListener 메서드의 점(.) 앞부분이 this
});

3-1-5 생성자 함수 내부에서의 this

자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

const Cat = function(name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
};
const choco = new Cat('초코', 7);
const nabi = new Cat('나비', 5);
console.log(choco, nabi);

/* 결과
Cat { bark: '야옹', name: '초코', age: 7 }
Cat { bark: '야옹', name: '나비', age: 5 }
*/

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

3-2-1 call 메서드

call 메서드는 메서드의 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

첫 번째 인자를 this로 바인딩하고 이후의 인자들은 호출한 함수의 매개변수이다.

// 함수
const 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

// 메서드
const 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 메서드

call 메서드와 기능은 동일하지만 두 번째 인자를 배열를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다.

Function.prototype.apply(thisArg, [argsArray])
// 함수
const func = function(a, b, c) {
  console.log(this, a, b, c);
};
func.apply({ x: 1 }, [4, 5, 6]); // { x: 1 } 4 5 6

// 메서드
const 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 메서드의 활용

1️⃣ 유사배열객체(array-like object)에 배열 메서드를 적용
객체에는 배열 메서드를 직접 사용할 수 없다. 그러나 키가 0또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0또는 양의 정수인 객체일때는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.

const obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
};
Array.prototype.push.call(obj, 'd'); // push() 배열 메서드로 프로퍼티 추가
console.log(obj); // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }

const arr = Array.prototype.slice.call(obj); // slice() 배열메서드로 얕은 복사
console.log(arr); // [ 'a', 'b', 'c', 'd' ]

2️⃣ arguments, NodeList에 배열 메서드를 적용

function a() {
  const 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>';
const nodeList = document.querySelectorAll('div');
const nodeArr = Array.prototype.slice.call(nodeList);
nodeArr.forEach(function(node) {
  console.log(node);
});

3️⃣ 문자열에 배열 메서드를 적용

const 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

const newArr = Array.prototype.map.call(str, function(char) {
  return char + '!';
});

console.log(newArr); // ['a!', 'b!', 'c!', ' !', 'd!', 'e!', 'f!']

const newStr = Array.prototype.reduce.apply(str, [
  function(string, char, i) {
    return string + char + i;
  },
  '',
]);

console.log(newStr); // "a0b1c2 3d4e5f6"

✨ ES6의 Array.from 메서드
call/apply를 이용해 유사배열객체를 배열로 변환하는 것은 'this를 원하는 값으로 지정해서 호출한다'라는 본래의 메서드의 의도와 동떨어진다. 따라서 ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 Array.from 메서드를 도입하였다.

const obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
};
const 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); // Person과 중복되는 내용
  this.school = school;
}
function Employee(name, gender, company) {
  Person.apply(this, [name, gender]); // Person과 중복되는 내용
  this.company = company;
}
const people1 = new Student('길동', 'female', '서울대');
const people2 = new Employee('심청', 'male', '애플');

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

여러개의 인수를 받는 메서드에게 하나의 배열로 인수들을 전달하고 싶을때 apply 메서드를 사용한다.

const numbers = [10, 20, 3, 16, 45];
// const max = Math.max.apply(null, numbers);
// const min = Math.min.apply(null, numbers);

// ES6 spread 연산자를 사용하면 더욱 간결하다.
const max = Math.max(...numbers);
const min = Math.min(...numbers)

console.log(max, min); // 45 3

3-2-4 bind 메서드

bind는 ES5에서 추가된 기능으로 call과 비슷히지만 즉시 호출하지 않고 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.

Function.prototype.apply(thisArg[, arg1[, arg2[, ...]]])

다시 새로운 함수를 호출할 때 인수를 넘기면 그 인수들은 기존 bind 메서드를 호출할 때 전달했던 인수들의 뒤에 이어서 등록된다.

const func = function(a, b, c, d) {
  console.log(this, a, b, c, d);
};
func(1, 2, 3, 4); // Window{ ... } 1 2 3 4

const bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8

const 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 프로퍼티에 bound 접두어가 붙는다.

const func = function(a, b, c, d) {
  console.log(this, a, b, c, d);
};

const bindFunc = func.bind({ x: 1 }, 4, 5);
console.log(func.name); // func
console.log(bindFunc.name); // bound func

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

call, apply 또는 bind 메서드를 사용하면 메서드의 내부 함수에서 메서드의 this를 그대로 바라보게 할 수 있다.
또한, 콜백 함수를 인자로 받는 함수나 메서드 중에서 기본적으로 콜백 함수 내에서의 this에 관여하는 함수나 메서드에 대해서도 bind를 사용하면 this를 사용자가 지정할 수 있다.

3-2-5 화살표 함수의 예외사항

화살표 함수 내부에는 this가 없으며 접근하고자하면 스코프체인상 가장 가까운 this에 접근하기에 편리하다.

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

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this를 지정할 객체(thisArg)를 인자로 지정할 수 있다. thisArg 값을 지정하게 되면 콜백 함수 내부에서 this 값을 원하는대로 변경할 수 있다. 이런 형태는 여러 내부요소에 대해 같은 동작을 반복 수행해야하는 배열 메서드에 많이 포진되어있다.

// 콜백 함수와 함께 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(arrayLike[, callback[, thisArg]])
Set.prototype.forEach(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
profile
🌱 매일 성장하는 개발자

0개의 댓글

관련 채용 정보