Front-end/Javascript

Effective Javascript(3,4장)

c62 2023. 7. 30. 02:38

3. 함수 사용하기


item18. 함수, 메서드, 생성자 호출의 차이를 이해하라

하나의 생성자 function을 사용하는 세가지 서로 다른 패턴
  1. 함수호출 (function hello(){})
    1. this 사용이 가능하지만 함수로써 호출되면 수신자가 전역객체가 되어 유용하지 않음
  2. 메서드 -> 함수로 동작하는 객체의 프로퍼티
    1. 메서드 호출시 호출 표현식 스스로가 수신자 객체 this로의 바인딩을 결정함
    2. 보통 어떤 객체의 메서드를 호출함면 메서드를 찾고, 해당 객체를 수신자 객체로 사용
  3. 생성자(new 연산자로 실행)
    1. this값으로 새로운 객체를 전달하고, 암묵적으로 이 객체를 결과로 반환
    2. 객체를 초기화 하는게 주요 역할

 

! strict mode에서 this의 기본바인딩이 undefined로 변경됨

item19. 고차 함수에 익숙해져라

다른 함수를 인자로 받거나 그 결과로 함수를 반환하는 함수
  • 인자로 받는 함수 -> 콜백함수
  • 중복된 코드가 많아지면 고차함수 추상의 신호
  • 공통부분을 고차함수로 만들고 분리되어야할 로직을 callback 함수로 받아서 실행
  • 라이브러리에 포함된 고차 함수의 사용에 익숙해져라 -> map(), reduce() ...
  • 고차함수로 대체될수 있는 공통 코딩 패턴을 찾는 방법을 익혀라 -> 반복되는 코드를 고차함수로 구현

item20. 지정된 수신자 객체로 함수를 호출하기 위해 call 메서드를 사용해라

함수나 메서드의 수신자객체( this에 바인딩되는 값 )는 호출자의 문법에 의해 결정된다.

프로퍼티를 객체에 임의적으로 추가하는것은 나쁜 사례이고 직접 생성하지 않은 객체라면 더욱 그렇다

 

call 내장 메서드

f.call(obj, arg1, arg2) // obj => 지정할 수신자 객체
  • 삭제, 수정, 오버라이딩 된 메서드를 호출하는데 도움
var dict = {}
var hasOwnProperty = dict.hasOwnProperty;
dict.foo = 1
delete dict.hasOwnProperty;
hasOwnProperty.call(dict, 'foo');	// true
hasOwnProperty.call(dict, 'hasOwnProperty')	// false

//이미 hasOwnProperty 가 참조하는 메소드는 삭제되었지만 호출이 가능함
  • 고차함수 정의에도 유용
var table = {
	...,
    forEach: function(f, thisArg) {
    	var entries = this.entries
    	for ... {
        	f.call(thisArgs, this.entry.key entry.value, i)
        }
    }
}

table1 = new table()
table1.addEntry(1, 'test')
table2 = new table()

table1.forEach(table2.addEntry, table2)	

/** 
table1의 forEach를 통해 table2의 addEntry를 table2의 this로 바인딩해서 반복실행
 => table1의 forEach를 실행하므로 entries는 table1의 것이고 f.call 시 table2의 this와 바인딩되어 
 => table2의 this.entries에 추가될것임
 **/

item21. 다른 개수의 인자로 함수를 호출하기 위해 apply를 사용하라

함수 호출시 가변인자(인자의 갯수가 제한이 없는것)를 사용할때 apply 내장 메소드를 사용
var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

첫번째 인자로 함수가 호출된 this의 바인딩을 명시할 수 있음

 


item22. 가변 인자 함수를 생성하기 위해 arguments를 사용하라

function average()  {
    for(var i = 0, sum=0, n = arguments.length;
        i < n; i++) {
        sum += arguments[i]
    }
    return sum/n;
}
모든 함수에 arguments라는 암묵적인 지역변수를 전달함
arguments 객체는 실제 인자의 배열과 비슷한 인터페이스를 제공함
length 프로퍼티는 몇개의 인자가 전달되었는지 나타냄

