JS Lexical Scope, 실행 컨텍스트

Jungwon Lee(Jenny)·2021년 5월 18일
0
post-thumbnail

이 글은 👉 zerocho 실행컨텍스트 게시물을 통해 공부한 점을 개인적으로 정리하며 되새길 겸 쓴 글이라 상당 부분이 제로초님의 게시물 내용을 포함하고 있습니다✍✍✍

Lexical Scope


스코프는 함수를 호출할 때가 아니라 선언할 때 생긴다.
const name = "Jenny";
function log() {
  console.log(name);
}

function wrapper() {
  const name = "babo";
  log();
}
wrapper();

코드참고: 제로초블로그

위 코드에 대한 결과물이 lexical Scoping에 대해 모를땐 babo 가 나올 것 같다.

하지만 실제로 결과는 Jenny 가 나온다.

위에 앞서 설명한대로 스코프는 함수를 호출할 때가 아니라 선언할 때 생긴다.

즉, log()함수를 처음 선언하는 순간, 함수 내부의 변수는 자기 스코프로부터 가장 가까운 곳(상위 범위에서)에 있는 변수를 계속 참조하게 된다.

실행 컨텍스트

var name = "Jenny";
function wow(word) {
  console.log(word + " " + name);
}
function say() {
  var name = "Lee";
  console.log(name);
  wow("hello");
}
say();

코드참고: https://www.zerocho.com/category/JavaScript/post/5741d96d094da4986bc950a0

Lexical scope에 대해 잘 이해했다면 위의 답이 "Lee hello Jenny"가 된다는것을 알 것이다.

위 코드가 어떻게 실행되는지 내부를 살펴보면, 일단 처음 브라우저가 스크립트를 로딩해서 실행하는 순간 모든 것을 포함하는 전역 컨텍스트가 생긴다. 이는 페이지가 종료될 때까지 유지된다.

전역 컨텍스트 말고도 함수 컨텍스트가 있는데, 함수를 호출할 때마다 함수 컨텍스트가 하나씩 더 생긴다.


컨텍스트의 원칙 네가지✍:

  • 전역 컨텍스트 하나 생성 후, 함수 호출 시마다 컨텍스트가 생긴다.
  • 컨텍스트 생성 시 컨텍스트 안에 변수객체(arguments, variable), scope chain, this가 생성된다.
  • 컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾는다.
  • 함수 실행이 마무리되면 해당 컨텍스트는 사라진다. 클로저는 사라지지 않는다. 페이지가 종료되면 전역 컨텍스트가 사라진다.

그럼 이제 컨텍스트의 네가지 원칙을 염두하며 아래의 코드를 분석해보자.

var name = "Jenny";
function wow(word) {
  console.log(word + " " + name);
}
function say() {
  var name = "Lee";
  console.log(name);
  wow("hello");
}
say();

전역 컨텍스트


일단 전역 컨텍스트가 생성된다.

그 후 변수객체, scope chain, this가 들어온다. 당연히 전역 컨텍스트는 arguments가 없고, variable은 해당 스코프의 변수들이다.

'전역 컨텍스트':{
	변수객체:{
		arguments:null,
		variable: ['name', 'wow', 'say'],
	},
	scopeChain: ['전역 변수객체']
	this:window,
}

전역 컨텍스트의 scope chain은 자기 자신인 전역 변수객체이다.

이제 코드를 위에서부터 실행하는데, wow랑 say는 호이스팅 때문에 선언과 동시에 대입된다.

'전역 컨텍스트':{
	변수객체:{
		arguments:null,
		variable: [{name:'Jenny'}, {wow:Function}, {say:Function}],
	},
	scopeChain: ['전역 변수객체']
	this:window,
}

여기까지가 전역 컨텍스트 관련된 실행 흐름이다.

함수 컨텍스트


아래 주석친 부분부터 어떻게 작동되나 파악해보자.
var name = "Jenny";
function wow(word) {
  console.log(word + " " + name); //3
}
function say() {
  var name = "Lee";
  console.log(name); //1
  wow("hello"); //2
}
say(); //==>이순간! 어떻게 되는지 볼거임

say() 를 하는 순간 새로운 컨텍스트인 say함수 컨텍스트가 생긴다.

'say 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: ['name'], // var name = 'Lee'코드로 초기화 후 [{ name: 'Lee' }]가 됨
  },
  scopeChain: ['say 변수객체', '전역 변수객체'],
  this: window,
}

