티스토리 뷰

책/자바스크립트 완벽 가이드

8장 함수

안양사람 2020. 11. 9. 00:44
728x90
SMALL

함수는 한 번 정의하면 몇번이든 실행할 수 있고 호출할 수 있는 자바스크립트 코드 블록이다.

함수는 매개변수라고 불리는 식별자 목록이 포함될 수 있는데 이 매개변수는 몸체 내에서 지역 변수처럼 취급된다. 함수 호출 시에는 함수의 매개변수로 값이나 arguments를 제공한다. 어떤 객체의 프로퍼티로 할당된 함수를 해당 객체의 메서드라 한다. 어떤 함수가 객체 상에서 호출되거나 객체를 통해 호출될 때, 이 객체는 호출 컨텍스트이거나 호출된 함수에서 this 값이다. 새로 생성된 객체를 초기화하는데 쓰이는 함수를 생성자라고 한다.

자바스크립트 함수는 다른 함수 내에 중첩되어 정의될 수 있고, 함수는 해당 함수가 정의되어 있는 유효범위에 속한 어떤 변수에도 접근할 수 있다. 이는 자바스크립트 함수가 클로저이며, 따라서 자바스크립트에서도 클로저라는 중요하고 강력한 프로그래밍 기법을 구사할 수 있음을 뜻한다.

함수 정의하기

함수는 function 키워드에 의해 정의되며, function 키워드는 함수 정의 표현식 또는 함수 정의 구문에서 사용된다. 표현식 형태와 구문 형태 모두, 함수 정의는 function 키워드로 시작하고, 그 뒤에 다음과 같은 구성요소를 가진다.

1. 함수 이름 식별자

2. 쉼표로 구분된 0개 혹은 임의 개수의 식별자들과 이 식별자들을 둘러싼 한 쌍의 괄호(식별자 : 매개변수 이름)

3. 0개 혹은 임의 개수의 자바 스크립트 구문을 포함하는 한 쌍의 중괄호(이 구문은 함수가 호출될때마다 실행)

함수 호출하기

자바스크립트 함수는 네 가지 방법으로 호출할 수 있다.

1. 일반적인 함수 형태

2. 메서드 형태

var calculator={
  operand1: 1,
  operand2: 1,
  add : function(){
    this.result=this.operand1+this.operand2;
  }
};
calculator.add();
calculator.result //2

3. 생성자

함수나 메서드 호출 앞에 new 키워드가 있다면, 그것은 생성자 호출이다. 생성자를 호출하면 생성자의 프로토타입 속성을 상속받은 새로운 빈 객체가 생성된다. 생성자 함수는 객체를 초기화하고, 새로 생성된 이 객체는 생성자 함수의 호출 컨텍스트로 사용된다.따라서 생성자 함수는 새로 생성된 객체를 this 키워드로 참조할 수 있다.

4. 해당 함수의 call()과 apply() 메서드를 통한 간접적 방식

call은 자신에게 주어진 전달인자를, 호출할 함수의 전달인자로 사용하고

apply는 값 배열을 전달인자로 사용한다.

함수 전달인자와 매개변수

생략 가능한 매개변수 

본래 정의된 것보다 적은 수의 전달인자로 함수가 호출되면, 나머지 매개변수는 undefined 값으로 설정된다.

함수를 정의할 때, 매개변수를 생략할 수 있음을 강조하려면 /*optional*/ 주석 사용

가변길이 전달인자 목록:Arguments 객체

함수가 호출될 때 정의된 매개변수보다 더 많은 인자가 전달되면, 매개변수 이름이 붙지 않은 인자 값을 직접적으로 참조할 방법은 없다. 이때 arguments[i]를 이용해 접근할 수 있다.

임의 개수의 전달인자를 받을 수 있는 함수는 varadic 함수, variable arity 함수, varargos 함수 등으로 부른다. arguments는 실제 배열이 아니라 Arguments 객체임을 기억하라. 함수안에서 arguments 값을 변경하면 전달인자도 바뀐다.

calle와 caller 속성

Arguments 객체는 배열 요소 외에도 callee와 caller 속성을 정의하고 있다.