+ 사용자가 apply 메서드를 사용할 필요없이 고정인자 버전의 가변인자 함수를 추가로 제공하는것을 고려해야함

function average() {
	return averageOfArray(arguments)
}
// 가변인자 함수를 고정인자 버전에 위임하기 위한 wrapping 함수

item23. 절대 arguments 객체를 수정하지 마라

함수의 arguments 객체와 이름이 지정된 파라미터 사이의 관계는 불안정하여
arguments를 수정하면 이름이 지정된 파라미터의 값이 달라지는 위험이 있음

arguments를 안전하게 복사하는법

var args = [].slice.call(arguments);	//array type instance를 반환

arguments 객체는 절대 수정하면 안됨

 


item24. 변수를 사용해 arguments의 참조를 저장하라

arguments 객체를 바로 접근하는 경우 호출 시점에 따라 다른 객체의 arguments 를 참조할 수 있음

따라서, arumnet를 사용할때 지역변수로 선언해 스코프를 제한해 주는것이 필요

 


item25. 고정된 수신자 객체로 메서드를 추출하기 위해 bind를 사용하라

  • 수신자 객체를 인자로 받는경우
var buffer = {
	entries: [],
    add: function(s){
    	this.entries.push(s)
    }
}

var source = ['1', '2', '3']
source.forEach(buffer.add);	// entries가 정의되지않음 오류!!

source.forEach(buffer.add, buffer)	// 수신자 객체를 전달하여 해결
  • 수신자 객체를 인자로 받지 않는경우
...

source.forEach(function(s) {
	buffer.add(s)
})
// 지역함수를 만들어 스코프를 제한
  • 특정 객체를 수신자로 받는 것을 function 객체의 메소드로 제공
source.forEach(buffer.add.bind(buffer));

buffer.add === buffer.add.bind(buffer)	//false

bind메서드는 새로운 함수를 생성하여 공유되는 함수를 호출하는데에도 안전함

 


item26. 커링 함수에 bind를 사용하라

커링은 함수를 그 인자의 부분집합으로 바인딩 하는 기법
  • bind를 사용하여 인자의 고정된 부분집합을 가지는 위임 함수를 만들수 있음
  • 수신자 객체를 무시하는 함수를 커링할때는 수신자 객체 인자로 null, undefined를 전달

 


item27. 코드를 캡슐화하기 위해 문자열보다 클로저를 사용하라

eval 사용시 전역변수로 간주되어 실행되므로 함수 내에서 호출되는경우 지역변수를 참조하지 못해 에러가 발생할 수 잇음

따라서 eval 대신 함수를 전달받아 실행하면 클로저 내부의 지역변수를 안전하게 참조할 수 있음

 

  • eval로 실행되는 api 에 전달한다면 문자열로 된 지역변수를 절대 포함해선 안됨
  • 문자열을 전달받아 eval 함수 실행 대신 함수를 전달받아 호출하는 방식을 권장

 


 

item28. 함수의 toString 메서드에 의존하지 마라

  1. ECMAScript 표준 함수의 toString 메서드의 결과는 엔진에 따라 달라질수 있고 함수내용을 리턴하지 않을 수 있음
    1. bind함수처럼 다른 프로그래밍언어로 구현된 경우 함수내용이 리턴되지 않음
  2. 하나의 자바스크립트 에선 제대로 작동하지만 다른 곳에서 실패하는 프로그램이 만들어지기 쉬움
  3. toString으로 생성된 소스코드는 내부 변수 참조에 연관된 클로저의 값을 표현하지 못함

 


item29. 비표준 스택 검사 프로퍼티를 사용하지 마라

몇몇 오래된 호스트 환경에서 모든 인자 객체는 아래 두개의 프로퍼티를 지원함
arguments.callee: 인자와 함께 호출한 함수를 참조하는 프로퍼티
arguments.caller: 호출한 함수를 참조하는 프로퍼티

