iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 22
12
Modern Web

重新認識 JavaScript系列 第 22

重新認識 JavaScript: Day 22 深入理解 JavaScript 物件屬性

本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。

購書連結 https://www.tenlong.com.tw/products/9789864344130

讓我們再次重新認識 JavaScript!


重新認識 JavaScript: Day 04 物件、陣列以及型別判斷 一文中,
我們曾經簡單介紹過物件。 那麼在接下來的幾篇文章,我們要更深入地來理解 JavaScript 的物件與其他程式語言的物件有什麼不同之處。


開始前先來複習一下。

之前說過,所有基本型別 (Primitives) 以外的值都是物件,基本型別有以下幾種:

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol (ES6 新增)

除了上述這些以外的型別,都是物件。


JavaScript 是一個「物件導向」的程式語言嗎?

這問題的答案很複雜,有人說它是,也有人說它不那麼像是。

JavaScript 確實是一門物件導向的程式語言,但它與其他語言很大不同的地方是,它的繼承方法是透過 "prototype" 來進行實作。 大多數的物件導向的程式語言是以「類別」為基礎 (class-based) ,但 JavaScript 沒有 class、沒有 extends [註1],卻可以透過「原型」(prototype-based) 來建立起物件之間的繼承關係。


JavaScript 自訂物件

先前曾介紹過,在 JavaScript 建立物件我們可以透過 new 關鍵字:

var person = new Object();
person.name = 'Kuro';

或是直接用大括號 { },即可建立起一個新的物件。

var person = {
  name: 'Kuro'
};

理解 JavaScript 建構式

雖然 JavaScript 沒有 class 的語法,但如果你希望 JavaScript 也能像其他物件導向程式語言一樣有類似 class 的語法時,可以怎麼做呢? 由於函式也是個物件,所以可以借用來當作「建構式」來建立其他物件:

function Person( name, age, gender ){
  this.name = name;
  this.age = age;
  this.gender = gender;

  this.greeting = function(){
    console.log('Hello! My name is ' + this.name + '.');
  };
}

var kuro = new Person( 'Kuro', 32, 'male');
kuro.greeting();      // "Hello! My name is Kuro."

var John = new Person( 'John', 10, 'male');
kuro.greeting();      // "Hello! My name is John."

像這樣,我們建立了一個 Person 建構式 (Constructor) ,然後可以就透過 new 關鍵字來建立各種對應的物件。

為什麼 JavaScript 明明沒有 class 卻可以透過 new 一個函式來建立物件?
這裡簡單拆解一下流程:

function Person( name, age, gender ){
  // 略
}

var kuro = new Person( 'Kuro', 32, 'male');

/*
===> var kuro = {};
===> Person.call(kuro, 'Kuro', 32, 'male');
*/

透過 new Person(...) 這個動作,傳回的物件會有 name, age, gender 以及 greeting 屬性,而 JavaScript 會在背景執行 Person.call 方法,將 kuro 作為 this 的參考物件,然後把這些屬性通通新增到 kuro 物件中。

但是,即使是透過建構式建立的物件,這個物件的屬性仍然可以透過 . 來公開存取:

function Person( name, age, gender ){
  // 略
}

var kuro = new Person( 'Kuro', 32, 'male');
console.log( kuro.age );    // 32

// 因為是公開屬性,所以可以很無恥地開放修改 (欸)
kuro.age = 18;

console.log( kuro.age );    // 18

屬性描述器 (Property descriptor)

好消息是,從 ES5 開始,我們可以透過新的物件模型來控制物件屬性的存取、刪除、列舉等功能。 這些特殊的屬性,我們將它稱為「屬性描述器」(Property descriptor)。

屬性描述器一共可以分為六種:

  • value: 屬性的值
  • writable: 定義屬性是否可以改變,如果是 false 那就是唯讀屬性。
  • enumerable: 定義物件內的屬性是否可以透過 for-in 語法來迭代。
  • configurable: 定義屬性是否可以被刪除、或修改屬性內的 writableenumerableconfigurable 設定。
  • get: 物件屬性的 getter function。
  • set: 物件屬性的 setter function。

