8/13 js 문법 종합 3주차

성준호·2024년 8월 13일
0
  • 얕은 복사: 객체의 최상위 속성만 복사
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의 이름까지 변경된다.

  • 해결1
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의 속성을 복사하였다

  • for ... in 구문 활용
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으로 접근하여 수정하였다.

  • null과 undefined
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) → 코드종료

  • VariableEnvironment, LexicalEnvironment
    두 가지 모두 담기는 항목은 동일하나 스냅샷 유지 여부에 차이가 있다.
    VariableEnvironment - 스냅샷 유지
    LexicalEnvironment - 스냅샷 유지하지 않음 -> 실시간으로 변경사항 반영
//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

  • 함수 선언문과 함수 표현식의 호이스팅 차이
  1. 함수 선언문
a();

function a() {
  console.log("hi");
}

hi => 함수 전체가 호이스팅 되어 선언 전에 호출할 수 있다

  1. 함수 표현식
// a(); // TypeError: a is not a function

const a = function() {
  console.log("hi");
};

a;

hi => 호이스팅 규칙에 따라 변수 선언만 호이스팅 된다. 따라서 정의 전에 호출이 불가능하다.

  1. 협업 시 함수 선언문 vs 함수 표현식
...

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);

==> 함수 선언문은 협업 시 함수를 재정의한 경우 함수 전체가 호이스팅되어 치명적인 오류가 생길 수 있다. 따라서 협업을 할 땐 함수 표현식을 사용하는 편이 좋다.

  • LexicalEvironment와 스코프 체인
// 아래 코드를 여러분이 직접 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 바인딩
  1. 전역 공간에서 this는 전역 객체를 가리킨다. 브라우저 -> window / 노드 -> global

  2. 함수와 메서드의 차이: 독립성에 차이가 있으며 함수는 그 자체로 독립적인 기능을 수행한다 하지만 메서드는 객체에 대한 동작을 수행한다.

// 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.

  1. 콜백 함수 호출 시 그 함수 내부에서의 this
    기본적으로 콜백 함수도 함수기 때문에 this는 전역 객체를 참조한다. 그러나 콜백함수를 넘겨받은 함수에서 콜백 함수에 별도로 this를 지정한 경우, 예외적으로 그 대상을 참조한다.
// 별도 지정 없음 : 전역객체
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

  1. 생성자 함수 내부에서의 this
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

  • 명시적 this 바인딩
  1. call 메서드
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} 객체에 바인딩한다.

  1. apply 메서드
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과는 다르게 뒤에 붙는 매개변수를 배열로 묶어줘야 한다.

  1. 유사배열에서 call, apply 활용
//객체에는 배열 메서드를 직접 적용할 수 없어요.
//유사배열객체에는 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 등의 메서드를 사용할 수 있다

  1. Array.from 메서드
// 유사배열
var obj = {
	0: 'a',
	1: 'b',
	2: 'c',
	length: 3
};

// 객체 -> 배열
var arr = Array.from(obj);

// 찍어보면 배열이 출력됩니다.
console.log(arr);

위에서처럼 call, apply를 활용하여 객체를 배열로 형변환할 수 있지만 Array.from을 쓰면 더 간단하다.

  1. 생성자 내부에서 다른 생성자 호출
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 인스턴스를 참조하게 하였다.

  1. 여러 인수를 묶어 배열로 전달할 때 apply 활용
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. bind 메서드
    call과 유사하나, 즉시 호출하지 않고 넘겨받은 this와 인수를 바탕으로 새로운 함수를 반환한다
// [목적]
// 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

profile
안녕하세요

0개의 댓글