재귀적으로 참조할때 사용되긴 하지만 직접 함수이름을 참조하는것이 좋음

  • arguments.caller, callee 사용 자제

 

4. 함수 사용하기


item30. prototype, getPrototypeOf, __proto__의 차이점을 이해하라

  • C.prototype은 new C()로 생성된 객체의 프로토타입을 만드는데 사용된다
  • Object.getPrototypeOf(obj)는 obj의 프로토타입 객체를 가져오기 위한 표준 ES5 메커니즘
  • obj.__proto__는 obj의 프로토타입 객체를 가져오는 비표준 메커니즘

 

prototype

  • 특정 인스턴스에서 직접 찾을수 있는 프로퍼티는 직접 반환
  • 직접 찾을 수 없는 프로퍼티는 인스턴스의 prototype 에서 찾게됨

Object.getPrototypeOf

  • 현재 객체의 프로토타입을 가져오는데 사용할 수 있음
  • Object.getPrototypeOf(u) === User.prototype     => true

__proto__

  • 위 두가지 방법을 지원하지 않는 환경에서 임시 방편으로 유용
  • u.__proto__ === User.prototype    => true

 

js에서 클래스는 생성자 함수와 클래스의 인스턴스 간에 메서드를 공유하기 위해 사용되는 프로토타입 객체의 조합이다

item31. __proto__보다 Object.getPrototypeOf를 사용하라

__proto__를 지원하는 엔진이더라도 실행환경마다 다르게 처리하는 부분이 많음

  • __proto__를 지원하고 ES5를 지원하지 않는 환경에서는 Object.getPrototypeOf를 구현해서 사용하는것을 권장

 


item32. __proto__를  절대 수정하지 마라

__proto__에서는 Object.getPrototypeOf에서 지원하지 않는
prototype링크를 수정할수 있는 기능을 제공하는데 이식성이 나쁘기때문에 피해야함
  • 성능때문에
    • 상속구조 자체를 변경하는 행위
    • 일반적인 프로퍼티 수정보다 훨씬 더 많은 최적화를 무효화 시킬수 있음
  • 예측가능한 동작을 유지하기 위해
    • 객체 전체의 전체 상속 체계를 교체하는것과 같음

=> 임의로 지정된 프로토타입 연결을 가지는 새로운 객체를 생성하기 위해서는 Object.create를 사용

 


item33. 생성자가 new와 관계 없이 동작하게 만들어라

new 키워드 없이 인스턴스 생성시 함수의 수신자 객체는 전역객체가 되어
인스턴스는 undefined가 되고 프로퍼티들은 전역변수로 생성됨
strict mode 에서는 수신자 객체가 undefined가 되어 생성 자체가 불가함
this 의 instanceof를 확인하여 어떻게 호출되어도 생성자처럼 동작하는 함수를 제공하는것이 견고하지만 
추가적인 함수 호출이 필요하기때문에 비용이 추가로 든다는점이 단점이다.
따라서 Object.create를 사용함

Object.create

  • 프로토타입 객체를 받아 이를 상속받는 새로운 객체를 반환

함수가 new로 호출되기 원한다면 이에 대해 문서화를 해야함

 


item34. 메서드를 프로토타입에 저장하라

프로토타입에 어떤 것도 정의하지 않고 구현할시
여러 인스턴스를 만들었을때 각 인스턴스는 모든 함수 객체가 저장되어 있음
  • 프로토타입 탐색을 고도로 최적화한 엔진때문에 객체에 메서드를 목사하는것이 속도 개선에 도움이 딱히 안됨
  • 인스턴스 메서드는 프로토타입 메서드에 비해 더 많은 메모리를 사용함

item35. 비공개 데이터를 저장하기 위해 클로저를 사용하라

