var user = {
name: "wonjang",
gender: "male",
};
// 이름 변경 함수
// 입력값: 변경대상 user 객체, 변경하고자 하는 이름
// 출력값: 새로운 user 객체
// 객체의 속성에 접근하여 이름 변경
var changeName = function (user, newName) {
var newUser = user;
newUser.name = newName;
return newUser;
};
// 변경한 user 정보를 user2 변수에 할당
// 가변이기 때문에 user1도 영향을 받음
var user2 = changeName(user, "twojang");
// 결국 아래의 로직은 스킵됨
if (user !== user2) {
console.log("유저 정보가 변경되었습니다");
}
console.log(user.name, user2.name);
console.log(user === user2);
twojang twojang
true
=> 문제점: var newUser = user; 여기서 user 객체를 newUser에 할당한다. 이때 같은 메모리 주소를 참조하기 때문에 한쪽의 데이터를 변경하면 다른 쪽의 데이터도 동일하게 변경된다.
즉 newUser.name = '변경할 이름'으로 user2를 바꾸면 user의 이름까지 변경된다.
var user = {
name: "wonjang",
gender: "male",
};
// 이름 변경 함수
// 입력값: 변경대상 user 객체, 변경하고자 하는 이름
// 출력값: 새로운 user 객체
// 객체의 속성에 접근하여 이름 변경
var changeName = function (user, newName) {
return {
name: newName,
gender: user.gender,
};
};
// 변경한 user 정보를 user2 변수에 할당
// 가변이기 때문에 user1도 영향을 받음
var user2 = changeName(user, "twojang");
// 결국 아래의 로직은 스킵됨
if (user !== user2) {
console.log("유저 정보가 변경되었습니다");
}
console.log(user.name, user2.name);
console.log(user === user2);
유저 정보가 변경되었습니다
wonjang twojang
false
=> changeName 함수를 수정하여 새로운 객체를 리턴하게 만들었다. 문제점은 해결했지만 user의 속성이 많아질 수록 유지보수가 힘들 수 있다.
var changeName = function (user, newName) {
return {
...user,
name: newName
};
};
=> 전개 연산자를 활용하여 기존 user의 속성을 복사하였다
var copyObject = function (target) {
var result = {};
// for ~ in 구문을 이용하여 객체의 모든 속성에 접근
for (var prop in target) {
result[prop] = target[prop];
}
return result;
};
var user = {
name: "wonjang",
gender: "male",
};
var user2 = copyObject(user);
user2.name = "twojang";
if (user !== user2) {
console.log("유저 정보가 변경되었습니다");
}
console.log(user.name, user2.name);
console.log(user === user2);
유저 정보가 변경되었습니다
wonjang twojang
false
=> for in으로 기존 user 객체의 속성을 순회하여 result에 복사한 후 user2에 할당하였다. 그후 user2.name으로 접근하여 수정하였다.
var n = null;
console.log(typeof n); // object
//동등연산자(equality operator)
console.log(n == undefined); // true
console.log(n == null); // true
//일치연산자(identity operator)
console.log(n === undefined);
console.log(n === null);
object
true
true
false
true
=> js 자체 버그로 typeof 연산자로 null을 찍으면 object로 나온다. 동등연산자로 null과 undefined를 비교하면 true를 반환한다. 따라서 null과 undefined의 정확한 비교를 원한다면 일치연산자를 사용할 필요가 있다.
// ---- 1번
var a = 1;
function outer() {
function inner() {
console.log(a); //undefined
var a = 3;
}
inner(); // ---- 2번
console.log(a);
}
outer(); // ---- 3번
console.log(a);
=> 코드실행 → 전역(in) → 전역(중단) + outer(in) → outer(중단) + inner(in) → inner(out) + outer(재개) → outer(out) + 전역(재개) → 전역(out) → 코드종료
//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기
function a (x) {
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a(1);
예상 결과: 1, undefined, 2
실행 결과: 1, 1, 2
=> 1. 매개변수를 다시 쓴다
//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기
function a () {
var x = 1;
console.log(x);
var x;
console.log(x);
var x = 2;
console.log(x);
}
a(1);
=> 2. 변수 선언을 끌어올리고 나머지 요소를 그대로 작성한다.
//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기
function a (x) {
var x;
var x;
var x;
x = 1;
console.log(x);
console.log(x);
x = 2;
console.log(x);
}
=> 따라서 결과는 1, 1, 2
a();
function a() {
console.log("hi");
}
hi => 함수 전체가 호이스팅 되어 선언 전에 호출할 수 있다
// a(); // TypeError: a is not a function
const a = function() {
console.log("hi");
};
a;
hi => 호이스팅 규칙에 따라 변수 선언만 호이스팅 된다. 따라서 정의 전에 호출이 불가능하다.
...
console.log(sum(3, 4));
// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
return x + y;
}
...
...
var a = sum(1, 2);
...
// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
return x + ' + ' + y + ' = ' + (x + y);
}
...
var c = sum(1, 2);
console.log(c);
==> 함수 선언문은 협업 시 함수를 재정의한 경우 함수 전체가 호이스팅되어 치명적인 오류가 생길 수 있다. 따라서 협업을 할 땐 함수 표현식을 사용하는 편이 좋다.
// 아래 코드를 여러분이 직접 call stack을 그려가며 scope 관점에서 변수에 접근해보세요!
// 어려우신 분들은 강의를 한번 더 돌려보시기를 권장드려요 :)
var a = 1;
var outer = function() {
var inner = function() {
console.log(a); // 이 값은 뭐가 나올지 예상해보세요! 이유는 뭐죠? scope 관점에서!
var a = 3;
};
inner();
console.log(a); // 이 값은 또 뭐가 나올까요? 이유는요? scope 관점에서!
};
outer();
console.log(a); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!
undefined, 1, 1
=> outer 함수 호출, inner 함수 호출, 호이스팅에 따라 var a, console.log(a), a = 3 순으로 정렬된다. 따라서 undefined. 그 후 중단됐던 outer 함수의 console.log(a)는 스코프 체인에 따라 전역으로 선언된 var a = 1을 참조. 즉 1. 마지막 console.log(a)는 마찬가지로 전역으로 선언된 1이 출력된다.
전역 공간에서 this는 전역 객체를 가리킨다. 브라우저 -> window / 노드 -> global
함수와 메서드의 차이: 독립성에 차이가 있으며 함수는 그 자체로 독립적인 기능을 수행한다 하지만 메서드는 객체에 대한 동작을 수행한다.
// CASE1 : 함수
// 호출 주체를 명시할 수 없기 때문에 this는 전역 객체를 의미해요.
var func = function (x) {
console.log(this, x);
};
func(1); // Window { ... } 1
// CASE2 : 메서드
// 호출 주체를 명시할 수 있기 때문에 this는 해당 객체(obj)를 의미해요.
// obj는 곧 { method: f }를 의미하죠?
var obj = {
method: func,
};
obj.method(2); // { method: ƒ } 2
2-1. 메서드는 .과 []로 호출한다
var obj = {
method: function (x) { console.log(this, x) }
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2
2-2. 메서드 내부에서의 this는 호출을 누가 했는지에 따라 달라진다
var obj = {
methodA: function () { console.log(this) },
inner: {
methodB: function() { console.log(this) },
}
};
obj.methodA(); // this === obj
obj['methodA'](); // this === obj
obj.inner.methodB(); // this === obj.inner
obj.inner['methodB'](); // this === obj.inner
obj['inner'].methodB(); // this === obj.inner
obj['inner']['methodB'](); // this === obj.inner
2-3. 메서드 내부에서 정의된 함수에서의 this
var obj1 = {
outer: function() {
console.log(this); // (1)
var innerFunc = function() {
console.log(this); // (2), (3)
}
innerFunc();
var obj2 = {
innerMethod: innerFunc
};
obj2.innerMethod();
}
};
obj1.outer();
onj1, 전역 객체, obj2
=> obj1.outer() 메소드 호출. (1)번의 this는 메소드로 호출된 것으로 obj1을 가리킨다.
그 후 innerFunc() 함수 호출. (2)번의 this는 함수로 호출된 것으로 전역 객체를 가리킨다.
마지막으로 obj2.innerMethod() 메소드 호출. (3)번의 this는 메소드로 호출된 것으로 obj2를 가리킨다.
2-4 메서드 내부에서 정의된 함수에서의 this 우회하기
(1) 변수 활용
var obj1 = {
outer: function() {
console.log(this); // (1) { outer: ƒ }
// AS-IS
var innerFunc1 = function() {
console.log(this); // (2) 전역객체
}
innerFunc1();
// TO-BE
var self = this;
var innerFunc2 = function() {
console.log(self); // (3) { outer: ƒ }
};
innerFunc2();
}
};
// 메서드 호출 부분
obj1.outer();
=> obj1.outer() 메소드 호출. (1)번 this는 obj1 객체를 참조한다.
innerFunc1() 함수 호출. (2)번 this는 전역 객체를 참조한다.
그후 var self = this로 obj1.outer(); 메소드를 호출할 때 참조한 obj1 객체를 self에 할당하였다. 그 다음 innerFunc2() 함수 호출. (3)번에서 self, 즉 obj1 객체를 출력한다.
(2) 화살표 함수
var obj = {
outer: function() {
console.log(this); // (1) obj
var innerFunc = () => {
console.log(this); // (2) obj
};
innerFunc();
}
}
obj.outer();
=> 화살표 함수는 자신이 정의된 스코프의 this를 유지한다. (1)번은 메서드로 호출로 this는 obj 객체를 가리킨다. (2)번은 함수 호출이지만 화살표 함수이기 때문에 outer 메서드의 this를 유지한다. 즉 obj.
// 별도 지정 없음 : 전역객체
setTimeout(function () { console.log(this) }, 300);
// 별도 지정 없음 : 전역객체
[1, 2, 3, 4, 5].forEach(function(x) {
console.log(this, x);
});
// addListener 안에서의 this는 항상 호출한 주체의 element를 return하도록 설계되었음
// 따라서 this는 button을 의미함
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function(e) {
console.log(this, e);
});
=> setTimeout 함수, forEach 메서드는 콜백함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 window 객체
addEventListener 메서드 안에서의 this는 호출한 주체의 element를 return한다. 따라서 button
var Cat = function (name, age) {
this.bark = '야옹';
this.name = name;
this.age = age;
};
var choco = new Cat('초코', 7); //this : choco
var nabi = new Cat('나비', 5); //this : nabi
=> 생성자 함수를 호출하여 새로운 객체를 생성한다. 이때 생성자 함수 내부의 this는 새로 생성된 객체를 참조한다. 즉 chico, nabi
var func = function (a, b, c) {
console.log(this, a, b, c);
};
// no binding
func(1, 2, 3);
// 명시적 binding
func.call({ x: 1 }, 4, 5, 6);
전역 객체 1 2 3, {x:1} 4 5 6
=> call 메서드를 통해 this를 명시적으로 {x:1} 객체에 바인딩한다.
var obj = {
a: 1,
method: function (x, y) {
console.log(this.a, x, y);
},
};
// method 함수 안의 this는 항상 obj
obj.method(2, 3);
obj.method.call({ a: 4 }, 5, 6);
obj.method.apply({ a: 4 }, [5, 6]);
1 2 3, 4 5 6, 4 5 6
=> 메소드 호출 시 this는 호출한 메소드의 객체 참조. 즉 obj
이때 apply를 통해 참조할 객체를 변경할 수 있다 call과는 다르게 뒤에 붙는 매개변수를 배열로 묶어줘야 한다.
//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 call 또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있어요.
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
Array.prototype.push.call(obj, 'd');
console.log(obj); // { 0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4 }
var arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]
call 혹은 apply로 유사배열객체에서 배열 push, slice 등의 메서드를 사용할 수 있다
// 유사배열
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
// 객체 -> 배열
var arr = Array.from(obj);
// 찍어보면 배열이 출력됩니다.
console.log(arr);
위에서처럼 call, apply를 활용하여 객체를 배열로 형변환할 수 있지만 Array.from을 쓰면 더 간단하다.
function Person(name, gender) {
this.name = name;
this.gender = gender;
}
function Student(name, gender, school) {
Person.call(this, name, gender); // 여기서 this는 student 인스턴스!
this.school = school;
}
function Employee(name, gender, company) {
Person.apply(this, [name, gender]); // 여기서 this는 employee 인스턴스!
this.company = company;
}
var kd = new Student("길동", "male", "서울대");
var ks = new Employee("길순", "female", "삼성");
=> 생성자 함수를 통해 새 객체 kd를 생성한다. 이때 생성자 함수 내부에서 Person 생성자 함수를 호출하고 this로 Student 인스턴스를 참조하게 하였다.
var numbers = [10, 20, 3, 16, 45];
var max = (min = numbers[0]); // 10
numbers.forEach(function (number, idx) {
console.log(idx + "번째 값 => ", number);
if (number > max) {
max = number;
}
if (number < min) {
min = number;
}
});
console.log(max, min);
// apply 활용
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log("apply => ", max, min);
// 전개 연산자
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log("spread => ", max, min);
Math.max 메서드에 apply를 사용하여 매개변수를 배열로 전달하였다
이때 전개 연산자를 사용하여 배열을 펼쳐 전달하는 것도 가능하다.
// [목적]
// 1. 함수에 this를 미리 적용
// 2. 부분 적용 함수
var func = function (a, b, c, d) {
console.log(this, a, b, c, d);
};
func(1, 2, 3, 4);
// 함수에 this를 미리 적용
var bindFunc1 = func.bind({ x: 1 });
bindFunc1(5, 6, 7, 8);
// 부분 적용 함수
var bindFunc2 = func.bind({ x: 1 }, 4, 5);
bindFunc2(6, 7);
console.log(func.name);
console.log(bindFunc1.name);
console.log(bindFunc2.name);
// name 프로퍼티
// bind ~ 'bound'라는 접두어
=> bindFunc1에 참조할 객체를 미리 적용하고 호출하였다.
bindFunc2에 참조할 객체와 매개변수를 미리 적용하고 호출하였다.
추가로 bind로 새로 만든 함수는 name 속성에 bound라는 접두어가 붙는다.
var fullname = "Ciryl Gane";
var fighter = {
fullname: "John Jones",
opponent: {
fullname: "Francis Ngannou",
getFullname: function () {
return this.fullname;
},
},
getName: function () {
return this.fullname;
},
getFirstName: () => {
return this.fullname.split(" ")[0];
},
getLastName: (function () {
return this.fullname.split(" ")[1];
})(),
};
console.log("Not", fighter.opponent.getFullname(), "VS", fighter.getName());
console.log(
"It is",
fighter.getName(),
"VS",
fighter.getFirstName(),
fighter.getLastName
);
Not Francis Ngannou VS John Jones
It is John Jones VS Ciryl Gane
=> 1. fighter.opponent.getFullname() 메서드 호출
1-1 getFullname의 this는 fighter.opponent 참조 -> Francis Ngannou
2. fighter.getName() 메서드 호출
2-1 getName의 this는 fighter 참조 -> John Jones
3. fighter.getName() 메서드 호출
3-1 getName의 this는 fighter 참조 -> John Jones
4. fighter.getFirstName() 메서드 호출
4-1 getFirstName은 화살표 함수이므로 상위 스코프인 fighter 객체의 this를 참조한다. 따라서 전역 객체를 참조한다. -> Ciryl
5. fighter.getLastName 메서드 호출
5-1 getLastName 즉시 호출 함수로 여기서 this는 전역 객체를 참조한다. -> Gane