카테고리 없음

javascript 2

parktest0325 2021. 4. 6. 21:51

데이터타입

자바스크립트에선 메모리 관리의 효율을 위해 변수에 값을 직접 담지 않고 주소를 담아 참조하는 방식으로 사용한다. 

런타임중 특정 시점이나 메모리가 포화되는 경우 GC가 실행되는데, 참조카운트가 0인 메모리는 사용하지 않는 것으로 간주되어 수거된다.

[ primitive type ]

number, string, boolean, null, undefined, symbol(ES6에서 추가됨)

* typeof null === 'object'

 

number 

자바스크립트는 모든 숫자를 64비트 부동소숫점 (double precision) 형태로만 표현한다. 그래서 표현할 수 있는 범위가 넓지만 정확도가 떨어진다. 

예시)

데이터가 저장되는 메모리에서 완전히 동일한 값이 있다면 해당 주소를 재활용하고 없는 경우에만 새로운 공간에 새로운 데이터를 생성하여 그 주소를 가리키게한다. 

a가 가진 값(3의 주소)이 복사되며 3 값이 저장된 메모리의 참조 카운트는 2가 된다. 

var a = 3;
var b = 3;

 

string

문자열도 마찬가지로 새로운 공간을 할당받아서 새로운 문자열을 저장한뒤 그 주소만 변수에 저장하는 개념이다.

abc는 참조카운트가 0이 되었기 때문에 GC가 실행될때 수거 대상이 된다. 

var a = 'abc';
a = 'abcdef';

 

겹치는 부분이 있더라도 새로운 문자열을 만들어서 주소를 저장하는 것은 primitive type은 immutable하기 때문이다.

var s = "abcdef";
s[0] = "b";
console.log(s);
// "abcdef"  -> 값이 변하지 않음

 

undefined

자바스크립트에서 null값과 함께 없음을 나타내는 값이다.

하지만 undefined는 자바스크립트 엔진이 아래의 경우 자동으로 undefined를 부여한다.

var a;
console.log(a);		// undefined. 값을 대입하지 않은 변수에 접근 (변수 선언시 undefined가 할당되는게 아님)

var obj = { a: 10 };
console.log(obj.b);	// undefined. 존재하지 않는 프로퍼티에 접근
var arr = [1, 2, 3];
console.log(arr[3]);	// undefined. 존재하지 않는 프로퍼티에 접근

var func = function() {}
var c = func();		// undefined. 리턴값이 없는 함수를 호출
console.log(c)		// undefined. 호출되지 않는 함수

 

값으로써 직접 할당한 undefined는 값으로 동작하기 때문에 주의해야한다. arr4[0] 은 직접 undefined로 할당해줬기 때문에 forEach에서 값으로 동작시켜 0번째 인덱스의 undefined 값까지 출력하게 된다. 

var arr2 = new Array(3);
console.log(arr2)	// [empty x 3].
console.log(arr2[0])	// undefined. 각각의 배열에 0 1 2 식별자가 선언되어있지 않음

var arr3 = [];
arr3[1] = 1;
console.log(arr3[0])	// undefined.
console.log(arr3)	// [empty, 1];

var arr4 = [undefined, 1];
console.log(arr4)	// [undefined, 1];

arr3.forEach(function (v, i) {console.log(i, v); });	// 0 undefined / 1 1
arr4.forEach(function (v, i) {console.log(i, v); });	// 1 1

 

undefined라는 값은 혼동이 올 수 있기 때문에 비어있음을 명시적으로 지정하고 싶을땐 null을 사용하는것이 맞다.

 

[ reference type ]

Object, Array, Function, Date, RegExp, Map(ES6), WeakMap(ES6), Set(ES6), WeakSet(ES6)

레퍼런스 타입은 typeof 연산자 사용 시 function 타입을 제외하고 모두 object 타입이다. 

객체의 메모리 할당 크기도 일단은 8byte이며 그 값을 통해 접근하는 메모리에 객체의 프로퍼티가 연속적으로 정의되어 있다. 

* 객체메모리(식별자지정) → 객체를 가리키는 메모리 → 실제 객체 프로퍼티

var obj = {
  x: 3,
  arr: [3, 4, 5]
};
obj.arr = 'str';

1002 주소에 식별자를 obj1로 값이 5001주소를 가리키게 되어있다. 5001에는 객체의 프로퍼티가 각각 담긴 7103 주소를 가리키게된다. 

obj1의 프로퍼티인 arr(@7104)도 객체이기 때문에 arr의 프로퍼티가 담긴 주소가리키는 주소를 가리킨다. 

객체의 프로퍼티의 값이 변경되었을때 객체가 직접적으로 가리키는 주소는 변경되지 않고, 프로퍼티 메모리 영역의 값이 새로운 주소로 변경될 뿐이다. 

GC가 실행된다면 @5003은 현재 아무도 가리키지 않아 참조카운트가 0이되었기 때문에 수거되고, 연쇄적으로 5003이 가리키던 주소인 8104~6까지도 전부 카운트가 0이되며 수거된다.

 

값의 복사 및 변경

보통 기본데이터 타입은 값을 복사하고, 참조형은 주소를 복사한다고 한다. 하지만 사실은 모두가 주소를 복사한다. 

