Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

보근은 참고 있다

함수 호출과 this 본문

Language/JS

함수 호출과 this

보근 2021. 8. 20. 15:00

 

 

 

 

 

 

 

JS의 함수 호출은 C/C++ 등의 엄격한 문법 체크를 하는 언어들과 달리 자유롭다.

 

 

 

arguments 객체

JS는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도, 에러가 발생하지 않는다.

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

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

함수에 정의된 파라미터가 들어오지 않으면 그냥 undefined로 할당해버린다. 반대로 함수에 정의된 인자의 개수보다 더 많은 인자가 들어오면 오류를 내지 않고 그 인자 값을 버려버린다.

 

작동이 멈추는 걸 최소화 하기 위한 JS의 특성 때문에 이런식으로 동작을 하지만, 런타임 시에 호출된 인자의 개수를 확인하고 그것에 따라 동작을 다르게 해줘야하는 경우도 있을 수 있다. 그것을 위한 것이 arguments 객체이다.

 

arguments 객체는 함수가 호출될 때, 인수들과 함께 암묵적으로 같이 내부로 전달된다. arguments 객체는 함수를 호출할 때 같이 넘기는 인자들을 배열 형태로 저장한 객체이다. 하지만 실제 배열이 아닌 유사 배열 객치이다.

 

function add(arg1, arg2) {
	console.dir(arguments);
    
    return arg1 + arg2;
}

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


// result

[Arguments] {}
NaN
[Arguments] { '0': 1 }
NaN
[Arguments] { '0': 1, '1': 2 }
3
[Arguments] { '0': 1, '1': 2, '2': 3 }
3

인자가 제대로 들어가지 않은 함수 호출들은 undefined로 산술 연산을 하려고 했기 때문에 NaN가 뜬다.

 

add();
add(1);
add(1, 2);

 

add(1, 2, 3);

호출 시에 넘긴 인자들을 배열 형태로 저장하고, 그 개수를 length에 저장한다. callee는 현재 실행중인 함수의 참조값이다. 여기서는 당연하게 add(a, b) 함수를 참조하고 있다.

 

 

 

arguments 객체는 전달 받은 인자의 개수에 따라 다르게 동작해야 하거나, 매개변수의 개수가 정확히 정해지지 않은 함수를 구현할 때에 사용할 수 있다.

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

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

 

 

 

 

 

 

 

 

this 객체

함수가 호출될 때 앞서말한 arguments 객체 외에도 this 객체가 함수 내부로 암묵적으로 전달된다. this는 헷갈릴 수도 있는게 함수가 호출되는 방식에 따라서 this가 다른 객체를 참조하기도 한다. 그래서 함수 호출 패턴과 해당 패턴에 따라 this가 어떤 객체에 바인딩이 되는지를 잘 알아야 한다.

 

 

메서드

객체의 프로퍼티로 들어간 함수를 메서드라고 한다. 메서드의 this는 해당 메서드를 호출한 객체로 바인딩된다.

 

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

var otherObj = {
    name: 'bar'
};

otherObj.sayName = myObj.sayName;

myObj.sayName();		// 'foo'
otherObj.sayName();		// 'bar'

메서드에서의 this는 메서드를 프로퍼티로 갖고 있는 객체를 가리킨다. 따라서 myObj와 otherObj 두 객체가 각각의 프로퍼티인 name을 출력한다.

만약 otherObj가 name이란 프로퍼티를 갖고 있지 않다면, undefined가 출력된다.

 

 

 

 

전역 객체

전역 객체는 전역 범위에 항상 존재하는 객체를 말한다. 브라우저에서 자바스크립트를 실행하는 경우는 window 객체가 전역 객체가 된다. node.js 환경에서는 global 객체가 전역 객체가 된다.

 

js에서 전역 변수는 전역 객체의 프로퍼티들이다. 

var foo = 'I`m foo';		// 전역 변수 선언

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

 

 

함수

함수에서의 this는 전역 객체에 바인딩된다. 

var test = 'Test!!!!!!!!!!!!';
console.log(window.test);		// 'Test!!!!!!!!!!!!'

var sayFoo = function () {
    console.log(this.test);		// 'Test!!!!!!!!!!!!'
};

sayFoo();

 

 

내부 함수에서도 this 객체가 전역 객체에 바인딩된다.

 

