본문 바로가기

CS/모던 자바스크립트 Deep Dive

11장 원시값과 객체의 비교 / 12장 함수

반응형

22.11.28 월요일 140p~150p

 

11장 원시값과 객체의 비교 || 원시값

 

  • 문자열과 불변성

원시 값을 저장하려면 먼저 확보해야하는 메모리 공간의 크기를 결정해야 한다. 이를 위해 원시 타입별로 메모리 공간의 크기가 미리 정해져있다.

원시 값인 문자열은 다른 원시 값과 비교할 때 독특한 특징이 있따. 문자열은 0개 이상의 문자로 이뤄진 집합을 말하며, 1개의 문자는 2바이트의 메모리 공간에 저장된다. 숫자 값은 모두 동일한 8바이트가 필요하지만, 문자열의 경우 몇 개의 문자로 이뤄졌느냐에 따라 필요한 메모리 공간의 크기가 결정된다.

C에서는 하나의 문자를 위한 데이터 타입만 있을 뿐 문자열 타입은 존재하지 않는다. C에서는 문자열을 문자의 배열로 처리하고 자바에서는 문자열을 String 객체로 처리한다. 하지만 자바스크립트는 개발자의 편의를 위해 원시 타입인 문자열 타입을 제공한다. 자바스크립트의 문자열은 원시타입이며 문자열이 생성된 이후에는 변경 불가능하다. 이로 인해 이미 생성된 문자열의 일부 문자를 변경해도 반영되지 않는다.

원시 값은 어떤 일이 있어도 불변하기 때문에 예기치 못한 변경으로부터 자유롭다. 이는 데이터의 신뢰성을 보장한다.

변수에 새로운 문자열을 재할당하는 것은 물론 가능하다. 이는 기존 문자열을 변경하는 것이 아니라 새로운 문자열을 새롭게 할당하는 것이기 때문이다.

* 유사 배열 객체

마치 배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고 length 프로퍼티를 갖는 객체를 말한다.

원시 값인 문자열이 객체 일 수 있다? => 원시 값을 객체처럼 사용하면 원시 값을 감싸는 래퍼 객체로 자동 변환된다.

  • 값에 의한 전달
let score = 80;
let copy = score;

console.log(score); // 80
console.log(copy); // 80
console.log(score === copy) // true

score = 100;

console.log(score); // 100
console.log(copy); // 80
console.log(score === copy) // false

이처럼 변수에 원시 값을 갖는 변수를 할당하면 할당받는 변수에는 ‘할당되는 변수의 원시 값이 복사되어 전달’되고, 이를 ‘값에 의한 전달’이라고 한다.

score 변수와 copy 변수의 값 80은 다른 메모리 공간에 저장된 별개의 값이다. 따라서 score 변수의 값을 변경해도 copy 변수의 값에도 어떠한 영향도 주지 않는다.

! 값에 의한 전달은 사실 값을 전달하는 것이 아니라 ‘메모리 주소를 전달’한다. 단, 전달된 메모리 주소를 통해 메모리 공간에 접근하면 값을 참조할 수 있다. !

변수와 같은 식별자는 값이 아니라 메모리 주소를 기억하고 있기 때문이다. 식별자로 값을 구별해서 식별한다는 것은 식별자가 기억하고 있는 메모리 주소를 통해 메모리 공간에 저장된 값에 접근할 수 있다는 것을 의미한다. 식별자는 메모리 주소에 붙힌 이름이라고 할 수 있다.

let x = 10;

위의 경우, 할당 연산자는 숫자 리터럴 10에 의해 생성된 숫자 값 10이 저장된 메모리 공간의 주소를 전달한다. 이로써 식별자 x는 메모리 공간에 저장된 숫자 값 10을 식별할 수 있다.

let copy = score;

위의 경우, score는 식별자 표현식으로서 숫자 값 80으로 평가된다. 이 때 두 가지 평가 방식이 가능하다.

  1. 새로운 80을 생성(복사)해서 메모리 주소를 전달하는 방식 => 할당 시점에 두 변수가 기억하는 메모리 주소가 다르다.
  2. score의 변수값 80의 메모리 주소를 그대로 전달하는 방식 => 할당 시점에 두 변수가 기억하는 메모리 주소가 같다.

중요한 것은 변수에 원시 값을 갖는 변수를 할당하면 변수 할당 시점이든, 두 변수 중 어느 하나의 변수에 값을 재할당하는 시점이든 결국은 두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없다는 것이다.

 

11장 원시값과 객체의 비교 || 객체

 

객체는 프로퍼티의 개수가 정해져 있지 않으며 동적으로 추가되고 삭제할 수 있고, 프로퍼티의 값에도 제약이 없다. 따라서 원시 값과 같이 확보해야 할 메모리 공간의 크기를 사전에 정해 둘 수 없다.

