본문 바로가기
JS/바닐라JS

[인사이드 JS] 함수와 프로토타입 체이닝

by 실버dev 2019. 11. 9.

1. 함수정의

자바스크립트에서 함수를 생성하는 방식은 3가지이다.

함수 선언문, 함수 표현식, Function() 생성자 함수.

 

 

1) 함수 리터럴

 

JS에서는 함수도 일반 객체처럼 값으로 취급되기 때문에 함수도 객체처럼 함수 리터럴을 이용해 생성할 수 있다.

function add(a, b) {
  return a + b
}

 함수 리터럴로 add 함수를 생성한 것이다.

 

 

2) 함수 선언문 방식으로 함수 생성하기

 

1번의 함수 리터럴로 함수를 생성하는 방식이 함수 선언문 방식이다.

함수 선언문 방식에서는 함수 이름이 반드시 필요하다.

 

 

3) 함수 표현식

 

자바스크립트에서 함수는 하나의 값으로 취급되어 숫자나 문자열처럼 변수에 할당하는 것도 가능하다.

함수 리터럴로 하나의 함수를 만들고, 여기서 생성된 함수를 변수에 할당하여 함수를 생성하는 것이 함수 표현식 방식이다.

 

const add = function(a, b) {
  return a + b
}

const add = (a, b) => {
  return a + b
} // ES6 화살표문법

여기서 함수 리터럴로 생성한 함수는 함수명이 없으므로 익명 함수이다.

 

함수 표현식 방법에서는 함수 이름이 선택사항이며, 보통 사용하지 않는다는 것이 선언문과의 차이점이다.

 

const add = function plus (a, b) {
  return a + b
}

다음과 같이 함수 표현식에서 함수 리터럴에 함수 이름을 포함시켰다.

여기서 add()는 작동하지만, plus 함수 호출의 경우 에러가 발생한다.

함수 표현식에서 사용된 함수 이름이 외부 코드에서 접근 불가능하기 때문이다.

함수 표현식에 사용된 함수 이름은 정의된 함수 내부에서 재귀 호출을 하거나 디버거 등에서 함수를 구분할 때 사용된다.

(근데 plus가 아닌 add 변수명으로 재귀 호출을 해도 잘 작동함)

 

 

4) Function() 생성자 함수를 통한 함수 생성하기

const add = new Function("a", "b", "return a + b")
console.log(add(3, 4)) // 7

 자바스크립트의 함수는 Function()이라는 기본 내장 생성자 함수로부터 생성된 객체이다.

함수 표현식과 선언문으로 생성된 함수도 내부적으로는 Function() 생성자 함수로 생성되는 것이다.

 

 

5) 함수 호이 스팅

 

함수의 3가지 생성 방식들로 생성된 함수들 사이엔 약간의 동작 차이가 있는데 그중 하나가 함수 호이 스팅임.

 

console.log(add(5, 10)) // 15

function add(a, b){
  return a + b
}

console.log(add(10, 20)) // 30

함수 선언문 형태로 정의한 함수의 유효 범위는 코드의 맨 처음부터 시작한다.

이것을 함수 호이 스팅이라고 한다.

 

 

console.log(add(5, 10)) // add is not defined

const add = function (a, b){
  return a + b
}

console.log(add(10, 20)) // 30

함수 표현식으로 함수를 생성하였을 때는 호이 스팅이 일어나지 않는다.

add함수 생성전에 add를 호출하면 에러가 발생함을 확인할 수 있다.

 

함수 호이스팅이 발생하는 원인은 자바스크립트의 변수 생성과 초기화의 작업이 분리돼서 진행되기 때문이라고 한다.

JS구루인 더글라스 크락 포드는 함수 표현식만 사용할 것을 권장한다.

 

 


2. 함수 객체 : 함수도 객체다

 

1. 자바스크립트에서는 함수도 객체다.

 

함수는 객체이므로 함수의 기본기능인 코드 실행뿐만 아니라, 함수 자체가 일반 객체처럼 프로퍼티들을 가질 수 있다.

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

minus.result = minus(5, 2)
minus.status = 'OK'

console.log(minus.result) // 3
console.log(minus) // { [Function: minus] result: 3, status: 'OK' }