上述除了 value 之外的值都可以不設定,writableenumerableconfigurable 的預設值是 true,而 getset 如果沒有特別指定則是 undefined

這些「屬性描述器」必須要透過 ES5 所提供的 Object.defineProperty() 來處理。

Object.defineProperty 與 Object.getOwnPropertyDescriptor

我們可以透過 Object.defineProperty 來定義物件的屬性描述,用法: Object.defineProperty(obj, prop, descriptor)

透過實際範例解說:

一般來說,要建立一個簡單物件,我們可以用這樣方式:

var person = {
  name: 'kuro'
};

當然,我們也可以透過 Object.defineProperty 來定義物件 person 的屬性:

var person = {};

Object.defineProperty(person, 'name', {
  value: 'kuro'
});

這樣的方式與直接指定物件字面屬性是一樣的結果。

然後,我們可以用 Object.getOwnPropertyDescriptor() 來檢查物件屬性描述器的狀態:

var person = {};

Object.defineProperty(person, 'name', {
  value: 'kuro'
});

Object.getOwnPropertyDescriptor(person, 'name');

可以看到在預設的情況下,writableenumerableconfigurable 都是 false。 (更正)

var person2 = {
  name: 'kuro'
};

console.log(Object.getOwnPropertyDescriptor(person2, 'name'));

而透過物件實字方式建立的屬性,預設值則會是 true


defineProperty 可以針對物件一次設定多個屬性描述:

Object.defineProperty(person, 'name', {
  value: 'kuro',
  writable: false,
  enumerable: false,
  configurable: false
});

或是分別設定:

Object.defineProperty(person, 'name', {
  enumerable: true
});

這些都是合法的做法。


假設我們已經定義 person.name 屬性描述 configurablefalse 的情況:

var person = {};

Object.defineProperty(person, 'name', {
  value: 'kuro',
  writable: false,
  enumerable: false,
  configurable: false
});

那麼此時,我們再去執行刪除屬性的行為:

delete person.name;   // it will return false

雖然不會出錯,但是你會發現執行結果會回傳 false ,且 person 物件的 name 屬性依然存在。 同樣地,當 writabletrue 時,你去嘗試刪除屬性「值」的時候,你會發現結果是無效的。

要注意的是,上面這些行為,若是在「嚴格模式」下則會發生 TypeError 的錯誤。


屬性的 get 與 set 方法

在本文的開始,我們介紹了早期在 ES5 以前透過 this.getXXX()this.setXXX() 來作為 getset 的存取方法。

而現在 ES5 提供了 Object.defineProperty() 之後,我們可以更直觀地來處理這些方法:

var person = {};

Object.defineProperty(person, 'name', {
  get: function(){
    console.log('get');
    return this._name_;
  },
  set: function(name){
    console.log('set');
    this._name_ = name;
  }
});

像這樣,我們可以分別為 name 屬性去定義 getset 方法,而實際上,我們是透過了另一個屬性 _name_ 來作為 name 屬性的封裝。 要注意的是,如果你定義了 getset 方法,表示你要自行控制屬性的存取,那麼就不能再去定義 valuewritable 的屬性描述。


理解了 ES5 的物件屬性描述器之後,往後我們在對物件的屬性處理就可以更加靈活,像是 VueJS 也是透過 Object.definePropertygetset 來做到對物件資料的更新追蹤:

https://vuejs.org/images/data.png
圖片來源:Vue.js: Reactivity in Depth


  • [註1]: ES6 雖然新增了 class 語法,但仍然屬於 prototype-based 的繼承。 class 實質上只是透過簡潔的語法來建立物件和處理繼承的語法糖。
  • 參考文件:MDN: Object.defineProperty()

上一篇
重新認識 JavaScript: Day 21 函式的 Combo 技: Cascade
下一篇
重新認識 JavaScript: Day 23 基本型別包裹器 Primitive Wrapper
系列文
重新認識 JavaScript37
0
ceall8650
iT邦新手 5 級 ‧ 2018-03-02 00:26:50