var a = 10;
var b = a;
b = 20;

a 의 주소가 메모리에 할당되고 숫자 10을 가리킨다. b의 주소도 할당된 이후 a와 동일한 주소를 가리키게된다. 

이후 메모리에 20이라는 숫자가 올라가고 그 주소를 b가 가리키는 방식으로 변경된다.

 

오브젝트가 복사된 경우에도 오브젝트의 주소(@5002)가 복사되어 오브젝트의 값을 변경하면 해당 오브젝트를 가리키는 모두가 영향을 받는다. 이것은 값이 동일해서 생기는 일은 아니다. 

값이 동일한 obj3은 사실 다른 오브젝트(@5001)를 가리키기 때문에 obj2.a(@7103)을 변경해도 obj3.a(@7106)에 영향을 받지 않는다.

var obj1 = { a: 10, b: 'bbb' };
var obj2 = obj1;
var obj3 = { a: 10, b: 'bbb' };
obj2.a = 20;

console.log(obj1 === obj2);
// true
console.log(obj1 === obj3);
// false
console.log(obj1.a);
// 20
console.log(obj3.a);
// 10

 

 

 

가변 vs 불변

원시타입은 불변값이고 객체타입은 가변값이라고 한다. 이 의미는 원시타입의 변수 식별자가 가리키는 값이 변경될 수 없다는 뜻이고, 객체타입이 가리키는 객체의 프로퍼티가 변경될 수 있다는 뜻이다. 

 

객체를 그대로 복사하는 방식을 사용하면 가변성으로 인해 원본값이 변경된다. 이 방식이 필요한 경우도 있지만 불변하는 객체를 만들기 위해서는 obj3을 만드는 방식처럼 동일한 값을 가진 객체를 새로 만들듯 복사하면 된다. 이후에는 user2의 프로퍼티를 변경해도 user에는 영향을 주지 않는다.

var user = {
    name: 'kdh',
    gender: 'male'
}
var copyUser = function(user) {
    return {
        name: user.name,
        gender: user.gender
    }
}
var user2 = copyUser(user);
console.log(user === user2);
// false

user2.name = 'park';
console.log(user.name);
// 'kdh'

 

얕은복사

위의 copyUser 함수는 user라는 객체에 대해서만 복사할 수 있다. 범용적으로 사용할 수 있는 함수를 만드는게 좋다.

var copyObject = function(target) {
    var ret = {};	// 새로운 오브젝트 생성
    for(var prop in target)
        ret[prop] = target[prop];
    return ret;
}

 

깊은복사

위의 copyObject는 프로퍼티를 각각 새로만든 ret 오브젝트에 복사하기 때문에 언뜻보면 제대로 복사된것 같이 보인다.

하지만 프로퍼티로 객체가 들어있는 경우 객체의 주소가 복사되기 때문에 오브젝트의 프로퍼티객체의 프로퍼티를 변경하는 경우에는 오브젝트까지 영향을 줄 수 있다. 

var deepCopyObject = function(target) {
    var ret = {};
    if (typeof target === 'object' && target !== null) {	// typeof null === 'object'
        for(var prop in target)
            ret[prop] = deepCopyObject(target[prop]);
    } else {
        ret = target;
    }
    return ret;
}

 

JSON형식의 문자열로 변경 후 다시 파싱하면 객체프로퍼티까지 전부 복사된다. 메서드(함수)나 숨겨진 프로퍼티인 __proto__는 무시되지만 순수한 객체는 손쉽게 복사할 수 있는 장점이 있다.

var copyObjectViaJSON = function(target) {
    return JSON.parse(JSON.stringify(target));
}

 


실행 컨텍스트 (EC) 1

자바스크립트가 실행되기 위해서 변수, 스코프체인, this 가 필요함 

Global EC : 글로벌로 자바스크립트가 실행될때 실행환경. 자바스크립트는 싱글스레드라 무조건 한개만 존재할 수 있음 

Functional EC : 함수가 호출되고 실행할때 함수단위로 생성되는 실행컨텍스트. 

Eval : eval 함수에서는 특별하게 동작한다.

 

1. creation phase

1) variable object, activation object 를 생성, 식별자 정보 저장

VO : 일반 함수 실행 컨텍스트가 생성. 변수들이 담길 오브젝트

AO : 글로벌 실행컨텍스트의 VO

 

변수 선언 키워드(var, let, const)와 함수 선언문을 찾아서 VO에 저장한다.

a = 0;
var b = 1;
var cfunc = function(){};
function dfunc(e){}

// creation phase 
// activation object = { argumentobj: { length:0 }, b: undefined, cfunc: undefined, dfunc: fn() }
// scope chain = [ Global Execution Context variable object ]

 

2) scope chain 생성

함수 선언 시점 자신의 VO를 포함하여 상위 스코프의 VO를 연결하는 방식으로 체인을 구성한다.

execution phase 진행 시 자신의 스코프에서 찾지 못하면 체인을 하나씩 올라가며 글로벌이 나올때까지 검색해본다. 

글로벌에서도 해당 변수가 없으면 글로벌 VO에 식별자를 등록한다. 

 