예제의 코드처럼 함수가 result, status와 같은 프로퍼티를 가질 수 있다.

 

 

2. 자바스크립트에서 함수는 값으로 취급된다.

 

자바스크립트는 객체이므로 다음과 같은 동작이 가능하다.

- 리터럴에 의한 생성

- 변수나 배열의 요소, 객체의 프로퍼티 등에 할당 가능

- 함수의 인자로 전달 가능

//매개변수로 함수를 받고 함수를 리턴하기
function foo(func) {
  return func()
}

console.log(
  foo(function() {
    return "hello"
  })
) // hello

- 함수의 리턴 값으로 리턴 가능

- 동적으로 프로퍼티를 생성 및 할당 가능

 

위의 동작이 다 가능한 객체를 일급 객체라고 부름.

함수는 자바스크립트에서 일급 객체이다.

자바스크립트에서 함수가 가지는 이러한 특성으로 함수형 프로그래밍이 가능하다.

 

 

3) 함수 객체의 기본 프로퍼티

 

함수 객체는 일반 객체와는 다르게 함수 객체만의 표준 프로퍼티가 정의되어 있다.

 

ECMA5 명세서에서는 함수는 length 프로퍼티와 prototype 프로퍼티를 가져야 한다고 기술함.

그것 외에도 크롬에서 console.dir(함수)를 찍어보면 name, caller, arguments, __proto__ 프로퍼티를 확인할 수 있다. (내가 할 때는 안돼서 확인은 못했다.)

 

name 프로퍼티는 함수의 이름을 나타낸다.

 

caller 프로퍼티는 자신을 호출한 함수를 나타낸다.

 

arguments는 함수를 호출할 때 전달된 인자 값을 나타낸다.

 

__proto__는 자신의 프로토타입을 가리키는 [[Prototype]]이다.

함수는 Function.prototype 객체를 프로토타입 객체로 가진다.

 

length 프로퍼티는 함수가 정상적으로 실행될 때 기대되는 인자의 개수를 나타낸다.

function a() {
  return 0
}

function b(x) {
  return x
}

function c(x, y) {
  return x + y
}

console.log(a.length) // 0
console.log(b.length) // 1
console.log(c.length) // 2

prototype 프로퍼티

이 프로퍼티는 모든 객체의 부모를 나타내는 내부 프로퍼티인 __proto__([[Prototype]]) 과는 다르다.

[[Prototype]]은 객체 입장에서 자신의 부모 역할을 하는 프로토타입을 가리키는 반면에, 함수 객체가 가지는 prototype은 이 함수가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모역할을 할 프로토타입 객체를 가리킨다.

 

prototype 프로퍼티는 함수가 생성될 때 만들어지며 처음엔 단지 constructor 프로퍼티 하나만 있는 객체를 가리킨다.

그리고 constructor 프로퍼티는 자신과 연결된 함수를 가리킴.

자바스크립트에서는 함수가 생성될 때 함수 자신과 연결될 프로토타입 객체를 동시에 생성하며 prototype과 constructor 프로퍼티로 서로를 가리키게 된다.

 

 

 


3. 함수의 다양한 형태

 

1) 콜백 함수

 

콜백 함수는 코드를 통해 명시적으로 호출하는 코드가 아니라, 어떤 이벤트가 발생했거나 특점 시점에 도달했을 때 시스템에서 호출되는 함수를 말한다.

특정 함수의 인자로 넘겨서, 코드 내부에서 호출되는 함수 또한 콜백 함수이다.

 

 

2) 즉시 실행 함수

 

함수를 정의함과 동시에 바로 실행하는 함수를 즉시 실행 함수라고 한다.

(function(message) {
  console.log(message)
})("hello~") // hello~ 출력

함수 리터럴을 괄호로 둘러싸고 (인자) 괄호 쌍을 추가시키면 된다.

최초 한 번의 실행만을 필요로 하는 초기화 코드 부분 등에 사용할 수 있다.

 

한 번만 실행하는 코드라면 전역에서 써도 되지 않는가?

=> 자바스크립트에서는 함수 내에서 선언된 변수들은 함수 내에서만 유효한 함수 유효 범위를 가진다.

