[JavaScript] 3. Scope, this, 실행 컨텍스트 동작 원리

rin·2020년 11월 30일
0
post-thumbnail

https://poiemaweb.com/coding

스코프와 this

스코프

스코프는 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙이다.

var x = 'global';

function foo () {
  var x = 'function scope';
  console.log(x);
}

foo(); // ?
console.log(x); // ?

위 예제에서 전역에 선언된 변수 x는 어디에든 참조할 수 있으나, 함수 foo 내에서 선언된 변수 x는 함수 foo 내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다.

스코프가 없다면 같은 식별자 이름은 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다.

스코프의 구분

JS에서 스코프는 2가지로 나뉜다.

  • 전역 스코프 : 코드 어디에서든지 참조할 수 있다.
  • 지역 스코프 : 함수 코드 블록이 만든 스코프로써 함수 자신과 하위 함수에서만 참조할 수 있다.

변수의 관점에서 스코프를 구분하면 다음과 같이 2가지로 나눌 수 있다.

  • 전역 변수 : 전역에서 선언된 변수이며 어디에든 참조할 수 있다. 전역 스코프를 가진다.
  • 지역 변수 : 지역(함수) 내에서 선언된 변수이며 그 지역과 그 지역 하부 지역에서만 참조할 수 있다. 지역 스코프를 가진다.

JS 스코프의 특징🤔

대부분 C-family language는 블록 레벨 스코프를 따른다. 즉, 코드 블록 {...} 내에서 유효(접근 가능)한 스코프를 의미한다.

int main(void) {
  // block-level scope
  if (1) {
    int x = 5;
    printf("x = %d\n", x);
  }

  printf("x = %d\n", x); // use of undeclared identifier 'x'

  return 0;
}

if문 내에서 선언된 변수 x는 if문 코드 블록 내에서만 유효하며, if문 코드 블록 밖에서는 참조가 불가하다.

하지만 JS는 함수 레벨 스코프를 따르며, 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역변수이다.

function main() {
    if(true) {
        var x = 5;
        console.log(x);    // 해당 함수 내 선언한 변수. 출력 가능.
    }
    console.log(x);        // 해당 함수 내 선언한 변수. 출력 가능.
}

위 예제를 보면 if문 코드 블록 내에서 선언된 x가 같은 함수인 main 내에서는 언제든 접근 가능함을 알 수 있다.

var x = 0;
{
  var x = 1;
  console.log(x); // 1
}
console.log(x);   // 1

let y = 0;
{
  let y = 1;
  console.log(y); // 1
}
console.log(y);   // 0

코드 블럭 내에서 var x=1를 선언한 경우, 전역변수(라고 생각한) x가 재정의된 것을 알 수 있다. 단, ECMAScript6에서 도입된 let 키워드를 사용하면 블록 레벨 스코프를 사용할 수 있다.

✔️ 전역 스코프
var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티이다.

JS는 특별한 시작점(C의 경우 main 메소드)이 없으며 코드가 나타나는 즉시 해석, 실행된다. 따라서 전역에 변수를 선언하기 쉬우며 이것은 전역 변수를 남발하는 문제를 야기시킨다.

전역 변수의 사용은

  • 변수 이름이 중복될 수 있고
  • 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만들 수 있다.

👉 사용을 최소화하자!
1️⃣ 어플리케이션에서 전역 변수 사용을 위해 전역 변수 객체를 만들어 사용할 수 있다.

var MYAPP = {};

MYAPP.student = {
  name: 'Lee',
  gender: 'male'
};

console.log(MYAPP.student.name);

2️⃣ 즉시 실행 함수를 사용하면 실행 후 전역에서 바로 사라지게 할 수 있다.

(function () {
  var MYAPP = {};

  MYAPP.student = {
    name: 'Lee',
    gender: 'male'
  };

  console.log(MYAPP.student.name); // Lee
}());

console.log(MYAPP.student.name); // Uncaught ReferenceError: MYAPP is not defined

❗️ 주의
비 블록 레벨 스코프
JS는 블록 레벨 스코프를 사용하지 않으므로 함수 밖에서 선언된 변수는 코드 블록 내에서 언언되었을지라도 모두 전역 스코프를 갖게된다.

if (true) {
  var x = 5;
}
console.log(x);

위 예제에서 x는 전역 변수이다.

✔️ 함수 레벨 스코프
함수 내에서 선언된 매개변수와 변수는 함수 외부에서는 유효하지않다.

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);
}

