本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
在 重新認識 JavaScript: Day 04 物件、陣列以及型別判斷 一文中,
我們曾經簡單介紹過物件。 那麼在接下來的幾篇文章,我們要更深入地來理解 JavaScript 的物件與其他程式語言的物件有什麼不同之處。
開始前先來複習一下。
之前說過,所有基本型別 (Primitives) 以外的值都是物件,基本型別有以下幾種:
string
number
boolean
null
undefined
symbol
(ES6 新增)除了上述這些以外的型別,都是物件。
這問題的答案很複雜,有人說它是,也有人說它不那麼像是。
JavaScript 確實是一門物件導向的程式語言,但它與其他語言很大不同的地方是,它的繼承方法是透過 "prototype" 來進行實作。 大多數的物件導向的程式語言是以「類別」為基礎 (class-based) ,但 JavaScript 沒有 class、沒有 extends [註1],卻可以透過「原型」(prototype-based) 來建立起物件之間的繼承關係。
先前曾介紹過,在 JavaScript 建立物件我們可以透過 new
關鍵字:
var person = new Object();
person.name = 'Kuro';
或是直接用大括號 { }
,即可建立起一個新的物件。
var person = {
name: 'Kuro'
};
雖然 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
好消息是,從 ES5 開始,我們可以透過新的物件模型來控制物件屬性的存取、刪除、列舉等功能。 這些特殊的屬性,我們將它稱為「屬性描述器」(Property descriptor)。
屬性描述器一共可以分為六種:
value
: 屬性的值writable
: 定義屬性是否可以改變,如果是 false
那就是唯讀屬性。enumerable
: 定義物件內的屬性是否可以透過 for-in
語法來迭代。configurable
: 定義屬性是否可以被刪除、或修改屬性內的 writable
、enumerable
及 configurable
設定。get
: 物件屬性的 getter function。set
: 物件屬性的 setter function。上述除了 value
之外的值都可以不設定,writable
、enumerable
及 configurable
的預設值是 true
,而 get
與 set
如果沒有特別指定則是 undefined
。
這些「屬性描述器」必須要透過 ES5 所提供的 Object.defineProperty()
來處理。
我們可以透過 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');
可以看到在預設的情況下,writable
、enumerable
及 configurable
都是 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
屬性描述 configurable
為 false
的情況:
var person = {};
Object.defineProperty(person, 'name', {
value: 'kuro',
writable: false,
enumerable: false,
configurable: false
});
那麼此時,我們再去執行刪除屬性的行為:
delete person.name; // it will return false
雖然不會出錯,但是你會發現執行結果會回傳 false
,且 person
物件的 name
屬性依然存在。 同樣地,當 writable
為 true
時,你去嘗試刪除屬性「值」的時候,你會發現結果是無效的。
要注意的是,上面這些行為,若是在「嚴格模式」下則會發生 TypeError
的錯誤。
在本文的開始,我們介紹了早期在 ES5 以前透過 this.getXXX()
與 this.setXXX()
來作為 get
與 set
的存取方法。
而現在 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
屬性去定義 get
與 set
方法,而實際上,我們是透過了另一個屬性 _name_
來作為 name
屬性的封裝。 要注意的是,如果你定義了 get
與 set
方法,表示你要自行控制屬性的存取,那麼就不能再去定義 value
或 writable
的屬性描述。
理解了 ES5 的物件屬性描述器之後,往後我們在對物件的屬性處理就可以更加靈活,像是 VueJS 也是透過 Object.defineProperty
的 get
與 set
來做到對物件資料的更新追蹤:
圖片來源:Vue.js: Reactivity in Depth
class
語法,但仍然屬於 prototype-based
的繼承。 class
實質上只是透過簡潔的語法來建立物件和處理繼承的語法糖。"同樣地,當 writable 為 true 時,你去嘗試修改屬性「值」的時候,你會發現結果是無效的"
是否應為"writable 為 false"時, 才會無法修改屬性「值」?
啊,其實我要講的應該是 嘗試「刪除」屬性值是無效的,謝謝提醒
你好,關於建構子模擬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在前面,會變成一個全域的方法嗎?
你好
測試了屬性描述器,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}
非常感謝你的文章~
你好,你的謝謝提醒,確實是文章有誤,已經修正。
「謝謝你的提醒」 orz
不會,獲益良多,謝謝你的文章 ~
您好:
請問
<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 存取值?
謝謝
person.name // call get
person.name = 1 // call set
請問第一個範例是否有誤?
var John = new Person( 'John', 10, 'male');
kuro.greeting(); // "Hello! My name is John."
宣告 John
變數,下一行卻使用 kuro
?
我還想說怎麼兩個輸出是一樣的 XD