[JS] 작동방식

Chanki Hong·2022년 12월 17일
0

JavaScript

목록 보기
14/30
post-thumbnail

스코프(Scope)

  • 가시성(visibility)이라고도 불리며, 시야 또는 범위로 생각하면 편함.
  • 프로그램의 현재 실행 중인 부분(실행 컨텍스트; execution context)에서 보이고 접근할 수 있는 식별자.
  • 변수, 상수, 매개변수, 함수 등을 선언할때 유효범위를 의미.
  • 변수의 스코프가 어떤 함수라고 말한다면, 함수를 호출할 때까지 함수 바디의 정해진 매개변수(formal argument)가 존재하지 않음.
  • 함수를 호출할 때마다 매개변수가 나타나고, 함수가 반환하면 사라짐.
// x가 잠시 존재하며, 이 때문에 x+3을 계산 가능.
function f(x) {
  return x + 3;
}
// 하지만 함수 바디를 벗어나면 x는 존재하지 않은 것 처럼 보임.
console.log(f(5)); // 8
console.log(x); // ReferenceError: x is not defined
// 따라서 x의 스코프는 함수 f라고 할 수 있음.

스코프와 존재

  • 아직 선언하지 않은 변수나 함수가 종료되면서 존재하지 않게 된 변수는 스코프에 있지 않음.
  • 변수가 스코프에 있지 않다는 말과 존재하지 않다는 말은 같지 않음.
  • 존재한다는 말은 그 식별자가 메모리에 할당된 무언가를 가리킨다는 것을 의미.
  • 존재하지만 스코프에 없는 변수들이 존재 할 수 있음.
  • 또한 존재하지 않는다고 해도 JS는 메모리를 바로 회수하지 않음.
  • 계속 유지할 필요가 없다는 표시가 있으면 주기적으로 가비지 콜렉션(garbage collection) 프로세스에서 메모리를 회수. (자동으로 일어남.)

정적 스코프

  • 프로그램의 소스 코드를 보는 것은 프로그램의 정적(어휘적; lexical) 구조를 살피는 것. (단순하게 위에서 아래로)
  • 하지만 프로그램을 실제 실행하면 이곳 저곳에서 움직임.
/*
정적으로 보면 이 프로그램은 단순히 위에서 아래로 읽어내리는 문의 연속.
하지만 실행 흐름은 읽는 순서와 다름.
f1이 f2보다 먼저 정의되었지만,
f2의 함수바디가 먼저 실행되고 f1으로 그리고 다시 f2로 넘어감.
*/
function f1() {
  console.log('one');
}
function f2() {
  console.log('two');
}

f2();
f1();
f2();
  • JS의 스코프는 정적이며, 소스 코드만 봐도 변수가 스코프에 있는지 판단가능.
/*
x는 함수 f를 정의할 때 존재하지만, y는 그렇지 않음.
y는 다른 스코프에 존재함.
다른 스코프에서 y를 선언하고, 그 스코프에서 f를 호출하더라도,
x는 그 바디안의 스코프에 있지만 y는 그렇지 않음.
함수 f는 자신이 정의될 때 접근 가능했던 식별자에는 여전히 접근 가능하지만,
호출할 때 스코프에 있는 식별자에는 접근이 불가함.
*/
const x = 3;
function f() {
  console.log(x);
  console.log(y);
}
{
  const y = 5;
  f();
}
  • JS의 정적 스코프는 Global Scope(전역), Function Scope, Block Scope.

전역 스코프(Global Scope)

  • 스코프는 계층적이며 트리의 맨 아래에 바탕이 되는 스코프.
  • 프로그램을 시작할 때 암시적으로 주어지는 스코프.
  • 전역 스코프에서 선언한 것은 모든 범위에서 접근 가능.
  • 전역 스코프에서 선언된 것들은 모두(변수, 함수, 객체 등) 전역속성.
  • 전역 변수는 충분히 생각해서 사용해야 하고, 전역 스코프에 의존하지 않는 것이 정말 중요함.
/*
함수가 호출하는 컨텍스트(스코프)에 대한히 의존적.
전역 변수 name, age는 언제든 (의도적이든 실수로든) 바꿀 수 있음.
함수 greet과 getBirthYear는 전역 변수에 의존.
*/
let name = 'Irena'; // 전역변수
let age = 25;