foo();          // local
console.log(x); // global

전역변수 x와 지역변수 x가 중복 선언되었다. 전역 영역에서는 전역 변수만이 참조 가능하고 함수 내 지역 영역에서는 전역과 지역 변수 모두 참조 가능하나 변수명이 중복된 경우, 지역변수를 우선 참조한다.

함수 영역에서는 전역 변수를 참조할 수 있으므로 전역 변수의 값도 변경할 수 있다.

var x = 'global';

function foo() {
  var x = 'local';
  console.log(x);

  function bar() {  // 내부함수
    console.log(x); 
  }

  bar();
}
foo(); // local local
console.log(x); // global

내부함수는 자신을 포함하고 있는 외부함수의 변수에 접근할 수 있다.

var foo = function ( ) {
  var a = 3, b = 5;
  var bar = function ( ) {
    var b = 7, c = 11;
    console.log(a, b, c); // a=전역, b/c=지역
    a += b + c;
    console.log(a, b, c); // a=전역, b/c=지역
  };

  console.log(a, b); // a/b=전역, c=지역(in bar) not defined
  bar( );
  console.log(a, b); // a/b=전역, c=지역(in bar) not defined
};

foo()를 실행한 결과는 다음과 같다.

3 5
3 7 11
21 7 11
21 5

✔️ 렉시컬 스코프
상위 스코프를 결정하는 패턴을 두 종류가 있다.

  1. 동적 스코프 : 함수를 어디서 호출하였는지에 따라 상위 스코프를 결정하는 것
  2. 렉시컬 스코프(정적 스코프) : 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정하는 것

JS를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프❗️를 따른다.

✔️ 암묵적 전역
선언하지 않은 식별자에 값을 할당하면 전역 객체의 프로퍼티가 된다.

  • 전역 변수는 모두 전역 객체의 프로퍼티이다.
  • 하지만, 위의 경우에는 변수가 아니므로 변수 호이스팅이 발생하지 않는다.

또한, 전역 변수는 프로퍼티이지만 delete 연산자로 삭제할 수 없으나 변수가 아닌 프로퍼티는 delete 연산자로 삭제할 수 있다.

this

JS의 함수는 호출될 때, 매개변수로 전달되는 인자값 이외에 argument 객체this를 암묵적으로 전달받는다.

  • Java에서 this는 인스턴스 자신을 가리키는 참조 변수이다.
  • JS에서 this에 바인딩되는 객체는 한가지가 아니라 해당 함수 호출 방식에 따라 달라진다.

함수 호출 방식과 this 바인딩

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

함수의 호출 방식은 다음과 같다.

  1. 함수 호출
  2. 메소드 호출
  3. 생성자 함수 호출
  4. apply/call/bind 호출
var foo = function () {
  console.dir(this);
};

// 1. 함수 호출
foo(); // window
// window.foo();

// 2. 메소드 호출
var obj = { foo: foo };
obj.foo(); // obj

// 3. 생성자 함수 호출
var instance = new foo(); // instance

// 4. apply/call/bind 호출
var bar = { name: 'bar' };
foo.call(bar);   // bar
foo.apply(bar);  // bar
foo.bind(bar)(); // bar

위 예제의 출력은 다음과 같다.

Window {
 ...
}

Object {
 foo: f ()
}

foo {
}

Object {
 name: "bar"
}

Object {
 name: "bar"
}

Object {
 name: "bar"
}

함수 호출

일반적으로 전역객체는 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미한다.

전역객체는 전역 스코프를 갖는 전역 변수를 프로퍼티로 소유한다. 글로벌 영역에 선언한 함수 또한 전역객체의 프로퍼티로 접근할 수 있는 전역 변수의 메소드이다.