3) this 값 바인딩

 

2. execution phase

코드가 실행되면서 값을 대입하거나 함수호출 등을 진행한다. 

a = 0;
var b = 1;
var cfunc = function(){};
function dfunc(e){}

// execution phase 
// activation object = { argumentobj: { length:0 }, a: 0, b: 1, cfunc: fn(), dfunc: fn() }
// scope chain = [ Global Execution Context variable object ]

 

이후에 cfunc 함수가 호출되면 cfunc의 creation phase -> execution phase가 진행된다. 

test는 자신의 스코프에도 없고 글로벌에도 없기 때문에 글로벌 VO에 값을 저장한다. 

var cfunc = function(e){
    test = 15;
    var efunc = function(){ };
};

cfunc(10);
console.log(test);	// 15

// creation phase 
// activation object = { argumentobj: { e: undefined, length:1 }, efunc: undefined }
// scope chain = [ cfunc variable object, Global Execution Context variable object ]

// execution phase
// activation object = { argumentobj: { e: 10, length:1 }, efunc: fn() }
// scope chain = [ cfunc variable object, Global Execution Context variable object ]

실행 컨텍스트 2

실행할 코드에 제공할 환경정보를 모아둔 객체. 코드가 실행되다가 함수가 호출되면 환경정보를 모아 실행 컨텍스트를 구성하고 콜스택에 쌓아올린뒤 이전 실행 컨텍스트 코드는 중단하고 방금 쌓은 컨텍스트와 관련된 코드를 실행하는 방법으로 코드의 순서를 보장한다. 

 

전역 컨텍스트

자바스크립트 프로그램이 실행되면 자동으로 생성된다. 함수 컨텍스트와는 다르게 arguments 변수가 없고, 외부스코프가 없기 때문에 스코프 체인에는 전역 스코프 하나만 존재한다.

전역 컨텍스트는 프로그램 실행 시 컨텍스트 객체를 생성하지 않고, 웹 브라우저는 window, 노드js는 global 과 같이 실행환경에서 제공한 객체를 사용한다. 

* eval() 은 동적으로 컨텍스트가 생성되기 때문에 다른 방식으로 동작하는 느낌이다

 

코드가 실행되다가 함수가 호출될때 엔진이 관련된 정보들을 전부 수집해서 실행컨텍스트 객체에 전부 저장한다. 

 

VariableEnvironment

실행 컨텍스트 생성 시 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부환경 정보가 스냅샷으로 저장되어 변경되지 않는다. environmentRecord(Snapshot) + outerEnvironmentReference(Snapshot)

 

 

LexicalEnvironment

VariableEnvironment를 복사해서 만든 뒤 함수가 실행되며 변경되는 사항이 계속해서 저장된다. 

 

1. environmentRecord

호출된 코드가 실행되기 전에 컨텍스트 전체를 처음부터 훑어가며 순서대로 식별자를 수집한 후 저장한다. 그렇기 때문에 자바스크립트는 코드 실행전부터 선언될 변수명을 미리 알 수 있고, 아래에서 변수가 선언돼도 최상단으로 끌어올려 해석한다.(호이스팅이라고 부름)

* arguments 는 컨텍스트가 생성될 때 함수 호출 시 전달된 인자를 전부 저장한 '유사'배열객체로, 코드 실행중 값이 변경되면 함께 변경되는 불안전한 특징이 있기 때문에 ES6부터는 rest parameter가 새로 나왔다. 

 

a함수가 호출될때 호이스팅이 발생하고 전달된 파라미터 변수까지 포함해서 식별자 선언문은 전부 맨 위로 올라간다.

* 실제로 코드가 끌어올려지는것이 아니라 environmentRecord가 컨텍스트를 훑어서 식별자를 수집하기 때문에 그렇게 보이는것이다. 

메모리 공간 두개를 할당해서 변수 c, b 를 각각 식별자를 지정하고, b에는 함수 선언문을 넣는다. 그리고 console.log를 통해 출력하고, 함수가 담겨있던 b에는 'bbb' 의 주소를 넣고 또 출력한다. 그 다음 함수 선언문은 맨 위로 올라가 있기 때문에 바로 'bbb'를 한번 더 출력하게된다.

마지막으로 함수가 종료되며 콜스택에서 제거되고 외부 코드가 계속해서 실행된다. 

function a(c) {
    console.log(b);	// [Function: b]
    var b = 'bbb';	// var b; b = 'bbb';
    console.log(b);	// 'bbb'
    function b() { }
    console.log(b);	// 'bbb'
}

// 호이스팅 후
function a() {
    var c;
    var b;
    function b() { }	// 함수 선언문
    console.log(b);	// [Function: b]
    b = 'bbb';
    console.log(b);	// 'bbb'
    console.log(b);	// 'bbb'
}

 

* 브랜든 아이크는 함수가 아래에 선언되어있어도 위에서 실행될 수 있도록 자바스크립트를 유연하게 설계했기 때문에 호이스팅이 발생하게된다. 

 

* 함수 선언문과 표현식은 다르다. 

