Functions

호출된 함수는 전달받은 인자 외에도 invocation context라는 값을 별도로 가져요. this 키워드의 값이 바로 이 값이에요. 함수가 객체를 통해(객체의 메서드로서) 호출되면 해당 객체가 invocation context가 됩니다.

따라서 this는 암묵적으로 전달된 함수 인자로도 볼 수 있어요. 이런 관점이 보다 명확한 파이썬의 self를 참고해보세요:

stackoverflow.comWhat is the purpose of the `self` parameter? Why is it needed?Consider this example: class MyClass: def func(self, name): self.name = name I know that self refers to the specific instance of MyClass. But why must func explicitly include self as a

Defining Functions

함수 선언문은 호이스팅됩니다. 함수 표현식은 호이스팅되지 않습니다:

f();
g();

function f() {
  console.log('f');
}
let g = () => console.log('g');

화살표함수는 정의된 환경의 this 값을 그대로 사용하며 prototype 프로퍼티를 가지지 않아 클래스 생성자로서 사용될 수 없습니다.

// 일반 객체 예제
const obj = {
  i: 10,
  b: () => this.i,
  c() {
    return this.i;
  },
};

console.log(obj.b(), obj.c());

let { b, c } = obj;
console.log(b(), c());

// 클래스 예제
class A {
  i = 10;
  d = () => this.i;
  e() {
    return this.i;
  }
}

let a = new A();
console.log(a.d(), a.e());

let { d, e } = a;
console.log(d());
console.log(e());

화살표 함수의 this값이 일반 객체와 클래스에서 다른 이유:

Because a class's body has a this context, arrow functions as class fields close over the class's this context, and the this inside the arrow function's body will correctly point to the instance (or the class itself, for static fields). However, because it is a closure, not the function's own binding, the value of this will not change based on the execution context.

developer.mozilla.orgArrow function expressions - JavaScript | MDNAn arrow function expression is a compact alternative to a traditional function expression, with some semantic differences and deliberate limitations in usage:

Invoking Functions

함수는 다음 방법들로 호출될 수 있어요:

  • 함수로
  • 메서드로
  • 생성자로
  • call이나 apply 메서드를 사용해 간접적으로
  • JS에 의해 암묵적으로
    • get/set
    • toString, valueOf
    • iteration 관련 함수들
    • tagged template literal,
    • Proxy등...

this에 해당하는 객체가 없을 때 strict mode면 undefined, 아니면 전역 객체가 돼요:

// 아래 예제는 현재 블로그상에서 작동하지 않습니다 :(
// 상위에 JS 샌드박스가 있어서 strict mode에 상관없이 this가 항상 window가 돼요.
// 브라우저 콘솔에서 실행해보세요 🙇‍♂️🙇‍♂️

// 'use strict'
const isStrict = (function () {
  return !this;
})();

console.log(isStrict);

함수가 this를 반환하도록하면 method chaining 스타일의 API를 만들 수 있어요:

class Square {
  x(val) {
    this.x = val;
    return this;
  }
  y(val) {
    this.y = val;
    return this;
  }
  outline(color) {
    this.outline = color;
    return this;
  }
  toString() {
    return `Square\n - x: ${this.x}\n - y: ${this.y}\n - outline: ${this.outline}`;
  }
}

console.log(String(new Square().x(100).y(100).outline('red')));

중첩 함수의 this는 외부 스코프의 this값과 다를 수 있어요. 책 말로는 JS의 결함으로 여겨진다고 하네요:

let o = {
  m: function () {
    console.log(this + '');
    let self = this;

    function f() {
      console.log(this + '');
      console.log(self + '');
    }

    f();
  },
  toString() {
    return '[object O]';
  },
};

o.m();

위 코드처럼 self와 같은 별도의 변수를 사용하거나, bind 메서드를 사용하거나, 화살표 함수를 사용하면 해결할 수 있습니다.

new 키워드를 붙이면 타겟 함수는 생성자(constructor)로 기능하고 일반 함수 호출과 다르게 처리돼요.

  • 생성자의 prototype 프로퍼티를 상속받는 객체가 생성되고 함수 본문의 this가 이 객체를 가르킵니다.
  • 함수 본문의 return에서 객체를 반환한다면 그 객체가 반환되고, 객체가 아닌걸 반환하면 해당 return문은 무시됩니다.

Function Arguments and Parameters

function getPropertyNames(o, a = []) {
  for (let property in o) a.push(property);
  return a;
}