var val = 100;

var obj = {
    val: 1,
    func1: function () {
    	console.log(`func1 this.val = ${this.val}`);
    	console.log(`func1 val = ${val}`);
        
        func2: function () {
        	console.log(`func2 this.val = ${this.val}`);
        	console.log(`func2 val = ${val}`);
            
            func3: function () {
            	console.log(`func3 this.val = ${this.val}`);
            	console.log(`func3 val = ${val}`);
            }
            
            func3();
        }
        
        func2();
    }
}

obj.func1();

결과

 

 

 

js는 내부 함수 호출 패턴을 정의해 놓지 않는다. 그렇기 때문에, 얘네를 메서드 취급하지 않고 그냥 함수로 취급하여 전역 객체에 바인딩된다. 객체의 내부에 있기 때문에, 내부 함수의 this가 객체에 바인딩 되었다고 착각할 수 있기 때문에 이 부분은 유의한다.

 

여기서 만약 내부 함수가 메서드처럼 부모 객체를 참조하길 원한다면, 메서드의 this를 다른 변수에 저장해 참조하는 방법이 있다. this를 저장하는 다른 변수는 관례상 이름을 that으로 한다.

var val = 100;

var obj = {
    val: 1,
    
    func1: function () {
        var that = this;
    
        console.log('func1 this.val = ' + this.val);

        func2 = function () {
            console.log('func2 that.val = ' + that.val);

            func3 = function () {
                console.log('func3 that.val = ' + that.val);
            };

            func3();
        };

        func2();
    }
};

obj.func1();

// result

func1 this.val = 1
func2 that.val = 1
func3 that.val = 1

 

다른 방법은 JS에서 제공하는 call과 apply 메서드 등이 있다. 이건 뒤에 나온다고 한다.

 

 

 

 

 

 

생성자 함수

JS에서 생성자 함수는 Java와 다르게 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수가 생성자 함수로 동작한다. 일반 함수와 구분하기 위해 관례상 생성자 함수로 정의된 함수는 맨 첫 글자를 대문자로 쓰도록 되어있다. 생성자 함수에서 this를 이해하려면 먼저 생성자 함수의 동작 방식을 이해해야 한다.

 

  1. 빈 객체 생성 및 this 바인딩
    1. 생성자 함수가 새로 생성할 객체를 위해 먼저 빈 객체를 하나 만든다.
    2. this는 이 빈 객체를 바인딩한다.
    3. 새로 생성된 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가ㄹ키니는 객체를 자신의 프로토타입 객체로 설정한다.
  2. this를 통한 프로퍼티 생성
    1. this를 통해 빈 객체에 동적으로 프로퍼티나 메서드를 생성한다.
  3. 생성된 객체 리턴
    1. return문이 없다면, this로 바인딩된 새로 생성된 객체가 반환된다. (return this; 와 같다.)
    2. return문이 있다면, return문이 반환하는 객체가 반환된다. (새로 생성된 객체가 아닐 수도 있다.)
var Person = function (name) {
	this.name = name;
};

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

 

만약 생성자 함수에 new를 붙이지 않고 사용하면 어떻게 될까? 결과는 원하는대로 동작해주지 않는다.

 

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

var p = Person('bogeun');

console.log(p);			// undefined
console.log(window.name);	// bogeun

new 연산자가 붙어야 this가 새로 생성되는 객체에 바인딩되고 또, 리턴문을 따로 명시하지 않아도 this를 리턴 해준다.

 

여기서는 그냥 함수로 인식이 되어서 this가 전역 객체인 window에 바인딩 되었고, 따로 리턴 값이 없었기 때문에 p는 undfined로 name 프로퍼티는 애먼 전역 객체에 붙어버렸다.

 

 

 

new 연산자를 붙이든 붙이지 않든 똑같이 동작하기 위해 이러한 패턴을 쓰기도 한다.

function A(arg) {
    if(!(this instanceof arguments.callee)) {
        return new A(arg);
    }

    this.value = arg ? arg : 0;
}

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

console.log(a);		//	100
console.log(b);		//	10
console.log(c);		//	0

this가 함수의 인스턴스인지 확인을 통해 new 연산자를 썼는지 그냥 호출했는지를 확인하고 그에 맞게 동작하도록 하여서 사용할 때 어떻게 사용해도 결과가 같게끔 해준다.

 

 

 

 

 

 

 

