function add(x, y) { // x, y => 매개변수(parameter)
return x + y;
}
add(1, 2); // 1, 2 => 인자(argument)
자바스크립트 함수는 객체이며 프로그램에서 조작할 수 있다.
함수는 객체이므로 프로퍼티를 정의할 수 있고 함수의 메서드를 호출하는 것도 가능하다.
자바스크립트 함수는 다른 함수 안에서 정의할 수 있으며, 이렇게 정의된 함수는 자신이 정의된 스코프의 변수에 접근할 수 있다.
이런 의미에서 자바스크립트 함수는 클로저다.
함수 선언문으로 정의한 함수는 정의하기 전에도 호출할 수 있다. => 호이스팅
표현식으로 정의한 함수는 정의하기 전에 호출할 수 없다.
호출 컨텍스트?
JavaScript에서 함수를 호출하는 다섯가지 방법
- 함수로 호출
- 메서드로 호출
- 생성자로 호출
- call(), apply()
메서드를 통해 간접적으로 호출
- 자바스크립트 언어 기능을 통한 묵시적 호출
함수로 호출
일반 모드에서 함수의 호출 컨텍스트(this)는 전역 객체다. 스트릭트 모드의 호출 컨텍스트는 undefined이다.
단, 화살표 문법으로 정의한 함수는 항상 '자신이 정의된 곳의 this 값' 을 상속한다.
메서드가 아니라 함수로 호출되도록 설계된 함수는 일반적으로 this 키워드를 전혀 사용하지 않는다.
메서드로 호출
let calculator = { // 객체 리터럴
operand1: 1,
operand2: 1,
add() { // 메서드 단축 문법을 썼다.
// this 키워드는 자신을 포함하는 객체를 참조한다.
this.result = this.operand1 + this.operand2;
}
};
calculator.add(); // 메서드 호출을 통해 1 + 1을 계산한다.
calculator.result // => 2
this 키워드는 변수의 스코프 규칙을 따르지 않는다.
화살표 함수의 예외를 제외하면, 중첩된 함수(내부 함수)는 자신을 포함하는 함수(외부 함수)의 this 값을 상속하지 않는다.
중첩된 함수(내부 함수)를 메서드로 호출하면 그 this 값은 자신을 호출한 객체다.
Q. 메서드 안에 정의된 함수를 함수로 호출하면 this를 통해 메서드의 호출 컨텍스트를 참조할 수 있다? X
let o = { // 객체 o
m: function() { // 객체의 메서드 m
let self = this; // this 값을 변수에 저장
this === o // => true: this는 객체 o
f(); // 보조 함수 f()를 호출
function f() { // 중첩된 함수 f
this === o // => false: this는 전역 객체이거나 undefined
self === o // => true: self는 외부 this 값
}
}
}
o.m(); // => 객체 o에서 메서드 m을 호출한다.
const f = () => {
this === o // true: 화살표 함수는 항상 this를 상속한다.
}
생성자로 호출
뭐라는지 하나도 모르겠다.
생성자: 객체를 새로 만들 목적으로 설계한 함수
선택 사항인 매개변수와 기본 값
// 객체 o의 열거 가능한 프로퍼티를 배열 a에 추가하고 a를 반환한다.
// a를 생략하면 새 배열을 생성해 반환한다.
function getPropertyNames(o, a){
if (a === undefined) a = []; // a = a || []; 와 같다.
for(let property in o) a.push(property);
return a;
}
// getPropertyNames()는 인자 한 개나 두 개로 호출할 수 있다.
let o = {x: 1}, p = {y: 2, z: 3};
let a = getPropertyNames(o); // a == ["x"]; o의 프로퍼티를 새 배열에 담는다.
getPropertyNames(p, a); // a == ["x", "y", "z"]; p의 프로퍼티를 추가한다.
매개변수 기본 값 표현식은 함수를 정의할 때가 아니라 호출할 때 평가된다.
매개변수 기본 값이 상수이거나 [], {} 같은 리터럴이라면 이해하기 쉽겠지만 꼭 그래야 하는 것은 아니다.
매개변수를 여러 개 받는 함수에서 앞의 매개변수 값을 사용해 그 다음 매개변수의 기본 값을 정의할 수도 있다.
const rectangle = (width, height=width*2) => ({width, height});
rectangle(1) // => { width: 1, height: 2 }
나머지 매개변수와 가변 길이 인자 리스트
function max(first = -Infinity, ...rest) {
let maxValue = first; // 첫 번째 인자가 가장 크다고 가정한다.
// 나머지 인자를 순회하면서 더 큰 값을 찾는다.
for(let n of rest){
if (n > maxValue){
maxValue = n;
}
}
// 가장 큰 값을 반환한다.
return maxValue;
}
max(1, 10, 100, 2, 3) // => 100
나머지 매개변수는 앞에 점 세개를 붙이는데, 반드시 함수 선언에서 마지막으로 정의된 매개변수여야 한다.
함수 바디 안에서 나머지 매개변수의 값은 항상 배열이다. 배열이 비어 있더라도 나머지 매개변수는 절대 undefined가 되지 않는다.
나머지 매개변수를 정의하는 ...과 분해 연산자는 다르다.
인자 개수에 제한이 없는 함수를 가변 함수라고 부른다.
Argument 객체
나머지 매개변수는 ES6에서 도입. 그 전에는 Arguments 객체를 써서 가변 함수를 만들었다. 지금은 안 쓴다.
...args 이런 식으로 쓴다.
function max(x) {
let maxValue = -Infinity;
for(let i = 0; i < arguments.length; i++){
if (arguments[i] > maxValue) maxValue = arguments[i];
}
return maxValue;
}
max(1, 10, 100, 2, 3) // => 100
함수 호출과 분해 연산자
let numbers = [5, 2, 10, -1];
Math.min(...numbers) // => -1
함수 정의에서 ... 문법은 분해 연산자의 정반대로 동작한다. 함수 정의에서 사용한 ...는 여러 개의 인자를 배열에 모은다.
분해 연산자는 개별 값이 예상되는 컨텍스트에서 배열이나 문자열 같은 이터러블 객체를 분해한다.
나머지 매개변수와 분해 연산자를 함께 쓰면 유용한 경우가 많다.
다음은 함수인 인자를 받고 래퍼 버전을 반환하는 함수다.
function timed(f){
return function(...args) { // 인자를 나머지 매개변수 배열에 모은다.
console.log(`Entering function ${f.name}`);
let startTime = Date.now();
try {
// 인자를 모두 래퍼 버전에 전달한다.
return f(...args); // 인자를 다시 분해한다.
}
finally {
// 반환하기 전에 소요된 시간을 출력한다.
console.log(`Exiting ${f.name} after ${Date.now()-startTime}ms`);
}
};
}
function benchmark(n) {
let sum = 0;
for(let i = 1; i <= n; i++) sum += i;
return sum;
}
timed(benchmark)(1000000) // => 500000500000; 숫자의 합
함수 인자를 매개변수로 분해
n에 3을 넣고, toIndex에 4를 넣었다는 것에 주목
값인 함수
const operators = {
add: (x,y) => x+y,
subtract: (x,y) => x-y,
multiply: (x,y) => x*y,
divide: (x,y) => x/y,
pow: Math.pow // 미리 정의된 함수도 사용 가능
}
function operate(operation, operand1, operand2) {
if(typeof operators[operation] === "function") {
return operators[operation](operand1, operand2);
}
else throw "unknown operator";
}
operate("add", "hello", operate("add"," ","world")) // => "hello world"
operate("pow", 10, 2) // => 100
함수 프로퍼티 직접 정의
호출할 때마다 서로 다른 고유한 정수를 반환하는 함수를 만들어 보자.
절대 같은 값을 반환해서는 안된다. 반환한 값을 추적하는 방법은?
// 이 함수는 호출할 때마다 다른 정수를 반환한다.
// 자신의 프로퍼티를 사용해 어떤 값을 반환할지 판단한다.
function uniqueInteger() {
return uniqueInteger.counter++;
}
// 함수 객체의 counter 프로퍼티를 초기화한다.
uniqueInteger.counter = 0;
uniqueInteger() // => 0
uniqueInteger() // => 1
다음 factorial() 함수는 자신의 배열 프로퍼티를 이용해 이미 계산한 결과를 캐시한다.
// 팩토리얼을 계산하고 그 결과를 함수 자체의 프로퍼티로 캐시한다.
function factorial(n){
if (Number.isInteger(n) && n > 0) {
if (!(n in factorial)) {
factorial[n] = n * factorial(n-1);
}
return factorial[n];
} else {
return NaN;
}
}
factorial[1] = 1; // 캐시 초기화
factorial(6) // => 720
factorial[5] // => 120; 이 값은 이미 캐시에 존재한다.
클로저
// 1
let scope = "global scope"; // 전역 변수
function checkscope() {
let scope = "local scope";
function f() { return scope; }
return f();
}
checkscope() // => "local scope"
// 2
let scope = "global scope"; // 전역 변수
function checkscope() {
let scope = "local scope";
function f() { return scope; }
return f;
}
let s = checkscope()();
아래의 s에는 무엇이 할당되었을까?
중첩된 함수 f()는 변수 scope가 "local scope"였던 스코프에서 정의됐다.
이 연결은 f를 어디에서 실행하든 상관없이 계속 유지된다. 따라서 위 코드의 마지막 행은 "local scope"를 반환한다.
위에서 고유한 정수를 반환하는 함수를 정의했다. uniqueInteger()
이 함수는 함수 자체의 프로퍼티를 사용해 이미 반환했던 값을 추적한다. 이런 방법에는 버그나 악의적인 코드가 개입해 목적을 잃게 만들 수 있다는 단접이 있다. 클로저는 함수 호출 시점의 로컬 변수를 캡처하므로 이 변수를 비공개 상태로 사용할 수 있다.
// 1
function uniqueInteger() {
return uniqueInteger.counter++;
}
uniqueInteger.counter = 0;
uniqueInteger() // => 0
uniqueInteger() // => 1
// 2
let uniqueInteger = ( function() {
let counter = 0;
return function() { return counter++; };
}() );
uniqueInteger() // => 0
uniqueInteger() // => 1
비교해서 살펴보자.
왼쪽은 counter의 값에 접근이 가능한 반면, 오른쪽은 counter 값을 변경해도 함수 내부의 변수가 영향을 받지 않는다.
오직 내부 함수만이 counter에 접근할 수 있다.
근데 괄호로 안 싸도 똑같은거 아닌가?
function counter() {
let n = 0;
return {
count: function() { return n++; },
reset: function() { n = 0; }
};
}
let c = counter(), d = counter();
c.count() // => 0
d.count() // => 0
c.reset(); // reset()과 count() 메서드는 상태를 공유한다.
c.count() // => 0
d.count() // => 1
클로저 기법과 프로퍼티 게터, 세터의 조합
function counter(n) {
return {
get count() { return n++; },
set count(m) {
if (m > n) n = m;
else throw Error("카운트는 더 큰 값으로만 바꿀 수 있습니다.");
}
};
}
let c = counter(1000);
c.count // => 1000
c.count // => 1001
c.count = 2000;
c.count // => 2000
c.count = 2000; // Error: 카운트는 더 큰 값으로만 바꿀 수 있습니다.
// 이 함수의 독특한 점은 게터와 세터 메서드가 조작하는 프로퍼티 값이
// 객체 o에 저장되지 않는다는 점이다. 값은 이 함수의 로컬 변수에만 저장된다.
// 게터와 세터 메서드는 함수에 로컬로 정의되었으므로 로컬 변수에 접근할 수 있다.
// 따라서 value는 두 접근자 메서드에서만 사용할 수 있으며, 세터 메서드를 통하지 않고서는
// 값을 수정하거나 저장할 수 없다.
function addPrivateProperty(o, name, predicate) {
let value;
o[`get${name}`] = function() { return value; };
o[`set${name}`] = function(v) {
if(predicate && !predicate(v)) {
throw new TypeError(`set${name}: invalid value ${v}`);
} else {
value = v;
}
};
}
let o = {};
addPrivateProperty(o, "Name", x => typeof x === "string");
o.setName("hajongon"); // 프로퍼티 값 저장
o.getName() // => "hajongon"
o.setName(0); // => TypeError: 올바르지 않은 타입을 사용했습니다.
왜 10을 리턴할까?
function constfuncs() {
let funcs = [];
for(var i= 0; i< 10; i++) {
funcs[i] = () => i;
}
return funcs;
}
let funcs = constfuncs();
funcs [5] () // => 10; 왜 5가 아닐까?
위 코드는 클로저 10개를 생성하고 배열에 저장한다.
이 클로저들은 모두 같은 함수 호출에서 정의되므로 모두 이 값을 공유한다. 따라서 반환된 배열에 포함된 함수는 모두 같은 값을 반환한다.
var가 아닌 let이라면 어떨까?
5를 리턴한다.
var i 로 선언하면 변수 i 는 루프 바디 안에만 존재하는 것이 아니라 함수 전체에 존재한다. i가 10까지 올라가야 for 문이 끝나므로 열 개의 클로저는 모두 10을 반환하는 것이다.
this는 변수가 아니라 자바스크립트 키워드다.
외부 함수의 this 값을 사용하는 클로저를 만들 때는 화살표 함수를 사용하거나, 클로저를 반환하기 전에 bind()를 호출하거나, 외부의 this 값을 클로저가 상속할 변수에 할당해야 한다.
call(), apply()
이 두 메서드는 함수를 다른 객체의 메서드인 것처럼 간접적으로 호출한다.
첫 번째 인자는 함수를 호출할 객체이고, 그 다음 인자는 호출될 함수에 전달된다.
call() 메서드는
f.call(o, 1, 2);
와 같이 호출하고
apply() 메서드는
f.apply(o, [1, 2]);
와 같이 호출한다. (인자가 배열로 제공됨)
첫 번째 인자만 사용할 경우 두 메서드는 다음 코드와 같이 작동한다.
f.call(o);
f.apply(o); // 이 두 코드는 아래와 같이 작동한다.
o.temp = f; // f를 o의 메서드로 만든다. (임시)
o.temp(); // 인자 없이 m을 호출한다.
delete o.temp; // 임시 메서드를 제거한다.
bind()
이 메서드의 주요 목적은 함수를 객체에 결합시키는 것이다.
함수 f에서 bind() 메서드를 호출하면서 객체 o를 전달하면 새 함수를 반환한다.
새 함수를 함수로 호출하면 원래 함수 f가 o의 메서드로 호출된다. 새 함수에 전달한 인자는 모두 원래 함수에 전달된다.
function f(y) { return this.x + y } // 결합할 함수
let o = { x: 1 }; // 결합될 객체
let g = f.bind(o); // g(x)를 호출하면 o에서 f()를 호출한다.
g(2) // => 3
let p = { x: 10, g }; // g()를 이 객체의 메서드로 호출한다.
p.g(2) // => 3: g는 여전히 o에 결합되어 있다.
위 코드의 함수 f()를 화살표 함수로 정의했다면 결합은 제대로 이루어지지 않았을 것이다.
화살표 함수는 자신이 정의된 환경의 this 값을 상속하며 이 값은 bind()에서 덮어쓸 수 없기 때문이다.
bind()를 호출하는 목적은 대개 화살표 함수가 아닌 함수를 화살표 함수처럼 사용하는 것임.
따라서 화살표 함수에서 결합이 이루어지지 않는다는 것은 사실 문제가 되지 않는다.
bind() 에 전달하는 인자 중 첫 번째를 제외한 나머지는 this 값과 함께 결합된다.
이러한 부분 적용(partial application)은 화살표 함수에도 동작한다. 부분 적용은 함수형 프로그래밍에서 널리 쓰이는 기법이며 커링(currying)이라고 부르기도 한다.
let sum = (x,y) => x+y;
let succ = sum.bind(null, 1);
succ(2) // => 3: x는 1이고 y 인자로 2를 전달
function f(y,z) { return this.x + y + z; }
let g = f.bind({x:1}, 2); // this와 y를 결합
g(3) // => 6: this.x는 1, y는 2에 결합, z는 3
고차함수
하나 이상의 함수를 인자로 받아 새 함수를 반환하는 함수
function not(f) {
return function(...args){ // 새로운 함수를 반환
let result = f.apply(this, args); // 이 함수는 f를 호출하고
return !result; // 그 결과를 부정한다.
};
}
const even = x => x % 2 === 0;
const odd = not(even);
[1,1,3,5,5].every(odd) // true
// 배열 인자를 받아 각 요소에 f를 호출하고, [반환 값으로 이루어진 배열]을 반환하는 함수를 반환한다.
const map = function(a, ...args) { return a.map(...args); };
function mapper(f) {
return a => map(a, f);
}
const increment = x => x+1;
const incrementAll = mapper(increment);
incrementAll([1,2,3]) // [2,3,4]
// f(g(...))를 계산하는 새 함수를 반환
// 반환되는 함수 h는 인자 전체를 g에 전달하고,
// g의 반환값을 f에 전달한 다음 f의 반환값을 반환
// f와 g는 모두 h가 호출된 this 값을 공유
function compose(f,g){
return function(...args) {
// f는 x+y 한 번만 계산하므로 call() 사용
// g는 2*2, 3*3 이렇게 두 번 계산해야 하므로 apply() 사용해서 값 배열 전달
return f.call(this, g.apply(this, args));
}
}
const sum = (x,y) => x+y;
const square = x => x*x;
compose(square, sum)(2,3) // 25
재귀(recursion)와 메모이제이션(memoization)
이전에 계산한 결과를 캐싱하는 것
=> 프로그래밍을 할 때 반복되는 결과를 메모리에 저장해서 다음과 같은 결과가 나올 때 빨리 실행하는 코딩 기법
const factorial = function(number) {
let result = 1;
for (var i = 1; i <= number; i++) {
result *= i;
}
return result;
};
factorial(3); // 6
factorial(4); // 24
위와 같이 써도 factorial 함수를 구현할 수는 있다. 하지만 확장 가능성이 없다.
재귀를 활용하면 아래와 같이 쓸 수 있다.
const factorial = function(number) {
if (number > 0) {
return number * factorial(number - 1);
} else {
return 1;
}
};
factorial(3); // 6
factorial(4); // 24
가독성은 높지만 컴퓨터에게는 많은 부담을 주므로, 성능을 중시한다면 가급적 재귀를 사용하지 않는 것이 좋다.
위의 코드에서 factorial(3)의 값을 얻은 뒤,
factorial(4)의 값을 얻는 과정이 뭔가 아쉽다.
factorial(3)의 값이 있다면, factorial(4)는 거기에다 4만 곱해주면 되기 때문이다.
const factorial = (function() {
const save = {};
const fact = function(number) {
if (number > 0) {
let saved = save[number - 1] || fact(number - 1);
let result = number * saved;
save[number] = result;
console.log(saved, result);
return result;
} else {
return 1;
}
};
return fact;
})();
factorial(7); // 1 1, 1 2, 2 6, 6 24, 24 120, 120 720, 720 5040
factorial(7); // 720 5040-
위 코드를 한 번 실행하면 처음부터 쭉 곱해가면서 factorial을 구한다.
하지만 두 번째 실행할 때 부터는 저장된 값이 있고 그 값을 이용해서 계산한다.
다른 예제로 피보나치 수열이 있다.
const fibonacci = function(number) {
if (number < 2) {
return number;
} else {
return fibonacci(number - 1) + fibonacci(number - 2);
}
};
const fibonacci = (function() {
const save = {};
const fibo = function(number) {
if (number < 2) {
return number;
} else {
let saved1 = save[number - 1] || fibo(number - 1);
let saved2 = save[number - 2] || fibo(number - 2);
let result = saved1 + saved2;
save[number] = result;
console.log(saved1, saved2, result);
return result;
}
};
return fibo;
})();
fibonacci(5); // 1 0 1, 1 1 2, 2 1 3, 3 2 5, 5
fibonacci(5); // 3 2 5, 5
'부트캠프 > 자바스크립트 완벽 가이드' 카테고리의 다른 글
12장. 이터레이터와 제너레이터 (0) | 2023.02.12 |
---|---|
9장. 클래스 (0) | 2023.02.06 |
7장 배열 (0) | 2023.01.26 |
6장. 객체 (0) | 2023.01.15 |
5장. 문 (단순 내용 나열) (0) | 2023.01.07 |