기본적으로 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
    function bar() {
      console.log("bar's this: ",  this); // window
      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();

위의 예제에서 obj는 전역객체이므로 this에 바인딩된다.
bar는 지역 함수이므로 window가 바인딩된다.

this를 명시적으로 바인딩하기 위해서는 apply, call, bind 메소드를 사용하여야한다.


❗️ apply vs. call vs. bind

각 함수의 매개변수는 다음과 같다.

함수명매개변수설명
Function.prototype.callfunc.call(thisArg[, arg1[, arg2[, ...]]])thisArg : func 호출에 제공되는 this의 값
arg1,arg2,... : func에 사용할 인수
Function.prototype.applyfunc.apply(thisArg, [argsArray])thisArg : func 호출에 제공되는 this의 값
argsArray : func에 사용할 인수를 지정하는 유사 배열 객체
Function.prototype.bindfunc.bind(thisArg[, arg1[, arg2[, ...]]])func 호출에 제공되는 this의 값
arg1,arg2,... : func에 사용할 인수

call

let person1 = {
    name: 'Jo'
};

let person2 = {
    name: 'Kim',
    study: function() {
        console.log(this.name + '이/가 공부를 하고 있습니다.');
    }
};

person2.study(); // Kim이/가 공부를 하고 있습니다.

// call()
person2.study.call(person1); // Jo이/가 공부를 하고 있습니다.

person2에 person1을 바인딩하기 때문에 this가 가리키는 대상이 바뀌었다.

apply
call과 동일하게 작동하나 두 번째 매개변수를 배열 형태로 넣는다.

bind
새롭게 바인딩한 함수를 만든다.

let person1 = {
    name: 'Jo'
};

let person2 = {
    name: 'Kim',
    study: function() {
        console.log(this.name + '이/가 공부를 하고 있습니다.');
    }
};

person2.study(); // Kim이/가 공부를 하고 있습니다.

// bind()
let student = person2.study.bind(person1);

student(); // Jo이/가 공부를 하고 있습니다.
person2.study(); // Kim이/가 공부를 하고 있습니다.

바인딩한 함수는 원본 함수 객체를 감싸는 함수로써, 변수를 할당하여 호출하는 방식으로 사용해야한다.
실제로 person2가 바인딩한 this는 변경되지 않았다.


메소드 호출

함수의 객체가 프로퍼티 값이면 메소드로서 호출된다. (이게 곧 메소드의 정의이다.🤔 )

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

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

var obj2 = {
  name: 'Kim'
}

obj2.sayName = obj1.sayName;

obj1.sayName(); // Lee
obj2.sayName(); // Kim

sayName메소드를 obj1이 가지고 있기 때문에 두 번의 메소드 호출에서 모두 obj1이 바인딩될거라 예상했지만, obj2의 경우에서는 "해당 메소드를 호출한 객체인" obj2이 바인딩된 것을 확인할 수 있다.

생성자 함수 호출

JS의 생성자 함수는 자바와 같은 객체지향 언어의 생성자 함수와는 다르게 그 형식이 정해진 것이 아니고, 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다.

즉, 생성자 함수가 아닌 일반 함수에 new 연산자를 붙여 호출해도 생성자 함수처럼 동작한다는 것이다. 🤔

따라서 혼란을 방지하기 위해 생성자 함수명은 첫문자를 대문자로 기술하도록한다.

✔️ 생성자 함수 동작 방식

  1. 빈 객체 생성
  2. 생성자 함수 내에서 사용되는 this에 빈 객체를 바인딩
  3. 빈 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정
  4. 빈 객체에 this를 사용해 동적으로 프로퍼티나 메소드를 생성한다.
    이렇게 생성된 프로퍼티/메소드는 새로 생성된 빈 객체에 추가된다.
  5. 생성된 객체를 반환한다.
    반환문이 없는 경우, this에 바인딩된 새로 생성한 객체가 반환(명시적으로 this를 반환하는 것과 같다.)
    this가 아닌 다른 객체를 명시적으로 반환하는 경우, 해당 객체(this 아님!)가 반환
unction Person(name) {
  // 생성자 함수 코드 실행 전 -------- 1 ~ 2
  this.name = name;  // --------- 3 ~ 4
  // 생성된 함수 반환 -------------- 5
}

var me = new Person('Lee');
console.log(me.name);

✔️ 생성자 함수에 new 연산자를 붙이지 않고 호출하는 경우

일반 함수를 호출하면 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에 바인딩된다. 또한 생성자 함수를 호출하는 경우에 암묵적으로 반환하던 this로 반환하지 않는다.

실행 컨텍스트와 JS의 동작 원리

실행 컨텍스트

기본 개념

실행 컨텍스트는 scope, hoisting, this, function, closure 등의 동작 원리를 담고 있는 자바스크립트의 핵심 원리이다.

실행 컨텍스트는 실행가능한 코드를 형상화하고 구분하는 추상적 개념이다. 즉, 실행 가능한 코드(전역 코드, Eval 코드, 함수 내 코드)가 실행되기 위해 필요한 환경이라고 말할 수 있다.

JS 엔진은 코드르 실행하기 위해 실행에 필요한 여러가지 정보를 알고 있어야하는데, 여기에는 변수(전역, 지역, 매개변수, 객체의 프로퍼티), 함수 선언, Scope, this같은 것들이 포함된다.

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}
foo();