arguments는 아무것도 없고, variable은 name이라는 변수 뿐이다.

scope chain은 say변수객체와 say함수의 상위의 전역 변수객체이다.

this는 따로 설정해준 적이 없으니 window가 된다.

이어서 say();를 하고나면 순서대로 console.log(name) wow('hello') console.log(word+ ' ' + name) 이 실행된다.

wow('hello')를 say함수 컨텍스트 안에서 찾으려고 보니 wow변수가 없다. 그럼 scope chain을 따라 올라가 상위의 전역 변수객체에서 찾아본다. 전역 변수 객체의 variable에 wow라는 함수가 있으니, 이걸 호출한다

wow함수가 wow('hello');를 통해 호출되었으니, say함수의 경우처럼 wow함수 컨텍스트도 생긴다.

'wow 컨텍스트': {
  변수객체: {
    arguments: [{ word : 'hello' }],
    variable: null,
  },
  scopeChain: ['wow 변수객체', '전역 변수객체'],
  this: window,
}

이제 컨텍스트가 생긴 후 함수가 실행된다. wow함수 안의 console.log(word+ ' ' + name) 을 보면 word랑 name변수는 wow컨텍스트 안에서 찾으면 된다.

word는 arguments에서 찾을 수 있고, name은 variable에도 없으니 wow 변수객체에 없다고 보면 된다. 그럼 scope chain을 따라 전역 변수객체를 살펴보니, variable에 name:Jenny 라고 되어있는 부분을 가져오면 된다.

wow함수 종료 후 wow컨텍스트가 사라지고, say함수의 실행도 마무리되면서 say컨텍스트도 사라진다.

😫조금 헷갈리는 예시 코드😫

일단 아래의 코드는 에러가 난다.

sayWow(); // (3)
sayYeah(); // (5) 여기서 대입되기 전에 호출해서 에러
var sayYeah = function() { // (1) 선언 (6) 대입
  console.log('yeah');
}
function sayWow() { // (2) 선언과 동시에 초기화(호이스팅)
  console.log('wow'); // (4)
}
  1. 처음 실행 시에는 전역 컨텍스트가 먼저 생성된다.
  2. sayWow함수는 함수 선언식이므로 컨텍스트 생성 후 바로 대입된다.
  3. 하지만 sayYeah는 대입되기 전에 호출해서 에러가 발생한다. 즉, {sayYeah:Function}이 되기 전에 호출해서 그렇다.

그래서 아래의 컨텍스트를 가진다.

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: [{ sayWow: Function }, 'sayYeah'],
  }, //sayWow는 바로 대입되었지만, sayYeah는 대입되어있지않다.
  scopeChain: ['전역 변수객체'],
  this: window,
}



클로저


var makeClosure = function () {
  var name = "Jenny";
  return function () {
    console.log(name);
  };
};
var closure = makeClosure(); // function () { console.log(name); }
closure(); // 'Jenny';

closure 함수 안에는 console.log(name 이 있다. name은 closure함수의 매개변수도 아니고, closure함수 내부에서 생성한 변수도 아니다. 바로 이런것이 비공개 변수이다.

컨텍스트로 분석해보면 아래와 같다.

"전역 컨텍스트": {
  변수객체: {
    arguments: null,
    variable: [{ makeClosure: Function }, 'closure'],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}
"makeClosure 컨텍스트": {
  변수객체: {
    arguments: null,
    variable: [{ name: 'Jenny' }],
  },
  scopeChain: ['makeClosure 변수객체', '전역 변수객체'],
  this: window,
}

전역 컨텍스트 생성 후 makeClosure함수 호출 시 makeClosure 컨텍스트도 만들어진다.

그 후 closure(); 을 통해 closure함수가 실행되고, closure함수 컨텍스트가 만들어진다.

"closure 컨텍스트":  {
  변수객체: {
    arguments: null,
    variable: null,
  scopeChain: ['closure 변수객체', 'makeClosure 변수객체', '전역 변수객체'],
  this: window,
}

closure=makeClosure()부분 때문에 function을 return하는데 그 function 선언 시의 scope chain은 lexical scoping을 따라서 ['makeClosure 변수객체', '전역 변수객체']를 포함한다.

따라서 closure 함수에서 scope chain을 통해 makeClosure의 name변수에 접근할 수 있게 된다!

profile
FE개발자가 되고싶은 말하는 감자

0개의 댓글