즉시 실행 함수는 전역 변수를 만들지 않고, 함수 외부에서 내부 변수를 액세스 하는 것도 불가능하기 때문에 사용함.

 

 

3) 내부 함수

 

자바스크립트에서는 함수 코드 내부에서도 함수 정의가 가능하다.

내부 함수는 클로저를 생성하거나, 부모 함수 코드에서 외부에서의 접근을 막고 독립적인 헬퍼 함수를 구현하는 용도로 사용한다.

function parent(){
  const a = 'parent a'
  const b = 'parent b'

  function child(){
    const b = 'child b'
    console.log(a)
    console.log(b)
  }
  child()
}

parent() // parent a , child b
child() // child is not defined

다음 코드로 알 수 있는 점.

- 내부 함수는 부모 함수의 변수에 접근이 가능하다.

내부 함수에서 a는 존재하지 않으므로 부모의 a를 출력했고, b는 자신의 변수를 출력함.

 

- 내부 함수는 자신이 정의된 부모 함수 내부에서만 호출이 가능하다.

함수는 함수 스코프를 가지고, 함수 스코프 밖에서는 접근이 불가능하다.

스코프 체이닝 때문에 함수 내부에서는 함수 밖에서 선언된 변수나 함수의 접근이 가능함.

하지만 클로저 방식으로는 함수 스코프 변수를 참조할 수 있음.

 

 

4) 함수를 리턴하는 함수.

 

 


4. 함수 호출과 this

 

1) arguments 객체

 

자바스크립트에서는 다른 엄격한 언어와 달리 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지 않는다.

function print(a, b){
  console.log(a, b)
}

print() // undefined undefined
print('a') // a undefined
print('a', 'b') // a b
print('a', 'b', 'c') // a b

다음 코드와 같이 인자의 수가 부족하더라도 undefined값이 할당될 뿐 에러가 발생하진 않는다.

초과된 인자는 무시한다.

 

자바스크립트의 이러한 특성 때문에 런타임 시에 호출된 인자의 개수를 확인하고 이에 따라 동작을 다르게 해야 할 경우가 있다.

이럴 때 arguments 객체를 사용한다.

 

arguments 객체는 함수를 호출할 때 넘긴 인자들이 배열 형태로 저장된 객체를 의미한다.

arguments 객체는 유사 배열 객체로 length(인자의 수) 값과 callee(실행 중인 함수의 참조값) 값을 가진다.

 

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

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

다음과 같이 함수 내에서 인자 값을 참조할 수 있음.

 

 

2) 호출 패턴과 this 바인딩

 

함수가 호출될 때 인자 값과 arguments 객체, 그리고 this인자가 함수 내부로 암묵적으로 전달된다.

this는 함수가 호출되는 패턴에 따라 다른 객체를 참조하게 된다.

 

 

- 객체의 메서드 호출할 때 this 바인딩

 

객체의 프로퍼티가 함수일 때 이를 메서드라고 부른다.

메서드를 호출될 때 this는 해당 메서드를 호출한 객체로 바인딩된다.

const Obj = {
  name: 'foo',
  printThis : function() {
    console.log(this)
  }
}

const Obj2 = {
  name: 'boo'
}

Obj2.printThis = Obj.printThis

Obj.printThis() // { name: 'foo', printThis: [Function: printThis] }
Obj2.printThis() // { name: 'boo', printThis: [Function: printThis] }

메서드에서 this를 출력하니 Obj 객체가 출력됨을 확인할 수 있다.

this는 자신을 호출한 객체를 가리키기 때문에 Obj2에서 printThis 프로퍼티를 Obj의 printThis를 가리키도록 하고 함수를 호출하면 this는 Obj2를 출력한다.

 

 

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

 

함수를 호출하면 함수 내부의 this는 전역 객체에 바인딩된다.

전역 객체는 브라우저에서는 window 객체이고, 자바스크립트 런타임 환경에서는 global 객체가 된다.

 

함수호 출시 this 바인딩 특성은 내부 함수를 호출했을 경우에도 적용된다.

const obj = {
  outerfunc: function() {
    console.log("outerfunc : ", this === global) // outer : false
    function inner() {
      console.log("innerfunc : ", this === global) // inner : true
    }
    inner()
  }
}

