[예시]
var c = 4;
function outer() {
var a = 1;
var b = 3;
function inner() {
var a = 2;
console.log(a); // 2
console.log(b); // 3
console.log(c); // 4
}
inner();
}
outer();
console.log(a)에서 a의 값을 찾으려고 할 때 들여다보는 곳이 스코프이고 스코프는 함수 단위이다. 이 때, inner 함수 스코프, outer 함수 스코프, global 스코프가 존재하는데 inner 스코프 안에서 먼저 a가 존재하는지 찾는다.
마찬가지로 console.log(b)에서 b의 값을 inner 스코프 안에서 먼저 찾는다. inner 스코프에는 b가 없기 때문에 그 다음 outer 스코프에 b가 있는지 찾아본다. inner 함수가 생성된 곳이 outer 함수 범위 안에 있기 때문이다.
마찬가지로 console.log(c)에서 c의 값을 inner 스코프 안에서 먼저 찾는다. inner 스코프에는 c가 없기 때문에 그 다음 outer 스코프에 c가 있는지 찾아본다. outer 스코프에도 c가 없기 때문에 global 스코프에서 찾는다. inner 함수가 생성된 곳이 outer 함수 범위 안에 있고, outer 함수가 생성된 곳이 global 범위 안에 있기 때문이다.
이런식으로 스코프끼리 연결된 것이 바로 스코프 체인이다.
var c = 4;
function outer() {
var a = 1;
var b = 3;
return function inner() {
var a = 2;
console.log(b); // 3
}
}
var someFunc = outer();
someFunc();
outer 함수는 inner 함수를 return 하고 있고 someFunc에는 outer 함수의 실행결과 값이 들어가기 때문에 inner 함수가 들어가게 된다.
그런데, var someFunc = outer(); 에서 outer 함수를 호출해서 실행이 완료되면 outer 함수 내 지역 변수들은 사라질 것 같지만 사라지지 않는다. 몇몇 프로그래밍 언어에서는 함수 안의 지역 변수들은 그 함수가 처리되는 동안에만 존재하지만 자바스크립트는 함수를 리턴하고 리턴하는 함수가 클로저를 형성하기 때문이다.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성된다.
someFunc는 outer이 실행될 때 생성된 inner 함수의 인스턴스에 대한 참조다. inner의 인스턴스는 변수 b가 있는 어휘적 환경에 대한 참조를 유지한다. 이런 이유로 someFunc가 호출될 때 b는 사용할 수 있는 상태로 남게된다.
즉, 원래는 함수 내부에 선언한 변수는 함수가 끝나면 사라지지만, 클로저가 스코프 체인을 계속 들고 있으므로 outer 함수 내부의 변수를 참조할 수 있게 된다.
JavaScript에서 일반적으로 변수를 선언하는 방법은 ‘var’ 키워드를 사용하는 방식이었다.
그러나 이 같은 방식은 값이 변할 수 있기 때문에 원본 훼손에 대한 위험에 노출되어 있었다.
따라서 ES6 이후 문법에서는 const라는 키워드를 도입해 상수(항상 같은 변수) 개념을 도입해 불변성을 지킬 수 있도록 해주었다.
var v = 1
// some code
v = 2
console.log('v: ', v) // v: 2 =>
/* 처음 v를 만든 개발자는 1값을 기대하고 만들었을 것이다.
그러나 다른 개발자가 위 코드를 모르고 새로운 값을 할당하면서 immutability가 훼손됐다. */
const c = 1;
//some code
c = 2; // TypeError: Assignment to constant Variable
/* 실행되기 전에 위와 같은 오류가 발생한다. immutability도 지키면서 오류도 쉽게 잡아낼 수 있게 되었다.
JavaScript에는 원시형 타입과, 객체형 타입을 이해할 필요가 있다.
원시형 타입은 (Number, String, Boolean, Null, Undefined, Symbol)이 있다.
객체형 타입은 (Object, Array, Function 등)이 있다.
var v1 = 1
var v2 = 1
console.log(v1 === v2) // true
var obj1 = {
name: 'Serzhul',
}
var obj2 = {
name: 'Serzhul',
}
console.log(obj1 === obj2) // false
const obj = { vaule: 1 }
const newObj = obj;
newObj.vaule = 2;
console.log(obj.vaule); // 2
console.log(obj === newObj); // true
obj 객체를 새로운 newObj 객체에 할당하였으며 이를 참조 할당 이라 부른다.
복사 후 newObj 객체의 value값을 변경하였더니 기존의 obj.value값도 같이 변경된 것을 알 수 있다. 두 객체를 비교해도 true로 나온다.
이렇게 자바스크립트의 참조 타입은 얕은 복사가 된다고 볼 수 있으며, 이는 데이터가 그대로 생성되는 것이 아닌 해당 데이터의 참조 값(메모리 주소)를 전달하여 결국 한 데이터를 공유하는 것이다.
즉, 데이터가 그대로 하나 더 생성된 것이 아닌 해당 데이터의 메모리 주소를 전달하게 돼서, 결국 한 데이터를 공유하게 되는 것이다.
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
console.log(b); // 2
console.log(a === b); // false
변수 a를 새로운 b에 할당하였고 b 값을 변경하여도 기존의 a의 값은 변경되지 않는다.
두 값을 비교하면 false가 출력되며 서로의 값은 단독으로 존재하다는 것을 알 수 있다.
이렇게 자바스크립트의 원시 타입은 깊은 복사가 되며, 이는 독립적인 메모리에 값 자체를 할당하여 생성하는 것이라 볼 수 있다.
let notDeep = {
a : 1,
b : 2,
c : {name : "park"}
}
let b = {...notDeep}
notDeep.a = 100
notDeep['c'].name = "Kim"
console.log(notDeep.a) // 100
console.log(b.a) // 1
console.log(notDeep.c) // { name:"Kim" }
console.log(b.c) // { name:"Kim" }
먼저 흔하게 사용하는 전개 연산자를 통한 예제를 만들어 보았다. notDeep 변수에 객체를 할당한뒤 변수 b에 전개 연산자를 통해 notDeep객체를 복사했다.
물론 notDeep 객체의 a란 키에 100을 재할당 후 b의 a를 출력했을때, 100이 아닌, 1로 출력되기에 정상적으로 깊은 복사가 이루어진 것 처럼 보이지만, notDeep 내부의 {name : "park"}이란 객체의 'name'의 값을 "Kim"으로 변경했더니 b의 name까지 변경된 것을 알 수 있다. 즉, 완벽한 복사가 이루어졌다고 하기 힘들다. ((Object.assign()도 동일)
const copyObj = obj => JSON.parse(JSON.stringify(obj));
let notDeep = {
a : 1,
b : 2,
c : {name : "park"}
}
const copied = copyObj(notDeep);
notDeep.a = 1000;
notDeep.c.name = "Kim";
console.log(notDeep.c) // { name:"Kim" }
console.log(copied.c); // { name:"park" }
JSON.stringify 메서드는 자바스크립트 객체를 JSON문자열로 변환 시킨다.
반대로 JSON.parse는 JSON문자열을 자바스크립트 객체로 변환시킨다. 그렇기에 객체를 JSON문자열로 변환했다가 다시 객체로 변환하기에, 객체에 대한 참조가 사라진것이다.
그러나 이 방법에는 2가지 문제점이 있는데, 타 방법들에 비해 성능이 떨어진다는점과 JSON.stringify 메서드가 함수를 undefined로 처리한다는 점이다.
const _ = require("lodash");
let notDeep = {
a: 1,
b: 2,
c: { name: "park" },
d: () => {
return "function is work!";
},
};
const copy = _.cloneDeep(notDeep);
notDeep["c"].name = "Kim";
console.log(notDeep); // { a: 1, b: 2, c: { name: 'Kim' }, d: [Function: d] }
console.log(copy); // { a: 1, b: 2, c: { name: 'park' }, d: [Function: d] }
console.log(copy.d()); // function is work!