console.log(getPropertyNames({ a: 1 }));
console.log(getPropertyNames(document));

인자의 기본값(위 코드에서는 빈 배열 a)은 호출마다 초기화되고 이는 파이썬과의 차이점이에요.

파이썬 인자 특

기본값이 상수일 필요는 없고 다른 인자값에 의존적인 것도 가능합니다:

let rec = (width, height = width) => ({ width, height });
console.log(rec(1));

Rest parameter 예시:

// Edge case인 빈 배열을 인자 기본값을 주어 해결한게 신기했어요
function max(first = -Infinity, ...rest) {
  // rest는 대응되는 인자가 없으면 빈 배열입니다.
  let maxVal = first;
  for (let n of rest) maxVal = maxVal < n ? n : maxVal;
  return maxVal;
}

console.log(max(3, 1, 4, 1, 5, 9, 2));

Spread operator를 사용한 재밌는 예시:

function timed(f) {
  return function (...args) {
    console.log(`Entering ${f.name}`);
    let startTime = performance.now();
    try {
      return f(...args);
    } finally {
      // finally문으로 처리했기에 f에서 에러를 던져도 괜찮습니다.
      console.log(`Exiting ${f.name} after ${performance.now() - startTime}ms`);
    }
  };
}

let fibo = (n) => (n <= 2 ? 1 : fibo(n - 1) + fibo(n - 2));

let fibo2 = (n) => {
  if (n <= 2) return 1;
  if (fibo2[n]) return fibo2[n];

  return (fibo2[n] = fibo2(n - 1) + fibo2(n - 2));
};

let N = 30;
timed(fibo)(N);
timed(fibo2)(N);

함수 매개변수에서도 destructuring이 아래와 같이 가능합니다:

function vectorAdd([x1, y1], [x2, y2]) {
  return [x1 + x2, y1 + y2];
}

console.log(vectorAdd([1, 2], [3, 4]));

let vectorMultiply = ({ x, y, z = 0 }, scalar) => ({
  x: x * scalar,
  y: y * scalar,
  z: z * scalar,
});

console.log(vectorMultiply({ x: 1, y: 2 }, 10));

Functions as Values

위의 fibo2 예제에서 미리 보여드렸지만, 함수도 객체이므로 프로퍼티를 가질 수 있어요:

uniqueInteger.counter = 0;

function uniqueInteger() {
  return uniqueInteger.counter++;
}

console.log(uniqueInteger());
console.log(uniqueInteger());

Functions as Namespaces

즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression) 은 정의되자마자 즉시 실행되는 Javascript Function 를 말한다.

developer.mozilla.orgIIFE - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN즉시 실행 함수 표현(IIFE, Immediately Invoked Function Expression) 은 정의되자마자 즉시 실행되는 Javascript Function 를 말한다. IIFE라는 이름은 Ben Alman이 블로그에서 처음으로 시작되었습니다.

IIFE를 네임스페이스로 활용할 수 있습니다.

(function () {
  let val = 10;
  console.log('IIFE');
  // 변수 val로 이것저것 하기
})();

// IIFE 내부의 변수에 접근할 수 없습니다.
console.log(val);

Closures

렉시컬 스코프 규칙을 따르는 자바스크립트의 함수는 호출 스택과 관계없이 각각의 (this를 제외한) 대응표를 소스코드 기준으로 정의하고, 런타임에 그 대응표를 변경시키지 않는다.

클로저 = 함수 + 함수를 둘러싼 환경(lexical environment)

ui.toast.com자바스크립트의 스코프와 클로저기본적으로 자바스크립트는 ECMAScript 언어 명세를 따르고있다. 이 명세 **8장의 실행코드와 실행컨텍스트부분에서 스코프에 관한 동작 방식을 확인할 수 있으며, 또 중요한 개념인 1급 객체로서의 함수는 그 특징을 명세의 전반적인 부분에서 나타내고 있다. 그리고, 클로저(Closure)에 대한 정의는 없다. 클로저는 자바스크립트가 채용하고 있는 기술적 기반 혹은 컨셉으로, 자바스크립트는 클로저를 이용하여 스코프적 특징과 일급 객체로서의 함수에 대한 명세를 구현한 것이다.

렉시컬 스코프를 구현하기 위해 함수 객체는 함수 정의가 위치한 스코프에 대한 정보도 가지고 있어야합니다. 이러한 객체와 스코프의 조합을 클로저라고 해요.