* 자바스크립트 객체의 관리 방식: 자바스크립트 객체는 프로퍼티 키를 인덱스로 사용하는 해시 테이블이라고 생각할 수 있다.

  • 변경 가능한 값

변수에 객체를 할당하면 그 변수에는 생성된 객체가 ‘실제로 저장된 메모리 공간의 주소’가 저장되어 있다. 이 값을 ‘참조 값’이라고 한다. 변수는 이 참조 값을 통해 객체에 접근할 수 있다.

일반적으로 원시 값을 할당한 변수의 경우 ‘변수는 ~ 값을 갖는다’ 또는 ‘값은 ~다’ 라고 표현한다. 하지만 객체를 할당한 변수의 경우 ‘변수는 객체를 참조하고 있다’ 또는 ‘변수는 객체를 가리키고 있다’ 라고 표현한다.

원시 값은 변경 불가능한 값이므로 재할당외에는 방법이 없지만, 객체를 할당한 변수는 재할당 없이 프로퍼티를 동적으로 추가, 갱신, 삭제할 수 있다. 객체는 변경 가능한 값이므로 메모리에 저장된 객체를 직접 수정할 수 있다. 이때 객체를 할당한 변수에 재할당하지 않았으므로 객체를 할당한 변수의 참조 값은 그대로이다.

이러한 구조로 인해 원시 값과는 다르게 여러 개의 식별자가 하나의 객체를 공유할 수 있다는 단점이 있다.

 

22.11.29 화요일 150p~160p

 

11장 원시값과 객체의 비교 || 객체

 

* 얕은 복사와 깊은 복사

얕은 복사는 한 단계까지만 복사하는 것을 말하고 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사하는 것을 말한다. 얕은 복사와 깊은 복사로 생성된 객체는 원본과는 다른 객체다. 즉 원본과 복사본은 참조 값이 다른 별개의 객체다. 하지만 얕은 복사는 객체에 중첩되어 있는 객체의 경우 참조 값을 복사하고 깊은 복사는 객체에 중첩되어 있는 객체까지 모두 복사해서 원시 값처럼 완전한 복사본을 만든다는 차이가 있다.

  • 참조에 의한 전달
let person = {
  name : 'Lee';
}

// 참조 값을 복사(얕은 복사), 두 개의 식별자가 하나의 객체를 공유한다.
let copy = person; 

객체를 가리키는 변수를 다른 변수에 할당하면 ‘원본의 참조 값이 복사되어 전달’된다. 이를 ‘참조에 의한 전달’이라 한다.

위의 예시에서는 원본 person과 사본 copy 모두 동일한 객체를 가리킨다. 두 개의 식별자가 하나의 객체를 공유하므로 원본 또는 사본 어느 한쪽에서 객체를 변경하면 서로 영향을 받는다.

copy.name = 'Kim';
person.address = 'Seoul';

console.log(person); // {name: 'Kim', address : 'Seoul}
console.log(copy); // {name: 'Kim', address : 'Seoul}

결국 값에 의한 전달과 참조에 의한 전달은 식별자가 기억하는 메모리 공간에 저장되어 있는 값을 복사해서 전달한다는 면에서 동일하다. 다만 식별자가 기억하는 메모리 공간, 즉 변수에 저장되어 있는 값이 원시 값이냐 참조 값이냐의 차이만 있을 뿐이다.

따라서 자바스크립트에는 참조에 의한 전달은 존재하지 않고 값에 의한 전달만이 존재한다고 말할 수 있다.

 

12장 함수

 

함수 내부로 입력을 전달받는 변수를 파라미터(매개변수), 입력을 아규먼트(인수), 출력을 반환 값이라고 한다.

  • 함수 정의

함수 정의란 함수를 호출하기 이전에 아규먼트를 전달받을 파라미터와 실행할 문들, 그리고 반환할 값을 지정하는 것을 말한다.

함수를 정의하는 방법에는 4가지가 있다.

함수 선언문
function add(x,y) { return x+y; }

함수 표현식
let add = function(x,y) { return x+y; }

