김민태님께서는 어떠한 것을 만들든 그 사람 그 문화의 철학이 담겨있다고 하셨습니다.
미국(서양)문화권에서는 합리주의 즉 논리학이 담겨져 있어 모든것은 논리가 있고 어떠한 절대 불변의 법칙인 공리가 존재 한다고 합니다.
자바스크립트 또한 그러한 공리들은 변하지 않고 그것 위주로 하나씩 쌓아가면서 이해한다면 어려운 코드도 이해 가능하다고 합니다.
자바스크립트의 공리 중 하나는 "변수안에 값을 넣을수 있다." 이다.
입니다. 값이라면 모든 것을 변수안에 넣을 수 있고, 값이라고 하는것들은 원시타입 뿐만아니라 객체, Array, function등 다양한 것들이 값으로써 변수안에 들어갈수 있습니다.
그중 오늘은 함수! 라는 것에 대하여 배우는 시간을 가지게 되었습니다.
자바스크립트에서의 함수는 어떠한 값을 무조건 리턴해야만 합니다. 함수 내부에 return이 없다면 일반적으로는 undefined가 나오고 new라는 생성자로 만들면 비어있는 객체로 나오게 됩니다.
함수의 선언에는 두가지가 있습니다.
모든 자바스크립트의 코드는 문과 식 두가지로 이루어져 있습니다.
값이 나오면 식, 값이 나오지 않으면 문으로 문에는 세미콜론(;)이 붙지 않지만 식에는 붙어야됩니다.
자바스크립트는 알아서 붙여서 읽어주기때문에 문제는 생기지 않지만 위의 문장처럼 하는게 원칙입니다.
식 - 0; 1+10; foo(); 1 + 10 + foo(); a? "first":"second";, forEach();
문 - if문(if, switch/분기문, conditional 문)
for문(for, while/ 반복문)
위의 두가지로 나뉘어지는데
함수정의문의 예제로는
function 함수이름(x){
return y;
}
이렇게 되고,
함수식의 예제로는
const 변수이름 = function 함수이름(x){
return y;
}
함수식에서 함수이름은 참조값으로 변수에 들어가는순간부터 벗겨지기 때문에 콜백함수를 제외하고는 거의 익명함수로써 합니다.
익명함수는 이름이 없는 함수로써
function(){
return y;
}
이렇게 되는게 일반적이고 함수 명이 없기때문에 나중에 실행이 불가능하다.
따라서 즉시실행콜(immediatly funcitoncall)로 한번만 실행하는 initialize함수등에 많이사용한다.
-> 즉시 함수실행 예제
(function(){
return y;
})()
const boo = function boo(){ boo(); } boo();위의 코드에서 함수 내부의 boo()는 함수의 이름의 boo()이고 바깥쪽의 boo()는 변수의 boo()이다. 내부 콜백을 위해서는 함수이름이 필요하지만 변수에 넣는순간 함수이름은 사라지고 참조값이 변수에 담기게 된다.
일반적인 함수형태인
function foo(x){
return y
}
을 보면 x도 값이고, y도 어떠한 값으로 첫번째 공리라고 말했던 문장에서 값이기 때문에 함수가 들어갈수도 있다.
따라서 함수를 호출할때 인자로 함수를 넣는
const y = foo(function(){
})
그리고 함수의 호출을 foo라는 함수에 맡겨 버릴수 있다. 이를 콜백 함수라고 부른다.
그리고 인자로 함수로 주는데 return값도 함수로 나올때 이를 하이오더 함수(고차함수)또는 함수의 합성이라고한다.
function foo(x){
...
return function(){
...
}
}
위의 코드처럼 return이 함수일때 합성함수 또는 고차함수라고 한다.
화살표함수는 람다식 한줄함수라고도 합니다.
es5에서의
var 변수명 = function(x){
return y;
}
의 형태를
const 변수명 = (x) => {
return y;
}
이렇게 변화 시켜놓습니다. 원래의 함수식에서 function이라는 함수 생성자를 화살표로 바꾸고 위치도 바꾸어 위치를 보기쉽게 바꾸어 놓았습니다. (개발자의 손가락은 소중하답니다..)
위의 식에서 인자가 하나로만 오면 괄호는 생략이 가능합니다. 그리고 한줄로 작성할경우 return도 생략이 가능합니다.
ex)
const 변수명 = x => x*x;
const 변수명2 = (x) => function(){};
new 연산자는 빈객체를 만들어 함수한테 전달하고 함수안의 this는 이 객체를 가르키게 됩니다.
new연산자로 생성된 함수는 return이 없어도 return으로 인스턴트 객체를 내뱉게 됩니다.
여기서! const y = new foo();로 생성된 생성자인지 알고싶다면(변경이 안되었는지 알고싶다면)
console.log(y instanceof foo)라는 것으로 확인이 가능합니다.
const x = {};
x.key = value;
이렇게 원래는 없었지만 따로 넣어주는방법을 동적 바인딩이라고 합니다 .
어떤 모양의 객체가 필요할때의 값을 체크할때는 구조가 복잡할수도 있습니다.
그래서 instanceof로 constructor가 만든 객체인지 확인할수 있지만 함수를 new로 해야되는지 그냥 해야되는지 서로 암묵적인 약속을 해야만한다. (new연산자로 생성을 해야되면 함수 제일앞을 대문자로.)
이를 명시적으로 더 보기쉽게 만들기위해 es6에서 class가 생성되었습니다.
class bar {
constructor{
this.name = 10;
}
}
-----------------------------
const Foo = function(){
this.name = 10;
}
const bar = new Foo();
위 두개의 코드는 똑같은 결과를 만들어냅니다.
하지만 이는 차이가 몇가지 있습니다.
this가 결정되는 방식은 복잡하게 하면 한도 끝도 없다고합니다.
하지만 쉽게 말한다면 실행컨텍스에서 맥락상 소유자가 this입니다.
const person = {
name : "maintainker",
getName(){
return this.name;
}
}
console.log(person.getName());
여기의 함수의 호출에서 person이 함수의 소유자입니다. 따라서 여기서 this는 person이 됩니다.
실행 컨텍스트관련도 언급해주셨는데 실행컨텍스트는 어려워서 ㅠㅠ
여러 자료를 읽어봤지만 이해까지는 못했습니다.
여기서 소유자가 사라지는 순간들이 있습니다.
const man = person.getName;
console.log(man());
이렇게 되면 실행되는 순간 호출자(소유자)가 사라지게 됩니다. 이때 호출자가 없기때문에 호출자는 전역인 window 객체가 됩니다.
ex2.
button.addEventListener("click",person.getName());
위처럼 해도 소유자가 벗겨집니다. 따라서 this는 항상 무엇인지 확인해야됩니다.
함수를 실행시키는 방법에는 call 과 apply가 있습니다 .
여기서는 call과 apply 에 관련에서 설명을 하려합니다.
함수명.call(this, arg1,arg2)
함수명.apply(this, [arg1,arg2])
함수call을 이용하는방법은 위의 코드처럼 함수다음에 .call로 호출이 가능합니다. 그 첫번째 인자로 this가 될 것을 넣어줍니다. 여기서 비어있다면 window를 가르키게 됩니다. 그 다음부터는 원하는 인수들을 넣어주면 됩니다.
apply는 두개의 인수만 넣어줍니다. 두번째인수로는 원래 함수의 인수들의 배열을 넣어주면 됩니다.
하나의 예제를 들겠습니다(티스토리 beomy 예제 참조)// apply함수 콜도 비슷하므로 패스하도록 하겠습니다.
type = "zero";
var type1 = { type: "one" };
var type2 = { type: "two" };
function getType() {
console.log(this);
console.log(this.type);
}
getType(); // window, zero
getType.call(this); // window, zero
getType.call(type1); // type1, one
getType.call(type2); // type2, two
아래 4줄의 함수들이 가르키는 this들은 각각 // 이후 첫번째를 가르키므로 this.type은 그 다음에 적혀져있는 결과가 나옵니다.
소유자가 고정되어야 한다면 사라지지 않게 하기위해서 bind를 사용하여 고정도 가능하다
const man = person.getName.bind;
변수의 scope에따라 변수는 사라지지만 function 의 생성 컨텍스트에 저장되어 내부함수 에서만 접근이 가능하게 됩니다. 이를 클로저라고 합니다.
function makePerson() {
let age = 10;
return{
getAge(){
return age;
},
setAge(x){
age = x > 1 && x < 130? x: age;
}
};
}
위의 함수에서 makePerson을 접근할수 있는 경로는 getAge로 받아오는것이나, setAge로 넣는 두가지 방법 이외에는 age에 접근할수 없습니다.
비동기를 어려워하는 이유는 자바스크립트는 싱글 스레드입니다. 그렇기 때문에 동기언어로 불가능합니다. 사람들은 동기적으로 생각하지만 자바스크립트는 비동기이기때문에 비동기를 어려워합니다.
여기서 비동기적 환경을 만들기위해 setTimeout을 사용했습니다.
setTimeout(function (x){
console.log("dd");
setTimeout(function(y){
console.log("ee");
},2000);
},1000);
위의 코드에서는 depth가 2밖에 되지 않지만 많은 비동기처리를 해야될때는 콜백지옥이라는 depth가 엄청 깊게 될수도 있습니다. 이를 위해서 Promise객체를 만들어 냈습니다.
일부 개발자들은 Promise가 더 복잡하다고 하시는분들도 있습니다.
(개발자의 손가락은 소중하거든요)
const p1 = new Promise((resolve,reject) => {
// resolve는 then을 실행시키는 명령어입니다. reject는 catch를 실행시키는 명령어입니다.
setTimeout(() => {
resolve("응답");
},1000);
});
p1.then(function(r){
console.log(r);
).catch(function(){});
이도 부족해서 동기처럼 펴는 명령어가 생겼습니다.(그래도 동기는 아닙니다. 동기화하면 자바스크립트는 싱글스레드라 멈춰버립니다.)
const delay = ms => new Promise(resolve => setTimeout(resolve,ms));
//reject는 나오지 않기 때문에 호출하지 않았습니다.
async function main (){
console.log("1");
await delay(2000);
console.log("2");
}
main();
위 처럼 async await를 사용한다면 async 함수에서는 awiat 에서 resolve가 실행되서 마무리되지 않는한 다음줄로 넘어가지 않습니다. async await덕분에 try catch문을 사용가능하게 되었습니다.
예제
try {
const x = await delay(2000);
} catch (e){
console.log(e);
}
중간에 질문이 하나 나왔는데 커링 패턴에 관련된 질문이었습니다.
function add(x){ return function(y){ return x + y; } } var first = add(1); // 함수를 반환합니다. first(2); // 3 -------------------------------------- function add(x,y){ return x+y; }위의 코드와 아래 코드는 결과적으로 같은 코드입니다. 하지만 위의 패턴은 커링 패턴이라고 합니다.
커링 패턴을 처음 들어봐서 정리 해보았습니다.
- 클로저는 항상 커링이 됩니다.
앱은 수많은 UI요소로 중첩이 되어있습니다. (리액트에서는 컴포넌트)
여기서 많은 UI요소끼리 Data들이 교환, 전달 등의 많은 얽힘이 있을건데 너무 많이 얽혀 있다면 복잡해집니다. 이를 해결하기 위하여 state management 가 필요합니다.
Data를 한곳에 모아놓고 필요한 data만 가져와서 사용하면 되는방법을 사용하는데 모아놓는 장소를 store라고 명명 했습니다.
리액트에서는 직접 돔에 들어가는게 아니라 가상돔 (이하 vdom)을 다룹니다.
리액트는 rendering동안 새로 vdom을 생성합니다. 이후 원래 있던 dom과 비교하여 다른 것만 적용하여 생성합니다.
redux.js에서
export function createStore(reducer) {
let state;
const listener = [];
const getState = () => ({ ...state });
const dispatch = (action) => {
state = reducer(state, action);
listener.forEach((fn) => fn());
};
const subscribe = (fn) => {
listener.push(fn);
};
return {
getState,
dispatch,
subscribe
};
}
내부 state는 클로저로 return으로 내보내준 함수 이외에는 접근이 불가능하게 합니다. (개발자의 간곡한 부탁으로 immutable을 지켜줘야됩니다. )
여기서 getState는 내부 state를 바깥으로 보여줍니다. 하지만 스프레드 문법으로 새로운 객체를 생성해서 보내줘야됩니다.
dispatch는 어떠한 action이 실행되었을때 실행시켜주어 내부 state를 변형시켜주는 함수입니다. 이도 마찬가지로 불변성을 지켜주는 reducer함수를 실행시켜줘야됩니다.
마지막 subscribe는 dispatch함수가 실행될때마다 실행시켜줘야되는 함수입니다. 내부에는 store내부에 원하는 state의 변환체크를 하는 분기문이 필요할거 같습니다.
import { createStore } from "./redux";
const INCREMENT = "increment";
const RESET = "reset";
function reducer(state = {}, action) {
if (action.type === "increment") {
return {
...state,
count: state.count ? state.count + 1 : 1
};
}else if(action.type === "reset"){
return{
...state,
count: 0
};
}
}
const store = createStore(reducer);
function update() {
console.log(store.getState());
}
store.subscribe(update);
// store.dispatch({type:INCREMENT});
function actionCreater(type, data) {
return {
...data,
type: type
};
}
function increment() {
store.dispatch(actionCreater(INCREMENT));
}
function reset(){
store.dispatch(actionCreater(RESET))
}
increment(); // 1
increment(); // 2
increment(); // 3
increment(); // 4
reset(); // 0
reducer 함수에서 return에서 스프레드 문법을 사용하여 불변성을 지켜주는것이 중요합니다. 그렇지 않다면 참조값을 찾아가서 원래 state를 변경시켜버리기 때문에 무슨일이 생길지 모릅니다.
update라는 subscribe에 넣어 reducer가 실행될때마다 실행시킬 함수를 생성합니다.
actionCreater는 원래는
store.dispatch({type:INCREMENT})
를 사용해야되지만 좀 더 쉽게 사용하기 위해 actionCreater로 {type:action.type}을 리턴으로해서 만들었습니다. 중요한건 여기서도 불변성을 지켜주었습니다.
그리고 좀더 쉽게 하기위해서 실행함수를 만들어
increment()
만 실행시켜도 증가되게 했습니다.
그래도 저는 closure에 관련하여 조금 알고있었다고 생각합니다. 그래서 저는 쉽게 이해가 가능했습니다.
redux를 하나하나 만들어보고 설명을 들으니 더욱 쉽게 접근할수 있었던거 같습니다.
이해는 되었지만 적용까지는 저의 몫이라고 생각됩니다. 로그인을 한번 만들어봐야될거같습니다.
그리고!! 초딩과 강아지의 싸움은 흥미로웟습니다.(누가이겼을까..)