JS를 함수형 프로그래밍에 어울리도록 하는 것이 고차함수이다. JS로 코드를 짜봤으면 자신도 모르게 썼을 가능성이 있다. 흔히 쓰인단다.
고차함수를 이해하기 전에 먼저 함수형 프로그래밍이 뭔지 그리고 일급함수( First-Class Functions )을 먼저 알아야 한다.
함수형 프로그래밍이 뭔가 ?
함수를 다른 함수의 파라미터 값으로 보내는 것 그리고 리턴도 함수로 하는것 이라고 간단하게 설명할 수 있다.
함수형 프로그래밍을 지원하는 언어는 JavaScript, Haskell, Clojure, Scala, Erlang 등이 있단다.
그럼 일급 함수는 뭔가 ?
JS는 함수들을 일급 시민( First-Class citizens )으로 취급한다. 왜냐면 함수는 객체이기 때문이다. 다른 언어도 같은 이유이다.
JS에서 함수는 그냥 객체가 아니라 함수객체이다. 예를 들면
// 함수 선언
function greeting() {
console.log('Hello World');
}
// 함수 호출
greeting(); // 'Hello World'
// 객체에 하는 것 처럼 함수에 프로퍼티를 추가할 수 있다.
greeting.lang = 'English';
console.log(greeting.lang); // 'English'
위의 코드의 경우 그렇게 추천하는 방식은 아니다. 설명하려고 만든 코드이다. 객체를 다뤄야 할 일이 있으면 그냥 객체를 따로 만들어 다뤄야 한다.
object, string, number 등으로 뭔가를 하는 모든것은 함수와 함께 할 수 있다. 이것들을 함수의 파라미터로 보낼 수 있다. 변수에 할당 할 수 도 있다. 이게 함수가 일급 함수인 이유이다.
고차함수는 다른 함수에서 인자로 또는 리턴 값으로 작동하는 함수라고 할 수 있다.
예를 들어 Array.prototype.map, Array.prototype.filter, Array.prototype.reduce는 고차함수의 일부이다.
고차함수를 사용하는 것과 사용하지 않는 예제를 보자
- Array.prototype.map
map 메소드는 새로운 배열을 만든다. map은 콜백 함수로부터 모든 리턴 값을 받고 이 값들로 새로운 배열을 만든다.
// 고차함수를 사용하지 않은 코드
const arr1 = [1, 2, 3];
const arr2 = [];
for(let i = 0; i < arr1.length; i++) {
arr2.push(arr1[i] * 2);
}
console.log(arr2); // [ 2, 4, 6 ]
// 고차함수를 사용한 코드
const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
return item * 2;
});
console.log(arr2); // [ 2, 4, 6 ]
JS에서 코드를 실행한다면 코드가 실행된 범위는 거의 둘 중 하나이다. Global or Function 거의 여기에서 실행된다. 거의
우리가 프로그램을 실행시킨다고 가정하자. 우선 우리는 global 실행 문맥( context, 이거 뭐라해야됨.. ) 안에서 실행하게 될거다. 뭐 몇 개의 변수들이 global안에 선언 되어있을거고, 우린 이걸 호출할거다. 변수 호출을 그렇다 치지만 함수를 실행하게 되면 어떻게 될까?
1. JS는 새로운 실행문맥( 함수 객체 )을 만들거다. 이건 local 실행 문맥이된다.
2. 이 local 실행 문맥은 자신의 몇 개의 변수들을 갖고 있을거고 이 변수들 또한 실행 문맥에 따라 local 변수가 될거다.
3. 만들어진 실행 문맥은 실행 스텍( execution stack )에 들어가게 된다.
스텍이 끝나서 함수가 끝나면 ?
1. 스텍에서 local 실행문맥이 빠져나온다.
2. 함수는 자신의 리턴 값을 자신을 실행한 문맥으로 보낸다. 그 문맥이 실행 문맥이고 이게 global이나 local 실행 문맥일 수 있다. 리턴값이 없다면 undefined가 리턴된다.
3. 다음으로 local 실행 문맥이 박살난다. 그 안에 있던 로컬 변수들 까지 다 박살난다. 없어져버린다. 이게 중요하다. 사라진다 완전.
간단한 예시를 보자
let a = 3
function addTwo(x) {
let ret = x + 2
return ret
}
let b = addTwo(a)
console.log(b)
1. 우린 글로벌 실행 문백에 변수 a를 선언했다. 그리고 3을 할당했다.
2. 그리고 우린 글로벌 실행 문맥에 새로운 변수 addTwo를 선언했다. 그리고 함수를 addTwo에 할당했다. 함수의 괄호 안에 무엇이 들어있든지 일단 변수에 다 저장한다.
3. 그리고 우린 b라는 변수를 글로벌 실행 문맥에 선언했다. b라는 변수가 선언 되자마자 변수는 일단 undefined 값을 갖게 된다.
4. 변수 b의 = 연산자 다음에 있는 문법이 함수를 호출한다. 함수가 리턴값으로 뭘 뱉어내던지 그걸 변수 b에 할당한다.
5. 근데 함수가 호출되기 전에 JS는 글로벌 실행 문맥의 메모리에 먼저 가서 addTwo라는 변수가 있는지 확인한다. 있네? 심지어 함수네? 오키 실행.
6. 다음으로 파라미터로 받은 a를 글로벌 실행 문맥의 메모리에서 찾는다. 찾았다. 까보니까 3이 할당 되어 있다. 그럼 3을 들고 addTwo 함수에 넣는다. 이제 실행할 준비가 되었다.
7. 지금 이 순간 실행 문맥이 전환된다. 새로운 local 실행 문맥이 만들어졌다. 이 local 실행 문맥의 이름은 addTwo가 된다. addTwo 실행 문맥은 콜 스텍에 넣어진다.
8. addTwo 실행문맥은 이제 뭘 하게 될까? 제일 먼저 function addTwo( x )의 x를 로컬 실행 문맥에 변수로 선언한다. 그리고 파라미터로 받은 3을 변수 x에 넣는다.
9. 다음으로 ret 변수를 선언한다. 그리고 x를 찾기위해 로컬 실행 문백을 뒤진다. 3을 찾으면 x자리에 갖다 놓는다. 그리고 2를 더한다. 5가된다. 이 5를 변수 ret에 할당한다.
10. 그 다음으로 변수 ret의 값을 리턴한다. 리턴 하려면 ret을 찾아야 한다. 로컬 실행 문맥에서 찾는다. 찾았다. 이게 5를 갖고 있다. 찾은 5를 리턴한다. 그리고 함수가 종료된다.
11. 함수가 종료되고 로컬 실행 문맥 역시 박살난다. 사라졌다. 변수 x와 ret도 없어졌다. 콜 스텍에서 빠져나오고 리턴 값이 addTwo 실행 문맥을 호출한 실행 문맥으로 던져진다.
12. 그래도 박살난 addTwo 실행 문맥이 남긴 유산은 있다. 바로 5. 이걸 변수 b에 넣는다.
13. 콘솔 로그가 b를 콘솔에 찍어낸다.
여기까지가 저 간단한 코드를 JS가 실행하는 방법이다. 내가 참조한 블로그에서는 클로저를 알기 위해서 더 많은 예제를 봐야 한다고 했다.
오키 묻고 더블로 간다.
Lexical scope .. 이걸 도대체 한국말로 뭐라 해야 하는지 모르겠다. 그냥 렉시칼 스코프라고 한다.
아래 예제이다.
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
JS의 복잡함은 이루 말할수 없다. this 때문인거 같다.
로컬과 글로벌 실행 문맥에 변수가 있다. 그냥 이렇게 가정을 해보자. 만약에 로컬 실행 문맥 안에서 변수를 못 찾는다면( 없으니까 못찾는거다. 숨어있어서 못찾는거 그런거 아니다 ) JS는 이 실행 문맥을 호출한 실행 문맥으로 거슬러 올라간다. 찾을 때 까지 계속 올라간다. 만약 계속 못 찾았다 ? 그럼 undefined를 뱉어낸다.
이걸 알고 위의 예제를 보자
1. val1이라는 변수를 글로벌 실행 문맥에 선언한다. 그리고 숫자 2를 할당한다.
2. multiplyThis라는 새로운 변수를 선언한다. 그리고 정의한 함수를 변수에 할당한다.
3. multiplied라는 새로운 변수를 글로벌 실행 문맥에 선언한다.
4. 그리고 글로벌 실행 문맥의 메모리에서 multitplyThis라는 변수를 꺼낸다. 꺼낸게 함수다. 파라미터로 6을 던져주면서 실행한다.
5. 새로운 실행 문맥이 만들어진다. 이 문맥의 이름은 multitplyThis이다.
6. 로컬 실행 문맥을 실행하면 처음으로 변수 n을 선언한다. 그리고 파라미터 값 숫자 6을 할당한다.
7. 다음으로 ret이 이름인 변수를 선언한다.
8.9. 로컬 실행문맥의 메모리에서 변수 n과 val1을 찾는다. n은 6이라고 찾았는데 val1을 못 찾았다. 어찌해야되나? 문맥을 타고 올라간다. 올라가니 val1이 있다. 찾았다. 찾은 변수의 값을 곱한다. 12가 됬다. 12를 ret에 넣는다.
10. ret을 리턴한다. 로컬 실행 문맥이 박살난다. val1은 안 박살난다. 로컬 실행 문맥이 아니기 때문이다.
11. 리턴된 숫자 12가 mltipied 변수에 할당된다.
12. 12를 콘솔로 찍어낸다.
위의 과정중에서 변수를 찾기 위해 문맥을 타고 올라갔었다. 이 현상의 공식적인 이름이 바로 Lexical scope이다.
함수는 뭐든지 리턴 할 수 있다. 배열이든 함수든 변수들 뭐시기든 리턴할 수 있다.
아래 예제를 보자
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
1. 글로벌 실행 문맥에 val 변수를 선언하고 7을 할당한다.
2. 글로벌 실행 문맥에 변수 createAdder이름의 변수를 선언한다. 그리고 함수를 변수에 할당한다. 근데 여기서 { } 안에 있는 걸 건드지 않는다.
3. 글로벌 실행 문맥에 adder 변수를 선언한다. 이 순간 일시적으로 undefined가 adder 변수에 할당된다.
4. 그리고 createAdder()를 호출 하기 위해 글로벌 실행 문맥 안의 메모리를 뒤진다. 찾았으면 꺼내온다.
5. 함수를 실행한다. 이 때 새로운 로컬 실행 문맥이 만들어진다. js엔진은 이 로컬 문맥을 콜 스텍에 집어 넣는다. 함수는 받은 파라미터가 없다. 그럼 바로 함수 내부로 들어간다.
6. 우린 또 addNumbers 변수를 로컬 실행 문맥에 생성하고 저장한다. addNumbers 변수는 바로 이 createAdder 실행 문맥에만 존재한다.
7. createAdder 함수는 addNumbers 변수를 리턴한다.그리고 로컬 실행 문맥이 없어진다.
8. 원래 addNumbers 변수는 없어진다. 그런데 정의된 함수는 여전히 존재한다. 이게 adder 변수에 할당된다.
9. 새로운 변수 sum을 글로벌 실행 문맥에 선언한다. 그리고 일시적으로 undefined가 할당된다.
10. sum 변수에 adder 변수에 있는 함수를 실행해야 한다. 먼저 글로벌 실행 문맥의 메모리를 찾아본다. 정의된 함수가 있다. 가져온다.
11. 함수를 실행하기 전 2개의 파라미터를 보낸다. 7 과 8이다.
12. 이제 진짜 함수를 실행한다. 새로운 로컬 실행 문맥이 생성된다. 이 로컬 실행 문맥에 a, b 2개의 변수가 만들어진다. 각각 7과 8이 할당된다.
13. ret 이름의 새 변수가 선언된다.
14. a와 b를 더한다. 결과는 15이고 이걸 ret변수에 할당한다.
15. 함수로부터 ret 변수가 리턴된다. 그리고 이 로컬 실행 문맥이 파괴된다. 그리고 콜 스텍에서 빠진다. 변수 a, b, ret은 이제 없어진다.
16. 리턴된 값은 sum 변수에 할당된다.
17. sum을 콘솔에 찍는다.
위의 과정에서 우리가 알 수 있는건 정의된 함수는 변수에 저장된다라는 점, 정의된 함수는 호출되기 전 까지는 프로그램에 보이지 않는다는 점이다. 또한 함수가 실행되는 매 순간 새로운 로컬 실행 문맥이 생성된다는 것이다. 이 로컬 실행 문맥은 함수가 종료되면 사라진다.
1. 글로벌 실행 문맥에 createCounter 변수를 만든다. 그리고 함수 정의를 할당한다.
2. 새로운 변수 increment를 선언한다.
3. 글로벌 실행 문맥 메모리에서 createCounter 변수를 찾는다. 변수에 정의 된 함수가 있다.
4. createCounter 로컬 실행 문맥이 생성된다.
5. 로컬 실행 문맥에 counter 변수를 선언하고 0을 할당한다.
6. 새로운 변수 myFunction을 선언한다. 변수의 내용은 또 다른 함수 정의이다. 지금 우리는 클로저를 만들었다. 클로저는 현재 실행문맥( 지금은 createCounter 실행 문맥 )의 변수들을 갖고있다. 지금의 경우 counter 변수를 갖고 있다.
7. myFunction 변수를 리턴하면서 createCounter 실행 문맥이 삭제된다. myFunction과 counter 변수는 존재하지 않는다.
8. increment 변수에 createCounter의 리턴 값이 할당된다. increment는 함수 정의( myFunction이 갖고 있던 함수 정의, 이게 클로저가 됨 )를 갖고 있다.
9. 새로운 변수 c1을 선언한다.
10. 글로벌 실행 문맥에서 increment 변수를 찾는다. 찾았다. 이거 함수다.
11. 새로운 실행 문맥을 만든다. 함수에 파라미터가 없다. 함수를 실행한다.
12. counter = counter + 1 //// 이라는게 있다. counter를 찾는다. 근데 글로벌이나 로컬 실행 문맥을 찾기 전에 먼저 클로저( myFunction였던 함수 정의를 포함한 그 스코프의 모든 내용) 내부를 체크한다. 잉? 클로저가 변수 counter를 갖고 있네 ? 값이 0 이네? 그럼 1을 더한다. 이제 클로저의 counter 변수는 1이된다.
13. 다음으로 counter 값 즉 1을 리턴한다.
14. 리턴된 값 1 이 c1에 할당된다.
15. 나머지 c2, c3 반복 그리고 로그 찍힘
클로저 내부에 저장된 정보는 로컬 실행 문맥처럼 없어지지 않기 때문에 ( 뭔가는 그 정보를 계속 갖고 있으니까 ) counter에 1을 계속 추가하는게 가능하다.
즉 클로저는 함수 생성당시 같은 스코프에 존재하는 모든 변수의 수집( collection )이다. 함수가 함수를 리턴하면서 실행 문맥이 생성되고 파괴되는 그 점을 이용하여 클로저를 사용한다.
const numbers = [2, 4, 8, 10];
numbers.map( x => x * 2 );
multiple // [4, 8, 16, 20]
예제2
// What you have
var officers = [
{ id: 20, name: 'Captain Piett' },
{ id: 24, name: 'General Veers' },
{ id: 56, name: 'Admiral Ozzel' },
{ id: 88, name: 'Commander Jerjerrod' }
];
// What you need
[20, 24, 56, 88]
const officersIds = officers.map(officer => officer.id); // [20, 24, 56, 88]
map을 사용한 배열의 길이와 리턴된 배열의 길이가 같아야한다.
- reduce()
배열의 각 요소들을 체크하여 배열이 아니라 하나의 결과값을 반환한다.
array.reduce( (accumulator, current, index, source) => {
// return logic here
}, initialValue)
// accumulator -> 리턴된 값을 저장한다.
// current -> 현재 배열 요소
// index -> 현재 배열의 인덱스 값 / 잘 안씀
// source -> 원재 주어진 배열 / 잘 안씀
예제1
const euros = [1, 2, 3];
const sum = euros.reduce((total, amount) => total + amount);
sum // 6
이 예제에서 리듀스는 2개 total과 amount 파라미터를 받았다. 리듀스는 반복문 처럼 각 배열 값을 돈다.
리듀스 함수가 실행되면 total값은 배열의 가장 왼쪽에 있는 값으로 할당된다. 그리고 amount는 그 다음 순서의 값이 된다.
여기 예제에서는 최초 total은 1이고 amount는 2이다. 그래서 둘이 합쳐 3을 리턴한다. 다시 반복이 시작될 때 total은 리턴된 3 값으로 되어있고 amount는 다음 배열로 넘어가 3이된다. 여기서 리턴하면 6이된다. 오른쪽 배열의 값이 이제 없으므로 리듀스함수는 종료한다.
euros배열의 값들에 2를 곱해 다시 배열로 뱉었다. multi에 표현된 리듀스함수의 끝 인자 initialvalue는 total의 초기값을 정한다. 여기서 빈 배열 [ ] 을 초기값으로 줘서 total은 [ ]이 된거다. amount는 주어진 배열의 처음 값인 1 부터 시작하는거다.
이런식으로 리듀스 내부에 조건을 줘서 이것저것 할 수 있다.
( total, amount, initialvalue 파라미터의 이름들은 그냥 예제에서 쓴 이름이다 순서로 기억해야한다. )
주어진 값이 배열이라고 배열로만 뱉는건 아니다. 객체로 만들어서 뱉을 수도 있고 객체를 받을 수 도 있다.
// 함수를 만든다
function SuperHuman (name, superPower) {
this.name = name;
this.superPower = superPower;
}
// 생성자 함수의 prototype기능을 사용하여 SuperHuman함수에 usePower메서드를 만든다.
SuperHuman.prototype.usePower = function () {
console.log(this.superPower + "!");
};
// banshee가 SuperHuman함수를 상속(?) 받았다.
var banshee = new SuperHuman("Silver Banshee", "sonic wail");
// 상속받은 속성인 usePower() 메서드를 쓸 수 있다.
banshee.usePower(); // sonic wail!
위의 코드를 Object.create()를 통해 구현하면
// 메서드를 포함한 함수를 만든다.
var superHuman = {
usePower: function () {
console.log(this.superPower + "!");
}
};
// Object.create()를 사용하여 superHuman()함수를 상속 받는다.
var banshee = Object.create(superHuman, {
name: { value: "Silver Banshee" },
superPower: { value: "sonic wail" }
});
// 상속 받음.
banshee.usePower(); // sonic wail!
모든 객체는 Prototype으로서 사용될수 있기 때문에 Object.create()를 사용하면 체인처럼 상속을 계속 할 수 있다.
생성자 함수와의 차이점은 Object.create()는 instanceOf에서 발견되지 않는다는 점이다. 대신에 isPrototypeOf 메서드를 써야한다.
이게 뭔 말이냐면 객체의 프로퍼티나 메소드가 공유될 수 있다는 얘기이다. 상속같이 사용된다.
이걸 prototypical inheritance이라 부르고 이건 class inheritance와는 다르다. JS는 php, java, python등의 클래스 기반 언어와는 다르다. ( 클래스 문법은 있지만 기본 컨셉이 다르다. )
- JavaScript Prototypes
모든 JS 객체는 [[Prototype]]이라 부르는 내부 프로퍼티를 갖고 있다.
[[ ]] 는 코드에 직접적으로 접근할 수 없는 내부 프로퍼티를 나타낸다.
let x = {};
위의 코드가 우리가 보통 새로운 객체를 만드는 방법이다. 그런데 또 다른 방법으로 새로운 객체를 만들 수 있다.
let x = new Object();
객체 x의 내부 프로토타입이 뭐가 있는지 확인해보자
// .getPrototypeOf()를 사용하여 확인 할 수 있다.
Object.getPrototypeOf( x );
// 결과 값
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
위의 코드를 보면 여러가지 함수들이 존재한다는 것을 알 수 있다.
또 다른 방법으로 확인해보자. __proto__ 라는 프로퍼티를 통해 확인 할 수 있다. 이건 객체의 내부 [[Prototype]]을 보여주는 프로퍼티이다.
( __proto__는 레거시이다. 상업용 프로그램에는 포함되면 안된다. 모든 브라우저의 엔진안에 존재하는 건 아니다. )
// __proto__ 를 통해 내부 프로퍼티 확인
x.__proto__;
// 결과 값
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
[[Prototype]]은 다른 객체들과의 연결고리가 된다는 점에서 중요하다. 이건 Data, Array 처럼 객체 안에 내장되어 있다.
- Prototype Inheritance
만약 우리가 어느 객체의 프로퍼티나 메소드에 접근하려고 한다면, JS는 일단 객체 그 자체를 찾아보고 없다면 객체의 [[Prototype]]을 찾는다. 이것도 없다면 연결된 모든 객체의 [[Prototype]]을 체크한다. 연결된 객체가 없을 때 까지 계속 찾는다. 아무것도 안나오면 null이 반환된다.
모든 객체들은 Object( 부모 Object, 선언한거 말고 내장된거 )의 프로퍼티와 메소드를 상속받는다.
x.toString(); // [object Object]
위의 코드에서 우리는 빈 객체인 x를 만들었고 아무것도 없는데 .toString() 메소드를 불러냈다. 이게 부모 객체인 Object로부터 자동으로 상속받은 메소드라 불러낼 수 있는거다.
let y = []; OR let y = new Array();
y.__proto__; // [constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
Array도 __proto__로 뭐가 들어있는지 알 수 있다. Array도 빈 객체와 비슷하게 부모 Array에게서 다양한 메소드를 자동 상속받는다.
instanceof은 기존에 존재하거나 정의된 값을 체크하고 맞다면 true 아니라면 false를 반환하는 연산자이다.
var colorTrue = new String(“black”);
colorTrue instanceof String; // returns true
var colorFalse = “green”; // object is not specified
colorFalse instanceof String; // returns false, it is not a string.
위의 예제처럼 colorTrue 변수는 생성자 함수로 String값인 black을 만들어서 true가 나왔다.
colorFalse의 green은 String이지만 String값을 정의한게 아니어서 false가 나온다.
생성자함수( Function Constructor )에서 함수 안에 this는 생성된 객체( 모든 함수는 객체이다. in js )를 가리킨다.
// es5, es6로 작성된 아래 코드는 같은 결과다. 문법설탕됨
// ES5 ( ECAMAScript2009 )
function Car(make) {
this.make = make;
}
// ES6 ( ECAMAScript2015 )
class Car {
constructor(make) {
this.make = make;
}
}
const myCar = new Car("BMW");
myCar.make; // "BMW"
생성자 함수 안에 있는 this는 Car() 이 함수를 가리킨다. ( ?? )
근데 JS가 Prototype기반 언어라는걸 알고있는가 ?
JS에는 prototype link와 prototype object가 있는데 이걸 통칭해서 prototype이라 부른다.
( 나중에 prototype에 대한 내용은 #17 Prototype Inheritance and Prototype Chain [33 Concepts Every JavaScript Developer Should Know ]에서 포스팅 할거다 )
JS에 클래스 문법은 있지만 개념은 없다 ( ES6/7 기준 ). 그래서 다른 언어처럼 상속을 하고 싶을 때 JS는 함수와 new를 통해 상속을 흉내낸다.
- prototype와 this의 콜라보 예제
// 대충 공유 할 함수 하나 만든다.
// 이 생성자 함수 사람안에 this는 window가 아니라 사람을 가리킨다.
function 사람( someting ) {
this.someting = someting;
}
// 사람이 공유하게 될 눈, 코를 어딘가에 존재하는 사람함수의 prototype에 넣는다.
사람.prototype.눈 = 2;
사람.prototype.코 = 1;
// 사람이 공유하게 될 눈몇개임 함수를 만들어 prototype에 넣는다.
// 여기에 this는 눈몇개임 함수(객체)를 부르는 함수(객체)의 인스턴스를 가리킨다.
사람.prototype.눈몇개임 = function() { return this.someting + this.눈 };
// 선언한 변수 김씨, 박씨는 new를 사용하여 사람함수를 생성한다. 상속받는 것 처럼 생각하면된다.
var 김씨 = new 사람("김씨는 눈이 ");
var 박씨 = new 사람("박씨는 눈이 ");
// 결과
김씨.눈몇개임(); // '김씨는 눈이 2';
foo함수(객체)에 bar메소드를 만들고 this를 리턴했다. bar에서 리턴한 this는 결국 foo를 가리킨다. ㅇㅋ? ㅇㅋ
Binding this.
.bind 아는가 ?
만약 this를 특정 객체에 묶고 싶다 ? this가 막 날라다닌데 고정시키고 싶다 ? 그럼 bind 메소드를 써야한다.
const foo = function () { console.log(this) };
const boundFoo = foo.bind("always me!"); // always me!를 boundFoo에 묶어버렸다.
foo(); // window
boundFoo(); // "always me!"
const a = {
b: foo,
boundB: boundFoo
}
a.b(); // a
a.boundB(); // "always me" <- a를 안 가리킨다.
bar = function () {};
bar.prototype.x = foo;
bar.prototype.boundX = boundFoo;
const myBar = new bar();
myBar.x(); // myBar
myBar.boundX(); // "always me" <- myBar를 안 가리킨다.
.bind는 ( Function.prototype.bind ) 객체에 묶인 this와 함께 new 함수를 리턴한다. 근데 원래 this가 가리켜야 하는 함수를 호출하지 못 한다.
이 때문에 .call ( Function.prototype.call ) , .apply ( Function.prototype.apply ) 함수를 사용한다. this를 고정시키지는 않지만 this를 특정 객체에 묶는다.
call, apply는 받는 첫 번째 인자를 함수 내부에서 사용할 this로 만들어준다. 차이점은 apply는 첫 번째 인자를 제외한 나머지를 배열로 받는다는 점이다. 이거 빼고는 차이점이 없다.
call, apply 예제확인
// a, b 객체 생성
const a = { here: 'a!!' };
const b = { here: 'b@@' };
// this.here에 따라 값이 바뀌도록 만듬
const say = function( whoyouare ) {
console.log( ' my this belongs to ${this.here}, im ${ whoyouare }' );
}
// say함수의 this가 a를 가리킴
say.call( a, "playCall" ); // my this belongs to a!!, im playCall
say.apply( a, ["playApply"] ); // my this belongs to a!!, im playApply
// say함수의 this가 b를 가리킴
say.call( b, "playCall" ); // my this belongs to b@@, im playCall
say.apply( b, ["playApply"] ); // my this belongs to b@@, im playApply
// this가 가리킬게 없음
say("play"); // my this belongs to , im play
웹 브라우저들은 페이지를 로드할 때 트리구조로 돔을 만든다. 브라우저에 의해 트리구조로 변경된게 바로 DOM이다.
JS는 웹 페이지에 있는 모든 엘리멘트들에 접근 할 수 있다. DOM을 사용하면서 말이다. 돔을 사용하면서 엘리멘트와 속성값을 만들고 바꾸고 제거한다. 또한 이벤트를 작동시키고 새로운 이벤트를 만들기도 한다.
<html>
<head>
<title>DOM!!!</title>
</head>
<body>
<h1 id="one">Welcome</h1>
<p>This is the welcome message.</p>
<h2>Technology</h2>
<p>This is the technology section.</p>
<script type="text/javascript">
<!-- JS 코드 -->
var text = document.getElementById("one").innerHTML;
alert("The first heading is " + text);
</script>
</body>
</html>
위의 코드를 보면 html로 만들어진 페이지에 여러 엘리멘트들이 있다.
여기서 JS는 getElementById()와 innerHTML을 통해 특정 id의 엘리멘트에 접근하고 있다.
id 말고 tag로 접근 할 수 도 있다.
<html>
<head>
<title>DOM!!!</title>
</head>
<body>
<h1>Welcome</h1>
<p>This is the welcome message.</p>
<h2>Technology</h2>
<p id="second">This is the technology section.</p>
<script type="text/javascript">
var paragraphs = document.getElementsByTagName("p");
alert("Content in the second paragraph is " + paragraphs[1].innerHTML);
document.getElementById("second").innerHTML = "The orginal message is changed.";
</script>
</body>
</html>