클로저는 내포한 변수에 데이터를 저장하고 직접적인 접근을 제공하지 않는다
내부에 접근 가능한 유일한 방법은 함수에 클로저의 접근을 명시적으로 제공하는것뿐
function User(name, passHash) {
	this.toString = function () {
    	return "[" + name + "]"	//this의 프로퍼티가 아닌 변수로 참조
    }
    this.checkPass = function(pass){
    	return hash(pass) === passHash	// 외부의 코드는 name, passHash에 직접 접근할 수 없음
    }
}

단점

  • 생성자의 변수가 그 변수를 사용하는 메서드의 스코프 내에 있게하기 위해서, 반드시 이 메서드가 인스턴스 객체에 위치해야 함
  • 메서드 복사가 금증하는 결과를 초래할 수 있음

 


item36. 인스턴스의 상태는 인스턴스 객체에만 저장하라

프로토타입과 인스턴스는 1대 다의 관계
따라서, 인스턴스마다 저장해야할 데이터는 프로토타입에 저장해서는 안됨
  • 보통, 수정 불가능한 데이터를 프로토타입으로 공유하는것은 안전
  • 상태값을 가지는 데이터도 각 인스턴스와 공유할 의도라면 저장할수 있음
  • 메서드가 프로토타입 객체에서 찾을 수 있는 가장 흔한 데이터

item37. this의 명시적인 바인딩에 대해 이해하라

this는 가장 가까이서 둘러싼 함수에 의해 명시적으로 바인딩됨

명시적 바인딩 방법

  • map()과 같이 수신자 객체를 인자로 받는경우
  • 함수 내에서 지역변수에 this 바인딩으로 참조를 저장하는 경우 (var self = this)
  • 콜백함수에 bind메서드 사용 ( .bind(this))

item38. 하위 클래스 생성자에서 상위 클래스 생성자를 호출하라

상위 클래스 생성자는 하위 클래스 프로토타입이 생성될때가 아니라,
반드시 하위 클래스의 생성자로부터 호출되어야함
  • 하위 클래스 생성자에서 상위 클래스 생성자를 명시적으로 호출하고 이때 this를 명시적인 수신자 객체로 전달
  • 상위 클래스 생성자를 호출하지 않기위해 Object.create를 사용해 하위 클래스의 프로토타입 객체를 생성

=> 클래스 상속을 하는경우 prototype은 상속되지 않기때문에 상위 클래스의 prototype을 Object.create로 하위클래스에 객체를 생성해줘야함


item39. 상위 클래스 프로퍼티 이름을 절대 재사용하지 마라

  • 상위 클래스가 사용하는 모든 프로퍼티 이름을 인지해야함
  • 상위 클래스의 프로퍼티 이름을 절대 하위 클래스에서 재사용하지 말아야 함

 


item40. 표준 클래스를 상속하지 마라

표준 라이브러리의 클래스들은 특별해서 제대로 작동하는 하위 클래스를 작성하기란 불가능함

표준 클래스의 [[Class]] 프로퍼티가 "Array" 등의 특수한 값을가지는데

확장시킨 하위 클래스의 인스턴스는 [[Class]] 로 "Object"를 갖게됨

 

따라서, 표준 클래스를 상속하는대신 프로퍼티로 위임하는것이 좋음

function Dir(path, entries){
	this.path = path
    this.entries = entries	// 배열 프로퍼티
}

 


item41. 프로토타입을 세부 구현 사항처럼 처리하라

  • 객체는 인터페이스이고 프로토타입은 구현 세부 사항이다
  • 직접 제어하지 않는 객체의 프로토타입 구조를 검사하지마라
  • 직접 제어하지 않는 객체의 내부를 구현하는 프로퍼티를 검사하지마라

item42. 무모한 몽키 패칭을 하지 마라

프로토타입은 객체로서 공유되기 때문에 누구든 프로토타입을 추가, 삭제, 수정이 가능하고 이것을 몽키패칭 이라고 부름

공유된 프로토타입을 수정하는 모든 라이브러리는 명백하게 그 동작에 대해 문서화 해야함

 

표준 API가 없는 경우 폴리필을 제공하기 위해 몽키패칭을 사용하라