obj.outerfunc()

outerfunc는 메서드의 this이므로 메서드를 호출하는 객체 obj를 가리키지만 내부 함수인 inner에서의 this는 global 전역 객체를 가리킨다.

 

내부 함수에서 호출 객체인 obj를 가리키게 하는 기법으로는 주로 메서드의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용된다.

const obj = {
  value: 0,
  outerfunc: function() {
    const that = this
    function inner() {
      that.value += 1
    }
    inner()
  }
}

obj.outerfunc()
console.log(obj.value) // 1

메서드에서 this를 변수 that에 저장해서 내부 함수에서는 that을 사용해 객체 obj에 접근할 수 있다.

 

 

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

 

생성자 함수를 생성하는 방법 : 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작한다.

const Person = function (name) {
  this.name = name
}

const foo = new Person('foo')
console.log(foo.name)

new로 함수를 생성자로 호출하면 다음과 같은 순서로 동작한다.

[1] 생성자 함수 코드가 실행되기 전 빈 객체가 생성된다.

생성자 함수의 코드 내부에서 사용된 this는 이 객체를 가리키게 된다.

이 객체의 프로토타입 객체는 생성자 함수의 prototype 프로퍼티가 가리키는 객체가 된다.

[2] this를 통해 프로퍼티를 생성한다

[3] this로 바인딩된 새로 생성한 객체가 리턴된다.

 

 

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

객체 리터럴 방식은 같은 형태의 객체를 재생성할 수 없음.

객체 리터럴 방식의 __proto__는 Object.prototype을 가리키고, 생성자 방식으로 생성된 함수는 생성자. prototype(Person.prototype)을 가리키게 된다.

 

- 생성자 함수를 new를 붙이지 않고 호출할 경우 this가 전역 객체에 바인딩되어 원하는 결과가 나오지 않을것이다.

 

 

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

 

call과 apply 메서드를 사용해 this를 특정 객체에 명시적으로 바인딩시킬 수 있다.

call과 apply는 넘겨받는 인자만 다르고 기능은 같다.

this값을 원하는 객체에 바인딩하고 함수를 호출한다.

 

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

const foo = {}

Person.apply(foo, ['foo', 30])
console.log(foo) // { name: 'foo', age: 30 }

빈객체 foo에 Person 생성자 함수를 apply 메서드로 this 바인딩 후 함수를 호출시켰다.

 

이와 같이 apply(), call() 메서드를 사용해 this를 원하는 값으로 명시적으로 매핑해서 특정 함수나 메서드를 호출할 수 있다.

arguments 객체와 같은 유사 배열 객체에 Array.prototype의 표준 메서드를 적용시키는 것도 apply() 메서드를 사용한다.

 

 

- 함수 리턴

 

함수는 항상 리턴 값을 반환한다.

return문을 사용하지 않더라도 다음 규칙으로 항상 리턴값을 전달하게 된다.

[1] 일반 함수나 메서드에서 리턴 값을 지정하지 않으면 undefined값이 리턴.

[2] 생성자 함수에서 리턴값을 지정하지 않으면 생성된 객체가 리턴.

 

 

 


5. 프로토타입 체이닝

 

1) 프로토타입의 두 가지 의미

 

자바스크립트는 프로토타입 기반의 객체지향 프로그래밍을 지원한다.

모든 객체는 [[Prototype]] 프로퍼티에서 객체의 부모 프로토타입 객체를 가리킨다.

이 속성을 이용해서 OOP의 상속 기능을 사용할 수 있다.

 

 

 

2) 객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝

 

객체는 자기 자신의 프로퍼티뿐만이 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 마치 자신의 것처럼 접근하는 게 가능하다.

이를 프로토타입 체이닝이라고 한다.

 

const obj = {
  name : 'foo',
  sayName: function(){
    console.log('my name is ' + this.name)
  }
}

obj.sayName(); // my name is foo
console.log(obj.hasOwnProperty('name')) // true

hasOwnProperty 메서드는 객체에 프로퍼티의 유무를 확인하는 메서드이다.

그런데 이 메서드는 obj에 정의시키지 않았는 데 사용할 수 있는데,

그 이유는 객체 리터럴로 생성한 객체는 Object라는 내장 생성자 함수로 생성된 것이고,

