본문 바로가기

Web + APP/JavaScript

JavaScript 함수와 프로토타입 체이닝 (4)

반응형
SMALL

사실 이렇게 길어질 줄은 몰랐네요.

 

계속 함수만 해서 지루하실 수도 있으시겠지만, 지금 당장 React, Vue, JQuery를 공부하는 것보다 이 내용을 알아놓으시면 분명히 도움이 될 겁니다.

 

제가 약속할게요 !

아니면 저를 죽이십시오


함수 호출과 this

1. arguments 객체

C와 달리 엄격한 언어가 아니기 때문에 JS는 함수 형식에 맞춰 인자를 넘기지 않아도 에러가 발생하지 않습니다. (이 것은 추후 큰 문제가 되는데...)

function func(arg1, arg2) {
	console.log(arg1, arg2);
}

func(); // undefined undefined
func(1); // 1 undefined
func(1, 2); // 1 2
func(1, 2, 3); // 1 2

이렇게 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해줄 수 있는 이유는 바로 arguments 객체 덕분입니다.

 

JS에서는 함수를 호출할 때 암묵적으로 arguments 객체가 함수 내부로 전달됩니다. (이 객체는 실제 배열이 아닌 유사 배열 객체입니다.

length 프로퍼티를 가진 객체를 유사 배열 객체라고 부릅니다.
function add(a, b) {
	console.dir(arguments);
    return a + b;
}

console.log(add(1)); // NaN
console.log(add(1, 2)) // 3
console.log(add(1, 2, 3)) // 3

보시면 arguments 객체는 세 부분으로 구성되어 있습니다. __proto__ 제외

 

  1. 함수를 호출할 때 넘겨진 인자 (배열 형태)
  2. length 프로퍼티
  3. callee 프로퍼티 : 현재 실행 중인 함수의 참조값

arguments는 객체이지 배열이 아니기에 push()와 같은 배열 메서드를 사용할 경우 에러가 발생합니다.

function sum() {
	var result = 0;
    
    for (var i = 0; i < arguments.length; i++) {
    	result += arguements[i];
	}
    
    return result;
}

console.log(sum(1, 2, 3));
console.log(sum(1, 2, 3, 4, 5, 6, 7, 8, 9));

위의 함수는 인자 개수에 상관없이 각각의 값을 모두 더해 리턴하는 함수인데, arguments 객체를 사용하기에 가능한 것입니다.

2. 호출 패턴과 this 바인딩

JS에선 함수를 호출할 때 인자와 더불어 arguments 객체가 전달된다고 했습니다. 이에 더해 this 라는 인자도 함수 내부로 암묵적으로 전달됩니다.

 

this 인자는 고급 자바스크립트 개발자로 거듭나려면 확실히 이해해야 하는 핵심 개념입니다.

 

this가 이해하기 어려운 이유는 자바스크립트의 여러 가지 함수가 호출되는 방식 (호출 패턴)에 따라 this가 다른 객체를 참조하기(this 바인딩) 때문입니다.

2.1 객체의 메소드 호출할 때 this 바인딩

객체의 프로퍼티가 함수일 경우, 이 함수를 메서드라고 합니다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩됩니다.

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

var otherObject = {
	name: 'bar'
};

otherObject.sayName = myObject.sayName;

myObject.sayName(); // foo
otherObject.sayName(); // bar

본 예제에서 알 수 있듯이 this는 자신을 호출한 객체에 바인딩 된다는 점을 기억하세요 !

2.2 함수를 호출할 때 this 바인딩

자바스크립트에서 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩 됩니다. JS의 경우 전역 객체는 window 객체가 됩니다.

브라우저 환경에서 JS를 실행하면, 전역 객체는 window 객체가 됩니다. node.js에서의 경우 (JS를 서버 프로그래밍을 해줄 수 있는 런타임 환경을 node js라고 합니다) global 객체가 전역 객체가 됩니다.
var foo = "I'm foo";

console.log(foo); // I'm foo
console.log(window.foo); // I'm foo

var test = 'This is test';
console.log(window.test) // This is test

var sayFoo = function () {
	console.log(this.test); // this는 전역 객체에 바인딩됩니다.
};

sayFoo(); // This is test

이러한 함수 호출에서의 this 바인딩 특성은 내부 함수를 호출했을 경우에도 그대로 적용이됩니다.

var value = 100;

var myObject = {
	value: 1,
    func1: function () {
    	this.value += 1;
        console.log(this.value);
        
        func2 = function () {
        	this.value += 1;
            console.log(this.value);
            
            func3 = function () {
            	this.value += 1;
                console.log(this.value);
			}
            
            func3();
		}
    
    	func2();
	}
};
myObject,func1();

여러분들이 생각하기에 어떤 정답이 나올 거 같나요?

2
3
4

이렇게 나올거 같나요??

 

땡 !

위의 코드 결과는 아래와 같습니다.

2
101
102

그 이유는 JS에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문입니다. 따라서 함수 호출 패턴 규칙에 따라 내부 함수의 this는 전역 객체 (window)에 바인딩 됩니다.

 

이러한 JS의 한계를 극복하려면 부모 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용됩니다.

var value = 100;

var myObject = {
	value: 1,
    
    func1: function() {
    	var that = this;
        
        this.value += 1;
        console.log(this.value);
        
        func2 = function() {
        	that.value += 1;
            console.log(that.value);
            
            func3 = function() {
            	that.value += 1;
                console.log(that.value);
			}
			func3();
		}
        func2();
	}
};

myObject.func1();

기존 부모 함수의 this를 that이라는 변수에 저장하고, 내부 변수에서는 that으로 부모 함수의 this가 가리키는 객체에 접근하게 하는 것입니다.

2.3 생성자 함수를 호출할 때 this 바인딩

자바스크립트 객체를 생성하는 방법은 크게 객체 리터럴 방식이나 생성자 함수를 이용하는 방법이 있는데, 이번엔 생성자 함수를 이용한 객체 생성 방법을 사용해보도록 합시다.

 

JS 생성자 함수는 JS의 객체를 생성합니다.

 

기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작합니다. 생성자 함수는 함수 이름의 첫 문자를 대문자로 쓰기를 권합니다.

 

JS에서 생성자 함수를 호출할 때, this는 앞서 알아본 메서드와 함수 호출 방식에서의 this 바인딩과는 다르게 동작합니다.

 

이를 이해하려면 생성자 함수가 호출됐을 때 동작하는 방식을 살펴봐야 합니다.

 

생성자 함수가 동작하는 방식

1.  빈 객체 생성 및 this 바인딩
생성자 함수 코드가 실행되기 전 빈 객체가 생성됩니다. 이 객체는 this로 바인딩 됩니다. 따라서 생성자 함수의 코드 내부에서 사용된 this는 이 빈 객체를 가리킵니다.

2. this를 통한 프로퍼티 생성
이후에는 함수 코드 내부에서 this를 사용해서, 앞에서 생성된 빈 객체에 동적으로 프로퍼티나 메서드를 생성할 수 있습니다.

3. 생성된 객체 리턴
this로 바인딩된 새로 생성한 객체가 리턴됩니다.
var Person = function (name) {
	this.name = name;
};

var foo = new Person('foo');
console.log(foo.name); // foo

Person이라는 생성자 함수를 정의하고, 이를 통해 foo 객체를 만드는 예제입니다. Person() 함수를 new로 호출하면 Person()은 생성자 함수로 동작합니다.

2.4 객체 리터럴 방식과 생성자 함수를 통한 객체 생성 방식의 차이

객체를 생성하는 두 가지 방법인 객체 리터럴 방식과 생성자 함수를 이용해서 객체를 생성하는 방식을 모두 알아봤는데, 두 가지 방법의 차이점은 무엇일까요?

 

var foo = {
	name: 'foo',
    age: 35,
    gender: 'man'
};
console.dir(foo);

function Person(name, age, gender, position) {
	this.name = name;
    this.age = age;
    this.gender = gender;
}

var bar = new Person('bar', 33, 'woman');
console.dir(bar);

var baz = new Person('baz', 25, 'woman');
console.dir(baz);

결과를 보면 아시듯, 객체 리터럴 방식과 생성자 함수 방식의 차이가 프로토타입 객체(__proto__ 프로퍼티)에 있음을 알 수 있습니다.

 

객체 리터럴 방식의 경우엔 Object(Object.prototype)를 프로토타입 객체로 가지며 함수 방식의 경우 Person(Person.prototype)으로 서로 다릅니다.

 

이 차이가 일어나는 이유는 JS 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정합니다. 객체 리터럴 방식에서는 객체 생성자 함수는 Object()이며, 생성자 함수 방식의 경우 생성자 함수입니다.

 

생성자 함수를 new를 붙이지 않고 호출할 경우

일반 함수와 생성자 함수가 별도의 차이가 없다고 말했습니다. 단지 new를 붙이면 생성자 함수로 동작할 뿐이죠.

그래서 new를 안 붙인다면 this가 window 전역 객체에 바인딩 되기에, 새로 생성되는 객체에 바인딩 되는 this를 가지는 생성자 함수 호출과 차이점이 있어서 아래와 같이 코드가 실행됩니다.
var qux = Person('qux', 20, 'man');

console.log(qux); // undefined, Person 함수의 특별한 리턴값이 없어서 이렇게 출력됩니다.

console.log(window.name); // qux
console.log(window.age); // 20
console.log(window.gender); // man
그렇기에 생성자 함수로 사용할 함수는 첫 글자를 대문자로 표현하는 네이밍 규칙을 권장합니다. 이래도 하지말라는 것을 하는 사람들 때문에 아래와 같은 코드 패턴을 사용하기도 합니다.

아래와 같이 패턴을 사용하면 함수 사용자가 함수 작성자의 의도와는 다르게 함수를 호출할 때에도 문제가 발생하지 않게 합니다. 이 패턴은 매우 광범위하게 사용되니 알아두시면 좋을거 같습니다.
function A(arg) {
	if (!(this instanceof A)) // this가 A의 인스턴스인지를 확인하는 분기문을 추가하여 this가 A의 인스턴스가 아니라면, new로 호출된 것이 아님을 의미하고, 이 경우 new로 A를 호출하여 반환합니다.
    	return new A(arg);
	this.value = arg ? arg : 0;
}

var a = new A(100);
var b = A(10);

console.log(a.value); // 100
console.log(b.value); // 10
console.log(global.value); // undefined

2.5 call과 apply 메서드를 이용한 명시적인 this 바인딩

JS에서 함수 호출이 발생할 때 각각의 상황에 따라 this가 정해진 객체에 자동으로 바인딩된다는 것을 확인했습니다. 이외에도 this를 명시적으로 바인딩시키는 방법도 제공합니다.

 

바로 apply()와 call() 메서드를 이용합니다.

 

이 메서드들은 모든 함수의 부모 객체인 Function.prototype 객체의 메서드이므로, 모든 함수는 위의 메서드를 호출하는 것이 가능합니다.

 

call() 메서드와 apply() 메서드와는 기능이 같고 단지 넘겨받는 인자의 형식만 다릅니다.

function Person(name, age, gender) {
	this.name = name;
    this.age = age;
    this.gender = gender;
}

var foo = {};

Person.apply(foo, ['foo', 30, 'man']);
console.dir(foo);

// 위의 apply() 메서드를 호출했을 때와 같은 결과를 만듭니다.
// Person.call(foo, 'foo', 30, 'man');

결국 Person 함수를 호출하면서, this를 foo 객체에 명시적으로 바인딩하는 것을 의미합니다.

3. 함수 리턴

JS 함수는 항상 리턴값을 반환합니다. return 문이 없더라도 말이죠.

3.1 일반 함수나 메서드는 리턴값을 지정하지 않을 경우, undefined 값이 리턴됩니다.

이건 꽤나 유명하니 짚지 않고 넘어가도록 하겠습니다.

3.2 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다.

생성자 함수에서 별도의 리턴값을 지정하지 않을 경우 this로 바인딩된 새로 생성된 객체가 리턴됩니다. 때문에 생성자 함수는 일반적으로 리턴값을 지정하지 않죠.

 

허나 몇 가지 예외 상황이 있습니다.

 

만약 this로 바인딩되는 생성된 객체가 아닌 다른 객체를 리턴한다면 어떻게 될까요?

function Person(name, age, gender) {
	this.name = name;
    this.age = age;
    this.gender = gender;
    
    // 명시적으로 다른 객체 반환
    return {name:'b', age:20, gender:'w'};
}

var foo = new Person('foo', 30, 'man');
console.(foo);
// {name:'b', age:20, gender:'w'}

여기서 보시면 생성된 객체가 아닌 명시적으로 지정된 객체가 리턴됨을 볼 수 있습니다.

 

그런데 만약 생성자 함수가 객체를 리턴하는게 아니라면, 불린, 숫자, 문자열의 경우엔 무시되면서 this로 바인딩 된 객체가 리턴됩니다.

function Person(name, age, gender) {
	this.name = name;
    this.age = age;
    this.gender = gender;
    
    return 100;
}

var foo = new Person('foo', 30, 'man');
console.log(foo); // {name: 'foo', age: 30, gender: 'man'}

아마 다음 글에서 JavaScript 함수와 프로토타입 체이닝의 끝을 볼거 같네요 !! 다들 화이팅

 

이상 JavaScript 함수와 프로토타입 체이닝 (4)였습니다. ^_^

반응형
LIST