var factorial=function(x){
  if(x<=1) return 1;
  return x*arguments.callee(x-1);

객체의 속성을 전달인자로 사용하기

어떤 함수에 세 개 이상의 매개변수가 있다면 인자의 올바른 순서를 기억하기가 어려워진다. 이를 해결하려면, 전달인자를 순서에 상관없이 이름/값의 쌍으로 함수에 전달하는 편이 효과적일 수 있다. 이런 방식으로 메서드 호출을 할 수 있게 하려면, 먼저 단일 객체를 전달인자로 받는 함수를 정의하고, 함수의 사용자에게 함수에서 요구하는 이름/값 쌍을 가진 객체를 함수의 인자로 넘기도록 하면 된다.

전달인자 형식

자바스크립트 메서드의 매개변수에는 정의된 형식도 없고, 함수에 전달한 값에 대해서 형식 검사도 하지 않는다. 함수 인자에 해당 인자를 잘 설명하는 이름을 선택하거나 주석에 인자 형식을 포함함으로써 코드 자체를 문서화하는데 도움을 줄 수 있다. 생략 가능한 전달인자에 대해서는 optional이라는 단어를 주석에 포함시키자. 그리고 메서드가 임의 개수의 인자를 받는다면 줄임표 부호를 사용하자.

한 두번만 사용하고 버릴 함수가 아니라면, 인자 형식을 검사하는 코드를 추가할 가치가 있다.

자스는 유연하며 형식을 느슨하게 처리하는 언어이기에, 때로는 인자 개수와 형식에 유연한 함수를 작성하는 데 적합

값으로서의 함수

함수의 가장 중요한 특징은 정의될 수 있고 또 호출될 수 있다는 점이다. 자스에서 함수는 문법뿐만 아니라 값이기도 한데, 이는 함수가 변수에 할당될 수 있고 객체의 프로퍼티나 배열 요소로 저장될 수도 있으며, 다른 함수의 인자로 전달될 수도 있고, 기타 여러 방식으로 사용될 수 있음을 뜻한다.

자스에서 함수는 원시 값이 아니지만 특별한 종류의 객체이고 이는 함수가 프로퍼티를 가질 수 있음을 의미한다. 만약 함수가 여러 번 호출되어도 그 값이 유지되어야 하는 정적 변수가 필요할 때는, 전역 변수를 선언해서 네임스페이스를 난잡하게 하기보다 함수의 프로퍼티를 사용하는 것이 대체로 편리하다. 오직 해당 함수만 사용하는 경우에는 함수 객체의 프로퍼티에 저장하는 편이 더 낫다.

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

네임스페이스로서의 함수

함수 내부에서 정의된 변수는 해당 함수가 정의된 함수 내부에서 접근할 수 있지만, 그 함수 바깥에는 존재할 수 없다. 간단한 임시 네임스페이스처럼 작동하는 함수를 정의하는 기법은 전역 네임스페이스를 어지럽히지 않고도 변수를 정의할 수 있기에 유용하게 사용된다.

모듈의 코드를 함수 내에 두고 그 함수를 호출하면 된다. 이런 방식으로 변수들을 전역 변수로 취급하는 대신 그 함수의 지역변수로 다룰 수 잇다. 하나의 속성(ex 함수)을 정의하는 것도 과하다면, 단일 표현식으로 익명 함수를 정의하고 호출할 수 있다.

(function(){ //일므이 없는 표현식으로 함수를 재작성
    //모듈 코드는 여기 위치
}()); //함수 리터럴을 끝내고 바로 호출

클로저

다른 현대 프로그래밍 언어와 마찬가지로 자스 또한 어휘적 유효 범위(lexical scoping)을 사용한다. 이는 함수를 호출하는 시점에서의 변수 유효 범위가 아니라, 함수가 정의되었을 때의 변수 유효범위를 사용하여 함수가 실행된다는 뜻이다. 함수 객체와 함수의 변수가 해석되는 범위(변수 바인딩의 집합)의 조합은 컴퓨터 과학 문헌에서 클로저(closure)라고 일컫는다.

중첩 함수의 어휘적 유효범위 규칙

var scope="global scope";
functio checkscope(){
  var scope="local scope";
  function f(){return scope;}
  return f();
}
checkscope()  //local scope


var scope="global scope";
functio checkscope(){
  var scope="local scope";
  function f(){return scope;}
  return f;
}
checkscope()()  //local scope

중첩 함수가 두 개 이상이라고 해도 같은 바깥쪽 함수에 정의되고 같은 유효범위 체인을 공유

function counter(){
  var n=0;
  return {
    count: function() { return n++; },
    reset: function() { n=0; }
  };
}
var c=counter(), d=counter();
c.count()  //0
d.count()  //0
c.reset()
c.count()  //0
d.count()  //1

클로저 기법과 getter/setter 프로퍼티 결합

function counter(n){
  return {
    get count() { return n++; },
    set count(m) {
      if(m>=n) n=m;
        else throw Error("count는 오직 더 큰 값으로만 설정될 수 있습니다.");
    }
  };
}

var c=counter(1000);
c.count  //1000
c.count  //1001
c.count=2000
c.count //2000
c.count=2000  //에러

클로저를 사용하는 내부 프로퍼티 접근 메서드

function addPrivateProperty(o, name, predicate) {
  var value;
  o["get"+name]=function(){return value};
  o["set"+name]=function(v){
    if(predicate && !predicate(v))
      throw Error("set"+name+": 유효하지 않은 값 "+v);
    else
      value=v;
  };
}

var o={};
addPrivateProperty(o,"Name",function(x){return typeof x=="string";});
o.setName("Frank");
console.log(o.getName());
o.setName(0);  //error

클로저들이 공유해서는 안 되는 변수를 공유하는 실수를 발견하는 것 또한 중요하다

function constfunc(v){return function(){return v; };}
var funcs=[];
for(var i=0;i<10;i++) funcs[i]=constfunc(i);
funcs[5]()  //5



function constfuncs(){
  var funcs=[];
  for(var i=0;i<10;i++)
    funcs[i]=function(){return i;};
  return funcs;
}

var funcs=constfuncs();
funcs[5]()
//열개의 클로저 모두 i(10) 값을 공유

클로저를 작성할 때 기억해야 할 또 다른 사항은, this가 자스 키워드이지 변수가 아니라는 점이다.

arguments도 마찬가지다.

따라서 바깥쪽 함수가 별도의 변수로 저장하지 않으면 클로저는 바깥쪽 함수의 this, arguments에 접근할수 없다.

함수 프로퍼티, 메서드, 생성자

length 프로퍼티

arguments.length(실제로 전달된 인자의 개수)

arguments.callee.length(요구하는 인자의 개수)

function check(args){
    var actual=args.length;
    var expected=args.callee.length;
    if(actual!==expected)
        throw Error("Expected "+expected+"args; got "+actual);
}
function f(x,y,z){
    check(arguments);
    return x+y+z;
}

prototype 프로퍼티

모든 함수에는 prototype 프로퍼티가 있는데, 이 프로퍼티는 프로토타입 객체를 참조한다. 모든 함수는 서로 다른 프로토타입 객체를 가지고 있다. 함수가 생성자로 사용될 때, 새로 생성된 객체는 함수의 프로토타입 객체로부터 프로퍼티들을 상속받는다.

call()과 apply() 메서드

어떤 함수를 간접적으로 호출할 수 있게 하며, 특정 함수를 다른 객체의 메서드인 것처럼 다룰 수도 있다.

첫번째 인자는 호출 컨텍스트이고 함수 몸체에서 this 키워드의 값이 된다. 함수 f()를 객체 o의 메서드로 호출하려면 f.call(o);  f.apply(o);를 사용할 수 있다.

엄격한 모드에서 첫번째 인자는 함수 내에서 this 값이 되는데, 인자로 주어진 값이 원시 값이든 null이든 undefined든 상관없다. 일반 모드에서는 null이나 undefined 값은 전역 객체로 바뀌고 원시 값은 이에 상응하는 래퍼 객체로 바뀐다.

apply는 함수에 전달하려는 인자가 배열이라는 것만 다르다.

함수가 임의 개수가 인자를 받도록 정의되었다면, apply() 메서드는 임의 길이의 배열을 사용하여 해당 함수를 호출할 수 있다.

ex) ar biggest = Math.max.apply(Math, array_of_numbers);