function greet() {
  console.log(`hello, ${name}`);
}
function getBirthYear() {
  return new Date().getFullYear() - age;
}
  • 전역 변수보다 단일 객체에 보관하는 방법이 옳음.
const user ={
    name ='Irena',
    age =25
}

function greet() {
    console.log(`hello, ${user.name}`);
  }
  function getBirthYear() {
    return new Date().getFullYear() - user.age;
  }
  • 더욱 개선한다면, 함수가 전역 속성(변수, 객체)에 의존하지 않게 만듬.
/*
이 함수들은 모든 스코프에서 호출 가능하고, 명시적으로 user를 전달받음.
*/
function greet(user) {
  console.log(`hello, ${user.name}`);
}
function getBirthYear(user) {
  return new Date().getFullYear() - user.age;
}

블록 스코프(Block Scope)

  • 블록문 스코프에서만 보이는 식별자.
/*
x는 블록 안에서 정의됨.
블록을 나가는 즉시 x도 스코프 밖으로 사라짐.
*/
console.log('before block'); // before block
{
  console.log('inside block'); // inside block
  const x = 3;
  console.log(x); // 3
}
console.log(`outside block; x=${x}`); // ReferenceError: x is not defined

변수 숨김(variable masking)

  • 같은 이름의 변수나 상수가 다른 계층의 스코프에 동시에 존재할 때, 발생하는 효과.
/*
내부 블록의 x는 외부 블록의 x와 이름만 같음.
외부 스코프의 x를 숨기는(가리는) 효과.
또한 실행 흐름이 내부 블록에 들어가 새 변수 x를 정의하는 순간,
두개의 x는 모두스코프 안에 존재.
변수 이름이 같으므로 외부 스코프의 x에 접근할 방법이 없음.
*/
{
  let x = 'blue';
  console.log(x); // blue
  {
    let x = 3;
    console.log(x); // 3
  }
  console.log(x); // blue
}
console.log(typeof x); // undefined
  • 스코프는 계층적이고, 어떤 변수가 스코프에 존재하는지 확인하는 스코프 체인(scope chain)을 이용하면 유용.
  • 또한 스코프 숨김을 하면 절대 접근할 수 없음.
{
  let x = { color: 'blue' };
  let y = x;
  let z = 3;
  {
    let x = 5; // 외부 스코프의 x 숨김.
    console.log(x); // 5
    console.log(y.color); // blue. 외부 스코프의 y.
    y.color = 'red'; // 외부 스코프의 y를 변경.
    console.log(z); // 3. 외부 스코프의 z.
  }
  console.log(x.color); // red. y 객체가 수정되어 같이 수정됨.(참조타입)
  console.log(y.color); // red. 내부 스코프에서 수정됨.
  console.log(z); // 3
}

함수 스코프(Function Scope)

  • 특정 함수 내에서 정의된 변수 및 매개변수.
  • 함수 내부에서만 접근 가능. (함수 내에서만 유효하며, 외부에서는 접근할 수 없음.)
function sayHello() {
  let message = "Hello, World!";
  console.log(message);
}

sayHello(); // "Hello, World!" 출력
console.log(message); // 에러: message는 함수 내에서만 유효

클로저(closure)

  • 함수 스코프는 클로저(Closure)를 구현하는 데 중요한 역할함.
  • 클로저는 함수가 다른 함수 내에서 정의되고 반환될 때 발생하며, 외부 스코프의 변수에 접근할 수 있는 함수를 의미함.
  • 즉, 함수가 생성될 당시의 외부 변수를 기억함. (생성 이후에도 계속 접근 가능)
function outerFunction() {
  let outerVar = "I am from outerFunction!";
  return function innerFunction() {
    console.log(outerVar);
  };
}

let innerFunc = outerFunction();
innerFunc(); // "I am from outerFunction!" 출력
function makeAdd(i) {
  // makeAdd함수의 리턴은 makeAdd함수의 i에 접근 가능.
  return function (w) {
    return i + w;
  };
}
// makeAdd함수의 리턴함수가 들어감.
// 이때, makeAdd함수에 들어갔던 i는 유지가 됨.
// 즉, 클로저 생성.
let x = makeAdd(1);
console.log(x(1)); // 2
console.log(x(2)); // 3