위 코드를 실행할 시에 실행 컨텍스트 스택은 다음과 같다.

현재 실행 중인 컨텍스트와 관련없는 코드가 실행되면 새로운 컨텍스트가 생성되어 스택에 쌓이고, 컨트롤(제어권)이 이동한다.

  1. 실행 가능한 코드로 컨트롤이 이동하면 새로운 컨텍스트 스택이 생성된다. (LIFO 구조)
  2. 역 실행 컨텍스트는 어플리케이션이 종료될 때(웹 페이지에서 나가거나 브라우저를 닫을 때)까지 유지된다.
  3. 함수를 호출하면 해당 함수의 실행 컨텍스트가 생성된다.
  4. 함수 실행이 끝나면 해당 함수의 실행 컨텍스트를 파기하고 직전 실행 컨텍스트에 컨트롤을 반환한다.

실행 컨텍스트의 3가지 객체

실행 컨텍스트가 추상적 개념이라하여도 물리적으로는 객체의 형태를 가지며 3개의 프로퍼티를 소유한다.

  1. Variable Object (변수 객체)
  2. Scope Chain
  3. this value

✔️ VO실행에 필요한 정보(변수, 매개변수, 인수 정보, 함수 선언-단, 함수 표현식은 제외)를 담을 객체로써 엔진에 의해 참조될 뿐 코드에서 접근이 불가하다.

VO의 값은 또 다른 객체를 가리키고 있는데, 전역 컨텍스트의 경우와 함수 컨텍스트의 경우가 가리키는 객체는 상이하다.

전역 컨텍스트함수 컨텍스트
GO(Grobal Object) : 유일하며 최상위에 위치. 모든 전역 변수와 전역 함수들을 프로퍼티로 포함한다. AO(Activation Object) : 해당 함수의 프로퍼티 뿐만 아니라, 매개변수와 인수들의 정보를 배열의 형태로 담고있는 객체인 argument object를 추가적으로 가지고 있다.

✔️ Scope Chain(SC)전역 객체와 중첩된 함수의 스코프의 레퍼런스를 차례로 저장하고 있는 리스트로써 결론적으론 해당 함수에서 접근가능한 정보를 포함하는 GO/AO를 가리키고 있다.

리스트를 0번 인덱스부터 순회하며 하위 함수 내에서 상위 함수(+전역 스코프)를 참조 할 수 있게 해준다.

❗️ 스코프 체인 vs. 프로토타입 체인
스코프 체인 : 식별자 중 객체(전역 객체 제외)의 프로퍼티가 아닌 식별자, 즉 변수를 검색하는 매커니즘이다.
프로토타입 체인 : 식별자 중 변수가 아닌 객체의 프로퍼티(물론 메소드 포함)를 검색하는 매커니즘이다.

✔️ this 프로퍼티에는 this 값이 할당된다.

실행 컨텍스트 생성과정

👉 전역 코드에의 진입

설명
유일한 전역객체 GO가 생성된다.
전역코드로 컨트롤 진입 시 전역 실행 컨텍스트가 생성되고 스택에 쌓인다.
스코프 체인의 생성과 초기화가 실행된다.


VO에 프로퍼티와 값(변수, 매개변수, 인수정보, 함수선언)을 VO에 추가하여 객체화한다. (= 변수 객체화)
1. 매개변수 -> 프로퍼티, 인수 -> 값으로 설정
2. 함수 호이스팅
3. 변수 호이스팅
this value가 결정되기 이전에 this는 전역객체를 가리키고 있다가 함수 호출 패턴에 의해 this에 할당되는 값이 결정된다.

전역 컨텍스트(전역 코드)의 경우 VO, scope chain, this value는 언제나 전역 객체이다.

👉 전역 코드의 실행

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}

foo();
설명
스코프 체인이 팜조하고 있는 VO를 선두(0)부터 검색하여 변수명에 해당하는 프로퍼티가 발견되면 값(xxx)을 할당한다.


함수가 실행되기 시작하면 새로운 함수 실행 컨텍스트가 생성되고 전역 코드의 경우와 마찬가지의 단계를 밟게된다.
내부 함수이므로 this value는 전역 객체가 된다.
profile
🌱 😈💻 🌱

0개의 댓글