함수 선언문은 아래에서 동일한 함수명이 있다면 호이스팅으로 위 코드에 영향을 주기 때문에 표현식으로 함수를 선언하는게 좋다. 

function a(a, b) { return a + b }	// 함수 선언문
console.log(a);		// [Function: a]. 하지만 호이스팅으로 아래에 선언된 마이너스 함수가 저장되어있다.

b();			// b is not a function. 호이스팅 해도 b변수 선언만 위로 올라감
var b = function() { }	// 익명함수 표현식
console.log(b);		// [Function: b]

var c = function d() { }// 기명함수 표현식
console.log(c);		// [Function: d]
d();			// NO. 함수 명이 d인 함수를 c변수에 저장한것. 대신 c를 출력하면 함수이름이 보인다.

function a(a, b) { return a - b; }

 

2. outerEnvironmentReference

함수가 실행될때 실행 컨텍스트가 생기면서 이 값이 세팅되어야 하는데, 이 값은 연결리스트 형태로 함수 선언 당시의 LexicalEnvironment를 참조하게된다. 그렇기 때문에 이 값을 계속 참조하다보면 모든 LexicalEnvironment 값을 접근할 수 있다. 이런 구조적인 이유로 여러 스코프에서 동일한 이름의 식별자를 사용해도 가장 먼저 발견되는 식별자에만 접근 가능하다. 

var a = 1;
var outer = function () {
    var inner = function () {
        console.log(a);
        var a = 3;
    };
    inner();
    console.log(a);
};
outer();
console.log(a);

전역 컨텍스트가 활성화된다. 컨텍스트 전체를 확인해서 environmentRecord에 { a, outer } 식별자를 저장하게되고, outerEnvironmentReference에는 아무것도 담기지 않는다. (this : 전역객체, scope: 전역)

OER -> undefined

a에는 1을 저장하고, outer에는 함수를 저장한다. 

 

outer() 함수가 호출되면서 새로운 컨텍스트에 { inner } 식별자를 저장하고 활성화된다. outerEnvironmentReference에는 outer 함수가 선언될 당시 LexicalEnvironment (전역컨텍스트)가 참조된다. (this : 전역객체, scope : outer)

OER -> [ GLOBAL, { a, outer } ]

 

inner() 함수가 호출되면서 a의 선언이 호이스팅되고 새로운 컨택스트에 { a }를 저장 후 outer 컨텍스트에 함수선언 당시의 LexEnviron를 참조하게한다. (this : 전역객체, scope : inner)

OER -> [ outer, { inner } ]

출력함수가 실행되며 inner의 컨텍스트를 뒤져 a를 찾게되고 아직 값이 지정되기 전이기 때문에 undefined가 출력된다. 

그리고 현재 컨텍스트(inner)의 a에 3을 할당 후 inner 컨텍스트가 콜스택에서 제거되며 outer 컨텍스트가 활성화된다.

 

outer에서 a에 접근하려 하는데, 현재 컨텍스트(LE)에는 a가 없기 때문에 a를 찾을때까지 OER을 참조하여 상위 스코프에 있는지 계속해서 검색해나간다. 전역 컨텍스트의 LE에는 a가 있어서 전역에 1로 설정된 a를 가지고 출력 후 outer 함수가 종료되어 콜스택에서 제거한다.

 

전역 컨텍스트가 다시 활성화되고, LE에서 a를 찾아보는데 a가 1로 설정되어있기 때문에 이 값을 출력후 전역 컨텍스트를 콜스택에서 제거한다. 

 

그렇기 때문에 전역에서 사용되어야하는 변수, 함수를 제외하고는 지역변수, 지역함수로 넣어주면 안전하다.

 

 

ThisBinding

this 식별자가 바라봐야 할 대상 객체를 가리킴. 함수를 호출한 주체에 대한 정보가 담긴다.

실행컨텍스트 활성화 당시 this가 지정되지 않은경우 글로벌객체가 저장되며, 함수를 호출하는 방식에 따라서도 저장되는 this가 다르다.

 

*변수를 선언하면 자바스크립트 엔진이 this 객체의 프로퍼티로 할당한다

var a = 1;
console.log(a);		// 1
console.log(global.a);	// 1
console.log(this.a);	// 1

 

1. 전역공간의 this

전역객체를 가리킨다.

 

2. 메서드로 호출할때 메서드 내부의 this

객체를 통해 메서드를 호출하기 때문에 . 앞이나 대괄호로 호출한 객체를 가리킨다.

var obj = {
    methodA : function() { console.log(this) },
    inner : {
        methodB : function() { console.log(this) }
    }
}
obj.methodA();
// { method A: f, inner: {...} }	=== obj

obj.inner.methodB();
obj.inner['methodB']();
obj['inner'].methodB();
obj['inner']['methodB']();
// { methodB: f }		=== obj.inner

 

3. 함수를 직접 호출할때의 this

객체를 명시하지 않고 개발자가 직접 함수를 호출한 것이기 때문에 this 가 지정되지 않아 전역객체를 가리킨다. 

 

4. 메서드 내부함수를 직접호출하는 경우

메서드 안에서 직접 호출한 innerFunc()의 경우 호출한 주체가 없기 때문에 global이 된다.

