지속적으로 업데이트 중입니다.
ECMAScript의 함수는 함수로 동작하기 위한 추가적인 기능을 가지고 있는 일반객체의 확장판이다. 함수 객체는 다음과 같은 데이터를 내부에 저장한다.
[[Environment]]
[[ECMAScriptCode]]
[[FunctionKind]]
(normal,classConstructor,generator,async)[[ConstructorKind]]
(base,derived)this
참조형태 [[ThisMode]]
[[Strict]]
super
참조 [[HomeObject]]
[[Call]]
, [[Construct]]
함수 실행메서드에서 [[Call]]
은 함수가 단순히 호출되었을때 함수 객체 내부에서 호출되고, new
또는 super
연산자와 함께 호출되면 [[Counstruct]]
가 호출된다.
[[Call]]
로 호출된 객체를 callable
, [[Counstruct]]
로 호출된 객체를 constructor
라고 한다. 자바스크립트에서 함수는 두개 모두에 해당될수도있고 아닐 수도 있다. Arrow function
으로 생성된 함수는 callable
하지만, 자신의 this
, super
, new.target
을 바인딩하지 않아 non-constructor
이다.
참고로 Arrow function
인 경우 this
키워드는 lexical을 참조한다.
자바스크립트에서 함수가 생성될때 다음 6가지의 정보가 사용된다.
Normal
, Arrow
, Method
즉, 함수 생성 방식을 통해 생성자가 될 수 있는지의 여부를 따지고, strict
여부와 실제 함수 객체의 종류를 구분하고, 함수 객체의 프로토타입을 저장하고(함수자체의 프로토타입), 그 후에 스코프,파라미터,함수 몸체,this
참조 방식등의 정보를 저장한다.
function foo() {}; // Normal
const fool () => {}; // Arrow
const person = {
sayHi() {} // Method
};
함수 생성을 구분하는 이유는 Arrow
나 Method
인 경우 생성자로 동작하지 못하도록 방지하고 , this
참조 방식을 결정하기 위해서다.
ES6 이후 함수를 어떻게 생성하는가에 따라 생성자로 쓰일 수 있는지 없는지가 결정되며, 이는 함수 생성시 함수 할당(FunctionAllocate)
단계에서 결정된다.
ECMAScript은 함수 호출을 Call(F, V, [,argumentsList])
로 표현한다. Call
은 함수 객체 내부 [[Call]]
메서드를 수행하는 동작이며, F
는 함수객체, V
는 [[Call]]
의 this
,argumentsList
는 전달할 인자의 배열이다.
함수 객체 내부 메서드인 [[Call]]
은 인자로 this
값과 argumentList
를 받는다.
함수 호출의 과정은 다음과 같다.
F.[[FunctionKind]]
가 classCounstructor라면 에러를 발생시킨다.callerContext
는 현재 실행중인 실행 컨텍스트(running execution context)calleeContext
에 새로운 실행 컨텍스트를 생성하고 지정한다. (PrepareForOrdinaryCall)calleeContext
가 현재의 running execution context가 된다.this
를 바인딩한다. (OrdinaryCallBindThis) / this
가 어떤 객체를 참조할지 결정result
에 결과 저장 (OrdinaryCallEvaluateBody)calleeContext
를 실행컨텍스트 스택에서 제거하고, callerContext
를 다시 현재 실행컨텍스트로 지정result
반환위의 함수 호출 과정에서의 단계들에대해 더 알아보자.
이 과정은, 결국 실행컨텍스트 스택에 새로운 실행컨텍스트를 생성하고 push하는 과정이다. 새로운 실행컨텍스트가 생성되는 시점 직후에, 렉시컬 환경이 생성된다. 따라서, 어떠한 함수가 호출될 당시의 렉시컬환경을 기억한다.
여기서 렉시컬환경이란, 자바스크립트 코드에서 변수나 함수 등의 식별자를 정의하는데 사용하는 객체이다. 렉시컬환경내부에는 식별자와 참조 값을 기록하는 Environment Record
와 outer
라는 또 다른 렉시컬환경을 참조하는 포인터로 구성된다. outer
는 외부 렉시컬환경을 참조하는 포인터이며, 이는 중첩된 코드에서 스코프탐색에 쓰인다.
Environment Record
는 렉시컬 환경 안에 함수와 변수를 기록한다.Declarative environment record
와 Object environment record
가 있는데,new.target
,this
,super
등에 대한 정보를 갖게 된다. 후자는 전역 코드에 대해서만 적용되며 전역 객체도 기록한다. 각 객체의 속성을 바인딩하기 위해 record에 새로운 엔트리를 형성한다.this
값을 결정하는 일도 수행하며, 전역 실행컨텍스트에서 this
는 전역객체이다. this
바인딩은 함수 호출에 따라 결정된다.
callerContext = runningExecutionContext;
calleeContext = new ExecutionContext;
calleeContext.Function = F;
localEnv = NewFunctionEnvironment(F, newTarget);
// 이부분이 렉시컬환경이 만들어지는 과정이다.
calleeContext.LexicalEnvironment = localEnv;
calleeContext.VariableEnvironment = localEnv;
executionContextStack.push(calleeContext);
return calleeContext;
실행컨텍스트 생성과 동시에 렉시컬환경이 생성된 이후, 실행컨텍스트 스택에 들어갈 실행컨텍스트가 반환됨고 스택에 push됨을 인지하면 된다. 이젠 더이상 this
바인딩이 실행컨텍스트가 아닌 렉시컬환경의 Environment Record
가 담당한다.
// NewFunctionEnvironment(F, newTarget)
env = new LexicalEnvironment;
envRec = new functionEnvironmentRecord;
envRec.[[FunctionObject]] = F;
if (F.[[ThisMode]] === lexical) {
envRec.[[ThisBindingStatus]] = 'lexical';
} else {
envRec.[[ThisBindingStatus]] = 'uninitialized';
}
home = F.[[HomeObject]];
envRec.[[HomeObject]] = home;
envRec.[[NewTarget]] = newTarget;
env.EnvironmentRecord = envRec.
env.outer = F.[[Environment]];
return env;
위 코드는 Declarative environment record
중의 functionEnvironment record
부분이다. 여기에 함수 환경으로 this
, super
, new.target
등의 정보를 Environment Record에 함께 초기화했다.
함수 객체의 [[ThisMode]]
에 따른 this
값 참조를 결정한다.
[[ThisMode]]
가 lexical 이면, 다른 처리를 하지 않는다. (arrow function)[[ThisMode]]
가 strict면, 인자로 넘어온 thisArgument를 Environment record에 설정한다. [[ThisMode]]
가 둘 모두 아니면, global에 있는 [[thisValue]]
를 Environment record에 설정한다.변수 선언 초기화와 코드수행 및 결과반환 단계이다. 변수 선언과 초기화는 식별자 이름과 값을 매핑한다.
함수의 인자로 들어가는 인자들의 마지막에 ...args
같이 작성하면, args
라는 변수에 ...args
앞의 인자들을 제외한 모든 인자들을 모아 배열로 저장하는 기능을 한다.
주의할 점은, 나머지 매개변수는 항상 마지막에 있어야 한다.
함수의 인자들을 끄집어내는 과정 말고, 그 반대의 과정의 내용이다. 예를들면,
console.log( Math.max([1,2,3]) ); // NaN
console.log( Math.max(1,2,3) ); // 3
배열의 요소들을 함수의 인자로 한번에 넣고 싶을 때 전개문법을 사용하면 된다.
함수를 호출할 때, 다음과 같이 코드를 작성하면 된다.
const arr = [1,2,3];
console.log( Math.max(...arr) ); // 3
서로 다른 두 배열을 합치는것도 가능하며, 합쳐서 인자로 넣는것도 가능하다.
또, iterable 한 값들을 전개하는 것도 가능하다.
let arr1 = [1,2,3];
let arr2 = [4,5,6];
let arr3 = [...arr1,...arr2];
const str1 = "abc";
console.log(arr3) // [1,2,3,4,5,6]
console.log([...str1]) // ["a","b","c"]
[...str1]
과 똑같은 기능을 하는 배열 메서드 Array.from()
가 있다.
다만, Array.from()
메서드는 유사배열객체를 대상으로도 사용이 가능하기 때문에, 보편적으로는 Array.from
을 사용한다.
let arr = [1,2,3];
let arrCopy = [...arr];
let obj = { 'a' : 1 , 'b':2 }
let objCopy = {...obj};
console.log(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
console.log( JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
본래의 배열,객체와 복사본의 배열,객체는 서로 같은 내용물을 담고있다. 하지만, 서로 같진 않으므로 복사본을 메서드로 조작하여도 원본의값에는 영향이 없다.
let list = {
value : 1,
next : {
value : 2,
next : {
value : 3,
next : {
value :4
next : null
}
}
}
}
리스트에서 요소를 삽입, 삭제 할 때는 많은 비용이 들기 때문에 시간이 오래걸린다. 이를 단축하기 위한 방법으로서 연결리스트 자료구조를 사용하는 방법이 있다. arr.push
,arr.pop
메서드를 사용한다면, 시간이 적게 걸리지만 이는 배열 끝을 조작하는 메서드이기 때문에 앞서 말한 것처럼 특정요소를 조작하기엔 부족하다. 빠르게 삽입 , 삭제를 해야 할 경우 다음과 같은 방식을 이용하면 빠르다.
연결리스트는 다음 2개의 요소를 가진다.
value
next
: 다음 연결리스트 요소를 참조한다. 다음 요소가 없을경우 null
을 할당한다.연결 리스트를 사용하면 전체 리스트를 여러 부분으로 쉽게 나눌 수 있고, 병합도 가능하다.
let secondList = list.next.next;
list.next.next = null;
list.next.next = secondList;