본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
JAVASCRIPT

자바스크립트 - 변수 스코프와 바인딩, 그리고 this

by 방구석 대학생 2022. 2. 2.

 

스코프(Scope)

스코프란 어떤 변수의 유효 범위를 뜻한다.

보통 다른 언어 에서의 전역 변수, 지역 변수 개념과 동일하다.

지역 스코프(지역 변수)는 다시 블록 스코프와 함수 스코프로 나뉜다.

- 블록 : 중괄호를 통해 표시된 범위 -> 중괄호 내부에서 선언된 변수는 중괄호 내부에서만 유효하다.

(블록 스코프는 ES6 에서 추가된 개념이다. let, const 키워드를 활용하여 블록 내부에서만 유효한 지역 변수를 만들 수 있다.)

- 함수 스코프 : 함수 내에서 선언된 변수는 함수 밖에서는 사용 불가능하다.

 

* 블록 스코프의 예시

var name = "minchul";

console.log(name); // minchul
{
    const name = "youngsu";
    console.log(name); // youngsu
}
console.log(name); // minchul

* 함수 스코프 예시

function funScope(){
    var num = 10;
    
    console.log(num);
}
console.log(num); // 에러 발생 -> num 의 범위가 유효하지 않다.

 

자바스크립트 에서의 전역 변수

자바스크립트에서 전역 스코프(변수) 라고 볼 수 있는 경우는 크게 두 가지가 있다.

1. 어떤 것으로도 둘러싸이지 않은 경우

2. let, const 키워드가 쓰인 특수한 경우가 아니라, 일반적인 블록 스코프의 경우 전역 스코프라고 볼 수 있다.

-> 앞서 말했듯 자바스크립트 에서는 원래 블록 스코프 라는 개념이 존재하지 않았다. 그러나 ES6에 와서 새롭게 생긴 개념이다.

즉, ES6 에서 추가된 let, const 라는 특수한 개념이 쓰이지 않는 한, 블록 스코프는 지역 변수가 아닌 것이다. 

-> let, const 가 쓰이지 않은 기존의 블록 스코프는 전역 변수 라고 볼 수 있다.

 

* 전역 스코프 예시1

var num = 10;
console.log("전역에서 출력하는 num : ", num);

function numTest(){
    console.log("함수 안에서 출력하는 num : ", num);
}

numTest();

* 전역 스코프 예시2

{
    var num = 10;
}

console.log("전역에서 출력하는 num : ", num);

function numTest(){
    console.log("함수 안에서 출력하는 num", num);
}

numTest();

 

그런데 가급적이면 전역 변수는 사용을 자제해야 한다.

전역 변수가 굉장히 많고, 그에 따라 코드도 굉장히 길게 작성된 프로그램이 있다고 가정해보자.

이렇게 프로그램의 규모가 커지면 모든 변수의 의미와 쓰임새를 기억하는 건 어려운 일이다

심지어 변수의 이름을 봐도, 이게 뭐하는 변수인지 잘 모르는 경우도 심심치 않게 발생한다.

 

그렇게 복잡한 상황에서 지역 변수의 경우는 아무리 많아진다고 해봤자, 시스템에 미칠 영향은 끽해봐야 해당 지역 변수가 선언된 범위 내외일 것이다.

즉, 데이터의 추적이나 디버깅이 쉽다.

하지만 전역 변수가 많아진다면, 전역 변수가 변경될 때마다 해당 변화가 시스템에 끼칠 영향의 범위는 시스템 전체가 되어버린다.

만약, 전역 변수에 의해 문제가 발생한다면 시스템 전체를 모두 점검해봐야 하는 상황이 생길 수도 있다.

 

이러한 대참사를 막기 위해서, 혹은 시스템의 각 기능별로 세분화하여 잘 관리해주기 위해서 가급적이면 전역 변수를 사용하지 않기를 권하는 것이다.

굳이 사용해야 한다면 가급적이면 해당 전역변수는 값이 변경될 일이 없는 데이터를 사용하는 것이 좋다.

* 참조 : 크롬 브라우저의 개발자 도구에서 자바스크립트의 각 변수의 범위(스코프) 가 어떻게 되는지를 확인해 볼 수 있다.

 

 

this 키워드

* binding 이란? : 함수를 호출했을 때, 함수를 호출한 대상에게 실제로 함수가 저장되어 있는 메모리 주소를 연결시켜 주는 것이다. - 메모리 주소 적재

아래와 같은 코드가 있다고 가정하자.

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