var obj1 = {
    outer : function() {
        console.log(this);
        var innerFunc = function() {
            console.log(this);
        }
        innerFunc();
        // this === global
        
        var obj2 = {
            innerMethod: innerFunc
        };
        obj2.innerMethod();
        // this === obj2
    }
}
obj1.outer();
// this === obj1

 

호출 주체가 없는경우 this를 지정하지 않는데, 변수를 사용하면 주변 환경의 this를 상속받아 사용할 수 있다. 

하지만 이렇게 호출하는 경우 객체를 이용해 호출한다 하더라도 위에서 바인딩된 this를 따라간다. 

var obj = {
    outer: function() {
        console.log(this);	// obj => obj
        var self = this;	// self가 제일 많이쓰이고 _this, that, _ 등 으로도 쓰인다. 
        var innerFunc = function() {
            console.log(self);
        }
        innerFunc();	// global => obj
        
        obj2 = {
            f: innerFunc
        };
        obj2.f();	// obj2 => obj
    }
};
obj.outer();

 

ES6부터 만들어진 화살표 함수는 this를 바인딩하지 않기 때문에 스코프체인상 가장 가까운 this 값을 가리킨다 

var obj = {
    outer: function() {
        console.log(this);	// obj
        var innerFunc = () => {
            console.log(this);
        }
        innerFunc();	// obj
        
        obj2 = {
            f: innerFunc
        };
        obj2.f();	// obj
    }
};
obj.outer();

 

5. 콜백함수에서의 this

기본적으로 전역객체를 가리키는데, 콜백함수를 사용하는 메서드마다 this를 무엇으로 할지 결정하기도 한다. 

ex) addEventListener 메서드는 콜백 함수를 호출할때 자신의 this를 상속하도록 되어있다.

document.body.querySelector('#a').addEventListener('click', function(e) {
    console.log(this);	// 메서드의 this인 '#a'를 가리킴
});

 

6. 생성자 함수에서의 this

new를 이용해 객체를 만드는 함수를 생성자라고하는데, 여기서 this는 생성자가 생성한 객체 자신을 의미한다.

 

7. 명시적인 this 바인딩 (call, apply, bind)

* call

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

var obj = {
    x: 99,
    func: function (a, b, c) {
        console.log(this.x, a, b, c);
    };
}

func(1, 2, 3);			// 99 1 2 3
func.call({ x: 1 }, 4, 5, 6);	// 1 4 5 6

 

* apply

Function.prototype.apply(thisArg[, argsArray])

func.apply({ x: 1 }, [4, 5, 6]);	// 1 4 5 6

 

* bind

코드 실행은 하지 않고, 전달한 this와 파라미터를 미리 바인딩하는 함수를 반환한다.

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

var bindFunc1 = func.bind({ x: 2 });
bindFunc1(4, 5, 6);	// 2 4 5 6

var bindFunc2 = func.bind({ x: 3 }, 7, 8);
bindFunc2(4);		// 3 7 8 4

console.log(func.name);		// func
console.log(bindFunc1.name);	// bound func (바인딩된 func함수라는 의미)

 

명시적인 바인딩을 통해 상위 컨텍스트의 this를 내부로 전달할 수 있다. 

var obj = {
    outer: function() {
        var innerFunc = function () {
            console.log(this);
        };
        innerFunc.call(this); // 현재 컨텍스트의 this를 call에 전달
    }
}

 

8. 콜백 함수와 함께 this를 인자로 받는 메서드

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.from(callback[, thisArg])
Set.prototype.forEach(callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])

 


콜백함수

함수나 메서드에 콜백 함수가 인자로 전달되며 제어권도 함께 넘기기 때문에 함수 내부적으로 일정한 동작 이후에 조건이 충족되면 스스로 콜백 함수를 실행시키게 된다.

메서드의 콜백 함수는 이미 함수와 각각의 파라미터, 가끔은 this까지 어떻게 전달하고 동작하는지 미리 정해져 있기 때문에 파라미터의 순서를 변경하거나 할 수 없다. 

 

* map 함수의 구현

배열의 각 요소에 함수를 기준으로 콜백함수를 호출한 결과값을 각각 저장한 배열을 리턴

Array.prototype.map(callback[, thisArg])

callback: function(currentValue, index, array)

var newArr = [10, 20, 30].map(function (currentVal, index) {
    console.log(index, currentValue);
    return currentValue + 5;
});

 

call 함수를 이용해 thisArg가 전달된 경우에만 콜백함수에 this를 전달해주고 아니면 global 전달, 인자로 배열의 현재 값, 인덱스, 현재 배열이 전달된다. 배열오브젝트를 통해 호출했기 때문에 this는 배열오브젝트가 된다. 

Array.prototype.map = function (callback, thisArg) {
    var mappedArr = [];
    for (var i = 0; i < this.length; i++) {
        var mappedValue = callback.call(thisArg || global, this[i], i, this);
        mappedArr[i] = mappedValue;
    }
    return mappedArr;
};

 

* 콜백함수 내부의 this에 다른 값 바인딩하기

내부에서 this는 기본적으로 global이나 자체 바인딩된 인스턴스가 되지만, 밖에서 콜백함수를 전달할때 this를 지정해줄 수 도 있다. 

