일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 리액트
- 클린코드
- 자바스크립트
- server component
- MFA
- 오블완
- 에세이
- CustomHook
- context.api
- react
- virtaullist
- CRA
- provider 패턴
- vite
- 회고
- sharedworker
- 이것저것
- TypeScript
- 합성 컴포넌트
- 프론트엔드
- JavaScript
- 티스토리챌린지
- radixui
- frontend
- 리팩토링
- 웹워커
- 아키텍처
- Webworker
- MicroFrontEnd
- Web
- Today
- Total
Lighthouse of FE biginner
[JavaScript] 함수 본문
모던 자바스크립트 Deep Dive 스터디 4회차
자바스크립트 스터디를 진행하며 해당 회차에 공부했던 내용을 직접 그림을 그리고 코드를 실행하며 저의 글로 작성합니다.
함수
자바스크립트에서 함수는 일급 객체이다. 즉 값처럼 변수에 할당하고 프로퍼티의 값이 될 수 있으며 배열의 요소가 될 수 있다.
자바스크립트에서 함수를 사용하는 방식은 크게 함수 선언문, 함수 리터럴 표현식이 있다.
이번 스터디 회차에서는 함수가 메모리에 선언되는 방법과 호이스팅, 함수의 클린 코드를 집중적으로 살펴본다.
일급 객체
일급 객체는 아래의 네 가지 조건을 만족해야 한다.
- 무명의 리터럴로 생성이 가능하다. 런타임에 생성이 가능하다.
const foo = function() {
console.log('foo')
}
- 변수나 자료구조(객체, 배열)에 저장할 수 있다.
- 함수의 매개변수로 전달할 수 있다.
- 함수의 반환 값으로 사용할 수 있다.
자바스크립트의 함수는 위 조건을 만족하며 객체와 동일하게 사용되며, 함수는 값으로 사용할 수 있는 어느 곳에서든 리터럴로 정의되며 런타임에 함수 객체로 평가된다.
함수의 선언
자바스크립트에는 렉시컬(lexical) 이라는 개념이 있다. 작성된 코드의 문맥에 따라서 다르게 해석되는 중의적인 표현을 담고 있다. {}은 코드 블럭으로 해석될 수 있고 객체 리터럴로 해석될 수 있다. 이는 자바스크립트의 렉시컬 환경에 의해 해석이 되는 것이다. 이처럼 기명 함수 리터럴도 중의적인 코드이다. 함수 리터럴을 피연산자로 사용하는 문맥(함수 리터럴이 값으로 평가 되어야 하는 문맥)에는 함수 선언문으로 해석하고, 함수 선언문이 값으로 평가되어야 하는 문맥에는 함수 리터럴 표현식으로 해석한다.
함수 선언문
함수 선언문으로 함수를 선언할 시 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 거기에 함수를 할당한다.
아래는 foo라는 기명 함수를 함수 선언문을 활용해 선언하고 해당 함수를 호출한다.
function foo() {}
foo();
위 스크립트를 실행하면 메모리에는 다음과 같은 일들이 벌어진다.
함수 선언문을 통해 선언 했기에 식별자가 존재하지 않지만 코드 평가 단계에서 자바스크립트 엔진에 의해 foo라는 식별자가 암묵적으로 생성된다. 그리고 해당 메모리에 함수 객체를 할당하고 foo 식별자는 해당 공간의 주소 값을 가지게 된다.
foo 함수를 실행하면 먼저 해당 주소 값에 접근해 함수 객체를 가져온다. 그리고 실행문인 ()을 통해 해당 함수 객체를 실행한다.
메모리 위의 함수 객체
함수 선언문으로 선언된 전역 함수는 코드의 평가 시점에 메모리에 함수 객체가 생성된다고 한다. 그럼 메모리에 올라가는 함수 객체는 어떤 모습일까?
코드 평가 단계에서 생성된 함수 객체의 모습
함수 객체는 Callable Object(호출 가능한 객체)로 생성되며, 다음과 같은 프로퍼티를 가진다.
- [[FunctionName]]: 함수의 이름 (익명 함수일 경우 빈 문자열 "")
- [[Environment]]: 함수를 정의한 환경(스코프 체인의 정보가 저장됨)
- [[Code]]: 함수의 실행 코드
- [[FormalParameters]]: 함수의 매개변수 정보
- [[ECMAScriptCode]]: 함수 본문 코드
- [[Call]]: 호출 시 실행되는 내부 메서드 (실제 함수 호출 시 동작을 정의)
- prototype: 함수 객체의 프로토타입 (일반적으로 constructor 프로퍼티를 가짐)
- length: 함수의 매개변수의 개수를 나타냄
- name: 함수의 이름 (기본적으로 함수 이름을 나타내며 익명 함수의 경우 anonymous로 설정)
예시
함수 코드
function add(a, b) {
return a + b;
}
함수 객체
[[FunctionName]]: "add"
[[FormalParameters]]: ["a", "b"]
[[Code]]: return a + b;
[[Environment]]: 전역 스코프의 참조
메모리에 올라가는 함수 객체의 모습
{
"name": "add",
"length": 2,
"prototype": { "constructor": "add" },
"Environment": "GlobalScope",
"Code": "return a + b;"
}
함수 표현식
자바스크립트의 함수는 일급 객체이기 때문에 변수에 할당할 수 있다.
호이스팅
자바스크립트의 함수는 코드 평가 단계에서 호이스팅 된다. 이때 함수 선언문과 함수 리터럴 표현식은 다르게 동작한다. 함수 선언문의 호이스팅과 함수 표현식의 호이스팅은 다르게 동작한다. 변수에 할당된 함수 표현식의 호이스팅은 변수 호이스팅으로 동작한다.
foo(); // foo
boo(); // undefined
function foo() {
console.log("foo");
}
var boo = function() {
console.log("boo");
};
boo(); // boo
위 코드에서 foo 함수는 함수 선언문으로, boo 함수는 함수 표현식으로 boo 변수에 할당된다.
코드의 평가 단계에서 자바스크립트 엔진은 foo 함수의 선언문을 메모리에 할당하고 암묵적으로 foo라는 식별자를 생성한다. 이때 암묵적으로 생성된 foo 식별자는 메모리에 할당된 foo 함수 객체를 할당 받았기 때문에 (식별자에 foo 함수 객체의 메모리 주소 값이 바인딩 됐다.) 함수의 실행이 가능하다.
boo에 할당된 익명 함수는 변수의 할당문 이기 때문에 할당문이 실행 되는 시점(런타임)에 평가(메모리에 할당)된다. 그리고 변수의 선언문은 실행이 됨으로 boo 식별자는 메모리에 공간을 할당 받는다. 코드의 평가 단계에서는 값의 할당이 이뤄지지 않기 때문에 boo 변수는 undefined로 초기화 된다. 그래서 2행에서 boo를 실행할 때 undefined 이기 때문에 실행이 되고 에러가 발생한다. boo 함수에 할당되는 익명 함수는 코드의 실행 단계(런타임)에 함수 표현식이 평가 된다. 이때서야 함수 객체로서 메모리에 공간을 할당 받고 boo 변수에는 해당 익명 함수의 주소 값이 바인딩 된다.
화살표 함수 (26.3절)
화살표 함수는 () => {} 함수를 간단하게 선언할 수 있다는 점으로 자주 사용된다. 문법만 간단한 것이 아닌 내부 동작도 간소화 됐다. 화살표 함수는 this 바인딩이 다르고, prototype 프로퍼티가 존재하지 않으며 arguments 객체를 생성하지 않는다. 화살표 함수 내부에서 this, arguments, super, new.target을 참조하면 상위 스코프의 this, arguments, super, new.target를 참조한다.
화살표 함수에서 this바인딩
화살표 함수에서 this바인딩은 상위 스코프를 가리킨다.
아래 코드를 살펴보자. foo 메서드에서는 인자를 받아 map함수를 통해 배열을 반환한다. map 함수에는 화살표 함수가 들어가는데 여기서 this를 참조하고 있다.
화살표 함수의 this는 클래스 내부 메서드이기에 객체 자신을 가리킨다. 10행에서 a 객체를 생성했고, 11행에서 foo메서드를 실행한다면 화살표 함수의 this는 a 객체를 가리키고 a.property인 2를 값으로 사용하게 된다.
class Class {
constructor(property) {
this.property = property;
}
foo(arr) {
return arr.map((a) => this.property + a);
}
}
const a = new Class(2);
console.log(a.foo([1, 2, 3])); // [3,4,5]
반대로 일반 함수 선언문에서의 this를 살펴보자. 일반 함수 내부에서 this 바인딩은 전역 객체이다. 이를 node.js 환경에서 실행하면 다음과 같다.
function func() {
return () => this;
}
console.log(func()());
메서드에서 this를 참조하고 싶다면 반드시 일반 메서드 선언문으로 메서드를 만들어야 한다. 아래 코드를 살펴보면 getSome 메서드를 화살표 함수로 만들고 있다.
상위 스코프인 this는 전역 객체이다. 전역 객체에는 ranNum이라는 프로퍼티가 존재하지 않는다.
const obj = {
ranNum: Math.random(),
getSome: () => console.log(this, this.ranNum),
};
obj.getSome(); // {} undefined
아래 객체를 살펴보자. getSome메서드는 일반 메서드 선언문으로 작성됐다. 해당 메서드의 this 바인딩은 객체 자신이 되며 obj2 객체 자체와 ranNum 프로퍼티가 출력된다.
const obj2 = {
ranNum: Math.random(),
getSome() {
console.log(this, this.ranNum);
},
};
obj2.getSome(); // { ranNum: 0.8168228885628372, getSome: [Function: getSome] } 0.8168228885628372
매개변수 (값 전달, 주소 전달)
원시 값은 변경 불가능하기 때문에 함수의 매개 변수로 전달할 때 역시 값 그 자체만 전달이 된다(call by value). 하지만 객체는 변경 가능하고 메모리에 참조 값을 가지고 있기 때문에 함수의 매개 변수로 전달할 때 역시 참조 주소를 전달한다 (call by address). 이는 함수 내부에서 객체를 변경한다면 원래의 객체 역시 변경이 된다는 뜻이다.
사이드 이펙트를 방지하기 위해 Object.freeze를 사용해 객체를 불변하게 만든다던지, 함수 내부에서 객체를 깊은 복사를 하여 새로운 객체를 생성해 사용하는 방법을 사용해야 한다.
'자바스크립트' 카테고리의 다른 글
[JavaScript] 스코프 (2) | 2024.11.24 |
---|---|
[JavaScript] Spread Operator(전개 연산자)로 객체를 복사한다면? (0) | 2024.11.21 |
[JavaScript] 원시 값과 객체 리터럴 (0) | 2024.11.16 |
[JavaScript] 배열 만들기 (0) | 2024.11.14 |
[JavaScript] 변수와 데이터 타입 (1) | 2024.11.09 |