apply()는 실제 배열과 마찬가지로 유사 배열 객체와도 잘 작동한다. 특히 arguments 배열을 직접 apply()에 넘김으로써, 다른 함수를 호출할 때 현재 함수에 전달된 인자와 같은 인자를 전달할 수 있다.

ex) trace() 함수로 객체와 메서드 이름이 전달된다. trace함수는 지정된 메서드를 새로운 메서드로 교체하는데 새로운 메서드는 원본 메서드를 추가 기능으로 둘러싼다.

기존 메서드를 동적으로 수정하는 이런 방식은 보통 'monkey-patching'이라 한다.

function trace(o,m){
    var original=o[m];
    o[m]=function(){
        console.log(new Date(), "entering: ",m);
        var result=original.apply(this,arguments);
        console.log(new  Date(), "Exiting:",m);
        return result;
    };
}

bind() 메서드

bind()의 주요 목적은 함수와 객체를 서로 묶는 것이다.

function f(y){return this.x+y;}  //바인드 되어야 하는 함수
var o={x:1};                     //바인드될 객체
var g=f.bind(o);                 //g(x)를 호출하면 o.f(x)가 호출한다.
g(2)                             //3  

//o의 메서드로서 f를 호출하는 하수를 반환한다. 인자 또한 모두 전달된다.
function bind(f,o){
  if(f.bind) return f.bind(o);
  else return function(){
    return f.apply(o.arguments);
  };
}