obj1을 통해 func를 호출했기 때문에 this는 obj1이되고 self에 obj1의 주소가 담긴다. 그리고 그 주소의 name을 콘솔에 출력하기 때문에 callback 함수 호출시 obj1이 출력된다.

obj2.func에는 obj1.func의 코드의 주소가 담겨있기 때문에 obj2.func()를 호출하면 self에는 obj2가 담기게 되고, 출력하는 함수가 리턴된다.

callback의 self와 callback2의 self는 다른 주소이기 때문에 callback() 함수를 다시 호출해도 obj1이 출력된다. 

var obj1 = {
    name: 'obj1',
    func: function() {
        var self = this;
        return function() {
            console.log(self.name);
        };
    }
};
var callback = obj1.func();
callback();	// obj1

var obj2 = {
    name: 'obj2',
    func: obj1.func	// obj1.func의 코드를 복사
};
var callback2 = obj2.func();
callback2();	// obj2
callback();	// obj1

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);	// call 메서드를 이용해 this를 직접 전달
callback3();	// obj3

 

bind 함수를 사용하면 더 쉽게 함수에 this를 바인딩할 수 있다.

var obj1 = {
    name: 'obj1',
    func: function() {
        console.log(this.name);
    }
};
var callback = obj1.func.bind(obj1)
setTimeout(callback, 1000);

 

* 콜백 지옥

콜백함수를 익명으로 전달하는 과정이 반복되어 코드 들여쓰기 수준으로 감당이 안되는 현상

웹의 기능이 복잡해지면서 비동기적인 코드가 콜백으로 포함되는 경우가 많아졌기 때문에 발생한다.

setTimeout(function(p) {
    console.log('a', p);
    setTimeout(function(p) {
        console.log('b', p);
        setTimeout(function(p) {
            console.log('c', p);
        }, 500, 'tset1');
    }, 500, 'test2');
}, 500, 'test3');

 

간단한 방법은 콜백함수를 모두 기명함수로 전환하게되면 콜백 지옥에서 빠져나올 수 있다.

var afunc = function(p){
    console.log('a', p);
    setTimeout(bfunc, 500, 'test2');
};
var bfunc = function(p){
    console.log('b', p);
    setTimeout(cfunc, 500, 'test1');
};
var cfunc = function(p){
    console.log('c', p);
};
setTimeout(afunc, 500, 'test3');

 

비동기 작업을 동기적으로 표현하는 함수들을 이용해도된다.

Promise, Generator, async/await 등이 있다. 

 


클로저

내부함수가 외부함수의 LE(스코프)에 접근할 때 발생하는 외부함수의 LE가 GC의 수집대상에서 제외되는 현상을 의미한다. 외부함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달하는 경우 A는 실행컨텍스트가 종료된 이후에도 변수 a를 포함한 LE가 사라지지 않는다. 

 

자바스크립트에서는 사용하지 않으면 가비지컬렉터가 수거해간다. outer함수가 실행되면서 실행 컨텍스트가 생성되고 a와 inner 라는 변수가 저장된다.

이후 inner함수가 실행되며 실행 컨텍스트가 생성되고 outerEnvironmentReference에 outer의 LE가 복사된다.

inner의 LE에는 a가 없기때문에 OER(outer의 LE)에 접근해보니 a가 있어서 1을 증가시키고 그 값이 복사되어 리턴된다. 

가비지 컬렉터는 리턴될때 a의 값이 이미 복사됐기 때문에 inner와 outer의 실행 컨텍스트의 내용을 전부 수거해간다. 

var outer = function() {
    var a = 1;
    var inner = function() {
        return ++a;
    };
    return inner();
};
var outer2 = outer();
console.log(outer2);	// 2
outer2 = outer();
console.log(outer2);	// 2

 

inner 함수를 리턴하는 방식으로 만들면 outer함수가 호출이 종료된 이후에도 inner함수를 호출할 수 있게 되고, inner 함수의 스코프는 outer함수의 LE를 참조하기 때문에 가비지 컬렉터는 outer 함수의 LE는 가비지 컬렉터의 수집 대상에서 제외된다. 

그렇기 때문에 outer 함수의 a 변수는 계속해서 살아있게 되고 inner 함수를 호출할때마다 같은 a 의 값을 증가시킨다.

* outer의 실행 컨텍스트가 계속 스택에 남아있는게 아니라 실제 데이터가 담긴 메모리 주소만 유지되는 것이다. 그래서 스코프 체인도 유지되고 글로벌 스코프까지 접근할 수 있게 되는 것이다. 

* 2019 버전부터는 실제 내부에서 사용하는 변수만 제외하고 전부 GC 수집대상이 된다. 

var outer = function() {
    var a = 1;
    var inner = function() {
        return ++a;
    }; 
    return inner;
};
var outer2 = outer();
console.log(outer2());	// 2
console.log(outer2());  // 3

 

리턴 뿐만이 아니라 외부객체의 콜백함수로 전달하는경우 등 다양한 이유로 외부로 전달될 수 있다.

