JavaScript

You Don't Know JS : this와 객체 프로토타입, 비동기와 성능(1)

ㅇㄱ9 2023. 3. 15. 16:56
728x90

You Don't Know JS : this와 객체 프로토타입, 비동기와 성능을 읽고 정리한 내용입니다.

====================================================================

 

함수는 this로 자기 참조를 할 수 없다. 

function foo(num) {	
	console.log("foo : " + num);
	this.count++;
}

foo.count = 0;
let i;
for(i=0; i<10; i++){
	if(i>5){
		foo(i)
	}
}
// foo:6
// foo:7
// foo:8
// foo:9
console.log(foo.count); //0

foo.count =0을 하면 foo라는 함수객체에 count프로퍼티가 추가된다. 하지만 this.count에서 this는 함수 객체를 바라보는 것이 아니며, 프로퍼티명이 똑같아 헷갈리지만 근거를 둔 객체가 다르다. 

 

함수가 내부에서 자신을 참조할 때 일반적으로 this만으로는 부족하며 렉시컬 식별자(변수)를 거쳐 함수 객체를 참조한다. 

function foo(){
	foo.count = 4; //foo는 자기자신
}

setTimeout(function(){
//익명 함수는 자기자신을 가리킬 방법이 없다. 
},10);

 call함수를 이용해 foo 함수 객체를 직접 가리키도록 강제하는 것도 방법이다. 

function foo(num) {	
	console.log("foo : " + num);
	this.count++;
}

foo.count = 0;
let i;
for(i=0; i<10; i++){
	if(i>5){
    	// call() 함수로 호출하므로 this는 이제 확실히 함수 객체 foo자신을 가리킨다. 
		foo.call(foo,i)
	}
}
// foo:6
// foo:7
// foo:8
// foo:9
console.log(foo.count); //4

this는 어떤 식으로도 함수의 렉시컬 스코프를 탐조하지 않는다. 

내부적으로 스코프는 별개의 식별자가 달린 프로퍼티로 구성된 객체의 일종이나 스코프 '객체'는 자바스크립트 구체인 '엔진'의 내부 부품이기 때문에 일반 자바스크립트 코드로는 접근하지 못한다. 

function foo() {
	var a = 2;
	this.bar();
}

function bar(){
	console.log(this.a);
}

foo(); //참조 에러 : a는 정의되지 않았습니다.(RefferenceError: a is not defined)

: 렉시컬 스코프 안에 있는 뭔가를 this레퍼런스로 참조하는 것은 애당초 가능하지 않다. 

 

this는 작성시점이 아닌 런타임 시점에 바인딩 되며 함수 호출 당시 상황에 따라 콘텍스트가 결정된다. 함수 선언 위치와 상관없이  this 바인딩은 오로지 어떻게 함수를 호출했느냐에 따라 정해진다. 

this는 실제로 함수 호출 시점에 바인딩 되며 무엇을 가리킬지는 전적으로 함수를 호출한 코드에 달렸다. 

 

this바인딩의 개념을 이해하려면 먼저 호출부, 즉 함수 호출(선언이 아니다) 코드부터 확인하고 'this가 가리키는 것'이 무엇인지 찾아봐야 한다. 

호출부를 꼼꼼히 살펴보고 다음에 열거할 4가지 규칙 중 어느 것이 해당하는지 확인한다. 

 

1) 기본 바인딩 

단독 함수 실행에 관한 규칙으로 나머지 규칙에 해당하지 않을 경우 적용되는 this의 기본 규칙이다.

function foo() {
	console.log(this.a);
}

var a=2;
foo(); //2

foo() 함수 호출 시 this.a는 전역객체 a다. 기본 바인딩이 적용되어 this는 전역객체를 참조한다. 

strict mode에서는 전역객체가 기본 바인딩 대상에서 제외된다. 그래서 this는 undefined가 된다. 

 

2) 암시적 바인딩

두번째 규칙은 함수 호출부에 콘텍스트 객체가 있는지, 즉 객체의 소유/포함 여부를 확인하는 것이다. 

function foo(){
	console.log(this.a);
}

var obj = {
	a:2,
	foo: foo
}

obj.foo(); //2