var otherObject = {
    name : 'youngsu'
}

// myObject 의 함수를 otherObject 에 추가(binding - 객체에 메소드를 묶어준다.)
otherObject.sayName = myObject.sayName; 
otherObject.sayName(); // youngsu
myObject.sayName(); // minchul
// 각각 자기자신의 name 변수 값을 출력한다.

 

전역 객체

전역 객체는 코드 전체를 아우르는 객체이다.

console.log(), clear() -> 이 녀석들 모두 전역 객체를 통해 실행되는 메소드이다.

전역 변수의 경우엔 전역 객체의 속성 이라고 할 수 있다.

 

브라우저에서 자바스크립트를 실행하는 경우, 브라우저 에서의 전역 객체는 window 객체라고 부르고,

서버에서 자바스크립트를 실행하는 경우(자바스크립트 파일을 터미널에서 직접 실행시키는 경우(예시 : Node.js 등)) 서버에서의 전역 객체는 global 객체라고 한다.

window 객체 덕분에 우리는 자바스크립트 코드로서 html, css 등에 접근이 가능하게 되었다.

 

그렇다면 각 경우에 있어서 this 키워드는 어디로 바인딩 될까?

 

1. 일반 함수를 호출했을 때 this 는 어디로 바인딩 되느냐

- 함수는 사실 전역 객체의 메소드이고, 전역 변수도 사실 전역 객체의 속성이다.

그냥 함수를 호출하든, window 키워드를 붙이고 호출하든 함수의 실행 결과는 동일하다.

// 일반 함수의 호출 과정에서의 this 는 전역 객체를 가리킨다.
function myFunc(){
    console.log("this 값 : ", this);
}

myFunc();
window.myFunc();

-> 출력 : this 값 :  Window {0: Window, 1: Window, window: Window, self: Window, document: document, name: '', location: Location, …}

var name = "minchul";

console.log("전역 변수 name : ", name);
console.log("전역 객체의 속성 name : ", window.name);

-> minchul

var name = "KangMinchul";
console.log(window.name);

var sayHello = function(){
    var name = "Kang Youngsu";
    console.log(this.name); // this 는 전역 객체인 name 을 가리킨다. -> KnagMinchul
}

sayHello();

-> KangMinchul

-> KangMinchul

 

2. 객체를 호출했을 때 this 는 어디로 바인딩 되느냐

- 객체의 메소드에서 사용된 this 는 그 메소드를 호출한 객체로 바인딩 된다.

* 자바에서 클래스 내부에서 사용한 this 키워드와 같은 기능이라고 생각하면 된다.

 

3. 생성자 함수를 호출 했을 때 this 는 어디로 바인딩 되느냐

- 생성자 함수에서의 this 는 그 생성자 함수를 통해 생성되어 반환되는 객체에 바인딩 된다.

(자바에서의 this 와 같은 개념)

var Person = function(name){
    this.name = name;
}

var boy = new Person("민철");
console.log(boy.name); // 민철

 

 

* 생성자 함수 this VS 일반 함수 this

new 로 새로운 객체를 만들면 생성자 함수, new 없이 그냥 호출되어 쓰이면 일반 함수이다.

var name = "전역변수 name";
var classno = "전역변수 classno";
var professor = "전역변수 professor";

console.log(name);
console.log(classno);
console.log(professor);

function ComputerClass(name, professor, classno){
    this.name = name;
    this.professor = professor;
    this.classno = classno;
    this.printInfo = function(){
        console.log(this.name + '강의 ' + this.classno + '분반입니다. 교수는 ' + this.professor + '입니다.');
    };
}

var class1 = ComputerClass('운영체제', '이동희', 2);
var class2 = ComputerClass('데이터베이스', '홍의경', 1);

console.log(name);
console.log(classno);
console.log(professor);

-> 전역변수 name

-> 전역변수 classno

-> 전역변수 professor

-> 데이터베이스

-> 1

-> 홍의경

 

위와 같이 ComputerClass 함수를 실행하면 new 연산자가 없기 때문에, 객체를 생성하는 생성자 함수가 아니라 일반 함수로 동작하므로, 함수 내부에 작성된 this 키워드는 전역 객체에 바인딩 된다.

 

* 주의 : 내부 함수에서의 this 는 일반 함수가 되었든, 객체가 되었든, 생성자 함수가 되었든 무조건 전역 객체에 바인딩 된다.

- 일반 함수의 내부함수 : 함수 안에 선언된 또다른 함수