let scope = 'global scope';

function foo() {
  let scope = 'local scope';
  return () => scope;
}

console.log(foo()());

여러 함수가 같은 스코프를 공유할 수도 있어요:

let createCounter = (n) => {
  return {
    // 함수 인자도 클로저에서 사용할 수 있습니다
    count: () => n++,
    reset: () => (n = 0),
  };
};

let c1 = createCounter(0),
  c2 = createCounter(10);

console.log(c1.count(), c2.count());
c1.reset();
console.log(c1.count(), c2.count());

var와 클로저의 조합으로 버그가 생긴 코드:

// 목표: 0-9를 반환하는 함수의 배열을 반환합니다.
const foo = () => {
  let funcs = [];
  // var가 함수 단위로 정의되어 i가 클로저들간에 공유됩니다.
  // var -> let 으로 바꿔보세요
  for (var i = 0; i < 10; i++) funcs[i] = () => i;
  return funcs;
};

let a = foo();
console.log(a[5]());

상위 함수의 this를 사용하는 클로저가 필요하다면...:

let obj = {
  x: 123,
  f() {
    // ⚠️ 일반 함수는 바깥 스코프의 this를 사용하지 않습니다.
    let g1 = function () {
      return this;
    };

    // ✅ 따라서 화살표 함수를 사용하거나,
    let g2 = () => this;

    // ✅ bind를 사용하거나,
    let g3 = g1.bind(this);

    // ✅ this를 클로저 내부의 별도 변수에 할당합니다.
    let self = this;
    let g4 = function () {
      return self;
    };

    // ✍️ 다른 g 함수들을 대신 반환해보세요
    return g1;
  },

  toString() {
    return '[object obj]';
  },
};

console.log(String(obj.f()()));

Function Properties, Methods, and Constructor

let sum2 = (a, b) => a + b;
let sum3 = (a, b, c) => a + b + c;
let sum = (...args) => args.reduce((prev, cur) => prev + cur, 0);

console.log(sum2.length, sum3.length, sum.length);
console.log(sum2.name);

모든 함수는 서로 다른 프로토타입 객체를 가집니다. 함수가 생성자로 호출되면 새로 생긴 객체는 이 프로토타입 객체를 상속받습니다:

function f() {}
function g() {}

console.log(f.prototype, g.prototype, f.prototype === g.prototype);

let a = new f();
let b = new g();
console.log(Object.getPrototypeOf(a) === f.prototype);

callapply 메서드는 함수가 다른 객체의 메서드인 것처럼 동작하게 해요:

function f() {
  console.log(this);
}

let o = { x: 123 };

f();
f.call(o, 1, 2);
f.apply(o, [1, 2]);

bind는 함수 내 this 키워드에 해당하는 객체를 지정하고 인자 일부를 제공할 수 있어요:

// this 객체 지정하기
let obj1 = { x: 123 };
let obj2 = { x: 456 };
function f() {
  console.log(this.x);
}

let boundf1 = f.bind(obj1);
// ⚠️ 한 번 bind된 함수는 override 할 수 없습니다
let boundf2 = f.bind(obj1).bind(obj2);
boundf1();
boundf2();

// 인자 일부를 미리 제공하기
let sum = (x, y) => x + y;
let inc = sum.bind(null, 1);
console.log(inc(10));

// 함수 이름도 바뀌어요
console.log(inc.name);
console.log(inc.bind().bind().bind().bind().name);

Function 생성자로 함수를 만들 수 있고 전역 함수인 것처럼 동작합니다:

let sum = new Function('x', 'y', 'return x+y');
console.log(sum(1, 2));

Functional Programming

고차 함수(Higher order function)는 함수를 인자로 받거나 함수를 반환하는 함수입니다:

// 고차함수 예시
function not(f) {
  return function (...args) {
    // ⚠️ return !f(args)
    return !f.apply(this, args);
  };
}

Array.prototype.isEmpty = function () {
  return this.length === 0;
};

Array.prototype.notEmpty = not(Array.prototype.isEmpty);

console.log([].isEmpty(), [].notEmpty());
console.log([1].isEmpty(), [1].notEmpty());

합성함수 예제:

function compose(f, g) {
  return function (...args) {
    // 🤔 왜 하나는 call이고 하나는 apply인지 고민해보세요
    return f.call(this, g.apply(this, args));
  };
}

const sum = (x, y) => x + y;
const square = (x) => x * x;
console.log(compose(square, sum)(2, 3));