앞에서 선언한 foo() 함수를 obj에서 프로퍼티로 참조하고 있다. foo()를 처음부터 foo프로퍼티로 선언하든 이 예제처럼 나중에 레퍼런스로 추가하든 obj객체가 이 함수를 정말로 '소유'하거나 '포함'한 것은 아니다. 그러나 호출부는 obj콘텍스트로 foo()를 참조하므로 obj객체는 함수 호출 시점에 함수의 레퍼런스를 '소유'하거나 '포함'한다고 볼 수 있다. 

 

foo() 호출 시 obj는 this이니 this.a는 obj.a가 된다. 

 

다음 예제처럼 객체 프로퍼티 참조가 체이닝 된 형태라면 최상위/최하위 수준의 정보만 호출부와 연관된다. 

function foo() {
	console.log(this.a);
}
var obj2 = {
	a:42,
	foo: foo
}
var obj1 = {
	a:2,
	obj2:obj2
};

obj1.obj2.foo(); //42

 

3) 암시적 소실

'암시적으로 바인딩 된' 함수에서 바인딩이 소실되는 경우가 있는데 this 바인딩이 헷갈리기 쉬운 경우다. 엄격 모드 여부에 따라 전역객체나 undefined 중 한 가지로 기본 바인딩 된다. 

function foo() {
	console.log(this.a);
}

var obj = {
	a:2,
	foo:foo
};

var bar = obj.foo; 
var a = "전역이네!"
bar(); //"전역이네!"

bar는 obj의 foo를 참조하는 변수처럼 보이지만 실은 foo를 직접 가리키는 또 다른 레퍼런스다. 

게다가 호출부에서 그냥 평범하게 bar()를 호출하므로 기본 바인딩이 적용된다. 

 

콜백함수를 전달하는 경우엔 좀 더 애매하게 실행되어 예상외의 결과가 나온다. 

function foo(){
	console.log(this.a);
}

function doFoo(fn){
//fn은 foo의 또 다른 레퍼런스일 뿐이다. 
	fn();
}

var obj = {
	a:2,
	foo: foo
};

var a = "전역";
doFoo(obj.foo); //전역

인자로 전달하는 건 일종의 암시적인 할당이다. 따라서 예제처럼 함수를 인자로 넘기면 암시적으로 레퍼런스가 할당되어 예전과 결과가 같다. 

 

명시적 바인딩

: call()과 apply()메서드를 이용하여 명시적으로 어떤 객체를 this바인딩에 이용하겠다는 의지를 코드에 밝히는 바인딩.

두 메서드는 this에 바인딩 할 객체를 첫째 인자로 받아 함수 호출 시 이 객체를 this로 세팅한다. 

 

하지만 이렇게 명시적으로 바인딩 해도 앞에서 언급한 this바인딩이 도중에 소실되거나 프레임워크가 임의로 덮어 버리는 문제는 해결할 수 없다. 

 

하드 바인딩

function foo(){
	console.log(this.a);
}

var obj = {
	a:2
};

var bar = function() {
	foo.call(obj);
};

bar(); //2
setTimeout(bar,100); //2

//하드 바인딩 된 'bar'에서 재정의된 this는 의미가 없다.
bar.call(window); //2

bar를 어떻게 호출하든 이 함수는 항상 obj를 바인딩하여 foo를 실행하는데 이런바인딩을 '하드 바인딩'이라고 한다. 

하드 바인딩으로 함수를 감싸는 형태의 코드는 다음과 같이 인자를 넘기고 반환 값을 돌려받는 창구가 필요할 때 주로 쓰인다.

function foo(something) {
	console.log(this.a, something);
	return this.a + something;
}

var obj = {
	a:2
};

var bar = function(){
	return foo.apply(obj, arguments);
};

var b = bar(3); // 2 3
console.log(b); // 5

재사용가능한 헬퍼함수 (Reusable Helper)를 쓰는 것도 같은 패턴이다. 

function foo(something){
	console.log(this.a, somrthing);
	return this.a + something;
}

//간단한 bind 헬퍼
function bind(fn, obj){
	return function() {
		return fn.apply(obj, arguments);
	};
}

var obj = {
	a: 2
};
var bar = bind(foo, obj);
var b = bar(3); // 2 3
console.log(b); //5

new 바인딩

자바스크립트 생성자는 앞에 new연산자가 있을 때 호출되는 일반 함수에 불과하다. 클래스에 붙은 것도 아니고 클래스 인스턴스화 기능도 없다.

 