만약 클로저를 사용하지 않으려면 임의로 outer2 변수에 null이나 undefined를 넣어서 참조카운트를 0으로 만들면 GC가 수집해간다. 

 

클로저의 활용은 다양하지만 다음에 실전 코딩할때 추가로 알아보도록 하자.

 


프로토타입

프로토타입 기반 언어에서는 어떤 객체를 원형(prototype)으로 삼고 이를 복제하는 식으로 상속과 비슷한 효과를 낸다.

 

생성자 함수를 new로 호출하면 생성자에 정의된 내용으로 instance가 생성되어 변수에 담긴다. 이때 instance에는 __proto__(dunder proto)라는 프로퍼티가 부여되며 생성자 함수의 prototype을 참조한다.

prototype은 객체인데 내부에는 인스턴스가 사용할 메서드를 저장한다. 인스턴스도 숨겨진 __proto__를 통해 이 메서드에 접근할 수 있다.

var instance = new Constructor();

console.log(Constructor.prototype === instance.__proto__)	// true

 

new키워드로 Person 인스턴스를 만들때 prototype의 주소를 suzi의 __proto__가 참조하는데 suzi.__proto__.getName() 명령으로 함수를 호출하면 __proto__객체를 주체로 .getName()을 호출한 것이기 때문에 suzi._name이 아니라__proto__.name에 접근하여 반환하게 된다.

var Person = function (name) {
    this._name = name;
};
Person.prototype.getName = function() {
    return this._name;
};
var suzi = new Person('Suzi');
suzi.__proto__.getName();	// undefined

suzi.__proto__._name = 'SUZI__proto__';
suzi.__proto__.getName();	// 'SUZI__proto__'

 

__proto__는 생략 가능한 프로퍼티라서 생략하고 메서드를 호출하면 suzi를 주체로 메서드를 호출할 수 있다.

var suzi = new Person('Suzi');
var iu = new Person('Jieun');

suzi.__proto__.getName();	// undefined
suzi.getName();		// 'Suzi'
iu.getName();		// 'Jieun'

 

__proto__가 prototype을 참조하는 것이기 때문에 new로 생성한 이후에 생성자의 prototype에 프로퍼티를 추가해도 __proto__에서 접근할 수 있다.

var suzi = new Person('Suzi');
suzi.getName();		// 'Suzi'

Person.prototype.age = 10;
suzi.age;		// 10

 

배열 변수를 만들게되면, Array.prototype 을 참조해서 만들어지게 된다. 그렇기때문에 prototype에 정의되어 있는 forEach 메서드는 사용할 수 있지만, isArray같이 Array의 본체에 정의되어있는 메서드는 사용할 수 없다. 

* Array.isArray();    => prototype 프로퍼티에 정의된게 아니기때문에 생성자함수에서 직접 접근해야한다

* Array.prototype.forEach();

var arr = [1, 2];
arr.forEach(function () {});	// 사용가능
Array.isArray(arr);		// true 사용가능
arr.isArray();			// TypeError: arr.isArray is not a function 사용불가

 

prototype 접근방법