Function 생성자 함수
var add = new Function(‘x’,’y’,’return x+y');

화살표 함수
let add = (x,y) => x+y;

변수는 선언한다고 하지만 함수는 정의한다고 표현한다. 함수 선언문이 평가되면 식별자가 암묵적으로 생성되고 함수 객체가 할당된다. 따라서 변수에는 선언, 함수에는 정의라고 표현한다.

 

22.11.30 수요일 159p~168p

 

12장 함수

 

1. 함수 선언문

함수 선언문은 표현식이 아닌 문이다. 크롬 개발자 도구의 콘솔에서 함수 선언문을 실행하면 완료 값 undefined가 출력된다. 함수 선언문이 만약 표현식인 문이라면 완료 값 undefined 대신 표현식이 평가되어 생성된 함수가 출력되어야 한다.

** 함수 리터럴

function (x,y){ return x+y; }

하지만 함수 표현식을 보면 함수 선언문이 변수에 할당되는 것처럼 보인다.

let add = function(x,y) { return x+y; }

이렇게 동작하는 이유는 자바스크립트 엔진이 코드의 문맥에 따라 동일한 함수 리터럴을 표현식이 아닌 문인 함수 선언문으로 해석하는 경우와 표현식인 문인 함수 리터럴 표현식으로 해석하는 경우가 있기 때문이다.

함수 선언문은 함수 이름을 생략할 수 없다는 점을 제외하면 함수 리터럴과 형태가 동일하다. 이는 함수 이름이 있는 기명 함수 리터럴은 함수 선언문 또는 함수 리터럴 표현식으로 해석될 가능성이 있다는 의미다.

{}은 블록문일 수도 있고 객체 리터럴일 수도 있다. {}가 단독으로 존재하면 블록문으로 해석되지만, 값으로 평가되어야 할 문맥(예를 들어, 할당 연산자의 우변)에서 피연산자로 사용되면 객체 리터럴로 해석한다. 이처럼 동일한 코드도 코드의 문맥에 따라 해석이 달라질 수 있다.

기명 함수 리터럴도 코드의 문맥에 따라 해석이 달라질 수 있다.

함수 선언문이든 함수 리터럴 표현식이든 함수가 생성되는 것은 동일하지만 함수를 생성하는 내부 동작에 차이가 있다.

// 기명 함수 리터럴은 단독으로 사용하면 함수 선언문으로 해석된다.
// 함수 선언문에서는 함수 이름을 생략할 수 없다.
function foo() {console.log('foo');}

foo(); // foo

// 함수 리터럴을 피연산자로 사용하면 함수 리터럴 표현식으로 해석된다.
// 함수 리터럴에서는 함수 이름을 생랼할 수 있다.

(function bar() {console.log('bar');});

bar(); // ReferenceErrorL bar is not defined

단독으로 사용된 함수 리터럴(foo)은 함수 선언문으로 해석되지만, 그룹 연산자 () 내에 있는 함수 리터럴은 함수 선언문으로 해석되지 않고 함수 리터럴 표현식으로 해석된다. 그룹 연산자의 피연산자는 값으로 평가될 수 있는 표현식이어야 한다. 따라서 표현식이 아닌 문인 함수 선언문은 피연산자로 사용할 수 없다.

함수 리터럴에서 함수 이름은 함수 몸체 내에서만 참조할 수 있는 식별자다. 이는 함수 몸체 외부에서는 함수 이름으로 함수를 참조할 수 없으므로 함수 몸체 외부에서는 함수 이름으로 함수를 호출할 수 없다는 의미다. 즉 함수를 가리키는 식별자가 없다는 것과 마찬가지다. 따라서 위 예제의 bar 함수는 호출할 수 없다.

하지만 위 예제에서 함수 선언문으로 정의된 함수는 foo라는 이름으로 호출할 수 있었다. foo는 함수 몸체 내부에서만 유효한 식별자인 함수 이름이므로 foo로 함수를 호출할 수 없어야 한다. foo는 무엇일까?

자바스크립트 엔진은 함수 선언문을 해석해 함수 객체를 생성한다. 이때 함수 이름은 함수 몸체 내부에서만 유효한 식별자이므로 함수 이름과는 별도로 생성된 함수 객체를 가리키는 식별자가 필요하다. 함수 객체를 가리키는 식별자가 없으면 생성된 함수 객체를 참조할 수 없으므로 호출할 수도 없다.

따라서 자바스크립트 엔진은 생성된 함수를 호출하기 위해 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고, 거기에 함수 객체를 할당한다.

즉, 함수는 함수 이름으로 호출하는 것이 아니라 함수 객체를 가리키는 함수 식별자로 호출한다. 함수 선언문으로 생성한 함수를 호출한 것은 자바스크립트 엔진이 암묵적으로 생성한 식별자인 것이다.

2. 함수 표현식

자바스크립트의 함수는 객체 타입의 값이다. 자바스크립트의 함수는 값처럼 변수에 할당할 수도 있고 프로퍼티 값이 될 수도 있으며 배열의 요소가 될 수도 있다. 이처럼 값의 성질을 갖는 객체를 ‘일급객체’라고 한다. 자바스크립트의 함수는 일급객체다. 함수가 일급 객체라는 것은 함수를 값처럼 자유롭게 사용할 수 있다는 의미다.

함수 리터럴로 생성한 함수 객체를 변수에 할당할 수 있고, 이러한 함수 정의 방식을 함수 표현식이라 한다.

let add = function (x,y) { return x+y;}

함수 리터럴의 함수 이름은 생략할 수 있다. 이러한 함수를 익명 함수라 한다.

  • 함수 생성 시점과 함수 호이스팅
// 함수 참조
console.dir(add); // f add(x,y)
console.dir(sub); // undefined

// 함수 호출
console.log(add(2,5)); // 7
console.log(sub(2,5)); // TypeError: sub is not a function

// 함수 선언문
function add(x,y){
  return x+y;
}

// 함수 표현식
var sub = function(x,y){
  return x-y;
}

위와 같이 함수 선언문으로 정의한 함수는 함수 선언문 이전에 호출할 수 있지만 함수 표현식으로 정의한 함수는 함수 표현식 이전에 호출할 수 없다. 이는 함수 선언문으로 정의한 함수와 함수 표현식으로 정의한 함수의 생성 시점이 다르기 때문이다.

모든 선언문이 그렇듯 함수 선언문도 코드가 한 줄씩 순차적으로 실행되는 시점인 런타임 이전에 자바스크립트 엔진에 의해 먼저 실행된다. 다시 말해, 함수 선언문으로 함수를 정의하면 런타임 이전에 함수 객체가 먼저 생성된다. 그리고 자바스크립트 엔진은 함수 이름과 동일한 이름의 식별자를 암묵적으로 생성하고 생성된 함수 객체를 할당한다.

즉 코드가 한 줄씩 순차적으로 실행되기 시작하는 런타임에는 이미 함수 객체가 생성되어 있고 함수 이름과 동일한 식별자에 할당까지 완료된 상태다. 따라서 함수 선언문 이전에 함수를 참조할 수 있으며 호출도 할 수 있다.

이처럼 함수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 함수 호이스팅이라 한다.

함수 호이스팅과 변수 호이스팅은 미묘한 차이가 있다.

var 키워드를 사용한 변수 선언문과 함수 선언문은 런타입 이전에 자바스크립트 엔진에 의해 먼저 실행되어 식별자를 생성한다는 점에서 동일하지만, var 키워드로 선언된 변수는 undefined로 초기화되고 함수 선언문을 통해 암묵적으로 생성된 식별자는 함수 객체로 초기화 된다. 따라서 var 키워드를 사용한 변수 선언문 이전에 변수를 참조하면 undefined로 평가되지만, 함수 선언문으로 정의한 함수를 함수 선언문 이전에 호출하면 함수 호이스팅에 의해 호출이 가능하다.

함수 표현식은 변수에 할당되는 값이 함수 리터럴인 문이다. 따라서 변수 선언문과 할당문을 한번에 기술한 축약 표현과 동일하게 동작한다. 변수 선언은 undefined로 초기화되지만 변수 할당문의 값은 할당문이 실행되는 시점, 즉 런타임에 평가되므로 함수 표현식의 함수 리터럴도 할당문이 실행되는 시점에 평가되어 함수 객체가 된다.

따라서 함수 표현식으로 함수를 정의하면 함수 호이스팅이 아닌 ‘변수’ 호이스팅이 발생한다.

함수 표현식 이전에 함수를 참조하면 undefined로 평가되고, 호출하면 undefined를 호출하는 것과 마찬가지이므로 타입 에러가 발생한다. 따라서 함수 표현식으로 정의한 함수는 반드시 함수 표현식 이후에 참조 또는 호출해야 한다.

3. Function 생성자 함수

자바스크립트가 기본 제공하는 빌트인 함수인 Function 생성자 함수에 매개변수 목록과 함수 몸체를 문자열로 전달하면서 new 연산자와 함께 호출하면 함수 객체를 생성해서 반환한다. 사실 new 연산자 없이 호출해도 결과는 동일하다.

** 생성자 함수는 객체를 생성하는 함수를 말한다.

Function 생성자 함수로 생성한 함수는 클로저를 생성하지 않는 등, 함수 선언문이나 함수 표현식으로 생성한 함수와 다르게 동작한다.

4. 화살표 함수

화살표 함수는 항상 익명 함수로 정의한다. 화살표 함수는 기존의 함수 선언문 또는 함수 표현식을 완전히 대체하기 위해 디자인된 것은 아니다. 화살표 함수는 기존의 함수보다 표현만 간략한 것이 아니라 내부 동작 또한 간략화되어 있다.

화살표 함수는 생성자 함수로 사용할 수 없고 기존 함수와 this 바인딩 방식이 다르고, prototype 프로퍼티가 없으며 arguments 객체를 생성하지 않는다.

반응형