실행 컨텍스트
환경 정보들을 모아놓은 객체
this
값을 설정실행 컨텍스트란?
콜 스택 : 바구니 (LIFO) -> 마지막에 들어간게 처음으로 나옴
큐 : 원통 (FIFO) -> 처음으로 들어간게 첫번째로 나감
콜 스텍
실행할 코드에 제공할 환경 정보를 모아놓은 객체
콜스텍에 쌓아올립니다. 다 실행되고 나면 없어짐
call stack
시작되자마자
전역 컨텍스트가 시작되고
outer()
가 실행되고 중단이되고 쌓인다.
그 다음에 outer
함수 내부에 inner() 함수
가 호출되는 걸 보면
outer
컨텍스트가 중단되고 inner
컨텍스트가 쌓인다.
다 실행되고 나면 inner
가 out
빠지고 outer
가 다 실행되면 out
되고
전역도 out
해서 순서대로 빠져나간다.
(코드 실행할 때 call stack
을 한번씩 그려보기)
그럼 실행 컨텍스트에는 뭐가 들어 있나?
ariableEnvironment
현재 컨텐스트 내의 식별자 정보 = record)
var a = 3이라 할 때 var a를 의미
외부 환경 정부(= outer)
(생길 때 그 모습을 그대로 간직한다 (snapshot))
LexicalEnvironment
VE
와 동일하지만 , 변경사항을 실시간으로 반영해요.
즉 실시간으로 반영되는 LE
를 쓴다. (VE와 똑같다. 실시간이나 아니냐의 차이)
ThisBinding
this
식별자가 바라봐야할 객체
VariableEnvironment
, LexicalEnvironment
VE
: 스냅샷을 유지한다 (실시간으로 바뀌지 않는다.)
LE
: 스냅샷을 유지하지 않는다 (실시간으로 변경사항을 계속 반영한다.)
VE LE
는 동일하다.
LexicalEnvironment - environmentRecord(=record)
와 호이스팅 식별자 정보
들이 저장된다.
수집 대상 정보: 함수에 지정된 매개변수, 식별자, 함수 자체 , var로 선언된 변수 식별자 등
(수집이 된다는건 호이스팅이 된다와 똑같다.)
컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집
순서대로 수집한다고 했지 코드가 실행한다고 하지는 않았다.
호이스팅 식별자 정보를 수집할 때 이 호이스팅 개념이 나오고
이건 가상 개념이다 그저 이해하기 쉽게 설명하려고 만든
호이스팅 규칙
호이스팅이 식별정보를 맨 위로 낚아 끌어 올리는걸 말함
식별자 정보만 위로 다 끌어올리는데 이게 레코드를 수집하는 과정을 의미한다.
법칙1. 매개변수나 변수는 선언부를 호이스팅 한다.
이 개념을 예제를 보면 예상한대로 값이 안나온다. (3주차 호이스팅)
function a () {
console.log(b);
var b = 'bbb';
console.log(b);
function b() { }
console.log(b);
}
예시로 위에서부터 순서대로 읽으면
첫 콘솔은 underfined
, 두번째는 bbb
, 세번째는 function
이 나와야 하는데
function a () {
var b; // 변수 선언부 호이스팅
function b() { } // 함수 선언은 전체를 호이스팅
console.log(b);
b = 'bbb'; // 변수의 할당부는 원래 자리에
console.log(b);
console.log(b);
}
실제로는 이렇게 호이스팅 과정을 거쳐 나온다는 것이다.
첫 콘솔은 function 두번째는 bbb , 세번째도 bbb
그러나 함수라고해서 모두 호이스팅 되지는 않는다.
JS 엔진이 그 함수 안에서 변수들을 수집하는 과정을 호이스팅이라 불렀고,
호이스팅 과정에서 변수의 선언부 함수는 위로 끌어올려졌다는 느낌.
함수라고 해서 모두 호이스팅 되지 않는다.
함수 정의의 3가지 방식
정의부만 존재 , 할당 명령이 없는 경우가
함수 선언문이라 부른다.
별도의 변수에 함수를 할당하는게 함수 표현식이다
여기에 익명 함수 표현식과 기명 함수 표현식 두가지가 있따.
(기명 함수 표현식은 => 거의 활용이 없다.)
실행 컨텍스트는 실행할 코드에 제공할 환경 정보를 모아놓은 객체고
우리가 지금 하고 있는게 실행 컨텍스트 이다.
LE
는 record
와 outer
두가지를 보유하고 있다.
우리가 지금 하고 있는게 record
의 hoisting
이다.
함수 선언문은 전체를 위로 끌어 올린다.
(그 자체로 선언이기 때문에 올라온다.)
그러나 함수 표현식은 변수 부분만 위로 끌어 올린다.
그러나 함수 선언문을 주의해야 하는 이유는?
함수 선언문으로 짜면 hoisting에 의해서 전체 영역에 쓰였는데,
나중에 신입이 5000 번째 줄에서 10군데 쓰일 수 있는
함수 선언문을 기존에 쓰인 똑같은 이름을 사용했는데
함수 선언문의 내용이 조금 달랐는데 이게 아래에 영향을 끼치지 않고
hoisiting
에 의해 전체 코드에 영향을 미치게 시작했다.
그러나 함수 표현식이였다면?
함수 선언부만 위로 올라가기 때문에 밑에 내용이 영향을 주지 않는다.
그래서 함수 표현식을 활용하는 습관을 들이도록 합시다.
그냥 선언해야지 협업할 때 사고가 안난다고 생각하자
1. 스코프
식별자에 대한 유효범위를 의미
대부분 언어에서 존재한다. JS에서도 존재한다
2. 스코프 체인
outer는 현재 호출된 함수가 선언될 당시에 외부환경 정보를 가지고 있다.
(call stack에서 항상 처음에 저장되는건 전역 컨텍스트다.)
A가 만들어질 때 전역컨텍스트의 환경정보를 가지고 있고
여기서 B도 A와 같은 정보를 가지고 그 위에 C는 B의 정보를 가지기 때문에
동일한 LE에 대한 정보를 가지고 있다.
쉽게 설명하자면
var a = 1;
var outer = function() {
var inner = function() {
console.log(a); // 이 값은 뭐가 나올지 예상해보세요! 이유는 뭐죠? scope 관점에서!
var a = 3;
};
inner();
console.log(a); // 이 값은 또 뭐가 나올까요? 이유는요? scope 관점에서!
};
outer();
console.log(a); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!
여기서 전역 컨텍스트 밑에 깔리고
outer();
실행 되니까 2번째로 오고
inner();
실행 되니까 3번째로 온다
근데 여기서 inner()
가 실행되면서
비로소 맨 처음 실행되는 console
이 inner()
안에 console
이다
당연하게도 underfined
가 찍힌다.
왜? 밑에 var a = 3;
가 호이스팅 되면서
var a; -> console log(a) -> a = 3;
순서로 호이스팅
되기 때문에
console
의 a함수
는 inner 함수
내부에서 해결함 (스코프 체인과 상관없음)
inner()
실행이 끝나면 사라지고
다음 실행되는게 console.log(a)
인데
이 a는 inner 안의 a = 3
을 찍을 수 없다
왜냐면 이미 inner()
는 콜스텍에서 사라져버렸기 때문에...
여기서 참조한 a
는 무조건 바깥 즉 전역 영역에서 가져올 수 있는
var a = 1;
밖에 가져올 수 없다.
즉 outer();
와 var a = 1
은 똑같은 전역에 존재하고 있고,
inner()
는 outer()
안에 있기 때문에 전역의 값을 받지 못한다.
각각의 실행 컨텍스트는 LE
안에 record
와 outer
를 가지고 있고,
outer
안에는 그 실행 컨텍스트가 선언될 당시의 LE정보가 다 들어있으니
scope chain
에 의해 상위 컨텍스트의 record
를 읽어 올 수 있다.
outer
입장에서는 전역 context
가 된다.
호이스팅이랑 상관없이 변수는 자기 블럭 안을 먼저 보고
없으면 바깥으로 나가고 또 없으면 바깥으로 나가고 outer
의 개념
var
는 호이스팅
때문에 햇갈려서 더이상 안쓴다
let
과 const
는 호이스팅
이 있긴한데 에러를 비춰준다.
Javascript
에서는 this
가 독특함
어떤 상황에선 a
이기도 하고 b
이기도 하다.
상황에 따라 달라지는 this
(실행 컨텍스트를 다루고 있고, 실행할 환경정보를 모아놓은 객체)
이제 마지막과정인 ThisBindings
를 배운다.
this
는 함수를 호출할 때 결정된다.
함수 vs 메서드
(두 가지의 차이점은 독립성)
함수는 그 자체로 독집적인 기능으로 수행한다.
함수는 그 자체로 실행이 가능하고 (호출 주체없이 스스로 실행)
메서드는 자신을 호출한 대상 객체에 대한 동작을 수행하니
메서드는 종속된 친구라는 거다.(누군가 어떤 객체가 실행시켜줘야 함)
함수랑 메서드는 호출의 주체가
함수는 없고 메서드는 있기 때문에
함수는 this
가 전역 객체다
메서드는 this
가 호출의 객체다
function(1) 의 this는 node에선 global이 된다.
var obj = {
method: func,
}
obj.method(2) 의 this는 {method: f} 가 된다.
즉 호출의 주체가 있냐 없냐로 구분하는데
이 구분하는 방법이 [대괄호] 혹은 . (점)
으로 구분할 수 있다.
var obj = {
methodA: function () { console.log(this) },
inner: {
methodB: function() { console.log(this) },
}
};
obj.methodA(); // this === obj
obj['methodA'](); // this === obj
obj.inner.methodB(); // this === obj.inner
obj.inner['methodB'](); // this === obj.inner
obj['inner'].methodB(); // this === obj.inner
obj['inner']['methodB'](); // this === obj.inner
함수로서 호출할 때 그 함수 내부에서의 this (함수 자체로)
함수 내부에서의 this
는 항상 전역객체
를 가리킨다.
메서드는 호출주체가 O
고 , 함수는 호출주체가 X
다
(node 커멘드 라인이 만들어졌으면 ctrl + C , ctrl + D)
즉 function();
이 형태로 호출되면 this
는 전역객체
객체.fucntion();
이 형태로 호출되면 this
는 객체를 호출
this
대해서 이해는 하지만 이게 이해가 되나?
함수를 다 호출하다 보면 흐름이 있는건데
객체.
안했다고 this
가 전역객체를 다 보지 않고
우회할 수 있지 않나? 라고 생각한 방법들
1. 변수를 활용한 방법 내부 스코프에 이미 존재하는 this를 별도의 변수에 할당하는 방법
var obj1 = {
outer: function() {
console.log(this); // (1) { outer: ƒ }
// AS-IS
var innerFunc1 = function() {
console.log(this); // (2) 전역객체
}
innerFunc1();
// TO-BE
var self = this;
var innerFunc2 = function() {
console.log(self); // (3) { outer: ƒ }
};
innerFunc2();
}
};
메서드 호출 부분
obj1.outer();
여기서 보면 TO-BE
를 자세히 보면 되는데
let self = this;
하고 , console
에도 self
를 부르고 innerFunc2();
함수로 바로 실행하면
obj1.
의 객체부분을 콘솔에 비춰준다 outer : f
번외 개발자로 일하면
AS-IS
는 말은 이제 기존 것 , TO-BE
는 이후 것 이라는 말입니다.
우회하는 두번째 방법 바로 화살표 함수
ES6
내부에서 this
가 전역 객체를 바라보는 문제 때문에
즉 유실하는 문제 때문에 화살표 함수를 들여왔다고 해도 과언이 아니다.
this binding
여부가 가장 두드러진 차이라고 할 수 있다.
화살표 함수는this binding
자체를 생략한다 아예 없는 것임
var obj = {
outer: function() {
console.log(this); // (1) obj
var innerFunc = () => {
console.log(this); // (2) obj
};
innerFunc();
}
}
obj.outer();
실제로 this
를 강제적으로 bind
하는 방법은 명시적 바인딩이다.
다음은 콜백 함수도 함수다
별도 지정 없음 : 전역객체
setTimeout(function () { console.log(this) }, 300);
별도 지정 없음 : 전역객체
[1, 2, 3, 4, 5].forEach(function(x) {
console.log(this, x);
});
addListener
안에서의 this
는 항상 호출한 주체의 element
를 return하도록 설계되었음
따라서 this
는 button
을 의미함
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
console.log(this, e);
});
ThisBinding
하면 모두 전역 객체가 된다
그래서 콜백함수는 무조건 기본적으로 전역 객체를 무조건 바라보게 되어있다.
단! 예외는 있다! 콜백 함수에 별도로 this
를 지정한 경우
addEventListner
자신이 콜백 함수로 호출할 때 this
를 상속하게 해놈
만든 사람들이 이렇게 설계하고 제작을 함.
여기서 말하는 this
는 addEventListner
호출한 주체 즉 버튼을 바라보고 있다.
이런 예외 사항이 없다면 다 전역 객체를 바라보게 되어 있습니다.
콜백 함수도 함수기 때문에 예외사항은 없다.
구체적인 인스턴스 (어려우면 객체로 이해해도 됨)
이런걸 만들기 위한 생성자 constructor
거기 안에서의 this
var Cat = function (name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
var choco = new Cat('초코', 7); //this : choco
var nabi = new Cat('나비', 5); //this : nabi
생성자 함수는 function (name, age)
저 부분의 this
인스턴스
를 지칭하는건
name
은 초코 age는 7
을 가지는 bark는 야옹
이라는 인스턴스
를 생성하는데
this
는 그 인스턴스
를 지칭하고,
아래의 나비 와 5를 이용해서 생성하는 this는 다른 인스턴스를 생성한다.
즉 당장 이해하기 어려우면 객체로 이해하면 좋다.
var func = function (a, b, c) {
console.log(this, a, b, c);
};
// no binding
func(1, 2, 3);
이렇게 하면 this
가 글로벌 객체를 바라보고 있다.
// 명시적 this binding
// call, apply, bind
// call
var func = function (a, b, c) {
console.log(this, a, b, c);
};
// no binding
// func(1, 2, 3);
// 명시적 binding
func.call({ x: 1 }, 4, 5, 6);
func.apply({x: 1},[4,5,6]);
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
}
};
// method 함수 안의 this는 항상 obj!!
// obj.method(2, 3);
obj.method.call({ a: 4 }, 5, 6);
이렇게 call
과 apply
의 방식으로 명시적 binding
이다.
이걸 잘 활용하면 유사배열객체(array-like-object)
에 배열 메서드를 적요
배열은 index
를 가지로 다음에 length
를 가지고 있다.
유사배열의 조건
1. length
가 반드시 필요하다 없으면 유사배열
이라고 인식 안함
2. index번호
가 0번
부터 시작해서 1
씩증가해야 한다.
call 과 apply
는 즉시 실행 함수
이기 때문에
즉시 실행하면서 this binding
을 하는 this
자리에 해당 유사 배열 객체를 넣어줌으로써
해당 기능을 수행하게 하는것
그래서 this binding
앞에 유사 배열 객체를 넣어준다.
call과 apply
는 원래 this
를 위한 기능이다.
유사 배열 객체에서 사용하는 방법은 조금 벗어난 방법인데
// 유사배열
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
// 객체 -> 배열
var arr = Array.from(obj);
// 찍어보면 배열이 출력됩니다.
console.log(arr);
ES6
으로 더 좋은 방법으로 슈아배열을 사용할 수 있다.
call과 apply
의 세부적인 내용들 나머지 부분들은
튜터님 조언 메모장을 보면 좋다.
Math.
에서 펼치는게 중요한 이유는
...
이 배열의 형태로 담는걸 풀어줘야 하기 때문에 중요하다
bind에서 bound
라는 수동태
가 있다 (bind가 되었다.)
name 프로퍼티 => 함수의 이름을 추적
console.log(func.name);
console.log(bindFunc1.name);
console.log(bindFunc2.name);
이런 경우에 1. func , 2. bound func , 3. bound func
로
bind
가 사용되면 bound
로 추적할 수 있다.
이번엔 this
를 내부함수나 콜백함수로 전달하는 방법
이건 상위 컨텍스트에 전달하기 위해 self
라는 변수를 활용한
To-BE
방식을 사용했는데,
편하지만 꼭 변수를 추가해야하는 하드코딩이 되기 때문에 불편하다.
그래서 call
을 쓰거나 bind(this)
방식으로 고정해서 사용한다.
우회 call apply bind
보다 편리한 방법
그냥 화살표 함수 할당과정
이 아예 생략이 되어서
this
가 함수로써 호출되어도 전역객체로 세팅이 될 일이 없다.
var user = {
name: "john",
age: 20,
}
// 객체 만들어 프로퍼티 복사하기
var getAged = function (user, passedTime) {
var result = {};
for (var prop in user) {
result[prop] = user[prop];
}
result.age += passedTime;
return result;
}
여기서 객체를 복사하기 위해 result
이라는 빈 객체를 지정하고
for 문
에서 prop === 객체의 key , value
도 접근할 수 있다.
그러나 우린 객체를 복사하기 위해 key
에 접근해
result[prop] = user[prop];
빈객체의 key에 user에 key를 user의 key 갯수
만큼 접근해 하나씩 넣어준다.
그 다음 result
의 나이가 passedTime
만큼 먹어야 하니까
+=
는 붙이는게 아니라 값을 할당하는 것이다
그리고 그 결과값을 return
해서 돌려주면 끝이다.