예를 들어 생성자 Number()  함수는 ES5.1명세에 'new 표현식의 일부로 호출 시 Number는 생성자이며 새로 만들어진 객체를 초기화한다.' 

 

함수 앞에 new를 붙여 생성자 호출을 하면 다음과 같은일들이 저절로 일어난다. 

1) 새 객체가 만들어진다. 

2) 새로 생성된 객체의 [[Prototype]]이 연결된다. 

3) 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩 된다. 

4) 이함수가 자신의 또 다른 객체를 반환하지 않는 한 new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다. 

function foo(a){
	this.a = a;
}

var bar = new foo(2);
console.log(bar.a); //2

앞에 new를 붙여 foo()를 호출했고 새로 생성된 객체는 foo 호출 시 this에 바인딩된다. 따라서 결국 new는 함수 호출 시 this를 새 객체와 바인딩하는 방법이며 이것이 'new 바인딩'이다.

 

바인딩 규칙 우선순위

명시적 바인딩 > 암시적 바인딩 > 기본 바인딩 순인건 쉽게 짐작할 수 있으니 new바인딩을 살펴보자.

ES5에 내장된 Function.prototype.bind() 함수를 살펴보면 하드바인딩 함수가 new로 호출되어 this가 새로 생성된 객체로 세팅됐는지 조사해 보고 맞으면 하드바인딩에 의한 this를 버리고 새로 생성된 this룰 대신 사용한다. 

 

굳이 new로 하드바인딩을 오버라이드 하려는 이유는??

기본적으로 this 하드 바인딩을 ㅜ시하는 함수를 생성하여 함수인자를 전부 또는 일부만 미리 세팅할때 유용하다.

 

즉, new바인딩 > 명시적 바인딩 > 암시적 바인딩 > 기본 바인딩

 

바인딩 예외

1) this 무시

call, apply, bind 메서드에 첫번째 인자로 null 또는 undefined를 넘기면 this바인딩이 무시되고 기본 바인딩 규칙이 적용된다. 

function foo() {
	console.log(this.a);
}
var a = 2;
foo.call(null); //2

왜 null 같은 값으로 this바인딩을 하려는 걸까? 

apply() 함수 호출시 다수의 인자를 배열값으로 펼쳐보내는 용도로 쓸 수 있다. (ES6이상에서는 spread연산자 사용)

bind()도 유사한 방법으로 인자들을 커링하는 메서드로 많이 사용한다. 

function foo(a,b){
	console.log("a:" + a +", b:" +b ); 
}

//인자들을 배열 형태로 펼친다
foo.apply(null, [2,3]); // a:2, b:3

//bind()로 커링한다.
var bar = foo.bind(null,2);
bar(3); //a:2, b:3

하지만 이런방법을 사용할 경우 (서드파트 라이브러리와 같은) 고칠 수 없는 함수 호출 시 null을 전달했는데 마침 그 함수가 내부적으로 this를 레퍼런스로 참조하면 기본바인딩이 적용되어 전역변수(브라우저는 window)를 참조하는 예기치 못한 일이 일어날 수 있다. 

 

더 안전한 방법으로는 프로그램에서 부작용과 100% 무관한 객체를 this로 바인딩하는게 좋다.

Object.create(null)로 빈객체를 만들어 전달하자. 이렇게 생성된 객체는 {}와 비슷하나 Object.prototype을 위임하지 않으므로 {}보다 더 텅 빈 객체이다.  

 

화살표 함수

 

function foo(){
	//화살표 함수를 반환한다.
	return (a) => {
		//여기서 'this'는 어휘적으로 'foo()'에 상속된다.
		console.log(this.a);
	};
}

var obj1 = {
	a:2
};

var obj2 = {
	a:3
};

var bar = foo.call(obj1);
bar.call(obj2); // 2, 3이 아니다!

foo() 내부에서 생성된 화살표 함수는 foo() 호출 당시 this를 무조건 어휘적으로 포착한다. 

foo()는 obj1에 this가 바인딩 되므로 bar(반환된 화살표 함수를 가리키는 변수)의 this 역시 obj1로 바인딩된다.

화살표 함수의 어휘적 바인딩은 절대로 (심지어 new로도) 오버라이드 할 수 없다. 

 

화살표 함수는 이벤트 처리기나 타이머 등의 콜백에 가장 널리쓰인다.

728x90
반응형