function myFunction(){
    console.log("myFunction's this : ", this); // window 에 바인딩
    function innerFunction(){
        console.log("innerFunctions's this : ", this); // window 에 바인딩
        // 일반 함수의 내부 함수 innerFunction 의 this 는 전역 객체에 바인딩된다.
    }
    innerFunction();
}

myFunction();

 

- 객체의 내부 함수 : 객체의 메소드 안에 선언된 또다른 함수

var value = 1;

var obj = {
    value : 100,
    objectmethod : function(){
        console.log("objmethod's this : ", this); // obj 에 바인딩된다.
        console.log("objmethod's this.value : ", this.value); // obj 의 속성 : 100
        
        function innerMethod(){
            console.log("innerMethod's this : ", this); // window 에 바인딩된다.
            console.log("innerMethod's this.value", this.value); // 전역변수 value : 1
            // 메소드 내부 함수 innerMethod 의 this 도 전역 객체에 바인딩 된다.
        }
        innerMethod();
    }
};

obj.objmethod();

 

- 생성자 함수의 내부 함수 : 생성자 함수 안에 선언된 또다른 함수

function constructor(){
    console.log("constructor's this : ", this);
    function innerFunction(){
        console.log("innerFunction's this : ", this); // window 에 바인딩 된다.
        // 생성자 함수의 내부 함수 innerFunction 의 this 도 전역 객체에 바인딩된다.
    }
    innerFunction();
}
constructor();

myobj = new constructor();

위의 세 가지 경우와 같이 내부 함수에서의 this 키워드로 인해 전역 객체 또는 변수에 값이 바인딩 되는 문제를 해결해줄 세 가지 키워드가 있다.

바로 apply / call / bind 이다.

 

* call 과 apply

아래의 코드를 살펴보자.

const obj = {name : 'Tom'};

const say = function(city){
    console.log('Hello, my name is ' + this.name + 'I live in ' + city);
}

say("seoul");
say.call(obj, "seoul");
say.apply(obj, ["seoul"]);

-> Hello, my name is , I live in seoul

-> Hello, my name is Tom, I live in seoul

-> Hello, my name is Tom, I live in seoul

 

call 과 apply 는 함수를 호출하는 함수이다. 그러나 그냥 실행하는 것이 아니라 첫번째 인자에 this로 setting 하고 싶은 객체를 넘겨주어 this 를 바꾸고 나서 실행한다.

 

첫번째 실행인 say("seoul") 의 경우는 say 가 실행될 때 this 에 아무런 setting 이 되어있지 않으므로 this는 window 객체이다.

두번째 실행인 say.call(obj, "seoul"); 의 경우와 세번째 실행인 say.apply(obj, ["seoul"]) 은 this 를 obj 로 변경 시켰으므로 원하는 값이 나온다.

 

call 과 apply 의 유일한 차이점은 첫번째 인자(this 를 대체할 값)를 제외하고, 실제 say 에 필요한 입력값을 입력하는 방식이다.

call 과는 다르게 apply 함수는 두번째 인자부터 모두 배열에 넣어야 한다.

 

* bind

const obj = {name : 'Tom'};

const say = function(city){
    console.log('Hello, my name is ' + this.name + ' I live in ' + city);
}

const boundSay = say.bind(obj);
boundSay("seoul");

-> Hello, my name is Tom, I live in seoul

 

bind 함수가 call, apply 와 다른 점은 함수를 실행하지 않는다는 점이다. 대신 bound 함수를 리턴한다.

이 bound 함수 boundSay 는 이제부터 this 를 obj 로 가지고 있기 때문에 나중에 사용해도 된다.

bind에 사용하는 나머지 입력값은 call 과 apply 와 동일하다.

 

 

 

참고 : https://edu.goorm.io/learn/lecture/19879/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-javascript-es6 

 

프레임워크를 위한 JavaScript ES6 - 구름EDU

자바스크립트 기반 웹 프레임워크, 막상 시작했지만 너무 막연하게 느껴지나요? 자바스크립트로 프레임워크를 '잘' 사용하는 방법을 알아봅시다.

edu.goorm.io

참고 : https://wooooooak.github.io/javascript/2018/12/08/call,apply,bind/

 

binding의 개념과 call, apply, bind의 차이점 · 쾌락코딩

binding의 개념과 call, apply, bind의 차이점 08 Dec 2018 | javascript basic this es6 binding이란? 프로젝트 경험이 거의 없었을 때는 this를 binding한다는 말 조차 이해가 가지 않았었다. javascript기본서에서 call, app

wooooooak.github.io