Object() 생성자 함수도 함수 객체이므로 prototype 속성이 있다.

이 Object() 함수의 prototype 프로퍼티가 가리키는 Object.prototype 객체를 자신의 프로토 타입 객체로 연결해서 Object.prototype에 있는 hasOwnProperty() 메서드를 사용할 수 있었다.

 

obj의 sayName처럼 해당 객체 내에 지목한 프로퍼티가 있으면 그 프로퍼티를 실행하고,

없을 때는 [[Prototype]] 링크를 따라 부모역할을 하는 프로토타입 객체의 프로퍼티를 차례로 검색해나간다.

 

 

 

3) 생성자 함수로 생성된 객체의 프로토타입 체이닝

function Person(name){
  this.name = name
}
const foo = new Person('foo')

다음과 같이 생성자 함수로 생성된 foo의 [[Prototype]]은 Person.prototype이다.

그리고 Person.prototype의 [[Prototype]]은 Object.prototype이다.

 

foo에서 hasOwnProperty() 메서드를 쓴다면?

 

우선 foo내의 프로퍼티중 hasOwnProperty()가 있는지 찾고, 그다음 Person.prototype으로 가서 찾고, 그리고 Object.prototpye으로 가서 hasOwnProperty()를 찾아 해당 메서드를 사용한다.

 

 

4) 프로토타입 체이닝의 종점

 

Object.prototype 객체가 프로토타입 체이닝의 종점이다.

 

 

5) 기본 데이터 타입 확장

 

자바스크립트의 숫자, 문자열, 배열 등에서 사용하는 표준 메서드들의 경우, 이들의 프로토타입인 Number.prototype, String.prototype, Array.prototype에 저장되어 있다.

각 기본 데이터 타입의 프로토타입들도 Object.prototype 객체를 부모로 가진다.

 

 

6) 프로토타입도 자바스크립트 객체이다.

그러므로 프로토타입 객체에도 동적으로 프로퍼티를 추가/삭제할 수 있다.

변경된 프로퍼티는 실시간으로 프로토타입에 반영된다.

 

function Person(name){
  this.name = name
}
const foo = new Person('foo')

foo.sayHello() // error. foo.sayHello is not a function

Person.prototype.sayHello = function(){
  console.log('Hello')
}

foo.sayHello() // Hello

 

 

7) 프로토타입 메서드와 this 바인딩

 

프로토타입 객체의 메서드 내부에서 this를 사용하면, this는 그 메서드를 호출한 객체에 바인딩된다.

function Person(name){
  this.name = name
}
const foo = new Person('foo')

Person.prototype.name = 'person'
Person.prototype.getName = function(){
  return this.name
}

console.log(foo.getName()) // foo
console.log(Person.prototype.getName()) // person

foo에서 호출하면 name이 foo로 출력되고, 프로토타입 객체 내에서 호출하면 person이 출력되었다.

 

 

 

8) 디폴트 프로토타입은 다른 객체로 변경이 가능하다.

 

디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되며, 함수의 prototype 프로퍼티에 연결된다.

해당 함수와 연결되는 디폴트 프로토타입 객체를 다른 일반 객체로 변경할 수 있다.

일반 객체로 변경된 시점 이후로 해당 함수로 생성된 객체들의 [[prototype]]링크는 일반 객체로 향한다.

이러한 특징을 이용해 객체지향의 상속을 구현한다.

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

console.log(Person.prototype.constructor) // Person()

const foo = new Person('foo')
console.log(foo.country) //undefined

//Person의 프로토타입을 일반객체로 변경
Person.prototype = {
  country: 'korea'
}
console.log(Person.prototype.constructor) // Object()

const bar = new Person('bar')
console.log(foo.country) // undefind prototype 변경 이전에 선언한 foo는 그대로 유지된다.
console.log(bar.country) // korea
console.log(foo.constructor) // Person()
console.log(bar.constructor) // Object()

 

 

9) 객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작한다.

 

객체의 프로퍼티를 읽는데 프로퍼티가 해당객체에 없을때만 프로토타입 체이닝이 동작함.

프로퍼티를 추가하거나 수정하는 경우에는 체이닝이 작동하지 않는다.