let y = makeAdd(2);
console.log(y(1)); // 3
console.log(y(2)); // 4
  • 블록문에서 함수가 할당 된다면,
let globalFunc; // 정의되지 않은 전역 함수.
{
  let blockVar = 'a'; // 블록 스코프에 있는 변수.
  // globalFunc는 블록 안에서 값을 할당.
  // 이때 이 블록 스코프와 전역 스코프가 클로저 형성.
  // 이제 globalFunc를 어디서 호출하든, 클로저에 들어있는 식별자에 접근 가능.
  globalFunc = function () {
    console.log(blockVar);
  };
}
globalFunc(); // a
  • 일반적으로 자신의 스코프에 없는 것들을 접근 가능.
let f;
{
  let o = { note: 'safe' };
  f = function () {
    return o;
  };
}
let oRef = f();
console.log(oRef.note); // safe
oRef.note = ' Not so safe after all!';
console.log(oRef.note); // Not so safe after all!

IIFE(즉시 실행 함수; Immediately Invoked Function Expression)

  • IIFE는 함수를 선언하고 즉시 실행하여 변수의 스코프를 제한하는 패턴.
  • (function(){// IIFE body})();
  • 익명 함수를 만들고 그 함수에 ()괄호를 붙여 즉시 호출.
  • 불필요한 전역변수가 필요없고, 또한 내부의 변수 접근 방지.
// 자신이 몇 번 호출됐는지 보고하는 함수.
// IIFE가 반환하는 함수가 식별자 f에 참조.
// 반환 되는 함수는 내부함수이며, 외부 함수의 count에 계속 접근할 수 있는 클로저가 생성.
const f = (function () {
  let count = 0;
  return function () {
    return console.log(`I have been called ${++count} time(s).`);
  };
})();
f(); // I have been called 1 time(s).
f(); // I have been called 2 time(s).

호이스팅(Hoisting)

  • JS는 전역 스코프 전체를 보고 functionvar 로 선언한 변수를 맨 위로 끌어 올리는 호이스팅 매커니즘을 따름.
  • 선언만 끌어 올라오며, 할당은 끌어올려지지 않음.
  • 함수 선언문의 경우는 모든것이 호이스팅 됨.
  • 호이스팅은 피해야 함.
  • 순서가 얽히면 헷갈리고 유지보수가 어려움.
  • 함수는 표현식으로 상수와 변수는 constlet 키워드로 이용하면, ReferenceError를 반환.

함수의 Hoisting

myFunction(); // hello world

function myFunction() {
  console.log('hello world');
}

함수 표현식

  • 함수 표현식을 이용하면 Hoisting을 방지.
myFunction(); // ReferenceError: Cannot access 'myFunction' before initialization

const myFunction = function myFunction() {
  console.log('hello world');
};

변수의 Hoisting

  • var 키워드에서만 Hoisting.
  • var는 또한 함수 스코프만을 따름.
console.log(number); // undefined
var number = 2;
  • 선언하지 않은 변수가 undefined 가 나오는 이유는,
  • JS가 아래와 같이 작동하기 때문.
var number;
console.log(number);
number = 2;
  • var 는 여러 번 정의하더라도 무시함.
  • 또한 새 변수를 만들 수 없고 하나만 존재.
var x = 3;
if (x === 3) {
  var x = 2;
  console.log(x); // 2
}
console.log(x); // 2
  • letconst 를 이용하면 Hoisting을 방지.

const 키워드

console.log(number); // ReferenceError: Cannot access 'number' before initialization
const number = 2;

스트릭트 모드(strict mode)

  • var 로 변수 선언하는 것을 잊고 이용하면, JS는 전역 변수를 참조하려 한다고 간주하고 존재하지 않으면 스스로 만들었음. (암시적 전역 변수)
  • 스트릭트 모드로 암시적 전역 변수를 허용하지 않을 수 있음.
  • 'use strict' 코드를 코드 맨 앞에 사용.
(function () {
  'use strict';
  // code 작성.
  // 모두 스트릭트 모드로 동작.
})();

0개의 댓글