[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

 

constructor 프로퍼티

생성자 함수를 참조하고 있기때문에 원형이 무엇인지 파악하는데 중요하다.

읽기전용 속성이 부여된 기본 리터럴 변수(number, string, boolean)를 제외하고는 값을 변경할 수 있다.

constructor를 변경하더라도 이미 만들어진 인스턴스의 원형이 바뀌진 않는다.

var test = {};
test.constructor = Array;
console.log(test instanceof Array);	// false

 

 

메서드 오버라이드

prototype에 메서드가 정의되어 있어도 객체 자체에 직접 정의한경우 객체의 getName에 접근한다.

call 메소드를 이용하여 __proto__의 메소드를 호출하고 호출 주체(this)를 변경하면 된다.

var Person = function(name){
    this.name = name;
};
Person.prototype.getName = function() {
    return '나는 ' + this.name;
};

var iu = new Person('지금');
iu.getName = function () {
    return '바로 ' + this.name;
};

console.log(iu.getName());			// '바로 지금'
console.log(iu.__proto__.getName());		// '나는 '  (__proto__에는 name이 없다.)
console.log(iu.__proto__.getName.call(iu));	// '나는 지금'  (__proto__의 getName을 iu를 주체로 호출)

 

 

프로토타입 체인

__proto__가 생성자의 prototype을 참조하는데, 배열 같은 경우에는 오브젝트를 다시 참조하게된다. 

[1, 2] 라는 배열 객체는 Array 생성자의 prototype을 참조하고, Array.__proto__ 객체는 Object.prototype을 참조한다.

어떤 객체의 메서드를 호출하면 자바스크립트 엔진은 객체의 프로퍼티를 검색해서 메서드를 확인해보고, 없으면 __proto__를 검색해서 메서드를 찾는 방식으로 동작하기 때문에 오버라이드가 발생하게된다. 

배열 뿐만이 아니라 Number, String, Boolean같은 타입들의 __proto__도 전부 오브젝트의 prototype을 참조한다.

또한 이 prototype에만 있는게 아니라 사실 Array나 Object도 생성자 함수이기 때문에 Function생성자를 참조한다.

 * 예외적으로 Object.create 메서드를 이용하면 __proto__ 프로퍼티가 없는 객체를 생성할 수 있다.

 

 

Object 전용 메서드

프로토타입 체인에 의해 모든 데이터타입의 최상위 prototype은 Object 타입이기 때문에 Object 전용 메소드를 만들려고 할때 Object.prototype.method 처럼 프로토타입에 정의하면 모든 데이터타입이 접근 가능하게 된다.

이를 방지하기 위해서 Object 전용 메소드는 prototype이 아닌 Object에 직접 정의되어있고 this를 지정할 수 없기 때문에 인자로 직접 this를 지정하는 방식으로 만들어져 있다.

Object.freeze(instance);
// instance.freeze(); 를 정의하려면 Object.prototype에 정의해야 됐기 때문에 이런방식은 사용 불가 

 


클래스

자바스크립트는 원래 상속의 개념이 없는 프로토타입 기반 언어였는데, ES6에서 클래스 문법이 추가되었다.

어떻게보면 [1, 2] 배열은 Array의 prototype을 상속받은것과 동일하게 동작하기 때문에 클래스의 개념과 같다고 볼 수 있다. 이때 prototype에 정의되어 인스턴스에 상속되는 메서드는 프로토타입 메서드(인스턴스 메서드)라고 불리고, Array에 직접 정의되어 상속되지 않는 멤버는 스태틱 메서드라고 불린다.

 

prototype이 클래스를 대체할 수 없는 이유

g.length 메소드를 지우게되면 프로토타입 체인에 의해 g.__proto__.length를 호출하게 된다. 하지만 g.__proto__ 는 빈 배열이 정의되어 있기 때문에 length 결과값이 0이 되고, push를 했기 때문에 0번째 인덱스에 70이 담기며 length 값은 1이 된다.

var Grade = function () {
    var args = Array.prototype.slice.call(arguments);
    for (var i = 0; i < args.length; i++)
        this[i] = args[i];
    this.length = args.length;
};
Grade.prototype = [];
var g = new Grade(100, 80);

g.push(90);
console.log(g);	// Grade { 0: 100, 1: 80, 2: 90, length: 3 }   push메서드는 배열의 맨뒤에 추가한다.

delete g.length;// g에서 length 메소드를 삭제
g.push(70);
console.log(g);	// Grade { 0: 70, 1: 80, 2: 90, length: 1 }    push한 값이 맨앞에 추가됨

 

prototype에 빈 배열이 아닌 4개 인자가 미리 들어간 배열을 만들게되면, __proto__.length() 는 4를 반환하게되고 5번째 칸(4번쨰인덱스)에 값을 push하게 된다.

이렇게 prototype이 가지고 있는 데이터가 인스턴스에 영향을 줄 수 있기 때문에 완전히 클래스를 대체할 순 없다.

Grade.prototype = ['a', 'b', 'c', 'd'];
var g = new Grade(100, 80);
g.push(90);
console.log(g);	// Grade { 0: 100, 1: 80, 2: 90, length: 3 }    아직까진 정상적으로 동작한다.

delete g.length;
g.push(70);
console.log(g);	// Grade { 0: 100, 1: 80, 2: 90, ____ 4: 70, length: 5 }

 

사실 클래스를 지원하기 전까지 많은 시도끝에 prototype에 구체적인 데이터를 지니지 않게 하는 코드 작성도 시도됐었고, 여러 방법으로 클래스와 비슷하게 만들게 되었다. 궁금하다면 코어자바스크립트 190페이지쯤을 보면된다.

 

ES5와 ES6의 클래스 비교

클래스 문법 내부에서는 function을 제외하더라도 메서드로 인식한다. 

메서드와 메서드 사이는 콤마로 구분하지 않는다.

static 메서드는 static 키워드로 구분할 수 있고, 생성자 자신만 호출 가능하다.

var ES5 = function (name) {
    this.name = name;
};
ES5.staticMethod = function() {
    return this.name + ' staticMethod';
};
ES5.prototype.method = function() {
    return this.name + ' method';
};
var es5Instance = new ES5('es5');
console.log(ES5.staticMethod());
console.log(es5Instance.method());

var ES6 = class {
    constructor (name) {
        this.name = name;
    }
    static staticMethod() {
        return this.name + ' staticMethod';
    }
    method () {
        return this.name + ' method';
    }
};
var es6Instance = new ES6('es6');
console.log(ES6.staticMethod());
console.log(es6Instance.method());

 

Square 클래스는 Rectangle 클래스를 상속받기 위해 extends 키워드를 사용

constructor 내부에서는 super 키워드를 함수(부모의 생성자를 의미)처럼 사용할 수 있다. 

constructor 이외의 메서드에서는 super 키워드를 객체(Rectangle.prototype)처럼 사용할 수 있다. 이때 this는 Rectangle이 아닌 Square의 인스턴스를 가리킨다.

var Rectangle = class {
    constructor (width, height) {
        this.width = width;
        this.height = height;
    }
    getArea () {
        return this.width * this.height;
    }
};
var Square = class extends Rectangle {
    constructor (width) {
        super(width, width);
    }
    printArea () {
        console.log('size is :', super.getArea());
    }
};