ECMAScript 5의 bind() 메서드는 단지 함수를 객체에 바인딩하는 것보다 더 많은 일을 한다. bind() 메서드는 파셜 애플리케이션을 구현하는데, bind()에 전달하는 인자 중 첫번째 이후의 모든 인자는 this 값과 함께 바인딩된다. 파셜 애플리케이션은 커링이라 부르기도 한다.

var sum=function(x,y){return x+y}; //두 인자의 합을 반환한다.
//sum과 비슷한 새 함수를 생성하지만, this 값은 null로 바인딩되고
//첫번째 인자는 1로 바인딩된다. 새로운 함수는 단지 하나의 인자만 요구한다.
var succ=sum.bind(null,1);
succ(2)                             //3  x는 1에 바인딩되고 y인자로 2를 넘긴다

function f(y,z){return this.x+y+z}; //합계를 구하는 다른 함수
var g=f.bind({x:1},2);              //this와 y를 바인딩한다.
g(3)                                //6  this.x는 1에 바인딩되고 y는 2에, z는 3에 바인딩된다

ECMAScript 5에 정의된 bind() 메서드의 특징

1. 실제 bind()메서드는 함수 객체를 length 프로퍼티와 함께 반환하는데, 이 length 프로퍼티는 바인딩된 함수에 정의되어 있는 인자 개수에서 바인딩된 인자의 수를 뺀 값이다.(0보다 작지는 않다.)

2. bind() 메서드는 함수 생성자에 대한 파셜 애플리케이션으로 사용될 수 있다.

toString() 메서드

명세는 이 메서드가 함수 선언 구문 다음에 나오는 문자열을 반환하고 요구하지만 대부분 toString() 메서드의 구현체들은 함수의 전체 소스코드를 반환한다.

Function() 생성자

함수 정의 구문, 함수 리터럴 표현식 두 경우 모두 function 키워드를 사용하여 함수를 정의한다. 하지만 함수는 또한 Function() 생성자를 통해서도 정의될 수 있다.

ex) var f=new Function("x", "y", "return x*y;");

Function() 생성자는 임의 개수의 문자열 인자를 요구한다. 마지막 인자는 함수 몸체에 대한 텍스트이고, 세미콜론으로 구분된 임의의 자스 구문을 포함할 수 있다.

Function() 생성자에는 몇 가지 중요한 점이 있다.

1. 동적으로 자스 함수를 생성하고 실행 시간에 컴파일되는 것을 가능케 한다.

2. 생성자가 호출될 때마다 함수 몸체를 분석하고 새로운 함수 객체를 생성한다. 루프 내부 또는 자주 호출되는 함수 내에서 생성자를 호출한다면 이는 비효율적이다. 이와 반대로 함수 정의 표현식은 중첩 함수와 루프 내에 있더라도 매번 재컴파일 되지 않는다.

3. 마지막으로 Function() 생성자와 관련하여 매우 중요한 점은 함수 생성자가 생성하는 함수는 어휘적 유효범위를 사용하지 않는다. 함수 생성자가 생성한 함수는 언제나 최상위 레벨 함수로 컴파일 된다.

Function() 생성자는 전역 유효범위에서 eval()을 사용한다고 생각하면 된다.

호출가능한 객체

호출가능 객체는 함수 호출 표현식을 통해 호출할 수 있는 객체다. 모든 함수는 호출 가능 객체지만, 호출 가능 객체가 모두 함수는 아니다. IE 웹브라우저는 Window.alert()와 Document.getElementsById() 같은 클라이언트 측 메서드를 구현한는데, 원시 Function 객체가 아니라 호출 가능한 호스트 객체를 사용했다. IE9에서는 진짜 함수를 사용하도록 바뀌었고 호출 가능 객체는 점차 없어지고 있다.  호출 가능 객체의 또 다른 일반적인 형태는 REGeXP 객체다.

만약 어떤 객체가 진짜 함수 객체(그리고 함수 메서드를 가지고 있는지)인지를 알아보고 싶다면

function isFunction(x){
  return Object.prototype.toString.call(x)==="[object Function]";
}

함수형 프로그래밍

함수로 배열 처리하기