"同樣地,當 writable 為 true 時,你去嘗試修改屬性「值」的時候,你會發現結果是無效的"

是否應為"writable 為 false"時, 才會無法修改屬性「值」?

Kuro Hsu iT邦新手 3 級 ‧ 2018-03-02 09:51:03 檢舉

啊,其實我要講的應該是 嘗試「刪除」屬性值是無效的,謝謝提醒

0
a2741890
iT邦新手 5 級 ‧ 2019-01-13 17:05:34

你好,關於建構子模擬private的部分,如果一開始只new了一個person,輸入的參數好像不會進到該person的屬性裡面所以當greeting的時候,this.name就會變成undefined,除非在呼叫一次setName方法才能輸入名字,跟物件導向的建構子建立時同時加入屬性好像又不太ㄧ樣了。
javascript還有其他可以 encapsulation 嗎? 謝謝

function Person1(name, gender, age){

  this.getName = function(){return name;};
  this.getGender = function(){return gender;};
  this.getAge = function(){return age;};

  this.setName = function(name){this.name = name;};
  this.setGender = function(gender){this.gender = gender;};
  this.setAge = function(age){this.age = age;};
  this.greeting1 = function (){
    console.log("Hello my name is " + this.name + ".");
  };
}
var william1 = new Person1("william", "male", 24);
william1.greeting1();
william1.setName("william");
william1.greeting1();

OUTPUT:

Hello my name is undefined.
Hello my name is william.

另外想請問一下,在person這個建構子內宣告的方法,如果不加上this在前面,會變成一個全域的方法嗎?

0
qaz013039
iT邦新手 5 級 ‧ 2019-07-26 21:40:01

你好

測試了屬性描述器,writable、enumerable 和 configurable 用兩種定義方式的預設值並不一樣

var person = {};
Object.defineProperty(person, 'name', {
  value: 'kuro'
});
person.name = "Tom";
console.log(person.name);   // kuro
console.log(Object.getOwnPropertyDescriptor(person, 'name'));   
// {value: "kuro", writable: false, enumerable: false, configurable: false}

var Person = {
  name: 'kuro'
};
Person.name = "Tom";
console.log(Person.name);   // Tom
console.log(Object.getOwnPropertyDescriptor(Person, 'name'))
// {value: "kuro", writable: true, enumerable: true, configurable: true}

非常感謝你的文章~

Kuro Hsu iT邦新手 3 級 ‧ 2019-07-26 21:48:50 檢舉

你好,你的謝謝提醒,確實是文章有誤,已經修正。

Kuro Hsu iT邦新手 3 級 ‧ 2019-07-26 21:49:33 檢舉

「謝謝你的提醒」 orz

qaz013039 iT邦新手 5 級 ‧ 2019-07-26 22:02:36 檢舉

不會,獲益良多,謝謝你的文章 ~

0
noway
iT邦新手 4 級 ‧ 2019-09-10 09:45:06

您好:
請問

<script>
var person = {};

Object.defineProperty(person, 'name', {
  get: function(){
    console.log('get');
    return this._name_;
  },
  set: function(name){
    console.log('set');
    this._name_ = name;
  }
});

console.log(Object.getOwnPropertyDescriptor(person, 'name'));
person.setName('XX');
console.log(person.getName());
</script>

1.Object.defineProperty(person, 'name', { } ) 這樣 有比 this.ggetXXX() 好用嗎?
2.這樣一次只能設定一個 屬性(如name) ?
3.defineProperty 設定好後,要如何用GET ,Set 存取值?

謝謝

0
jiatool
iT邦新手 4 級 ‧ 2021-07-18 20:54:26

請問第一個範例是否有誤?

var John = new Person( 'John', 10, 'male');
kuro.greeting();      // "Hello! My name is John."

宣告 John 變數,下一行卻使用 kuro
我還想說怎麼兩個輸出是一樣的 XD

我要留言

立即登入留言