call과 apply를 이용한 명시적 this 바인딩

지금까지는 js엔진이 자동으로 this를 바인딩하는 것을 봤다. 하지만, 이러한 내부적인 this 바인딩 외에도 this를 특정 객체에 명시적으로 바인딩 시키는 방법도 있다. call과 apply 메서드이다.

call과 apply는 Function.prototype 객체의 메서드이므로, 모든 함수는 call과 apply를 통해 명시적인 this 바인딩이 가능하다.

 

apply 메서드는 인자를 2개 받는다. 첫 번째 인자는 this에 바인딩 할 객체를 넣고, 두 번째 인자는 원래 함수의 인자를 배열 형태로 넣어준다.

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

var person1 = {};
var person2 = {};

person1 = Person('bogeun1', 20, 'male');		// this가 전역 객체에 바인딩됨.
Person.apply(person2, ['bogeun2', 21, 'female']);	// this가 person2 객체에 바인딩 됨.

console.log(person1);		// undefined
console.log(window.name);	// 'bogeun1'
console.log(window.age);	// 20
console.log(window.gender);	// 'male'

console.log(person2);		// {name: 'bogeun2', age: 21, gender: 'female'}

apply 메서드나 new 연산자를 쓰지 않은 person1 객체는 리턴을 아무 것도 받지 못하기 때문에 undefined가 들어갔고, this 객체는 전역 객체에 바인딩 되었기 때문에 전역 변수가 되어버렸다.

 

반면에, apply 메서드를 쓴 person2는 this 객체가 새로 생성되는 객체에 바인딩 되었기 때문에, 원하는 대로 결과가 나왔다.

 

 

call은 apply랑 똑같은 기능을 한다. 차이가 있다면, apply는 인자들을 배열 형태로 한 번에 보냈고, call은 인자들을 하나하나 보낸다.

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

var person = {};

Person.call(person, 'bogeun', 22, 'male');	// this가 person 객체에 바인딩 됨.

console.log(person);		// {name: 'bogeun', age: 22, gender: 'male'}

 

 

 

 

apply와 call을 이용하면 유사 배열 객체도 배열처럼 사용할 수 있다.

function func() {
	console.log(arguments);		// [Arguments] {'0': 1, '1': 2, '2': 3}
    
    var args = Array.prototype.slice.apply(arguments);
    
    console.log(args);			// [1, 2, 3]
}

func(1, 2, 3);

Array.prototype의 slice 메서드의 this 객체가 유사 배열 객체인 arguments에 바인딩 시킨다. 이러면 유사 배열 객체가 배열인 것 마냥 배열의 메서드를 사용할 수 있다.

 

 

 

 

 

 

 

 

 

함수 리턴

JS의 함수는 항상 리턴 값을 반환한다. return 문이 명시되어 있지 않더라도, 어떠한 규칙을 통해 리턴 값을 전달한다.

 

  1. 일반 함수나 메서드는 리턴 값을 지정하지 않을 경우, undefined 값이 리턴된다.
  2. 생성자 함수에서 리턴 값을 지정하지 않을 경우, 새로 생성된 객체가 리턴된다.
var noReturnFunction = function () {
	// 명시된 return이 없으니 undefined가 리턴됨.
}

var NoReturnConstructor = function () {
	this.name = 'name';
	this.age = 'age';
    
	// 명시된 return이 없으니 생성된 객체가 리턴됨.
}

var YesReturnConstructor = function () {
	this.name = 'name';
	this.age = 'age';
    
	return {name: 'hello', age: 20};	// 명시된 return이 있으니 얘가 리턴됨.
}

var result1 = noReturnFunction();
var result2 = new NoReturnConstructor();
var result3 = new YesReturnConstructor();

console.log(result1);		// undefined
console.log(result2);		// {name: 'name', age: 'age'}
console.log(result3);		// {name: 'hello', age: 20}

 

 

 

 

 

 

 

 

 

 

 

'Language > JS' 카테고리의 다른 글

JS의 함수  (0) 2021.08.16
JS의 배열  (0) 2021.08.06
JS 데이터 타입과 연산자  (0) 2021.08.04
Comments