var sum=function(x,y){return x+y;};
var square=function(x){return x*x};
var data=[1,1,3,5,5];
var mean=data.reduce(sum)/data.length;
var deviations=data.map(function(x){return x-mean;});
var stddev=Math.sqrt(deviations.map(square).reduce(sum)/(data.length-1));

고차함수

고차함수 : 하나 이상의 함수를 인자로 받고, 새 함수를 반환하는 함수.

//이 고차함수는 자신의 인자를 f에 전달하고
//f의 반환 값에 대해 논리적 부정을 계산하는 함수를 반환한다.
function not(f){
  return function(){
    var result=f.apply(this,arguments);
    return !result;
  };
}

var even=function(x){
  return x%2===0;
};

var odd=not(even);
[1,1,3,5,5].every(odd);  //true

 

function mapper(f){
  return function(a){return a.map(f);};
}

var increment=function(x){return x+1;};
var incrementer=mapper(increment);
incrementer([1,2,3])
//책 오류라서 코드내가고침...

 

function compose(f,g){
  return function(){
    return f.call(this,g.apply(this,arguments));
  };
}

var square=function(x){return x*x;};
var sum=function(x,y){return x+y;};
var squareofsum=compose(square,sum);
squareofsum(2,3)  //25

함수의 파셜 애플리케이션

function array(a,n){return Array.prototype.slice.call(a,n||0);}
function partialLeft(f /*...*/){
    var args=arguments;
    return function(){
    	var a=array(args,1);
        a=a.concat(array(arguments));
        return f.apply(this,a);
    };
}
function partialRight(f /*...*/){
    var args=arguments;
    return function(){
        var a=array(arguments);
        a=a.concat(array(args,1));
        return f.apply(this,a);
    };
}
function partial(f /*...*/){
    var args=arguments;
    return function(){
        var a=array(args,1);
        var i=0,j=0;
        for(;i<a.length;i++)
            if(a[i]===undefined) a[i]=arguments[j++];
        a=a.concat(array(arguments,j))
        return f.apply(this,a);
    };
}

var f=function(x,y,z){return x*(y-z);};
partialLeft(f,2)(3,4)
partialRight(f,2)(3,4)
partial(f,undefined,2)(3,4)

 

var increment=partialLeft(Math.sum,1);  //책오타... 엄청많네
var cuberoot=partialRight(Math.pow,1/3);
String.prototype.first=partial(String.prototype.charAt,0);
String.prototype.last=partial(String.prototype.substr,-1,1);

function compose(f,g){
  return function(){
    return f.call(this,g.apply(this,arguments));
  };
}

var not=partialLeft(compose,function(x){return !x;});
var even=function(x){return x%2===0;};
var odd=not(even);
var isNumber=not(isNaN);

var data=[1,1,3,5,5];
var sum=function(x,y){return x+y;};
var product=function(x,y){return x*y;};
var neg=partial(product,-1);
var square=partial(Math.pow,undefined,2);
var sqrt=partial(Math.pow,undefined,.5);
var reciprocal=partial(Math.pow,undefined,-1);

var mean=product(reduce(data,sum),reciprocal(data.length));
var stddev=sqrt(product.reduce(map(data,
                                   compose(square,
                                           partial(sum,neg(mean)))),
                               sum),
                        reciprocal(sum(data.length,-1)));

메모이제이션

이전 계산 결과를 캐시해두는 함수를 정의하기도 하는데 함수형 프로그래밍에서는 이러한 캐싱 방식을 메모이제이션이라고 부른다. 다음 함수는 인자로 함수를 받아들이고 그 함수의 실행 결과를 저장한 버전을 반환한다.

function memoize(f){
    var cache={};
    
    return function(){
        var key=arguments.length+Array.prototype.join.call(arguments,",");
        if(key in cache) return cache[key];
        else return cache[key]=f.apply(this,arguments);
    };
}

function gcd(a,b){
    var t;
    if(a<b) t=b,b=a,a=t;
    while(b!=0) t=b,b=a%b,a=t;
    return a;
}

var gcdmemo=memoize(gcd);
gcdmemo(85,187)  //17

var factorial=memoize(function(n){
    return (n<=1)?1:n*factorial(n-1);
});
factorial(5)    //120

 

www.zerocho.com/category/JavaScript/post/579248728241b6f43951af19

728x90
LIST

' > 자바스크립트 완벽 가이드' 카테고리의 다른 글

10장 정규 표현식을 사용한 패턴 매칭  (0) 2020.11.14
9장 클래스와 모듈  (0) 2020.11.12
7장 배열  (0) 2020.11.08
6장 객체  (0) 2020.11.07
5장 구문  (0) 2020.11.06
댓글
공지사항